Spring-AOP的理解与简单使用

Spring-AOP的简单使用

1. Spring-AOP

1.1 AOP概念

(1)概念

  • Aspect Oriented Program(面向切面编程)

  • AOP思想:将功能实现的特定核心方法与其前后执行的一套通用的程序执行流程模板分离开来,实现核心业务的开发与相应的前置处理与结果处理的后置业务的开发相互独立起来,实现核心与其他方法的任意组合使用。

  • 好处:将于业务无关的方法与业务分离开来,提高了代码的复用率,同时使得代码更容易维护。

image-20200608202753834

(2)spring中AOP概念介绍

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
  • 切点(PointCut): 可以插入增强处理的连接点。
  • 切面(Aspect): 切面是通知和切点的结合,比如日志管理,性能统计,事务管理等对于大多数功能都能适用的处理流程。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

1.2 动态代理

(1)动态代理分类

  • 动态代理的根据实现方式可以分为JDK动态代理和CGlib动态代理。
    • JDK 动态代理:利用反射机制生成一个实现代理接口的类,在调用具体方法前调用InvokeHandler来处理。
    • CGlib 动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
  • 两者区别:
    • JDK代理只能对实现接口的类生成代理
    • CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类

(2)JDK动态代理示例

  • 首先创建一个接口:

    public interface UserLoginService {
        /**
         * 用户登录查询数据库
         * @param username 用户名
         * @param password 用户密码
         * @return 是否成功
         */
        boolean login(String username, String password);
    }
    
  • 创建实现InvocationHandler接口的代理类

    /**
     * @author Ni187
     */
    public class ProxyHandler implements InvocationHandler{
        Logger logger;
    
        /**
         * 被代理的对象
         */
        Object target;
    
        public ProxyHandler(Object target){
            this.target = target;
            logger = LogManager.getLogger(this.getClass());
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            logger.info(target.getClass().getName()+" 接口对象开始执行 "+method.getName()+" 方法,参数为:{"+ Arrays.toString(args) +"}");
            Object result = method.invoke(target, args);
            logger.info(target.getClass().getName()+" 接口对象执行的方法结果为 "+result+" ;参数为:{"+ Arrays.toString(args) +"}");
            return result;
        }
    }
    
  • 测试:

    public class ProxyHandlerTest {
    
        @Test
        public void invoke() {
            //创建接口
            UserLoginService service = (username,password)->{
                System.out.println("开始连接数据库,查询{username="+username+", password="+password);
                return new Random().nextDouble()>0.5;
            };
    
            // 代理接口方法处理类
            ProxyHandler proxyHandler = new ProxyHandler(service);
    
            // 生成代理对象
            UserLoginService proxyUserLoginService = (UserLoginService) Proxy.newProxyInstance(
                    UserLoginService.class.getClassLoader()
                        ,service.getClass().getInterfaces()
                        , proxyHandler);
    
            proxyUserLoginService.login("niss", "12345678");
    
        }
    }
    
    21:32:58.775 [main] INFO com.niss.proxy.dynamic.DynamicProxy - com.niss.proxy.dynamic.DynamicProxyTest$$Lambda$33/0x00000008000b3c40 接口对象开始执行 login 方法,参数为:{[niss, 12345678]}
    开始连接数据库,查询{username=niss, password=12345678
    21:32:58.783 [main] INFO com.niss.proxy.dynamic.DynamicProxy - com.niss.proxy.dynamic.DynamicProxyTest$$Lambda$33/0x00000008000b3c40 接口对象执行的方法结果为 false ;参数为:{[niss, 12345678]}
    

(3)CGlib创建动态代理类示例

  • 定义实现MethodInterceptor的实现类

    导入maven依赖:

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
    
    • 在MethodIntercepter接口中,定义了public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)方法

      obj表示增强的对象,即实现这个接口类的一个对象;
      method表示要被拦截的方法;
      args表示要被拦截方法的参数;
      methodProxy表示要触发父类的方法对象;

    /**
     * @author Ni187
     */
    public class CGlibProxyIntercepter implements MethodInterceptor {
        Logger logger;
    
    
        public CGlibProxyIntercepter(){
            logger = LogManager.getLogger(CGlibProxyIntercepter.class);
        }
    
        /**
         * 获取方法代理
         * @param o
         * @param method
         * @param objects
         * @param methodProxy
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            logger.info(o.getClass().getName()+" 接口对象开始执行 "+method.getName()+" 方法,参数为:{"+ Arrays.toString(objects) +"}");
            Object result = methodProxy.invokeSuper(o, objects);
            logger.info(o.getClass().getName()+" 接口对象执行的方法结果为 "+result+" ;参数为:{"+ Arrays.toString(objects) +"}");
            return result;
        }
    }
    
  • 测试:

    public class CGlibProxyIntercepterTest {
    
        @Test
        public void intercept() {
            //创建接口
            UserLoginService service = (username, password)->{
                System.out.println("开始连接数据库,查询{username="+username+", password="+password);
                return new Random().nextDouble()>0.5;
            };
    
            CGlibProxyIntercepter cGlibProxyIntercepter = new CGlibProxyIntercepter();
            Enhancer enhancer = new Enhancer();
            // 设置被调用者的实现该接口的父类
            enhancer.setSuperclass(UserLoginServiceImpl.class);
            enhancer.setCallback(cGlibProxyIntercepter);
            UserLoginService userLoginServiceProxy = (UserLoginService)enhancer.create();
            userLoginServiceProxy.login("niss2", "87654321");
        }
    }
    
    21:49:15.821 [main] INFO com.niss.proxy.dynamic.CGlibProxy - com.niss.service.serviceimpl.UserLoginServiceImpl$$EnhancerByCGLIB$$b960a009 接口对象开始执行 login 方法,参数为:{[niss2, 87654321]}
    查询数据库,在验证:{username=niss2, password=87654321}
    21:49:15.845 [main] INFO com.niss.proxy.dynamic.CGlibProxy - com.niss.service.serviceimpl.UserLoginServiceImpl$$EnhancerByCGLIB$$b960a009 接口对象执行的方法结果为 true ;参数为:{[niss2, 87654321]}
    

1.3 SpringAOP的使用

(1)引入依赖

  • 需要的依赖:

    spring-aop:AOP核心功能,例如代理工厂等等

    aspectjweaver:支持切入点表达式等等

    aspectjrt: 支持aop相关注解等等(该模块包含在aspectjweaver中)

  • maven坐标

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
    

(2)通过实现java API接口来实现增强处理

  • 实现MethodBeforeAdvice接口的public void before(Method method, Object[] args, Object target) throws Throwable方法,实现核心功能的前置处理;

  • 实现AfterReturningAdvice接口的public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable方法,实现核心接口的后置处理;

  • 创建一个前置日志记录类,实现MethodBeforeAcvice接口

    public class LogBefore implements MethodBeforeAdvice {
    
        Logger logger = LogManager.getLogger(LogBefore.class);
    
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            logger.info("{} 开始执行:{}, 参数:{}",target.getClass(),method.getName(),Arrays.toString(args));
        }
    }
    
  • 创建一个后置日志记录类,实现AfterRunturningAdvice

    public class LogAfter implements AfterReturningAdvice {
        Logger logger = LogManager.getLogger(AfterReturningAdvice.class);
        
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
            logger.info("{} 的{}方法执行完毕,参数={{}},return={}",target.getClass(),method.getName(), Arrays.toString(args),returnValue);
        }
    }
    
  • 在xml中配置AOP信息

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans 
                             http://www.springframework.org/schema/beans/spring-beans.xsd 
                             http://www.springframework.org/schema/aop 
                             https://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--Beans-->
        <bean id="userLoginServiceImpl" class="com.niss.service.serviceimpl.UserLoginServiceImpl"/>
        <bean id="logBefore" class="com.niss.service.LogBefore"/>
    
        <!--AOP代理配置-->
        <aop:config>
            <!--切点定义-->
            <aop:pointcut id="userLoginPoint" expression="execution(* com.niss.service.*impl.UserLoginServiceImpl.*(String,String))"/>
    
            <!--增强-->
            <aop:advisor advice-ref="logBefore" pointcut-ref="userLoginPoint"/>
            <aop:advisor advice-ref="logAfter" pointcut-ref="userLoginPoint"/>
        </aop:config>
    </beans>
    
    • 常用标签:img

    • 在使用spring框架配置AOP的时候,不管是通过XML配置文件还是注解的方式都需要定义pointcut"切入点"

      例如定义切入点表达式 execution (* com.sample.service.impl..*.*(..))

      execution()是最常用的切点函数,其语法如下所示:

      整个表达式可以分为五个部分:

      1. execution(): 表达式主体。

      2. 第一个*号:表示返回类型,*号表示所有的类型。

      3. 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。一个句点只表示当前包。

        • 可以通过使用包名前缀*包名后置的方式指定包名前缀与后缀,比如在上面xmlAOP配置中可以写为execution(* com.niss.service.*impl.*.* (..))表示service包下所有的后缀名为impl的包的所有的类的所有方法;
      4. 第二个*号:表示类名,*号表示所有的类。

      5. *(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

        • 可以通过使用方法名前缀*方法名后置的方式指定方法名名前缀与后缀

        • (String,int)匹配方法名(String,int)方法,如果方法中的入参类型是java.lang包下的类,可以直接使用类名,否则必须使用全限定类名,如m(java.util.List,int);

          (String,*)该方法第一个入参为String,第二个入参可以是任意类型,如 m(String s1,String s2)和 m(String s1,double d2)都匹配,但 m(String s1,double d2,String s3)则不匹配;

          (String,..)该方法第一个入参为String,后面可以有任意个入参且入参类型不限,如m(String s1)、m(String s1,String s2)和m(String s1,double d2,String s3)都匹配。

          m(Object+)该方法拥有一个入参,且入参是Object类型或该类的子类。它匹配m(String s1)和m(Client c)。如果我们定义的切点是execution(* m(Object)),则只匹配m(Object object)而不匹配m(String cc)或m(Client c)。

  • 测试类:

    @Test
    public void test() {
        ApplicationContext app = new ClassPathXmlApplicationContext("ApplicationConfig.xml");
        UserLoginService userLoginService = app.getBean("userLoginServiceImpl",UserLoginService.class);
        userLoginService.login("川建国", "MAG");
    }
    
    13:42:16.226 [main] INFO com.niss.service.LogBefore - class com.niss.service.serviceimpl.UserLoginServiceImpl 开始执行:login, 参数:[川建国, MAG]
    查询数据库,正在验证:{username=川建国, password=MAG}
    13:42:16.235 [main] INFO org.springframework.aop.AfterReturningAdvice - class com.niss.service.serviceimpl.UserLoginServiceImpl 的login方法执行完毕,参数={[川建国, MAG]},return=false
    

(2)通过自定义切面来配置AOP

  • 自定义计时处理类:

    public class Timer {
    
        long startTime;
        long endTime;
    
        public void beginLog(){
            System.out.println("开始执行:");
        }
    
        public void before(){
            System.out.println("开始计时");
            startTime = System.currentTimeMillis();
        }
    
        public void after(){
            endTime = System.currentTimeMillis();
        }
        public void endLog(){
            System.out.println("结束执行,共用时:"+(endTime-startTime)+" ms");
        }
    
    }
    
  • 在刚才的xml中添加bean以及配置AOP:

    <bean id="timer" class="com.niss.service.Timer"/>
    
    <aop:config>
        <aop:aspect id="time" ref="timer" order="1">
            <aop:pointcut id="login" expression="execution(* com.niss.service.uu*.*.* (..))"/>
            <aop:before method="beginLog" pointcut-ref="userLoginPoint"/>
            <aop:before method="before" pointcut-ref="userLoginPoint"/>
            <aop:after method="after" pointcut-ref="userLoginPoint"/>
            <aop:after method="endLog" pointcut-ref="userLoginPoint"/>
        </aop:aspect>
    </aop:config>
    
    • order用来指定不同切面的执行顺序,值越小优先度越高;
    • before与method指定了切点执行前后的处理方法,可指定多个,按照配置的顺序执行;
  • 使用刚才的测试方法进行测试:

    开始执行:
    开始计时
    14:31:52.787 [main] INFO com.niss.service.LogBefore - class com.niss.service.serviceimpl.UserLoginServiceImpl 开始执行:login, 参数:[川建国, MAG]
    查询数据库,正在验证:{username=川建国, password=MAG}
    14:31:52.787 [main] INFO org.springframework.aop.AfterReturningAdvice - class com.niss.service.serviceimpl.UserLoginServiceImpl 的login方法执行完毕,参数={[川建国, MAG]},return=false
    结束执行,共用时:13 ms
    

(3)通过Spring注解来实现AOP

  • 注意如果使用位置文件配置AOP后,然后使用注解来指定前置与后置处理的方法会直接插入到核心方法的前后;

  • 修改Timer类:

    package com.niss.service;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    /**
     * @author Ni187
     */
    @Aspect // 标注这是一个切面类
    public class Timer {
    
        long startTime;
        long endTime;
    
        @Before("execution(* com.niss.service.*impl.*.*(..))")
        public void beginLog(){
            System.out.println("开始执行:");
        }
    
        @Before("execution(* com.niss.service.*impl.*.*(..))")
        public void before(){
            System.out.println("开始计时");
            startTime = System.currentTimeMillis();
        }
        @After("execution(* com.niss.service.*impl.*.*(..))")
        public void after(){
            endTime = System.currentTimeMillis();
        }
    
        @After("execution(* com.niss.service.*impl.*.*(..))")
        public void endLog(){
            System.out.println("结束执行,共用时:"+(endTime-startTime)+" ms");
        }
    
        @Around("execution(* com.niss.service.*impl.*.*(..))")
        public boolean around(ProceedingJoinPoint proceedingJoinPoint){
            System.out.println("环绕前");
            try {
                Object result = proceedingJoinPoint.proceed();
                System.out.println("环绕后,结果为"+result);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            return false;
        }
    
    }
    
  • 注释掉之前的配置,并添加以下标签来开启AOP注解:

    <aop:aspectj-autoproxy proxy-target-class="false"/>
    
    • proxy-target-class="false":使用JDK实现动态代理;true:使用cglib实现动态代理;
  • 继续使用刚才的测试类:

    环绕前
    开始计时
    开始执行:
    查询数据库,正在验证:{username=川建国, password=MAG}
    环绕后,结果为false
    结束执行,共用时:12 ms
    

Spring AOP 参考

posted @ 2020-06-09 15:17  NIShoushun  阅读(336)  评论(0编辑  收藏  举报