FeignClient服务端,Controller与它的interface们

关键词:FeignClientfeign-interface


大家好,我是入错行的bug猫。(http://blog.csdn.net/qq_41399429,谢绝转载)



上一篇写到使用动态代理,仿写一个了FeignClient客户端 可以戳这里(终于把这个坑补上了)

在使用FeignClient客户端,看见feign-interface的时候,相信大家应该都有一个大胆的想法!
feign-interface使用的注解,和Controller是多么滴相似,能不能直接用feign-interface来定义Controller呢?


@FeignClient("")
public interface IDemoService {

    @RequestMapping(value = "/server/demo41", method = RequestMethod.POST)
    Demo demo1(@RequestBody Demo req);

    @RequestMapping(value = "/server/demo43", method = RequestMethod.GET)
    List<Demo> demo3(@ModelAttribute Demo req);
   
    @RequestMapping(value = "/server/demo44", method = RequestMethod.GET)
    ResponseEntity<Demo> demo4(@RequestParam("userName") String name, @RequestParam("userMark") String mark);

    @RequestMapping(value = "/server/demo46/{uid}", method = RequestMethod.GET)
    Void demo6(@PathVariable("uid") Long userId, @RequestParam("userName") String name);

    @RequestMapping(value = "/server/demo47", method = RequestMethod.GET)
    Demo demo7(@RequestHeader("token") String token);
  
}

如上,基本上定义一个Controller的几个要素都有了。反观FeignClient服务端,其实也就是调用远程服务的API,一般用得比较多的是restful风格提供接口,响应为Json字符串。feign-interface和定义一个Controller,就差那么亿点点了!



在使用dubbo的时,服务端会有一个dubbo的配置xml文件,暴露提供业务的Service层信息(高版本中直接可以使用注解申明)。
然后在服务端直接使用一个class实现这个interface接口,实现interface中的方法,即可被远程服务调用(当然还需要配合注册中心)。
看上去就好像客户端,注入一个Service实现类的interface,就完成了调用远程服务端的Service实现类!
并且,服务端和客户端,直接通过service-interface耦合在一起,如果服务端输入模型、响应模型中,新增了一个字段,那么同版本的客户端可以直接使用这个字段了。


  客户端                    调用                      服务端	                   
 服务消费者   ───────────────────────────────────>   服务提供者           
	 │                                                 │ 
	 │  注入                                           │         
	 │                                                 │ 
	 └──────────────  service interface                │                                 
	                           │                       │
       	                       │                 实现  │   
                               └───────────────────────┤   
	                                                   │
                                                       │
	                                               Service 实现类


再看看feign,使用restful风格的Controller作为服务端代码,服务端和客户端,分别有自己的输入输出数据模型,通过Json字符串耦合在一起。
如果服务端响应,增加了一个属性,但是客户端不做改动的话,客户端是无法使用这个字段的!


虽然这样做,可以说是为了解耦合。
但是怎么办,FeignClient也好想像dubbo一样, (ಥ_ಥ) 人家也想一呼百应,改一个地方,其他客户端都能集中一起改动,避免在各个客户端中手动重复添加代码、甚至还有些地方改漏了!


回到最初的问题,到底能不能使用feign-interface定义Controller?
如果可以的话,服务端使用interface-controller类实现feign-interface,完善具体业务代码,充当Controller角色;
客户端注入feign-interface,调用其方法,等价于向服务端发起http请求。
如此,便使得feign能像dubbo那调用了!




我们先大胆假设,再小心求证:
https://github.com/bugCats/cat-client-all这是示例代码仓库,可以一边走读代码,一边动手试试。


1. 如果服务端Controller类,直接实现FeignClient的interface,不也是耦合在一起了?
[示例:examples-csdn/example-csdn1]

@RestController
public class DemoServiceImpl implements IDemoService {

    @Override
    public Demo demo1(Demo req) {
        return null;
    }

    @Override
    public List<Demo> demo3(Demo req) {
        return null;
    }

    @Override
    public ResponseEntity<Demo> demo4(String name, String mark) {
        return null;
    }

    @Override
    public Void demo6(Long userId, String name) {
        return null;
    }

    @Override
    public Demo demo7(String token) {
        return null;
    }
}

  • IDemoService中的Demo,和DemoServiceImpl中Demo类是同一个,两边无论是谁增减字段,对方都会立即知道;
  • IDemoService的方法上入参类型、数量、顺序、响应类型发生变化,DemoServiceImpl类也必须对应变化;

可以观察到项目正常启动,并且URL能正常映射到DemoServiceImpl的方法上:

但是就是不能正常运行――

假的,其实部分还是可以用的


通过swagger生成的文档:
在这里插入图片描述
在这里插入图片描述

  • /server/demo44对应的demo4方法,namemark明显是query类型参数,但是swagger上却变成了body类型。
  • 同样/server/demo46对应的demo6,uidpath类型参数,也变成了body类型 ,甚至参数名称都错了。
  • 另外demo1方法根本接收不到入参…

大概原因是,interface方法上的注解:@RequestBody、@ModelAttribute、@RequestParam、@PathVariable,没有元注解@Inherited。这些注解是作用于方法上,子类如果重写、或者实现了改方法,子类是无法继承到这些注解的!

Controller在扫描注册Mapped时,扫描到的是实现类上的方法,因此无法获取到以上4种注解。swagger在生成文档时,同样也无法获取到这4种注解。导致生成的文档,参数类型、参数名称都不正确。






2. 把@RestController放在interface上会肿么样呢?
[示例:examples-csdn/example-csdn2]

@RestController
// @FeignClient 由于示例均为服务端,@FeignClient注解只影响客户端,所以此处屏蔽。有无改注解不影响结果
public interface IDemoService{

    @RequestMapping(value = "/server/demo41", method = RequestMethod.POST)
    Demo demo1(@RequestBody Demo req);

    @RequestMapping(value = "/server/demo43", method = RequestMethod.GET)
    List<Demo> demo3(@ModelAttribute Demo req);

    @RequestMapping(value = "/server/demo44", method = RequestMethod.GET)
    ResponseEntity<Demo> demo4(@RequestParam("userName") String name, @RequestParam("userMark") String mark);

    @RequestMapping(value = "/server/demo46/{uid}", method = RequestMethod.GET)
    Void demo6(@PathVariable("uid") Long userId, @RequestParam("userName") String name);

    @RequestMapping(value = "/server/demo47", method = RequestMethod.GET)
    Demo demo7(@RequestHeader("token") String token);

}

还是可以正常启动,想不到吧?!只不过没有没有扫描到Mapped…


因为spring默认扫描组件时,会忽略孤立的interface。
如果此时在DemoServiceImpl类上加上@Component,又可以正常扫描到Mapped,
不过最终Mapped,仍然映射到实现类的方法上了,swagger效果和第一版一样…


为什么在实现类上加@Component,又可以扫描到?

大概意思是在实现类上加@Component,Spring会生成一个FactoryBean工厂,专门用来创建实现类对象。在Srping扫描到interface后,会检查能否根据interface,在Spring容器中获取到组件。
同样,在做Mapped映射时,虽然@RestController加在interface上,但是仍然先根据interface在Spring容器中获取实现类,如果获取到了,就直接映射到实现类上的方法。@see org.springframework.web.method.HandlerMethod#bridgedMethod






3. 重写组件扫描,强制将Mapped映射到interface方法上:
[示例:examples-csdn/example-csdn3]

@Target({ ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiCtrl{
    String value() default "";	//标记feign-interface
}
public class CatScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    //资源加载器
    private ResourceLoader resourceLoader;
    
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    /**
     * 注册扫描事件
     * */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        
        String pkgs = "com.bugcat.example";

        //所有被@ApiCtrl标记的类
        Set<Object> servers = new HashSet<>();
        
        // 定义扫描对象
        CatScanner scanner = new CatScanner(servers, registry);
        scanner.setResourceLoader(resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(ApiCtrl.class));   //筛选带有@ApiCtrl注解的类

        //执行扫描
        scanner.scan(pkgs);

        BeanDefinitionBuilder ctrlInit = BeanDefinitionBuilder.genericBeanDefinition(CatCtrlInitializingBean.class);
        ctrlInit.addConstructorArgValue(servers);	//通过构造器,将所有被@ApiCtrl标记的类的class传到CatCtrlInitializingBean对象
        registry.registerBeanDefinition("catCtrlInitializingBean", ctrlInit.getBeanDefinition());
    }
        
    /**
     * 自定义扫描
     * */
    private static class CatScanner extends ClassPathBeanDefinitionScanner {

        private Set<Object> servers;
        
        public CatScanner(Set<Object> servers, BeanDefinitionRegistry registry) {
            super(registry);
            this.servers = servers;
        }
        
        /**
         * ApiCtrl标记在interface上,spring扫描时,默认会排除interface,重写此方法,将interface类名存入servers
         * */
        @Override
        protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
            AnnotationMetadata metadata = beanDefinition.getMetadata();
            if (metadata.isIndependent()) {
                if ( !metadata.isAnnotation() && metadata.hasAnnotation(ApiCtrl.class.getName())) {
                    servers.add(beanDefinition.getBeanClassName());
                }
            }
            return super.isCandidateComponent(beanDefinition);
        }
    }
}
public class CatCtrlInitializingBean implements InitializingBean, ApplicationContextAware{

    private ApplicationContext context;

    // interface的class
    private final Set<Class> servers;

    public CatCtrlInitializingBean(Set<Class> servers) {
        this.servers = servers;
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {

        IntFunction<RequestMethod[]> requestMethodToArray = RequestMethod[]::new;
        IntFunction<String[]> stringToArray = String[]::new;
        RequestMappingHandlerMapping mapper = context.getBean(RequestMappingHandlerMapping.class);
        String annName = RequestMapping.class.getName();
        
        for(Class clazz : servers){
            try {
                // 根据interface获取到组件
                Object bean = context.getBean(clazz);
                Method[] methods = clazz.getMethods();
                for(Method method : methods ){
                    StandardMethodMetadata metadata = new StandardMethodMetadata(method);
                    Map<String, Object> attr = metadata.getAnnotationAttributes(annName);
                    if( attr == null ){
                        continue;
                    }
                    RequestMappingInfo mappingInfo = RequestMappingInfo
                            .paths(getValue(attr, "value", stringToArray))
                            .methods(getValue(attr, "method", requestMethodToArray))
                            .params(getValue(attr, "params", stringToArray))
                            .headers(getValue(attr, "headers", stringToArray))
                            .produces(getValue(attr, "produces", stringToArray))
                            .consumes(getValue(attr, "consumes", stringToArray))
                            .build();
                    mapper.unregisterMapping(mappingInfo);
                    mapper.registerMapping(mappingInfo, bean, method); // 手动注册映射处理
                }
            } catch ( BeansException e ) {
                e.printStackTrace();
            }
        }
    }

    private final <T> T[] getValue(Map<String, Object> map, String key, IntFunction<T[]> func){
        Object value = map.get(key);
        if( value instanceof List ){
            List<T> list = ((List<T>)value);
            return list.toArray(func.apply(list.size()));
        } else if(value.getClass().isArray()){
            return (T[]) value;
        } else {
            T[] arr = func.apply(1);
            arr[0] = (T) value;
            return arr;
        }
    }
}
@ApiCtrl	//注解放置在 feign-interface 上面
public interface IDemoService{

    @RequestMapping(value = "/server/demo41", method = RequestMethod.POST)
    Demo demo1(@RequestBody Demo req);

    @RequestMapping(value = "/server/demo43", method = RequestMethod.GET)
    List<Demo> demo3(@ModelAttribute Demo req);

    @RequestMapping(value = "/server/demo44", method = RequestMethod.GET)
    ResponseEntity<Demo> demo4(@RequestParam("userName") String name, @RequestParam("userMark") String mark);

    @RequestMapping(value = "/server/demo46/{uid}", method = RequestMethod.GET)
    Void demo6(@PathVariable("uid") Long userId, @RequestParam("userName") String name);

    @RequestMapping(value = "/server/demo47", method = RequestMethod.GET)
    Demo demo7(@RequestHeader("token") String token);

}

@ResponseBody	//此处也可以使用@RestController,为了演示,就使用@ResponseBody + @Component
@Component
public class DemoServiceImpl implements IDemoService {

    @Override
    public Demo demo1(Demo req) {
        return null;
    }

    @Override
    public List<Demo> demo3(Demo req) {
        return null;
    }

    @Override
    public ResponseEntity<Demo> demo4(String name, String mark) {
        return null;
    }

    @Override
    public Void demo6(Long userId, String name) {
        return null;
    }

    @Override
    public Demo demo7(String token) {
        return null;
    }
}
@Import(CatScannerRegistrar.class)
@SpringBootApplication
public class CatServerApplication {

	public static void main(String[] args) {
        SpringApplication app = new SpringApplication(CatServerApplication.class);
        app.run(args);
        System.out.println("http://localhost:8112/swagger-ui.html");
    }
}

启动发现没问题,控制台打印Mapped映射到抽象方法上:
在这里插入图片描述


观察一下swagger: 参数类型、别名都没有问题,可以正常访问!

貌似没有问题了~




当然,这是最精简的,还要考虑如果IDemoService有其他类实现、DemoServiceImpl被其他类继承的情况,以及API权限、切面功能等。


最终代码https://github.com/bugCats/cat-client-all,更多示例在examples模块:

  1. @CatServer标记的类,最后仍然充当Controller角色。但是又可以像普通Service一样,可以被其他组件注入调用!

  2. 生成的Controller类,支持swagger框架:
    在interface的方法、输入模型、输出模型上,使用swagger注解,同样可以生成API文档,并且能正常调用

  3. 可以为每个生成的Controller单独配置拦截器:
    仅当通过API调用,作为Controller角色时,拦截器生效;而一般情况,作为组件注入调用时,不拦截

  4. 可以为Controller的响应,自动加上包装器类:
    很多情况下,API接口的响应是同一个类,具体的业务数据,是响应类的一个泛型属性。CatServer组件,可以让开发人员专注业务数据,程序将业务对象自动封装到公共的响应类中

  5. 通过类的继承特性,实现对API接口升级:
    例如有个新类DemoServiceExtImpl继承DemoServiceImpl,并且重写了父类方法,那么这个API便升级成了子类重新的方法!

  6. 可搭配FeignClient使用:
    可以实现如同dubbo框架风格,客户端与服务器通过interface耦合。客户端注入interface,服务端实现interface

  7. 可以像普通Service类一样,支持@Transactional事务配置。



最后总结一下,

  1. 先创建一个项目模块,使用feign-interface接口定义好API接口地址、请求方式、输入输出模型
  2. 客户端引入模块,使用@FeignClient,发起http调用
  3. 服务端也引入模块,实现feign-interface接口,完善业务代码。使用@CatServer结合实现类,直接生成Controller角色
  4. 此时feign-interface接口,就将客户端与服务端耦合在一起了!

如何,是不是和dubbo很相似了?dubbo使用RPC实现数据传输,FeignClient和CatServer仍然使用Http协议传输Json

那么问题来了,(*・´ω`・)っDemoServiceImpl到底是属于控制层,还是业务层呢?





PS:(づ。◕‿‿◕。)づ @CatServer不光能和FeignClient配合使用,甚至普通的Controller,也可以这样写~


PS:将DemoService类中的@RequestMapping注解换成@CatMethod,就可以支持bug猫仿写的FeignClient客户端哟,并且仍然兼容标准版的FeignClient客户端 (◕ω<) ♪


PS:demo使用 Springboot 1.5.18.RELEASE + swagger2 2.5.0,如果出现版本不兼容情况,请下方留言





评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值