浅入浅出 Spring Data Jpa(深入浅出springboot)
lipiwang 2024-11-15 22:01 14 浏览 0 评论
前言
目前在做的项目使用的是 Spring Data Jpa,以前都是使用 Mybatis ,前段时间研究了 JPA 的使用。
目前公司项目的架构许多的技术点是我没有实践过的,所以我这段时间在学习这些东西,从 2021-5-15 开始博客的更新尽量保证一周一篇。
下周更新 GraphQL 。
本文例子全部在 https://github.com/zhangpanqin/jpa-study ,数据库使用的是内存数据库H2。
JPA
认识JPA
JPA 是 Java Persistence API 的简称,定义了 Java 对象与数据库表的映射关系,以及定义运行时期怎么 CRUD 的接口规范。
Hibernate 提供了 JPA 的实现。除此之外还有别的实现,比如 Open Jpa 等等。
Spring Data 为数据访问提供了一个熟悉且一致的,基于Spring的编程模型,同时仍保留基础数据存储的特殊特征。
- Spring Data JPA 用于操作关系型数据库
- Spring Data MongoDB 用于操作 MongoDB
- Spring Data Elasticsearch 用于操作 Es
- Spring Data Redis 用于操作 Redis
Spring Data JPA 底层的 JPA 实现采用的是 Hibernate ,也可以说是封装了 Hibernate,提供了 Spring 统一的编程模型。
统一的编程模型是指:下面这段代码,可以操作 JPA,ES,Redis 等等,只是 Person 上的注解不一样。
又可以通过更换 CrudRepository 接口,提供更细粒度的不同数据库的数据控制。
public interface PersonRepository extends CrudRepository<Person, Long> {
List<Person> findByLastname(String lastname);
List<Person> findByFirstnameLike(String firstname);
}
JPA 常用注解介绍
使用 JPA 的时候不要使用数据库的外键,一是影响性能,二是不利于更换数据库。
不要使用 Hibernate 生成表结构,使用 flyway 组件,通过 SQL 来控制数据库表、索引,字段管理,flyway 灵活性更强
@Data
@Entity
@Table(name = "sys_user")
public class SysUserEntity extends BaseEntity {
private String nickname;
private Integer age;
/**
* name 定义的是关联表字段,referencedColumnName 是当前表中的主键字段
*/
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
private List<SysBlogEntity> sysBlogEntities;
}
@Entity
标记该类是一个 Entity ,被 JPA 管理
@Table
指定 Entity 与数据库中的那个表映射
@JoinColumn
指定了两个关联表之间使用哪两个字段关联
@Column
指定了Entity 字段与表的那个字段关联
@Id
指定主键字段
@GeneratedValue
指定主键的生成策略,下文详细介绍
@Transient
忽略字段与表字段的映射关系
@OneToMany
一对多关系指定,当前 Entity 与另一个 Entity 的映射关系。
/**
* name 定义的是关联表字段,referencedColumnName 是当前表中的主键字段
*/
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
private List<SysBlogEntity> sysBlogEntities;
@ManyToOne
参考 OneToMany
@ManyToMany
/**
* @JoinTable 指定中间表,及中间表中的字段映射
* @JoinColumn(name = ROLE_ID,referencedColumnName = ID) 指定了中间表的字段(name) 和另一个表那个字段关联(referencedColumnName)
*/
@ManyToMany
@JoinTable(name = SYS_USER_ROLE, joinColumns = {@JoinColumn(name = USER_ID, referencedColumnName = ID)},
inverseJoinColumns = {@JoinColumn(name = ROLE_ID,referencedColumnName = ID)})
private List<SysRoleEntity> sysRoleEntities;
@OneToOne
@OneToOne(optional=false)
@JoinColumn(name="CUSTREC_ID", unique=true, nullable=false, updatable=false)
private CustomerRecord customerRecord;
@Query
可以写 SQL 操作数据库
public interface SysBlogRepository extends JpaRepository<SysBlogEntity,Long> {
@Query(nativeQuery = true,value = "select * from sys_blog where user_id = :userId")
List<SysBlogEntity> findByUserId(Long userId);
@Query(nativeQuery = true ,value = "select * from sys_blog where title = :#{#sysBlogDTO.title}")
List<SysBlogEntity> findByTitle(@Param("sysBlogDTO") SysBlogDTO sysBlogDTO);
}
主键生成策略
/**
* strategy 取值
*
* AUTO 由程序控制,默认策略。Oracle 默认是 SEQUENCE,Mysql默认是 IDENTITY
*
* IDENTITY: 主键自增长,需要在表中定义表字段自增长,Mysql ,PostgreSQL,SQL Server 可以采用)
*
* SEQUENCE:使用序列作为主键 ,Oracle、PostgreSQL、DB2 可以使用
* @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emailSeq")
* @SequenceGenerator(initialValue = 1, name = "emailSeq", sequenceName = "EMAIL_SEQUENCE")
* private long id;
* 然后再数据库创建一个序列 create sequence EMAIL_SEQUENCE;
* 当不在 SequenceGenerator 指定 sequenceName ,默认使用 Hibernate 提供的序列名称为 hibernate_sequence
*
* TABLE 一般不适用这一个
*/
public @interface GeneratedValue {
GenerationType strategy() default AUTO;
String generator() default "";
}
主键生成策略例子
@Data
@Table
@Entity
public class KeyGeneratorEntity {
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid",strategy = "uuid")
private String id;
private String username;
}
@SpringBootTest
class KeyGeneratorRepositoryTest {
@Autowired
private KeyGeneratorRepository keyGeneratorRepository;
@Test
public void run(){
final KeyGeneratorEntity keyGeneratorEntity = new KeyGeneratorEntity();
keyGeneratorEntity.setUsername(LocalDateTime.now().toString());
keyGeneratorRepository.save(keyGeneratorEntity);
final List<KeyGeneratorEntity> all = keyGeneratorRepository.findAll();
// [KeyGeneratorEntity(id=ff808081796f260c01796f2616aa0000, username=2021-05-15T16:30:37.722)]
System.out.println(all);
}
}
hibernate 提供了以下主键生成策略
当 @GeneratedValue(strategy = GenerationType.SEQUENCE) 使用的是 SequenceStyleGenerator.class 控制主键生成。
当 @GenericGenerator(name = "system-uuid",strategy = "uuid") ,使用的是 UUIDHexGenerator.class
public class DefaultIdentifierGeneratorFactory
implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {
private ConcurrentHashMap<String, Class> generatorStrategyToClassNameMap = new ConcurrentHashMap<String, Class>();
@SuppressWarnings("deprecation")
public DefaultIdentifierGeneratorFactory() {
register( "uuid2", UUIDGenerator.class );
register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy
register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use
register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated
register( "assigned", Assigned.class );
register( "identity", IdentityGenerator.class );
register( "select", SelectGenerator.class );
register( "sequence", SequenceStyleGenerator.class );
register( "seqhilo", SequenceHiLoGenerator.class );
register( "increment", IncrementGenerator.class );
register( "foreign", ForeignGenerator.class );
register( "sequence-identity", SequenceIdentityGenerator.class );
register( "enhanced-sequence", SequenceStyleGenerator.class );
register( "enhanced-table", TableGenerator.class );
}
public void register(String strategy, Class generatorClass) {
LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );
final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );
if ( previous != null ) {
LOG.debugf( " - overriding [%s]", previous.getName() );
}
}
}
Lazy 需要在一个事务内执行
public class Order1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem> orderItemList;
}
// @Transactional(readOnly = true)
public List<Order1> listOrder(){
System.out.println("---------------------开始查询---------------------");
final List<Order1> all = orderRepository.findAll();
System.out.println("---------------------开始懒加载---------------------");
System.out.println(JSON.toJSONString(all));
return all;
}
当数据需要懒加载的时候,JPA不会查询 Lazy 的数据,只有在使用的时候才会查询,但是使用的时候需要和原来的查询在同一个事务中,不然会抛出以下异常
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mflyyou.jpa.n1.Order1.orderItemList, could not initialize proxy - no Session
由于没有开启事务,orderRepository.findAll() 执行之后这个查询事务就关闭了,所以获取 Order1.orderItemList 的时候报错。
当添加事务注解 @Transactional ,整个方法在一个事务内执行,就不会报错了。
N+1 问题
@Entity
@Table(name = "order1")
@Data
public class Order1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem> orderItemList;
}
@Transactional(readOnly = true)
public Order1 findOne(Long id){
System.out.println("---------------------开始查询---------------------");
final Optional<Order1> byId = orderRepository.findById(id);
System.out.println("---------------------开始懒加载---------------------");
System.out.println(JSON.toJSONString(byId.get()));
return byId.get();
}
当查询 Order1 的时候,实际上不会查询 orderItemList ,当使用 orderItemList 的时候再查询一次。
当 Order1 有 N 个关联属性的时候,就会查询 N 次来获取对应的数据。
当数据都处于 FetchType.LAZY 获取数据,就会产生懒加载问题
---------------------开始查询---------------------
---------------------开始查询---------------------
Hibernate:
select
order1x0_.id as id1_2_0_,
order1x0_.description as descript2_2_0_
from
order1 order1x0_
where
order1x0_.id=?
---------------------开始懒加载---------------------
Hibernate:
select
orderiteml0_.order_id as order_id3_3_0_,
orderiteml0_.id as id1_3_0_,
orderiteml0_.id as id1_3_1_,
orderiteml0_.name as name2_3_1_,
orderiteml0_.order_id as order_id3_3_1_,
orderiteml0_.price as price4_3_1_
from
order_item orderiteml0_
where
{
"description": "测试2021-05-15T17:35:50.349",
"id": 1,
"orderItemList": [
{
"id": 2,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 3,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 4,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 5,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 6,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
}
]
}
解决办法呢,可以使用 @OneToMany(fetch = FetchType.EAGER) 。
查询一次获取了全部数据
Hibernate:
select
order1x0_.id as id1_2_0_,
order1x0_.description as descript2_2_0_,
orderiteml1_.order_id as order_id3_3_1_,
orderiteml1_.id as id1_3_1_,
orderiteml1_.id as id1_3_2_,
orderiteml1_.name as name2_3_2_,
orderiteml1_.order_id as order_id3_3_2_,
orderiteml1_.price as price4_3_2_
from
order1 order1x0_
left outer join
order_item orderiteml1_
on order1x0_.id=orderiteml1_.order_id
where
order1x0_.id=?
当出现 orderItemList 和 orderItemList2 的时候,@OneToMany(fetch = FetchType.EAGER) 会报错
public class Order1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem> orderItemList;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem2> orderItemList2;
}
@NamedEntityGraph 和 @EntityGraph 可以解决 N+1 问题。又可以解决级联查询的时候,查询哪些成员变量,不查询哪些成员变量。让我们可以根据业务有更高的自由度查询数据。
EntityGraph
@NamedEntityGraph 定义查询的时候查询哪些数据,@EntityGraph 用于标记 Repository 使用哪个 NamedEntityGraph 。
@Entity
@Table(name = "order1")
@Data
@NamedEntityGraph(name = "searchOrderGraphItem",
attributeNodes = {
@NamedAttributeNode(value = "orderGraphItemList", subgraph = "OrderGraphItem_productGraphs"),
},
subgraphs = {
@NamedSubgraph(name = "OrderGraphItem_productGraphs", attributeNodes = {
@NamedAttributeNode(value = "productGraphs")
})
}
)
public class OrderGraph1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))
private Set<OrderGraphItem> orderGraphItemList;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))
private Set<OrderGraphItem2> orderGraphItemList2;
}
public interface OrderGraphRepository extends JpaRepository<OrderGraph1, Long> {
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)
OrderGraph1 findByIdEquals(Long id);
}
@Test
public void findById() {
final OrderGraph1 orderGraph1s = orderGraphService.findById(1L);
assertThat(orderGraph1s, notNullValue());
}
@EntityGraph 中指定的的 type 可以取值 FETCH与 LOAD
- FETCH 对于 NamedEntityGraph 定义的 attributeNodes 使用eager,未声明的使用 lazy
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)
OrderGraph1 findByIdEquals(Long id);
只会查询出 OrderGraph1 对应表中的字段和 orderGraphItemList。orderGraphItemList2 当用的时候才会查询。
- LOAD 对于 NamedEntityGraph 定义的 attributeNodes 使用eager,未声明的属性使用属性配置的 FetchType
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.LOAD)
OrderGraph1 findByIdEquals(Long id);
审计功能
一般表中都会有,主键 id ,创建时间 ,更新事件,谁创建,谁更新,再加上乐观锁。
实现 AuditorAware,填充用户 id。
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@Id
private Long id;
/**
* 创建时间
*/
@CreatedDate
@Column(name = "create_date", updatable = false)
private Instant createDate;
/**
* 修改时间
*/
@LastModifiedDate
@Column(name = "update_date")
private Instant updateDate;
/**
* 被谁创建
*/
@CreatedBy
@Column(name = "create_by", updatable = false)
private Integer createBy;
/**
* 被谁修改
*/
@LastModifiedBy
@Column(name = "update_by")
private Integer updateBy;
/**
* 乐观锁
*/
@Version
@Column(name = "version")
private Long version = 0L;
}
@Component
public class MyAuditorAware implements AuditorAware<Integer> {
/**
* 获取当前登录的 id
*/
@Override
public Optional<Integer> getCurrentAuditor() {
// 在请求头获取登录标识,再查询用户主键 id
return Optional.ofNullable(100);
}
}
乐观锁,更新的version 必须等于数据库中的版本,否则更新会抛出异常。也可以使用 Spring-retry 捕获 ObjectOptimisticLockingFailureException 重试更新。
@Data
@Entity
@Table(name = "sys_user")
public class SysUserEntity extends BaseEntity {
private String nickname;
private Integer age;
}
@SpringBootTest
class JpaStudyApplicationTests {
private static final Long USER_ID_EQUALS_1 = 1L;
private static final Long USER_ID_EQUALS_2 = 2L;
private static final Long USER_ID_EQUALS_3 = 3L;
@Resource
private SysUserRepository sysUserRepository;
private SysUserEntity saveSysUserEntity;
private SysUserEntity saveSysUserEntity2;
private SysUserEntity saveSysUserEntity3;
@BeforeEach
public void beforeEach() {
saveSysUserEntity = new SysUserEntity();
saveSysUserEntity.setAge(10);
saveSysUserEntity.setId(USER_ID_EQUALS_1);
saveSysUserEntity.setNickname("测试");
saveSysUserEntity.setVersion(10L);
saveSysUserEntity2 = new SysUserEntity();
saveSysUserEntity2.setAge(10);
saveSysUserEntity2.setId(USER_ID_EQUALS_2);
saveSysUserEntity2.setNickname("测试");
saveSysUserEntity2.setVersion(10L);
saveSysUserEntity3 = new SysUserEntity();
saveSysUserEntity3.setAge(10);
saveSysUserEntity3.setId(USER_ID_EQUALS_3);
saveSysUserEntity3.setNickname("测试");
saveSysUserEntity3.setVersion(10L);
}
@Test
public void should_update_error() {
sysUserRepository.save(saveSysUserEntity);
final Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_1);
assertThat(byId.isPresent(), is(Boolean.TRUE));
final SysUserEntity sysUserEntity = byId.get();
// 设置版本 2 也报错
// sysUserEntity.setVersion(2L);
sysUserEntity.setVersion(12L);
sysUserEntity.setNickname("乐观锁更新" + LocalDateTime.now().toString());
final Executable executable = () -> sysUserRepository.save(sysUserEntity);
assertThrows(ObjectOptimisticLockingFailureException.class, executable);
}
@Test
public void should_update_success() {
sysUserRepository.save(saveSysUserEntity2);
Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_2);
assertThat(byId.isPresent(), is(Boolean.TRUE));
SysUserEntity sysUserEntity = new SysUserEntity();
sysUserEntity.setId(USER_ID_EQUALS_2);
sysUserEntity.setVersion(10L);
sysUserEntity.setNickname("乐观锁更新" + LocalDateTime.now().toString());
SysUserEntity save = sysUserRepository.save(sysUserEntity);
assertThat(save.getVersion(), is(11L));
}
}
相关推荐
- 一个简单便捷搭建个人知识库的开源项目(MDwiki)
-
这里我通过自动翻译软件,搬运总结MDwiki官网的部署和使用方法。第一步:下载编译好的后MDwiki文件,只有一个HTML文件“mdwiki.html”。第二步:在mdwiki.html同级目录创建“...
- 强大、简洁、快速、持续更新 PandaWiki新一代 AI 驱动的开源知识库
-
PandaWiki是什么PandaWiki是一款AI大模型驱动的开源知识库搭建系统,帮助你快速构建智能化的产品文档、技术文档、FAQ、博客系统,借助大模型的力量为你提供AI创作、AI问答...
- DeepWiki-Open: 开源版Deepwiki,可自己构建github文档库
-
Deepwiki是Devin团队开发的github文档库,用户能免费使用,但代码不是开源,而DeepWiki-Open侧是开源版本的实现。DeepWiki-Open旨在为GitHub和GitLa...
- 最近爆火的wiki知识管理开源项目PandaWiki
-
项目介绍PandaWiki是一款AI大模型驱动的开源知识库搭建系统,帮助你快速构建智能化的产品文档、技术文档、FAQ、博客系统,借助大模型的力量为你提供AI创作、AI问答、AI搜索等...
- 轻量级开源wiki系统介绍(轻量开源论坛系统)
-
wiki系统有很多DokuWiki、MediaWiki、MinDoc等等都是开源的wiki系统。商业版的wiki,像很多企业在用的confluence等。今天我们讲的是一款轻量级且开源的文档管理系统:...
- DNS解析错误要怎么处理(dns解析状态异常怎么办)
-
在互联网时代,网络已经成为人们生活和工作中不可或缺的一部分。然而,当遇到DNS解析错误时,原本畅通无阻的网络访问会突然陷入困境,让人感到十分困扰。DNS,即域名系统,它如同互联网的电话簿,将人们易于...
- 网页加载慢?这些方法让你秒开网页!
-
打开浏览器,信心满满地准备查资料、看视频或者追剧,却发现网页怎么都打不开!是不是瞬间感觉手足无措?别慌,这个问题其实挺常见,而且解决起来并没有你想象的那么复杂。今天就来聊聊网页打不开究竟是怎么回事,以...
- windows11 常用CMD命令大全(windows11msdn)
-
Windows11中的命令提示符(CMD)是一个强大的工具,可以通过命令行执行各种系统操作和管理任务。以下是一些常用的CMD命令,按功能分类整理,供你参考:一、系统信息与状态systeminfo显...
- 电脑提示DNS服务器未响应怎么解决?
-
我们在使用电脑的时候经常会遇到各种各样的网络问题,例如最近就有Win11电脑用户在使用的时候遇到了DNS未响应的问题,遇到这种情况我们应该怎么解决呢? 方法一:刷新DNS缓存 1、打开运行(W...
- 宽带拨号错误 651 全解析:故障定位与修复方案
-
在使用PPPoE拨号连接互联网时,错误651提示「调制解调器或其他连接设备报告错误」,通常表明从用户终端到运营商机房的链路中存在异常。以下从硬件、系统、网络三层维度展开排查:一、故障成因分类图...
- 如何正确清除 DNS 缓存吗?(解决你访问延时 )
-
DNS缓存是一个临时数据库,用于存储有关以前的DNS查找的信息。换句话说,每当你访问网站时,你的操作系统和网络浏览器都会保留该域和相应IP地址的记录。这消除了对远程DNS服务器重复查询的...
- 网络配置命令:ipconfig和ifconfig,两者有啥区别?
-
在计算机网络的世界里,网络接口就像是连接你电脑和外部网络的桥梁,而网络配置则是确保这座桥梁稳固、通信顺畅的关键。提到网络配置工具,ipconfig和ifconfig绝对是两个绕不开的名字。它们一...
- 救急的命令 你会几个?(救急一下)
-
很多人都说小编是注册表狂魔,其实不完全是,小编常用的命令行才是重点。其实所谓的命令行都是当初DOS时代的标准操作方式,随着Windows不断演化,DOS的命令早已成为Windows的一部分了——开始菜...
- 电脑有网却访问不了GitHub原来是这样
-
当满心欢喜打开电脑,准备在GitHub这个“开源宝藏库”里挖掘点超酷的项目,却遭遇了网页无法访问的尴尬。看着屏幕上那令人无奈的提示,原本高涨的热情瞬间被泼了一盆冷水,是不是感觉世界都不美好了...
- rockstargames更新慢| r星更新速度 怎么办 解决办法
-
rockstargames更新慢|r星更新速度怎么办解决办法说到RockstarGames,那可是游戏界的大佬,作品个顶个的经典。但话说回来,每当新内容更新时,那蜗牛般的下载速度,真是让人急得...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)