Spring 容器原始 Bean 是如何创建的?

news2024/12/24 12:37:13

以下内容基于 Spring6.0.4。

这个话题其实非常庞大,我本来想从 getBean 方法讲起,但一想这样讲完估计很多小伙伴就懵了,所以我们还是一步一步来,今天我主要是想和小伙伴们讲讲 Spring 容器创建 Bean 最最核心的 createBeanInstance 方法,这个方法专门用来创建一个原始 Bean 实例。

松哥这里就以 Spring 源码中方法的执行顺序为例来和小伙伴们分享。

1. doCreateBean

AbstractAutowireCapableBeanFactory#doCreateBean 就是 Bean 的创建方法,但是 Bean 的创建涉及到的步骤非常多,包括各种需要调用的前置后置处理器方法,今天我主要是想和大家聊聊单纯的创建 Bean 的过程,其他方法咱们后面文章继续。

在 doCreateBean 方法中,有如下一行方法调用:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
		throws BeanCreationException {
	// Instantiate the bean.
	BeanWrapper instanceWrapper = null;
	if (mbd.isSingleton()) {
		instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
	}
	if (instanceWrapper == null) {
		instanceWrapper = createBeanInstance(beanName, mbd, args);
	}
	Object bean = instanceWrapper.getWrappedInstance();
    //...
	return exposedObject;
}

createBeanInstance 这个方法就是真正的根据我们的配置去创建一个 Bean 了。

2. createBeanInstance

先来看源码:

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
	// Make sure bean class is actually resolved at this point.
	Class<?> beanClass = resolveBeanClass(mbd, beanName);
	if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
		throw new BeanCreationException(mbd.getResourceDescription(), beanName,
				"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
	}
	Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
	if (instanceSupplier != null) {
		return obtainFromSupplier(instanceSupplier, beanName);
	}
	if (mbd.getFactoryMethodName() != null) {
		return instantiateUsingFactoryMethod(beanName, mbd, args);
	}
	// Shortcut when re-creating the same bean...
	boolean resolved = false;
	boolean autowireNecessary = false;
	if (args == null) {
		synchronized (mbd.constructorArgumentLock) {
			if (mbd.resolvedConstructorOrFactoryMethod != null) {
				resolved = true;
				autowireNecessary = mbd.constructorArgumentsResolved;
			}
		}
	}
	if (resolved) {
		if (autowireNecessary) {
			return autowireConstructor(beanName, mbd, null, null);
		}
		else {
			return instantiateBean(beanName, mbd);
		}
	}
	// Candidate constructors for autowiring?
	Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
	if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
			mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
		return autowireConstructor(beanName, mbd, ctors, args);
	}
	// Preferred constructors for default construction?
	ctors = mbd.getPreferredConstructors();
	if (ctors != null) {
		return autowireConstructor(beanName, mbd, ctors, null);
	}
	// No special handling: simply use no-arg constructor.
	return instantiateBean(beanName, mbd);
}

这里就是核心的 Bean 的创建方法了,因此这个方法我来和大家详细分析一下。

2.1 resolveBeanClass

这个方法是用来解析出来当前的 beanClass 对象,它的核心逻辑就是根据我们在 XML 文件中配置的类的全路径,通过反射加载出来这个 Class

@Nullable
protected Class<?> resolveBeanClass(RootBeanDefinition mbd, String beanName, Class<?>... typesToMatch)
		throws CannotLoadBeanClassException {
    if (mbd.hasBeanClass()) {
        return mbd.getBeanClass();
    }
    return doResolveBeanClass(mbd, typesToMatch);
}

首先会调用 mbd.hasBeanClass() 方法去判断是否已经通过反射加载出来 beanClass 了,如果加载出来了就直接返回,没有加载的话,就继续执行下面的 doResolveBeanClass 去加载。

什么时候会走 if 这条线呢?松哥举一个例子,如果我们设置某一个 Bean 的 Scope 是 prototype 的话,那么当第二次获取该 Bean 的实例的时候,就会走 if 这条线。

@Nullable
private Class<?> doResolveBeanClass(RootBeanDefinition mbd, Class<?>... typesToMatch)
		throws ClassNotFoundException {
	//...
	String className = mbd.getBeanClassName();
	if (className != null) {
		Object evaluated = evaluateBeanDefinitionString(className, mbd);
		if (!className.equals(evaluated)) {
			// A dynamically resolved expression, supported as of 4.2...
			if (evaluated instanceof Class<?> clazz) {
				return clazz;
			}
			else if (evaluated instanceof String str) {
				className = str;
				freshResolve = true;
			}
			else {
				throw new IllegalStateException("Invalid class name expression result: " + evaluated);
			}
		}
		if (freshResolve) {
			// When resolving against a temporary class loader, exit early in order
			// to avoid storing the resolved Class in the bean definition.
			if (dynamicLoader != null) {
                return dynamicLoader.loadClass(className);
			}
			return ClassUtils.forName(className, dynamicLoader);
		}
	}
	// Resolve regularly, caching the result in the BeanDefinition...
	return mbd.resolveBeanClass(beanClassLoader);
}

按理说,根据我们配置的类的全路径加载出来一个 Class 应该是非常容易的,直接 Class.forName 就可以了。

但是!!!

如果对 Spring 用法比较熟悉的小伙伴就知道,配置 Class 全路径的时候,我们不仅可以像下面这样老老实实配置:

<bean class="org.javaboy.bean.Book"/>

我们甚至可以使用 SpEL 来配置 Bean 名称,例如我有如下类:

public class BeanNameUtils {
    public String getName() {
        return "org.javaboy.bean.User";
    }
}

这里有一个 getName 方法,这个方法返回的是一个类的全路径,现在我们在 XML 文件中可以这样配置:

<bean class="org.javaboy.bean.BeanNameUtils" id="beanNameUtils"/>
<bean class="#{beanNameUtils.name}" id="user"/>

在 XML 的 class 属性中,我们可以直接使用 SpEL 去引用一个方法的执行,用该方法的返回值作为 class 的值。

了解了 Spring 中的这个玩法,再去看上面的源码就很好懂了:

  • 首先调用 mbd.getBeanClassName(); 去获取到类路径。
  • 接下来调用 evaluateBeanDefinitionString 方法进行 SpEL 运算,这个运算的目的是为了解析 className 中的 SpEL 表达式,当然,一般情况下 className 就是一个普通的字符串,不是 SpEL 表达式,那么解析完成之后就还是原本的字符串。如果是 className 是一个 SpEL,那么合法的解析结果分为两种:
    • 首先就是解析之后拿到了一个 Class,那这个就是我们想要的结果,直接返回即可。
    • 要么就是解析出来是一个字符串,松哥上面举的例子就是这种情况,那么就把这个字符串赋值给 className,并且将 freshResolve 属性设置为 true,然后在接下来的 if 分支中去加载 Class。

当然,上面这些都是处理特殊情况,一般我们配置的普通 Bean,都是直接走最后一句 mbd.resolveBeanClass(beanClassLoader),这个方法的逻辑其实很好懂,我把代码贴出来小伙伴们来瞅一瞅:

@Nullable
public Class<?> resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException {
	String className = getBeanClassName();
	if (className == null) {
		return null;
	}
	Class<?> resolvedClass = ClassUtils.forName(className, classLoader);
	this.beanClass = resolvedClass;
	return resolvedClass;
}

这个方法就相当直白了,根据 className 加载出来 Class 对象,然后给 beanClass 属性也设置上值,这就和一开始的 if (mbd.hasBeanClass()) 对应上了。

好了,到此,我们总算是根据 className 拿到 Class 对象了。

2.2 Supplier 和 factory-method

好了,回到一开始的源码中,接下来该执行如下两行代码了:

Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {
	return obtainFromSupplier(instanceSupplier, beanName);
}
if (mbd.getFactoryMethodName() != null) {
	return instantiateUsingFactoryMethod(beanName, mbd, args);
}

这两个松哥在前面的文章中和小伙伴们已经讲过了(Spring5 中更优雅的第三方 Bean 注入):前面的 obtainFromSupplier 方法是 Spring5 开始推出来的 Supplier,通过回调的方式去获取一个对象;第二个方法 instantiateUsingFactoryMethod 则是通过配置的 factory-method 来获取到一个 Bean 实例。

对这两个方法不熟悉的小伙伴可以参考前面的文章:Spring5 中更优雅的第三方 Bean 注入。

2.3 re-create 逻辑

继续回到一开始的源码中,接下来是一段 re-create 的处理逻辑,如下:

boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {
	synchronized (mbd.constructorArgumentLock) {
		if (mbd.resolvedConstructorOrFactoryMethod != null) {
			resolved = true;
			autowireNecessary = mbd.constructorArgumentsResolved;
		}
	}
}
if (resolved) {
	if (autowireNecessary) {
		return autowireConstructor(beanName, mbd, null, null);
	}
	else {
		return instantiateBean(beanName, mbd);
	}
}

根据前面的介绍,我们现在已经获取到 Class 对象了,接下来直接调用相应的构造方法就可以获取到 Bean 实例了。但是这个 Class 对象可能存在多个构造方法,所以还需要一堆流程去确定到底调用哪个构造方法。

所以这里会先去判断 resolvedConstructorOrFactoryMethod 是否不为空,不为空的话,说明这个 Bean 之前已经创建过了,该用什么方法创建等等问题都已经确定了,所以这次就不用重新再去确定了(resolved = true)。另一方面,autowireNecessary 表示构造方法的参数是否已经处理好了,这个属性为 true 则表示构造方法的参数已经处理好了,那么就可以调用 autowireConstructor 方法去创建一个 Bean 出来,否则调用 instantiateBean 方法初始化 Bean。

这里涉及到的 autowireConstructor 和 instantiateBean 方法我们先不细说了,因为在后面还会再次涉及到。

2.4 构造器注入

继续回到一开始的源码中,接下来就是针对各种处理器的预处理了:

Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
		mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
	return autowireConstructor(beanName, mbd, ctors, args);
}

先来看 determineConstructorsFromBeanPostProcessors 方法,这个方法主要是考虑到你可能提供了 SmartInstantiationAwareBeanPostProcessor,松哥在前面的文章中和大家专门讲过 BeanPostProcessor(BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?),这里的 SmartInstantiationAwareBeanPostProcessor 算是 BeanPostProcessor 的一种,也是 Bean 的一种增强器。SmartInstantiationAwareBeanPostProcessor 中有一个 determineCandidateConstructors 方法,这个方法返回某一个 Bean 的构造方法,将来可以通过这个构造方法初始化某一个 Bean。

我给大家举一个简单例子,假设我有如下类:

public class User {
    private String username;
    private String address;

    public User() {
        System.out.println("=====no args=====");
    }

    public User(ObjectProvider<String> username) {
        System.out.println("args==username");
        this.username = username.getIfAvailable();
    }

    //省略 getter/setter/toString
}

现在我在 Spring 容器中注册这个对象:

<bean class="org.javaboy.bean.User" id="user">
</bean>

按照我们已有的知识,这个将来会调用 User 的无参构造方法去完成 User 对象的初始化。

但是现在,假设我添加如下一个处理器:

public class MySmartInstantiationAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {
    @Override
    public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException {
        if ("user".equals(beanName)) {
            Constructor<?> constructor = null;
            try {
                constructor = beanClass.getConstructor(ObjectProvider.class);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
            return new Constructor[]{constructor};
        }
        return SmartInstantiationAwareBeanPostProcessor.super.determineCandidateConstructors(beanClass, beanName);
    }
}

在 determineCandidateConstructors 方法中,返回一个有参构造方法,那么将来 Spring 容器会通过这里返回的有参构造方法去创建 User 对象,而不是通过无参构造方法去创建 User 对象。

最后,将这个处理器注册到 Spring 容器:

<bean class="org.javaboy.bean.MySmartInstantiationAwareBeanPostProcessor"/>

现在,当我们启动 Spring 容器的时候,User 就是通过有参构造方法初始化的,而不是无参构造方法。之所以会这样,就是因为本小节一开始提到的源码 determineConstructorsFromBeanPostProcessors,这个方法就是去查看有无 SmartInstantiationAwareBeanPostProcessor,如果有,就调用对应的方法找到处理器并返回。

这个弄懂之后,if 中其他几种情况就好理解了,mbd.getResolvedAutowireMode() 是查看当前对象的注入方式,这个一般是在 XML 中配置的,不过日常开发中我们一般不会配置这个属性,如果需要配置,方式如下:

<bean class="org.javaboy.bean.User" id="user" autowire="constructor">
</bean>

如果添加了 autowire="constructor" 就表示要通过构造方法进行注入,那么这里也会进入到 if 中。

if 里边剩下的几个条件都好说,就是看是否有配置构造方法参数,如果配置了,那么也直接调用相应的构造方法就行了。

这里最终执行的是 autowireConstructor 方法,这个方法比较长,我就不贴出来了,和大家说一说它的思路:

  1. 首先把能获取到的构造方法都拿出来,如果构造方法只有一个,且目前也没有任何和构造方法有关的参数,那就直接用这个构造方法就行了。
  2. 如果第一步不能解决问题,接下来就遍历所有的构造方法,并且和已有的参数进行参数数量和类型比对,找到合适的构造方法并调用。

2.5 PreferredConstructors

继续回到一开始的源码中,接下来是这样了:

ctors = mbd.getPreferredConstructors();
if (ctors != null) {
	return autowireConstructor(beanName, mbd, ctors, null);
}

这块代码看字面好理解,就是获取到主构造方法,不过这个是针对 Kotlin 的,跟我们 Java 无关,我就不啰嗦了。

2.6 instantiateBean

最后就是 instantiateBean 方法了,这个方法就比较简单了,我把代码贴一下小伙伴们应该自己都能看明白:

protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) {
	try {
		Object beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, this);
		BeanWrapper bw = new BeanWrapperImpl(beanInstance);
		initBeanWrapper(bw);
		return bw;
	}
	catch (Throwable ex) {
		throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex);
	}
}
@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
	// Don't override the class with CGLIB if no overrides.
	if (!bd.hasMethodOverrides()) {
		Constructor<?> constructorToUse;
		synchronized (bd.constructorArgumentLock) {
			constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
			if (constructorToUse == null) {
				final Class<?> clazz = bd.getBeanClass();
				if (clazz.isInterface()) {
					throw new BeanInstantiationException(clazz, "Specified class is an interface");
				}
				try {
					constructorToUse = clazz.getDeclaredConstructor();
					bd.resolvedConstructorOrFactoryMethod = constructorToUse;
				}
				catch (Throwable ex) {
					throw new BeanInstantiationException(clazz, "No default constructor found", ex);
				}
			}
		}
		return BeanUtils.instantiateClass(constructorToUse);
	}
	else {
		// Must generate CGLIB subclass.
		return instantiateWithMethodInjection(bd, beanName, owner);
	}
}

从上面小伙伴么可以看到,本质上其实就是调用了 constructorToUse = clazz.getDeclaredConstructor();,获取到一个公开的无参构造方法,然后据此创建一个 Bean 实例出来。

3. 小结

好了,这就是 Spring 容器中 Bean 的创建过程,我这里单纯和小伙伴们分享了原始 Bean 的创建这一个步骤,这块内容其实非常庞杂,以后有空我会再和小伙伴们分享。

最后,给上面分析的方法生成了一个时序图,小伙伴们作为参考。

其实看 Spring 源码,松哥最大的感悟就是小伙伴们一定要了解 Spring 的各种用法,在此基础之上,源码就很好懂,如果你只会 Spring 一些基本用法,那么源码一定是看得云里雾里的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/827550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

笔试编程题常用框架/方法

目录 考核方式 ACM模式 JavaScript(V8) JavaScript(Node) 数组 折半 / 二分查找 螺旋矩阵* 前缀和-区间求和 差分数组-区间增减 滑动窗口-子串 链表 双指针&#xff08;快慢指针&#xff09; 有序数组的平方 删除/覆盖数组元素 最小长度的子数组 三数之和abcta…

【RTT驱动框架分析05】-spi驱动框架分析

spi 1.应用层的spi操作 spi 消息结构 struct rt_spi_message {const void *send_buf;//发送数据的缓存void *recv_buf;//接收数据的缓存rt_size_t length;//数据长度struct rt_spi_message *next;//指向下一个消息结构unsigned cs_take : 1;//是否执行获取csunsigned cs_…

SQL-每日一题【1174. 即时食物配送 II】

题目 配送表: Delivery 如果顾客期望的配送日期和下单日期相同&#xff0c;则该订单称为 「即时订单」&#xff0c;否则称为「计划订单」。 「首次订单」是顾客最早创建的订单。我们保证一个顾客只会有一个「首次订单」。 写一条 SQL 查询语句获取即时订单在所有用户的首次订…

零碎小知识点汇总——记录工作中遇到的问题——基础积累

1.npm install安装包时&#xff0c;常用的-S -D有什么区别&#xff1f; 参考链接&#xff1a;https://blog.csdn.net/sunyctf/article/details/127667543 主要的区别就是依赖配置写入package.json文件的位置不同而已 npm install有一个别名&#xff1a;npm i -S:写入dependen…

抢夺本地生活万亿蛋糕:“抖音美团们”的攻防战

有人就有市场&#xff0c;有市场就有竞争。面对本地生活这一大“富矿”&#xff0c;互联网大厂们正在开启一场争夺战。 前有抖音、小红书在团购业务上火速推进&#xff0c;近日&#xff0c;随着拼多多正式上线本地生活入口&#xff0c;这个领域的玩家越来越多。 艾瑞咨询数据…

Linux中的特殊进程(孤儿进程、僵尸进程、守护进程)

一、孤儿进程 1&#xff09;父进程退出&#xff0c;子进程不退出&#xff0c;此时子进程被1号&#xff08;init&#xff09;进程收养&#xff0c;变成孤儿进程。 2&#xff09;孤儿进程会脱离终端控制&#xff0c;且运行在后端&#xff0c;不能用ctrlc杀死后端进程&#xff0c;…

Linux ALSA音频工具aplay、arecord、amixer的使用方法

ALSA 是Advanced Linux Sound Architecture的缩写&#xff0c;先进的Linux音频架构&#xff0c;为Linux操作系统提供音频和MIDI功能。 aplay命令 aplay是播放命令。 rootimx6ul7d:~# aplay -h Usage: aplay [OPTION]... [FILE]...-h, --help help--version …

MyBatis-Plus 和达梦数据库实现高效数据持久化

一、添加依赖 首先&#xff0c;我们需要在项目的 pom.xml 文件中添加 MyBatis-Plus 和达梦数据库的依赖&#xff1a; <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifac…

pyspark_自定义udf_解析json列【附代码】

pyspark_自定义udf_解析json列【附代码】 一、背景&#xff1a;二、调研方案&#xff1a;三、利用Pyspark udf自定义函数实现大数据并行计算整体流程案例代码运行结果&#xff1a;案例代码&#xff1a;代码地址&#xff1a;代码 一、背景&#xff1a; 车联网数据有很多车的时…

【MATLAB第63期】基于MATLAB的改进敏感性分析方法IPCC,拥挤距离与皮尔逊系数法结合实现回归与分类预测

【MATLAB第63期】基于MATLAB的改进敏感性分析方法IPCC&#xff0c;拥挤距离与皮尔逊系数法结合实现回归与分类预测 思路 考虑拥挤距离指标与PCC皮尔逊相关系数法相结合&#xff0c;对回归或分类数据进行降维&#xff0c;通过SVM支持向量机交叉验证得到平均指标&#xff0c;来…

《华为认证》SR MPLS BE配置

实验需求&#xff1a;在PE1和PE3之间建立mp-bgp邻居传递CE1和CE2的私网路由&#xff0c;并且使用SR mpls BE的方式传递私网流量 实验步骤 步骤1&#xff1a;配置设备接口ip地址以及AS 100内的igp协议&#xff08;略&#xff09; 步骤2&#xff1a;AS 100内的设备开启mpls &am…

Vue2 第十四节 scoped样式和本地存储

1.scoped样式 2.本地存储 一.scoped样式 ① 作用&#xff1a;让样式在局部生效&#xff0c;防止冲突 ② 写法&#xff1a;<style scoped> ③ 代码示例&#xff1a; <style scoped> .demo {background-color: lightblue; } </style> ④ scoped样式一般…

【redis】创建集群

这里介绍的是创建redis集群的方式&#xff0c;一种是通过create-cluster配置文件创建部署在一个物理机上的伪集群&#xff0c;一种是先在不同物理机启动单体redis&#xff0c;然后通过命令行使这些redis加入集群的方式。 一&#xff0c;通过配置文件创建伪集群 进入redis源码…

分布式应用:ELK企业级日志分析系统

目录 一、理论 1.ELK 2.ELK场景 3.完整日志系统基本特征 4.ELK 的工作原理 5.ELK集群准备 6.Elasticsearch部署&#xff08;在Node1、Node2节点上操作&#xff09; 7.Logstash 部署&#xff08;在 Apache 节点上操作&#xff09; 8.Kiabana 部署&#xff08;在 Node1 节点…

C++设计模式之适配器设计模式

文章目录 C适配器设计模式什么是适配器设计模式该模式有什么优缺点优点缺点 如何使用 C适配器设计模式 什么是适配器设计模式 适配器设计模式是一种行为型设计模式&#xff0c;它允许你将两个不兼容的接口组合在一起&#xff0c;使它们能够协同工作。 该模式有什么优缺点 优…

elementUI全屏loading的使用(白屏的解决方案)

官网中有使用方法&#xff0c;但是我实际上手之后会出现白屏&#xff0c;解决办法如下&#xff1a; <el-button type"text" size"small" click"delRow(scope)"> 删除</el-button>loading: false, // loading 动画loadingInstance…

玩转Java IO流:轻松读写文件、网络

申明&#xff1a;本人于公众号Java筑基期&#xff0c;CSDN先后发当前文章&#xff0c;标明原创&#xff0c;转载二次发文请注明转载公众号&#xff0c;另外请不要再标原创 &#xff0c;注意违规 字符流和字节流 在Java中&#xff0c;IO&#xff08;输入输出&#xff09;操作涉…

TCP三次握手与四次断开

TCP三次握手机制 三次握手是指建立一个TCP连接时&#xff0c;需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。 1、客户端发送建立TCP连接的请求报文&#xff0c;其…

IDEA设置快捷操作

步骤&#xff1a; 1、 2、 3、 4、 然后直接用就可以啦 常用的接口测试模板&#xff1a; given().contentType(JSON).body($requestBody$).log().all().when().post($path$).then().log().all().statusCode(200);given().contentType(ContentType.JSON).body().log().all().w…

使用低代码平台提高生产力

一、前言 低代码平台的概念很火爆&#xff0c;产品也是鱼龙混杂。 对于开发人员来说&#xff0c;在使用绝大部分低代码平台的时候都会遇到一个致命的问题&#xff1a;我在上面做的项目无法得到源码&#xff0c;完全黑盒。一旦我的需求平台满足不了&#xff0c;那就是无解。 与其…