如何 return 来自 Spring 引导端点服务的自定义 SOAP 错误?

How to return custom SOAP Error from Spring Boot Endpoint Service?

我已经设置了一个 Web 服务应用程序,它接收并仅记录来自第三方的 SOAP 请求。记录后必须返回定义的响应。如果没有错误并且接收到的 SOAP 请求与 WSDL 相匹配,那么这将毫无问题地工作。 不幸的是,当第三方发送无效内容甚至随机数据时,它也希望得到正确的 SOAP 响应。

如果请求包含随机数据(例如 "zewrzasjkfklj"),我的服务 returns 一个 HTTP/400 带有空主体的错误请求。 如果请求包含 x XML 但不是 Soap(例如“”),服务 returns 一个 HTTP/500 服务器错误和 JSON 主体

{"timestamp":"2018-12-06T16:16:29.375+0000","status":500,"error":"Internal Server Error","message":"Could not create message from InputStream: Unable to create envelope from given source: ; nested exception is com.sun.xml.internal.messaging.saaj.SOAPExceptionImpl: Unable to create envelope from given source: ","path":"/NotificationServicePort"}

这让我特别困惑,因为我在项目中的任何地方都没有与 JSON.

相关的跟踪或配置

端点是一个 class 用 @Endpoint 注释的实现

...    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "notify")
    @ResponsePayload
    public JAXBElement<NotifyResponse> notify(@RequestPayload Notify request) {
...}

(但在无效请求的情况下永远不会达到此方法)。

我已经尝试 implement/provide 拦截器、调度器、ErrorMappers,...但结果没有改变。 似乎在后一种情况下(有效 XML 但没有 SOAP)在尝试提取 SOAPPartImpl.lookForEnvelope() 处的信封时失败并失败 抛出新的 SOAPExceptionImpl("Unable to create envelope from given source because the root element is not named \"Envelope\""); 该错误处的断点提供以下堆栈:

lookForEnvelope:153, SOAPPartImpl (com.sun.xml.internal.messaging.saaj.soap)
getEnvelope:121, SOAPPartImpl (com.sun.xml.internal.messaging.saaj.soap)
createEnvelope:110, EnvelopeFactory (com.sun.xml.internal.messaging.saaj.soap)
createEnvelopeFromSource:69, SOAPPart1_1Impl (com.sun.xml.internal.messaging.saaj.soap.ver1_1)
getEnvelope:128, SOAPPartImpl (com.sun.xml.internal.messaging.saaj.soap)
createWebServiceMessage:189, SaajSoapMessageFactory (org.springframework.ws.soap.saaj)
createWebServiceMessage:60, SaajSoapMessageFactory (org.springframework.ws.soap.saaj)
receive:92, AbstractWebServiceConnection (org.springframework.ws.transport)
handleConnection:87, WebServiceMessageReceiverObjectSupport (org.springframework.ws.transport.support)
handle:61, WebServiceMessageReceiverHandlerAdapter (org.springframework.ws.transport.http)
doService:293, MessageDispatcherServlet (org.springframework.ws.transport.http)
processRequest:974, FrameworkServlet (org.springframework.web.servlet)
doPost:877, FrameworkServlet (org.springframework.web.servlet)
service:661, HttpServlet (javax.servlet.http)
service:851, FrameworkServlet (org.springframework.web.servlet)
service:742, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:246, AbstractRequestLoggingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
filterAndRecordMetrics:158, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
filterAndRecordMetrics:126, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
doFilterInternal:111, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:90, HttpTraceFilter (org.springframework.boot.actuate.web.trace.servlet)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:320, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
invoke:127, FilterSecurityInterceptor (org.springframework.security.web.access.intercept)
doFilter:91, FilterSecurityInterceptor (org.springframework.security.web.access.intercept)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:119, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:137, SessionManagementFilter (org.springframework.security.web.session)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:111, AnonymousAuthenticationFilter (org.springframework.security.web.authentication)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:170, SecurityContextHolderAwareRequestFilter (org.springframework.security.web.servletapi)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:63, RequestCacheAwareFilter (org.springframework.security.web.savedrequest)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:158, BasicAuthenticationFilter (org.springframework.security.web.authentication.www)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:116, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:66, HeaderWriterFilter (org.springframework.security.web.header)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:105, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:56, WebAsyncManagerIntegrationFilter (org.springframework.security.web.context.request.async)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:215, FilterChainProxy (org.springframework.security.web)
doFilter:178, FilterChainProxy (org.springframework.security.web)
invokeDelegate:357, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:270, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:99, RequestContextFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:109, HttpPutFormContentFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, HiddenHttpMethodFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:200, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:198, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:496, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:140, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:803, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:790, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1468, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

如果有任何提示或进一步的建议,我将不胜感激,如果请求甚至没有到达 SOAP 处理逻辑。

最后它必须是几个挂钩的组合,因为似乎没有一个点或配置可用,其中所有 errors/issues 与端点相关并允许生成自定义响应。

以下是我最终想出的解决方案:

我可以捆绑自定义响应生成的主要位置是自定义 MessageDispatcherServlet:

...
// this custom dispatcher is responsible for sending back a faked "SOAP" like response upon any type of
// misformatted request or error.
@Component
public class CustomSoapErrorMessageDispatcherServlet extends MessageDispatcherServlet {

    @Override
    protected void doService(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws Exception {
        Exception thrownException = null;

        try {
            super.doService(httpServletRequest, httpServletResponse);
        } catch (CustomSoapValidationException | SoapMessageCreationException e) {
            LOG.warn("Processing resulted in exception: " + e.getMessage()); //
            thrownException = e;
            httpServletResponse.setStatus(400);
        } catch (Exception e) {
            LOG.warn("Processing resulted in generic exception: " + e.getMessage()); //
            thrownException = e;
            httpServletResponse.setStatus(500);
        }

        int responseStatus = httpServletResponse.getStatus();

        // Response in HTTP OK Range? Do nothing.
        if (responseStatus >= 200 && responseStatus <= 299) {
            return;
        }

        /*
        In any case of any error send a SOAP-like response. 
         */
        String errorCode, errorMessage;

        // failure during SOAP interpretion? ie. XML received but not SOAP or invalid structure, ....
        if(thrownException instanceof SoapMessageException) {
            errorCode = "110";
            errorMessage = "Generic SOAP Exception: " + thrownException.getMessage();
        }
        // did our structure validation fail?
        else if (thrownException instanceof CustomSoapValidationException) {
            errorCode = "110";
            errorMessage = "Structure error in request: " + thrownException.getMessage();
        }
        // another exception unrelated to Soap Processing?
        else if (thrownException != null) {
            errorCode = "999";
            errorMessage = "Internal error: " + thrownException.getMessage();
        }
        // generic internal error, but not throwing exception?
        else if (responseStatus >= 400 && responseStatus <= 499) {
            errorCode = String.valueOf(responseStatus);
            errorMessage = "Generic unspecific request processing error.";
        }
        // something completely unexpected
        else {
            errorCode = "500";
            errorMessage = "Unexpected condition.";
        }

        String responseBody = generateSoapErrorContent(errorCode, errorMessage);
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        outputStream.print(responseBody);
        outputStream.flush();
    }   
    ...
}
...

我通过我的配置 class 和

激活了
...
    @Autowired
    private CustomSoapErrorMessageDispatcherServlet dispatcherServlet;

    @Bean
    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        dispatcherServlet.setApplicationContext(applicationContext);
        dispatcherServlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(dispatcherServlet, "/NotificationServicePort/*");
    }
...

仅此自定义调度程序就能够捕获包含(有效和无效)XML但不完全是 SOAP 或包含随机数据的请求。为了同时覆盖无效的 SOAP 请求,还需要一些额外的步骤。

首先是执行模式验证并抛出自定义异常的自定义拦截器(而不是像 PayloadValidatingInterceptor 那样立即响应 SOAP 错误):

...
public class CustomValidatingInterceptor extends PayloadValidatingInterceptor {

    @Override
    protected boolean handleRequestValidationErrors(MessageContext messageContext, SAXParseException[] errors)
            throws TransformerException {

        // if any validation errors, convert them to a string and throw on as Exception to be handled by CustomSoapErrorMessageDispatcherServlet
        if (errors.length > 0) {
            String validationErrorsString = Arrays.stream(errors)
                    .map(error -> "[" + error.getLineNumber() + "," + error.getColumnNumber() + "]: " + error.getMessage())
                    .collect(Collectors.joining(" -- "));
            throw new CustomSoapValidationException("Validation Errors: " + validationErrorsString);
        }
        return true;
    }
}
...

这是在我的配置 class 中配置的(现在必须从 WsConfigurerAdapter 扩展)通过

...
public class WebServiceConfig extends WsConfigurerAdapter {
...
    @Override
    public void addInterceptors(List<EndpointInterceptor> interceptors) {
        // validate requests and responses
        // cannot use PayloadValidatingInterceptor because that one would generate an unwanted/unavoidable SoapFault
        CustomValidatingInterceptor validatingInterceptor = new CustomValidatingInterceptor();
        validatingInterceptor.setValidateRequest(true);
        validatingInterceptor.setValidateResponse(false);
        validatingInterceptor.setXsdSchema(customApiSchema());
        interceptors.add(validatingInterceptor);
    }
...

其次,现在抛出的 CustomSoapValidationException 仍会在端点解析逻辑中导致标准 SOAP 错误,这就是我们还创建自定义 EndpointExceptionResolver 的原因。这是在异常处理期间调用的,并将我们的拦截器验证错误再次修改为 "live" 异常,然后可以从第一步将调用堆栈弹回我们的 CustomSoapErrorMessageDispatcherServlet。

...
// class is automatically picked up by MessageDispatcher during request handling when an exception occurs after dispatching
@Component
public class CustomizedSoapFaultDefinitionExceptionResolver implements EndpointExceptionResolver {
    public boolean resolveException(MessageContext messageContext, Object endpoint, Exception ex) {
        if (ex instanceof CustomSoapValidationException) {
            throw (CustomSoapValidationException) ex;
        }
        return false;
    }
}
...

这不需要额外的配置,但 Spring Boot MessageDispatcher 现在会自动获取。

随着所有这些步骤的发生 errors/exceptions/failures/... 在我们的 CustomSoapErrorMessageDispatcherServlet.doService() 中以一种或另一种方式结束,我们在其中获取异常或调查尚未发送的 HttpServletResponse并且可以构建自定义的 SOAP 外观响应以满足我们的要求。