请求区分
- 请求路径,比如:uri加/rpc前缀用来标识RPC请求
- 请求头信息,比如:Accept:application/sc-rpc 用来标识RPC请求
输入参数和响应内容
方式一(旧):
对Spring MVC的消息转换进行封装:
- 输入(@RequestBody): 重写com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter#read方法,对本地请求和RPC请求做兼容。
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException {
try {
// transform inputStream to string
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
IOUtils.copy(inputMessage.getBody(), byteArrayOutputStream);
String str = byteArrayOutputStream.toString(StandardCharsets.UTF_8.name());
// parse json object
JSONObject jsonObject = JSON.parseObject(str, super.getFastJsonConfig().getFeatures());
// if RPC, transform the data format
if (jsonObject.containsKey("data")) {
return JSON.parseObject(jsonObject.getString("data"), type, super.getFastJsonConfig().getFeatures());
}
// otherwise, call super method
return readType(super.getType(type, contextClass), new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
} catch (JSONException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex);
} catch (IOException ex) {
throw new IOException("I/O error while reading input message", ex);
}
}
private Object readType(Type type, InputStream in) {
try {
return JSON.parseObject(in,
super.getFastJsonConfig().getCharset(),
type,
super.getFastJsonConfig().getParserConfig(),
super.getFastJsonConfig().getParseProcess(),
JSON.DEFAULT_PARSER_FEATURE,
super.getFastJsonConfig().getFeatures());
} catch (JSONException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex);
} catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
}
}
- 输出(@ResponseBody):
重写com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter#writeInternal方法,本地请求和RPC请求的数据格式保持一致。
package com.caiya.web.base;
import com.alibaba.fastjson.JSONPObject;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.caiya.web.constant.CommonConstant;
import com.google.common.base.Joiner;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
/**
* fastjson消息转换器.
*/
public class ExtendedFastJsonHttpMessageConverter extends FastJsonHttpMessageConverter {
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
super.writeInternal(wrapResult(object), outputMessage);
}
private Object wrapResult(Object object) {
// 防止json请求重复包装
if (object instanceof ResponseDataWrapper) {
return object;
}
if (object instanceof JSONPObject) {
JSONPObject jsonpObject = (JSONPObject) object;
JSONPObject newJsonpObject = new JSONPObject(jsonpObject.getFunction());
ResponseDataWrapper data;
if (jsonpObject.getParameters().size() == 1) {
// 防止jsonp请求重复包装
if (jsonpObject.getParameters().get(0) instanceof ResponseDataWrapper) {
return object;
}
data = ResponseDataWrapperBuilder.build(jsonpObject.getParameters().get(0));
} else if (jsonpObject.getParameters().size() > 1) {
data = ResponseDataWrapperBuilder.build(Joiner.on(",").join(jsonpObject.getParameters()));
} else {
data = ResponseDataWrapperBuilder.build(CommonConstant.PLACEHOLDER_OBJECT_EMPTY);
}
newJsonpObject.addParameter(data);
return newJsonpObject;
}
return ResponseDataWrapperBuilder.build(object);
}
}
方式二:
- 输入:
不需要处理,RPC请求时指定Accept即可:
package com.caiya.test.spring.cloud.configuration;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
@Configuration
public class FeignConfig {
@Bean
public HttpClient httpClient() {
System.out.println("init feign httpclient configuration 1111");
// 生成默认请求配置
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
// 超时时间
requestConfigBuilder.setSocketTimeout(5 * 1000);
// 连接时间
requestConfigBuilder.setConnectTimeout(5 * 1000);
// 设置代理
// requestConfigBuilder.setProxy(new HttpHost("127.0.0.1", 8880));
RequestConfig defaultRequestConfig = requestConfigBuilder.build();
// 连接池配置
// 长连接保持30秒
final PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.MILLISECONDS);
// 总连接数
pollingConnectionManager.setMaxTotal(5000);
// 同路由的并发数
pollingConnectionManager.setDefaultMaxPerRoute(100);
// httpclient 配置
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
// 保持长连接配置,需要在头添加Keep-Alive
httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
httpClientBuilder.setConnectionManager(pollingConnectionManager);
httpClientBuilder.setDefaultRequestConfig(defaultRequestConfig);
httpClientBuilder.setDefaultHeaders(Collections.singleton(new BasicHeader("Accept", "application/sc-rpc")));
HttpClient client = httpClientBuilder.build();
// 启动定时器,定时回收过期的连接
/*Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// System.out.println("=====closeIdleConnections===");
pollingConnectionManager.closeExpiredConnections();
pollingConnectionManager.closeIdleConnections(5, TimeUnit.SECONDS);
}
}, 10 * 1000, 5 * 1000);*/
System.out.println("===== Apache httpclient 初始化连接池===");
return client;
}
}
- 输出:
根据mediaType区分消息转换。
package com.caiya.web.configuration;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.caiya.web.base.ExtendedFastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import java.util.Arrays;
import java.util.Collections;
/**
* fastjson消息转换器配置.
*
* @author caiya
* @since 1.0
*/
@Configuration
public class FastJsonConfiguration {
@Bean
public HttpMessageConverters extendedFastJsonHttpMessageConverter() {
// for web controller
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new ExtendedFastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
// fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8));
// for web resource(Spring Cloud RPC)
FastJsonHttpMessageConverter fastJsonHttpMessageConverterPlain = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfigPlain = new FastJsonConfig();
fastJsonConfigPlain.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
// fastJsonConfigPlain.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
fastJsonHttpMessageConverterPlain.setFastJsonConfig(fastJsonConfigPlain);
fastJsonHttpMessageConverterPlain.setSupportedMediaTypes(Collections.singletonList(MediaType.valueOf("application/sc-rpc")));
return new HttpMessageConverters(fastJsonHttpMessageConverter, fastJsonHttpMessageConverterPlain);
}
}
方式三
在每个controller的method,返回最终响应内容。
方式四
通过aop处理不同的请求(RPC请求不处理即可):
package com.caiya.web.base;
import com.alibaba.fastjson.JSONPObject;
import com.caiya.web.constant.CommonConstant;
import com.google.common.base.Joiner;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 对响应内容的包装处理AOP.
*/
@Component
@Aspect
public class ResponseDataWrapperAspect {
@Pointcut("execution(* com.caiya.web.controller.rest..*(..))")
public void aspect() {
}
@Around("aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object object = joinPoint.proceed(joinPoint.getArgs());
return wrapResult(object);
}
private Object wrapResult(Object object) {
// 防止json请求重复包装
if (object instanceof ResponseDataWrapper) {
return object;
}
if (object instanceof JSONPObject) {
JSONPObject jsonpObject = (JSONPObject) object;
JSONPObject newJsonpObject = new JSONPObject(jsonpObject.getFunction());
ResponseDataWrapper data;
if (jsonpObject.getParameters().size() == 1) {
// 防止jsonp请求重复包装
if (jsonpObject.getParameters().get(0) instanceof ResponseDataWrapper) {
return object;
}
data = ResponseDataWrapperBuilder.build(jsonpObject.getParameters().get(0));
} else if (jsonpObject.getParameters().size() > 1) {
data = ResponseDataWrapperBuilder.build(Joiner.on(",").join(jsonpObject.getParameters()));
} else {
data = ResponseDataWrapperBuilder.build(CommonConstant.PLACEHOLDER_OBJECT_EMPTY);
}
newJsonpObject.addParameter(data);
return newJsonpObject;
}
return ResponseDataWrapperBuilder.build(object);
}
}
拦截器和过滤器
对RPC请求都不必拦截,放行处理(包括会话拦截器、权限拦截器、XSS过滤器等)
nginx
RPC请求只允许通过Spring Cloud注册中心、网关等调用[schema]://[ip]:[port]/[request_uri] 的形式 ,nginx 需要拦截路径包含 /rpc/ 目录的 RPC 接口调用(彻底隔离只需分离本地请求和RPC请求的应用即可):
location ~* /rpc/ {
return 403;
}
Spring Cloud 异常处理
服务端异常处理
推荐最外层包裹一层Result对象,表示接口执行结果,包含经过处理的异常信息
客户端异常处理
- 开启feignclient的hystrix熔断:
feign:
hystrix:
enabled: true
- 实现接口feign.codec.ErrorDecoder,处理服务端异常:
package com.caiya.web.configuration;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* Spring Cloud(feign with hystrix)自定义异常处理.
*
* @see FeignClientsConfiguration.HystrixFeignConfiguration#feignHystrixBuilder() 开启hystrix入口
* @see HystrixBadRequestException 此异常类型不会进行熔断操作
* HystrixBadRequestException.message:
* {"path":"/rpc/session/user/info","error":"Internal Server Error","message":"Illegal Agument Exception..","timestamp":1540266379459,"status":500}
*/
@Configuration
public class FeignErrorDecoder implements ErrorDecoder {
private static final Logger logger = LoggerFactory.getLogger(FeignErrorDecoder.class);
@Override
public Exception decode(String methodKey, Response response) {
String message = null;
if (response.status() >= 400 && response.status() <= 500) {
try {
if (response.body() != null) {
String body = Util.toString(response.body().asReader());
try {
JSONObject jsonObject = JSON.parseObject(body);
if (jsonObject.containsKey("data")) {
JSONObject content = jsonObject.getJSONObject("data");
if (StringUtils.isNotBlank(content.getString("message"))) {
message = content.getString("message");
if ("connect timed out".equals(message)) {
return feign.FeignException.errorStatus(methodKey, response);
}
} else {
message = content.getString("error");
}
}
if (message == null) {
message = jsonObject.getString("message");
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
message = body;
}
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return new HystrixBadRequestException(message);
}
return feign.FeignException.errorStatus(methodKey, response);
}
}
- 处理超时异常,实现@FeignClient注解中的属性fallback或fallbackFactory的相关类,进行熔断处理