整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

spring mvc DispatcherServl

spring mvc DispatcherServlet详解之三获取ModelAndView过程

个spring mvc的架构如下图所示:

上篇文件讲解了DispatcherServlet通过request获取控制器Controller的过程,现在来讲解DispatcherServletDispatcherServlet的第二步:通过request从Controller获取ModelAndView。

DispatcherServlet调用Controller的过程:

DispatcherServlet.java

doService()--->doDispatch()--->handlerAdapter的handle()方法

 try {// Actually invoke the handler.
 mv=ha.handle(processedRequest, response, mappedHandler.getHandler());
 }
 finally {
 if (asyncManager.isConcurrentHandlingStarted()) {
 return;
 }
 }

最常用的实现了HandlerAdapter接口是SimpleControllerHandlerAdapter类,该类将

两个不兼容的类:DispatcherServlet 和Controller 类连接到一起。

 Adapter to use the plain {@link Controller} workflow interface with
 the generic {@link org.springframework.web.servlet.DispatcherServlet}.
 Supports handlers that implement the {@link LastModified} interface.
 
 <p>This is an SPI class, not used directly by application code.

类之间的转换代码如下所示,调用了Controller类的handleRequest()方法来处理请求:

 @Override
 public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
 throws Exception {
 return ((Controller) handler).handleRequest(request, response);
 }

重量级人物控制器Controller开始闪亮登场,Controller是一个基本的接口,它接受request和response,从这点上来说,它有点像servlet,但不同之处在于它在mvc模式流程中起作用,它和struts中的Action作用类似。继承该接口的控制器或者类应该保证是线程安全的,可复用的,能够在一个应用生命周期中处理大量的request。为了使Controller的配置更便捷,通常使用javaBeans来继承Controller。

/**
 * Base Controller interface, representing a component that receives
 * {@code HttpServletRequest} and {@code HttpServletResponse}
 * instances just like a {@code HttpServlet} but is able to
 * participate in an MVC workflow. Controllers are comparable to the
 * notion of a Struts {@code Action}.
 *
 * <p>Any implementation of the Controller interface should be a
 * <i>reusable, thread-safe</i> class, capable of handling multiple
 * HTTP requests throughout the lifecycle of an application. To be able to
 * configure a Controller easily, Controller implementations are encouraged
 * to be (and usually are) JavaBeans.
 * </p>
 *
 * <p><b><a name="workflow">Workflow</a></b></p>
 *
 * <p>
 * After a <cde>DispatcherServlet</code> has received a request and has
 * done its work to resolve locales, themes and suchlike, it then tries
 * to resolve a Controller, using a
 * {@link org.springframework.web.servlet.HandlerMapping HandlerMapping}.
 * When a Controller has been found to handle the request, the
 * {@link #handleRequest(HttpServletRequest, HttpServletResponse) handleRequest}
 * method of the located Controller will be invoked; the located Controller
 * is then responsible for handling the actual request and - if applicable -
 * returning an appropriate
 * {@link org.springframework.web.servlet.ModelAndView ModelAndView}.
 * So actually, this method is the main entrypoint for the
 * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
 * which delegates requests to controllers.</p>
 *
 * <p>So basically any <i>direct</i> implementation of the Controller interface
 * just handles HttpServletRequests and should return a ModelAndView, to be further
 * interpreted by the DispatcherServlet. Any additional functionality such as
 * optional validation, form handling, etc should be obtained through extending
 * one of the abstract controller classes mentioned above.</p>
 *
 * <p><b>Notes on design and testing</b></p>
 *
 * <p>The Controller interface is explicitly designed to operate on HttpServletRequest
 * and HttpServletResponse objects, just like an HttpServlet. It does not aim to
 * decouple itself from the Servlet API, in contrast to, for example, WebWork, JSF or Tapestry.
 * Instead, the full power of the Servlet API is available, allowing Controllers to be
 * general-purpose: a Controller is able to not only handle web user interface
 * requests but also to process remoting protocols or to generate reports on demand.</p>
 *
 * <p>Controllers can easily be tested by passing in mock objects for the
 * HttpServletRequest and HttpServletResponse objects as parameters to the
 * {@link #handleRequest(HttpServletRequest, HttpServletResponse) handleRequest}
 * method. As a convenience, Spring ships with a set of Servlet API mocks
 * that are suitable for testing any kind of web components, but are particularly
 * suitable for testing Spring web controllers. In contrast to a Struts Action,
 * there is no need to mock the ActionServlet or any other infrastructure;
 * HttpServletRequest and HttpServletResponse are sufficient.</p>
 *
 * <p>If Controllers need to be aware of specific environment references, they can
 * choose to implement specific awareness interfaces, just like any other bean in a
 * Spring (web) application context can do, for example:</p>
 * <ul>
 * <li>{@code org.springframework.context.ApplicationContextAware}</li>
 * <li>{@code org.springframework.context.ResourceLoaderAware}</li>
 * <li>{@code org.springframework.web.context.ServletContextAware}</li>
 * </ul>
 *
 * <p>Such environment references can easily be passed in testing environments,
 * through the corresponding setters defined in the respective awareness interfaces.
 * In general, it is recommended to keep the dependencies as minimal as possible:
 * for example, if all you need is resource loading, implement ResourceLoaderAware only.
 * Alternatively, derive from the WebApplicationObjectSupport base class, which gives
 * you all those references through convenient accessors - but requires an
 * ApplicationContext reference on initialization.
 *
 * <p>Controllers can optionally implement the {@link LastModified} interface.
*/

Controller的handleRequest()方法处理请求,并返回ModelAndView给DispatcherServlet去渲染render。

Controller接口的抽象实现类为:AbstractController,它通过互斥锁(mutex)来保证线程安全。

 /**
 * Set if controller execution should be synchronized on the session,
 * to serialize parallel invocations from the same client.
 * <p>More specifically, the execution of the {@code handleRequestInternal}
 * method will get synchronized if this flag is "true". The best available
 * session mutex will be used for the synchronization; ideally, this will
 * be a mutex exposed by HttpSessionMutexListener.
 * <p>The session mutex is guaranteed to be the same object during
 * the entire lifetime of the session, available under the key defined
 * by the {@code SESSION_MUTEX_ATTRIBUTE} constant. It serves as a
 * safe reference to synchronize on for locking on the current session.
 * <p>In many cases, the HttpSession reference itself is a safe mutex
 * as well, since it will always be the same object reference for the
 * same active logical session. However, this is not guaranteed across
 * different servlet containers; the only 100% safe way is a session mutex.
 * @see AbstractController#handleRequestInternal
 * @see org.springframework.web.util.HttpSessionMutexListener
 * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession)
 */

线程安全实现:

 public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
 throws Exception {
 // Delegate to WebContentGenerator for checking and preparing.
 checkAndPrepare(request, response, this instanceof LastModified);
 // Execute handleRequestInternal in synchronized block if required.
 if (this.synchronizeOnSession) {
 HttpSession session=request.getSession(false);
 if (session !=null) {
 Object mutex=WebUtils.getSessionMutex(session);
 synchronized (mutex) {
 return handleRequestInternal(request, response);
 }
 }
 }
 return handleRequestInternal(request, response);
 }

handleRequestInternal()为抽象方法,留待具体实现类来实现。它的直接子类有:

AbstractUrlViewController, MultiActionController, ParameterizableViewController, ServletForwardingController, ServletWrappingController

简单Controller实现

在web.xml中有时候定义节点<welcome-list>index.html</welcome-list>等,这种简单的请,Controller是如何实现的呢?我们来看看UrlFilenameViewController,它是Controller的一个间接实现,实现了AbstractUrlViewController。它把url的虚拟路径转换成一个view的名字,然后返回这个view。

 protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
 String lookupPath=getUrlPathHelper().getLookupPathForRequest(request);
 String viewName=getViewNameForRequest(request);
 if (logger.isDebugEnabled()) {
 logger.debug("Returning view name '" + viewName + "' for lookup path [" + lookupPath + "]");
 }
 return new ModelAndView(viewName, RequestContextUtils.getInputFlashMap(request));
 }

复杂Controller实现

一个可以处理多种请求类型的Controller实现:MultiActionController。它类似于struts中的DispatcherAction,但更灵活,而且支持代理。

/**
 * {@link org.springframework.web.servlet.mvc.Controller Controller}
 * implementation that allows multiple request types to be handled by the same
 * class. Subclasses of this class can handle several different types of
 * request with methods of the form
 *
 * <pre class="code">public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response, [,HttpSession] [,AnyObject]);</pre>
 *
 * A Map return value indicates a model that is supposed to be passed to a default view
 * (determined through a {@link org.springframework.web.servlet.RequestToViewNameTranslator}).
 * A String return value indicates the name of a view to be rendered without a specific model.
 *
 * <p>May take a third parameter (of type {@link HttpSession}) in which an
 * existing session will be required, or a third parameter of an arbitrary
 * class that gets treated as the command (that is, an instance of the class
 * gets created, and request parameters get bound to it)
 *
 * <p>These methods can throw any kind of exception, but should only let
 * propagate those that they consider fatal, or which their class or superclass
 * is prepared to catch by implementing an exception handler.
 *
 * <p>When returning just a {@link Map} instance view name translation will be
 * used to generate the view name. The configured
 * {@link org.springframework.web.servlet.RequestToViewNameTranslator} will be
 * used to determine the view name.
 *
 * <p>When returning {@code void} a return value of {@code null} is
 * assumed meaning that the handler method is responsible for writing the
 * response directly to the supplied {@link HttpServletResponse}.
 *
 * <p>This model allows for rapid coding, but loses the advantage of
 * compile-time checking. It is similar to a Struts {@code DispatchAction},
 * but more sophisticated. Also supports delegation to another object.
 *
 * <p>An implementation of the {@link MethodNameResolver} interface defined in
 * this package should return a method name for a given request, based on any
 * aspect of the request, such as its URL or an "action" parameter. The actual
 * strategy can be configured via the "methodNameResolver" bean property, for
 * each {@code MultiActionController}.
 *
 * <p>The default {@code MethodNameResolver} is
 * {@link InternalPathMethodNameResolver}; further included strategies are
 * {@link PropertiesMethodNameResolver} and {@link ParameterMethodNameResolver}.
 *
 * <p>Subclasses can implement custom exception handler methods with names such
 * as:
 *
 * <pre class="code">public ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception);</pre>
 *
 * The third parameter can be any subclass or {@link Exception} or
 * {@link RuntimeException}.
 *
 * <p>There can also be an optional {@code xxxLastModified} method for
 * handlers, of signature:
 *
 * <pre class="code">public long anyMeaningfulNameLastModified(HttpServletRequest request)</pre>
 *
 * If such a method is present, it will be invoked. Default return from
 * {@code getLastModified} is -1, meaning that the content must always be
 * regenerated.
 *
 * <p><b>Note that all handler methods need to be public and that
 * method overloading is <i>not</i> allowed.</b>
 *
 * <p>See also the description of the workflow performed by
 * {@link AbstractController the superclass} (in that section of the class
 * level Javadoc entitled 'workflow').
 *
 * <p><b>Note:</b> For maximum data binding flexibility, consider direct usage of a
 * {@link ServletRequestDataBinder} in your controller method, instead of relying
 * on a declared command argument. This allows for full control over the entire
 * binder setup and usage, including the invocation of {@link Validator Validators}
 * and the subsequent evaluation of binding/validation errors.*/

根据方法名决定处理的handler

 protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
 throws Exception {
 try {
 String methodName=this.methodNameResolver.getHandlerMethodName(request);
 return invokeNamedMethod(methodName, request, response);
 }
 catch (NoSuchRequestHandlingMethodException ex) {
 return handleNoSuchRequestHandlingMethod(ex, request, response);
 }
 }

触发执行方法:

protected final ModelAndView invokeNamedMethod(
 String methodName, HttpServletRequest request, HttpServletResponse response) throws Exception {
 Method method=this.handlerMethodMap.get(methodName);
 if (method==null) {
 throw new NoSuchRequestHandlingMethodException(methodName, getClass());
 }
 try {
 Class<?>[] paramTypes=method.getParameterTypes();
 List<Object> params=new ArrayList<Object>(4);
 params.add(request);
 params.add(response);
 if (paramTypes.length >=3 && paramTypes[2].equals(HttpSession.class)) {
 HttpSession session=request.getSession(false);
 if (session==null) {
 throw new HttpSessionRequiredException(
 "Pre-existing session required for handler method '" + methodName + "'");
 }
 params.add(session);
 }
 // If last parameter isn't of HttpSession type, it's a command.
 if (paramTypes.length >=3 &&
 !paramTypes[paramTypes.length - 1].equals(HttpSession.class)) {
 Object command=newCommandObject(paramTypes[paramTypes.length - 1]);
 params.add(command);
 bind(request, command);
 }
 Object returnValue=method.invoke(this.delegate, params.toArray(new Object[params.size()]));
 return massageReturnValueIfNecessary(returnValue);
 }
 catch (InvocationTargetException ex) {
 // The handler method threw an exception.
 return handleException(request, response, ex.getTargetException());
 }
 catch (Exception ex) {
 // The binding process threw an exception.
 return handleException(request, response, ex);
 }

处理返回结果,要么返回null要么返回ModelAndView实例。当返回一个Map类型时,ModelAndView实例包装的Map类型。

/**
 * Processes the return value of a handler method to ensure that it either returns
 * {@code null} or an instance of {@link ModelAndView}. When returning a {@link Map},
 * the {@link Map} instance is wrapped in a new {@link ModelAndView} instance.
 */
 @SuppressWarnings("unchecked")
 private ModelAndView massageReturnValueIfNecessary(Object returnValue) {
 if (returnValue instanceof ModelAndView) {
 return (ModelAndView) returnValue;
 }
 else if (returnValue instanceof Map) {
 return new ModelAndView().addAllObjects((Map<String, ?>) returnValue);
 }
 else if (returnValue instanceof String) {
 return new ModelAndView((String) returnValue);
 }
 else {
 // Either returned null or was 'void' return.
 // We'll assume that the handle method already wrote the response.
 return null;
 }
 }

小结:

DispatcherServlet接受一个请求,然后解析完locales, themes等后,通过HadlerMapping解析控制器Controller去处理请求。

找到Controller后,出发当前controller的handleRequest()方法,此controller负责真正处理请求,然后一个ModelAndView实例。

DispatcherServlet 代理此Controller,接收返回结果,然后进行渲染。

近在项目中在做一个消息推送的功能,比如客户下单之后通知给给对应的客户发送系统通知,这种消息推送需要使用到全双工的websocket推送消息。

所谓的全双工表示客户端和服务端都能向对方发送消息。不使用同样是全双工的http是因为http只能由客户端主动发起请求,服务接收后返回消息。websocket建立起连接之后,客户端和服务端都能主动向对方发送消息。

上一篇文章Spring Boot 整合单机websocket介绍了websocket在单机模式下进行消息的发送和接收:

用户A用户Bweb服务器建立连接之后,用户A发送一条消息到服务器,服务器再推送给用户B,在单机系统上所有的用户都和同一个服务器建立连接,所有的session都存储在同一个服务器中。

单个服务器是无法支撑几万人同时连接同一个服务器,需要使用到分布式或者集群将请求连接负载均衡到到不同的服务下。消息的发送方和接收方在同一个服务器,这就和单体服务器类似,能成功接收到消息:

但负载均衡使用轮询的算法,无法保证消息发送方和接收方处于同一个服务器,当发送方和接收方不是在同一个服务器时,接收方是无法接受到消息的:

websocket集群问题解决思路

客户端和服务端每次建立连接时候,会创建有状态的会话session,服务器的保存维持连接的session。客户端每次只能和集群服务器其中的一个服务器连接,后续也是和该服务器进行数据传输。

要解决集群的问题,应该考虑session共享的问题,客户端成功连接服务器之后,其他服务器也知道客户端连接成功。

方案一:session 共享(不可行)

websocket类似的http是如何解决集群问题的?解决方案之一就是共享session,客户端登录服务端之后,将session信息存储在Redis数据库中,连接其他服务器时,从Redis获取session,实际就是将session信息存储在Redis中,实现redis的共享。

session可以被共享的前提是可以被序列化,而websocketsession是无法被序列化的,httpsession记录的是请求的数据,而websocketsession对应的是连接,连接到不同的服务器,session也不同,无法被序列化。

方案二:ip hash(不可行)

http不使用session共享,就可以使用Nginx负载均衡的ip hash算法,客户端每次都是请求同一个服务器,客户端的session都保存在服务器上,而后续请求都是请求该服务器,都能获取到session,就不存在分布式session问题了。

websocket相对http来说,可以由服务端主动推动消息给客户端,如果接收消息的服务端和发送消息消息的服务端不是同一个服务端,发送消息的服务端无法找到接收消息对应的session,即两个session不处于同一个服务端,也就无法推送消息。如下图所示:

解决问题的方法是将所有消息的发送方和接收方都处于同一个服务器下,而消息发送方和接收方都是不确定的,显然是无法实现的。

方案三:广播模式

将消息的发送方和接收方都处于同一个服务器下才能发送消息,那么可以转换一下思路,可以将消息以消息广播的方式通知给所有的服务器,可以使用消息中间件发布订阅模式,消息脱离了服务器的限制,通过发送到中间件,再发送给订阅的服务器,类似广播一样,只要订阅了消息,都能接收到消息的通知:

发布者发布消息到消息中间件,消息中间件再将发送给所有订阅者:

广播模式的实现

搭建单机 websocket

参考以前写的websocket单机搭建 文章,先搭建单机websocket实现消息的推送。

1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2. 创建 ServerEndpointExporter 的 bean 实例

ServerEndpointExporter 的 bean 实例自动注册 @ServerEndpoint 注解声明的 websocket endpoint,使用springboot自带tomcat启动需要该配置,使用独立 tomcat 则不需要该配置。

@Configuration
public class WebSocketConfig {
    //tomcat启动无需该配置
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3. 创建服务端点 ServerEndpoint 和 客户端端

  • 服务端点
@Component
@ServerEndpoint(value="/message")
@Slf4j
public class WebSocket {

 private static Map<String, WebSocket> webSocketSet=new ConcurrentHashMap<>();

 private Session session;

 @OnOpen
 public void onOpen(Session session) throws SocketException {
  this.session=session;
  webSocketSet.put(this.session.getId(),this);

  log.info("【websocket】有新的连接,总数:{}",webSocketSet.size());
 }

 @OnClose
 public void onClose(){
  String id=this.session.getId();
  if (id !=null){
   webSocketSet.remove(id);
   log.info("【websocket】连接断开:总数:{}",webSocketSet.size());
  }
 }

 @OnMessage
 public void onMessage(String message){
  if (!message.equals("ping")){
   log.info("【wesocket】收到客户端发送的消息,message={}",message);
   sendMessage(message);
  }
 }

 /**
  * 发送消息
  * @param message
  * @return
  */
 public void sendMessage(String message){
  for (WebSocket webSocket : webSocketSet.values()) {
   webSocket.session.getAsyncRemote().sendText(message);
  }
  log.info("【wesocket】发送消息,message={}", message);

 }

}
  • 客户端点
<div>
    <input type="text" name="message" id="message">
    <button id="sendBtn">发送</button>
</div>
<div style="width:100px;height: 500px;" id="content">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script type="text/javascript">
    var ws=new WebSocket("ws://127.0.0.1:8080/message");
    ws.onopen=function(evt) {
        console.log("Connection open ...");
    };

    ws.onmessage=function(evt) {
        console.log( "Received Message: " + evt.data);
        var p=$("<p>"+evt.data+"</p>")
        $("#content").prepend(p);
        $("#message").val("");
    };

    ws.onclose=function(evt) {
        console.log("Connection closed.");
    };

    $("#sendBtn").click(function(){
        var aa=$("#message").val();
        ws.send(aa);
    })

</script>

服务端和客户端中的OnOpenoncloseonmessage都是一一对应的。

  • 服务启动后,客户端ws.onopen调用服务端的@OnOpen注解的方法,储存客户端的session信息,握手建立连接。
  • 客户端调用ws.send发送消息,对应服务端的@OnMessage注解下面的方法接收消息。
  • 服务端调用session.getAsyncRemote().sendText发送消息,对应的客户端ws.onmessage接收消息。

添加 controller

@GetMapping({"","index.html"})
public ModelAndView index() {
 ModelAndView view=new ModelAndView("index");
 return view;
}

效果展示

打开两个客户端,其中的一个客户端发送消息,另一个客户端也能接收到消息。

添加 RabbitMQ 中间件

这里使用比较常用的RabbitMQ作为消息中间件,而RabbitMQ支持发布订阅模式

添加消息订阅

交换机使用扇形交换机,消息分发给每一条绑定该交换机的队列。以服务器所在的IP + 端口作为唯一标识作为队列的命名,启动一个服务,使用队列绑定交换机,实现消息的订阅:

@Configuration
public class RabbitConfig {

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("PUBLISH_SUBSCRIBE_EXCHANGE");
    }

    @Bean
    public Queue psQueue() throws SocketException {
        // ip + 端口 为队列名 
        String ip=IpUtils.getServerIp() + "_" + IpUtils.getPort();
        return new Queue("ps_" + ip);
    }

    @Bean
    public Binding routingFirstBinding() throws SocketException {
        return BindingBuilder.bind(psQueue()).to(fanoutExchange());
    }
}

获取服务器IP和端口可以具体查看Github源码,这里就不做详细描述了。

修改服务端点 ServerEndpoint

WebSocket添加消息的接收方法,@RabbitListener 接收消息,队列名称使用常量命名,动态队列名称使用 #{name},其中的nameQueuebean 名称:

@RabbitListener(queues="#{psQueue.name}")
public void pubsubQueueFirst(String message) {
  System.out.println(message);
  sendMessage(message);
}

然后再调用sendMessage方法发送给所在连接的客户端。

修改消息发送

WebSocket类的onMessage方法将消息发送改成RabbitMQ方式发送:

@OnMessage
public void onMessage(String message){
  if (!message.equals("ping")){
    log.info("【wesocket】收到客户端发送的消息,message={}",message);
    //sendMessage(message);
    if (rabbitTemplate==null) {
      rabbitTemplate=(RabbitTemplate) SpringContextUtil.getBean("rabbitTemplate");
    }
    rabbitTemplate.convertAndSend("PUBLISH_SUBSCRIBE_EXCHANGE", null, message);
  }
}

消息通知流程如下所示:

启动两个实例,模拟集群环境

打开idea的Edit Configurations

点击左上角的COPY,然后添加端口server.port=8081

启动两个服务,端口分别是80808081。在启动8081端口的服务,将前端连接端口改成8081:

var ws=new WebSocket("ws://127.0.0.1:8081/message");

效果展示

源码

github源码

参考

  • Spring Websocket in a tomcat cluster
  • WebSocket 集群方案

. SpringBoot的默认错误处理策略

1. 对404的默认处理策略

我们在发送请求的时候,如果发生了404异常,SpringBoot是怎么处理的呢?

我们可以随便发送一个不存在的请求来验证一下,就会看到如下图所示:

2. 对500的默认处理策略

当服务器内部发生代码等错误的时候,会发生什么呢?

比如我们人为的制造一个异常出来,如下面的代码所示:

@GetMapping("/user/{id:\\d+}")
public User get(@PathVariable String id) {
    throw new RuntimeException();
}

结果产生了如下所示效果图:

3.在error页面中可以获取的错误信息

timestamp: 时间戳.
status: 状态码.
error: 错误提示.
exception: 异常对象.
message: 异常消息.
errors: 数据效验相关的信息.

二. Spring Boot错误处理机制探究

通过上面的两个案例,我们发现无论发生了什么错误,Spring Boot都会返回一个对应的状态码以及一个错误页面,那么这个错误页面是怎么来的呢?

要弄明白这个问题,我们需要从Spring Boot中错误处理的底层源码来进行分析。

1. SpringBoot的错误配置信息

SpringBoot的错误配置信息是通过ErrorMvcAutoConfiguration这个类来进行配置的,这个类中帮我们注册了以下组件:

DefaultErrorAttributes: 帮我们在页面上共享错误信息;
ErrorPageCustomizer: 项目中发生错误后,该对象就会生效,用来定义请求规则;
BasicErrorController: 处理默认的 ’/error‘ 请求,分为两种处理请求方式:一种是html方式,一种是json方式;
DefaultErrorViewResolver: 默认的错误视图解析器,将错误信息解析到相应的错误视图.

2. Spring Boot处理error的流程

  • 一旦系统中出现 4xx 或者 5xx 之类的错误, ErrorPageCustomizer就会生效(定义错误的相应规则);
  • 然后内部的过滤器就会映射到 ’/error‘ 请求,接着该/error请求就会被BasicErrorController处理;
  • 然后BasicErrorController会根据请求中的Accept来区分该请求是浏览器发来的,还是由其它客户端工具发来的.此时一般分为两种处理方式:errorHtml()和error().
  • 在errorHtml()方法中,获取错误状态信息,由resolveErrorView解析器解析到默认的错误视图页面,默认的错误页面是/error/404.html页面;
  • 而如果templates目录中的error目录里面有这个页面,404错误就会精确匹配404.html;
  • 如果没有这个404.html页面它就会模糊匹配4xx.html页面;
  • 如果templates中没有找到错误页面,它就会去static文件中找.

注:

static文件夹存放的是静态页面,它没有办法使用模板引擎表达式.

简单概括就是:

1??. 当出现4xx或5xx的错误:ErrorPageCustomizer开始生效;

2??. 然后ErrorPageCustomizer发起 /error 请求;

3??. /error 请求被BasicErrorController处理;

4??. BasicErrorController根据请求头中的Accept决定如何响应处理.

其实在以上的所有类中,最重要的一个类就是BasicErrorController,所以接下来我们来分析BasicErrorController源码,看看Spring Boot底层到底是怎么实现error处理的。

三. BasicErrorController源码详解

在Spring Boot中,当发生了错误后,默认情况下,Spring Boot提供了一个处理程序出错的结果映射路径 /error。当发生错误后,Spring Boot就会将请求转发到BasicErrorController控制器来处理这个错误请求。

1. BasicErrorController源码

所以我们重点分析BasicErrorController源码,首先呈上源码内容:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    private final ErrorProperties errorProperties;

    /**
     * Create a new {@link BasicErrorController} instance.
     * @param errorAttributes the error attributes
     * @param errorProperties configuration properties
     */
    public BasicErrorController(ErrorAttributes errorAttributes,
            ErrorProperties errorProperties) {
        this(errorAttributes, errorProperties,
                Collections.<ErrorViewResolver>emptyList());
    }

    /**
     * Create a new {@link BasicErrorController} instance.
     * @param errorAttributes the error attributes
     * @param errorProperties configuration properties
     * @param errorViewResolvers error view resolvers
     */
    public BasicErrorController(ErrorAttributes errorAttributes,
            ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorViewResolvers);
        Assert.notNull(errorProperties, "ErrorProperties must not be null");
        this.errorProperties=errorProperties;
    }

    @Override
    public String getErrorPath() {
        return this.errorProperties.getPath();
    }

    @RequestMapping(produces="text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status=getStatus(request);
        Map<String, Object> model=Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView=resolveErrorView(request, response, status, model);
        return (modelAndView==null ? new ModelAndView("error", model) : modelAndView);
    }

    @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);
}

2. error.path分析

这个源码的注释信息中说明了,这是一个Spring Boot自带的全局错误Controller.

这个Controller中有一个RequestMapping注解,内部有一个相当于三元运算符的操作。如果你在配置文件配置了server.error.path的话,就会使用你配置的异常处理地址,如果没有就会使用你配置的error.path路径地址,如果还是没有,则默认使用/error来作为发生异常时的处理地址。

所以我们可以按照如下图中的配置,来设置自定义的错误处理页面。

3. errorHtml()与error()方法解析

从上面的源码我们可以看到,BasicErrorController中有两个RequestMapping方法,分别是errorHtml()与error()方法来处理错误请求。

那么为什么会是两个呢?请看下面的截图:

BasicErrorController内部是通过读取request请求头中的Accept属性值,判断其内容是否为text/html;然后以此来区分请求到底是来自于浏览器(浏览器通常默认自动发送请求头内容Accept:text/html),还是客户端,从而决定是返回一个页面视图,还是返回一个 JSON 消息内容。

可以看到其中errorHtml()方法是用来处理浏览器发送来的请求,它会产生一个白色标签样式(whitelabel)的错误视图页面,该视图将以HTML格式渲染出错误数据。

该方法返回了一个error页面,如果你的项目静态页面下刚好存在一个error所对应的页面,那么Spring Boot会得到你本地的页面,如下图:

而error()方法用来处理来自非浏览器,也就是其他软件app客户端(比如postman等)发送来的错误请求,它会产生一个包含详细错误,HTTP状态及其他异常信息的JSON格式的响应内容。

注:

BasicErrorController可以作为自定义ErrorController的基类,我们只需要继承BasicErrorController,添加一个public方法,并添加@RequestMapping注解,让该注解带有produces属性,然后创建出该新类型的bean即可。

4. /error映射默认处理策略总结

经过上面的源码分析得知,在默认情况下,Spring Boot为这两种情况提供了不同的响应方式.

4.1 响应'Whitelabel Error Page'页面

一种是浏览器客户端请求一个不存在的页面或服务端处理发生异常时,这时候一般Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”:

4.2 响应JSON字符串

另一种是当我们使用Postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,Spring Boot会返回类似如下的一个 JSON 字符串信息: