运用AOP来记录用户的操作日志在项目中比较常见,优点是只需在一个地方编写Advice,通过AOP声明(织入)然后就可以记录很多不同的操作(API)。但是也有其缺点,因为Advice服务于不同的API,而各个API的参数,返回值不同,甚至服务的对象都不一样,那么能做到的也只能是判断是否有异常,异常的具体信息等简单的内容。如果想要个性化的为每一个API都记录执行参数,返回值,甚至Target的属性时就无能为力,因为能够拿到对象,但是不知道到底是什么类型的。本文就利用Java反射机制来获取这些信息。
首先要能使用反射,那么就必须知道方法或者属性名称,所以需要配置相关信息。本示例中用Bean的属性(Field)来配置。一个操作对应一个API,由开发人员配置。最后记录的信息以key=value的形式保存,key就是key,容易理解,value是指对对象(参数和返回值两种)的field属性值,如果嵌套多层则用.(dot)隔开。值得注意的的参数的情形,[0]表示第一个参数,[1]表示第二个参数。之所以使用序号是由于Java参数名在编译完之后会丢失,如果AOP使用AspectJ编译的话可以保存。XML文件具体设置如下:
<apis>
<api name="ServerWebServiceImp.login">
<params>
<param key="user_name" value="[0]" />
<param key="password" value="[1]" />
</params>
<returns>
<return key="aspclient_id" value="ASPClient_Id" />
<return key="user_id" value="users_Id" />
</returns>
</api>
</apis>
需要把XML解析为JavaBean存到内存里面,解析引擎有很多选择,如Dom4j等。具体的解析过程不贴了,很简单,JavaBean如下:
/**
* API实体类
*/
public class ApiEntity {
// 名称
private String name;
// API的参数
private List<Pair> params;
// API返回值
private List<Pair> returns;
public ApiEntity() {
params = new ArrayList<ApiEntity.Pair>();
returns = new ArrayList<ApiEntity.Pair>();
}
public void addParam(ApiEntity.Pair pair) {
params.add(pair);
}
public void addReturn(ApiEntity.Pair pair) {
returns.add(pair);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Pair> getParams() {
return params;
}
public void setParams(List<Pair> params) {
this.params = params;
}
public List<Pair> getReturns() {
return returns;
}
public void setReturns(List<Pair> returns) {
this.returns = returns;
}
public static class Pair {
private String key;
private String value;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
上面是说明如何设置以及表示API,下面就需要利用设置的内容来具体处理参数以及返回值。通常我们记录的是JavaBean的属性值,但是也会出现List,Map,甚至HttpSession的Attribute。为了能够处理所有的内容,我们需要设置一个插件的钩子,有特殊需求的人可以自己去处理获取对象内容的过程。声明一个接口:
/**
* 如何获取对象的值或者属性值接口
*/
public interface ValueGetter {
/**
* 判断是否可以处理该对象的属性值
*
* @param target
* 需要处理的对象,已经保证不会为NULL
* @param key
* 配置文件中的key值,起辅助作用
* @param fieldName
* 对应的属性名称,已经保证不会为空
* @return
*/
Boolean canGet(Object target, String key, String fieldName);
/**
* 获取对象的属性值
*
* @param target
* 需要处理的对象,已经保证不会为NULL
* @param fieldName
* 对应的属性名称,已经保证不会为空
* @return
*/
Object getValue(Object target, String fieldName);
}
具有特殊需求的开发人员可以实现该接口并注册,那么框架自动会调用个性化的实现。下面是普通的JavaBean实现,框架自己提供的,主要用到Java的反射机制:
/**
* 默认的JavaBean获取属性实现. 支持处理私有的,父类的属性
*/
public class JavaBeanValueGetter implements ValueGetter {
@Override
public Boolean canGet(Object target, String key, String fieldName) {
Field field = getField(target.getClass(), fieldName);
if (field != null) {
return true;
} else {
return false;
}
}
@Override
public Object getValue(Object target, String fieldName) {
Field field = getField(target.getClass(), fieldName);
Object fieldValue = null;
if (field != null) {
field.setAccessible(true);
try {
fieldValue = field.get(target);
} catch (Exception e) {
e.printStackTrace();
}
}
return fieldValue;
}
@SuppressWarnings("rawtypes")
private Field getField(Class c, String fieldName) {
Field field = null;
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
Class parentClass = c.getSuperclass();
if (parentClass != null) {
field = getField(parentClass, fieldName);
}
}
return field;
}
}
Around的Advice实现我也不贴了,主要用Spring的AOP实现。该Advice里面会维护一个List<ValueGetter>,并且BeanValueGetter为第一个(效率的考虑,用得最多的是BeanValueGetter)。下面举一个最简单的例子:
public class Customer {
private String name;
private Address address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
public class Address {
private String roadNo;
public String getRoadNo() {
return roadNo;
}
public void setRoadNo(String roadNo) {
this.roadNo = roadNo;
}
}
API为 public Long saveCustomer(Customer customer);
配置文件为
<api name="saveCustomer">
<params>
<param key="roadNo" value="[0].address.roadNo" />
</params>
<returns>
<return key="customer_id" value="" />
</returns>
</api>
特别说明: .表示的嵌套属性先分割开再处理,处理的逻辑单元为没有点的情形,逐级调用,roadNo调用了两次BeanValueGetter。value为空表示对象自己,不再获取属性值。
最后的描述信息可能为:saveCustomer{params(roadNo=1024),returns(customer_id=67)}
上面只是展示了最简单的情形,你可以添加更多的API属性,比如API描述信息,所属模块等。总之就是使用XML配置+Java反射+AOP详细记录操作日志信息。