一点一滴学习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注解标注的方法

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值