整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          spring mvc DispatcherServl

          spring mvc DispatcherServlet詳解之三獲取ModelAndView過程

          個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用戶Bweb服務(wù)器建立連接之后,用戶A發(fā)送一條消息到服務(wù)器,服務(wù)器再推送給用戶B,在單機系統(tǒng)上所有的用戶都和同一個服務(wù)器建立連接,所有的session都存儲在同一個服務(wù)器中。

          單個服務(wù)器是無法支撐幾萬人同時連接同一個服務(wù)器,需要使用到分布式或者集群將請求連接負載均衡到到不同的服務(wù)下。消息的發(fā)送方和接收方在同一個服務(wù)器,這就和單體服務(wù)器類似,能成功接收到消息:

          但負載均衡使用輪詢的算法,無法保證消息發(fā)送方和接收方處于同一個服務(wù)器,當發(fā)送方和接收方不是在同一個服務(wù)器時,接收方是無法接受到消息的:

          websocket集群問題解決思路

          客戶端和服務(wù)端每次建立連接時候,會創(chuàng)建有狀態(tài)的會話session,服務(wù)器的保存維持連接的session。客戶端每次只能和集群服務(wù)器其中的一個服務(wù)器連接,后續(xù)也是和該服務(wù)器進行數(shù)據(jù)傳輸。

          要解決集群的問題,應(yīng)該考慮session共享的問題,客戶端成功連接服務(wù)器之后,其他服務(wù)器也知道客戶端連接成功。

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

          websocket類似的http是如何解決集群問題的?解決方案之一就是共享session,客戶端登錄服務(wù)端之后,將session信息存儲在Redis數(shù)據(jù)庫中,連接其他服務(wù)器時,從Redis獲取session,實際就是將session信息存儲在Redis中,實現(xiàn)redis的共享。

          session可以被共享的前提是可以被序列化,而websocketsession是無法被序列化的,httpsession記錄的是請求的數(shù)據(jù),而websocketsession對應(yīng)的是連接,連接到不同的服務(wù)器,session也不同,無法被序列化。

          方案二:ip hash(不可行)

          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ā)送給所有訂閱者:

          廣播模式的實現(xiàn)

          搭建單機 websocket

          參考以前寫的websocket單機搭建 文章,先搭建單機websocket實現(xiàn)消息的推送。

          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. 創(chuàng)建 ServerEndpointExporter 的 bean 實例

          ServerEndpointExporter 的 bean 實例自動注冊 @ServerEndpoint 注解聲明的 websocket endpoint,使用springboot自帶tomcat啟動需要該配置,使用獨立 tomcat 則不需要該配置。

          @Configuration
          public class WebSocketConfig {
              //tomcat啟動無需該配置
              @Bean
              public ServerEndpointExporter serverEndpointExporter() {
                  return new ServerEndpointExporter();
              }
          }
          

          3. 創(chuàng)建服務(wù)端點 ServerEndpoint 和 客戶端端

          • 服務(wù)端點
          @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ù)端和客戶端中的OnOpenoncloseonmessage都是一一對應(yīng)的。

          • 服務(wù)啟動后,客戶端ws.onopen調(diào)用服務(wù)端的@OnOpen注解的方法,儲存客戶端的session信息,握手建立連接。
          • 客戶端調(diào)用ws.send發(fā)送消息,對應(yīng)服務(wù)端的@OnMessage注解下面的方法接收消息。
          • 服務(wù)端調(diào)用session.getAsyncRemote().sendText發(fā)送消息,對應(yīng)的客戶端ws.onmessage接收消息。

          添加 controller

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

          效果展示

          打開兩個客戶端,其中的一個客戶端發(fā)送消息,另一個客戶端也能接收到消息。

          添加 RabbitMQ 中間件

          這里使用比較常用的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源碼,這里就不做詳細描述了。

          修改服務(wù)端點 ServerEndpoint

          WebSocket添加消息的接收方法,@RabbitListener 接收消息,隊列名稱使用常量命名,動態(tài)隊列名稱使用 #{name},其中的nameQueuebean 名稱:

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

          然后再調(diào)用sendMessage方法發(fā)送給所在連接的客戶端。

          修改消息發(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);
            }
          }
          

          消息通知流程如下所示:

          啟動兩個實例,模擬集群環(huán)境

          打開idea的Edit Configurations

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

          啟動兩個服務(wù),端口分別是80808081。在啟動8081端口的服務(wù),將前端連接端口改成8081:

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

          效果展示

          源碼

          github源碼

          參考

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

          . SpringBoot的默認錯誤處理策略

          1. 對404的默認處理策略

          我們在發(fā)送請求的時候,如果發(fā)生了404異常,SpringBoot是怎么處理的呢?

          我們可以隨便發(fā)送一個不存在的請求來驗證一下,就會看到如下圖所示:

          2. 對500的默認處理策略

          當服務(wù)器內(nèi)部發(fā)生代碼等錯誤的時候,會發(fā)生什么呢?

          比如我們?nèi)藶榈闹圃煲粋€異常出來,如下面的代碼所示:

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

          結(jié)果產(chǎn)生了如下所示效果圖:

          3.在error頁面中可以獲取的錯誤信息

          timestamp: 時間戳.
          status: 狀態(tài)碼.
          error: 錯誤提示.
          exception: 異常對象.
          message: 異常消息.
          errors: 數(shù)據(jù)效驗相關(guān)的信息.

          二. Spring Boot錯誤處理機制探究

          通過上面的兩個案例,我們發(fā)現(xiàn)無論發(fā)生了什么錯誤,Spring Boot都會返回一個對應(yīng)的狀態(tài)碼以及一個錯誤頁面,那么這個錯誤頁面是怎么來的呢?

          要弄明白這個問題,我們需要從Spring Boot中錯誤處理的底層源碼來進行分析。

          1. SpringBoot的錯誤配置信息

          SpringBoot的錯誤配置信息是通過ErrorMvcAutoConfiguration這個類來進行配置的,這個類中幫我們注冊了以下組件:

          DefaultErrorAttributes: 幫我們在頁面上共享錯誤信息;
          ErrorPageCustomizer: 項目中發(fā)生錯誤后,該對象就會生效,用來定義請求規(guī)則;
          BasicErrorController: 處理默認的 ’/error‘ 請求,分為兩種處理請求方式:一種是html方式,一種是json方式;
          DefaultErrorViewResolver: 默認的錯誤視圖解析器,將錯誤信息解析到相應(yīng)的錯誤視圖.

          2. Spring Boot處理error的流程

          • 一旦系統(tǒng)中出現(xiàn) 4xx 或者 5xx 之類的錯誤, ErrorPageCustomizer就會生效(定義錯誤的相應(yīng)規(guī)則);
          • 然后內(nèi)部的過濾器就會映射到 ’/error‘ 請求,接著該/error請求就會被BasicErrorController處理;
          • 然后BasicErrorController會根據(jù)請求中的Accept來區(qū)分該請求是瀏覽器發(fā)來的,還是由其它客戶端工具發(fā)來的.此時一般分為兩種處理方式:errorHtml()和error().
          • 在errorHtml()方法中,獲取錯誤狀態(tài)信息,由resolveErrorView解析器解析到默認的錯誤視圖頁面,默認的錯誤頁面是/error/404.html頁面;
          • 而如果templates目錄中的error目錄里面有這個頁面,404錯誤就會精確匹配404.html;
          • 如果沒有這個404.html頁面它就會模糊匹配4xx.html頁面;
          • 如果templates中沒有找到錯誤頁面,它就會去static文件中找.

          注:

          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處理的。

          三. BasicErrorController源碼詳解

          在Spring Boot中,當發(fā)生了錯誤后,默認情況下,Spring Boot提供了一個處理程序出錯的結(jié)果映射路徑 /error。當發(fā)生錯誤后,Spring Boot就會將請求轉(zhuǎn)發(fā)到BasicErrorController控制器來處理這個錯誤請求。

          1. 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);
          }

          2. error.path分析

          這個源碼的注釋信息中說明了,這是一個Spring Boot自帶的全局錯誤Controller.

          這個Controller中有一個RequestMapping注解,內(nèi)部有一個相當于三元運算符的操作。如果你在配置文件配置了server.error.path的話,就會使用你配置的異常處理地址,如果沒有就會使用你配置的error.path路徑地址,如果還是沒有,則默認使用/error來作為發(fā)生異常時的處理地址。

          所以我們可以按照如下圖中的配置,來設(shè)置自定義的錯誤處理頁面。

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

          從上面的源碼我們可以看到,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即可。

          4. /error映射默認處理策略總結(jié)

          經(jīng)過上面的源碼分析得知,在默認情況下,Spring Boot為這兩種情況提供了不同的響應(yīng)方式.

          4.1 響應(yīng)'Whitelabel Error Page'頁面

          一種是瀏覽器客戶端請求一個不存在的頁面或服務(wù)端處理發(fā)生異常時,這時候一般Spring Boot默認會響應(yīng)一個html文檔內(nèi)容,稱作“Whitelabel Error Page”:

          4.2 響應(yīng)JSON字符串

          另一種是當我們使用Postman等調(diào)試工具發(fā)送請求一個不存在的url或服務(wù)端處理發(fā)生異常時,Spring Boot會返回類似如下的一個 JSON 字符串信息:


          主站蜘蛛池模板: 无码精品人妻一区二区三区免费| 亚洲国产精品第一区二区| 国产一区三区二区中文在线| 成人h动漫精品一区二区无码| 亚洲综合色一区二区三区| 91video国产一区| 欧美日韩精品一区二区在线观看| 韩国福利一区二区美女视频| 亚洲精品国产suv一区88| 日韩精品中文字幕视频一区| 一区二区福利视频| 国产亚洲一区二区三区在线不卡 | 亚洲熟妇AV一区二区三区宅男| 亚洲日本乱码一区二区在线二产线 | 色天使亚洲综合一区二区| 国产激情一区二区三区在线观看 | 蜜桃传媒一区二区亚洲AV| 国产婷婷色一区二区三区| 一区二区三区日韩| 人妻少妇精品视频三区二区一区| 中文字幕永久一区二区三区在线观看| 国产在线精品一区二区三区直播 | 中文字幕一区二区三区四区 | 伦精品一区二区三区视频| 亚洲视频一区二区| 久久亚洲AV午夜福利精品一区| 美女视频在线一区二区三区| 影院成人区精品一区二区婷婷丽春院影视| 精品女同一区二区三区免费站| 国产成人精品日本亚洲专一区 | 国精产品一区一区三区MBA下载| 国产精品男男视频一区二区三区 | 国产乱码精品一区二区三| 国产高清在线精品一区二区 | 亚洲国产精品一区二区三区在线观看 | 中文字幕一区二区三区在线观看 | 亚洲福利秒拍一区二区| 韩国精品福利一区二区三区| 国产精品美女一区二区视频| 国产一区二区在线视频播放| 亚洲日本一区二区|