本系列文章:
Spring(一)IOC、DI、@Autowired、@Resource、作用域
Spring(二)IOC容器的初始化流程
Spring(三)IOC容器的依赖注入流程
Spring(四)IOC容器的高级特性
Spring(五)AOP、事务
Spring(六)Spring MVC
Spring(七)SpringBoot
Spring(八)Spring Cloud
AOP本质:在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、日志代码、事务控制代码、性能监控代码。
AOP: Aspect oriented Programming,面向切面编程。
AOP是OOP的延续(或完善)
。
OOP的特征:封装、继承和多态。OOP是一种垂直继承体系:
OOP编程思想可以解决大多数的代码重复问题,但是有一些情况是处理不了的。比如权限校验、日志追踪等非业务性代码会多次重复出现。这种代码可以简单理解为横切逻辑代码:
横切逻辑代码存在的问题:
- 横切代码重复问题。
- 横切逻辑代码和业务代码混杂在一起,代码臃肿,维护不方便。
因此AOP解决的问题是:在不改变原有业务逻辑情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。
切
:指的是横切逻辑,原有业务逻辑代码我们不能动,只能操作横切逻辑代码,所以面向横切逻辑。
面
:横切逻辑代码往往要影响的是很多个方法,每个方法都如同一个点,多个点构成面。
AOP,一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块
。这个模块被命名为“切面”(Aspect),这样就减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
AOP可用于日志管理、权限认证、安全检查、事务控制
等。
通知和切入点共同定义了切面的全部内容【AOP核心1:可以简单理解为要实现切面功能的那个类】
。指方法,在Spring AOP中,一个连接点总是代表一个方法的执行
。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。切点的定义会匹配通知所要织入的一个或多个连接点【AOP核心3:定义一个方法,和原有业务代码中需要实现AOP功能的相应方法关联起来】
。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。切面的工作被称为通知【AOP核心2:需要在连接点实现的具体功能,有五种通知类型】
。被一个或者多个切面(aspect)所通知(advise)的对象
。它通常是一个代理对象。也有人把它叫做被通知(adviced) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。织入是把切面应用到目标对象并创建新的代理对象的过程
。在目标对象的生命周期里有多少个点可以进行织入:
- 编译期
切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。- 类加载期
切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。- 运行期
切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。
AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。
AspectJ静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring提供了两种方式来生成代理对象: JDK动态代理和Cglib动态代理。
修改其(代理对象类的class文件)字节码生成子类
来处理。
- 添加CGLIB库;
- 在Spring配置文件中加入
。
JDK动态代理主要涉及到java.lang.reflect包中的两个类:Proxy(Proxy.newProxyInstance()方法)和InvocationHandler。
InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。
Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
将被代理类作为构造函数的参数传入代理类proxy,调用Proxy.newProxyInsatnce(classloader,interfaces,handler)方法生成代理类,实现Invocation接口,重写invoke()方法,调用被代理类方法时默认调用此方法。
总结: 代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。
CGLib(Code Generation Library),是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展Java类与实现Java接口,CGLib封装了asm,可以再运行期动态生成新的class。和JDK动态代理相比较:JDK 创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过CGLib创建动态代理。
生成对象类型为Enhancer。实现原理类似于JDK动态代理,只是他在运行期间生成的代理对象是针对目标类扩展的子类。
JDK动态代理只能对实现了接口的类生成代理,而不能针对类
;CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法因为是继承
,所以该类或方法最好不要声明成final ; 通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的Bean中。代理封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标Bean方法之前,会执行切面逻辑
。
直到应用需要被代理的bean时,Spring才创建代理对象
。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有Bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
在AOP术语中,切面的工作被称为通知,实际上是程序执行时要通过Spring AOP框架触发的代码段。
Spring切面可以应用5种类型的通知:
五种通知的常见使用场景:
前置通知---->记录日志(方法将被调用)
环绕通知---->控制事务、权限控制
后置通知---->记录日志(方法已经成功调用)
异常通知---->异常处理、控制事务
最终通知---->记录日志(方法已经调用,但不一定成功)
环绕通知的执行顺序是优于普通通知的
。
同一个Aspect,不同advice的执行顺序:
around before advice
before advice
目标方法执行
around after advice
after advice
afterReturning
如果出现异常的时候,在环绕通知中解决了,那么普通通知是接受不到的。如果想让普通通知接收到,需要在环绕通知中进行抛出异常。
around before advice
before advice
目标方法执行
around after advice
after advice
afterThrowing:异常发生
java.lang.RuntimeException: 异常发生
按照切面类的名称的首字母进行排序操作
,按照字典序。比如有两个切面类:LogUtil、SecurityUtil,就是LogUtil先执行。
如果需要人为地规定顺序,可以在切面类上添加@Order注解同时可以添加具体的值,值越小,越优先
。示例:
@Aspect
@Component
@Order(100)
public class SecurityUtil {
}
@Aspect
@Component
@Order(200)
public class LogUtil {
}
此时就是SecurityUtil切面先执行。
定义注解实现的AnnotationPointCut增强类:
@Aspect //标注这个类是一个切面
public class AnnotationPointCut {
@Before("execution(* com.spring.test.UserServiceImpl.*(..))")
public void before(){
System.out.println("====方法执行前====");
}
@After("execution(* com.spring.test.UserServiceImpl.*(..))")
public void after(){
System.out.println("====方法执行后====");
}
//在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点;
@Around("execution(* com.spring.test.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable{
System.out.println("环绕前");
Signature signature = jp.getSignature();// 获得签名
System.out.println("signature:"+signature);
Object proceed = jp.proceed(); //执行方法
System.out.println("环绕后");
System.out.println(proceed);
}
}
配置文件中增加对注解的配置:
<bean id="userService" class="com.spring.test.UserServiceImpl"/>
<bean id="beforeLog" class="com.spring.test.BeforeLog"/>
<bean id="afterLog" class="com.spring.test.AfterLog"/>
<bean id="annotationPointCut" class="com.spring.test.AnnotationPointCut"/>
<aop:aspectj-autoproxy/>
测试结果:
环绕前
signature:void com.spring.test.UserService.add()
方法执行前
增加了一个用户!
环绕后
null
方法执行后
@Pointcut(value = "execution(public * com.train.aop.controller.AopController.*(..))")
public void pointCut(){
}
以execution(* com.itcodai.course09.controller..*.*(..)))
表达式为例,语法如下:
execution()
:表达式主体;
第一个 * 号的位置
:表示返回值类型, * 表示所有类型;
包名
:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包, com.itcodai.course09.controller 包、子包下所有类的方法;
第二个 * 号的位置
:表示类名, * 表示所有类;
*(..)
:这个星号表示方法名, * 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex)
事务管理一般在Service层。如果在dao层,回滚的时候只能回滚到当前方法,但一般我们的service层的方法都是由很多dao层的方法组成的。同时,如果在dao层,commit的次数会过多。
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的
。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。Spring事务是在代码层面利用AOP实现,执行事务的时候使用TransactionInceptor进行拦截,然后处理。本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
开启事务开关后,在代码里就可以加入@Transactional
注解,就可以使用事务,这也是事务的一般使用方式。
如果一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性
;
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
对于只读查询,可以指定事务类型为readonly,即只读事务。由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段。示例:
@Transactional(readOnly = true)
运行时异常默认回滚,编译时异常默认不回滚
,可以手动指定哪些异常不会滚:
@Transactional(noRollbackFor = {ArithmeticException.class,NullPointerException.class})
设置哪些异常回滚:
@Transactional(rollbackFor = {FileNotFoundException.class})
spring事务的传播行为说的是,当多个事务同时存在(当一个事务方法被另一个事务方法调用)的时候,Spring如何处理这些事务的行为。 事务的传播行为,默认值为 Propagation.REQUIRED
。其七个分类:
事务传播行为类型 | 说明 |
---|---|
REQUIRED (重要) | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
REQUIRES_NEW(重要) | 当前的方法必须启动新事务,并且在它自己的事务内运行,如果有事务正在运行,应该将它挂起。 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中 |
NOT_SUPPORTED | 当前方法不应该运行在事务中,如果当前存在事务,就把当前事务挂起。 |
NEVER | 当前方法不应该运行在事务中,如果当前存在事务,则抛出异常。 |
MANDATORY | 当前的方法必须运行在事务内部,如果当前没有事务,就抛出异常。 |
NESTED(重要) | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新事物,并在它自己的事务内运行 |
使用示例:
@Transactional(propagation = Propagation.REQUIRED)
展开来说:
<!--开启注解的方式-->
<tx:annotation-driven transaction-manager="transactioManager" />
Java代码示例:
//如果有事务, 那么加入事务, 没有的话新建一个(默认情况)
@Transactional(propagation=Propagation.REQUIRED)
//容器不为这个方法开启事务
@Transactional(propagation=Propagation.NOT_SUPPORTED)
//不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
//必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.MANDATORY)
//必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.NEVER)
//如果其他bean调用这个方法,在其他bean中声明事务,那就用事务;
//如果其他bean没有声明事务,那就不用事务
@Transactional(propagation=Propagation.SUPPORTS)
Spring事务在具体使用时,可以分为两类:编程式事务和声明式事务。
声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式
。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理少了一点灵活性。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
如果捕获了异常,还想让事务生效,示例:
@Transactional
public void createUserRight1(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
Spring Boot默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚
。比如代码中抛出RuntimeException 就没有问题,但是抛出SQLException就无法回滚。针对非运行时异常,如果要进行事务回滚的话,可以在@Transactional注解中使用rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor =Exception.class),这样就没有问题了,所以在实际项目中,一定要指定异常。
@Transactional(propagation=Propagation.REQUIRED)
:如果有事务, 那么加入事务, 没有的话新建一个(默认情况下);@Transactional(propagation=Propagation.NOT_SUPPORTED)
:容器不为这个方法开启事务@Transactional(propagation=Propagation.REQUIRES_NEW)
:不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务@Transactional(propagation=Propagation.MANDATORY)
:必须在一个已有的事务中执行,否则抛出异常@Transactional(propagation=Propagation.NEVER)
:必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)@Transactional(propagation=Propagation.SUPPORTS)
:如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.@Transactional(timeout=30)
,超时时间默认是30秒。参数名称 | 功能描述 |
---|---|
readOnly | 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true) |
rollbackFor | 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如: 指定单一异常类:@Transactional(rollbackFor=RuntimeException.class) 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class}) |
rollbackForClassName | 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如: 指定单一异常类名称:@Transactional(rollbackForClassName=“RuntimeException”) 指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,“Exception”}) |
noRollbackFor | 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如: 指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class) 指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) |
noRollbackForClassName | 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如: 指定单一异常类名称:@Transactional(noRollbackForClassName=“RuntimeException”) 指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,“Exception”}) |
propagation | 该属性用于设置事务的传播行为,例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true) |
isolation | 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置 |
timeout | 该属性用于设置事务的超时秒数,默认值为-1表示永不超时 |
注解是不能继承的
,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。 嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于save point。
如果子事务回滚,父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。
如果父事务回滚,父事务回滚,子事务也会跟着回滚。为什么呢,因为父事务结束之前,子事务是不会提交的。
事务的提交:子事务是父事务的一部分,由父事务统一提交。
@Component
:组件,没有明确的角色。@Service
:在业务逻辑层使用。@Repository
:在数据访问层使用。@Controller
:在展现层使用,控制层的声明。@RestController
:@Controller和@ResponseBody组合。@Autowired
:Spring自带的注解,用来自动装配Bean。@Resource
:Java自带的注解,用来自动装配Bean。@Configuration
:声明当前类为配置类,相当于xml形式的Spring配置(类上),声明当前类为配置类,其中内部组合了@Component注解,表明这个类是一个Bean(类上)。@Bean
:注解在方法上,声明当前方法的返回值为一个Bean,替代xml中的方式(方法上)。@ComponentScan
:声明扫描注解的范围 。@Aspect
:声明一个切面(类上),可直接将拦截规则(切点)作为参数。@After
:在方法执行之后执行(方法上)。@Before
:在方法执行之前执行(方法上)。@Around
:在方法执行之前与之后执行(方法上)。@PointCut
:声明切点在Java配置类中使用@EnableAspectJAutoProxy注解,开启Spring对AspectJ代理的支持(类上)。@Value
:为属性注入值。@EnableAsync
:配置类中,通过此注解开启对异步任务的支持。@Async
:在实际执行的 bean 方法使用该注解来申明其是一个异步任务(方法上或类上所有的方法都将异步,需要@EnableAsync 开启异步任务)。@EnableScheduling
:在配置类上使用,开启计划任务的支持(类上)。@Scheduled
:来申明这是一个任务。@EnableAspectJAutoProxy
:开启对AspectJ自动代理的支持。@EnableAsync
:开启异步方法的支持。@EnableScheduling
:开启计划任务的支持。@EnableWebMvc
:开启MVC的配置支持。@EnableConfigurationProperties
:开启对@ConfigurationProperties注解配置Bean的支持。@EnableJpaRepositories
:开启对SpringData JPA Repository的支持。@EnableTransactionManagement
:开启注解式事务的支持。@EnableCaching
:开启注解式的缓存支持。@EnableWebMvc
:在配置类中开启Web MVC的配置支持,如一些 ViewResolver 或者 MessageConverter 等,若无此句,重写WebMvcConfigurerAdapter 方法(用于对Spring MVC 的配置)。@Controller
:声明该类为SpringMVC中的Controller。@RequestMapping
:用于映射Web请求,包括访问路径和参数(类或方法上)。@ResponseBody
:支持将返回值放在response内,而不是一个页面,通常用户返回json 数据(返回值旁或方法上)。@PathVariable
:用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为 Restful 的接口实现方法。@RestController
:该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。@ControllerAdvice
:用于处理全局异常。@ExceptionHandler
:用于更细化的异常处理。
<context:component-scan base-package="com.controller"/>
@RestController = @Controller + @ResponseBody
。用来处理请求地址映射的注解
,它可以用于类上,也可以用于方法上。用于类上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方法都是以该地址作为父路径;方法的级别上注解表示进一步指定到处理方法的映射关系
。示例:@Controller
public class IndexController {
@RequestMapping("/index")
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView("index");
mav.addObject("message", "Hello Spring MVC");
return mav;
}
}
@RequestMapping ( "requestParam" )
public String testRequestParam( @RequestParam(required=false) String name, @RequestParam ( "age" ) int age) {
return "requestParam" ;
}