java高级:利用Lambda表达式解决代码中硬编码的问题

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;

实际上就是实现了Functionapply()方法:

@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

好,我们看一下FunctionSerializable的源码:

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 IOExceptionprivate 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);
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L-960

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值