文章目录
- 前言
- 例子
- 原理
- writeReplace
- 反序列化对象
- 缓存元数据
- 写一个工具
前言
实体类:方法
这种方式获取字段名,摒弃了字符串拼接方式,避免拼接出现的问题,提高框架维护性和可修改性。
例子
-
引入·Mybatis-Plus·
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency>
-
增加配置
server: port: 8080 spring: application: name: data-import datasource: url: jdbc:mysql://192.168.158.129:3306/data-import?useSSl=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf-8&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 idle-timeout: 600000 maximum-pool-size: 10 auto-commit: true pool-name: datimportpool max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1
-
添加扫描
@MapperScan("com.liry.dataimport.repository.mapper")
-
写一个简单的查询逻辑在
service
@Override public Demo getByName(String name) { LambdaQueryWrapper<Demo> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Demo::getName, name); return demoMapper.selectOne(wrapper); }
-
测试
@SpringBootTest @RunWith(SpringRunner.class) public class SpringTest { @Resource private DemoService demoService; @Test public void testGetName() { Demo demo = demoService.getByName("ali222"); System.out.println(demo.getName()); } }
好了,我们看第4步里的写法,它使用了Mybatis-Plus
的Wrapper
类构建SQL语句,然后我们可用使用Demo::getName
获取到实体Demo
的字段name
的数据库属性名称,这个它底层的原理就是反序列化。
原理
以这个为人口我们深入看看
public Demo getByName(String name) {
LambdaQueryWrapper<Demo> wrapper = Wrappers.lambdaQuery();
wrapper.eq(Demo::getName, name);
return demoMapper.selectOne(wrapper);
}
进入看 wrapper.eq(Demo::getName, name);
,它会走到这个位置:
com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper#columnToString(com.baomidou.mybatisplus.core.toolkit.support.SFunction<T,?>)
注意看,这里column
是一个lambda
表达式,它并不是一个实际的字段。
我们可以放开焦点,看看当前这个类做了写什么AbstractLambdaWrapper
可以看出,这个类是做了一个字段的缓存,其实就是我们的数据库实体的缓存信息。
再往下看,有这样的一段:
可以看到这里通过LambdaUtils.extract(column);
就获取到了lambda
的元数据,包含方法名、属性名、类名,然后通过方法名推断属性名。
这里它有两种处理方式,反射方式是通过序列化中的方法writeReplace
操作的,而序列化方式他是将字节系列转化为对象后,然后再反射读取,所以前者明显性能比后者高,但后者它保证了异常情况下正常执行的能力。
writeReplace
那什么是writeReplace
?
writeReplace
是在序列化对象时写入的一个方法,这个方法会动态的创建并返回了一个SerializedLambda
对象,该对象是Lambda
表达式的序列化类,它记录了Lambda
表达式创建时需要的必要信息,包括接口类型、方法签名、方法参数等,因此,我们可以通过这个对象获取到相关信息,但需要函数式接口实现Serializable
接口,如下面,必须要有Seriablizable
。
@FunctionalInterface
public interface CusFunction<T, R> extends Function<T, R> , Serializable {
}
这里再提一嘴,我们可以自己实现writeReplace
方法,这里举个例子:
public class TestA implements Serializable {
String name = "aaaa";
// writeReplace执行序列化替换对象
private final Object writeReplace() {
return new TestB();
}
}
public class TestB implements Serializable {
String name = "bbbb";
}
public class TestSeri {
public static void main(String[] args) {
// 序列化 TestA
try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("./osa"))) {
os.writeObject(new TestA());
os.flush();
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 反序列化 TestA
try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("./osa"))) {
Object obj = is.readObject();
is.close();
System.out.println("类名:"+ obj.getClass().getName());
System.out.println("属性值:" + ((TestB)obj).name);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
结果如下:
反序列化对象
我们再看另一种情况,它其实就是还是从字节序列中构建SerializedLambda
对象
位置:com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda#extract
这段代码较为简单,就是将Lambda
表达式中通过反序列化的方式解析相关信息。
缓存元数据
在解析得到Lambda
表达式后,反射获取到类名,属性字段信息,然后缓存,之后通过key读取。
protected ColumnCache getColumnCache(SFunction<T, ?> column) {
// 解析得到lambda的表达式信息
LambdaMeta meta = LambdaUtils.extract(column);
// 根据方法名解析字段名称,如方法名为 getName,则属性应该是 name
String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
// 获取到类
Class<?> instantiatedClass = meta.getInstantiatedClass();
// 初始化当前类的映射信息
tryInitCache(instantiatedClass);
// 根据字段名返回字段记录信息
return getColumnCache(fieldName, instantiatedClass);
}
/*
* 初始化映射信息
*/
private void tryInitCache(Class<?> lambdaClass) {
// 是否初始化过映射信息
if (!initColumnMap) {
// 反射获取类型
final Class<T> entityClass = getEntityClass();
if (entityClass != null) {
lambdaClass = entityClass;
}
// 反射获取字段信息
// ** 并缓存 **
columnMap = LambdaUtils.getColumnMap(lambdaClass);
Assert.notNull(columnMap, "can not find lambda cache for this entity [%s]", lambdaClass.getName());
initColumnMap = true;
}
}
COLUMN_CACHE_MAP
是类名为外面的key,然后字段名为里面的key。
写一个工具
上面我们弄清楚了实体类:方法
这样的写法是怎么实现的,现在我们可以根据它的自己弄一个较为通用的,一个工具。
- 定义一个函数式接口
@FunctionalInterface
public interface CusFunction<T, R> extends Function<T, R>, Serializable {
}
-
Lambda
元数据对象public class LambdaMetaCache { private Class<?> clazz; private String methodName; public LambdaMetaCache(Class<?> clazz, String methodName) { this.clazz = clazz; this.methodName = methodName; } public Class<?> getClazz() { return clazz; } public void setClazz(Class<?> clazz) { this.clazz = clazz; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } }
-
写一个Lambda解析工具
public final class LambdaUtils {
/**
* lambda表达式反序列化
*
* @param func lambda表达式
*/
private static <T> SerializedLambda resolve(CusFunction<T, ?> func) {
try {
Method method = func.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(true);
// 安全访问
return (SerializedLambda) AccessController.doPrivileged(new CusPrivilegedAction<>(method)).invoke(func);
} catch (Exception e) {
// 反序列化解析
return extractLambda(func);
}
}
public static <T> LambdaMetaCache extractLambdaMeta(CusFunction<T, ?> func) {
SerializedLambda resolve = resolve(func);
return new LambdaMetaCache(getClass(resolve), getMethodName(resolve));
}
/**
* 读取Lambda的方法名
*
* @param lambda lambda表达式序列化对象
* @return 方法名
*/
private static String getMethodName(SerializedLambda lambda) {
return lambda.getImplMethodName();
}
/**
* 读取Lambda的类型信息
*
* @param lambda lambda表达式序列化对象
* @return 类型信息
*/
private static Class<?> getClass(SerializedLambda lambda) {
String instantiatedMethodType = lambda.getImplClass();
String instantiatedType = instantiatedMethodType.replace("/", ".");
try {
// 加载类
return loadClass(instantiatedType, getClassLoaders(null));
} catch (ClassNotFoundException e) {
throw new RuntimeException("找不到指定的class!请仅在明确确定会有 class 的时候,调用该方法", e);
}
}
/**
* 反序列化
*
* @param serializable 可序列化对象
*/
private static SerializedLambda extractLambda(Serializable serializable) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(serializable);
oos.flush();
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
return super.resolveClass(desc);
}
}) {
return (SerializedLambda) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 类加载器列表
*/
private static ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[] {
classLoader,
Thread.currentThread().getContextClassLoader(),
LambdaUtils.class.getClassLoader()};
}
/**
* 加载类
*
* @param className 类名
* @param classLoaders 类加载器
* @return 类型
*/
private static Class<?> loadClass(String className, ClassLoader[] classLoaders) throws ClassNotFoundException {
for (ClassLoader classLoader : classLoaders) {
if (classLoader != null) {
try {
return Class.forName(className, true, classLoader);
} catch (ClassNotFoundException e) {
// ignore
}
}
}
throw new ClassNotFoundException("Cannot find class: " + className);
}
/**
* 为安全访问创建的对象信息
*
* @param <T>
*/
static class CusPrivilegedAction<T extends AccessibleObject> implements PrivilegedAction<T> {
private final T obj;
public CusPrivilegedAction(T obj) {
this.obj = obj;
}
@Override
public T run() {
obj.setAccessible(true);
return obj;
}
}
}
- 然后把
Mybatis-Plus
里的PropertyNamer
复制过来
public final class PropertyNamer {
private PropertyNamer() {
}
/**
* 方法名转成属性名
*/
public static String methodToProperty(String name) {
if (name.startsWith("is")) {
name = name.substring(2);
} else {
if (!name.startsWith("get") && !name.startsWith("set")) {
throw new RuntimeException("Error parsing property name '" + name + "'. Didn't start with 'is', 'get' or 'set'.");
}
name = name.substring(3);
}
if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) {
name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
}
return name;
}
public static boolean isProperty(String name) {
return isGetter(name) || isSetter(name);
}
public static boolean isGetter(String name) {
return name.startsWith("get") && name.length() > 3 || name.startsWith("is") && name.length() > 2;
}
public static boolean isSetter(String name) {
return name.startsWith("set") && name.length() > 3;
}
}
测试一下:
@Test
public void testLambda() {
LambdaMetaCache lambdaMetaCache = LambdaUtils.extractLambdaMeta(Job::getName);
System.out.println(lambdaMetaCache.getClazz().getName());
System.out.println(lambdaMetaCache.getMethodName());
}
以上作为基础工具,基于不同的框架可以添加实体类工具以获取到对应的字段名。
比如Neo4j
所对应的实体是由注解@Node、@Property、@Id、@Relationship
等这些标记,而Mybatis
是由@TableName、@TableField、@TableId
这些标记,所以,针对不同框架我们可以写对应的反射工具获取对应实体的字段,然后做映射处理。