1. 程式人生 > >一點一滴學習Spring(七)之Spring MVC的高階技術

一點一滴學習Spring(七)之Spring MVC的高階技術

Servlet 3.0提供了既能在容器中動態註冊servlet的方法,也提供了通過實現ServletContainerInitializer介面的方法實現在容器啟動階段為容器動態註冊Servlet、Filter和listeners。
容器會在應用的啟動階段,呼叫所有實現ServletContainerInitializer介面類中的onStartup()方法。
而Spring 3.2中,則進一步簡化了這點,只需要實現WebApplicationInitializer介面就可以了,檢視這個介面的原始碼,裡面也非常簡單,只有一個方法,傳入的引數是ServletContext

public interface
WebApplicationInitializer { public abstract void onStartup(ServletContext servletcontext) throws ServletException; }

spring提供了相關的實現類->
AbstractAnnotationConfigDispatcherServletInitializer、AbstractDispatcherServletInitializer
AbstractContextLoaderInitializer可以動態註冊DispatcherServlet。

一、自定義DispatcherServlet配置

通過下面的spring的實現類AbstractAnnotationConfigDispatcherServletInitializer相關原始碼:

public void onStartup(ServletContext servletContext) throws ServletException
    {
        registerContextLoaderListener(servletContext);
    }

    protected void registerDispatcherServlet(ServletContext servletContext)
    {
        String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() may not return empty or null"); WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, (new StringBuilder()).append("createServletApplicationContext() did not return an application context for servlet [").append(servletName).append("]").toString()); DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext); javax.servlet.ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); Assert.notNull(registration, (new StringBuilder()).append("Failed to register servlet with name '").append(servletName).append("'.").append("Check if there is another servlet registered under the same name.").toString()); registration.setLoadOnStartup(1); registration.addMapping(getServletMappings()); registration.setAsyncSupported(isAsyncSupported()); Filter filters[] = getServletFilters(); if(!ObjectUtils.isEmpty(filters)) { Filter afilter[] = filters; int i = afilter.length; for(int j = 0; j < i; j++) { Filter filter = afilter[j]; registerServletFilter(servletContext, filter); } } customizeRegistration(registration); }

我們可以知道AbstractAnnotationConfigDispatcherServletInitializer將DispatcherServlet註冊到Servlet容器中之後,會呼叫customizeRegistration()方法,並將Servlet註冊後得到的Dynamic registration傳遞進來。所以通過customizeRegistration()方法的重寫我們可以對DispatcherServlet進行額外的配置。如下程式碼所示:

public class SpitterWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{

    @Override
    protected Class<?>[] getRootConfigClasses() {
        // TODO Auto-generated method stub
        return new Class<?>[]{RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // TODO Auto-generated method stub
        return new Class<?>[]{WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        // TODO Auto-generated method stub
        return new String[] {"/web/*"};
    }

    //重寫customizeRegistration方法,實現DispatcherServlet的額外配置
    @Override
    protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic dynamic){

    }

}

藉助customizeRegistration()方法中的javax.servlet.ServletRegistration.Dynamic dynamic,我們能完成多項任務。
包括通過呼叫setLoadOnStartup()設定load-on-start的優先順序,通過setInitOarameter()設定初始化引數。通過setMultipartConfig()配置Servlet3.0對multipart的支援。
這裡寫圖片描述

二、新增其他的Servlet和Filter

按照AbstractAnnotationConfigDispatcherServletInitializer的定義,他會建立DispatcherServlet和ContextLoaderListener。
但是,如果你想註冊其他的servlet、filter或Listen的話,那該怎麼辦呢?
基於java的初始化容器(initializer)的一個好處就在於,我們可以定義任意數量的初始化類。因此,我們想往web容器中註冊其他元件的話,只需建立一個新的初始化容器
就可以了。最簡單的方法就是實現Spring的WebApplicationInitializer介面
下面例項->新增一個過濾器:

過濾器配置:

public class FilterConfig implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletcontext) throws ServletException {
        // TODO Auto-generated method stub
        Dynamic filter = servletcontext.addFilter("myFilter",CustomerFilter.class);
        filter.addMappingForUrlPatterns(null, false, "/web/*");
    }

}

這裡addMappingForUrlPatterns(EnumSet dispatcherTypes, boolean isMatchAfter, String urlPatterns[])方法,urlPatterns對映需要執行過濾的路徑

CustomerFilter類

public class CustomerFilter implements Filter{


    @Override
    public void init(FilterConfig filterconfig) throws ServletException {
        // TODO Auto-generated method stub
        System.out.println("CustomerFilter init...");
    }

    @Override
    public void doFilter(ServletRequest servletrequest, ServletResponse servletresponse, FilterChain filterchain)
            throws IOException, ServletException {
        // TODO Auto-generated method stub
        System.out.println("測試過濾器.....");
        filterchain.doFilter(servletrequest, servletresponse);
    }

    @Override
    public void destroy() {
        // TODO Auto-generated method stub
        System.out.println("CustomerFilter destroy...");
    }

}

這個類需要實現Filter
同理:可新增servlet和listen,只要類實現WebApplicationInitializer,並重寫其中的onStartup方法

這裡寫圖片描述

這裡寫圖片描述

如果要將應用部署到支援Servlet3.0的容器中,那麼WebApplicationInitializer提供了一種通用的方式,是現在JAVA中註冊Servlet、Filter和Listener。不過,如果你只是註冊Filter,並且該Filter只會對映到DispatcherServlet上的話,那麼在AbstractAnnotationConfigDispatcherServletInitializer中還有一種快捷方式。
為了註冊Filter,並將其對映到DispatcherServlet,所需要做的僅僅是重寫AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters()方法。如下所示:

@Override
    protected Filter[] getServletFilters() {
        // TODO Auto-generated method stub
        return new Filter[]{new CustomerFilter()};
    }

getServletFilters方法返回的所有Filter都會對映到DispatcherServlet上。

三、在web.xml中宣告DispatcherServlet

在典型的Spring MVC應用中,我們會需要DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer會自動
註冊它們。但是如果需要在web.xml中註冊的話,那就需要我們自己註冊。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

    <display-name>canal</display-name>

    <!--設定上下文配置檔案的位置:檔案會被ContextLoaderListener載入-->
    <context-param>  
        <param-name>contextConfigLocation</param-name>  
        <param-value>   
            classpath*:/applicationContext-*.xml
        </param-value>   
    </context-param>  

    <!-- 設定字元過濾器非必須-->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <!-- 指定字元過濾器對映路徑非必須 -->
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Spring MVC前端處理器,註冊DispatcherServlet -->
    <servlet>
        <servlet-name>Dispatcher Servlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--檔案beans.xml會被DispatcherServlet載入-->
        <init-param>
            <description>Spring MVC定義Bean檔案,該檔案為空配置,所有配置交給上級WebApplicationContext來處理</description>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/beans.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!--DispatcherServlet對映路徑,所有以.html結尾的路徑 -->
    <servlet-mapping>
        <servlet-name>Dispatcher Servlet</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>

    <!--註冊ContextLoaderListener-->
    <listener>    
        <listener-class>    
            org.springframework.web.context.ContextLoaderListener    
        </listener-class>    
    </listener>

    <!--配置其他過濾器及servlet--> 
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/web/*</url-pattern>
    </filter-mapping> 

    <servlet>
        <servlet-name>CXFService</servlet-name>
        <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>CXFService</servlet-name>
        <url-pattern>/web/*</url-pattern>
    </servlet-mapping>

</web-app>

ContextLoaderListener和DispatcherServlet各自都會載入一個Spring應用上下文。上下文contextConfigLocation指定了xml檔案的位置。
要在Spring MVC中使用基於Java的配置,我們需要告訴DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,
這是WebApplicationContext的一個實現類。他會載入Java配置類,而不是使用xml。要實現這種配置,我們可以設定contextClass上下文引數以及
DispatcherServlet的初始化引數。

如下所示,新的web.xml基於java配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

  <!-- 指定使用Java配置 -->
  <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  </context-param>

  <context-param>  
     <param-name>contextConfigLocation</param-name>  
     <param-value>com.cn.test.config.RootConfig</param-value>   
  </context-param>

  <listener>    
     <listener-class>    
         org.springframework.web.context.ContextLoaderListener    
     </listener-class>    
  </listener> 

    <!-- Spring MVC前端處理器,註冊DispatcherServlet -->
  <servlet>
    <servlet-name>Dispatcher Servlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--檔案beans.xml會被DispatcherServlet載入-->
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.cn.test.config.WebConfig</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
   </servlet>
   <!--DispatcherServlet對映路徑,所有以.html結尾的路徑 -->
   <servlet-mapping>
    <servlet-name>Dispatcher Servlet</servlet-name>
    <url-pattern>/web/*</url-pattern>
   </servlet-mapping> 

        <!--配置其他過濾器及servlet--> 
   <filter>
        <filter-name>customerFilter</filter-name>
        <filter-class>com.cn.test.filter.CustomerFilter</filter-class>
   </filter>
   <filter-mapping>
        <filter-name>customerFilter</filter-name>
        <url-pattern>/web/*</url-pattern>
   </filter-mapping>
</web-app>

四、配置multipart解析器

DispatcherServlet並沒有實現任何解析mulipart請求資料的功能,他將該任務委託給了Spring中的MultipartResolver策略介面的實現。

通過這個實現類來解析multipart請求的內容。從3.1開始,Spring內建類兩個MultipartResolver的實現供我們選擇:
1、CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart請求;
2、StandardServletMultipartResolver依賴於Servlet3.0對multipart請求的支援。始於Spring3.1(優選方案)

使用Servlet3.0解析multipart請求
相容Servlet3.0的StandardServletMultipartResolver沒有任何構造引數,也沒有要設定的屬性。這樣,在Spring應用的上下文中,將其
宣告為bean就會變得非常簡單,如下所示:

    @Bean
    public MultipartResolver multipartResolver(){
        return new StandardServletMultipartResolver();
    }

既然@Bean方法如此簡單,那麼我們該如何限制StandardServletMultipartResolver的工作方式呢?怎麼設定上傳檔案的大小及臨時儲存目錄呢?
對於沒有建構函式和設定屬性的StandardServletMultipartResolver來說,這似乎是很難限制的。
其實並不是這樣的,我們是有辦法設定StandardServletMultipartResolver的限制條件的,只不過不在Spring中配置StandardServletMultipartResolver,而只要在Servlet中指定multipart的配置。還記得我們前面所說的customizeRegistration()方法嗎?下面就用上了此方法:

    //過載customizeRegistration方法,實現DispatcherServlet的額外配置
    @Override
    protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic dynamic){
        //"/tmp/uploads"為臨時儲存路徑
        MultipartConfigElement configElement = new MultipartConfigElement("/tmp/uploads");
        dynamic.setMultipartConfig(configElement);
    }

整個檔案:

public class SpitterWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{

    @Override
    protected Class<?>[] getRootConfigClasses() {
        // TODO Auto-generated method stub
        return new Class<?>[]{RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // TODO Auto-generated method stub
        return new Class<?>[]{WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        // TODO Auto-generated method stub
        return new String[] {"/web/*"};
    }

    //過載customizeRegistration方法,實現DispatcherServlet的額外配置
    @Override
    protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic dynamic){
        //"/tmp/uploads"為臨時儲存路徑--強制設定
        MultipartConfigElement configElement = new MultipartConfigElement("/tmp/uploads");
        dynamic.setMultipartConfig(configElement);
    }

    @Override
    protected Filter[] getServletFilters() {
        // TODO Auto-generated method stub
        return new Filter[]{new CustomerFilter()};
    }
}

這裡寫圖片描述
使用web.xml配置的程式碼片段

<servlet>
        <servlet-name>Dispatcher Servlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--檔案beans.xml會被DispatcherServlet載入-->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.cn.test.config.WebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <multipart-config>
            <location>/tmp/uploads</location>
            <file-size-threshold>0</file-size-threshold>
            <max-file-size>2097152</max-file-size><!-- 2M -->
            <max-request-size>4194304</max-request-size><!-- 4M -->
        </multipart-config>
   </servlet>
   <!--DispatcherServlet對映路徑,所有以.html結尾的路徑 -->
   <servlet-mapping>
        <servlet-name>Dispatcher Servlet</servlet-name>
        <url-pattern>/web/*</url-pattern>
   </servlet-mapping> 

配置Jakarta Commons FileUpload multipart解析器

@Bean
    public MultipartResolver multipartResolver() throws IOException{
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setUploadTempDir(new FileSystemResource("臨時檔案路徑"));//,非必須設定
        resolver.setMaxInMemorySize(4096);//最大記憶體大小
        resolver.setMaxUploadSize(2097152);//上傳檔案大小限制
        return resolver;
    }
xml檔案設定
    <bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="2097152" />
        <property name="maxInMemorySize" value="4096" />
    </bean>

處理multipart請求
在controller方法的接收引數上新增@RequestPart(“file”) byte[] file
例如:

    @RequestMapping(value="/home/file",method=RequestMethod.POST)//處理對/web/home/test的請求
    public String home_file(Model model,@RequestPart("file") byte[] file){
    }

若是提交表單時,沒有選擇圖片,那麼這個陣列是空,而非null。那麼我們要如何將byte陣列轉化為儲存檔案那,看下一部分

接收MultipartFile

public String home_file(Model model,@RequestPart("file") MultipartFile file){

}

這裡寫圖片描述

五、處理異常

專案中實際用到的統一捕獲異常方式:

1、Spring boot中使用的異常捕獲

@Component
@ControllerAdvice
public class GlobalDefaultExceptionHandler{  

      private Logger exception = LoggerFactory.getLogger("exception");

      //新增全域性異常處理流程,根據需要設定需要處理的異常
      @ExceptionHandler(value=Exception.class)
      @ResponseBody
      public MsgHeader defaultErrorHandler(HttpServletRequest request,  
              Exception e) throws Exception  
      {  
          //按需重新封裝需要返回的錯誤資訊  
          //此處列印錯誤日誌
          e.printStackTrace();
          return new MsgHeader(CodeEnum.EXCEPTION.getCode(),CodeEnum.EXCEPTION.getDesc_enu());
      } 

      //新增全域性異常處理流程,捕獲客戶端自己的異常
      @ExceptionHandler(value={ServiceException.class})
      @ResponseBody
      public MsgHeader jsonErrorHandler(HttpServletRequest request,  
              ServiceException e) throws Exception  
      {  
          //此處列印錯誤日誌
          exception.error(e.getCode()+"---"+e.getDesc());
          return new MsgHeader(e.getCode(), e.getDesc());  
      }

      //新增全域性異常處理流程,捕獲服務層丟擲的自定義異常
      @ExceptionHandler(value={BaseException.class})
      @ResponseBody
      public MsgHeader jsonErrorHandler(HttpServletRequest request,  
              BaseException e) throws Exception  
      {  
          //此處列印錯誤日誌
          exception.error(e.getCode()+"---"+e.getMsg());
          return new MsgHeader(e.getCode(), e.getMsg());  
      }
} 

@ExceptionHandler標註的方法處理給定的異常
類級別使用@ControllerAdvice註解:標明他是一個控制器通知
@ResponseBody返回json格式資料
這個類一定要配置在spring能夠掃描到的位置

2、Spring中統一捕獲異常

public class ExceptionAdvisor implements ThrowsAdvice{

    private static final Logger log = LoggerFactory.getLogger(ExceptionAdvisor.class);

    public void afterThrowing(Method method, Object[] args, Object target,  
            Exception ex) throws Throwable  
    {  
        // 在後臺中輸出錯誤異常異常資訊,通過log4j輸出。  
        log.info("**************************************************************");  
        log.info("Error happened in class: " + target.getClass().getName());  
        log.info("Error happened in method: " + method.getName());  
            for (int i = 0; i < args.length; i++)  
            {  
                log.info("arg[" + i + "]: " + args[i]);  
            }  
        log.info("Exception class: " + ex.getClass().getName());  
        log.info("ex.getMessage():" + ex.getMessage());  
        log.info("**************************************************************");  
        // 在這裡判斷異常,根據不同的異常返回錯誤。  
        if (ex.getClass().equals(ConstraintViolationException.class)){  
            ex.printStackTrace();  
            ConstraintViolationException exc = (ConstraintViolationException) ex;
            String enumName = exc.getConstraintViolations().iterator().next().getMessage();
            log.info("enumName--------"+enumName);
            CodeEnum enumCode;
            try {
                enumCode = CodeEnum.valueOf(enumName);
            } catch (Exception e) {
                //若是名稱不能成功轉化為列舉,則給定common列舉
                enumCode = CodeEnum.VALIDATE_COMMON;
            }
            log.info("code:"+enumCode.getCode()+"--enu desc--"+enumCode.getDesc_enu());
            throw new BaseException(enumCode.getCode(), enumCode.getDesc_enu()); 
        }else{
            ex.printStackTrace();
            throw ex;
        } 

    }  
}

springContext.xml中新增配置

<bean id="exceptionHandler" class="com.isgo.gallerydao.core.exception.ExceptionAdvisor"></bean>   
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >  
       <property name="beanNames">  
        <list>    <!-- 配置需要進行日誌記錄的Service和Dao -->  
            <value>*Service</value> <!-- Service層的Bean ID 命名要以Service結尾 -->  
        </list>  
       </property>  
       <property name="interceptorNames">  
        <list>  
             <value>exceptionHandler</value>  
        </list>  
       </property>  
    </bean> 

3、CXF配置統一捕獲異常:

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.ExceptionMapper;

public class InvokeFaultExceptionMapper implements ExceptionMapper  {

    private static Logger logger = LogManager.getLogger("exception");

    @Override
    public Response toResponse(Throwable ex) {
        StackTraceElement[] trace = new StackTraceElement[1];  
        trace[0] = ex.getStackTrace()[0];  
        ex.setStackTrace(trace);  
        ResponseBuilder rb = Response.status(Response.Status.OK);  
        rb.type("application/json;charset=UTF-8");  
        if (ex instanceof ServiceException) {//自定義的異常類  
            ServiceException e = (ServiceException) ex;  
            ServiceExceptionEntity entity = new ServiceExceptionEntity(e.getCode(),e.getDesc());
            rb.entity(entity);  
        }else{
            ServiceExceptionEntity entity = new ServiceExceptionEntity(CodeEnum.EXCEPTION.getCode(),
                    CodeEnum.EXCEPTION.getDesc_enu());
            rb.entity(entity);  
        } 
        if(null!=trace[0]){
            logger.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            logger.error("className:{},fileName:{},methodName:{},lineNumber:{},cause:{}.",trace[0].getClassName(),
                    trace[0].getFileName(),trace[0].getMethodName(),trace[0].getLineNumber(),ex);
        }
        rb.language(Locale.SIMPLIFIED_CHINESE);  
        Response r = rb.build();  
        return r;    
    }

}

ExceptionMapper在包:javax.ws.rs-api.jar中

    <bean id="invokeFaultExceptionMapper" class="com.canal.api.exception.InvokeFaultExceptionMapper"/>

六、@ControllerAdvice註解

控制類通知,這個類會包含一個或多個如下型別的方法:
1、@ExceptionHandler註解標註的方法–使所有的控制類異常在一個地方統一處理。參考Spring boot中使用的異常捕獲
2、@InitBinder註解標註的方法
3、@ModelAttribute註解標註的方法