Spring系列14:IoC容器的扩展点
Spring系列14:IoC容器的扩展点
回顾
知识需要成体系地学习,本系列文章前后有关联,建议按照顺序阅读。上一篇我们详细介绍了Spring Bean的生命周期和丰富的扩展点,没有阅读的强烈建议先阅读。本篇来详细讲讲容器提供的扩展点,完整的生命周期图镇楼。
本文内容
- 详解BeanPostProcessor
- 详解
BeanFactoryPostProcessor
- 详解
FactoryBean
详解BeanPostProcessor
作用和定义
常规 BeanPostProcessor 的作用是提供自定义的实例化逻辑、初始化逻辑、依赖关系解析逻辑等,对bean进行增强。主要的作用阶段是初始阶段前后。该接口定义了2接口,分别是前置增强和后置增强。 Spring AOP 功能主要是通过 BeanPostProcessor 实现的。
public interface BeanPostProcessor {
// 在任何 bean 初始化回调(如 InitializingBean 的 afterPropertiesSet 或自定义 init 方法)之前 // 将此 BeanPostProcessor 应用于给定的新 bean 实例。 bean 将已填充属性值。返回的 bean 实例可能是 // 原始的包装器。默认实现按原样返回给定的 bean
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// 在任何 bean 初始化回调(如 InitializingBean 的 afterPropertiesSet 或自定义 init 方法)之 // 后,将此 BeanPostProcessor 应用于给定的新 bean 实例。 bean 将已填充属性值。返回的 bean 实例可 /// 能是原始的包装器。默认实现按原样返回给定的 bean
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
自定义BeanPostProcessor
自定义一个 BeanPostProcessor 实现,该实现调用每个 bean 的 toString() 方法打印内容输出到控制台。
类定义如下
/**
* @author zfd
* @version v1.0
* @date 2022/1/20 11:21
* @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
*/
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 返回原始bean
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println( bean + " 名称是: " + beanName);
return bean;
}
}
@Component("xxxBeanOne")
public class BeanOne {
}
@Configuration
@ComponentScan
public class AppConfig {
}
测试程序及结果
@org.junit.Test
public void test1() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
BeanOne bean = context.getBean(BeanOne.class);
System.out.println(bean);
context.close();
}
// 结果
com.crab.spring.ioc.demo12.AppConfig$$EnhancerBySpringCGLIB$$da23c216@4445629 名称是: appConfig
com.crab.spring.ioc.demo12.BeanOne@45b9a632 名称是: xxxBeanOne
com.crab.spring.ioc.demo12.BeanOne@45b9a632
从结果看,MyBeanPostProcessor#postProcessAfterInitialization 输出了容器内初始化bean的名称。
使用 @Order 控制 BeanPostProcessor 执行顺序
实际程序程序中肯定存在多个 BeanPostProcessor 通过 @Order 来指定顺序。
增加 MyBeanPostProcessor2 指定顺序是 -2
@Component
@Order(-2)
public class MyBeanPostProcessor2 implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 返回原始bean
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("MyBeanPostProcessor2 输出:" + bean + " 名称是: " + beanName);
return bean;
}
}
同样的测试程序,观察下结果
com.crab.spring.ioc.demo12.AppConfig$$EnhancerBySpringCGLIB$$6fb51f6@54c562f7 名称是: appConfig
MyBeanPostProcessor2 输出:com.crab.spring.ioc.demo12.AppConfig$$EnhancerBySpringCGLIB$$6fb51f6@54c562f7 名称是: appConfig
com.crab.spring.ioc.demo12.BeanOne@318ba8c8 名称是: xxxBeanOne
MyBeanPostProcessor2 输出:com.crab.spring.ioc.demo12.BeanOne@318ba8c8 名称是: xxxBeanOne
com.crab.spring.ioc.demo12.BeanOne@318ba8c8
MyBeanPostProcessor2 在 MyBeanPostProcessor 之前执行。
通过源码了解下Spring是如何将多个 BeanPostProcessor 排序的,对应 PostProcessorRegistrationDelegate#registerBeanPostProcessors()。
可以看出顺序是: PriorityOrdered > Ordered > 其它常规的。
public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
// Separate between BeanPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}
// First, register the BeanPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);
// Next, register the BeanPostProcessors that implement Ordered.
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);
// Now, register all regular BeanPostProcessors.
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);
// Finally, re-register all internal BeanPostProcessors.
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);
}
AutowiredAnnotationBeanPostProcessor
分析
将回调接口或注释与自定义BeanPostProcessor
实现结合使用是扩展 Spring IoC 容器的常用方法。`AutowiredAnnotationBeanPostProcesso由 Spring 提供的实现,并自动注入依赖到带注解的字段、setter 方法和任意配置方法。
源码头说明: 自动装配带注释的字段、设置方法和任意配置方法的 BeanPostProcessor 实现。此类要注入的成员是通过注解检测的:默认情况下,Spring 的@Autowired 和@Value 注解。还支持 JSR-330 的 @Inject 注解(如果可用)作为 Spring 自己的 @Autowired 的直接替代品。
来看一下源码里面的关键方法
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
// 关键方法1: 默认构造方法 默认值支持注解 @Autowired @Value @Inject
public AutowiredAnnotationBeanPostProcessor() {
this.autowiredAnnotationTypes.add(Autowired.class);
this.autowiredAnnotationTypes.add(Value.class);
try {
this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
}
catch (ClassNotFoundException ex) {
// JSR-330 API not available - simply skip.
}
}
// 关键方法2: 将支持的注解标准的内容合并到 BeanDefinition 中为后续设置属性值做准备
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}
// 关键方法3: 自动注入依赖属性
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}
}
既然简单提到源码了,深入一下,结合Spring的生命周期,看下对应的生效位置
关键方法2: 将支持的注解标准的内容合并到 BeanDefinition,实例化之后属性填充之前。
关键方法3: 自动注入依赖属性,发生在属性赋值阶段
类似的 BeanPostProcessor
汇总下,可以对应看下源码
名称 | 作用 |
---|---|
ConfigurationClassPostProcessor | 处理@Configuration |
CommonAnnotationBeanPostProcessor | 处理JSR 250 中的 @PostConstruct,@PreDestroy和@Resource |
AutowiredAnnotationBeanPostProcessor | 处理@Autowired 、@Value 、 JSR-330 的 @Inject |
PersistenceAnnotationBeanPostProcessor | 处理 PersistenceUnit 和 PersistenceContext 注解的 BeanPostProcessor,用于注入对应的 JPA 资源 EntityManagerFactory 和 EntityManager。 |
详解BeanFactoryPostProcessor
BeanPostProcessor
是对bean实例进行增强,类似地,BeanFactoryPostProcessor 对 bean 配置元数据也就是 BeanDefinition 进行操作。Spring IoC 容器允许BeanFactoryPostProcessor
读取配置元数据并可能在容器实例化任何 bean之前BeanFactoryPostProcessor
更改它,当然不包括 BeanFactoryPostProcessor
实例。Spring 包括许多预定义的 BeanFactoryPostProcessor ,例如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。
多个 BeanFactoryPostProcessor
的属性可以通过 @Ordered 来控制。
PropertySourcesPlaceholderConfigurer 分析
可以使用 PropertySourcesPlaceholderConfigurer 通过使用标准 Java 属性格式将 bean 定义中的属性值外部化到一个单独的文件中。针对当前 Spring 环境及其一组 PropertySource 解析 bean 定义属性值和 @Value 注释中的 ${...} 占位符。
PropertySourcesPlaceholderConfigurer 不仅在指定的属性文件中寻找属性。默认情况下,如果它不能在指定的属性文件中找到属性,它将检查Spring Environment属性和常规Java System属性。
来看一个实际的场景: 通常的我们的数据库的连接信息是配置在配置文件中的而不是固定写在程序中。
数据库配置文件 jdbc.properties
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
通过 @Value 注入到我们数据库配置类中
@Configuration("myDataSource")
public class MyDataSource {
@Value("${jdbc.driverClassName}")
private String driverClassName;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
// ...
}
看下配置类,注入一个 PropertySourcesPlaceholderConfigurer
@Configuration
@ComponentScan
// @PropertySource("classpath:demo12/jdbc.properties")
public class AppConfig2 {
// 自定义一个 PropertySourcesPlaceholderConfigurer
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer =
new PropertySourcesPlaceholderConfigurer();
// 配置配置文件的路径
configurer.setLocation(new ClassPathResource("demo12/jdbc.properties"));
return configurer;
}
}
等价于下面的xml配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer" >
<property name="locations" value="classpath:demo12/jdbc.properties"/>
</bean>
<bean class="com.crab.spring.ioc.demo12.MyDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
观察下输出结果
MyDataSource{driverClassName='org.hsqldb.jdbcDriver', url='jdbc:hsqldb:hsql://production:9002', username='sa', password='root'}
配置在 MyDataSource 的外部属性值已经成功注入了。
过多关于 @Value
和外部配置属性的注入可以看下之前的文章 :Spring系列12: @Value
@Resource
@PostConstruct
@PreDestroy
详解。
扩展: 思考下Springboot 可以使用 ${xxx.xxx} 引用外部的 application.yml或是application.properties,是不是注入了一个自定义的 PropertySourcesPlaceholderConfigurer?
PropertyOverrideConfigurer
详解
类似于 PropertySourcesPlaceholderConfigurer , PropertyOverrideConfigurer 用于覆盖bean定义中的属性值。如果覆盖属性文件没有特定 bean 属性值,则使用默认上下文定义的。覆盖属性文件的格式如下:
# bean定义名称.属性名称=覆盖的属性值
beanName.property=value
来一个案例,在上面的案例的基础上,myDataSource.username替换为sa-override,
myDataSource.password替换为root-override。
配置文件如下
myDataSource.username=sa-override
myDataSource.password=root-override
配置类注入一个 PropertySourcesPlaceholderConfigurer
@Configuration
@ComponentScan
// @PropertySource("classpath:demo12/jdbc.properties")
public class AppConfig2 {
// 自定义一个 PropertySourcesPlaceholderConfigurer
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer =
new PropertySourcesPlaceholderConfigurer();
configurer.setLocation(new ClassPathResource("demo12/jdbc.properties"));
return configurer;
}
// 注入一个 PropertyOverrideConfigurer
@Bean
public static PropertyOverrideConfigurer propertyOverrideConfigurer() {
PropertyOverrideConfigurer overrideConfigurer = new PropertyOverrideConfigurer();
overrideConfigurer.setLocation(new ClassPathResource("demo12/jdbc-override.properties"));
return overrideConfigurer;
}
}
观察下输出结果
MyDataSource{driverClassName='org.hsqldb.jdbcDriver', url='jdbc:hsqldb:hsql://production:9002', username='sa-override', password='root-override'}
username 和 password 已经成功被替换了。
详解FactoryBean
FactoryBean 接口是Spring IoC容器实例化逻辑的可插点。如果您有复杂的初始化代码,用Java更好地表达,而不是(可能)冗长的XML,那么您可以创建自己的FactoryBean,在该类中编写复杂的初始化代码,然后将定制的FactoryBean 插入到容器中。
FactoryBean 概念和接口在 Spring 框架中的许多地方都使用。 Spring 本身附带了 50 多个 FactoryBean 接口的实现。
FactoryBean 接口的定义如下
public interface FactoryBean<T> {
// 返回此工厂创建的对象的实例。该实例可能会被共享,具体取决于该工厂是返回单例还是原型。
T getObject() throws Exception;
// 返回由 getObject() 方法返回的对象类型,如果事先不知道该类型,则返回 null。
Class<?> getObjectType();
// 如果此 FactoryBean 返回单例,则返回 true,否则返回 false。此方法的默认实现返回 true。
default boolean isSingleton() {
return true;
}
}
FactoryBean注入到Spring容器中产生2个实例,FactoryBean实例和其生产处理的实例假设id是 myBean 。那么如何获取这个2个bean?
- 获取FactoryBean实例: ApplicationContext#getBean("&myBean")
- 获取生产出来的bean实例:ApplicationContext#getBean("myBean")
来个实战案例
定义一个 MyFactoryBean ,生产非单例,每次new一个
@Component("myFactoryBean")
public class MyFactoryBean implements FactoryBean<BeanOne> {
@Override
public BeanOne getObject() throws Exception {
// 每次生成一个
return new BeanOne();
}
@Override
public Class<?> getObjectType() {
return BeanOne.class;
}
@Override
public boolean isSingleton() {
// 非单例 每次生成一个
return false;
}
}
观察下输出
MyFactoryBean 实例: com.crab.spring.ioc.demo12.MyFactoryBean@2ef3eef9
true
com.crab.spring.ioc.demo12.BeanOne@71809907 名称是: myFactoryBean
MyBeanPostProcessor2 输出:com.crab.spring.ioc.demo12.BeanOne@71809907 名称是: myFactoryBean
com.crab.spring.ioc.demo12.BeanOne@3ce1e309 名称是: myFactoryBean
MyBeanPostProcessor2 输出:com.crab.spring.ioc.demo12.BeanOne@3ce1e309 名称是: myFactoryBean
MyFactoryBean 生产的bean实例: com.crab.spring.ioc.demo12.BeanOne@71809907
false
细心的人会发现调用 FactoryBean#getObject() 生产bean 也是会走 BeanPostProcessor的增强流程的。
在深入了解下使用 FactoryBean 的注意点:
- FactoryBean 是一种程序化契约。实现不应该依赖注解驱动的注入或其他反射设施。 getObjectType() getObject() 可能会在Spring引导过程的早期调用,甚至在任何后处理器设置之前。如果需要访问其他 bean可以实现 BeanFactoryAware 并以编程方式获取它们。
- 容器只负责管理FactoryBean 实例的生命周期,而不是FactoryBean 创建的对象的生命周期。因此,暴露的 bean 对象(例如 java.io.Closeable.close() 上的销毁方法不会被自动调用。相反,FactoryBean 应该实现 DisposableBean 并将任何此类关闭调用委托给底层对象。
类似的接口还有 org.springframework.aop.framework.ProxyFactoryBean 、SmartFactoryBean 感兴趣的可以了解下
总结
本文详细分析了Spring 容器的扩展点,包括BeanPostProcessor、BeanFactoryPostProcessor、FactoryBean的原理和使用,结合上一篇Spring的生命周期和回调理解会更好。完全消化这2篇内容,Spring开发会更加顺手,离成为Spring高手更进一步。
本篇源码地址: https://github.com/kongxubihai/pdf-spring-series/tree/main/spring-series-ioc/src/main/java/com/crab/spring/ioc/demo12
知识分享,转载请注明出处。学无先后,达者为先!
Spring系列14:IoC容器的扩展点的更多相关文章
- Spring系列之IOC容器
一.概述 IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化.定位.配置应用程序中的对象及建立这些对象之间的依赖.应用程序无需直接在代码中new 相关的对象,应用程序由IOC容器进行组装.在S ...
- Spring系列之IOC的原理及手动实现
目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 导语 Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架.也是几乎所有J ...
- Spring.NET的IoC容器(The IoC container)——简介(Introduction)
简介 这个章节介绍了Spring Framework的控制反转(Inversion of Control ,IoC)的实现原理. Spring.Core 程序集是Spring.NET的 IoC 容器实 ...
- Spring5源码解析系列一——IoC容器核心类图
基本概念梳理 IoC(Inversion of Control,控制反转)就是把原来代码里需要实现的对象创建.依赖,反转给容器来帮忙实现.我们需要创建一个容器,同时需要一种描述来让容器知道要创建的对象 ...
- 比Spring简单的IoC容器
比Spring简单的IoC容器 Spring 虽然比起EJB轻量了许多,但是因为它需要兼容许多不同的类库,导致现在Spring还是相当的庞大的,动不动就上40MB的jar包, 而且想要理解Spring ...
- Spring 系列教程之容器的功能
Spring 系列教程之容器的功能 经过前面几章的分析,相信大家已经对 Spring 中的容器功能有了简单的了解,在前面的章节中我们一直以 BeanFacotry 接口以及它的默认实现类 XmlBea ...
- 使用Spring.NET的IoC容器
使用Spring.NET的IoC容器 0. 辅助类库 using System; using System.Collections.Generic; using System.Linq; using ...
- Spring之一:IoC容器体系结构
温故而知心. Spring IoC概述 常说spring的控制反转(依赖反转),看看维基百科的解释: 如果合作对象的引用或依赖关系的管理要由具体对象来完成,会导致代码的高度耦合和可测试性降低,这对复杂 ...
- Spring Framework------>version4.3.5.RELAESE----->Reference Documentation学习心得----->使用spring framework的IoC容器功能----->方法一:使用XML文件定义beans之间的依赖注入关系
XML-based configuration metadata(使用XML文件定义beans之间的依赖注入关系) 第一部分 编程思路概述 step1,在XML文件中定义各个bean之间的依赖关系. ...
随机推荐
- django 使用createView创建视图是form_valid()没有通过?
django 使用createView创建视图是form_valid()没有通过的原因: fields中定义的字段要与from表单中的字段相对应 修改后 接着又报错: 查看没有取到id,最后通过req ...
- jsp文件中文乱码解决
文件顶加上 <%@ page contentType="text/html;charset=UTF-8" language="java" %>即可
- Python与Javascript相互调用超详细讲解(2022年1月最新)(一)基本原理 Part 1 - 通过子进程和进程间通信(IPC)
TL; DR 适用于: python和javascript的runtime(基本特指cpython[不是cython!]和Node.js)都装好了 副语言用了一些复杂的包(例如python用了nump ...
- 抓包分析与mock实战
Charles下载安装 官网下载安装:https://www.charlesproxy.com/ 电脑证书配置 如果不配置证书,无法抓取https协议 配置证书: 1 - 打开Charles,在hel ...
- Solon Web 开发,九、跨域处理
Solon Web 开发 一.开始 二.开发知识准备 三.打包与运行 四.请求上下文 五.数据访问.事务与缓存应用 六.过滤器.处理.拦截器 七.视图模板与Mvc注解 八.校验.及定制与扩展 九.跨域 ...
- 【记录一个问题】golang中copy []byte类型的slice无效,为什么?
有这样一段代码: src := []byte{xxxxx} dst := make([]byte, 0, len(src)) copy(dst, src) //这一行居然没生效! // dst = a ...
- Choregraphe 2.8.6.23动作失效
动作和动画执行完以后,无法自动还原成默认状态,自然接下来动作无法执行了.之后各种操作可能诱发软件原有的bug.需要开关自主生活模块才能恢复. 部分连贯的动作不需要恢复就能执行,动画不行. 站立动作好像 ...
- 访问者模式(Visitor模式)
模式的定义与特点 访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提 ...
- 项目管理软件jira安装
JIRA是Atlassian公司出品的项目与事务跟踪工具,被广泛应用于缺陷跟踪.客户服务.需求收集.流程审批.任务跟踪.项目跟踪和敏捷管理等工作领域. 官方文档https://confluence.a ...
- IntelliJ IDEA 热部署,修改java文件 不用重启tomcat
详情见大佬:https://www.cnblogs.com/chenweichu/articles/6838842.html