在进行业务操作的时候,我们会需要记录用户的操作日志。例如,XX发布了文章,XX删除了文章,XX评论了文章。主要用于记录,展示。
在spring工程下,大多方案都是基于aop来实现的。但是,具体怎么做,我一直在纠结。怎么让这个方案更加通用呢?我咨询过几位大佬,似乎也没有特别好的方案。主要原因是,业务日志的共性可以抽象,但是业务无法趋于共性。所以,最终只能有限共性处理。
我们主要考虑如下几个问题:
1.业务日志如何进行共性抽象
2.不同业务的返回值不一样,如果把相关的字段进行共性处理
3.如果是做成公共模块,怎么达到共用性
基于上面的问题,我们主要思路是
1.业务日志的主要是操作者的对具体数据的行为,可以抽象成,{user}对{subject}主体的{objectId}数据进行了{actionMethod}的{action}操作
2.既然没有办法统一只返回值,那就只能进行妥协,对不同的返回值进行单独处理
3.既然是公共模块,那么必要要把具体业务进行剥离,这样就需要在具体项目中实现某接口或者继承某个抽象类的方式来实现。
我们的解决方案如下
1.抽象actrecord
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class ActRecord {
private String id;
private String subject;
private String subjectId;
private String actionMethod;
private String action;
private String objectId;
private String description;
}
2.根据actrecord的自动,设计注解
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ActLog {
String subject() default "";
String actionMethod() default "";
String action() default "";
String description() default "";
}
3.根据aop的织入点,写一个interface ActLogHandler,提供多个方法给aop实现调用
public interface ActLogHandler {
void before(ActLog actLog,Object[] args);
void after(ActLog actLog,Object[] args);
void aroundBefore(ActLog actLog,Object[] args);
void aroundAfter(ActLog actLog,Object[] args,Object result);
void afterRetuning(ActLog actLog,Object[] args,Object result);
void afterThrowing(ActLog actLog,Object[] args,Exception ex);
}
4.实现aop
@Aspect
@Component
public class ActLogAspect {
@Autowired
@Lazy //这里需要懒加载,否则如果没有实现类就会报错
private ActLogHandler actorLogHandler;
@Pointcut("@annotation(com.moensun.springboot2.mvc.aop.ActLog)")
public void actorLogAspect() {
}
@Before(value = "actorLogAspect()")
public void before(JoinPoint joinPoint){
ActLog actorLog = getMethodAnnotation(joinPoint);
if(Objects.nonNull(actorLog)){
actorLogHandler.before(actorLog, joinPoint.getArgs());
}
}
@After(value = "actorLogAspect()")
public void after(JoinPoint joinPoint){
ActLog actorLog = getMethodAnnotation(joinPoint);
if(Objects.nonNull(actorLog)){
actorLogHandler.after(actorLog, joinPoint.getArgs());
}
}
@Around(value = "actorLogAspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ActLog actorLog = getMethodAnnotation(joinPoint);
if(Objects.nonNull(actorLog)){
actorLogHandler.aroundBefore(actorLog, joinPoint.getArgs());
}
Object result = joinPoint.proceed();
if(Objects.nonNull(actorLog)){
actorLogHandler.aroundAfter(actorLog, joinPoint.getArgs(), result);
}
return result;
}
@AfterReturning(value = "actorLogAspect()",returning = "returnValue")
public void afterReturning(JoinPoint joinPoint, Object returnValue){
ActLog actorLog = getMethodAnnotation(joinPoint);
if(Objects.nonNull(actorLog)){
actorLogHandler.afterRetuning(actorLog,joinPoint.getArgs(),returnValue);
}
}
@AfterThrowing(value = "actorLogAspect()",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex){
ActLog actorLog = getMethodAnnotation(joinPoint);
if(Objects.nonNull(actorLog)){
actorLogHandler.afterRetuning(actorLog, joinPoint.getArgs(), ex);
}
}
private ActLog getMethodAnnotation(JoinPoint joinPoint){
if(Objects.isNull(actorLogHandler)){
return null;
}
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
return AnnotationUtils.getAnnotation(methodSignature.getMethod(), ActLog.class);
}
}
5.模拟一个业务场景发布文章,并进行业务日志记录
5.1 定义一个article模型
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class Article {
private String id;
private String title;
}
5.2 实现一个发布文章的函数,并且加上我们的注解
@Service
public class ArticleService {
@ActLog(subject = ARTICLE,actionMethod = PUBLISH,action = PUBLISH_ARTICLE,description = "发布文章")
public Article create(){
Article article = Article.builder().id("1").title("spring 入门").build();
return article;
}
}
5.3 实现前面我们提到的ActLogHandler,对进行操作日志处理
@Component
public class ActLogHandlerImpl implements ActLogHandler {
@Override
public void before(ActLog actLog, Object[] args) {
}
@Override
public void after(ActLog actLog, Object[] args) {
}
@Override
public void aroundBefore(ActLog actLog, Object[] args) {
}
@Override
public void aroundAfter(ActLog actLog, Object[] args, Object result) {
}
@Override
public void afterRetuning(ActLog actLog, Object[] args, Object result) {
if(Objects.equals(actLog.action(),PUBLISH_ARTICLE)){
if( result instanceof Article){
Article articleResult = (Article) result;
ActRecord actRecord = ActRecord.builder()
.subject(actLog.subject())
.actionMethod(actLog.actionMethod())
.action(actLog.action())
.description(actLog.description())
.objectId(articleResult.getId())
.build();
log(actRecord);
}
}
}
@Override
public void afterThrowing(ActLog actLog, Object[] args, Exception ex) {
}
private void log(ActRecord actRecord){
System.out.println(actRecord);
}
}
由于大部分操作日志 都是在操作成功之后记录,所以,我们就只处理返回值之后的情况
执行这个函数,打印日志如下
ActRecord(id=null, subject=ARTICLE, subjectId=null, actionMethod=PUBLISH, action=PUBLISH_ARTICLE, objectId=1, description=发布文章)
可见,已经达到预期效果。暂时只能想到这么做了,如果有更好的方案,欢迎指点。