借着这篇说一下 @RequestBody注解修饰方法参数时,继承相关的影响:
首先,在 springboot 2.0.7 版本里,如果需要提供一个既给FeignClient使用,又作为普通的RestController使用的情况下,原本是这样声明的
@FeignClient(value = "oa-service", path = "/oa-service")
@RequestMapping(value = "/oa")
public interface ISystemLoginFeignService {
@RequestMapping(value = "/6.0/auth/token", method = RequestMethod.PUT)
SysLoginResult login(@RequestBody SysLoginParam param) throws DLException;
@RestController
public class SystemLoginFeignService implements ISystemLoginFeignService {
@Autowired
private ISystemLoginService systemLoginService;
@Override
public SysLoginResult login(SysLoginParam param) throws DLException {
return systemLoginService.login(param);
}
可以注意到 param 对象前的 @RequestBody 注解 放在了 feignClient层,大家也知道,一次request中获取body体的字节流只能调用一次。由于我们自身有一个aop层会使用 request.getReader/getInputStream 取 request 中的 body对象,如果都生效的话,势必AOP中的request获取会报错(SpringBoot的@RequestBody注解拦截会优先生效,因为AOP拦截的是最终handlerMethod的方法调用,此时方法入参已构造好),而我们的AOP层并没有因为使用 request中的字节流导致报错,所以推测出实际上这里的 @RequestBody是没有生效的,由此也可以推测出 在 SpringBoot 2.0.7 这个版本下 @RequestBody 注解是不会继承至子类的,当然,这也导致了我们忽略了加在FeignClient层的 @RequestBody 注解。
接下来,由于一些原因对 SpringBoot 的版本升级至了 2.2.2 ,启动后发现 AOP层中的 request.getReader 开始报错 【java.lang.IllegalStateException: getInputStream() has already been called for this request】,推测出 父类的 @RequestBody 貌似生效了,查看源码后发现
HandlerMethod 获取参数上的注解时,多了获取方法上层接口的注解。。。,至此,知道了报错的原因,后续只能进行相应的逻辑变更,确保只有一侧的 reqeust读取body体方法生效
在A服务的公共模块中声明了一个FeignClient接口,
然后A的业务模块Controller实现了这个接口之后,就可保证A对外提供的restapi的规范性?
同时B服务如果依赖了A的公共模块,就不必要再自己写一份FeignClient接口对应A服务的restapi了,直接引用A的公共模块包含的FeignClient接口即可。
这可以在服务之间的通讯接口进行规范。A 和 B服务都用了相同的接口,保证了一致性,减少开发过程中的接口调试问题
使用Spring Cloud做项目的同学会使用Feign这个组件进行远程服务的调用,Feign这个组件采用模板的方式,有着优雅的代码书写规范。核心原理对Feign等相关注解进行解析,并提取信息,在Spring Boot工程启动时,通过反射生产Request的bean,并将提取的信息,设置到bean中,最后注入到ioc容器中。
现在有这样的场景,服务A提高RestApi接口,服务B、C、D等服务需要调用服务A提供的RestApi接口,这时最常见的做法是在服务B、C、D分别写一个FeignClient,并需要写RestApi接口的接收参数的实体和接收响应的实体DTo类。这样的做法就是需要不停复制代码。
有没有办法简洁上面的操作呢?有一种最常见的做法是将将服务A进行模块拆分,将FeignClient和常见的model、dto对外输出的类单独写一个模块,可以类似于取名a-service-open_share。这样将服务A服务分为两个模块,即A服务的业务模块和A服务需要被其他服务引用的公共类的模块。服务B、C、D只需要引用服务A的a-service-open_share就具备调用服务A的能力。
笔者在这里遇到一个有趣的其问题。首先看问题:
写一个FeignClient:
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users")
List<User> getUsers();
写一个实现类:
@RestController
public class UserController implements UserClient {
@Autowired
UserService userService;
@OverRide
List<User> getUsers(){
return userService.getUsers();
}
启动工程,浏览器访问接口localhost:8008/users,竟然能正确访问?!明明我在UserController类的getUsers方法没有加RequestMapping这样的注解。为何能正确的映射?!
带着这样的疑问,我进行了一番的分析和探索!
首先就是自己写了一个demo,首先创建一个接口类:
public interface ITest {
@GetMapping("/test/hi")
public String hi();
}
写一个Controller类TestController
@RestController
public class TestController implements ITest {
@Override
public String hi() {
return "hi you !";
}
启动工程,浏览器访问:http://localhost:8762/test/hi,浏览器显示:
hi you !
我去,TestController类的方法 hi()能够得到ITest的方法hi()的 @GetMapping("/test/hi")注解吗? 答案肯定是获取不到的。
特意编译了TestController字节码文件:
javap -c TestController
public class com.example.demo.web.TestController implements com.example.demo.web.ITest {
public com.example.demo.web.TestController();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.String hi();
Code:
0: ldc #2 // String hi you !
2: areturn
}
上面的字节码没有任何关于@GetMapping("/test/hi")的信息,可见TestController直接获取不到@GetMapping("/test/hi")的信息。
那应该是Spring MVC在启动时在向容器注入Controller的Bean(HandlerAdapter)时做了处理。初步判断应该是通过反射获取到这些信息,并组装到Controller的Bean中。首先看通过反射能不能获取ITest的注解信息:
public static void main(String[] args) throws ClassNotFoundException {
Class c = Class.forName("com.example.demo.web.TestController");
Class[] i=c.getInterfaces();
System.out.println("start interfaces.." );
for(Class clz:i){
System.out.println(clz.getSimpleName());
Method[] methods = clz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(GetMapping.class)) {
GetMapping w = method.getAnnotation(GetMapping.class);
System.out.println("value:" + w.value()[0] );
}
}
}
System.out.println("end interfaces.." );
Method[] methods = c.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(GetMapping.class)) {
GetMapping w = method.getAnnotation(GetMapping.class);
System.out.println("value:" + w.value());
}
}
}
允运行上面的代码:
start interfaces…
ITest
value:/test/hi
end interfaces…
可见通过反射是TestController类是可以获取其实现的接口的注解信息的。为了验证Spring Mvc 在注入Controller的bean时通过反射获取了其实现的接口的注解信息,并作为urlMapping进行了映射。于是查看了Spring Mvc 的源码,经过一系列的跟踪在RequestMappingHandlerMapping.java类找到了以下的方法:
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
}
return info;
}
继续跟踪源码在AnnotatedElementUtils 类的searchWithFindSemantics()方法中发现了如下代码片段:
// Search on methods in interfaces declared locally
Class<?>[] ifcs = method.getDeclaringClass().getInterfaces();
result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor,
visited, metaDepth, ifcs);
if (result != null) {
return result;
}
这就是我要寻找的代码片段,验证了我的猜测。
写这篇文章我想告诉读者两件事:
- 可以将服务的对外类进行一个模块的拆分,比如很多服务都需要用的FeignClient、model、dto、常量信息等,这些信息单独打Jar,其他服务需要使用,引用下即可。
- url映射不一定要写在Contreller类的方法上,也可以写在它实现的接口里面。貌似并没有是luan用,哈。