自定義注解實(shí)現(xiàn)RPC遠(yuǎn)程調(diào)用

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

本文涉及知識(shí)點(diǎn):

  • 自定義注解
  • 動(dòng)態(tài)代理
  • Spring bean加載
  • Java 8 優(yōu)化的策略模式

項(xiàng)目背景:由于原直播系統(tǒng)采用SpringCloud部署在虛擬機(jī)(僅使用SpringCloud的注冊(cè)中心)车胡,現(xiàn)打算使用k8s部署恨搓,由于k8s和SpringCloud功能很多重合了方仿,如果在k8s系統(tǒng)使用SpringCloud將無(wú)法用到k8s的注冊(cè)中心、負(fù)載均衡等功能都弹,而且項(xiàng)目部署也過(guò)臃腫。 因此有2種方案:

  • 方案一

    采用spring-cloud-starter-kubernetes 組件替換SpringCloud匙姜,該方案存在一個(gè)問(wèn)題畅厢,研發(fā)需要熟悉k8s功能,而且未來(lái)項(xiàng)目必須使用k8s部署氮昧。

  • 方案二

    采用Http請(qǐng)求替換SpringCloud的注冊(cè)中心Feign調(diào)用框杜。由于接口較多浦楣,如果每個(gè)接口都改動(dòng)工作量較大,也不利于代碼維護(hù)咪辱。因此使用自定義注解來(lái)替換@FeignClient注冊(cè)振劳。

具體實(shí)現(xiàn):

  • 創(chuàng)建自定義注解

    /**
     * 遠(yuǎn)程調(diào)用,替換SpringCloud Feign
     * @Author: huangyichun
     * @Date: 2021/2/22
     */
    @Target(ElementType.TYPE)
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RemoteTransfer {
    
        String hostName();
    }
    
  • 定義handler接口用于處理實(shí)際業(yè)務(wù)邏輯

    /**
     * @Author: huangyichun
     * @Date: 2021/2/24
     */
    public interface RemoteTransferHandler {
    
        Object handler(String host, Method method, Object[] args);
    }
    
  • 采用反射動(dòng)態(tài)創(chuàng)建類油狂,并注冊(cè)到Spring容器中

創(chuàng)建 RemoteTransferRegister類历恐,并實(shí)現(xiàn)BeanFactoryPostProcessor接口,該接口的方法postProcessBeanFactory是在bean被實(shí)例化之前被調(diào)用的专筷。這樣保證了自定義注解聲明的Bean能被其他模塊Bean依賴弱贼。

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

    /**
     * 設(shè)置掃描的包
     */
    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());

            //動(dòng)態(tài)代理的handler
            InvocationHandler handler = (proxy, method, args) -> transferInvoke.invoke(cls, method, args);
            //生成代理類
            Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[]{cls}, handler);

            //注冊(cè)到Spring容器
            beanFactory.registerSingleton(cls.getName(), proxy);
        }
    }
  
  private RestTemplate invokeRestTemplate() {
       //生成restTemplate 省略代碼 
  }
}
  • 創(chuàng)建RemoteTransferInvoke類,處理動(dòng)態(tài)代理類的請(qǐng)求

    支持解析@GetMapping磷蛹、@PostMapping吮旅、@PutMapping、以及@DeleteMapping味咳。使用策略模式+Map優(yōu)化 if else 語(yǔ)句庇勃。具體代碼如下

    @Slf4j
    @Data
    public class RemoteTransferInvoke {
    
        private final RestTemplate restTemplate;
    
        /**
         * 用于實(shí)際遠(yuǎn)程調(diào)用處理
         */
        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);
            });
        }
    
    
        /**
         * 動(dòng)態(tài)代理調(diào)用方法
         * @param tClass 類
         * @param method 方法
         * @param args 請(qǐng)求參數(shù)
         * @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(), "注解使用錯(cuò)誤");
    
            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參數(shù)
         * @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;
        }
    
        /**
         * 獲取請(qǐng)求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;
        }
    
    
        /**
         * 處理請(qǐng)求參數(shù)
         * @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);
}

測(cè)試類:

@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);
    }
}

最終測(cè)試通過(guò),請(qǐng)求正常返回莺葫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末匪凉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捺檬,更是在濱河造成了極大的恐慌再层,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堡纬,死亡現(xiàn)場(chǎng)離奇詭異聂受,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)烤镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)蛋济,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人炮叶,你說(shuō)我怎么就攤上這事碗旅。” “怎么了镜悉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵祟辟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我侣肄,道長(zhǎng)旧困,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮吼具,結(jié)果婚禮上僚纷,老公的妹妹穿的比我還像新娘。我一直安慰自己拗盒,他們只是感情好怖竭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著锣咒,像睡著了一般侵状。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上毅整,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天趣兄,我揣著相機(jī)與錄音,去河邊找鬼悼嫉。 笑死艇潭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的戏蔑。 我是一名探鬼主播蹋凝,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼总棵!你這毒婦竟也來(lái)了鳍寂?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤情龄,失蹤者是張志新(化名)和其女友劉穎迄汛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體骤视,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鞍爱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了专酗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睹逃。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祷肯,靈堂內(nèi)的尸體忽然破棺而出沉填,到底是詐尸還是另有隱情,我是刑警寧澤佑笋,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布翼闹,位于F島的核電站,受9級(jí)特大地震影響允青,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一颠锉、第九天 我趴在偏房一處隱蔽的房頂上張望法牲。 院中可真熱鬧,春花似錦琼掠、人聲如沸拒垃。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)悼瓮。三九已至,卻和暖如春艰猬,著一層夾襖步出監(jiān)牢的瞬間横堡,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工冠桃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留命贴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓食听,卻偏偏與公主長(zhǎng)得像胸蛛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子樱报,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354