
暂无个人介绍
集合注解映射 集合关系映射可以看成是一对多关系映射的一种简化,在一个电商系统里,出售的产品可能会有多张展示图片,如果我们使用一对多来建立关联映射时,需要创建一个实体类Images,里面可能有属性:图片在服务器的访问路径url和图片所属产品productId。但如果我们使用集合关系映射,则无需新建一个实体类,只需在Product中定义一个集合成员属性即可。 Set集合 在产品中,我们的图片路径一般是不会相同的,我们可以使用Set集合来建立映射 我们下面来看这一需求的配置示例: @Entity @Table(name = "t_product") public class Product { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; @ElementCollection(fetch = FetchType.LAZY)//使用此注解配置集合映射关联 private Set<String> images ; //忽略get和set方法 } 下面是我们的测试方法: Product product = new Product(); product.setName("product"); Set<String> imagesUrl = new HashSet<String>(); for(int i = 0 ; i < 5; i ++){ imagesUrl.add("imageUrl"+ i); } product.setImages(imagesUrl); session.save(product); 执行上述测试方法,我们查询数据库: 即集合映射的内部实现是hibernate会为我们单独创建一张表来存储这些信息。 如果我们想要自定义这张表的表明,属性名,我们可以通过以下配置: @ElementCollection(fetch = FetchType.LAZY) @JoinTable(name = "t_product_images",joinColumns = @JoinColumn(name = "proId")) @Column(name = "imagesUrl") private Set<String> images ; 重新执行测试方法,查询数据库: 表名和属性名都被成功修改了。 List集合 使用Set集合能确保元素属性不重复,但我们无法确定元素的插入顺序,与Set集合不同,List集合在数据库中多了一个索引属性,我们根据索引属性来获取记录,同时也可以根据索引属性得知元素的插入先后顺序。 下面我们使用list来模拟一个数据库级别的排队系统:在一个医院有很多个医生,每个医生都有很多病人排队就诊。下面是我们的医生类: @Entity @Table(name = "t_doctor") public class Doctor { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; @ElementCollection(fetch = FetchType.LAZY) @JoinTable(name = "t_patient",joinColumns = @JoinColumn(name = "doctor_id")) @Column(name = "patient_name") //@IndexColumn(name = "orders",base = 100)在新版本中,此注解已被废弃,推荐使用下面两个注解取代 @OrderColumn(name = "orders")//表示索引列名为patient_name @ListIndexBase(100)//表示索引从100开始 } 下面是我们的添加测试代码: Doctor doctor = new Doctor(); doctor.setName("doctor"); List<String> patients = new ArrayList<String>(); for(int i = 0 ; i < 5; i ++){ patients.add("patient" + i); } doctor.setPatientName(patients); session.save(doctor); 运行代码,查看数据库: 数据被成功插入,并且细心对比前面set集合图片,我们发现这里是有序的,执行下列查询操作: Doctor doctor = session.get(Doctor.class, 1); System.out.println(doctor); for(String string : doctor.getPatientName()){ System.out.println(string); } 控制台打印: 说明我们获得的数据是有序的,这样就可以动态删除第一个,而新纪录从尾部加入,实现一个队列的效果。 事实上,我们使用List来配置一对多映射中的多方,也能完成同样的效果 Map集合 在产品中,可能会有不同的规格对象不同的价格,这种需求我们可以通过Map集合映射来实现: package com.zeng4.model; import java.util.Map; import java.util.Set; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.MapKey; import javax.persistence.MapKeyColumn; import javax.persistence.Table; import org.hibernate.annotations.Type; @Entity @Table(name = "t_product") public class Product { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; @ElementCollection @JoinTable(name = "t_specs_prize",joinColumns = @JoinColumn(name = "product_id")) @Column(name = "prize")//对应规格的价格 @MapKeyColumn(name = "specs")//对应键值列名称 private Map<String, Double> prize; } 下面是我们的测试方法: Product product = new Product(); product.setName("prodcut_with_many_prize"); HashMap prizes= new HashMap(); for(int i = 0 ; i < 5 ; i ++){ prizes.put("specs"+i,1.2* i); } product.setPrize(prizes); session.save(product); 执行测试方法,查询数据库: 我们的Product类中的Map集合属性key是规格,对应与specs,值是价格,对应于peize。 Map集合关联操作,我们可以通过类似下面代码进行: Product product = session.get(Product.class, 1); for(Entry<String, Double> entry : product.getPrize().entrySet()){ System.out.println("specs = " + entry.getKey() + "————prize = " + entry.getValue()); } 运行方法,控制台打印: 源码下载 本节内容测试代码可到https://github.com/jeanhao/hibernate/tree/master/collection下载
在我们的角色管理系统中,一个用户可以有多种角色,一种角色可以赋予多个用户,显然用户和角色就是典型的多对多关系。又或者博客网站上,用户与文章点赞记录也是一个多对多关系,即一个用户可以点赞多篇文章,一篇文章可以给多个用户点赞等,这时候,我们往往需要附加一些信息,比如授权时间、点赞时间等。在上面两个实例中,都可对应于hibernate多对多映射关系的两种方式,在多对多映射中,我们往往使用中间表来建立关联关系,而且会是双向关联,确保任意一方添加或删除,都可以对中间表进行操作来维护关联关系。 下面我们来看多对多映射的第一种实现: 我们先看一个错误的配置: /****************用户类***************/ @Entity @Table(name = "t_user3") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String userName; @ManyToMany(cascade = CascadeType.ALL) private Set<Role> roles; //忽略get 和set方法 } /****************角色类***************/ @Entity @Table(name = "t_role3") public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String roleName; @ManyToMany(cascade = CascadeType.ALL) private Set<User> users; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((roleName == null) ? 0 : roleName.hashCode()); return result; } //忽略get 和set方法 } 在这里我们简单的通过注解ManyToMany建立了两边关联关系,并且级联所有操作,这时候,调用我们的测试方法: @Test public void test1(){ User user = new User(); user.setUserName("userName"); Role role = new Role(); role.setRoleName("roleName"); Set<Role> roles = new HashSet<Role>(); roles.add(role); user.setRoles(roles);//建立关联关系 session.save(user); } 执行方法,我们会看到数据记录如下图 在数据库中,hibernate帮我们生成了4张表,其中2张是中间表,是因为User和Role都是关联关系主控方,会以自己为主建立一张中间表,通过级联插入操作我们会发现,我们添加User,即使级联添加了Role,但维护关系在由User主控的中间表t_user3_t_role3中,这样,如果我尝试从Role方对这条记录进行级联操作,因为在Role放的主控表t_role3_t_user3中找不到维护关系,则会导致级联操作失败! 比如我们执行如下操作: Role role = session.get(Role.class, 2);//获取刚刚插入的记录 System.out.println(role.getUsers().size());//结果打印0,即从Role端找不到关联的User session.delete(role);//尝试删除操作,看能否级联删除 这是查看记录 发现确实只有Role表的记录被删除了。当然,如果数据库存在外键关联的话,这个删除操作是会失败的,因为中间表t_user3_t_role3的记录roles_id=2会进行约束 接下来,我们先恢复role端的记录: 再执行如下操作: User user = session.get(User.class, 1); System.out.println(user.getRoles().size());//打印1 session.delete(user); 我们会看到控制台记录hibernate使用了如下sql语句: Hibernate: delete from t_user3_t_role3 where User_id=? Hibernate: delete from t_role3 where id=? Hibernate: delete from t_user3 where id=? 这里主要想说明的是,如果我们设置了级联删除,不仅中间表的关系维护记录会被清除,通过另一方的记录也会被删除。可在实际应用中,我们往往不希望存在这种级联删除,这好比我要除去一条没有意义的角色,不小心把关联的用户也删除了,这不是我们想要的结果,因此要控制好级联关系。 下面我们来看一个正确关联关系配置: /********************用户方**************/ @ManyToMany @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)//使用hibernate注解级联保存和更新 @JoinTable(name = "t_user_role", joinColumns = {@JoinColumn(name = "user_id")},//JoinColumns定义本方在中间表的主键映射 inverseJoinColumns = {@JoinColumn(name = "role_id")})//inverseJoinColumns定义另一在中间表的主键映射 private Set<Role> roles; /********************角色方**************/ @ManyToMany @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)//使用hibernate注解级联保存和更新 @JoinTable(name = "t_user_role", joinColumns = {@JoinColumn(name = "role_id")},//JoinColumns定义本方在中间表的主键映射 inverseJoinColumns = {@JoinColumn(name = "user_id")})//inverseJoinColumns定义另一在中间表的主键映射 private Set<User> users; 在这里,我们将两边都设为主控方,这样两边都可以对关联关系进行维护。假如我们只需要User方维护关联关系,可以将角色方改成如下配置: @ManyToMany(mapperBy = "roles") @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)//使用hibernate注解级联保存和更新 private Set<User> users; 上面是第一种建立多对多关系,但这样配置的缺点是,我们无法知道两者关联关系的上下文属性,比如授权时间等,针对这一需求,我们可以单独建立一张中间表,然后让用户表和角色表分别和中间表建立一对多关联关系,这样,我们可以在中间表中添加一些附加信息如授权开始时间、授权结束时间等,这在实际开发中是很有意义的。 关于这种方法的实现这里不再举例。关于一对多关联关系的配置实现可参考我前面的文章。
本地分支解析 git 通过可变指针来实现对提交数据的历史版本的控制,每当我们提交新的更新,当前分支(设为master)则指向最后一个提交更新A,而最后一个提交对象则存在一个指针指向前一次的提交更新Q。如果我们创建一个新的分支,child,它和master共同指向A,这时,如果我们向child分支提交更新B,我们会发现child指向B,而master依然指向A。无论我们在child分支进行了任何开发,只要回到master分支,就能恢复到更新A的数据状态了。 在图片里,我们还注意到有一个head指针,一般来说,它会指向我们目前所在的工作分支。现在它指向了我们的master分支,意思是master是我们目前的工作分支。一旦提交更新,就会在master分支上提交。现在,让我们看看与git分支有关的操作命令: 1. git branch [option] [name] 如果不使用任何参数,它可以用来查看所有的分支,而在分支名前有*标记的则为主分支,如果加上name为创建新分支,,如git branch child,则会创建一个名为child的分支,此外,它有一些常用的参数: 参数 解释 -v 用于查看各个分支的最后一次commit信息 -d 删除分支。 -r 查看远程主机分支 -a 查看所有分支。 –merge 查看哪些分支已被当前分支合并 –no-merge 查看尚未合并的工作,如果其中的分支还包含尚未合并的工作,而我们尝试使用git branch -d 删除时,我们会被提示:error: The branch 'xxxxx' is not an ancestor of your current HEAD.,如果想要强制删除的话,可以使用git branch -D xxxxx,即使用大写的D来实现。 2. git checkout [name] 切换到对应的分支,对于上图,如果我们是使用 git checkout child,我们的head指针就会指向child分支了。这时候,如果我们提交新的更新D,我们会发现: 我们的D指向C,而C依然指向A,也就是说,以后我们在child分支上做的任何更新,都不会对master分支所在的之路造成任何影响!一旦使用了checkout命令,我们还会发现,不仅head指针会指向新的分支,而且当前工作目录中的文件也会换成了新分支对应的文件了。 此外,我们还可以使用git checkout -b [name]命令,它会新建一个分支,并自动将当前的工作目录切换到该分支上。 3. git merge [name] 合并分支,有的时候,我们创建次要分支,可能是为了修改原有程序的bug,或为了拓展新的功能,这时候如果我们想把次要分支的修改何并进主分支中,我们可以使用git merge 命令来实现。 1. “Fast forward”(快进)式合并: 如果像上图所示,我们要把child分支合并进master中,因为child分支所指向的更新在master分支的直接上游,git会使用“Fast forward”(快进)式合并,直接将master分支指针指向child分支所指向更新,如下图所示: 这时候,如果我们觉得child分支没什么用了,我们可以使用git branch -d child来删除分支。 2. 基本合并 如果我们这次要合并的分支不在我们目前分支的上游,如下图所示: 这时,如果使用快进式合并(将master分支指向更新E),这样就会丢失更新D了,于是,我们采用另一种合并方式,它的合并结果如下图所示: 我们会发现,此时master分支所指向的合并更新F出现了两个祖先。 3. 冲突合并 基本合并的冲突源于两个分支间的所指向的版本更新不能根据箭头方向从一方抵达另一方,即两个分支在更新C单向分岔了,但我们还发现,更新C、A、Q还是master和child分支的共同父更新,如果两个分支都对C或A或Q版本的相同文本相同位置做了不同的修改,git就无法智能地将两者合并一起,因为它不能判断master的修改和child的修改哪个是更佳的,事实上,这个只能由人来解决。比如两个分支共同修改了版本C中的README文件的第1行: 1 master: I’m master! 2 child: I’m child! 当我们尝试从master上合并child时,会出现: $ git merge child 自动合并 README 冲突(内容):合并冲突于 README 自动合并失败,修正冲突然后提交修正的结果。 或英文版的: Auto-merging README CONFLICT (content): Merge conflict in README Automatic merge failed; fix conflicts and then commit the result. 这时调用git status命令,会看到: $ git status 位于分支 master 您有尚未合并的路径。 (解决冲突并运行 “git commit”) 未合并的路径: (使用 “git add …” 标记解决方案) 双方修改: README 或英文版的: $ git status README: needs merge On branch master Changed but not updated: (use “git add …” to update what will be committed) (use “git checkout – …” to discard changes in working directory) unmerged: README 这时候我们打开README文件,就会看到: 1 <<<<<<< HEAD 2 I’m master 3 ======= 4 I’m child! 5 >>>>>>> child “=======”分开了两个分支的冲突部分,'''<<<<<<<HEAD为主分支的,而>>>>>>>child上面就是要合并部分的了。这时候,我们需要修改冲突部分,比如改成 1 I'master and child! 修改完后,我们还需要通过git add 和git commit来提交对冲突的修改,这样。我们就完成了这次冲突合并了! 远程分支解析 git fetch [远程主机名] [远程分支名][:本地分支名] 如果不指定分支名,会获取远程主机的全部最新更新。如果指定了分支,则获取该分支的最新更新,如果还指定了本地分支名,则会新建对应的分支来来保存远程分支的所有数据。此时获取的更新会放在“远程主机昵称/远程分支名”这样的分支上,如origin/master,如果像要合并到我们本地分支master,需要使用git merge命令,但此时需考虑之前提到的合并冲突问题。 git pull [远程主机名] [远程分支名][:本地分支名] 先从远程主机的特定分支获取更新,并合并到本地分支上,如果不指定本地分支,则默认合并到当前分支上,如果当前分支与远程分支(从远程分支检出的本地分支)存在追踪关系,git pull就可以省略远程分支名 git branch –set-upstream [本地分支名] [远程主机名/远程分支名] 如果我们想要手动建立本地分支和远程分支的跟踪关系,可以使用此指令 git push [远程主机名] [本地分支]:[远程分支] 如果省略远程主机名,则将其推送到具有跟踪关系的远程分支上,如果远程分支不存在,则会新建。 如果省略本地分支,则相当推送一个空的分支当远程分支,即会删除远程分支。 如果当前分支和远程分支存在跟踪关系,则可以忽略本地/远程分支名 如果当前分支只有一个追踪的远程分支,则可以把远程主机名,本地/远程分支名都省略掉 不带任何参数的git push,默认只推送当前分支,这叫做simple方式(Git 2.0版本后的默认模式)。此外,还有一种matching方式,会推送所有有对应的远程分支的本地分支。如果需要修改默认配置,git config –global push.default simple/default来设置 如果不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,可以使用–all参数,如: $ git push –all origin 上面命令表示,将所有本地分支都推送到origin主机
一对一共享主键 下面我们直接通过实例来讲解共享主键配置: 主键主控方:Article package com.zeng2.model; @Table(name = "t_article2") @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String title; @OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY,optional = false) @PrimaryKeyJoinColumn//配置共享主键,否则会额外生成外键关联列 private ArticleContent articleContent; //忽略get 和set方法 } 引用主键方:ArticleContent。这是共享主键关联的关键配置地方所在类 package com.zeng2.model; @Table(name = "t_article_content") @Entity public class ArticleContent { @Id @GenericGenerator(name = "foreignKey" ,//生成器名称 strategy = "foreign",//使用hibernate的外键策略 parameters = @Parameter(value = "article",name = "property"))//指定成员属性中的article所在类的主键为本类的主键,这里的参数属性name必须为"property" @GeneratedValue(generator = "foreignKey")//使用上述定义的id生成器 private Integer id; @Lob private String content; //如果试图不加此注解来形成单向关联,会抛出异常, //因为设置了共享主键这里是否配置mapperBy放弃维护关联关系已失去作用。 @OneToOne(cascade = CascadeType.ALL) @PrimaryKeyJoinColumn//如果不加此注解,hibernate会在数据库默认生成一条article_id属性 private Article article; //忽略get 和set方法 下面是我们的测试方法: Article article = new Article(); article.setTitle("title"); ArticleContent articleContent = new ArticleContent(); articleContent.setContent("content"); article.setArticleContent(articleContent); articleContent.setArticle(article);//必须双方设置 session.save(article); 下面是几点需要注意的: 在设置属性ID的时候必须注意字段的长度,如笔者这样使用oracle的sequence来生成ID,其长度有14位之长,则应选择hibernate类型long,对应的实体中应选择Long,这样不会出现溢出的情况。 在测试的时候必须要注意这两张表之间因为已经存在了一对一的关系,所以我们不能只写articleContent.setArticle(article);而忽略了articleContent.setArticle(article);这样在做插入的时候会报出attempted to assign id from null one-to-one property: address的错误. 如果不写cascade=”all”或者写成cascade=”none”的话,即使写了article.setArticleContent(articleContent);和articleContent.setArticle(article);也不会发生任何事情,只有user会被存储。 one-to-one的效率问题——-one-to-one在查询时,总是查出和主表关联的表,而且one-to-one的lazy属性只有false proxy no-proxy三种,没有true。outer-join=”false”也只是徒增查询语句条数,把本来的一条sql语句变成多条。所以在one-to-one这种一对一的关系不是很强的情况下(one-to-one关系强即总是查出这所有的几个关联表),或者是在一张表中存在多个one-to-one的情况下,使用最好one-to-many来代替one-to-one。
在实际博客网站中,文章内容的数据量非常多,它会影响我们检索文章其它数据的时间,如查询发布时间、标题、类别的等。这个时候,我们可以尝试将文章内容存在另一张表中,然后建立起文章——文章内容的一对一映射 一对一关联有两种方式,一种是外键关联,另一种是复合主键关联。 外键关联 下面我们先看一个一对一单向关联的实例 /*************关联关系维护方************/ @Table(name = "t_article") @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String title; @OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY,orphanRemoval = true,targetEntity = ArticleContent.class) @JoinColumn(name = "article_content_id") private ArticleContent articleContent; //忽略get和set方法 } 下面是我们的文章内容类 @Table(name = "t_article_content") @Entity public class ArticleContent { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Lob private String content; //忽略get和set方法 } 下面是我们的测试类 public class Test3 { private ApplicationContext ac; private SessionFactory sessionFactory; private Session session; private Transaction transaction; @BeforeClass//在测试类初始化时调用此方法,完成静态对象的初始化 public static void before(){ } @Before//每一个被注解Test方法在调用前都会调用此方法一次 public void setup(){//建立针对我们当前测试方法的的会话和事务 ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); session = sessionFactory.openSession(); transaction = session.beginTransaction(); } //测试级联关系映射注解配置:一对一单向关联 @Test public void test1(){ //测试级联添加 Article article = new Article(); article.setTitle("title"); ArticleContent articleContent = new ArticleContent(); articleContent.setContent("content"); article.setArticleContent(articleContent);//建立映射关系 session.save(articleContent); session.save(article); //测试级联删除 // Article article = (Article) session.get(Article.class,1); // session.delete(article); @After//每一个被注解Test方法在调用后都会调用此方法一次 public void teardown(){ if(transaction.isActive()){//如果当前事务尚未提交,则 transaction.commit();//提交事务,主要为了防止在测试中已提交事务,这里又重复提交 } session.close(); } 调用我们的测试方法test1。控制台打印: Hibernate: insert into t_article_content (content) values (?) Hibernate: insert into t_article (article_content_id, title) values (?, ?) 此时查看数据库: mysql> show tables; ————————————hibernate帮我们新建的表格 +———————+ | Tables_in_hibernate | +———————+ | t_article | | t_article_content | +———————+ 2 rows in set (0.00 sec) mysql> desc t_article; ————————————单方维护映射关系,通过article_content_id维护 +——————–+————–+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +——————–+————–+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(255) | YES | | NULL | | | article_content_id | int(11) | YES | MUL | NULL | | +——————–+————–+——+—–+———+—————-+ 3 rows in set (0.00 sec) mysql> desc t_article_content; +———+———-+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +———+———-+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | content | longtext | YES | | NULL | | +———+———-+——+—–+———+—————-+ 2 rows in set (0.00 sec) mysql> select * from t_article; +—-+——-+——————–+ | id | title | article_content_id | +—-+——-+——————–+ | 1 | title | 1 | +—-+——-+——————–+ 1 row in set (0.00 sec) mysql> select * from t_article_content; +—-+———+ | id | content | +—-+———+ | 1 | content | +—-+———+ 1 row in set (0.00 sec) 注释掉测试代码的级联添加部分,运行级联删除部分: Hibernate: delete from t_article where id=? Hibernate: delete from t_article_content where id=? 在这里,我们观察到它是先删除文章(维护关系方),再删除t_article_content的,回想我们之前的一对多关联测试,都是先删除维护关系方的,这其实很好理解,我们肯定要清除掉相应的关联关系(体现在数据库的外键上)才能完成被关联内容的删除操作 一对一双向关联很简单,直接在articleContent上添加: @OneToOne(cascade = CascadeType.ALL,mapperBy = "articleContent") private Article article; //忽略getter/setter 使用和上面一样的测试代码,hibernate会帮我们生成表格并插入数据: mysql> select * from t_article_content; +—-+———+ | id | content | +—-+———+ | 1 | content | +—-+———+ 1 row in set (0.00 sec) mysql> select * from t_article; +—-+——-+——————–+ | id | title | article_content_id | +—-+——-+——————–+ | 1 | title | 1 | +—-+——-+——————–+ 1 row in set (0.00 sec) 这时候如果我们尝试在放弃维护的articleContent端进行级联添加: //测试articleContent级联添加 Article article = new Article(); article.setTitle("title"); ArticleContent articleContent = new ArticleContent(); articleContent.setContent("content"); articleContent.setArticle(article); session.save(articleContent); 我们的article对象能被成功保存,但是,两者的关联关系建立失败: mysql> select * from t_article_content; +—-+———+ | id | content | +—-+———+ | 1 | content | | 2 | content | +—-+———+ 2 rows in set (0.00 sec) mysql> select * from t_article; +—-+——-+——————–+ | id | title | article_content_id | +—-+——-+——————–+ | 1 | title | 1 | | 2 | title | NULL | +—-+——-+——————–+ 2 rows in set (0.00 sec) 这时候我们再尝试从放弃维护端删除: //这次删除是有级联关系的 ArticleContent articleContent = (ArticleContent) session.get(ArticleContent.class, 1);//注意这里id为1 session.delete(articleContent); mysql> select * from t_article_content; +—-+———+ | id | content | +—-+———+ | 5 | content | +—-+———+ 1 row in set (0.00 sec) mysql> select * from t_article; +—-+——-+——————–+ | id | title | article_content_id | +—-+——-+——————–+ | 6 | title | NULL | +—-+——-+——————–+ 1 row in set (0.00 sec) 会看到我们相应article对象也被删除了!因此,我们需要明确放弃维护关联关系并不代表放弃关联关系,从ArticleContent端,我们一样能进行与关联关系双管的级联添加、删除操作。只是不对两者关系进行维护,因而在添加时Article端的外键属性article_content_id=null 我们使用mappedBy属性放弃关联,但级联操作依然有效,因此需要区分开维护关联关系和级联操作的区别。 这里需要特别注意的是,在这种一对一映射中,我们最好选择一个被动方并设定mapperBy属性,即让一方放弃维护关联关系,否则,我们会看到下述现象: mysql> desc t_article; +——————–+————–+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +——————–+————–+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(255) | YES | | NULL | | | article_content_id | int(11) | YES | MUL | NULL | | +——————–+————–+——+—–+———+—————-+ 3 rows in set (0.00 sec) mysql> desc t_article_content; +————+———-+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +————+———-+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | content | longtext | YES | | NULL | | | article_id | int(11) | YES | MUL | NULL | | +————+———-+——+—–+———+—————-+ 3 rows in set (0.00 sec) 两个表中都建立了关于对方的关联映射。这是完全没有必要的,而且这样会造成的更严重后果,我们来测试级联添加 先调用如下测试代码: //测试article级联添加 Article article = new Article(); article.setTitle("title"); ArticleContent articleContent = new ArticleContent(); articleContent.setContent("content"); article.setArticleContent(articleContent); session.save(article); 再调用如下测试代码: //测试articleContent级联添加 Article article = new Article(); article.setTitle("title"); ArticleContent articleContent = new ArticleContent(); articleContent.setContent("content"); articleContent.setArticle(article); session.save(articleContent); 我们会看到数据库对应记录: mysql> select * from t_article; +—-+——-+——————–+ | id | title | article_content_id | +—-+——-+——————–+ | 1 | title | 1 | | 2 | title | NULL | +—-+——-+——————–+ 2 rows in set (0.00 sec) mysql> select * from t_article_content; +—-+———+————+ | id | content | article_id | +—-+———+————+ | 1 | content | NULL | | 2 | content | 2 | +—-+———+————+ 2 rows in set (0.00 sec) 即双方各维护各的关联关系,如果这时候我们尝试交换测试级联删除: Article article = (Article) session.get(Article.class,2); session.delete(article); 会看到如下结果: mysql> select * from t_article; +—-+——-+——————–+ | id | title | article_content_id | +—-+——-+——————–+ | 1 | title | 1 | +—-+——-+——————–+ 1 row in set (0.00 sec) mysql> select * from t_article_content; +—-+———+————+ | id | content | article_id | +—-+———+————+ | 1 | content | NULL | | 2 | content | 2 | +—-+———+————+ 2 rows in set (0.00 sec) 即级联删除失败了,而这是显然的,因为id为2的文章,对应article_content_id属性为null,在文章方看来,两者都没建立关联关系,这种时候肯定不是报错就是级联删除失败,而报错是因为如果设置了数据库在t_article_content中设置了对article_id的的外键关联,因为存在记录article_id=2,这时候我们尝试删除article表中id为2的记录,则会由于外键关系约束失败而报错
在SpringMVC中,我们会经常使用到拦截器,虽然SpringAOP也能帮我们实现强大的拦截器功能,但在Web资源供给上,却没有SpringMVC来得方便快捷。 使用SpringMVC拦截器的核心应用场景是根据我们的实际需求,个性化定制拦截器,再对特定url进行拦截处理。 而自定义拦截器,首先需要我们实现HandlerInterceptor拦截器接口,下面是它的定义: package org.springframework.web.servlet; public interface HandlerInterceptor { //在控制器方法调用前执行 //返回值为是否中断,true,表示继续执行(下一个拦截器或处理器) //false则会中断后续的所有操作,所以我们需要使用response来响应请求 boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; //在控制器方法调用后,解析视图前调用,我们可以对视图和模型做进一步渲染或修改 void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception; //整个请求完成,即视图渲染结束后调用,这个时候可以做些资源清理工作,或日志记录等 void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception; } 很多时候,我们只需要实现以上三个方法的任意一个或两个,这个时候我们可以选择继承HandlerInterceptorAdapter。它实现了AsyncHandlerInterceptor接口,为每个方法提供了空实现,这样,我们就可以根据需求重写自己用到的拦截方法即可。具体定义如下: public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } @Override public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } @Override public void afterConcurrentHandlingStarted( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { } } 相对于HandlerInterceptor,HandlerInterceptorAdapter多了一个实现方法afterConcurrentHandlingStarted(),它来自HandlerInterceptorAdapter的直接实现类AsyncHandlerInterceptor,AsyncHandlerInterceptor接口直接继承了HandlerInterceptor,并新添了afterConcurrentHandlingStarted()方法用于处理异步请求,当Controller中有异步请求方法的时候会触发该方法时,异步请求先支持preHandle、然后执行afterConcurrentHandlingStarted。异步线程完成之后执行preHandle、postHandle、afterCompletion。 下面我们以登陆请求为例,编写我们的自定义拦截器: public class LoginInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception { // 获得请求路径的uri String uri = request.getRequestURI(); // 进入登录页面,判断session中是否有key,有的话重定向到首页,否则进入登录界面 if(uri.contains("login")) { if(request.getSession().getAttribute("user") != null) { response.sendRedirect(request.getContextPath());//默认跟路径为首页 } else { return true;//继续登陆请求 } } // 其他情况判断session中是否有key,有的话继续用户的操作 if(request.getSession().getAttribute("user") != null) { return true; } // 最后的情况就是进入登录页面 response.sendRedirect(request.getContextPath() + "/login"); return false; } } 下面是我们的拦截器配置: <mvc:interceptors> <mvc:interceptor><!--配置局部拦截器,需要满足下列路径条件--> <mvc:exclude-mapping path="/user/logout"/><!--注销--> <mvc:exclude-mapping path="/home/"/><!--在home中定义了无须登陆的方法请求,直接过滤拦截--> <mvc:mapping path="/**"/> <bean class="com.mvc.interceptor..LoginInterceptor"/><!--自定义拦截器注册--> </mvc:interceptor> <!-- 我们可以直接在者注册自定义拦截器Bean来配置全局拦截器,会对所有请求拦截--> </mvc:interceptors> 在我们的拦截中,如果配置了多个拦截器,会形成一条拦截器链,执行顺序类似于AOP,前置拦截先定义的先执行,后置拦截和完结拦截(afterCompletion)后注册的后执行,关于拦截器的执行顺序的深入理解可参考我的另一篇文章《 spring学习笔记(12)@AspectJ研磨分析[3]增强织入顺序实例详解》
在上两篇文章里,我们详细地分别讲解了一对多和多对一的单向关联配置的具体属性含义,在这一篇文章里,我们完成两者的的整合建立双向关联。 在实际的博客网站中,我们可能需要根据文章读取作者(用户)信息,但肯定也要让用户能获取自己的文章信息,针对这种需求,我们可以建立文章(多)对用户(一)的双向关联映射。 下面先看实例映射配置文件: /********************一方配置User********************/ @Entity//声明当前类为hibernate映射到数据库中的实体类 @Table(name = "t_user1")//声明在数据库中自动生成的表名为t_user public class User { @Id//声明此列为主键,作为映射对象的标识符 @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true,mappedBy = "user")//用户作为一方使用OneToMany注解 @JoinColumn(name = "user_id") // @JoinTable(name = "t_user_articles",inverseJoinColumns = {@JoinColumn(name = "article_id")},joinColumns = {@JoinColumn(name = "user_id")}) private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章 } /*****************多方配置****************/ @Table(name = "t_article1") @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String content; /** * @ManyToOne 使用此标签建立多对一关联,此属性在“多”方使用注解在我们的“一”方属性上 * @cascade 指定级联操作,以数组方式指定,如果只有一个,可以省略“{}” * @fetch 定义抓取策略 * @optional 定义是否为必需属性,如果为必需(false),但在持久化时user = null,则会持久化失败 * @targetEntity 目标关联对象,默认为被注解属性所在类 */ @ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY) private User user; 映射关系确立好,开始编写我们的测试文件: public class Test2 { private ApplicationContext ac; private SessionFactory sessionFactory; private Session session; private Transaction transaction; @BeforeClass//在测试类初始化时调用此方法,完成静态对象的初始化 public static void before(){ } @Before//每一个被注解Test方法在调用前都会调用此方法一次 public void setup(){//建立针对我们当前测试方法的的会话和事务 ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); session = sessionFactory.openSession(); transaction = session.beginTransaction(); } @Test public void test3(){ User user = new User(); user.setName("oneObject"); Set<Article> articles = new HashSet<Article>(); for(int i = 0 ; i < 3;i ++){ Article article = new Article(); article.setContent("moreContent" + i) ; articles.add(article); } user.setArticles(articles);//建立关联关系 session.save(user); } @After//每一个被注解Test方法在调用后都会调用此方法一次 public void teardown(){ if(transaction.isActive()){//如果当前事务尚未提交,则 transaction.commit();//提交事务,主要为了防止在测试中已提交事务,这里又重复提交 } session.clear(); session.close(); sessionFactory.close(); } @After//在类销毁时调用一次 public void after(){ } } 这个时候,我们运行测试方法test3会发现报错: org.hibernate.AnnotationException: Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: com.zeng.model.User.articles 意思是,一旦被注解@mapperBy,即放弃了维护关联关系,而@JoinColumn注解的都是在“主控方”,因而我们需要注解在Article类中 @ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY) @JoinColumn(name = "user_id",unique = false,updatable = true) private User user; 然后我们再运行测试方法:会看到: Hibernate: drop table if exists t_article1 Hibernate: drop table if exists t_user1 Hibernate: create table t_article1 (id integer not null auto_increment, content varchar(255), user_id integer, primary key (id)) Hibernate: create table t_user1 (id integer not null auto_increment, name varchar(255), primary key (id)) Hibernate: alter table t_article1 add index FK6D6D45665B90FD3C (user_id), add constraint FK6D6D45665B90FD3C foreign key (user_id) references t_user1 (id)——————在这里,我们添加了外键约束,这也是hibernate对象关联在数据库的重要体现 上面是我们的表创建工作,下面是记录创建工作 Hibernate: insert into t_user1 (name) values (?) Hibernate: insert into t_article1 (content, user_id) values (?, ?) Hibernate: insert into t_article1 (content, user_id) values (?, ?) Hibernate: insert into t_article1 (content, user_id) values (?, ?) DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.Article{content=moreContent0, id=3, user=null} DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.User{id=1, articles=[com.zeng.model.Article#1, com.zeng.model.Article#2, com.zeng.model.Article#3], name=oneObject} DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.Article{content=moreContent2, id=1, user=null} DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.Article{content=moreContent1, id=2, user=null} 参考前面一篇文章的测试结果,在一对多单向配置中,因为关联关系是有一方维护,所以在最后总有三句额外的update语句,来完成article表到user表的映射关系,但在user放弃维护权后,如果我们再尝试通过保存用户通过建立起两表的映射关系,是不成功的。 从蓝色粗体部分,似乎User和article建立了关联关系。事实上,这是一种伪关联,它看似让我们通过session.save(user)。就完成了4者的关联创建,但在数据库层次,他们的关联关系是没有建立的,这从蓝色记录Article记录中user=null可以说明这一点。 此外,我们可以通过测试尝试从user中获取article对象来进一步验证: User user = (User) session.get(User.class, 1); System.out.println("获取用户对应的文章数据:"+user.getArticles()); 打印结果:获取用户对应的文章数据:[] 这是因为我们的关联信息是由多方维护的(user_id),我们想要真正完成两者,必须从主维护方:article下手 运行以下测试代码: User user = new User(); user.setName("oneObject1"); for(int i = 0 ; i < 3;i ++){ Article article = new Article(); article.setContent("moreContent1" + i) ; article.setUser(user);//有article来建立关联关系 session.save(article);//持久化 } 得到打印结果: Hibernate: insert into t_user1 (name) values (?) Hibernate: insert into t_article1 (content, user_id) values (?, ?) Hibernate: insert into t_article1 (content, user_id) values (?, ?) Hibernate: insert into t_article1 (content, user_id) values (?, ?) DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.Article{content=moreContent10, id=4, user=com.zeng.model.User#2} DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.Article{content=moreContent12, id=6, user=com.zeng.model.User#2} DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.Article{content=moreContent11, id=5, user=com.zeng.model.User#2} DEBUG: org.hibernate.internal.util.EntityPrinter - com.zeng.model.User{id=2, articles=null, name=oneObject1} 再查看数据库: mysql> select * from t_article1; +—-+—————+———+ | id | content | user_id | +—-+—————+———+ | 1 | moreContent2 | NULL |——————上次操作遗留 | 2 | moreContent1 | NULL |——————上次操作遗留 | 3 | moreContent0 | NULL |——————上次操作遗留 | 4 | moreContent10 | 2 | | 5 | moreContent11 | 2 | | 6 | moreContent12 | 2 | +—-+—————+———+ 6 rows in set (0.00 sec) 从sql语句和蓝色DEBUG、数据库记录我们能够看出,这才是最优雅的添加关联操作,既没有多余的update语句,同时完成了数据库关联关系的建立。
在博客网站中,我们可能需要从某一篇文章找到其所关联的作者,这就需要从文章方建立起对用户的关联,即是多对一的映射关系。 现在先看一个配置实例:我们的文章实体类 package com.zeng.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.Table; @Table(name = "t_article") @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Lob//数据可能会非常长,映射为数据库支持的“大对象” private String content; /** * @ManyToOne 使用此标签建立多对一关联,此属性在“多”方使用注解在我们的“一”方属性上 * @cascade 指定级联操作,以数组方式指定,如果只有一个,可以省略“{}” * @fetch 定义抓取策略 * @optional 定义是否为必需属性,如果为必需(false),但在持久化时user = null,则会持久化失败 * @targetEntity 目标关联对象,默认为被注解属性所在类 */ @ManyToOne(cascade ={CascadeType.ALL},fetch = FetchType.LAZY,optional = false,targetEntity = User.class) private User user; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } } 因为这里是单向关联,所以我们无须在在User类中建立对文章的关联属性 接下来编写我们的测试类 package com.zeng.test; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.zeng.model.Article; import com.zeng.model.User; public class Test2 { private static ApplicationContext ac; private static SessionFactory sessionFactory; private Session session; private Transaction transaction; @BeforeClass//在测试类初始化时调用此方法,完成静态对象的初始化 public static void before(){ ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); } @Before//每一个被注解Test方法在调用前都会调用此方法一次 public void setup(){//建立针对我们当前测试方法的的会话和事务 session = sessionFactory.openSession(); transaction = session.beginTransaction(); } //测试级联关系映射注解配置:多对一单向关联 @Test public void test1(){ User user = new User(); user.setName("name1"); Article article = new Article(); article.setContent("content1"); article.setUser(user);//建立级联关系 session.save(article);//注意这里我们没有保存我们的user对象 } @After//每一个被注解Test方法在调用后都会调用此方法一次 public void teardown(){ transaction.commit(); session.clear(); session.close(); } @After//在类销毁时调用一次 public void after(){ sessionFactory.close(); } } 调用上面测试方法,我们会发现,hibernate帮我们在上篇文章已建立User类的基础上,又帮我们创建了t_article数据表: mysql> desc t_article; +———+————–+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +———+————–+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | content | varchar(255) | YES | | NULL | | | user_id | int(11) | NO | MUL | NULL | | +———+————–+——+—–+———+—————-+ 3 rows in set (0.00 sec) 然后我们查看用户表和文章表,会看到: mysql> select * from t_user; +—-+——-+ | id | name | +—-+——-+ | 1 | name1 | +—-+——-+ 1 row in set (0.00 sec) mysql> select * from t_article; +—-+———-+———+ | id | content | user_id | +—-+———-+———+ | 1 | content1 | 1 | +—-+———-+———+ 1 row in set (0.00 sec) 可以看到,这里我们的user_id和user表的新建记录id是对应的。 看完实例,下面我们针对配置的属性进行具体分析: 1. cascade属性 属性 说明 CascadeType.MERGE 级联更新:若user属性修改了那么article对象保存/更新时同时修改user在数据库里的属性值 CascadeType.PERSIST 级联保存:对article对象保存时也对user里的对象也会保存。 CascadeType.REFRESH 级联刷新:获取article对象里也同时也重新获取最新的user时的对象。即会重新查询数据库里的最新数据 CascadeType.REMOVE 级联删除:对article对象删除也会使对应user的象删除 CascadeType.ALL 包含PERSIST, MERGE, REMOVE, REFRESH, DETACH等; 级联属性对于一方和多方的作用效果是不一样的。经测试发现,在多对一中,多方使用CascadeType.PERSIST无法级联保存对象,必须使用CascadeType.ALL。而级联删除既可使用CascadeType.REMOVE也可使用CascadeType.ALL 对于上述方法,如果我们没有设置级联保存,在我们保存文章对象时,用户对象自然不会持久化到数据库,这时候会报错: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.zeng.model.User 在我们提交事务的时候,hibernate总会flush(清理)我们的session缓存,所谓清理,是指hibernate按照持久化对象的属性变化来同步更新数据库,当发现我们的article对象引用了临时对象user,而article.user.id = null,会判断user对象是瞬时的Transient,这个我们要持久化到数据库中的article对象发生冲突,因此会保存失败。这里我们也意在说明,关系直接的级联映射是通过用户对象标识符id来确认的。意思是说,即使article.user.其它属性全为null,但只要article.user.id在数据库中有相关记录(saved)这时就能建立两者的级联关系了。 另一方面,如果我们习惯了xxx.htm.xml的方式来配置我们的实体映射关系,那我们必然对hibernate的级联属性更加熟悉,这时我们可以通过@Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE})来使用hibernate内置级联属性。关于hibernate的内置级联属性常见有: 属性名 说明 save-update 级联保存(load以后如果子对象发生了更新,也会级联更新)。 但它不会级联删除 delete 级联删除, 但不具备级联保存和更新 all-delete-orphan 在解除父子关系时,自动删除不属于父对象的子对象, 也支持级联删除和级联保存更新。 all 级联删除, 级联更新,但解除父子关系时不会自动删除子对象。 delete-orphan 删除所有和当前对象解除关联关系的对象 2. @JoinColumn 它的具体值可参照下表 属性 默认值 说明 columnDefinition 空 JPA 使用最少量 SQL 创建一个数据库表列。如果需要使用更多指定选项创建列,将 columnDefinition 设置为在针对列生成 DDL 时希望 JPA 使用的 String SQL 片断。 insertable true 默认情况下,JPA 持续性提供程序假设它可以插入到所有表列中。如果该列为只读,请将 insertable 设置为 false。 name 默认值 如果使用一个连接列,则 JPA 持续性提供程序假设外键列的名称是以下名称的连接:1. 引用关系属性的名称 +“”+ 被引用的主键列的名称。2. 引用实体的字段名称 +“”+ 被引用的主键列的名称。3. 如果实体中没有这样的引用关系属性或字段(请参阅 @JoinTable),则连接列名称格式化为以下名称的连接:实体名称 +“_”+ 被引用的主键列的名称。这是外键列的名称。如果连接针对“一对一”或“多对一”实体关系,则该列位于源实体的表中。如果连接针对“多对多”实体关系,则该列位于连接表(请参阅 @JoinTable)中。4. 如果连接列名难于处理、是一个保留字、与预先存在的数据模型不兼容或作为数据库中的列名无效,请将 name 设置为所需的 String 列名。 nullable true 默认情况下,JPA 持续性提供程序假设允许所有列包含空值。如果不允许该列包含空值,请将nullable 设置为 false。 referencedColumnName 无 如果使用一个连接列,则 JPA 持续性提供程序假设在实体关系中,被引用的列名是被引用的主键列的名称。如果在连接表(请参阅 @JoinTable)中使用,则被引用的键列位于拥有实体(如果连接是反向连接定义的一部分,则为反向实体)的实体表中。要指定其他列名,请将 referencedColumnName 设置为所需的 String 列名。 table 无 JPA 持续性提供程序假设实体的所有持久字段存储到一个名称为实体类名称的数据库表中(请参阅 @Table)。如果该列与辅助表关联(请参阅 @SecondaryTable),请将 name 设置为相应辅助表名称的 String 名称 unique false 默认情况下,JPA 持续性提供程序假设允许所有列包含重复值。如果不允许该列包含重复值,请将 unique 设置为 true。 updatable true 默认情况下,JPA 持续性提供程序假设它可以更新所有表列。如果该列为只读,则将 updatable 设置为 false 参考:http://blog.sina.com.cn/s/blog_4bc179a80100kd0k.html
在上一篇文章里,我们从端方向一端建立关联关系,完成了从文章到作者的关联关系建立,但在实际的博客网站中,用户肯定还需要获取自己所写的文章,这时可以建立用户(一)对文章(多)的单向关联映射。 先来看我们的一方配置实例 package com.zeng.model; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; @Entity//声明当前类为hibernate映射到数据库中的实体类 @Table(name = "t_user")//声明在数据库中自动生成的表名为t_user public class User { @Id//声明此列为主键,作为映射对象的标识符 @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,mappedBy = "user",targetEntity = Article.class,orphanRemoval = true)//用户作为一方使用OneToMany注解 private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章 public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Set<Article> getArticles() { return articles; } public void setArticles(Set<Article> articles) { this.articles = articles; } //重写hashcode方法提高比较效率 @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } //重写equals比较对象相等 @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; User other = (User) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } } 下面是我们对应的多方配置 package com.zeng.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Table(name = "t_article1") @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String content; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } 根据这些配置,我们来编写测试方法: package com.zeng.test; import java.util.HashSet; import java.util.Set; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.zeng.model.Article; import com.zeng.model.User; public class Test2 { private ApplicationContext ac; private SessionFactory sessionFactory; private Session session; private Transaction transaction; @BeforeClass//在测试类初始化时调用此方法,完成静态对象的初始化 public static void before(){ } @Before//每一个被注解Test方法在调用前都会调用此方法一次 public void setup(){//建立针对我们当前测试方法的的会话和事务 ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); session = sessionFactory.openSession(); transaction = session.beginTransaction(); } //测试一对多单向关联 @Test public void test2(){ User user = new User(); user.setName("oneObject"); Set<Article> articles = new HashSet<Article>(); for(int i = 0 ; i < 3;i ++){//添加三篇文章 Article article = new Article(); article.setContent("moreContent" + i) ; articles.add(article); } user.setArticles(articles);//建立关联关系 session.save(user);//仅保存用户 } @After//每一个被注解Test方法在调用后都会调用此方法一次 public void teardown(){ if(transaction.isActive()){//如果当前事务尚未提交,则 transaction.commit();//提交事务,主要为了防止在测试中已提交事务,这里又重复提交 } session.clear(); session.close(); sessionFactory.close(); } @After//在类销毁时调用一次 public void after(){ } } 执行测试方法,我们会看到控制台打印下列sql语句: Hibernate: insert into t_user1 (name) values (?) Hibernate: insert into t_article1 (content) values (?) Hibernate: insert into t_article1 (content) values (?) Hibernate: insert into t_article1 (content) values (?) Hibernate: insert into t_user1_t_article1 (t_user1_id, articles_id) values (?, ?) Hibernate: insert into t_user1_t_article1 (t_user1_id, articles_id) values (?, ?) Hibernate: insert into t_user1_t_article1 (t_user1_id, articles_id) values (?, ?) 在前四句,我们看到在保存user对象时,级联保存了我们的文章对象,最后面三条信息又是什么?原来在我们没有设置@JoinColumn(具体使用方法请参考我的上篇文章)。那么在一对多的关联配置中,hibernate会默认帮我们生成中间表来完成两者的映射关系,查询数据库,我们会发现 mysql> select * from t_user1_t_article1; +————+————-+ | t_user1_id | articles_id | +————+————-+ | 1 | 1 | | 1 | 2 | | 1 | 3 | +————+————-+ 3 rows in set (0.00 sec) 确实是通过中间表,将用户和文章关联起来了。 这时进行级联删除测试: “`java User user = (User) session.get(User.class, 1); session.delete(user); >我们会得到打印信息: > Hibernate: delete from t_user1_t_article1 where t_user1_id=? Hibernate: delete from t_article1 where id=? Hibernate: delete from t_article1 where id=? Hibernate: delete from t_article1 where id=? Hibernate: delete from t_user1 where id=? </font> 可见,它的删除顺序是<font color=red>先清楚中间表数据->再删除多方文章4数据->最后清楚一方用户数据</font> 如果我们不像使用中间表,而想像上一篇配置多对一关联那样,在文章表生成user_id,我们就要一方配置@JoinColumn属性,对应属性的实例如下: ```java @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true)//用户作为一方使用OneToMany注解 @JoinColumn(name = "user_id")//添加了这个注解 private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章 <div class="se-preview-section-delimiter"></div> 修改对应表名,让hibernate重新在数据库中生成表,此时再运行我们的测试方法,会看到: Hibernate: insert into t_user2 (name) values (?) Hibernate: insert into t_article2 (content) values (?) Hibernate: insert into t_article2 (content) values (?) Hibernate: insert into t_article2 (content) values Hibernate: update t_article2 set user_id=? where id=? Hibernate: update t_article2 set user_id=? where id=? Hibernate: update t_article2 set user_id=? where id=? 此时我们会看到,最后三行换成了更新我们的表属性值。从这里我们看出为了更新aritlce表中user_id对应值,我们额外使用多了三条数据,这是很不值的,会额外消耗数据库的性能,有没方法使得在插入文章表的同时插入user_id的值呢?查看hibernate源码,我们会发现这是因为user作为主动方,它处理关联对象时必须通过update来完成,如果我们想取消update,应该将user放弃主动,让另一方(多方)去维护,这又涉及到我们的一对多、多对一双向关联了,我们在下一篇文章再具体解决这一问题。 这个时候我们来测试级联删除: User user = (User) session.get(User.class, 1); session.delete(user); <div class="se-preview-section-delimiter"></div> 会得到如下打印信息: Hibernate: update t_article2 set user_id=null where user_id=? Hibernate: delete from t_article2 where id=? Hibernate: delete from t_article2 where id=? Hibernate: delete from t_article2 where id=? Hibernate: delete from t_user2 where id=? 注意到,它的删除顺序是:清除用户表和文章表的关联关系(这又是因为用户表作为主动方,它必须通过此方法来维护关联关系->然后清除多方文章信息->最后才删除我们的一方用户 上面我们基本完成了我们的测试工作,下面我们对配置属性加以分析: 1. 相对于上一篇我们提到的ManyToOne属性,OneToMany独有的属性有:mapperBy和orphanRemoval,mpperBy是指放弃维护级联关系,具体我们在双向关联中再详细分析,这里比较独特的属性是orphanRemoval 表面意思是去除孤儿,当一方不再关联多方某一实体A时,自动从数据库中删除A。下面来看实例测试,假如我们先将其设为false @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = false)//用户作为一方使用OneToMany注解 @JoinColumn(name = "user_id") private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章 <div class="se-preview-section-delimiter"></div> 先看看我们数据库的初始记录信息: +—-+———–+ | id | name | +—-+———–+ | 2 | oneObject | +—-+———–+ 1 row in set (0.00 sec) mysql> select * from t_article2; +—-+————–+———+ | id | content | user_id | +—-+————–+———+ | 6 | moreContent0 | 2 | | 5 | moreContent1 | 2 | | 4 | moreContent2 | 2 | +—-+————–+———+ 3 rows in set (0.00 sec) 接着开始我们的测试: User user = (User) session.get(User.class,2); Article article = user.getArticles().iterator().next();//获取与用户有对应关系的一篇文章 user.getArticles().remove(article);//从用户对应关系中清除出来 session.update(user);//更新用户 <div class="se-preview-section-delimiter"></div> 运行测试代码,我们会看到控制台仅输出一条sql语句: Hibernate: update t_article2 set user_id=null where user_id=? and id=? 即 查询数据库,此时变成: mysql> select * from t_article2; +—-+————–+———+ | id | content | user_id | +—-+————–+———+ | 6 | moreContent0 | 2 | | 5 | moreContent1 | 2 | | 4 | moreContent2 | NULL | +—-+————–+———+ 3 rows in set (0.00 sec) 现在我们先恢复测试前的数据: mysql> update t_article2 set user_id = 2 where id = 4; 然后再将对应注解里的orphanRemoval设为true @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true) @JoinColumn(name = "user_id") private Set<Article> articles; <div class="se-preview-section-delimiter"></div> 再次运行我们的测试代码,控制台输出: Hibernate: update t_article2 set user_id=null where user_id=? and id=? Hibernate: delete from t_article2 where id=? 我们的文章记录就对应被删除了,这就是去除“孤儿”的含义,在实际开发中,正如我们的身份证总是从属于某个人的,如果失去这种从属关系,身份证就没有意义而可以去除了。 2. @JoinTable 在默认不使用@JoinColumn时,多对一关联中hibernate会为我们自动生成中间表,但如果我们像自己来配置中间表,就可以使用@JoinTable注解。它的实例配置如下: @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true)//用户作为一方使用OneToMany注解 @JoinTable(name = "t_user_articles",inverseJoinColumns = {@JoinColumn(name = "article_id")},joinColumns = {@JoinColumn(name = "article_id")}) private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章 <div class="se-preview-section-delimiter"></div> 其中: 1. name为创建的中间表名称。 2. inverseJoinColumns指向对方的表,在这里指多方的表。 3. joinColumns指向自己的表,即一方的表,这些指向都是通过主键映射来完成的。 运行我们的测试代码: User user = new User(); user.setName("oneObject"); Set<Article> articles = new HashSet<Article>(); for(int i = 0 ; i < 3;i ++){ Article article = new Article(); article.setContent("moreContent" + i) ; articles.add(article); } user.setArticles(articles);//建立关联关系 session.save(user); <div class="se-preview-section-delimiter"></div> 会发现我们的用户、文章对应关系都在中间表中建立起来了: mysql> select * from t_user_articles; +———+————+ | user_id | article_id | +———+————+ | 1 | 1 | | 1 | 2 | | 1 | 3 | +———+————+ 3 rows in set (0.00 sec) 使用中间表的好处是对原来两张表的结构不对造成任何影响。尤其是在一些老项目中我们可以不修改既定的表结构(事实上在一个项目古老庞大到一定程度就很难去改)、以不侵入原来表的方式构建出一种更清淅更易管理的关系。当然缺点是我们的我们需要维护多一张表,一旦中间表多了,维护起来会愈加麻烦。但综合来看,我们显然更推荐用中间表的方式来完成配置。 3. eqauls和hashCode方法 在我们开始配置一对多的一方时,我们通过Set来和多方建立关系,其中提到的一点是可以防止多方相同对象出现。这个相同对应我们数据库中就是某些属性列相同,比如:对于Article,如果id和content在两条记录中都一样,我们就可以认为两条记录是一致的,因此会自动去重那么我们来判断它们的重复关系呢?这个时候就要通过重写hashCode和equals方法了。 示例如下: //重写equals比较对象相等 @Override public boolean equals(Object obj) { if (this == obj)//如果地址引用相同,直接判断为相等 return true; if (obj == null)//如果目标对象为null,直接判断不等 return false; if (getClass() != obj.getClass())//两者类不一致 return false; User other = (User) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id))//判断两者id是否都存在且相等 return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name))//判断两者名字是否都存在且相等 return false; return true; } 一般来说,我们重写equal方法就能判断两个对象是否相等了,为什么还要重写hashCode方法呢?主要是考虑到效率的问题,对于equals方法,当比较规则比较复杂的话就会比较耗时了,而hashCode为每一个对象生成一个散列码(通过一种神秘的算法,一般为关键属性乘以一个质数),避免了比较慢的运算。不过我们不能因为快就单凭hash码来判断两个对象是否相等,因为hashCode并不能保证能为每一个不同的对象生成唯一的散列码,所以可能会有两个hash码相同,但对象确实不一致的情况。不过我们知道的是如果连hash码都不一致,那两个对象肯定是不一致的。根据此思路,我们可以很好地理解在java内部,是如何判断两个对象是否相等的:
4# 混合使用多种视图技术。 在前面文章里,我们对jsp、json、xml个中视图都进行了较为详细的实例解析,但涉及到的都是单视图使用配置。在实际开发中,我们可能需要混合是使用多种视图技术。尤其是针对REST编程风格,我们可以通过一个URL、多种视图来切合REST风格的同一资源、多种表述。 现在加入我们要输出JSP、JSON、XML多种视图技术,如果使用我之前文章《springMVC4(4)json与对象互转实例解析请求响应数据转换器 》提到的HttpMessageConvert来完成数据类型输出切换。它相对于多视图输出的局限性是: 1. 必须通过HTTP请求头的Accept来控制转换器的使用类型,如果客户端是安卓等还能通过HttpClient、RestTemplate等控制,但如果客户端是游览器,除非使用AJAX技术,否则很难控制请求头内容 2. 无法通过URL扩展名或请求参数来控制服务端的资源输出类型。而使用多种视图技术,我们可以通过以下形式控制输出不同视图: 1. 扩展名: 1. /user.xml 呈现xml文件 2. /user.json 呈现json格式 3. /user.xls 呈现excel文件 4. /user.pdf 呈现pdf文件 5. /user 使用默认view呈现,比如jsp等 2. 请求参数: 1. /user?type=xml 呈现xml文件 2. /user?type=json 呈现json格式 3. /user?type=xls 呈现excel文件 4. /user?type=pdf 呈现pdf文件 5. /user? 使用默认view呈现,比如jsp等 ContentNegotiatingViewResolver 我们使用ContentNegotiatingViewResolver视图解析器来完成多种视图混合解析,从它的名字上看,它是一个视图协调器,负责根据请求信息从当前环境选择一个最合适的解析器进行解析,也即是说,它本身并不负责解析视图。 它有3个关键属性: 1. favorPathExtension:如果设置为true(默认为true),则根据URL中的文件拓展名来确定MIME类型 2. favorPathExtension:如果设置为true(默认为false),可以指定一个请求参数确定MIME类型,默认的请求参数为format,可以通过parameterName属性指定一个自定义属性。 3. ignoreAcceptHeader(默认为false),则采用Accept请求报文头的值确定MIME类型。由于不同游览器产生的Accept头不一致,不建议采用Accept确定MIME类型。 在实际流程中,ContentNegotiatingViewResolver也是根据以上三个互斥属性的配置情况来确定视图类型,其中属性1优先级最高,属性3优先级最低 除了以上三个属性,还有一个关键属性是mediaTypes,用来配置不同拓展名或参数值映射到不同的MIME类型 在前面,我们展示了使用jsp/模板、json、xml、Excel等来呈现我们的视图,下面我们通过整合上述视图来分析我们的多视图混合技术。 多视图混合Rest呈现实例 1. 配置视图解析器 <!-- 根据确定出的不同MIME名,使用不同视图解析器解析视图 --> <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <!-- 设置优先级 --> <property name="order" value="1" /> <!-- 设置默认的MIME类型,如果没有指定拓展名或请求参数,则使用此默认MIME类型解析视图 --> <property name="defaultContentType" value="text/html" /> <!-- 是否不适用请求头确定MIME类型 --> <property name="ignoreAcceptHeader" value="true" /> <!-- 是否根据路径拓展名确定MIME类型 --> <property name="favorPathExtension" value="false" /> <!-- 是否使用参数来确定MIME类型 --> <property name="favorParameter" value="true" /> <!-- 上一个属性配置为true,我们指定type请求参数判断MIME类型 --> <property name="parameterName" value="type" /> <!-- 根据请求参数或拓展名映射到相应的MIME类型 --> <property name="mediaTypes"> <map> <entry key="html" value="text/html" /> <entry key="xml" value="application/xml" /> <entry key="json" value="application/json" /> <entry key="excel" value="application/vnd.ms-excel"></entry> </map> </property> <!-- 设置默认的候选视图,如果有合适的MIME类型,将优先从以下选择视图,找不到再在整个Spring容器里寻找已注册的合适视图 --> <property name="defaultViews"> <list> <bean class="org.springframework.web.servlet.view.InternalResourceView"> <property name="url" value="WEB-INF/views/hello.jsp"></property> </bean> <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" /> <ref local="myXmlView" /> <bean class="com.mvc.view.ExcelView" /> </list> </property> </bean> <!-- Excel视图 --> <bean class="com.mvc.view.ExcelView" id="excelView" /><!-- 注册自定义视图 --> <bean class="org.springframework.web.servlet.view.xml.MarshallingView" id="myXmlView"> <property name="modelKey" value="articles" /> <property name="marshaller" ref="xmlMarshaller" /> </bean> <bean class="org.springframework.oxm.xstream.XStreamMarshaller" id="xmlMarshaller"><!-- 将模型数据转换为XML格式 --> <property name="streamDriver"> <bean class="com.thoughtworks.xstream.io.xml.StaxDriver" /> </property> </bean> 关于以上视图文件的配置实体讲解可移步参考我前面的文章 2. jsp视图文件 <%@page import="com.mvc.model.Article"%> <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>hello spring mvc</title> </head> <body> <c:out value="${articles}"></c:out> </body> </html> 3. Excel配置文件 package com.mvc.view; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.springframework.web.servlet.view.document.AbstractExcelView; import com.mvc.model.Article; public class ExcelView extends AbstractExcelView { @Override protected void buildExcelDocument(Map<String, Object> model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception { List<Article> articles= (List<Article>) model.get("articles"); HSSFSheet sheet = workbook.createSheet("文章列表");//创建一页 HSSFRow header = sheet.createRow(0);//创建第一行 header.createCell(0).setCellValue("标题"); header.createCell(1).setCellValue("正文"); for( int i = 0; i < articles.size();i++){ HSSFRow row = sheet.createRow(i + 1); Article article = articles.get(i); row.createCell(0).setCellValue(article.getTitle()); row.createCell(1).setCellValue(article.getContent()); } } } 4. Article POJO类 package com.mvc.model; public class Article { private String title; private String content; //忽略get和set方法 @Override public String toString() { return "Article [ title=" + title + ", content=" + content + "]"; } } 5. 控制器测试方法 @RequestMapping("views") public String views(ModelMap map,HttpServletRequest request){ List<Article>articles = new ArrayList<Article>(); for(int i = 0 ; i < 5; i ++){ Article article = new Article(); article.setTitle("title" +i); article.setContent("content" + i); articles.add(article); } map.addAttribute("articles",articles);//将文章对象绑定到 return "views"; } 6. 进行测试 我们使用了参数type来映射不同的视图类型: 1. 默认参数类型: 2. html参数类型 3. json参数类型 4. xml参数类型 5. excel参数类型 点击下载后打开如下图所示: 7. 使用拓展名类型 上面我们使用参数的方法访问,如果我们改成使用拓展名的形式,如下所示,只需去掉其中的4行配置: <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"><!-- 根据确定出的不同MIME名,使用不同视图解析器解析视图 --> <property name="order" value="1" /><!-- 设置优先级 --> <property name="defaultContentType" value="text/html" /><!-- 设置默认的MIME类型,如果没有指定拓展名或请求参数,则使用此默认MIME类型解析视图 --> <property name="mediaTypes"><!-- 根据请求参数映射到相应的MIME类型 --> <map> <entry key="html" value="text/html" /> <entry key="xml" value="application/xml" /> <entry key="json" value="application/json" /> <entry key="excel" value="application/vnd.ms-excel"></entry> </map> </property> <property name="defaultViews"><!-- 设置默认的候选视图,如果有合适的MIME类型,将优先从以下选择视图,找不到再在整个Spring容器里寻找已注册的合适视图 --> <list> <bean class="org.springframework.web.servlet.view.InternalResourceView"> <property name="url" value="WEB-INF/views/hello.jsp"></property> </bean> <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" /> <ref local="myXmlView" /> <bean class="com.mvc.view.ExcelView" /> </list> </property> </bean> 这个时候,我们就可以使用: http://localhost:8080/springMVC/views http://localhost:8080/springMVC/views.html, http://localhost:8080/springMVC/views.json, http://localhost:8080/springMVC/views.xml 来对应得到与上面相同的内容。感兴趣的朋友可在下面下载源码自行测试 源码下载 本节内容源码可到http://github.com/jeanhao/spring的multiViews文件夹下下载
1. 模板视图 FreeMarkerViewResolver 、 VolocityViewResolver 这两个视图解析器都是 UrlBasedViewResolver 的子类。 FreeMarkerViewResolver 会把 Controller 处理方法返回的逻辑视图解析为 FreeMarkerView ,而 VolocityViewResolver 会把返回的逻辑视图解析为 VolocityView 。这两个视图解析器是类似的。 对于 FreeMarkerViewResolver 而言,它会按照 UrlBasedViewResolver 拼接 URL 的方式进行视图路径的解析。但是使用 FreeMarkerViewResolver 的时候不需要我们指定其 viewClass ,因为 FreeMarkerViewResolver 中已经把 viewClass 默认指定为 FreeMarkerView 了。 对于 FreeMarkerView 我们需要给定一个 FreeMarkerConfig 的 bean 对象来定义 FreeMarker 的配置信息。 FreeMarkerConfig 是一个接口, Spring 已经为我们提供了一个实现,它就是 FreeMarkerConfigurer 。我们可以通过在 SpringMVC 的配置文件里面定义该 bean 对象来定义 FreeMarker 的配置信息。当 FreeMarker 的模板文件放在多个不同的路径下面的时候,我们可以使用 templateLoaderPaths 属性来指定多个路径。 下面我们来看一个使用FreeMaker的实例: 在使用FreeMaker前需要导入相关的jar包,使用Maven可在pom.xml下添加如下信息: <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.24-incubating</version> </dependency> 在spring容器中配置视图解析器 <bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver"> <property name="prefix" value="fm_"/><!-- 指定文件前缀 --> <property name="suffix" value=".ftl"/><!-- 指定文件后缀 --> <property name="order" value="1"/><!-- 指定当前视图解析器的优先级 --> <property name="contentType" value="text/html; charset=utf-8" /><!-- 指定编码类型输出,防止出现中文乱码现象 --> </bean> <bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"> <property name="templateLoaderPath" value="/WEB-INF/freeMaker"/><!-- 指定模板文件存放位置 --> <property name="defaultEncoding" value="UTF-8" /><!-- 由于模板文件中使用utf-8编码,如果不显式指定,会采用系统默认编码,易造成乱码 --> <property name="freemarkerSettings"><!-- 定义FreeMaker丰富的自定义属性 --> <props> <prop key="classic_compatible">true</prop><!-- 当碰到对象属性为null时,返回一个空字符串而非抛出系统异常 --> </props> </property> </bean> 接下来我们定义如下一个 Controller : @Controller public class MyController { @RequestMapping("test") public ModelAndView test() { mav.addObject("hello", "hello World!"); mav.setViewName("freemarker"); return mav; } } 接下来在/WEB-INF/freeMaker目录下创建一个名为fm_hello.ftl的模板文件,内容如下: <html> <head> <title>FreeMarker</title> </head> <body> ${hello} </body> </html> 经过上面的定义当我们访问 /freemarker 的时候就会返回一个逻辑视图名称为“hello”的 ModelAndView 对象,根据定义好的视图解析的顺序,首先进行视图解析的是 FreeMarkerViewResolver ,这个时候 FreeMarkerViewResolver 会试着解析该视图,根据它自身的定义,它会先解析到该视图的 URL 为 fm_freemarker.ftl ,然后它会看是否能够实例化该视图对象,即在定义好的模板路径下是否有该模板存在,如果有则返回该模板对应的 FreeMarkerView。在本例中访问结果如下所示: hello World! 2. JSON视图输出 我们可以使用BeanNameViewResolver来输出JSON视图,spring配置文件如下所示: <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" > <property name="order" value="1"/> </bean> <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" id="userJson"><!--指定json视图类型--> <property name="modelKey" value="map" /> <!--实际开发中在控制器我们可能会有很多模型数据, 这里通过modelKey指定只输出名为user的模型数据--> <!--如果我们想指定多个模型数据输出,可以使用modelKeys,它是一个Set集合, 实例配置如下: <property name="modelKeys"> <set > <value>name1</value> <value>name2</value> </set> </property> --> </bean> 指定使用此视图输出后,我们编写我们的控制器输出一个Map来测试视图 @RequestMapping("jsonView") public String jsonView(ModelMap map ){ for(int i = 0 ; i < 5; i ++){ map.put("key" + i," value" + i); } return "userJson";//对应json视图的Bean名 } 访问jsonView,输出视图如下,注意箭头部分,我们的相应Content-Type变为了application/json,这也是json视图输出的特点 3. XML视图输出 我们使用spring-oxm来完成我们java对象到xml格式文本的转换,需先导入相关的jar包,使用maven可在pom.xml上添加: <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>4.1.5.RELEASE</version> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.9</version> </dependency> 我们使用MarshallingView来输出xml视图,还是使用BeanNameViewResolver来解析xml视图,它的配置如下所示: <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" /> <bean class="org.springframework.web.servlet.view.xml.MarshallingView" id="myXmlView"> <property name="modelKey" value="articles"/><!--输出模型中的articles属性--> <property name="marshaller" ref="xmlMarshaller" /><!--指定解析工具--> </bean> <bean class="org.springframework.oxm.xstream.XStreamMarshaller" id="xmlMarshaller"><!-- 将模型数据转换为XML格式 --> <property name="streamDriver"> <bean class="com.thoughtworks.xstream.io.xml.StaxDriver" /> </property> </bean> 下面是我们的Article POJO类和控制层配置: /*******************POJO类****************/ public class Article { private String title; private String content; //忽略get和set方法 } /**************控制层拦截方法********************/ @RequestMapping("xmlView") public String xmlView(ModelMap map){ List<Article>articles = new ArrayList<Article>(); for(int i = 0 ; i < 5; i ++){ Article article = new Article(); article.setTitle("title" +i); article.setContent("content" + i); articles.add(article); } map.addAttribute("articles",articles); return "myXmlView"; } 我们在游览器上访问xmlView,视图输出如下数据: <?xml version="1.0" ?> <list> <com.mvc.model.Article> <title>title0</title> <content>content0</content> </com.mvc.model.Article> <com.mvc.model.Article> <title>title1</title> <content>content1</content> </com.mvc.model.Article> <com.mvc.model.Article> <title>title2</title> <content>content2</content> </com.mvc.model.Article> <com.mvc.model.Article> <title>title3</title> <content>content3</content> </com.mvc.model.Article> <com.mvc.model.Article> <title>title4</title> <content>content4</content> </com.mvc.model.Article> </list> 同时相应Content-Type是application/xml;charset=UTF-8 4. 输出excel视图 输出excel视图需要先导入jar包: <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.14</version> </dependency> excel视图需要拓展AbstractExcelView并实现其抽象方法buildExcelDocument,下面是一个实例配置: package com.mvc.view; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.springframework.web.servlet.view.document.AbstractExcelView; import com.mvc.model.Article; public class ExcelView extends AbstractExcelView { @Override protected void buildExcelDocument(Map<String, Object> model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception { List<Article> articles= (List<Article>) model.get("articles"); HSSFSheet sheet = workbook.createSheet("文章列表");//创建一页 HSSFRow header = sheet.createRow(0);//创建第一行 header.createCell(0).setCellValue("标题"); header.createCell(1).setCellValue("正文"); for( int i = 0; i < articles.size();i++){ HSSFRow row = sheet.createRow(i + 1); Article article = articles.get(i); row.createCell(0).setCellValue(article.getTitle()); row.createCell(1).setCellValue(article.getContent()); } } } spring容器配置如下所示: <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" /> <bean class="com.mvc.view.ExcelView" id="excelView" /><!-- 注册自定义视图 --> 编写控制器: @RequestMapping("excelView") public String excelView(ModelMap map){ List<Article>articles = new ArrayList<Article>(); for(int i = 0 ; i < 5; i ++){ Article article = new Article(); article.setTitle("title" +i); article.setContent("content" + i); articles.add(article); } map.addAttribute("articles",articles); return "excelView"; } 在游览器访问,则提示下载文件: 下载打开显示如下内容:
为什么要说是“封装方法”呢?因为它帮我们封装好了底层的增删改查操作,直接调用相应方法即可灵活地操作我们数据库数据。它们由Session接口提供,下面我们通过实例一一分析这些方法。 1.save方法 Session 的 save() 方法使一个临时对象转变为持久化对象 Session 的 save() 方法完成以下操作: 1. 把 User对象加入到 Session 缓存中,使它进入持久化状态 2. 选用映射文件指定的标识符生成器,为持久化对象分配唯一的 OID。在 使用代理主键的情况下,setId() 方法为 User对象设置 OID 是无效的。 3. 计划在 flush 缓存的时候,执行一条 insert 语句。 注意: 1. Hibernate 通过持久化对象的 OID 来维持它和数据库相关记录的对应关系。当 User 对象处于持久化状态时,不允许程序随意修改它的 ID,否则会报异常:org.hibernate.HibernateException: identifier of an instance of com.zeng2.model.User was altered from 17(持久化状态的原有值) to 100(新修改的值) 。 所以在实际开发中,我们应该将setId()设为private防止用户更改实体id 2. 在一般完成持久化对象的初始化工作时,我们应先完成资源准备(设定成员属性值)后,再调用save方法。否则会增加额外的sql语句,如下所示: //先设值再保存 User user1 = new User(); user1.setName("name1"); session.save(user1); //Hibernate: insert into t_user2 (name, id) values (?, ?) //先保存再设值 User user2 = new User(); session.save(user2); user2.setName("name2"); //Hibernate: insert into t_user2 (name, id) values (?, ?) //Hibernate: update t_user2 set name=? where id=? update t_user2 set name=? where id=? 2. persist方法 persist和save都能用来持久化对象,但它和user的区别是:当对一个 OID 不为 Null 的对象执行 save() 方法时,会把该对象以一个新的 oid 保存到数据库中;但执行 persist() 方法时会抛出一个异常。 //测试persist和save的区别 User user = new User(); user.setId(100); session.save(user); //执行Hibernate: insert into t_user2 (name, id) values (?, ?) User user2 = new User(); user2.setId(200); session.persist(user2); //报异常org.hibernate.PersistentObjectException: detached entity passed to persist: com.zeng2.model.User 3. get和load方法 相同点: 都可以根据跟定的 OID 从数据库中加载一个持久化对象 区别: 当数据库中不存在与 OID 对应的记录时,load() 方法抛出 ObjectNotFoundException 异常,而 get() 方法返回 null。 两者采用不同的延迟检索策略:load默认使用懒加载策略,除非将相应对象的获取策略设为即时加载。而get只要调用了,都会立即从数据库中甲在数据。 4. update方法 使一个游离对象转变为持久化对象,并且计划在flush时执行一条 update 语句 在一下情况使用会抛出异常: 1. 当 update() 方法关联一个游离对象时,如果在 Session 的缓存中已经存在相同 OID 的持久化对象,会抛出异常 2. 当 update() 方法关联一个游离对象时,如果在数据库中不存在相应的记录,也会抛出异常. 下面是我们的测试代码: //测试update User user = new User(); session.update(user); //报异常:org.hibernate.TransientObjectException: The given object has a null identifier: com.zeng2.model.User User user = new User(); user.setId(1);//数据库中存在的id session.update(user); //Hibernate: update t_user2 set name=? where id=? User user = new User(); user.setId(111111);//数据库中不存在的id session.update(user); //报异常:org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1 User user = session.get(User.class, 1); User user2 = new User(); user2.setId(1); session.update(user2);//更新一个在session缓存中已存在的对象 //报异常:org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.zeng2.model.User#1] 5. saveOrUpdate方法 Session 的 saveOrUpdate()可以看成是save和update的整合,两者形成有效互补。 它的执行过程吐下图所示: Created with Raphaël 2.1.0saveOrUpdate对象游离对象(yes),临时对象(no)更新对象保存对象yesno 我们常常根据id是否为null来判定来判断一个对象的状态,下面来看一个测试实例: //测试saveOrUpdate //1. 临时对象 User user = new User(); session.saveOrUpdate(user); //Hibernate: insert into t_user2 (name, id) values (?, ?) //2. 设置id构造伪游离对象 User user = new User(); user.setId(1);//数据库存在此id对应记录 session.saveOrUpdate(user); //Hibernate: update t_user2 set name=? where id=? //3. 构造id,但不存在于数据库,会报异常 User user = new User(); user.setId(111111);//数据库不存在此id对应记录 session.saveOrUpdate(user); //org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1 6. merge方法 把一个游离对象的属性复制到一个持久化对象中.先看我们的测试代码: //先尝试使用update方法用新对象更新Session缓存中已存在id标识为1的对象 // User user = session.get(User.class, 1); // User user2 = new User(); // user2.setId(1); // session.update(user2); //报异常:org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.zeng2.model.User#1] //测试merge User user = session.get(User.class, 1); System.out.println(user3.getName());//name User user2 = new User(); user2.setId(1); user2.setName("newName"); session.merge(user2); System.out.println(user3.getName());//newName System.out.println(user == user2);//false System.out.println(user2 == user3);//false System.out.println(user3 == user);//true //Hibernate: update t_user2 set name=? where id=? /*查看数据库,记录成功更新: +----+---------+ | id | name | +----+---------+ | 1 | newName | */ 从上面我们可以看出,当session存在特定id的对象时,我们尝试自己伪造一个相同id对象并通过update来对其持久化时,会出现错误.而我们使用merge时却能成功更新,下面我们结合这个例子来分析调用merge方法的运行流程: 1. 如果user2是一个游离对象(id不为null) 先到session缓存中查找id和user2相同的User,如果找到了(找到user),并将user2的值复制给user,在flush时执行update语句,从这里看,这就是我们操作更新的原因.还没完,它还会返回user的一个引用(这时候,user的值是user2复制完成后的值)所以打印时user3.getName=newName,并且user3 == user 如果没在session缓存中找到,会查询数据库有没相同的记录: 如果存在,则获取下来(设为oUser,此时未持久化对象),并将user2的属性复制到oUser中,同时返回oUser的引用到user3,并计划在flush时执行一条update语句更新数据库 如果不存在,则会新建一个对象(设为nUser),将user2的属性复制到这个nUser中,再调用save方法持久化nUser,并返回nUser的引用,此时nUser == user3 为true 2. 如果user2是一个临时对象(id为null) 新建一个对象(设为nUser),将user2的属性复制到这个nUser中,再调用save方法持久化nUser,并返回nUser的引用,此时nUser == user3 为true 根据这个流程分析,在整个过程中,我们的user2始终是非持久性对象.不像使用update,方法调用成功后,入参必定为持久化对象. 下面是一张流程执行框图,能够加深我们的理解: 7. delete方法 Session 的 delete() 方法既可以删除一个游离对象,也可以删除一个持久化对象 Session 的 delete() 方法处理过程: 1. 计划执行一条 delete 语句(在flush时正式执行),把对象从 Session 缓存中删除,该对象进入删除状态。 2. Hibernate 的 cfg.xml 配置文件中有一个 hibernate.use_identifier_rollback 属性,其默认值为 false,若把它设为 true,将改变 delete() 方法的运行行为:delete() 方法会把持久化对象或游离对象的 OID 设置为 null,使它们变为临时对象。这样程序就可以重复利用这些对象了 参考:http://www.myexception.cn/software-architecture-design/1996251.html
在 《springMVC4(7)模型视图方法源码综合分析》 一文中,我们介绍了ModelAndView的用法,它会在控制层方法调用完毕后作为返回值返回,里面封装好了我们的业务逻辑数据和视图对象或视图名 。下一步,视图对象往往会对模型进一步渲染,再由视图解析器进一步解析并向前端发出响应。在下面,我们详细介绍视图和视图解析器的各种分类。 在View接口中,定义了一个核心方法是: void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception; 它的作用主要是渲染模型数据,整合web资源,并以特定形式响应给客户,这些形式可以是复杂JSP页面,也可以是简单的json、xml字符串。 针对不同的响应形式,spring为我们设计了不同的View实现类: 针对不同的视图对象,我们使用不同的视图解析器来完成实例化工作。我们可以在Spring上下文配置多个视图解析器,并通过order属性来指定他们之间的解析优先级顺序,order 越小,对应的 ViewResolver 将有越高的解析视图的权利。当一个 ViewResolver 在进行视图解析后返回的 View 对象是 null 的话就表示该 ViewResolver 不能解析该视图,这时候就交给优先级更低的进行解析,直到解析工作完成,如果所有视图解析器都不能完成将解析,则会抛出异常。 类似于视图,Spring也为我们提供了众多的视图解析器实现类: 1. AbstractCachingViewResolver 这是一个抽象类,这种视图解析器会把它曾经解析过的视图保存起来,然后每次要解析视图的时候先从缓存里面找,如果找到了对应的视图就直接返回,如果没有就创建一个新的视图对象,然后把它放到一个用于缓存的 map 中,接着再把新建的视图返回。使用这种视图缓存的方式可以把解析视图的性能问题降到最低。 2. UrlBasedViewResolver 它继承了AbstractCachingViewResolver ,通过prefix、suffix**拼接 URL** 的方式来解析视图,支持返回的视图名称中包含 redirect: 、forward等前缀进行重定向或转发。使用 UrlBasedViewResolver 的时候必须指定属性 viewClass ,表示解析成哪种视图,一般使用较多的就是 InternalResourceView ,利用它来展现 jsp ,但是当我们使用 JSTL 的时候我们必须使用 JstlView 。下面是一段 UrlBasedViewResolver 的实例定义: <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="prefix" value="/WEB-INF/" /> <property name="suffix" value=".jsp" /> <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceView"/> </bean> 3. InternalResourceViewResolver 它是 URLBasedViewResolver 的子类,所以 URLBasedViewResolver 支持的特性它都支持. InternalResourceViewResolver 会把返回的视图名称都解析为 InternalResourceView 对象, InternalResourceView 会把 Controller 处理器方法返回的模型属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 重定向到目标 URL 。下面是一个配置实例: <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/"/> <property name="suffix" value=".jsp"></property> </bean> 如果我们需要使用JstlView,则需指定vlewClass属性为JstlView。 在前面的测试实例中,我们一直在使用这种视图解析器,所以不再举例。 4. BeanNameViewResolver 根据它的名字,我们将视图在spring容器中注册为Bean,Bean的id即为视图名。在我们控制层返回视图时,BeanNameViewResolver会自动根据我们的试图名找到对应的视图Bean进行解析,下面我们看一个实例: 1. 配置视图解析器和视图 <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> <property name="order" value="1"/><!-- 设置优先级最高,最先开始解析--> </bean> <bean id="hello" class="org.springframework.web.servlet.view.InternalResourceView"> <property name="url" value="/WEB-INF/views/hello.jsp"/><!--访问对应的jsp文件--> </bean> 2. 配置控制器 配置好视图和视图解析器后,我们可以在控制层通过如下方法访问视图: @Controller public class ViewController { @RequestMapping("view1") public String view1(){ return "hello";//返回字符串直接为视图名称,解析器会找到名称对应的视图Bean解析视图 } } 我们在游览器访问view1即会跳转到对应的hello.jsp视图。 3. XmlViewResolver 它继承自 AbstractCachingViewResolver 抽象类,所以它也是支持视图缓存的。 XmlViewResolver 需要给定一个 xml 配置文件来定义视图的 bean 对象。在该文件中定义的每一个视图的 bean 对象都给定一个名字,然后 XmlViewResolver 将根据 Controller 处理器方法返回的逻辑视图名称到 XmlViewResolver 指定的配置文件中寻找对应名称的视图 bean 用于处理视图。该配置文件默认是 /WEB-INF/views.xml 文件,如果不使用默认值的时候可以在 XmlViewResolver 的 location 属性中指定它的位置。 这里需要明确的是,使用XmlViewResolver最终不一定需要输出xml视图 以下是使用 XmlViewResolver 的一个示例: 1. 在 SpringMVC 的配置文件中加入 XmlViewResolver 的 bean 定义。 使用 location 属性指定其配置文件所在的位置, order 属性指定当有多个 ViewResolver 的时候其处理视图的优先级。 <bean class="org.springframework.web.servlet.view.XmlViewResolver"> <property name="location" value="/WEB-INF/views.xml"/> <property name="order" value="1"/> </bean> 2. 在 XmlViewResolver 对应的配置文件中配置好所需要的视图定义。 在下面的代码中我们就配置了一个名为 hello 的 InternalResourceView ,其 url 属性为“ /index.jsp ”。 <bean id="hello" class="org.springframework.web.servlet.view.InternalResourceView"> <property name="url" value="/WEB-UBF/view/hello.jsp"/> </bean> 3. 配置web层控制器 @Controller public class ViewController { @RequestMapping("view2") public String view2(){ return "hello";//返回字符串直接为视图名称,解析器会找到名称对应的视图Bean解析视图 } } 类似于BeanNameViewResolver的实例,我们访问view2,XmlViewResolver会到/WEB-INF/views.xml中寻找相应id的视图完成解析。 5. ResourceBundleViewResolver 它继承自 AbstractCachingViewResolver ,但是它缓存的不是视图.和 XmlViewResolver 一样它也需要有一个配置文件来定义逻辑视图名称和真正的 View 对象的对应关系,不同的是 ResourceBundleViewResolver 的配置文件是一个属性文件,而且必须是放在 classpath 路径下面的,默认情况下这个配置文件是在* classpath 根目录下的 views.properties 文件,如果不使用默认值的话,则可以通过属性 baseName 或 baseNames 来指定*。 baseName 只是指定一个基名称, Spring 会在指定的 classpath 根目录下寻找以指定的 baseName 开始的属性文件进行 View 解析,如指定的 baseName 是 base ,那么 base.properties 、 baseabc.properties 等等以 base 开始的属性文件都会被 Spring 当做 ResourceBundleViewResolver 解析视图的资源文件。 ResourceBundleViewResolver 使用的属性配置文件的内容类似于这样: resourceBundle.(class)=org.springframework.web.servlet.view.InternalResourceView resourceBundle.url=/index.jsp test.(class)=org.springframework.web.servlet.view.InternalResourceView test.url=/test.jsp 对应与上述配置,spring会将其解析成如下Bean定义: <bean id="resourceBundle" class="org.springframework.web.servlet.view.InternalResourceView"> <property name="url" value="/index.jsp"/> </bean> <bean id="test" class="org.springframework.web.servlet.view.InternalResourceView"> <property name="url" value="/test.jsp"/> </bean> 接下来讲讲 Spring 通过 properties 文件生成 bean 的规则。它会把 properties 文件中定义的属性名称按最后一个点“ . ”进行分割,把点前面的内容当做是 bean 名称,点后面的内容当做是 bean 的属性。这其中有几个特别的属性, Spring 把它们用小括号包起来了,这些特殊的属性一般是对应的 attribute ,但不是 bean 对象所有的 attribute 都可以这样用。其中 (class) 是一个,除了 (class) 之外,还有 (scope) 、 (parent) 、 (abstract) 、 (lazy-init) 。而除了这些特殊的属性之外的其他属性, Spring 会把它们当做 bean 对象的一般属性进行处理,就是 bean 对象对应的 property 。所以根据上面的属性配置文件将生成如下两个 bean 对象: 从 ResourceBundleViewResolver 使用的配置文件我们可以看出,它和 XmlViewResolver 一样可以解析多种不同类型的 View ,因为它们的 View 是通过配置的方式指定的,这也就意味着我们可以指定 A 视图是 InternalResourceView , B 视图是 JstlView 。 除了以上的视图解析器,常用的还有下面两个模板视图解析器:FreeMarkerViewResolver 、 VolocityViewResolver,在后面的文章会专门提到。 参考书籍:《Spring 3.x企业应用开发实战》 参考文章:http://www.tuicool.com/articles/RVb67r
状态类型 在hibernate中,java对象的声明周期对应有4种状态: 状态 说明 瞬时(Transient) 由new操作符创建,且尚未与Hibernate Session 关联的对象被认定为瞬时(Transient)的。瞬时(Transient)对象不会(在清理Session时)被持久化到数据库中,也不会被赋予持久化标识(identifier)。使用Hibernate Session可以将其变为持久(Persistent)状态。(Hibernate会自动执行必要的SQL语句) 持久(Persistent) 持久(Persistent)的实例在数据库中有对应的记录,并拥有一个持久化标识(identifier)。 持久(Persistent)的实例可能是刚被保存的,或刚被加载的,无论哪一种,按定义,它存在于相关联的Session作用范围内。 Hibernate会检测到处于持久(Persistent)状态的对象的任何改动,在清理Session时将对象数据(state)与数据库同步(synchronize)。我们不需要手动执行UPDATE。将对象从持久(Persistent)状态变成瞬时(Transient)状态同样也不需要手动执行DELETE语句,也会将数据库中相应对象删除。 脱管(Detached),也叫游离 与持久(Persistent)对象关联的Session被关闭后,对象就变为脱管(Detached)的。 对脱管(Detached)对象的引用依然有效,对象可继续被修改。脱管(Detached)对象如果重新关联到某个新的Session上, 会再次转变为持久(Persistent)的(在Detached其间的改动将被持久化到数据库)。这在现实开发场景中颇有意义,如果我们某个对象属性要等待用户输入修改,可以先关闭session,释放数据库资源,在获取到用户修改信息后,再将此对象关联到新的Session中更新数据库 删除(removed) Session将要对象从数据库中删除,但此时在程序中改对象仍存在,变为removed状态,如对使用级联删除User,则其对应Article对象也会被删除 下列这张图片展示了在hibernate操作中对象状态的转换关系 状态特征 下面是这4种状态的相应特征 对象状态 状态特征 临时对象(Transient) 在使用代理主键的情况下,OID 通常为 null2. 不处于 Session 的缓存中3. 在数据库中没有对应的记录 持久化对象(也叫”托管”)(Persist) OID 不为 null2. 位于 Session 缓存中3. 若在数据库中已经有和其对应的记录,持久化对象和数据库中的相关记录对应4. Session 在 flush 缓存时,会根据持久化对象的属性变化,来同步更新数据库5. 在同一个 Session 实例的缓存中,数据库表中的每条记录只对应唯一的持久化对象 删除对象(Removed) 在数据库中没有和其 OID 对应的记录2. 不再处于 Session 缓存中3. 一般情况下,应用程序不该再使用被删除的对象 游离对象(也叫”脱管”) (Detached) OID 不为 null2. 不再处于 Session 缓存中3. 一般情况需下,游离对象是由持久化对象转变过来的,因此在数据库中可能还存在与它对应的记录 在下一篇文章里,我们会介绍如何通过Session接口操纵各种对象状态,通过零SQL语句,完成相应的数据库增删改查操作。
java对象在JVM中的存活条件 在java中,我们使用User user = new User();来创建一个java对象时,JVM会为其分配一块内存空间,此时,这个对象被变量“user”引用,那么它就会一直存在于内存中,而如果我们我们的“引用者user”升级了,User user = new VipUser()。那么原来new User()不再被任何变量引用,它就会结束自己的生命周期,然后会被JVM的智能垃圾回收期回收处理,以免再占用内存。 从以上分析,我们知道了java对象存活的条件就是:被(至少一个)变量引用 hibernate的对象存活条件 同样的,假设在我们使用hibernate访问数据库获取了一个小A对象,这个小A一样有它的存活条件,但与一般java对象不同,即使我们没有创建任何变量来引用小A,我们的小A还是能够活得好好的,这是因为小A被hibernate的Session缓存下来了。 理解Session的缓存机制 1. 缓存的实现机制: 在Session接口的实现类中,我们定义了一系列的java集合来存放从数据库中获取的数据,只要我们的Session实例没有结束声明周期,那么存放其中的对象就不会结束其生命周期。 2. Session缓存的作用 1. 减少对数据库的访问次数,优化性能。 比如我们来看下面的例子 Long time1 = System.currentTimeMillis();//记录时间 User user1_1 = session.get(User.class,1); Long time2 = System.currentTimeMillis();//记录结束时间1 System.out.println("user1_1获取完毕,耗时:"+(time2 - time1)+"毫秒,准备开始获取user1_2"); User user1_2 = session.get(User.class,1);//与上面id相同 System.out.println("user1_2获取完毕,耗时:"+(System.currentTimeMillis() - time2 )+"毫秒,准备开始获取user2"); User user2 = session.get(User.class,2); System.out.println(user1_1 == user1_2); System.out.println(user1_2== user2); 运行程序,观察我们的打印信息: Hibernate: select user0_.id as id1_0_0_, user0_.name as name2_0_0_ from t_user2 user0_ where user0_.id=? user1_1获取完毕,耗时:26毫秒,准备开始获取user1_2 user1_2获取完毕,耗时:0毫秒,准备开始获取user2 Hibernate: select user0_.id as id1_0_0_, user0_.name as name2_0_0_ from t_user2 user0_ where user0_.id=? true false 在我们获取user1_2时,并没有查询数据库而且获取时间几乎为0,说明是直接从缓存中读取的,而在比较对象属性中,user1_1和user1_2相等,说明它们的引用地址也相同,而且必定与session缓存中的引用地址一致 从上面我们还能看到,Session标识缓存的不同对象,是通过对象类型和对象标识符id共同判别的,一旦两者一致,session即判别为同一对象,同时,我们也可归纳出利用session查询数据库的过程:比如我们要查询id为1的User,则查询过程如下时序图所示: Created with Raphaël 2.1.0SessionSessionSession缓存Session缓存数据库数据库:1. 查找缓存是否有对象类型为User且id为12. 找到并返回2. 没找到则查询数据库3. 返回数据结果并缓存4. 返回数据到引用变量 2. 保证数据库中的记录和缓存中的相应对象内容一致 在session清理缓存(flush)时,会进行脏检查,如果发现缓存中的最新数据与数据库记录不一致,会将最新数据更新到数据库中。 那么,session是如何进行脏检查的呢?难道每次清理前,针对所有的缓存数据访问数据库来进行匹对?这样效率太低了。实际上,在上述时序图的第3步到第4步之间,Session会将获得的数据结果先copy一份(这份copy还未经任何处理,肯定是和数据库记录一致的),再返回给引用变量。这样我们将所有最新的数据与最初copy的校对一下,一旦出现差异,就将最新数据更新到数据库。 3. Session缓存的清理 在我们每次针对引用变量修改对象属性后,对应的Session缓存中的数据也会被修改,这是显然的,因为它们的所指向的内存地址是一致的。但修改后,hibernate并不会马上执行相应的数据库操作,只有在特定条件下,如session被清理或特定的方法被调用才会访问数据库。这里谈谈session被清理的三个时间点: 1. 在完成事务提交之前,session会被清理一次。这样的好处是一方面可以减少在事务作用过程中,大量执行的数据库记录修改操作。另一方面还可以尽可能缩短当前事务对相关资源的锁定时间 2. 在执行一些复杂的查询操作时,需要清理缓存,更新数据库,确保查询得到的数据是最新的。 3. 显示地调用Session.flush()方法 如果我们不希望在上述的某些时刻清理,我们可以通过Session的setFlushMode()方法来定制,它提供了3种模式共我们选择: 模式 复杂查询方法被执行 事务提交时 显式调用flush() 使用场景 FlushMode.AUTO(默认模式) 清理 清理 清理 正常应用场景 FlushMode.COMMIT 不清理 清理 清理 需要避免过多查询操作清理缓存以提高性能的场景 FlushMode.NEVER 不清理 不清理 清理 需要长时间运行的复杂事务场景 tips:从上面我们还能看出,我们要修改用户信息,完全用显式地执行session.update(user)语句,只需直接修改Session缓存对象属性即可,如下所示 user.setName("newName"); session.flush(); 我们数据库中相应的User记录name属性也被修改了! 此外,Session在清理缓存时,按照以下顺序执行sql语句。 1。按照应用程序调用save()方法的先后顺序,执行所有的对实体进行插入的insert语句。 2。所有对实体进行更新的update语句。 3。所有对实体进行删除的delete语句。 4。所有对集合元素进行删除、更新或插入的sql语句。 5。执行所有对集合进行插入的insert语句。 6。按照应用程序调用delete()方法的先后执行,执行所有对实体进行删除的delete语句。
1. 复杂对象参数绑定 对于普通的对象参数绑定,我们只需要对象成员变量名与请求参数名一一对应即可完成绑定。 而求对于组合对象,我们可以使用级联的方式来绑定方法参数。见下面实例: 我们先定义两个POJO类:User,Article其中Atricle是User的成员属性: public class Article { private Integer id; private String title; private String content; //忽略get和set方法 } package com.mvc.model; public class User { public User() { super(); } private Integer id; private String userName; private String password; private Article article;//组合对象 //忽略get和set方法 } 下面是我们的测试前端表单: <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>test</title> </head> <body> <form action="saveUser"> 用户名:<input type="text" name="userName"><br> 密码:<input type="text" name="password"><br> 文章标题:<input type="text" name="article.title"><br> 文章内容:<input type="text" name="article.content"><br> <input type="submit" value="提交"> </form> </body> </html> 下面是我们的控制层方法: @Controller public class UserController { @RequestMapping("saveUser") @ResponseBody public User saveUser(User user){ return user;//直接将获取的user对象json格式化输出 } } 我们在前端表单输入参数如下所示: 点击提交后,页面输出: 或者我们也可直接通过访问如下链接得到相同结果: http://localhost:8080/springMVC/saveUser?userName=username&password=password&article.title=title&article.content=content 从上可知,对于User的成员属性article,如果我们绑定其对应的参数,可以通过级联article.title,article.content来完成。 在这里,如果我在控制器的方法入参中,同时定义多个user,或者有多个不同的类实例对象。只要它们的成员属性名和参数名相同,都会完成绑定 2. 数组参数绑定 数组参数分为普通类型数组和复杂对象数组两种,但由于没有复杂对象数组的构造方法,springMVC只能绑定普通类型数组。 普通类型数组是指Integer、Stirng、Long等基本数据类型的包装类,下面通过实例来看如何完成绑定工作: 控制器方法: @RequestMapping("getIds") @ResponseBody public Integer[] saveUser(Integer[] ids){ return ids; } 前端表单定义: <form action="getIds" method="post"> id1:<input type="text" name="ids"><br> id2:<input type="text" name="ids"><br> id3:<input type="text" name="ids"><br> <input type="submit" value="提交"> </form> 提交如下数据: 或我们也可直接在游览器地址栏访问:http://localhost:8080/springMVC/getIds?ids=11&ids=22&ids=33 此时游览器输出: 集合类型参数绑定 对于list、Set、Map等的参数绑定,如果我们尝试直接绑定,是会失败的,必须将其作为一个具体类对象的成员属性,这个时候我们也可称这个具体类对象为一个包装类。先看下面失败实例: @RequestMapping("getIds2") @ResponseBody public ArrayList<Integer> getIds2(ArrayList<Integer> ids){//我们尝试将id集绑定一个List中 return ids; } 我们的请求url和输出结果如下图所示: 它的输出结果为空json数组,说明我们的绑定失败了。 这里遇到的一个主要问题是,如果我们绑定Set类型参数时,必须先为Set添加预定大小的容器,否则会报错。而且也不支持基本数据类型包装类的Set绑定,**如果需要完成这一转换,需要我们自定义转换器来实现。 下面我们通过一个完整的综合例子来展示集合类型的参数绑定: 1. POJO类 package com.mvc.model; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class User { private String userName; private String password; private List<Integer> numList; private List<Article> articleList; private Set<Article> articleSet; private Map<String, Integer> numMap; private Map<String, Article> articleMap; public User(){//这里我们为Set预初始化两个Article的容量大小 articleSet = new HashSet<Article>(); Article article = new Article(); articleSet.add(article); Article article2 = new Article(); articleSet.add(article2); numSet = new HashSet<Integer>(); } //忽略get和set方法 } 2. 控制层配置 我们的控制层方法极为简单: @RequestMapping("getAll") @ResponseBody public User getAll(User user){ return user; } 3. 测试表单代码: <form action="getAll" > 用户名:<input type="text" name="userName"><br> 密码:<input type="text" name="password"><br> numList部分:<br> numList[0]:<input type="text" name="numList[0]"><br> numList[1]:<input type="text" name="numList[1]"><br> articleList部分:<br> articleList[0].title:<input type="text" name="articleList[0].title"><br> articleList[0].content:<input type="text" name="articleList[0].content"><br> articleList[1].title:<input type="text" name="articleList[1].title"><br> articleList[1].content:<input type="text" name="articleList[1].content"><br> articleSet部分:<br> articleSet[0].title:<input type="text" name="articleSet[0].title"><br> articleSet[0].content:<input type="text" name="articleSet[0].content"><br> articleSet[1].title:<input type="text" name="articleSet[1].title"><br> articleSet[1].content:<input type="text" name="articleSet[1].content"><br> numMap部分:<br> numMap[0]:<input type="text" name="numMap['num1']"><br> numMap[1]:<input type="text" name="numMap['num2']"><br> articleMap部分:<br> articleMap[0].title:<input type="text" name="articleMap['article1'].title"><br> articleMap[0].content:<input type="text" name="articleMap['article2'].content"><br> articleMap[1].title:<input type="text" name="articleMap['article2'].title"><br> articleMap[1].content:<input type="text" name="articleMap['article2'].content"><br> <input type="submit" value="提交"> 3. 测试参数输入 我们输入如下图所示的参数: 点击提交按钮,获得输出数据: 或者我们也可以通过游览器地址访问: http://localhost:8080/springMVC/getAll?userName=username1&password=password1&numList[0]=11&numList[1]=22&articleList[0].title=title1&articleList[0].content=content1&articleList[1].title=title2&articleList[1].content=content2&articleSet[0].title=title3&articleSet[0].content=cotent3&articleSet[1].title=title4&articleSet[1].content=cotent4&numMap[%27num1%27]=55&numMap[%27num2%27]=66&articleMap[%27article1%27].title=title5&articleMap[%27article1%27].content=content5&articleMap[%27article2%27].title=title6&articleMap[%27article2%27].content=content6 从上面我们可以看到,使用绑定List和Set入参都是以成员属性名[索引](.级联成员属性名)的形式完成绑定,使用Map的话则以成员属性名[键名](.级联成员属性名)的形式完成绑定
需求实例引入 在实际开发中,我们会常常遇到需要对日期格式、数值格式进行转换的需求。在spring中,我们可以轻松通过注解的方式完成对数据的格式化处理,比如现在有个User POJO类: package com.mvc.model; import java.util.Date; public class Person { private String name; @DateTimeFormat(pattern = "yyyy-MM-dd") private Date birthday; @NumberFormat(pattern = "#.###k") private Long salary; //ignore getter and setter @Override public String toString() { return "Person [name=" + name + ", birthday=" + birthday + ", salary=" + salary + "]"; } } 我们希望通过上面两个注解,将birthdat如1995-01-01的字符串形式与java.util.Date的日期形式相互转换,将salary如15.000K的字符串形式与Long型的15000相互转换。 为了完成我们的需求,我们需要先了解如下知识。 AnnotationFormatterFactory接口 它使我们的注解与属性类型关联起来。它的定义如下: public interface AnnotationFormatterFactory<A extends Annotation> { //通过此方法获取(也能理解为设置)哪些属性类可以被注解A标注 Set<Class<?>> getFieldTypes(); //获取特定属性的格式化输出器 Printer<?> getPrinter(A annotation, Class<?> fieldType); //获取特定属性格式化输入(解析)器 Parser<?> getParser(A annotation, Class<?> fieldType); } FormattingConversionService 继承自ConversionService,运行时类型转换和格式化服务接口,提供运行期类型转换和格式化的支持。 其对应存在一个工厂类FormattingConversionServiceFactoryBean,我们也可以通过其注册自定义转换器。它的地位相当于我们上一篇文章提到的ConversionServiceFactoryBean。我们将其装配在的conversion-service属性中。下面是我们的实例配置: <!-- 通过:annotation-driven的conversion-service属性来装配我们的类型转换器 --> <mvc:annotation-driven /> <bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="converters"><!-- 在属性converters注册 --> <property name="converters"> <list> <bean class="com.mvc.convertor.MyConvertorFactory" /> </list> </property> </bean> 注册完后,我们即可进行我们的web测试: @Controller public class PersonController { @RequestMapping("convert") public void convert( Person person){ System.out.println(person); } } 启动服务器,我们在游览器中访问: 控制台对应输出:http://localhost:8080/springMVC/convert?name=myName&birthday=1995-01-01&salary=5.000k Person [name=myName, birthday=Sun Jan 01 00:00:00 CST 1995, salary=5] 从这里我们看出,成功完成了请求参数到User对象属性的格式化转换。 另一方面,如果我们想单独使用格式转化器,也可直接在方法入参中使用注解,看下面实例: @RequestMapping("convert2") public void convert2(@DateTimeFormat( pattern = "yyyy-MM-dd") Date date,@NumberFormat( pattern = "#k") Long salary ){ System.out.println(date); System.out.println(salary); } 启动服务器,我们在游览器中访问:http://localhost:8080/springMVC/convert?name=myName&birthday=1995-01-01&salary=50k 控制台对应输出: Sun Jan 01 00:00:00 CST 1995
在《springMVC4(9)属性编辑器剖析入参类型转换原理 》一文中,我们通过分析Sping内置的属性编辑器来理解springMVC是如何完成请求参数到入参的类型的转换的。而在新版本中,SpringMVC使用了新的架构来完成类型转换的工作,而且它的工作更加强大,支持格式化参数输入输出,它的另一个实例可见我的另一篇文章《springMVC4(4)json与对象互转实例解析请求响应数据转换器》。在文中,我们使用了Spring内置的格式转换器完成了服务端输入输出过程中json字符串与java对象的相互转换。此外,还提到了其他多种的格式转换器,如xml、ByteArray等。下面,我们以自定义格式转换器的实现思路,来理解新架构的类型转换器的使用方法,同时在实际开发中,我们可能会有自己的格式转换需求,这个时候我们也可以通过自定义格式转换器来完成这些个性化需求。 自定义格式转换器 完成自定义转换器需要实现以下三个中的任意一个接口:Convertor<S,T>、GenericConvertor或ConvertorFacoty。下面我们对这些接口进行逐一分析: 1. Convertor<S,T> 这是最为简单的一个接口,定义了从源类到目标类的转换方法。该接口的定义如下 public interface ConverterFactory<S, R> { //将S类型的对象转换为T类型,R为目标类型T的基类 <T extends R> Converter<S, T> getConverter(Class<T> targetType); } 2. GenericConvertor GenericConvertor会根据源类对象及目标类对象所在宿主类的上下文信息进行类型转换工作,该接口的定义如下: public interface GenericConverter { //ConvertiblePair包含了源类型和目标类型,它的定义在下面 Set<ConvertiblePair> getConvertibleTypes(); //TypeDescriptor包含了需转换类型对象所在宿主类的信息,我们根据此信息,完成源到目标类型的转换 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); /** * 内部类定义 */ public static final class ConvertiblePair { //源类型 private final Class<?> sourceType; //目标类类型 private final Class<?> targetType; /** * 创建一个源-目标对子 */ public ConvertiblePair(Class<?> sourceType, Class<?> targetType) { Assert.notNull(sourceType, "Source type must not be null"); Assert.notNull(targetType, "Target type must not be null"); this.sourceType = sourceType; this.targetType = targetType; } public Class<?> getSourceType() { return this.sourceType; } public Class<?> getTargetType() { return this.targetType; } //忽略hashCode\equals\toString等重写方法 } } 我们常使用其实现类接口: public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { } 它除了实现GenericConverter,还实现了另一个“条件转换器”: public interface ConditionalConverter { /** * Should the conversion from {@code sourceType} to {@code targetType} currently under */ //根据源类型和目标类型所在宿主类型的上下文信息判断是否要进行类型转换 boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } 在实际开发中,我们能实现此接口自定义转换器,来根据具体类型上下文来灵活配置我们的类型转换 3. ConvertorFacoty 这是一个将我们源类转换为一个目标类或其子类的”多转换器共存“接口工厂。它的定义如下: public interface ConverterFactory<S, R> { //获取将源类转换为特定R类或其子类的转换器 <T extends R> Converter<S, T> getConverter(Class<T> targetType); } 这个接口一个常见的实现类是StringToNumberConvertor,能将String类型数据转换为Number类型或其子类:Long,Integer,Double等。 注册自定义转换器 ConversionService ConversionService则是Spring类型转换体系的核心接口,ConversionService接口的定义如下: package org.springframework.core.convert; public interface ConversionService { //判断sourceType是否可以转换为targetType boolean canConvert(Class<?> sourceType, Class<?> targetType); //TypeDescriptor描述了转换类的各类上下文信息,在类型转换实现方法中可以根据这些信息进行灵活控制 //比如这里通过源类和目标类的上下文信息判断是否可以进行转换 boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); //将source转换为targetType <T> T convert(Object source, Class<T> targetType); //利用源、目标类的上下文信息,将源类型转换为目标类型 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } ConversionServiceFactoryBean 实现以上类型完成我们的自定义转换器定义后,我们还要在Spring容器中通过ConversionServiceFactoryBean注册创建后才能使用。 ConversionServiceFactoryBean创建了我们的ConversionService很多内置转换器,利用这些转换器,我们可以完成大部分常见的类型转换工作 而如果我们想使用自定义的类型转换器,可以通过ConversionServiceFactoryBean的convertor属性来注册。 实例分析1:测试Convertor 通过以上的分析,我们接下来尝试自定实现Convert 1. 自定义属性转换器 public class MyConvertor implements Converter<String, User>{ @Override public User convert(String source) {//source为要转换的字符串 String[] values = source.split(",");//根据我们的需求,用逗号来区分 Integer id = Integer.valueOf(values[0]); User user = new User(id,values[1],values[2]); return user; } } /**********下面是我们的UserPOJO类**********/ public class User { public User() { super(); } private Integer id; private String userName; private String password; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public User(Integer id, String userName, String password) { super(); this.id = id; this.userName = userName; this.password = password; } //忽略get和set方法 @Override public String toString() { return "User [id=" + id + ", userName=" + userName + ", password=" + password + "]"; } } 2. 注册自定义属性转换器 <!-- 通过:annotation-driven的conversion-service属性来装配我们的类型转换器 --> <mvc:annotation-driven conversion-service="factoryBean" /> <!-- 通过ConversionServiceFactoryBean注册我们的自定义转换器 --> <bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="factoryBean" > <property name="converters"><!-- 在属性converters注册 --> <list> <bean class="com.mvc.convertor.MyConvertor" /> </list> </property> </bean> 3. 配置控制器 在控制层,我们通过以下方法测试我们的转换器 @RequestMapping("convert") public String convert(User user){ System.out.println(user); return "model1"; } 4. 测试 启动服务器,在游览器中访问[项目根路径]/convert?user=11,myUserName,myPassword。 控制台会打印信息:User [id=11, userName=myUserName, password=myPassword]。即springMVC帮我们完成了字符串到User类型的转换。**这里需注意的是,我们的请求参数名”user”是和控制层方法入参变量User user像对应的,才能完成参数绑定进而转换类型 实例分析2:测试ConvertorFactory 1. 自定义类型转换器 在实例1的基础上,我们添加User的一个子类:SuperUser,作为”super”子类,它拥有了自己的专属名字,我们将字符串”11,myUserName,myPassword,myName“转换为我们的superUser对象,下面相对应的自定义转换器和POJO类 public class MySuperConvertor implements Converter<String, SuperUser>{ @Override public SuperUser convert(String source) { String[] values = source.split(","); Integer id = Integer.valueOf(values[0]); SuperUser superUser = new SuperUser(values[3], new User(id,values[1],values[2])); return superUser; } } /**********下面是SuperUser POJO类*********/ package com.mvc.model; public class SuperUser extends User { private String name; //忽略get和set方法 public SuperUser(String name,User user) { super(user.getId(),user.getUserName(),user.getPassword()); this.name = name; } public SuperUser() { super(); } @Override public String toString() { return "SuperUser [name=" + name + ", toString()=" + super.toString() + "]"; } } 除了配置上面的转换器,还需自定义我们的转换器工厂,在转换器工厂中,我们根据目标类型是User还是其子类SuperUser来调用相应的自定义转换器: public class MyConvertorFactory implements ConverterFactory<String, User>{ @Override //T类型必须是User或其子类,Stirng是我们的转换源类 public <T extends User> Converter<String, T> getConverter( Class<T> targetType) { if(targetType == User.class){ return (Converter<String, T>) new MyConvertor(); }else{ return (Converter<String, T>) new MySuperConvertor(); } } } 2. 注册自定义属性转换器 <!-- 通过:annotation-driven的conversion-service属性来装配我们的类型转换器 --> <mvc:annotation-driven conversion-service="factoryBean" /> <!-- 通过ConversionServiceFactoryBean注册我们的自定义转换器 --> <bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="factoryBean" > <property name="converters"><!-- 在属性converters注册 --> <list> <!--这里只要注册我们自定义的转换器工厂即可--> <bean class="com.mvc.convertor.MyConvertorFactory" /> </list> </property> </bean> 3. 配置控制器 在实例1的基础上,我们添加一个新方法 //这是原来的 @RequestMapping("convert") public String convert( User user){ System.out.println(user); return "model1"; } //下面是新添加的方法 @RequestMapping("convertSuper") public String convert( SuperUser user){ System.out.println(user); return "model1"; } 4. 测试 运行服务器,我们在游览器中输入: 1. root/convert?user=10,myUserName,myPassword 控制台输出:User [id=10, userName=myUserName, password=myPassword] 2. root/convertSuper?superUser=11,myUserName,myPassword,myName 控制台输出:SuperUser [name=myName, toString()=User [id=11, userName=myUserName, password=myPassword]] 我们根据入参类型,并通过ConvertFactory,完成对同一系列(某一类及其子类)的类型转换 源码下载 本篇文章测试源码可到https://github.com/jeanhao/spring的dataConvertor文件夹下下载
Bean Validation 1.1当前实现是Hibernate validator 5,且spring4才支持。接下来我们从以下几个方法讲解Bean Validation 1.1,当然不一定是新特性: 集成Bean Validation 1.1到SpringMVC 分组验证、分组顺序及级联验证 消息中使用EL表达式 方法参数/返回值验证 自定义验证规则 类级别验证器 脚本验证器 cross-parameter,跨参数验证 混合类级别验证器和跨参数验证器 组合多个验证注解 本地化 因为大多数时候验证都配合web框架使用,而且很多朋友都咨询过如分组/跨参数验证,所以本文介绍下这些,且是和SpringMVC框架集成的例子,其他使用方式(比如集成到JPA中)可以参考其官方文档: 规范:http://beanvalidation.org/1.1/spec/ hibernate validator文档:http://hibernate.org/validator/ 1、集成Bean Validation 1.1到SpringMVC 1.1、项目搭建 首先添加hibernate validator 5依赖: Java代码 <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.0.2.Final</version> </dependency> 如果想在消息中使用EL表达式,请确保EL表达式版本是 2.2或以上,如使用Tomcat6,请到Tomcat7中拷贝相应的EL jar包到Tomcat6中。 Java代码 <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>2.2.4</version> <scope>provided</scope> </dependency> 请确保您使用的Web容器有相应版本的el jar包。 对于其他POM依赖请下载附件中的项目参考。 1.2、Spring MVC配置文件(spring-mvc.xml): Java代码 <!-- 指定自己定义的validator --> <mvc:annotation-driven validator="validator"/> <!-- 以下 validator ConversionService 在使用 mvc:annotation-driven 会 自动注册--> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> <!-- 如果不加默认到 使用classpath下的 ValidationMessages.properties --> <property name="validationMessageSource" ref="messageSource"/> </bean> <!-- 国际化的消息资源文件(本系统中主要用于显示/错误消息定制) --> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames"> <list> <!-- 在web环境中一定要定位到classpath 否则默认到当前web应用下找 --> <value>classpath:messages</value> <value>classpath:org/hibernate/validator/ValidationMessages</value> </list> </property> <property name="useCodeAsDefaultMessage" value="false"/> <property name="defaultEncoding" value="UTF-8"/> <property name="cacheSeconds" value="60"/> </bean> 此处主要把bean validation的消息查找委托给spring的messageSource。 1.3、实体验证注解: Java代码 public class User implements Serializable { @NotNull(message = "{user.id.null}") private Long id; @NotEmpty(message = "{user.name.null}") @Length(min = 5, max = 20, message = "{user.name.length.illegal}") @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}") private String name; @NotNull(message = "{user.password.null}") private String password; } 对于验证规则可以参考官方文档,或者《第七章 注解式控制器的数据验证、类型转换及格式化》。 1.4、错误消息文件messages.properties: Java代码 user.id.null=用户编号不能为空 user.name.null=用户名不能为空 user.name.length.illegal=用户名长度必须在5到20之间 user.name.illegal=用户名必须是字母 user.password.null=密码不能为空 1.5、控制器 Java代码 @Controller public class UserController { @RequestMapping("/save") public String save(@Valid User user, BindingResult result) { if(result.hasErrors()) { return "error"; } return "success"; } } 1.6、错误页面: Java代码 <spring:hasBindErrors name="user"> <c:if test="${errors.fieldErrorCount > 0}"> 字段错误:<br/> <c:forEach items="${errors.fieldErrors}" var="error"> <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/> ${error.field}------${message}<br/> </c:forEach> </c:if> <c:if test="${errors.globalErrorCount > 0}"> 全局错误:<br/> <c:forEach items="${errors.globalErrors}" var="error"> <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/> <c:if test="${not empty message}"> ${message}<br/> </c:if> </c:forEach> </c:if> </spring:hasBindErrors> 大家以后可以根据这个做通用的错误消息显示规则。比如我前端页面使用validationEngine显示错误消息,那么我可以定义一个tag来通用化错误消息的显示:showFieldError.tag。 1.7、测试 输入如:http://localhost:9080/spring4/save?name=123 , 我们得到如下错误: Java代码 name------用户名必须是字母 name------用户名长度必须在5到20之间 password------密码不能为空 id------用户编号不能为空 基本的集成就完成了。 如上测试有几个小问题: 1、错误消息顺序,大家可以看到name的错误消息顺序不是按照书写顺序的,即不确定; 2、我想显示如:用户名【zhangsan】必须在5到20之间;其中我们想动态显示:用户名、min,max;而不是写死了; 3、我想在修改的时候只验证用户名,其他的不验证怎么办。 接下来我们挨着试试吧。 2、分组验证及分组顺序 如果我们想在新增的情况验证id和name,而修改的情况验证name和password,怎么办? 那么就需要分组了。 首先定义分组接口: Java代码 public interface First { } public interface Second { } 分组接口就是两个普通的接口,用于标识,类似于java.io.Serializable。 接着我们使用分组接口标识实体: Java代码 public class User implements Serializable { @NotNull(message = "{user.id.null}", groups = {First.class}) private Long id; @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {Second.class}) @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class}) private String name; @NotNull(message = "{user.password.null}", groups = {First.class, Second.class}) private String password; } 验证时使用如: Java代码 @RequestMapping("/save") public String save(@Validated({Second.class}) User user, BindingResult result) { if(result.hasErrors()) { return "error"; } return "success"; } 即通过@Validate注解标识要验证的分组;如果要验证两个的话,可以这样@Validated({First.class, Second.class})。 接下来我们来看看通过分组来指定顺序;还记得之前的错误消息吗? user.name会显示两个错误消息,而且顺序不确定;如果我们先验证一个消息;如果不通过再验证另一个怎么办?可以通过@GroupSequence指定分组验证顺序: Java代码 @GroupSequence({First.class, Second.class, User.class}) public class User implements Serializable { private Long id; @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class}) @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class}) private String name; private String password; } 通过@GroupSequence指定验证顺序:先验证First分组,如果有错误立即返回而不会验证Second分组,接着如果First分组验证通过了,那么才去验证Second分组,最后指定User.class表示那些没有分组的在最后。这样我们就可以实现按顺序验证分组了。 另一个比较常见的就是级联验证: 如: Java代码 public class User { @Valid @ConvertGroup(from=First.class, to=Second.class) private Organization o; } 1、级联验证只要在相应的字段上加@Valid即可,会进行级联验证;@ConvertGroup的作用是当验证o的分组是First时,那么验证o的分组是Second,即分组验证的转换。 3、消息中使用EL表达式 假设我们需要显示如:用户名[NAME]长度必须在[MIN]到[MAX]之间,此处大家可以看到,我们不想把一些数据写死,如NAME、MIN、MAX;此时我们可以使用EL表达式。 如: Java代码 @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class}) 错误消息: Java代码 user.name.length.illegal=用户名长度必须在{min}到{max}之间 其中我们可以使用{验证注解的属性}得到这些值;如{min}得到@Length中的min值;其他的也是类似的。 到此,我们还是无法得到出错的那个输入值,如name=zhangsan。此时就需要EL表达式的支持,首先确定引入EL jar包且版本正确。然后使用如: Java代码 user.name.length.illegal=用户名[${validatedValue}]长度必须在5到20之间 使用如EL表达式:${validatedValue}得到输入的值,如zhangsan。当然我们还可以使用如${min > 1 ? '大于1' : '小于等于1'},及在EL表达式中也能拿到如@Length的min等数据。 另外我们还可以拿到一个java.util.Formatter类型的formatter变量进行格式化: Java代码 ${formatter.format("%04d", min)} 4、方法参数/返回值验证 这个可以参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》,概念是类似的,具体可以参考Bean Validation 文档。 5、自定义验证规则 有时候默认的规则可能还不够,有时候还需要自定义规则,比如屏蔽关键词验证是非常常见的一个功能,比如在发帖时帖子中不允许出现admin等关键词。 1、定义验证注解 Java代码 package com.sishuok.spring4.validator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; /** * <p>User: Zhang Kaitao * <p>Date: 13-12-15 * <p>Version: 1.0 */ @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Retention(RUNTIME) //指定验证器 @Constraint(validatedBy = ForbiddenValidator.class) @Documented public @interface Forbidden { //默认错误消息 String message() default "{forbidden.word}"; //分组 Class<?>[] groups() default { }; //负载 Class<? extends Payload>[] payload() default { }; //指定多个时使用 @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented @interface List { Forbidden[] value(); } } 2、 定义验证器 Java代码 package com.sishuok.spring4.validator; import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.util.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.io.Serializable; /** * <p>User: Zhang Kaitao * <p>Date: 13-12-15 * <p>Version: 1.0 */ public class ForbiddenValidator implements ConstraintValidator<Forbidden, String> { private String[] forbiddenWords = {"admin"}; @Override public void initialize(Forbidden constraintAnnotation) { //初始化,得到注解数据 } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(StringUtils.isEmpty(value)) { return true; } for(String word : forbiddenWords) { if(value.contains(word)) { return false;//验证失败 } } return true; } } 验证器中可以使用spring的依赖注入,如注入:@Autowired private ApplicationContext ctx; 3、使用 Java代码 public class User implements Serializable { @Forbidden() private String name; } 4、当我们在提交name中含有admin的时候会输出错误消息: Java代码 forbidden.word=您输入的数据中有非法关键词 问题来了,哪个词是非法的呢?bean validation 和 hibernate validator都没有提供相应的api提供这个数据,怎么办呢?通过跟踪代码,发现一种不是特别好的方法:我们可以覆盖org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl实现(即复制一份代码放到我们的src中),然后覆盖buildAnnotationParameterMap方法; Java代码 private Map<String, Object> buildAnnotationParameterMap(Annotation annotation) { …… //将Collections.unmodifiableMap( parameters );替换为如下语句 return parameters; } 即允许这个数据可以修改;然后在ForbiddenValidator中: Java代码 for(String word : forbiddenWords) { if(value.contains(word)) { ((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word); return false;//验证失败 } } 通过((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);添加自己的属性;放到attributes中的数据可以通过${} 获取。然后消息就可以变成: Java代码 forbidden.word=您输入的数据中有非法关键词【{word}】 这种方式不是很友好,但是可以解决我们的问题。 典型的如密码、确认密码的场景,非常常用;如果没有这个功能我们需要自己写代码来完成;而且经常重复自己。接下来看看bean validation 1.1如何实现的。 6、类级别验证器 6.1、定义验证注解 Java代码 package com.sishuok.spring4.validator; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotNull; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; /** * <p>User: Zhang Kaitao * <p>Date: 13-12-15 * <p>Version: 1.0 */ @Target({ TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) //指定验证器 @Constraint(validatedBy = CheckPasswordValidator.class) @Documented public @interface CheckPassword { //默认错误消息 String message() default ""; //分组 Class<?>[] groups() default { }; //负载 Class<? extends Payload>[] payload() default { }; //指定多个时使用 @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented @interface List { CheckPassword[] value(); } } 6.2、 定义验证器 Java代码 package com.sishuok.spring4.validator; import com.sishuok.spring4.entity.User; import org.springframework.util.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; /** * <p>User: Zhang Kaitao * <p>Date: 13-12-15 * <p>Version: 1.0 */ public class CheckPasswordValidator implements ConstraintValidator<CheckPassword, User> { @Override public void initialize(CheckPassword constraintAnnotation) { } @Override public boolean isValid(User user, ConstraintValidatorContext context) { if(user == null) { return true; } //没有填密码 if(!StringUtils.hasText(user.getPassword())) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("{password.null}") .addPropertyNode("password") .addConstraintViolation(); return false; } if(!StringUtils.hasText(user.getConfirmation())) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("{password.confirmation.null}") .addPropertyNode("confirmation") .addConstraintViolation(); return false; } //两次密码不一样 if (!user.getPassword().trim().equals(user.getConfirmation().trim())) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("{password.confirmation.error}") .addPropertyNode("confirmation") .addConstraintViolation(); return false; } return true; } } 其中我们通过disableDefaultConstraintViolation禁用默认的约束;然后通过buildConstraintViolationWithTemplate(消息模板)/addPropertyNode(所属属性)/addConstraintViolation定义我们自己的约束。 6.3、使用 Java代码 @CheckPassword() public class User implements Serializable { } 放到类头上即可。 7、通过脚本验证 Java代码 @ScriptAssert(script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}") public class User implements Serializable { } 通过脚本验证是非常简单而且强大的,lang指定脚本语言(请参考javax.script.ScriptEngineManager JSR-223),alias是在脚本验证中User对象的名字,但是大家会发现一个问题:错误消息怎么显示呢? 在springmvc 中会添加到全局错误消息中,这肯定不是我们想要的,我们改造下吧。 7.1、定义验证注解 Java代码 package com.sishuok.spring4.validator; import org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = {PropertyScriptAssertValidator.class}) @Documented public @interface PropertyScriptAssert { String message() default "{org.hibernate.validator.constraints.ScriptAssert.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; String lang(); String script(); String alias() default "_this"; String property(); @Target({ TYPE }) @Retention(RUNTIME) @Documented public @interface List { PropertyScriptAssert[] value(); } } 和ScriptAssert没什么区别,只是多了个property用来指定出错后给实体的哪个属性。 7.2、验证器 Java代码 package com.sishuok.spring4.validator; import javax.script.ScriptException; import javax.validation.ConstraintDeclarationException; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import com.sishuok.spring4.validator.PropertyScriptAssert; import org.hibernate.validator.constraints.ScriptAssert; import org.hibernate.validator.internal.util.Contracts; import org.hibernate.validator.internal.util.logging.Log; import org.hibernate.validator.internal.util.logging.LoggerFactory; import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluator; import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluatorFactory; import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES; public class PropertyScriptAssertValidator implements ConstraintValidator<PropertyScriptAssert, Object> { private static final Log log = LoggerFactory.make(); private String script; private String languageName; private String alias; private String property; private String message; public void initialize(PropertyScriptAssert constraintAnnotation) { validateParameters( constraintAnnotation ); this.script = constraintAnnotation.script(); this.languageName = constraintAnnotation.lang(); this.alias = constraintAnnotation.alias(); this.property = constraintAnnotation.property(); this.message = constraintAnnotation.message(); } public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { Object evaluationResult; ScriptEvaluator scriptEvaluator; try { ScriptEvaluatorFactory evaluatorFactory = ScriptEvaluatorFactory.getInstance(); scriptEvaluator = evaluatorFactory.getScriptEvaluatorByLanguageName( languageName ); } catch ( ScriptException e ) { throw new ConstraintDeclarationException( e ); } try { evaluationResult = scriptEvaluator.evaluate( script, value, alias ); } catch ( ScriptException e ) { throw log.getErrorDuringScriptExecutionException( script, e ); } if ( evaluationResult == null ) { throw log.getScriptMustReturnTrueOrFalseException( script ); } if ( !( evaluationResult instanceof Boolean ) ) { throw log.getScriptMustReturnTrueOrFalseException( script, evaluationResult, evaluationResult.getClass().getCanonicalName() ); } if(Boolean.FALSE.equals(evaluationResult)) { constraintValidatorContext.disableDefaultConstraintViolation(); constraintValidatorContext .buildConstraintViolationWithTemplate(message) .addPropertyNode(property) .addConstraintViolation(); } return Boolean.TRUE.equals( evaluationResult ); } private void validateParameters(PropertyScriptAssert constraintAnnotation) { Contracts.assertNotEmpty( constraintAnnotation.script(), MESSAGES.parameterMustNotBeEmpty( "script" ) ); Contracts.assertNotEmpty( constraintAnnotation.lang(), MESSAGES.parameterMustNotBeEmpty( "lang" ) ); Contracts.assertNotEmpty( constraintAnnotation.alias(), MESSAGES.parameterMustNotBeEmpty( "alias" ) ); Contracts.assertNotEmpty( constraintAnnotation.property(), MESSAGES.parameterMustNotBeEmpty( "property" ) ); Contracts.assertNotEmpty( constraintAnnotation.message(), MESSAGES.parameterMustNotBeEmpty( "message" ) ); } } 和之前的类级别验证器类似,就不多解释了,其他代码全部拷贝自org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator。 7.3、使用 Java代码 @PropertyScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}") 和之前的区别就是多了个property,用来指定出错时给哪个字段。 这个相对之前的类级别验证器更通用一点。 8、cross-parameter,跨参数验证 直接看示例; 8.1、首先注册MethodValidationPostProcessor,起作用请参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》 Java代码 <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator"/> </bean> 8.2、Service Java代码 @Validated @Service public class UserService { @CrossParameter public void changePassword(String password, String confirmation) { } } 通过@Validated注解UserService表示该类中有需要进行方法参数/返回值验证; @CrossParameter注解方法表示要进行跨参数验证;即验证password和confirmation是否相等。 8.3、验证注解 Java代码 package com.sishuok.spring4.validator; //省略import @Constraint(validatedBy = CrossParameterValidator.class) @Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented public @interface CrossParameter { String message() default "{password.confirmation.error}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; } 8.4、验证器 Java代码 package com.sishuok.spring4.validator; //省略import @SupportedValidationTarget(ValidationTarget.PARAMETERS) public class CrossParameterValidator implements ConstraintValidator<CrossParameter, Object[]> { @Override public void initialize(CrossParameter constraintAnnotation) { } @Override public boolean isValid(Object[] value, ConstraintValidatorContext context) { if(value == null || value.length != 2) { throw new IllegalArgumentException("must have two args"); } if(value[0] == null || value[1] == null) { return true; } if(value[0].equals(value[1])) { return true; } return false; } } 其中@SupportedValidationTarget(ValidationTarget.PARAMETERS)表示验证参数; value将是参数列表。 8.5、使用 Java代码 @RequestMapping("/changePassword") public String changePassword( @RequestParam("password") String password, @RequestParam("confirmation") String confirmation, Model model) { try { userService.changePassword(password, confirmation); } catch (ConstraintViolationException e) { for(ConstraintViolation violation : e.getConstraintViolations()) { System.out.println(violation.getMessage()); } } return "success"; } 调用userService.changePassword方法,如果验证失败将抛出ConstraintViolationException异常,然后得到ConstraintViolation,调用getMessage即可得到错误消息;然后到前台显示即可。 从以上来看,不如之前的使用方便,需要自己对错误消息进行处理。 下一节我们也写个脚本方式的跨参数验证器。 9、混合类级别验证器和跨参数验证器 9.1、验证注解 Java代码 package com.sishuok.spring4.validator; //省略import @Constraint(validatedBy = { CrossParameterScriptAssertClassValidator.class, CrossParameterScriptAssertParameterValidator.class }) @Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented public @interface CrossParameterScriptAssert { String message() default "error"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; String script(); String lang(); String alias() default "_this"; String property() default ""; ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT; } 此处我们通过@Constraint指定了两个验证器,一个类级别的,一个跨参数的。validationAppliesTo指定为ConstraintTarget.IMPLICIT,表示隐式自动判断。 9.2、验证器 请下载源码查看 9.3、使用 9.3.1、类级别使用 Java代码 @CrossParameterScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}") 指定property即可,其他和之前的一样。 9.3.2、跨参数验证 Java代码 @CrossParameterScriptAssert(script = "args[0] == args[1]", lang = "javascript", alias = "args", message = "{password.confirmation.error}") public void changePassword(String password, String confirmation) { } 通过args[0]==args[1] 来判断是否相等。 这样,我们的验证注解就自动适应两种验证规则了。 10、组合验证注解 有时候,可能有好几个注解需要一起使用,此时就可以使用组合验证注解 Java代码 @Target({ FIELD}) @Retention(RUNTIME) @Documented @NotNull(message = "{user.name.null}") @Length(min = 5, max = 20, message = "{user.name.length.illegal}") @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.length.illegal}") @Constraint(validatedBy = { }) public @interface Composition { String message() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; } 这样我们验证时只需要: Java代码 @Composition() private String name; 简洁多了。 11、本地化 即根据不同的语言选择不同的错误消息显示。 1、本地化解析器 Java代码 <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"> <property name="cookieName" value="locale"/> <property name="cookieMaxAge" value="-1"/> <property name="defaultLocale" value="zh_CN"/> </bean> 此处使用cookie存储本地化信息,当然也可以选择其他的,如Session存储。 2、设置本地化信息的拦截器 Java代码 <mvc:interceptors> <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <property name="paramName" value="language"/> </bean> </mvc:interceptors> 即请求参数中通过language设置语言。 3、消息文件 4、 浏览器输入 http://localhost:9080/spring4/changePassword?password=1&confirmation=2&language=en_US 转自:http://jinnianshilongnian.iteye.com/blog/1990081
我们通过Http请求提交的参数都以字符串的形式呈现,但最终在springMVC的方法入参中,我们却能得到各种类型的数据,包括Number、Boolean、复杂对象类型、集合类型、Map类型等,这些都是springMVC内置的数据类型转换器帮我们完成的。springMVC的将请求数据绑定到方法入参的流程如下所示: Created with Raphaël 2.1.0数据绑定流程图解ServletRequestServletRequestDataBinderDataBinderConversionServiceConversionServiceValidatorValidatorBindingResultBindingResult请求数据提交数据类型转换格式化数据合法性验证生成数据绑定结果 在本文里,我们通过属性编辑器来理解springMVC的数据转换、绑定过程。 PropertyEditorRegistrySupport 而对于常见的数据类型,Spring在PropertyEditorRegistrySupport中提供了默认的属性编辑器,这些常见的数据类型如下图所示: 在PropertyEditorRegistrySupport中,有两个重要的Map类型成员变量: 1. private Map<Class<?>, PropertyEditor> defaultEditors:用于保存默认属性类型的编辑器,元素的key为属性类型,值为对应属性编辑器的实例 2. private Map<Class<?>, PropertyEditor> customEditors:用于保存用户自定义的属性编辑器,元素的键值和defaultEditors一致。 在PropertyEditorRegistrySupport中,有一个重要的成员方法:createDefaultEditors()来创建默认的属性编辑器,它的定义如下所示: /** * Actually register the default editors for this registry instance. */ private void createDefaultEditors() { //创建一个HashMap存储默认的属性编辑器 this.defaultEditors = new HashMap<Class<?>, PropertyEditor>(64); // 简单的属性编辑器,没有参数化功能,在JDK中没有包含下列任意目标类型的编辑器 //这里和我们上表的资源类相对应 this.defaultEditors.put(Charset.class, new CharsetEditor()); this.defaultEditors.put(Class.class, new ClassEditor()); this.defaultEditors.put(Class[].class, new ClassArrayEditor()); this.defaultEditors.put(Currency.class, new CurrencyEditor()); this.defaultEditors.put(File.class, new FileEditor()); this.defaultEditors.put(InputStream.class, new InputStreamEditor()); this.defaultEditors.put(InputSource.class, new InputSourceEditor()); this.defaultEditors.put(Locale.class, new LocaleEditor()); this.defaultEditors.put(Pattern.class, new PatternEditor()); this.defaultEditors.put(Properties.class, new PropertiesEditor()); this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); this.defaultEditors.put(TimeZone.class, new TimeZoneEditor()); this.defaultEditors.put(URI.class, new URIEditor()); this.defaultEditors.put(URL.class, new URLEditor()); this.defaultEditors.put(UUID.class, new UUIDEditor()); if (zoneIdClass != null) { this.defaultEditors.put(zoneIdClass, new ZoneIdEditor()); } // 默认的集合类编辑器实例,这里和我们上表的集合类相对应 // 我们能够通过注册自定义的相同类型属性编辑器来重写下面的默认属性编辑器 this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class)); // 基本数据的数组类型的默认编辑器 this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); this.defaultEditors.put(char[].class, new CharArrayPropertyEditor()); this.defaultEditors.put(char.class, new CharacterEditor(false)); this.defaultEditors.put(Character.class, new CharacterEditor(true)); this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true)); // JDK中没有Number包装类的相关属性编辑器 // 通过自定义我们的CustomNumberEditor来重写JDK默认的属性编辑器 this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false)); this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true)); this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false)); this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true)); this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false)); this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true)); this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false)); this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true)); this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false)); this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true)); this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false)); this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true)); this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true)); this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true)); // 只有我们显式将configValueEditorsActive设为true,才会注册下面类型的编辑器 if (this.configValueEditorsActive) { StringArrayPropertyEditor sae = new StringArrayPropertyEditor(); this.defaultEditors.put(String[].class, sae); this.defaultEditors.put(short[].class, sae); this.defaultEditors.put(int[].class, sae); this.defaultEditors.put(long[].class, sae); } } PropertyEditor PropertyEditor是Java原生的属性编辑器接口,它的核心功能是将一个字符串转换为一个java对象。 它的定义和常用方法如下所示: public interface PropertyEditor { //设置属性的值,基本属性类型要以包装类传入 void setValue(Object value); //返回属性的值,基本数据类型会被封装成相应的包装类 Object getValue(); //为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值 String getJavaInitializationString(); //将属性对象用一个字符串表示,一遍外部的属性编辑器能以可视化的方式显示。 //默认返回null,表示改属性不能以字符串形式表示 String getAsText(); //利用所给字符串text更新属性内部的值 void setAsText(String text) throws java.lang.IllegalArgumentException; } 实例解析自定义属性编辑器 1. 自定义编辑器类 它的一个核心实现类是PropertyEditorSupport,如果我们要编写自定义的属性编辑器,只需要继承这个类,然后重写setAsText方法即可。下面我们来看一个自定义属性编辑器的实例:尝试将字符串“myName,1995-01-01,15k”转换为Person POJO对象,Person对象的定义如下: package com.mvc.model; import java.util.Date; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.NumberFormat; public class Person { private String name; private Date birthday; private Long salary; //ignore getter and setter @Override public String toString() { return "Person [name=" + name + ", birthday=" + birthday + ", salary=" + salary + "]"; } } 下面是我们自定义的属性编辑器: public class MyEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { String[] values = text.split(","); Person person = new Person(); person.setName(values[0]); try { person.setBirthday(new SimpleDateFormat("yyyy-MM-dd").parse(values[1]));//格式化字符串并解析成日期类型 } catch (ParseException e) { e.printStackTrace(); } person.setSalary(Long.valueOf(values[2].replace("k", "000")));//转换为工资格式 setValue(person);//调用setValue来将我们的Person对象设置为编辑器的属性值 super.setAsText(text); } } 2. 注册编辑器 自定义完属性编辑器后,我们需要将其注册才能生效,SpringMVC中使用自定义的属性编辑器有3种方法: 1. Controller方法中添加@InitBinder注解的方法 实例: @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(Person.class, new MyEditor()); } 2. 实现 WebBindingInitializer接口 方法1是针对特定的控制器的,如果我们需要对全局控制器生效,可以编写自己的WebBindingInitializer,然后在spring容器中注册,如下所示: public class MyWebBindingInitializer implements WebBindingInitializer { @Override public void initBinder(WebDataBinder binder, WebRequest request) { binder.registerCustomEditor(Dept.class, new CustomDeptEditor()); } } 在容器中注册: <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="webBindingInitializer"> <bean class="com.mvc.editor.MyWebBindingInitializer" /> </property> </bean> 3. @ControllerAdvice注解 我们可以通过此注解配置一个控制器增强, @ControllerAdvice public class InitBinderControllerAdvice { @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(Dept.class, new CustomDeptEditor()); } } 我们需要将其纳入<context:component-scan>的扫描路径中才能生效。 从上面的分析我们能看到,springMVC注册了大量的数据类型编辑器,恰是通过这些属性编辑器,springMVC帮助我们完成了请求参数字符串到入参数据的绑定。在一篇文章里,我们会谈到SpringMVC对新的转换器框架的支持。
使用@ModelAttribute、Model、Map、@SessionAttributes能便捷地将我们的业务数据封装到模型里并交由视图解析调用。下面开始一一分析 在方法入参上使用@ModelAttribute 使用@ModelAttribute可以直接将我们的方法入参添加到模型中。我们先看一个实例: 1. springMVC核心文件配置: <!-- 扫描com.mvc.controller包下所有的类,使spring注解生效 --> <context:component-scan base-package="com.mvc.controller" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"></property><!-- 前缀,在springMVC控制层处理好的请求后,转发配置目录下的视图文件 --> <property name="suffix" value=".jsp"></property><!-- 文件后缀,表示转发到的视图文件后缀为.jsp --> </bean> 2. 编写控制器: @Controller @RequestMapping("/user") public class UserController { @RequestMapping("model1") public String model1(@ModelAttribute User user){//绑定user属性到视图中 user.setId(1); user.setPassword("myPassword"); user.setUserName("myUserName"); return "model1"; //直接返回视图名,springMVC会帮我们解析成/WEB-INF/views/model1.jsp视图文件 } @RequestMapping("model2") public String model2(@ModelAttribute User user){//绑定user属性到视图中 user = new User(2,"myUserName","myPwd");//这里直接新建一个对象 return "model1"; } } 3. 编写视图层文件 在目录/WEB-INF/views文件夹下添加: <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>hello spring mvc</title> </head> <body> 用户id:${user.id }<br> 用户名:${user.userName }<br> 用户密码:${user.password } </body> </html> 4. 启动服务器测试 开启服务器后,假设tomcat监听8080端口,项目名为springMVC,则游览器访问结果如下图所示: 这里我们访问了控制器第一个方法,通过@ModelAttribute注解成功的完成地将user数据添加到模型中。我们再访问第二个,却会得到如下结果: 这是因为我们在model2()中设置成员属性时,新建了一个对象,这个对象尽管使用了引用变量user,但却不是原来注解绑定的那个对象。这就好像:A是被标记的房子,原来小明住在A里,但后面小明跑到B房子去了。这时候标记仍在A房子,不会因为小明跑到了B房子就使标记出现在B房子上。这里A房子就是我们被注解的实例对象,小明就是引用变量user,B房子就是我们使用带三个参数的构造方法新建的实例对象。 在方法定义上使用@ModelAttribute 在SpringMVC调用任何方法前,被@ModelAttribute注解的方法都会先被依次调用,并且这些注解方法的返回值都会被添加到模型中。对于上例的moedel1方法,我们改写成如下所示: @ModelAttribute public User getUser1(){ System.out.println("getUser1方法被调用"); return new User(1,"myUserName1","myPwd1"); } @ModelAttribute public User getUser2(){ System.out.println("getUser2方法被调用"); return new User(2,"myUserName2","myPwd2"); } @RequestMapping("model1") public String model1(){//绑定user属性到视图中 return "model1";//直接返回视图名,springMVC会帮我们解析成/WEB-INF/views/model1.jsp视图文件 } 这时候访问游览器,得到结果 用户id:2 用户名:myUserName2 用户密码:myPwd2 此时控制台输出: getUser2方法被调用 getUser1方法被调用 这里说明,getUser1()方法首先被调用了,但模型属性认为id为2的user,说明后面调用的getUser1()的模型数据兵并不能对前面的进行覆盖,即当存在多个同类型的模型数据时,第一个才是有效的。 而如果我们想添加多个相同类型的模型数据,可使用如下方法: @ModelAttribute public void getUser3(Model model){//注入model入参 System.out.println("getUser3方法被调用"); model.addAttribute("user1",new User(1,"myUserName1","myPwd1")); model.addAttribute("user2",new User(2,"myUserName2","myPwd2")); } @RequestMapping("model3") public String model3(Model model){//我们可以在这绑定入参model读取前面的数据 System.out.println(model.asMap().get("user1")); System.out.println(model.asMap().get("user2")); return "model3"; } 访问结果示意如下: 控制台也打印: User [id=1, userName=myUserName1, password=myPwd1] User [id=2, userName=myUserName2, password=myPwd2] 使用@ModelAttribute完成数据准备工作 在实际开发中,我们常常需要在控制器每个方法调用前做些资源准备工作,如获取当次请求的servletAPI,输入输出流等,我们可以直接在方法入参上注明,springMVC会帮我们完成注入,如下所示: @RequestMapping("doSth") public void doSth(HttpServletRequest request,HttpServletResponse response,BufferedReader bufferedReader,PrintWriter printWriter){ //可以直接调用每个方法参数,spring已帮我们完成注入 System.out.println("do something...."); } 但现在问题来了,如果我们很多个方法都要使用到这些web资源,是否都要在方法入参上一一写明呢?这未免过于繁琐,事实上,我们可以结合被@ModelAttribute注解的方法会在控制器每个方法调用前执行的特点来完成全局资源统一准备工作,见下面的例子: package com.mvc.controller; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; @Controller public abstract class BaseController { //准备web资源 protected ServletContext servletContext;//声明为protected方便子类继承使用 protected HttpSession session; protected HttpServletRequest request; protected HttpServletResponse response; //可以用来读取上传的IO流 protected BufferedReader bufferedReader; //可以用来给安卓、IOS或网页ajax调用输出数据 protected PrintWriter printWriter; @ModelAttribute protected void setReqAndRes(HttpServletRequest request, HttpServletResponse response) throws IOException { this.request = request; this.response = response; this.session = request.getSession(); this.servletContext = session.getServletContext(); this.bufferedReader = request.getReader(); this.printWriter = response.getWriter(); } } 然后我们可能有UserContrller,ArticleController,xxxxController等等,都可以直接继承BaseController,然后在每一个方法都可以自由使用以上web资源,这样就简便高效很多了。 使用Model和Map操作模型数据 在上面的实例中,我们就尝试使用Model存储和读取数据操作。在springMVC中,Model、ModelMap、Map及其实现类,尽管各自的操作方法不一样,但它们存储的数据都会被spring获取,并绑定到模型数据中。我们来看下面的实例: @ModelAttribute public void getUser3(Map map){//我们使用map类型存储的数据一样会被绑定到model中 System.out.println("getUser3方法被调用"); map.put("user1",new User(1,"myUserName1","myPwd1")); map.put("user2",new User(2,"myUserName2","myPwd2")); } @RequestMapping("model3") public String model3(Model model){//我们可以在这绑定入参model读取前面的数据 System.out.println(model.asMap().get("user1")); System.out.println(model.asMap().get("user2")); return "model3"; } @RequestMapping("model4") public String model4(HashMap map){//我们可以在这绑定入参map的任意实现类读取前面的数据 System.out.println(map.get("user1")); System.out.println(map.get("user2")); return "model3"; } @RequestMapping("model5") public String model4(ModelMap map){//我们可以在这绑定入参modelMap读取前面的数据 System.out.println(map.get("user1")); System.out.println(map.get("user2")); return "model3"; } 访问三个不同的url触发不同的方法,得到的结果都是一致的: 用户1id:1 用户1名:myUserName1 用户1密码:myPwd1 用户2id:2 用户2名:myUserName2 用户2密码:myPwd2 这说明,在springMVC中Model、ModelMap、Map(及其实现类)三者的地位是等价的,都可以将数据绑定到模型中供前端视图获取使用。 @SessionAttribute 在项目中,我们可能需要将登陆用户的信息存储到Session中, 使用@SessionAttribute我们可以将特定的模型属性存储到一个Session域的隐含模型中,然后可以在同一个会话多个请求内共享这些信息。我们先来看一个没使用@sessionAttribute注解的实例: @Controller @RequestMapping("/user") //@SessionAttributes("user")——————————————注意看!!我这里注释了 public class UserController3 { @RequestMapping("req1") public String req1(HttpSession session,ModelMap map ){ User user = new User(1,"myUserName1","myPwd1"); map.put("user",user);//将数据存储在模型中 return "redirect:req2";//返回字符串,且使用redirect:表示重定向 //同样我们可以使用forward:完成跳转。 //注意这里redirect:前后都不能有空格 //如果为redirect: req2,则会重定向到" req2" //如果为redirect :req2,则重定向无效,直接转向视图”redirect :req2.jsp" } @RequestMapping("req2") public String req2(ModelMap modelMap,HttpSession session){ User user = (User) modelMap.get("user"); System.out.println("user from model :" + user);//输出“user from model :null” User suser = (User) session.getAttribute("user"); System.out.println("user from session :" + suser);//输出"user from session :null" return "model1"; } } 我们上面通过重定向跳转到另一个链接,它们处于不同的请求上下文,所以无法通过model和session访问到我们之前存储的user对象。最终都输出null。 接下来,我们把上例类定义头上的@SessionAttributes注释去掉。再访问链接,req2中的两个打印信息分别为: user from model :User [id=1, userName=myUserName1, password=myPwd1] user from session :User [id=1, userName=myUserName1, password=myPwd1] 在这里,如果我将map.put("user",user);中的key:”user”改为“user1”,则输出又变为null 从上面实验,我们可以根据“控制变量法”可以得出结论:@SessionAttribute(“val”)会自动查找模型中名称对应为”val”的数据,并将其存储到一个Session域的隐含模型中(并且可以直接通过HttpSession API获取),在以后的相同会话请求中,SessionAttribute会自动将其存放在调用方法的模型中。 但是,如果我们直接将数据存到Session中,在下次请求中,springMVC不会将改数据放到当次调用方法的模型里。看下面实例: @RequestMapping("req1") public String req1(HttpSession session,ModelMap map ){ User newUser = new User(10,"myNewPassword","myNewUserName");//这里我们新建了一个User,并将其存储在Session中 session.setAttribute("newUser", newUser);//存储操作 return "redirect:req2"; } @RequestMapping("req2") public String req2(ModelMap modelMap,HttpSession session){ User newUser = (User) modelMap.get("newUser");//这里我们尝试从模型中取出上次请求存储在Session中的数据 System.out.println("newUser from model :" + newUser); User snewUser = (User) session.getAttribute("newUser");//这里我们尝试直接通过Session读取 System.out.println("newUser from session :" + snewUser); return "model1"; } 访问上面方法,我们得到的打印结果是: newUser from model :null newUser from session :User [id=10, userName=myNewPassword, password=myNewUserName] 说明在第二次请求中,模型中并没有自动放入Session的数据 SessionStatus @SessionAttribute相关的还有一个SessionStatus接口,它有唯一的实现类: public class SimpleSessionStatus implements SessionStatus { //判断当前会话是否结束,如果为true,则sprng会清空我们使用@SessionAttributes注册的Session数据 //但它不会清空我们手动设置到Session域隐含模型中的数据。 private boolean complete = false; //设置会话结束 @Override public void setComplete() { this.complete = true; } //判断会话是否结束 @Override public boolean isComplete() { return this.complete; } } 通过SessionStatus,能灵活控制我们在@SessionAttributes注册的会话属性。 这里的理解是容易的,所以我们不再进行实例测试,感兴趣的朋友可到文尾下载本篇本章测试源码。 理解@ModelAttribute和@SessionAttributes的交互流程 我们稍微改造一下我们上面的实例,将Model入参改为使用@ModelAttribute User user。看下面实例主要代码: @Controller @RequestMapping("/user") @SessionAttributes("user") public class UserController2 { @RequestMapping("req1") public String req1(HttpSession session,@ModelAttribute User user){ user.setId(1); user.setPassword("myPassword"); user.setUserName("myUserName"); return null; } } 在这里我们尝试使用@ModelAttribute将user存储到模型中,然后再让@SessionAttributes将模型中user存储到Session中,(后面本来是重定向req2再通过模型获取打印出来,这里为了测试实例直接返回null)。 然后我们通过链接请求调用req1方法,结果却报错: org.springframework.web.HttpSessionRequiredException: Expected session attribute ‘user’ 这已错误咋一看非常莫名奇妙,为什么会期望Session中有user呢?首先弄明白:谁会去期望有呢?当然不会是我们的@SessionAttributes,因为它的作用就是将user存储到Session中呀,那么就是我们的@ModelAttribute。 在这里,我们希望通过@ModelAttribute来将user放入隐含模型中,但事实上,它会先到模型中寻找名字对应的属性,并将其赋给入参,如果在当前请求域的隐含模型中没有这个属性,它会到Session域的隐含模型中去找,对于一般属性,如果还是找不到,就会创建一个新的实例。 但是,如果我们的user属性被@SessionAttributes注明为会话属性,如果在Session域隐含模型找不到,就会报错,报什么错呢?就报: org.springframework.web.HttpSessionRequiredException: Expected session attribute ‘user’ 这很好理解,既然user都被标注了Session域属性了,如果在自己老家都找不着人,肯定很着急,自然就要报错了 下面,我们结合流程图来理解@ModelAttribute和@SessionAttributes的运作流程: 我们上面出现的问题就主要体现在三个红底白字框里 源码下载 本节内容测试源码可到https://github.com/jeanhao/spring下的modelAttribute文件夹下下载
在完整web开发中,springMVC主要充当了控制层的角色。它接受视图层的请求,获取视图层请求数据,再对数据进行业务逻辑处理,然后封装成视图层需要的模型数据,再将数据导向到jsp等视图界面。 在前面,我们通过对@RequestMapping和方法入参绑定的分析,完成了视图层->控制层的数据交接,然后业务逻辑处理主要由Service层进行。那么接下来很关键的就是,如何将视图数据导向到特定的视图中。 广泛意义上,视图,并非是单指前端界面如jsp\html等,我们可能需要给安卓、IOS等写后台接口、因前后端分离而放弃视图界面导向如对前端ajax请求的纯数据流输出等。这时,我们的视图可以为json视图、xml视图、乃至PDF视图、Excel视图等 springMVC为我们提供了多种途径输出模型数据: 输出途径 功能说明 ModelAndView 将处理方法返回类型设为ModelAndView,里面封装了我们的模型数据,同时指明了视图导向。 @modelAttribute 方法入参标注改注解后,入参的对象就会放到数据模型中。 Map及Model 入参为org.springframework.ui.Model或org.springframework.ui.ModelMap或java.util.Map时,方法返回时会将Map中的数据自动添加到模型中 @SessionAttributes 将模型中的某个属性暂存到HttpSession中,以便多个请求之间完成属性共享 下面我们主要介绍ModelAndView ModelAndView(下面简称MAV)就像它的名字一样,既包含了模型数据又包含视图信息,我们返回一个MAV,springMVC就会将模型数据转发给相应的视图界面。 在学习ModelAndView的使用方法前,我们先用肢解的方法学习其两个重要组成部分 1. model model是一个接口,我们可以简单地将model的实现类理解成一个Map,将模型数据以键值对的形式返回给视图层使用。在springMVC中,每个方法被前端请求触发调用前,都会创建一个隐含的模型对象,作为模型数据的存储容器。这是一个Request级别的模型数据,我们可以在前端页面如jsp中通过HttpServletRequest等相关API读取到这些模型数据。 在model中,定义有如下常用接口方法: /** * 添加键值属性对 */ Model addAttribute(String attributeName, Object attributeValue); /** * 以属性的类型为键添加属 */ Model addAttribute(Object attributeValue); /** * 以属性和集合的类型构造键名添加集合属性,如果有同类型会存在覆盖现象 */ Model addAllAttributes(Collection<?> attributeValues); /** * 将attributes中的内容复制到当前的model中 * 如果当前model存在相同内容,会被覆盖 */ Model addAllAttributes(Map<String, ?> attributes); /** * 将attributes中的内容复制到当前的model中 * 如果当前model存在相同内容,不会被覆盖 */ Model mergeAttributes(Map<String, ?> attributes); /** * 判断是否有相应的属性值 */ boolean containsAttribute(String attributeName); /** * 将当前的model转换成Map */ Map<String, Object> asMap(); 如果我们添加的属性没有指定键名,我们称之为匿名数据绑定,它们遵循如下规则: 1. 对于普通数据类型,我们直接以类型(第一字母小写)作为键值 2. 对于集合类型(Collection接口的实现者们,包括数组),生成的模型对象属性名为“简单类名(首字母小写)”+“List”,如List生成的模型对象属性名为“stringList”,List生成的模型对象属性名为“userModelList”。 在ModelAndView中,我们更多的是直接操作modelMap和Map来完成我们的模型参数准备即可。modelMap继承自java.util.LinkedHashMap。它在LinkedHashMap的基础上,新增了很多便利的构造方法如: public ModelMap(String attributeName, Object attributeValue) { addAttribute(attributeName, attributeValue); } public ModelMap addAllAttributes(Map<String, ?> attributes) { if (attributes != null) { putAll(attributes); } return this; } 使用这些构造方法能进一步简化我们的模型数据封装。 2. view view也是一个接口,它表示一个响应给用户的视图如jsp文件,pdf文件,html文件。 它有两个接口方法: 1. String getContentType():返回视图的内容类型 2. void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception:根据给定模型和web资源定义视图的渲染形式。 view接口有众多的实现类,如下图所示: 在spring中,通过ViewResolver来解析对应View实例的行为, 它的定义相当简单: public interface ViewResolver { //通过view name 解析View View resolveViewName(String viewName, Locale locale) throws Exception; } spring为我们提供ViewResolver实现类用来解析不同的view: 在我们最开始配置springMVC核心文件时,就用到了InternalResourceViewResolver,它是一个内部资源视图解析器。会把返回的视图名称都解析为 InternalResourceView 对象, InternalResourceView 会把 Controller 处理器方法返回的模型属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 重定向到目标 URL。下面我们来看配置实例: <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"></property><!-- 前缀,在springMVC控制层处理好的请求后,转发配置目录下的视图文件 --> <property name="suffix" value=".jsp"></property><!-- 文件后缀,表示转发到的视图文件后缀为.jsp --> </bean> 解析完Model and(和) View后,再来看我们的ModelAndView: public class ModelAndView { //视图成员 private Object view; //模型成员 private ModelMap model; //是否调用clear()方法清空视图和模型 private boolean cleared = false; /** * 空构造方法 */ public ModelAndView() { } /** * 简便地使用视图名生成视图,具体解析由DispatcherServlet的视图解析器进行 */ public ModelAndView(String viewName) { this.view = viewName; } /** * 指定一个视图对象生成视图 */ public ModelAndView(View view) { this.view = view; } /** * 指定视图名同时绑定模型数据,这里模型数据以追加的形式添加在原来的视图数据中 */ public ModelAndView(String viewName, Map<String, ?> model) { this.view = viewName; if (model != null) { getModelMap().addAllAttributes(model); } } /** * 指定一个视图对象同时绑定模型数据,这里模型数据以追加的形式添加在原来的视图数据中 */ public ModelAndView(View view, Map<String, ?> model) { this.view = view; if (model != null) { getModelMap().addAllAttributes(model); } } /** * 简便配置:指定视图名,同时添加单个属性 */ public ModelAndView(String viewName, String modelName, Object modelObject) { this.view = viewName; addObject(modelName, modelObject); } /** * 简便配置:指定一个视图对象,同时添加单个属性 */ public ModelAndView(View view, String modelName, Object modelObject) { this.view = view; addObject(modelName, modelObject); } /** * 设置当前视图名 */ public void setViewName(String viewName) { this.view = viewName; } /** * 获取视图名,如果当前视图属性为view而非名字(String)则返回null */ public String getViewName() { return (this.view instanceof String ? (String) this.view : null); } /** * 设置当前视图对象 */ public void setView(View view) { this.view = view; } /** * 获取视图,如果非view实例,则返回null */ public View getView() { return (this.view instanceof View ? (View) this.view : null); } /** * 判断当前视图是否存在 */ public boolean hasView() { return (this.view != null); } /** * 获取模型Map,如果为空,则新建一个 */ public ModelMap getModelMap() { if (this.model == null) { this.model = new ModelMap(); } return this.model; } /** * 同getModelMap */ public Map<String, Object> getModel() { return getModelMap(); } /** * 添加单个键值对属性 */ public ModelAndView addObject(String attributeName, Object attributeValue) { getModelMap().addAttribute(attributeName, attributeValue); return this; } /** * 以属性类型为键添加属性 */ public ModelAndView addObject(Object attributeValue) { getModelMap().addAttribute(attributeValue); return this; } /** * 将Map中的所有属性添加到成员属性ModelMap中 */ public ModelAndView addAllObjects(Map<String, ?> modelMap) { getModelMap().addAllAttributes(modelMap); return this; } /** * 清空视图模型,并设为清空状态 */ public void clear() { this.view = null; this.model = null; this.cleared = true; } /** * 判断是否为不含视图和模型 */ public boolean isEmpty() { return (this.view == null && CollectionUtils.isEmpty(this.model)); } }
反射断言 反射对象断言 在实际场景中,我们比较两个对象是否相等,可能会去选择重写equals方法去比较对象里的每一个属性,或者是直接将对象的属性一个个取出来比较,但这都比较麻烦,Unitils为我们提供了反射断言可直接完成这一任务,如下例所示: @Test public void test1(){ User user1 = new User(1,"a","b"); User user2 = new User(1,"a","b"); System.out.println(user1 == user2);//false ReflectionAssert.assertReflectionEquals(user1, user2);//断言成功 } 断言方法的完整方法签名为:assertReflectionEquals(Object expected, Object actual, ReflectionComparatorMode... modes 第一个参数为期望值,第二个为实际值,第三个为断言模式,有一下三种: 断言模式 说明 ReflectionComparatorMode.LENIENT_ORDER 忽略断言集合或数组中元素的顺序,如list(1,2,3)和list(3,2,1)将视为相等(括号内为集合的值) ReflectionComparatorMode.IGNORE_DEFAULTS 忽略Java类型默认值,如引用类型为null,整数类型为0,布尔类型为false等如我们只想比较两个用户的id和名称是否相同,而忽略密码比较User("id","name","password"),User("id","name",null)则这两个对象是相等的 ReflectionComparatorMode.LENIENT_DATES 比较两个日期只比较是否都是值,或者都是null的而忽略具体的日期数值 unitils还未我们提供了既忽略顺序、又忽略默认值的断言方法: .assertLenientEquals(Object expected, Object actual) 反射属性断言 使用方法类似与对象断言,下面是两个实例: assertPropertyLenEquals("id", 1, user); //断言user的id属性的值是1 assertPropertyLenEquals("address.street", "First street", user); //断言user的address的street属性 反射属性断言有多种重载方式,下面是两种集合重载,他会比较:集合actualObjects中的所有对象的成员属性propertyName是否和集合actualObjects匹配,这里存在同样存在顺序的问题、默认值、日期数值的断言模式区分。 1. assertPropertyReflectionEquals(String propertyName, Collection<?> expectedPropertyValues, Collection<?> actualObjects, ReflectionComparatorMode... modes) 2. assertPropertyLenientEquals(String propertyName, Collection<?> expectedPropertyValues, Collection<?> actualObjects) 下面是一个实例: User user1 = new User(2,"a","b"); User user2 = new User(3,"a","b"); ReflectionAssert.assertPropertyLenientEquals("id", Arrays.asList(null, 2), Arrays.asList(user1,user2));//true //junit.framework.AssertionFailedError: Expected: [3], actual: [2, 3] ReflectionAssert.assertPropertyLenientEquals("id", Arrays.asList(3), Arrays.asList(user1,user2)); ReflectionAssert.assertPropertyLenientEquals("id", Arrays.asList(3,2), Arrays.asList(user1,user2));//true //junit.framework.AssertionFailedError: Expected: [3, 4], actual: [2, 3] ReflectionAssert.assertPropertyLenientEquals("id", Arrays.asList(3,4), Arrays.asList(user1,user2)); 整合spring测试 使用unitils测试能简化我们的测试配置,下面看一个web测试实例: public class UnitilsTestSpring extends UnitilsJUnit4{ @SpringApplicationContext("servlet-context.xml")//初始化spring容器 private ApplicationContext applicationContext; @SpringBeanByType//根据类型注入控制器 private UserController userController; @Test public void test1(){ User user = new User(10,"name","password"); userController.getUser(user); //输出User [id=10, userName=name, password=password] } } 其中注入属性有三种方法, 1. @SpringBeanByType:从Spring容器中加载一个与被注解属性相同类型的Bean,找不到则抛出异常 2. @SpringBeanByName:从Spring容器中加载一个与被注解属性相同名称的Bean 3. @SpringBean(value = “myId”):从Spring容器中加载一个id为myId的Bean 在这里我们针对web层测试而专门提到了RestTemplate和Unitils,它们不仅能完成单元测试,在集成测试方面也十分便利。后面在引入service层和dao层配置后,我们还会引入其他单元测试利器,如mock模拟测试以及DBunit数据库测试等。通过合理的单元测试和集成测试有助于我们实际项目的高效开发。
在前面我们进行web测试,总要在游览器进行,数据组装、请求方法更给等都极为麻烦。 RestTemplate是Spring提供的一个web层测试模板类,我们可以通过RestTemplate在客户端方便的进行web层功能测试。它支持REST风格的URL,而且具有AnnotationMethodHandlerAdapter的数据转换器HttpMessageConverters的装配功能。RestTemplate已默认帮我们完成了一下数据转换器的注册: ByteArrayHttpMessageConverter StringHttpMessageConverter ResourceHttpMessageConverter SourceHttpMessageConverter XmlAwareFormHttpMessageConverter 在默认情况下,我们可以直接利用以上转换器对响应数据进行转换处理。而如果我们像拓展其他的转换器如Jaxb2RootElementHttpMessageConverter或MappingJacksonHttpMessageConverter。我们可以使用setMessageConverters(List<HttpMessageConverter<?>> messageConverters)来注册我们所需的转换器。 使用RestTemplate能为我们构建restful风格的客户端请求模板,提供post、get、put、delete、head、options、trace等请求方法,在这里,我们主要分析使用post和get方法来模拟我们web请求,它的优势在于可以通过编程组装解析我们的web请求和响应数据,同时还能方便的修改请求头信息。 在上一篇文章《springMVC(4)json与对象互转实例解析请求响应数据转换器 》我们意图测试发送json格式字符串使后端格式化json字符串并转化为相应的json对象。其中要求contentType必须为application/json。如果这一请求我们直接从游览器输入,会导致出现NetworkError: 415 Unsupported Media Type错误。而使用RestTemplate能解决这个问题并方便的完成我们的web测试。 再以我们上一篇的控制器为例: @RequestMapping("getUser") public void getUser( @RequestBody User user){//将输入数据转化为User对象 System.out.println(user); } @ResponseBody//将输出的java对象转换为合适的相应正文输出 @RequestMapping("getUser2") public User getUser2(User user){ System.out.println(user); return user; } 第一个请求要求输入json格式字符串,spring自动将其转换为User对象,第二个方法要求以键值对形式输入User成员属性,然后直接返回User对象,交由spring转换为json字符串输出。 下面来看我们如何使用RestTemplate来请求getUser方法: import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; public static void main(String args[]){ String user = "{\"id\":10,\"password\":\"myPassword\",\"userName\":\"myUserName\"}";//实例请求参数 HttpHeaders headers = new HttpHeaders();//创建一个头部对象 //设置contentType headers.setContentType(MediaType.valueOf("application/json;UTF-8")); //设置我们的请求信息,第一个参数为请求Body,第二个参数为请求头信息 //完整的方法签名为:HttpEntity<String>(String body, MultiValueMap<String, String> headers) HttpEntity<String> strEntity = new HttpEntity<String>(user,headers); RestTemplate restTemplate = new RestTemplate(); //使用post方法提交请求,第一参数为url,第二个参数为我们的请求信息,第三个参数为我们的相应放回数据类型,与String result对厅 //完整的方法签名为:postForObject(String url, Object request, Class<String> responseType, Object... uriVariables) ,最后的uriVariables用来拓展我们的请求参数内容。 String result = restTemplate.postForObject("http://localhost:8080/springMVC/user/getUser1",strEntity,String.class); System.out.println(result);//运行方法,这里输出: //User [id=10, userName=myUserName, password=myPassword] } 上面我们使用post方法完成请求,如果我们要使用get方法的话可以使用下列方法 getForObject(String url, Class<T> responseType, Object... urlVariables) 我们在url中使用占位符,然后在urlVariables中注入,使用Object…按次序注入,如果我们想要按名称注入,可以使用如下重载方法: getForObject(String url, Class<T> responseType, Map urlVariables) 上面实例我们完成了以application/json的媒体格式、以json字符串为参数请求服务器,并在后端完成json->java对象的解析。下面我们再看一个发送普通表单参数的的例子: RestTemplate restTemplate = new RestTemplate(); //使用占位符绑定入参,这里使用了按顺序注入,所以占位符的参数名任意 //如果使用map注入,则占位符名称需与map中key对应。 String result = restTemplate.postForObject("http://localhost:8080/springMVC/user/getUser2?id={1}&password={2}&userName={3}" ,uEntity,String.class,10,"myPassword","myUserName"); System.out.println(result);//输出{"id":10,"userName":"myUserName","password":"myPassword"} 因为RestTemplate默认装配了前面提到的5个数据转换器,如果我们希望RestTemplate帮我们将上例的json字符串自动转为User对象,也是很轻松的,看下面示例: RestTemplate restTemplate = new RestTemplate(); ResponseEntity<User> result = restTemplate.postForEntity("http://localhost:8080/springMVC/user/getUser2?id={1}&password={2}&userName={3}" ,null,User.class,10,"myPassword","myUserName"); System.out.println(result2.getBody()); 我们仅需将String返回值改成User,并将我们的result的类型定义为ResponseEntity即可,使用这种方法,除了可以获取我们的响应正文Body,还可以获取到正文头信息Header。 在本篇本章中,我们使用RestTemplate完成了客户端测试工作。但在标准的web开发中,我们不希望总是在修改后重新部署服务器,然后再在客户端测试。在下一篇文章中,将会引入Untils配合RestTemplate对我们的web层进行测试而无须依托服务器环境。
格式化数据输入输出 Spring3.0的重要接口:HttpMessageConveter为我们提供了强大的数据转换功能,将我们的请求数据转换为一个java对象,或将java对象转化为特定格式输出等。比如我们常见的从前端注册表单获取json数据并转化为User对象,或前端获取用户信息,后端输出User对象转换为json格式传输给前端等。 spring 为我们提供了众多的HttpMessageConveter实现类,其中我们可能用得最多的三个实现类是: 实现类 功能 FormHttpMessageConverter 从请求和响应读取/编写表单数据。默认情况下,它读取媒体类型 application/x-www-form-urlencoded 并将数据写入MultiValueMap<String,String> MarshallingHttpMessageConverter 使用 Spring 的 marshaller/un-marshaller 读取/编写 XML 数据。它转换媒体类型为 application/xml MappingJacksonHttpMessageConverter 使用 Jackson 的 ObjectMapper 读取/编写 JSON 数据。它转换媒体类型为application/json 转换器的装配方式有两种,一种是通过注册org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter来装配messageConverters,如下所示: <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"><!-- 装配数据转换器 --> <list> <ref bean="jsonConverter" /><!-- 指定装配json格式的数据转换器 --> </list> </property> </bean> <bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"> <!-- 使用MappingJacksonHttpMessageConverter完成json数据转换 --> <property name="supportedMediaTypes" value="application/json" /> <!-- 设置转换的media类型为application/json --> </bean>> 另一种是启用注解<mvc:annotation-driven /> 该注解会会初始化7个转换器: - ByteArrayHttpMessageConverter - StringHttpMessageConverter - ResourceHttpMessageConverter - SourceHttpMessageConverter - XmlAwareFormHttpMessageConverter - Jaxb2RootElementHttpMessageConverter - MappingJacksonHttpMessageConverter 通过以上两种方法,我们即可完成我们的转换器注册。 但我们想要在控制层完成数据的输入输出转换,需要通过下列途径: 1. 使用@RequestBody和@ResponseBody对处理方法进行标注。其中@RequestBody通过合适的HttpMessageConverter将HTTP请求正文转换为我们需要的对象内容。而@ResponseBody则将我们的对象内容通过合适的HttpMessageConverter转换后作为HTTP响应的正文输出。 2. 使用HttpEntity、ResponseEntity作为处理方法的入参或返回值 实例分析 通过以上讲解,我们已经有足够的知识准备,来完成我们的实例:将json数据转换为合适的java对象输入,并将java对象转换为符合格式的json字符输出: 1. 导入需要的jar包 装配MappingJacksonHttpMessageConverter需要我们的jackson相关jar包,我们使用maven来管理项目,在pom.xml中配置如下信息: <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-asl</artifactId> <version>1.9.2</version> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.2</version> </dependency> 2. 装配MappingJacksonHttpMessageConverter 这里我们使用上面提到的方式二更为便利,在spring容器中加入: <mvc:annotation-driven /> 关于springMVC所需的其他配置,可参考我的另一篇文章,或通过文尾的源码下载获取 3. 编写测试文件 下面是我们的User POJO测试类 public class User { private Integer id; private String userName; private String password; @Override public String toString() { return "User [id=" + id + ", userName=" + userName + ", password=" + password + "]"; } //忽略每个参数的get和set方法 } 下面是我们的控制层测试文件: @Controller @RequestMapping("/user") public class UserController { @RequestMapping("getUser") public void getUser( @RequestBody User user){//将输入数据转化为User对象 System.out.println(user); } @RequestMapping("getUser1") public void getUser1( HttpEntity<User> userEntity){//将输入数据转化为User对象 System.out.println(userEntity.getBody()); } @ResponseBody//将输出的java对象转换为合适的相应正文输出 @RequestMapping("getUser2") public User getUser2(User user){ System.out.println(user); return user; } @RequestMapping("getUser3") public HttpEntity<User> getUser3(User user){ System.out.println(user); HttpEntity<User> uEntity = new HttpEntity<User>(user); return uEntity; } } 上面分别展示了注解和HttpEntity的用法。关于HttpResponse只是在HtppEntity的基础上进一步对相应信息进行封装,如修改一些相应头信息等 关于以上的getUser()getUser1()能将我们的json字符串转换为相应的对象,我们可以任何参数名输入: {“id”:10,”password”:”myPassword”,”userName”:”myUserName”} 在这里我们需要的是需将请求头的contentType设置为”application/json;UTF-8“。这样spring才能找到对应的json解析器对我们的json字符串进行解析。否则会报错误:415 Unsupported Media Type 程序中方法调用User的toString()在控制台打印字段: User [id=10, userName=asd, password=qwe] 对于以上的getUser2()和getUser3()方法,我们访问如: http://localhost:8080/springMVC/user/getUser3?id=10&password=qwe&userName=asd。然后spring会帮我们自动将参数对应User对象的属性名绑定到方法入参的user对象中(关于复杂对象、集合绑定可参考我后面系列的文章)。根据上面url传入的参数,spring自动将我们的User对象转换为json格式字符串输出,内容如下: {"id":10,"userName":"asd","password":"qwe"} 选择合适的数据转换器 在前面讲解中,我们通过AnnotationMethodHandlerAdapter注册了众多的数据转换器,而spring会针对不同的请求响应媒体类型,spring会为我们选择最恰当的数据转换器,它是按以下流程进行寻找的: 首先获取注册的所有HttpMessageConverter集合 然后客户端的请求header中寻找客户端可接收的类型,比如 Accept application/json,application/xml等,组成一个集合 所有的HttpMessageConverter 都有canRead和canWrite方法 返回值都是boolean,看这个HttpMessageConverter是否支持当前请求的读与写,读对应@RequestBody注解, 写对应@ResponseBody注解 遍历HttpMessageConverter集合与前面获取可接受类型进行匹配,如果匹配直接使用当前第一个匹配的HttpMessageConverter,然后return(一般是通过Accept和返回值对象的类型进行匹配) 源码下载 本例的示例代码可到https://github.com/jeanhao/spring的mvc_messageConvertor1文件夹中下载
在原生Servlet中,我们通过在doGet和doPost方法绑定web资源访问接口:HttpServletRequest和HttpServletResponse到入参来进一步通过request.getParameter()等方法获取我们的web资源。在SpringMVC中,我们一样可以将HttpServletRequest和HttpServletResponse绑定到入参中使用,但除此之外,SpringMVC还能进一步分析我们处理方法的入参信息,将各类请求资源绑定到我们的方法入参上,并将数据类型转化为我们定义的类型,为我们可以节省了大量的参数获取、初始化工作。 简单名称对应绑定参数 最基本的数据绑定是通过参数名对应完成绑定,如下例 @RequestMapping("test") public void test8(Integer id ){ System.out.println(id); } 下面我们访问root/test?id=111(后面示例项目根路径默认使用“root”表示,),会发现控制台打印“111”,即控制台轻松的帮我们完成了入参绑定,这里我们定义id为Integer,如果我们定义String类型,spring也会帮我们完成相应的类型转换。而如果我们直接访问root.test,即不带任何参数,则方法入参id找不到对应的请求参数名,就为null了。 但现在,我们尝试访问root/test?id=aaa则服务器会响应400错误。这是因为类型转换异常“aaa”不能转换java.lang.Integer。 在这里,我们谈到的是基本类型绑定,但实际上,springMVC还可以帮我们完成很多复杂类型的数据绑定,如:自定义对象User,数组User[],List,Set,Map等,这些都会在我后面的文章提到,敬请关注 使用@RequestParam绑定参数 在方法入参上使用注解@RequestParam能为我们完成更灵活的参数绑定工作,在上一个实例中,我们通过名称对应简单地完成了参数绑定,假如我们现在有需求: 1. 某个关键参数必须传入(比如登陆,我们要求必须有用户名和密码) 2. 某个参数可以不传,但必须要有默认值。 针对这些需求,我们来看下面实例: @RequestMapping("login") public void login(@RequestParam(value = "userName",required = true) String userName,//绑定userName,要求必须 @RequestParam(value = "password",required = true) String pwd){//绑定password,因为在value上对应了,所以方法入参名称可以为其他字符串 System.out.println(userName + "-" + pwd); } @RequestMapping("list") public void list(@RequestParam(value = "pageNow",required = false , defaultValue = "1") Integer pageNow){//模拟分页查询,当前页码可以不传,默认为第一页 System.out.println(pageNow); } @RequestParam涉及三个参数: 参数 说明 value 参数名 required 是否必须,一旦使用了@RequestParam注解,默认为true,如果不存在对应请求参数会出现异常:HTTP Status 400 - Required String parameter 'userName' is not present defaultValue 默认参数值 根据项目需求合理地使用以上三个参数值,能在实际开发中为我们带来一定的便利。 使用@CookieValue绑定cookie 在一般情况下,我们要获取cookie信息,需要使用request.getHeader("cookie")或request.getCookies()等方法来获取,而使用@CookieValue能将我们需要的特定cookie信息绑定到方法入参中。 @RequestMapping("cookie") public void cookie(@CookieValue(value = "JSESSIONID",required = false) String cookie ){ System.out.println(cookie); } value指定了对应cookie的名称,required设置为false表示非必须的,它还有一个属性defaultValue设置不存在时的默认值 使用@RequestHeader绑定头信息 我们先来看一个完整的的头信息组成 Accept text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 Accept-Encoding gzip, deflate Accept-Language en-US,en;q=0.5 Connection keep-alive Host localhost:8080 User-Agent Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0 在springMVC中,我们可以使用@RequestHeader来获取上述头信息的相应数值,如下面实例 @RequestMapping("headerInfo") public void headerInfo(@RequestHeader("Accept-Encoding") String Encoding, @RequestHeader("Accept-Language") String Language, @RequestHeader("Cache-Control") String Cache, @RequestHeader("Connection") String Connection, @RequestHeader("Cookie") String Cookie, @RequestHeader("Host") String Host, @RequestHeader("User-Agent") String Agent ){ System.out.println(Language); System.out.println(Cache); System.out.println(Connection); System.out.println(Cookie); System.out.println(Host); System.out.println(Agent); } 访问/root/headerInfo,控制台打印: en-US,en;q=0.5 max-age 0 keep-alive JSESSIONID=854678F41A231776AFB366DE8A90A356 localhost:8080 Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0 需要注意的是,如果我们这里缺少对应的头信息,而尝试获取的话,而抛出异常类似: Missing header ‘xxxxx’ of type [java.lang.String] 使用IO对象作为入参 在HttpServlet中,我们可以通过request.getReader()方法读取请求信息,使用response.getOutputStream(),getWriter()方法输出响应信息,通样的,在springMVC中,我们可以将上述IO对象绑定到方法入参中使用。我们来看代码实例 @RequestMapping("IO") public void IO(BufferedReader reader,PrintWriter printWriter ){ System.out.println(reader);//输出org.apache.catalina.connector.CoyoteReader@368433b8 System.out.println(printWriter);//输出org.apache.catalina.connector.CoyoteWriter@215f90fe } @RequestMapping("IO2") public void IO2(InputStream inputStream,OutputStream outputStream){ System.out.println(inputStream);//输出org.apache.catalina.connector.CoyoteInputStream@3c37a4ed System.out.println(outputStream);//输出org.apache.catalina.connector.CoyoteOutputStream@692ce27d } @RequestMapping("IO3") public void IO3(InputStream inputStream,BufferedReader reader){ System.out.println(inputStream); System.out.println(reader); //报错java.lang.IllegalStateException: getInputStream() has already been called for this request } @RequestMapping("IO4") public void IO4(PrintWriter printWriter ,OutputStream outputStream){ System.out.println(printWriter); System.out.println(outputStream); //报错java.lang.IllegalStateException: getWriter() has already been called for this response } 从这里我们看到,spring会帮我们完成IO**包装和绑定**,输入输出各一个,如果同类超过一个,就会报错
在springMVC的控制器中,我们常使用@RequestMapping来完成我们的请求映射,我们可以在类定义上和方法定义上使用注解,其配置的路径将为类中定义的所有方法的父路径,如上篇实例中的/user(类)/hello(方法)。 一般的,我们类定义上的路径注解起到命名空间的作用,防止不同方法的路径映射产生冲突,比如我在UserController和ArticleController下都定义了如下的方法: @RequestMapping("list") public void list(){ .... } 一个list映射路径,这时候springMVC就不知道该将请求交给到哪个方法处理。当然,我们也能在方法上进行二级路径配置区分: /*************UserController***********/ @RequestMapping("user/list") public void list(){ .... } /*************ArticleController***********/ @RequestMapping("article/list") public void list(){ .... } 这样就能有效防止冲突了,但如果我有很多个方法存在这样的冲突,是否都要在每个方法加上前缀呢?这时候我们可以选择在类路径上注解@RequestMapping来对全体方法进行区分。 通过url进行映射 1. Ant风格字符匹配 除了标准的url外,@RequestMapping还支持Ant风格字符,即”?”、”*”、”**”,其中 1. “?”:匹配一个任意字符,如/user/a?,匹配user/aa,user/ab等路径 2. “*”:匹配任意字符串,如/user/a*,匹配/user下任意以a开头的路径如/user/abc,/user/aqw等 3. “**“:匹配多级路径字符串,如/user/**/list,匹配/user/user1/list,/user/1resu/list等 在这里,需要注意的是当*的位置与个数不同时,*可以代表的字符数有区别,看下面示例: @RequestMapping("u1/*")//只能匹配u1/a,u1/b,不能匹配u1/————即此时*表示一个或多个字符 public void test(HttpServletResponse response) throws IOException{ response.getWriter().print("u1/*"); } @RequestMapping("u1/**")//能够匹配u1/,u1/qq,u1/qq/ww,这里要特别注意的是,“**“能匹配零个而“*”不能 public void test(HttpServletResponse response) throws IOException{ response.getWriter().print("u1/*"); } @RequestMapping("u2/a*")//能够匹配u2/a,u2/ab,u2/aqqqq等————即此时*表示零个或零个以上字符 public void test1(HttpServletResponse response) throws IOException{ response.getWriter().print("u2/a*"); } 2. restful占位符匹配 除了使用上面风格,@RequestMapping还支持restful风格占位符的形式,假如我们需要针对特定用户查看其特定文章,restful风格路径匹配如下所示: @Controller//注解为控制器,通过spring容器扫描,会注册为一个Bean @RequestMapping("/user/{uid}")//一级访问路径,对类中所有方法生效 public class UserController { @RequestMapping("article/{aid}") public String detail(@PathVariable("uid")Integer uid,@PathVariable("aid")Integer aid){ System.out.println( "查看id为" + uid + "的用户文章,且文章id为"+aid); return "someplace"; } } 这里,如果我们想访问用户id为1,文章id为2的用户文章,就可以访问如下路径:[项目根路径]/user/1/article/2来完成。 我们使用@PathVariable(“val”)来完成对应路径中{val}的资源请求,这里的两个val名称需一致,紧接着的方法入参名字任意,我们刚刚示例了一个多路径参数绑定,假设只有一个,如下也是合法的: @RequestMapping("user/{uid}") public String detail(@PathVariable("uid")Integer notUid){//notUid名字也能成功绑定 return "someplace"; } 此外,如果我们入参名字和url路径资源名称一致,则可以省略配置@PathVariable中的value值,如下实例也能正确绑定路径资源到入参 @RequestMapping("user/{uid}") public String detail(@PathVariable Integer uid){//notUid名字也能成功绑定 return "someplace"; } 3. 优先匹配规则 url还有如下两个常见匹配准则:最长最精确优先匹配和占位符优先匹配 1. 最长最精确优先匹配 下面我们来看一个匹配实例: @RequestMapping("test/**") public void test2(HttpServletResponse response) throws IOException{ response.getWriter().print("test/**"); } @RequestMapping("test/*") public void test3(HttpServletResponse response) throws IOException{ response.getWriter().print("test/*"); } @RequestMapping("test/*/**") public void test4(HttpServletResponse response) throws IOException{ response.getWriter().print("test/*/**"); } @RequestMapping("test/*/*") public void test5(HttpServletResponse response) throws IOException{ response.getWriter().print("test/*/*"); } @RequestMapping("test/1/*") public void test6(HttpServletResponse response) throws IOException{ response.getWriter().print("test/1/*"); } @RequestMapping("test/1/2") public void test7(HttpServletResponse response) throws IOException{ response.getWriter().print("test/1/2"); } 直接看上面匹配会觉得很乱,我们直接看下面的测试: 测试 匹配结果 test/a 匹配test/*而不匹配test/**(更精确优先匹配) test/a/a/aa/a 匹配test/**而不匹配test/*/**,(在多层匹配中,**比*/**更精确) test/a/a 匹配test/*/*,因为/*/*比**精确 test/1/a 匹配test/1/*,因为/1/*比/*/*精确 test/1/2 匹配test/1/2,这是完全匹配 2. 占位符优先匹配原则 占位符是指@PathVariable等路径资源占位符,下面我们在看一个实例 @RequestMapping("test/1/2") public void test7(HttpServletResponse response) throws IOException{ response.getWriter().print("test/1/2"); } @RequestMapping("test/1/{id}") public void test8(HttpServletResponse response,@PathVariable Integer id ) throws IOException{ response.getWriter().print("test/1/(myId=)" + id ); } @RequestMapping("test/1/a") public void test7(HttpServletResponse response) throws IOException{ response.getWriter().print("test/1/a"); } 从上一个实例的所有路径映射中,我们测试出test/1/2是最精确的。但我们根据添加了占位符映射,在游览器输入test/1/2,此时游览器返回test/1/(myId=)2,即占位符的优先级比普通字符串的优先级更高!但如果我们此时输入test/1/a。程序不会因为我们的在方法入参中id映射为Integer类型而放弃匹配,占位符的优先级依然比字符(串)a的优先级高,但由于“a”不能转化为Integer类型,所以服务器会返回400错误 通过HTTP其它请求资源映射 除了使用url外,我们还能通过请求参数、请求方法、或请求头进行映射 我们先看看@RequestMapping的完整属性列表: 属性 说明 value 指定请求的实际地址, 比如 /action/info之类。 method 指定请求的method类型, GET、POST、PUT、DELETE等 consumes 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html; produces 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回 params 指定request中必须包含某些参数值是,才让该方法处理 headers 指定request中必须包含某些指定的header值,才能让该方法处理请求 其中,consumes, produces使用content-type信息进行过滤信息;headers中可以使用content-type进行过滤和判断。 在前面的使用中,我们发现并没有指定value属性,直接在括号里输入字符串也能向value属性赋值,这是因为在java注解中不加其他属性,直接赋值必定是针对注解的value成员,如果该注解没有名为value的成员,则会报错 下面我们先看几个示例: 示例1:vmethod,headers @RequestMapping(value = "testa",method = RequestMethod.POST,headers = "content-type=text/*") 表示映射路径为testa,请求方法必须为POST方法(如果我们用post发出请求,会返回错误信息HTTP Status 405 - Request method ‘GET’ not supported),headers部分表示请求头信息中必须包含等号后相应部分内容,*匹配任意字符串 示例2:consumes @RequestMapping(value = "testb", consumes="application/json") 表示方法仅匹配request Content-Type为“application/json”类型的请求。 示例3:produces @RequestMapping(value = "/testc", produces="application/json") 表示方法匹配的请求需要请求头中Accept部分包含”application/json“,同时在响应时,会将返回内容同时设置为”application/json‘’ 示例4:params @RequestMapping(value = "testd",method = RequestMethod.GET,params = {"id1","id2"}) public void test12(HttpServletResponse response,Integer id1,Integer id2) throws IOException{ response.getWriter().print(id1 + "——" + id2); } 示例表示入参需包含参数名为id1,id2的两个参数,这里如果我输入: 1. http://localhost:8080/springMVC/user/testd—- 2. http://localhost:8080/springMVC/user/testd?id1=1—报404错误 3. ttp://localhost:8080/springMVC/user/testd?id1=1&id2=2—-返回1——2 4. ttp://localhost:8080/springMVC/user/testd?id1=1&id2=2&id3=3—-返回1——2 从以上我们可以看出,只有具有相应参数的才能完成映射,且可以有除了params中要求以外的参数,如id3。 在params的常见映射规则如下: 示例规则 说明 ”param1” 请求必须包含名为param1的参数 “!param1” 请求中不能包含名为param1的参数 “param1!=value1 请求中必须包含param1参数,但其值不能为value1 {“param1=value1”,”param2”} 请求中需要包含param1参数和param2参数,且param1的值必须为value1
在一个web项目中,典型的MVC架构将后台分为Controller、Service、DAO三层,分别实现不同的逻辑功能,下面是一个web请求过程中,我们后台的处理过程: Created with Raphaël 2.1.0客户端客户端controllercontrollerserviceserviceDAO/数据库DAO/数据库发送请求进行业务逻辑处理调用DAO层API访问数据库进行数据处理返回数据封装返回相应业务逻辑处理结果发送响应。 springMVC就充当着其中的控制层角色,它和我们的原生servlet所起的作用基本一致,但相对原生Servlet,SpringMVC的Web转发请求处理功能就强大得多了。 1. 导入jar包 使用springMVC需导入相应的jar包,我们使用maven来管理我们的项目: <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- spring版本号 --> <spring.version>4.0.2.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency> <!-- 导入dbcp的jar包,用来在applicationContext.xml中配置数据库 --> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.2.2</version> </dependency> <!-- spring核心包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- servlet jsp --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.0</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> </dependencies> 2. 配置web.xml web.xml文件用于在web服务启动时,初始化一些配置信息,对于一般Web项目来说,web.xml文件是非必须的。但在springMVC中,我们需要通过web.xml配置servlet拦截特定url来实现控制器的功能,因而也是必须的。 下面先来看一个web.xml的实例配置 <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <!-- 定义欢迎页,当通过跟路径/访问项目时,默认跳转到index.jsp视图中 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 声明spring上下文配置文件所在位置,可以使用通配符*等进行模糊匹配,当有多个配置文件时,可以使用逗号间隔 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml,classpath:spring/spring-*.xml</param-value> </context-param> <!-- 审批ringmvc的核心分发器,它会默认自动加载WEB-INF文件夹下的<servlet-names>-servlet.xml文件,在这里,我们的servlet-nane为springMVC --> <servlet> <servlet-name>springMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 如果我们的配置文件不在默认路径下,我们需要通过<init-param>配置一个contextConfigLocation指定springMVC分发器配置文件位置 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:servlet-context.xml</param-value> </init-param> <!-- 设置当前servlet在所有servlet中第一个启动 --> <load-on-startup>1</load-on-startup> </servlet> <!-- 配置springMVC控制器的拦截url,默认所有url都被拦截--> <servlet-mapping> <servlet-name>springMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 配置上下文载入监听器,它会在web服务启动时,根据contextConfigLocation中声明的spring配置文件位置载入配置信息 需要注意的是,它不会载入DispatcherServlet已经载入的配置文件 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 字符集处理过滤器,能够强制对所有web请求编码进行utf-8编码,从而有效避免乱码产生 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> 在spring容器之间可配置成父子级关系,父级容器的内容在子容器课件,子容器的内容在父容器不可见。在本例中,父容器applicationContext.xml访问不到子容器DispatcherServlet对应的spring容器servlet-context.xml中的内容。如果子容器配置了和父容器相同的内容,可能存在一个配置覆盖的问题,这个会在后面我们分析事务注入的时候再提到 一个web.xml可以根据我们的项目需求配置多个DispatcherServlet,通过对应的实现对不同逻辑的请求拦截。 3. 配置springMVC核心文件servlet-context.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd "> <!-- 扫描com.mvc.controller包下所有的类,使spring注解生效 --> <context:component-scan base-package="com.mvc.controller"/> <!-- 定义视图解析器 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"></property><!-- 前缀,在springMVC控制层处理好的请求后,转发配置目录下的视图文件 --> <property name="suffix" value=".jsp"></property><!-- 文件后缀,表示转发到的视图文件后缀为.jsp --> </bean> </beans> 在没有引入其他配置之前,我们可以先不用配置父容器applicationContext.xml。 4. 配置控制器 接下来,我们需要配置一个从功能上相当于servlet的控制器:获取前端请求->处理请求->转发视图(view)层 package com.mvc.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller//注解为控制器,通过spring容器扫描,会注册为一个Bean @RequestMapping("/user")//一级访问路径,对类中所有方法生效 public class UserController { @RequestMapping("/hello")//二级访问路径 public String hello(){ //返回视图文件名,和servlet-context.xml,会将请求转发到/WEB-INF/views/hello.jsp文件中 return "hello";。 } } 5. 编写视图层文件 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>hello spring mvc</title> </head> <body> Hello </body> </html> 6. 测试配置 至此配置已基本完成,我们可以将当前应用部署到服务器中,然后访问相应路径,这里我们的项目名为springMVC,使用tomcat服务器监控本地8080端口,于是我们的请求url是http://localhost:8080/springMVC/user/hello。访问后,游览器挑转到我们的jsp页面
@GeneratedValue基本注解类型 在上一篇文章中,我们讲到了JPA使用@GeneratedValue注解来定义生成策略,而关于注解生成策略有4种基本支持类型: 1. GenerationType.TABLES 当前主键的值单独保存到一个数据库的表中 2. GenerationType.SEQUENCE 利用底层数据库提供的序列生成标识符 3. GenerationType.IDENTITY 采取数据库的自增策略 4. GenerationType.AUTO 根据不同数据库自动选择合适的id生成方案,这里使用mysql,为递增型 而在配置GenerationType.SEQUENCE和GenerationType.TABLES我们可以使用如下来拓展配置: 1. GenerationType.SEQUENCE 实例配置如下: @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "idGenetator") //下面的name与GeneratedValue的generator属性一致 @SequenceGenerator(name = "idGenetator", sequenceName = "t_user_seqId",//数据库中序列的名字 allocationSize = 1,//每次增长数值 initialValue = 2)//初始数值 private Integer id; 这里需注意的是mysql是不支持sequence增长方式的。 2. GenerationType.TABLES 实例配置如下: @GeneratedValue(strategy = GenerationType.TABLE, generator = "idGenerator") @TableGenerator(name = "idGenerator", table = "user_idSeq", pkColumnName = "user_pk", pkColumnValue = "2", valueColumnName = "gen_val", initialValue = 2, allocationSize = 5) 其对应属性说明为: 属性 说明 name 属性表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中。 table 属性表示表生成策略所持久化的表名,例如,这里表使用的是数据库中的“tb_generator”。 catalog 属性和schema具体指定表所在的目录名或是数据库名。 pkColumnName 属性的值表示在持久化表中,该主键生成策略所对应键值的名称。例如在“tb_generator”中将“gen_name”作为主键的键值 valueColumnName 属性的值表示在持久化表中,该主键当前所生成的值,它的值将会随着每次创建累加。例如,在“tb_generator”中将“gen_value”作为主键的值 pkColumnValue 属性的值表示在持久化表中,该生成策略所对应的主键。例如在“tb_generator”表中,将“gen_name”的值为“CUSTOMER_PK”。 initialValue 表示主键初始值值,默认为0。 allocationSize 表示每次主键值增加的大小,例如设置成1,则表示每次创建新记录后自动加1,默认为50。 UniqueConstraint 与@Table标记中的用法类似,用以设置唯一约束,具体使用格式可参考上一篇文章《hibernate5(4)实体映射注解配置[1]注解全面解析》 如果我们向数据库中尝试插入两条操作,我们对数据库进行相应的操作后,会看到如下信息: mysql> show tables; +———————+ | Tables_in_hibernate | +———————+ | t_user | | user_idSeq | +———————+ 2 rows in set (0.00 sec) mysql> desc t_user; +———–+————-+——+—–+———+——-+ | Field | Type | Null | Key | Default | Extra | +———–+————-+——+—–+———+——-+ | id | int(11) | NO | PRI | NULL | | | user_name | varchar(20) | NO | | NULL | | +———–+————-+——+—–+———+——-+ 2 rows in set (0.00 sec) mysql> desc user_idSeq; +———+————–+——+—–+———+——-+ | Field | Type | Null | Key | Default | Extra | +———+————–+——+—–+———+——-+ | user_pk | varchar(255) | YES | | NULL | | | user_id | int(11) | YES | | NULL | | +———+————–+——+—–+———+——-+ 2 rows in set (0.01 sec) mysql> select * from user_idSeq; +———+———+ | user_pk | user_id | +———+———+ | 2 | 1 |——————————这里的user_pk值对应pkColumnValue=2,pkColumnValue的值还可以是英文,我们可以将不同数据库的主键共同配置在这个表中。 +———+———+ 1 row in set (0.00 sec) /———————————我们是在这里插入第2条数据库的——————————/ mysql> select * from user_idSeq; +———+———+ | user_pk | user_id | +———+———+ | 2 | 2 |————————自增了 +———+———+ 1 row in set (0.00 sec) mysql> select * from t_user; +—-+————–+ | id | user_name | +—-+————–+ | 1 | hello spring | | 5 | hello spring |————————用户id以5递增 +—-+————–+ hibernate 内置主键生成器 我们也可以通过@GenericGenerator来使用hibernate的内置主键生成器,下面是一个实例配置: //是个32位难读的长字符串,但是它没有跨数据库的问题,将来切换数据库极其简单方便,推荐使用 @GenericGenerator(strategy = "uuid。hex",name = "user_uuid")//使用uuid的hibernate内置生成策略 @GeneratedValue(generator = "user_uuid") private String id;//uuid生成策略需使用String类型 下面是我们常用的hibernate内置主键生成器说明: 主键生成器 说明 increment 适用于代理主键。由Hibernate自动以递增的方式生成标识符,每次增加1。优点:由于它的机制不依赖于底层数据库系统,因此它适合于所有的数据库系统。缺点:只适合有单个Hibernate应用进程访问同一个数据库,在集群环境下不推荐使用它。 另外,OID必须为long,int,short类型,如果为byte类型,则会有异常。 identity 适用于代理主键。由底层数据库生成标识符。前提条件是底层数据库支持自动增长字段类型。(oracle数据库不能用它) sequence 适用于代理主键。Hibernate根据底层数据库的序列来生成标识符。前提条件是底层数据库支持序列。(oracle数据库能用它) hilo 适用于代理主键。Hibernate根据high/low算法来生成标识符。Hibernate把特定表的字段作为”high”值.在默认情况下选用hibernate_unique_key表的next_hi字段。它的机制不依赖于底层数据库系统,因此它适合于所有的数据库系统。high/low算法生成的标识符只能在一个数据库中保证唯一。 native 适用于代理主键。根据底层数据库对自动生成标识符的支持能力,来选择identity, sequence, hilo。很适合于跨平台开发,即同一个Hibernate应用需要连接多种数据库系统。 uuid.hex 适用于代理主键。Hibernate采用128位的UUID算法来生成标识符。UUID算法能够在网络环境中生成唯一的字符串标识符。这种标识符生成策略并不流行,因为字符串类型的主键比整数类型的主键占用更多的数据库空间。 assigned 适用于自然主键。由Java应用程序负责生成标识符,为了能让Java应用程序设置OID,不能把setID()方法声明为private类型,应该尽量避免使用自然主键。 源码下载 本实例源码可到https://github.com/jeanhao/hibernate下载。
在另一篇文章hibernate5(2)初入门配置实例中,我们针对hibernate5.1版本的崭新引导配置方法,完成了对数据库的的插入实例操作,在本节内容中,我们开始引入spring4,完成spring4与hibernate5.1的整合工作,像数据库中插入一条记录。在后面学习hibernate中,我们都会使用spring来管理我们的Bean容器。 1. 导入spring4所需jar包 我们推荐使用maven来管理项目,下面是maven中的spring整合hibernate完整配置。 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <org.hibernate-version>5.1.0.Final</org.hibernate-version> <!-- spring版本号 --> <spring.version>4.0.2.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency> <!-- 导入dbcp的jar包,用来在applicationContext.xml中配置数据库 --> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.2.2</version> </dependency> <!-- hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${org.hibernate-version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>${org.hibernate-version}</version> </dependency> <!-- spring核心包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- mybatis核心包 --> <!-- servlet jsp --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.0</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> </dependencies> 在这里,我们顺便引入了springMVC相关jar包,在我们的后续学习测试中可能会用到,现在暂时不用理会 2. 编写spring容器文件 在上一节中,我们的数据库、hibernate的相关配置都在hibernate.cfg.xml文件中完整,使用spring后,这些统统交给spring来进行管理。spring完整实例配置文件如下 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"><!-- 设置为close使Spring容器关闭同时数据源能够正常关闭,以免造成连接泄露 --> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/yc" /> <property name="username" value="yc" /> <property name="password" value="yc" /> <property name="defaultReadOnly" value="false" /><!-- 设置为只读状态,配置读写分离时,读库可以设置为true 在连接池创建后,会初始化并维护一定数量的数据库安连接,当请求过多时,数据库会动态增加连接数, 当请求过少时,连接池会减少连接数至一个最小空闲值 --> <property name="initialSize" value="5" /><!-- 在启动连接池初始创建的数据库连接,默认为0 --> <property name="maxActive" value="15" /><!-- 设置数据库同一时间的最大活跃连接默认为8,负数表示不闲置 --> <property name="maxIdle" value="10"/><!-- 在连接池空闲时的最大连接数,超过的会被释放,默认为8,负数表示不闲置 --> <property name="minIdle" value="2" /><!-- 空闲时的最小连接数,低于这个数量会创建新连接,默认为0 --> <property name="maxWait" value="10000" /><!-- 连接被用完时等待归还的最大等待时间,单位毫秒,超出时间抛异常,默认为无限等待 --> </bean> <!-- 配置我们的回话工厂--> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="hibernateProperties"> <props> <!-- MySQL的方言 --> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="javax.persistence.validation.mode">none</prop> <!-- 必要时在数据库新建所有表格 --> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.show_sql">true</prop> <!-- 配置current session的上下文环境,方便我们调用sessionFactory获取当前线程统一个session对象 --> <prop key="current_session_context_class">thread</prop> <!-- 用更漂亮的格式显示sql语句--> <!-- <prop key="hibernate.format_sql">true</prop> --> </props> </property> <property name="packagesToScan" value="com.zeng.model" /><!-- 配置需要扫描的包路径,在该包下,所有的类注解配置都会被扫描 --> </bean> </beans> 关于spring的配置学习,可参考我另一博客专栏《Spring研磨分析》。 3. 编写测试实体类 测试实体类与我们上一篇文章实例一样。 package com.zeng.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity//声明当前类为hibernate映射到数据库中的实体类 @Table(name = "t_user")//声明在数据库中自动生成的表名为t_user public class User { @Id//声明此列为主键 @GeneratedValue(strategy = GenerationType.AUTO)//根据不同数据库自动选择合适的id生成方案,这里使用mysql,为递增型 private Integer id; private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 4. 编写测试方法 @Test public void test2(){ //使用此方法获取并初始化我们的spring容器,注意pring-datasource.xml必须存放在类路径的根目录下。 ApplicationContext ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); //从spring容器中获取我们的会话工厂实例,里面已完成好各个属性的配置工作 SessionFactory sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); //下面开始我们的数据库操作 Session session = sessionFactory.openSession();//从会话工厂获取一个session Transaction transaction = session.beginTransaction();//开启一个新的事务 User user = new User(); user.setName("hello spring"); session.save(user); transaction.commit();//提交事务 } 运行测试文件,看到打印信息: Hibernate: insert into t_user (name) values (?) 说明我们的插入操作已完成,查看数据,会看到一条新的记录。 至此,我们轻松地完成了spring与hibernate的整合工作,从下一节开始,我们以快速入门为目的,先介绍hibernate的各类使用方法,在结合实例熟悉hibernate的基本使用后,我们再深入开展对hibernate的分析学习。 源码下载 本实例源码可到https://github.com/jeanhao/hibernate下载。
入门实例:向数据库插入一个对象 1. 第一步需要引入我们的jar包,推荐使用maven管理项目,直接在pom.xml中添加 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <org.hibernate-version>5.1.0.Final</org.hibernate-version> </properties> <dependencies> <dependency> <groupId>junit</groupId><!-- 我们测试时使用junit--> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId><!-- hibernate内置日志记录所需包--> <artifactId>slf4j-api</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency> <!-- hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${org.hibernate-version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>${org.hibernate-version}</version> </dependency> <dependencies> 2. 配置hibernate.cfg.xml 在类根路径下创建hibernate.cfg.xml,在测试文件中,我们会默认读取此位置下此名字的hibernate配置文件。 <?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- 数据库连接配置 --> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="connection.url">jdbc:mysql://localhost:3306/hibernate</property> <property name="connection.username">root</property> <property name="connection.password">root</property> <!-- 数据库连接池的大小 --> <property name="connection.pool_size">5</property> <!-- 每次从数据库中取出并放到JDBC的Statement中的记录条数。Fetch Size设的越大,读数据库的次数越少,速度越快,Fetch Size越小,读数据库的次数越多,速度越慢--> <property name="jdbc.fetch_size">50 </property> <!--批量插入,删除和更新时每次操作的记录数。Batch Size越大,批量操作的向数据库发送Sql的次数越少,速度就越快,同样耗用内存就越大--> <property name="jdbc.batch_size">23 </property> <!-- SQL 方言 --> <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property> <!-- Enable Hibernate's automatic session context management --> <property name="current_session_context_class">thread</property> <!-- 在控制台输出sql语句 --> <property name="show_sql">true</property> <!-- 在启动时根据配置更新数据库 --> <property name="hbm2ddl.auto">update</property> <mapping class="com.zeng.model.User"/><!-- 注册我们的实体映射类--> </session-factory> </hibernate-configuration> 3. 编写实体类对象 hibernate是一个ORM(Object-Relation-Mapping)对象关系映射型框架,我们通过创建实体类,一一对应到我们的数据库表。一旦配置好我们的实体类,hibernate能够自动帮我们完成数据库建表操作。本系列环境基于hibernate4,这里优先使用注解的形式来配置实体。 package com.zeng.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity//声明当前类为hibernate映射到数据库中的实体类 @Table(name = "t_user")//声明在数据库中自动生成的表名为t_user public class User { @Id//声明此列为主键 @GeneratedValue(strategy = GenerationType.AUTO)//根据不同数据库自动选择合适的id生成方案,这里使用mysql,为递增型 private Integer id; private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 关于注解配置实体类的更多属性,我们会在后面系列文章详细提到。 4. 编写测试文件 在我们完成数据库操作前,需要先知道hibernate的两个核心类: 类名 说明 SessionFactory (org.hibernate.SessionFactory) 针对单个数据库映射关系经过编译后的内存镜像,是线程安全的(不可变)。 它是生成Session的工厂。 Session (org.hibernate.Session) 表示应用程序与持久储存层之间交互操作的一个单线程对象,此对象生存期很短,隐藏了JDBC连接,也是Transaction的工厂。 一般在使用hibernate中,我们往往初始话一个SessionFactory对象,因为它是重量级对象,创建需要耗费大量的资源。一旦我们需要进行数据库操作时,我们可以创建新的Session会话对象,来进行我们的数据库操作。明白这一点后,我们开始我们的测试文件编写 @Test//我们使用junit进行测试 public void test1(){ //相对于3.x.x版本hibernate,我们在4.x.x采用如下方式获取我们的会话工厂: //1. 解析我们在hibernate.cfg.xml中的配置 // Configuration configuration = new Configuration().configure(); //2. 创建服务注册类,进一步注册初始化我们配置文件中的属性 // ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(configuration.getProperties()).buildServiceRegistry(); //3. 创建我们的数据库访问会话工厂 // SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry); //但在5.1.0版本汇总,hibernate则采用如下新方式获取: //1. 配置类型安全的准服务注册类,这是当前应用的单例对象,不作修改,所以声明为final //在configure("cfg/hibernate.cfg.xml")方法中,如果不指定资源路径,默认在类路径下寻找名为hibernate.cfg.xml的文件 final StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure("cfg/hibernate.cfg.xml").build(); //2. 根据服务注册类创建一个元数据资源集,同时构建元数据并生成应用一般唯一的的session工厂 SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata().buildSessionFactory(); /****上面是配置准备,下面开始我们的数据库操作******/ Session session = sessionFactory.openSession();//从会话工厂获取一个session Transaction transaction = session.beginTransaction();//开启一个新的事务 User user = new User(); user.setName("zengh"); session.save(user); transaction.commit();//提交事务 } 运行测试文件。我们看到控制台输出: Hibernate: insert into t_user (name) values (?) 查看mysql数据库,发现hibernate已自动帮我们创建好表格,同时User数据插入成功。 mysql> desc t_user; +——-+————–+——+—–+———+—————-+ | Field | Type | Null | Key | Default | Extra | +——-+————–+——+—–+———+—————-+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | +——-+————–+——+—–+———+—————-+ 2 rows in set (0.00 sec) 源码下载 本实例源码可到https://github.com/jeanhao/hibernate下载。
在hibernate5中,有了一些新的变动: 新引导 API Spatial/GIS 支持 Java 8 支持 扩展 AUTO id 生成支持 命名策略分离 属性转换器支持 更好的 “bulk id table” 支持 事务管理 模式工具链 Session API类化 改进 OSGi 支持 改进 bytecode 增强功能 新的引导API 用来引导Hibernate(建立一个SessionFactory)的经典方式一直都是利用Configuration配置类。从hibernate的古老版本到现在,它一直支持用户按任意的顺序添加新的配置和关系映射,并允许我们在程序运行过程中查询获取相应的状态和映射信息。但这也意味着我们不能根据一些实时配置高效地建立映射信息。这导致许多限制和问题。 5.0引入了一个新的引导API旨在减轻这些限制和问题,同时允许我们更好的完成整合工作。想要连接更多关于新的引导API配置指南可到hibernate 官网的User Guide部分 在一定的限制上,Configuration配置方法仍然可以使用,不过它的一些方法已被删除。在新的引导API底层实现部分,Configuration类仍大有作用. Spatial/GIS 支持 Hibernate Spatial是一个已经存在了数年的项目.Karel Maesen对此做出了卓越贡献. 从hibernate5.0开始Hibernate Spatial已经是Hibernate项目的一部分,来使其跟上发展的主流,如果你的项目需要使用到GIS数据,我们高度推荐你尝试使用hibernate-spatial 支持Java 8 虽然并非完全支持,更准确来说,hibernate5.0增加了对Java 8 Date 和Time API的支持,以使我们能够更轻松地完成我们的实体映射类的配置属性到数据库的支持.这种支持通过使用专用利器 hibernate-java8来隔离java8的依赖性.有关更多信息,请参阅hibernate官方API Domain Model Mapping Guide中的Basic Types章节 扩展 AUTO id 生成支持 JPA定义的GenerationType.AUTO属性仅支持数字类型.从5.0开始,hibernate高度扩展并支持更广泛的类型,包括内置支持数字类型(如整型(Integer)\长整型(Long))和UUID.通过新的拓展类org.hibernate.boot.model.IdGeneratorStrategyInterpreter,用户还能自由地定制自己的策略来使用GenerationType.AUTO属性 命名策略分离 为了支持更好地接口设计,命名策略被分离成两个主要部分: 1. org.hibernate.boot.model.naming.ImplicitNamingStrategy:使用此属性当 我们使用的表或列没有明确指定一个使用的名称 2. org.hibernate.boot.model.naming.PhysicalNamingStrategy:用于转换“逻辑名称”(隐式或显式)的表或列成一个物理名称 属性转换器支持 hibernate 5.0 极大地改进了对JPA2.1属性转换器的支持: 1. 充分地支持非@Enumerated注解的枚举值的使用 2. 适用于与@Nationalized结合使用的支持 3. 可以在hbm.xml文件中通过使用下列格式设置type="converter:fully.qualified.AttributeConverterName" 4. 整合了hibernate-envers 5. 集合数值,映射键值 6. 现在能够有效处理null值 7. 支持参数化类型的转换 更好的 “bulk id table” 支持 对于bulk id table的支持已经被重新设计以更好地适配不同数据库的支持 事务管理 事务SPI也完成了主要的重构设计作为hibernate5.0更新的一部分.从用户的角度来看,这些一般只在涉及到配置部分时才会有所接触,此前应用程序直接将与不同的后端事务策略通过org.hibernate.Transaction有效工作.在5.0中,一定程度地支持已经添加进来,org.hibernate.Transaction的API实现,现在是永远不变的。在后端,该org.hibernate.Transaction IMPL会涉及到org.hibernate.resource.transaction.TransactionCoordinator它代表了“事务上下文”根据后端事务策略给定的会话。用户一般不需要关心的区别。 在此我们要注意这种变化,它可能会影响到我们的引导配置.以前的应用我们指定hibernate.transaction.factory_class并且指向了org.hibernate.engine.transaction.spi.TransactionFactory FQN.在hibernate5.0中,新约定是org.hibernate.resource.transaction.TransactionCoordinatorBuilder以及特定使用 hibernate.transaction.coordinator_class setting.关于更多细节,请查看JAVADocs中的org.hibernate.cfg.AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY 下面的短名被识别为: jdbc::(默认值)表示使用基于JDBC的事务(org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl) jta::says示使用基于JTA的事务 (org.hibernate.resource.transaction.backend.jta.internal.JtaTransactionCoordinatorImpl) 请参阅用户手册了解更多详细信息。 模式工具链 hibernate5.0对于模式工具链提供了大量的支持(比如导出\验证\导入等) Session API类化 Hibernate的大量内置API,比如(Session等),全部省级成类,不用再进行复杂的类型转化 改进 OSGi 支持 这始于一个不满的脆弱性hibernate-osgi测试.第一部分是一个使用了Pax Exam 和 Karaf的更好的测试启动.这会导致我们生成一个hibernate Karaf风格的文件 OSGi支持经过了很多改善,这需要归功了来自Karaf和Pax开发者和用户的支持 改进 bytecode 增强功能 在hibernate5.0的文档中已经对此进行了很多工作,但它仍有很大的提升空间,更多信息查看http://hibernate.org/orm/documentation/5.0/
在《Quartz任务调度(3)存储与持久化操作配置详细解析 》一文中,我们通过配置quartz.properties属性文件实现了Quartz的数据库持久化操作。现在整合spring的原理,就是相当于把我们在属性文件中的配置属性整合进SchedulerFactoryBean中,来生成我们的Scheduler类。 这里需要特别注意的是,我们通过Bean配置生成的JobDetail和CronTrigger或SimpleTrigger不能被序列化,因而不能持久化到数据库中,如果想要使用持久化任务调度,我们需要编程式创建Quartz的Job等相关实现类。下面是我们的配置实例: spring容器配置 <bean id="quartzDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/quartz"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean> <!-- quartz持久化存储 --> <bean name="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="dataSource"> <ref bean="quartzDataSource" /> </property> <property name="applicationContextSchedulerContextKey" value="applicationContext" /> <property name="quartzProperties"> <props> <!-- JobStore 配置 --> <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop> <!-- 数据表设置 --> <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop> <prop key="org.quartz.jobStore.dataSource">myDatadource</prop> </props> </property> </bean> 在连接数据前,我们需要先在数据库中创建好对应的表格,在我开始提到的文章内有相关的sql语句。此外,关于quartzProperties还有很多配置属性,如配置线程池、集群等,在我开头提到的那篇文章内都有详细说明。 测试类配置 package tool.job; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import org.quartz.Job; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobKey; import org.quartz.ObjectAlreadyExistsException; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class pickNewsJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在" + sdf.format(new Date()) + "更新日志"); } public static void main(String args[]) throws SchedulerException { JobDetail jobDetail = JobBuilder.newJob(pickNewsJob.class) .withIdentity("job1", "jgroup1").build(); SimpleTrigger simpleTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1") .withSchedule( SimpleScheduleBuilder .repeatSecondlyForTotalCount(10, 2)).startNow() .build(); try{ ApplicationContext ac = new ClassPathXmlApplicationContext("spring/spring-task.xml"); Scheduler scheduler = (Scheduler) ac.getBean("quartzScheduler"); scheduler.scheduleJob(jobDetail, simpleTrigger); scheduler.start(); }catch ( ObjectAlreadyExistsException e) { resumeJob(); } } /** *根据数据库中的记录 恢复异常中断的任务 */ public static void resumeJob() throws SchedulerException { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // ①获取调度器中所有的触发器组 List<String> triggerGroups = scheduler.getTriggerGroupNames(); // ②重新恢复在tgroup1组中,名为trigger1触发器的运行 for (int i = 0; i < triggerGroups.size(); i++) { List<String> triggers = scheduler.getTriggerGroupNames(); for (int j = 0; j < triggers.size(); j++) { Trigger tg = scheduler.getTrigger(new TriggerKey(triggers .get(j), triggerGroups.get(i))); // ②-1:根据名称判断 if (tg instanceof SimpleTrigger && tg.getDescription().equals("jgroup1.DEFAULT")) {//由于我们之前测试没有设置触发器所在组,所以默认为DEFAULT // ②-1:恢复运行 scheduler.resumeJob(new JobKey(triggers.get(j), triggerGroups.get(i))); } } } scheduler.start(); } } 在这里,如果我们测试时在运行中断后再重复运行,会出现ObjectAlreadyExistsException异常,原因是我们创建的JobDetail和Trigger等的名字和数据库中已有记录冲突,新的任务尝试持久到数据库失败。在程序中,我的处理方法是先捕捉异常,然后调用恢复任务方法,来重新执行异常中断的任务。 源码下载 本例源码可到我的github仓库https://github.com/jeanhao/spring的springQuartz文件夹下载
在我们的另一个专栏《深入浅出Quartz任务调度》详细的讲解了使用Quartz适用于从普通门户至网站企业级系统的任务调度实现方法。在下面我们结合实例来完整spring和quartz的整合工作,将我们对quartz的配置统一交给spring容器进行管理。quartz1与quartz2两个版本的差别较大,他们的具体差别可参考我的另一篇文章Quartz任务调度(1)概念例析快速入门 。鉴于我们的实际项目中很多依旧使用着quartz1版本,下面我们会针对quartz1和quartz2的配置分别进行分析。但是: 下面我们会先讲解spring与Quartz1.8.6的整合,不过讲完之后,我们会发现,Quartz1.8.6与Quartz2.2.2的配置区别是很小,因为Quartz2中将原来1.+中的JobDetail和Trigger都变成了接口。但只需将下面的XXXBean换成XXXFactoryBean,即可完美兼容1.+版本配置。 JobDetailBean 扩展自Quartz的JobDetail,当使用Bean声明JobDetail时,Bean的name/id极为任务名字,如果没有指定所属组,就使用默认组,它的常见属性有: 1. jobClass 指定我们自定义的Job接口实现类 2. name 默认为Bean的id名,通过此属性显示指定任务名 3. jobDataAsMap 类型为Map,通过设置此Map的值,对应设置到JobDetail的jobDataMap中 4. applicationContextJobDataKey 通过指定一个key,然后可以通过任务的JobDataMap来访问ApplicationContext,如果不设置此key,则JobDetailbean不会将ApplicationContext放入JobDataMap中 5. jobListenerNames:类型为String[],指定注册在Scheduler中的Joblistener名字,以便这些监听器对本任务进行监听。 6. group:设定组名 下面是一个配置实例: <bean id="forumMaintainJob" class="tool.job.ForumMaintainJob" /> <bean class="org.springframework.scheduling.quartz.JobDetailBean" id="myJobDetail"> <property name="applicationContextJobDataKey" value="acKey" /> <property name="jobClass" value="tool.job.ForumMaintainJob" /> <property name="description" value="I'm desc" /> <property name="group" value="group1" /> <property name="name" value="testJob2" /> <property name="jobDataAsMap"> <map> <entry key="key1" value="value1" /> <entry key="key2" value="value2" /> </map> </property> <property name="jobListenerNames"> <list> <value>listener1</value> <value>listener2</value> </list> </property> </bean> 下面是我们的测试类 public class Test1 { @Test public void test1(){ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-task.xml"); JobDetail jobDetail = (JobDetail) ac.getBean("myJobDetail"); System.out.println(jobDetail.getDescription()); System.out.println(jobDetail.getFullName()); System.out.println(jobDetail.getGroup()); System.out.println(jobDetail.getName()); System.out.println(jobDetail.getJobClass()); String[] listenerNames = jobDetail.getJobListenerNames(); for(String name : listenerNames){ System.out.println(name); } Map map = jobDetail.getJobDataMap(); for(Iterator it = map.keySet().iterator(); it.hasNext();){ Object key = it.next(); System.out.println( key + "——" + map.get(key)); } } } 运行方法后,打印信息: I’m desc group1.testJob2 group1 testJob2 class tool.job.ForumMaintainJob listener1 listener2 acKey——org.springframework.context.support.ClassPathXmlApplicationContext@4d1d54a5: startup date [Sun Mar 27 00:09:10 CST 2016]; root of context hierarchy key2——value2 key1——value1 MethodInvokingJobDetailFactoryBean 有的时候,我们的任务需要对操作数据库,完成特定的业务处理,如在Service定义了一个备份文章表的方法,要求在每天特定时刻将文章内容复制到一份备份表中,如果使用JobDetailBean,我们可能需要单独配置一个实现类,再完成相应的依赖配置工作来实现调用,但如果使用MethodInvokingJobDetailFactoryBean,我们可以把特定对象的某个方法封装成一个Job,就能直接在我们的Service类中完成任务调度了。 下面是我们的配置实例: <bean class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" id="myJobFactoryBean"> <property name="targetClass" value="com.yc.service.ArticleServiceImpl" /><!-- 指定目标类 --> <property name="targetMethod" value="copyArticle" /><!-- 指定目标类中的方法 --> <!-- <property name="staticMethod" value="com.yc.service.ArticleServiceImpl.copyArticle" /> 以上两句等同与这一句 --> <property name="concurrent" value="false" /><!-- 指定最终封装出的任务类是否无状态,默认为true,表示无状态 --> <property name="name" value="job1" /> <property name="group" value="group1" /> <property name="jobListenerNames"> <list> <value>listener1</value> <value>listener2</value> </list> </property> </bean> 下面是我们的测试方法: @Test public void test2(){ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-task.xml"); JobDetail jobDetail = (JobDetail) ac.getBean("myJobFactoryBean"); System.out.println(jobDetail.getDescription()); System.out.println(jobDetail.getFullName()); System.out.println(jobDetail.getGroup()); System.out.println(jobDetail.getName()); System.out.println(jobDetail.getJobClass()); String[] listenerNames = jobDetail.getJobListenerNames(); for(String name : listenerNames){ System.out.println(name); } Map map = jobDetail.getJobDataMap(); for(Iterator it = map.keySet().iterator(); it.hasNext();){ Object key = it.next(); System.out.println( key + "——" + map.get(key)); } } 打印信息 null group1.job1 group1 job1 class org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBeanStatefulMethodInvokingJoblistener1listener2methodInvoker——org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean@3d50475b与之前对比,我们的getJobClass变成了:MethodInvokingJobDetailFactoryBeanStatefulMethodInvokingJob 这里是有状态的,如果在我们配置中将concurrent设为true或不设置,上述类就会变成: MethodInvokingJobDetailFactoryBean$MethodInvokingJob SimpleTriggerBean 直接上配置实例: <bean class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean" id="mySimpleTrigger" > <property name="jobDetail" ref="myJobDetail" /> <property name="name" value="trigger1" /> <property name="description" value="I'm desc of trigger" /> <property name="group" value="tgroup1" /> <property name="repeatCount" value="10" /><!-- 设置重复次数--> <property name="repeatInterval" value="1000" /><!-- 设置调用间隔 --> <property name="startDelay" value="1000" /><!-- 设置延迟执行时间,单位为毫秒 --> <property name="startTime"> <bean class="java.util.Date" /> </property> </bean> 测试方法 @Test public void test3(){ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-task.xml"); SimpleTrigger simpleTrigger= (SimpleTrigger) ac.getBean("mySimpleTrigger"); System.out.println("name = " + simpleTrigger.getName()); System.out.println("group = " + simpleTrigger.getGroup()); System.out.println("jobGroup = " + simpleTrigger.getJobGroup()); System.out.println("jobName = " + simpleTrigger.getJobName()); System.out.println("jobDataMap = " + simpleTrigger.getJobDataMap()); System.out.println("fullName = " + simpleTrigger.getFullName()); System.out.println("fullJobName = " + simpleTrigger.getFullJobName()); System.out.println("repearCount = " + simpleTrigger.getRepeatCount()); System.out.println("repeatInterval = " + simpleTrigger.getRepeatInterval()); System.out.println("TimesTriggered = " + simpleTrigger.getTimesTriggered());//被触发次数 System.out.println("nextFireTime = " + simpleTrigger.getNextFireTime());//下次触发时间,如果不会再触发,则为null System.out.println("previousFireTime = " + simpleTrigger.getPreviousFireTime());//上次触发时间,如果还没开始触发,则为null System.out.println("startTime = " + simpleTrigger.getStartTime());//开始时间 System.out.println("finalFireTime = " + simpleTrigger.getFinalFireTime());//最后触发时间 } 打印结果: name = trigger1 group = tgroup1 jobGroup = group1 jobName = testJob2 jobDataMap = org.quartz.JobDataMap@d9ffa6cd fullName = tgroup1.trigger1 fullJobName = group1.testJob2 repearCount = 10 repeatInterval = 1000 TimesTriggered = 0 nextFireTime = null previousFireTime = null startTime = Sun Mar 27 00:35:12 CST 2016 finalFireTime = Sun Mar 27 00:35:22 CST 2016 注意到,这里的finalFireTime是根据startTime和重复次数、间隔时间来算的。而即时我们一次都没调用,但触发器处于不会触发状态,nextFireTime也为null。 CronTriggerBean 实例配置如下: <bean class="org.springframework.scheduling.quartz.CronTriggerBean" id="myCronTriggerBean"> <property name="cronExpression" value="0/5 * * * * ?" /> <property name="jobDetail" ref="myJobFactoryBean" /> </bean> 其中涉及到的其他属性和SimpleTrigger大同小异,这里就不再提及,关于Cron表达式的使用方法可参考我前面的一篇文章 SchedulerFactoryBean SchedulerFactoryBean能感知Spring容器的生命周期,在Spring容器开启后,Scheduler自动开始工作,而在Spring容器关闭后,自动关闭Scheduler. 下面是我们的配置实例: <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean" id="mySecheduler"> <property name="autoStartup" value="true" /><!-- 设置是否为初始化后自动开启,默认为true,如果需要手动开启,则将此属性设为false --> <!-- <property name="configLocation" value="classpath:quartz.properties" /> --><!-- 读取我们的quartz属性文件资源 --> <property name="jobDetails" > <list > <ref local="myJobDetail"/> <ref local="myJobFactoryBean"/> </list> </property> <property name="triggers"> <list> <ref local="mySimpleTrigger"/> <ref local="myCronTriggerBean"/> </list> </property> <property name="startupDelay" value="2" /><!-- 以秒为单位,设置延迟开始时间 --> </bean> 这里,我们集成了前面所有的配置进行测试。 注释一些冲突的属性,完整的xml配置如下所示: <bean id="forumMaintainJob" class="tool.job.ForumMaintainJob" /> <bean class="org.springframework.scheduling.quartz.JobDetailBean" id="myJobDetail"> <property name="applicationContextJobDataKey" value="acKey" /> <property name="jobClass" value="tool.job.ForumMaintainJob" /> <property name="description" value="I'm desc" /> <property name="group" value="group1" /> <property name="name" value="testJob1" /> <property name="jobDataAsMap"> <map> <entry key="key1" value="value1" /> <entry key="key2" value="value2" /> </map> </property> <!-- <property name="jobListenerNames"> <list> <value>listener1</value> <value>listener2</value> </list> </property> --> </bean> <bean class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" id="myJobFactoryBean"> <property name="targetClass" value="com.yc.service.ArticleServiceImpl" /><!-- 指定目标类 --> <property name="targetMethod" value="copyArticle" /><!-- 指定目标类中的方法 --> <!-- <property name="staticMethod" value="com.yc.service.ArticleServiceImpl.copyArticle" /> 以上两句等同与这一句 --> <!-- <property name="concurrent" value="true" /> --><!-- 指定最终封装出的任务类是否有状态 --> <property name="name" value="job2" /> <property name="group" value="group2" /> <!-- <property name="jobListenerNames"> <list> <value>listener1</value> <value>listener2</value> </list> </property> --> </bean> <bean class="org.springframework.scheduling.quartz.SimpleTriggerBean" id="mySimpleTrigger" > <property name="jobDetail" ref="myJobDetail" /> <property name="name" value="trigger1" /> <property name="description" value="I'm desc of trigger" /> <property name="group" value="tgroup1" /> <property name="repeatCount" value="10" /><!-- 设置重复次数--> <property name="repeatInterval" value="1000" /><!-- 设置调用间隔 --> <property name="startDelay" value="1000" /><!-- 设置延迟执行时间,单位为毫秒 --> <property name="startTime"> <bean class="java.util.Date" /> </property> </bean> <bean class="org.springframework.scheduling.quartz.CronTriggerBean" id="myCronTriggerBean"> <property name="cronExpression" value="0/5 * * * * ?" /> <property name="jobDetail" ref="myJobFactoryBean" /> </bean> <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean" id="mySecheduler"> <property name="autoStartup" value="true" /><!-- 设置是否为初始化后自动开启,默认为true,如果需要手动开启,则将此属性设为false --> <!-- <property name="configLocation" value="classpath:quartz.properties" /> --><!-- 读取我们的quartz属性文件资源 --> <property name="jobDetails" > <list > <ref local="myJobDetail"/> <ref local="myJobFactoryBean"/> </list> </property> <property name="triggers"> <list> <ref local="mySimpleTrigger"/> <ref local="myCronTriggerBean"/> </list> </property> <property name="startupDelay" value="2" /><!-- 以秒为单位,设置延迟开始时间 --> </bean> 其中用到的两个目标类如下 public class ForumMaintainJob implements Job{ @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("论坛维护"); } } public class ArticleServiceImpl { public static void copyArticle() {//注意,我们的代理方法必须是静态的 System.out.println("复制文章操作"); } } 进行测试 @Test public void test4() throws InterruptedException{ //只需初始化我们的Spring容器即可完成定时器配置 ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-task.xml"); Thread.sleep(100000);//这里让主线程休眠确保定时器能正常运行 } 运行程序后,部分打印结果如下: INFO : org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor INFO : org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl INFO : org.quartz.core.QuartzScheduler - Quartz Scheduler v.1.8.6 created. INFO : org.quartz.simpl.RAMJobStore - RAMJobStore initialized. INFO : org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v1.8.6) ‘mySecheduler’ with instanceId ‘NON_CLUSTERED’ Scheduler class: ‘org.quartz.core.QuartzScheduler’ - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool ‘org.quartz.simpl.SimpleThreadPool’ - with 10 threads. Using job-store ‘org.quartz.simpl.RAMJobStore’ - which does not support persistence. and is not clustered. INFO : org.quartz.impl.StdSchedulerFactory - Quartz scheduler ‘mySecheduler’ initialized from an externally provided properties instance. INFO : org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 1.8.6 INFO : org.quartz.core.QuartzScheduler - JobFactory set to: org.springframework.scheduling.quartz.AdaptableJobFactory@65d1fefa DEBUG: org.springframework.beans.factory.support.DefaultListableBeanFactory - Finished creating instance of bean ‘mySecheduler’ DEBUG: org.springframework.context.support.ClassPathXmlApplicationContext - Unable to locate LifecycleProcessor with name ‘lifecycleProcessor’: using default [org.springframework.context.support.DefaultLifecycleProcessor@3e81d0bf] DEBUG: org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean ‘mySecheduler’ DEBUG: org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean ‘lifecycleProcessor’ INFO : org.springframework.context.support.DefaultLifecycleProcessor - Starting beans in phase 2147483647 DEBUG: org.springframework.context.support.DefaultLifecycleProcessor - Starting bean ‘mySecheduler’ of type [class org.springframework.scheduling.quartz.SchedulerFactoryBean] INFO : org.springframework.scheduling.quartz.SchedulerFactoryBean - Will start Quartz Scheduler [mySecheduler] in 2 seconds DEBUG: org.springframework.context.support.DefaultLifecycleProcessor - Successfully started bean ‘mySecheduler’ DEBUG: org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key ‘spring.liveBeansView.mbeanDomain’ in [systemProperties] DEBUG: org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key ‘spring.liveBeansView.mbeanDomain’ in [systemEnvironment] DEBUG: org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key ‘spring.liveBeansView.mbeanDomain’ in any property source. Returning [null] DEBUG: org.quartz.utils.UpdateChecker - Checking for available updated version of Quartz… INFO : org.springframework.scheduling.quartz.SchedulerFactoryBean - Starting Quartz Scheduler now, after delay of 2 seconds INFO : org.quartz.core.QuartzScheduler - Scheduler mySecheduler_$_NON_CLUSTERED started. DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group2.job2 复制文章操作 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group2.job2 复制文章操作 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 DEBUG: org.quartz.core.JobRunShell - Calling execute on job group1.testJob1 论坛维护 这里“论坛维护”任务每隔1s运行一次,共运行十次,“复制文章”操作每隔5s执行一次,无次数限制 以上是基于1.8.6的,如果我们想要在Quartz2.+版本中运行,我们只需改动: <bean class="org.springframework.scheduling.quartz.JobDetailFactoryBean" id="myJobDetail" > <property name="durability" value="true"></property> .... </bean> <bean class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean" id="mySimpleTrigger" > .... </bean> <bean class="org.springframework.scheduling.quartz.CronTriggerFactoryBean" id="myCronTriggerBean"> ..... </bean> 即将原来的xxxBean换成xxxFactoryBean来生成相应的接口实现类即可。 源码下载 本篇文章源码请移步https://github.com/jeanhao/spring的springQuartz文件夹下载。
Quartz框架需求引入 在现实开发中,我们常常会遇到需要系统在特定时刻完成特定任务的需求,在《spring学习笔记(14)引介增强详解:定时器实例:无侵入式动态增强类功能》,我们通过引介增强来简单地模拟实现了一个定时器。它可能只需要我们自己维护一条线程就足以实现定时监控。但在实际开发中,我们遇到的需求会复杂很多,可能涉及多点任务调度,需要我们多线程并发协作、线程池的维护、对运行时间规则进行更细粒度的规划、运行线程现场的保持与恢复等等。如果我们选择自己来造轮子,可能会遇到许多难题。这时候,引入Quartz任务调度框架会是一个很好的选择,它: 1. 允许我们灵活而细粒度地设置任务触发时间,比如常见的每隔多长时间,或每天特定时刻、特定日子(如节假日),都能灵活地配置这些成相应时间点来触发我们的任务 2. 提供了调度环境的持久化机制,可以将任务内容保存到数据库中,在完成后删除记录,这样就能避免因系统故障而任务尚未执行且不再执行的问题。 3. 提供了组件式的侦听器、各种插件、线程池等。通过侦听器,我们可以引入我们的事件机制,配合上异步调用,既能为我们的业务处理类解耦,同时还可能提升用户体验,关于事件机制的基本概念、解耦特性及其异步调用可移步参考我的另一篇文章《spring学习笔记(15)趣谈spring 事件:实现业务逻辑解耦,异步调用提升用户体验 》 实例解析概念 在quartz中,有几个核心类和接口:Job、JobDetail、Trigger、Calendar、Scheduler。 下面我们结合实例来分析这些类的角色定位。 现在我们有一个新闻网站,它有一张任务日志表,记录着我们的不同任务,比如每隔三十分钟要根据文章的阅读量和评论量来生成我们的最热文章列表。在每天早晚12点,定时从其他新闻网站扒取一定量新闻,在每周一晚上12点到3点进行论坛封闭维护,而如果遇到节假日则不维护等。在以上实例中: 生成最热文章,扒取新闻,论坛封闭维护都是我们的Job,它定义了我们的需要执行的任务,是一个抽象的接口. 如果我们要具体到每隔三十分钟生成最热文章,早晚12点扒取新闻等,在我们的具体任务执行时刻,我们就需要能够描述Job及其他相关静态信息的jobDetail,它相当于是我们的Job+具体实现细节 而Trigger则描述了Job执行的时间触发规则,比如每隔三十分钟、早晚12点等 而这里的Calendar可以看成是一些日历特定时间点的集合,比如我们这里遇到节假日则不维护,节假日如国庆节、愚人节等等,都是我们的日历特定时间点。 而Scheduler就是我们的任务日志表,它是一个容器,记载(容纳)了我们前面的工作、触发时间等内容。 具体用法详解 通过实例,我们对quartz的核心类有了较清晰的功能定位,根据Quratz的不同版本,这几个核心类有较大改动,具体的操作不太相同,但是思路是相同的;比如1.+版本jar包中,JobDetail是个类,直接通过构造方法与Job类关联。SimpleTrigger和CornTrigger是类;在2.+jar包中,JobDetail是个接口,SimpleTrigger和CornTrigger是接口。下面详细地分析它们的具体用法: 1. Job Job是一个接口,只有一个void execute(JobExecutionContext jec)方法,JobExecutionContext提供了我们的任务调度上下文信息,比如,我们可以通过JobExecutionContext获取job相对应的JobDetail、Trigger等信息,我们在配置自己的内容时,需要实现此类,并在execute中重写我们的任务内容。下面是我们的扒取新闻工作实例: public class pickNewsJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在"+sdf.format(new Date())+"扒取新闻"); } } 2. JobDetail Quartz在每次执行任务时,都会创建一个Job实例,并为其配置上下文信息,jobDetail有一个成员属性JobDataMap,存放了我们Job运行时的具体信息,在后面我们会详细提到。 1. 在1.+版本中,它作为一个类,常用的构造方法有:JobDetail(String name, String group, Class jobClass),我们需要指定job的名称,组别和实现了Job接口的自定义任务类。实例如JobDetail jobDetail =new JobDetail("job1", "jgroup1", pickNewsJob.class); 2. 而在2.+版本中,我们则通过一下方法创建 JobBuilder.newJob(自定义任务类).withIdentity(任务名称,组名).build();实例如JobDetail jobDetail = JobBuilder.newJob(pickNewsJob.class).withIdentity(“job1”,”group1”).build();` 3. Scheduler 先讲Scheduler,方便后讲解Trigger时测试。 Scheduler作为我们的“任务记录表”,里面(可以)配置大量的Trigger和JobDetail,两者在 Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。下面是使用Schduler的实例: SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); //将jobDetail和Trigger注册到一个scheduler里,建立起两者的关联关系 scheduler.scheduleJob(jobDetail,Trigger); scheduler.start();//开始任务调度 在一个scheduler被创建后,它处于”STAND-BY”模式,在触发任何job前需要使用它的start()方法来启动。同样的,如果我们想根据我们的业务逻辑来停止定时方案执行,可以使用scheduler.standby()方法 3. Trigger Trigger描述了Job执行的时间触发规则,主要有SimpleTrigger和CronTrigger两个子类。 1. SimpleTrigger 如果嵌入事件机制只触发一次,或意图使Job以固定时间间隔触发,则使用SimpleTrigger较合适,它有多个构造函数,其中一个最复杂的构造函数为: SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval)参数依次为触发器名称、触发器所在组名、工作名、工作所在组名、开始日期、结束日期、重复次数、重复间隔。 1. 如果我们不需同时设置这么多属性,可调用其他只有部分参数的构造方法,其他参数也可以通过set方法动态设置。 2. 这里需要注意的是,如果到了我们设置的endTime,即时重复次数repeatCount还没有达到我们预设置的次数。任务也不会再此执行。 下面是1.+版本的创建实例 //创建一个触发器,使任务从现在开始、每隔两秒执行一次,共执行10次 SimpleTrigger simpleTrigger = new SimpleTrigger("triiger1");//至少需要设置名字以标识当前触发器,否则在调用时会报错 simpleTrigger.setStartTime(new Date()); simpleTrigger.setRepeatInterval(2000); simpleTrigger.setRepeatCount(10); 下面是2.+版本的创建实例 SimpleTrigger simpleTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1")//配置触发器名称 .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2))//配置重复次数和间隔时间 .startNow()//设置从当前开始 .build();//创建操作 通过TriggerBuilder,我们可以通过方法方便地配置触发器的各种参数。 2. CronTrigger 通过Cron表达式定义复杂的时间调度方案,具体内容我们在下一篇详细提到 4. Calendar 在实际的开发中,我们可能需要根据节假日来调整我们的任务调度方案。实例如下: //第一步:创建节假日类 // ②四一愚人节 Calendar foolDay = new GregorianCalendar(); //这里的Calendar是 java.util.Calendar。根据当前时间所在的默认时区创建一个“日子” foolDay.add(Calendar.MONTH, 4); foolDay.add(Calendar.DATE, 1); // ③国庆节 Calendar nationalDay = new GregorianCalendar(); nationalDay.add(Calendar.MONTH, 10); nationalDay.add(Calendar.DATE, 1); //第二步:创建AnnualCalendar,它的作用是排除排除每一年中指定的一天或多天 AnnualCalendar holidays = new AnnualCalendar(); //设置排除日期有两种方法 // 第一种:排除的日期,如果设置为false则为包含(included) holidays.setDayExcluded(foolDay, true); holidays.setDayExcluded(nationalDay, true); //第二种,创建一个数组。 ArrayList<Calendar> calendars = new ArrayList<Calendar>(); calendars.add(foolDay); calendars.add(nationalDay); holidays.setDaysExcluded(calendars); //第三步:将holidays添加进我们的触发器 simpleTrigger.setCalendarName("holidays"); //第四步:设置好然后需要在我们的scheduler中注册 scheduler.addCalendar("holidays",holidays, false,false);,注意这里的第一个参数为calendarName,需要和触发器中添加的Calendar名字像对应。 在这里,除了可以使用AnnualCalendar外,还有CronCalendar(表达式),DailyCalendar(指定的时间范围内的每一天),HolidayCalendar(排除节假日),MonthlyCalendar(排除月份中的数天),WeeklyCalendar(排除星期中的一天或多天) 至此,我们的核心类基本讲解完毕,下面附上我们的完整测试代码: /*********************1.+版本*********************/ public class pickNewsJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在" + sdf.format(new Date()) + "扒取新闻"); } public static void main(String args[]) throws SchedulerException { JobDetail jobDetail =new JobDetail("job1", "jgroup1", pickNewsJob.class); SimpleTrigger simpleTrigger = new SimpleTrigger("triiger1"); simpleTrigger.setStartTime(new Date()); simpleTrigger.setRepeatInterval(2000); simpleTrigger.setRepeatCount(10); simpleTrigger.setCalendarName("holidays"); //设置需排除的特殊假日 AnnualCalendar holidays = new AnnualCalendar(); // 四一愚人节 Calendar foolDay = new GregorianCalendar(); // 这里的Calendar是 ava.util.Calendar。根据当前时间所在的默认时区创建一个“日子” foolDay.add(Calendar.MONTH, 4); foolDay.add(Calendar.DATE, 1); // 国庆节 Calendar nationalDay = new GregorianCalendar(); nationalDay.add(Calendar.MONTH, 10); nationalDay.add(Calendar.DATE, 1); //排除的日期,如果设置为false则为包含(included) holidays.setDayExcluded(foolDay, true); holidays.setDayExcluded(nationalDay, true); /*方法2:通过数组设置 ArrayList<Calendar> calendars = new ArrayList<Calendar>(); calendars.add(foolDay); calendars.add(nationalDay); holidays.setDaysExcluded(calendars);*/ //创建scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.addCalendar("holidays", holidays, false, false); scheduler.scheduleJob(jobDetail, simpleTrigger); scheduler.start(); } } /*******************2.+版本***************/ public class pickNewsJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在"+sdf.format(new Date())+"扒取新闻"); } public static void main(String args[]) throws SchedulerException { JobDetail jobDetail = JobBuilder.newJob(pickNewsJob.class) .withIdentity("job1", "group1").build(); SimpleTrigger simpleTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1") .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2)) .startNow() .build(); //设置需排除的特殊假日 AnnualCalendar holidays = new AnnualCalendar(); // 四一愚人节 Calendar foolDay = new GregorianCalendar(); // 这里的Calendar是 ava.util.Calendar。根据当前时间所在的默认时区创建一个“日子” foolDay.add(Calendar.MONTH, 4); foolDay.add(Calendar.DATE, 1); // 国庆节 Calendar nationalDay = new GregorianCalendar(); nationalDay.add(Calendar.MONTH, 10); nationalDay.add(Calendar.DATE, 1); //排除的日期,如果设置为false则为包含(included) holidays.setDayExcluded(foolDay, true); holidays.setDayExcluded(nationalDay, true); /*方法2:通过数组设置 ArrayList<Calendar> calendars = new ArrayList<Calendar>(); calendars.add(foolDay); calendars.add(nationalDay); holidays.setDaysExcluded(calendars);*/ //创建scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.addCalendar("holidays", holidays, false, false); scheduler.scheduleJob(jobDetail, simpleTrigger); scheduler.start(); } } 可见,两个不同版本的主要区别在于JobDetail和Triiger的配置。 此外,除了使用scheduler.scheduleJob(jobDetail, simpleTrigger)来建立jobDetail和simpleTrigger的关联外,在1.+版本中的配置还可以采用如下所示方式 simpleTrigger.setJobName("job1");//jobName和我们前面jobDetail的的名字一致 simpleTrigger.setJobGroup("jgroup1");//jobGroup和我们之前jobDetail的组名一致 scheduler.addJob(jobDetail, true);//注册jobDetail,此时jobDetail必须已指定job名和组名,否则会抛异常Trigger's related Job's name cannot be null scheduler.scheduleJob(simpleTrigger);//注册triiger必须在注册jobDetail之后,否则会抛异常Trigger's related Job's name cannot be null 这里还需要注意的是,如果我们使用scheduler.addCalendar("holidays", holidays, false, false)必须在向scheduler注册trigger之前scheduler.scheduleJob(simpleTrigger),否则会抛异常:Calendar not found: holidays 而在2.+版本中,我尝试在创建triiger时用forJob(“job1”, “jgroup1”)来绑定job名和组名 SimpleTrigger simpleTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1") .forJob("job1", "jgroup1")//在这里绑定 .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2)) .startNow() .build(); //后面是一样的 scheduler.addJob(jobDetail, true); scheduler.scheduleJob(simpleTrigger); 在运行时,却会抛出异常: Jobs added with no trigger must be durable. 显然是绑定失败了,目前暂未找到解决方法,如果有找到解决方法的朋友,恳请告诉我一下,十分感谢!
schedulerListener 在我们的监听器实现类中,这个类中需实现的方法很多,不需要的可以给出空实现,下面是一些常用的用法: 方法 说明 jobScheduled() Scheduler 在有新的 JobDetail 部署时调用此方法。 jobUnscheduled() Scheduler 在有新的 JobDetail卸载时调用此方法 triggerFinalized() 当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。 triggersPaused() Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。 triggersResumed() Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,triggerName 参数将为 null。 jobsPaused() 当一个或一组 JobDetail 暂停时调用这个方法。 jobsResumed() 当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。 schedulerError() Scheduler 的正常运行期间产生一个严重错误时调用这个方法。错误的类型会各式的,但是下面列举了一些错误例子:初始化 Job 类的问题,试图去找到下一 Trigger 的问题,JobStore 中重复的问题,数据存储连接的问题。我们可以使用 SchedulerException 的 getErrorCode() 或者 getUnderlyingException() 方法或获取到特定错误的更详尽的信息。 schedulerShutdown() Scheduler 调用这个方法用来通知 SchedulerListener Scheduler 将要被关闭。 1.x版本配置 下面是一个1.+版本实例配置: package tool.job; import org.quartz.JobDetail; import org.quartz.SchedulerException; import org.quartz.SchedulerListener; import org.quartz.Trigger; public class MySchedulerListener implements SchedulerListener { @Override public void jobScheduled(Trigger trigger) { System.out.println("任务被部署时被执行"); } @Override public void triggerFinalized(Trigger trigger) { System.out.println("任务完成了它的使命,光荣退休时被执行"); } @Override public void jobAdded(JobDetail jobDetail) { System.out.println("一个新的任务被动态添加时执行"); } @Override public void jobUnscheduled(String triggerName, String triggerGroup) { System.out.println("任务被卸载时被执行"); } @Override public void triggersPaused(String triggerName, String triggerGroup) { System.out.println(triggerGroup + "所在组的全部触发器被停止时被执行"); } @Override public void triggersResumed(String triggerName, String triggerGroup) { System.out.println(triggerGroup + "所在组的全部触发器被回复时被执行"); } @Override public void jobDeleted(String jobName, String groupName) { System.out.println(groupName + "." + jobName + "被删除时被执行"); } @Override public void jobsPaused(String jobName, String jobGroup) { System.out.println(jobGroup + "(一组任务)被暂停时被执行"); } @Override public void jobsResumed(String jobName, String jobGroup) { System.out.println(jobGroup + "(一组任务)被回复时被执行"); } @Override public void schedulerError(String msg, SchedulerException cause) { System.out.println("出现异常" + msg + "时被执行"); cause.printStackTrace(); } @Override public void schedulerInStandbyMode() { System.out.println("scheduler被设为standBy等候模式时被执行"); } @Override public void schedulerStarted() { System.out.println("scheduler启动时被执行"); } @Override public void schedulerShutdown() { System.out.println("scheduler关闭时被执行"); } @Override public void schedulerShuttingdown() { System.out.println("scheduler正在关闭时被执行"); } } 下面是我们的测试方法,关于方法中没提到的类的配置可参考我前面系列的文章。 public static void main(String args[]) throws SchedulerException { JobDetail pickNewsJob =new JobDetail("job1", "jgroup1", PickNewsJob.class); JobDetail getHottestJob =new JobDetail("job2", "jgroup2", GetHottestJob.class); SimpleTrigger pickNewsTrigger = new SimpleTrigger("trigger1", "group1",1,2000); SimpleTrigger getHottestTrigger = new SimpleTrigger("trigger2", "group2",1,3000); SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobListener myJobListener = new MyJobListener(); /**********局部Job监听器配置**********/ pickNewsJob.addJobListener("myJobListener");//这里的名字和myJobListener中getName()方法的名字一样 scheduler.addJobListener(myJobListener);//向scheduler注册我们的监听器 /*********全局Job监听器配置************/ // scheduler.addGlobalJobListener(myJobListener);//直接添加为全局监听器 TriggerListener myTriggerListener = new MyTriggerListener(); /**********局部Trigger监听器配置**********/ pickNewsTrigger.addTriggerListener("myTriggerListener"); scheduler.addTriggerListener(myTriggerListener); /*********全局Trigger监听器配置************/ // scheduler.addGlobalTriggerListener(myTriggerListener);//直接添加为全局监听器 /************SchedulerListener配置*************/ SchedulerListener mySchedulerListener = new MySchedulerListener(); scheduler.addSchedulerListener(mySchedulerListener); scheduler.scheduleJob(pickNewsJob,pickNewsTrigger); scheduler.scheduleJob(getHottestJob,getHottestTrigger); scheduler.start(); } 运行方法,我们会看到: 一个新的任务被动态添加时执行————SchedulerListener中的方法被调用 任务被部署时被执行————SchedulerListener中的方法被调用 一个新的任务被动态添加时执行————SchedulerListener中的方法被调用 任务被部署时被执行————SchedulerListener中的方法被调用 scheduler启动时被执行————SchedulerListener中的方法被调用 Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 不否决Job,正常执行 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在13:53:18扒取新闻 在13:53:18根据文章的阅读量和评论量来生成我们的最热文章列表 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 Trigger 被触发并且完成了 Job 的执行,此方法被调用 Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 不否决Job,正常执行 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在13:53:20扒取新闻 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 Trigger 被触发并且完成了 Job 的执行,此方法被调用 任务完成了它的使命,光荣退休时被执行————SchedulerListener中的方法被调用 在13:53:21根据文章的阅读量和评论量来生成我们的最热文章列表 任务完成了它的使命,光荣退休时被执行————SchedulerListener中的方法被调用 2.x 版本配置 2.+版本与1.+版本的主要区别是新添加了一些方法,并将jobName,groupName参数对换成了JobKey等。 下面是配置实例: package tool.job; import org.quartz.JobDetail; import org.quartz.JobKey; import org.quartz.SchedulerException; import org.quartz.SchedulerListener; import org.quartz.Trigger; import org.quartz.TriggerKey; public class MySchedulerListener implements SchedulerListener { @Override public void jobScheduled(Trigger trigger) { System.out.println("任务被部署时被执行"); } @Override public void jobUnscheduled(TriggerKey triggerKey) { System.out.println("任务被卸载时被执行"); } @Override public void triggerFinalized(Trigger trigger) { System.out.println("任务完成了它的使命,光荣退休时被执行"); } @Override public void triggerPaused(TriggerKey triggerKey) { System.out.println(triggerKey + "(一个触发器)被暂停时被执行"); } @Override public void triggersPaused(String triggerGroup) { System.out.println(triggerGroup + "所在组的全部触发器被停止时被执行"); } @Override public void triggerResumed(TriggerKey triggerKey) { System.out.println(triggerKey + "(一个触发器)被恢复时被执行"); } @Override public void triggersResumed(String triggerGroup) { System.out.println(triggerGroup + "所在组的全部触发器被回复时被执行"); } @Override public void jobAdded(JobDetail jobDetail) { System.out.println("一个JobDetail被动态添加进来"); } @Override public void jobDeleted(JobKey jobKey) { System.out.println(jobKey + "被删除时被执行"); } @Override public void jobPaused(JobKey jobKey) { System.out.println(jobKey + "被暂停时被执行"); } @Override public void jobsPaused(String jobGroup) { System.out.println(jobGroup + "(一组任务)被暂停时被执行"); } @Override public void jobResumed(JobKey jobKey) { System.out.println(jobKey + "被恢复时被执行"); } @Override public void jobsResumed(String jobGroup) { System.out.println(jobGroup + "(一组任务)被回复时被执行"); } @Override public void schedulerError(String msg, SchedulerException cause) { System.out.println("出现异常" + msg + "时被执行"); cause.printStackTrace(); } @Override public void schedulerInStandbyMode() { System.out.println("scheduler被设为standBy等候模式时被执行"); } @Override public void schedulerStarted() { System.out.println("scheduler启动时被执行"); } @Override public void schedulerStarting() { System.out.println("scheduler正在启动时被执行"); } @Override public void schedulerShutdown() { System.out.println("scheduler关闭时被执行"); } @Override public void schedulerShuttingdown() { System.out.println("scheduler正在关闭时被执行"); } @Override public void schedulingDataCleared() { System.out.println("scheduler中所有数据包括jobs, triggers和calendars都被清空时被执行"); } } 在2.+版本中,我们通过以下方式注册我们的监听器: SchedulerListener mySchedulerListener = new MySchedulerListener(); scheduler.getListenerManager().addSchedulerListener(mySchedulerListener); 其它测试代码可参考我前面系列文章的,测试结果和之前1.+版本内容基本一致 源码下载 关于本节测试源码内容可到https://github.com/jeanhao/spring下quartzEvent文件夹下载
TriggerListener 在我们的触发器监听器中,也包含了一系列监听方法 方法 说明 getName() 定义并返回监听器的名字 triggerFired() 当与监听器相关联的 Trigger 被触发,Job 上的 execute() 方法将要被执行时,Scheduler 就调用这个方法。在全局 TriggerListener 情况下,这个方法为所有 Trigger 被调用。 vetoJobExecution() 在 Trigger 触发后,Job 将要被执行时由 Scheduler 调用这个方法。TriggerListener 给了一个选择去否决 Job 的执行。假如这个方法返回 true,这个 Job 将不会为此次 Trigger 触发而得到执行。 triggerMisfired() Scheduler 调用这个方法是在 Trigger 错过触发时。如这个方法的 JavaDoc 所指出的,你应该关注此方法中持续时间长的逻辑:在出现许多错过触发的 Trigger 时,长逻辑会导致骨牌效应。你应当保持这上方法尽量的小。 triggerComplete() Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用这个方法。这不是说这个 Trigger 将不再触发了,而仅仅是当前 Trigger 的触发(并且紧接着的 Job 执行) 结束时。这个 Trigger 也许还要在将来触发多次的。 下面是我们的监听器实例配置 1. 自定义监听器 public class MyTriggerListener implements TriggerListener { @Override public String getName() { return "myTriggerListener"; } @Override public void triggerFired(Trigger trigger, JobExecutionContext context) { System.out.println(" Trigger 被触发了,此时Job 上的 execute() 方法将要被执行"); } @Override public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) { System.out.println("发现此次Job的相关资源准备存在问题,不便展开任务,返回true表示否决此次任务执行"); return true; } @Override public void triggerMisfired(Trigger trigger) { System.out.println( "当前Trigger触发错过了"); } @Override//1.+版本 public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode) { System.out.println("Trigger 被触发并且完成了 Job 的执行,此方法被调用"); } /* @Override//这是2.+版本的配置,差别在于将triggerInstructionCode从整型改成了枚举类型 public void triggerComplete(Trigger trigger, JobExecutionContext context, CompletedExecutionInstruction triggerInstructionCode) { System.out.println("Trigger 被触发并且完成了 Job 的执行,此方法被调用"); } */ } 使用TriggerListener和JobListener的方法大同小异,思路都是一样的。 2. 1.x版本 相对于上一篇文章的配置,我们只需将JobListener替换成TriggerListener即可。下面是我们的完整测试代码: public static void main(String args[]) throws SchedulerException { JobDetail pickNewsJob =new JobDetail("job1", "jgroup1", PickNewsJob.class); JobDetail getHottestJob =new JobDetail("job2", "jgroup2", GetHottestJob.class); SimpleTrigger pickNewsTrigger = new SimpleTrigger("trigger1", "group1",1,2000); SimpleTrigger getHottestTrigger = new SimpleTrigger("trigger2", "group2",1,3000); SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); JobListener myJobListener = new MyJobListener(); /**********局部Job监听器配置**********/ pickNewsJob.addJobListener("myJobListener");//这里的名字和myJobListener中getName()方法的名字一样 scheduler.addJobListener(myJobListener);//向scheduler注册我们的监听器 /*********全局Job监听器配置************/ // scheduler.addGlobalJobListener(myJobListener);//直接添加为全局监听器 TriggerListener myTriggerListener = new MyTriggerListener(); /**********局部Trigger监听器配置**********/ pickNewsTrigger.addTriggerListener("myTriggerListener"); scheduler.addTriggerListener(myTriggerListener); /*********全局Trigger监听器配置************/ // scheduler.addGlobalTriggerListener(myTriggerListener);//直接添加为全局监听器 scheduler.scheduleJob(pickNewsJob,pickNewsTrigger); scheduler.scheduleJob(getHottestJob,getHottestTrigger); scheduler.start(); } 运行程序,我们会看到: Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 发现此次Job的相关资源准备存在问题,不便展开任务,返回true表示否决此次任务执行——————我们的Trigger监听器要否决我们的任务,触发了相应的监听方法,同时后续的complete监听方法自然不会再被执行 被否决执行了,可以做些日志记录。——————我们的pickNewsJob被否决了,触发了相应的监听方法 在13:15:39根据文章的阅读量和评论量来生成我们的最热文章列表 Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 发现此次Job的相关资源准备存在问题,不便展开任务,返回true表示否决此次任务执行 被否决执行了,可以做些日志记录。 在13:15:42根据文章的阅读量和评论量来生成我们的最热文章列表 如果我们将TriggerListener中的vetoJobExecution()方法改成如下所示: @Override public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) { // System.out.println("发现此次Job的相关资源准备存在问题,不便展开任务,返回true表示否决此次任务执行"); // return true; System.out.println("不否决Job,正常执行"); return false; } 再运行我们的测试程序,会打印: Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 不否决Job,正常执行 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在13:20:20扒取新闻 在13:20:20根据文章的阅读量和评论量来生成我们的最热文章列表 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 Trigger 被触发并且完成了 Job 的执行,此方法被调用 Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 不否决Job,正常执行 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在13:20:22扒取新闻 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 Trigger 被触发并且完成了 Job 的执行,此方法被调用 在13:20:23根据文章的阅读量和评论量来生成我们的最热文章列表 我们的Job不被否决,同时有后续的Job成功执行的监听方法调用 2.x版本 我们可以调用如下所示测试代码: public static void main(String args[]) throws SchedulerException { final JobDetail pickNewsJob = JobBuilder.newJob(PickNewsJob.class) .withIdentity("job1", "jgroup1").build(); JobDetail getHottestJob = JobBuilder.newJob(GetHottestJob.class) .withIdentity("job2", "jgroup2").build(); SimpleTrigger pickNewsTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1","tgroup1") .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(2, 1)).startNow() .build(); SimpleTrigger getHottestTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger2","tgroup2") .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(2, 2)).startNow() .build(); Scheduler scheduler = new StdSchedulerFactory().getScheduler(); JobListener myJobListener = new MyJobListener(); KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(pickNewsJob.getKey()); scheduler.getListenerManager().addJobListener(myJobListener, keyMatcher); /********下面是新加部分***********/ TriggerListener myTriggerListener = new MyTriggerListener(); KeyMatcher<TriggerKey> tkeyMatcher = KeyMatcher.keyEquals(pickNewsTrigger.getKey()); scheduler.getListenerManager().addTriggerListener(myTriggerListener, tkeyMatcher); scheduler.scheduleJob(pickNewsJob, pickNewsTrigger); scheduler.scheduleJob(getHottestJob,getHottestTrigger); scheduler.start(); } 调用此方法,我们和得到和1.+版本中类似的结果: Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 发现此次Job的相关资源准备存在问题,不便展开任务,返回true表示否决此次任务执行 被否决执行了,可以做些日志记录。 根据文章的阅读量和评论量来生成我们的最热文章列表 Trigger 被触发了,此时Job 上的 execute() 方法将要被执行 发现此次Job的相关资源准备存在问题,不便展开任务,返回true表示否决此次任务执行 被否决执行了,可以做些日志记录。 根据文章的阅读量和评论量来生成我们的最热文章列表 源码下载 关于本节测试源码内容可到https://github.com/jeanhao/spring下quartzEvent文件夹下载
在《spring学习笔记(15)趣谈spring 事件:实现业务逻辑解耦,异步调用提升用户体验》我们通过实例分析讲解了spring的事件机制,或许你会觉得其中的配置略显繁琐,而在Quartz框架中,它为我们集成了强大的事件机制,轻松地帮助我们在任务调度中完成各类辅佐操作,高内聚而耦合。 相对spring的事件实现,quartz这边简化了许多,我们只需: 1. 自定义监听器接口实现类 2. 向scheduler中注册监听器实现类 只需以上两步即可我完成我们的事件监听。对于监听器实现类中,可能有些方法不是我们需要的,这时候我们只需给出空实现即可。在Quartz中,监听器类型主要分为三种,和Quartz三个核心类相对应:JobListener,TriggerListener,SchedulerListener。下面我们先分析JobListener的使用方法。其他两种监听器留待后面系列文章详解 JobListener 我们的jobListener实现类必须实现其以下方法: 方法 说明 getName() getName() 方法返回一个字符串用以说明 JobListener 的名称。对于注册为全局的监听器,getName() 主要用于记录日志,对于由特定 Job 引用的 JobListener,注册在 JobDetail 上的监听器名称必须匹配从监听器上 getName() 方法的返回值。 jobToBeExecuted() Scheduler 在 JobDetail 将要被执行时调用这个方法。 jobExecutionVetoed() Scheduler 在 JobDetail 即将被执行,但又被 TriggerListener 否决了时调用这个方法。 jobWasExecuted() Scheduler 在 JobDetail 被执行之后调用这个方法。 接下来我们以《Quartz任务调度(1)概念例析快速入门》一文中的定时扒取新闻任务和获得最热新闻任务为例,分析我们的监听器方法。 1. 自定义监听器接口实现类 public class MyJobListener implements JobListener { @Override//相当于为我们的监听器命名 public String getName() { return "myJobListener"; } @Override public void jobToBeExecuted(JobExecutionContext context) { System.out.println(getName() + "触发对"+context.getJobDetail().getJobClass()+"的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录"); } @Override//“否决JobDetail”是在Triiger被其相应的监听器监听时才具备的能力 public void jobExecutionVetoed(JobExecutionContext context) { System.out.println("被否决执行了,可以做些日志记录。"); } @Override public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { System.out.println(getName() + "触发对"+context.getJobDetail().getJobClass()+"结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作"); } } 2. 在scheduler中注册监听器 这里有两种方式,一种是注册为全局监听器,对所有的JobDetail都有效,另一种是注册为针对特定JobDetail的局部监听器。针对不同的版本,有不同的配置方式 1. 准备工作 在测试中我们用到工作实现类为 public class PickNewsJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在" + sdf.format(new Date()) + "扒取新闻"); } } public class GetHottestJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在" + sdf.format(new Date()) +"根据文章的阅读量和评论量来生成我们的最热文章列表"); } } 2. 1.x版本配置 在1.+版本中,我们可以通过如下代码监听job /**********局部监听器配置**********/ JobListener myJobListener = new MyJobListener(); pickNewsJob.addJobListener("myJobListener");//这里的名字和myJobListener中getName()方法的名字一样 scheduler.addJobListener(myJobListener);//向scheduler注册我们的监听器 /*********全局监听器配置************/ JobListener myJobListener = new MyJobListener(); scheduler.addGlobalJobListener(myJobListener);//直接添加为全局监听器 下面是我们的完整测试代码: public static void main(String args[]) throws SchedulerException { JobDetail pickNewsJob =new JobDetail("job1", "jgroup1", PickNewsJob.class); JobDetail getHottestJob =new JobDetail("job2", "jgroup2", GetHottestJob.class); SimpleTrigger pickNewsTrigger = new SimpleTrigger("trigger1", "group1",1,2000); SimpleTrigger getHottestTrigger = new SimpleTrigger("trigger2", "group2",1,3000); SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); /**********局部监听器配置**********/ JobListener myJobListener = new MyJobListener(); pickNewsJob.addJobListener("myJobListener");//这里的名字和myJobListener中getName()方法的名字一样 scheduler.addJobListener(myJobListener);//向scheduler注册我们的监听器 /*********全局监听器配置************/ // JobListener myJobListener = new MyJobListener(); // scheduler.addGlobalJobListener(myJobListener);//直接添加为全局监听器 scheduler.scheduleJob(pickNewsJob,pickNewsTrigger); scheduler.scheduleJob(getHottestJob,getHottestTrigger); scheduler.start(); } 现在是使用局部监听器的配置,运行程序,控制台打印: myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在11:18:31扒取新闻 在11:18:31根据文章的阅读量和评论量来生成我们的最热文章列表————————从这里我们可以看出两个工作是异步进行的 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在11:18:33扒取新闻 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 在11:18:34根据文章的阅读量和评论量来生成我们的最热文章列表 我们细心观察还会发现,我们两个工作都运行了三次,但我们在配置触发器时,repeatCount都是设为2。这说明我们的任务调度特点是:主执行了1次,重复了2次,于是共执行3(1+repeatCount)次。 如果我们注释掉局部监听代码,启用全局监听,会看到控制台打印: myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 myJobListener触发对class tool.job.GetHottestJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在11:25:41扒取新闻 在11:25:41根据文章的阅读量和评论量来生成我们的最热文章列表 myJobListener触发对class tool.job.GetHottestJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在11:25:43扒取新闻 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 myJobListener触发对class tool.job.GetHottestJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在11:25:44根据文章的阅读量和评论量来生成我们的最热文章列表 myJobListener触发对class tool.job.GetHottestJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 即我们的两个任务都被监听了 3. 2.x版本配置 在2.+版本中,引入了**org.quartz.ListenerManager和org.quartz.Matcher **来对我们的监听器进行更细粒度的管理配置 1. ListenerManager 我们通过ListenerManager向scheduler中添加我们的监听器。它针对JobDetail的常用方法有: 1. public void addJobListener(JobListener jobListener) 添加全局监听器,即所有JobDetail都会被此监听器监听 2. public void addJobListener(JobListener jobListener, Matcher matcher) 添加带条件匹配的监听器,在matcher中声明我们的匹配条件 3. public void addJobListener(JobListener jobListener, Matcher … matchers) 添加附带不定参条件陪陪的监听器 4. public boolean removeJobListener(String name) 根据名字移除JobListener 5. public List getJobListeners() 获取所有的监听器 6. public JobListener getJobListener(String name) 根据名字获取监听器 2. matcher 我们通过matcher让不同的监听器监听不同的任务。它有很多实现类,先逐一分析如下: 1. KeyMatcher<JobKey> 根据JobKey进行匹配,每个JobDetail都有一个对应的JobKey,里面存储了JobName和JobGroup来定位唯一的JobDetail。它的常用方法有: /************构造Matcher方法************/ KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(pickNewsJob.getKey());//构造匹配pickNewsJob中的JobKey的keyMatcher。 /*********使用方法************/ scheduler.getListenerManager().addJobListener(myJobListener, keyMatcher);//通过这句完成我们监听器对pickNewsJob的唯一监听 2. GroupMatcher 根据组名信息匹配,它的常用方法有: GroupMatcher<JobKey> groupMatcher = GroupMatcher.jobGroupContains("group1");//包含特定字符串 GroupMatcher.groupEndsWith("oup1");//以特定字符串结尾 GroupMatcher.groupEquals("jgroup1");//以特定字符串完全匹配 GroupMatcher.groupStartsWith("jgou");//以特定字符串开头 3. AndMatcher 对两个匹配器取交集,实例如下: KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(pickNewsJob.getKey()); GroupMatcher<JobKey> groupMatcher = GroupMatcher.jobGroupContains("group1"); AndMatcher<JobKey> andMatcher = AndMatcher.and(keyMatcher,groupMatcher);//同时满足两个入参匹配 4. OrMatcher 对两个匹配器取并集,实例如下: OrMatcher<JobKey> orMatcher = OrMatcher.or(keyMatcher, groupMatcher);//满足任意一个即可 5. EverythingMatcher 局部全局匹配,它有两个构造方法: EverythingMatcher.allJobs();//对全部JobListener匹配 EverythingMatcher.allTriggers();//对全部TriggerListener匹配 下面是我们的完整测试测序: public static void main(String args[]) throws SchedulerException { final JobDetail pickNewsJob = JobBuilder.newJob(PickNewsJob.class) .withIdentity("job1", "jgroup1").build(); JobDetail getHottestJob = JobBuilder.newJob(GetHottestJob.class) .withIdentity("job2", "jgroup2").build(); SimpleTrigger pickNewsTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1","tgroup1") .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(2, 1)).startNow() .build(); SimpleTrigger getHottestTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger2","tgroup2") .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(2, 2)).startNow() .build(); Scheduler scheduler = new StdSchedulerFactory().getScheduler(); JobListener myJobListener = new MyJobListener(); KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(pickNewsJob.getKey()); scheduler.getListenerManager().addJobListener(myJobListener, keyMatcher); scheduler.scheduleJob(pickNewsJob, pickNewsTrigger); scheduler.scheduleJob(getHottestJob,getHottestTrigger); scheduler.start(); } 运行程序,我们得到下列打印信息: myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 根据文章的阅读量和评论量来生成我们的最热文章列表 在12:48:58扒取新闻 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 myJobListener触发对class tool.job.PickNewsJob的开始执行的监听工作,这里可以完成任务前的一些资源准备工作或日志记录 在12:48:59扒取新闻 myJobListener触发对class tool.job.PickNewsJob结束执行的监听工作,这里可以进行资源销毁工作或做一些新闻扒取结果的统计工作 根据文章的阅读量和评论量来生成我们的最热文章列表 显然,myJobListener只和我们的PickNewsJob匹配了。 关于测试代码的其他配置可移步参考本系列前面的文章,里面都有详细的配置实例讲解 源码下载 关于本节测试源码内容可到https://github.com/jeanhao/spring下quartzEvent文件夹下载
Cron表达式 1. 时间字段与基本格式 Cron表达式有6或7个空格分割的时间字段组成: 位置 时间域名 允许值 允许的特殊字符 1 秒 0-59 ,-*/ 2 分支 0-59 ,-*?/ 3 小时 0-23 ,-*/ 4 日期 1-31 ,-*/LWC 5 月份 1-12或 JAN-DEC ,-*/ 6 星期 1-7 或 SUN-SAT ,-*?/LC# 7 年(可选) 1970-2099 ,-*/ 在月份和星期中,我们也可以使用英文单词的缩写形式 2. 特殊字符 在Cron表达式的时间字段中,除允许设置数值外,还能你使用一些特殊的字符,提供列表、范围、通配符等功能 1. 星号(*) 可用在所有字段下,表示对应时间域名的每一个时刻,如*用在分钟字段,表示“每分钟”。 2. 问号(?) 只能用在日期和星期字段,代表无意义的值,比如使用L设定为当月的最后一天,则配置日期配置就没有意义了,可用?作占位符的作用。 3. 减号(-) 表示一个范围,如在日期字段5-10,表示从五号到10号,相当于使用逗号的5,6,7,8,9,10 4. 逗号(,) 表示一个并列有效值,比如在月份字段使用JAN,DEC表示1月和12月 5. 斜杠(/) x/y表示一个等步长序列,x为起始值,y为增量步长值,如在小时使用1/3相当于1,4,7,10当时用*/y时,相当于0/y 6. L L(Last)只能在日期和星期字段使用,但意思不同。在日期字段,表示当月最后一天,在星期字段,表示星期六(如果按星期天为一星期的第一天的概念,星期六就是最后一天。如果L在星期字段,且前面有一个整数值X,表示“这个月的最后一个星期X”,比如3L表示某个月的最后一个星期二 7. W 选择离给定日期最近的工作日(周一至周五)。例如你指定“15W”作为day of month字段的值,就意味着“每个月与15号最近的工作日”。所以,如果15号是周六,则触发器会在14号(周五)触发。如果15号是周日,则触发器会在16号(周一)触发。如果15号是周二,则触发器会在15号(周二)触发。但是,如果你指定“1W”作为day of month字段的值,且1号是周六,则触发器会在3号(周一)触发。quartz不会“跳出”月份的界限。 8. LW组合 在日期字段可以组合使用LW,表示当月最后一个工作日(周一至周五) 9. 井号(#) 只能在星期字段中使用指定每月第几个星期X。例如day of week字段的“6#3”,就意味着“每月第3个星期五”(day3=星期五,#3=第三个);“2#1”就意味着“每月第1个星期一”;“4#5”就意味着“每月第5个星期3。需要注意的是“#5”,如果在当月没有第5个星期三,则触发器不会触发。 10. C 只能在日期和星期字段中使用,表示计划所关联的诶其,如果日期没有被关联,相当于日历中的所有日期,如5C在日期字段相当于5号之后的第一天,1C在日期字段使用相当于星期填后的第一天 3. 一些实例 Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。如MON和mon是一样的 cron表达式 含义 0 0 12 * * ? 每天12点整触发一次 0 15 10 ? * * 每天10点15分触发一次 0 15 10 * * ? 每天10点15分触发一次 0 15 10 * * ? * 每天10点15分触发一次 0 15 10 * * ? 2005 2005年内每天10点15分触发一次 0 * 14 * * ? 每天的2点整至2点59分,每分钟触发一次 0 0/5 14 * * ? 每天的2点整至2点55分,每5分钟触发一次 0 0/5 14,18 * * ? 每天的2点整至2点55分以及18点整至18点55分,每5分钟触发一次 0 0-5 14 * * ? 每天的2点整至2点5分,每分钟触发一次 0 10,44 14 ? 3 WED 每年3月的每个星期三的2点10分以及2点44分触发一次 0 15 10 ? * MON-FRI 每月周一、周二、周三、周四、周五的10点15分触发一次 0 15 10 15 * ? 每月15的10点15分触发一次 0 15 10 L * ? 每月最后一天的10点15分触发一次 0 15 10 ? * 6L 每月最后一个周五的10点15分触发一次 0 15 10 ? * 6L 每月最后一个周五的10点15分触发一次 0 15 10 ? * 6L 2002-2005 2002年至2005年间,每月最后一个周五的10点15分触发一次 0 15 10 ? * 6#3 每月第三个周五的10点15触发一次 0 0 12 1/5 * ? 每月1号开始,每5天的12点整触发一次 0 11 11 11 11 ? 每年11月11日11点11分触发一次 使用示例 在quartz1.+版本中,我们通过如下方法创建CronTrigger //定义调度触发规则,每天上午10:15执行 CronTrigger cornTrigger=new CronTrigger("cronTrigger","triggerGroup"); //执行规则表达式 cornTrigger.setCronExpression("0 15 10 * * ? *"); 而在2.+版本中,则通过如下方式创建 //使用cornTrigger规则 每天10点42分 Trigger trigger=TriggerBuilder.newTrigger().withIdentity("simpleTrigger", "triggerGroup") .withSchedule(CronScheduleBuilder.cronSchedule("0 42 10 * * ? *")) .startNow().build(); 参考:http://blog.csdn.net/yuan8080/article/details/6583603
内存存储RAMJobStore Quartz默认使用RAMJobStore,它的优点是速度。因为所有的 Scheduler 信息都保存在计算机内存中,访问这些数据随着电脑而变快。而无须访问数据库或IO等操作,但它的缺点是将 Job 和 Trigger 信息存储在内存中的。因而我们每次重启程序,Scheduler 的状态,包括 Job 和 Trigger 信息都丢失了。 Quartz 的内存 Job 存储的能力是由一个叫做 org.quartz.simple.RAMJobStore 类提供。在我们的quartz-2.x.x.jar包下的org.quartz包下即存储了我们的默认配置quartz.properties。打开这个配置文件,我们会看到如下信息 # Default Properties file for use by StdSchedulerFactory # to create a Quartz Scheduler Instance, if a different # properties file is not explicitly specified. # org.quartz.scheduler.instanceName: DefaultQuartzScheduler org.quartz.scheduler.rmi.export: false org.quartz.scheduler.rmi.proxy: false org.quartz.scheduler.wrapJobExecutionInUserTransaction: false org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount: 10 org.quartz.threadPool.threadPriority: 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true org.quartz.jobStore.misfireThreshold: 60000 org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore #这里默认使用RAMJobStore 持久性JobStore Quartz 提供了两种类型的持久性 JobStore,为JobStoreTX和JobStoreCMT,其中: 1. JobStoreTX为独立环境中的持久性存储,它设计为用于独立环境中。这里的 “独立”,我们是指这样一个环境,在其中不存在与应用容器的事物集成。这里并不意味着你不能在一个容器中使用 JobStoreTX,只不过,它不是设计来让它的事特受容器管理。区别就在于 Quartz 的事物是否要参与到容器的事物中去。 2. JobStoreCMT 为程序容器中的持久性存储,它设计为当你想要程序容器来为你的 JobStore 管理事物时,并且那些事物要参与到容器管理的事物边界时使用。它的名字明显是来源于容器管理的事物(Container Managed Transactions (CMT))。 持久化配置步骤 要将JobDetail等信息持久化我们的数据库中,我们可按一下步骤操作: 1. 配置数据库 在 /docs/dbTables 目录下存放了几乎所有数据库的的SQL脚本,这里的 是解压 Quartz 分发包后的目录。我们使用常用mysql数据库,下面是示例sql脚本代码 # # Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar # # PLEASE consider using mysql with innodb tables to avoid locking issues # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; DROP TABLE IF EXISTS QRTZ_LOCKS; DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; DROP TABLE IF EXISTS QRTZ_TRIGGERS; DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; DROP TABLE IF EXISTS QRTZ_CALENDARS; CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(80) NOT NULL, JOB_NAME VARCHAR(100) NOT NULL, JOB_GROUP VARCHAR(100) NOT NULL, DESCRIPTION VARCHAR(100) NULL, JOB_CLASS_NAME VARCHAR(100) NOT NULL, IS_DURABLE VARCHAR(1) NOT NULL, IS_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(80) NOT NULL, TRIGGER_NAME VARCHAR(100) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, JOB_NAME VARCHAR(100) NOT NULL, JOB_GROUP VARCHAR(100) NOT NULL, DESCRIPTION VARCHAR(100) NULL, NEXT_FIRE_TIME BIGINT(13) NULL, PREV_FIRE_TIME BIGINT(13) NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT(13) NOT NULL, END_TIME BIGINT(13) NULL, CALENDAR_NAME VARCHAR(100) NULL, MISFIRE_INSTR SMALLINT(2) NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(80) NOT NULL, TRIGGER_NAME VARCHAR(100) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, REPEAT_COUNT BIGINT(7) NOT NULL, REPEAT_INTERVAL BIGINT(12) NOT NULL, TIMES_TRIGGERED BIGINT(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(80) NOT NULL, TRIGGER_NAME VARCHAR(100) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, CRON_EXPRESSION VARCHAR(100) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(80) NOT NULL, TRIGGER_NAME VARCHAR(100) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, STR_PROP_1 VARCHAR(120) NULL, STR_PROP_2 VARCHAR(120) NULL, STR_PROP_3 VARCHAR(120) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(80) NOT NULL, TRIGGER_NAME VARCHAR(100) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, BLOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(80) NOT NULL, CALENDAR_NAME VARCHAR(100) NOT NULL, CALENDAR BLOB NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(80) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(80) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(100) NOT NULL, TRIGGER_GROUP VARCHAR(100) NOT NULL, INSTANCE_NAME VARCHAR(100) NOT NULL, FIRED_TIME BIGINT(13) NOT NULL, SCHED_TIME BIGINT(13) NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(100) NULL, JOB_GROUP VARCHAR(100) NULL, IS_NONCONCURRENT VARCHAR(1) NULL, REQUESTS_RECOVERY VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(80) NOT NULL, INSTANCE_NAME VARCHAR(100) NOT NULL, LAST_CHECKIN_TIME BIGINT(13) NOT NULL, CHECKIN_INTERVAL BIGINT(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(80) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); commit; 其中各表的含义如下所示: 表名 描述 QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息 QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括 Cron 表达式和时区信息 QRTZ_FIRED_TRIGGERS 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的 Trigger 组的信息 QRTZ_SCHEDULER_STATE 存储少量的有关 Scheduler 的状态信息,和别的 Scheduler 实例(假如是用于一个集群中) QRTZ_LOCKS 存储程序的非观锁的信息(假如使用了悲观锁) QRTZ_JOB_DETAILS 存储每一个已配置的 Job 的详细信息 QRTZ_JOB_LISTENERS 存储有关已配置的 JobListener 的信息 QRTZ_SIMPLE_TRIGGERS 存储简单的 Trigger,包括重复次数,间隔,以及已触的次数 QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候) QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener 的信息 QRTZ_TRIGGERS 存储已配置的 Trigger 的信息 2. 使用JobStoreTX 首先,我们需要在我们的属性文件中表明使用JobStoreTX: org.quartz.jobStore.class = org.quartz.ompl.jdbcjobstore.JobStoreTX 然后我们需要配置能理解不同数据库系统中某一特定方言的驱动代理: 数据库平台 Quartz 代理类 Cloudscape/Derby org.quartz.impl.jdbcjobstore.CloudscapeDelegate DB2 (version 6.x) org.quartz.impl.jdbcjobstore.DB2v6Delegate DB2 (version 7.x) org.quartz.impl.jdbcjobstore.DB2v7Delegate DB2 (version 8.x) org.quartz.impl.jdbcjobstore.DB2v8Delegate HSQLDB org.quartz.impl.jdbcjobstore.PostgreSQLDelegate MS SQL Server org.quartz.impl.jdbcjobstore.MSSQLDelegate Pointbase org.quartz.impl.jdbcjobstore.PointbaseDelegate PostgreSQL org.quartz.impl.jdbcjobstore.PostgreSQLDelegate (WebLogic JDBC Driver) org.quartz.impl.jdbcjobstore.WebLogicDelegate (WebLogic 8.1 with Oracle) org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate Oracle org.quartz.impl.jdbcjobstore.oracle.OracleDelegate 如果我们的数据库平台没在上面列出,那么最好的选择就是,直接使用标准的 JDBC 代理 org.quartz.impl.jdbcjobstore.StdDriverDelegate 就能正常的工作。 以下是一些相关常用的配置属性及其说明: 属性 默认值 描述 org.quartz.jobStore.dataSource 无 用于 quartz.properties 中数据源的名称 org.quartz.jobStore.tablePrefix QRTZ_ 指定用于 Scheduler 的一套数据库表名的前缀。假如有不同的前缀,Scheduler 就能在同一数据库中使用不同的表。 org.quartz.jobStore.userProperties False “use properties” 标记指示着持久性 JobStore 所有在 JobDataMap 中的值都是字符串,因此能以 名-值 对的形式存储,而不用让更复杂的对象以序列化的形式存入 BLOB 列中。这样会更方便,因为让你避免了发生于序列化你的非字符串的类到 BLOB 时的有关类版本的问题。 org.quartz.jobStore.misfireThreshold 60000 在 Trigger 被认为是错过触发之前,Scheduler 还容许 Trigger 通过它的下次触发时间的毫秒数。默认值(假如你未在配置中存在这一属性条目) 是 60000(60 秒)。这个不仅限于 JDBC-JobStore;它也可作为 RAMJobStore 的参数 org.quartz.jobStore.isClustered False 设置为 true 打开集群特性。如果你有多个 Quartz 实例在用同一套数据库时,这个属性就必须设置为 true。 org.quartz.jobStore.clusterCheckinInterval 15000 设置一个频度(毫秒),用于实例报告给集群中的其他实例。这会影响到侦测失败实例的敏捷度。它只用于设置了 isClustered 为 true 的时候。 org.quartz.jobStore.maxMisfiresToHandleAtATime 20 这是 JobStore 能处理的错过触发的 Trigger 的最大数量。处理太多(超过两打) 很快会导致数据库表被锁定够长的时间,这样就妨碍了触发别的(还未错过触发) trigger 执行的性能。 org.quartz.jobStore.dontSetAutoCommitFalse False 设置这个参数为 true 会告诉 Quartz 从数据源获取的连接后不要调用它的 setAutoCommit(false) 方法。这在少些情况下是有帮助的,比如假如你有这样一个驱动,它会抱怨本来就是关闭的又来调用这个方法。这个属性默认值是 false,因为大多数的驱动都要求调用 setAutoCommit(false)。 org.quartz.jobStore.selectWithLockSQL SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE 这必须是一个从 LOCKS 表查询一行并对这行记录加锁的 SQL 语句。假如未设置,默认值就是 SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE,这能在大部分数据库上工作。{0} 会在运行期间被前面你配置的 TABLE_PREFIX 所替换。 org.quartz.jobStore.txIsolationLevelSerializable False 值为 true 时告知 Quartz(当使用 JobStoreTX 或 CMT) 调用 JDBC 连接的 setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法。这有助于阻止某些数据库在高负载和长时间事物时锁的超时。 4. 我们还需要配置Datasource 属性 属性 必须 说明 org.quartz.dataSource.NAME.driver 是 JDBC 驱动类的全限名 org.quartz.dataSource.NAME.URL 是 连接到你的数据库的 URL(主机,端口等) org.quartz.dataSource.NAME.user 否 用于连接你的数据库的用户名 org.quartz.dataSource.NAME.password 否 用于连接你的数据库的密码 org.quartz.dataSource.NAME.maxConnections 否 DataSource 在连接接中创建的最大连接数 org.quartz.dataSource.NAME.validationQuary 否 一个可选的 SQL 查询字串,DataSource 用它来侦测并替换失败/断开的连接。例如,Oracle 用户可选用 select table_name from user_tables,这个查询应当永远不会失败,除非直的就是连接不上了。 下面是我们的一个quartz.properties属性文件配置实例: org.quartz.scheduler.instanceName = MyScheduler org.quartz.threadPool.threadCount = 3 org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.tablePrefix = QRTZ_ org.quartz.jobStore.dataSource = myDS org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8 org.quartz.dataSource.myDS.user = root org.quartz.dataSource.myDS.password = root org.quartz.dataSource.myDS.maxConnections =5 配置好quartz.properties属性文件后,我们只要**将它放在类路径下,然后运行我们的程序,即可覆盖在quartz.jar包中默认的配置文件 3. 测试 编写我们的测试文件,我们的测试环境是在quartz-2.2.2版本下进行的。下面的测试用例引用了上篇文章 ,关于Quartz的快速入门配置可移步参考这篇文章。 public class pickNewsJob implements Job { @Override public void execute(JobExecutionContext jec) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println("在"+sdf.format(new Date())+"扒取新闻"); } public static void main(String args[]) throws SchedulerException { JobDetail jobDetail = JobBuilder.newJob(pickNewsJob.class) .withIdentity("job1", "jgroup1").build(); SimpleTrigger simpleTrigger = TriggerBuilder .newTrigger() .withIdentity("trigger1") .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2)) .startNow() .build(); //创建scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.scheduleJob(jobDetail, simpleTrigger); scheduler.start(); } } 执行测试方法,能看到控制台打印如下日志信息,关注红色部分,更注意其中的粗体部分,是我们quartz调用数据库的一些信息: INFO : org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl INFO : org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.2.2 created. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Using thread monitor-based data access locking (synchronization). INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - JobStoreTX initialized. INFO : org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.2) ‘MyScheduler’ with instanceId ‘NON_CLUSTERED’ Scheduler class: ‘org.quartz.core.QuartzScheduler’ - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool ‘org.quartz.simpl.SimpleThreadPool’ - with 3 threads. Using job-store ‘org.quartz.impl.jdbcjobstore.JobStoreTX’ - which supports persistence. and is not clustered. INFO : org.quartz.impl.StdSchedulerFactory - Quartz scheduler ‘MyScheduler’ initialized from default resource file in Quartz package: ‘quartz.properties’ INFO : org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.2.2 INFO : com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource - Initializing c3p0 pool… com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> z8kfsx9f1dp34iubvoy4d|7662953a, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> z8kfsx9f1dp34iubvoy4d|7662953a, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8, lastAcquisitionFailureDefaultUser -> null, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 5, maxStatements -> 0, maxStatementsPerConnection -> 120, minPoolSize -> 1, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ] INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Freed 0 triggers from ‘acquired’ / ‘blocked’ state. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Recovering 0 jobs that were in-progress at the time of the last shut-down.这里代表在我们任务开始时,先从数据库查询旧记录,这些旧记录是之前由于程序中断等原因未能正常执行的,于是先Recovery回来并执行 INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Recovery complete. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Removed 0 ‘complete’ triggers. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Removed 0 stale fired job entries. INFO : org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED started. 在21:28:12扒取新闻 在21:28:13扒取新闻 在21:28:15扒取新闻 在21:28:17扒取新闻 …. 4. 拓展测试 我们再次运行测试方法,然后马上中断程序,查询我们数据库,会看到如下内容: SELECT * FROM QRTZ_SIMPLE_TRIGGERS; +————-+————–+—————+————–+—————–+—————–+ | SCHED_NAME | TRIGGER_NAME | TRIGGER_GROUP | REPEAT_COUNT | REPEAT_INTERVAL | TIMES_TRIGGERED | +————-+————–+—————+————–+—————–+—————–+ | MyScheduler | trigger1 | DEFAULT | 9 | 2000 | 1 | +————-+————–+—————+————–+—————–+—————–+ 1 row in set (0.00 sec) 然后我们再运行程序,发现报错了。 org.quartz.ObjectAlreadyExistsException: Unable to store Job : ‘jgroup1.job1’, because one already exists with this identification. 一般的,在我们的任务调度前,会先将相关的任务持久化到数据库中,然后调用完在删除记录,这里在程序开始试图将任务信息持久化到数据库时,显然和(因为我们之前中断操作导致)数据库中存在的记录起了冲突。 5. 恢复异常中断的任务 这个时候,我们可以选择修改我们的job名和组名和triiger名,然后再运行我们的程序。查看控制台打印的信息部分展示如下: INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Freed 1 triggers from ‘acquired’ / ‘blocked’ state. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Handling 1 trigger(s) that missed their scheduled fire-time.这里我们开始处理上一次异常未完成的存储在数据库中的任务记录 INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Recovering 0 jobs that were in-progress at the time of the last shut-down. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Recovery complete. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Removed 0 ‘complete’ triggers. INFO : org.quartz.impl.jdbcjobstore.JobStoreTX - Removed 1 stale fired job entries. INFO : org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED started. 在21:42:13扒取新闻 在21:42:13扒取新闻 在21:42:14扒取新闻 在21:42:15扒取新闻 在21:42:16扒取新闻 在21:42:17扒取新闻 在21:42:18扒取新闻 在21:42:19扒取新闻 在21:42:20扒取新闻 在21:42:21扒取新闻 在21:42:22扒取新闻 在21:42:23扒取新闻 在21:42:24扒取新闻 在21:42:25扒取新闻 在21:42:26扒取新闻 在21:42:27扒取新闻 在21:42:28扒取新闻 在21:42:29扒取新闻 在21:42:30扒取新闻 我们会发现,“扒取新闻”一句的信息打印次数超过十次,但我们在任务调度中设置了打印十次,说明它恢复了上次的任务调度。 而如果我们不想执行新的任务,只想纯粹地恢复之前异常中断任务,我们可以采用如下方法: SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // ①获取调度器中所有的触发器组 List<String> triggerGroups = scheduler.getTriggerGroupNames(); // ②重新恢复在tgroup1组中,名为trigger1触发器的运行 for (int i = 0; i < triggerGroups.size(); i++) {//这里使用了两次遍历,针对每一组触发器里的每一个触发器名,和每一个触发组名进行逐次匹配 List<String> triggers = scheduler.getTriggerGroupNames(); for (int j = 0; j < triggers.size(); j++) { Trigger tg = scheduler.getTrigger(new TriggerKey(triggers .get(j), triggerGroups.get(i))); // ②-1:根据名称判断 if (tg instanceof SimpleTrigger && tg.getDescription().equals("jgroup1.DEFAULT")) {//由于我们之前测试没有设置触发器所在组,所以默认为DEFAULT // ②-1:恢复运行 scheduler.resumeJob(new JobKey(triggers.get(j), triggerGroups.get(i))); } } } scheduler.start(); } 调用此方法,我们在数据库中异常中断任务记录就会被读取执行,然后被删除掉。
在上一篇文章中,我们使用了声明式事务来配置事务,使事务配置从service逻辑处理中解耦出来。但它还存在一些缺点: 1. 我们只针对方法名的特定进行拦截,但无法利用方法签名的其它信息定位,如修饰符、返回值、方法入参、异常类型等。如果我们需要为同名不同参的同载方法配置不同事务就会出问题了。 2. 事务属性的配置串虽然能包含较多信息,但配置较易出错。 针对这些问题,我们可以基于Schema,引入tx和aop的命名空间来改进我们的配置: 引入命名空间 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd"> tx\aop核心配置 <!-- 配置事务属性 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRES_NEW" /> <tx:method name="update*" propagation="REQUIRES_NEW" /> <tx:method name="delete*" propagation="REQUIRES_NEW" /> <tx:method name="*" read-only="true"/> </tx:attributes> </tx:advice> <!-- 配置事务切入点,以及把事务切入点和事务属性关联起来 --> <aop:config proxy-target-class="true"> <aop:pointcut expression="execution(* com.yc.service.*.*(..))" id="ServicePointcut" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="ServicePointcut" /> </aop:config> 这里需要特别注意的是,我们需要在标签中将proxy-target-class配置成true,否则会出现和上一篇文章相同的错误:我们定义的类无法转换成代理类 这里我们通过来配置我们的事务增强属性。在标签中,常见属性及其说明如下,其中,除了name属性是必选外,其他都是可选的: 属性 说明 默认 允许值 name 匹配方法名 必须声明,至少为* 可使用*通配符 propagation 事务传播行为 REQUIRED REQUIRED,SUPPORTS和MANDATORY和REQUIRES_NEW和NOT_SUPPORTED和NEVER和NESTED read-only 设置当前事务是否只读 false true,false isolation 事务隔离级别 DEFAULT READ_UNCOMMITTED和READ_COMMITTED和REPEATABLE_READ和SERIALIZABLE timeout 设置事务的超时时间 -1 默认由顶层事务系统决定 rollback-for 内容为异常名,表示当抛出这些异常时事务回滚,可以用逗号分隔配置多个 无默认值 可以使用异常名称的片段进行匹配如ception等 no-rollback-for 内容为异常名,表示当抛出这些异常时继续提交事务,可以用逗号分隔配置多个 无默认值 可以使用异常名称的片段进行匹配如ception等。 在这里运行和上一篇文章同样的测试程序,我们会得到相同的结果
在上一节内容中,我们使用了编程式方法来配置事务,这样的优点是我们对每个方法的控制性很强,比如我需要用到什么事务,在什么位置如果出现异常需要回滚等,可以进行非常细粒度的配置。但在实际开发中,我们可能并不需要这样细粒度的配置。另一方面,如果我们的项目很大,service层方法很多,单独为每个方法配置事务也是一件很繁琐的事情。而且也可能会造成大量重复代码的冗杂堆积。面对这些缺点,我们首要想到的就是我们spring中的AOP了。spring声明式事务的实现恰建立在AOP之上。 在这一篇文章中,我们介绍spring的声明式事务配置。 实例分析 声明式事务配置原理相当于使用了环绕增强,拦截目标方法,在其调用前织入我们的事务,然后在调用结束根据执行情况提交或回滚事务。通过横切的逻辑,能够让我们的service层更专注于自身业务逻辑的处理而免去繁琐的事务配置。 配置声明式事务的核心在于配置我们的TransactionProxyFactoryBean和BeanNameAutoProxyCreator。先看下面一个实例配置 事务核心类配置 <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="transactionManager" /><!-- 指定一个事务管理器--> <property name="transactionAttributes"><!-- 配置事务属性 `--> <props> <prop key="add*" >PROPAGATION_REQUIRED,-Exception</prop> <prop key="update*">PROPAGATION_REQUIRED,+Exception</prop> <prop key="delete*">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames"><!-- 配置需要代理的Bean--> <list> <value>myBaseServiceImpl</value> </list> </property> <property name="interceptorNames"><!-- 声明拦截器--> <list> <value>transactionInterceptor</value> </list> </property> </bean> <!-- 测试用到的相关依赖--> <bean id="myBaseDao" class="com.yc.dao.MyBaseDaoImpl"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <bean id="myBaseServiceImpl" class="com.yc.service.MyBaseServiceImpl"> <property name="myBaseDao" ref="myBaseDao" /> </bean> 属性详细分析 在实例中我们通过配置拦截器和代理生成器。在配置TransactionInterceptor事务属性时,key对应于方法名,我们以add*来匹配目标类中所有以add开头的方法,在针对目标对象类的方法进行拦截配置事务时,我们根据属性的定义顺序拦截,如果它被key="add*"所在事务属性拦截,即使后面有key="*"可以匹配任意方法,也不会再次被拦截。关于标签内的事务属性格式如下: 传播行为 [,隔离级别] [,只读属性] [,超时属性] [,-Exception] [,+Exception] 其中除了传播行为外,其他都是可选的。每个属性说明可见下表 属性 说明 传播行为 取值必须以“PROPAGATION_”开头,具体包括:PROPAGATION_MANDATORY、PROPAGATION_NESTED、PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED、PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_SUPPORTS,共七种取值。 隔离级别 取值必须以“ISOLATION_”开头,具体包括:ISOLATION_DEFAULT、ISOLATION_READ_COMMITTED、ISOLATION_READ_UNCOMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE,共五种取值。 只读属性 如果事务是只读的,那么我们可以指定只读属性,使用“readOnly”指定。否则我们不需要设置该属性。 超时属性 取值必须以“TIMEOUT_”开头,后面跟一个int类型的值,表示超时时间,单位是秒。 +Exception 即使事务中抛出了这些类型的异常,事务仍然正常提交。必须在每一个异常的名字前面加上“+”。异常的名字可以是类名的一部分。比如“+RuntimeException”、“+tion”等等。可同时指定多个,如+Exception1,+Exception2 -Exception 当事务中抛出这些类型的异常时,事务将回滚。必须在每一个异常的名字前面加上“-”。异常的名字可以是类名的全部或者部分,比如“-RuntimeException”、“-tion”等等。可同时指定多个,如-Exception1,-Exception2 从配置文件中可以看出,我们可以配置多个拦截器和多个Bean来适配不同的事务。这种声明式事务使用起来还是很方便的。 service层配置 使用声明式事务后,相对于上篇文章例子,我们的service层需改写成: public class MyBaseServiceImpl implements MyBaseService{ private MyBaseDao myBaseDao; @Override public void queryUpdateUser(final Integer id,final String newName) { User user = myBaseDao.queryUnique(User.class, id); System.out.println(user); user.setName(newName); myBaseDao.update(user); System.out.println(user); } public void setMyBaseDao(MyBaseDao myBaseDao) { this.myBaseDao = myBaseDao; } } 可见,我们去除了事务模板的侵入式注入,同时还去除了事务(在每一个方法中的)侵入式配置。当然,编程式事务的好处是能将事务配置细粒度到每个方法当中。。当我们大部分方法的事务还是一致的,我们可以使用声明式事务,针对那些需要独立配置的,我们可以将其排除出声明式事务,然后使用编程式事务或后面我们会提到的注解式事务单独配置。 测试结果和分析 下面,运行我们相同的测试方法: public class Test1 { @Test public void test(){ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-datasource.xml"); MyBaseServiceImpl myBaseService= (MyBaseServiceImpl) ac.getBean("myBaseServiceImpl"); myBaseService.queryUpdateUser(1, "newName2"); } } 运行测试方法,会发现报错: java.lang.ClassCastException: com.sun.proxy.$Proxy8 cannot be cast to com.yc.service.MyBaseServiceImpl 意思是我们的代理类无法转换成我们自定义的Service实现类。究其原因,是因为我们的BeanNameAutoProxyCreator没有默认使用CGLib代理,这样我们的代理类是利用JDK动态代理基于接口创建的,而非基于类创建,我们有以下两种解决方法: 1. 将代理类转换成MyBaseServiceImpl所实现的接口MyBaseService而非MyBaseServiceImpl: MyBaseService myBaseService= (MyBaseService) ac.getBean("myBaseServiceImpl"); 2. 在BeanNameAutoProxyCreator配置下添加: <property name="proxyTargetClass" value="true"/>,即 <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="proxyTargetClass" value="true"/> <property name="beanNames"> <list> <value>myBaseServiceImpl</value> </list> </property> <property name="interceptorNames"> <list> <value>transactionInterceptor</value> </list> </property> </bean> 然后,再运行测试程序,我们会得到正确的结果,部分打印信息如下所示: DEBUG: org.hibernate.engine.jdbc.internal.LogicalConnectionImpl - Obtaining JDBC connection DEBUG: org.hibernate.engine.jdbc.internal.LogicalConnectionImpl - Obtained JDBC connection DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - begin DEBUG: org.hibernate.loader.Loader - Done entity load User [id=1, name=newName] User [id=1, name=newName2] DEBUG: org.springframework.orm.hibernate4.HibernateTransactionManager - Initiating transaction commit DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - committing 这和我们使用编程式事务的结果基本是一致的。 拓展测试 现在,在我们的拦截器中稍微修改一行: <prop key="*">PROPAGATION_REQUIRED,readOnly</prop> 我们将其设置为只读模式,这时候,调用我们的测试方法,queryUpdateUser(1,”newName3”)(因为前面测试已将name修改成newName2,为了显示不同的结果,这里射程newName3做参数)。显然,前面的add*,update*,delete*都不能匹配。这时候必定启动key="*"所属事务。运行方法,我们会发现结果: User [id=1, name=newName2] User [id=1, name=newName3] 这似乎和我们没设置readOnly应有的结果一致,但我们再次运行,程序没有抛出异常,而且会发现结果仍是: User [id=1, name=newName2] User [id=1, name=newName3] 说明我们的修改实际上并没有生效!这时在看DEBUG信息,发现在: DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - begin信息上面多了一行: DEBUG: org.springframework.jdbc.datasource.DataSourceUtils - Setting JDBC Connection [jdbc:mysql://localhost:3306/yc, UserName=yc@localhost, MySQL Connector Java] read-only 说明当前事务确实为只读模式 归纳 这里单独拿出readOnly来分析,主要是针对实际开发中可能遇到的麻烦。设想我们哪天只读属性配置错了。但我们没发现,而当我们试图进行相应的写数据操作时,发现程序并没有出现异常,但数据无论怎么都写不进去。这个时候就要好好看看我们的只读属性有没有跑到它不该到的地方去了!
访问数据库事务导入 在我之前的文章《spring学习笔记(19)mysql读写分离后端AOP控制实例》中模拟数据库读写分离的例子,在访问数据库时使用的方法是: public <E> E add(Object object) { return (E) getSessionFactory().openSession().save(object); } 通过直接开启session而后保存对象、查询数据等操作,是没有事务的。而如果我们的项目规模变大,业务逻辑日益复杂,我们在一个方法中进行大量的数据库操作,而没有事务管理的话,一旦中间哪一个操作环节出错,后果是严重的。比如,一个用户通过支付宝转100块到银行账户,于是用户的100块先转到了银行,但这时数据库异常中断,银行无法把100块转给用户账户,这时事务又没有回滚,那么可能用户的100块就白白损失掉了。 在spring中,有多种方式可以进行我们的事务配置,比如我们可以直接修改上面的方法,加上事务: public <E> E add(Object object) { Session session = getSessionFactory().openSession(); Transaction tx = session.beginTransaction(); E id = (E) session.save(object); tx.commit(); return id; } 这样,我们就能为我们的add方法加上简单的事务了。 多数据库操作事务配置——引入Service层 但这样的话我们针对的只是dao层add这个方法,但在实际中,我们可能需要同时控制大量DAO的方法在同一个事务中,为此,我们可以创建service层来统一进行我们的业务逻辑处理。比如我们根据需求,需要先获取用户id,并修改用户名称,这里设计两个数据库操作,但我们在同一个service类方法中完成。 编程式事务模板类:TransactionTemplate 概念 在下例中,我们依然使用编程式事务,spring为此专门提供了模板类TransactionTemplate来满足我们的需求。TransactionTemplate是线程安全的,也即是说,我们可以在多个业务类中共享同一个TransactionTemplate实例进行事务管理。 常用属性 TransactionTemplate有很多常用的属性如: 1. isolationLevel:设置事务隔离级别 2. propagationBehavior:设置我们的事务传播行为 3. readOnly:设置为只读事务,即数据写操作会失败 4. timeout:设置链接过期时间,-1和默认为无超时限制 5. transactionManager:它是我们的IOC容器配置时的必要属性,设置我们的事务管理对象。在本例中用到hibernate,为HibernateTransactionManager。 核心方法 TransactionTemplate类在调用时主要用到的方法为execute(TransactionCallback action)。 有返回值的回调接口 其中TransactionCallback为我们的回调接口,它只有一个方法: T doInTransaction(TransactionStatus status),这个方法内是有事务的。通常我们的数据库查询操作就在这个方法里完成。 方法入参TransactionStatus接口 doInTransaction方法的唯一入参是TransactionStatus,它常用于查看我们当前的事务状态,它有两个常用的方法: 1. createSavepoint():创建一个记录点。 2. rollbackToSavepoint(savepoint):将事务回滚到特定记录点,这样从回滚处到记录点范围内所有的数据库操作都会失效。 无返回值的接口TransactionCallback 另外,doInTransaction是有返回值的,如果我们不需要返回值,我们可以使用TransactionCallback接口的一个子类TransactionCallbackWithoutResult,它对应的抽象方法doInTransactionWithoutResult(TransactionStatus status)是没有返回值的。 实例演示 下面开始我们的实例演示 1. service层配置 public class MyaseServiceImpl implements MyBaseService{ private MyBaseDao myBaseDao; private TransactionTemplate transactionTemplate; @Override//测试方法 public void queryUpdateUser(final Integer id,final String newName) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { User user = myBaseDao.queryUnique(User.class, id);//根据id获取用户 System.out.println(user); user.setName(newName);//修改名称 myBaseDao.update(user);//更新数据库 System.out.println(user); } }); /*下面的方法是由返回值的,在这里我们假设为User。 User user = transactionTemplate.execute(new TransactionCallback<User>() { @Override public User doInTransaction(TransactionStatus status) { User user = myBaseDao.queryUnique(User.class, id); System.out.println(user); user.setName(newName); myBaseDao.update(user); System.out.println(user); return user; } }); */ } public void setMyBaseDao(MyBaseDao myBaseDao) {//set属性注入 this.myBaseDao = myBaseDao; } public void setTransactionTemplate(TransactionTemplate transactionTemplate) { this.transactionTemplate = transactionTemplate; } } 2. DAO层配置 对应的DAO层类和部分方法如下所示: public class MyBaseDaoImpl implements MyBaseDao{ private SessionFactory sessionFactory; private Session getCurrentSession (){//根据参数来选择创建一个新的session还是返回当前线程的已有session return sessionFactory.getCurrentSession(); } @Override public <E> E queryUnique(Class<E> clazz, Integer entityId) {//查询唯一的对象 return (E) getCurrentSession().get(clazz, entityId); } @Override public void update(Object object) {//更新对象 getCurrentSession().update(object); } public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } } 3. Spring容器配置Bean依赖关系 如果对于Hibernate不太理解,可以先不管我们的方法实现原理,只需要知道对应的方法实现了什么功能即可。在这里。接下来我们要配置我们的spring容器,主要完成Bean之间的依赖配置: <bean id="myBaseDao" class="com.yc.dao.MyBaseDaoImpl" > <property name="sessionFactory" ref="sessionFactory" /> </bean> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="transactionManager" /> </bean> <bean id="myBaseServiceImpl" class="com.yc.service.MyBaseServiceImpl" > <property name="myBaseDao" ref="myBaseDao" /> <property name="transactionTemplate" ref="transactionTemplate" /> </bean> 关于数据源和sessionFactory的配置实例如下: <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"><!-- 设置为close使Spring容器关闭同时数据源能够正常关闭,以免造成连接泄露 --> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/yc" /> <property name="username" value="yc" /> <property name="password" value="yc" /> <property name="defaultReadOnly" value="false" /><!-- 设置为只读状态,配置读写分离时,读库可以设置为true 在连接池创建后,会初始化并维护一定数量的数据库安连接,当请求过多时,数据库会动态增加连接数, 当请求过少时,连接池会减少连接数至一个最小空闲值 --> <property name="initialSize" value="5" /><!-- 在启动连接池初始创建的数据库连接,默认为0 --> <property name="maxActive" value="15" /><!-- 设置数据库同一时间的最大活跃连接默认为8,负数表示不闲置 --> <property name="maxIdle" value="10"/><!-- 在连接池空闲时的最大连接数,超过的会被释放,默认为8,负数表示不闲置 --> <property name="minIdle" value="2" /><!-- 空闲时的最小连接数,低于这个数量会创建新连接,默认为0 --> <property name="maxWait" value="10000" /><!-- 连接被用完时等待归还的最大等待时间,单位毫秒,超出时间抛异常,默认为无限等待 --> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="hibernateProperties"> <props> <!-- MySQL的方言 --> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="javax.persistence.validation.mode">none</prop> <!-- 必要时在数据库新建所有表格 --> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.show_sql">true</prop> <prop key="current_session_context_class">thread</prop> <!-- <prop key="hibernate.format_sql">true</prop> --> </props> </property> <property name="packagesToScan" value="com.yc.model" /> </bean> 4. 测试方法和结果分析 配置完成后,就可以进行我们的测试了。在这里,我用到了Junit测试组件 public class Test1 { @Test public void test(){ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-datasource.xml"); MyBaseServiceImpl myBaseServiceImpl = (MyBaseServiceImpl) ac.getBean("myBaseServiceImpl"); myBaseServiceImpl.queryUpdateUser(1, "newName");//在这里调用我们的service层方法 } } 调用测试方法,我们会看到控制台输出如下相关信息: DEBUG: org.hibernate.engine.jdbc.internal.LogicalConnectionImpl - Obtaining JDBC connection DEBUG: org.hibernate.engine.jdbc.internal.LogicalConnectionImpl - Obtained JDBC connection DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - begin————————这里我们的事务开始了 DEBUG: org.hibernate.loader.Loader - Loading entity: [com.yc.model.User#1]——————————读取id为1的用户 DEBUG: org.hibernate.loader.Loader - Done entity load//完成装载工作 User [id=1, name=zenghao]——————获得了我们的User信息 User [id=1, name=newName]——————完成了修改操作 DEBUG: org.springframework.orm.hibernate4.HibernateTransactionManager - Initiating transaction commit//初始化数据库提交 DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - committing————————这时候才完成了事务提交 观察打印信息,我们会发现我们像数据库发出了两次请求(分别为获取和更新)但事务才提交了一次。说明这两个请求在同一个事务中。这样我们就能确保多个数据库操作在同一个事务内完成,一旦中间出现异常,能立即回滚,取消前面的数据库操作。 很多人都知道我们的mvc模式将后端业务分成了三层(DAO,service,controller),从这里,我们也能略微看出DAO层和service层的功能职责了。DAO主要完成数据库查询的封装,而Service层则调用DAO层的数据库查询方法来完成我们的业务逻辑处理
在这一篇文章中,我们要用JNDI访问我们的应用服务器配置好的多数据源。在本实例中,我们使用本地的tomcat服务器来模拟远程服务器,由于本地只有mysql数据库,故通过访问不同的mysql数据库不同database来模拟同时访问不同数据库如mysql和oracle等。 下面是我们的配置步骤。 1. 在服务器配置全局数据源 首先在我们的tomcat服务器下找到conf文件夹里的server.xml文件,打开并找到 <GlobalNamingResources>。在该节点下会有一个形如下面所示的全局资源 <Resource auth="Container" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase"/> 我们需要在这后面添加我们的数据源配置。如: <Resource name="jdbc/mysql1" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" username="root" password="root" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/yc1"/> <Resource auth="Container" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase"/> <Resource name="jdbc/mysql2" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" username="root" password="root" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/yc2"/> 然后需要在存放server.xml的同一个conf目录下找到context.xml,在节点下加入如下示例配置信息 <ResourceLink global="jdbc/mysql1" name="jdbc/mysql1" type="javax.sql.DataSource" /> <ResourceLink global="jdbc/mysql2" name="jdbc/mysql2" type="javax.sql.DataSource" /> 注意这里的global要和我们在server.xml中配置的数据源名字一样 2. 在Spring容器中配置JNDI连接池信息 <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/mysql1</value> <!--java:comp/env是默认部分, 我们修改jdbc/mysql1对应我们在context.xml中配置<ResourceLink>中的name--> </property> </bean> <bean id="dataSource2" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/mysql2</value> </property> </bean> 在这里我配置两个数据源来模拟连接两个不同的数据库。 3. 测试连接 在本实例中通过Hibernate整合SpringMVC来进行测试。 1. 在IOC容器中配置 <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="hibernateProperties"> <props> <!-- MySQL的方言 --> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="javax.persistence.validation.mode">none</prop> <!-- 必要时在数据库新建所有表格 --> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.show_sql">true</prop> <prop key="current_session_context_class">thread</prop> <!-- <prop key="hibernate.format_sql">true</prop> --> </props> </property> <property name="packagesToScan" value="com.yc.model1" /> </bean> <bean id="sessionFactory2" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource2" /> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="javax.persistence.validation.mode">none</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.show_sql">true</prop> <prop key="current_session_context_class">thread</prop> <prop key="hibernate.format_sql">true</prop> </props> </property> <property name="packagesToScan" value="com.yc.model2" /> </bean> 2. 定义我们的实体类 在这里我们定义两个实体类,对应到两个不同的库表中。 /*****************实体类1*****************/ package com.yc.model1; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; //忽略getter和setter } /*****************实体类2*****************/ package com.yc.model2; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String name; //忽略getter和setter } 3. 在mysql中创建数据库yc1和yc2: create database yc1; create database yc2; 至此,我们的初步测试配置已完成,这时候,运行tomcat服务器,进入mysql数据库,依次执行命令 `use yc1;show tables;use yc2;show tables;` 会看到如下图所示的结果 即hibernate帮我们自动对应实体创建表格了。到这里位置,我们的JNDI多数据源配置测试基本完成,但为了使我们的测试更加充分,不妨通过web配置具体测试一遍。 4. 在数据表中插入测试数据 依次执行如下命令: use yc1;INSERT INTOUser(name) VALUES ('JNDITest1'); use yc2;INSERT INTOUser(name) VALUES ('JNDITest2'); 5. 配置Controller 示例如下: package com.yc.controller; @Controller public class JNDITestController { @Autowired @Qualifier("sessionFactory") private SessionFactory sessionFactory; @Autowired @Qualifier("sessionFactory2") private SessionFactory sessionFactory2; @RequestMapping("testJNDI") @ResponseBody public String testJNDI(){ String name1 = (String) sessionFactory.openSession().createQuery("select name from User where id = 1").uniqueResult(); String name2 = (String) sessionFactory2.openSession().createQuery("select name from User where id = 1").uniqueResult(); return "name1 = " + name1 + "————name2 = "+ name2 ; } } 6. 重启tomcat服务器 在游览器中输入如下url:http://localhost:8090/yc/testJNDI,会得到游览器响应:name1 = JNDITest1————name2 = JNDItest2。示例图片如下所示: 4. 测试小结 至此我们的配置和测试都已经完成了。这里囿于我的本地资源限制,没有进行远程服务器测试。也没有进行真正的多数据库测试,但原理都是类似的。 上面示例只是为了展示JDNI连接应用服务器配置多数据源的实际操作流程测试。但在实际应用中,面对多数据源,我们还需要针对我们的具体需求作更深入的DAO层设计配置。比如,我们可以结合AOP来针对我们的DAO层的不同访问数据库方法来完成我们的读写分离。具体实现请移步下篇文章。
在这里,我们接上一篇文章,利用JNDI访问应用服务器配置的两个数据源来模拟同时操作不同的数据库如同时操作mysql和oracle等。实际上,上个例子可能用来模拟mysql数据库主从配置读写分离更贴切些。既然如此,在本例中,我们就完成读写分离的模拟在web端的配置实例。 续上次的例子,关于JNDI数据源的配置和spring datasource的配置这里不再重复。下面着重加入AOP实现DAO层动态分库调用。可先看上篇文章《spring学习笔记(18)使用JNDI模拟访问应用服务器多数据源实例 》 1. DAO层设计 /***************接口设计************/ public interface MyBaseDao { SessionFactory getSessionFactory(); <E> E add(Object object); <E>E queryUnique(Class<E> clazz, Integer entityId); void setSourceType(Integer sourceType); } /****************实现类设计******************/ @Repository public class MyBaseDaoImpl implements MyBaseDao{ //由于本类是单例了,考虑到线程安全问题,这是使用ThreadLocal作为判断调用哪个数据源的依据 private ThreadLocal<Integer> sourceType = new ThreadLocal<Integer>(); @Autowired @Qualifier("sessionFactory") private SessionFactory sessionFactory; @Autowired @Qualifier("sessionFactory2") private SessionFactory sessionFactory2; @Override public SessionFactory getSessionFactory() { if(sourceType.get() == null){//如果没有值则默认使用数据源1 sourceType.set(1); } switch (sourceType.get()) { case 1://使用数据源1,本例中这里是主库,主要负责写 return sessionFactory; case 2: //使用数据源2,本例中这里是从库,主要负责读 return sessionFactory2; default: throw new IllegalArgumentException("unknown sourceType"); } } @Override//模拟一个写的操作,要让主库数据源调用 public <E> E add(Object object) { return (E) getSessionFactory().openSession().save(object); } @Override//模拟一个读的操作,要让从库数据源调用 public <E> E queryUnique(Class<E> clazz, Integer entityId) { return (E) getSessionFactory().openSession().get(clazz, entityId); } @Override//共AOP增强类修改数据源类型 public void setSourceType(Integer sourceType) { this.sourceType.set(sourceType); } } 2. AOP类设计 @Aspect public class DataSourceSelector {//使用前置增强 @Before("execution( * com.yc.dao.MyBaseDaoImpl.add*(..))")//写操作 public void before1(JoinPoint joinPoint){ ((MyBaseDao)joinPoint.getTarget()).setSourceType(1);//切换到主库 } @Before("execution( * com.yc.dao.MyBaseDaoImpl.query*(..))")//读操作 public void before2(JoinPoint joinPoint){ ((MyBaseDao)joinPoint.getTarget()).setSourceType(2);//切换到从库 } } 关于AOP的配置教程,可移步参考本系列前面AOP部分的文章。然后我们还需要在IOC容器中注册我们的切面。 <aop:aspectj-autoproxy /> <!-- 使@AspectJ注解生效 --> <bean class="com.yc.aop.DataSourceSelector" /><!-- 注册切面 --> 3. 修改控制器 针对上一篇文章的控制器,作如下修改: @Controller public class JNDITestController { @Autowired private MyBaseDao myBaseDao; @RequestMapping("testJNDI") @ResponseBody public Object testJNDI(){ User user = new User(); user.setName("new_user_fron_yc1"); Integer newId = myBaseDao.add(user); System.out.println("new UserId = " + newId);//获取我们新插入的id System.out.println("is new User here? " + myBaseDao.queryUnique(com.yc.model2.User.class, newId)); return newId; } } 4. 测试与结果分析 运行服务器,然后在游览器中输入http://localhost:8090/yc/testJNDI,我们会看到控制台打印信息: new UserId = 4 is new User here? null newUserId是我们插入到主库中的,id为4。 但我们紧接着读取,却并未读取到。这说明我们存的数据库和读的数据库并不是同一个,从而简单地实现了读写分离。为了验证这一点,我们所示数据库,如下图所示,显然我们在yc1存进去了我们的测试数据,在yc2中并没有!
数据连接池 在spring中,常使用数据库连接池来完成对数据库的连接配置,类似于线程池的定义,数据库连接池就是维护有一定数量数据库连接的一个缓冲池,一方面,能够即取即用,免去初始化的时间,另一方面,用完的数据连接会归还到连接池中,这样就免去了不必要的连接创建、销毁工作,提升了性能。当然,使用连接池,有一下几点是连接池配置所考虑到的,也属于配置连接池的优点,而这些也会我们后面的实例配置中体现: 1、 如果没有任何一个用户使用连接,那么那么应该维持一定数量的连接,等待用户使用。 2、 如果连接已经满了,则必须打开新的连接,供更多用户使用。 3、 如果一个服务器就只能有100个连接,那么如果有第101个人过来呢?应该等待其他用户释放连接 4、 如果一个用户等待时间太长了,则应该告诉用户,操作是失败的。 在spring中,常用的连接池有:jdbc,dbcp,c3p0,JNDI4种,他们有不同的优缺点和适用场景。其中,spring框架推荐使用dbcp,hibernate框架推荐使用c3p0。经测试发现,c3p0与dbcp相比较,c3p0能够更好的支持高并发,但是在稳定性方面略逊于dpcp。 下面对几个连接池进行示例配置: jdbc连接池配置示例 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"> </property> <property name="url" value="jdbc:mysql://localhost:3306/yc" /> <property name="username" value="yc"></property> <property name="password" value="yc"></property> </bean> DriverManagerDataSource没有实现连接池化连接的机制,每次调用getConnection()获取新连接时,只是简单地创建一个新的连接。所以,一般这种方式常用于开发时测试,不用于生产。 dbcp连接池配置示例 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"><!--设置为close使Spring容器关闭同时数据源能够正常关闭,以免造成连接泄露 --> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/yc" /> <property name="username" value="yc" /> <property name="password" value="yc" /> <property name="defaultReadOnly" value="false" /><!-- 设置为只读状态,配置读写分离时,读库可以设置为true --> <!-- 在连接池创建后,会初始化并维护一定数量的数据库安连接,当请求过多时,数据库会动态增加连接数, 当请求过少时,连接池会减少连接数至一个最小空闲值 --> <property name="initialSize" value="5" /><!-- 在启动连接池初始创建的数据库连接,默认为0 --> <property name="maxActive" value="15" /><!-- 设置数据库同一时间的最大活跃连接默认为8,负数表示不闲置 --> <property name="maxIdle" value="10"/><!-- 在连接池空闲时的最大连接数,超过的会被释放,默认为8,负数表示不闲置 --> <property name="minIdle" value="2" /><!-- 空闲时的最小连接数,低于这个数量会创建新连接,默认为0 --> <property name="maxWait" value="10000" /><!-- 连接被用完时等待归还的最大等待时间,单位毫秒,超出时间抛异常,默认为无限等待 --> </bean> 以上参数是我们在实际开发中常用到的。关于分析都在注释里。 c3p0连接池配置示例 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/yc" /> <property name="user" value="yc" /> <property name="password" value="yc" /> </bean> 它的常用配置属性见下表: 属性 说明 默认值 acquireIncrement 当连接池中的连接用完时,C3P0一次性创建新连接的数目 5 acquireRetryAttempts 定义在从数据库获取新连接失败后重复尝试获取的次数 30 checkoutTimeout 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒 0 initialPoolSize 初始化时创建的连接数,应在minPoolSize与maxPoolSize之间取值 3 maxIdleTime 最大空闲时间,超过空闲时间的连接将被丢弃。为0或负数则永不丢弃 0 maxPoolSize 连接池中保留的最大连接数 15 numHelperThreads C3P0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能,通过多线程实现多个操作同时被执行 3 4. JNDI连接池配置示例 如果我们需要使用远程服务器(如WebLogic等)自带的数据源时,常使用这种配置。JNDI在spring中有两种配置方式,一种是利用spring内置的JndiObjectFactoryBean。 <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName" value="java:comp/env/jdbc/yc/> </bean> 另一种则是利用Spring为获取j2ee资源提供的一个jee命名空间: <!--1.现在xmlns下添加: jee=http://www.springframework.org/schema/jee 2. 然后在xsi:schemaLocation下添加: http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd"> 3. 然后我们可以直接使用<jee:jndi-lookup>标签完成配置 --> <jee:jndi-lookup id="dataSource" jndi-name=" java:comp/env/jdbc/yc"/> 在下一篇文章,我会示例如何通过JNDI在我们的应用服务器上配置多数据源,然后在我们的web项目中进行访问。同时,我们会结合AOP简单模拟主从分库的读写分离实例。通过针对我们的DAO层的不同访问数据库方法来完成我们的读写分离。
CGLib动态代理基本原理 CGLib——Code Generation Library,它是一个动态字节代码生成库,基于asm。使用CGLib时需要导入asm相关的jar包。而asm又是何方神圣? asm是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。 了解asm的功能原理,有助于更好地理解我们的CGLib,但在本节不会细究asm。我们的主要关注点还是如何使用CGLib实现AOP。为我们后面分析Spring AOP铺垫。 在上一节中,我们用JDK的动态代理来模拟实现了一个性能监控的例子,用到了JDK内置的反射技术和java.lang.reflect.Proxy代理类,通过例子我们发现,它只能通过让被代理类实现代理接口的方式来生成代理,而CGLib的区别在于通过在程序运行时动态生成一个被代理类的子类的方式来完成代理。它有几个核心类: 1. Enhancer 它用于动态生成被代理的类的子类。使用此类生成子类的前奏是指定被代理类和指定CallBack接口 2. CallBack:它是一个很关键的接口,我们常常通过CallBack接口来配置我们的拦截方法, 3. MethodInterceptor:是CallBack的实现类,他会拦截我们被代理类的所有方法,来实现自己的增强细节。比如做点日志记录,方法处理等,处理完后,还能通过MethodProxy重新调用拦截掉的方法。 4. MethodProxy:主要用于重新调用MethodInterceptor拦截掉的方法,是jdk反射包中Method的代理类。 5. CallbackFilter:一个Enhancer生成类可以指定多个Callback,这样我们可以设定条件过滤,让被代理类中不同的方法被调用时使用不同的CallBack来进行处理。 实例导入,需求分析 在上篇文章的例子基础上,我们为我们“老类”的每个方法(例子中有method1、method2、method3三个方法)都实现了耗时统计,但现在,对于method3,因为它经常被用户调用,每次被调用都统计耗时会对性能造成一定影响,因此,现在需要过滤掉对method3的的耗时统计,而且我们还想对其进行日志记录,看看哪些用户什么时候调用了这个方法。 现在,结合前面提到的核心类,我们通过CGLib来完成这一轮新需求 源码实例展示 1. 定义被代理对象 我们的被代理对象:OldClass当然是不(能)变的啦。 public class OldClass { public void method1() throws InterruptedException{ System.out.println("正在处理业务逻辑1"); Thread.sleep(100);//模拟处理业务逻辑1过程 System.out.println("业务逻辑1处理完成"); } public void method2() throws InterruptedException{ System.out.println("正在处理业务逻辑2"); Thread.sleep(200);//模拟处理业务逻辑2过程 System.out.println("业务逻辑2处理完成"); } public void method3(String userName) throws InterruptedException{ System.out.println("正在处理业务逻辑3"); Thread.sleep(300);//模拟处理业务逻辑3过程 System.out.println("业务逻辑3处理完成"); } //下面还有很多很多。。 } 2. 定义代理生成工厂 public class ProxyFactory { private Enhancer enhancer = new Enhancer();//动态的类生成器 public Object createSubObject(Class<?> clazz){ enhancer.setSuperclass(clazz);//设置需要创建的类,这个类的父类是clazz类 //当通过enhancer创建的类中的方法被调用时,该方法会被CallBack指定的对象拦截。 enhancer.setCallbacks(new Callback[]{new MyTimeInterceptor(),new MyRecordInterceptor()}); enhancer.setCallbackFilter(new MyCallBackFilter());//设置我们自定义的过滤器 return enhancer.create();//通过字节码技术动态创建子类实例 } } 3. 定义增强拦截器 下面定义我们的两个CallBack实现类,一个负责拦截需要统计耗时的方法,另一个拦截需要进行日志记录的方法 1. 耗时统计拦截器 public class MyTimeInterceptor implements MethodInterceptor { @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {//拦截所有父类方法的调用 Long beginTime = System.currentTimeMillis();//记录开始时间 //调用目标对象的方法,同时获取该方法的返回值,作为我们本代理方法(invoke)的返回值 Object returnValue = proxy.invokeSuper(target, args);//target为我们方法所在的目标类,args为方法参数 System.out.println("方法" + method.getName() + "调用结束,耗时"+ (System.currentTimeMillis() - beginTime)); return returnValue; } } 2. 日志记录拦截器 public class MyRecordInterceptor implements MethodInterceptor { @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {// 拦截所有父类方法的调用 System.out.println(args[0] + "在" + new SimpleDateFormat("yyyy-MM-dd HH-mm").format(new Date()) + "调用了方法" + method.getName()); Object returnValue = proxy.invokeSuper(target, args);// target为我们方法所在的目标类,args为方法参数 return returnValue; } } 4. 定义我们的拦截过滤器 public class MyCallBackFilter implements CallbackFilter{//需要实现特定接口 @Override public int accept(Method method) { if("method3" .equals(method.getName())){//如果被拦截的方法名满足特定条件 //这里的序号对应于enhancer.setCallbacks(new Callback[]{new MyTimeInterceptor(),new MyRecordInterceptor()})中的Callback数组的拦截器索引 return 1; }else{ return 0; } } } 5. 测试方法 public static void main(String args[]) throws InterruptedException{ ProxyFactory cgLibProxy = new ProxyFactory();//创建我们的代理类 //通过enhancer创建我们的子类 //因为oldClass是我们子类的父类,所以这里向上转型成功 OldClass oldClass = (OldClass) cgLibProxy.createSubObject(OldClass.class); oldClass.method1();//调用方法 oldClass.method2();//调用方法 oldClass.method3("zenghao");//调用方法 } 6. 结果分析 运行5中的测试方法,控制台打印: 正在处理业务逻辑1 业务逻辑1处理完成 方法method1调用结束,耗时116 正在处理业务逻辑2 业务逻辑2处理完成 方法method2调用结束,耗时201 zenghao在2016-03-24 18-32调用了方法method3 正在处理业务逻辑3 业务逻辑3处理完成 在这里,我们的method1和method2还是被拦截下来统计耗时,但我们的method3就在调用前被做了日志记录了。 小结 使用CGlib来通过生成子类来完成代理,这样,我们就不用强迫我们的被代理类实现代理接口了,侵入性更低。而且,使用CGLib还能为我们的拦截方法实现智能过滤,相对于使用JDK的动态代理,还是优雅了很多。 但在实际的应用场景中,如果我们每次使用AOP,都要进行如上所示一堆配置,还是挺繁琐的,但如果我们把配置的工作,交给spring完成,那么我们只要通过简洁的配置,就能轻松实现我们的动态代理。甚至结合上spring的许多特性,我们的代理功能还会更加的灵活强大。 通过对JDK和CGLib动态代理的实例理解,我们对AOP有一个更全面而感性地认识,从下篇文章我们开始进入springAOP部分的学习分析。 源码下载 本篇博文源码可到https://github.com/jeanhao/spring的CGLibProxy文件下载。
JDK动态代理技术 动态代理最常见应用是AOP(面向切面编程)。通过AOP,我们能够地拿到我们的程序运行到某个节点时的方法、对象、入参、返回参数,并动态地在方法调用前后新添一些新的方法逻辑,来满足我们的新需求,比如日志记录等。 动态代理常见有两种方式:基于JDK的反射技术的动态代理和基于CGLib的动态代理。 使用反射技术创建动态代理 JDK创建动态代理的核心是java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy类。让我们先分析需求,拿出模型示例,再依据示例来进行讲解这两个核心接口/类的用法。 需求分析: 面对一个大型项目,里面的类可能已设计得非常庞大臃肿,一个类里可能有上十个方法,现在,我们需要为对每个方法进行性能监控。统计方法的运行时间。如果我们通过直接在设计好的每个类方法开始结束记录时间戳来计算方法运行耗时,会有如下缺点: 1. 我们的日志记录是侵入式,同时还嵌入了大量重复冗杂的代码,如果日后需要修改,则要针对每个方法修改一遍,既不符合开放封闭的设计原则,同时也不便维护还容易出错。 2. 从业务逻辑角度来看,这些性能统计的代码和我们既有类实现的业务功能没有任何关系,如果把它们整合在一起,会造成两个不相关功能之间的耦合,不符合职责分明的原则。 那么,有没办法既不修改我们的原有类,同时又能增强我们的类功能呢?比如这里为我们的类每个方法都添加性能监控?答案便是使用动态代理。 实例展示 1. 定义我们的代理接口 package com.proxy.demo1; public interface MyProxy { void method1() throws InterruptedException; void method2() throws InterruptedException; void method3() throws InterruptedException; } 2. 定义我们的被代理对象——庞大臃肿的“老类” package com.proxy.demo1; //这是我们项目中“历史悠久”的类,功能完整,有很多方法。现在我们需要为每个方法都实现性能统计 //我们的被代理类要实现我们的代理接口,总某种程度讲,这也是侵入式的。但是最微弱的侵入。 public class OldClass implements MyProxy { @Override public void method1() throws InterruptedException{ System.out.println("正在处理业务逻辑1"); Thread.sleep(100);//模拟处理业务逻辑4过程 System.out.println("业务逻辑1处理完成"); } @Override public void method2() throws InterruptedException{ System.out.println("正在处理业务逻辑2"); Thread.sleep(200);//模拟处理业务逻辑2过程 System.out.println("业务逻辑2处理完成"); } @Override public void method3() throws InterruptedException{ System.out.println("正在处理业务逻辑3"); Thread.sleep(300);//模拟处理业务逻辑3过程 System.out.println("业务逻辑3处理完成"); } //下面还有很多很多方法。。 } 3. 定义我们的invokationHandler package com.proxy.demo1; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; //这里我们使用了泛型,假设我们有很多的类都需要进行性能监控,就可以通过在创建本类对象时在泛型标识处改成对应需要监控的类即可。 //注意需要实现JDK反射包中的InvocationHandler接口 public class //这里我们使用了泛型,假设我们有很多的类都需要进行性能监控,就可以通过在创建本类对象时在泛型标识处改成对应需要监控的类即可。 //注意需要实现JDK反射包中的InvocationHandler接口<E> implements InvocationHandler { //需要被代理的对象 private E target; public MyInvokationHandler(E target){ this.target = target; } /** * @param: * proxy : 在其上调用方法的代理实例 * method : 对应于在目标对象调用的方法。 * args : 包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为 null * 基本类型的参数被包装在适当基本包装器类(如 java.lang.Integer 或 java.lang.Boolean)的实例中。 * @return 从代理实例的方法调用返回的值。如果接口方法的声明返回类型是基本类型, * 则此方法返回的值一定是相应基本包装对象类的实例;否则,它一定是可分配到声明返回类型的类型。 * 如果此方法返回的值为 null 并且接口方法的返回类型是基本类型,则代理实例上的方法调用将抛出 NullPointerException * 否则,如果此方法返回的值与上述接口方法的声明返回类型不兼容,则代理实例上的方法调用将抛出 ClassCastException。 * @throws Throwable - 从代理实例上的方法调用抛出的异常。 * 该异常的类型必须可以分配到在接口方法的 throws 子句中声明的任一异常类型 * 或未经检查的异常类型 java.lang.RuntimeException 或 java.lang.Error。 * 如果此方法抛出经过检查的异常,该异常不可分配到在接口方法的 throws 子句中声明的任一异常类型. * 代理实例的方法调用将抛出包含此方法曾抛出的异常的 UndeclaredThrowableException。 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Long beginTime = System.currentTimeMillis();//记录开始时间 //调用目标对象的方法,同时获取该方法的返回值,作为我们本代理方法(invoke)的返回值 Object returnValue = method.invoke(target, args);//target为我们方法所在的目标类,args为方法参数 System.out.println("方法" + method.getName() + "调用结束,耗时"+ (System.currentTimeMillis() - beginTime)); return returnValue; } } 从字面意思理解是InvocationHandler是调用处理器,在这里,它是一个方法调用处理器。更通俗来说,我们可以将它理解成一个拦截器,当我们调用被代理类中的方法时,就会被MyInvocationHandler拦截下来,再调用我们的invoke方法。 4. 测试方法 package com.proxy.demo1; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class MainTest { public static void main(String args[]) throws InterruptedException{ //新建我们的被代理对象 OldClass oldClass = new OldClass(); //创建我们的“拦截器”,并注入被代理对象 InvocationHandler handler = new MyInvocationHandler<OldClass>(oldClass); /*getProxy返回一个指定接口的代理类实例, 该接口可以将方法调用指派到指定的调用处理程序,这里是我们自定义的handler 第一个参数 - 定义代理类的类加载器 第二个参数 - 代理类要实现的接口列表 第三个参数- 指派方法调用的调用处理程序 InvocationHandler */ MyProxy myProxy = (MyProxy)Proxy.newProxyInstance(MyProxy.class.getClassLoader(),new Class[]{MyProxy.class},handler); myProxy.method1(); myProxy.method2(); myProxy.method3(); } } 5. 打印结果 正在处理业务逻辑1 业务逻辑1处理完成 方法method1调用结束,耗时101 正在处理业务逻辑2 业务逻辑2处理完成 方法method2调用结束,耗时200 正在处理业务逻辑3 业务逻辑3处理完成 方法method3调用结束,耗时301 我们通过代理类来调用OldClass的方法,实现了对OldClass类中所有方法耗时统计的性能监控功能,但我们并未在OldClass中嵌入任何相关的业务逻辑代码,唯一的修改就是实现了我们的代理接口。 实例分析 我们方法调用的核心实现在于使用invocationHandler,实际上,我们在通过代理接口调用被代理对象的方法如myProxy.method1()的时候,我们实际调用的是我们自定义的handler里面的invoke方法,只是,我们在invoke方法又根据传入对象(oldClass)和参数(这里没有传参),重新调用了我们oldClass里面的method1而已。而且通过这种动态代理,我们还需要修改我们的上层接口,比如我是在oldClassBoss中调用oldClass的method1方法的,现在要在我们的oldClassBoss中创建代理并通过myProxy.method1();来实现我们对原发的性能监控增强功能。这是我们需要明确的。 源码下载 本篇文章的实例源码如果需要请到https://github.com/jeanhao/spring的jdkProxy文件夹下载
上一篇我们使用到的ApplicationListener是无序的,结合异步调度它能满足了我们的大部分应用场景,但现在我们来个另类的需求,我们来模拟一条作业调度流水线,它不能异步,必须按照先后次序执行不同的任务才能得到我们的最终结果。 需求示例:现在假如华中科技大学的小白想要为它的智能机器人作品申报国家创新奖,需要经过学校、省级创新科研机构、国家创新科研机构逐层审核。我们尝试通过事件来实现,核心就在监听器实现SmartApplicationListener接口。示例如下: 1. 配置事件发布者小白: public class XiaoBai implements ApplicationContextAware { private ApplicationContext applicationContext;//底层事件发布者 public void reportWorks(){//申报作品 AuditEvent auditEvent = new AuditEvent(this); applicationContext.publishEvent(auditEvent); //小白获取期待已久的最终结果 System.out.println("最终审核结果:" + auditEvent.getStatus() + "——" + auditEvent.getAdvice()); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } } 关于上述配置可参考我的上一篇文章《spring学习笔记(13)趣谈spring 事件:实现业务逻辑解耦,异步调用提升用户体验》 2. 配置审核事件源 public class AuditEvent extends ApplicationEvent { private Boolean status ; //当前申报状态 private String work;//申报作品 private String advice;//当前申报意见 public AuditEvent(Object source) { super(source); status = true;//初始窗台 advice = "尚未审核"; } //ignore getter and setter } 3. 配置事件监听器 我们实现需求的核心部分来了,先上代码 /******************学校审核监听器******************/ public class SchoolListener implements SmartApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { System.out.println("获取当前的申报状态为:"+((AuditEvent)event).getStatus() + "——" + ((AuditEvent)event).getAdvice()); ((AuditEvent)event).setStatus(true); ((AuditEvent)event).setAdvice("学校审核意见:有创意,非常棒!"); } @Override public int getOrder() { return 1;//获取学校监听(审核)的优先级 } @Override//这是我们的监听器智能所在之一,能够根据事件类型动态监听 public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { return eventType == AuditEvent.class; } @Override//这是我们的监听器智能所在之二,能够根据事件发布者类型动态监听 public boolean supportsSourceType(Class<?> sourceType) { return sourceType == XiaoBai.class; } } /******************省级审核监听器******************/ public class ProvinceListener implements SmartApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if(((AuditEvent)event).getStatus()){//如果上层审核通过 System.out.println("获取当前的申报状态为:"+((AuditEvent)event).getStatus() + "——" + ((AuditEvent)event).getAdvice()); ((AuditEvent)event).setStatus(true); ((AuditEvent)event).setAdvice("省级审核意见:还行,通过吧!"); } } @Override public int getOrder() { return 2; } @Override public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { return eventType == AuditEvent.class; } @Override public boolean supportsSourceType(Class<?> sourceType) { return sourceType == XiaoBai.class; } } /******************国家级审核监听器******************/ public class CountryListener implements SmartApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if(((AuditEvent)event).getStatus()){//如果上层审核通过 System.out.println("获取当前的申报状态为:"+((AuditEvent)event).getStatus() + "——" + ((AuditEvent)event).getAdvice()); ((AuditEvent)event).setStatus(true); ((AuditEvent)event).setAdvice("国家审核意见:一般般,勉强通过吧!"); } } @Override public int getOrder() { return 3; } @Override public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { return eventType == AuditEvent.class; } @Override public boolean supportsSourceType(Class<?> sourceType) { return sourceType == XiaoBai.class; } } 在这里,我们的实例为了方便演示而简化配置。我们的发布源支持类型是小白,根据更实际的需求,我们可以让小白继承Student类,然后让sourceType只要是Student的子类即可,这样就能满足任何继承了Student的学生都能申报了。 在实现了SmartApplicationListener的监听器中,我们通过重写GetOrder方法来修改不同监听器的顺序,优先级越小,则越先被调用。通过配置不同的优先级,且让监听器之间阻塞调用。我们就能实现流水线式的有序事件调用,这在实际应用场景中还是蛮有意义的 5. 在IOC容器中注册监听器 <bean id="schoolListener" class="test.event2.SchoolListener" /> <bean id="provinceListener" class="test.event2.ProvinceListener" /> <bean id="countryListener" class="test.event2.CountryListener" /> <bean id="xiaoBai" class="test.event2.XiaoBai" /><!-- 测试时注入使用 --> 6. 测试方法 public class MainTest { public static void main(String args[]) throws InterruptedException{ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/event2/event.xml"); XiaoBai xiaobai = (XiaoBai) ac.getBean("xiaoBai"); xiaobai.reportWorks();//小白要开始申报项目啦 } } 调用测试代码,我们得到运行结果: 获取当前的申报状态为:true——尚未审核 获取当前的申报状态为:true——学校审核意见:有创意,非常棒! 获取当前的申报状态为:true——省级审核意见:还行,通过吧! 最终审核结果:true——国家审核意见:一般般,勉强通过吧! 于是,小白的作品终于评上了国家创新科技奖了,这让他高兴了好一阵子。 源码下载 本实例源码可到我的github仓库https://github.com/jeanhao/spring下的event2文件夹下载
分析需求引入事件机制 使用spring的事件机制有助于对我们的项目进一步的解耦。假如现在我们面临一个需求: 我需要在用户注册成功的时候,根据用户提交的邮箱、手机号信息,向用户发送邮箱认证和手机号短信通知。传统的做法之一是在我们的UserService层注入邮件发送和短信发送的相关类,然后在完成用户注册同时,调用对应类方法完成邮件发送和短信发送 但这样做的话,会把我们邮件、短信发送的业务与我们的UserService的逻辑业务耦合在了一起。耦合造成的常见缺点是,我(甚至假设很频繁的)修改了邮件、短信发送的API,我就可能需要在UserService层修改相应的调用方法,但这样做人家UserService就会很无辜并吐槽:你改邮件、短信发送的业务,又不关我的事,干嘛老改到我身上来了?这就是你的不对了。 对呀!根据职责分明的设计原则,人家UserService就只该管用户管理部分的业务逻辑,你老让它干别人干的事,它当然不高兴了! 那该怎么拌?凉拌?不不不。。。我们可以通过spring的事件机制来实现解耦呀。利用观察者设计模式,设置监听器来监听userService的注册事件(同时,我们可以很自然地将userService理解成了事件发布者),一旦userService注册了,监听器就完成相应的邮箱、短信发送工作(同时,我们也可以很自然地将发送邮件、发送短信理解成我们的事件源)。这样userService就不用管别人的事了,只需要在完成注册功能时候,当下老大,号令手下(监听器),让它完成短信、邮箱的发送工作。 spring的事件通信常按下列流程进行 Created with Raphaël 2.1.0事件发布者广播事件(源)监听器收到广播,获取事件源监听器根据事件源采取相应的处理措施 事件实例分析 在这里面,我们涉及到三个主要对象:事件发布者、事件源、事件监听器。根据这三个对象,我们来配置我们的注册事件实例: 1. 定义事件源 利用事件通信的第一步往往便是定义我们的事件。在spring中,所有事件都必须扩展抽象类ApplicationEvent,同时将事件源作为构造函数参数,在这里,我们定义了发邮件、发短信两个事件如下所示 /*****************邮件发送事件源*************/ public class SendEmailEvent extends ApplicationEvent { //定义事件的核心成员:发送目的地,共监听器调用完成邮箱发送功能 private String emailAddress; public SendEmailEvent(Object source,String emailAddress ) { //source字面意思是根源,意指发送事件的根源,即我们的事件发布者 super(source); this.emailAddress = emailAddress; } public String getEmailAddress() { return emailAddress; } } /*****************短信发送事件源*************/ public class sendMessageEvent extends ApplicationEvent { private String phoneNum; public sendMessageEvent(Object source,String phoneNum ) { super(source); this.phoneNum = phoneNum; } public String getPhoneNum() { return phoneNum; } } 2. 定义事件监听器 事件监听类需要实现我们的ApplicationListener接口,除了可以实现ApplicationListener定义事件监听器外,我们还可以让事件监听类实现SmartApplicationListener(智能监听器)接口,。关于它的具体用法和实现可参考我的下一篇文章《spring学习笔记(14)趣谈spring 事件机制[2]:多监听器流水线式顺序处理 》。而此外,如果我们事件监听器监听的事件类型唯一的话,我们可以通过泛型来简化配置。 现在我们先来看看本例定义: public class RegisterListener implements ApplicationListener { /* *当我们的发布者发布时间时,我们的监听器收到信号,就会调用这个方法 *我们对其进行重写来适应我们的需求 *@Param event:我们的事件源 */ @Override public void onApplicationEvent(ApplicationEvent event) { //我们定义了两个事件:发短信,发邮箱,他们一旦被发布都会被此方法调用 //于是我们需要判断当前event的具体类型 if(event instanceof SendEmailEvent){//如果是发邮箱事件 System.out.println("正在向" + ((SendEmailEvent) event).getEmailAddress()+ "发送邮件......");//模拟发送邮件事件 try { Thread.sleep(1* 1000);//模拟请求邮箱服务器、验证账号密码,发送邮件耗时。 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("邮件发送成功!"); }else if(event instanceof sendMessageEvent){//是发短信事件 event = (sendMessageEvent) event; System.out.println("正在向" + ((sendMessageEvent) event).getPhoneNum()+ "发送短信......");//模拟发送邮短信事件 try { Thread.sleep(1* 1000);//模拟发送短信过程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("短信发送成功!"); } } } /******************通过泛型配置实例如下******************/ public class RegisterListener implements ApplicationListener<SendEmailEvent> {//这里使用泛型 @Override//因为使用了泛型,我们的重写方法入参事件就唯一了。 public void onApplicationEvent(SendEmailEvent event) { ..... } .... } 3. 定义事件发布者 事件发送的代表类是ApplicationEventPublisher我们的事件发布类常实现ApplicationEventPublisherAware接口,同时需要定义成员属性ApplicationEventPublisher来发布我们的事件。 除了通过实现ApplicationEventPublisherAware外,我们还可以实现ApplicationContextAware接口来完成定义,ApplicationContext接口继承了ApplicationEventPublisher。ApplicationContext是我们的事件容器上层,我们发布事件,也可以通过此容器完成发布。下面使用两种方法来定义我们的发布者 在本例中,我们的时间发布者自然就是我们的吐槽者,userService: /**********方法一:实现除了通过实现ApplicationEventPublisherAware接口************/ public class UserService implements ApplicationEventPublisherAware { private ApplicationEventPublisher applicationEventPublisher;//底层事件发布者 @Override public void setApplicationEventPublisher(//通过Set方法完成我们的实际发布者注入 ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void doLogin(String emailAddress,String phoneNum) throws InterruptedException{ Thread.sleep(200);//模拟用户注册的相关业务逻辑处理 System.out.println("注册成功!"); //下列向用户发送邮件 SendEmailEvent sendEmailEvent = new SendEmailEvent(this,emailAddress);//定义事件 sendMessageEvent sendMessageEvent = new sendMessageEvent(this, phoneNum); applicationEventPublisher.publishEvent(sendEmailEvent);//发布事件 applicationEventPublisher.publishEvent(sendMessageEvent); } //...忽略其他用户管理业务方法 } /**********方法二:实现除了通过实现ApplicationContext接口************/ public class UserService2 implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void doLogin(String emailAddress,String phoneNum) throws InterruptedException{ Thread.sleep(200);//模拟用户注册的相关业务逻辑处理 System.out.println("注册成功!"); //下列向用户发送邮件 SendEmailEvent sendEmailEvent = new SendEmailEvent(this,emailAddress);//定义事件 sendMessageEvent sendMessageEvent = new sendMessageEvent(this, phoneNum); applicationContext.publishEvent(sendEmailEvent);//发布事件 applicationContext.publishEvent(sendMessageEvent); } //...忽略其他用户管理业务方法 } 4. 在IOC容器注册监听器 <!-- 在spring容器中注册事件监听器, 应用上下文将会识别实现了ApplicationListener接口的Bean, 并在特定时刻将所有的事件通知它们 --> <bean id="RegisterListener" class="test.event.RegisterListener" /> <!-- 注册我们的发布者,后面测试用到 --> <bean id="userService" class="test.event.UserService" /> 5. 测试方法 public static void main(String args[]) throws InterruptedException{ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/event/event.xml"); UserService userService = (UserService) ac.getBean("userService"); Long beginTime = System.currentTimeMillis(); userService.doLogin("zenghao@google.com","12345678911");//完成注册请求 System.out.println("处理注册相关业务耗时" + (System.currentTimeMillis() - beginTime )+ "ms"); System.out.println("处理其他业务逻辑"); Thread.sleep(500);//模拟处理其他业务请求耗时 System.out.println("处理所有业务耗时" + (System.currentTimeMillis() - beginTime )+ "ms"); System.out.println("向客户端发送注册成功响应"); } 6. 测试结果及分析 调用上面测试方法,控制台打印信息 注册成功! 正在向zenghao@google.com发送邮件…… 邮件发送成功! 正在向12345678911发送短信…… 发送成功! 处理注册相关业务耗时2201ms 处理其他业务逻辑开始.. 处理其他业务逻辑结束.. 处理所有业务耗时2701ms 向客户端发送注册成功响应 在本例中,我们通过事件机制完成了userService和邮件、短信发送业务的解耦。但观察我们的测试结果,我们会发现,这样的用户体验真是糟糕透了:天呐,我去你那注册个用户,要我等近3秒钟!这太久了! 为什么会这么久?我们根据方法分析: 1. 注册查询数据库用了200ms(查询用户名、邮箱、手机号有没被使用,插入用户信息到数据库等操作) 2. 发送邮件用了1000ms 3. 发送短信用了1000ms 4. 处理其他业务逻辑(保存用户信息到session,其他信息数据处理等) 第1,4步的时间耗损我们很难优化,但2,3步是主要耗时的地方,我们能不能想办法把它缩减掉了,它把我们的正常的业务处理堵塞了。什么?堵塞,想到堵塞,我们会很自然地想到非堵塞,那就通过异步来完成2,3呗! 7. 异步拓展。 在spring3以上,拓展了自己独立的时间机制,我们可以使用@Async来完成异步配置。 首先我们需要在我们的IOC容器增加 <!--先在命名空间中增加我们的task标签,注意它们的添加位置 xmlns 多加下面的内容: xmlns:task="http://www.springframework.org/schema/task" 然后xsi:schemaLocation多加下面的内容 http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd --> <!-- 我们的异步事件配置,非常简单 --> <!--开启注解调度支持 @Async @Scheduled--> <task:annotation-driven/> 然后在我们的事件监听器中添加@Async注解 /***************我们可以在类名上添加****************/ @Async public class RegisterListener implements ApplicationListener { ...... } /****************也可以在方法体上添加************/ @Async public class RegisterListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { ..... } } 然后,再调用我们的同样的测试方法,这次我们的结果变成: 注册成功! 正在向zenghao@google.com发送邮件…… 处理注册相关业务耗时201ms ————此时邮件发送还没有结束,和邮件发送异步了 正在向12345678911发送短信….. ————–短信发送和邮件发送和主业务处理程序都异步了! 处理其他业务逻辑开始.. 处理其他业务逻辑结束.. 处理所有业务耗时701ms 向客户端发送注册成功响应 ——客户端耗时701ms就收到响应了。 邮件发送成功! —-这个时候邮箱才发完 短信发送成功! 从以上的测试结果我们,我们的邮箱发送和短信发送都分别单独地异步完成了,大大缩短了我们主业务处理事件,也提高了用户体验 小结 从本例可以看出,不同业务功能的生硬组合,会出现逻辑处理混乱的严重耦合现象,比如userService类既处理自己的用户逻辑,还要处理邮箱等发送的逻辑,这是不是也意味着,如果以后我们拓展更多的功能,我们的userService类还要出现更多的逻辑处理,来个大杂烩?,这同时还可能会为我们主要业务处理带来不必要的阻塞。当然,为了防止阻塞,我们还可以创建新的线程来异步,但这样原来的类就显得更加杂乱臃肿了。 使用spring事件机制能很好地帮助我们消除不同业务间的深耦合关系。它强大的任务调度还能帮助我们简洁地实现事件异步。 关于事件的一些其他用法可参考我的下一篇博文《趣谈spring 事件机制[2]:多监听器流水线式顺序处理》 关于任务调度的相关框架和使用可参考我的专栏《深入浅出Quartz任务调度》。 实例代码下载 本篇博文的实例代码可到我的github仓库https://github.com/jeanhao/spring的event文件夹下下载。