使用 SerializedLambda 替代字符串

 2023-02-05
原文作者:蒋先森 原文地址:https://jlj98.top/

最近在项目中使用 mongo,ORM 使用的是 spring-boot-starter-data-mongo。在使用过程中,对于数据库字段,每次都要写,觉得麻烦,然后想起以前使用 mybatis-plus,只需要使用 lambda 获取字段名称即可,如下:

    public List<UserInfo> getListByName(String name) {
      LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
      queryWrapper.eq(UserInfo::getName, name);
      return list(queryWrapper);
    }

下面利用Lambda 函数式接口,通过 SerializedLambda 原理来获取 Java Bean 的实例。

定义函数接口

    @FunctionalInterface
    public interface Func<E, R> extends Function<E, R>, Serializable {
    
    }

这里函数式接口继承 Serializable 接口,在 JDK 1.8 之后,JDK提供了一个新的类,凡是继承了 Serializable 的函数式接口的实例,都可以获取一个属于它的 SerializedLambda 实例,并通过它获取实例的信息。所以,我们可以通过这个原理来实现如何通过 Lambda 来获取 Java Bean 的属性名称。

实现获取方法字段

    public class ReflectLambdaUtils {
    
        private static final Logger logger = LoggerFactory.getLogger(ReflectLambdaUtils.class);
    
        /**
         * 根据参数获取字段名称
         *
         * @param func
         * @return
         */
        public static <E, R> String getFieldName(Func<E, R> func) {
            Field field = getField(func);
            return field.getName();
        }
    
        /**
         * 获取表达式的字段
         *
         * @param func
         * @return
         */
        public static <E, R> Field getField(Func<E, R> func) {
            try {
                Method method = func.getClass().getDeclaredMethod("writeReplace");
                method.setAccessible(Boolean.TRUE);
                // 1.调用 writeReplace 方法,返回一个 writeReplace 对象
                SerializedLambda serializedLambda = (SerializedLambda) method.invoke(func);
                String getterMethod = serializedLambda.getImplMethodName();
                String fieldName = Introspector.decapitalize(getterMethod.replace("get", ""));
                // 2. 获取的Class是字符串,并且包名是“/”分割,需要替换成“.”,才能获取到对应的Class对象
                String declaredClass = serializedLambda.getImplClass().replace("/", ".");
                Class<?> aClass = Class.forName(declaredClass, false, ClassUtils.getDefaultClassLoader());
                // 3.通过Spring 中的反射工具类获取Class中定义的Field
                return ReflectionUtils.findField(aClass, fieldName);
            } catch (ReflectiveOperationException e) {
                logger.error("解析类字段出现异常", e);
                throw new MongoPlusException("解析字段时出现异常");
            }
        }
    }

其中 writeReplace() 这个方法,是虚拟机加上去的,虚拟机会自动给实现 Serializable 接口的 Lambda 表达式生成 writeReplace() 方法。如果是被序列化后,实体对象就会有 writeReplace() 方法,调用该方法,会返回 SerializedLambda 对象去做序列化,即被序列化的对象被替换了。

这个时候,返回的 SerializedLambda 对象中包含了 Lambda 表达式中所有的信息,比如函数名implMethodName、函数签名 implMethodSignature 等。我们就可以根据这些信息,获取我们所需要的属性字段了。

测试

    public class UserInfo {
        private String name;
        @Field(value = "user_age")
        private Integer age;
        private String userId;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        public String getUserId() {
            return userId;
        }
    
        public void setUserId(String userId) {
            this.userId = userId;
        }
    
        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", userId='" + userId + '\'' +
                    '}';
        }
    }
    
    public class AppTest {
    
        @Test
        public void getFieldName() {
            UserInfo userInfo = new UserInfo();
            userInfo.setAge(20);
            userInfo.setName("张三");
            System.out.println(ReflectLambdaUtils.getFieldName(UserInfo::getUserId));
        }
    }

总结

上面的一个简单例子,通过 SerializedLambda 实现了通过 Lambda 获取 Java Bean 实例属性的简单案例。对于字段的处理,比如 is、get 等不同开头的,需要大家自己去做处理,这里我只展示了处理getField这个字段。