使用Feign,@RequestBody无法继承的问题
根据官网FeignClient的例子,编写一个简单的updateUser接口,定义如下
1
2
3
4
5
6
7
|
@RequestMapping ( "/user" )
public interface UserService {
@RequestMapping (value = "/{userId}" , method = RequestMethod.GET)
UserDTO findUserById( @PathVariable ( "userId" ) Integer userId);
@RequestMapping (value = "/update" , method = RequestMethod.POST)
boolean updateUser( @RequestBody UserDTO user);
}
|
实现类
1
2
3
4
5
6
|
@Override
public boolean updateUser(UserDTO user)
{
LOGGER.info( "===updateUser, id = " + user.getId() + " ,name= " + user.getUsername());
return false ;
}
|
执行单元测试,发现没有获取到预期的输入参数
2018-09-07 15:35:38,558 [http-nio-8091-exec-5] INFO [com.springboot.user.controller.UserController] {} - ===updateUser, id = null ,name= null
原因分析
SpringMVC中使用RequestResponseBodyMethodProcessor类进行入参、出参的解析。以下方法根据参数是否有@RequestBody注解判断是否进行消息体的转换。
1
2
3
4
|
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody. class );
}
|
解决方案
既然MVC使用RequestResponseBodyMethodProcessor进行参数解析,可以实现一个定制化的Processor,修改supportParameter的判断方法。
1
2
3
4
5
6
7
8
9
|
@Override
public boolean supportsParameter(MethodParameter parameter)
{
//springcloud的接口入参没有写@RequestBody,并且是自定义类型对象 也按JSON解析
if (AnnotatedElementUtils.hasAnnotation(parameter.getContainingClass(), FeignClient. class ) && isCustomizedType(parameter.getParameterType())) {
return true ;
}
return super .supportsParameter(parameter);
}
|
此处的判断逻辑可以根据实际框架进行定义,目的是判断到为Spring Cloud定义的接口,并且是自定义对象时,使用@RequestBody相同的内容转换器。
实现定制化的Processor后,还需要让自定义的配置生效,有两种方案可选:
- 直接替换RequestResponseBodyMethodProcessor,在SpringBoot下需要自定义RequestMappingHandlerAdapter。
- 实现WebMvcConfigurer中的addArgumentResolvers接口
这里采用较为简单的第二种方式,初始化时的消息转换器根据需要进行加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class XXXWebMvcConfig implements WebMvcConfigurer
{
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
{
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset( false );
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>( 5 );
messageConverters.add( new ByteArrayHttpMessageConverter());
messageConverters.add(stringHttpMessageConverter);
messageConverters.add( new SourceHttpMessageConverter<>());
messageConverters.add( new AllEncompassingFormHttpMessageConverter());
CustomizedMappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new CustomizedMappingJackson2HttpMessageConverter();
jackson2HttpMessageConverter.setObjectMapper(defaultObjectMapper());
messageConverters.add(jackson2HttpMessageConverter);
ViomiMvcRequestResponseBodyMethodProcessor resolver = new ViomiMvcRequestResponseBodyMethodProcessor(messageConverters);
resolvers.add(resolver);
}
|
修改完成后,微服务的实现类即可去掉@RequestBody注解。
使用feign遇到的问题
spring cloud 使用feign 项目的搭建 在这里就不写了,本文主要讲解在使用过程中遇到的问题以及解决办法
1、示例
1
2
|
@RequestMapping (value = "/generate/password" , method = RequestMethod.POST)
KeyResponse generatePassword( @RequestBody String passwordSeed);
|
在这里 只能使用 @RequestMapping(value = "/generate/password", method = RequestMethod.POST) 注解 不能使用
@PostMapping 否则项目启动会报
Caused by: java.lang.IllegalStateException: Method generatePassword not annotated with HTTP method type (ex. GET, POST) 异常
2、首次访问超时问题
原因:Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。
而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了。
解决方法:
<1:配置Hystrix的超时时间改为5秒
1
|
hystrix.command. default .execution.isolation.thread.timeoutInMilliseconds: 5000
|
<2:禁用Hystrix的超时时间
1
|
hystrix.command. default .execution.timeout.enabled: false
|
<3:禁用feign的hystrix 功能
1
|
feign.hystrix.enabled: false
|
注:个人推荐 第一 或者第二种 方法
3、FeignClient接口中
如果使用到@PathVariable,必须指定其value
spring cloud feign 使用 Apache HttpClient
问题:1 没有指定 Content-Type 是情况下 会出现如下异常 ? 这里很纳闷?
Caused by: java.lang.IllegalArgumentException: MIME type may not contain reserved characters
在这里有兴趣的朋友可以去研究下源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
ApacheHttpClient. class
private ContentType getContentType(Request request) {
ContentType contentType = ContentType.DEFAULT_TEXT;
for (Map.Entry<String, Collection<String>> entry : request.headers().entrySet())
// 这里会判断 如果没有指定 Content-Type 属性 就使用上面默认的 text/plain; charset=ISO-8859-1
// 问题出在默认的 contentType : 格式 text/plain; charset=ISO-8859-1
// 转到 ContentType.create(entry.getValue().iterator().next(), request.charset()); 方法中看
if (entry.getKey().equalsIgnoreCase( "Content-Type" )) {
Collection values = entry.getValue();
if (values != null && !values.isEmpty()) {
contentType = ContentType.create(entry.getValue().iterator().next(), request.charset());
break ;
}
}
return contentType;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
ContentType. class
public static ContentType create( final String mimeType, final Charset charset) {
final String normalizedMimeType = Args.notBlank(mimeType, "MIME type" ).toLowerCase(Locale.ROOT);
// 问题在这 check 中 valid f方法中
Args.check(valid(normalizedMimeType), "MIME type may not contain reserved characters" );
return new ContentType(normalizedMimeType, charset);
}
private static boolean valid( final String s) {
for ( int i = 0 ; i < s.length(); i++) {
final char ch = s.charAt(i);
// 这里 在上面 text/plain;charset=UTF-8 中出现了 分号 导致检验没有通过
if (ch == '"' || ch == ',' || ch == ';' ) {
return false ;
}
}
return true ;
}
|
解决办法 :
1
|
@RequestMapping (value = "/generate/password" , method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
|
注解中指定: Content-Type 即 指定 consumes 的属性值 : 这里 consumes 属性的值在这不做具体讲解,有兴趣的可以去研究下
以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/phoebechen_gz/article/details/82500904