PostProcessor是Spring IOC提供的一种允许在容器生命周期(BeanFactoryPostProcessor)、bean生命周期(BeanPostProcessor)过程中进行定制化的hook机制。了解这种机制无论对于学习Spring本身的设计思想,还是对于工作中进行定制,实现通用功能都有很大帮助
本文首先介绍了PostProcessor的概念及作用,然后通过实战加深理解
概念:
BeanFactoryPostProcessor与BeanPostProcessor都是一种hook机制,区别如下:
BeanFactoryPostProcessor的作用粒度是容器,在容器启动过程中被执行。接口只有一个方法,其调用时机是BeanFactory创建后、所有的bean开始创建前,用于处理beanDefinition,可以创建、注册新的beanDefinition,也可以修改现有的beanDefinition
BeanPostProcessor的作用粒度是bean,每个bean的创建过程中都会被执行。接口声明了两个方法,调用时机都是在bean实例化完成、并且属性已填充完成后(@Autowired、@Value注解的属性已被赋值),两个方法的区别在于:一个在初始化方法执行前回调,一个在初始化方法执行后回调(若类实现了InitializingBean的afterPropertiesSet方法,或者配置了init-method,二者统称为初始化方法),其目的是为了更细粒度的回调时机控制。可以对bean做代理,也可以设置、修改bean的属性
下面这个spring程序,把它运行起来的过程中,PostProcessor机制发挥了核心作用。在此过程中,上述两类PostProcessor都有参与,分别对应ConfigurationClassPostProcessor和AutowiredAnnotationBeanPostProcessor。前者负责处理@Configuration、@Bean、@ComponentScan、@PropertySources、@PropertySource、@Import等注解,总的来说管的是beanDefinition的注册;后者负责处理@Autowired、@Value、@Inject这几个与依赖注入、属性注入相关的注解,总的来说管的是bean属性的赋值。这二者可以说是Spring IOC中最重要的PostProcessor 了
public class ApplicationContextDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext=new AnnotationConfigApplicationContext();
applicationContext.register(MyConfiguration.class);
applicationContext.refresh();
applicationContext.getBean(FileServer.class).uploadFile(new File("D:/readme.md"));
}
}
@ComponentScan(basePackages = "com.example.spring")
@Configuration
public class MyConfiguration {
@Autowired
private FileServerProperties fileServerProperties;
//fileServer是三方包中的组件,用于上传文件
@Bean
public FileServer fileServer(){
return new FileServer(fileServerProperties.getUser(),fileServerProperties.getPwd());
}
}
@Component
@PropertySource("classpath:fileServer.properties")
@Data
public class FileServerProperties {
@Value("${file.server.user}")
private String user;
@Value("${file.server.pwd}")
private String pwd;
}
//src/main/resources/fileServer.properties
file.server.user=spring-example
file.server.pwd=123456
除了ConfigurationClassPostProcessor和AutowiredAnnotationBeanPostProcessor这两个PostProcessor外,Spring还内置了一些PostProcessor,比如
处理各种Aware接口回调的ApplicationContextAwareProcessor(包括ApplicationContextAware、EnvironmentAware、ResourceLoaderAware、ApplicationEventPublisherAware等)
处理@PostConstruct、@PreDestroy、@Resource的CommonAnnotationBeanPostProcessor
处理@EventListener的EventListenerMethodProcessor
处理JPA相关注解的PersistenceAnnotationBeanPostProcessor(需要引入JPA相关包才会注册)
通过这种插件式的Hook机制,Spring极具灵活、扩展性的实现了IOC的核心功能
考虑如下场景:应用程序在和诸如数据库、文件服务、三方公共API服务交互时,密码信息是必不可少的,将密码以明文形式存在配置文件中很多人一定都干过,很明显这是个坏主意(比如我上面举的文件上传的例子),那么如何做才更安全呢?
开发小组经过讨论决定采用这种方式:所有程序涉及的密码由专门的密码服务统一管理,包括密码保存、下发、解密,应用程序向密码服务申请密码,得到的是一串密文,将密文存在配置文件中,程序以@EncryptedValue("${key}") String pwd;这种方式声明读取的是密文,需要在运行时通过解密得到原文。下面通过定制BeanPostProcessor来实现这个功能,还拿上面的文件上传程序作演示,调整后的程序代码如下:
/**
* 密码服务,辅助我们验证定制逻辑的,关注它的方法声明即可,实现并不重要,不用过多关注
* 密码服务应该是独立的服务,这里为了降低演示复杂度,就放在一起了
*/
public class PasswordService {
private static final String plainText="123456";
/**
* 申请密码
* 申明:请忽略这里用base64进行"加解密"是否合理的问题,不影响咱们的目标
* @param user 根据用户申请密码,实际业务场景下会将user/生成的密码以键值对的形式保存下来,方便管理
* @return 申请到的密码,实际业务场景下只会返回密文,这里为了验证定制的解密逻辑正确性,将明文也一并返回
*/
public Password applyPassword(String user) {
return new Password(plainText,Base64.getEncoder().encodeToString(new String(user+"::123456").getBytes(StandardCharsets.UTF_8)));
}
/**
* 解密
* @param encryptedValue 密文
* @return 解密后的明文
* 申明:请忽略这里用base64进行"加解密"是否合理的问题,不影响咱们的目标
*/
public String decrypt(String user, String encryptedValue) {
byte[] decodeResult= Base64.getDecoder().decode(encryptedValue);
String decryptValue=new String(decodeResult, StandardCharsets.UTF_8);
return decryptValue.replace(user+"::","");
}
}
@ToString
@AllArgsConstructor
@Data
public class Password {
/**
* 明文
*/
private String plainText;
/**
* 密文
*/
private String cipherText;
}
//我们首先通过调用new PasswordService().applyPassword("spring-example")拿到密码明文和密文,Password(plainText=123456, cipherText=c3ByaW5nLWV4YW1wbGU6OjEyMzQ1Ng==)
//密文会被存放到配置文件中,明文用于测试用例中验证定制逻辑的正确性
//src/main/resources/fileServer.properties文件内容
file.server.user=spring-example
file.server.pwd=c3ByaW5nLWV4YW1wbGU6OjEyMzQ1Ng==
@Component
@PropertySource("classpath:fileServer.properties")
@Data
public class FileServerProperties {
@Value("${file.server.user}")
private String user;
@EncryptedValue("${file.server.pwd}")
private String pwd;
}
/**
* 注解在field上,表示该字段需要被解密
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptedValue {
String value();
}
/**
* 处理@EncryptedValue的BeanPostProcessor,这是我们的重点,它的职责是获取field原始值,调用密码服务进行解密,并将解密后的值赋值给field
*/
@Component
public class EncryptedValueBeanPostProcessor implements BeanPostProcessor, EnvironmentAware {
private Environment environment;
/**
* 获取Environment对象,这里用于配置读取、占位符解析
* Environment的详细内容,你可以参考https://blog.csdn.net/wb_snail/article/details/121619040
*/
@Override
public void setEnvironment(Environment environment) {
this.environment=environment;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//这是Spring提供的反射工具类,会对指定class中的每个field运用指定回调方法,很强大哦。。。
ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
EncryptedValue encryptedValueAnnotation=field.getAnnotation(EncryptedValue.class);
if(encryptedValueAnnotation!=null){
String key=encryptedValueAnnotation.value();
String fileServerUser=environment.getProperty("file.server.user");
//读取配置文件中的密文
String encryptedValue=environment.resolvePlaceholders(key);
//调用密码服务解密
String decryptValue=new PasswordService().decrypt(fileServerUser,encryptedValue);
//通过反射将字段赋值为解密后的值
field.setAccessible(true);
field.set(bean,decryptValue);
}
}
});
return bean;
}
}
@ComponentScan(basePackages = "com.example.spring")
@Configuration
public class MyConfiguration {
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MyConfiguration.class)
public class FileServerPropertiesTest {
@Autowired
FileServerProperties fileServerProperties;
@Test
public void fieldValueTest(){
//验证fileServerProperties#pwd属性值==明文
Assert.assertEquals(fileServerProperties.getPwd(),"123456");
}
}
好了,演示就到这里,希望你能熟练掌握这种扩展机制,在日常开发中用它实现通用功能