如何从 Spring Boot Endpoint Service 返回自定义 SOAP 错误?

Kos*_*801 5 soap web-services custom-error-handling spring-boot jakarta-ee

我已经设置了一个 Web 服务应用程序,它接收并仅记录来自第三方的 SOAP 请求。记录后必须返回定义的响应。如果没有错误并且接收到的 SOAP 请求与 WSDL 匹配,则此操作不会出现任何问题。不幸的是,第三方在发送无效内容甚至随机数据时也期望正确的 SOAP 响应。

如果请求包含随机数据(例如“zewrzasjkfklj”),我的服务将返回带有空正文的 HTTP/400 错误请求。如果请求包含 XML 但不包含 Soap(例如“”),则服务将返回带有 JSON 正文的 HTTP/500 服务器错误

{"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"}
Run Code Online (Sandbox Code Playgroud)

这让我特别困惑,因为我在项目中没有任何与 JSON 相关的跟踪或配置。

端点是一个用@Endpoint注释的类,它实现了

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

(但如果请求无效,则永远不会到达此方法)。

我已经尝试实现/提供拦截器、调度程序、ErrorMappers,...但结果没有改变。似乎在后一种情况下(有效的 XML 但没有 SOAP),尝试在 SOAPPartImpl.lookForEnvelope() 处提取信封时失败,并失败并抛出 throw new SOAPExceptionImpl("Unable to create Envelope from给定源,因为根元素不是命名为“信封”);该错误处的断点给出以下堆栈:

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)
Run Code Online (Sandbox Code Playgroud)

我将不胜感激任何提示或进一步的建议,如果请求甚至没有到达 SOAP 处理逻辑,我可以在其中找到有关如何设置默认 SOAP 响应(或具有 SOAP 消息文本内容的 HTML 响应)的更多信息。

Kos*_*801 4

最后,它必须是多个钩子的组合,因为似乎没有可用的单个点或配置可以使与端点相关的所有错误/问题通过并允许生成自定义响应。

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

我可以捆绑生成自定义响应的主要位置是自定义的 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();
    }   
    ...
}
...
Run Code Online (Sandbox Code Playgroud)

我通过我的配置类激活它

...
    @Autowired
    private CustomSoapErrorMessageDispatcherServlet dispatcherServlet;

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

仅此自定义调度程序就只能捕获包含(有效和无效)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;
    }
}
...
Run Code Online (Sandbox Code Playgroud)

这是在我的配置类中配置的(现在必须从 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);
    }
...
Run Code Online (Sandbox Code Playgroud)

其次,现在抛出的 CustomSoapValidationException 仍然会在端点解析逻辑中导致标准 SOAP 错误,这就是我们还创建自定义 EndpointExceptionResolver 的原因。这在异常处理期间被调用,并将我们的拦截器验证错误再次修改为“实时”异常,然后可以将调用堆栈从第一步弹回到我们的 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;
    }
}
...
Run Code Online (Sandbox Code Playgroud)

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

通过所有这些步骤,所有发生的错误/异常/失败/...最终都会以某种方式结束在我们的 CustomSoapErrorMessageDispatcherServlet.doService() 中,我们在其中获取异常或调查尚未发送的 HttpServletResponse 并可以构建自定义 SOAP-寻找满足我们要求的回应。