AOP实战小案例

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技术来实现,从而使得代码更加清晰,具有更好的维护性,在实现的过程中,顺便回顾了反射技术以及注解技术,将这三个技术整合起来,该案例确实是一个不错的尝试,感谢老大给的这个练手机会以及练手案例。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容