使用注解可以大大减少开发的代码量,所以在实际项目的开发中会使用到很多的注解。特别是做一些公共基础的功能,比如日志记录,事务管理,权限控制这些功能,使用注解会非常高效且优雅。
而对于自定义注解,主要有三个步骤,定义注解,标记注解,解析注解,其实并不是很难。所以关于自定义注解的更多内容就不在这里展开说了,大家也可以看我之前的文章Java注解Annotation小结。
正如文章标题所言,通过自定义注解+AOP切面编程实现日志记录功能,开发实现流程主要分为下面四步:
- 1 定义注解
- 2 标记注解:在对应需要记录日志的方法上标记注解
- 3 解析注解:编写切面类,进行相应处理逻辑
- 4 开启切面能力
下面实战一下,自定义一个注解@DoLog,用于方法上,当方法被调用时即打印日志,在控制台显示调用方传入的参数和调用返回的结果。
1 定义注解
首先定义注解@DoLog
,在方法上使用,为了能在反射中读取注解信息,当然是设置为RUNTIME
。
@Target(value = ElementType.METHOD) @Documented @Retention(value = RetentionPolicy.RUNTIME) public @interface DoLog { String dataStatus() default ""; String[] labelNames() default {}; }
说明:这里注解里面定义的属性,对于记录日志没有实际意义,这里只是顺便演示一下如何在切面类中获取注解中的属性值。
2 标记注解:在对应需要记录日志的方法上标记注解
注意:使用切面的注解不能加在私有方法上,否则切入不了。这个要注意一下,你也可以自己试一下。
@RestController @Slf4j public class DemoController { @GetMapping("/demo2/{orderno}") @DoLog(dataStatus = "1",labelNames = {"name","age"}) public BaseResult<String> test1(@PathVariable(value = "orderno") String orderno) { return BaseResult.success("aaa"); } }
3 解析注解:编写切面类,进行相应处理逻辑
最关键的一步来了,解析注解,一般在项目中会使用Spring的AOP技术解析注解,当然如果只需要解析一次的话,也可以使用Spring处理器BeanPostProcessor来读取注解,相关内容可以参考我的文章使用Spring的BeanPostProcessor优雅的实现工厂模式。
咱这里的场景是打印每次方法被调用的日志,所以使用AOP比较合适。
创建一个切面类DoLogAspect进行解析。
@Aspect @Slf4j @Component public class DoLogAspect { //切面点为标记了@DoLog注解的方法 @Pointcut("@annotation(cn.test.util.DoLog)") public void doLog() { } //环绕通知 @Around("doLog()") @SuppressWarnings("unchecked") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long starTime = System.currentTimeMillis(); //通过反射获取被调用方法的Class Class type = joinPoint.getSignature().getDeclaringType(); //获取类名 String typeName = type.getSimpleName(); //方法名 String methodName = joinPoint.getSignature().getName(); //获取参数列表 Object[] args = joinPoint.getArgs(); //参数Class的数组 Class[] clazz = new Class[args.length]; for (int i = 0; i < args.length; i++) { clazz[i] = args[i].getClass(); } //通过反射获取调用的方法method Method method = type.getMethod(methodName, clazz); DoLog doLog = method.getAnnotation(DoLog.class); log.info("注解参数:{},{}",logApi.dataStatus(),JSONObject.toJSONString(logApi.labelNames())); //获取方法的参数 Parameter[] parameters = method.getParameters(); //拼接字符串,格式为{参数1:值1,参数2::值2} StringBuilder sb = new StringBuilder(); for (int i = 0; i < parameters.length; i++) { Parameter parameter = parameters[i]; String name = parameter.getName(); sb.append(name).append(":").append(args[i]).append(","); } if (sb.length() > 0) { sb.deleteCharAt(sb.lastIndexOf(",")); } //执行结果 Object res; try { //执行目标方法,获取执行结果 res = joinPoint.proceed(); log.info("调用{}.{}方法成功,参数为[{}],返回结果[{}]", typeName, methodName, sb.toString(), JSONObject.toJSONString(res)); } catch (Exception e) { log.error("调用{}.{}方法发生异常", typeName, methodName); //如果发生异常,则抛出异常 throw e; } finally { log.info("调用{}.{}方法,耗时{}ms", typeName, methodName, (System.currentTimeMillis() - starTime)); } //返回执行结果 return res; } }
注意:自己在主逻辑之外使用切面加入的处理逻辑,一定要try处以免影响到正常业务逻辑。
扩展(Pointcut定义):
public class DoLogAspect { //切面点为标记了@DoLog注解的方法 @Pointcut("@annotation(cn.test.util.DoLog)") public void doLog() { } //环绕通知 @Around("doLog()") @SuppressWarnings("unchecked") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //处理代码 }
以上代码同样可以替换为下面的代码:
public class DoLogAspect { //环绕通知 @Around("@annotation(cn.test.util.DoLog)") @SuppressWarnings("unchecked") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //处理代码 }
4 开启切面能力
定义完切面类后,就需要在启动类添加启动AOP的注解。
@SpringBootApplication //添加此注解,开启AOP @EnableAspectJAutoProxy public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
5 测试
启动项目,请求接口,我们可以看到控制台出现被调用方法的日志信息,如下:
2021-11-13 06:16:26.617|||INFO|||-|||-|||RMI TCP Connection(2)-192.168.0.100|||DispatcherServlet--->Completed initialization in 22 ms 2021-11-13 06:30:08.807|||INFO|||-|||-|||http-nio-8084-exec-1|||DoLogAspect--->注解参数:1,["name","age"] 2021-11-13 06:30:08.823|||INFO|||-|||-|||http-nio-8084-exec-1|||DoLogAspect--->调用DemoController.test1方法成功,参数为[arg0:1001],返回结果[{"data":"aaa","msg":"success","ret":"0"}] 2021-11-13 06:30:08.823|||INFO|||-|||-|||http-nio-8084-exec-1|||DoLogAspect--->调用DemoController.test1方法,耗时20ms
总结
这种使用注解和切面编程,记录接口请求参数和返回值的功能,因为使用起来特别方便,而且代码侵入性又特别小,所以在实际项目中基本上都会使用,而最常见的场景就是日志、监控打点,异常收口。
关于自定义注解+AOP实现记录日志功能就分享到这里,期待能够给有缘的同学带来一些启发和帮助。