Springboot使用AOP+自定义注解方式实现日志记录
在Spring框架中,可以使用AOP配合自定义注解可以方便的实现用户操作的监控,来简化我们的代码。
什么是自定义注解?
了解自定义注解之前必须了解四个元注解,什么是元注解?元注解指作用于注解之上的元数据或者元信息,简单通俗的讲,元注解就是注解的注解。
Java5.0定义的元注解(java.lang.annotation)
- 1.@Target
@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、
类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
示例:注解Table 可以用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变量。
@Target(ElementType.TYPE)
public @interface Table {
/**
* 数据表名称注解,默认值为类名称
* @return
*/
public String tableName() default "className";
}
@Target(ElementType.FIELD)
public @interface NoDBColumn {
}
- 2.@Retention
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;
编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class
在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)
Retention meta-annotation类型有唯一的value作为成员,它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值。
示例:Column注解的的RetentionPolicy的属性值是RUTIME,这样注解处理器可以通过反射,获取到该注解的属性值,从而去做一些运行时的逻辑处理
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
- 3.@Documented
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
Documented是一个标记注解,没有成员。
示例:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
- 4.@Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,
则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。
如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的
annotation类型被发现,或者到达类继承结构的顶层。
示例:
@Inherited
public @interface Greeting {
public enum FontColor{ BULE,RED,GREEN};
String name();
FontColor fontColor() default FontColor.GREEN;
}
自定义注解
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。
在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。
方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。
可以通过default来声明参数的默认值。
定义注解格式:
public @interface 注解名 {定义体}
注解参数的可支持数据类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2.String类型
3.Class类型
4.enum类型
5.Annotation类型
6.以上所有类型的数组
示例:
/**
* 水果名称注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
使用AOP+自定义注解方式实现日志记录
1、定义一个方法级别的@Log注解,用于标注需要监控的方法:
package cn.pconline.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description 日志注解
* @Author jie.zhao
* @Date 2019/8/7 15:47
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
2、定义切面和切点
定义一个LogAspect类,使用@Aspect标注让其成为一个切面,切点为使用@Log注解标注的方法,使用@Around环绕通知。
关于日志表保存根据自己的业务实现即可
package cn.pconline.common.aspect;
import cn.pconline.common.utils.HttpContextUtil;
import cn.pconline.common.utils.IpUtil;
import cn.pconline.config.authentication.JwtUtil;
import cn.pconline.config.properties.AuthProperties;
import cn.pconline.modules.sys.entity.SysLog;
import cn.pconline.modules.sys.service.SysLogService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @Description AOP 记录用户操作日志
* @Author jie.zhao
* @Date 2019/8/7 15:47
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
@Autowired
private AuthProperties authProperties;
@Autowired
private SysLogService logService;
@Pointcut("@annotation(cn.pconline.common.annotation.Log)")
public void pointcut() {
// do nothing
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result;
long beginTime = System.currentTimeMillis();
// 执行方法
result = point.proceed();
// 获取 request
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
// 设置 IP 地址
String ip = IpUtil.getIpAddr(request);
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
if (authProperties.isOpenAopLog()) {
// 保存日志
String token = (String) SecurityUtils.getSubject().getPrincipal();
String username = "";
if (StringUtils.isNotBlank(token)) {
username = JwtUtil.getUsername(token);
}
SysLog log = new SysLog();
log.setUsername(username);
log.setIp(ip);
log.setTime(time);
logService.saveLog(point, log);
}
return result;
}
}
3、使用方法
@RestController
public class TestController {
@Log("新增用户")
@GetMapping("/one")
public void addUser(String name) { }
}