背景:使用AOP面向切面记录用户进入系统后的操作轨迹。
其他:SpringBoot + MybatisPlus + …
- 相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义日志监控切面
@Aspect
@Component
public class LogAspect {
@Autowired
private SystemLogService service;
/**
* 环绕通知
* ("within(com.yc..*) && @annotation(log)")[清楚记录操作痕迹,需添加@writeLog()注解]
* ("within(com.yc.*.controller..*)")[只记录方法名称,无需修改原代码]
*
* @param jp 连接点
* @param log 注解
* @return 对象
*/
@Around("within(com.yc..*) && @annotation(log)")
public Object around (ProceedingJoinPoint jp,WriteLog log){
long startTimeMillis = System.currentTimeMillis();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
try{
Object result = jp.proceed();
long costTimeMillis = System.currentTimeMillis() - startTimeMillis;
service.write(request,log.opPosition(),log.optype(),log.logType(),
jp.getSignature().getDeclaringTypeName()+"."+jp.getSignature().getName(),
costTimeMillis,CommonConstant.OPSTATE_SUCCESS);
return result;
}catch (Throwable throwable) {
long costTimeMillis = System.currentTimeMillis() - startTimeMillis;
service.write(request,log.opPosition(),log.optype(),log.logType(),
jp.getSignature().getDeclaringTypeName()+"."+jp.getSignature().getName(),
costTimeMillis,CommonConstant.OPSTATE_FAILURE);
throwable.printStackTrace();
throw new RuntimeException(throwable.getMessage());
}
}
}
说明:
- 根据具体业务,可定义不同的通知(前置,异常,后置…)
- 切点的定义语法详见文章的扩展 2
- 定义日志注解接口
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface WriteLog {
/**
* 操作位置
* @return 所处位置
*/
String opPosition();
/**
* 操作类型(0:增 1:删 2:改 3:查)
* @return 操作类型
*/
int optype() default CommonConstant.OPTYPE_READ;
/**
* 日志类型
* 0:操作日志;1:登录日志;2:定时任务;
*
* @return 日志类型
*/
int logType() default CommonConstant.LOG_TYPE_0;
}
- 架构日志服务类
@Service
@Slf4j
public class SystemLogService {
private final SysLogMapper sysLogMapper;
private final DaoApi daoApi;
@Autowired
public SystemLogService(SysLogMapper sysLogMapper, DaoApi daoApi) {
this.sysLogMapper = sysLogMapper;
this.daoApi = daoApi;
}
boolean write(HttpServletRequest request,String opPosition,int opType,int logType,String requestMethod,
long costTimeMillis,int opResult) {
String message = new String[] { "创建", "删除", "更新", "读取" }[opType] + "位置【" + opPosition + "】" + (opResult != 1
? "成功" : "失败");
String requestParams = JSONObject.toJSONString(request.getParameterMap());
return write(daoApi.getCurrentUser(),opType,logType, requestMethod,request.getRequestURI(),
request.getMethod(),requestParams,costTimeMillis,message);
}
private boolean write(SysUser sysUser,int opType,int logType,
String requestMethod,String requestUrl,String requestType,String requestParams,
long costTimeMillis,String... describe) {
sysUser = sysUser !=null ? sysUser : new SysUser();
SysLog log = new SysLog();
log.setRequestMethod(requestMethod);
log.setRequestUrl(requestUrl);
log.setRequestType(requestType);
log.setRequestParam(requestParams.trim());
log.setCreateTime(LocalDateTime.now());
log.setCreateUserId(sysUser.getSysUserId());
log.setIpAddress(LocalHostUtil.getIpAddress());
log.setOpType(opType);
log.setCostTime(costTimeMillis);
log.setLogType(logType);
String tmpDesc = "";
for(int i = 0; i<describe.length; i++){
tmpDesc += describe[i] + (i<describe.length-1?"\r\n":"");
}
log.setLogContent(tmpDesc);
int result = sysLogMapper.insert(log);
return result >0 ? true : false;
}
}
说明
- @Target(ElementType.METHOD):用于限定注解使用范围,method:用于方法上
- @Retention(RetentionPolicy.RUNTIME):指定注解不仅保存在class文件中,JVM加载class文件之后,仍然存在
- @Documented:表明使用这个注解会被javadoc记录,注解类型信息会被记录在生成的文档中
- @Inherited:该注解会被子类继承
- 日志实体类
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "sys_log_id", type = IdType.UUID)
private String sysLogId;
/**
* 操作类型(0:增 1:删 2:改 3:查)
*/
private Integer opType;
/**
* 日志类型(0.操作日志 1.登录登出日志 2.定时任务)
*/
private Integer logType;
/**
* 日志内容
*/
private String logContent;
/**
* 请求方法
*/
private String requestMethod;
/**
* 请求参数
*/
private String requestParam;
/**
* 请求路径
*/
private String requestUrl;
/**
* 请求类型
*/
private String requestType;
/**
* IP地址
*/
private String ipAddress;
/**
* 耗时
*/
private Long costTime;
/**
* 备注
*/
private String remark;
/**
* 创建人
*/
private String createUserId;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
- Controller 中使用@WriteLog注解记录用户操作轨迹
@GetMapping(value = "/dictTree")
@WriteLog(opPosition = "加载字典树" ,optype = CommonConstant.OPTYPE_READ)
public List<TreeNode> dictTree(@RequestParam(value = "name",required = false)String name){
try {
return service.dictTree(name);
}catch (Exception e){
throw new RunningException("".equals(e.getMessage()) ? "系统错误,请联系管理员!" : e.getMessage());
}
}
扩展 1 Spring AOP
- 面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如:事务,权限,过滤器,日志等功能
- 相关概念
图片引用 - 基本概念
1.Advice(通知):
切面类要完成的工作就是通知,定义切面"何时做"和"做什么"
(1)Before(前置通知):在目标方法调用之前调用通知,
(2)After(后置通知):在目标方法调用之后调用通知;
(3)AfterReturning(返回通知):在目标方法执行成功后执行
(4)AfterThrowing(异常通知):在目标方法出现异常时候执行
(5)Around(环绕通知):在目标方法调用之前和之后执行
2.Join Point(连接点)
在我们的应用程序中有可能有数以万计的时机可以应用通知,而这些时机就被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时 甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点是一个虚概念,可以把连接点看成是切点的集合。
3.Aspect(切面):
通知和切点的结合,两者共同定义了切面的全部内容。因为通知定义的是"在何时做"和"要做什么",而切点定义的是"在何处做"。两者结合,完美展现切面在何时,何地,做什么。
4.PointCut(切点)
切点定义"去何处做",让通知找到处理的方法。切点会定义匹配通知所要植入的一个或多个连接点;
“何处做”:@within,@args,@target,@annotation;举例:
//匹配com.lh下的所有方法以及@Service类中的所有方法
within(com.lh..*)||within(org.springframework.stereotype.Service)
//匹配标注了@RequestMapping的方法
annotation(org.springframework.web.bind.annotation.RequestMapping)
5.Weaving (织入)
可以理解为对方法的增强,将切面的代码应用到目标函数的过程。
扩展 2 切入点指示符
为了使方法能够应用到对应的目标函数中,Spring AOP 提供了匹配表达式
通配符
[…]匹配任意数量的参数,以及类定义中的任意数量包
//任意返回值,任意名称,任意参数的公共方法
execution(public * *(..))
//匹配com.yc.common包及其子包中所有类中的所有方法
within(com.yc.common..*)
[+] 匹配给定类的任意子类
//匹配实现了UserService接口的所有子类的方法
within(com.yc.common.UserService+)
[*]匹配任意数量的字符
//匹配com.yc.common包及其子包中所有类的所有方法
within(com.yc.common..*)
//匹配以set开头,参数为int类型,任意返回值的方法
execution(* set*(int))
within
//匹配com.yc.common包及其子包中所有类中的所有方法
@Pointcut("within(com.yc.common..*)")
//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.yc.common.UserDaoImpl)")
//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.yc.common.UserDaoImpl+)")
//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.yc.common.UserDao+)")
execution
//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.yc.common.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.yc.common.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.yc.common.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.yc.common.UserDaoImpl.*(int , ..))")
其他指示符
bean:Spring AOP扩展的,用于匹配特定名称的Bean对象的执行方法;
//匹配名称中带有后缀Service的Bean。
@Pointcut("bean(*Service)")
private void myPointcut(){}
this :用于匹配当前AOP代理对象类型的执行方法;
//匹配了任意实现了UserDao接口的代理对象的方法进行过滤
@Pointcut("this(com.yc.common.UserDao)")
private void myPointcut(){}
target :用于匹配当前目标对象类型的执行方法;
//匹配任意实现UserDao接口的目标对象的方法进行过滤
@Pointcut("target(com.yc.common.UserDao)")
private void myPointcut(){}
@within:用于匹配所以持有指定注解类型内的方法;与within是有区别的, within是用于匹配指定类型内的方法执行;
//匹配使用MarkerAnnotation注解的类(注意是类)
@Pointcut("@within(com.yc.common.MarkerAnnotation)")
private void myPointcut(){}
@annotation: 根据所应用的注解进行方法过滤
//匹配使用MarkerAnnotation注解的方法
@Pointcut("@annotation(com.cy.common.MarkerAnnotation)")
private void myPointcut(){}
附:切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下:
//匹配任意实现UserDao接口的目标对象的方法并且该接口不在com.yc.dao包及其子包下
@Pointcut("target(com.yc.common.UserDao) !within(com.yc.dao..*)")
private void myPointcut(){}
//匹配任意实现UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.yc.common.UserDao)&&execution(* com.yc.common.UserDao.addUser(..))")
private void myPointcut(){}