Spring的AOP和事务

简介: Spring的AOP和事务

动态代理

代理模式:给一个对象提供一个代理,并由代理对象来控制真实对象的访问(调用者并不知道真实对象是什么)。

代理模式分静态代理和动态代理。这里只讨论动态代理,通俗的讲,动态代理就是在不修改代码的基础对被代理对象进行方法的增强

基于接口的动态代理

JDK自带的动态代理就是基于接口的动态代理,被代理对象至少要实现一个接口,否则就无法使用代理。底层还是基于Java的反射来创建代理对象的。

JDK动态代理主要涉及两个类,java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler 。下面来看个例子。

被代理类的接口IProduct

public interface IProduct {
    void saleProduct(Float money);
}
复制代码

被代理类Product实现上面这个接口

public class Product implements IProduct{
    @Override
    public void saleProduct(Float money) {
        System.out.println("卖出一个产品,收到金额:" + money);
    }
}
复制代码

被代理类Product实现上面这个接口

public class Product implements IProduct{
    @Override
    public void saleProduct(Float money) {
        System.out.println("卖出一个产品,收到金额:" + money);
    }
}
复制代码

编写一个客户端类获取一个Product的动态代理的对象,来控制被代理对象的访问。 InvocationHandler的实现主要通过内部类实现(可以直接使用lambda表达式更简洁)。

public class Client {
    public static void main(String[] args){
        Product product = new Product();
        // 基于接口的动态代理(jdk自带的)
        // proxyProduct就是Proxy.newProxyInstance生成的代理对象,需要强转为被代理对象的接口类。
        // 参数:  
        //    ClassLoader:代理对象的类加载器。使用和被代理对象相同的类加载器,直接调用getClass().getClassLoader()获取。
        //    Class<?>[]:被代理对象的接口的字节码。直接调用getClass().getInterfaces()获取就行
        //    InvocationHandler:进行代码的增强。直接使用内部类或者new一个InvocationHandler的实现类(在实现类中进行代码增强)
        IProduct proxyProduct = (IProduct) Proxy.newProxyInstance(product.getClass().getClassLoader()
                , product.getClass().getInterfaces(), new InvocationHandler() {
                    /**
                     * 进行代理增强的方法
                     * 参数:
                     *     proxy:当前的代理对象
                     *     method:被代理对象当前执行的方法
                     *     args:被代理对象当前执行的方法的参数
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object res = null;
                        // 获取方法执行的参数
                        Float money = (Float)args[0];
                        // 判断当前方法是否是销售方法
                        if ("saleProduct".equals(method.getName())){
                            // 返回值与被代理对象方法的相同
                            res = method.invoke(product, money * 0.8f);
                        }
                        return res;
                    }
                });
        // 调用这个代理对象,执行被代理对象的方法。
        proxyProduct.saleProduct(1000f);
    }
}
复制代码

基于子类的动态代理

CGLIB动态代理是一个基于ASM字节码操作框架的代码生成库,它被广泛用于基于代理的AOP框架提供方法拦截。

本质上说,对于需要被代理的类,它只是动态的生成一个子类以覆盖非final的方法(所以它不能代理final类或final方法),同时绑定钩子回调自定义的拦截器。它比使用Java反射的JDK动态代理方法更快。

CGLIB动态代理的核心是net.sf.cglib.proxy.Enhancer类(用于创建被代理对象子类),net.sf.cglib.proxy.MethodInterceptor是最常用的回调类型(它是net.sf.cglib.proxy.Callback的一个子接口)。

<!--cglib动态代理的依赖-->
<dependency>
     <groupId>cglib</groupId>
     <artifactId>cglib</artifactId>
     <version>3.2.12</version>
</dependency>
复制代码

下面来看一个示例(被代理对象还是和上面的相同,这里实现一个客户端类)。

public class Client {
    public static void main(String[] args){
        Product product = new Product();
        /**
         * 基于子类的动态代理(cglib)
         * Enhancer的create方法,通过Enhancer来创建代理对象,MethodInterceptor来增强方法
         * 参数:
         *      Class:被代理对象的字节码
         *      Callback:用于增强方法。一般使用这个接口的子接口MethodInterceptor。
         */
        Product proxyProduct =  (Product) Enhancer.create(product.getClass(), new MethodInterceptor() {
            /**
             * 执行被代理对象的任何方法都会经过此方法
             * @param proxy 当前代理对象
             * @param method 被代理对象要执行的方法
             * @param args1 要执行的方法的参数
             * 以上三个参数与基于接口的动态代理(jdk)的参数一致
             * @param methodProxy 当前执行方法的代理对象 一般使用这个对象来调用原始对象的方法,因为它性能更高
             * @return 与被代理对象要执行的方法的返回值一致
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args1, MethodProxy methodProxy) throws Throwable {
                Object res = null;
                // 获取方法执行的参数
                Float money = (Float)args1[0];
                // 判断当前方法是否是销售方法
                if ("saleProduct".equals(method.getName())){
                    // 修改原始方法的参数
                    args1[0] = money * 0.8f;
                    // 与JDK动态代理的一个区别,使用MethodProxy对象来调用原始对象的方法
                    res = methodProxy.invoke(product, args1);
                }
                return res;
            }
        });
        // 通过代理对象执行被代理对象的方法
        proxyProduct.saleProduct(1000f);
    }
}
复制代码

spring AOP

spring AOP是基于动态代理实现的,spring默认使用JDK动态代理实现AOP,也可以强制使用CGLIB。AOP是IoC的一种补充,IoC不依赖与AOP,但是AOP依赖与IoC。

AOP的一些概念

  • JoinPoint(连接点):指被拦截到的点。在spring AOP中,就是指那些被拦截的方法。
  • PointCut(切入点):是指筛选出的连接点(因为所有的切入点都是连接点,但是有的连接点并不需要进行拦截增强方法)
  • Advice(通知/增强):需要完成的工作叫做通知,就是写的业务逻辑代码中的公共代码,比如事务、日志等。通知的类型有五种。
  • Before(前置通知):在连接点(拦截的方法)执行前的通知。
  • AfterReturning(后置通知):在连接点正常完成返回后的通知。
  • AfterThrowing(异常通知):如果方法执行过程中抛出了异常,则执行此通知。
  • After(最终通知):无论方法执行是否成功,最终都要执行这个通知。
  • Around(环绕通知):围绕连接点的通知,它是最常用的通知,因为它可以包括以上四种通知。
  • Aspect(切面):其实就是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行。
  • Introduction(引入):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。
  • Target(目标):被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。
  • Weaving(织入):切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点(编译期,类加载期,运行期)可以进行织入,spring是在运行期织入的。

@Aspectj支持

Spring 使用 AspectJ 提供的 library 解释与 AspectJ 5 相同的注释,用于切入点解析和匹配。但是,AOP 运行时仍然是纯粹的 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。(也就是spring只是利用Aspectj来对切入点进行解析和匹配,真正的AOP的执行还是使用Spring AOP)。

在springboot中使用spring AOP非常的方便,只需引入一个spring支持AspectJ的starter依赖即可。

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
复制代码

在spring中也很简单,在引用spring AOP的基础上再引入一个aspectjweaver依赖就行。


<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>
复制代码

在spring中,要使用 Java @Configuration启用 @AspectJ 支持,还需添加@EnableAspectJAutoProxy注解(在springboot中不用)。

下面来看一个简单的示例(基于springboot)。

先写个Service类(包括接口和实现类)。

public interface AccountService {
    Account findById(Integer aid);
}
@Service
public class AccountServiceImpl implements AccountService {
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
    @Override
    public Account findById(Integer aid) {
        log.info("正在执行方法");
        return accountMapper.findById(aid);
    }
}
复制代码

一个配置类,对service中的方法进行切面。

@Aspect // 声明切面, 这个bean将被spring自动检测,并用于配置Spring AOP
@Component // 将这个类注册为一个bean,交给spring来管理(所以spring AOP依赖于IoC)
public class Logger {
    private static org.slf4j.Logger log = LoggerFactory.getLogger(Logger.class);
    /**
     * 通用化切入点表达式,其他切入点可以直接引用这个
     */
    @Pointcut("execution(* tkmybatis.service.impl.*.*(..))")
    public void pointCut(){
    }
    /**
     * 前置通知,在切入点方法执行之前执行
     */
    @Before("pointCut()")
    public void printLogBefore(){
        log.info("我在方法执行前");
    }
    /**
     * 后置通知,方法正常返回后执行
     */
    @AfterReturning("pointCut()")
    public void printLogAfterReturning(){
        log.info("我在方法执行正常返回后");
    }
    /**
     * 异常通知,方法发生异常时执行
     */
    @AfterThrowing("pointCut()")
    public void printLogAfterThrow(){
        log.info("我在方法执行抛出异常后");
    }
    /**
     * 最终通知,无论方法是否执行正常,它都会在最后执行
     */
    @After("pointCut()")
    public void printLogAfter(){
        log.info("我在方法执行最后");
    }
    @Around("pointCut()")
    public Object printAround(ProceedingJoinPoint pjp){
        Object res;
        try{
            Object[] args = pjp.getArgs();
            log.info("我在方法执行前");
            res = pjp.proceed(args);
            log.info("我在方法执行正常返回后2");
            return res;
        }catch (Throwable t){
            log.info("我在方法执行抛出异常后2");
            throw new RuntimeException(t);
        }finally {
            log.info("我在方法执行最后");
        }
    }
}
复制代码

对于切入点表达式execution(* tkmybatis.service.impl.*.*(..)) ,这里做一下解释。

execution表达式本来有三个部分(可见类型 返回值 连接点的全限定名),这里的可见类型可以省略。

  • *代表返回值可以为任何值。
  • tkmybatis.service.impl代表service实现类所在的包(进一步还可以直接用* .来替代,表示所有包)。
  • .* 代表这个包下的所有类。下一个.* 代表这个类下的所有方法(还可以用方法前缀来表示,比如find*)。
  • 括号里的 .. 代表参数可以为任意参数,可为多个或无。

更多高级用法详见Spring官方文档 Spring AOP

Spring 事务管理

Spring支持声明式的事务管理和编程式的事务管理。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。Spring推荐使用声明式的事务管理。

而声明式的事务管理就是基于Spring AOP的,其本质就是对要执行的方法进行拦截,在方法执行前加入一个事务,方法执行完成后根据情况判断是提交事务还是回滚事务(抛出了异常)。

事务的隔离级别

通过org.springframework.transaction.annotation.Isolation 这个枚举类来指定。

public enum Isolation {
    // 这是默认值,表示使用底层数据库的默认隔离级别。对于MySQL的InnoDB存储引擎,它是REPEATABLE_READ,其它一般的都是READ_COMMITTED
    DEFAULT(-1),
    // 跟没有一样,几乎不使用。
    READ_UNCOMMITTED(1),
    // 只能读取另一个事务已提交的事务,能防止脏读。也是一般数据库的默认隔离级别。
    READ_COMMITTED(2),
    // 可重复读(在一个事务内多次查询的结果相同,其它事务不可修改该查询条件范围内的数据,相当于加了个读锁)
    REPEATABLE_READ(4),
    // 所有的事务依次逐个执行,相当于串行化了,效率太低,一般也不使用。
    SERIALIZABLE(8);
}
复制代码

事务的传播行为

如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。org.springframework.transaction.annotation.Propagation枚举类中定义了7表示传播行为的枚举值。

public enum Propagation {
    // 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    REQUIRED(0),
    // 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    SUPPORTS(1),
    // 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    MANDATORY(2),
    // 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    REQUIRES_NEW(3),
    // 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    NOT_SUPPORTED(4),
    // 以非事务方式运行,如果当前存在事务,则抛出异常。
    NEVER(5),
    // 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED。
    NESTED(6);
}
复制代码

声明式事务的使用

声明式事务可以直接使用@Transactional注解(这里只讨论这个)来配置,当然也可以使用XML配置文件。

先来看一下@Transactional注解都有啥

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 别名为transactionManager,其实这两是同一个。就是事务的名字。
    @AliasFor("transactionManager")
  String value() default "";
    // 事务的传播行为,默认值为REQUIRED
    Propagation propagation() default Propagation.REQUIRED;
    // 事务的隔离级别,默认为默认值(也就是使用底层数据库的隔离级别)
    Isolation isolation() default Isolation.DEFAULT;
    // 超时时间,默认为 -1,也就是没有超时时间。
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    // 是否只读,默认为false。
    boolean readOnly() default false;
    // 会触发事务回滚的异常的字节码数组
    Class<? extends Throwable>[] rollbackFor() default {};
    // 不会触发事务回滚的异常的字节码数组
    Class<? extends Throwable>[] noRollbackFor() default {};
}
复制代码

这里直接在实现类上使用,这个注解还可以在接口(一般不在接口上使用)和方法上使用(可见性必须为public,否则会被忽略)。

如果是在spring中使用,需要在配置类中加上@EnableTransactionManagement注解(springboot则不需要)。proxy-target-class属性可以控制动态代理的类型,如果值为false或忽略此属性,则使用JDK的动态代理,可以设置为true以强制使用CGLIB来进行动态代理(如果被代理的类没有实现接口,将会自动使用CGLIB进行动态代理)。

@Service
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) // 这里触发回滚的异常设置为Exception
public class AccountServiceImpl implements AccountService {
}
复制代码

参考:

CGLIB动态代理介绍

Spring AOP

Spring AOP的使用

Spring 事务管理

Spring 事务

目录
相关文章
|
14天前
|
缓存 安全 Java
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
39 2
Spring高手之路26——全方位掌握事务监听器
|
15天前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
1月前
|
XML Java 数据安全/隐私保护
Spring Aop该如何使用
本文介绍了AOP(面向切面编程)的基本概念和术语,并通过具体业务场景演示了如何在Spring框架中使用Spring AOP。文章详细解释了切面、连接点、通知、切点等关键术语,并提供了完整的示例代码,帮助读者轻松理解和应用Spring AOP。
Spring Aop该如何使用
|
17天前
|
监控 安全 Java
什么是AOP?如何与Spring Boot一起使用?
什么是AOP?如何与Spring Boot一起使用?
43 5
|
21天前
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
49 8
|
1月前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
51 1
Spring高手之路24——事务类型及传播行为实战指南
|
21天前
|
Java 开发者 Spring
Spring AOP 底层原理技术分享
Spring AOP(面向切面编程)是Spring框架中一个强大的功能,它允许开发者在不修改业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理等。本文将深入探讨Spring AOP的底层原理,包括其核心概念、实现方式以及如何与Spring框架协同工作。
|
21天前
|
XML 监控 安全
深入调查研究Spring AOP
【11月更文挑战第15天】
32 5
|
21天前
|
Java 开发者 Spring
Spring AOP深度解析:探秘动态代理与增强逻辑
Spring框架中的AOP(Aspect-Oriented Programming,面向切面编程)功能为开发者提供了一种强大的工具,用以将横切关注点(如日志、事务管理等)与业务逻辑分离。本文将深入探讨Spring AOP的底层原理,包括动态代理机制和增强逻辑的实现。
30 4
|
20天前
|
JavaScript Java 关系型数据库
Spring事务失效的8种场景
本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。