• 在Spring中轻松写日志


    最近觉得写的一点代码(JAVA),还觉得颇为自得,贡献出来供大家参考。

    首先,先上代码:

    @Controller
    public class Controller1{
        
         @WriteLog(value = "${p0.username}从${ctx.ip}登录, 登录${iif(ret.success,'成功','失败')}")
         public Object login(Login loginObj, HttpServletRequest req){
             //blablabla...
         }  
    }

    在代码中,给login方法加上@WriteLog——相当于C#的[WriteLog],当代码运行了login方法时,spring就会自动记录日志——而日志内容则是会自动替换其中${}的占位符号。

    如上就会记录日志:“.net bean从127.0.0.1登录,登录成功”

    其它地方想要怎么记日志,也是如此,比起原来

    public class ClassA{
        private static final Log log = LogFactory.getLog(ClassA.class);
        
        public void Method(String p1, String p2){
            log.info("日志, 参数p1:"+p1+", 参数p2:"+ p2);
            //blablabla
        }
    }

    我个人觉得方便太多了。

    按照原来的代码,客户说要增加日志,那我们肯定不愿意搞这些事情,但是现在只是加@WriteLog这样的方式的话,还是能接受的。

    以下,我就将我的实现方法,公知于众,就算不能使用其中的代码,也可以从中得到一些启发吧。

    首先声明一下,我是用java,基于spring框架——对于.net人员最多的博客园可能直接拷贝代码怕是不可能了。

    第1步,声明@WriteLog(java中叫Annotation, .net里叫Attribute)

    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={ElementType.METHOD})
    public @interface WriteLog {
        public String value() default "";
        public WriteType type() default WriteType.after;
        
        public enum WriteType{
            before,
            after
        }
    }

    java中的Annotation和.net中的Attribute是有些不同的,java中的Annotation可以声明为SOURCE, CLASS, RUNTIME,表明Annotation的保存时效。这里需要声明为RUNTIME。

    WriteType表明了是在什么时候记录日志,before,只能取到参数的信息,after还可以取到返回值是什么。

    第2步,配置Spring Aop注入

    在spring的主配置文件里,需要配置织入点(Pointcut)

    <bean id="userLogPointcut" class="org.springframework.aop.support.annotation.AnnotationMatchingPointcut">
            <constructor-arg index="0" value="org.springframework.stereotype.Controller"></constructor-arg>
            <constructor-arg index="1" value="annotations.WriteLog"></constructor-arg>
        </bean>

    这个 org.springframework.aop.support.annotation.AnnotationMatchingPointcut 是 Pointcut接口的一个实现。

    什么是Pointcut呢,就是Spring做Aop时,需要在什么位置进行Aop。

    那这个AnnotationMatchingPointcut 就是通过Annotation来进行查找哪些位置需要进行Pointcut。

    第一个构造函数参数是表明有@Controller的类,第二个构造函数参数表明有@WriteLog的方法。

    这只是配置了织入点,还需要告诉怎么处理这个织入点

        <bean id="userLogAdvice" class="WriteLogAdvice">
        </bean>

    在java中有各种Advice(有before, after, methodInceptor),配置一个bean然后实现Advice,实现如下

    public class WriteLogAdvice implements MethodInterceptor {
        private UserLogService service;    
        
        @Override
        public Object invoke(MethodInvocation arg0) throws Throwable {
            if(service==null){
                service = ContextUtil.getBean(UserLogService.class);
            }
            if(service != null){
                return service.insertUserLog(arg0);            
            }
            return arg0.proceed();
        }
        
    }

    这里直接将方法交给service.insertUserLog中进行处理。

    Spring接收Aop的bean是Advisor,所以还需要配置Advisor

        <bean id="userLogAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
            <property name="advice" ref="userLogAdvice" />
            <property name="pointcut" ref="userLogPointcut" />
        </bean>

    分别引用上面已经配置好的advice还pointcut。

    第3步,进行日志记录的实现

    在上一步中,我们将日志记录的实现交给service.insertUserLog来处理了。

    这个日志记录实现,需要考虑的是参数信息如何填充到日志里,其它把日志放到哪(放到数据库,还是文件),都是简单的事情。

    取值

    我们可以从 MethodInvocation 对象中取到每个参数的值——但是不能取到参数名(到了运行时,都是什么 arg0, arg1之类无意义的参数名了)。

    Object[] args = mi.getArguments();

    这样就能取到全部参数的值对象了。

    而使用

    Object result = mi.proceed();

    得到的就是方法的返回值对象。

    填充

    从“${p0.username}从${ctx.ip}登录, 登录${iif(ret.success,'成功','失败')}” 这个字符串来看,很像jstl中的代码,其实正是使用jstl来实现的。

    使用jstl的原因是

    1. 这个代码就是放到web环境中运行的,那么jstl的jar包肯定会在其中

    2. p0.username,调用的是p0.getUsername(),这个是jstl默认支持的

    当然也可以使用其它模板引擎,如freemarker,或者自己实现一个也行——就是比较花时间。

    我们用jstl都是在jsp中使用,那么在代码里怎么使用呢?这个我也在网上找了很久,都没有找到,最后只好反编译jstl的jar来查找一下。

    下面是在java代码中使用jstl的关键代码

        /**
         * 实际执行日志的写入
         * @param mi 当前调用的函数
         * @param annotation WriteLog对象
         * @param result 函数返回值
         */
        private void realWriteLog(final MethodInvocation mi,
                final WriteLog annotation, final Object result) {
            try {
                if (annotation != null) {        
                    //声明一个VariableResolver 用于初始化 Evaluator
                    MapVariableResolver vr = new MapVariableResolver(mi, result);
                    //ELEvaluator 用来 evaluate
                    ELEvaluator eval = new ELEvaluator(vr);    
                    //允许包含函数
                    System.setProperty("javax.servlet.jsp.functions.allowed", "true");
                    String msg = annotation.value();
                    if(msg!=null){
                        //为了便书写,WriteLog中的单引号就表示双引号(不然还需要转义)
                        msg = msg.replaceAll("'", """);
                        //执行evaluate,String.class表示eval返回的类型,fns是函数映射map,fn是函数前缀
                        Object obj = eval.evaluate(msg, null, String.class, fns, "fn");
                        //记录日志
                        userLog.info(obj);
                        //插入到数据库
                        dao.insert((String)obj);
                    }
                    vr.map.clear();
                }
            } catch (Exception ex) {
                log.warn("记录用户日志失败", ex);
            }
        }            

    其中fns这个map对象,使用如下的方法得到

    static Map<String, Method> fns = new HashMap<String, Method>();    
        static{        
            try {            
                //此处添加jstl中的默认方法
                Method[] methods = Functions.class.getMethods();
                for(Method m : methods){
                    if((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC){
                        fns.put("fn:"+m.getName(), m);                    
                    }
                }
                //还有一些自己定义的方法,也加入进去
                methods = WriteLogFunctions.class.getMethods();
                for(Method m : methods){
                    if((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC){
                        fns.put("fn:"+m.getName(), m);                    
                    }
                }
            } catch (SecurityException e) {
                e.printStackTrace();
            }
        }

    其中 MapVariableResolver 就是将方法的参数值放入到map中,要取的时候就从map中取出来。我这里是将该类的实现直接放到内部类来实现

    private class MapVariableResolver implements VariableResolver{
            private Map<String, Object> map;
            public MapVariableResolver(MethodInvocation mi, Object result){
                Object[] args = mi.getArguments();
                map = new LinkedHashMap<String, Object>(args.length+2);
                for(int i=0; i<args.length; i++){
                    map.put("p"+i, args[i]);
                    if((!map.containsKey("ctx")) && args[i] instanceof HttpServletRequest){
                        map.put("ctx", new LogContextImpl((HttpServletRequest)args[i]));
                    }
                }
                map.put("ret", result);
            }
            @Override
            public Object resolveVariable(String arg0, Object arg1)
                    throws ELException {
                if(map.containsKey(arg0)){
                    return map.get(arg0);
                }
                return "[no named("+arg0+") value]";
            }        
        }
    }

    使用斜体的3行,增加了一个 LogContextImpl 对象,主要用于取HttpServletRequest对象里的session,以及ip。

    private class LogContextImpl implements LogContext{
            private Map session;
            private String ip;
            public LogContextImpl(HttpServletRequest req){
                HttpSession session2 = req.getSession();
                if(session2 instanceof Map){
                    session = (Map) session2;
                }else{
                    session = new HashMap();
                    Enumeration names = session2.getAttributeNames();
                    while(names.hasMoreElements()){                    
                        String next = (String)names.nextElement();
                        session.put(next, session2.getAttribute(next));
                    }                
                }
                
                ip = req.getHeader("x-forwarded-for");
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = req.getHeader("Proxy-Client-IP");
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = req.getHeader("WL-Proxy-Client-IP");
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = req.getRemoteAddr();
                }
            }
            public String getIp() {
                return ip;
            }
    
            public Map getSession(){
                return session;
            }
        }
    public interface LogContext {
        @SuppressWarnings("rawtypes")
        public Map getSession();
        public String getIp();
    }

    至此,这个WriteLog的实现就全部完成了。

     代码下载

  • 相关阅读:
    Bootstrap 插件收集
    target和currentTarget
    微信小程序
    flex
    vue中使用icon和打包
    rem的使用
    vue中使用sass和compass
    vue父子组件传递参数
    weex
    常用软件&&网址
  • 原文地址:https://www.cnblogs.com/binblog/p/3814951.html
Copyright © 2020-2023  润新知