说明:本文参考至https://www.jianshu.com/p/baba62bbc107

MyBatis 在进行参数处理、结果映射时等操作时,会涉及大量的反射操作。为了简化这些反射相关操作,MyBatisorg.apache.ibatis.reflection 包下提供了专门的反射模块,对反射操作做了近一步封装,提供了更为简洁的 API

一. Reflector

MyBatis 提供 Reflector 类来缓存类的字段名和 getter/setter 方法的元信息,使得涉及反射的操作时不用再去获取这些元信息,使操作更加便捷。使用方式是将原始类对象传入其构造方法,生成 Reflector 对象。

public class Reflector {

  private final Class<?> type;
//可读的属性名称
private final String[] readablePropertyNames;
//可写的属性名称
private final String[] writablePropertyNames;
//set方法的属性
private final Map<String, Invoker> setMethods = new HashMap<>();
//get方法的属性
private final Map<String, Invoker> getMethods = new HashMap<>();
//setter类型的列表
private final Map<String, Class<?>> setTypes = new HashMap<>();
//getter类型的列表
private final Map<String, Class<?>> getTypes = new HashMap<>();
//默认的构造函数
private Constructor<?> defaultConstructor; //不区分大小写的属性映射
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>(); public Reflector(Class<?> clazz) {
type = clazz;
//加入无参构造器
addDefaultConstructor(clazz);
//加入getter方法
addGetMethods(clazz);
//加入setter方法
addSetMethods(clazz);
//加入字段
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[0]);
writablePropertyNames = setMethods.keySet().toArray(new String[0]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
} ...
}

addGetMethodsaddSetMethods 分别获取类的所有方法,从符合 getter/setter 规范的方法中解析出字段名,并记录方法的参数类型、返回值类型等信息:

/**
* 注意博主这个版本是3.5.5-SNAPSHOT,采用的是Java8提供的流式操作,老版本的MyBatis源码和它不一样,但是功能上是一致的
* @param clazz
*/
private void addGetMethods(Class<?> clazz) {
Map<String, List<Method>> conflictingGetters = new HashMap<>();
Method[] methods = getClassMethods(clazz);
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
resolveGetterConflicts(conflictingGetters);
}

getter/setter 方法进行去重是通过类似 java.lang.String#getSignature:java.lang.reflect.Method 的方法签名来实现的,如果子类在实现过程中,参数、返回值使用了不同的类型(使用原类型的子类),则会导致方法签名不一致,同一字段就会对应不同的 getter/setter 方法,因此需要进行去重。

private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
Method winner = null;
// 属性名
String propName = entry.getKey();
for (Method candidate : entry.getValue()) {
if (winner == null) {
winner = candidate;
continue;
}
// 字段对应了多个get方法
Class<?> winnerType = winner.getReturnType();
Class<?> candidateType = candidate.getReturnType();
if (candidateType.equals(winnerType)) {
// 返回值类型相同
if (!boolean.class.equals(candidateType)) {
throw new ReflectionException(
"Illegal overloaded getter method with ambiguous type for property "
+ propName + " in class " + winner.getDeclaringClass()
+ ". This breaks the JavaBeans specification and can cause unpredictable results.");
} else if (candidate.getName().startsWith("is")) {
// 返回值为boolean的get方法可能有多个,如getIsSave和isSave,优先取is开头的
winner = candidate;
}
} else if (candidateType.isAssignableFrom(winnerType)) {
// OK getter type is descendant
// 可能会出现接口中的方法返回值是List,子类实现方法返回值是ArrayList,使用子类返回值方法
} else if (winnerType.isAssignableFrom(candidateType)) {
winner = candidate;
} else {
throw new ReflectionException(
"Illegal overloaded getter method with ambiguous type for property "
+ propName + " in class " + winner.getDeclaringClass()
+ ". This breaks the JavaBeans specification and can cause unpredictable results.");
}
}
// 记录字段名对应的get方法对象和返回值类型
addGetMethod(propName, winner);
}
}

去重的方式是使用更规范的方法以及使用子类的方法。在确认字段名对应的唯一 getter/setter 方法后,记录方法名对应的方法、参数、返回值等信息。MethodInvoker 可用于调用 Method 类的 invoke 方法来执行 getter/setter 方法(addSetMethods 记录映射关系的方式与 addGetMethods 大致相同)。

private void addFields(Class<?> clazz) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!setMethods.containsKey(field.getName())) {
// issue #379 - removed the check for final because JDK 1.5 allows
// modification of final fields through reflection (JSR-133). (JGB)
// pr #16 - final static can only be set by the classloader
int modifiers = field.getModifiers();
if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
// 非final的static变量,没有set方法,可以通过File对象做赋值操作
addSetField(field);
}
}
if (!getMethods.containsKey(field.getName())) {
addGetField(field);
}
}
if (clazz.getSuperclass() != null) {
// 递归查找父类
addFields(clazz.getSuperclass());
}
}

二. Invoker

Invoker 接口用于抽象设置和读取字段值的操作。对于有 getter/setter 方法的字段,通过 MethodInvoker 反射执行;对应其它字段,通过 GetFieldInvokerSetFieldInvoker 操作 Field 对象的 getter/setter 方法反射执行。

/**
* 用于抽象设置和读取字段值的操作
*
* {@link MethodInvoker} 反射执行getter/setter方法
* {@link GetFieldInvoker} {@link SetFieldInvoker} 反射执行Field对象的get/set方法
*
* @author Clinton Begin
*/
public interface Invoker { /**
* 通过反射设置或读取字段值
*
* @param target
* @param args
* @return
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException; /**
* 字段类型
*
* @return
*/
Class<?> getType();
}

三. TypeParameterResolver

针对 Java-Type 体系的多种实现,TypeParameterResolver 提供一系列方法来解析指定类中的字段、方法返回值或方法参数的类型。

Type 接口包含 4 个子接口和 1 个实现类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RaVMfK5-1588231783205)(…/images/35.png)]

  • Class:原始类型
  • ParameterizedType:泛型类型,如:List<String>
  • TypeVariable:泛型类型变量,如: List<T> 中的 T
  • GenericArrayType:组成元素是 ParameterizedTypeTypeVariable 的数组类型,如:List<String>[]T[]
  • WildcardType:通配符泛型类型变量,如:List<?> 中的 ?

TypeParameterResolver 分别提供 resolveFieldTyperesolveReturnTyperesolveParamTypes 方法用于解析字段类型、方法返回值类型和方法入参类型,这些方法均调用 resolveType 来获取类型信息:

/**
* 获取类型信息
*
* @param type 根据是否有泛型信息签名选择传入泛型类型或简单类型
* @param srcType 引用字段/方法的类(可能是子类,字段和方法在父类声明)
* @param declaringClass 字段/方法声明的类
* @return
*/
private static Type resolveType(Type type, Type srcType, Class<?> declaringClass) {
if (type instanceof TypeVariable) {
// 泛型类型变量,如:List<T> 中的 T
return resolveTypeVar((TypeVariable<?>) type, srcType, declaringClass);
} else if (type instanceof ParameterizedType) {
// 泛型类型,如:List<String>
return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass);
} else if (type instanceof GenericArrayType) {
// TypeVariable/ParameterizedType 数组类型
return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass);
} else {
// 原始类型,直接返回
return type;
}
}

resolveTypeVar 用于解析泛型类型变量参数类型,如果字段或方法在当前类中声明,则返回泛型类型的上界或 Object 类型;如果在父类中声明,则递归解析父类;父类也无法解析,则递归解析实现的接口。

private static Type resolveTypeVar(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass) {
Type result;
Class<?> clazz;
if (srcType instanceof Class) {
// 原始类型
clazz = (Class<?>) srcType;
} else if (srcType instanceof ParameterizedType) {
// 泛型类型,如 TestObj<String>
ParameterizedType parameterizedType = (ParameterizedType) srcType;
// 取原始类型TestObj
clazz = (Class<?>) parameterizedType.getRawType();
} else {
throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass());
} if (clazz == declaringClass) {
// 字段就是在当前引用类中声明的
Type[] bounds = typeVar.getBounds();
if (bounds.length > 0) {
// 返回泛型类型变量上界,如:T extends String,则返回String
return bounds[0];
}
// 没有上界返回Object
return Object.class;
} // 字段/方法在父类中声明,递归查找父类泛型
Type superclass = clazz.getGenericSuperclass();
result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass);
if (result != null) {
return result;
} // 递归泛型接口
Type[] superInterfaces = clazz.getGenericInterfaces();
for (Type superInterface : superInterfaces) {
result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface);
if (result != null) {
return result;
}
}
return Object.class;
}

通过调用 scanSuperTypes 实现递归解析:

private static Type scanSuperTypes(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass, Class<?> clazz, Type superclass) {
if (superclass instanceof ParameterizedType) {
// 父类是泛型类型
ParameterizedType parentAsType = (ParameterizedType) superclass;
Class<?> parentAsClass = (Class<?>) parentAsType.getRawType();
// 父类中的泛型类型变量集合
TypeVariable<?>[] parentTypeVars = parentAsClass.getTypeParameters();
if (srcType instanceof ParameterizedType) {
// 子类可能对父类泛型变量做过替换,使用替换后的类型
parentAsType = translateParentTypeVars((ParameterizedType) srcType, clazz, parentAsType);
}
if (declaringClass == parentAsClass) {
// 字段/方法在当前父类中声明
for (int i = 0; i < parentTypeVars.length; i++) {
if (typeVar == parentTypeVars[i]) {
// 使用变量对应位置的真正类型(可能已经被替换),如父类 A<T>,子类 B extends A<String>,则返回String
return parentAsType.getActualTypeArguments()[i];
}
}
}
// 字段/方法声明的类是当前父类的父类,继续递归
if (declaringClass.isAssignableFrom(parentAsClass)) {
return resolveTypeVar(typeVar, parentAsType, declaringClass);
}
} else if (superclass instanceof Class && declaringClass.isAssignableFrom((Class<?>) superclass)) {
// 父类是原始类型,继续递归父类
return resolveTypeVar(typeVar, superclass, declaringClass);
}
return null;
}

解析方法返回值和方法参数的逻辑大致与解析字段类型相同,MyBatis 源码的TypeParameterResolverTest 类提供了相关的测试用例。

四. ReflectorFactory

MyBatis 还提供 ReflectorFactory 接口用于创建 Reflector 容器,其默认实现为 DefaultReflectorFactory,其中可以使用 classCacheEnabled 属性来配置是否使用缓存。

public class DefaultReflectorFactory implements ReflectorFactory {

  /**
* 是否缓存Reflector类信息
*/
private boolean classCacheEnabled = true; /**
* Reflector缓存容器
*/
private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>(); public DefaultReflectorFactory() {
} @Override
public boolean isClassCacheEnabled() {
return classCacheEnabled;
} @Override
public void setClassCacheEnabled(boolean classCacheEnabled) {
this.classCacheEnabled = classCacheEnabled;
} /**
* 获取类的Reflector信息
*
* @param type
* @return
*/
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) {
//如果开启了缓存机制,则尝试从缓存中拿,如果没有则创建一个
return reflectorMap.computeIfAbsent(type, Reflector::new);
} else {
return new Reflector(type);
}
} }

五. ObjectFactory

ObjectFactory 接口是 MyBatis 对象创建工厂,其默认实现 DefaultObjectFactory 通过构造器反射创建对象,支持使用无参构造器和有参构造器。

六. Property 工具集

MyBatis 在映射文件定义 resultMap 支持如下形式:

<resultMap id="map" type="Order">
<result property="orders[0].items[0].name" column="col1"/>
<result property="orders[0].items[1].name" column="col2"/>
...
</resultMap>

orders[0].items[0].name 这样的表达式是由 PropertyTokenizer 解析的,其构造方法能够对表达式进行解析;同时还实现了 Iterator 接口,能够迭代解析表达式。

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
private String name;//名称
private final String indexedName;//带索引的名称
private String index;//索引
private final String children;//子名称 public PropertyTokenizer(String fullname) {
//找出第一个“.”的索引
int delim = fullname.indexOf('.');
if (delim > -1) {
//存在“.”
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
//不存在“.”
name = fullname;
children = null;
}
indexedName = name;
//第一个“[”的索引
delim = name.indexOf('[');
if (delim > -1) {
index = name.substring(delim + 1, name.length() - 1);
name = name.substring(0, delim);
}
}
...
}

PropertyNamer 可以根据 getter/setter 规范解析字段名称;PropertyCopier 则支持对有相同父类的对象,通过反射拷贝字段值。

七. MetaClass

MetaClass 类依赖 PropertyTokenizerReflector 查找表达式是否可以匹配 Java 对象中的字段,以及对应字段是否有 getter/setter 方法。

八. ObjectWrapper

ObjectWrapper是对象的包装器,提供最基本的get和set方法,不支持多级操作。ObejctWrapper 体系如下:

ObjectWrapper 的默认实现包括了对 MapCollection 和普通 JavaBean 的包装。MyBatis 还支持通过 ObjectWrapperFactory 接口对 ObejctWrapper 进行扩展,生成自定义的包装类。

例如赋值操作,BeanWrapper 的实现如下:

  @Override
public Object get(PropertyTokenizer prop) {
if (prop.getIndex() != null) {
// 当前表达式是集合,如:items[0],就需要获取items集合对象
Object collection = resolveCollection(prop, object);
return getCollectionValue(prop, collection);
} else {
return getBeanProperty(prop, object);
}
}
@Override
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
// 当前表达式是集合,如:items[0],就需要获取items集合对象
Object collection = resolveCollection(prop, object);
// 在集合的指定索引上赋值
setCollectionValue(prop, collection, value);
} else {
// 解析完成,通过Invoker接口做赋值操作
setBeanProperty(prop, object, value);
}
} protected Object resolveCollection(PropertyTokenizer prop, Object object) {
if ("".equals(prop.getName())) {
return object;
} else {
// 在对象信息中查到此字段对应的集合对象
return metaObject.getValue(prop.getName());
}
}

九. MetaObject

相对于 MetaClass 关注类信息,MetalObject 关注的是对象的信息,它在内部包装了MyBatis中五个核心的反射类。也是提供给外部使用的反射工具类,可以利用它可以读取或者修改对象的属性信息:

public class MetaObject {

  //原始对象
private final Object originalObject;
//对象的包装类对象
private final ObjectWrapper objectWrapper;
//对象的工厂
private final ObjectFactory objectFactory;
//对象包装器的工厂
private final ObjectWrapperFactory objectWrapperFactory;
//反射器工厂
private final ReflectorFactory reflectorFactory;

MetaObject 对对象的具体操作,就委托给真正的 ObjectWrapper 处理。

private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
this.reflectorFactory = reflectorFactory; if (object instanceof ObjectWrapper) {
//如果对象本身已经是ObjectWrapper型,则直接赋给objectWrapper
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) {
//如果有包装器,调用ObjectWrapperFactory.getWrapperFor
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
//如果是Map型,返回MapWrapper
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
//如果是Collection型,返回CollectionWrapper
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
//除此以外,返回BeanWrapper
this.objectWrapper = new BeanWrapper(this, object);
}
}

例如赋值操作:

/**
* 设置具体值,支持多级操作
* @param name 属性名称,类似于OGNL表达式,如果是多级结构,直接person[0].birthdate.year即可
* @param value
*/
public void setValue(String name, Object value) {
//将name解析为PropertyTokenizer,方便进行对象的赋值
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
//如果获取出来的设置空的MetaObject
if (value == null) {
// 如果值为空,则不会实例化子路径
return;
} else {
//如果value不为空,则委派给ObjectWrapper.instantiatePropertyValue创建子级对象并获取子级对象的MetaObject
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
//继续给子节点赋值(伪递归)
metaValue.setValue(prop.getChildren(), value);
} else {
//到了最后一层了,最终还是委派给ObjectWrapper.set
objectWrapper.set(prop, value);
}
}

十. 测试

由于MetaObject是MyBatis反射模块对外提供的反射工具类,所以我们对其进行测试:

public class Area {
private String name;
private Area child;
}
/**
* 使用MetaObject工具进行多级赋值测试
*/
@Test
public void testMetaObjectForChildBean() {
//初始化对象工厂
DefaultObjectFactory objectFactory = new DefaultObjectFactory();
//通过对象工程创建 Area实例
Area area = objectFactory.create(Area.class);
//创建包装类工厂
DefaultObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
//创建反射器工厂
DefaultReflectorFactory reflectorFactory = new DefaultReflectorFactory();
//创建MetaObject
MetaObject metaObject = MetaObject.forObject(area, objectFactory, objectWrapperFactory, reflectorFactory);
//赋值
metaObject.setValue("name","湖北省");
metaObject.setValue("child.name","武汉市");
metaObject.setValue("child.child.name","洪山区");
}

其他反射模块代码注释请移步至GitHubGitee

MyBatis反射模块源码分析的更多相关文章

  1. Springboot中mybatis执行逻辑源码分析

    Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...

  2. nginx健康检查模块源码分析

    nginx健康检查模块 本文所说的nginx健康检查模块是指nginx_upstream_check_module模块.nginx_upstream_check_module模块是Taobao定制的用 ...

  3. Spark Scheduler模块源码分析之TaskScheduler和SchedulerBackend

    本文是Scheduler模块源码分析的第二篇,第一篇Spark Scheduler模块源码分析之DAGScheduler主要分析了DAGScheduler.本文接下来结合Spark-1.6.0的源码继 ...

  4. Spark Scheduler模块源码分析之DAGScheduler

    本文主要结合Spark-1.6.0的源码,对Spark中任务调度模块的执行过程进行分析.Spark Application在遇到Action操作时才会真正的提交任务并进行计算.这时Spark会根据Ac ...

  5. Zepto事件模块源码分析

    Zepto事件模块源码分析 一.保存事件数据的handlers 我们知道js原生api中要移除事件,需要传入绑定时的回调函数.而Zepto则可以不传入回调函数,直接移除对应类型的所有事件.原因就在于Z ...

  6. Django(51)drf渲染模块源码分析

    前言 渲染模块的原理和解析模块是一样,drf默认的渲染有2种方式,一种是json格式,另一种是模板方式. 渲染模块源码入口 入口:APIView类中dispatch方法中的:self.response ...

  7. MyBatis 之 SqlSessionManager 源码分析

    MyBatis 的 4 个基本构成: SqlSessionFactoryBuilder(构造器): 根据配置信息或者代码来生成 SqlSessionFactory(工厂接口) SqlSessionFa ...

  8. Django(48)drf请求模块源码分析

    前言 APIView中的dispatch是整个请求生命过程的核心方法,包含了请求模块,权限验证,异常模块和响应模块,我们先来介绍请求模块 请求模块:request对象 源码入口 APIView类中di ...

  9. Django(49)drf解析模块源码分析

    前言 上一篇分析了请求模块的源码,如下: def initialize_request(self, request, *args, **kwargs): """ Retu ...

  10. Django(50)drf异常模块源码分析

    异常模块源码入口 APIView类中dispatch方法中的:response = self.handle_exception(exc) 源码分析 我们点击handle_exception跳转,查看该 ...

随机推荐

  1. oracle database recover database (下篇)

    1. recover database 恢复级别一共三个:recover database > recover tablespace > recover datafile ,最高级别 da ...

  2. # AssertionError: The `num_classes` (3) in Shared2FCBBoxHead of MMDataParallel does not matches the length of `CLASSES` 80) in CocoDataset

    我看很多人都遇到了这个问题,有很多解决了的.我就把这篇博文再完善一下,让大家对mmdetection使用得心应手. mmdetection训练自己的数据集时报错 ️ : # AssertionErro ...

  3. 力扣523(java&python)-连续的子数组和(中等)

    题目: 给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组: 子数组大小 至少为 2 ,且子数组元素总和为 k 的倍数.如果存在,返回 true ...

  4. Java 应用压测性能问题定位经验分享

    简介: 问题千千万,但只要修练了足够深厚的内功,形成一套属于自己的排查问题思路和打法,再加上一套支撑问题排查的工具,凭借已有的经验还有偶发到来的那一丝丝灵感,相信所有的问题都会迎刃而解. 作者:凡勇 ...

  5. 实时计算pv/uv Demo

    简介: 本文由阿里巴巴高级技术专家邓小勇(静行)分享,主要用 Demo 演示如何通过实时计算 Flink 实时计算pv/uv的场景. 本文由阿里巴巴高级技术专家邓小勇(静行)分享,主要用 Demo 演 ...

  6. 深入理解C++中的RVO

    前言 考虑存在这样一个类如HeavyObject,其拷贝赋值操作比较耗时,通常你在使用函数返回这个类的一个对象时会习惯使用哪一种方式?或者会根据具体场景选择某一种方式? // style 1 Heav ...

  7. 双引擎驱动Quick BI十亿数据0.3秒分析,首屏展示时间缩短30%

    ​简介:在规划中,Quick BI制定了产品竞争力建设的三大方向,包括Quick(快)能力.移动端能力和集成能力.针对其中的产品"报表查看打开慢""报表开发数据同步慢&q ...

  8. Snowflake如日中天是否代表Hadoop已死?大数据体系到底是什么?

    ​简介: 本文作者关涛是大数据系统领域的资深专家,在微软(互联网/Azure云事业群)和阿里巴巴(阿里云)经历了大数据发展20年过程中的后15年.本文试从系统架构的角度,就大数据架构热点,每条技术线的 ...

  9. [FAQ] eggjs/egg 自定义 favicon.ico

      从  egg 项目配置里找到这一段代码: https://github.com/eggjs/egg/blob/master/config/config.default.js#L205C21-L20 ...

  10. IIncrementalGenerator 增量 Source Generator 生成代码应用 将构建时间写入源代码

    本文将和大家介绍一个 IIncrementalGenerator 增量 Source Generator 生成代码技术的应用例子,将当前的构建时间写入到代码里面.这个功能可以比较方便实现某些功能的开关 ...