自定义注解实现RPC远程调用

源码地址:https://github.com/huangyichun/remotetransfer

本文涉及知识点:

  • 自定义注解
  • 动态代理
  • Spring bean加载
  • Java 8 优化的策略模式

项目背景:由于原直播系统采用SpringCloud部署在虚拟机(仅使用SpringCloud的注册中心),现打算使用k8s部署,由于k8s和SpringCloud功能很多重合了,如果在k8s系统使用SpringCloud将无法用到k8s的注册中心、负载均衡等功能,而且项目部署也过臃肿。 因此有2种方案:

  • 方案一

    采用spring-cloud-starter-kubernetes 组件替换SpringCloud,该方案存在一个问题,研发需要熟悉k8s功能,而且未来项目必须使用k8s部署。

  • 方案二

    采用Http请求替换SpringCloud的注册中心Feign调用。由于接口较多,如果每个接口都改动工作量较大,也不利于代码维护。因此使用自定义注解来替换@FeignClient注册。

具体实现:

  • 创建自定义注解

    /**
     * 远程调用,替换SpringCloud Feign
     * @Author: huangyichun
     * @Date: 2021/2/22
     */
    @Target(ElementType.TYPE)
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RemoteTransfer {
    
        String hostName();
    }
    
  • 定义handler接口用于处理实际业务逻辑

    /**
     * @Author: huangyichun
     * @Date: 2021/2/24
     */
    public interface RemoteTransferHandler {
    
        Object handler(String host, Method method, Object[] args);
    }
    
  • 采用反射动态创建类,并注册到Spring容器中

创建 RemoteTransferRegister类,并实现BeanFactoryPostProcessor接口,该接口的方法postProcessBeanFactory是在bean被实例化之前被调用的。这样保证了自定义注解声明的Bean能被其他模块Bean依赖。

/**
 * @Author: huangyichun
 * @Date: 2021/2/23
 */
@Slf4j
@Component
public class RemoteTransferRegister implements BeanFactoryPostProcessor {

    /**
     * 设置扫描的包
     */
    private static final String SCAN_PATH = "com.huang.web";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        RemoteTransferInvoke transferInvoke = new RemoteTransferInvoke(invokeRestTemplate());

        //扫描注解声明类
        Set<Class<?>> classSet = new Reflections(SCAN_PATH).getTypesAnnotatedWith(RemoteTransfer.class);
        for (Class<?> cls : classSet) {
            log.info("create proxy class name:{}", cls.getName());

            //动态代理的handler
            InvocationHandler handler = (proxy, method, args) -> transferInvoke.invoke(cls, method, args);
            //生成代理类
            Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[]{cls}, handler);

            //注册到Spring容器
            beanFactory.registerSingleton(cls.getName(), proxy);
        }
    }
  
  private RestTemplate invokeRestTemplate() {
       //生成restTemplate 省略代码 
  }
}
  • 创建RemoteTransferInvoke类,处理动态代理类的请求

    支持解析@GetMapping、@PostMapping、@PutMapping、以及@DeleteMapping。使用策略模式+Map优化 if else 语句。具体代码如下

    @Slf4j
    @Data
    public class RemoteTransferInvoke {
    
        private final RestTemplate restTemplate;
    
        /**
         * 用于实际远程调用处理
         */
        private Map<Class<? extends Annotation>, RemoteTransferHandler> requestMethodMap = new HashMap<>();
    
        public RemoteTransferInvoke(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
            init();
        }
    
        /**
         * 使用策略模式+Map 去除
         */
        private void init() {
            requestMethodMap.put(GetMapping.class, (host, method, args) -> {
                GetMapping methodAnnotation = method.getAnnotation(GetMapping.class);
                String path = methodAnnotation.value()[0];
                return getOrDeleteMapping(method, args, host, path, HttpMethod.GET);
            });
    
            requestMethodMap.put(PutMapping.class, (host, method, args) -> {
                PutMapping methodAnnotation = method.getAnnotation(PutMapping.class);
                String path = methodAnnotation.value()[0];
                return putOrPostMapping(method, args, host, path, HttpMethod.PUT);
            });
    
    
            requestMethodMap.put(PostMapping.class, (host, method, args) -> {
                PostMapping methodAnnotation = method.getAnnotation(PostMapping.class);
                String path = methodAnnotation.value()[0];
                return putOrPostMapping(method, args, host, path, HttpMethod.POST);
            });
            requestMethodMap.put(DeleteMapping.class, (host, method, args) -> {
                DeleteMapping methodAnnotation = method.getAnnotation(DeleteMapping.class);
                String path = methodAnnotation.value()[0];
                return getOrDeleteMapping(method, args, host, path, HttpMethod.DELETE);
            });
        }
    
    
        /**
         * 动态代理调用方法
         * @param tClass 类
         * @param method 方法
         * @param args 请求参数
         * @return 返回值
         */
        public Object invoke(Class<?> tClass, Method method, Object[] args) {
            RemoteTransfer remoteAnnotation = tClass.getAnnotation(RemoteTransfer.class);
            String host = RemoteTransferConfig.map.get(remoteAnnotation.hostName());
    
            Annotation[] annotations = method.getAnnotations();
            Optional<Annotation> first = Arrays.stream(annotations).filter(annotation1 -> requestMethodMap.containsKey(annotation1.annotationType())).findFirst();
            Preconditions.checkArgument(first.isPresent(), "注解使用错误");
    
            Annotation methodAnnotation = first.get();
            
            return requestMethodMap.get(methodAnnotation.annotationType()).handler(host, method, args);
        }
    
    
        private Object putOrPostMapping(Method method, Object[] args, String host, String url, HttpMethod httpMethod) {
            url = RemoteTransferUtil.dealPathVariable(method, args, url);
            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(host + url);
    
            HttpHeaders httpHeaders = new HttpHeaders();
            HttpEntity<JSONObject> entity = new HttpEntity<>(RemoteTransferUtil.extractBody(method, args), httpHeaders);
            ResponseEntity<?> exchange = restTemplate.exchange(builder.toUriString(), httpMethod, entity, method.getReturnType());
            return exchange.getBody();
        }
    
        private Object getOrDeleteMapping(Method method, Object[] args, String host, String url, HttpMethod httpMethod) {
    
            UriComponentsBuilder builder = buildGetUrl(method, args, host, url);
            HttpHeaders httpHeaders = new HttpHeaders();
            HttpEntity<JSONObject> entity = new HttpEntity<>(null, httpHeaders);
            return this.restTemplate.exchange(builder.toUriString(), httpMethod, entity, method.getReturnType()).getBody();
        }
    
    
        private UriComponentsBuilder buildGetUrl(Method method, Object[] args, String host, String url) {
            url = RemoteTransferUtil.dealPathVariable(method, args, url);
            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(host + url);
    
            for (Map.Entry<String, String> entry : RemoteTransferUtil.extractParams(method, args).entrySet()) {
                builder.queryParam(entry.getKey(), entry.getValue());
            }
            return builder;
        }
    
  • 配置类

    用于获取注解的host地址

    @Component
    public class RemoteTransferConfig {
    
        public static Map<String, String> map = new HashMap<>();
    
        @Value("${remote.transfer.host}")
        private String port;
    
        @PostConstruct
        public void init() {
            map.put("TRADE-PLAY", port);
        }
    }
    
  • 工具类

    /**
     * @Author: huangyichun
     * @Date: 2021/2/23
     */
    @Slf4j
    public class RemoteTransferUtil {
    
        /**
         * 获取Path参数
         * @param method 方法
         * @param args 值
         * @return
         */
        public static Map<String, String> extractPath(Method method, Object[] args) {
            Map<String, String> params = new HashMap<>();
            Parameter[] parameters = method.getParameters();
    
            if (parameters.length == 0) {
                return params;
            }
    
            for (int i = 0; i < parameters.length; i++) {
                PathVariable param = parameters[i].getAnnotation(PathVariable.class);
                if (param != null) {
                    params.put(param.value(), String.valueOf(args[i]));
                }
            }
            return params;
        }
    
        /**
         * 获取请求body
         * @param method
         * @param args
         * @return
         */
        public static JSONObject extractBody(Method method, Object[] args) {
            JSONObject object = new JSONObject();
            Parameter[] parameters = method.getParameters();
            if (parameters.length == 0) {
                return null;
            }
    
            for (int i = 0; i < parameters.length; i++) {
                RequestBody param = parameters[i].getAnnotation(RequestBody.class);
                if (param != null) {
                    String returnStr = JSON.toJSONString(args[i], SerializerFeature.WriteMapNullValue, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteDateUseDateFormat);
                    object = JSONObject.parseObject(returnStr);
                }
            }
            return object;
        }
    
        /**
         * 处理url的Path
         * @param method 方法
         * @param args 值
         * @param url url
         * @return
         */
        public static String dealPathVariable(Method method, Object[] args, String url) {
            for (Map.Entry<String, String> entry : RemoteTransferUtil.extractPath(method, args).entrySet()) {
                if (url.contains("{" + entry.getKey() + "}")) {
                    url = url.replace("{" + entry.getKey() + "}", entry.getValue());
                }
            }
            return url;
        }
    
    
        /**
         * 处理请求参数
         * @param method 方法
         * @param args 值
         * @return
         */
        public static LinkedHashMap<String, String> extractParams(Method method, Object[] args) {
            LinkedHashMap<String, String> params = new LinkedHashMap<>();
            Parameter[] parameters = method.getParameters();
    
            if (parameters.length == 0) {
                return params;
            }
    
            for (int i = 0; i < parameters.length; i++) {
                RequestParam param = parameters[i].getAnnotation(RequestParam.class);
                if (param != null) {
                    params.put(param.value(), String.valueOf(args[i]));
                }
            }
            return params;
        }
    }
    

注解的具体使用:

@RemoteTransfer(hostName = "TRADE-PLAY")
public interface TradePlayServiceApi {

     @GetMapping("/open/get")
     String test(@RequestParam("type") String type);

     @PostMapping("/open/post")
     ResponseResult post(@RequestBody PostRequest request);
}

测试类:

@SpringBootTest
class WebApplicationTests {

    @Autowired
    private TradePlayServiceApi tradePlayServiceApi;

    @Test
    public void get() {
        String type = tradePlayServiceApi.test("type");
        System.out.println(type);
    }

    @Test
    public void post() {
        PostRequest request = new PostRequest();
        request.setId("id");
        PostRequest.Person person = new PostRequest.Person("name", "age");

        request.setPerson(person);
        ResponseResult post = tradePlayServiceApi.post(request);
        System.out.println(post);
    }
}

最终测试通过,请求正常返回。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352