一次 HTTP 加解密尝试
工作要处理外部系统对接,虽然网络为政务内网,几乎不可能出现中间人攻击或网络嗅探。但传输内容为敏感信息,仍然要进行加密,最后选择使用 SM4 对整个请求体信息进行加密,并与对接系统约定固定密钥。
确定了一个简单的方案(不建议使用,后面会提到,这个方式局限性很强,唯一优点是实现简单),通过配置高优先级的 Servlet Filter 对请求进行预处理,在请求 Servlet 后读取请求体(此处通过调用 Request.getInputStream 方法),并对请求体进行解密。创建自定义 RequestWrapper 继承 HttpServletRequestWrapper,并将解密后的自定义 RequestWrapper 中,重写 getReader 和 getInputStream 方法。
这样请求在进入 DispatcherServlet 之前就会完成解密。通过同样方式创建 ResponseWrapper 继承 HttpServletResponseWrapper,在请求返回前由 Servlet Filter 实现对返回响应的整体加密。
事实上,DispatcherServlet 通过调用 RequestMappingHandlerAdapter 选择合适的 Controller 方法并调用。但在调用之前,还需要完成两步,第一步是解析请求体内容并转换为 Java 对象,第二步是根据 Controller 方法入参,匹配对应的 Java 对象再传入 Controller 方法进行调用。
5.0 版本源码
下面是 RequestMappingHandlerAdapter 的 handle 方法:
// 5.0
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");
InitBinderBindingContext bindingContext = new InitBinderBindingContext(
getWebBindingInitializer(), this.methodResolver.getInitBinderMethods(handlerMethod));
// 设置并获取 Resolver
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
Function<Throwable, Mono<HandlerResult>> exceptionHandler =
ex -> handleException(ex, handlerMethod, bindingContext, exchange);
return this.modelInitializer
.initModel(handlerMethod, bindingContext, exchange)
// 发起调用
.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
.doOnNext(result -> bindingContext.saveModel())
.onErrorResume(exceptionHandler);
}
在 invocableMethod#invoke 的内部实现中,通过 getMethodArgumentValues 方法,调用 Resolvers 解析控制器方法参数。这里的 Resolvers 实现了 HandlerMethodArgumentResolverComposite 接口,也就是多个 HandlerMethodArgumentResolver 的复合实现,可以灵活地组合多个参数解析器,以支持更多的参数类型和注解。
而具体的处理逻辑应该看 HandlerMethodArgumentResolver 接口实现类,比如说 @RequestBody 注解的参数通过 RequestBodyMethodArgumentResolver 进行解析。查看 RequestBodyMethodArgumentResolver#resolveArgument 方法可以找到具体实现逻辑。HandlerMethodArgumentResolver 也不仅仅包括解析控制器的方法入参,也能够根据 @PathVariable、@RequestParam、@RequestHeader 等注解解析 HTTP 请求的其他信息。
// 5.0
private Mono<Object[]> getMethodArgumentValues(
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
List<Mono<Object>> argMonos = new ArrayList<>(parameters.length);
for (MethodParameter parameter : parameters) {
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
Object providedArg = findProvidedArgument(parameter, providedArgs);
if (providedArg != null) {
argMonos.add(Mono.just(providedArg));
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
return Mono.error(new IllegalStateException(
formatArgumentError(parameter, "No suitable resolver")));
}
try {
// Resolver 处理参数
argMonos.add(this.resolvers.resolveArgument(parameter, bindingContext, exchange)
.defaultIfEmpty(NO_ARG_VALUE)
.doOnError(ex -> logArgumentErrorIfNecessary(exchange, parameter, ex)));
}
catch (Exception ex) {
logArgumentErrorIfNecessary(exchange, parameter, ex);
argMonos.add(Mono.error(ex));
}
}
return Mono.zip(argMonos, values ->
Stream.of(values).map(value -> value != NO_ARG_VALUE ? value : null).toArray());
}
invoke 发放中的入参 providedArgs 默认不传递,若能找到 parameter 匹配的 Resolver,会调用 resolveArgument 方法。假设此处调用了 RequestBodyMethodArgumentResolver 的方法,则可以通过 readBody 方法获取到入参对象。Spring WebFlux 使用 HttpMessageReader 和 HttpMessageWriter 接口来转换 HTTP 请求和响应,因此在 readBody 方法中,可以看到从 reader 获取对象的逻辑:
// 5.0
for (HttpMessageReader<?> reader : getMessageReaders()) {
if (reader.canRead(elementType, mediaType)) {
Map<String, Object> readHints = Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix());
if (adapter != null && adapter.isMultiValue()) {
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "0..N [" + elementType + "]");
}
// 使用 HttpMessageReader 读取请求
Flux<?> flux = reader.read(actualType, elementType, request, response, readHints);
flux = flux.onErrorResume(ex -> Flux.error(handleReadError(bodyParam, ex)));
if (isBodyRequired) {
flux = flux.switchIfEmpty(Flux.error(() -> handleMissingBody(bodyParam)));
}
if (hints != null) {
flux = flux.doOnNext(target ->
validate(target, hints, bodyParam, bindingContext, exchange));
}
return Mono.just(adapter.fromPublisher(flux));
}
else {
// Single-value (with or without reactive type wrapper)
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "0..1 [" + elementType + "]");
}
Mono<?> mono = reader.readMono(actualType, elementType, request, response, readHints);
mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParam, ex)));
if (isBodyRequired) {
mono = mono.switchIfEmpty(Mono.error(() -> handleMissingBody(bodyParam)));
}
if (hints != null) {
mono = mono.doOnNext(target ->
validate(target, hints, bodyParam, bindingContext, exchange));
}
return (adapter != null ? Mono.just(adapter.fromPublisher(mono)) : Mono.from(mono));
}
}
}
看到这里会有一个疑惑,就是为什么代码中是 HttpMessageReader 而不是 HttpMessageConverter。HttpMessageReader 是由 5.0 版本引入的,支持响应式编程的接口,支持读取 ReactiveHttpInputMessage 并编码为对象流。
3.0 版本源码
3.0 版本的代码,逻辑则更加清晰:
下面是通过 DispatcherServlet 进入到 RequestMappingHandlerAdapter 的 handleInternal 方法。
// RequestMappingHandlerAdapter
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
// 根据请求的内容,解析出方法需要的参数,并准备好这些参数的值
// 通过Java的反射机制,调用目标处理器方法
// 处理器方法执行完毕后,invokeHandlerMethod 会获取返回值,并根据返回值的类型做出相应的处理
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
// 同上
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
这里进入 invokeHandlerMethod,该方法会通过传入的 handlerMethod 获取到 invocableMethod,并调用 invocableMethod 的 invokeAndHandle 方法:
// RequestMappingHandlerAdapter
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// 使用HTTP请求和响应对象创建 ServletWebRequest 对象,它封装了请求和响应的相关信息
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 根据处理器方法,获取数据绑定工厂和模型工厂,用于处理请求参数的绑定和模型数据的准备
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 根据处理器方法创建 ServletInvocableHandlerMethod 对象
// 它是一个可调用的处理器方法,封装了目标处理器方法的信息
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
// 如果已配置了参数解析器和返回值处理器,则将它们设置到 ServletInvocableHandlerMethod 对象中
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
// 创建 ModelAndViewContainer 对象,用于存储处理结果和视图信息
// 并且从请求中获取输入的 FlashMap 并添加到 ModelAndViewContainer 中
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
// 为异步处理准备 AsyncWebRequest 对象
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
// 获取当前请求的 WebAsyncManager 对象,用于处理异步请求
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
// 如果存在并发结果(通过异步方式处理的结果),则将其处理,并更新 ServletInvocableHandlerMethod 对象以包含这些结果
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
// 调用 ServletInvocableHandlerMethod 对象的 invokeAndHandle 方法,执行目标处理器方法并处理结果
invocableMethod.invokeAndHandle(webRequest, mavContainer);
// 检查异步处理是否已经启动,如果是,则返回 null
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
// 从 ModelAndViewContainer 和 ModelFactory 中获取数据,构建并返回最终的 ModelAndView 对象
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
// 标记请求完成
webRequest.requestCompleted();
}
}
invokeAndHandle 方法内通过 invokeForRequest 方法获取到返回值,该方法内部则又看到了熟悉的 getMethodArgumentValues
// ServletInvocableHandlerMethod
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 调用 invokeForRequest 方法执行目标处理器方法
// 传入 ServletWebRequest 对象、ModelAndViewContainer 对象和其他提供的参数
// 方法的返回值保存在 returnValue 变量中
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 调用 setResponseStatus 方法来设置响应的状态码
setResponseStatus(webRequest);
// 返回值为 null
if (returnValue == null) {
// 如果
// 1. 请求未被修改(通过判断是否为未修改请求)
// 2. 或者已经设置了响应状态码
// 3. 或者 ModelAndViewContainer 标记为已处理请求
// 则禁用内容缓存并将 ModelAndViewContainer 标记为已处理,并直接返回
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
// 返回值不为 null,但是具有响应状态码的原因(通过 getResponseStatusReason 方法判断)
} else if (StringUtils.hasText(getResponseStatusReason())) {
// 则将 ModelAndViewContainer 标记为已处理请求,并直接返回
mavContainer.setRequestHandled(true);
return;
}
// 如果没有满足上述条件,将 ModelAndViewContainer 标记为未处理请求,并确保 returnValueHandlers 不为 null
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 尝试使用 returnValueHandlers 来处理返回值
// returnValueHandlers 是一组用于处理控制器方法返回值的策略
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
------
// InvocableHandlerMethod
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 调用 getMethodArgumentValues 方法获取目标处理器方法的参数值
// 这个方法会解析请求,准备方法调用所需的参数
// 入参包括 NativeWebRequest 对象、ModelAndViewContainer 对象(可能为 null),以及其他提供的参数
// 这些参数将在方法内部被解析和处理,最终得到一个参数值的数组
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
------
// InvocableHandlerMethod
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 调用 getMethodParameters 方法获取目标处理器方法的参数信息
MethodParameter[] parameters = getMethodParameters();
// 如果没有参数,则直接返回一个空数组
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
// 遍历方法的参数列表
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
// 对每个方法参数对象调用 initParameterNameDiscovery 方法初始化参数名发现器,以便后续解析参数名
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
// 首先尝试在提供的参数数组中查找是否有提供的参数值。如果有则直接将提供的参数值赋给对应的参数变量
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 如果没有提供的参数值,则判断当前方法参数是否由当前的参数解析器(resolvers)支持
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 如果支持当前参数,则调用参数解析器的 resolveArgument 方法来解析参数值,并将解析结果赋给对应的参数变量
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
------
// HandlerMethodArgumentResolverComposite
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 获取参数处理器
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
// 参数处理
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
最终的链路还是一致的,到了 resolver.resolveArgument 方法。我们还是以 RequestResponseBodyMethodProcessor 为例:
// RequestResponseBodyMethodProcessor
// 该方法用于解析控制器方法的参数,特别是用于处理使用 @RequestBody 和 @ResponseBody 注解的方法参数
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 调用 readWithMessageConverters 方法
// 使用 HttpMessageConverters 去解析请求体,并获取参数值。这一步骤将请求体中的数据转换成方法参数所需的类型
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 获取参数的名称,用于后续数据绑定
String name = Conventions.getVariableNameForParameter(parameter);
// 如果存在 binderFactory(数据绑定工厂)
if (binderFactory != null) {
// 调用 createBinder 方法创建数据绑定器,并对参数值进行数据绑定
// 数据绑定器会将请求参数的值绑定到方法参数上,并进行验证
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
// 如果参数值不为空,则进行数据验证,如果验证失败且需要抛出绑定异常,则抛出 MethodArgumentNotValidException 异常
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 如果存在 mavContainer(ModelAndViewContainer),则将数据绑定的结果添加到 mavContainer 中
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
// 根据需要对参数值进行适应。这一步骤可能涉及对参数值的进一步处理或转换
return adaptArgumentIfNecessary(arg, parameter);
}
------
// RequestResponseBodyMethodProcessor
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// 从 NativeWebRequest 中获取 HttpServletRequest 对象。这个对象是原生的HTTP请求对象
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
// 创建 ServletServerHttpRequest 对象。这个对象封装了HTTP请求的信息,方便后续对请求体的处理
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
// 调用另一个重载的 readWithMessageConverters 方法,传入 ServletServerHttpRequest 对象、方法参数对象和参数类型
// 这个方法是实际执行消息转换的关键步骤,它会尝试使用消息转换器将请求体转换为方法参数所需的类型,并返回转换后的参数值
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
最后进入 readWithMessageConverters 方法,这里也是完成与 HttpMessageConverter 关联的位置:
// AbstractMessageConverterMethodArgumentResolver
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
try {
// 尝试从输入消息的头部获取内容类型(Content-Type)
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
// 如果获取失败,则抛出 HttpMediaTypeNotSupportedException 异常
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
// 如果没有指定内容类型,则将内容类型设为默认的 MediaType.APPLICATION_OCTET_STREAM。
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
// 根据方法参数的类型和目标类型,确定转换后的目标类型
Class<?> contextClass = parameter.getContainingClass();
// 如果目标类型不是一个 Class 类型,则从方法参数中获取相应的 Class 类型
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}
// 如果输入消息是一个 HttpRequest 类型的消息,则获取其中的 HTTP 请求方法
// 这用于后续判断是否支持当前 HTTP 请求方法
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;
AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage message;
try {
// 初始化一些变量,包括 body、message 等
message = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
// 对注册的消息转换器列表进行遍历,尝试使用每个消息转换器来读取输入消息并进行转换
for (HttpMessageConverter<?> converter : this.messageConverters) {
// 对每个消息转换器,判断它是否能够处理当前请求
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
// 如果能够处理,则根据消息转换器的类型进行相应的读取和转换操作
// genericConverter 不为 null,表示当前转换器是一个通用的消息转换器,可以处理任意类型的目标对象
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
// genericConverter 为 null,表示当前转换器不是通用的,此时判断是否支持指定目标类型和内容类型
(targetClass != null && converter.canRead(targetClass, contentType))) {
// 输入消息中存在消息体
if (message.hasBody()) {
// 调用 beforeBodyRead 方法,对消息体进行预处理
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
// 使用消息转换器将输入消息的消息体转换为目标类型的对象
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
// 对转换后的结果进行后处理
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
// 如果输入消息中没有消息体,则调用相应的回调方法处理空消息体
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// 如果没有成功地读取到消息体
if (body == NO_VALUE) {
// 并且请求方法不在支持的方法列表中,或者消息体为空且没有指定内容类型
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
// 则返回 null
return null;
}
// 否则,抛出 HttpMediaTypeNotSupportedException 异常
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}
MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});
// 如果成功读取到了消息体并进行了转换,则返回转换后的结果
// 在返回之前,记录日志以便跟踪转换的结果和过程
return body;
}
处理 Multipart/form-data 时会遇到的困难
创建自定义 RequestWrapper 继承 HttpServletRequestWrapper 前,就需要读取请求体再进行解密操作。
Servlet 容器(比如 Tomcat、Jetty 等)在接收到 HTTP 请求后,会解析请求头和消息体,并提供一种方式让 Servlet 来读取这些数据。Servlet 通常通过 HttpServletRequest 对象提供的 getReader() 方法(用于读取字符数据)或 getInputStream() 方法(用于读取字节数据)来读取请求体中的数据。
然而,由于 HTTP 协议的设计,一旦读取了请求体中的数据,它们就会从输入流中被读取并且被消费掉了。换句话说,一旦调用了 getReader() 或 getInputStream() 方法,Servlet 容器会从请求中读取消息体,并将它们提供给Servlet。一旦 Servlet 读取完消息体或者关闭了输入流,消息体中的数据就不再可用了。因此,再次调用 getReader() 或getInputStream() 方法将不会返回有效的数据。可以在 Catalina 的 Request 方法中看到,usingReader 字段用于控制 getReader() 或 getInputSream() 方法被调用两次。
// Request
@Override
public BufferedReader getReader() throws IOException {
if (usingInputStream) {
throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
}
// InputBuffer has no easily accessible reference chain to the Context
// to check for a default request character encoding at the Context.
// Therefore, if a Context default should be used, it is set explicitly
// here. Need to do this before setting usingReader.
if (coyoteRequest.getCharacterEncoding() == null) {
// Nothing currently set explicitly.
// Check the content
Context context = getContext();
if (context != null) {
String enc = context.getRequestCharacterEncoding();
if (enc != null) {
// Explicitly set the context default so it is visible to
// InputBuffer when creating the Reader.
setCharacterEncoding(enc);
}
}
}
usingReader = true;
inputBuffer.checkConverter();
if (reader == null) {
reader = new CoyoteReader(inputBuffer);
}
return reader;
}
对普通的 application/json 的请求来说,只调用 getReader() 方法,通过 RequestWrapper 缓存流内容,并重写 getReader() 方法,即可规避,比较好处理。
但是在处理 multipart/form-data 的请求时,会使用 StandardServletMultipartResolver
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
在 StandardMultipartHttpServletRequest 中,又有如下方法:
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}
private void parseRequest(HttpServletRequest request) {
try {
Collection<Part> parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
for (Part part : parts) {
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
String filename = disposition.getFilename();
if (filename != null) {
if (filename.startsWith("=?") && filename.endsWith("?=")) {
filename = MimeDelegate.decode(filename);
}
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
}
catch (Throwable ex) {
handleParseFailure(ex);
}
}
可以看到调用了 request.getParts() 方法。这就要求 RequestWrapper 也必须实现 getParts() 方法,通过缓存的流内容构建 parts 列表,否则因为未实现该方法,会尝试调用 Request 的 getParts() 方法。此次调用 getParts() 方法将会导致异常如下:
java.io.IOException: Stream closed
at org.apache.catalina.connector.InputBuffer.throwIfClosed(InputBuffer.java:545) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:356) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at java.io.FilterInputStream.read(FilterInputStream.java:133) ~[na:1.8.0_431]
at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:132) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:953) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:857) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at java.io.InputStream.read(InputStream.java:101) ~[na:1.8.0_431]
at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:96) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:67) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.MultipartStream.readBodyData(MultipartStream.java:571) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.MultipartStream.discardBodyData(MultipartStream.java:595) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.MultipartStream.skipPreamble(MultipartStream.java:613) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl.findNextItem(FileItemIteratorImpl.java:228) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl.<init>(FileItemIteratorImpl.java:142) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:252) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:276) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.catalina.connector.Request.parseParts(Request.java:2625) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.catalina.connector.Request.getParts(Request.java:2526) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:795) ~[tomcat-embed-core-9.0.102.jar:9.0.102]
at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:326) ~[tomcat-embed-core-9.0.102.jar:4.0.FR]
不通过缓存的流构造 Parts 将会导致 IOException,因为流已被关闭。
可以通过 apache-commons-fileupload 处理 part,通过缓存中的流返回 parts 列表,规避异常。
最后 RequestWrapper 实现如下:
package com.wujunhao1024.wrapper;
import com.wujunhao1024.common.FileItemAdapter;
import lombok.SneakyThrows;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.Part;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author wujh
* @since 2024-2-6
*/
public class DecryptedRequestWrapper extends HttpServletRequestWrapper {
private final byte[] decryptedBody;
private final String contentType;
private final String characterEncoding;
private Map<String, Part> cachedParts;
private int pos = 0;
public DecryptedRequestWrapper(HttpServletRequest request, byte[] decryptedBody) {
super(request);
this.decryptedBody = decryptedBody;
// 保留原始 Content-Type
this.contentType = request.getContentType();
this.characterEncoding = request.getCharacterEncoding();
}
@Override
public ServletInputStream getInputStream() {
// return new DecryptedServletInputStream(new ByteArrayInputStream(decryptedBody));
return new ServletInputStream() {
@Override
public int read() {
if (pos >= decryptedBody.length) {
return -1;
}
return decryptedBody[pos++] & 0xFF;
}
@Override
public boolean isFinished() {
return pos >= decryptedBody.length;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("Not supported");
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public int getContentLength() {
return decryptedBody.length;
}
@Override
public long getContentLengthLong() {
return decryptedBody.length;
}
@Override
public String getCharacterEncoding() {
return super.getCharacterEncoding() != null ? super.getCharacterEncoding() : "UTF-8";
}
@Override
public String getContentType() {
return this.contentType;
}
private static class DecryptedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream bais;
public DecryptedServletInputStream(ByteArrayInputStream bais) {
this.bais = bais;
}
@Override
public int read() {
return bais.read();
}
@Override
public boolean isFinished() {
return bais.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("不支持异步读取");
}
}
@SneakyThrows
@Override
public Collection<Part> getParts() {
// 如果不是 multipart 类型,直接调用父类方法(可能返回空)
if (!isMultipart()) {
return super.getParts();
}
// 使用 Apache Commons FileUpload 解析
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(new RequestContext() {
@Override
public String getCharacterEncoding() {
return characterEncoding;
}
@Override
public String getContentType() {
return getRequest().getContentType();
}
@Override
public int getContentLength() {
return decryptedBody.length;
}
@Override
public InputStream getInputStream() {
return DecryptedRequestWrapper.this.getInputStream();
}
});
// 转换为 Part 接口
List<Part> parts = new ArrayList<>();
for (FileItem item : items) {
parts.add(new FileItemAdapter(item));
}
if (cachedParts == null) {
cachedParts = parts.stream().collect(Collectors.toMap(Part::getName, part -> part));
}
return parts;
}
@Override
public Part getPart(String name) {
if (cachedParts == null) {
getParts();
}
return cachedParts.get(name);
}
private boolean isMultipart() {
String contentType = getContentType();
return contentType != null && contentType.toLowerCase().startsWith("multipart/");
}
}
package com.wujunhao1024.common;
import org.apache.commons.fileupload.FileItem;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public class FileItemAdapter implements Part {
private final FileItem fileItem;
private final Map<String, List<String>> headers = new HashMap<>();
public FileItemAdapter(FileItem fileItem) {
this.fileItem = fileItem;
initDefaultHeaders();
}
private void initDefaultHeaders() {
// 设置 Content-Disposition
String disposition = "form-data; name=\"" + fileItem.getFieldName() + "\"";
if (fileItem.getName() != null && !fileItem.getName().isEmpty()) {
disposition += "; filename=\"" + fileItem.getName() + "\"";
}
addHeader("Content-Disposition", disposition);
// 设置 Content-Type(如果有)
if (fileItem.getContentType() != null) {
addHeader("Content-Type", fileItem.getContentType());
}
}
public void addHeader(String name, String value) {
headers.computeIfAbsent(name.toLowerCase(), k -> new ArrayList<>()).add(value);
}
@Override
public InputStream getInputStream() throws IOException {
return fileItem.getInputStream();
}
@Override
public String getContentType() {
return fileItem.getContentType();
}
@Override
public String getName() {
return fileItem.getFieldName();
}
@Override
public String getSubmittedFileName() {
return fileItem.getName();
}
@Override
public long getSize() {
return fileItem.getSize();
}
@Override
public void write(String fileName) throws IOException {
try {
fileItem.write(new File(fileName));
} catch (Exception e) {
throw new IOException("写入文件失败", e);
}
}
@Override
public void delete() throws IOException {
fileItem.delete();
}
@Override
public Collection<String> getHeaderNames() {
return Collections.unmodifiableSet(headers.keySet());
}
@Override
public String getHeader(String name) {
List<String> values = headers.get(name.toLowerCase());
return values == null || values.isEmpty() ? null : values.get(0);
}
@Override
public Collection<String> getHeaders(String name) {
List<String> values = headers.get(name.toLowerCase());
return values == null ? Collections.emptyList() : Collections.unmodifiableList(values);
}
@Override
public String toString() {
return "FileItemAdapter{" +
"name='" + getName() + '\'' +
", submittedFileName='" + getSubmittedFileName() + '\'' +
", size=" + getSize() +
'}';
}
}