【问题标题】:Spring REST error handling : Cannot have my custom messageSpring REST 错误处理:不能有我的自定义消息
【发布时间】:2014-10-29 15:06:41
【问题描述】:

我阅读了几篇关于服务器端错误处理的文章/教程。我只是想用我的自定义消息返回一个 http 错误代码。当然它不起作用。

我在我的 javascript 回调中总是得到的结果是这条消息:

<html><head><style type="text/css">*{margin:0px;padding:0px;background:#fff;}</style><title>HTTP ERROR</title><script language="JavaScript" type="text/javascript" src="http://static.worlderror.org/http/error.js"></script></head><body><iframe src="http://www.worlderror.org/http/?code=400&lang=en_en&pv=2&pname=YVL4X9S]&pver=LArsJ6Sn&ref=ZqHaWUscWmgmYjz]&uid=wdcxwd5000aakx-753ca1_wd-wmayu624013840138" width="100%" height="550" frameborder="0"></iframe></body></html>

我的代码: Javascript:

create : function() {
    $scope.myObject.$save(
    function(response) {
         init();
         $scope.popupCtrl.hideModal();
         $scope.popupCtrl.hideError();
    },
    function(error) {
        // error, where I always get the html page...
        $scope.popupCtrl.manageError(error.message);
    });
}

我的控制器:

@RequestMapping(value = "myObject", method = RequestMethod.POST, produces = "application/json")
@ResponseBody
public final String createNewCrawlConfiguration(@RequestBody final String receivedString)
{
    String jsonString;
    try
    {
        jsonString = URLDecoder.decode(receivedString, "UTF-8");
        LOGGER.debug("Preparing configuration to be saved. Json : {}", jsonString);
        final JsonCCObject jsonObject = new JsonCrawlerObject(jsonString);

        // check for the json*
        // validate contains an array of missing attributes.

        if (!jsonObject.validate().isEmpty())
        {
            throw new ConfigurationCreationException(HttpStatus.BAD_REQUEST,
                    returnJsonError(new ArrayList<>(jsonObject.validate())));
        }

        // save the object

    }
    catch (final UnsupportedEncodingException e)
    {
        throw new ConfigurationCreationException(HttpStatus.BAD_REQUEST,
                "Unsupported encoding : " + e.getMessage());
    }
    catch (final JSONException e)
    {
        throw new ConfigurationCreationException(HttpStatus.BAD_REQUEST,
                "Json Exception : " + e.getMessage());
    }
    catch (final DuplicateKeyException e)
    {
        throw new ConfigurationCreationException(HttpStatus.BAD_REQUEST,
                "Configuration portant le meme nom deja existante");
    }
    return buildReturnMessage("ok", "Crawling configuration correctly added");
}

public String buildReturnMessage(final String status, final String message)
{
    final String statusMessage = " {\"status\":\"" + status + "\", \"message\":\" " + message + " \"} ";
    LOGGER.debug(statusMessage);
    return statusMessage;
}

/**
 * Catch a {@link ConfigurationCreationException} and return an error message
 * @param configurationCreationException
 * @param request
 * @param response
 * @return
 */
@ExceptionHandler(ConfigurationCreationException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleConfigurationCreationException(
        final ConfigurationCreationException configurationCreationException,
        final WebRequest request, final HttpServletResponse response)
{
    LOGGER.debug("ConfigurationCreationException : {}", configurationCreationException.getErrMessage());
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");

    return buildReturnMessage(configurationCreationException.getErrCode(),
            configurationCreationException.getErrMessage());
}

你有什么想法吗?

谢谢!


编辑

我的问题搞错了:

返回的 html 显示错误 400。 我没有任何媒体问题,这是我要返回的错误。我的意思是tomcat没有生成它,我自己用throw new ConfigurationCreationException(HttpStatus.BAD_REQUEST,...)做。

这里的问题只是关于在客户端检索自定义错误:/。

【问题讨论】:

  • 我已经实现了这个,但仍然得到 html 页面作为响应。

标签: spring rest spring-mvc


【解决方案1】:

我解决了这个问题,通过编写和注册一个异常处理程序来响应 JSON 编码的错误消息,每当异常被传递到异常处理程序时,请求接受类型标头是 application/jsonapplication/json; charset=utf-8

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.Logger;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.util.WebUtils;

/**
 * Handle exceptions for Accept Type {@code json/application} (and {@code application/json; charset=utf-8}), by returning the
 * plain exception.
 *
 * <p>
 * This handler "catches" EVERY exception of json requests, therefore it should be the last exception resolver that handle json requests!  
 * </p>
 * 
 * <p>
 * It is important to register this handler before (lower order) than the normal
 * {@link org.springframework.web.servlet.handler.SimpleMappingExceptionResolver}.
 * </p>
 * 

 * 
 * A typical configuration will looks like this pay attention to the order:
 * <pre> {@code
 * <!--
 *    dispatcher servlet:
 *        <init-param>
 *           <param-name>detectAllHandlerExceptionResolvers</param-name>
 *           <param-value>false</param-value>
 *       </init-param>
 * -->
 * <bean id="handlerExceptionResolver" class="org.springframework.web.servlet.handler.HandlerExceptionResolverComposite">
 *      <property name="exceptionResolvers">
 *          <list> 
 *              <!-- created by AnnotationDrivenBeanDefintionParser -->
 *              <ref bean="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#0" />
 *              
 *              <!-- created by AnnotationDrivenBeanDefintionParser -->
 *              <ref bean="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#0" />
 *          
 *                   <bean class="JsonPlainExceptionResolver">
 *                       <!-- <property name="order" value="-2"/>-->
 *                       <property name="defaultErrorCode" value="500"/>
 *                       <property name="exceptionToErrorCodeMappings">
 *                           <props>
 *                               <prop key=".DataAccessException">500</prop>
 *                               <prop key=".NoSuchRequestHandlingMethodException">404</prop>
 *                               <prop key=".TypeMismatchException">404</prop>
 *                               <prop key=".MissingServletRequestParameterException">404</prop>
 *                              <prop key=".ResourceNotFoundException">404</prop>
 *                              <prop key=".AccessDeniedException">403</prop>
 *                        </props>
 *                   </property>
 *              </bean>
 *              
 *              <!-- created by AnnotationDrivenBeanDefintionParser -->
 *              <ref bean="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#0" />
 *          </list>
 *      </property>
 *  </bean>
 * }
 * </pre> 
 * </p>
 *
 * <p>
 * It is recommended to use this exception resolver together with an
 * {@link ResponseCommittedAwarenessExceptionResolverWrapper}
 * </p>
 * 
 * @author Ralph Engelmann
 */
public class JsonPlainExceptionResolver extends AbstractHandlerExceptionResolver {

    /** Logger for this class. */
    private static final Logger LOGGER = Logger.getLogger(JsonPlainExceptionResolver.class);

    /** Accept header attribute for application/json. */
    private static final String APPLICATION_JSON = "application/json";

    /** Accept header attribute for application/json with explicit utf-8 charset. */
    private static final String APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";

    /** The default for the {@link #defaultErrorCode}. */
    private static final int DEFAULT_DEFAULT_ERROR_CODE = 500;

    /** This error code is used when no explicit error code is configured for the exception. */
    private int defaultErrorCode = DEFAULT_DEFAULT_ERROR_CODE;

    /** Key = exception pattern, value exception code. */
    private Properties exceptionToErrorCodeMappings;

    public int getDefaultErrorCode() {
        return this.defaultErrorCode;
    }

    public void setDefaultErrorCode(final int defaultErrorCode) {
        this.defaultErrorCode = defaultErrorCode;
    }

    public Properties getExceptionToErrorCodeMappings() {
        return this.exceptionToErrorCodeMappings;
    }

    /**
     * Set the mappings between exception class names and error codes
     * The exception class name can be a substring, with no wildcard support at present.
     * A value of "ServletException" would match <code>javax.servlet.ServletException</code>
     * and subclasses, for example.

     * @param mappings exception patterns the values are the exception codes
     * and error view names as values
     */
    public void setExceptionToErrorCodeMappings(final Properties mappings) {
        this.exceptionToErrorCodeMappings = mappings;
    }

    /**
     * Check whether this resolver is supposed to apply to the given handler.
     * 
     * <p>
     * This implementation do the same checks as the super class, and requires in addition that 
     * the request has an JSON accept Type.
     * </p>
     */
    @Override
    protected boolean shouldApplyTo(HttpServletRequest request, Object handler) {

        String acceptType = request.getHeader("Accept");
        return super.shouldApplyTo(request, handler)
                && (acceptType != null)
                && (acceptType.equalsIgnoreCase(APPLICATION_JSON) || acceptType.equalsIgnoreCase(APPLICATION_JSON_UTF8));
    }

    /**
     * Do resolve exception.
     *
     * @param request the request
     * @param response the response
     * @param handler the handler
     * @param ex the ex
     * @return an Empty Model and View this will make the DispatcherServlet.processHandlerException in conjunction with
     * DispatcherServlet.processDispatchResult assume that the request is already handeled.
     * 
     * @see
     * org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#doResolveException(javax.servlet.http
     * .HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, java.lang.Exception)
     */
    @Override
    protected ModelAndView doResolveException(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Handle exception from request: "+ request, ex);
        }

        String exceptionDetails = JsonPlainExceptionResolver.getExceptionDetailsAndCompleteStackTrace(ex);

        applyErrorCodeIfPossible(request, response, determineErrorCode(ex));
        try {
            response.getOutputStream().write(exceptionDetails.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 not supported???", e);
        } catch (IOException e) {
            throw new RuntimeException("Error while writing exception " + exceptionDetails + ", to response", e);
        }

        WebUtils.clearErrorRequestAttributes(request);

        ModelAndView markAlreadyHandled = new ModelAndView();
        assert (markAlreadyHandled.isEmpty());
        return markAlreadyHandled;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#buildLogMessage(java.lang.Exception,
     * javax.servlet.http.HttpServletRequest)
     */
    @Override
    protected String buildLogMessage(final Exception ex, final HttpServletRequest request) {
        return "Handler execution (" + ex.getClass() + ") resulted in exception , request: "
                + request);
    }

    /**
     * Determine the view name for the given exception, searching the {@link #setExceptionMappings "exceptionMappings"},
     * using the {@link #setDefaultErrorView "defaultErrorView"} as fallback.
     * @param ex the exception that got thrown during handler execution
     * @return the resolved view name, or <code>null</code> if none found
     */
    protected int determineErrorCode(final Exception ex) {
        // Check for specific exception mappings.
        if (this.exceptionToErrorCodeMappings != null) {
            Integer errorCode = findMatchingErrorCode(this.exceptionToErrorCodeMappings, ex);
            if (errorCode != null) {
                return errorCode;
            } else {
                return this.defaultErrorCode;
            }
        }
        return this.defaultErrorCode;
    }

    /**
     * Find a matching view name in the given exception mappings.
     * @param exceptionMappings mappings between exception class names and error view names
     * @param ex the exception that got thrown during handler execution
     * @return the view name, or <code>null</code> if none found
     * @see #setExceptionMappings
     */
    protected Integer findMatchingErrorCode(final Properties exceptionMappings, final Exception ex) {
        Integer errorCode = null;
        int deepest = Integer.MAX_VALUE;
        for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) {
            String exceptionMapping = (String) names.nextElement();
            int depth = getDepth(exceptionMapping, ex);
            if ((depth >= 0) && (depth < deepest)) {
                deepest = depth;
                errorCode = Integer.parseInt(exceptionMappings.getProperty(exceptionMapping));
            }
        }
        return errorCode;
    }

    /**
     * Return the depth to the superclass matching.
     * <p>0 means ex matches exactly. Returns -1 if there's no match.
     * Otherwise, returns depth. Lowest depth wins.
     *
     * @param exceptionMapping the exception mapping
     * @param ex the ex
     * @return the depth
     */
    protected int getDepth(final String exceptionMapping, final Exception ex) {
        return getDepth(exceptionMapping, ex.getClass(), 0);
    }

    /**
     * Gets the depth.
     *
     * @param exceptionMapping the exception mapping
     * @param exceptionClass the exception class
     * @param depth the depth
     * @return the depth
     */
    private int getDepth(final String exceptionMapping, final Class<?> exceptionClass, final int depth) {
        if (exceptionClass.getName().contains(exceptionMapping)) {
            // Found it!
            return depth;
        }
        // If we've gone as far as we can go and haven't found it...
        if (exceptionClass.equals(Throwable.class)) {
            return -1;
        }
        return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
    }

    /**
     * Apply the specified HTTP status code to the given response, if possible (that is,
     * if not executing within an include request).
     * @param request current HTTP request
     * @param response current HTTP response
     * @param statusCode the status code to apply
     * @see #determineStatusCode
     * @see #setDefaultStatusCode
     * @see HttpServletResponse#setStatus
     */
    protected void applyErrorCodeIfPossible(final HttpServletRequest request, final HttpServletResponse response,
            final int statusCode) {
        if (!WebUtils.isIncludeRequest(request)) {
            response.setStatus(statusCode);
            request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, statusCode);
        }
    }

    /**
     * Gets the exception details and complete stack trace.
     *
     * @param e the e
     * @return the exception details and complete stack trace
     */
    public static String getExceptionDetailsAndCompleteStackTrace(final Throwable e) {
        StringBuilder detailedMessage = new StringBuilder();
        if (e.getLocalizedMessage() != null) {
            detailedMessage.append(e.getLocalizedMessage());
        }

        if (detailedMessage.length() > 0) {
            detailedMessage.append("\n");
        }

        detailedMessage.append(e.getClass().getName());

        /** Save: Commons lang does not support generics in this old version. */
        @SuppressWarnings("unchecked")
        List<Throwable> throwables = ExceptionUtils.getThrowableList(e);
        for (int i = 1; i < throwables.size(); i++) {
            detailedMessage.append("\n cause: ");
            detailedMessage.append(throwables.get(i).getClass().getName());
            if (StringUtils.isNotBlank(throwables.get(i).getLocalizedMessage())) {
                detailedMessage.append(" -- " + throwables.get(i).getLocalizedMessage());
            }
            detailedMessage.append(";");
        }

        detailedMessage.append("\n\n --full Stacktrace--\n");
        detailedMessage.append(ExceptionUtils.getFullStackTrace(e));

        return detailedMessage.toString();
    }

}

【讨论】:

  • 这正是我多年前为这个问题所做的。在自定义异常解析器时,您可能需要查看 MediaTypeException 问题:jira.spring.io/browse/…
  • 嗨!谢谢你的回复。这让我有点害怕,认为它应该在教程中如此简单。谢谢你们,我会尝试:/
  • 哼,我的问题搞错了。我实际上没有任何媒体类型问题。我自己构建了异常,因为我想要这种错误类型。 Tomcat 没有生成它。我的问题只是实际上检索错误消息。在这种情况下你的答案可以吗?我阅读了您的代码,但目前还没有完全理解。再次感谢
  • 不要担心我对媒体类型的评论,因为它更适合@Ralph。首先按照 Ralph 的建议去做。
  • 好的@AdamGent,但拉尔夫的建议与教程相去甚远,似乎有些矫枉过正。无论如何我今天都在努力:)
【解决方案2】:

您的计算机上可能安装了 worlderror.org 恶意软件,该恶意软件正在拦截 400-600 状态代码响应。 您甚至没有看到 spring 错误,因为恶意软件正在拦截它。

尝试运行其他浏览器或运行间谍软件删除软件。

【讨论】:

  • 谢谢,我有这个恶意软件:)。它没有让我的旧代码返回一条消息,但至少我可以检索一个正常的 tomcat 异常页面!
【解决方案3】:

我有以下并且工作正常:

注意:做事简单。

在扩展 WebMvcConfigurerAdapter 的类中添加以下内容:

@Bean
public StringHttpMessageConverter stringHttpMessageConverter(){
    StringHttpMessageConverter converter = new StringHttpMessageConverter();
    converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
    return converter;
}

@Bean
public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver(){
    ExceptionHandlerExceptionResolver eher = new ExceptionHandlerExceptionResolver();
    eher.setMessageConverters(Arrays.asList(stringHttpMessageConverter()));
    return eher;
}

@Bean
public ResponseStatusExceptionResolver responseStatusExceptionResolver(){
    return new ResponseStatusExceptionResolver();
}


@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers){
        exceptionResolvers.add(exceptionHandlerExceptionResolver());
        exceptionResolvers.add(responseStatusExceptionResolver());
}

因此

@ExceptionHandler(value=MyException.class)
public ResponseEntity<String> errorHandlerHttpHeaderRestrict(MyException me){
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        return new ResponseEntity<>("ERROR FATAL: "+me.getMessage(), httpHeaders, HttpStatus.BAD_REQUEST);//400
}

我建议你使用ResponseEntity 而不是 @ResponseStatus(value = HttpStatus.BAD_REQUEST)@ResponseBody

因为:

  1. ResponseEntity 更灵活
  2. 对于@ResponseStatus,您必须完全确定该方法只能返回那个HttpStatus(只有一个)

【讨论】:

  • 我会试试的,谢谢,我没有看到任何扩展“WebMvcConfigurerAdapter”的类,我正在搜索:)
  • 我只使用 JavaConfig,因此我不使用 xmlweb.xml
  • 是的,这就是我所理解的,作为我正在与 xmlweb.xml 合作的项目,我首先尝试 Ralph 回答。
  • 我的配置共享也可以用 XML 表示。
【解决方案4】:

这是我解决这个问题的方法:

我将 Spring 版本从 3.2.2 更新为 4.0.3(更新的版本也应该可以)。

然后,@ControllerAdvice 接受 basePackage 参数:

@ControllerAdvice(basePackages = "me.myself.hi")
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler
{
    /** The associated logger. */
    private static final Logger LOGGER = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class);

    @ExceptionHandler(value = { HttpRestException.class })
    protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request)
    {
        if (ex instanceof HttpRestException)
        {
            final HttpRestException restEx = (HttpRestException) ex;
            return handleExceptionInternal(ex, restEx.getMessage(), new HttpHeaders(), restEx.getHttpStatus(), request);
        }

        return handleExceptionInternal(ex, "Internal server error",
                new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
    }
}

basePackage 指定分配给处理程序类的包。这意味着这个包(或子包)中定义的每个控制器都会受到关注。

对我来说,me.myself.hi 是至少 10 个项目共享的基本包名称。所以即使一个控制器在另一个项目中,只要它在一个以me.myself.hi开头的包下,HttpRestException就会在那里被捕获。

记住异常一定不能在Controller级别处理,只能抛出,@ControllerAdvice注解的控制器会处理这个异常并返回状态码+错误信息。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-12-14
    • 1970-01-01
    • 2020-02-07
    • 1970-01-01
    • 1970-01-01
    • 2017-11-01
    • 2020-05-26
    相关资源
    最近更新 更多