相关文章:
{% post_link 拦截和分发 %}
针对所有的 /core/ycApi 的接口路径进行拦截,拦截器为 YcForwardFilter。
存在 YcForwardFilter 实例时注入该策略
@Bean
@ConditionalOnBean(YcForwardFilter.class)
public FilterRegistrationBean myFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
// 注入过滤器
registration.setFilter(ycForwardFilter);
// 设置拦截规则
registration.addUrlPatterns("/core/ycApi/*");
// 设置拦截名称
registration.setName("ycFilter");
// 设置拦截顺序
registration.setOrder(1);
return registration;
}
拦截器需要实现接口 Filter 中的 doFilter 方法
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 拦截的逻辑
}
拦截到请求后,可使用 filterChain 留在系统中处理,也可以转发出去
private void forwardFilter(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取转发路径
String forwardUrl = getUrlByRule();
HttpRequestBase httpRequestBase = null;
// 1. 请求地址
String uri = request.getRequestURI();
// 此处为 application/x-www-form-urlencoded
String query = request.getQueryString();
String urlParam = Arrays.asList(
query,
Optional.ofNullable(appKey).filter(s -> !ObjectUtils.isEmpty(s)).map(s -> "appkey=" + s).orElse(""),
Optional.ofNullable(random).filter(s -> !ObjectUtils.isEmpty(s)).map(s -> "random=" + s).orElse(""),
Optional.ofNullable(digest).filter(s -> !ObjectUtils.isEmpty(s)).map(s -> "digest=" + s).orElse("")
).stream()
.filter(s -> !ObjectUtils.isEmpty(s))
.collect(Collectors.joining("&&"));
String newUrl = forwardUrl + uri + Optional.of(urlParam).filter(s -> s.length() > 0).map(s -> "?" + s).orElse("");
log.info("接口转发:newUrl={}", newUrl);
// 2. 设置请求方式
String method = request.getMethod();
log.info("请求方式:method={}", method);
if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
httpRequestBase = new HttpGet(newUrl);
} else if (HttpMethod.POST.name().equalsIgnoreCase(method)) {
httpRequestBase = new HttpPost(newUrl);
}
// 3. 设置请求头
copyRequestHeader(request, httpRequestBase);
// 重置请求头中的认证信息
// resetAuthInfo(httpRequestBase);
// 4. 设置POST参数
String contentType = request.getContentType();
log.info("contentType={}", contentType);
if (httpRequestBase instanceof HttpPost && !ObjectUtils.isEmpty(contentType)) {
if (contentType.startsWith(ContentType.APPLICATION_JSON.getMimeType())) {
// application/json
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String body = reader.lines().collect(Collectors.joining());
// 请求中的空格不能去,为了发现空格
log.info("request body={}", StrUtil.brief(body, 300));
((HttpPost) httpRequestBase).setEntity(new StringEntity(body));
} else if (contentType.startsWith(ContentType.MULTIPART_FORM_DATA.getMimeType())) {
// multipart/form-data
copyFormData(request, httpRequestBase);
} else {
log.warn("不支持 application/json、multipart/form-data 之外的POST类型");
}
}
// 5. 连接设置
RequestConfig config = RequestConfig.custom()
.setConnectionRequestTimeout(connectionRequestTimeout)
.setSocketTimeout(socketTimeout)
.setConnectTimeout(connectionTimeout)
.build();
httpRequestBase.setConfig(config);
// 6. 提交请求
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse execute = httpClient.execute(httpRequestBase);
// 打印结果需要读取流,尽量不要对源数据进行处理,只打印响应码
int statusCode = execute.getStatusLine().getStatusCode();
log.info("响应结果码:{}", statusCode);
// 7. 返回前的处理
HttpEntity responseEntity = execute.getEntity();
// 8. 设置响应头
Header[] responseHeaders = execute.getAllHeaders();
for (Header header : responseHeaders) {
// Content-Length 和 Content-Encoding 不能同时存在,故两个属性都不复制
if (HTTP.CONTENT_LEN.equalsIgnoreCase(header.getName())
|| HTTP.TRANSFER_ENCODING.equalsIgnoreCase(header.getName())) {
continue;
}
log.debug("response header name={}, value={}", header.getName(), header.getValue());
// 复制响应头
response.setHeader(header.getName(), header.getValue());
}
// 9. 转发结果写入响应体
if (statusCode == HttpStatus.SC_NOT_FOUND){
// 针对404单独处理
response404(response, request.getServletPath());
} else {
responseEntity.writeTo(response.getOutputStream());
}
}
/**
* 针对404单独处理
* @param response 响应体
* @param path 请求URI
*/
private void response404(HttpServletResponse response, String path){
try (ServletOutputStream outputStream = response.getOutputStream()) {
YcRspBase rsp404 = YcRspBase.failed("404", "Not Found : " + path);
response.setStatus(HttpStatus.SC_NOT_FOUND);
ObjectMapper objectMapper = new ObjectMapper();
String str404 = objectMapper.writeValueAsString(rsp404);
outputStream.write(str404.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (Exception e){
log.error("写入数据到response失败", e);
}
}
/**
* 复制 POST 请求 form-data 请求方式中的请求数据
*
* @param request 待复制请求
* @param httpRequestBase 转发请求
* @throws IOException
* @throws ServletException
*/
private void copyFormData(HttpServletRequest request, HttpRequestBase httpRequestBase) throws IOException, ServletException {
// 附带文件的请求,contentType后有数据描述,小数据为base64的值,大数据疑似为文件大小
// e.g. multipart/form-data; boundary=--------------------------394758598706425280136232
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
// 防止中文文件名导致的乱码
.setMode(HttpMultipartMode.RFC6532);
// multipart/form-data 时才能获取,否则报错
Collection<Part> parts = request.getParts();
if (!ObjectUtils.isEmpty(parts)) {
// 数据为文件时,Content-Type 中含有长度信息 [boundary],需要去除长度
httpRequestBase.removeHeaders(HTTP.CONTENT_TYPE);
for (Part part : parts){
try {
InputStream is = part.getInputStream();
entityBuilder.addBinaryBody(
part.getName(),
is,
ContentType.APPLICATION_OCTET_STREAM,
part.getSubmittedFileName());
} catch (Exception e) {
log.error("copy part failed", e);
}
}
}
// 流读取放在 getParameterMap 之前,防止读取一次后数据丢失
Map<String, String[]> parameterMap = request.getParameterMap();
// 多个重复的 k 会在 v[] 数组中处理,一般不建议传数组 or 后续再处理
// parameterMap.forEach((k, v) -> entityBuilder.addTextBody(k, v[0]));
parameterMap.forEach((k, v) -> Stream.of(v).forEach(i -> entityBuilder.addTextBody(k, i)));
((HttpPost) httpRequestBase).setEntity(entityBuilder.build());
}
/**
* 复制请求中的请求头
* <p>
* 1. 不能设置 Content-Length 的值,让 CloseableHttpClient 自动设置
* 2. multipart/form-data 不能复制请求头,错误的请求头,会导致接收端文件读取不到,委托 CloseableHttpClient 自动设置
*
* @param request 原请求
* @param httpRequestBase 转发请求
*/
private void copyRequestHeader(HttpServletRequest request, HttpRequestBase httpRequestBase) {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
if (HTTP.CONTENT_LEN.equalsIgnoreCase(key)) {
// 不设置header长度
continue;
}
String value = request.getHeader(key);
log.debug("request header key={}, value={}", key, value);
httpRequestBase.setHeader(key, value);
}
}
需要借助 HandlerExceptionResolver 异常处理器来协助我们处理局部异常
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver exceptionResolver;
HandlerExceptionResolver 是个多实例注入对象,Spring MVC 使用的实例名为 handlerExceptionResolver
但是注入的也不是单个处理器,而是 HandlerExceptionResolverComposite 对象,持有多个处理器
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
@Nullable
private List<HandlerExceptionResolver> resolvers;
private int order = Integer.MAX_VALUE;
...
}
通过处理器中的 resolveException 方法,将拦截器中的异常捕获并统一处理
exceptionResolver.resolveException(request, response, null, e);
以上方法会将异常交给spring处理
有时因对接不同的系统,要求不同的返回数据结构,我们会要求指定异常处理类
获取该controller的引用(也许不需要)
@Autowired
private YcApiController ycController;
在 resolveException 指定方法handler对象
HandlerMethod handlerMethod = new HandlerMethod(ycController, ycController.getClass().getDeclaredMethods()[0]);
exceptionResolver.resolveException(request, response, handlerMethod, e);
正常情况下,请求在servlet分发时,会匹配到 HandlerMethod
如果没有特别的需求,找到controller对应的Advice即可
备注,exceptionResolver在处理 basePackageClasses 指定的类时,匹配的是同包及其子包
// org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
// 方法handler在这里转换成类型
if (handlerMethod != null) {
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver)this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
...
}
...
while(var9.hasNext()) {
Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry = (Map.Entry)var9.next();
ControllerAdviceBean advice = (ControllerAdviceBean)entry.getKey();
// 这里会检查basePackages和类型,handlerType为空将交由下一个Advice处理异常
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver)entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
return null;
}
在Advice类头指定basePackages时的生效地
// org.springframework.web.method.HandlerTypePredicate
private boolean hasSelectors() {
// basePackages 是以'com.xx.xxx.'方式存储,表示的是注解的当前包及其子包
return !this.basePackages.isEmpty() || !this.assignableTypes.isEmpty() || !this.annotations.isEmpty();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
// 转发
forwardFilter(request, response);
} catch (Exception e){
log.error("转发服务异常", e);
// 交给Spring的异常解析器去处理
// exceptionResolver.resolveException(request, response, null, e);
// 必须得传 HandlerMethod 否则会走GlobalExceptionHandler,而不是指定的YcApiAdvice
// 没有通过servlet进行分发,因此没有获取到方法对象
// 如有必要再添加对方法的匹配,这里默认去找Controller的第一个方法
HandlerMethod handlerMethod = new HandlerMethod(ycController, ycController.getClass().getDeclaredMethods()[0]);
exceptionResolver.resolveException(request, response, handlerMethod, e);
}
}
/**
* 会捕获指定类[basePackageClasses]同包及其子包下的异常
* 拥有类包及其子包下异常捕获的最高优先级
*
* @author **
* @date 2023/04/18 09:42:50
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(basePackageClasses = YcApiController.class)
public class YcApiAdvice {
/**
* 参数校验未通过
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public YcRspBase methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException {}", e.getMessage());
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return YcRspBase.failed("80111016", objectError.getDefaultMessage());
}
}