有时为了系统上的安全,会对用户的操作进行记录,而aop无疑是非常适合用于这方面的。而同时为了编写代码的方便,利用自定义注解进行记录也是个非常不错的选择。
1.引入AOP:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.两个枚举类,用于自定义行为和操作人的类别
public enum BusinessType {
/**
* 其它
*/
OTHER,
/**
* 新增
*/
INSERT,
/**
* 修改
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 授权
*/
ASSGIN,
/**
* 导出
*/
EXPORT,
/**
* 导入
*/
IMPORT,
/**
* 强退
*/
FORCE,
/**
* 更新状态
*/
STATUS,
/**
* 清空数据
*/
CLEAN
}
/**
* 操作人类别
*/
public enum OperatorType {
/**
* 其它
*/
OTHER,
/**
* 后台用户
*/
MANAGE,
/**
* 手机端用户
*/
MOBILE
}
3.编写自定义注解:
/**
* 自定义操作日志记录注解
* 自定义注解类编写的一些规则:
* Annotation 类型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口。
* 参数成员只能用public 或默认(default) 这两个访问权修饰。语法:类型 属性名() [default 默认值];
* default表示默认值 ,也可以不编写默认值的.
* 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
* 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法。
* 注解也可以没有定义成员,,不过这样注解就没啥用了。
* 注意: 自定义注解需要使用到元注解。
*
* 注解方法不能有参数。
* 注解方法的返回类型局限于原始类型,字符串,枚举,注解,或以上类型构成的数组。
* 注解方法可以包含默认值。
*/
/**
* @Target:指定注解使用的目标范围(类、方法、字段等),其参考值见类的定义:java.lang.annotation.ElementType
* ● ElementType.CONSTRUCTOR :用于描述构造器。
* ● ElementType.FIELD :成员变量、对象、属性(包括enum实例)。
* ● ElementType.LOCAL_VARIABLE: 用于描述局部变量。
* ● ElementType.METHOD : 用于描述方法。
* ● ElementType.PACKAGE :用于描述包。
* ● ElementType.PARAMETER :用于描述参数。
* ● ElementType.ANNOTATION_TYPE:用于描述参数
* ● ElementType.TYPE :用于描述类、接口(包括注解类型) 或enum声明。
*/
@Target({ElementType.PARAMETER,ElementType.METHOD})
/**
* @Retention: 指定注解的生命周期(源码、class文件、运行时),其参考值见类的定义:java.lang.annotation.RetentionPolicy
* ● RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
* ● RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
* ● RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。自定义的注解通常使用这种方式。
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
public String title() default "";
/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
}
4.编写自定义aspect
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
//微服务切换为feign调用接口
@Autowired
private AsyncOperLogService asyncOperLogService;
//切点(Pointcut):如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。
//Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
//pointcut/value:其实pointcut和value这两宝贝作用是一样的,只不过pointcut设定之后value会被覆盖掉(即pointcut>value)
//returning : 指定一个返回值形参名
@AfterReturning(pointcut = "@annotation(controllerLog)",returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog,Object jsonResult){
handleLog(joinPoint,controllerLog,null,jsonResult);
}
//连接点(Joinpoint):程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。
// 一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点。
private void handleLog(JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try{
//RequestContextHolder指持有上下文的Request容器
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
SysOperLog operLog = new SysOperLog();
operLog.setStatus(1);
String ip = IpUtil.getIpAddress(request);
operLog.setOperIp(ip);
operLog.setOperUrl(request.getRequestURI());
String token = request.getHeader("token");
String username = JwtHelper.getUsername(token);
operLog.setOperName(username);
if(e!=null){
operLog.setStatus(0);
operLog.setErrorMsg(e.getMessage());
}
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(request.getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
asyncOperLogService.saveSysLog(operLog);
}catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
private void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) {
// 设置action动作
operLog.setBusinessType(log.businessType().name());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().name());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog);
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && !StringUtils.isEmpty(jsonResult)) {
operLog.setJsonResult(JSON.toJSONString(jsonResult));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) {
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(params);
}
}
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (!StringUtils.isEmpty(o) && !isFilterObject(o)) {
try {
Object jsonObj = JSON.toJSON(o);
params += jsonObj.toString() + " ";
} catch (Exception e) {
}
}
}
}
return params.trim();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}