前段时间公司的项目有这样的一个需求,需要将现有的项目中构建一个日志模块,可以记录用户操作到数据库中,这样一来就可以实现对用户操作的记录,有助于还原和追踪。
项目本身的日志使用的是log4j,但仅仅只是代码层面上的日志信息,只能面向程序开发人员。项目中用到了spring的IoC和DI,web框架用的是struts,我决定使用spring的aop特性,编写一个aspect,切入点为service层的所有方法,由于项目中所有的业务方法都是编写在service层,为action(Controller)提供服务,所以用户的所有操作都需要经过service方法,那么aspect自然就可以对用户的操作执行日志记录了。
考虑到公司的架构与目前这个项目非常相似,均都用到了spring的相关功能,于是决定构建一个基于spring-aop的通用的操作日志框架。本文所描述的是我的整个设计过程,阅读本文需要一定的spring相关知识,以及一定的程序设计与分析能力。
先说明一下我的基本思路,把service层所有的业务方法作为切入点,在这些方法执行之前或之后做日志记录操作。每个业务方法对应了一种操作,通过业务方法的参数可以了解到修改的对象、修改之后的值等其他相关信息。
通过上面的思路分析,可以清晰知道spring-aop的切面作为将要构建的框架的入口,同时该切面作为框架的核心控制器。切面中具体执行的为一个个通知,通知则分为很多种,适用于作日志记录的通知有BeforeAdvice、ThrowsAdvice和AfterReturningAdvice,三者可分别执行不同的功能,这里我们仅记录用户的成功操作,失败或者异常操作暂不在内。
代码清单(AfterReturningAdvice.java)
import org.aspectj.lang.JoinPoint;
public interface AfterReturningAdvice {
/**
* @Description
* @parampoint 切入点的相关信息
* @paramreturnVale 切入点方法的返回值
*/
void afterReturning(JoinPoint point, Object returnValue);
}
声明after-returningAdvice的定义接口,将org.aspectj.lang.JoinPoint作为通知方法的参数。
并非是用户的所有操作都需要记录下来,目前需要记录的仅仅是用户对数据源的修改的成功操作,像查询这种操作是不需要记录的。那么如何来区分这之间的差异呢?我选择使用对方法的注解的方式来标示一个方法对应的操作是否为我们需要记录日志的对象。注解的定义如下。
代码清单(Operation.java)
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Descrption该注解描述方法的操作类型和方法的参数意义
* @author shawyeok
* @Date2014年4月8日
*/
@Target(value=ElementType.METHOD)
@Retention(value=RetentionPolicy.RUNTIME)
@Documented
public @interface Operation {
/**
* @Description描述操作类型,参见{@linkOperationType},为必填项
*/
OperationType type();
/**
* @Description描述操作意义,比如申报通过或者不通过等
*/
String desc() default "";
/**
* @Description描述操作方法的参数意义,数组长度需与参数长度一致,否则无效
*/
String[] arguDesc() default {};
}
上面定义了一个注解,其中type描述了一个方法对应操作的类型。由一个枚举类来修饰,枚举类的定义如下。
代码清单(OperationType.java)
public enum OperationType {
/**
* 新增,添加
*/
ADD,
/**
* 修改,更新
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 下载
*/
DOWNLOAD,
/**
* 查询
*/
QUERY,
/**
* 登入
*/
LOGIN,
/**
* 登出
*/
LOGOUT
}
现在明确了什么样的操作可以进入框架里面来,由Operation注解修饰只能作为条件之一,我在前面已经说过,由于项目已经完成了基本开发阶段,service层方法的返回值并未能统一,现在去一一修改几乎不可能了,service方法出现异常并不会执行afterReturnAdvice的内容,但有些方法虽然执行失败了,但对外并没有抛出异常,而是通过方法返回值来确定的,例如返回了一个false或者是错误信息、错误代码等等。
这时候我们就需要对切入点方法的返回值进行判断,来确定该方法是否执行成功了。但具体的实现是根据具体项目来确定的,所以这里只需要定义相关接口即可。
代码清单(OperationLogContext.java)
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import com.seit.operationlog.domain.OperationLog;
/**
* @Descrption定义操作日志对象的上下文信息
* @author shawyeok
* @Date2014年4月9日
*/
public interface OperationLogContext {
/**
* @Description根据上下文信息生成一个日志对象
* @return
*/
OperationLog getOperationLog();
Method getMethod();
MethodChecker getMethodChecker();
interface MethodChecker{
booleanisSuccess();
}
void setContext(JoinPoint joinPoint, Object returnValue);
}
代码清单(AbstractOperationLogContext.java)
public abstract classAbstractOperationLogContext implementsOperationLogContext {
protected JoinPoint joinPoint;
protected Object returnValue;
protected MethodInvocation methodInvocation;
public void init() {
if(joinPoint != null) {
methodInvocation = getMethodInvocation();
}
}
public void setJoinPoint(JoinPoint joinPoint) {
this.joinPoint =joinPoint;
}
public void setReturnValue(Object returnValue) {
this.returnValue =returnValue;
}
public Method getMethod() {
return methodInvocation.getMethod();
}
private MethodInvocation getMethodInvocation() {
if(methodInvocation ==null) {
Class<?extends JoinPoint> pointClazz = joinPoint.getClass();
try {
Field methodInvocationField = pointClazz.getDeclaredField("methodInvocation");
methodInvocationField.setAccessible(true);
methodInvocation = (MethodInvocation) methodInvocationField.get(joinPoint);
} catch(NoSuchFieldException e) {
e.printStackTrace();
} catch(SecurityException e) {
e.printStackTrace();
} catch(IllegalArgumentException e) {
e.printStackTrace();
} catch(IllegalAccessException e) {
e.printStackTrace();
}
}
return methodInvocation;
}
public void setContext(JoinPoint joinPoint, Object returnValue) {
this.setJoinPoint(joinPoint);
this.setReturnValue(returnValue);
init();
}
}
上面两个类定义了操作日志的上下文信息,getOperationLog方法由和MethodChecker接口由具体的框架使用者来实现,前者可以获得一个操作日志的对象,OperationLog接口在本框架中作为一个标记接口,框架使用者的具体log类必须实现该接口。
那么如何在框架中得到该类的具体实现类呢?这里的OperationLogContext的生命周期为一次切入点方法的调用,在框架入口创建,伴随一次日志记录操作结束而销毁。
方法很简答,由框架使用者来配置具体的OperationLogContext的实现类的完全限定名,通过反射构建该类的对象即可。
OperationLogContext operationLogContext = OperationLogConfigration.getInstance().getOperationLogContext();
这里OperationLogConfigration的代码就不再列出了,该类的职责就是作为框架的核心配置类存在,通过它可以得到配置文件中的所有信息。
下面就是将从上下文解析得到的OperationLog实例执行存储操作了,框架声明存储策略,具体实现由框架的使用者来完成,这是策略设计模式的很好体现哦。
代码清单(OperationLogPolicy.java)
import com.seit.operationlog.domain.OperationLog;
/**
* @Descrption日志策略,此接口定义日志的新增和查询策略
* @author shawyeok
* @Date2014年4月9日
*/
public interface OperationLogPolicy {
/**
* @Description新增日志记录
*/
void addLog(OperationLog log);
}
最后自然是框架的核心控制器OperationLogAspect的实现内容了
代码清单(OperationLogAspect.java)
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import com.seit.operationlog.annotation.Operation;
import com.seit.operationlog.configration.OperationLogConfigration;
import com.seit.operationlog.context.OperationLogContext;
import com.seit.operationlog.context.OperationLogContext.MethodChecker;
import com.seit.operationlog.policy.OperationLogPolicy;
/**
* @Descrption独立日志模块的切面
* @author wangsucheng
* @Date2014年4月8日
*/
public class OperationLogAspect implementsAfterReturningAdvice {
@Autowired
private OperationLogPolicy operationLogPolicy;
public voidafterReturning(JoinPoint point, Object returnValue) {
//获取切入点的方法
OperationLogContext operationLogContext = OperationLogConfigration.getInstance().getOperationLogContext();
operationLogContext.setContext(point,returnValue);
Method method = operationLogContext.getMethod();
//获取方法上的注解信息
Operation operation = method.getAnnotation(Operation.class);
//判断是否是需要记录的操作方法以及方法是否操作成功
MethodChecker methodChecker = operationLogContext.getMethodChecker();
if(operation==null ||!methodChecker.isSuccess()) {
return;
}
System.out.println("执行了切面方法");
operationLogPolicy.addLog(operationLogContext.getOperationLog());
}
}
至此,框架的基本功能也就实现了。应用到具体项目中时,使用者需要做如下操作:
1,定义项目自己的日志类,使其实现OperationLog标记接口。
2,实现OperationLogContext,主要实现getOperationLog方法。
3,实现OperationLogContext内部接口MethodChecker,主要实现isSuccess方法。
4,定义项目中具体用到的日志存储策略,不管是存储到文件中还是数据库中都需要实现OperationLogPolicy的addLog方法。
5,声明配置文件operationLog.properties指定OperationLogContext的具体实现类
除了OperationLogContext的实现类,其他类均由spring的IoC容器来管理。该框架主要为类似项目(使用spring)的日志模块的建立提供了基础,用户只需根据自己的项目实现具体操作策略即可完成统一的日志模块搭建。
|