自定義註解簡單實現類似Feign的功能

自定義註解簡單實現類似Feign的功能

  • 最近在玩spring源碼,上文 spring自定義組件掃描,模仿@MapperScan 利用spring的beanFactoryProcessor進行掃描註冊,最後結合jdbc完成簡單封裝。
  • feign的源碼怎麼樣子我沒看過,只知道基於http調用(本文暫未集成配置中心)
  • 本文內容深度依賴於spring。

涉及主要知識點:

  • spring的BeanPostProcessor後置處理器
  • jdk動態代理
  • restTemplate進行http請求

1.註解定義

@EnableRemoteClient 開啓,對比一下@EnableFeignClients

/**
 * 開啓http遠程調用,爲什麼叫rpc,因爲本項目要結合dubbo一起使用
 * @author authorZhao
 * @date 2019/12/20
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RpcInstantiationAwareBeanPostProcessor.class)//這個類重點注意
public @interface EnableRemoteClient {
}

@RpcClient 對比一下@FeignClient

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RpcClient {

    /**
     * 服務在註冊中心的名稱,暫未集成配置中心,預計使用nacos
     * @return
     */
    String value() default "";

    /**
     * url前綴,訪問的url前綴,如果沒有註冊中心這個路徑代表訪問的url前綴
     * @return
     */
    String URLPre() default "";


    /**
     * 調用方式包括http、rpc
     * @return
     */
    String cllType() default "";

}

2.@EnableRemoteClient 註解的解析

說明:本文采用beanPostProcessor形式進行bean的代理生成,不採用上篇文章的beanFactoryPosrProcessor進行註冊,目的是爲了理解springaop的實現.

BeanPostProcessor的功能

  • 在bean實例化前後執行一些事情,類似自己的初始化和銷燬方法
  • BeanPostProcessor有一個子接口的執行時機和一般的不一樣,InstantiationAwareBeanPostProcessor 接口實在bean創建之前執行,aop這篇先不多說

spring源碼分析

  • finishBeanFactoryInitialization(beanFactory);spring創建bean的方法,不清楚的可以看看spring的refresh()方法
	try {	
			//在創bean實例化之前給一個後置處理器返回代理對象的機會,這裏就是操作的地方
			// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.//給後置處理器一個機會返回一個目標代理對象
			Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
			if (bean != null) {
				return bean;
			}
		}
		catch (Throwable ex) {
			throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
					"BeanPostProcessor before instantiation of bean failed", ex);
		}

		try {
			Object beanInstance = doCreateBean(beanName, mbdToUse, args);
			if (logger.isTraceEnabled()) {
				logger.trace("Finished creating instance of bean '" + beanName + "'");
			}
			return beanInstance;
		}

spring的分析暫時結束

RpcInstantiationAwareBeanPostProcessor 的作用

  • RpcInstantiationAwareBeanPostProcessor 重寫了postProcessBeforeInstantiation方法,就是上面spring源碼需要重寫的方法
  • 爲什麼實現ApplicationContextAware ,我用來檢測本地有沒有註冊自己的接口定義,不然代理不到,debug用的,可以不用實現
/**
 * @author authorZhao
 * @date 2019/12/20
 */
@Slf4j
public class RpcInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor , ApplicationContextAware {


    private ApplicationContext applicationContext;
    /**
     * 實例化前,後面的方法可以不用看了
     * @param beanClass
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {

        log.info("正在爲:{}生成代理對象,被代理的類爲:{}",beanName,beanClass.getName());
		//檢測需要實例化的bean有沒有@RpcClient註解,有的話進行代理,沒有返回null就行
        RpcClient rpcClient = AnnotationUtil.getAnnotation(beanClass, RpcClient.class);
        if (rpcClient == null) return null;
		//動態代理裏面需要實現的方法,本文采用的是jdk動態代理
        Supplier<ProxyMethod<Object, Method, Object[], Object>> supplier = RpcMethodImpl::httpRemote;
        //返回代理對象
        Object object = ProxyUtil.getObject(beanClass, supplier.get());
        return object;

    }

    /**
     * 實例化後
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;
    }

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        return pvs;
    }


    /**
     * 初始化錢
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    /**
     * 初始化後
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        this.applicationContext=applicationContext;
    }
}

@RequestMapping的解析

  • 已經深度使用spring了,就直接利用spring的註解

使用方式

//使用案例如下
@RpcClient(URLPre="http://localhost:8099/menu")
public interface MenuService {

    @PostMapping("/getMenuByQuery")
    ApiResult getMenuByQuery();


    @GetMapping("/getMenuByRoleId")
    ApiResult getMenuByRoleId(@PathVariable String roleId);
}

代理方法

//這個就是jdk的動態代理所需要實現的方法,不熟悉的看作者上一篇文章即可
public static ProxyMethod<Object, Method,Object[],Object> httpRemote() {
        ProxyMethod<Object, Method,Object[],Object>  cgLibProxyMethod =
                (proxy,method,args)->{
                    //1.決定請求方式

                    String methodName = method.getName();
                    String url = "";

                    //2.得到請求路徑
                    RpcClient annotationImpl = AnnotationUtil.getAnnotationImpl(proxy.getClass(), RpcClient.class);//這裏的AnnotationUtil是自己寫的,簡單獲取註解,後面帶s的是spring的可以獲得元註解信息
                    if(annotationImpl!=null)url = annotationImpl.URLPre();

                    HttpMapping requestMapping = getRequestMapping(method);//HttpMapping用於統一GetMapping、PostMapping、RequestMapping的
                    //@RpcClient 的請求前綴
                    String urlFix = requestMapping.getValue();
                    if(StringUtils.isBlank(urlFix)){
                    //如果沒寫,默認將方法名作爲路徑
                        url = url+"/"+methodName;
                    }else{
                        url = url +urlFix;
                    }
					//3.執行http請求
                    return doHttp(url,method,args, requestMapping.getHttpMethod());
                };
        return cgLibProxyMethod;
    }

dohttp

private static Object doHttp(String url, Method method, Object[] args, HttpMethod httpMethod) {

        //1.restTemplate構建
        RestTemplate restTemplate = new RestTemplate();
        //2.請求頭與請求類型
        //這個執行了兩次,暫時未調整
        HttpMapping httpMapping = getRequestMapping(method);

        //1.獲得請求頭
        HttpHeaders headers = getHeader(); // http請求頭

        List<MediaType> mediaTypeList = new ArrayList<>();
        mediaTypeList.add(MediaType.APPLICATION_JSON_UTF8);
        //content-type的設置,默認json+u8,畢竟後臺接口返回的都是json
        //2.設置consumer
        String[] consumes = httpMapping.getConsumes();
        if(consumes!=null&&consumes.length>0)headers.setContentType(MediaType.parseMediaType(consumes[0]));
        String[] produces = httpMapping.getProduces();
        if(produces!=null&&produces.length>0)mediaTypeList.add(MediaType.parseMediaType(produces[0]));
        headers.setAccept(mediaTypeList);
        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();

        Map<String, Object> map = new HashMap<>();
		//重點註解解析
        //支持三個註解,不要亂用
        //1.PathVariable,將帶有這個註解的放在url上面,這列採用手工操作

        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Parameter[] parameters = method.getParameters();

        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            if(parameter==null)continue;
            String paramname = parameter.getName();
            PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
            if(pathVariable!=null){
                //爲空會報錯
                url = url +"/"+args[i].toString();
            }
            //@RequestParam的value作爲http請求參數的key,如果沒有采用方法的參數名,注意方法的參數名沒做處理可能會是args0等
            RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
            if(requestParam!=null){
                String name = requestParam.value();
                paramname = StringUtils.isNotBlank(name)?name:paramname;
            }
            if(args[i]!=null){
                form.add(paramname,(args[i]));
            }
        }
		//@RequestBody,如果帶有這個註解的話將所有非url參數轉化爲json
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            RequestBody requestBody = parameter.getAnnotation(RequestBody.class);
            if(requestBody!=null){
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                //帶有requestBody時候全體當做一個參數打包成json
                String json = JSON.toJSONString(form);
                log.error("正在發送json形式的請求,請求數據爲:{}",json);
                HttpEntity<String> httpEntity = new HttpEntity<>(json, headers);
                return httpEntity;
            }
        }
        //2.RequestBody
        //3.PathVariable
        //RequestParam,
        //RequestBody,
        //PathVariable
        //3.設置參數
        //普通post
        //json
        //headers.setContentType(MediaType.APPLICATION_JSON_UTF8); // 請求頭設置屬性
        //headers.setContentType(MediaType.parseMediaType("multipart/form-data; charset=UTF-8"));
        //
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(form, headers);
        //3.參數解析
        //4.結果返回
        Class<?> returnType = method.getReturnType();

        //後臺接收的地址
        //String url = "http://localhost:8092/process/saveProcess";

        log.info("正發起http請求,url={},請求參數={}",url,httpEntity.getBody().toString());
        //restTemplate.postForEntity()
        ResponseEntity result = restTemplate.exchange(url, httpMethod, httpEntity, returnType);
       
            log.info("http請求結果爲{}", JSON.toJSONString(result.getBody()));          
            log.info("http請求結果爲{}", JSON.toJSONString(result.getBody()));
        return result.getBody();
    }

測試

1.啓動類


@SpringBootApplication
@ComponentScan({"com.git.file","com.git.gateway.controller","com.git.gateway.service"})//掃描
@MapperScan("com.git.file.mapper")
@EnableRemoteClient//開啓http代理
@Import({TestService.class,ArticleService.class, MenuService.class})//接口註冊,注意接口上面加@Services的時候spring不不會註冊這個bean定義的

2.controller,消費者,我這裏是網關的


@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestService testService;

    @Autowired
    private ArticleService articleService;
    
    @Autowired
    private MenuService menuService;
    
    @RequestMapping("/getById/{id}")
    public ApiResult getById(@PathVariable String id){
        return testService.getById(id);

    }
    
    @RequestMapping("/read/{id}")
    public ApiResult read(@PathVariable String id){
        return articleService.getById(id);

    }
    
    @RequestMapping("/getMenuByQuery")
    public ApiResult getMenuByQuery(){
        return menuService.getMenuByQuery();

    }
    
    @RequestMapping("/getMenuByRoleId/{id}")
    public ApiResult getMenuByRoleId(@PathVariable String id){
        return menuService.getMenuByRoleId(id);

    }

}

3.service

這個就不全部列出來了,其中json的還未測試

/**
 * @author authorZhao
 * @date 2019/12/20
 */

@RpcClient(URLPre="http://localhost:8099/menu")
public interface MenuService {

    @PostMapping("/getMenuByQuery")
    ApiResult getMenuByQuery();

    @GetMapping("/getMenuByRoleId")
    ApiResult getMenuByRoleId(@PathVariable String roleId);
}

4.啓動另一個服務提供接口

這裏不展示了

5.效果圖

1.網管的截圖
網管的controller

2.員服務直接調用的
原服務採用的端口和方式不一樣
3.網關的日誌
網關的日誌

結束

1.說明

  • 本文還有很多方法未實現
  • 本文的註解工具類一個是自己寫的,一個是spring的,spring的帶s,並且可以獲得元註解,這個很重要
  • 本文還未實現與註冊中心的對接
  • 本文爲貼出來的代碼暫時沒有公開有不理解的直接留言即可,作者還在繼續學習繼續完善中
    2.打碼感想
  • 雖然沒有看feign的源碼們現在也基本猜到怎麼實現的了,不包括註冊中心和負載均衡等等組件
  • 對spring註冊bean和創房bean的認識進一步加深

本文爲作者原創,轉載請申明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章