Some commonly used extension points in Spring

The first thing that comes to mind when mentioning the Spring framework is IOC and AOP. In addition to these two basic core points, Spring also provides many extension points, so that we can implement unique functions according to our actual situation.

1. Import configuration

Sometimes we need to introduce other classes in a configuration class, and the imported classes also need to be added to the Spring container, which can be achieved by using the @Import annotation. @Import supports imported classes:

  • common class
  • Configuration class annotated with @Configuration
  • A class that implements the ImportSelector interface
  • A class that implements the ImportBeanDefinitionRegistrar interface
(1) Common class
public class Lyc{
}

@Import(Lyc.class)
@Configuration
public class TestService{
}

Introducing the Lyc class in this way, Spring will automatically instantiate the Lyc object, and then inject it through the @Autowired annotation where Lyc needs to be used.

(2) Configuration class annotated with @Configuration
public class Lyc{
}

public class LycCoder{
}

@Import(Lyc.class)
@Configuration
public class LycConfiguration{
	@Bean
	public LycCoder lycCoder(){
		return new LycCoder();
	}
}

@Import(LycConfiguration.class)
@Configuration
public class TestService{
}

The LycConfiguration introduced in this way will import all the classes introduced by annotations such as @Import, @ImportResource, @PropertySource, etc. related to the configuration class.

(3) Classes that implement the ImportSelector interface

The specific use of this approach can be seen by following the @EnableTransactionManagement annotation. The code is as follows, in this way, multiple classes can be introduced at the same time.

@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement{
	...
}


public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
	@Override
	protected String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			case PROXY:
				return new String[] {AutoProxyRegistrar.class.getName(),
						ProxyTransactionManagementConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {determineTransactionAspectClass()};
			default:
				return null;
		}
	}
}

public abstract class AdviceModeImportSelector<A extends Annotation> implements ImportSelector {
	@Override
	public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
		AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
		// ...
		AdviceMode adviceMode = attributes.getEnum(getAdviceModeAttributeName());
		String[] imports = selectImports(adviceMode);
		if (imports == null) {
			throw new IllegalArgumentException("Unknown AdviceMode: " + adviceMode);
		}
		return imports;
	}

	@Nullable
	protected abstract String[] selectImports(AdviceMode adviceMode);

}
(4) Classes that implement the ImportBeanDefinitionRegistrar interface
public class LycMessageImportConfiguration implements ImportBeanDefinitionRegistrar {
    private static final String CLASS_ANNOTATION_BEAN_POST_PROCESSOR = "com.lyc.config.lycMessageAnnotationsBeanPostProcessor";

    public LycMessageImportConfiguration() {
    }

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        if (!registry.containsBeanDefinition("com.lyc.config.lycMessageAnnotationsBeanPostProcessor")) {
            registry.registerBeanDefinition("com.lyc.config.lycMessageAnnotationsBeanPostProcessor", new RootBeanDefinition(LycMessageAnnotationsBeanPostProcessor.class));
        }

    }
}

In this way, the BeanDefinitionRegistry container registration object can be obtained in the registerBeanDefinitions method, and the creation and registration of BeanDefinitions can be controlled by themselves.


2. Initialization processing when the project starts

In actual work, there are usually some scenarios. When the project starts, we need to deal with some things, such as caching data. Spring provides us with some extension points. There are many ways to implement them. You can refer to another blog: [ Several implementations of handling things at Spring project startup]

3. Get the Spring container object

In the Spring project, we hand over the creation of objects to Spring, so when we need to use other objects in the Spring container in a class, we can obtain them in the following two ways:

(1) Implement the ApplicationContextAware interface
@Component
public class MyAnnotationTest implements ApplicationContextAware {

	private ApplicationContext applicationContext;
	
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void testMethod() {
        //Get the Bean object annotated with MyAnnotation in the Spring container
        Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(MyAnnotation.class);
        ....
    }
}

This method is used in the current class. Usually, a Spring tool class can be created, and the Spring container object can be obtained directly through the method in the tool class. The code example is as follows:

@Component
public class SpringBeanUtil implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringBeanConfig.applicationContext = applicationContext;
    }

    public static <T> T getBean(String name, Class<T> clazz) throws BeansException {
        return applicationContext.getBean(name, clazz);
    }

    public static <T> T getBean(Class<T> clazz) throws BeansException {
        return applicationContext.getBean(clazz);
    }

    public static  <T> Map<String, T> getBeansOfType(Class<T> clazz) throws BeansException {
        return applicationContext.getBeansOfType(clazz);
    }
}

Usage: TestService testService= SpringBeanUtil.getBean(testService, TestService.class);

(2) Implement the BeanFactoryAware interface

This method is similar to the above, by implementing an interface, the code example is as follows:

@Component
public class MyTestService implements BeanFactoryAware {
	
	private BeanFactory beanFactory;
	
    @Override
    public void setApplicationContext(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public void testMethod() {
        TestService testService = (TestService)beanFactory.getBean("testService");
        ....
    }
}

4. Custom interceptor

For scenarios such as authorization authentication and logging, we usually use interceptors for unified processing. The top-level interface of the interceptor in SpringMVC is HandlerInterceptor, which has three methods, the code is as follows:

public interface HandlerInterceptor {
	//Before the target method is executed
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }
	//After the target method is executed
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
	//Executed when the request completes
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

Taking the user permission verification scenario as an example, we only need to create a new interceptor for permission verification, implement the HandlerInterceptor interface, and then register the interceptor in the container. The sample code is as follows:

@Component
public class UserAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // The token passed by the header information
        String tokenJson = request.getHeader("token_info");
        if (StrUtil.isEmpty(tokenJson)) {
            // TODO custom validation failure return information
            return false;
        }
        // decoding
        tokenJson = URLDecoder.decode(tokenJson, "UTF-8");
        UserInfoDto userInfo = JSONUtil.toBean(tokenJson, UserInfoDto.class);
        if (null == userInfo || StrUtil.isEmpty(userInfo.getToken())) {
            // TODO custom token verification failure return information
            return false;
        }
        // Get user information based on token
        UserInfoDto userInfoDto = getUserByToken(user, uri);
        // TODO Custom token verification is completed after the processing
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.removeUser();
    }
@Configuration
public class LycWebMvcConfigurer implements WebMvcConfigurer {

    @Autowired
    @Lazy
    private UserAuthInterceptor userAuthInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOrigins("*")
                .exposedHeaders("access-control-allow-headers",
                        "access-control-allow-methods",
                        "access-control-allow-origin",
                        "access-control-max-age",
                        "X-Frame-Options");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userAuthInterceptor);
    }
}

5. Customize global exception handling

It is unavoidable to throw some exceptions in the program. For example, the parameter verification is illegal. If we do not do anything, it is very bad for the user experience to throw such code-level error messages. Add try...catch to the exception code, and throw a friendly error message after catching the exception. for example:

try{
	int i = 10/0
}catch(Exception e){
	throw new Exception("Data exception");
}

However, this method will lead to a lot of try...catch in the code, and the maintainability and readability are not very good. A better processing method is to add exception capture processing uniformly, and business code is more concerned about the implementation of business logic. Such an extension point is provided in Spring to implement global exception handling. The code example is as follows:

@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * global exception
     * 
     * @param exception
     * @param response
     * @return
     */
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public LycResult handle(Throwable exception, HttpServletResponse response) {
        Throwable throwable = exception.getCause();
        if (null != throwable) {
            exception = throwable;
        }
        //Give different handling for different exception types
        if (exception instanceof LycAssertException) {
            // Assertion exception information
        } else if (exception instanceof LycBizException) {
            // Information about specific business exceptions
        } else if (exception instanceof BizException) {
            // General business exception information
        } else {
            // System exception information
        }
        ...
    }
}

6. Custom scope

There are only two types of scope s supported by default in Spring, singleton and prototype. Singleton means that the bean object obtained from the Spring container is the same every time, and prototype means that the bean object obtained from the Spring container is different each time. In some scenarios, such as To obtain the same bean object from the Spring container in the same thread, singleton and prototype may not meet our requirements, which requires a custom scope implementation. An example of the implemented code is as follows:

(1) Scope of custom thread scope

public class LycThreadScope implements Scope {

    private static final ThreadLocal LYC_THREAD_SCOPE = new ThreadLocal();

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object obj = LYC_THREAD_SCOPE.get();
        if (null != obj) {
            return obj;
        }
        Object object = objectFactory.getObject();
        LYC_THREAD_SCOPE.set(object);
        return object;
    }

    @Override
    public Object remove(String name) {
        LYC_THREAD_SCOPE.remove();
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}

(2) Inject the newly defined Scope into the Spring container

public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("lycThreadScope",new LycThreadScope());
    }
}

(3) Use a custom scope

@Component
@Scope("lycThreadScope")
public class TestService{
	...
}

Tags: Java Spring Back-end

Posted by benutne on Wed, 05 Oct 2022 09:27:07 +0530