在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的實現就全部完成了。

 代碼下載

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM