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

2 篇文章 0 订阅
1 篇文章 0 订阅


前言

  • 最近遇到这么一个需求:需要调用很多某第三方的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
 */
@Service
public interface ApiLoginService {
    /**
     * 第三方登录接口
     */
    static final String API_LOGIN_URL = "/api/mobile/auth/login";

    /**
     * 第三方登录
     * 标记一些请求参数信息,方便通过反射获取到并发起相应的网络请求
     * 
     * @param loginData 登录参数
     */
    @ApiURL(value = API_LOGIN_URL, requestMethod = RequestMethod.POST)
    public JsonResult login(@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
 */
@Component
public class ApiServiceScannerConfig implements BeanDefinitionRegistryPostProcessor {

    /**
     * 需要被代理增强的interface所在的包
     */
    private static final String BASE_PACKAGE = "demo.proxy.service.api";

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        //指定自定义的扫描器进行扫描 并会自动调用beanDefinitionRegistry进行注册
        ApiServiceScanner apiServiceScanner = new ApiServiceScanner(beanDefinitionRegistry);
        apiServiceScanner.scan(BASE_PACKAGE);
    }


    @Override
    public void postProcessBeanFactory(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
 */
@Slf4j
public class ApiServiceScanner extends ClassPathBeanDefinitionScanner {

    public ApiServiceScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        //扫描后的预处理
        processBeanDefinitions(beanDefinitionHolders);

        return beanDefinitionHolders;
    }

    /**
     * 对扫描到的interface设置实例工厂(ApiServiceFactory)
     *
     * 参考: ClassPathMapperScanner#processBeanDefinitions(Set)
     * @see ClassPathMapperScanner#doScan(String...)
     */
    private void processBeanDefinitions(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()}
     */
    public void registerFilters(){
    }

    /**
     * 扫描且只扫描接口
     * 不重写此方法的话会扫描不出来
     * 参考: ClassPathMapperScanner#isCandidateComponent(AnnotatedBeanDefinition)
     */
    @Override
    protected boolean isCandidateComponent(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
 */
public class ApiServiceFactory<T> implements FactoryBean<T> {

    private final Class<T> interfaceType;

    public ApiServiceFactory(Class<T> interfaceType) {
        this.interfaceType = interfaceType;
    }

    @Override
    public T getObject() throws Exception {
        //这里主要是创建接口对应的实例,便于注入到spring容器中
        InvocationHandler handler = new ApiServiceProxy(interfaceType);
        return (T) Proxy.newProxyInstance(interfaceType.getClassLoader(),
                new Class[]{interfaceType}, handler);
    }

    @Override
    public Class<T> getObjectType() {
        return interfaceType;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}
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
@Component
public class ApiServiceProxy implements InvocationHandler {

    /**
     * restTemplate 通过set方法注入
     */
    private static RestTemplate restTemplate;
    private Class<?> interfaceType;

    public ApiServiceProxy(Class<?> interfaceType) {
        this.interfaceType = interfaceType;
    }

    public ApiServiceProxy() {
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //return method.invoke(this, args);
        //这里可以得到参数数组和方法等,可以通过反射,注解等,进行结果集的处理
        //mybatis就是在这里获取参数和相关注解,然后根据返回值类型,进行结果集的转换
        return sendRequestByAnnotationInfo(method, args);
    }

    /**
     * 请求第三方接口 根据方法上的注解信息
     *
     * @return {@link JsonResult}
     * @see ApiURL
     * @see JsonParamData
     * @see JsonParamTimestamp
     * @see UrlParam
     */
    private Object sendRequestByAnnotationInfo(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);
        } else if (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参数
     */
    private static Map<String, Object> getUrlParams(Method method, Object[] paramValues) {
        Map<String, Object> urlParams = new HashMap<>(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参数值
     */
    private static String getJsonParamTimestamp(Method method, Object[] paramValues) {
        Object timestamp = getParamValueByAnnotation(method, paramValues, JsonParamTimestamp.class);
        if (timestamp == null) {
            return null;
        } else if (timestamp instanceof String) {
            return (String) timestamp;
        } else {
            //timestamp只能是string类型
            throw new IllegalArgumentException("参数类型错误:timestamp只能是String类型");
        }
    }

    /**
     * 获取被@JsonParamData标记的参数值
     */
    private static Object getParamValueByAnnotation(Method method, Object[] paramValues, Class<? extends Annotation> clazz) {

        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].isAnnotationPresent(clazz)) {
                return paramValues[i];
            }
        }

        return null;
    }

    /**
     * get请求
     */
    private static ResponseEntity<JsonResult> getRequest(String apiUrl, Map<String, Object> urlParams) {
        return restTemplate.getForEntity(apiUrl, JsonResult.class, urlParams);
    }

    /**
     * POST请求
     */
    private static ResponseEntity<JsonResult> postRequest(String apiUrl, Map<String, Object> urlParams, Object data, String timestamp) {
        //构建请求体
        JsonParam jsonParam;
        if (timestamp != null) {
            jsonParam = new JsonParam(data, timestamp);
        } else {
            jsonParam = new JsonParam(data);
        }
        HttpEntity<JsonParam> httpEntity = new HttpEntity<>(jsonParam);

        //发起请求
        return restTemplate.postForEntity(
                apiUrl,
                httpEntity,
                JsonResult.class,
                urlParams);
    }

    /**
     * 获取baseUrl
     */
    private String baseUrl() {
        //根据当前登录用户,返回对应的接口ip地址
        return "http://localhost:8080";
    }

    @Autowired
    public void setRestTemplate(RestTemplate restTemplate) {
        ApiServiceProxy.restTemplate = restTemplate;
    }
}


小总结

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

参考文章

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值