AOP实战小案例

作者: 颜洛滨 | 来源:发表于2018-07-31 10:49 被阅读0次

    AOP实战小案例

    前言

    AOP技术是指面向切面编程技术,主要用于在具有横切逻辑的场景中,将横切逻辑提取出来,形成独立的模块,然后通过特殊的技术,如Java中的动态代理技术,动态地将横切逻辑织入到具体的应用场景中

    大概在去年的这个时候,学习过AOP技术,对AOP中的一些概念也有一些了解,不过基本都是理论上的内容,缺乏实战经验,所以,对AOP的理解并不是很充分,加上最近刚好有个项目需要用到,老大说让我通过AOP来实现,加深对AOP的理解,所以有了这篇文章

    环境准备

    这篇文章中采用的是SpringBoot 1.5.14.RELEASE版本,当然,其实哪个版本都可以。不用SpringBoot其实也可以,主要是SpringBoot中配置起来比较简单,可以将主要精力集中在AOP上而不是环境的配置上、

    需要引入aop依赖以及测试环境(主要是为了方便测试)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    

    日志记录及性能监控

    有时候,为了对程序有更好的控制以及分析,我们需要记录方法的调用情况,调用结果,以及执行时间等信息,这种情况下,基本上就是很多的方法都需要记录,而记录的这些操作又跟业务情况无关,也就是符合横切逻辑的概念,所以,通过AOP来实现是比较理想而且可维护性比较恰当的。

    在本案例中,为了回顾之前学习到的注解以及反射等技术,这里通过为方法加上注解的形式来实现日志记录开关,当然,通过切面表达式直接匹配方法实现起来更加简单,不过,作为学习的案例,能在一个小案例中将尽量多的知识用上,也是一个不错的选择

    首先是注解的设计以及实现

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface LogInfo {
    
        String value();
    }
    

    这里我们的注解设计比较简单,只包含一个基本的元素,用于记录方法的用途。这里需要注意的是注解的保留策略,由于我们需要在运行时获取注解的内容,所以注解的保留策略需要为RetentionPolicy.RUNTIME

    接下来是切面的设计

    切面,其实就是对应的操作逻辑(增强)以及注入点(切点)的结合,通过切点来定位,增强来实现功能增强

    我们知道,SpringAOP中有多种方式来实现,比如通过环绕来实现,或者通过前置以及后置来实现,这两种方式理论上都是可行的,不过实现细节上有所区别

    先来看下环绕实现

    @Component
    @Aspect
    public class LogAspect {
    
        @Pointcut("@annotation(cn.xuhuanfeng.annnotation.LogInfo)")
        public void logPointCut(){}
    
        @Around(value = "logPointCut()")
        public Object logAround(ProceedingJoinPoint joinPoint) {
    
            // 获取执行方法签名,这里强转为MethodSignature
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Object[] args = joinPoint.getArgs();
            // 通过反射来获取注解内容
            LogInfo loginfo = signature.getMethod().getAnnotation(LogInfo.class);
            String description = loginfo.value();
            System.out.println("****** "+ description + " Before: " + signature + " args " + Arrays.toString(args));
    
            Object result = null;
    
            long start = System.currentTimeMillis();
            try {
                // 调用原来的方法
                result = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            long end = System.currentTimeMillis();
            System.out.println("****** " + description +" After: " + signature + " " + result + "total time: " + (end - start) + " ms");
            return result;
        }
    }
    

    编写一个简单的操作类

    @Service
    public class BlogService {
    
        @LogInfo("获取博客")
        public String getBlog() {
            return "Blog info";
        }
    
        @LogInfo("增加博客")
        public boolean addBlog(String blog) {
            System.out.println("adding blog");
            return true;
        }
    }
    

    对应的测试类如下

    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class BlogServiceTest {
    
        @Autowired
        private BlogService blogService;
    
        @Test
        public void getBlog() {
            blogService.getBlog();
        }
    
        @Test
        public void addBlog() {
            blogService.addBlog("blog");
        }
    }
    

    运行结果

    ****** 增加博客 Before: boolean cn.xuhuanfeng.blog.BlogService.addBlog(String) args [blog]
    adding blog
    ****** 增加博客 After: boolean cn.xuhuanfeng.blog.BlogService.addBlog(String) truetotal time: 4 ms
    ****** 获取博客 Before: String cn.xuhuanfeng.blog.BlogService.getBlog() args []
    ****** 获取博客 After: String cn.xuhuanfeng.blog.BlogService.getBlog() Blog infototal time: 0 ms
    

    可以看到,我们所需要的功能已经成功通过AOP中的环绕实现了,既保持了代码的整洁性,以及模块性,又基本实现了功能

    前置以及后置实现

    除了上面环绕试下外,还可以通过前置以及后置增强来实现,如下所示

    @Component
    @Aspect
    public class LogAspect {
    
        @Pointcut("@annotation(com.cmft.springbootdemo.annnotation.LogInfo)")
        public void logPointCut(){}
    
        // 通过ThreadLocal来记录进入方法的时间
        private ThreadLocal<Long> logRecorder = new ThreadLocal<>();
    
        private String getDescription(JoinPoint joinPoint) {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            return methodSignature.getMethod().getAnnotation(LogInfo.class).value();
        }
    
        private long calculateExecutionTime() {
            long end = System.currentTimeMillis();
            long duration = end - logRecorder.get();
            logRecorder.remove();
            return duration;
        }
    
        @Before("logPointCut()")
        public void beforeMethod(JoinPoint joinPoint) {
            logRecorder.set(System.currentTimeMillis());
            String description = getDescription(joinPoint);
            System.out.println("****** "+ description +
                    " Before: " + joinPoint.getSignature() +
                    " args " + Arrays.toString(joinPoint.getArgs()));
    
        }
    
        @AfterReturning(value = "logPointCut()", returning = "result")
        public void afterReturning(JoinPoint joinPoint, Object result) {
            String description = getDescription(joinPoint);
            System.out.println("****** " + description +
                    " After: " + joinPoint.getSignature() +
                    " " + result +
                    " total time: " + calculateExecutionTime() + " ms");
    
        }
    }
    

    测试类以及执行结果同上,这里不重复,结果肯定是一样的啦

    从上面的代码中可以看到,通过AOP的两种不同的操作机制,均能够实现日志记录以及方法性能记录

    然而,上面第二种操作中存在一个问题,就是当方法抛出异常的时候,@AfterReturing是不会执行的,原因在于,@AfterReturing是在方法调用结束,返回之前进行织入的,所以一旦抛出异常,就无法处理了。这时候有两种解决方案,第一种是使用@After@After是在方法调用结束之后织入的,所以可以正常记录,另一种方案就是使用@AfterThrowing在异常抛出的时候进行处理,这两种方案均能实现我们所需要的功能,具体的就根据个人习惯来处理了。

    总结

    上面通过一个简单的日志记录案例,使用了AOP技术来实现,从而使得代码更加清晰,具有更好的维护性,在实现的过程中,顺便回顾了反射技术以及注解技术,将这三个技术整合起来,该案例确实是一个不错的尝试,感谢老大给的这个练手机会以及练手案例。

    相关文章

      网友评论

        本文标题:AOP实战小案例

        本文链接:https://www.haomeiwen.com/subject/gjmcvftx.html