最近覺得寫的一點代碼(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的實現就全部完成了。