Annotation与SpEL实现系统记录操作日志

以往都在自己的博客站点上发文章,很少在公共站点上发博文。第一次在OSChina上发博文,真心感觉OSChina确实是国内程序员的风水宝地。这段时间,一心想写点技术文章却迟迟没能提笔。今天终于,打开音乐播放器,戴上耳机。享受着宁静的夜晚与指尖跃起的文字。甚至于还想着,什么时候才能有合适的机会回到山东,守在爸妈身边。好了,废话不多说了,开始记录正文。

需求:

系统中的一个模块属于关键区,它所有的操作主要针对修改与删除是要求记录下日志来的。而这个记录的日志并不是像我们把它们打印在log文件里,而是需要标准的记录到数据库中。以便于后来专门日志操作模块的查询。

思考:

当然针对这个需求并不困难,在Service层的每个方法逻辑里添加一段代码专门用来插入到对应的log表中即可。可是,那太丑陋了!也不宜于维护。要是能用Annotation标注在方法头,然后凡是做标注的方法,当它们执行的时候,会将结果和相关信息插入到DB中就好了。针对这个YY需求,我做了如下的设计:

1.建立一个AnnotationMyLogger),内容用SpringSpEL表达式模版。

2..增加一个AOP方法级的拦截器,切面就在Service层。拦截所标注MyLoggerService方法。

3.拦截到方法时,根据SpEL表达式模版生成记录内容,记录到DB中。

方案:

MyLogger.java

/**
 * 记录日志标注
 * Created by chenzhiguo 
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLogger {
 
    // 功能分类
    public enum serviceType{
        /**
         * 未知类型
         */
        Unknown,
        /**
         * 类型一
         */
        Type1
    }
 
    /**
     * 任务执行类型
     * @return
     */
    serviceType type()  default  serviceType.Unknown;
 
    /**
     * SpEL表达式模板
     * @return
     */
    String logModel() default "";
 
    /**
     * 传入执行类
     * @return
     */
    Class[] argPlus() default {};
 
    /**
     * 是否需要返回某个值到执行方法内
     * @return
     */
    boolean needLogId() default false;
 
}

LoggerInterceptor.java 方法拦截器

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
 
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;
 
/**
 * 操作日志记录拦截器
 * Created by chenzhiguo
 */
 
public class LoggerInterceptor implements MethodInterceptor{
 
    @Autowired
    private UserService userService;
 
    @Autowired
    private HistoryLogRepository historyLogRepository; //操作日志表
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Autowired
    private LoggerService loggerService;  //操作日志表的Service
 
    private long logId;
 
 
    @Override
    public synchronized Object invoke(MethodInvocation invocation) {
 
        Method method = invocation.getMethod();
        Annotation[] annotations = method.getDeclaredAnnotations();
        boolean flag = false;
        int i = -1;
        Object obj = null;
 
        for(Annotation annotation : annotations){
            i ++;
            if(MyLogger.class.isInstance(annotation)){
                flag = true;
                break;
            }
        }
        if(flag) {
            // 拦截到指定Service
            try {
                logId = doLog(method, invocation.getArguments(), (MyLogger) annotations[i], false);
                obj = invocation.proceed();
            } catch (Throwable throwable) {
                // 记录执行失败日志
                logStateUpdate(logId, true);
                throw new ServiceException(throwable);
            }
            return obj;
        }else{
            try {
                obj = invocation.proceed();
            } catch (Throwable throwable) {
                throw new ServiceException(throwable);
            }
        }
        return obj;
    }
 
    /**
     * 拦截指定方法 记录工作日志
     * @param method
     * @param annotation
     */
    private Long doLog(Method method, Object[] argObjects, MyLogger annotation, boolean hasException) throws Exception {
        HistoryLog historyLog = new HistoryLog();
        historyLog.setDate(new Date());
        historyLog.setActor(userService.getCurrentUser());
        historyLog.setType(annotation.type());
        // 获取SpEL表达式模板
        String content = annotation.logModel();
        //method.getParameterTypes()
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext ec = new StandardEvaluationContext();
        int i = 0;
        for(Object obj : argObjects){
            ec.setVariable("arg"+(i++),obj);
        }
        int x = 0;
//参数增强
        for(Class clazz : annotation.argPlus()){
            ec.setVariable("argPlus" + (x++), applicationContext.getBean(clazz));
        }
//将SpEL模版生成动态内容
        content = parser.parseExpression(content).getValue(ec, String.class);
        historyLog.setContent(content);
        historyLog.setHasException(hasException);
        historyLogRepository.save(historyLog);
 
        if(annotation.needLogId()) {
            Class[] clazzs = method.getDeclaringClass().getInterfaces();
            Class clazz = clazzs[0];
            Object obj = applicationContext.getBean(clazz);
            // 对Service对象的logId属性进行赋值
            clazz.getDeclaredMethod("setLogId",long.class).invoke(obj, historyLog.getId());
        }
        return historyLog.getId();
    }
 
    /**
     * 更新日志执行结果状态
     * @param logId
     * @param hasException
     */
    private void logStateUpdate(long logId, boolean hasException){
        loggerService.updateState(logId, hasException);
    }
}

 

至于AOP的拦截器配置,我就不贴了。这块资料很多,可以随便Google一下。

 

看了这些代码,可能你已经懵了,说实话确实有点乱,那我们先看看具体怎么应用呢?

举个简单的例子:

@MyLogger(type = MyLogger.serviceType.Type1, logModel = "'XXX:'+#arg0+',XXX:'+#arg1")
public void doSomething(String a, String b) {....}

 

上面的logModel内容就是SpringSpEL表达式模版语句了,具体语法就不细讲了,我也没有太细的去研究。其中#arg0#arg1代表这方法的第12个参数,当然这个名字是我定义的,方便我调用而已,并不是SpEL的规范。这样在执行SpEL解析后,logModel的内容就变成了XXX变量a的值,XXX:变量b的值

你的疑问可能来了,要是我需要的变量不在参数里面怎么办?argPlus就是来解决这个问题的。

@MyLogger(type = MyLogger.serviceType.Type1, logModel = "'XXX:'+#arg0+',XXX:'+#arg1+’, XXX:’+#argPlus0.getSomething()+’", argPlus= ABC.class)
public void doSomething(String a, String b) {....}

 

其中ABC.class在拦截的时候会被Spring取出它的对象,然后放进SpEL解析器,执行它对应的方法后获取你想要的值。可能还会觉得有点不理解,那只能这样,如果你看到这个方案感觉不错,想在你的项目采用并且还没看明白的话,来信问吧。me[at]chenzhiguo.cn ^_^

原文地址:http://www.chenzhiguo.cn/archives/log_with_annotation_and_spel

你可能感兴趣的:(spring,annotation,SPEL)