暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Springboot 自定义注解实现日志功能

码酱 2020-10-24
245
在生产开发中,难免遇到很多地方使用到Java自定义注解,结合使用Spring Aop方式实现日志记录、权限验证、统计等的功能,现在我们就来看看具体是如何实现的。

1


自定义注解


1.元注解:java.lang.annotation中提供了元注解,可以使用这些注解来定义自己的注解。主要使用的是Target和Retention注解。

2.常用元注解

@Target:描述了注解修饰的对象范围,取值在java.lang.annotation.ElementType定义,常用的包括:

  • METHOD:应用于描述方法

  • TYPE:应用于描述类、接口或enum类型

  • PACKAGE:应用于描述包

  • FIELD:应用于属性(包括枚举中的常量)

@Retention: 表示注解保留时间长短。取值在java.lang.annotation.RetentionPolicy中,取值为:

  • SOURCE:编译时被丢弃,不包含在类文件中

  • CLASS:JVM加载时被丢弃,包含在类文件中,默认值

  • RUNTIME:由JVM 加载,包含在类文件中,在运行时可以被获取到

    注:只有定义为RetentionPolicy.RUNTIME时,我们才能通过注解反射获取到注解

@Document:表明该注解标记的元素可以被Javadoc 或类似的工具文档化

例:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface ApiOperateLog {
}

2


Aop理解


AOP为何那么重要呢?在我们的程序中经常会存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。AOP将这些非业务代码完全提取出来,与业务代码分离并寻找节点切入业务代码中。

简单地去理解,其实AOP要做三类事:

  • 在哪里切入,也就是这些非业务操作在哪些业务代码中执行

  • 在什么时候切入,是业务代码执行前还是执行后

  • 切入后做什么事,比如做权限校验、日志记录、统计等

简单画了个体系图梳理下

  • Pointcut:切点,决定处理如日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和annotation方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。

  • Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。

  • Aspect:切面,即Pointcut和Advice。

  • Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。

  • Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

3


日志记录的实现


1.自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface ApiOperateLog {


String logName() default "操作日志";


String logType();


String logModule();
}
2.注解用到的类型
public interface LogConst {
/**
* 日志类型
* 增 add
* 删 del
* 改 update
* 查 select
*/
interface LogType{


String LogType_ADD = "add";


String LogType_DEL = "del";


String LogType_UPDATE = "update";


String LogType_SELECT = "select";
}


/**
* 模块名称常量_评论管理
*/
interface LogModule {
/**
* 用户信息管理
*/
String LOG_USER_INFO = "userInfo";
}
}
3.核心切入类
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 此处的切点是注解的方式(注解类路径),也可以用包名的方式达到相同的效果
* '@Pointcut("execution(* com.diku.provider.service.impl.*.*(..))")'
*/
@Pointcut("@annotation(com.diku.provider.annotion.ApiOperateLog)")
public void execute(){
    }
/**
* 后置返回通知
* 处理完请求,返回内容
*/
@AfterReturning(returning = "result", value = "execute()")
public void afterReturning(JoinPoint point, Object result) throws Throwable {
Object[] args = point.getArgs();
//得到注解对象
ApiOperateLog annotation = ((MethodSignature)point.getSignature()).getMethod().getAnnotation(ApiOperateLog.class);
String name = annotation.logName();
String type = annotation.logType();
String logModule = annotation.logModule();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String createTime = simpleDateFormat.format(new Date());
//获取方法返回值
log.info(" @AfterReturning:{}",result);
saveLog(name, type, logModule, args, createTime);
}


private void saveLog(String name, String type, String logModule, Object[] args, String createTime) {
        //保存相关日志操作,调用kafka(其他消息中间件),通知Es保存相关日志记录
log.info("{}->类型:{}->模块:{}->参数:{}->创建时间:{}",
name,type,logModule,Arrays.toString(args), createTime);
}


/**
* 前置通知
*/
@Before("execute()")
public void doBeforeAdvice(JoinPoint joinPoint){
log.info("测试@Before.....");
}
/**
* 后置异常通知
*/
@AfterThrowing("execute()")
public void afterThrowing(JoinPoint joinPoint){
log.info("测试@AfterThrowing.....");
}
/**
* 后置通知
*/
@After("execute()")
public void after(JoinPoint joinPoint){
log.info("测试@After.....");
}
/**
* 环绕通知
*/
@Around("execute()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
log.info("进入环绕通知");
long start=System.currentTimeMillis();
Object proceed = point.proceed(point.getArgs());
log.info("环绕通知.....统计方法执行的时间为:{}ms",String.valueOf(System.currentTimeMillis()-start));
log.info("退出环绕通知");
return proceed;
}
}
注:@Around功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。例如案例中我们统计方法执行的时间。
4.接口使用注解测试
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;


@ApiOperateLog(logName = "查询日志",logType = LogConst.LogType.LogType_SELECT,logModule = LogConst.LogModule.LOG_USER_INFO)
@GetMapping("/{id}")
public User selectById(@PathVariable("id") int id) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userMapper.selectById(id);
}
}
浏览器访问:

控制台打印日志:

通过控制台打印的日志我们还可以看出这几种通知的执行顺序:

进入环绕通知--->前置通知--->执行ProceedingJoinPoint.proceed--->退出环绕通知--->后置通知--->后置返回通知




喜欢就加个关注吧,


往期精选

SpringCloud配置中心的使用
SpringCloud-Eureka高可用搭建
‍‍
SpringCloud-Zuul服务网关‍‍
SpringCloud-Ribbon负载均衡
SpringCloud-Feign远程调用
SpringCloud-Hystrix解决雪崩
Nacos入门案例
Mybatis动态SQL 


文章转载自码酱,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论