实现接口代理增强,并动态注入到IOC容器中(类似于Feign或Mybatis的Mapper)

2022-08-13 11:45:45

前言

  • 最近遇到这么一个需求:需要调用很多某第三方的api接口,然后将结果进一步处理后返回给前端;有点像nginx的反向代理,但是由于第三方接口的ip地址不固定(需要根据当前用户信息找到对应的ip),所以想了下还是只能通过代码来实现;
  • 因为会发起若干的网络请求,代码都类似(设置url、设置请求方式、设置请求参数、接收响应结果、打印日志等),所以就会编写很多重复的代码,而这些代码又都是非业务代码,所以我想到了AOP,想到了Feign,想到了Mybatis的Mapper;
  • Feign我不太熟,只是用过,体验过那种只需编写一个接口类,加一些SpringMVC的注解,就可以发起网络请求的爽快感觉;
  • 此时我就心想,能不能自己实现一个类似于Feign的网络请求封装(对接口类进行代理增强);
  • 试过用AOP实现,发现AOP好像只能代理增强某个class,代理增强interface好像不得行,feign我不太熟嘛,我就心想Mybatis的Mapper接口是怎么做到代理增强的?并且还注入到了IOC容器中?
  • 带着问题百度了一翻,大概了解到Mybatis是用到了JDK动态代理,再加上实现Spring的BeanDefinitionRegistryPostProcessor,将代理类注入到了IOC容器中;(估计Feign也是这样的原理,后面有空去看看)
  • 实现思路大概想清楚了(JDK代理 + BeanDefinitionRegistryPostProcessor),下面是代码实现;

项目结构

项目结构

  • 红框中为四个主要的类;作用就是扫描指定包下的interface,并创建动态代理类,然后注入到IOC容器中;

代码

1.ApiLoginService:
package demo.proxy.service.api;import demo.proxy.common.annotation.ApiURL;import demo.proxy.common.annotation.JsonParamData;import demo.proxy.common.annotation.JsonParamTimestamp;import demo.proxy.common.base.JsonResult;import demo.proxy.model.LoginData;import org.springframework.stereotype.Service;import org.springframework.web.bind.annotation.RequestMethod;/**
 * 第三方登录
 * 该interface会被ApiServiceScanner扫描到,动态生成代理类,然后注入到IOC容器中
 * @author xiesq
 * @version 1.0
 * @date 2021/6/24 15:28
 */@ServicepublicinterfaceApiLoginService{/**
     * 第三方登录接口
     */staticfinal String API_LOGIN_URL="/api/mobile/auth/login";/**
     * 第三方登录
     * 标记一些请求参数信息,方便通过反射获取到并发起相应的网络请求
     * 
     * @param loginData 登录参数
     */@ApiURL(value= API_LOGIN_URL, requestMethod= RequestMethod.POST)public JsonResultlogin(@JsonParamData LoginData loginData);}

2.ApiServiceScannerConfig:扫描指定的包,并注册到spring中
package demo.proxy.common.config;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;import org.springframework.stereotype.Component;/**
 * 动态扫描并注入interface
 *
 * @author xiesq
 * @version 1.0
 * @date 2021/6/27 22:38
 */@ComponentpublicclassApiServiceScannerConfigimplementsBeanDefinitionRegistryPostProcessor{/**
     * 需要被代理增强的interface所在的包
     */privatestaticfinal String BASE_PACKAGE="demo.proxy.service.api";@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry)throws BeansException{//指定自定义的扫描器进行扫描 并会自动调用beanDefinitionRegistry进行注册
        ApiServiceScanner apiServiceScanner=newApiServiceScanner(beanDefinitionRegistry);
        apiServiceScanner.scan(BASE_PACKAGE);}@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)throws BeansException{}}

3.ApiServiceScanner:自定义的bean扫描器
为每个BeanDefinition指定一个bean工厂(ApiServiceFactory)来产生代理对象;
package demo.proxy.common.config;import lombok.extern.slf4j.Slf4j;import org.mybatis.spring.mapper.ClassPathMapperScanner;import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;import org.springframework.beans.factory.config.BeanDefinitionHolder;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.beans.factory.support.GenericBeanDefinition;import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;import java.util.Set;/**
 * 自定义的bean扫描器,
 * 主要是重写doScan方法,增加扫描后的自定义逻辑(如何生成接口对应的代理对象)
 * @author xiesq
 * @version 1.0
 * @date 2021/6/27 22:31
 */@Slf4jpublicclassApiServiceScannerextendsClassPathBeanDefinitionScanner{publicApiServiceScanner(BeanDefinitionRegistry registry){super(registry);}@Overrideprotected Set<BeanDefinitionHolder>doScan(String... basePackages){
        Set<BeanDefinitionHolder> beanDefinitionHolders=super.doScan(basePackages);//扫描后的预处理processBeanDefinitions(beanDefinitionHolders);return beanDefinitionHolders;}/**
     * 对扫描到的interface设置实例工厂(ApiServiceFactory)
     *
     * 参考: ClassPathMapperScanner#processBeanDefinitions(Set)
     * @see ClassPathMapperScanner#doScan(String...)
     */privatevoidprocessBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitionHolders){
        GenericBeanDefinition beanDefinition;for(BeanDefinitionHolder holder: beanDefinitionHolders){
            beanDefinition=(GenericBeanDefinition) holder.getBeanDefinition();//在这里,我们可以给该对象的属性注入对应的实例。//比如mybatis,就在这里注入了dataSource和sqlSessionFactory,// 注意,如果采用definition.getPropertyValues()方式的话,// 类似definition.getPropertyValues().add("interfaceType", beanClazz);// 则要求在FactoryBean(本应用中即ServiceFactory)提供setter方法,否则会注入失败// 如果采用definition.getConstructorArgumentValues(),// 则FactoryBean中需要提供包含该属性的构造方法,否则会注入失败try{
                Class<?> interfaceType= Class.forName(beanDefinition.getBeanClassName());
                beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(interfaceType);}catch(ClassNotFoundException e){
                e.printStackTrace();}//注意,这里的BeanClass是生成Bean实例的工厂,不是Bean本身。// FactoryBean是一种特殊的Bean,其返回的对象不是指定类的一个实例,// 其返回的是该工厂Bean的getObject方法所返回的对象。
            beanDefinition.setBeanClass(ApiServiceFactory.class);//设置为byType方式注入
            beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);}}/**
     * 注册一些过滤器
     * 参考:{@link ClassPathMapperScanner#registerFilters()}
     */publicvoidregisterFilters(){}/**
     * 扫描且只扫描接口
     * 不重写此方法的话会扫描不出来
     * 参考: ClassPathMapperScanner#isCandidateComponent(AnnotatedBeanDefinition)
     */@OverrideprotectedbooleanisCandidateComponent(AnnotatedBeanDefinition beanDefinition){return beanDefinition.getMetadata().isInterface();}}
4.ApiServiceFactory:bean的实例化工厂
通过JDK代理产生一个代理对象;
package demo.proxy.common.config;import org.springframework.beans.factory.FactoryBean;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;/**
 * 自定义interface动态实例 的工厂
 *
 * @author xiesq
 * @version 1.0
 * @date 2021/6/27 22:46
 */publicclassApiServiceFactory<T>implementsFactoryBean<T>{privatefinal Class<T> interfaceType;publicApiServiceFactory(Class<T> interfaceType){this.interfaceType= interfaceType;}@Overridepublic TgetObject()throws Exception{//这里主要是创建接口对应的实例,便于注入到spring容器中
        InvocationHandler handler=newApiServiceProxy(interfaceType);return(T) Proxy.newProxyInstance(interfaceType.getClassLoader(),newClass[]{interfaceType}, handler);}@Overridepublic Class<T>getObjectType(){return interfaceType;}@OverridepublicbooleanisSingleton(){returntrue;}}
5.ApiServiceProxyJDK动态代理 的具体增强逻辑,
这里我根据目标方法上的注解标记,获取发起网络请求的一些必要参数(url、请求方式、请求参数等),然后再根据当前登录用户获取到对应的baseURL,最后发起网络请求,打印日志,返回结果。
package demo.proxy.common.config;import demo.proxy.common.annotation.ApiURL;import demo.proxy.common.annotation.JsonParamData;import demo.proxy.common.annotation.JsonParamTimestamp;import demo.proxy.common.annotation.UrlParam;import demo.proxy.common.base.JsonParam;import demo.proxy.common.base.JsonResult;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpEntity;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Component;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.client.RestTemplate;import java.lang.annotation.Annotation;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Parameter;import java.util.HashMap;import java.util.Map;/**
 * 自定义interface 动态代理 增强逻辑
 *
 * @author xiesq
 * @version 1.0
 * @date 2021/6/27 22:49
 */@Slf4j@ComponentpublicclassApiServiceProxyimplementsInvocationHandler{/**
     * restTemplate 通过set方法注入
     */privatestatic RestTemplate restTemplate;private Class<?> interfaceType;publicApiServiceProxy(Class<?> interfaceType){this.interfaceType= interfaceType;}publicApiServiceProxy(){}@Overridepublic Objectinvoke(Object proxy, Method method, Object[] args)throws Throwable{//return method.invoke(this, args);//这里可以得到参数数组和方法等,可以通过反射,注解等,进行结果集的处理//mybatis就是在这里获取参数和相关注解,然后根据返回值类型,进行结果集的转换returnsendRequestByAnnotationInfo(method, args);}/**
     * 请求第三方接口 根据方法上的注解信息
     *
     * @return {@link JsonResult}
     * @see ApiURL
     * @see JsonParamData
     * @see JsonParamTimestamp
     * @see UrlParam
     */private ObjectsendRequestByAnnotationInfo(Method method, Object[] args){
        ApiURL apiUrlAnnotation= method.getAnnotation(ApiURL.class);if(apiUrlAnnotation== null|| StringUtils.isEmpty(apiUrlAnnotation.value())){
            log.error("缺少@ApiURL信息");return null;}//第三方api地址
        String apiUrl=baseUrl()+ apiUrlAnnotation.value();
        log.info("请求地址:{}", apiUrl);
        Map<String, Object> urlParams=getUrlParams(method, args);
        urlParams.forEach((key, value)-> log.info("url param {}:{}", key, value));//根据请求类型 发起请求
        ResponseEntity<JsonResult> responseEntity;if(apiUrlAnnotation.requestMethod()== RequestMethod.POST){
            Object data=getParamValueByAnnotation(method, args, JsonParamData.class);
            String timestamp=getJsonParamTimestamp(method, args);
            log.info("json data:{}", data);//post
            responseEntity=postRequest(apiUrl, urlParams, data, timestamp);}elseif(apiUrlAnnotation.requestMethod()== RequestMethod.GET){//get
            responseEntity=getRequest(apiUrl, urlParams);}else{
            log.error("不支持的请求方式,{}", apiUrl);return null;}

        log.info("响应码:{}", responseEntity.getStatusCodeValue());
        log.info("业务响应体:{}", responseEntity.getBody());return responseEntity.getBody();}/**
     * 获取url参数
     */privatestatic Map<String, Object>getUrlParams(Method method, Object[] paramValues){
        Map<String, Object> urlParams=newHashMap<>(4);

        Parameter[] parameters= method.getParameters();for(int i=0; i< parameters.length; i++){if(parameters[i].isAnnotationPresent(UrlParam.class)){if(parameters[i].getType()== Map.class){//如果是Map对象,则直接返回return(Map) paramValues[i];}
                urlParams.put(parameters[i].getName(), paramValues[i]);}}return urlParams;}/**
     * 获取timestamp参数值
     */privatestatic StringgetJsonParamTimestamp(Method method, Object[] paramValues){
        Object timestamp=getParamValueByAnnotation(method, paramValues, JsonParamTimestamp.class);if(timestamp== null){return null;}elseif(timestampinstanceofString){return(String) timestamp;}else{//timestamp只能是string类型thrownewIllegalArgumentException("参数类型错误:timestamp只能是String类型");}}/**
     * 获取被@JsonParamData标记的参数值
     */privatestatic ObjectgetParamValueByAnnotation(Method method, Object[] paramValues, Class<?extendsAnnotation> clazz){

        Parameter[] parameters= method.getParameters();for(int i=0; i< parameters.length; i++){if(parameters[i].isAnnotationPresent(clazz)){return paramValues[i];}}return null;}/**
     * get请求
     */privatestatic ResponseEntity<JsonResult>getRequest(String apiUrl, Map<String, Object> urlParams){return restTemplate.getForEntity(apiUrl, JsonResult.class, urlParams);}/**
     * POST请求
     */privatestatic ResponseEntity<JsonResult>postRequest(String apiUrl, Map<String, Object> urlParams, Object data, String timestamp){//构建请求体
        JsonParam jsonParam;if(timestamp!= null){
            jsonParam=newJsonParam(data, timestamp);}else{
            jsonParam=newJsonParam(data);}
        HttpEntity<JsonParam> httpEntity=newHttpEntity<>(jsonParam);//发起请求return restTemplate.postForEntity(
                apiUrl,
                httpEntity,
                JsonResult.class,
                urlParams);}/**
     * 获取baseUrl
     */private StringbaseUrl(){//根据当前登录用户,返回对应的接口ip地址return"http://localhost:8080";}@AutowiredpublicvoidsetRestTemplate(RestTemplate restTemplate){
        ApiServiceProxy.restTemplate= restTemplate;}}

小总结

  1. 利用JDK代理为interface动态创建实例,从而实现动态增强(特殊的动态代理?代理模式的妙用?🙀);
  2. 通过实现BeanDefinitionRegistryPostProcessor接口,对beanDefinition进行预处理,自定义bean实例化的逻辑(对spring bean生命周期又有了新的认识);
  3. 重复代码还是能不写就不写的😂;

参考文章

  • 作者:xsq-
  • 原文链接:https://blog.csdn.net/qq_40518157/article/details/118329330
    更新时间:2022-08-13 11:45:45