在Spring中輕鬆寫日誌

最近以爲寫的一點代碼(JAVA),還以爲頗爲自得,貢獻出來供你們參考。java

首先,先上代碼:web

@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就會自動記錄日誌——而日誌內容則是會自動替換其中${}的佔位符號。spring

如上就會記錄日誌:「.net bean從127.0.0.1登陸,登陸成功」數據庫

其它地方想要怎麼記日誌,也是如此,比起原來session

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這樣的方式的話,仍是能接受的。jsp

 

如下,我就將個人實現方法,公知於衆,就算不能使用其中的代碼,也能夠從中獲得一些啓發吧。ide

首先聲明一下,我是用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。lua

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的實現就所有完成了。

 代碼下載

相關文章
相關標籤/搜索