Spring 学习笔记②:动态代理及面向切面编程


收藏这三篇笔记,完整回顾Spring常见问题及使用方式速查:

  1. Spring 学习笔记①:IoC容器、Bean与注入
  2. Spring 学习笔记②:动态代理及面向切面编程(即本篇)
  3. Spring 学习笔记③:JDBC与事务管理

0. 基本概念

  • 面向切面编程,它将业务逻辑的各个部分进行隔离,使开发人员在编写业务逻辑时可以专心于核心业务,从而提高了开发效率。
  • 关注点(切入点)代码,就是指重复执行的代码。
  • 业务代码与关注点代码分离:关注点代码写一次即可;开发者只需要关注核心业务,运行时期,执行核心业务代码时候动态植入关注点代码。
   // 关注点代码举例
   public void add(User user) { 
       Session session = null; 
       Transaction trans = null; 
       try { 
           session = HibernateSessionFactoryUtils.getSession();   // 【关注点代码】
           trans = session.beginTransaction();    // 【关注点代码】
           session.save(user);     // 核心业务代码:如何保存、用户有效性校验
           trans.commit();     //…【关注点代码】
       } catch (Exception e) {     
           e.printStackTrace(); 
           if(trans != null){ 
               trans.rollback();   //..【关注点代码】
           } 
       } finally{ 
           HibernateSessionFactoryUtils.closeSession(session);   ////..【关注点代码】
   
       } 
   }

1. AOP概念及术语

术语 解释
Joinpoint(连接点) 指那些被拦截到的点,在 Spring 中,可以被动态代理拦截目标类的方法。
Pointcut(切入点) 指要对哪些 Joinpoint 进行拦截,即被拦截的连接点(方法)。
Advice(通知) 指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容
Target(目标) 指代理的目标对象。
Weaving(植入) 指把增强代码应用到目标上,生成代理对象的过程。
Proxy(代理) 指生成的代理对象。
Aspect(切面) 切入点和通知的结合。

2. 动态代理

2.1 代理模式

为其他对象提供一个代理以控制对某个对象的访问,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。在Spring中被用来做无侵入的代码增强。

和装饰器模式有什么不同?答:不会产生太多的装饰类。

2.1.1 静态代理

  1. 被代理类承担、插入自己的方法。
  2. 创建一个代理类,持有被代理类(目标对象)的引用,实现接口(但具体业务由创建一个接口,被代理类(目标对象)实现接口。

显然,一个代理类只能代理一个目标对象,会造成目标类的泛滥。这也是“静态”的意思。


业务逻辑的接口:

public interface TargetInterface {
    void doSomething();
}

目标对象:

public class TargetImpl implements TargetInterface{
    @Override
    public void doSomething() {
        System.out.println("Hello World!");
    }
}

代理类:

public class TargetProxy implements TargetInterface{
    private TargetInterface target = new TargetImpl(); // 持有引用
    @Override
    public void doSomething() {
        System.out.println("Before invoke" );
        this.target.doSomething();
        System.out.println("After invoke");
    }
}

UML图:

image.png

2.1.2 动态代理

  1. 目标接口和目标对象和静态代理类一致。
  2. 运用反射来创建代理类。

代理类对象:

   public class ProxyHandler implements InvocationHandler{
       private Object object;
       public ProxyHandler(Object object){
           this.object = object;
       }
       
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
           System.out.println("Before invoke");
           method.invoke(object, args);
           System.out.println("After invoke");
           return null;
       }
   }

最后基于反射完成代理过程,详见2.2小节:

InvocationHandler handler = new ProxyHandler(new TargetImpl());
TargetInterface targetProxy = (TargetInterface) Proxy.newProxyInstance(TargetImpl.getClassLoader(), TargetImpl.getInterfaces(), handler);
targetProxy.doSomething();

2.1.3 代理模式的缺点

  • 静态代理:由于静态代理需要实现目标对象的相同接口,那么可能会导致代理类会非常非常多,不好维护。
  • 动态代理:目标对象一定是要有接口的,没有接口就不能实现动态代理。

2.2 java.lang.reflect.Proxy

  • java.lang.reflect.Proxy 是基于反射的动态代理,是属于JDK的原生实现。

2.2.1 实现Invoke接口、注入增强代码

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

例如:

   public class ProxyHandler implements InvocationHandler{
       private Object object;
       public ProxyHandler(Object object){
           this.object = object;
       }
       
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
           System.out.println("Before invoke");  // 增强代码1
           Object obj = method.invoke(object, args);
           System.out.println("After invoke"); // 增强代码2
           return obj;
       }
   }

2.2.2 基于JDK的Proxy获得代理对象

利用 static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler invocationHandler ) 构建代理对象。其中各参数如下:

  1. ClassLoader loader :指定当前target对象使用类加载器,获取加载器的方法是固定的,如 TargetInterface.class.getClassLoader()
  2. Class<?>[] interfaces :target对象实现的接口的类型,使用泛型方式确认类型,如 new Class[] { TargetInterface.class}
  3. InvocationHandler invocationHandler :事件处理,执行target对象的方法时,会触发事件处理器的方法,会把当前执行target对象的方法作为参数传入。

2.3 CGLib

相较于基于JDK的动态代理仍有局限性,即其目标对象必须要实现至少一个接口。而借用CGlib则不需要,其凭借一个小而快的字节码处理框架ASM转换字节码并生成新的类。由于其基于内存构建出一个子类来扩展目标对象的功能,也被称为“子类代理”。

需要注意的是,目标类不能为不可继承的 final 类型或目标对象的方法不能为静态类型。

public class TargetProxyFactory {
    public static TargetProxy getProxyBean() {
        // 1. 准备目标类和自定义的切面类(用于增强目标对象)
        final Target goodsDao = new Target();
        final Aspect aspect = new Aspect();
        // 2. 构建CgLib的核心类`Enhancer`
        Enhancer enhancer = new Enhancer(); 
        // 3. 确定需要增强的类
        enhancer.setSuperclass(goodsDao.getClass());
        // 4. 添加回调函数:实现一个MethodInterceptor接口
        enhancer.setCallback(() -> {
            // intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                aspect.myBefore(); // 前增强
                Object obj = method.invoke(goodsDao, args); // 目标方法执行
                aspect.myAfter(); // 后增强
                return obj;
            }
        });
        // 5. 创建代理类
        TargetProxy targetProxy = (TargetProxy) enhancer.create();
        return targetProxy;
    }
}

构建CGLib依赖的pom.xml 文件为:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

3. Spring中的AOP

3.1 pom.xml文件

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

<!-- Spring 2.0 以后,新增了对 AspectJ 方式的支持,新版本的 Spring 框架,建议使用 AspectJ 方式开发 AOP -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

3.2 基于通知类型代理增强的Bean

3.2.1 AOP通知类型

  • 通知(Advice)其实就是对目标切入点进行增强的内容。
名称 说明
org.springframework.aop.MethodBeforeAdvice(前置通知) 在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。
org.springframework.aop.AfterReturningAdvice(后置通知) 在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。
org.aopalliance.intercept.MethodInterceptor(环绕通知) 在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。
org.springframework.aop.ThrowsAdvice(异常通知) 在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。
org.springframework.aop.IntroductionInterceptor(引介通知) 在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)。

3.2.2 示例:拦截器与工厂方法

现在,假设要增强 UserDao ,切入点是 save() 方法,要在之前加入自己面向 User 的增强方法,如校验等切面业务。核心要点有:

  1. 目标对象要实现一个通用接口(除非使用CGlib)。
  2. 代码增强类(切面类)实现一种通知的接口,在其中做增强。
  3. 在配置文件中利用 org.springframework.aop.framework.ProxyFactoryBean 创建代理类,需要给出 proxyInterfaces (目标对象的接口)、 target (目标对象的引用)、 interceptorNames (拦截器/切面类的名字)。

@Repository("userDao")
public class UserDao implements UserDaoInterface { // 实现一个通用接口
    public void save(User user){
        System.out.println("数据库已保存" + user); // 业务代码
    }
}

代码切面类:

public class UserDaoAspect implements MethodInterceptor { // 此处以环绕通知为例子
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("Dao enhanced before"); // 增强1(重复的切入点代码)
        Object obj = methodInvocation.proceed();   // 这里会由Spring替我们注入target对象
        System.out.println("Dao enhanced after");  // 增强2(重复的切入点代码)
        return obj;
    }
}

创建配置文件:

   <beans>
       <bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 拦截器/切面类-->
   
       <bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目标类 -->
       
       <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
           <!--代理对象实现的接口、目标对象、拦截者(切面类) -->
           <property name="proxyInterfaces" value="MVC.Model.Dao.UserDaoInterface"/>
           <property name="target" ref="targetUserDao"/> <!-- 此处是一个引用ref -->
           <property name="interceptorNames" value="userDaoAspect"/>
           <!-- 如何生成代理,true:使用CGLib; false :使用JDK动态代理(默认) -->
           <property name="proxyTargetClass" value="true"/>
       </bean>
   </beans>

其中, UserService 类需要进行修改:

   @Service("userService")
   public class UserService {
   
       @Resource(name = "userDaoProxy")  // 注入ProxyFactoryBean工厂方法获得的代理类(增强类)
       private UserDaoInterface userDao; // 修改为其接口
   
       public void service(User user){
           System.out.println("MVC Service sth. with " + user);
           this.userDao.save(user);
           System.out.println("MVC Service Over.");
       }
   }

工程结构:

TIM截图20200609230711.png

3.3 使用AspectJ开发AOP(推荐)

  • AspectJ 是一个基于 Java 语言的 AOP 框架,它扩展了 Java 语言。Spring 2.0 以后,新增了对 AspectJ 方式的支持,新版本的 Spring 框架,建议使用 AspectJ 方式开发 AOP。
  • 主要有两种开发方法:“基于XML的声明式开发”和“基于注解的声明式开发”。

3.3.1 示例①:基于XML的声明式开发

需要引入命名空间:

<beans xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd">

编写切面类:

   public class UserDaoAspect { // 以前、后通知为例
       public void asBefore() {
           System.out.println("Dao enhanced before"); // 一些重复的代码
       }
   
       public void asAfter() {
           System.out.println("Dao enhanced after");
       }
   }

被增强的目标类(不再需要接口):

   @Repository("userDao")
   public class UserDao {
       public void save(User user){
           System.out.println("数据库已保存" + user); // 业务代码
       }
   }

编写配置文件:

   <beans>
       <bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 切面类 -->
   
       <bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目标类(随后都会变成代理类) -->
   
       <!-- 面向切面编程,交由Spring管理-->
       <aop:config>
           <!-- 配置切入点 -->
           <aop:pointcut expression="execution(* MVC.Model.Dao.UserDao.save(..))" id="pointcut-save"/>
   
           <!-- 对切入点配置切面 -->
           <aop:aspect ref="userDaoAspect">
               <!-- 配置前置增强 -->
               <aop:before method="asBefore" pointcut-ref="pointcut-save" />
               <aop:after method="asAfter" pointcut-ref="pointcut-save"/>
           </aop:aspect>
       </aop:config>
       <!-- 如何生成代理,true:使用CGLib; false :使用JDK动态代理(默认)|如果目标类没有声明接口,则Spring将自动使用CGLib动态代理 -->
       <aop:aspectj-autoproxy  proxy-target-class="true"/>
   </beans>

注意此处 proxy-target-class="false" 的话注入会报错 ...but was actually of type ‘com.sun.proxy.$Proxy7'

获取增强类:

 UserDao userDaoProxy = (UserDao) applicationContext.getBean("targetUserDao");

3.3.2 示例中的标签及对应的切面类解析

<aop> 标签格式概览:

  <aop:config>
    <!-- 配置切入点,通知最后增强哪些方法 -->
    <aop:pointcut expression="execution ( * target.* (..))" id="pointcut-id-x" />
    
    <aop:aspect ref="myAspect">
      <!--前置通知,关联通知 Advice和切入点PointCut -->
      <aop:before method="myBefore" pointeut-ref="pointcut-id-x" />
      
      <!--后置通知,在方法返回之后执行,就可以获得返回值returning 属性 -->
      <aop:after-returning method="myAfterReturning" pointcut-ref="pointcut-id-x" returning="returnVal" />
      
      <!--环绕通知 -->
      <aop:around method="myAround" pointcut-ref="pointcut-id-x" />
      
      <!--抛出通知:用于处理程序发生异常,可以接收当前方法产生的异常 -->
      <!-- * 注意:如果程序没有异常,则不会执行增强 -->
      <!-- * throwing属性:用于设置通知第二个参数的名称,类型Throwable -->
      <aop:after-throwing method="myAfterThrowing" pointcut-ref="pointcut-id-x" throwing="e" />
      
      <!--最终通知:无论程序发生任何事情,都将执行 -->
      <aop:after method="myAfter" pointcut-ref="pointcut-id-x" />
    </aop:aspect>
  </aop:config>

对应的切面类:

   //切面类
   public class MyAspect {
       // 前置通知
       public void myBefore(JoinPoint joinPoint) {
           System.out.print("前置通知,目标:" + joinPoint.getTarget() + " 方法名称: " + joinPoint.getSignature().getName());
       }
       
       // 后置通知
       public void myAfterReturning(JoinPoint joinPoint) {
           System.out.print("后置通知,方法名称:" + joinPoint.getSignature().getName());
       }
       
       // 环绕通知
       public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
               throws Throwable {
           System.out.println("环绕开始"); // 开始
           Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
           System.out.println("环绕结束"); // 结束
           return obj;
       }
       
       // 异常通知
       public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
           System.out.println("异常通知" + "出错了" + e.getMessage());
       }
       
       // 最终通知
       public void myAfter() {
           System.out.println("最终通知");
       }
   }

3.3.3 示例②:基于注解的声明式开发

名称 说明
@Aspect 用于定义一个切面。
@Before 用于定义前置通知,相当于 BeforeAdvice。
@AfterReturning 用于定义后置通知,相当于 AfterReturningAdvice。
@Around 用于定义环绕通知,相当于MethodInterceptor。
@AfterThrowing 用于定义抛出通知,相当于ThrowAdvice。
@After 用于定义最终final通知,不管是否异常,该通知都会执行。
@DeclareParents 用于定义引介通知,相当于IntroductionInterceptor。

编写配置文件:

<context:component-scan base-package="MVC"/> <!-- 扫描注解 -->
<aop:aspectj-autoproxy  proxy-target-class="true"/> <!-- 使用CGlib植入切面代码 -->

构建切面类:

   @Aspect
   @Component
   public class UserDaoAspect {
       @Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))") // 配置切入点
       private void pointCut(){} // 要求:方法必须是private,没有值,名称自定义,没有参数
       
       @Before("pointCut()")
       public void asBefore() {
           System.out.println("Dao enhanced before");
       }
       @After("pointCut()")
       public void asAfter() {
           System.out.println("Dao enhanced after");
       }
   }

3.4 一对多的切面及相关问题

3.4.1 实现多切面的有序运行

  • 当有多个切面时,它不会存在任何顺序,这些顺序代码会随机生成,但是有时候我们希望它按照指定的顺序运行。
  • 此时,需要借助 org.springframework.core.annotation.Order 注解类或实现 org.springframework.core.Ordered 接口。

构建一个新的切面:

   @Aspect
   @Component
   @Order(value = 2) // 会在第二个执行
   public class UserDaoAspect2 {
       @Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
       private void pointCut(){} // 要求:方法必须是private,没有值,名称自定义,没有参数
   
   
       @Before("pointCut()")
       public void asBefore() {
           System.out.println("Dao enhanced before 2");
       }
   
       @After("pointCut()")
       public void asAfter() {
           System.out.println("Dao enhanced after 2");
       }
   }

3.4.2 在注解中实现多个切入点

假设有一个新的业务需要被 UserDao 切入:

@Repository
public class ShopDao {
    public void load(){
        System.out.println("载入商品");
    }
}

UserDao 需要修改为:

   @Aspect
   @Component
   public class UserDaoAspect {
       @Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
       private void pointCut(){} 
   
       @Pointcut("execution(* MVC.Model.Dao.ShopDao.load())")
       private void pointCut2(){} // 新的切入点
   
       @Before("pointCut()")
       public void asBefore() {
           System.out.println("Dao enhanced before");
       }
   
       @After("pointCut() || pointCut2()") // 修改表达式语句,植入代码
       public void asAfter() {
           System.out.println("Dao enhanced after");
       }
   }

3.4.3 获取代理对象的目标对象

由于被CGLib植入之后,IoC容器中所有的目标对象都会变成代理对象,且Spring没有提供获取原生对象的API。

参考解决方法:CSDN@在spring中获取代理对象代理的目标对象工具类

import java.lang.reflect.Field;  
import org.springframework.aop.framework.AdvisedSupport;  
import org.springframework.aop.framework.AopProxy;  
import org.springframework.aop.support.AopUtils;  
  
public class AopTargetUtils {  
    public static Object getTarget(Object proxy) throws Exception {  
        return !AopUtils.isAopProxy(proxy) ? proxy :
            (AopUtils.isJdkDynamicProxy(proxy) ? getJDKDynamicProxyTargetObject(proxy) : getCGlibProxyTargetObject(proxy))
    }  
    // 获取CGLib 代理的对象
    private static Object getCGlibProxyTargetObject(Object proxy) throws Exception { 
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");  
        h.setAccessible(true);  
        Object dynamicAdvisedInterceptor = h.get(proxy);  
        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");  
        advised.setAccessible(true);  
        return ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();  
    }  
    // 获取JDK代理的对象
    private static Object getJDKDynamicProxyTargetObject(Object proxy) throws Exception {  
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");  
        h.setAccessible(true);  
        AopProxy aopProxy = (AopProxy) h.get(proxy); 
        Field advised = aopProxy.getClass().getDeclaredField("advised");  
        advised.setAccessible(true);  
        return ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();  
    }
} 

3.5 切入点表达式

切入点表达式为:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

符号讲解

  • ? 号代表0或1,表明可选参数。
  • * 号代表任意类型取0或多,常用作通配符。

表达式匹配参数讲解

  • modifiers-pattern? :【可选】连接点的类型。
  • ret-type-pattern :【必填】连接点返回值类型,常用 * 做匹配。
  • declaring-type-pattern? :【可选】连接点的类型(包.类),如 com.example.User ,通常不省略。
  • name-pattern(param-pattern) :【必填】要匹配的连接点名称,即 方法 (如果给出了连接点的类型,要用 . 隔开),如 save(..) ;括号里面是方法的参数(匹配方法见下)。
  • throws-pattern? :【可选】连接点抛出的异常类型。

方法参数****的匹配方法:

  • () 匹配不带参数的方法。
  • (..) 匹配带参数的方法(任意个)。
  • (*, String) 匹配带两个参数的方法且第二个必为String。

3.6 PointCut指示符

除了使用 execution 作为切入点表达式进行配置,还可以使用以下表达式内容(需要保证所有的连接点都在IoC容器内):

  • within :匹配所有在指定子包内的类的连接点,如 within(com.xyz.service.*)within(com.xyz.service..*);严格匹配目标对象,不理会继承关系 。
  • this : 匹配所有代理对象为目标类型中的连接点,如this(com.xyz.service.AccountService)
  • target :匹配所有实现了指定接口的目标对象中的连接点,如 target(com.xyz.service.UserDaoInterfece)
  • bean :匹配所有指定命名方式的类的连接点,如 bean(userDao)
  • args :匹配任何方法参数是指定的类型的连接点,如 args(*, java.lang.String)args(java.lang.Long, ..)
  • @within :匹配标注有指定注解的类(不可为接口)的所有连接点(要求注解的Retention级别为CLASS),如 @within(com.google.common.annotations.Beta) ;对子类不起效,除非使用 @within(xxxx)+ 或者子类中继承的方法未进行重载。
  • @target :匹配标注有指定注解的类(不可为接口)的所有连接点(要求注解的Retention级别为RUNTIME),如 @target(org.springframework.stereotype.Repository) ;对子类不起效。
  • @args :匹配传入的参数类标注有指定注解的所有连接点,如 @args(org.springframework.stereotype.Repository)
  • @anntation :匹配所有标注有指定注解的连接点,如 @annotation(com.aop.annotation.AdminOnly)

除此之外,表达式还可以用 &&||! 进行合并,详见3.4.2小节。

@within 和 @target的区别:

  • @within:若当前类有注解,则该类对父类重载及自有方法被拦截。子类中未对父类的方法进行重载时,亦被拦截。
  • @target:若当前类有注解,则该类对父类继承、重载及自有的方法被拦截;对子类不起效。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章