共计 11061 个字符,预计需要花费 28 分钟才能阅读完成。
背景
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 容器中。
@Component
public 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));