Response.sendError()和setStatus()方法的区别

3,387 阅读5分钟

1.干啥的

首先我们知道setStatus()方法可以设置请求的状态值,对就是跟在返回头信息第一行HTTP版本后边的那个数字,如下图 setError()方法也有这个功能,从它的重载方法来看它来可以带个错误描述的参数

他们都可以改变返回体的状态值

2.不同处

先说结果setError()的状态值可以在web.xml中配置的<error-page></error-page>再次处理一遍,比如返回特定的友好的错误提示页面或者是返回具体的错误返回内容都是可以的,我觉得这个主要是对返回的body内容进行填充,当然在此也是可以改变状态码的,下边会说到。 <error-page>标签如下

<error-page>
		<error-code>401</error-code>
		<location>/noPermissionError.htm</location>
</error-page>

这里的意思是如果状态码是401的话,会再次请求/noPermissionError.htm这个请求,在此期间可以对返回内容再次调整。除了状态码外,还可以对具体的异常类型进行处理,这里就不解释了

3.为啥setError()可以有特权

从代码层次来debug一下 首先我们来到setError()方法处

这是一个权限拦截器中的一个代码,如果没有权限话的进行提示并终止调用,项目中用到了sitemesh进行返回页面的封装,所以Response是被包装过的,但是发现方法进去之后好多都不能打断点,观察Variables窗口的httpServletResponse变量可以发现最终调用的Response对象为 org.apache.catalina.connector.ResponseFacade类,这个类是tomcat-catalina这个包里的

进入方法

首先判断有没有提交,提交了直接报错 然后调用了org.apache.catalina.connector.Response类中的sendError()方法 注意这个setError()这个方法 使用cas方法进行对一个状态值errorState设置为1 ,正是这状态值让后续的异常处理决定是否再次调用<error-page>的配置进行处理,再看下setStatus()方法一目了然

4.再进一步

<error-page>标签如何被解析

进入catalina包的WebXml类的configureContext()我们猜测(为啥子是猜测,因为我就是猜的哈)这个就是tomcat解析web.xml的类,然后可以进入StandardContext类的addErrorPage()方法,如下

@Override
    public void addErrorPage(ErrorPage errorPage) {
        // Validate the input parameters
        if (errorPage == null)
            throw new IllegalArgumentException
                (sm.getString("standardContext.errorPage.required"));
        String location = errorPage.getLocation();
        if ((location != null) && !location.startsWith("/")) {
            if (isServlet22()) {
                if(log.isDebugEnabled())
                    log.debug(sm.getString("standardContext.errorPage.warning",
                                 location));
                errorPage.setLocation("/" + location);
            } else {
                throw new IllegalArgumentException
                    (sm.getString("standardContext.errorPage.error",
                                  location));
            }
        }

        // Add the specified error page to our internal collections
        String exceptionType = errorPage.getExceptionType();
        if (exceptionType != null) {
            synchronized (exceptionPages) {
                exceptionPages.put(exceptionType, errorPage);
            }
        } else {
            synchronized (statusPages) {
                if (errorPage.getErrorCode() == 200) {
                    this.okErrorPage = errorPage;
                }
                statusPages.put(Integer.valueOf(errorPage.getErrorCode()),
                                errorPage);
            }
        }
        fireContainerEvent("addErrorPage", errorPage);

    }

可以清楚的看到根据异常和状态值的异常处理的整理,再猜一下,搜一下对这个statusPages的调用

就它了也是在StandardContext

@Override
    public ErrorPage findErrorPage(int errorCode) {
        if (errorCode == 200) {
            return (okErrorPage);
        } else {
            return (statusPages.get(Integer.valueOf(errorCode)));
        }

    }

查下这个方法的调用 进入StandardHostValvestatus(Request request, Response response)方法 这个方法上的注释已经说的很清楚了哈,只有在isError()属性为真才会执行下边的代码,啥时候为真呢,就是调用了sendError()方法 依次进去看看 再次看到了熟悉的errorState属性。

在下边的代码中我们还可以看到将statusCodemessage放入了request中,名字是啥

public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
public static final String ERROR_MESSAGE = "javax.servlet.error.message";

所以在之后的异常处理时如果想获得sendError()时的参数,可以get这俩个属性得到

request.getAttribute("javax.servlet.error.status_code")
request.getAttribute("javax.servlet.error.message")

找到对应的异常处理配置之后,进入custom()方法

使用RequestDispatcher对象forward到配置的<location>继续对结果进行包装,这个<location>最终可以是个页面跳转,也可以是json字符串 如下

@RequestMapping("error.htm")
    public String error() {
        return "error.ftl";
    }
@RequestMapping(value = "noPermissionError.htm", produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    public Map<String, Object> noPermissionError(HttpServletRequest request) {

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));
        return map;
    }

这边其实也可以改变状态码,可以使用@ResponseStatus注解,比如

 @RequestMapping(value = "noPermissionError.htm", produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public Map<String, Object> noPermissionError(HttpServletRequest request) {

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));
        return map;
    }

这个状态码最后就是200,可以添加自己的业务错误码进行业务判断

附录

想在项目中跟踪到tomcat的类时需要在项目中添加依赖

<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-catalina</artifactId>
			<version>7.0.86</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-coyote</artifactId>
			<version>7.0.86</version>
			<scope>provided</scope>
		</dependency>

作用域一定要是provided,否则启动时会冲突,但是调试的时候发现还是有一些类不知道需要引入哪些包会缺失,大家可以到apache官网下载tomcat的源代码看,更方便

下载地址 tomcat.apache.org/download-60…

PS:我们知道response.sendError()后容器会将请求再次转发给spring项目。在springboot项目中,我们没有xml格式配置<error-page>标签,那么是怎么处理的。 springboot中有一个默认过滤器ErrorPageFilter ,会forward到配置的error展示地址 核心代码如下

private void doFilter(HttpServletRequest request, HttpServletResponse response,  
FilterChain chain) throws IOException, ServletException {  
    ErrorWrapperResponse wrapped = new ErrorWrapperResponse(response);  
    try {  
        chain.doFilter(request, wrapped);  
        // 如果有response.sendError()的话会走到这里
    if (wrapped.hasErrorToSend()) {  
        handleErrorStatus(request, response, wrapped.getStatus(),  
    wrapped.getMessage());  
    response.flushBuffer();  
    }  
    else if (!request.isAsyncStarted() && !response.isCommitted()) {  
        response.flushBuffer();  
    }  
    }  
    catch (Throwable ex) {  
    Throwable exceptionToHandle = ex;  
    if (ex instanceof NestedServletException) {  
        exceptionToHandle = ((NestedServletException) ex).getRootCause();  
    }  
        handleException(request, response, wrapped, exceptionToHandle);  
    response.flushBuffer();  
}  
}  
  
private void handleErrorStatus(HttpServletRequest request,  
    HttpServletResponse response, int status, String message)  
    throws ServletException, IOException {  
    if (response.isCommitted()) {  
        handleCommittedResponse(request, null);  
    return;  
    }  
    // 有个global兜底也是ErrorProperties的path字段默认是`/error` 可以和
    //BasicErrorController对上
    String errorPath = getErrorPath(this.statuses, status);  
    if (errorPath == null) {  
        response.sendError(status, message);  
    return;  
    }  
    response.setStatus(status);  
    setErrorAttributes(request, status, message);  
    // 转发请求到`/error`
    request.getRequestDispatcher(errorPath).forward(request, response);  
}  
  
private void handleException(HttpServletRequest request, HttpServletResponse response,  
    ErrorWrapperResponse wrapped, Throwable ex)  
    throws IOException, ServletException {  
    Class<?> type = ex.getClass();  
    String errorPath = getErrorPath(type);  
    if (errorPath == null) {  
        rethrow(ex);  
        return;  
    }  
    if (response.isCommitted()) {  
        handleCommittedResponse(request, ex);  
    return;  
    }  
  
    forwardToErrorPage(errorPath, request, wrapped, ex);  
}  
  
private void forwardToErrorPage(String path, HttpServletRequest request,  
                            HttpServletResponse response, Throwable ex)  
                            throws ServletException, IOException {  
    if (logger.isErrorEnabled()) {  
        String message = "Forwarding to error page from request "  
        + getDescription(request) + " due to exception [" + ex.getMessage()  
        + "]";  
        logger.error(message, ex);  
    }  
    setErrorAttributes(request, 500, ex.getMessage());  
    request.setAttribute(ERROR_EXCEPTION, ex);  
    request.setAttribute(ERROR_EXCEPTION_TYPE, ex.getClass().getName());  
    response.reset();  
    response.sendError(500, ex.getMessage());  
    request.getRequestDispatcher(path).forward(request, response);  
}

并且提供了一个默认的类BasicErrorController来处理异常的展示信息

@Controller  
@RequestMapping("${server.error.path:${error.path:/error}}")  
public class BasicErrorController extends AbstractErrorController {  
  
private final ErrorProperties errorProperties;  
  
@Override  
public String getErrorPath() {  
return this.errorProperties.getPath();  
}  
  
@RequestMapping(produces = "text/html")  
public ModelAndView errorHtml(HttpServletRequest request,  
HttpServletResponse response) {  
response.setStatus(getStatus(request).value());  
Map<String, Object> model = getErrorAttributes(request,  
isIncludeStackTrace(request, MediaType.TEXT_HTML));  
return new ModelAndView("error", model);  
}  
  
@RequestMapping  
@ResponseBody  
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {  
Map<String, Object> body = getErrorAttributes(request,  
isIncludeStackTrace(request, MediaType.ALL));  
HttpStatus status = getStatus(request);  
return new ResponseEntity<Map<String, Object>>(body, status);  
}  
  
  
}

可以发现默认的请求为/error,可以用参数配置,返回的视图为error,具体的取值可以去getErrorAttributes 方法看下