撸一撸Spring Framework-IOC-实现通用功能的利器(PostProcessor机制)

  撸一撸Spring Framework-IoC系列文章目录

  • IoC概述
  • BeanFactory
  • ApplicationContext
  • BeanDefinition
  • BeanWrapper
  • 资源管理利器(Resource与ResourceLoader)
  • 配置管理(Environment与PropertySource)
  • 实现通用功能的利器(PostProcessor机制)


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都有参与,分别对应ConfigurationClassPostProcessorAutowiredAnnotationBeanPostProcessor。前者负责处理@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

除了ConfigurationClassPostProcessorAutowiredAnnotationBeanPostProcessor这两个PostProcessor外,Spring还内置了一些PostProcessor,比如

处理各种Aware接口回调的ApplicationContextAwareProcessor(包括ApplicationContextAware、EnvironmentAware、ResourceLoaderAware、ApplicationEventPublisherAware等)

处理@PostConstruct、@PreDestroy、@Resource的CommonAnnotationBeanPostProcessor

处理@EventListener的EventListenerMethodProcessor

处理JPA相关注解的PersistenceAnnotationBeanPostProcessor(需要引入JPA相关包才会注册)

通过这种插件式的Hook机制,Spring极具灵活、扩展性的实现了IOC的核心功能

实战一下:除了内置的PostProcessor,spring当然也支持我们自定义PostProcessor

考虑如下场景:应用程序在和诸如数据库、文件服务、三方公共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");
    }
}

 好了,演示就到这里,希望你能熟练掌握这种扩展机制,在日常开发中用它实现通用功能

你可能感兴趣的:(Spring,Framework源码,spring,java,spring源码,postProcessor)