背景
Mybatis Plus QueryWrapper
的lambda
用起来很便当,比方Wrappers.<Order>lambdaQuery().eq(Order::getOrderCode, 'test')
。
然而在须要对SQL做一些非凡解决时,比方distinct
、sum
时,无奈用到lambda
,只能硬编码字段的数据库字段名,例如Wrappers.<Order>query().select("distinct order_code")
,这种在代码里硬编码数据库字段名,给人感觉十分的不标准,那是否能够像lambdaQuery
那样,不硬编码也能够获取到数据库字段名呢?相似这样子:
String columName = columnResolver.getColumn(Order::getOrderCode);Wrappers.<Order>query().select("distinct " + columName);
思路
Mybatis Plus
的lambda
既然在惯例的SQL下既然能够做到获取数据库字段名,那是否能够复用它的代码呢?
这就须要先看下Mybatis Plus
是怎么实现的。
原理
初始化
Mybatis
在我的项目启动时,会生成每个Mapper
对应的Bean
,Mybatis Plus
在解析Mapper
时,会解析实体类信息,生成TableInfo
,解析入口是TableInfoHelper.initTableInfo
。
public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) { TableInfo targetTableInfo = TABLE_INFO_CACHE.get(clazz); final Configuration configuration = builderAssistant.getConfiguration(); if (targetTableInfo != null) { Configuration oldConfiguration = targetTableInfo.getConfiguration(); if (!oldConfiguration.equals(configuration)) { // 不是同一个 Configuration,进行从新初始化 targetTableInfo = initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz); TABLE_INFO_CACHE.put(clazz, targetTableInfo); } return targetTableInfo; } return TABLE_INFO_CACHE.computeIfAbsent(clazz, key -> initTableInfo(configuration, builderAssistant.getCurrentNamespace(), key));}
先从缓存TABLE_INFO_CACHE
里获取信息,如果没有,就通过initTableInfo
解析信息并放入缓存。
private synchronized static TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class<?> clazz) { /* 没有获取到缓存信息,则初始化 */ TableInfo tableInfo = new TableInfo(clazz); tableInfo.setCurrentNamespace(currentNamespace); tableInfo.setConfiguration(configuration); GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration); /* 初始化表名相干 */ final String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo); List<String> excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList(); /* 初始化字段相干 */ initTableFields(clazz, globalConfig, tableInfo, excludePropertyList); /* 主动构建 resultMap */ tableInfo.initResultMapIfNeed(); /* 缓存 lambda */ LambdaUtils.installCache(tableInfo); return tableInfo;}
通过initTableName
办法初始化表名信息,再通过initTableFields
办法初始化字段相干信息,最初放入缓存中。
private static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) { /* 数据库全局配置 */ GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig(); ReflectorFactory reflectorFactory = tableInfo.getConfiguration().getReflectorFactory(); //TODO @咩咩 有空一起来撸完这反射模块. Reflector reflector = reflectorFactory.findForClass(clazz); List<Field> list = getAllFields(clazz); // 标记是否读取到主键 boolean isReadPK = false; // 是否存在 @TableId 注解 boolean existTableId = isExistTableId(list); // 是否存在 @TableLogic 注解 boolean existTableLogic = isExistTableLogic(list); List<TableFieldInfo> fieldList = new ArrayList<>(list.size()); for (Field field : list) { if (excludeProperty.contains(field.getName())) { continue; } /* 主键ID 初始化 */ if (existTableId) { TableId tableId = field.getAnnotation(TableId.class); if (tableId != null) { if (isReadPK) { throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", clazz.getName()); } else { initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId, reflector); isReadPK = true; continue; } } } else if (!isReadPK) { isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, reflector); if (isReadPK) { continue; } } final TableField tableField = field.getAnnotation(TableField.class); /* 有 @TableField 注解的字段初始化 */ if (tableField != null) { fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, tableField, reflector, existTableLogic)); continue; } /* 无 @TableField 注解的字段初始化 */ fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic)); } /* 字段列表 */ tableInfo.setFieldList(fieldList); /* 未发现主键注解,提醒正告信息 */ if (!isReadPK) { logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName())); }}
initTableFields
办法通过获取注解TableId
、TableField
配置来生成字段相干信息,TableField
解析信息逻辑在TableFieldInfo
构造函数内。
public TableFieldInfo(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo, Field field, TableField tableField, Reflector reflector, boolean existTableLogic) { field.setAccessible(true); this.field = field; this.version = field.getAnnotation(Version.class) != null; this.property = field.getName(); this.propertyType = reflector.getGetterType(this.property); this.isPrimitive = this.propertyType.isPrimitive(); this.isCharSequence = StringUtils.isCharSequence(this.propertyType); this.fieldFill = tableField.fill(); this.withInsertFill = this.fieldFill == FieldFill.INSERT || this.fieldFill == FieldFill.INSERT_UPDATE; this.withUpdateFill = this.fieldFill == FieldFill.UPDATE || this.fieldFill == FieldFill.INSERT_UPDATE; this.update = tableField.update(); JdbcType jdbcType = tableField.jdbcType(); final Class<? extends TypeHandler> typeHandler = tableField.typeHandler(); final String numericScale = tableField.numericScale(); String el = this.property; if (JdbcType.UNDEFINED != jdbcType) { this.jdbcType = jdbcType; el += (COMMA + "jdbcType=" + jdbcType.name()); } if (UnknownTypeHandler.class != typeHandler) { this.typeHandler = (Class<? extends TypeHandler<?>>) typeHandler; if (tableField.javaType()) { String javaType = null; TypeAliasRegistry registry = tableInfo.getConfiguration().getTypeAliasRegistry(); Map<String, Class<?>> typeAliases = registry.getTypeAliases(); for (Map.Entry<String, Class<?>> entry : typeAliases.entrySet()) { if (entry.getValue().equals(propertyType)) { javaType = entry.getKey(); break; } } if (javaType == null) { javaType = propertyType.getName(); registry.registerAlias(javaType, propertyType); } el += (COMMA + "javaType=" + javaType); } el += (COMMA + "typeHandler=" + typeHandler.getName()); } if (StringUtils.isNotBlank(numericScale)) { el += (COMMA + "numericScale=" + numericScale); } this.el = el; this.initLogicDelete(dbConfig, field, existTableLogic); String column = tableField.value(); if (StringUtils.isBlank(column)) { column = this.property; if (tableInfo.isUnderCamel()) { /* 开启字段下划线申明 */ column = StringUtils.camelToUnderline(column); } if (dbConfig.isCapitalMode()) { /* 开启字段全大写申明 */ column = column.toUpperCase(); } } String columnFormat = dbConfig.getColumnFormat(); if (StringUtils.isNotBlank(columnFormat) && tableField.keepGlobalFormat()) { column = String.format(columnFormat, column); } this.column = column; this.sqlSelect = column; if (tableInfo.getResultMap() == null && !tableInfo.isAutoInitResultMap() && TableInfoHelper.checkRelated(tableInfo.isUnderCamel(), this.property, this.column)) { /* 未设置 resultMap 也未开启主动构建 resultMap, 字段规定又不合乎 mybatis 的主动封装规定 */ String propertyFormat = dbConfig.getPropertyFormat(); String asProperty = this.property; if (StringUtils.isNotBlank(propertyFormat)) { asProperty = String.format(propertyFormat, this.property); } this.sqlSelect += (" AS " + asProperty); } this.insertStrategy = this.chooseFieldStrategy(tableField.insertStrategy(), dbConfig.getInsertStrategy()); this.updateStrategy = this.chooseFieldStrategy(tableField.updateStrategy(), dbConfig.getUpdateStrategy()); this.whereStrategy = this.chooseFieldStrategy(tableField.whereStrategy(), dbConfig.getSelectStrategy()); if (StringUtils.isNotBlank(tableField.condition())) { // 细粒度条件管制 this.condition = tableField.condition(); } // 字段是否注入查问 this.select = tableField.select();}
因为MybatisConfiguration
默认开启驼峰转下划线
模式:this.mapUnderscoreToCamelCase = true
,即@TableField
如果没有配置value
属性,则数据库字段名默认辨认为下划线格局
。
在初始化配置信息,将信息放入缓存之后,后续的查问就能够用到这些信息了。
数据执行流程
Wrappers.<Order>lambdaQuery().eq(Order::getOrderCode, 'test')
中的Order::getOrderCode
传入的其实是SFunction
对象,通过AbstractLambdaWrapper.columnsToString
办法将SFunction
转为column
名称。
protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) { return getColumn(LambdaUtils.resolve(column), onlyColumn);}
LambdaUtils
是解析的外围代码类。
public final class LambdaUtils { public static <T> SerializedLambda resolve(SFunction<T, ?> func) { Class<?> clazz = func.getClass(); String name = clazz.getName(); return Optional.ofNullable(FUNC_CACHE.get(name)) .map(WeakReference::get) .orElseGet(() -> { SerializedLambda lambda = SerializedLambda.resolve(func); FUNC_CACHE.put(name, new WeakReference<>(lambda)); return lambda; }); }} public class SerializedLambda implements Serializable { /** * 通过反序列化转换 lambda 表达式,该办法只能序列化 lambda 表达式,不能序列化接口实现或者失常非 lambda 写法的对象 * * @param lambda lambda对象 * @return 返回解析后的 SerializedLambda */ public static SerializedLambda resolve(SFunction<?, ?> lambda) { if (!lambda.getClass().isSynthetic()) { throw ExceptionUtils.mpe("该办法仅能传入 lambda 表达式产生的合成类"); } try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) { @Override protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException { Class<?> clazz; try { clazz = ClassUtils.toClassConfident(objectStreamClass.getName()); } catch (Exception ex) { clazz = super.resolveClass(objectStreamClass); } return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz; } }) { return (SerializedLambda) objIn.readObject(); } catch (ClassNotFoundException | IOException e) { throw ExceptionUtils.mpe("This is impossible to happen", e); } }} public class SerializationUtils { /** * Serialize the given object to a byte array. * * @param object the object to serialize * @return an array of bytes representing the object in a portable fashion */ public static byte[] serialize(Object object) { if (object == null) { return null; } ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(object); oos.flush(); } catch (IOException ex) { throw new IllegalArgumentException("Failed to serialize object of type: " + object.getClass(), ex); } return baos.toByteArray(); } }
- 先听过
SerializationUtils.serialize
把SFunction
序列化,获取类字节。 - 再通过重组成
SerializedLambda.resolve
对象,通过lambda.getClass().isSynthetic()
确保SFunction
肯定是个lambda表达式
。 - 最初放入
FUNC_CACHE
缓存。
学到了一手 ^_^
解析进去的lambda
对象信息是这样的:
public abstract class AbstractLambdaWrapper<T, Children extends AbstractLambdaWrapper<T, Children>> private String getColumn(SerializedLambda lambda, boolean onlyColumn) { Class<?> aClass = lambda.getInstantiatedType(); tryInitCache(aClass); String fieldName = PropertyNamer.methodToProperty(lambda.getImplMethodName()); ColumnCache columnCache = getColumnCache(fieldName, aClass); return onlyColumn ? columnCache.getColumn() : columnCache.getColumnSelect(); } }
- 从初始化时创立的缓存里,获取
对象缓存列表
。 - 调用
PropertyNamer.methodToProperty(lambda.getImplMethodName())
从办法中提取属性名,例如从getCodeName
提取到codeName
。 - 依据
属性名
从缓存中获取对应的列信息。
以上就是数据执行流程,通过LambdaWrapper.columnToString
可用获取到属性的数据库字段名,然而columnToString
办法是protected
,无奈间接应用,能够通过继承的形式来扩大 。
实现
- 扩大类继承
AbstractLambdaWrapper
,重写columnToString
办法, 然而内容是调用父类的办法,只是把办法从protected
改为public
。 - 因为
mybatis-plus
会在我的项目启动时初始化信息,也就是扩大类必须是在相干的类初始化实现之后能力用,所以也要把扩大类退出到spring容器中。
@Componentpublic class MybatisPlusColumnResolver { public <T> ColumnResolver<T> create() { return new ColumnResolver<>(); } /** * @author * @date */ public static class ColumnResolver<T> extends AbstractLambdaWrapper<T, ColumnResolver<T>> { @Override protected ColumnResolver<T> instance() { return null; } @Override public String columnsToString(SFunction<T, ?>... columns) { return super.columnsToString(columns); } @Override public String columnsToString(boolean onlyColumn, SFunction<T, ?>... columns) { return super.columnsToString(onlyColumn, columns); } @Override public String columnToString(SFunction<T, ?> column) { return super.columnToString(column); } @Override public String columnToString(SFunction<T, ?> column, boolean onlyColumn) { return super.columnToString(column, onlyColumn); } }}
应用
@Autowired private MybatisPlusColumnResolver lambdaColumnResolvers; ColumnResolver<Order> columnResolver = lambdaColumnResolvers.create(); Wrappers.<OmsOrder>query().select("distinct " + columnResolver.columnToString(OmsOrder::getCodeName));