Spring framework IOC postprocessor mechanism

Posted by digibrain on Thu, 06 Jan 2022 13:04:32 +0100

Consider the following simple program that uses file service to upload files. What should Spring do to make it run normally?

public class ApplicationContextDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext=new AnnotationConfigApplicationContext();
        applicationContext.register(MyConfiguration.class);
        applicationContext.refresh();
        applicationContext.getBean(FileService.class).uploadFile(new File("D:/readme.md"));
    }
}

@ComponentScan(basePackages = "com.example.spring")
@Configuration
public class MyConfiguration {

    @Autowired
    private FileServerProperties fileServerProperties;

    //fileServer is a component in a third-party package provided by file services
    @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;
}

@Component
public class FileService {

    @Autowired
    FileServer fileServer;

    public void uploadFile(File file){
        fileServer.uploadFile(file);
    }
}

//src/main/resources/fileServer.properties
file.server.user=spring-example
file.server.pwd=123456

If you simply describe what Spring does in one sentence, it is to help us create beans and assign values to bean properties, so that we can use complete beans to implement business logic at runtime. The detailed process is as follows:

1. Create a BeanDefinition object for the entry class (usually a config class)MyConfiguration and register it in the container

2. Process the @ ComponentScan annotation on MyConfiguration and scan the Component components under basePackages (classes with @ Component or its derived annotations (such as @ Configuration, @ Service, @ Controller). In this application, FileServerProperties and FileService meet the conditions

  • 2.1. Process FileServerProperties, create a BeanDefinition object for it, and register it in the container. Subsequently, Spring finds that there is @ PropertySource annotation on the class, and uses ResourceLoader to load the corresponding configuration file as PropertySource, which is added to the Spring environment as a configuration source Mutablepropertysources
  • 2.2. Handle the FileService. Similarly, create a BeanDefinition object for it and register it in the container

3. Process the @ bean method in MyConfiguration. Instead of directly creating beans, the method is encapsulated as a BeanMethod object and saved

4. Create a BeanDefinition object for the FileServer according to the BeanMethod in step 3 and register it in the container

Note: it can be seen that the above steps mainly do two things: one is to load the configuration meta information, and the other is to create and register beandefinitions. These beandefinitions come from direct registration (applicationContext.register()), @ ComponentScan scans classes with @ Component, and @ Bean methods

5. Next, perform bean instance creation, dependency injection, attribute filling and initialization (hereinafter referred to as bean creation and initialization) for all singleton classes according to the BeanDefinition registered in the above steps

  • 5.1. During the bean creation and initialization of myconfiguration, it is found that it depends on FileServerProperties during property assembly, so the bean creation and initialization of FileServerProperties are performed
    • 5.1.1. The bean of fileserverproperties is created and initialized, and its property assignment is realized through the @ Value annotation. The Value value of the corresponding key is read from the PropertySource loaded in step 2.1 and assigned to the bean through reflection
    • 5.1.2. Inject the created and initialized FileServerProperties object reference into MyConfiguration
  • 5.2. Similarly, perform bean creation and initialization of FileService and FileServer

Starting from step 3, all the steps I listed are completed by PostProcessor. PostProcessor is a hook mechanism provided by Spring IOC that allows customization in the process of container life cycle and bean life cycle. It corresponds to BeanFactoryPostProcessor and BeanPostProcessor respectively. The difference between them is the action granularity and the time of being called

The role of beanfactoryprocessor is container, which is executed during container startup. The interface has only one method, which is called after the BeanFactory is created and before all beans are created to process beanDefinition. You can create and register a new beanDefinition or modify an existing beanDefinition

The function granularity of BeanPostProcessor is bean, and each bean will be executed during the creation process. The interface declares two methods. The calling time is after the bean instantiation is completed and the properties have been filled (@ Autowired and the properties of @ Value annotation have been assigned). The difference between the two methods is that one is a callback before the initialization method is executed, A callback after the initialization method is executed (if the class implements the afterpropertieset method of InitializingBean or configures the init method, they are collectively referred to as initialization methods), the purpose is to control the callback timing more finely. You can act as a proxy for beans, or set and modify bean properties

In the above example, two kinds of postprocessors are involved, corresponding to ConfigurationClassPostProcessor and autowirediannotationbeanpostprocessor respectively. The former is responsible for processing @ Configuration and its associated annotations (including @ Bean, @ ComponentScan, @ PropertySources, @ PropertySource, @ Import, etc.). The latter is responsible for handling @ Autowired, @ Value, @ Inject and other annotations related to dependency injection and attribute injection. These two can be said to be the most important PostProcessor in Spring IOC

In addition to these two postprocessors, Spring also has some built-in postprocessors, such as

CommonAnnotationBeanPostProcessor for @ PostConstruct, @ PreDestroy, @ Resource

EventListener methodprocessor that processes @ EventListener

Persistenceannotationbeanpostprocessor for handling JPA related annotations (it needs to introduce JPA related packages to register)

Through this plug-in Hook mechanism, Spring has realized the core functions of IOC flexibly and extensibly. Understanding this mechanism is very helpful not only for learning Spring's own design ideas, but also for customizing and realizing general functions in work. You know, this kind of customization is still common in application development.

Consider the following scenarios: when an application interacts with a database, a file service, and a three-party public API service, password information is essential. Many people must have done it by storing the password in clear text in the configuration file. Obviously, this is a bad idea (for example, the file upload example I mentioned above). So how can we do it more safely?

After discussion, the development team decided to adopt this method: the passwords involved in all programs are uniformly managed by a special password service, including password storage, distribution and decryption. The application applies for a password from the password service and obtains a string of ciphertext. The ciphertext is stored in the configuration file, and the program is @ EncryptedValue("${key}") String pwd; In this way, it is declared that the ciphertext is read, and the original text needs to be decrypted at run time. This function is realized by customizing beanfactoryprocessor. The above file upload program is also used as a demonstration. The adjusted program code is as follows:

/**
 * The password service helps us verify the custom logic. Just pay attention to its method statement. The implementation is not important and need not pay too much attention
 * The password service should be an independent service. In order to reduce the complexity of demonstration, it is put together
 */
public class PasswordService {

    private static final String plainText="123456";

    /**
    * Apply for password
    * Statement: please ignore whether it is reasonable to use base64 for "encryption and decryption" here, which will not affect our goal
    * @param user According to the password applied by the user, the password generated by user / will be saved in the form of key value pairs in the actual business scenario for easy management
    * @return For the applied password, only the ciphertext will be returned in the actual business scenario. In order to verify the correctness of the customized decryption logic, the plaintext will also be returned
    */
    public Password applyPassword(String user) {
        return new Password(plainText,Base64.getEncoder().encodeToString(new String(user+"::123456").getBytes(StandardCharsets.UTF_8)));
    }

    /**
    * decrypt
    * @param encryptedValue ciphertext
    * @return Decrypted plaintext
    * Statement: please ignore whether it is reasonable to use base64 for "encryption and decryption" here, which will not affect our goal
    */
    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 {
    /**
    * Plaintext
    */
    private String plainText;
    /**
    * ciphertext
    */
    private String  cipherText;
}


//We first call new passwordservice() Applypassword ("spring example") gets the password plaintext and ciphertext, and Password(plainText=123456, cipherText=c3ByaW5nLWV4YW1wbGU6OjEyMzQ1Ng = =)
//The ciphertext will be stored in the configuration file, and the plaintext will be used to verify the correctness of the customized logic in the test case

//src/main/resources/fileServer.properties file content
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;
}

/**
 * The annotation on the field indicates that the field needs to be decrypted
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptedValue {

    String value();
}


/**
 * The BeanPostProcessor that processes @ EncryptedValue is our focus. Its responsibility is to obtain the original value of the field, call the cryptographic service for decryption, and assign the decrypted value to the field
 */
@Component
public class EncryptedValueBeanPostProcessor implements BeanPostProcessor, EnvironmentAware {

    private Environment environment;

    /**
    * Get the Environment object, which is used to configure reading and placeholder resolution
    * Environment For details, you can refer to 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 {
        //This is a reflection tool class provided by Spring. It will use the specified callback method for each field in the specified class. It is very powerful...
        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");
                    //Read ciphertext in configuration file
                    String encryptedValue=environment.resolvePlaceholders(key);
                    //Call password service decryption
                    String decryptValue=new PasswordService().decrypt(fileServerUser,encryptedValue);
                    //Assign the field to the decrypted value through reflection
                    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(){
        //Verify fileServerProperties#pwd property value = = clear text
        Assert.assertEquals(fileServerProperties.getPwd(),"123456");
    }
}

Well, that's all for the demonstration. I hope you can master this extension mechanism and use it to realize general functions in daily development

Topics: Spring