接上篇 完全征服 Spring AOP 之 理论篇html
看了上面这么多的理论知识, 不知道你们有没有以为枯燥哈. 不过不要急, 俗话说理论是实践的基础, 对 Spring AOP 有了基本的理论认识后, 咱们来看一下下面几个具体的例子吧.
下面的几个例子是我在工做中所碰见的比较经常使用的 Spring AOP 的使用场景, 我精简了不少有干扰咱们学习的注意力的细枝末节, 以力求整个例子的简洁性.java
下面几个 Demo 的源码均可以在个人 Github 上下载到.git
首先让咱们来想象一下以下场景: 咱们须要提供的 HTTP RESTful 服务, 这个服务会提供一些比较敏感的信息, 所以对于某些接口的调用会进行调用方权限的校验, 而某些不太敏感的接口则不设置权限, 或所须要的权限比较低(例如某些监控接口, 服务状态接口等).
实现这样的需求的方法有不少, 例如咱们能够在每一个 HTTP 接口方法中对服务请求的调用方进行权限的检查, 当调用方权限不符时, 方法返回错误. 固然这样作并没有不可, 不过若是咱们的 api 接口不少, 每一个接口都进行这样的判断, 无疑有不少冗余的代码, 而且颇有可能有某个粗心的家伙忘记了对调用者的权限进行验证, 这样就会形成潜在的 bug.
那么除了上面的所说的方法外, 还有没有别的比较优雅的方式来实现呢? 固然有啦, 否则我在这啰嗦半天干吗呢, 它就是咱们今天的主角: AOP
.程序员
让咱们来提炼一下咱们的需求:github
根据上面所提出的需求, 咱们能够进行以下设计:spring
AuthChecker
, 这个是一个方法注解, 有此注解所标注的 Controller 须要进行调用方权限的认证.AuthChecker
所标注的 joinpoint.根据上面的设计, 咱们来看一下具体的源码吧.
首先是 AuthChecker
注解的定义:
AuthChecker.java:api
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthChecker { }
AuthChecker
注解是一个方法注解, 它用于注解 RequestMapping 方法.cookie
有了注解的定义, 那咱们再来看一下 aspect 的实现吧:
HttpAopAdviseDefine.java:app
@Component @Aspect public class HttpAopAdviseDefine { // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. @Pointcut("@annotation(com.xys.demo1.AuthChecker)") public void pointcut() { } // 定义 advise @Around("pointcut()") public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getRequest(); // 检查用户所传递的 token 是否合法 String token = getUserToken(request); if (!token.equalsIgnoreCase("123456")) { return "错误, 权限不合法!"; } return joinPoint.proceed(); } private String getUserToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return ""; } for (Cookie cookie : cookies) { if (cookie.getName().equalsIgnoreCase("user_token")) { return cookie.getValue(); } } return ""; } }
在这个 aspect 中, 咱们首先定义了一个 pointcut, 以 @annotation 切点标志符来匹配有注解 AuthChecker
所标注的 joinpoint, 即:dom
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. @Pointcut("@annotation(com.xys.demo1.AuthChecker)") public void pointcut() { }
而后再定义一个 advice:
// 定义 advise @Around("pointcut()") public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getRequest(); // 检查用户所传递的 token 是否合法 String token = getUserToken(request); if (!token.equalsIgnoreCase("123456")) { return "错误, 权限不合法!"; } return joinPoint.proceed(); }
当被 AuthChecker
注解所标注的方法调用前, 会执行咱们的这个 advice, 而这个 advice 的处理逻辑很简单, 即从 HTTP 请求中获取名为 user_token
的 cookie 的值, 若是它的值是 123456
, 则咱们认为此 HTTP 请求合法, 进而调用 joinPoint.proceed()
将 HTTP 请求转交给相应的控制器处理; 而若是user_token
cookie 的值不是 123456
, 或为空, 则认为此 HTTP 请求非法, 返回错误.
接下来咱们来写一个模拟的 HTTP 接口:
DemoController.java:
@RestController public class DemoController { @RequestMapping("/aop/http/alive") public String alive() { return "服务一切正常"; } @AuthChecker @RequestMapping("/aop/http/user_info") public String callSomeInterface() { return "调用了 user_info 接口."; } }
注意到上面咱们提供了两个 HTTP 接口, 其中 接口 /aop/http/alive 是没有 AuthChecker
标注的, 而 /aop/http/user_info 接口则用到了 @AuthChecker
标注. 那么天然地, 当请求了 /aop/http/user_info 接口时, 就会触发咱们所设置的权限校验逻辑.
接下来咱们来验证一下, 咱们所实现的功能是否有效吧.
首先在 Postman 中, 调用 /aop/http/alive 接口, 请求头中不加任何参数:
能够看到, 咱们的 HTTP 请求彻底没问题.
那么再来看一下请求 /aop/http/user_info 接口会怎样呢:
当咱们请求 /aop/http/user_info 接口时, 服务返回一个权限异常的错误, 为何会这样呢? 天然就是咱们的权限认证系统起了做为: 当一个方法被调用而且这个方法有 AuthChecker
标注时, 那么首先会执行到咱们的 around advice
, 在这个 advice 中, 咱们会校验 HTTP 请求的 cookie 字段中是否有携带 user_token
字段时, 若是没有, 则返回权限错误.
那么为了可以正常地调用 /aop/http/user_info 接口, 咱们能够在 Cookie 中添加 user_token=123456, 这样咱们能够愉快的玩耍了:
注意
, Postman 默认是不支持 Cookie 的, 因此为了实现添加 Cookie 的功能, 咱们须要安装 Postman 的interceptor
插件. 安装方法能够看官网的文章
第二个 AOP 实例是记录一个方法调用的log. 这应该是一个很常见的功能了.
首先假设咱们有以下需求:
根据上面的需求, 咱们可使用 before advice 来在调用方法前打印调用的参数, 使用 after returning advice 在方法返回打印返回的结果. 而当方法调用失败后, 可使用 after throwing advice 来作相应的处理.
那么咱们来看一下 aspect 的实现:
@Component @Aspect public class LogAopAdviseDefine { private Logger logger = LoggerFactory.getLogger(getClass()); // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. @Pointcut("within(NeedLogService)") public void pointcut() { } // 定义 advise @Before("pointcut()") public void logMethodInvokeParam(JoinPoint joinPoint) { logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); } @AfterReturning(pointcut = "pointcut()", returning = "retVal") public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) { logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); } @AfterThrowing(pointcut = "pointcut()", throwing = "exception") public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) { logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage()); } }
第一步, 天然是定义一个 pointcut
, 以 within 切点标志符来匹配类 NeedLogService
下的全部 joinpoint, 即:
@Pointcut("within(NeedLogService)") public void pointcut() { }
接下来根据咱们前面的设计, 咱们分别定义了三个 advice, 第一个是一个 before advice:
@Before("pointcut()") public void logMethodInvokeParam(JoinPoint joinPoint) { logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); }
它在一个符合要求的 joinpoint 方法调用前执行, 打印调用的方法名和调用的参数.
第二个是 after return advice:
@AfterReturning(pointcut = "pointcut()", returning = "retVal") public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) { logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); }
这个 advice 会在方法调用成功后打印出方法名还反的参数.
最后一个是 after throw advice:
@AfterThrowing(pointcut = "pointcut()", throwing = "exception") public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) { logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage()); }
这个 advice 会在指定的 joinpoint 抛出异常时执行, 打印异常的信息.
接下来咱们再写两个 Service 类:
NeedLogService.java:
@Service public class NeedLogService { private Logger logger = LoggerFactory.getLogger(getClass()); private Random random = new Random(System.currentTimeMillis()); public int logMethod(String someParam) { logger.info("---NeedLogService: logMethod invoked, param: {}---", someParam); return random.nextInt(); } public void exceptionMethod() throws Exception { logger.info("---NeedLogService: exceptionMethod invoked---"); throw new Exception("Something bad happened!"); } }
NormalService.java:
@Service public class NormalService { private Logger logger = LoggerFactory.getLogger(getClass()); public void someMethod() { logger.info("---NormalService: someMethod invoked---"); } }
根据咱们 pointcut 的规则, 类 NeedLogService 下的全部方法都会被织入 advice, 而类 NormalService 则不会.
最后咱们分别调用这几个方法:
@PostConstruct public void test() { needLogService.logMethod("xys"); try { needLogService.exceptionMethod(); } catch (Exception e) { // Ignore } normalService.someMethod(); }
咱们能够看到有以下输出:
---Before method NeedLogService.logMethod(..) invoke, param: [xys]--- ---NeedLogService: logMethod invoked, param: xys--- ---After method NeedLogService.logMethod(..) invoke, result: [xys]--- ---Before method NeedLogService.exceptionMethod() invoke, param: []--- ---NeedLogService: exceptionMethod invoked--- ---method NeedLogService.exceptionMethod() invoke exception: Something bad happened!--- ---NormalService: someMethod invoked---
根据 log, 咱们知道, NeedLogService.logMethod 执行的先后确实有 advice 执行了, 而且在 NeedLogService.exceptionMethod 抛出异常后, logMethodInvokeException
这个 advice 也被执行了. 而因为 pointcut 的匹配规则, 在 NormalService
类中的方法则不会织入 advice.
做为程序员, 咱们都知道服务监控对于一个服务可以长期稳定运行的重要性, 所以不少公司都有本身内部的监控报警系统, 或者是使用一些开源的系统, 例如小米的 Falcon 监控系统.
那么在程序监控中, AOP 有哪些用武之地呢? 咱们来假想一下以下场景:
有一天, leader 对小王说, "小王啊, 你负责的那个服务不太稳定啊, 常常有超时发生! 你有对这些服务接口进行过耗时统计吗?"
耗时统计? 小王嘀咕了, 小声的回答到: "尚未加呢."
leader: "你看着办吧, 我明天要看到各个时段的服务接口调用的耗时分布!"
小王这就犯难了, 虽说计算一个方法的调用耗时并非一个很难的事情, 可是整个服务有二十来个接口呢, 一个一个地添加统计代码, 那还不是要累死人了.
看着同事一个一个都下班回家了, 小王眉头更加紧了. 不过此时小王灵机一动: "噫, 有了!".
小王想到了一个好方法, 当即动手, 吭哧吭哧地几分钟就搞定了.
那么小王的解决方法是什么呢? 天然是咱们的主角 AOP
啦.
首先让咱们来提炼一下需求:
有了需求, 天然设计实现就很简单了. 首先咱们可使用 around advice, 而后在方法调用前, 记录一下开始时间, 而后在方法调用结束后, 记录结束时间, 它们的时间差就是方法的调用耗时.
咱们来看一下具体的 aspect 实现:
ExpiredAopAdviseDefine.java:
@Component @Aspect public class ExpiredAopAdviseDefine { private Logger logger = LoggerFactory.getLogger(getClass()); // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. @Pointcut("within(SomeService)") public void pointcut() { } // 定义 advise // 定义 advise @Around("pointcut()") public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 开始 Object retVal = pjp.proceed(); stopWatch.stop(); // 结束 // 上报到公司监控平台 reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis()); return retVal; } public void reportToMonitorSystem(String methodName, long expiredTime) { logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime); // } }
aspect 一开始定义了一个 pointcut
, 匹配 SomeService
类下的全部的方法.
接着呢, 定义了一个 around advice:
@Around("pointcut()") public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 开始 Object retVal = pjp.proceed(); stopWatch.stop(); // 结束 // 上报到公司监控平台 reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis()); return retVal; }
advice 中的代码也很简单, 它使用了 Spring 提供的 StopWatch 来统计一段代码的执行时间. 首先咱们先调用 stopWatch.start() 开始计时, 而后经过 pjp.proceed()
来调用咱们实际的服务方法, 当调用结束后, 经过 stopWatch.stop() 来结束计时.
接着咱们来写一个简单的服务, 这个服务提供一个 someMethod 方法用于模拟一个耗时的方法调用:
SomeService.java:
@Service public class SomeService { private Logger logger = LoggerFactory.getLogger(getClass()); private Random random = new Random(System.currentTimeMillis()); public void someMethod() { logger.info("---SomeService: someMethod invoked---"); try { // 模拟耗时任务 Thread.sleep(random.nextInt(500)); } catch (InterruptedException e) { e.printStackTrace(); } } }
这样当 SomeService
类下的方法调用时, 咱们所提供的 advice 就会被执行, 所以就能够自动地为咱们统计此方法的调用耗时, 并自动上报到监控系统中了.
看到 AOP
的威力了吧, 咱们这里仅仅使用了寥寥数语就把一个需求完美地解决了, 而且还与原来的业务逻辑彻底解耦, 扩展及其方便.
经过上面的几个简单例子, 咱们对 Spring AOP
的使用应该有了一个更为深刻的了解了. 其实 Spring AOP 的使用的地方不止这些, 例如 Spring 的 声明式事务
就是在 AOP 之上构建的. 读者朋友也能够根据本身的实际业务场景, 合理使用 Spring AOP, 发挥它的强大功能!