Spring核心之AOP详解篇
一、设计模式之代理模式
之前谈Spring的IOC时说到了工厂设计模式,这一次谈Spring的AOP也离不开代理设计模式,Spring的面向切面编程AOP的重要基础就是动态代理。Spring动态代理是Spring框架提供的一种代理机制,它可以在运行时动态地创建代理对象。
在Spring中,有两种常用的动态代理方式:JDK动态代理和CGLIB动态代理。Spring会根据具体情况选择使用JDK动态代理还是CGLIB动态代理来创建代理对象。在配置文件中,可以通过配置 aop:config 元素来声明需要使用代理的类和代理方式。也可以使用基于注解的方式,通过在目标类或方法上添加相关注解来实现动态代理。
动态代理不需要创建代理类的文件,代理类是在JVM运行期间动态生成的,利用的是动态字节码技术。
JDK动态代理
假设现在有这么一个接口UserService及其实现类:
| 12
 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动态代理来创建代理对象。代理对象会实现与目标对象相同的接口,并将方法的调用委托给目标对象。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | 
 
 @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的标准库实现,但是它只能为实现了接口的类创建代理对象。

CGLIB动态代理
假设现在有这么一个类UserService:
| 12
 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是一个强大的字节码生成库,它通过继承目标对象来创建代理对象,并重写目标对象的方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | 
 
 @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方法无法进行代理。

二、Spring动态代理
MethodBeforeAdvice
Spring的MethodBeforeAdvice接口是Spring AOP框架提供的一个通知接口,用于在被拦截的方法执行之前执行特定的逻辑操作。MethodBeforeAdvice通常用于实现前置通知(Before Advice),在目标方法执行之前执行一些预处理操作。例如,可以在before方法中添加==日志记录、参数校验、权限验证==等操作。
MethodBeforeAdvice只能进行前置通知,如果需要在方法执行结束后进行操作,可以使用其他类型的通知,如AfterReturningAdvice、ThrowsAdvice等。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | public class MyBefore implements MethodBeforeAdvice {
 
 
 
 
 
 
 @Override
 public void before(Method method, Object[] args, Object o) throws Throwable {
 System.out.println("-------MyBefore.before log-------");
 }
 }
 
 | 
对应的配置文件内容如下:
| 12
 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>
 
 | 
测试程序如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | 
 
 @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");
 }
 
 | 

MethodInterceptor
MethodInterceptor接口是Spring AOP框架提供的一个通知接口,用于在被拦截的方法执行前后执行一些特定的逻辑操作。和其他类型的通知(如MethodBeforeAdvice)不同,MethodInterceptor接口通常用于实现环绕通知(Around Advice),可以在目标方法执行前后以及出现异常时执行特定的逻辑操作,并且对方法的返回值进行处理。比如对数据库事务的控制都需要在原始方法之前和之后都需要处理。
| 12
 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;
 }
 }
 
 | 
对应的配置文件内容如下:
| 12
 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>
 
 | 
测试程序如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | 
 
 @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");
 }
 
 | 

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

需要注意的是,在编写切入点表达式时,可以使用通配符 * 匹配任意字符(但不包括路径分隔符/),使用 .. 匹配任意多层路径。同时,切入点表达式中的参数列表中,如果参数类型是java.lang包下的不需要指明包路径,否则需要指明如com.huling.proxy.User。
| 12
 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>
 
 | 
切入点函数
| 12
 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实现,当然真实的实现要复杂得多,需要考虑为哪些类做代理、代理方案的选择等等:
| 12
 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;
 
 
 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;
 }
 };
 
 return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), handler);
 }
 }
 
 | 
配置文件的内容如下:
| 12
 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>
 
 | 
测试程序如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | 
 
 @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");
 }
 
 | 

五、基于注解的AOP编程
半注解开发步骤
首先,第一步是定义我们的切面类:
| 12
 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编程,并且注册原始对象和切面类对象:
| 12
 3
 4
 5
 6
 
 | <aop:aspectj-autoproxy/>
 
 <bean id="userService" class="com.huling.aspect.UserServiceImpl"/>
 
 <bean id="myAspect" class="com.huling.aspect.MyAspect"/>
 
 | 
第三步,测试基于注解的AOP编程的结果:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | 
 
 @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");
 }
 
 | 

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

我们也可以强制Spring使用CGLIB动态代理,只需要在配置文件中aop:aspectj-autoproxy标签中修改属性proxy-target-class值为true:
| 12
 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"/>
 
 <aop:aspectj-autoproxy proxy-target-class="true"/>
 </beans>
 
 | 
其中OrderServiceImpl类的代码:
| 12
 3
 4
 5
 
 | public class OrderServiceImpl {public void showOrder() {
 System.out.println("OrderServiceImpl.showOrder");
 }
 }
 
 | 
测试程序如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | 
 
 
 
 @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动态代理方案:

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