查看: 1109|回复: 0

[Java语言] spring boot 使用Aop通知打印控制器请求报文和返回报文问题

发表于 2018-5-2 08:00:01

一、简介

开发过程中我们往往需要写许多例如: 

  1. @GetMapping("/id/get")
  2. public Result getById( String id) throws Exception{
  3. log.info("请求参数为:"+id);
  4. verify(new VerifyParam("部门id", id));
  5. Result result = new Result("通过id获取部门信息成功!", service.queryById(id));
  6. log.info("返回报文为:"+result.toString());
  7. return result;
  8. }
复制代码

打印请求参数以及返回参数的方法,而这些操作存在于每个方法之中,使得我们代码较为冗余,为此我们可以通过动态代理将打印参数和打印返回报文作为切面,使用切入点表达式将其切入至每个方法之中。

二、步骤

1、引入Aop相关的依赖:

  1. <!--AOP相关的依赖-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-aop</artifactId>
  5. </dependency>
复制代码

引入依赖后spring-aop会加载其需要的依赖,spring默认使用aspectJ实现通知

其中aspectjweaver.jar中包含了解析aspectJ切入点表达式的文件,使用切入点表达式处理事务的时候也需要加入此依赖。

2、配置:

1)、创建配置类:

  1. /**
  2. * @功能描述:用于controller层操作的AOP类
  3. * @author Administrator
  4. */
  5. @Component // 将对象交由spring进行管理
  6. @Aspect // 代表此类为一个切面类
  7. public class ControllerAop {
  8. }
复制代码

其中@Aspect 注解代表其为一个切面管理类,可以在其下定义切入点表达式,aspectJ框架会进行解析。

2)、定义切入点表达式:

  1. @Pointcut("execution(public * com.hzt.manage.*.web.controller..*.*(..))") // 切入点表达式
  2. public void privilege() {
  3. }
复制代码

其中,@Pointcut代表此方法为一个切入点表达式。其value值为切入点表达式,其中value可以省略其大致格式为:

  @注解(表达标签+表达式格式)
的格式,Spring AOP支持的AspectJ切入点指示符如下:

1、 execution:用于匹配方法执行的连接点;

2、within:用于匹配指定类型内的方法执行;

3、this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;

4、target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;

5、args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;

6、@within:用于匹配所以持有指定注解类型内的方法;

7、@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;

8、@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;

9、@annotation:用于匹配当前执行方法持有指定注解的方法;

10、bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

11、reference pointcut:表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持。

args中定义了切入点表达式方法执行时候的参数:

  1. @Pointcut(value="execution(public * com.hzt.manage.*.web.controller..*.*(..))&&args(param)",argNames="param") // 切入点表达式
  2. public void privilege1(String param) {
  3. }
复制代码

我们重点介绍 execution 方法连接点的表达式,其大概结构为:

  1. execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
复制代码

1、修饰符匹配(modifier-pattern?)(可省略)

2、返回值匹配(ret-type-pattern)可以为*表示任何返回值 ,如 (String) 代表只筛选返回String类型的切入点 ,全路径的类名等(不可省略)

3、类路径匹配(declaring-type-pattern?)如*.manage代表一级包为任意,二级包为manage的名称。*..manage代表所有manage包下的子类包。com..*.comtroller代表com包下所有的controller包等,*代表所有包都匹配。(不可省略)

4、方法名匹配(name-pattern)可以指定方法名 或者 *代表所有, get* 代表以get开头的所有方法,也可指定前缀*get代表任意后缀为get的方法(不可省略)

5、参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“*”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(*,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(..)表示任意参数(不可省略)

6、异常类型匹配(throws-pattern?)

3、定义切面方法

  1. @Around("privilege()")
  2. public Object around(ProceedingJoinPoint pjd) throws Throwable {
  3. // 获取方法名
  4. String className = pjd.getSignature().getClass().getName();
  5. // 获取执行的方法名称
  6. String methodName = pjd.getSignature().getName();
  7. /** 初始化日志打印 */
  8. Logger log = LoggerFactory.getLogger(className);
  9. // 定义返回参数
  10. Object result = null;
  11. // 记录开始时间
  12. long start = System.currentTimeMillis();
  13. // 获取方法参数
  14. Object[] args = pjd.getArgs();
  15. String params = "前端请求参数为:";
  16. //获取请求参数集合并进行遍历拼接
  17. for (Object object : args) {
  18. params += object.toString() + ",";
  19. }
  20. params = params.substring(0, params.length() - 1);
  21. //打印请求参数参数
  22. log.info(className+"类的"+methodName + "的" + params);
  23. // 执行目标方法
  24. result = pjd.proceed();
  25. // 打印返回报文
  26. log.info("方法返回报文为:" + (result instanceof Result ? (Result) result : result));
  27. // 获取执行完的时间
  28. log.info(methodName + "方法执行时长为:" + (System.currentTimeMillis() - start));
  29. return result;
  30. }
复制代码

5、@Around 环绕通知,如上代码所示便是环绕通知,其有ProceedingJoinPoint参数

其中 pjd.proceed();方法代表去执行目标方法,并获得一个Object类型的返回值 ,我们可以对返回值进行加工处理,如装饰加工等。

return的值为方法执行的结果。上述代码中首先获取类名、方法名、方法请求参数等,进行打印的拼接,并且记录方法执行的开始时间,并进行打印至日志。

然后执行方法,获取到方法返回结果,进行打印执行时间和执行结果。

最后返回执行结果。即使用Aop打印请求报文和返回报文的aop切面编码结束。

其中@Around代表其为一个环绕通知方法,其有以下几种类型:

1、@Before前置通知,拥有请求参数 JoinPoint ,用来连接当前连接点的连接细节,一般包括方法名和参数值。在方法执行前进行执行方法体,不能改变方法参数,也不能改变方法执行结果。

  1. @Before(value = "privilege()")
  2. public void before(JoinPoint joinPoint) {
  3. }
复制代码

2、@After 后置通知:在目标方法执行之后,无论是否发生异常,都进行执行的通知。在后置通知中,不能访问目标方法的执行结果(因为有可能发生异常),不能改变方法执行结果。

  1. @Before(value = "privilege()")
  2. public void after(JoinPoint joinPoint) {
  3. }
复制代码

3、@AfterReturning 返回通知,在目标方法执行结束时,才执行的通知,同后置方法相同。其能访问方法执行结果(因为正常执行)和方法的连接细节,但是不能改变方法执行结果。

  1. @AfterReturning(value = "privilege()")
  2. public void afterReturing(JoinPoint joinPoint,Object result) {
  3. }
复制代码

result中存放的为方法的返回值。

4、@AfterThrowing 异常通知:在目标方法出现异常时才会进行执行的代码。 throwing属性代表方法体执行时候抛出的异常,其值一定与方法中Exception的值需要一致。

  1. @AfterThrowing(value="privilege()",throwing="ex")
  2. public void exce(JoinPoint joinPoint, Exception ex) {
  3. }
复制代码

三、测试

编写一个Controller方法

  1. @RestController
  2. @RequestMapping("/api/v1/dept")
  3. public class DeptController extends BaseController{
  4. /** 日志记录类 */
  5. private Logger log = LoggerFactory.getLogger(getClass());
  6. /** 自家的service */
  7. @Autowired
  8. private DeptService service;
  9. /**
  10. * @功能描述:根据id查询部门内容的方法
  11. * @return Dept
  12. */
  13. @GetMapping("/id/get")
  14. public Result getById( String id) throws Exception{
  15. verify(new VerifyParam("部门id", id));
  16. return new Result("通过id获取部门信息成功!", service.queryById(id));
  17. }
  18. }
复制代码

如此我们的controller层中的方法就大大的简洁了。

测试结果:

2018-04-10 22:59:27.468 INFO 1460 --- [nio-8088-exec-5] nProceedingJoinPoint$MethodSignatureImpl : getById的前端请求参数为:22
2018-04-10 22:59:27.470 INFO 1460 --- [nio-8088-exec-5] nProceedingJoinPoint$MethodSignatureImpl : 方法返回报文为:Result [result_code=suc, result_message=通过id获取部门信息成功!, data=Dept [id=22, no=22, name=22, manager=22, description=22, phone=22, createTime=Thu Apr 19 23:38:37 CST 2018, editTime=null]]
2018-04-10 22:59:27.470 INFO 1460 --- [nio-8088-exec-5] nProceedingJoinPoint$MethodSignatureImpl : getById方法执行时长为:2

如此便能很雅观简洁隐式的打印请求参数、返回结果和执行时间等!



回复

使用道具 举报