Spring的AOP编程【Spring学习笔记七】

144 阅读11分钟

1、静态代理设计模式

1.1、为什么需要代理设计模式

1.1.1、问题
  • 在JavaEE分层开发中,哪个层次对于我们来讲最重要

    DAO ----> Service ----> Controller
    
    JavaEE分层开发中,最为重要的是Service层
    
  • Service层中包含了哪些代码?

    Service层中 = 核心功能(几十行 上百代码) + 额外功能(附加功能)
    
    1. 核心功能
    	业务运算
    	DAO调用
    2. 额外功能
    	1. 不属于业务
    	2. 可有可无
    	3. 代码量很小
    	
    	事务、日志、性能...
    
  • 额外功能书写在Service层好不好?

    Service层的调用者的角度(Controller):需要在Service层书写额外的功能。
    						软件设计者:Service层不需要额外的功能
    
  • 现实生活中的解决方法

微信截图_20211103122039.png

1.2、代理设计模式

1.2.1、概念
通过代理类,为原始类(目标)增加额外的功能
好处:利于原始类(目标)的维护
1.2.2、名词解释
1. 目标类 原始类
	指的是 业务类 (核心功能----> 业务运算 DAO调用)
	
2. 目标方法,原始方法
	目标类(原始类)中的发发,就是目标方法(原始方法)
	
3. 额外功能(附加功能)
	日志、事务、性能
1.2.3、代理开发的核心要素
代理类 = ⽬标类(原始类) + 额外功能 + 原始类(⽬标类)实现相同的接⼝

房东 --->    public interface UserService{
                 m1
                 m2
            }
            UserServiceImpl implements UserService{
                 m1 ---> 业务运算 DAO调⽤
                 m2
            }
            UserServiceProxy implements UserService
            	  m1
            	  m2
1.2.4、编码

静态代理:为每一个原始类,手工编写一个代理类(.java .class)

微信截图_20211103122626.png

1.2.5、静态代理存在问题
1. 静态类⽂件数量过多,不利于项⽬管理
	
	UserServiceImpl UserServiceProxy
	OrderServiceImpl OrderServiceProxy
	
2. 额外功能维护性差

	代理类中 额外功能修改复杂(麻烦)

2、Spring的动态代理开发

2.1、Spring动态代理的概念

概念:通过代理类为原始类(⽬标类)增加额外功能
好处:利于原始类(⽬标类)的维护

2.2、搭建开发环境

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.14.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.3</version>
</dependency>

2.3、Spring代理的开发步骤

2.3.1、创建原始对象(目标对象)
public class UserServiceImpl implements UserService {
    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register 业务运算 + DAO ");
    }
    @Override
    public boolean login(String name, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}
<bean id="userService" class="cn.edu.njtech.proxy.UserServiceImpl"></bean>
2.3.2、额外功能 MethodBeforeAdvice接口
 额外的功能书写在接口的实现中,运行在原始方法执行之前运行额外功能
package cn.edu.njtech.dynamic;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

/**
 * @author Tim
 * @date 2021/11/2 23:05
 */
public class Before implements MethodBeforeAdvice {

    /**
     * 需要把运行在原始方法之前运行的额外功能,书写在before方法中
     * @param method
     * @param objects
     * @param o
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("-----method before advice log------");
    }
}
<bean id="before" class="cn.edu.njtech.dynamic.Before"></bean>
2.3.3、定义切入点
切⼊点:额外功能加⼊的位置

⽬的:由程序员根据⾃⼰的需要,决定额外功能加⼊给那个原始⽅法
register
login

简单的测试:所有⽅法都做为切⼊点,都加⼊额外的功能。
<aop:pointcut id="pc" expression="execution(* *(..))"/>
2.3.4、组装
<aop:advisor advice-ref="before" pointcut-ref="pc"></aop:advisor>
2.3.5、调用
目的:获得Spring工厂创建的动态代理对象,并进行调用。

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.register(new User());
userService.login("tim","123");

3、动态代理细节分析

3.1、Spring创建的动态代理类在哪里?

Spring框架在运⾏时,通过动态字节码技术,在JVM创建的,运⾏在JVM内部,等程序结束后,会和JVM⼀起消失

什么叫动态字节码技术:通过第三放动态字节码框架,在JVM中创建对应类的字节码,进⽽创建对象,当虚拟机结束,动态字节码跟着消失。

结论:动态代理不需要定义类⽂件,都是JVM运⾏过程中动态创建的,所以不会造成静态代理类⽂件数量过多,影响项⽬管理的问题

微信截图_20211103123715.png

3.2、动态代理编程简化代理的开发

在额外功能不改变的前提下,创建其他⽬标类(原始类)的代理对象时,只需要指定原始(⽬标)对象即可。

3.3、动态代理额外功能的维护性大大增强

4、Spring动态代理详解

4.1、额外功能的详解

  • MethodBeforeAdvice分析

    package cn.edu.njtech.dynamic;
    
    import org.springframework.aop.MethodBeforeAdvice;
    
    import java.lang.reflect.Method;
    
    /**
     * @author Tim
     * @date 2021/11/2 23:05
     */
    public class Before implements MethodBeforeAdvice {
        /**
         * 需要把运行在原始方法之前运行的额外功能,书写在before方法中
         * @param method 额外功能所增加给的那个原始方法 login方法 register方法 showOrder方法
         * @param objects 额外功能所增加给的那个原始方法的参数 User name password
         * @param o 原始对象 userService orderService
         * @throws Throwable
         */
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("-----method before advice log------");
        }
    }
    
  • MethodInterceptor(方法拦截器)

    methodInterceptor接口:额外功能可以根据需要运行在原始方法执行 前、后、前后
    
    package cn.edu.njtech.dynamic;
    
    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    
    import java.lang.reflect.Method;
    
    /**
     * @author Tim
     * @date 2021/11/2 23:56
     */
    public class Arround implements MethodInterceptor {
    
        /**
         * invoke 方法的作用:额外功能写在invoke
         *             额外功能     原始方法之前 之后 或者环绕
         *
         * 确定:原始方法怎么运行
         *
         * 参数:MethodInvocation(Method) 额外功能所增加给的那个原始方法
         *
         * 返回值:Object 原始方法的返回值
         *
         * @param methodInvocation
         * @return
         * @throws Throwable
         */
    
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
    
            Object[] arguments = methodInvocation.getArguments();
            Object object = methodInvocation.getThis();
            Method method = methodInvocation.getMethod();
            System.out.println("----额外功能之前 log----");
            Object proceed= null;
            try {
                proceed = methodInvocation.proceed();
            }catch (Exception e){
                System.out.println("----额外功能抛出异常 log----");
                e.printStackTrace();
            }
            System.out.println("----额外功能之后 log----");
    
            return proceed;
        }
    }
    
  • MethodInterceptor影响原方法的返回值

    原始⽅法的返回值,直接作为invoke⽅法的返回值返回,MethodInterceptor不会影响原始⽅法的返回值
    
    
    MethodInterceptor影响原始⽅法的返回值
    Invoke⽅法的返回值,不要直接返回原始⽅法的运⾏结果即可。
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable
        {
            System.out.println("------log-----");
            Object ret = invocation.proceed();
            return false;
        }
    

4.2、切入点详解

切入点决定额外功能加入位置(方法)

<aop:pointcut id = "pc" expression="execution( * * (..))"/>

execution(* *(..)) -----> 匹配了所有方法 a b c

1. execution() 切入点函数
2. * * (..) 切入点表达式
4.2.1、切入点表达式
  1. 方法切入点表达式

微信截图_20211103153021.png

* * (..) --> 所有方法
* ---> 修饰符 返回值
* ---> 方法名
() ---> 参数表
.. ---> 对于参数没有要去(参数有没有,参数有几个都行,参数是什么类型的都行)
  • 定义login方法作为切入点

    * login(..)
    
    ## 定义rreeggiisstteerr作为切⼊点
    * register(..)
    
  • 定义login方法且login方法有两个字符串类型的参数作为切入点

    * login(String,String)
    # 注意:⾮jjaavvaa..llaanngg包中的类型,必须要写全限定名
    * register(cn.edu.njtech.proxy.User)
    
    # ....可以和具体的参数类型连⽤
    * login(String,..) --> login(String),login(String,String),login(String,cn.edu.njtech.proxy.User)
    
  • 精准方法切入点限定

    修饰符 返回值 包.类.⽅法(参数)
    * com.baizhiedu.proxy.UserServiceImpl.login(..)
    * com.baizhiedu.proxy.UserServiceImpl.login(String,String)
    
  1. 类切入点

    指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加上对应的额外功能。
    
    • 语法1

      ##类中的所有⽅法加⼊了额外功能
      *  com.baizhiedu.proxy.UserServiceImpl.*(..)
      
    • 语法2

      # 忽略包
      1. 类只存在⼀级包 com.UserServiceImpl
      * *.UserServiceImpl.*(..)
      
      2. 类存在多级包 com.baizhiedu.proxy.UserServiceImpl
      * *..UserServiceImpl.*(..)
      
  2. 包切入点表达式 实战

    指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能。
    
    • 语法1

      # 切⼊点包中的所有类,必须在pprrooxxyy中,不能在pprrooxxyy包的⼦包中
      * com.baizhiedu.proxy.*.*(..)
      
    • 语法2

      # 切⼊点当前包及其⼦包都⽣效
      * com.baizhiedu.proxy..*.*(..)
      
4.2.2、切入点函数
切入点函数:用于执行切入点表达式
  1. execution

    最为重要的切⼊点函数,功能最全。
    执⾏ ⽅法切⼊点表达式 类切⼊点表达式 包切⼊点表达式
    
    弊端:execution执⾏切⼊点表达式 ,书写麻烦
    	 execution(* com.baizhiedu.proxy..*.*(..))
    	 
    注意:其他的切⼊点函数 简化是execution书写复杂度,功能上完全⼀致
    
  2. args

    作⽤:主要⽤于函数(⽅法) 参数的匹配
    
    切⼊点:⽅法参数必须得是2个字符串类型的参数
    
    	execution(* *(String,String))
    	args(String,String)
    
  3. within

    作⽤:主要⽤于进⾏类、包切⼊点表达式的匹配
    
    切⼊点:UserServiceImpl这个类
    
    execution(* *..UserServiceImpl.*(..))
    
    within(*..UserServiceImpl)
    
    execution(* com.baizhiedu.proxy..*.*(..))
    
    within(com.baizhiedu.proxy..*)
    
  4. @annotation

    作⽤:为具有特殊注解的⽅法加⼊额外功能
    
    <aop:pointcut id="" expression="@annotation(com.baizhiedu.Log)"/>
    
    package cn.edu.njtech;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author Tim
     * @date 2021/11/3 14:58
     */
    
    /**
     * 用到哪上面 什么事实生效
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Log {
    }
    
    package cn.edu.njtech.proxy;
    
    import cn.edu.njtech.Log;
    
    /**
     * @author Tim
     * @date 2021/11/2 22:17
     */
    public class UserServiceImpl implements UserService {
        @Log
        @Override
        public void register(User user) {
            System.out.println("UserServiceImpl.register 业务运算 + DAO");
    //        throw new RuntimeException("测试异常");
        }
    
        @Override
        public boolean login(String name, String password) {
            System.out.println("UserServiceImpl.login");
            return true;
        }
    }
    
  5. 切入点函数的逻辑运算

    指的是 整合多个切入点函数一起配合工作,进而完成更为复杂的需求
    
    • and 与 操作

      案例:login 同时 参数 2个字符串
      
      1. execution(* login(String,String))
      
      2. execution(* login(..)) and args(String,String)
      
      注意:与操作不同⽤于同种类型的切⼊点函数
      
      案例:register⽅法 和 login⽅法作为切⼊点
      
      execution(* login(..)) or execution(* register(..))
      
      
    • or 或 操作

      案例:register⽅法 和 login⽅法作为切⼊点
      
      execution(* login(..)) or execution(* register(..))
      

5、AOP编程

5.1、AOP概念

AOP(Aspect Oriented Programing) 面向切面编程 = Spring动态代理开发
以切面为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建
切面 = 切入点 + 额外功能

OOP(Object Oriented Programing) 面向对象编程 Java
以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用,完成程序的构建

POP(Producer Oriented Programing) 面向过程(方法、函数)编程 C
以过程为基本单位的程序开发,通过过程间的彼此协同,相互调用,完成程序的构建

5.2、AOP编程的开发步骤

1. 原始对象
2. 额外功能(MethodInterceptor)
3. 切入点
4. 组装切面(额外功能+切入点)

5.3、切面的名次解释

切面 = 切入点 + 额外功能
几何学:
	面 = 点 + 相同的性质

6、AOP的底层实现原理

6.1、核心问题

1. AOP如何创建动态代理类(动态字节码技术)
2. Spring工厂如何加工创建代理对象
	通过原始对象的id值,获得的是代理对象

6.2、动态代理类的创建

6.2.1、JDK的动态代理
  • Proxy.newProxyInstance方法参数详解

微信截图_20211106123656.png

微信截图_20211106123731.png

  • 编码

    package cn.edu.njtech.jdk;
    
    import cn.edu.njtech.proxy.User;
    import cn.edu.njtech.proxy.UserService;
    import cn.edu.njtech.proxy.UserServiceImpl;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * @author Tim
     * @date 2021/11/4 19:50
     */
    public class TestJDKProxy {
        public static void main(String[] args) {
            // 1.创建原始对象
            UserService userService = new UserServiceImpl();
    
            // 2. JDK创建动态对象
            /*
    
             */
            UserService userServiceProxy = (UserService) Proxy.newProxyInstance(TestJDKProxy.class.getClassLoader(), userService.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("-----proxy log-----");
                    return method.invoke(userService, args);
                }
            });
            userServiceProxy.login("tim", "123456");
            userServiceProxy.register(new User());
        }
    }
    
6.2.2、CGlib的动态代理

CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证两者方法一直,同时在代理类中提供新的实现(原始方法+额外功能)

微信截图_20211106124106.png

  • CGlib编码

    package cn.edu.njtech.cglib;
    
    import org.springframework.cglib.proxy.Enhancer;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    /**
     * @author Tim
     * @date 2021/11/4 20:35
     */
    public class TestCglib {
        public static void main(String[] args) {
            // 1.创建原始对象
            UserService userService = new UserService();
    
            // 2.通过cglib方式创建动态代理对象
    
            /**
             * Proxy.newProxyInstance(classloader,interface,invocationhandler)
             *
             * Enhancer.setClassLoader
             * Enhancer.setSuperClass
             * Enhancer.setCallback(); --> MethodInterceptor(cglib)
             * Enhancer.create() -> 代理
             */
    
            Enhancer enhancer = new Enhancer();
            enhancer.setClassLoader(TestCglib.class.getClassLoader());
            enhancer.setSuperclass(userService.getClass());
            enhancer.setCallback(new MethodInterceptor() {
                // 等同于 InvocationHandler -- > invoker
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    System.out.println("-----cglib-----");
                    Object ret = method.invoke(userService, objects);
                    return ret;
                }
            });
            UserService service = (UserService) enhancer.create();
            service.login("tim", "123");
    
        }
    }
    
  • 总结

    1. JDK动态代理 Proxy.newProxyInstance() 通过接口创建代理的实现类
    2. CGlib动态代理 Enhancer 通过继承父类创建的代理类

6.3、Spring工厂如何加工原始对象

  • 思路分析

微信截图_20211106124631.png

  • 编码

    package cn.edu.njtech.factory;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.BeanPostProcessor;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * @author Tim
     * @date 2021/11/4 20:59
     */
    public class ProxyBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    
        @Override
        /**
         * Proxy.newProxyInstance();
         */
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
            return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(),
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("-----log-----");
                            return method.invoke(bean, args);
                        }
                    });
        }
    }
    
    <bean id="beanPostProcessor" class="cn.edu.njtech.factory.ProxyBeanPostProcessor"></bean>
    <bean id="userService" class="cn.edu.njtech.factory.UserServiceImpl"></bean>
    

7、基于注解的AOP编程

7.1、基于注解的AOP编程的开发步骤

  1. 原始对象

  2. 额外功能

  3. 切入点

  4. 组装切面

    package cn.edu.njtech.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    /**
     * @author Tim
     * @date 2021/11/4 21:12
     */
    
    /**
     * 1. 额外功能
     * public class MyAround implements MethodInterceptor
     * 2. 切入点
     */
    @Aspect
    public class MyAspect {
    
        @Pointcut("execution(* login(..))")
        public void myPointCut() {
        }
    
        @Around(value = "myPointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            System.out.println("----aspect log----");
            Object proceed = point.proceed();
            return proceed;
        }
    
        @Around(value = "myPointCut()")
        public Object around1(ProceedingJoinPoint point) throws Throwable {
            System.out.println("----aspect tx---");
            Object proceed = point.proceed();
            return proceed;
        }
    }
    
    <bean id="userService" class="com.baizhiedu.aspect.UserServiceImpl"/>
    <!--
        切⾯
        1. 额外功能
        2. 切⼊点
        3. 组装切⾯
    -->
    <bean id="arround" class="com.baizhiedu.aspect.MyAspect"/>
    
    <!--告知Spring基于注解进⾏AOP编程-->
    <aop:aspectj-autoproxy />
    

7.2、动态代理的创建方式

AOP底层实现 2种代理创建⽅式

1. JDK 通过实现接⼝ 做新的实现类⽅式 创建代理对象
2. Cglib通过继承⽗类 做新的⼦类 创建代理对象
	默认情况 AOP编程 底层应⽤JDK动态代理创建⽅式

如果切换Cglib
1. 基于注解AOP开发
<aop:aspectj-autoproxy proxy-target-class="true" />
2. 传统的AOP开发
<aop:config proxy-target-class="true"></aop>

8、AOP开发中的一个坑

// 坑:在同⼀个业务类中,进⾏业务⽅法间的相互调⽤,只有最外层的⽅法,才是加⼊了额外功能(内部的⽅法,通过普通的⽅式调⽤,都调⽤的是原始⽅法)。如果想让内层的⽅法也调⽤代理对象的⽅法,就要AppicationContextAware获得⼯⼚,进⽽获得代理对象。
public class UserServiceImpl implements UserService,ApplicationContextAware {
    private ApplicationContext ctx;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
	    this.ctx = applicationContext;
    }
    
    @Log
    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register 业务运算 + DAO ");
        //throw new RuntimeException("测试异常");
        //调⽤的是原始对象的login⽅法 ---> 核⼼功能
        /*
            设计⽬的:代理对象的login⽅法 ---> 额外功能+核⼼功能
            ApplicationContext ctx = new
            ClassPathXmlApplicationContext("/applicationContext2.xml");
            UserService userService = (UserService)
            ctx.getBean("userService");
            userService.login();
            Spring⼯⼚重量级资源 ⼀个应⽤中 应该只创建⼀个⼯⼚
        */
        UserService userService = (UserService)
        ctx.getBean("userService");
        userService.login("tim", "123456");
    }
    
    @Override
    public boolean login(String name, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}

9、AOP阶段知识总结

微信截图_20211106125407.png