Spring核心之AOP详解篇

Spring核心之AOP详解篇

一、设计模式之代理模式

之前谈Spring的IOC时说到了工厂设计模式,这一次谈Spring的AOP也离不开代理设计模式,Spring的面向切面编程AOP的重要基础就是动态代理。Spring动态代理是Spring框架提供的一种代理机制,它可以在运行时动态地创建代理对象。

在Spring中,有两种常用的动态代理方式:JDK动态代理CGLIB动态代理。Spring会根据具体情况选择使用JDK动态代理还是CGLIB动态代理来创建代理对象。在配置文件中,可以通过配置 aop:config 元素来声明需要使用代理的类和代理方式。也可以使用基于注解的方式,通过在目标类或方法上添加相关注解来实现动态代理。

动态代理不需要创建代理类的文件,代理类是在JVM运行期间动态生成的,利用的是动态字节码技术。

JDK动态代理

假设现在有这么一个接口UserService及其实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface UserService {
public void register(User user);

public void login(String name, String password);
}

public class UserServiceImpl implements UserService{
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register");
}

@Log
@Override
public void login(String name, String password) {
System.out.println("UserServiceImpl.login");
}
}

JDK动态代理是通过Java反射机制来实现的。当目标对象实现了接口时,Spring会使用JDK动态代理来创建代理对象。代理对象会实现与目标对象相同的接口,并将方法的调用委托给目标对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 测试JDK动态代理:原始类和代理类都实现同一接口如UserService,代理类实现接口的所有方法并添加额外功能(userServiceImpl.login+额外功能)
*/
@Test
public void test2() {
// 创建原始对象
UserService userService = new UserServiceImpl();
// 创建额外功能
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("-----log-----");
Object ret = method.invoke(userService, args);
return ret;
}
};
// 创建代理对象
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(MyTest.class.getClassLoader(), userService.getClass().getInterfaces(), handler);
userServiceProxy.login("huling", "123456");
}

ClassLoader用于创建代理类的Class对象,从而创建代理对象。对象是由类创建而来,先有类才有对象,而类的加载是由类加载器完成,动态创建的代理类本身是没有绑定类加载器的,所以需要借用一个外界的任何类的类加载器。

JDK动态代理不需要依赖第三方库,能够直接使用Java的标准库实现,但是它只能为实现了接口的类创建代理对象

image-20231031113847842

CGLIB动态代理

假设现在有这么一个类UserService

1
2
3
4
5
6
7
8
9
public class UserService {
public void register(User user) {
System.out.println("UserService.register");
}

public void login(String name, String password) {
System.out.println("UserService.login");
}
}

当目标对象没有实现任何接口时,Spring会使用CGLIB动态代理来创建代理对象。CGLIB是一个强大的字节码生成库,它通过继承目标对象来创建代理对象,并重写目标对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 测试CGlib动态代理:代理类继承原始类,重写原始类的所有方法并添加额外功能(super.login+额外功能)
*/
@Test
public void test3() {
// 创建原始对象
com.huling.cglib.UserService userService = new com.huling.cglib.UserService();
// 创建额外功能
MethodInterceptor interceptor = new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("-----log-----");
Object ret = method.invoke(userService, objects);
return ret;
}
};
// 创建代理对象
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(MyTest.class.getClassLoader());
enhancer.setSuperclass(com.huling.cglib.UserService.class);
enhancer.setCallback(interceptor);
com.huling.cglib.UserService userServiceProxy = (com.huling.cglib.UserService) enhancer.create();
userServiceProxy.register(new User());
userServiceProxy.login("huling", "123456");
}

CGLIB动态代理可以为没有实现接口的类创建代理对象,但是需要依赖CGLIB库,并且因为生成的代理对象继承了目标对象,对final方法和private方法无法进行代理。

image-20231031114136490

二、Spring动态代理

MethodBeforeAdvice

Spring的MethodBeforeAdvice接口是Spring AOP框架提供的一个通知接口,用于在被拦截的方法执行之前执行特定的逻辑操作。MethodBeforeAdvice通常用于实现前置通知(Before Advice),在目标方法执行之前执行一些预处理操作。例如,可以在before方法中添加==日志记录、参数校验、权限验证==等操作。

MethodBeforeAdvice只能进行前置通知,如果需要在方法执行结束后进行操作,可以使用其他类型的通知,如AfterReturningAdvice、ThrowsAdvice等。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyBefore implements MethodBeforeAdvice {
/**
*
* @param method 切入点目标方法
* @param objects 切入点方法参数
* @param o 切入点目标对象
* @throws Throwable
*/
@Override
public void before(Method method, Object[] args, Object o) throws Throwable {
System.out.println("-------MyBefore.before log-------");
}
}

对应的配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册目标对象-->
<bean id="userService" class="com.huling.proxy.UserServiceImpl"/>
<!--注册前置通知-->
<bean id="myBefore" class="com.huling.proxy.MyBefore"/>
<aop:config>
<!--精确的全限定切入点表达式-->
<aop:pointcut id="pc" expression="execution(* com.huling.proxy.UserServiceImpl.login(String,String))"/>
<!--组合切面-->
<aop:advisor advice-ref="myBefore" pointcut-ref="pc"/>
</aop:config>
</beans>

测试程序如下:

1
2
3
4
5
6
7
8
9
10
/**
* 测试Spring的动态代理
*/
@Test
public void test1() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean("userService", UserService.class);
userService.register(new User());
userService.login("huling","123456");
}

image-20231031115211080

MethodInterceptor

MethodInterceptor接口是Spring AOP框架提供的一个通知接口,用于在被拦截的方法执行前后执行一些特定的逻辑操作。和其他类型的通知(如MethodBeforeAdvice)不同,MethodInterceptor接口通常用于实现环绕通知(Around Advice),可以在目标方法执行前后以及出现异常时执行特定的逻辑操作,并且对方法的返回值进行处理。比如对数据库事务的控制都需要在原始方法之前和之后都需要处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ublic class Arround implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) {
System.out.println("-----日志监控开始-----");
// 必须的目标方法调用步骤
Object ret = null;
try {
ret = methodInvocation.proceed();
} catch (Throwable e) {
System.out.println("-----日志监控结束-----");
throw new RuntimeException(e);
}
System.out.println("-----日志监控结束-----");
return ret;
}
}

对应的配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册目标对象-->
<bean id="userService" class="com.huling.proxy.UserServiceImpl"/>
<!--注册环绕通知-->
<bean id="arround" class="com.huling.proxy.Arround"/>
<aop:config>
<!--精确的全限定切入点表达式-->
<aop:pointcut id="pc" expression="execution(* com.huling.proxy.UserServiceImpl.login(String,String))"/>
<!--组装切面-->
<aop:advisor advice-ref="arround" pointcut-ref="pc"/>
</aop:config>
</beans>

测试程序如下:

1
2
3
4
5
6
7
8
9
10
/**
* 测试Spring的动态代理
*/
@Test
public void test1() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean("userService", UserService.class);
userService.register(new User());
userService.login("huling","123456");
}

image-20231031120026744

三、Spring切入点详解

切入点表达式

切入点表达式的规则:execution(返回类型 包名.类名.方法名(参数列表))

img

需要注意的是,在编写切入点表达式时,可以使用通配符 * 匹配任意字符(但不包括路径分隔符/),使用 .. 匹配任意多层路径。同时,切入点表达式中的参数列表中,如果参数类型是java.lang包下的不需要指明包路径,否则需要指明如com.huling.proxy.User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.huling.proxy.UserServiceImpl"/>
<bean id="myBefore" class="com.huling.proxy.MyBefore"/>
<bean id="arround" class="com.huling.proxy.Arround"/>
<aop:config>
<!--精确的全限定切入点表达式-->
<aop:pointcut id="pc1" expression="execution(* com.huling.proxy.UserServiceImpl.login(String,String))"/>
<!--方法切入点表达式-->
<aop:pointcut id="pc2" expression="execution(* login(String,String))"/>
<!--类切入点表达式,两个点表示不限定包嵌套深度-->
<aop:pointcut id="pc3" expression="execution(* *..UserServiceImpl.*(..))"/>
<!--包切入点表达式,两个点表示不限定子目录嵌套深度-->
<aop:pointcut id="pc4" expression="execution(* com.huling.proxy..*.*(..))"/>
<!--复杂切入点表达式,使用and或or连接-->
<aop:pointcut id="pc5" expression="execution(* login(..)) and execution(* *(String,String))"/>
<aop:pointcut id="pc6" expression="execution(* login(String,String)) or execution(* register(com.huling.proxy.User))"/>
<aop:advisor advice-ref="arround" pointcut-ref="pc1"/>
</aop:config>
</beans>

切入点函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.huling.proxy.UserServiceImpl"/>
<bean id="myBefore" class="com.huling.proxy.MyBefore"/>
<bean id="arround" class="com.huling.proxy.Arround"/>
<aop:config>
<!--简化开发1:args(String,String)等同于execution(* *(String,String))-->
<aop:pointcut id="pc7" expression="args(String,String))"/>
<!--简化开发2:within(*..UserServiceImpl)等同于execution(* *..UserServiceImpl.*(..))-->
<!--简化开发2:within(com.huling.proxy..*)等同于execution(* com.huling.proxy..*.*(..))-->
<aop:pointcut id="pc8" expression="within(*..UserServiceImpl)"/>
<aop:pointcut id="pc9" expression="within(com.huling.proxy..*)"/>
<!--简化开发3:@annotation(com.huling.annotation.Log)切入点为Log注解的类或方法-->
<aop:pointcut id="pc10" expression="@annotation(com.huling.annotation.Log)"/>
<aop:advisor advice-ref="arround" pointcut-ref="pc7"/>
</aop:config>
</beans>

四、Spring AOP

Spring的AOP概念

Spring的AOP(面向切面编程)是Spring框架中的一个核心特性,它允许开发者在不修改原有代码的情况下,通过添加额外的逻辑来实现横切关注点的功能。

在传统的面向对象编程中,应用程序的业务逻辑通常分散在多个对象中,例如数据持久化、日志记录、事务管理、性能监控等,这些横切关注点会导致代码重复和散乱,使得维护和扩展变得困难。==AOP通过将这些横切关注点从主要业务逻辑中剥离出来,以切面的方式进行统一管理和配置,从而提高代码的可维护性和可重用性。==

Spring的AOP基于代理模式实现。它通过动态代理或者字节码生成技术,在运行时为目标对象生成一个代理对象,并将切面逻辑织入到代理对象的方法调用中。当应用程序调用代理对象的方法时,切面逻辑会在目标方法执行前、执行后或抛出异常时被触发执行。

Spring的AOP实现原理

Spring使用动态代理技术来创建代理对象,并通过BeanPostProcessor来进行扩展和定制。在创建代理对象时,Spring会根据目标对象的实现情况选择使用JDK动态代理或Cglib动态代理。它可以自动识别切面对象并为其创建代理对象,在代理对象创建完成后还可以通过BeanPostProcessor对代理对象进行进一步的定制和拓展。

我们简单模拟一下Spring的AOP实现,当然真实的实现要复杂得多,需要考虑为哪些类做代理、代理方案的选择等等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!beanName.equals("userService")) return bean;
// 1.省略创建原始对象这一步,Spring工厂已经帮我实现-->bean
// 2.创建额外功能
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("-----log------");
Object ret = method.invoke(bean, args);
return ret;
}
};
// 3.创建代理对象并返回!
return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), handler);
}
}

配置文件的内容如下:

1
2
3
4
5
6
7
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.huling.factory.UserServiceImpl"/>
<bean id="proxyBeanPostProcessor" class="com.huling.factory.ProxyBeanPostProcessor"/>
</beans>

测试程序如下:

1
2
3
4
5
6
7
8
9
10
/**
* 测试Spring的AOP底层原理:BeanPostProcessor+动态代理技术如JDK动态代理或CGlib动态代理,悄无声息替换原始对象为代理对象
*/
@Test
public void test4() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext1.xml");
com.huling.factory.UserService userService = ctx.getBean("userService", com.huling.factory.UserService.class);
userService.register(new User());
userService.login("huling","123456");
}

image-20231031123914551

五、基于注解的AOP编程

半注解开发步骤

首先,第一步是定义我们的切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 切面类:包括切入点和额外功能
*/
@Aspect
public class MyAspect {
// 切入点复用
@Pointcut("execution(* com.huling.aspect.UserServiceImpl.login(..))")
public void myPointCut(){}

@Around(value = "myPointCut()")
public Object invoke1(ProceedingJoinPoint joinPoint) {
System.out.println("-----前置通知-----");
Object ret = null;
try {
ret = joinPoint.proceed();
} catch (Throwable e) {
System.out.println("-----异常通知-----");
throw new RuntimeException(e);
}
System.out.println("-----后置通知-----");
return ret;
}

@Around("execution(* com.huling.aspect.OrderServiceImpl.*(..))")
public Object invoke2(ProceedingJoinPoint joinPoint) {
System.out.println("-----前置通知-----");
Object ret = null;
try {
ret = joinPoint.proceed();
} catch (Throwable e) {
System.out.println("-----异常通知-----");
throw new RuntimeException(e);
}
System.out.println("-----后置通知-----");
return ret;
}
}

第二步需要开启基于注解的AOP编程,并且注册原始对象和切面类对象:

1
2
3
4
5
6
<!--告知Spring使用基于注解的AOP编程-->
<aop:aspectj-autoproxy/>
<!--注册原始对象/目标对象-->
<bean id="userService" class="com.huling.aspect.UserServiceImpl"/>
<!--注册切面类-->
<bean id="myAspect" class="com.huling.aspect.MyAspect"/>

第三步,测试基于注解的AOP编程的结果:

1
2
3
4
5
6
7
8
9
10
/**
* 测试基于注解的AOP编程
*/
@Test
public void test5() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext2.xml");
com.huling.aspect.UserService userService = ctx.getBean("userService", com.huling.aspect.UserService.class);
userService.register(new User());
userService.login("huling", "123456");
}

image-20231031130929165

切换动态代理

我们调试上面的程序,会发现针对UserServiceImpl类的代理对象采用的是JDK动态代理方案:

image-20231031131324049

我们也可以强制Spring使用CGLIB动态代理,只需要在配置文件中aop:aspectj-autoproxy标签中修改属性proxy-target-class值为true

1
2
3
4
5
6
7
8
9
10
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.huling.aspect.UserServiceImpl"/>
<bean id="orderService" class="com.huling.aspect.OrderServiceImpl"/>
<bean id="myAspect" class="com.huling.aspect.MyAspect"/>
<!--告知Spring使用基于注解的AOP编程,修改Spring动态代理默认方案-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

其中OrderServiceImpl类的代码:

1
2
3
4
5
public class OrderServiceImpl {
public void showOrder() {
System.out.println("OrderServiceImpl.showOrder");
}
}

测试程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试Spring使用的动态代理方案和切换方法:
* 1.默认有接口的原始类使用JDK动态代理增强,否则使用CGlib动态代理(更加通用)
* 2.proxy-target-class="true"全部变成CGlib动态代理
*/
@Test
public void test6() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext3.xml");
com.huling.aspect.UserService userService = ctx.getBean("userService", com.huling.aspect.UserService.class);
userService.register(new User());
userService.login("huling", "123456");
OrderServiceImpl orderService = ctx.getBean("orderService", OrderServiceImpl.class);
orderService.showOrder();
}

再次调试上面的程序,会发现针对UserServiceImpl类的代理对象采用的是CGLIB动态代理方案:

image-20231031131909073

代理方法调用

在同一个业务类中,进行业务方法间的调用时,只有最外层的方法才是通过代理对象调用的(加入了额外功能),内部调用的本类的其他方法都是通过原始对象调用,如果想要让内部的方法也通过代理对象调用,那么就需要实现ApplicationContextAware接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserServiceImpl implements UserService, ApplicationContextAware {

private ApplicationContext context;

@Override
public void register() {
System.out.println("UserServiceImpl.register");
UserService userService = context.getBean("userService", UserService.class);
userService.login();
}

@Override
public void login() {
System.out.println("UserServiceImpl.login");
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
}