個spring mvc的架構(gòu)如下圖所示:
上篇文件講解了DispatcherServlet通過request獲取控制器Controller的過程,現(xiàn)在來講解DispatcherServletDispatcherServlet的第二步:通過request從Controller獲取ModelAndView。
DispatcherServlet調(diào)用Controller的過程:
DispatcherServlet.java
doService()--->doDispatch()--->handlerAdapter的handle()方法
try {// Actually invoke the handler. mv=ha.handle(processedRequest, response, mappedHandler.getHandler()); } finally { if (asyncManager.isConcurrentHandlingStarted()) { return; } }
最常用的實現(xiàn)了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.
類之間的轉(zhuǎn)換代碼如下所示,調(diào)用了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作用類似。繼承該接口的控制器或者類應(yīng)該保證是線程安全的,可復(fù)用的,能夠在一個應(yīng)用生命周期中處理大量的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接口的抽象實現(xiàn)類為: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) */
線程安全實現(xiàn):
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()為抽象方法,留待具體實現(xiàn)類來實現(xiàn)。它的直接子類有:
AbstractUrlViewController, MultiActionController, ParameterizableViewController, ServletForwardingController, ServletWrappingController
簡單Controller實現(xiàn)
在web.xml中有時候定義節(jié)點<welcome-list>index.html</welcome-list>等,這種簡單的請,Controller是如何實現(xiàn)的呢?我們來看看UrlFilenameViewController,它是Controller的一個間接實現(xiàn),實現(xiàn)了AbstractUrlViewController。它把url的虛擬路徑轉(zhuǎn)換成一個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)); }
復(fù)雜Controller實現(xiàn)
一個可以處理多種請求類型的Controller實現(xiàn):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.*/
根據(jù)方法名決定處理的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); } }
觸發(fā)執(zhí)行方法:
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); }
處理返回結(jié)果,要么返回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; } }
小結(jié):
DispatcherServlet接受一個請求,然后解析完locales, themes等后,通過HadlerMapping解析控制器Controller去處理請求。
找到Controller后,出發(fā)當前controller的handleRequest()方法,此controller負責(zé)真正處理請求,然后一個ModelAndView實例。
DispatcherServlet 代理此Controller,接收返回結(jié)果,然后進行渲染。
近在項目中在做一個消息推送的功能,比如客戶下單之后通知給給對應(yīng)的客戶發(fā)送系統(tǒng)通知,這種消息推送需要使用到全雙工的websocket推送消息。
所謂的全雙工表示客戶端和服務(wù)端都能向?qū)Ψ桨l(fā)送消息。不使用同樣是全雙工的http是因為http只能由客戶端主動發(fā)起請求,服務(wù)接收后返回消息。websocket建立起連接之后,客戶端和服務(wù)端都能主動向?qū)Ψ桨l(fā)送消息。
上一篇文章Spring Boot 整合單機websocket介紹了websocket在單機模式下進行消息的發(fā)送和接收:
用戶A和用戶B和web服務(wù)器建立連接之后,用戶A發(fā)送一條消息到服務(wù)器,服務(wù)器再推送給用戶B,在單機系統(tǒng)上所有的用戶都和同一個服務(wù)器建立連接,所有的session都存儲在同一個服務(wù)器中。
單個服務(wù)器是無法支撐幾萬人同時連接同一個服務(wù)器,需要使用到分布式或者集群將請求連接負載均衡到到不同的服務(wù)下。消息的發(fā)送方和接收方在同一個服務(wù)器,這就和單體服務(wù)器類似,能成功接收到消息:
但負載均衡使用輪詢的算法,無法保證消息發(fā)送方和接收方處于同一個服務(wù)器,當發(fā)送方和接收方不是在同一個服務(wù)器時,接收方是無法接受到消息的:
客戶端和服務(wù)端每次建立連接時候,會創(chuàng)建有狀態(tài)的會話session,服務(wù)器的保存維持連接的session。客戶端每次只能和集群服務(wù)器其中的一個服務(wù)器連接,后續(xù)也是和該服務(wù)器進行數(shù)據(jù)傳輸。
要解決集群的問題,應(yīng)該考慮session共享的問題,客戶端成功連接服務(wù)器之后,其他服務(wù)器也知道客戶端連接成功。
和websocket類似的http是如何解決集群問題的?解決方案之一就是共享session,客戶端登錄服務(wù)端之后,將session信息存儲在Redis數(shù)據(jù)庫中,連接其他服務(wù)器時,從Redis獲取session,實際就是將session信息存儲在Redis中,實現(xiàn)redis的共享。
session可以被共享的前提是可以被序列化,而websocket的session是無法被序列化的,http的session記錄的是請求的數(shù)據(jù),而websocket的session對應(yīng)的是連接,連接到不同的服務(wù)器,session也不同,無法被序列化。
http不使用session共享,就可以使用Nginx負載均衡的ip hash算法,客戶端每次都是請求同一個服務(wù)器,客戶端的session都保存在服務(wù)器上,而后續(xù)請求都是請求該服務(wù)器,都能獲取到session,就不存在分布式session問題了。
websocket相對http來說,可以由服務(wù)端主動推動消息給客戶端,如果接收消息的服務(wù)端和發(fā)送消息消息的服務(wù)端不是同一個服務(wù)端,發(fā)送消息的服務(wù)端無法找到接收消息對應(yīng)的session,即兩個session不處于同一個服務(wù)端,也就無法推送消息。如下圖所示:
解決問題的方法是將所有消息的發(fā)送方和接收方都處于同一個服務(wù)器下,而消息發(fā)送方和接收方都是不確定的,顯然是無法實現(xiàn)的。
將消息的發(fā)送方和接收方都處于同一個服務(wù)器下才能發(fā)送消息,那么可以轉(zhuǎn)換一下思路,可以將消息以消息廣播的方式通知給所有的服務(wù)器,可以使用消息中間件發(fā)布訂閱模式,消息脫離了服務(wù)器的限制,通過發(fā)送到中間件,再發(fā)送給訂閱的服務(wù)器,類似廣播一樣,只要訂閱了消息,都能接收到消息的通知:
發(fā)布者發(fā)布消息到消息中間件,消息中間件再將發(fā)送給所有訂閱者:
參考以前寫的websocket單機搭建 文章,先搭建單機websocket實現(xiàn)消息的推送。
<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>
ServerEndpointExporter 的 bean 實例自動注冊 @ServerEndpoint 注解聲明的 websocket endpoint,使用springboot自帶tomcat啟動需要該配置,使用獨立 tomcat 則不需要該配置。
@Configuration
public class WebSocketConfig {
//tomcat啟動無需該配置
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@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】有新的連接,總數(shù):{}",webSocketSet.size());
}
@OnClose
public void onClose(){
String id=this.session.getId();
if (id !=null){
webSocketSet.remove(id);
log.info("【websocket】連接斷開:總數(shù):{}",webSocketSet.size());
}
}
@OnMessage
public void onMessage(String message){
if (!message.equals("ping")){
log.info("【wesocket】收到客戶端發(fā)送的消息,message={}",message);
sendMessage(message);
}
}
/**
* 發(fā)送消息
* @param message
* @return
*/
public void sendMessage(String message){
for (WebSocket webSocket : webSocketSet.values()) {
webSocket.session.getAsyncRemote().sendText(message);
}
log.info("【wesocket】發(fā)送消息,message={}", message);
}
}
<div>
<input type="text" name="message" id="message">
<button id="sendBtn">發(fā)送</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>
服務(wù)端和客戶端中的OnOpen、onclose、onmessage都是一一對應(yīng)的。
@GetMapping({"","index.html"})
public ModelAndView index() {
ModelAndView view=new ModelAndView("index");
return view;
}
打開兩個客戶端,其中的一個客戶端發(fā)送消息,另一個客戶端也能接收到消息。
這里使用比較常用的RabbitMQ作為消息中間件,而RabbitMQ支持發(fā)布訂閱模式:
交換機使用扇形交換機,消息分發(fā)給每一條綁定該交換機的隊列。以服務(wù)器所在的IP + 端口作為唯一標識作為隊列的命名,啟動一個服務(wù),使用隊列綁定交換機,實現(xiàn)消息的訂閱:
@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());
}
}
獲取服務(wù)器IP和端口可以具體查看Github源碼,這里就不做詳細描述了。
在WebSocket添加消息的接收方法,@RabbitListener 接收消息,隊列名稱使用常量命名,動態(tài)隊列名稱使用 #{name},其中的name是Queue的bean 名稱:
@RabbitListener(queues="#{psQueue.name}")
public void pubsubQueueFirst(String message) {
System.out.println(message);
sendMessage(message);
}
然后再調(diào)用sendMessage方法發(fā)送給所在連接的客戶端。
在WebSocket類的onMessage方法將消息發(fā)送改成RabbitMQ方式發(fā)送:
@OnMessage
public void onMessage(String message){
if (!message.equals("ping")){
log.info("【wesocket】收到客戶端發(fā)送的消息,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:
啟動兩個服務(wù),端口分別是8080和8081。在啟動8081端口的服務(wù),將前端連接端口改成8081:
var ws=new WebSocket("ws://127.0.0.1:8081/message");
github源碼
我們在發(fā)送請求的時候,如果發(fā)生了404異常,SpringBoot是怎么處理的呢?
我們可以隨便發(fā)送一個不存在的請求來驗證一下,就會看到如下圖所示:
當服務(wù)器內(nèi)部發(fā)生代碼等錯誤的時候,會發(fā)生什么呢?
比如我們?nèi)藶榈闹圃煲粋€異常出來,如下面的代碼所示:
@GetMapping("/user/{id:\\d+}")
public User get(@PathVariable String id) {
throw new RuntimeException();
}
結(jié)果產(chǎn)生了如下所示效果圖:
timestamp: 時間戳.
status: 狀態(tài)碼.
error: 錯誤提示.
exception: 異常對象.
message: 異常消息.
errors: 數(shù)據(jù)效驗相關(guān)的信息.
通過上面的兩個案例,我們發(fā)現(xiàn)無論發(fā)生了什么錯誤,Spring Boot都會返回一個對應(yīng)的狀態(tài)碼以及一個錯誤頁面,那么這個錯誤頁面是怎么來的呢?
要弄明白這個問題,我們需要從Spring Boot中錯誤處理的底層源碼來進行分析。
SpringBoot的錯誤配置信息是通過ErrorMvcAutoConfiguration這個類來進行配置的,這個類中幫我們注冊了以下組件:
DefaultErrorAttributes: 幫我們在頁面上共享錯誤信息;
ErrorPageCustomizer: 項目中發(fā)生錯誤后,該對象就會生效,用來定義請求規(guī)則;
BasicErrorController: 處理默認的 ’/error‘ 請求,分為兩種處理請求方式:一種是html方式,一種是json方式;
DefaultErrorViewResolver: 默認的錯誤視圖解析器,將錯誤信息解析到相應(yīng)的錯誤視圖.
static文件夾存放的是靜態(tài)頁面,它沒有辦法使用模板引擎表達式.
1??. 當出現(xiàn)4xx或5xx的錯誤:ErrorPageCustomizer開始生效;
2??. 然后ErrorPageCustomizer發(fā)起 /error 請求;
3??. /error 請求被BasicErrorController處理;
4??. BasicErrorController根據(jù)請求頭中的Accept決定如何響應(yīng)處理.
其實在以上的所有類中,最重要的一個類就是BasicErrorController,所以接下來我們來分析BasicErrorController源碼,看看Spring Boot底層到底是怎么實現(xiàn)error處理的。
在Spring Boot中,當發(fā)生了錯誤后,默認情況下,Spring Boot提供了一個處理程序出錯的結(jié)果映射路徑 /error。當發(fā)生錯誤后,Spring Boot就會將請求轉(zhuǎn)發(fā)到BasicErrorController控制器來處理這個錯誤請求。
所以我們重點分析BasicErrorController源碼,首先呈上源碼內(nèi)容:
@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);
}
這個源碼的注釋信息中說明了,這是一個Spring Boot自帶的全局錯誤Controller.
這個Controller中有一個RequestMapping注解,內(nèi)部有一個相當于三元運算符的操作。如果你在配置文件配置了server.error.path的話,就會使用你配置的異常處理地址,如果沒有就會使用你配置的error.path路徑地址,如果還是沒有,則默認使用/error來作為發(fā)生異常時的處理地址。
所以我們可以按照如下圖中的配置,來設(shè)置自定義的錯誤處理頁面。
從上面的源碼我們可以看到,BasicErrorController中有兩個RequestMapping方法,分別是errorHtml()與error()方法來處理錯誤請求。
那么為什么會是兩個呢?請看下面的截圖:
BasicErrorController內(nèi)部是通過讀取request請求頭中的Accept屬性值,判斷其內(nèi)容是否為text/html;然后以此來區(qū)分請求到底是來自于瀏覽器(瀏覽器通常默認自動發(fā)送請求頭內(nèi)容Accept:text/html),還是客戶端,從而決定是返回一個頁面視圖,還是返回一個 JSON 消息內(nèi)容。
可以看到其中errorHtml()方法是用來處理瀏覽器發(fā)送來的請求,它會產(chǎn)生一個白色標簽樣式(whitelabel)的錯誤視圖頁面,該視圖將以HTML格式渲染出錯誤數(shù)據(jù)。
該方法返回了一個error頁面,如果你的項目靜態(tài)頁面下剛好存在一個error所對應(yīng)的頁面,那么Spring Boot會得到你本地的頁面,如下圖:
而error()方法用來處理來自非瀏覽器,也就是其他軟件app客戶端(比如postman等)發(fā)送來的錯誤請求,它會產(chǎn)生一個包含詳細錯誤,HTTP狀態(tài)及其他異常信息的JSON格式的響應(yīng)內(nèi)容。
BasicErrorController可以作為自定義ErrorController的基類,我們只需要繼承BasicErrorController,添加一個public方法,并添加@RequestMapping注解,讓該注解帶有produces屬性,然后創(chuàng)建出該新類型的bean即可。
經(jīng)過上面的源碼分析得知,在默認情況下,Spring Boot為這兩種情況提供了不同的響應(yīng)方式.
一種是瀏覽器客戶端請求一個不存在的頁面或服務(wù)端處理發(fā)生異常時,這時候一般Spring Boot默認會響應(yīng)一個html文檔內(nèi)容,稱作“Whitelabel Error Page”:
另一種是當我們使用Postman等調(diào)試工具發(fā)送請求一個不存在的url或服務(wù)端處理發(fā)生異常時,Spring Boot會返回類似如下的一個 JSON 字符串信息:
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。