【spring】spring常用接口分析,看完等效提升几年工作经验

前言:我们知道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方法传参容易误传的问题,欢迎各位下载该免费插件~在这里插入图片描述

  • 17
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟秋与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值