在基于 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)