1 硬编码存在的问题
在日常开发中,常常遇到一些软硬编码的问题,比如说我有一个关于文档的类DocumentInfoBo
,其定义如下:
@Data
public class DocumentInfoBo {
/**
* 文档uuid
*/
private String fileUuid;
/**
* 文件状态
*/
private Integer fileStatus;
...其他字段
现在我有一个需求,将这个对象存入redis中,存为hash结构,hash能够做到只更新hash中的某个column,val,而不影响其他的column,val
所以我们需要得到这个对象的字段名,在java中,我们一般的编码方式为:
redisTemplate.opsForHash().put(key, column, val);
可以看出,字段名我们是手动传进来的,封装一下方法变为:
public void updateFieldFromRedis(String fileUuid, String column, Object val) {
redisTemplate.opsForHash().put(fileUuid, column, val);
}
既如此,那么我们在调用方法时,就要传入硬编码updateFieldFromRedis("key","xxxcolumn","xxxval");
很显然,这样并不优雅,如果后续DocumentInfoBo
的字段有修改,那么我就要更新对应java代码中的硬编码。
2 mybatisplus是如何解决数据库字段操作硬编码的问题
那么有什么好的方式能够避免吗,答案是肯定的,我们先看下面的这个例子,mybatisplus是如何解决硬编码的问题的:
QueryWrapper<DocumentInfoBo> wrapper = new QueryWrapper<>();
wrapper.eq("fileUuid", documentInfoBo.getFileUuid());
LambdaQueryWrapper<DocumentInfoBo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DocumentInfoBo::getFileUuid, documentInfoBo.getFileUuid());
我们发现,他使用的Lambda函数的方式解决硬编码的问题,究其源码可知,他将Lambda表达式进行反序列化,我们都知道普通的Lambda是不可以反序列化的,但是只要实现了Serializable接口
,就拥有的反序列化的能力,最后经由反射得到SerializedLambda
对象,注意这个对象是Lambda函数反射过来的,通过getImplMethodName()
方法就可以很方便的得到字段名啦,下面我们自己实践一下。
3 实践解决硬编码问题
3.1 通过函数式接口Function编写Lambda表达式
Function代表的含义是“函数”,可以理解为一个计算的通道,既然是通道,那么就会有输入输出。
因此它含有一个apply()
方法,包含一个输入与一个输出,也就是说我们的表达式,实际上是利用Lambda表达式实现了apply()方法。
看下面例子:
public static void main(String[] args) {
// 定义一个函数:得到DocumentInfoBo对象的fileUuid字段的值
Function<DocumentInfoBo, String> func_old = documentInfoBo->documentInfoBo.getFileUuid();
// 新写法(推荐)
Function<DocumentInfoBo, String> func = DocumentInfoBo::getFileUuid;
final DocumentInfoBo documentInfoBo = new DocumentInfoBo();
documentInfoBo.setFileUuid("xxx");
final String fileUuid = func.apply(documentInfoBo);
System.out.println(fileUuid);
}
如何理解我们书写的Lambda表达式
这是非常重要的一步
经过上面的例子
Function<DocumentInfoBo, String> func_old = documentInfoBo->documentInfoBo.getFileUuid();
也就是说,我们的入参为一个DocumentInfoBo对象,输出为String类型的uuid;
实际上就是实现了Function
的apply()
方法:
@Override
public String apply(DocumentInfoBo documentInfoBo){
return documentInfoBo.getFileUuid();
}
3.2 我们写出来的函数式接口的对象到底是什么
实际上它是一个匿名类,实现了Function接口并利用Lambda表达式实现了apply()方法的匿名类,具体验证请结合3.1和3.2.2验证。
3.2.1 通过func.getClass()
查看
通过debug查看func 的对象,结果为:
com.lvxy.redis.RedisUtils$$Lambda$2/811587677
类名A$类名B:类名A中的类名B
类名A$2:类名A中的匿名内部类,内部类索引为2,也就是第二个内部类(索引从1开始)
类名A$$Lambda$2:类名A中的Lambda表达式,Lambda索引为2,也就是第二个Lambda表达式(索引从1开始)
3.2.2 通过func.getClass().getMethods()
查看
注意:getMethod获取的是类的所有public方法,包括自身的和从父类、接口继承的。
getFields()规则与此相同。
result={Method[12@1075]}
0={Method@1076} public java.lang.Object com.lvxy.redis.RedisUtils$$Lambda$2/811587677.apply(java.lang.Object)
1={Method@1077} public java.lang.Object public final void java.lang.Object.wait() throws java.lang.InterruptedException
2={Method@1078} public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
3={Method@1079} public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
4={Method@1080} public boolean java.lang.Object.equals(java.lang.Object)
5={Method@1081} public java.lang.String java.lang.Object.toString()
6={Method@1082} public public native int java.lang.Object.hashCode()
7={Method@1083} public final native java.lang.Class java.lang.Object.getClass()
8={Method@1084} public final native void java.lang.Object.notify()
9={Method@1085} public final native void java.lang.Object.notifyAll()
10={Method@1086} public default java.util.function.Function java.util.function.Function.andThen(java.util.function.Function)
11={Method@1087} public default java.util.function.Function java.util.function.Function.compose(java.util.function.Function)
3.2.3 通过func.getClass().getDeclaredMethods()
查看
注意:getDeclaredMethod获取的是类自身声明的方法,包含public、protected和private方法。
getDeclaredFields()规则与此相同。
result={Method[1@1091]}
0={Method@1092} public java.lang.Object com.lvxy.redis.RedisUtils$$Lambda$2/811587677.apply(java.lang.Object)
3.3 认识一下SerializedLambda类
/**
* <p>Implementors of serializable lambdas, such as compilers or language
* runtime libraries, are expected to ensure that instances deserialize properly.
* One means to do so is to ensure that the {@code writeReplace} method returns
* an instance of {@code SerializedLambda}, rather than allowing default
* serialization to proceed.
*/
public final class SerializedLambda implements Serializable {
...
}
大概意思是:
- 普通的Lambda表达式是无法进行反序列化的。
- 如果被期望确保实例正确地反序列化,就要使用可序列化lambdas的实现者
SerializedLambda
类。 - 可以通过继承
Serializable
,然后使用writeReplace
方法就可以返回SerializedLambda的
实例。
3.4 如何通过函数式接口获取SerializedLambda类
上面说的已经很清楚了,我们需要使用Serializable
接口的writeReplace
方法来获取SerializedLambda
类,但是看Function的源码发现,它并没有实现Function
接口,也就是普通的Lambda表达式是无法进行反序列化的,那么应该如果操作呢,继续往下看?
答案就是自己实现一个函数式接口,继承Function<T, R>、Serializable
好,我们看一下Function
和Serializable
的源码:
Function<T, R>
源码:
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
...
Serializable
接口源码
public interface Serializable {
}
虽然什么也没有,但是观察注释发现,Serializable
给我们提供了如下方法,在这里找到了我们需要的readResolve
方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException:
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
3.4.1 自己实现一个可序列话的函数式接口
我们写一个属于我们自己的可序列化的函数式接口SFunction
,需要注意的是@FunctionalInterface
,一般注释在接口上,作用为标注这个接口为一个函数式接口
/**
* 使Function获取序列化能力
*/
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}
3.4.2 通过反射获取SerializedLambda类
/**
* 通过Lambda表达式,获取Lambda表达式的SerializedLambda对象
*/
private static <T> SerializedLambda getSerializedLambda(SFunction<T, ?> func) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
// 通过反射对象获取 writeReplace() 方法
Method writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace");
// 获取方法的访问权限
boolean isAccessible = writeReplaceMethod.isAccessible();
// 给予访问权限
writeReplaceMethod.setAccessible(true);
//通过 writeReplace() 取出序列化的lambda信息
SerializedLambda serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func);
// 恢复之前的访问权限
writeReplaceMethod.setAccessible(isAccessible);
return serializedLambda;
}
3.5 通过SerializedLambda获取Lambda表达式具体信息
编写一个注解,用于指定字段别名
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisField {
String value() default "";
}
对SerializedLambda
的具体操作
public static <T> String getFieldName(SFunction<T, ?> fn) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
final SerializedLambda serializedLambda = getSerializedLambda(fn);
// 获取Lambda表达式中传输的方法名
String methodName = serializedLambda.getImplMethodName();
// 获取字段名
String fieldName = methodName.substring("get".length());
// 字段首字母小写
fieldName = fieldName.replaceFirst(fieldName.charAt(0) + "", (fieldName.charAt(0) + "").toLowerCase());
Field field;
try {
field = Class.forName(serializedLambda.getImplClass().replace("/", ".")).getDeclaredField(fieldName);
} catch (ClassNotFoundException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
// 从field取出字段名,可以根据实际情况调整
RedisField tableField = field.getAnnotation(RedisField.class);
if (tableField != null && tableField.value().length() > 0) {
return tableField.value();
} else {
// 根据业务要求,对字段名做出大小写等调整
return fieldName;
}
}
4 解决硬编码问题的完整代码
@Component
@Slf4j
public class RedisUtils{
@Resource
private RedisTemplate<String, Object> redisTemplate;
public static <T> String getFieldName(SFunction<T, ?> fn) {
SerializedLambda serializedLambda = getSerializedLambda(fn);
// 获取Lambda表达式中传输的方法名
String methodName = serializedLambda.getImplMethodName();
// 获取字段名
String fieldName = methodName.substring("get".length());
// 字段首字母小写
fieldName = fieldName.replaceFirst(fieldName.charAt(0) + "", (fieldName.charAt(0) + "").toLowerCase());
Field field;
try {
field = Class.forName(serializedLambda.getImplClass().replace("/", ".")).getDeclaredField(fieldName);
} catch (ClassNotFoundException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
// 从field取出字段名,可以根据实际情况调整
RedisField tableField = field.getAnnotation(RedisField.class);
if (tableField != null && tableField.value().length() > 0) {
return tableField.value();
} else {
// 根据业务要求,对字段名做出大小写等调整
return fieldName;
}
}
/**
* 通过Lambda表达式,获取Lambda表达式的SerializedLambda对象
*/
private static <T> SerializedLambda getSerializedLambda(SFunction<T, ?> func) {
// 通过反射对象获取 writeReplace() 方法
Method writeReplaceMethod;
try {
writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
// 获取方法的访问权限
boolean isAccessible = writeReplaceMethod.isAccessible();
// 给予访问权限
writeReplaceMethod.setAccessible(true);
//通过 writeReplace() 取出序列化的lambda信息
SerializedLambda serializedLambda = null;
try {
serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
// 恢复之前的访问权限
writeReplaceMethod.setAccessible(isAccessible);
return serializedLambda;
}
/**
* 添加数据到redis
*/
/**
* 添加数据到redis
*/
public void updateFieldFromRedis(String fileUuid, SFunction<DocumentInfoBo, Object> column, Object val) {
if (StringUtils.isBlank("fileUuid")) {
throw new MyException(ResultEnum.FILEUUID_IS_NULL);
}
redisTemplate.opsForHash().put(fileUuid, getFieldName(column), val);
}
}
通过如下代码调用即可:
updateFieldFromRedis(documentInfoBo.getFileUuid(), DocumentInfoBo::getFileStatus, 3);