关键词:FeignClient,feign-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方法,name、mark明显是query类型参数,但是swagger上却变成了body类型。- 同样
/server/demo46
对应的demo6,uid是path类型参数,也变成了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模块:
-
被
@CatServer
标记的类,最后仍然充当Controller角色。但是又可以像普通Service一样,可以被其他组件注入调用! -
生成的Controller类,支持swagger框架:
在interface的方法、输入模型、输出模型上,使用swagger注解,同样可以生成API文档,并且能正常调用 -
可以为每个生成的Controller单独配置拦截器:
仅当通过API调用,作为Controller角色时,拦截器生效;而一般情况,作为组件注入调用时,不拦截 -
可以为Controller的响应,自动加上包装器类:
很多情况下,API接口的响应是同一个类,具体的业务数据,是响应类的一个泛型属性。CatServer组件,可以让开发人员专注业务数据,程序将业务对象自动封装到公共的响应类中 -
通过类的继承特性,实现对API接口升级:
例如有个新类DemoServiceExtImpl
继承DemoServiceImpl
,并且重写了父类方法,那么这个API便升级成了子类重新的方法! -
可搭配
FeignClient
使用:
可以实现如同dubbo
框架风格,客户端与服务器通过interface耦合。客户端注入interface,服务端实现interface -
可以像普通Service类一样,支持
@Transactional
事务配置。
最后总结一下,
- 先创建一个项目模块,使用feign-interface接口定义好API接口地址、请求方式、输入输出模型
- 客户端引入模块,使用@FeignClient,发起http调用
- 服务端也引入模块,实现feign-interface接口,完善业务代码。使用@CatServer结合实现类,直接生成Controller角色
- 此时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,如果出现版本不兼容情况,请下方留言