[关闭]
@zero1036 2018-04-26T10:53:22.000000Z 字数 8481 阅读 4177

Spring AOP与AspectJ应用及差异

Java-Spring


AOP概念

参考自:AOP面向方面编程

  1. Concerns关注 – 这是基于功能的模块部分,有两种类型关注:. 1. 核心关注 2. 跨切面关注(Aspect). 核心关注是有关业务逻辑,比如生成工资单,让员工记录,做银行转帐。跨切面关注是配合业务的一些活动任务,如日志 缓存等。
  2. Joinpoint连接点 – Joinpoint是在执行时的切入点,Aspect也就是跨切面关注的一些功能要加入主要业务功能的地方,一个连接点可以是一个被调用的方法。
  3. Advice建议 – 每个Aspect都有一个目标,它的任务是什么,这个任务会被切入到哪个连接点,这些都被称为Advice. Advice能够定义Aspect什么时候执行任务,是在核心关注也就是主要业务活动的之前 之后或者前后执行?
  4. Pointcut 切入点– 一个系统有很多连接点,但是并不是所有连接点都需要被选择切入Aspect的,Aspect从切入点能够获得帮助,选择哪个连接点介入。
  5. Aspect方面 – Advice和Pointcut定义了一个方面Aspect.Advice定义了Aspect的任务和什么时候执行它,而切入点Pointcut定义在哪里具体地方切入,也就是说,Aspect定义了它是什么东西 什么时候切入和在哪里切入。
  6. Target目标 – 目标是一个被切入的地方,它一般是核心关注,业务功能。
  7. Proxy代理 – 当一个advice应用到目标对象时,这时一个代理对象将被创建.AOP容器创建和管理代理对象的生命周期。
  8. Weaving织入 – Weaving是一个混合横向方面到目标业务对象的过程,织入可以是在编译时间,也可以在运行时间使用classload, Spring AOP缺省是在运行时间。

动态代理

Spring AOP本质就是配合使用JDK Proxy动态代理和CGLIB工具,从而实现方法的切入。Spring会优先使用JDK动态代理,当调用方法不是接口方法时,选择使用CGLIB,参考大牛的总结:Spring AOP源码分析(二)JDK动态代理和CGLIB介绍,如下:

  1. 当你所调用的目标对象的方法是接口所定义的方法时,可以使用JDK动态代理或者Cglib。即当你的目标类虽然实现了接口,但是所调用的方法却不是接口方法时,就无法使用JDK动态代理,因为JDK动态代理的原理就是实现和目标对象同样的接口,因此只能调用那些接口方法。
  2. Cglib则没有此限制,因为它所创建出来的代理对象就是目标类的子类,因此可以调用目标类的任何方法(除去final方法,final方法不可继承),都会进行拦截。

原理分析见下:

JDK Proxy动态代理

  1. public class ProxyTest {
  2. public static void main(String[] args) {
  3. Say say1 = new Person("lg");
  4. say1 = (Say) JDKDynamicProxy.createProxy(say1);
  5. say1.sayHello();
  6. System.out.println("-------------------------------");
  7. Say say2 = new Animals();
  8. say2 = (Say) JDKDynamicProxy.createProxy(say2);
  9. say2.sayHello();
  10. }
  11. }
  12. public class JDKDynamicProxy {
  13. public static Object createProxy(final Object target) {
  14. /**
  15. * newProxyInstance参数解释:
  16. * classLoader:将目标类接口的实现类加载到jvm,需要用到ClassLoader
  17. * interfaces:目标类实现的接口
  18. * invocationHandler:执行处理器
  19. **/
  20. return Proxy.newProxyInstance(ProxyTest.class.getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
  21. public Object invoke(Object proxy, Method method, Object[] args)
  22. throws Throwable {
  23. if (method.getName().equals("sayHello")) {
  24. doBefore();
  25. try {
  26. method.invoke(target, args);
  27. } catch (Exception e) {
  28. doThrowing();
  29. }
  30. doAfter();
  31. }
  32. return null;
  33. }
  34. });
  35. }
  36. private static void doThrowing() {
  37. System.out.println("AOP say throw a exception");
  38. }
  39. private static void doBefore() {
  40. System.out.println("AOP before say");
  41. }
  42. private static void doAfter() {
  43. System.out.println("AOP after say");
  44. }
  45. }

源码分析:

  1. /**
  2. * 生成代理类
  3. */
  4. @Override
  5. public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
  6. Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
  7. for (Class<?> intf : interfaces) {
  8. /*
  9. * 1、验证接口Class存在,通过Class.forName()方法获取interfaceClass,再比对intf;
  10. * 2、验证intf为接口:if (!interfaceClass.isInterface()){}
  11. * 3、验证intf非重复实现接口:if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {}
  12. */
  13. String proxyPkg = null; // 定义代理类所属包
  14. int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
  15. /*
  16. * 获取所有interfaceClass所属包,要求所有interfaceClass同属一个包,否则报错IllegalArgumentException
  17. * 定义代理类proxyPkg为此包
  18. */
  19. proxyPkg = pkg;
  20. /*
  21. * 按序生成代理类名称,通过自增编号命名,AtomicLong.getAndIncrement()原子操作,确保编号唯一
  22. */
  23. long num = nextUniqueNumber.getAndIncrement();
  24. String proxyName = proxyPkg + proxyClassNamePrefix + num;
  25. /*
  26. * 关键步骤:根据名称、接口Class生成指定代理类字节组;
  27. * 通过native方法defineClass0()获取代理类Class
  28. */
  29. byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
  30. proxyName, interfaces, accessFlags);
  31. try {
  32. return defineClass0(loader, proxyName,
  33. proxyClassFile, 0, proxyClassFile.length);
  34. } catch (ClassFormatError e) {
  35. throw new IllegalArgumentException(e.toString());
  36. }
  37. }
  38. }

CGLIB工具库

暂缺


Spring AOP与AspectJ差异

核心差异是:Spring AOP基于代理模式,在运行时(runtime)时为业务类建立代理类,再通过代理类实现实现对业务方法的调用拦截,从而实现织入(weaving);而AspectJ则在编译(compile)过程中进行织入,AJ修改了业务代码的结构树从而实现切面编程。(待参考源码核实)。

基于这个核心差异,Spring AOP与AspectJ在应用上以下区别:


区别1:是否支持普通Java对象切入

Spring AOP切入点仅支持Spring Beans;而AspectJ可用于基于普通Java对象的切面模块化。

例子:

  1. <beans>
  2. <!-- 被代理对象 -->
  3. <bean id="driver" class="com.spring.aop.cutbean.Driver"></bean>
  4. <!-- 通知类 -->
  5. <bean id="advices" class="com.spring.aop.xmlconfig.Advices"></bean>
  6. <!-- aop配置 -->
  7. <aop:config proxy-target-class="true">
  8. <!--切面 -->
  9. <aop:aspect ref="advices">
  10. <!-- 切点,所有public方法均为切入点 -->
  11. <aop:pointcut expression="execution(public *.*(..))" id="pointcut1"/>
  12. <!--连接通知方法与切点 -->
  13. <aop:before method="before" pointcut-ref="pointcut1"/>
  14. <aop:after method="after" pointcut-ref="pointcut1"/>
  15. </aop:aspect>
  16. </aop:config>
  17. </beans>
  1. public class Program {
  2. public static void main(String[] args) {
  3. ApplicationContext ctx = new ClassPathXmlApplicationContext("cutbean.xml");
  4. Driver driver = ctx.getBean("driver", Driver.class);
  5. driver.drivingCar();
  6. }
  7. }
  8. public class Advices {
  9. public void before(JoinPoint jp) {
  10. System.out.println("----------前置通知----------");
  11. System.out.println(jp.getSignature().getName());
  12. }
  13. public void after(JoinPoint jp) {
  14. System.out.println("----------最终通知----------");
  15. }
  16. }
  17. public class Driver {
  18. public void drivingCar() {
  19. this.startEngine(); //通过this调用对象持有方法
  20. // driving car
  21. }
  22. public void startEngine() {
  23. // try to start Engine
  24. }
  25. }

打印结果:

  1. ----------前置通知----------
  2. drivingCar
  3. ----------最终通知----------

切入点表达式为expression="execution(public *.*(..))",所有public方法均可以被切入,观察以上结果,startEngine()方法并未切入,证明this调用对象本身为Java对象非Spring Beans对象情况下,Spring AOP无法为其创建代理实现切入。

补充:可以通过AopContext.currentProxy()获取对象本身的代理类实现,如下:

  1. public class Driver {
  2. public void drivingCar() {
  3. //直接调用this本身方法,Spring AOP无法切入
  4. //this.startEngine();
  5. //通过AopContext获取当前对象的代理类,通过代理调用对象的方法,Spring AOP可以切入
  6. ((Driver) AopContext.currentProxy()).startEngine();
  7. }
  8. public void startEngine() {
  9. // try to start engine
  10. }
  11. }

打印结果:

  1. ----------前置通知----------
  2. drivingCar
  3. ----------前置通知----------
  4. startEngine
  5. ----------最终通知----------
  6. ----------最终通知----------

区别2:static与final类限制

根据前文结束可知,Spring是通过配合使用JDK动态代理及CGLIB工具库实现AOP,当目标切入方法非接口方法时,Spring通过CGLIB**创建目标类的子类**,再调父类的目标方法实现AOP。但是,一旦目标父类使用了关键字final,子类无法继承,切入就不能实现。

什么是final类:使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。下面是final类的实例:

  1. final class PersonalLoan{}
  2. class CheapPersonalLoan extends PersonalLoan{ //compilation error: cannot inherit from final class

将前文例子Driver类改为final报错:

  1. public final class Driver { ... }
  2. //运行报错:
  3. //Caused by: java.lang.IllegalArgumentException: Cannot subclass final class com.spring.aop.cutbean.Driver

因此,无法在使用final修饰的bean上应用横切关注点。


Aspect

SpringBoot定义切面方法:
注意通过@Component注解注入Spring上下文

通过@Pointcue定义切点

  1. @Component
  2. @Aspect
  3. public class WebControllerAop {
  4. //指定切点
  5. //匹配com.zkn.learnspringboot.web.controller包及其子包下的所有类的所有方法
  6. @Pointcut("execution(* com.zkn.learnspringboot.web.controller..*.*(..))")
  7. public void executeService(){
  8. }
  9. }

Pointcut

Pointcut expression表达式

内容项 示例
任意公共方法的执行 execution(public * *(..))
任何一个以“set”开始的方法的执行 execution(* set*(..))
CustomerService接口的任意方法的执行 execution(* com.tg.service.CustomerService.*(..))
定义在service包里的任意方法的执行 execution(* com.tg.service..(..))
定义在service包和所有子包里的任意类的任意方法的执行 execution(* com.xyz.service...(..))
定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行 execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")
在多个表达式之间使用or 表示,使用&&and 表示表示 aop:pointcut expression="(execution(* com.travelsky.ccboy.dao...find(..))) or (execution(* com.travelsky.ccboy.dao...query(..)))"

JoinPoint

配置前置通知:

  1. /**
  2. * 前置通知,方法调用前被调用
  3. * @param joinPoint
  4. */
  5. @Before("executeService()")
  6. public void doBeforeAdvice(JoinPoint joinPoint){
  7. System.out.println("我是前置通知!!!");
  8. //获取目标方法的参数信息
  9. Object[] obj = joinPoint.getArgs();
  10. //AOP代理类的信息
  11. joinPoint.getThis();
  12. //代理的目标对象
  13. joinPoint.getTarget();
  14. //用的最多 通知的签名
  15. Signature signature = joinPoint.getSignature();
  16. //代理的是哪一个方法
  17. System.out.println(signature.getName());
  18. //AOP代理类的名字
  19. System.out.println(signature.getDeclaringTypeName());
  20. //AOP代理类的类(class)信息
  21. signature.getDeclaringType();
  22. //获取RequestAttributes
  23. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  24. //从获取RequestAttributes中获取HttpServletRequest的信息
  25. HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
  26. //如果要获取Session信息的话,可以这样写:
  27. //HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
  28. Enumeration<String> enumeration = request.getParameterNames();
  29. Map<String,String> parameterMap = Maps.newHashMap();
  30. while (enumeration.hasMoreElements()){
  31. String parameter = enumeration.nextElement();
  32. parameterMap.put(parameter,request.getParameter(parameter));
  33. }
  34. String str = JSON.toJSONString(parameterMap);
  35. if(obj.length > 0) {
  36. System.out.println("请求的参数信息为:"+str);
  37. }
  38. }

注意:这里用到了JoinPoint和RequestContextHolder。通过JoinPoint可以获得通知的签名信息,如目标方法名、目标方法参数信息等。通过RequestContextHolder来获取请求信息,Session信息。
接下来我们在Controller类里添加一个请求处理方法来测试一下前置通知:

配置后置返回通知的代码如下:

  1. /**
  2. * 后置返回通知
  3. * 这里需要注意的是:
  4. * 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
  5. * 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
  6. * returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
  7. * @param joinPoint
  8. * @param keys
  9. */
  10. @AfterReturning(value = "execution(* com.zkn.learnspringboot.web.controller..*.*(..))",returning = "keys")
  11. public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){
  12. System.out.println("第一个后置返回通知的返回值:"+keys);
  13. }
  14. @AfterReturning(value = "execution(* com.zkn.learnspringboot.web.controller..*.*(..))",returning = "keys",argNames = "keys")
  15. public void doAfterReturningAdvice2(String keys){
  16. System.out.println("第二个后置返回通知的返回值:"+keys);
  17. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注