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
|
@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
:
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
|
@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等。
1 2 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-------"); } }
|
对应的配置文件内容如下:
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
|
@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),可以在目标方法执行前后
以及出现异常时
执行特定的逻辑操作,并且对方法的返回值进行处理。比如对数据库事务的控制都需要在原始方法之前和之后都需要处理。
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
|
@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
。
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; 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); } }
|
配置文件的内容如下:
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
|
@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编程
半注解开发步骤
首先,第一步是定义我们的切面类:
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
| <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
|
@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
:
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"/> <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
|
@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
接口:
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; } }
|