Spring3.0 + 自定义注解实现操作日志记录功能
?
?最近项目组长分配给我一个任务,让我完成一个操作日志的管理功能。需求是这样的:项目很大,有好几个子系统,而且这些子系统已经都在开发过程中了,都进行了大半了。现在要实现的操作日志管理是要将用户在登录系统后所做的重要操作记录下来并查询。记录的内容包括操作人的相关信息(比如:用户ID,用户名,用户IP地址,所属机构等)和所执行的操作的相关信息(比如:所属模块名称、类名、方法名、参数、是否操作成功、描述信息和错误信息)。操作日志查询功能没有什么可说的,难点是在于操作日志的记录,首先要考虑到的是日志的记录不能或要尽量少地让其他人改动自己的代码;其次要考虑到日志记录的灵活性。因此我采用了注解的方式来实现,只要将注解添加到想要记录日志的方法体上就可以实现操作日志的记录。
实现步骤如下:
?
?
第一步:启用@AspectJ支持。
? ?在你的工程lib目录下添加aspectjweaver.jar和aspectjrt.jar并在spring?的配置文件中添加如下代码:
?
<aop:aspectj-autoproxy/>
?这一步就完成了@AspectJ的支持,从而可以实现通过注解方式将通知编织到非公共方法中。
?
?
第二步:编写自定义注解。
? ?实现对方法所实现的功能进行描述,以便在通知中获取描述信息,代码非常简单,如下:
?
package com.abchina.rmpf.logmng.ann;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface rmpfLog {String desc() default "无描述信息";}
?第三步:编写操作日志切面通知实现类。
?
? ? ? ?在编写切面通知实现类之前,我们需要搞清楚我们需要哪些通知类型,是前置通知、后置通知、环绕通知或异常通知?根据我的需求,我们知道我们记录的操作日志有两种情况,一种是操作成功,一种是操作失败。操作成功时则方法肯定已经执行完成,顾我们需要实现一个后置通知;操作失败时则说明方法出现异常无法正常执行完成,顾还需要一个异常通知。因此我们就需要实现这两个通知即可。代码如下:
?
package com.abchina.rmpf.logmng.aop;import java.io.File;import java.lang.reflect.Array;import java.util.Collection;import java.util.Iterator;import net.sf.json.JSONArray;import net.sf.json.JSONObject;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader;import org.springframework.util.ResourceUtils;import plantix.core.business.exception.BusinessException;import plantix.core.context.ThreadContext;import com.abchina.rmpf.privmng.web.vo.ActorVO;import com.abchina.rmpf.common.Constant;import com.abchina.rmpf.common.DateTool;import com.abchina.rmpf.logmng.ann.rmpfLog;import com.abchina.rmpf.logmng.service.ILogService;import com.abchina.rmpf.logmng.web.vo.LogVO;import com.opensymphony.xwork2.ActionContext;@Aspect //该注解标示该类为切面类public class LogAspect {/** * LogService * @generated */private ILogService logService;//标注该方法体为后置通知,当目标方法执行成功后执行该方法体@AfterReturning("within(com.abchina.irms..*) && @annotation(rl)")public void addLogSuccess(JoinPoint jp, rmpfLog rl){Object[] parames = jp.getArgs();//获取目标方法体参数String params = parseParames(parames); //解析目标方法体的参数String className = jp.getTarget().getClass().toString();//获取目标类名className = className.substring(className.indexOf("com"));String signature = jp.getSignature().toString();//获取目标方法签名String methodName = signature.substring(signature.lastIndexOf(".")+1, signature.indexOf("("));String modelName = getModelName(className); //根据类名获取所属的模块String ip = (String)ActionContext.getContext().getSession().get(Constant.THREAD_APP_IP_KEY); //用户IPActorVO actor = ((ActorVO)ActionContext.getContext().getSession().get(Constant.SESSION_ACTOR_KEY));LogVO logvo = new LogVO();logvo.setId(java.util.UUID.randomUUID().toString());logvo.setClassname(className);logvo.setMethodname(methodName);logvo.setArgument(params);logvo.setMemo(rl.desc());logvo.setModelname(modelName);logvo.setIp(ip);logvo.setOperationtime(DateTool.getDateTime4());//logvo.setErr("");logvo.setFlag("1");if(actor!=null){logvo.setOrgid(actor.getOrgcode());logvo.setUserid(actor.getUserid());logvo.setUsername(actor.getUsername());}logService.insertLog(logvo);}//标注该方法体为异常通知,当目标方法出现异常时,执行该方法体@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){Object[] parames = jp.getArgs();String params = parseParames(parames);String className = jp.getTarget().getClass().toString();className = className.substring(className.indexOf("com"));String signature = jp.getSignature().toString();String methodName = signature.substring(signature.lastIndexOf(".")+1, signature.indexOf("("));String modelName = getModelName(className);String ip = (String)ActionContext.getContext().getSession().get(Constant.THREAD_APP_IP_KEY);ActorVO actor = ((ActorVO)ActionContext.getContext().getSession().get(Constant.SESSION_ACTOR_KEY));LogVO logvo = new LogVO();logvo.setId(java.util.UUID.randomUUID().toString());logvo.setClassname(className);logvo.setMethodname(methodName);logvo.setArgument(params);logvo.setMemo(rl.desc());logvo.setModelname(modelName);logvo.setIp(ip);logvo.setOperationtime(DateTool.getDateTime4());logvo.setErr(ex.toString());//记录异常信息logvo.setFlag("0");if(actor!=null){logvo.setOrgid(actor.getOrgcode());logvo.setUserid(actor.getUserid());logvo.setUsername(actor.getUsername());}logService.insertLog(logvo);}/** * 根据包名查询检索其所属的模块 * @param packageName 包名 * @return 模块名称 */private String getModelName(String packageName){String modelName = "";SAXReader reader = new SAXReader();try {//读取project.xml模块信息描述xml文档File proj = ResourceUtils.getFile("classpath:project.xml");Document doc = reader.read(proj);//获取文档根节点Element root = doc.getRootElement();//查询模块名称modelName = searchModelName(root, packageName);} catch (Exception e) {e.printStackTrace();}return modelName;}/** * 采用递归方式根据包名逐级检索所属模块 * @param element 元素节点 * @param packageName 包名 * @return 模块名称 */private String searchModelName(Element element, String packageName){String modelName = searchModelNodes(element, packageName);//若将包名解析到最后的根目录后仍未检索到模块名称,则返回空if(packageName.lastIndexOf(".")==-1){return modelName;}//逐级检索模块名称if(modelName.equals("")){packageName = packageName.substring(0, packageName.lastIndexOf("."));modelName = searchModelName(element, packageName);}return modelName;}/** * 根据xml文档逐个节点检索模块名称 * @param element 节点元素 * @param packageName 包名 * @return 模块名称 */@SuppressWarnings("unchecked")private String searchModelNodes(Element element, String packageName){String modelName = "";Element modules = element.element("modules");Iterator it = modules.elementIterator();if(!it.hasNext()){return modelName;}while (it.hasNext()) {Element model = (Element) it.next();String pack = model.attributeValue("packageName");String name = model.elementText("moduleCHPath");if(packageName.equals(pack)){modelName = name;return modelName;}if(modelName!=null && !modelName.equals("")){break;}modelName = searchModelNodes(model, packageName);}return modelName;}/** * 解析方法参数 * @param parames 方法参数 * @return 解析后的方法参数 */private String parseParames(Object[] parames) {StringBuffer sb = new StringBuffer();for(int i=0; i<parames.length; i++){if(parames[i] instanceof Object[] || parames[i] instanceof Collection){JSONArray json = JSONArray.fromObject(parames[i]);if(i==parames.length-1){sb.append(json.toString());}else{sb.append(json.toString() + ",");}}else{JSONObject json = JSONObject.fromObject(parames[i]);if(i==parames.length-1){sb.append(json.toString());}else{sb.append(json.toString() + ",");}}}String params = sb.toString();params = params.replaceAll("("\\w+":"",)", "");params = params.replaceAll("(,"\\w+":"")", "");return params;}public ILogService getLogService() {return logService;}public void setLogService(ILogService logService) {this.logService = logService;}}
?
?大家看上面的代码会发现这两个方法体:
?
@AfterReturning("within(com.abchina.irms..*) && @annotation(rl)")public void addLogSuccess(JoinPoint jp, rmpfLog rl){…}
@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){…}
这两个方法体分别是后置通知和异常通知的实现。它们有两个相同的参数jp和rl,jp是切点对象,通过该对象可以获取切点所切入方法所在的类,方法名、参数等信息,具体方法可以看方法体的实现;rl则是我们的自定义注解的对象,通过该对象我们可以获取注解中参数值,从而获取方法的描述信息。在异常通知中多出了一个ex参数,该参数是方法执行时所抛出的异常,从而可以获取相应的异常信息。此处为我写的自定义异常。注意:如果指定异常参数,则异常对象必须与通知所切入的方法体抛出的异常保持一致,否则该通知不会执行。
?
addLogSuccess方法签名上的@AfterReturning("within(com.abchina.irms..*) && @annotation(rl)")注解,是指定该方法体为后置通知,其有一个表达式参数,用来检索符合条件的切点。该表达式指定com/abchina/irms目录下及其所有子目录下的所有带有@rmpfLog注解的方法体为切点。
?
addLog方法签名上的@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")注解,是指定方法体为异常通知,其有一个表达式参数和一个抛出异常参数。表达式参数与后置通知的表达式参数含义相同,而抛出异常参数,则表示如果com/abchina/irms目录下及其所有子目录下的所有带有@rmpfLog注解的方法体在执行时抛出BusinessException异常时该通知便会执行。
?
?
注意:切面通知实现类是一个普通的pojo对象,如果要想指定其为通知对象,则需在其类名上添加@Aspect注解
?
第四步:在spring配置文件中创建通知bean对象。
?
<bean id="logAspect" style="margin-top: 0cm; margin-right: 0cm; margin-bottom: 0pt; margin-left: 0cm; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; font-size: 14px; line-height: 25px; text-align: left;">通过以上四步操作后,操作日志的记录功能就算完成了,那我们该如何使用呢?很简单!在com/abchina/irms目录下及其所有子目录下任意找到一个service层的某个类的方法,在其方法体上添加@rmpfLog(desc=”描述信息”)即可。代码如下:?
@rmpfLog(desc="创建关联交易合同")@Transactionalpublic void insertRtcont(RtcontVO rtcontVO) throws BusinessException {rtcontAL.insertRtcont(toRtcontDomain(rtcontVO));}??