前言:我们知道spring有很多对外提供的接口,在使用时会不会分不清呢?什么时候该用什么接口,什么接口是干嘛用的,如果缺乏相关知识的整理或思考,很多时候我们的技术都是止步不前的。本文博主盘点一些常用接口,博主深知个人能力有限 ,非常欢迎各位在评论区补充其它常用接口的使用,共同进步。
原创不易,转载请声明出处:csdn博主 孟秋与你
生命周期相关
ApplicationContextInitializer 接口
这个接口作用可以理解为: 在spring初始化前 用户可以拓展些功能 ,我们可以在启动类实现这个接口。
/**
* @author 孟秋与你
*/
public class Application implements ApplicationContextInitializer {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Application.class);
// 默认是启动的 也就是我们平常启动springboot能看到的一个图案
// 启动时的 banner 可以自定义内容在 resources/banner.txt
// 当然 生产环境为了节省启动速度 可以关闭这个
application.setBannerMode(Banner.Mode.CONSOLE);
application.run(args);
}
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
// 获取环境
ConfigurableEnvironment environment =
configurableApplicationContext.getEnvironment();
System.out.println("==init operation here =====springboot初始化完成某些操作在这里定义========");
}
}
我们看看网上已有的分析是如何描述的:
这是整个spring容器在刷新之前初始化ConfigurableApplicationContext的回调接口,简单来说,就是在容器刷新之前调用此类的initialize方法。这个点允许被用户自己扩展。用户可以在整个spring容器还没被初始化之前做一些事情。
可以想到的场景可能为,在最开始激活一些配置,或者利用这时候class还没被类加载器加载的时机,进行动态字节码注入等操作。
扩展方式为
public class TestApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("[ApplicationContextInitializer]");
}
}
- 在启动类中用springApplication.addInitializers(new TestApplicationContextInitializer())语句加入
- 配置文件配置context.initializer.classes=com.example.demo.TestApplicationContextInitializer
- Spring SPI扩展,在spring.factories中加入org.springframework.context.ApplicationContextInitializer=com.example.demo.TestApplicationContextInitializer
看似很复杂,但其实稍微调试一下源码,我们就能发现 无论是博主的方式 还是网上其它教程的种种操作, 无非就是
给SpringApplication类的 List<ApplicationContextInitializer<?>> initializers 变量赋值,
简单粗暴点理解,就是通过不同的方式 间接setInitializers
ApplicationContextAware接口
很经典的一个接口 很多项目会自定义工具类通过这个接口来保存spring上下文信息, 工具类代码如下:
/** * spring 工具类 * */ @Slf4j public class SpringUtil implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(@Nullable ApplicationContext context) throws BeansException { SpringUtil.context = context; } /** * 获取bean * * @param clazz class类 * @param <T> 泛型 * @return T */ public static <T> T getBean(Class<T> clazz) { if (clazz == null) { return null; } return context.getBean(clazz); } /** * 获取bean * * @param beanId beanId * @param <T> 泛型 * @return T */ public static <T> T getBean(String beanId) { if (beanId == null) { return null; } return (T) context.getBean(beanId); } /** * 获取bean * * @param beanName bean名称 * @param clazz class类 * @param <T> 泛型 * @return T */ public static <T> T getBean(String beanName, Class<T> clazz) { if (null == beanName || "".equals(beanName.trim())) { return null; } if (clazz == null) { return null; } return (T) context.getBean(beanName, clazz); } /** * 获取 ApplicationContext * * @return ApplicationContext */ public static ApplicationContext getContext() { if (context == null) { return null; } return context; } /** * 发布事件 * * @param event 事件 */ public static void publishEvent(ApplicationEvent event) { if (context == null) { return; } try { context.publishEvent(event); } catch (Exception ex) { log.error(ex.getMessage()); } } }
调用时机在bean初始化前 早于BeanFactoryPostProcessor 接口 ,具体源码位置如下:
BeanFactoryPostProcessor 接口
BeanFactory的后置处理器(beanFactory的扩展接口)
在BeanFactory组建完之后(注:组建完并不是指所有bean装载完) 可以对beanFactory里面的东西 比如beanDefinition相关属性 进行操作 (beanDefinition就位于beanFactory中)
调用时机在spring在读取beanDefinition信息之后,实例化bean之前。
BeanPostProcessor 接口
BeanPostProcessor提供了两个方法postProcessBeforeInitialization,postProcessAfterInitialization
postProcessBeforeInitialization: 在Bean的初始化之前调用
postProcessAfterInitialization: 在Bean的初始化之后调用 (bean后置处理器)
InstantiationAwareBeanPostProcessor 接口
该接口继承了BeanPostProcess接口,区别如下:
BeanPostProcess接口只在bean的初始化阶段进行扩展(注入spring上下文前后),而InstantiationAwareBeanPostProcessor接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段 执行时间略早于BeanPostProcess。public class TestInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // postProcessBeforeInstantiation()在Spring中Bean实例化前触发执行; System.out.println("[TestInstantiationAwareBeanPostProcessor] before initialization " + beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // postProcessAfterInstantiation()在Spring中Bean实例化后,属性注入前触发执行; System.out.println("[TestInstantiationAwareBeanPostProcessor] after initialization " + beanName); return bean; } @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] before instantiation " + beanName); return null; } @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { System.out.println("[TestInstantiationAwareBeanPostProcessor] after instantiation " + beanName); return true; } @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { // postProcessProperties()在Spring中Bean实例化后,属性注入前触发执行; System.out.println("[TestInstantiationAwareBeanPostProcessor] postProcessProperties" + beanName); return pvs; }
这个接口对于用户来说,使用较多的场景可能是收集bean,并对bean进行统一赋值了; 这个接口就可以在初始化时 getBean byType byName, 所以是个对bean进行统一处理的好时机。
SmartInstantiationAwareBeanPostProcessor接口
该扩展接口有3个触发点方法:
- predictBeanType:该触发点发生在postProcessBeforeInstantiation之前,这个方法用于预测Bean的类型,返回第一个预测成功的Class类型,如果不能预测返回null;当你调用BeanFactory.getType(name)时当通过bean的名字无法得到bean类型信息时就调用该回调方法来决定类型信息。
- determineCandidateConstructors:该触发点发生在postProcessBeforeInstantiation之后,用于确定该bean的构造函数之用,返回的是该bean的所有构造函数列表。用户可以扩展这个点,来自定义选择相应的构造器来实例化这个bean。
- getEarlyBeanReference:该触发点发生在postProcessAfterInstantiation之后,当有循环依赖的场景,当bean实例化好之后,为了防止有循环依赖,会提前暴露回调方法,用于bean实例化的后置处理。这个方法就是在提前暴露的回调方法中触发。
很多框架的代理数据源 就是实现了该接口。
BeanNameAware接口
如果实现了BeanNameAware接口 会执行setBeanName方法,
虽然是setBeanName, 但实际使用中 博主并不认为我们会经常去修改beanName,更多的用途可能是赋值给 thisBeanName,thisBeanName拿到beanName后进行更多的操作。
InitializingBean 接口 (@PostConstruct)
bean初始化前的一些操作 ,这个接口应该是非常常见了,里面可以写众多的初始化前配置、业务逻辑。
实现InitializingBean 和@PostConstruct都是类似的功能。public interface InitializingBean { // 实现该方法 进行些bean初始化前的操作 void afterPropertiesSet() throws Exception; }
CommandLineRunner和ApplicationRunner 接口
博主(csdn: 孟秋与你)之前写过一篇单独的文章介绍这两个接口,感兴趣可以在博主主页搜索 ApplicationRunner 。
这两个接口其实已经不属于bean的生命周期了范畴了,在项目启动完毕时执行,我知道这句话会有歧义,我们直接上源码:可以看到application.run()方法的最后一行代码:
这回应该不会有歧义了吧~这两个接口用于监听启动时项目外部传入的参数,但由于其执行时间特性,我们又比较少在启动时给项目传入参数,所以一定程度上 也可以用于业务“初始化”操作,比如希望某依赖关联较少的业务 在项目启动完就执行 。
拦截器
我们一般所说的拦截器,其实都是指接口层面的拦截器(类似filter),但是我们会发现 Interceptor 接口却是位于aop包下的,博主这里大致解释一下。
接口拦截器
HandlerInterceptor 接口
我们通常所理解的拦截器,都是指HandlerInterceptor及其延伸的子类,HandlerInterceptor 位于spring-webmvc包下。我们也可以自己实现该接口,对request进行拦截, 拦截了request 我们就可以获取到remoteAddr , requestURI , request Method, requestURI等等 , 从而满足我们的业务需求。
(需要调用WebMvcConfigurer实现类的addInterceptors方法才能生效,后文关于WebMvcConfigurer接口有示例)业务场景一般用于接口权限,下面给出一段不完整的代码 应该能看出思路
@Configuration public class HttpAuthInterceptor implements HandlerInterceptor { // R:url , C:method GET,POST,PUT... , V:roleIdList HashBasedTable<String, String, List<Long>> urlStrategy = InitUrlAuth.urlStrategy; // 进入controller方法之前 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取配置中 所有需要被拦截的url Set<String> urlSet = urlStrategy.rowKeySet(); for (String url : urlSet) { PathMatcher pathMatcher = new AntPathMatcher(); // 当前请求是否为需要拦截的接口 if (pathMatcher.match(url, request.getRequestURI())) { // 当前http请求方法 String method = request.getMethod(); // 获取当前接口配置的http方法、角色 Map<String, List<Long>> columnValueMap = urlStrategy.row(url); // 当前请求方法是否在配置中 ( ""表示拦截所有http方法 get ,post ,put...等) if (columnValueMap.containsKey(method) || columnValueMap.containsKey("")) { // 获取当前用户的角色 String userRole = AuthUtil.getUser().getRoleId(); List<String> roleIdList = Arrays.stream(userRole.split(",")).collect(Collectors.toList()); // 获取配置中 当前接口需要被哪些角色访问 List<Long> configRoleList = columnValueMap.get(method); if (configRoleList.stream().anyMatch(configRole -> roleIdList.contains(String.valueOf(configRole)))) { // 授予访问权限 return true; } } } } // 写入权限不足提示 ResponseProvider.write(response); return true; } }
我们如果要用于参数拦截,也是可以的,不过参数解析一般实现后文中提到的HandlerMethodArgumentResolver 接口会更简便,HandlerInterceptor 拦截参数的方式 博主也给出示例:
@Configuration public class CustomHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; // 获取方法名 String methodName = handlerMethod.getMethod().getName(); // 获取方法参数 Class<?>[] parameterTypes = handlerMethod.getMethod().getParameterTypes(); // 在这里可以对方法名、方法参数进行操作 System.out.println("Method Name: " + methodName); System.out.println("Parameter Types: " + parameterTypes); } return HandlerInterceptor.super.preHandle(request, response, handler); } }
这里就会产生一个疑问 为什么拦截器能拦截http请求呢? 我们知道拦截器是spring定义的,它并不是servlet的标准 不是filter, 为什么实现了HandlerInterceptor接口 就可以被拦截?
这里要回到一个经典的八股文,早些年问得比较多,尤其是问structs2和springmvc区别的面试题的时候,springmvc的入口其实是一个DispatcherServlet(它间接继承了HttpServlet)
既然所有请求都会进入到DispatcherServlet,那拦截器相关逻辑 在DispatcherServlet里面处理 就可以实现拦截了。
具体代码在DispatcherServlet类的doDispatch方法里面,处理拦截器链:HandlerExecutionChain ,以下是简化代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Apply preHandle methods of registered interceptors. if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // Apply postHandle methods of registered interceptors. mappedHandler.applyPostHandle(processedRequest, response, mv); }
WebRequestInterceptor 接口
WebRequestInterceptor 位于spring-web包下,从包路径我们也不难看出,WebRequestInterceptor可以适用于非mvc的环境下,不过我们基本很少遇到非mvc了。我们通过接口方法可以发现,WebRequestInterceptor 接口无法拦截request,日常开发中 我们自己拦截接口 通常都是实现HandlerInterceptor。
切面通知
博主把拦截器分为接口拦截器和切面拦截器, 切面拦截器我们通常叫切面 ,
Interceptor (拦截器)接口位于aop包下 就是实现的Advice接口的。我们先复习一下切面的5种通知(advice)类型:
- 前置通知(Before Advice):
在目标方法执行前执行的通知。
- 环绕通知(Around Advice):
在目标方法执行前后都能执行的通知,可以完全控制目标方法的执行过程。
- 返回后通知(After Returning Advice):
在目标方法成功执行并返回结果后执行的通知。
- 后置通知(After Advice):
无论目标方法执行成功还是抛出异常,都会执行的通知。(相当于异常中的finally)
- 异常通知(After Throwing Advice):
在目标方法抛出异常时执行的通知。
代码层级:
AbstractAspectJAdvice implements Advice ├── BeforeAdvice (前置) ├── AspectJAroundAdvice(环绕) ├── AfterAdvice ├ ├── AspectJAfterAdvice (后置) ├ ├── AspectJAfterReturningAdvice (返回后) ├ ├── AspectJAfterThrowingAdvice (异常)
其中 环绕,后置,异常通知,又还实现了一个接口:MethodInterceptor
Interceptor implements Advice├── ConstructorInterceptor (构造方法拦截器)
├── MethodInterceptor (方法拦截器 非常重要)
├ ├── AspectJAfterAdvice (后置)
├ ├── AspectJAroundAdvice (环绕)
├ ├── AspectJAfterThrowingAdvice (异常)MethodInterceptor 接口
定义了在目标方法执行前后,甚至可以完全控制目标方法执行过程的方法。
用通俗的话来说,HandlerInterceptor 用来拦截接口,而MethodInterceptor 则是用来拦截方法。ConstructorInterceptor 接口
与MethodInterceptor 类似,可以拦截构造方法,使得对象创建前 ,在构造方法里面执行某些逻辑。
mvc相关接口
WebMvcConfigurer接口 (重要)
这个接口大家应该非常熟悉了,convert、跨域、addInterceptors,包括HandlerMethodArgumentResolver 等配置,都是要实现该接口 调用相关的add方法,才会生效的。
@Configuration public class WechatAppWebMvcConfig implements WebMvcConfigurer { // ZonedDateTimeConverter implements Converter<String, ZonedDateTime> @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new ZonedDateTimeConverter()); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new WechatAppAuthInterceptor()) .addPathPatterns("/app/**") .excludePathPatterns("/test/**") .excludePathPatterns("/public/**") ; } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { // MyMethodArgumentResolver implements HandlerMethodArgumentResolver resolvers.add(new MyMethodArgumentResolver()); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedHeaders("*") .allowedMethods("*") .maxAge(3600) .allowCredentials(true); }
参数相关
方法参数解析器
HandlerMethodArgumentResolver 接口
(注:路径为 org.springframework.web.method.support.HandlerMethodArgumentResolver )
与HandlerInterceptor 有点类似,都是作用于http请求时,不过HandlerMethodArgumentResolver 解析器专注于对方法参数进行解析,当然 我们使用HandlerInterceptor 来解析参数也是可以的 步骤稍微复杂了一点点。
执行先后顺序 HandlerInterceptor > HandlerMethodArgumentResolver/** * 注意在WebMvcConfigurer实现类中调用addArgumentResolvers */ @Configuration public class CustomHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter methodParameter) { // 在 supportsParameter 方法中,确保对你要解析的参数类型返回 true,(实际应该替换为自己的业务校验) // 以便告诉 Spring 这个解析器支持解析这个参数。 return true; } @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { Method method = methodParameter.getMethod(); Class<?> parameterType = methodParameter.getParameterType(); // 本例中 入参所在方法: public String test( String ...strings) String strings = nativeWebRequest.getParameter("strings"); String[] arr = strings.split(","); // 返回值类型要与原始的参数类型保持一致 return arr; } }
方法返回值处理
HandlerMethodReturnValueHandler 接口
与HandlerMethodArgumentResolver 是孪生兄弟关系,一个负责入参时的处理, 一个负责返回时的处理
需要在WebMvcConfigurer实现类中调用addReturnValueHandlers方法HandlerMethodReturnValueHandler 接口代码:
public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
参数类型转换器
Convert接口
例如: 将ZonedDateTime参数转换成String,这通常都是前后端交互时的参数类型转换
public class ZonedDateTimeConverter implements Converter<String, ZonedDateTime> { // 需要在WebMvcConfigurer的实现类中调用addFormatters方法 @Override public ZonedDateTime convert(String source) { if (Func.isEmpty(source)) { return null; } long parseLong = Long.parseLong(source); return Instant.ofEpochMilli(parseLong).atZone(ZoneId.systemDefault()); } }
监听器
ApplicationListener接口
博主也有专门讲ApplicationListener的博客,主要是监听事件,实现一个简单的发布-订阅功能。事件需要是ApplicationEvent的子类
最后,再次安利博主的原创idea轻量级小插件: equals inspection
用于解决Objects.equals方法传参容易误传的问题,欢迎各位下载该免费插件~