SpringAOP配置与使用

SpringAOP简介

    面向切面编程(Aspect Oriented Programming)提供了另一种角度来思考程序的结构,通过这种方式弥补面向对象编程(Object Oriented Programming)的不足。除了类以外,AOP提供了切面,切面对关注点进行模块化,例如横切多个类型和对象的事务管理(这些关注点术语通常称作横切(crosscutting)关注点)。Spring AOP是Spring的一个重要组件,但是Spring IOC并不依赖于Spring AOP,这意味着你可以自由选择是否使用AOP,AOP提供了强大的中间件解决方案,这使得Spring IOC更加完善。我们可以通过AOP来实现日志监听,事务管理,权限控制等等。

AOP概念

  • 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是Java应用程序中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用通过类(基于模式(XML)的风格)或者在普通类中以@Aspect注解(AspectJ风格)来实现。
  • 连接点(Join point):程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中一个连接点总是代表一个方法的执行。个人理解:AOP拦截到的方法就是一个连接点。通过声明一个org.aspectj.lang.JoinPoint类型参数我们可以在通知(Advice)中获得连接点的信息。这个在稍后会给出案例。
  • 通知(Advice):在切面(Aspect)的某个特定连接点上(Join point)执行的动作。通知的类型包括"around","before","after"等等。通知的类型将在后面进行讨论。许多AOP框架,包括Spring 都是以拦截器作为通知的模型,并维护一个以连接点为中心的拦截器链。总之就是AOP对连接点的处理通过通知来执行。个人理解:Advice指当一个方法被AOP拦截到的时候要执行的代码。
  • 切入点(Pointcut):匹配连接点(Join point)的断言。通知(Advice)跟切入点表达式关联,并在与切入点匹配的任何连接点上面运行。切入点表达式如何跟连接点匹配是AOP的核心,Spring默认使用AspectJ作为切入点语法。个人理解:通过切入点的表达式来确定哪些方法要被AOP拦截,之后这些被拦截的方法会执行相对应的Advice代码。
  • 引入(Introduction):声明额外的方法或字段。Spring AOP允许你向任何被通知(Advice)对象引入一个新的接口(及其实现类)。个人理解:AOP允许在运行时动态的向代理对象实现新的接口来完成一些额外的功能并且不影响现有对象的功能。
  • 目标对象(Target object):被一个或多个切面(Aspect)所通知(Advice)的对象,也称作被通知对象。由于Spring AOP是通过运行时代理实现的,所以这个对象永远是被代理对象。个人理解:所有的对象在AOP中都会生成一个代理类,AOP整个过程都是针对代理类在进行处理。
  • AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能),在Spring中AOP可以是JDK动态代理或者是CGLIB代理。
  • 织入(Weaving):把切面(aspect)连接到其他的应用程序类型或者对象上,并创建一个被通知对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯AOP框架一样,在运行时完成织入。个人理解:把切面跟对象关联并创建该对象的代理对象的过程。

通知(Advice)的类型:

  • 前置通知(Before advice):在某个连接点(Join point)之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)。
  • 返回后通知(After returning advice):在某个连接点(Join point)正常完成后执行的通知。例如,一个方法没有抛出任何异常正常返回。
  • 抛出异常后通知(After throwing advice):在方法抛出异常后执行的通知。
  • 后置通知(After(finally)advice):当某个连接点(Join point)退出的时候执行的通知(不论是正常返回还是发生异常退出)。
  • 环绕通知(Around advice):包围一个连接点(Join point)的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

Spring AOP简单流程图

    上图为我个人对AOP流程的一个理解。我把面向对象的过程从一个HttpRquest到访问数据库DB的整个流程看做是一条直线。AOP定义了一个切面(Aspect),一个切面包含了切入点,通知,引入,这个切面上定义了许多的切入点(Pointcut),一旦访问过程中有对象的方法跟切入点匹配那么就会被AOP拦截。此时该对象就是目标对象(Target Object)而匹配的方法就是连接点(Join Point)。紧接着AOP会用过JDK动态代理或者CGLIB生成一个目标对象的代理对象(AOP proxy),这个过程就是织入(Weaving)。这个时候我们就可以按照我们的需求对连接点进行一些拦截处理。可以看到,我们可以引入(Introduction)一个新的接口,让代理对象来实现这个接口来,以实现额外的方法和字段。也可以在连接点上进行通知(Advice),通知的类型包括了前置通知,返回后通知,抛出异常后通知,后置通知,环绕通知。最后也是最骚的是整个过程不会改变代码原有的逻辑。下面我将通过简单的案例对Spring AOP进行演示,过程会包括XML以及Annotation。实例结构基本跟基于SpringMVC+Spring+Hibernate+Maven+Bootstrap的简单Demo以及SpringMVC整合Mybatis+Maven+Bootstrap的简单Demo一致不一样的地方我会贴出代码。

Spring AOP之Annotation

    首先在bean.xml文件中添加<aop:aspectj-autoproxy />开启Spring对@Aspect的支持。

  1. <context:component-scan base-package="com.ctc" />//IOC自动扫包

  2. <aop:aspectj-autoproxy />//使用AOP注解

    声明一个切面,在类UserAspect上加上@Aspect注解。并定义了两个切入点addLog()以及skipMethod()。Spring主要使用的execetion来匹配连接点。此外还有within,this,target等等,这边不再解释有需要可以参考官方文档。此外Spring文档要求定义切入点(Pointcut)的方法的返回值必须的void类型。但是我自己测试了下其他返回类型,还是可以正常使用。不知道是不是因为测试环境的原因,总之就按照官方的来吧。

  1. @Aspect

  2. public class UserAspect {

  3.  
  4. //匹配所有ServiceImpl包下面的所有类的所有方法

  5. @Pointcut("execution(* com.ctc.ServiceImpl.*.*(..))")

  6. public void addLog(){}

  7.  
  8.  
  9. //@Pointcut("execution(public * *(..))")

  10. //public void skipMethod(){}

  11.  
  12. }

    接着定义一个Advice类在上面加上@Aspect注解,同时必须在类上添加注解@Component否则Spring就扫描不到这个类。下面将对5种通知类型以及一个引入(Introduction)AgeGroup进行演示。

 
  1. @Aspect

  2. @Component

  3. public class LogAdvice {

  4.  
  5. /* @DeclareParents(value="com.ctc.ServiceImpl.*+",

  6. defaultImpl=Adult.class)

  7. public static AgeGroup ageGroup;*/

  8.  
  9. //所有的通知都可以使用这种方式,直接把Pointcut跟Advice连接起来,但是为了更好的理解前文的概念以及图片,这边分开定义。

  10. //@Before("execution(* com.ctc.ServiceImpl.*.*(..))");

  11. @Before("com.ctc.AspectJ.UserAspect.addLog()")

  12. public void before(){

  13. System.out.println("LogAdvice before advice ");

  14. }

  15.  
  16. /* @AfterReturning("com.ctc.AspectJ.UserAspect.addLog()")

  17. public void AfterReturning(){

  18. System.out.println("LogAdvice after returning advice ");

  19. }

  20.  
  21. @AfterThrowing("com.ctc.AspectJ.UserAspect.addLog()")

  22. public void AfterThrowing(){

  23. System.out.println("LogAdvice after throwing advice ");

  24. }

  25.  
  26. @After("com.ctc.AspectJ.UserAspect.addLog()")

  27. public void After(){

  28. System.out.println("LogAdvice after advice ");

  29. }

  30.  
  31. //除了可以通过名字来指向对应的切入点表达式,还可以可以使用'&&', '||' 和 '!'来合并。

  32. //切入点表达式的 args(user,..) 表示某个与切入表达式匹配的连接点它把User对象作为第一个参数,通过这个语法我们可以在通知中访问到这个User对象。

  33. @Around("com.ctc.AspectJ.UserAspect.addLog()&&" +"args(user,..)")

  34. public void around(ProceedingJoinPoint joinPoint,User user) throws Throwable{

  35. System.out.println("log begin!");

  36. System.out.println("log end");

  37. }*/

  38.  
  39. }

 
  1. @Test

  2. public void Aspect(){

  3. User user = new User();

  4. user.setUserName("test");

  5. user.setPassword("123");

  6. UserService userService = (UserService) cx.getBean("userServiceImpl");

  7. userService.addUser(user);

  8. /* AgeGroup userAdult = (AgeGroup) cx.getBean("userServiceImpl");

  9. userAdult.isAdult();

  10. System.out.println(userAdult instanceof UserService);*/

  11. }

  12.  
  13.  //UserServiceImpl中的方法

  14. @Override

  15. public void addUser(User user) {

  16. System.out.println("add into DB");

  17. }

  18.  

前置通知(Before advice)

返回后通知(After reurning advice)

    如果方法抛出异常,那么返回后通知就不会执行:

抛出异常后通知(After throwing advice)

   

后置通知(After (finally) advice)

   

把userDaoImpl = null删掉。

 

环绕通知(Around advice)

    环绕通知是一种功能比较强大的通知类型,它可以把第一个参数定义为org.aspectj.lang.Joinpoint类型(环绕通知需要定义为ProcessJoinPoint类型,它是JoinPoint的一个子类)。通过ProcessJoinPoint可以调用process()决定是否让连接点(Join point)继续执行,或者是调用getArgs()返回方法参数,getThis()返回代理对象,getTarget()返回目标对象,getSignature()返回正在被通知的方法的相关信息和toString()打印出正在被通知的方法的有用信息。

下面将从四种情况进行演示:

第一种在方法执行前后添加通知:

第二种方法不执行:

第三种抛出异常停止执行:

第四种返回方法的返回值:

 
  1.  //因为原有的方法为void就是一个空值,这边改用String方便测试。

  2. @Override

  3. public String addUser(User user) {

  4. userDaoImpl.addUser(user);

  5. return "add success!";

  6. }

  7.  

引入(Introduction)

    引入在AspectJ中被称为inter-tye声明,它可以使代理对象实现一个给定的接口用来添加额外的方法或字段。在下面案例中,我们首先引入一个新的接口以及接口的实现类,然后再通过@DeclareParents来定义一个引入,其中value表示要引入的目标对象,defaultImpl表示要实现接口的实现类的Class对象。

 
  1. public interface AgeGroup {

  2.  
  3. public void isAdult();

  4.  
  5. }

  6.  
  7. public class Adult implements AgeGroup {

  8.  
  9. @Override

  10. public void isAdult() {

  11. System.out.println("Yes,he is an adult.");

  12. }

  13.  
  14. }

  15.  

SpringAOP之XML

    XML的实例就是把前面用Annotation注解的方式转到配置文件中,案例代码不变并且只给出一个案例,不全部还原。

 
  1. //为了方便,只保留了一个before用来演示。

  2.  public class LogAdvice {

  3.  
  4. public void before(){

  5. System.out.println("LogAdvice before advice ");

  6. }

  7.  
  8. }

  在选择XML还是Annotation上面,XML的配置是Spring用户最熟悉的,可以很清楚的从配置文件中了解到AOP的应用。但是它的缺点在于XML能够支持的功能会比Annotation的方式差一点,例如在注解上面,我们支持两个切入点表达式进行组合。而XML的方式无法做到。而且通过Annotation的方式如果有需要的话可以很容易的移植到AspectJ上,所以Spring团队更喜欢用Annotation的方式。总之仁者见仁智者见智,看需要吧。

AOP日志实现

  AOP能够实现的事情比较多,此处给出如何通过aop进行日志处理,包括方法调用时长,日志链添加,前置通知等。(注:项目中实现日志的方式有很多种。)

 
  1. @Aspect

  2. @Component

  3. public class LogAdvice {

  4.  
  5. private final Logger logger = LoggerFactory.getLogger(this.getClass());

  6.  
  7. /**

  8. * 横切所有controller下的public方法

  9. */

  10. @Pointcut("execution(public * com.ctc.controller.*.*(..))")

  11. public void controllerPointCut(){}

  12.  
  13. /**

  14. * 横切所有service下的public方法

  15. */

  16. @Pointcut("execution(public * com.ctc.service.*.*(..))")

  17. public void servicePointCut(){}

  18.  
  19. /**

  20. * 环绕通知

  21. * @param joinPoint

  22. * @return

  23. * @throws Throwable

  24. */

  25. @Around("controllerPointCut()")

  26. public BaseResponse aroundController(ProceedingJoinPoint joinPoint) throws Throwable{

  27. String uuid = UUID.randomUUID().toString().replace("-", "");

  28. // 添加日志链

  29. MDC.put("mdcId", uuid);

  30. // 获取Controller入参

  31. Object[] objects = joinPoint.getArgs();

  32. String request = "";

  33. for (Object o : objects) {

  34. if (o instanceof BaseRequest) {

  35. request = o.toString();

  36. break;

  37. }

  38. }

  39. String methodName = joinPoint.getSignature().getName();

  40. logger.info("请求开始: methodName = {}, request = {}", methodName, request);

  41. long startTime = System.currentTimeMillis();

  42. BaseResponse response = (BaseResponse) joinPoint.proceed();

  43. long endTime = System.currentTimeMillis();

  44. long executeTime = endTime - startTime;

  45. logger.info("请求结束: methodName = {}, result = {}, 执行时间: time = {}ms", methodName, response, executeTime);

  46. if (MDC.get("reqId") != null) {

  47. // 请求结束后移除日志链

  48. MDC.remove("reqId");

  49. }

  50. return response;

  51. }

  52.  
  53. /**

  54. * 前置通知业务层

  55. */

  56. @Before("servicePointCut()" )

  57. public void aroundService(JoinPoint joinPoint) {

  58. MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

  59. String[] names = methodSignature.getParameterNames();

  60. // 获取Service入参

  61. Object[] objects = joinPoint.getArgs();

  62. StringBuilder sb = new StringBuilder();

  63. for (int i = 0; i < objects.length; i++) {

  64. sb.append(names[i] + " = " + objects[i]);

  65. if (i != objects.length) {

  66. sb.append(", ");

  67. }

  68. }

  69. String methodName = methodSignature.getName();

  70. logger.info("执行方法:{}; {}", methodName, sb.toString());

  71. }

  72. }

 
  1. @RestController

  2. @RequestMapping("/test")

  3. public class TestController {

  4.  
  5. @Autowired

  6. private TestService testService;

  7.  
  8. @GetMapping("/hello")

  9. public BaseResponse getInfo() {

  10. return ResponseUtil.getSuccessResponse();

  11. }

  12.  
  13. @PostMapping("/post")

  14. public BaseResponse getInfo2(@RequestBody ProductInfoRequest request) {

  15. testService.test1();

  16. testService.test2(1, "adff", request);

  17. return ResponseUtil.getSuccessResponse(request);

  18. }

  19. }

  20.  
  21. @Service

  22. public class TestServiceImpl implements TestService {

  23.  
  24. @Override

  25. public void test1() {

  26. test3();

  27. }

  28.  
  29. @Override

  30. public void test2(int i, String s, BaseRequest request) {

  31. }

  32.  
  33. // 该示例中test3不会被横切到,因为其方法修饰符为private

  34. private void test3() {

  35. }

  36. }

执行结果:

 
  1. --2019-08-03 14:01:59.631 - INFO 7624 --- [6d4848cd552c46c5bcbd8d083f30c2d4] com.ctc.aspect.LogAdvice : 请求开始: methodName = getInfo, request =

  2. --2019-08-03 14:01:59.631 - INFO 7624 --- [6d4848cd552c46c5bcbd8d083f30c2d4] com.ctc.aspect.LogAdvice : 请求结束: methodName = getInfo, result = BaseResponse(code=0, data=null, msg=success), 执行时间: time = 0ms

  3. --2019-08-03 14:03:00.670 - INFO 7624 --- [799d9b6a1c27498b8ab765f5b409c146] com.ctc.aspect.LogAdvice : 请求开始: methodName = getInfo2, request = ProductInfoRequest(id=1, name=haha)

  4. --2019-08-03 14:03:00.671 - INFO 7624 --- [799d9b6a1c27498b8ab765f5b409c146] com.ctc.aspect.LogAdvice : 执行方法:test1;

  5. --2019-08-03 14:03:00.675 - INFO 7624 --- [799d9b6a1c27498b8ab765f5b409c146] com.ctc.aspect.LogAdvice : 执行方法:test2; i = 1, s = adff, request = ProductInfoRequest(id=1, name=haha),

  6. --2019-08-03 14:03:00.675 - INFO 7624 --- [799d9b6a1c27498b8ab765f5b409c146] com.ctc.aspect.LogAdvice : 请求结束: methodName = getInfo2, result = BaseResponse(code=0, data=ProductInfoRequest(id=1, name=haha), msg=success), 执行时间: time = 4ms

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值