传送门
在比较早的时候,就讨论过java反射的一些用法及概念:
以及反射的基石Class对象!
从工作中实际的例子来看看反射的应用
Java反射系列(3):从spring反射工具ReflectionUtils说起
ObjectLogger
Java反射机制,不论是概念还是用法对于刚接触到的使用者来说,都不是很好理解!还记得当年看厚厚的《Thinking in Java》里面的注解章节,里面提到了注解处理器APT,一直无法理解。就在想啊,这个注解到底怎么用啊,跟反射有什么关系呢(当然也不排除这本书可能不适合初学者,或者写的没有传说中的那么好)?
甚至工作多年之后,偶尔被问起原理,也会一时卡壳。但是随着jdk1.6的发布,项目代码里面用到注解+反射的场景是越来越多了,套路一般是:注解+springAOP+反射实现某个通用功能,比如日志记录(打印请求)、操作鉴权(一般是URL)、参数检验等等:
- 先定义一个spring的AOP切面,里面控制切面范围,比如到Controller类或者方法上
- 在要处理的代码上添加注解,即不加该注解就不处理,相当于一个标记
- 执行请求时,根据切面逻辑处理
在多数业务为主的项目中,通常就是定义的切面 (或者拦截器)相当于注解处理器APT了,并且在切面中通过反射获取注解信息结合起来使用。在前面Java反射系列(3):从spring反射工具ReflectionUtils说起中提到的一些提供的工具,不论是spring框架的ReflectionUtils,还是hutool包的相关类,虽然都是比较方便的反射工具类,毕竟不够完整,所以今天就来看一个利用反射实现的日志框架ObjectLogger!
源码地址:ObjectLogger,具体使用方法可以直接看源码介绍,这里只是大致介绍一下它的工作原理,并重点讨论反射在这种日志框架中如何使用!
ObjectLogger是什么
ObjectLogger是一套强大且易用的对象日志记录系统。它能够将任意对象的变动日志记录下来,并支持查询。可以应用在用户操作日志记录、对象属性变更记录等诸多场景中。
ObjectLogger特性
- 一站整合:系统支持日志的记录与查询,开发者只需再开发前端界面即可使用。
- 完全独立:与业务系统无耦合,可插拔使用,不影响主业务流程。
- 应用共享:系统可以同时供多个业务系统使用,互不影响。
- 简单易用:服务端直接jar包启动;业务系统有官方Maven插件支持。
- 自动解析:能自动解析对象的属性变化,并支持富文本的前后对比。
- 便于扩展:支持自定义对象变动说明、属性变动说明。支持更多对象属性类型的扩展。
这里不对框架的设计和代码质量做任何评价,对于它所说的这些特性并依据代码实现,补充了一张图方便理解:
ObjectLogger用法
具体用法参考源码日志写入。ObjectLogger主要提供了两个方法:
- 简单使用:使用者直接将对象的零个、一个、多个属性变化放入
List<BaseAttributeModel>
后调用logAttributes
方法发出即可。List<BaseAttributeModel>
置为null
则表示此次对象无需要记录的属性变动 - 对象变动自动记录:该功能可以自动完成新老对象的对比,并根据对比结果,将多个属性变动一起写入日志系统中。使用时,要确保传入的新老对象属于同一个类
简单使用方式并不复杂(好像简单就不复杂理所当然,字面意思),其实就是将对象的属性信息全部转为字符串存储,这里重点看看对象变动自动记录:
// 1. 创建对象task
CleanRoomTask task = new CleanRoomTask();
task.setId(5);
task.setTaskName("Demo Task");
task.setStatus("TODO");
task.setDescription("Do something...");
// 2. 拷贝旧对象oldTask,注意这里是深拷贝
CleanRoomTask oldTask = logClient.deepCopy(task);
task.setId(5);
task.setTaskName("Demo Task");
task.setStatus("DOING");
task.setDescription("The main job is to clean the floor.");
task.setAddress("Sunny Street");
task.setRoomNumber(702);
// 3. 记录日志,自动记录对象task、oldTask发生变化的属性值
logClient.logObject(
cleanRoomTask.getId().toString(),
"Tom",
"update",
"Update a Task",
null,
null,
oldTask,
task);
日志效果:
{
"respMsg": "SUCCESS",
"respData": [
{
"id": 4,
"appName": "ObjectLoggerDemo",
"objectName": "CleanRoomTask",
"objectId": 5,
"operator": "Tom",
"operationName": "update",
"operationAlias": "Update a Task",
"extraWords": null,
"comment": null,
"operationTime": "2019-07-04T07:22:59.000+0000",
"attributeModelList": [
{
"attributeType": "NORMAL",
"attributeName": "roomNumber",
"attributeAlias": "roomNumber",
"oldValue": "",
"newValue": "702",
"diffValue": null,
"id": 5,
"operationId": 4
},
{
"attributeType": "NORMAL",
"attributeName": "address",
"attributeAlias": "address",
"oldValue": "",
"newValue": "Sunny Street",
"diffValue": null,
"id": 6,
"operationId": 4
},
{
"attributeType": "NORMAL",
"attributeName": "status",
"attributeAlias": "Status",
"oldValue": "TODO",
"newValue": "DOING",
"diffValue": null,
"id": 7,
"operationId": 4
},
{
"attributeType": "TEXT",
"attributeName": "description",
"attributeAlias": "Description",
"oldValue": "Do something...",
"newValue": "The main job is to clean the floor.",
"diffValue": "Line 1<br/> -: <del> Do something... </del> <br/> +: <u> The main job is to clean the floor. </u> <br/>",
"id": 8,
"operationId": 4
}
]
}
],
"respCode": "1000"
}
对象变动自动记录的原理
要搞清楚ObjectLogger是如何做到自动记录原理,就先看看它的代码:
logObject接口
首先在client包里面定义了LogClient类,里面提供了一个接口logObject:
public void logObject(String objectId, String operator, String operationName, String operationAlias,
String extraWords, String comment,
Object oldObject, Object newObject)
这里面参数很多,重点关注oldObject、newObject这两个:
- oldObject代表旧对象,比如上面例子中oldTask,这个对象里面的设置的字段属性值都被当做是变化前的
- newObject代表新对象,比如上面例子中task,这个对象里面的设置的字段属性值都被当做是变化后的
那么如何对比同一个类两个对象之间的属性值是否发生变化就成了关键点(源代码使用异步线程,并try...catch了异常,但不是主要功能所以这里去掉了,不影响阅读):
// 操作记录对象,非重点可不关注
OperationModel operationModel = new OperationModel(objectLoggerConfig.getBusinessAppName(), oldObject.getClass().getSimpleName(),
objectId, operator, operationName, operationAlias, extraWords, comment, new Date());
// 真正的开始处理属性对比
// handle attributes of operation
// 新对象Class
Class modelClazz = newObject.getClass();
// 旧对象Class
Class oldModelClazz = oldObject.getClass();
// 如果不是同一个对象,则不处理,这正是前面提到的:使用时,要确保传入的新老对象属于同一个类。
if (oldModelClazz.equals(modelClazz)) {
// 这里主要是通过Claas对象获取里面的属性列表:因为对象一致所以只要通过新对象获取即可,没必要2个class都获取一遍
ClazzWrapper clazzWrapper = new ClazzWrapper(modelClazz); // issue #1
List<Field> fieldList = clazzWrapper.getFieldList();
// 循环处理属性值比较
for (Field field : fieldList) {
field.setAccessible(true);
// 这里主要是通过field获取属性值信息:直接利用反射方法field获取属性值,包括新、旧对象上的属性
FieldWrapper fieldWrapper = new FieldWrapper(field, field.get(oldObject), field.get(newObject));
// 这里是说如果属性上打了LogTag注解,或者系统开启了自动记录开关,才记录日志:关于LogTag注解等会再介绍
if (fieldWrapper.isWithLogTag() || "true".equals(objectLoggerConfig.getAutoLogAttributes())) {
// 这一行是真正的比较了:如果属性值不相同,则进行日志记录,fieldWrapper在刚才创建时已经从对象中获取了对应的属性值,可见
if (!nullableEquals(fieldWrapper.getOldValue(), fieldWrapper.getNewValue())) {
BaseAttributeModel baseAttributeModel;
if (fieldWrapper.isWithExtendedType()) {
baseAttributeModel = handleExtendedTypeItem(fieldWrapper);
} else {
baseAttributeModel = handleBuiltinTypeItem(fieldWrapper);
}
if (baseAttributeModel != null) {
operationModel.addBaseActionItemModel(baseAttributeModel);
}
}
}
}
}
// 利用HTTP请求调用接口发送日志到server端存储
if (!operationModel.getAttributeModelList().isEmpty()) {
httpUtil.sendLog(new Gson().toJson(operationModel));
}
跟进ClazzWrapper类,看看代码:
public class ClazzWrapper {
private List<Field> fieldList;
public ClazzWrapper(Class clazz) {
this.fieldList = getFields(clazz);
}
public List<Field> getFieldList() {
return fieldList;
}
private List<Field> getFields(Class clazz) {
List<Field> fieldList = new ArrayList<>();
return getFields(fieldList, clazz);
}
private List<Field> getFields(List<Field> fieldList, Class clazz) {
// 获取所有声明的属性列表,看到这个地方是不是觉得很熟悉,典型的反射用法
fieldList.addAll(Arrays.asList(clazz.getDeclaredFields()));
Class superClazz = clazz.getSuperclass();
if (superClazz != null) {
getFields(fieldList, superClazz);
}
return fieldList;
}
}
再看看FieldWrapper类:
public class FieldWrapper {
private String attributeName; // 属性名称
private String attributeAlias; // 注解的属性名称,如果不存在则使用attributeName
private Object oldValue; // 属性的旧值
private Object newValue; // 属性的新值
private String oldValueString; // 属性旧值字符串
private String newValueString; // 属性新值字符串
private boolean withLogTag; // 是否有注解
private LogTag logTag; // 属性注解
private boolean withExtendedType; // 是否是外部类型
private String extendedType; // 外部类型具体值
// 这个构造方法中,两个参数oldValue、newValue且类型为Object,就是真正的属性field.get获取的,在存储时都直接转换为toString()字符串
public FieldWrapper(Field field, Object oldValue, Object newValue) {
this.attributeName = field.getName();
this.oldValue = oldValue;
this.newValue = newValue;
this.oldValueString = oldValue == null ? "" : oldValue.toString();
this.newValueString = newValue == null ? "" : newValue.toString();
// 通过field获取注解LogTag,这又是反射的field的典型用法
this.logTag = field.getAnnotation(LogTag.class);
this.withLogTag = logTag != null;
this.attributeAlias = (withLogTag && logTag.alias().length() != 0) ? logTag.alias() : field.getName();
this.withExtendedType = withLogTag && logTag.extendedType().length() != 0;
this.extendedType = withExtendedType ? logTag.extendedType() : null;
}
从上面的client代码中,可以看到其实现原理主要的还是反射的基础,并且是原生实现(意思是没有使用其它的封装的工具类,比如 spring框架的ReflectionUtils,还是hutool包的相关类) :
- Claas类:获取要记录的对象信息,包括属性field列表、自定义注解LogTag
- Field类: 获取要记录的对象属性field列表及对应的属性值
- LogTag类:自定义注解,为了更细化的控制记录行为 定义一些字段attributeName、extendedType、attributeAlias等
由此可见,不论是工具包还是一些框架,最终在使用反射时,其实现也就是由很基础的部分组成。所以在很多时候建议不要一味追求类似我会XX框架,我会XX技术之类(当然多读优秀的源码肯定是对技术有提高),这里引用周志明老师《深入理解JAVA虚拟机》中的一段话(第2版第9.2.3 字节码生成技术与动态代理的实现章节):
“字节码生成”并不是什么高深的技术,读者在看到“字节码生成”这个标题时也先不必去 想诸如Javassist、CGLib、ASM之类的字节码类库,因为JDK里面的javac命令就是字节码生 成技术的“老祖宗”,并且javac也是一个由Java语言写成的程序,它的代码存放在OpenJDK的 langtools/src/share/classes/com/sun/tools/javac目录中[1]。要深入了解字节码生成,阅读javac的 源码是个很好的途径,不过javac对于我们这个例子来说太过庞大了。在Java里面除了javac和 字节码类库外,使用字节码生成的例子还有很多,如Web服务器中的JSP编译器,编译时植 入的AOP框架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运 行时生成字节码来提高执行速度。我们选择其中相对简单的动态代理来看看字节码生成技术 是如何影响程序运作的。
相信许多Java开发人员都使用过动态代理,即使没有直接使用过java.lang.reflect.Proxy或 实现过java.lang.reflect.InvocationHandler接口,应该也用过Spring来做过Bean的组织管理。如 果使用过Spring,那大多数情况都会用过动态代理,因为如果Bean是面向接口编程,那么在 Spring内部都是通过动态代理的方式来对Bean进行增强的。动态代理中所谓的“动态”,是针 对使用Java代码实际编写了代理类的“静态”代理而言的,它的优势不在于省去了编写代理类 那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为, 当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。
属性处理扩展
前面源码中的LogTag,其实就是一个注解,利用它来实现属性的扩展处理,这也是一个注解+反射的典型应用!首先定义一个注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTag {
String alias() default "";
BuiltinTypeHandler builtinType() default BuiltinTypeHandler.NORMAL;
String extendedType() default "";
}
在需要进行变化日志记录的属性上增加@LogTag
注解。凡是没有增加该注解的属性在日志记录时会被自动跳过,注解属性介绍如下:
- alias:属性别名。默认情况下会将属性名写入。
- builtinType:ObjectLoggerClient的内置类型,为BuiltinTypeHandler的值。默认为
BuiltinTypeHandler.NORMAL
。- BuiltinTypeHandler.NORMAL:记录属性的新值和旧值,对比值为null
- BuiltinTypeHandler.RICHTEXT: 用户富文本对比。记录属性值的新值和旧值,并将新旧值转化为纯文本后逐行对比差异,对比值中记录差异
- extendedType:扩展属性类型。使用ObjcetLogger时,用户可以扩展某些字段的处理方式,此时,
alias
等信息均可以被用户自主覆盖。
相关处理的关键代码,在上面已经贴出来了:field.getAnnotation(LogTag.class); 至于具体如何去选择内置的NORMAL(字符串)、RICHTEXT(富文本),还是自定义的扩展处理,这里就不再讨论了,代码实现比较简单。
深拷贝与浅拷贝区别
这里还要特别说明的一点是,不知道有没有发现,在记录对象的时候,虽然传递的是2个对象task、oldTask,但是在声明oldTask的时候使用了一个deepCopy方法:
public <T> T deepCopy(T originalObject) {
Gson gson = new Gson();
return gson.fromJson(gson.toJson(originalObject), (Class<T>)originalObject.getClass());
}
这个代码看起来似乎就是一个对象的序列化而已,但其实里面还是涉及到一个很重要的java知识点:引用传递,具体可以参考重学Java深拷贝与浅拷贝
日志不是一件简单的事情
日志作为系统必不可少的一部分,占据相当重要的地位:
- 完整的日志请求,对于系统的操作追踪起到至关重要的作用,比如前面利用MDC来简单实现的日志追踪
- 良好日志记录对于日常的问题处理,尤其是出异常的时候,可以快速定位
- 规范的日志是其它系统的应用基础,比如APM监控系统用来监测系统状况+实现告警、数据分析系统采集分析用户行为+统计报表等
对于ObjectLogger这种框架,在小型系统上或许可以运行良好,实现一些关键审计日志:
- 它的存储使用mysql,对容量是一个考验(好一些会选择nosql,比如ES)
- 它设计的client+server模型,利用HTTP请求调用没有提供对应的容错策略(默认安全失败),也有丢失的风险
但是对于请求日志这种,一般还是采用slf4j这种打印到文件中,然后自动归档,所以要合理选择