Spring | 动态代理详解

Spring | 动态代理详解

额外功能

MethodBeforeAdvice

MethodBeforeAdvice 接口:定义额外功能,运行在原始方法执行之前,额外功能操作都写在该接口的 before() 方法中,例如

public class Before implements MethodBeforeAdvice {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("----- log:method before advice -----");
    }
}

before() 方法的三个参数
Method:额外功能所增加给的那个原始方法,例如 UserDAOImplsave() 方法
Object[]原始方法的参数
Object:额外功能所增加给的那个原始类对象,例如 UserDAOImpl 的对象

在实战中,三个参数根据需要进行使用,不一定都会用到

MethodInterceptor

MethodIntercepter 接口:定义额外功能,可以根据需要运行在原始方法执行之前、之后或前+后,额外功能操作都写在该接口的 invoke() 方法中,例如

package cool.yzt.dynamic;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class Around implements MethodInterceptor {
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("---- log:原始方法运行之前 -----");
        Object ret = methodInvocation.proceed();
        System.out.println("---- log:原始方法运行之后 -----");
        return ret;
    }
}

定义切入点

<bean id="around" class="cool.yzt.dynamic.Around"/>

<aop:config>
    <aop:pointcut id="pc" expression="execution(* * (..))"/>
    <aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>

测试

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean("userService",UserService.class);
userService.save(new User());

打印结果

---- log:原始方法运行之前 -----
UserServiceImpl.save() run
UserDAOImpl.save() run
---- log:原始方法运行之后 -----

可以看到,额外功能成功的运行在原始方法执行之前和之后,在实战中,事务功能是需要在原始方法之前和之后添加的

MethodInvocation
invoke() 的参数,MethodInvocation 可以看做是额外功能所加给的那个原始方法的再一次封装,原始方法 MethodInvocation 必须手动调用 proceed() 才可以执行,其返回值就是原始方法的返回值

原始方法抛出异常

public Object invoke(MethodInvocation methodInvocation) throws Throwable {
    Object ret = null;
    try {
        ret = methodInvocation.proceed();
        System.out.println("---- log:原始方法运行成功 -----");
    } catch (Throwable throwable) {
        System.out.println("---- log:原始方法抛出异常 -----");
        throwable.printStackTrace();
    }
    return ret;
}

MethodInterceptor 影响原始方法的返回值

  • 原始方法 MethonInvocation 的返回值直接作为 invoke() 方法的返回值返回,则 MenthonInterceptor 不会影响原始方法的返回值
  • 若要影响原始方法的返回值,不直接返回原始方法的返回值而是做一些额外加工即可

切入点

切入点决定了额外功能的加入位置,Spring 配置文件中,使用标签 <aop:pointcut id="pc" expression="execution(* * (..))"/> 定义切入点,其中

  1. id:切入点的唯一标识
  2. execution:切入点函数
  3. * * (..):切入点表达式,* * (..) 匹配了所有方法

切入点表达式

1. 方法切入点

首先,看一个方法的定义,可以看做由三个部分组成,A:修饰符合返回值,B:方法名,C:参数列表

public User findById(int id);
//    A         B        C

方法切入点表达式也分为三个部分,用于定位一个需要额外功能的方法,例如 * * (..),第一个 * 表示任意的方法修饰符与返回值,第二个 * 表示任意的方法名, (..) 表示任意的参数列表

实例
再定义几个 UserServiceImplUserDAOImpl 的方法

public void save(User user);
public User findById(int id);
public List<User> findAll();
public void login(String username,String password);
public void login(User user);

定义切入点

<aop:config>
    <!--切入点:所有方法-->
    <aop:pointcut id="pc1" expression="execution(* * (..))"/>

    <!--切入点:所有的save方法-->
    <aop:pointcut id="pc2" expression="execution(* save (..))"/>

    <!--切入点:UserServiceImpl的findAll方法-->
    <aop:pointcut id="pc3" expression="execution(* cool.yzt.service.UserServiceImpl.findAll (..))"/>

    <!--切入点:UserServiceImpl的login(String,String)方法-->
    <aop:pointcut id="pc4" expression="execution(* cool.yzt.service.UserServiceImpl.login (String,String))"/>

    <!--切入点:UserServiceImpl的login(User)方法-->
    <aop:pointcut id="pc5" expression="execution(* cool.yzt.service.UserServiceImpl.login (cool.yzt.entity.User))"/>
</aop:config>

注意

  1. java.lang 包下的类可以只写类名,例如 String,非 java.lang 包下的类必须写全类名
  2. 精准限定方法:包名.类名.方法名
  3. 方法参数列表的表达式中,.. 可以与具体参数类型连用,例如 login(String,..)

2. 类切入点

类切入点选择整个类作为额外功能的加入位置,该类下的所有方法都会加上额外功能

  • 类中所有方法加入额外功能
* cool.yzt.service.UserServiceImpl.* (..)
  • 简写:忽略包名,类只存在一级包,即 cool.UserServiceImpl
* *.UserServiceImpl.* (..)
  • 简写:忽略包名,类存在多级包,即 cool.yzt.service.UserServiceImpl
* *..UserServiceImpl.* (..)

3. 包切入点

包切入点选择整个包作为额外功能的加入位置,该包下所有类的所有方法都会加上额外功能

  • 当前包,不包括其子包
# service 包下的所有类的所有方法
* cool.yzt.service.*.* (..)
  • 当前包及其所有子包
# cool.yzt包下及其所有子包下的所有类的所有方法
* cool.yzt..*.* (..)

切入点函数

切入点函数用于执行切入点表达式

1. execution

execution 是最终要的切入点函数,功能最全,可以执行方法切入点表达式、类切入点表达式、包切入点表达式,其他的切入点函数都是 execution 的简化书写,功能上一致

2. args

args 函数主要用于方法参数的匹配,例如,选择参数为 String,String 的方法作为切入点,切入点函数和表达式可以有如下写法

execution(* * (String,String))
<!--或者-->
args(String,String)

3. within

within 函数主要用于类、包切入点的匹配,例如,选择 UserServiceImpl 类作为切入点,可以有如下写法

execution(* *..UserServiceImpl.* (..))
<!--或者-->
within(*..UserServiceImpl)

再如,选择 service 包作为切入点, 可以有如下写法

execution(* cool.yzt.service..*.* (..))
<!--或者-->
within(cool.yzt.service..*.*)

4. @annotation

@annotation 主要用于匹配具有特殊注解(例如自定义注解)的方法

自定义注解

package cool.yzt.annotaion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

在想要加入额外功能的方法上加上自定义的 Log 注解,然后配置切入点,切入点表达式语法为 @annotation(注解的全类名)

<aop:config>
    <aop:pointcut id="pc" expression="@annotation(cool.yzt.annotaion.Log)"/>
    <aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>

5. 切入点函数的逻辑运算

切入点函数的逻辑运算可以整合多个切入点函数一起配合工作,进而完成更为复杂的需求

  1. and
    需求:方法名为 login 且参数为 String,String 的方法作为切入点
execution(* login(String,String))
<!--或者-->
execution(* login(..)) and args(String,String)

注意:and 操作不可以用于连接同种类型的切入点函数

  1. or
    需求:方法名为 login 或者方法名为 save 的方法作为切入点
execution(* login(..)) or execution(* save(..))

参考

孙哥说Spring5

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://yzt.cool/archives/spring动态代理详解