Spring
Spring框架
Spring.transaction
JPA和事务管理
很重要的一点是JPA本身并不提供任何类型的声明式事务管理。如果在依赖注入容器之外 使用JPA,事务处理必须由开发人员编程实现。
UserTransaction utx = entityManager.getTransaction(); try { utx.begin(); businessLogic(); utx.commit(); } catch(Exception ex) { utx.rollback(); throw ex; }
这种方式的事务管理使事务范围可以在代码中很清晰地表达出来,但它有以下缺点:
- 容易出现重复代码和错误
- 任何错误可能产生较大的影响
- 错误难以调试和复现
- 降低了代码库的可读性
- 如果该方法调用了其他的事务方法如何处理呢?
使用Spring Transactional注解
使用Spring Transactional注解,上面的代码就简化为:
@Transactional public void businessLogic() { // ... use entity manager inside a transaction ... }
代码更加简洁,可读性更好,也是目前Spring中事务处理的推荐方式。
通过使用@Transactional
,事务传播等很多重要方面可以自动处理。这种情况下如果
businessLogic()
调用了其他事务方法,该方法将根据选项确定如何加入正在运行事务。
这个强大机制的一个潜在缺点是它隐藏了底层的运行,当它不能正常工作时很难调试。
@Transactional含义
关于@Transactional
,关键点之一是要考虑两个独立的概念,它们都有各自的范围和
生命周期:
- persistence context(持久化上下文)
- database transaction(事务)
@Transactional
本身定义了单个事务的范围。这个事务在persistence context的范围内
。
JPA中的持久化上下文是EntityManager,内部实现使用了Hibernate Session(使用 Hibernate作为持久化provider)。
持久化上下文仅仅是一个同步对象,它记录了有限集合的Java对象的状态,并且保证这些 对象的变化最终持久化到数据库。
这是与单个事务非常不同的概念。一个Entity Manager可以跨越多个事务使用,而且的确 是这样使用的。
EntityManager何时跨越多个事务?
最常见的情况是应用使用Open Session In View模式处理懒初始化异常时,之前的文章 介绍过这种做法的优势和劣势。
这种情况下视图层运行的多个查询处于独立的事务中,而不是单事务的业务逻辑,但这些 查询由相同的entity manager管理。
另一种情况是开发人员将持久化上下文标记为PersistenceContextType.EXTENDED
,
这表示它能够响应多个请求。
如何定义EntityManager和Transaction之间的关系?
这由应用开发者来选择,但是JPA Entity Manager最常用的方式是「Entity Manager per application transaction」(每个事务都有自己的实体管理器)模式。entity manager 注入的常用方法是:
@PersistenceContext private EntityManager em;
这里默认为「Entity Manager per transaction」模式。这种模式下如果在@Transactional
方法内部使用该Entity Manager,那么该方法将在单一事务中运行。
@PersistenceContext如何工作?
随之而来的问题就是@PersistenceContext如何仅在容器启动时注入entity manager,假定 entity manager生命周期很短暂,而且每次请求需要多个entity manager。
答案是它不能:EntityManager是一个接口,注入到spring bean中的不是entity manager 本身,而是在运行时代理具体entity manager的context aware proxy(上下文感知代理) 。
通常用于代理的具体类为SharedEntityManagerInvocationHandler,借助调试器可以确认 这一点。
那么@Transactional如何工作?
实现了EntityManager接口的持久化上下文代理并不是声明式事务管理的唯一部分,事实上 包含三个组成部分:
- EntityManager Proxy本身
- 事务的切面
- 事务管理器
看一下这三部分以及它们之间的相互作用。
事务的切面
事务的切面是一个「around(环绕)」切面,在注解的业务方法前后都可以被调用。实现 切面的具体类是TransactionInterceptor。
事务的切面有两个主要职责:
-
在
before
时,切面提供一个调用点,来决定被调用业务方法应该在正在进行事务的 范围内运行,还是开始一个新的独立事务。 -
在
after
时,切面需要确定事务被提交,回滚或者继续运行。
在before
时,事务切面自身不包含任何决策逻辑,是否开始新事务的决策委派给事务
管理器完成。
事务管理器
事务管理器需要解决下面两个问题:
- 新的Entity Manager是否应该被创建?
- 是否应该开始新的事务?
这些需要事务切面before
逻辑被调用时决定。事务管理器的决策基于以下两点:
- 事务是否正在进行
-
事务方法的propagation属性(比如
REQUIRES_NEW
总要开始新事务)
如果事务管理器确定要创建新事务,那么将:
- 创建一个新的entity manager
- entity manager绑定到当前线程
- 从数据库连接池中获取连接
- 将连接绑定到当前线程
使用ThreadLocal变量将entity manager和数据库连接都绑定到当前线程。
事务运行时他们存储在线程中,当它们不再被使用时,事务管理器决定是否将他们清除。
程序的任何部分如果需要当前的entity manager和数据库连接都可以从线程中获取。
EntityManager proxy
EntityManager proxy(前面已经介绍过)就是谜题的最后一部分。当业务方法调用
entityManager.persist()
时,这不是由entity manager直接调用的。
而是业务方法调用代理,代理从线程获取当前的entity manager,前面介绍过事务管理器 将entity manager绑定到线程。
了解了@Transactional
机制的各个部分,我们来看一下实现它的常用Spring配置。
整合三个部分
如何将三个部分组合起来使事务注解可以正确地发挥作用呢?首先定义entity manager工厂 。
这样就可以通过持久化上下文注解注入Entity Manager proxy。
@Configuration public class EntityManagerFactoriesConfiguration { @Autowired private DataSource dataSource; @Bean(name = "entityManagerFactory") public LocalContainerEntityManagerFactoryBean emf() { LocalContainerEntityManagerFactoryBean emf = ... emf.setDataSource(dataSource); emf.setPackagesToScan( new String[] {"your.package"}); emf.setJpaVendorAdapter( new HibernateJpaVendorAdapter()); return emf; } }
下一步实现配置事务管理器和在@Transactional
注解的类中应用事务的切面。
@Configuration @EnableTransactionManagement public class TransactionManagersConfig { @Autowired EntityManagerFactory emf; @Autowired private DataSource dataSource; @Bean(name = "transactionManager") public PlatformTransactionManager transactionManager() { JpaTransactionManager tm = new JpaTransactionManager(); tm.setEntityManagerFactory(emf); tm.setDataSource(dataSource); return tm; } }
注解@EnableTransactionManagement通知Spring,@Transactional注解的类被事务的切面 包围。这样@Transactional就可以使用了。
常见错误
- MySQL引擎必须是InnoDB才能启用事务。
-
方法上需要使用
@Transactional
才能开启事务,事务必须是public方法 -
@Transactional
除了可以用于方法,还可以用于类,表示这个类所有的public方法都会配置事务。 - 必须是运行期的异常才会触发回滚。
- 多个数据源配置或者多个事务管理器的时候,注意如果操作数据库A,不能使用B的事务,虽然这个问题很幼稚,但是有时候用错难查找问题。
-
如果在Spring中,需要配置
@EnableTransactionManagement
来开启事务,等同于配置xml文件<tx:annotation-driven/>
,但是在Springboot中已经不需要了,在springboot中SpringBootApplication
注解包含了@EnableAutoConfiguration
注解,会自动注入。 - 事务方法不能在同个类里面调用。Spring用切面对方法进行包装,只对外部调用方法进行拦截,内部方法没有进行拦截。
-
使用
new Thread(...).start
新建线程内的事务失效。因为不同的线程使用的是不同SqlSession,相当于另外一个连接,根本不会用到同一个事务。
总结
Spring声明式事务管理机制非常强大,但它可能被误用或者容易发生配置错误。
当这个机制不能正常工作或者未达到预期运行结果等问题出现时,理解它的内部工作情况 是很有帮助的。
需要记住的最重要的一点是,要考虑到两个概念:事务和持久化上下文,每个都有自己 不可读的明显的生命周期。
Spring.quartz
最近在研究Spring中的定时任务功能,最好的办法当然是使用Quartz来实现。对于一个新手来说,花了我不少时间,这里我写个笔记,给大家参考。 我使用的是Maven来管理项目,需要的Jar包我给大家贴出来。
quartz-1.8.5.jar commons-logging.jar spring-core-3.0.5.RELEASE.jar spring-beans-3.0.5.RELEASE.jar spring-context-3.0.5.RELEASE.jar spring-context-support-3.0.5.RELEASE.jar spring-asm-3.0.5.RELEASE.jar spring-expression-3.0.5.RELEASE.jar spring.transaction-3.0.5.RELEASE.jar spring-web-3.0.5.RELEASE.jar Maven的pom.xml的配置:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>QtzTest</groupId> <artifactId>QtzTest</artifactId> <version>1.0</version> <properties> <springframework.version>3.0.5.RELEASE</springframework.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>1.8.5</version> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>7.5.4.v20111024</version> <configuration> <scanIntervalSeconds>10</scanIntervalSeconds> <webApp> <contextPath>/${project.artifactId}</contextPath> </webApp> </configuration> </plugin> </plugins> </build> </project>
特别注意一点,与Spring3.1以下版本整合必须使用Quartz1,最初我拿2.1.3的,怎么搞都报错:
Caused by: org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [org.springframework.scheduling.quartz.CronTriggerBean] for bean with name 'mytrigger' defined in class path resource [applicationContext.xml]: problem with class file or dependent class; nested exception is java.lang.IncompatibleClassChangeError: class org.springframework.scheduling.quartz.CronTriggerBean has interface org.quartz.CronTrigger as super class
查看发现spring3.0.5中org.springframework.scheduling.quartz.CronTriggerBean继承了org.quartz.CronTrigger(public class CronTriggerBeanextends CronTrigger),而在quartz2.1.3中org.quartz.CronTrigger是个接口(publicabstract interface CronTrigger extends Trigger),而在quartz1.8.5及1.8.4中org.quartz.CronTrigger是个类(publicclass CronTrigger extends Trigger),从而造成无法在applicationContext中配置触发器。这是spring3.1以下版本和quartz2版本不兼容的一个bug。(感谢tiren的回复,spring3.1以及以后版本支持quartz2)
在Spring中使用Quartz有两种方式实现:第一种是任务类继承QuartzJobBean,第二种则是在配置文件里定义任务类和要执行的方法,类和方法仍然是普通类。很显然,第二种方式远比第一种方式来的灵活。
第一种方式的JAVA代码:
Java代码 收藏代码
package com.ncs.hj; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean; public class SpringQtz extends QuartzJobBean{ private static int counter = 0; protected void executeInternal(JobExecutionContext context) throws JobExecutionException { System.out.println(); long ms = System.currentTimeMillis(); System.out.println("\t\t" + new Date(ms)); System.out.println(ms); System.out.println("(" + counter++ + ")"); String s = (String) context.getMergedJobDataMap().get("service"); System.out.println(s); System.out.println(); } }
第二种方式的JAVA代码: Java代码 收藏代码
package com.ncs.hj; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean; import java.util.Date; public class SpringQtz { private static int counter = 0; protected void execute() { long ms = System.currentTimeMillis(); System.out.println("\t\t" + new Date(ms)); System.out.println("(" + counter++ + ")"); } }
Spring的配置文件: Xml代码 收藏代码
<!------------ 配置调度程序quartz ,其中配置JobDetail有两种方式--------------> <!--方式一:使用JobDetailBean,任务类必须实现Job接口 --> <bean id="myjob" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="name" value="exampleJob"></property> <property name="jobClass" value="com.ncs.hj.SpringQtz"></property> <property name="jobDataAsMap"> <map> <entry key="service"><value>simple is the beat</value></entry> </map> ;/property> </bean> <!--运行时请将方式一注释掉! --> <!-- 方式二:使用MethodInvokingJobDetailFactoryBean,任务类可以不实现Job接口,通过targetMethod指定调用方法--> <!-- 定义目标bean和bean中的方法 --> <bean id="SpringQtzJob" class="com.ncs.hj.SpringQtz"/> <bean id="SpringQtzJobMethod" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject"> <ref bean="SpringQtzJob"/> </property> <property name="targetMethod"> <!-- 要执行的方法名称 --> <value>execute</value> </property> </bean> <!-- ======================== 调度触发器 ======================== --> <bean id="CronTriggerBean" class="org.springframework.scheduling.quartz.CronTriggerBean"> <property name="jobDetail" ref="SpringQtzJobMethod"></property> <property name="cronExpression" value="0/5 * * * * ?"></property> </bean> <!-- ======================== 调度工厂 ======================== --> <bean id="SpringJobSchedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="CronTriggerBean"/> </list> </property> </bean>
关于cronExpression表达式,这里讲解一下:
字段 允许值 允许的特殊字符 秒 0-59 , - * / 分 0-59 , - * / 小时 0-23 , - * / 日期 1-31 , - * ? / L W C 月份 1-12 或者 JAN-DEC , - * / 星期 1-7 或者 SUN-SAT , - * ? / L C # 年(可选) 留空, 1970-2099 , - * / 表达式意义 "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期间的每1分钟触发 "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 "0 10,44 14 ? 3 WED" 每年三月的星期三的下午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 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发 每天早上6点 0 6 * * * 每两个小时 0 */2 * * * 晚上11点到早上8点之间每两个小时,早上八点 0 23-7/2,8 * * * 每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点 0 11 4 * 1-3 1月1日早上4点 0 4 1 1 *
最后别忘了在web.xml里面配置Spring:
<?xml version="1.0" encoding="UTF-8"?> <web-app 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" version="2.5"> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-config.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
运行结果:
Wed Feb 08 13:58:30 CST 2012 (0) Wed Feb 08 13:58:35 CST 2012 (1) Wed Feb 08 13:58:40 CST 2012 (2) Wed Feb 08 13:58:45 CST 2012 (3) Wed Feb 08 13:58:50 CST 2012 (4) Wed Feb 08 13:58:55 CST 2012 (5) Wed Feb 08 13:59:00 CST 2012 (6)
spring 4 quartz 2.2
一、首先加入spring(4.1.9.RELEASE)的依赖包,然后再加入quartz(2.2.1)的包,如下:
Xml代码 收藏代码
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.2.1</version> </dependency>
二、配置spring.xml文件
spring.xml文件可能有人喜欢用applicationContext.xml命名,这个是Spring的主配置文件,非springMvc.xml
1、配置定时任务Java类
Xml代码 收藏代码
<!-- 配置任务bean类 --> <bean id="billsCheckJob" class="com.chinagas.biz.task.BillsCheckJob"></bean>
Java类代码如下:
Java代码 收藏代码
public class BillsCheckJob{ private Logger log = Logger.getLogger(BillsCheckJob.class); public void runTask(){ log.info("===========runTask()"); } }
也可以采用注解方式,这样Bean就不用在xml配置了,看个人喜好吧: Java代码 收藏代码
@Component("billsCheckJob") public class BillsCheckJob{ private Logger log = Logger.getLogger(BillsCheckJob.class);
public void runTask(){ log.info("===========runTask()"); } }
2、配置定时任务详细jobDetail,其中有对应的任务执行类:ref="billsCheckJob",及里面的执行方法:runTask
方法一、方法二只需要选择一种。
方法一:方法映射
Xml代码 收藏代码
<!-- 配置方法映射工厂类 --> <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="billsCheckJob"></property> <property name="targetMethod" value="runTask"></property> <property name="concurrent" value="false"></property> <!-- concurrent : false表示等上一个任务执行完后再开启新的任务 --> </bean>
方法二:继承QuartzJobBean类,实现executeInternal方法
Xml代码 收藏代码
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> <property name="jobClass" value="com.chinagas.biz.task.ExtendsJob"> </property> <property name="durability" value="true"></property> </bean>
ExtendsJob类如下: Java代码 收藏代码
public class ExtendsJob extends QuartzJobBean{ private Logger log = Logger.getLogger(ExtendsJob.class); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { log.info("===========ExtendsJob runTask()"); } }
3、配置定时任务的执行时间或周期,同时关联任务详细jobDetail
方法一、方法二只需要选择一种。
(1)方法一:
使用CronTriggerFactoryBean类配置,这个方法的好处在于可以使用cronExpression表达式。
Xml代码 收藏代码
<!-- 配置任务高度的的时间/周期 --> <bean id="billsCheckJobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="jobDetail"></property> <property name="cronExpression" value="0 */1 * * * ?"></property> <property name="startDelay" value="3000"></property> </bean>
(2)方法二
Xml代码 收藏代码
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"> <property name="jobDetail" ref="jobDetail"/> <property name="startDelay" value="5000"/> <property name="repeatInterval" value="5000"/> </bean>
4、配置SchedulerFactoryBean类,调用时间触发
Xml代码 收藏代码
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <!-- <ref bean="billsCheckJobTrigger"/> --> <ref bean="simpleTrigger"/> </list> </property> </bean>
配置完后就可以启动看效果了。
5、cronExpression表达式
cron的表达式被用来配置CronTrigger实例。 cron的表达式是字符串,实际上是由七子表达式,描述个别细节的时间表。这些子表达式是分开的空白,代表:
1. Seconds 2. Minutes 3. Hours 4. Day-of-Month 5. Month 6. Day-of-Week 7. Year (可选字段)
例 "0 0 12 ? * WED" 在每星期三下午12:00 执行,
个别子表达式可以包含范围, 例如,在前面的例子里("WED")可以替换成 "MON-FRI", "MON, WED, FRI"甚至"MON-WED,SAT".
「*」 代表整个时间段.
每一个字段都有一套可以指定有效值,如
Seconds (秒) :可以用数字0-59 表示,
Minutes(分) :可以用数字0-59 表示,
Hours(时) :可以用数字0-23表示,
Day-of-Month(天) :可以用数字1-31 中的任一一个值,但要注意一些特别的月份
Month(月) :可以用0-11 或用字符串 「JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC」 表示
Day-of-Week(每周):可以用数字1-7表示(1 = 星期日)或用字符口串「SUN, MON, TUE, WED, THU, FRI and SAT」表示
「/」:为特别单位,表示为「每」如「0/15」表示每隔15分钟执行一次,「0」表示为从「0」分开始, 「3/20」表示表示每隔20分钟执行一次,「3」表示从第3分钟开始执行
「?」:表示每月的某一天,或第周的某一天
「L」:用于每月,或每周,表示为每月的最后一天,或每个月的最后星期几如「6L」表示「每月的最后一个星期五」
「W」:表示为最近工作日,如「15W」放在每月(day-of-month)字段上表示为「到本月15日最近的工作日」
「#」:是用来指定「的」每月第n个工作日,例 在每周(day-of-week)这个字段中内容为"6#3" or "FRI#3" 则表示「每月第三个星期五」
1)Cron表达式的格式:秒 分 时 日 月 周 年(可选)。
字段名 允许的值 允许的特殊字符 秒 0-59 , - * / 分 0-59 , - * / 小时 0-23 , - * / 日 1-31 , - * ? / L W C 月 1-12 or JAN-DEC , - * / 周几 1-7 or SUN-SAT , - * ? / L C # 年 (可选字段) empty, 1970-2099 , - * /
「?」字符:表示不确定的值
「,」字符:指定数个值
「-」字符:指定一个值的范围
「/」字符:指定一个值的增加幅度。n/m表示从n开始,每次增加m
「L」字符:用在日表示一个月中的最后一天,用在周表示该月最后一个星期X
「W」字符:指定离给定日期最近的工作日(周一到周五)
「#」字符:表示该月第几个周X。6#3表示该月第3个周五
2)Cron表达式范例:
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
6、官网文档
官方quartz的Jar包下载地址:
http://www.quartz-scheduler.org/downloads/
Spring官方配置文档:
http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#scheduling-quartz