1、Spring介绍
(1) 概述
Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。 Spring 根据代码的功能特点,使用 Ioc 降低业务对象之间耦合度。IoC 使得主业务在相 调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring 容器统一管理,自动“注入”,注入即赋值。 而 AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成“织入”。
(2) spring容器
Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象,容器是装东西的, Spring 容器不装文本,数字。装的是对象。 Spring 是存储对象的容器。
-
放入容器的对象:将dao类,service类,controller类,工具类放入容器
-
不放入容器的对象:实体类对象,实体类数据来自数据库。servlet,listener,filter等,这些对象应该放在tomcat容器中。
(3) spring的优点
-
轻量:Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需 的 jar 总共在 3M 左右。 Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar
-
针对接口编程:解耦合 Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。
-
AOP编程的支持:通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
-
方便集成各种优秀框架:Spring 不排斥各种优秀的开源框架,Spring 提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持,简化框架的使用。
2、IOC控制反转
(1) IOC介绍
控制反转(IoC, Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。 通过容器实现对象的创建,属性赋值,依赖的管理。
Spring 框架使用依赖注入(DI)实现 IoC。底层使用的是反射机制,将创建的对象放在map中。
Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称为 Bean。 Spring 容器管理着容器中 Bean 之间的依赖关系, Spring 使用“依赖注入”的方式来管理 Bean 之间的依赖关系。 使用 IoC 实现对象之间的解耦和。
(2) 使用spring容器管理对象步骤(朴素用法)
-
依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency>
-
定义Service接口与实体类
-
创建 Spring 配置文件,在配置文件中使用<bean>标签声明对象,会将创建好的对象放入到spring容器中。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" 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.xsd"> <bean id="someService" class="com.luca.service.SomeServiceImpl"/> </beans>
-
获取容器中的对象
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); //创建spring容器对象 SomeService service = context.getBean("someService", SomeService.class); //通过bean的id获取容器中的对象
(3) 原理
ApplicationContext 容器,会在容器对象初始化时,将其中的所有对象一次性全部装配好。以后代码中若要使用到这些对象,只需从内存中直接获取即可。
3、基于XML的注入(DI)
(1) set注入:要求java对象属性实现setXXX方法
<!--1.简单类型注入--> bean id="school" class="com.luca.domain.School"> <property name="name" value="北工大"></property> <property name="address" value="平乐园100号"></property> </bean> <!--2.引用类型注入--> <bean id="stu" class="com.luca.domain.Student"> <property name="name" value="张三"></property> <property name="school" ref="school"></property> </bean>
(2) 构造注入:要求java对象实现带参构造方法
<bean id="stu2" class="com.luca.domain.Student"> <constructor-arg index="0" value="李四"/> <!--index:指明该参数对应着构造器的第几个参数,从 0 开始。--> <constructor-arg name="age" value="39"/> <!--name:指定参数名称。--> <constructor-arg name="school" ref="school"/> </bean>
(3) 引用类型的自动注入
<bean id="school" class="com.luca.domain.School"> <property name="name" value="北工大"></property> <property name="address" value="平乐园100号"></property> </bean> <!--给com.luca.domain.Student对象自动注入school属性--> <!--byname方式:要求school属性的名称与上面创建的bean的id一致--> <bean id="stu3" class="com.luca.domain.Student" autowire="byName"> <property name="name" value="张三"></property> <property name="age" value="18"></property> </bean> <!--byType方式:要求school属性的类型与上面创建的bean的class一致--> <bean id="stu4" class="com.luca.domain.Student" autowire="byType"> <property name="name" value="张三"></property> <property name="age" value="18"></property> </bean>
(4) 为应用指定多个 Spring 配置文件
为了避免配置文件变得非常庞大、臃肿,提高配置文件的可读性与可维护性,可以将Spring 配置文件分解成多个配置文件。
多个配置文件中有一个总文件,总配置文件将各其它子文件通过<import/>引入。在 Java代码中只需要使用总配置文件对容器进行初始化即可。
-
<import/>引入多个配置文件
<import resource="classpath:spring-school.xml"/> <import resource="classpath:spring-student.xml"/>
-
使用通配符*
//要求父配置文件名不能满足*所能匹配的格式,否则将出现循环递归包含。就本例而言,父配置文件不能匹配 spring-*.xml 的格式,即不能起名为spring-total.xml。 <import resource="classpath:spring-*.xml"/>
4、基于注解的注入(DI)
(1) 配置组件扫描器
对于 DI 使用注解,将不再需要在 Spring 配置文件中声明 bean 实例。Spring 中使用注解,需要在原有 Spring 运行环境基础上再做一些改变。需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。
<!--开启组件扫描,把base-package中所有包含@Component,@Repository,@Service,@Controller注解的类对象添加到spring容器中-->
<context:component-scan base-package="com.luca"/>
指定多个包的三种方式:
-
使用多个 context:component-scan 指定不同的包路径
<context:component-scan base-package="com.luca.service"/> <context:component-scan base-package="com.luca.domain"/>
-
指定 base-package 的值使用分隔符
<!--分隔符可以使用逗号(,)分号(;)还可以使用空格,不建议使用空格。--> <context:component-scan base-package="com.luca.service,com.luca.domain"/>
-
base-package 是指定到父包名
<!--base-package 的值表是基本包,容器启动会扫描包及其子包中的注解,当然也会扫描到子包下级的子包。--> <context:component-scan base-package="com.luca"/>
(2) 定义Bean的注解
以下注解的value属性用于指定该bean的id值。@Component不指定value属性,bean的id是类名的首字母小写。
-
@Component
-
@Repository:用于对DAO实现类进行注解
-
@Service:用于对Service实现类进行注解
-
@Controller:用于对Controller实现类进行注解
(3) 属性注入
@Value:简单类型属性注入
@Component("myschool") public class School { @Value("湖北工业大学") //使用该注解完成属性注入时,类中无需setter。当然,若属性有setter,则也可将其加到setter上。 private String name; @Value("湖北省武汉市洪山区南李路28号") private String address; }
(4) 引用类型注入
-
@Autowired:需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配 Bean 的方式。
//byType方式注入,使用该注解完成属性注入时,类中无需setter。若属性有setter,则也可将其加到setter上。 @Autowired private School school; //byName方式注入 @Autowired @Qualifier("myschool") //@Qualifier的value属性用于指定要匹配的Bean的id值。 private School school; //@Autowired 还有一个属性 required,默认值为 true,表示当匹配失败后,会终止程序运行。若将其值设置为 false,则匹配失败,将被忽略,未匹配的属性值为 null。 @Autowired(required=false) private School school;
-
@Resource:JDK提供的注解,默认按名称注入。
//byType方式:@Resource注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入bean,则会按照类型进行Bean的匹配注入。 @Resource private School school; //byName方式:@Resource注解指定其name属性,则name的值即为按照名称进行匹配的Bean的id。 @Resource("myschool") private School school;
(5) 注解与XML方式注入对比
-
注解优点是:
-
直观
-
方便
-
高效(代码少,没有配置文件的书写那么复杂)。
-
缺点:以硬编码的方式写入到 Java 代码中,修改是需要重新编译代码的。
-
XML 方式优点是:
-
配置和代码是分离的
-
在 xml 中做修改,无需编译代码,只需重启服务器即可将新的配置加载。
-
xml 的缺点是:编写麻烦,效率低,大型项目过于复杂。
5、AOP面向切面编程介绍
(1) 不使用AOP的开发方式
需要在业务方法中添加非业务方法
-
写两个非业务方法。非业务方法也称为交叉业务逻辑:doTransaction():用于事务处理doLog():用于日志处理然后,再使主业务方法调用它们。
public void doSome(String name, Integer age) { doLog(); //非业务方法 System.out.println("业务方法doSome("); //业务方法的功能 doTrans(); //非业务方法 }
-
定义非业务方法的工具类,在业务方法中调用
public void doSome(String name, Integer age) { ServiceTools.doLog(); //工具类中的非业务方法 System.out.println("业务方法doSome("); //业务方法的功能 ServiceTools.doTrans(); //工具类中的非业务方法 }
以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。
-
采用动态代理方式,扩展和增强业务方法功能。
public class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ServiceTools.doLog(); //在业务方法之前调用非业务方法 Object obj = method.invoke(target, args); //执行目标方法,即target对象的方法 ServiceTools.doTrans(); //在业务方法之后调用非业务方法 return obj; } } //在主程序中使用调用动态代理的方法 public class MyApp { public static void main(String[] args) { SomeService target = new SomeServiceImpl(); //使用jdk的Proxy创建代理对象,创建目标对象 InvocationHandler handler = new MyIncationHandler(target); //创建InvocationHandler对象 SomeService proxy = (SomeService) Proxy.newProxyInstance( //使用Proxy创建代理 target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); //通过代理执行方法,会调用handler中的invoke() proxy.doSome(); proxy.doOther(); } }
(2) AOP简介
-
AOP(Aspect Orient Programming),面向切面编程是从动态角度考虑程序运行过程。AOP底层采用动态代理模式实现,采用了两种代理:JDK的动态代理,与CGLIB的动态代理。
-
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。
-
面向切面编程的好处
-
减少重复;
-
专注业务;
-
注意:面向切面编程只是面向对象编程的一种补充。
(3) AOP术语
-
切面(Aspect:切面泛指交叉业务逻辑。上例中的事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。
-
连接点(JoinPoint):连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。
-
切入点(Pointcut):切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
-
目标对象(Target):目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。上例中的StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。
-
通知(Advice):通知表示切面的执行时间, Advice也叫增强。 上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说, 通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
-
切入点定义切入的位置,通知定义切入的时间。
6、AspectJ对AOP的实现
(1) AspectJ 的通知类型
-
前置通知
-
后置通知
-
环绕通知
-
异常通知
-
最终通知
(2) AspectJ 的切入点表达式
execution(modifiers-pattern? ret-type-patterndeclaring-type-pattern?name-pattern(param-pattern)throws-pattern?)
-
modifiers-pattern:访问权限类型
-
ret-type-pattern:返回值类型
-
declaring-type-pattern:包名类名
-
name-pattern(param-pattern):方法名(参数类型和参数个数)
-
throws-pattern:抛出异常类型
-
?:表示可选的部分
以上表达式共 4 个部分:execution(访问修饰符 方法返回值 包名.类名.方法名(参数) 异常类型) 表达式中访问修饰符和异常类型可以省略,各部分间用空格分开。可以使用以下符号:
-
execution(public * *(..)):任意公共方法。
-
execution(* set*(..)):任何一个以“set”开始的方法。
-
execution(* com.xyz.service.*.*(..)):在 service 包里的任意类的任意方法。
-
execution(* com.xyz.service..*.*(..)):在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
-
execution(* *..service.*.*(..)):所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
(3) 依赖与约束
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency>
AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式。
(4) AspectJ基于注解的AOP实现流程
-
定义业务接口与实现类
-
定义切面类
//@Aspect 是aspectj框架中的注解。作用:表示当前类是切面类 @Aspect public class MyAspectj { //前置通知方法的定义要求:1)公共方法public;2)方法没有返回值;3)方法名称自定义;4)方法可以有参数,也可以无参数。如果有参数,参数不是自定义的,有几个参数类型可以使用 @Before("execution(* *..SomeServiceImpl.doSome(..))") public void myBefore1() { //切面执行的功能代码 System.out.println("切面功能:在目标方法之前输出执行时间:" + new Date()); } }
-
声明目标对象,切面类对象
<bean id="someService" class="com.luca.service.SomeServiceImpl"/> <!--声明切面对象--> <bean id="myAspect" class="com.luca.service.MyAspectj"/>
-
注册AspectJ的自动代理
<!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象--> <aop:aspectj-autoproxy/>
<aop:aspectj-autoproxy/>的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的,是基于 AspectJ 的注解适配自动代理生成器。<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。
-
调用目标对象,即业务方法,就能实现功能增强
(5) @Before前置通知-方法有JoinPoint参数
JoinPoint 类型参数。该对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。所有的通知方法均可包含JoinPoint 类型参数
/* * 指定通知方法中的参数:JoinPoint * JoinPoint:业务方法,要加入切面功能的业务方法 * 作用是:可以在通知方法中获取方法执行时的信息,例如方法名称,方法的实参 * 如果需要用到业务方法的信息,就可以加入JoinPoint * JoinPoint的值是由框架赋予的,必须是第一个位置的参数 */ @Before("execution(* com.luca.service.SomeServiceImpl.doSome(String,Integer))") public void myBefore2(JoinPoint jp) { //切面执行的功能代码 System.out.println("方法的名称:" + jp.getSignature().getName()); for (Object arg : jp.getArgs()) { System.out.println("参数:" + arg); } System.out.println("切面功能:在目标方法之前输出执行时间:" + new Date()); }
(6) @AfterReturning 后置通知-注解有 returning 属性
可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。
/* *后置通知方法的定义要求:1)公共方法public;2)方法没有返回值;3)方法名称自定义;4)方法有参数,推荐使用Object,参数名自定义 *@AfterReturning:后置通知 * 属性:1.value 切入点表达式 * 2.returning 自定义变量,表示目标方法的返回值 * 自定义变量名必须和通知方法的形参名一样 * 特点: * 1、在目标方法之后执行 * 2、能够获取到目标方法的返回值,根据返回值做不同的处理功能 * Object res = doOther(); * 3、修改返回值 */ @AfterReturning(value = "execution(* com.luca.service.SomeServiceImpl.doOther(..))",returning = "res") public void myAfterReturning(Object res) { //Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理 System.out.println("后置通知:在目标方法之后执行的,获得的返回值是:" + res); }
(7) @Around 环绕通知-增强方法有ProceedingJoinPoint参数
在目标方法执行之前之后执行。被注解为环绕增强的方法要有Object 类型的返回值。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。
@Around("myPointCut()") public Object myAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕通知,在方法之前执行"); String obj = (String) pjp.proceed(); System.out.println("环绕通知,在方法之后执行"); return obj+"********"; }
(8) @AfterThrowing 异常通知-注解中有 throwing 属性
在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。TODO:SpringMVC异常映射机制是否是通过这种方式实现的
(9) @After 最终通知
无论目标方法是否抛出异常,该增强均会被执行
(10) @Pointcut 定义切入点
当较多的通知增强方法使用相同的 execution 切入点表达式时,可以使用AspectJ 提供的@Pointcut 注解,定义 execution 切入点表达式。
-
将@Pointcut 注解在一个方法之上,以后所有通知方法的execution的value属性值均可使用该方法名作为切入点。
-
使用@Pointcut 注解的方法一般使用 private 的标识方法,没有实际作用。
@Pointcut("execution(* com.luca.service.SomeService.doFirst(..))") public void myPointCut() {} 使用:见(7)@Around 环绕通知
7、Spring事务管理API
事务原本是数据库中的概念,在Dao层。但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特性来管理具体的业务。在 Spring 中通常可以通过以下两种方式来实现对事务的管理:
-
使用 Spring 的事务注解管理事务
-
使用 AspectJ 的 AOP 配置管理事务
(1) 事务管理接口
-
事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。 有两个常用的实现类:
-
DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
-
HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。
-
Spring 事务的默认回滚方式是: 发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。
-
Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时, 才能通过 Java 虚拟机或者 Java 的 throw 语句抛出。
-
Error 是程序在运行过程中出现的无法处理的错误,当这些错误发生时程序是无法处理,JVM一般会终止线程。
-
运行时异常(RuntimeException 类及其子类),只有在运行时才出现的异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)。
-
受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理,则无法通过编译。
(2) 事务定义接口
事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。
-
五个事务隔离级别常量
-
DEFAULT: 采用 DB 默认的事务隔离级别。 MySql 的默认为 REPEATABLE_READ; Oracle默认为 READ_COMMITTED。
-
READ_UNCOMMITTED: 读未提交。未解决任何并发问题。
-
READ_COMMITTED: 读已提交。解决脏读,存在不可重复读与幻读。
-
REPEATABLE_READ: 可重复读。解决脏读、不可重复读,存在幻读
-
SERIALIZABLE: 串行化。不存在并发问题。
-
七个事务传播行为常量
-
PROPAGATION_REQUIRED: 若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。Spring 默认的事务传播行为。
-
PROPAGATION_REQUIRES_NEW: 总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
-
PROPAGATION_SUPPORTS: 支持当前事务,但若当前没有事务,也可以以非事务方式执行。
-
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
-
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
-
PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
-
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
-
默认事务超时时限
-
常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限, sql 语句的执行时长。使用默认值。
(3) @Transactional注解
通过@Transactional注解方式,可将事务织入到相应public方法中,实现事务管理。@Transactional的所有可选属性如下所示:
-
propagation: 用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为Propagation.REQUIRED。
-
isolation: 用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为Isolation.DEFAULT。
-
readOnly: 用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false。
-
timeout: 用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为-1,即没有时限。
-
rollbackFor: 指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
-
rollbackForClassName: 指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
-
noRollbackFor: 指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
-
noRollbackForClassName: 指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
注意:@Transactional若用在方法上,只能用于public方法上。对于其他非public方法,如果加上了注解@Transactional,虽然Spring不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。
(4) 实现注解的步骤
-
声明事务管理器
<!--1.声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接数据库--> <property name="dataSource" ref="dataSource"/> </bean>
-
开启注解驱动
<!--2.开启事务注解驱动,让spring使用注解管理驱动,创建代理对象。transaction-manager:事务管理器bean的id--> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
-
业务层public方法加入事务属性
@Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false, rollbackFor = {NullPointerException.class,NotEnoughException.class} ) @Override public void buy(Integer GoodsId, Integer amount) { //业务功能 }
8、AspectJ的AOP配置管理事务
使用 XML 配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类较多,配置文件会变得非常臃肿。
使用 XML 配置顾问方式可以自动为每个符合切入点表达式的类生成事务代理。其用法很简单,只需将前面代码中关于事务代理的配置删除,再替换为如下内容即可。
(1) 依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.5.RELEASE</version> </dependency>
(2) 步骤
-
在容器中添加事务管理器
<!--声明式事务处理:和源代码完全分离--> <!--1.声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接数据库--> <property name="dataSource" ref="dataSource"/> </bean>
-
配置事务通知
<!--2.声明业务方法的事务属性(隔离级别,传播行为,超出时间) id:自定义名称,表示<tx:advice>和</tx:advice>之间的配置内容 --> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <!-- tx:attributes:配置事务属性 --> <tx:attributes> <!--tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务 name:方法名称,1)完整的方法名称,不带有包和类 2)方法可以使用通配符,*表示任意字符 --> <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"/> <!--使用通配符,指定很多的方法--> <!--建立统一命名规范,指定添加方法--> <tx:method name="add*"/> <!--指定修改方法--> <tx:method name="modify*"/> <!--删除方法--> <tx:method name="remove*"/> <!--查询方法,query,search,find--> <tx:method name="*" read-only="true"/> </tx:attributes> </tx:advice>
-
配置增强器
<!--3.配置AOP--> <aop:config> <!--配置切入点表达式:指定哪些包中的类要使用事务 id:切入点表达式的名称,唯一值 expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理事务 --> <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/> <!--配置增强器:关联advice和pointcut advice-ref:通知,上面 tx:advice 的相关配置信息 pointcut-ref:切入点表达式的id --> <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/> </aop:config>
-
此时"* *..service..*.*(..)"对应的方法上已经有了添加的事务,不用再使用@Transactional注解。
9、Spring的监听器ContextLoaderListener
在 Web 项目中使用 Spring 框架,首先要解决在 web 层(这里指 Servlet) 中获取到 Spring容器的问题。只要在 web 层获取到了 Spring 容器,便可从容器中获取到 Service 对象。
(1) 存在的问题:spring容器不唯一
-
Spring容器放在Servlet的doGet()或doPost()方法中创建,每次提交请求都会创建spring容器
-
Spring容器放在 Servlet 初始化时创建,即执行 init()方法时创建。会有多个servlet,每次初始化都会创建spring容器
(2) 使用Spring的监听器ContextLoaderListener
对于Web应用来说,ServletContext 对象是唯一的,该对象是在Web应用装载时初始化的。ServletContext初始化时创建Spring容器,就保证了Spring容器在整个应用中的唯一性。 将创建好的Spring容器,以属性的形式放入到ServletContext的空间中,就保证了Spring容器的全局性。
(3) 依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.5.RELEASE</version> </dependency>
(4) 注册监听器ContextLoaderListener
在 web.xml 中注册ContextLoaderListener监听器,来监听ServletContext。
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
Spring 为该监听器接口定义了一个实现类ContextLoaderListener, 完成了两个很重要的工作:1)创建容器对象;2)将容器对象放入到了 ServletContext 的空间中
原理分析:ContextLoaderListener的初始化方法contextInitialized()实现了:
this.context = createWebApplicationContext(servletContext); //创建容器对象 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); //将容器对象放入到了ServletContext的空间中
(5) 指定 Spring 配置文件的位置<context-param>
<context-param> <param-name>contextConfigLocation</param-name> <!--spring配置文件的位置--> <param-value>classpath:conf/applicationContext.xml</param-value> </context-param>
(6) 获取spring容器
-
直接从ServletContext中获取
WebApplicationContext ac = (WebApplicationContext) this.getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
-
通过WebApplicationContextUtils获取
WebApplicationContext ac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
10、Spring配置文件总结
-
<bean>:声明对象
-
<context:component-scan>:组件扫描器
-
整合mybatis
-
<context:property-placeholder>:加载数据库属性文件
-
<bean class="com.alibaba.druid.pool.DruidDataSource"/>:声明数据源
-
<bean class="org.mybatis.spring.SqlSessionFactoryBean">:声明SqlSessionFactoryBean对象
-
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">:声明mybatis扫描器,创建dao对象
-
AOP实现
-
<bean class="com.luca.service.MyAspectj"/>:声明切面对象
-
<aop:aspectj-autoproxy/>:aspectj自动代理生成器
-
spring事务管理
-
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">:声明事务管理器
-
<tx:annotation-driven>:开启事务注解驱动
-
aspectj事务管理
-
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">:声明事务管理器
-
<tx:advice>:配置事务通知
-
<aop:config>:配置增强器
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" 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.xsd"> <!--创建对象--> <bean id="someService" class="com.luca.service.SomeServiceImpl"/> <!--开启组件扫描--> <context:component-scan base-package="com.luca"/> <!--整合MyBatis--> <!--加载外部属性文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--声明数据源DataSource,作用是连接数据库--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--set注入给DruidDataSource提供连接数据库信息--> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxActive" value="${jdbc.maxActive}"/> </bean> <!--声明mybatis提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionaFactory对象--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--set注入,把数据库连接池付给了dataSource属性--> <property name="dataSource" ref="dataSource"/> <!--mybatis主配置文件的位置--> <property name="configLocation" value="classpath:mybatis.xml"/> </bean> <!--注册Mapper扫描配置器--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定SqlSessionFactory对象的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!--指定包名,包名是dao接口所在的包名--> <property name="basePackage" value="com.luca.dao"/> </bean> <!--Aspectj实现AOP--> <!--声明切面对象--> <bean id="myAspect" class="com.luca.service.MyAspectj"/> <!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象--> <aop:aspectj-autoproxy/> <!--Spring事务--> <!--1.声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接数据库--> <property name="dataSource" ref="dataSource"/> </bean> <!--2.开启事务注解驱动,让spring使用注解管理驱动,创建代理对象--> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/> <!--Aspectj事务管理--> <!--声明式事务处理:和源代码完全分离--> <!--1.声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--2.声明业务方法的事务属性(隔离级别,传播行为,超出时间) id:自定义名称,表示<tx:advice>和</tx:advice>之间的配置内容 --> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <!-- tx:attributes:配置事务属性 --> <tx:attributes> <!--tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务 name:方法名称,1)完整的方法名称,不带有包和类 2)方法可以使用通配符,*表示任意字符 --> <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"/> <!--使用通配符,指定很多的方法--> <!--建立统一命名规范,指定添加方法--> <tx:method name="add*"/> <!--指定修改方法--> <tx:method name="modify*"/> <!--删除方法--> <tx:method name="remove*"/> <!--查询方法,query,search,find--> <tx:method name="*" read-only="true"/> </tx:attributes> </tx:advice> <!--3.配置AOP--> <aop:config> <!--配置切入点表达式:指定哪些包中的类要使用事务 id:切入点表达式的名称,唯一值 expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理事务 --> <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/> <!--配置增强器:关联advice和pointcut advice-ref:通知,上面 tx:advice 的相关配置信息 pointcut-ref:切入点表达式的id --> <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/> </aop:config> </beans>
11、思维导图