在基于 Spring 框架开发的时候,有的时候我们依赖的库会内置一些被 Spring 管理的 Bean。
场景1
接口 Test
有一个实现类 TestImpl
,在 TestImpl上
使用了 @Service
或者 @Component
注解,我们在其他地方使用的时候是通过 @Autowired
来注入 Test
接口的方式来使用的。
这个 Test
和实现是被封装后的一个 jar 包库,现在新的代码工程在依赖这个 jar 包之后,假设 Spring 会自动将 Test 及其实现类进行初始化并载入 Spring 容器的管理。
现在我们发现这个 TestImpl
里面有一个方法不能满足我们的需求,我们需要对其中一个方法进行重写,此时我们的第一想法就是写一个 CustomTestImpl extends TestImpl
然后重写需要重写的那个方法。
如果你对这个 CustomTestImpl
添加 @Component
注解,则启动服务的时候必然会报错,出现 Test
接口的注入失败,因为 Spring 容器发现 Test
接口有2个实现类,它不知道要注入哪一个,而我们的实际想法是使用新实现的 CustomTestImpl
类。
基于这个场景,可以在 CustomTestImpl
类上添加一个注解 @Primary
,这个注解就是告诉 Spring 容器,在一个接口有多个实现类时,自动注入接口的时候主要默认选择的的是哪一个实例对象。
如果你的需求场景和这个场景契合,那么你可以通过这个注解来处理。
场景2
某Java类 TestBean1 implements Test
和 TestBean2 implements Test
,并且在 Configuration
类中进行了如下配置:
@Bean
public Test getTestBean1(){
return new TestBean1();
}
@Bean
public Test getTestBean2(){
return new TestBean2();
}
注意这里并没有添加 @ConditionalOnMissingBean
注解,在这种情况下。其他地方使用 @Autowired private List<Test> testList;
来注入使用,然后循环 testList 集合调用 Test 接口中的某个方法。
这种场景是不是很熟悉,在Spring源码中有很多处理都是采用这种接口集合的模式来实现的。此时如果我们想对 TestBean1
中的方法进行重写,应该怎么办呢?
如果按照上面一个场景使用 @Primary
是不能达到我们的目标的,因为注入的 List<Test>
会出现3个 Test 实例Bean对象。
解决这个问题,我们需要使用新实现的类完全替换掉容器中的类,绝对的偷梁换柱,比如 CustomTestBean1 extends TestBean1
进行方法重写,然后这个类不添加 @Component
注解,再使用 BeanDefinitionRegistryPostProcessor
接口来实现,具体代码示例如下:
// 这里不需要任何注解
public CustomTestBean1 extends TestBean1 {
}
@Configuration
public class TestBean1Configuration implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
String beanName = StringUtils.uncapitalize(TestBean1.class.getSimpleName());
// 先移除原来的bean定义
beanDefinitionRegistry.removeBeanDefinition(beanName);
// 注册我们自己的bean定义
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(CustomTestBean1.class);
// 如果有构造函数参数, 有几个构造函数的参数就设置几个 没有就不用设置
// beanDefinitionBuilder.addConstructorArgValue("构造参数1");
// beanDefinitionBuilder.addConstructorArgValue("构造参数2");
// beanDefinitionBuilder.addConstructorArgValue("构造参数3");
// 设置 init方法 没有就不用设置
// beanDefinitionBuilder.setInitMethodName("init");
// 设置 destory方法 没有就不用设置
// beanDefinitionBuilder.setDestroyMethodName("destory");
// 将Bean 的定义注册到Spring环境
beanDefinitionRegistry.registerBeanDefinition(StringUtils.uncapitalize(CustomTestBean1.class.getSimpleName()), beanDefinitionBuilder.getBeanDefinition());
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
场景2 适用于几乎任何 Spring Bean,包括 Controller 你也可以这样做(比如继承已有的 Controller 重写方法添加方法都可以)
如下 Controller
被继承重写的代码示例:
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public boolean hello() {
return true;
}
@GetMapping("/world")
public String world() {
return "world";
}
}
CustomTestController 继承 TestController 后重写world方法,然后再添加一个print方法
public class CustomTestController extends TestController {
@Override
public String world() {
return "world-extend";
}
@GetMapping("/print")
public String print() {
return "print-extend";
}
}
测试访问 http://localhost:8080/test/hello
输出父类的 true
测试访问 http://localhost:8080/test/world
输出子类重写后的 world-extend
测试访问 http://localhost:8080/test/print
输出子类新添加的 print-extend
Controller 被继承和重写后,类上的相关注解和方法上的相关注解都不需要拷贝,只需要重点关心 Override 的方法体即可,这样可以保证如果基础包升级后的 mapping path 发生变更不受影响。如果你非要给示例中子类的
world()
方法添加一个不同的@GetMapping("/world2")
那么Path/world2
将会覆盖并取代/world
,你再访问/world
接口就是404,访问/world2
将返回子类重写的方法的返回结果
(END)