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 事务

目录
相关文章
|
8天前
|
XML 监控 安全
Spring特性之一——AOP面向切面编程
Spring特性之一——AOP面向切面编程
17 1
|
5天前
|
前端开发 Java 关系型数据库
使用IDEA搭建一个Spring + AOP (权限管理 ) + Spring MVC
使用IDEA搭建一个Spring + AOP (权限管理 ) + Spring MVC
|
7天前
|
Java 关系型数据库 MySQL
【JavaEE】Spring事务-@Transactional参数介绍-事务的隔离级别以及传播机制
【JavaEE】Spring事务-@Transactional参数介绍-事务的隔离级别以及传播机制
12 0
|
7天前
|
消息中间件 Java 关系型数据库
【JavaEE】Spring事务-事务的基本介绍-事务的实现-@Transactional基本介绍和使用
【JavaEE】Spring事务-事务的基本介绍-事务的实现-@Transactional基本介绍和使用
19 0
|
7天前
|
JSON 前端开发 Java
【JavaEE】Spring全家桶实现AOP-统一处理
【JavaEE】Spring全家桶实现AOP-统一处理
7 0
|
7天前
|
前端开发 Java 开发者
【JavaEE】面向切面编程AOP是什么-Spring AOP框架的基本使用
【JavaEE】面向切面编程AOP是什么-Spring AOP框架的基本使用
14 0
|
8天前
|
Java Spring 容器
Spring AOP浅谈
Spring AOP浅谈
14 1
|
8天前
|
XML Java 数据格式
Spring高手之路18——从XML配置角度理解Spring AOP
本文是全面解析面向切面编程的实践指南。通过深入讲解切面、连接点、通知等关键概念,以及通过XML配置实现Spring AOP的步骤。
24 6
Spring高手之路18——从XML配置角度理解Spring AOP
|
8天前
|
Java 数据库连接 数据库
AOP&事务
AOP&事务
10 0
|
8天前
|
SQL Java 关系型数据库
Spring 事务
Spring 事务
15 1