为什么要使用WebMvcConfigurer?
WebMvcConfigurer是一个接口,提供很多自定义的拦截器,例如跨域设置、类型转化器等等。可以说此接口为开发者提前想到了很多拦截层面的需求,方便开发者自由选择使用。由于Spring5.0废弃了WebMvcConfigurerAdapter,所以WebMvcConfigurer继承了WebMvc-ConfigurerAdapter大部分内容。
WebMvcConfigurer方法介绍
由于内容太多,只展示3个关键的接口,用的比较少的,只是阐述下含义,不再详解,用的更少的,就不看了。
1.configurePathMatch(PathMatchConfigurer configurer)
这个用到的比较少,这个是和访问路径有关的。举个例子,比如说PathMatchConfigurer 有个配置是setUseTrailingSlashMatch(),如果设置为true的话(默认为true),后面加个斜杠并不影响路径访问,例如“/user”等同于“/user/"。我们在开发中很少在访问路径上搞事情,所以这个方法如果有需要的请自行研究吧。
2.configureContentNegotiation(ContentNegotiationConfigurer configurer)
这个东西直译叫做内容协商机制,主要是方便一个请求路径返回多个数据格式。ContentNegotiationConfigurer这个配置里面你会看到MediaType,里面有众多的格式。此方法不在多赘述。
3.configureAsyncSupport(AsyncSupportConfigurer configurer)
顾名思义,这是处理异步请求的。只能设置两个值,一个超时时间(毫秒,Tomcat下默认是10000毫秒,即10秒),还有一个是AsyncTaskExecutor,异步任务执行器。
4.configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer)
这个接口可以实现静态文件可以像Servlet一样被访问。
5.addFormatters(FormatterRegistry registry)
增加转化器或者格式化器。这边不仅可以把时间转化成你需要时区或者样式。还可以自定义转化器和你数据库做交互,比如传进来userId,经过转化可以拿到user对象。
6.addInterceptors(InterceptorRegistry registry)
盼望着,盼望着,你一个常用的方法来了。这个方法可以自定义写拦截器,并指定拦截路径。来,咱们写一个拦截器。
public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle,ok,假设给你一个true,运行去吧"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle,ok,看看我什么时候运行的。"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion,ok,看完不点个赞再走吗?"); } }
然后配置一下:
@Configuration public class MyConfigurer implements WebMvcConfigurer { @Bean public MyInterceptor getMyInterceptor(){ return new MyInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(this.getMyInterceptor()) .addPathPatterns("/abc","/configurePathMatch"); } }
可以看出addPathPatterns()里面可以尝试添加多个路径,或者写成”/**“,包含所有路径都需要尝试拦截一下。
测试一下,输出:
preHandle,ok,假设给你一个true,运行去吧 ===》执行业务逻辑===》 postHandle,ok,看看我什么时候运行的。 afterCompletion,ok,看完不点个赞再走吗?
7.addResourceHandlers(ResourceHandlerRegistry registry)
自定义资源映射。这个东西也比较常用,业务场景就是自己的服务器作为文件服务器,不利用第三方的图床,就需要一个虚拟路径映射到我们服务器的地址。值得一提的是,如果你的项目是war包启动,一般都是再Tomcat中配置一下(配置方法请百度);如果是jar包启动(SpringBoot经常用这种方式启动),就可以用到这个方法了。例如:
public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/my/**") .addResourceLocations("file:E:/my/"); super.addResourceHandlers(registry); }
真实路径,wndows当服务器的情况下,前面一定要加上一个file:。
8.addCorsMappings(CorsRegistry registry)
这个是设置跨域问题的,几乎是每个后台服务器都需要配置的东西。我曾写过一篇文章,专门讲跨域问题和SpringBoot怎么配置的,请查询:
https://juejin.im/post/5cfe6367f265da1b9163887f
9.addViewControllers(ViewControllerRegistry registry)
这个方法可以实现,一个路径自动跳转到一个页面。不过现在多为前后端分离的项目,是不是可以把跳转路由的问题直接扔给前端。
后面还有七个:configureViewResolvers、addArgumentResolvers、addReturnValueHandlers、configureMessageConverters、extendMessageConverters、configureHandlerExceptionResolvers、extendHandlerExceptionResolvers。是在用的太少了,就不再看了。
小结
本篇先大概知道下这些都是什么方法,最重要的是知道了WebMvcConfigurer为我们再拦截层做了一些通用拦截器,方便开发者使用。当然也可以自己实现拦截器。最常用的是还是6、7、8。其他的以后有机会研究好了再更新。
好了,接下来我们看看怎么实现无文件配置来配置SpringMVC
Servlet3.0以上可以不使用web.xml进行配置,而是实现Spring提供的WebApplicationInitializer接口进行相关的Web配置。当然我们的web服务器也需要时tomcat7.x以上版本。
现在我们首先看看我们这个实验环境的maven pom文件:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.tony</groupId> <artifactId>AnnotationSpringmvc</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>AnnotationSpringmvc Maven Webapp</name> <url>http://maven.apache.org</url> <repositories> <repository> <id>io.spring.repo.maven.release</id> <url>http://repo.spring.io/release/</url> </repository> <repository> <id>maven-eu.nuxeo.org</id> <url>https://maven-eu.nuxeo.org/nexus/content/repositories/public-releases/</url> </repository> </repositories> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>4.3.9.RELEASE</spring.version> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/jsp-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>AnnotationSpringmvc</finalName> <pluginManagement> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <uriEncoding>UTF-8</uriEncoding> <server>tomcat8spring</server> </configuration> </plugin> </plugins> </pluginManagement> </build> </project>
先来回顾一下SpringMVC在web.xml的配置是什么样的,我们需要使用Spring提供的DispatcherServlet对所有请求进行拦截。具体的配置如下:
<servlet> <servlet-name>springMvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
当然我们今天的目标不是使用XML进行配置而且使用WebInitializer。以下代码就是关于WebInitializer的具体实现:
public class WebInitializer implements WebApplicationInitializer { public void onStartup(ServletContext servletContext) throws ServletException { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(ApplicationConfig.class); context.setServletContext(servletContext); ServletRegistration.Dynamic servlet = servletContext.addServlet("springMvc",new DispatcherServlet(context)); servlet.addMapping("/"); servlet.setLoadOnStartup(0); } }
上面的代码主要完成两件事:
1、在onStartup方法当中,创建Spring ApplicationContext 并 注册到相关的配置类
2、创建一个Servlet,这个Serlvet正是我们平时使用XML上配置的DispatcherServlet。在addMapping方法中,我们选择拦截所有请求。
当然也少不了我们的配置类。在配置类当中我们最重要的是,需要配置一个viewResolver,熟悉SpringMVC的同学应该也知道,一般情况下都是用InternalResourceViewResolver,所以同理我们也需要建立一个方法去创建InternalResourceViewResolver:
@Configuration @EnableWebMvc @ComponentScan("com.gamer") public class ApplicationConfig { @Bean public ViewResolver viewResolver(){ InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/views/"); viewResolver.setSuffix(".jsp"); viewResolver.setOrder(99); return viewResolver; } }
然后我们来规矩,创建一个Controller,随随便便映射到一个JSP当中去。我已经在WEB-INF/views目录下创建了一个index.jsp。所以下面的controller非常简单做一下跳转即可。
@Controller public class IndexController { @RequestMapping("/index") public String index(){ return "aaa"; } }
OK!现在估计已经完成了最基本的SpringMVC的搭建了,其他拦截器配置、JSON输出配置、静态资源过滤等我们一个个来说。
一、静态资源
以前我们在Sping静态资源有两种方法
1、 <mvc:default-servlet-handler /> 自动识别静态资源
2、使用mvc:resources配置静态支援映射
以下就是Spring无配置文件的静态资源映射的方法,需要注意的是,我们的配置类需要继承WebMvcConfigurerAdapter类。WebMvcConfigurerAdapter提供非常多的方法,其中有一个方法是public void addResourceHandlers(ResourceHandlerRegistry registry),只要我们在配置类重写这个方法即可:
@Configuration @EnableWebMvc @ComponentScan("com.gamer") public class ApplicationConfig extends WebMvcConfigurerAdapter { @Bean public ViewResolver viewResolver(){ InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/views/"); viewResolver.setSuffix(".jsp"); viewResolver.setOrder(99); return viewResolver; } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/assets/"); } }
可以看到我们的资源位置为classpath:/assets/ 其实际位置就是resources目录下的assets,当然也可以按照以前常规的讨论直接写上/assets/即可。动态资源目录防止旧资源被客户端缓存导致新资源无法及时获取的问题,在代码层面上也非常简单。
上面的红色字体提到了动态资源目录防止旧资源被客户端缓存导致新资源无法及时获取的问题,那么我们先来说说这个问题
动态资源和静态资源访问冲突问题:
web.xml配置文件:
<servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMVC.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
springMVC.xml配置文件:
<context:component-scan base-package="com.young"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"/> </bean>
HelloController处理器:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class HelloController { @RequestMapping("hello") public String hello(){ System.out.println("Hello SpringMVC"); return "index"; } }
index.jsp代码,在与index.jsp同目录下存在文件"style.css"和图片"123.png":
<html> <head> <link rel="stylesheet" href="style.css" type="text/css"/> </head> <body> <h2>Hello World!</h2> <img src="123.png"> </body> </html>
style.css代码
h2{
color: aqua;
}
问题:运行程序后图片无法显示,css文件没有生效,检查网络出现如下情况:style.css和123.png状态码为404:
查看后台控制台日志,存在如下记录:
13-May-2020 09:49:06.534 警告 [http-nio-8080-exec-4] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /style.css 13-May-2020 09:49:06.536 警告 [http-nio-8080-exec-5] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /123.png
原因:DispatcherServlet的映射路径为"/",Tomcat服务器中默认的Servlet(servlet-name为default)的映射路径也为"/",所以DispatcherServlet会覆盖Tomcat中默认的Servlet,处理除jsp之外的所有资源,导致静态资源无法被访问
Tomcat服务器中默认的Servlet
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
解决方案
1.可以通过配置<servlet-mapping>来访问静态资源,但这不是最佳解决方案
<!--使".html"结尾的资源能够被访问--> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping>
2.在springMVC.xml配置文件中配置
<mvc:default-servlet-handler/> <mvc:resources mapping="/" location="/"/> <mvc:annotation-driven/>
分析这三个标签:
<mvc:default-servlet-handler/>标签会加载服务器默认的Servlet映射 配置该标签后会在SpringMVC上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,它会像一个检查员,对进入DispatcherServlet的URL进行筛查,如果发现是静态资源的请求,
就将该请求转由Web应用服务器默认的Servlet处理,如果不是静态资源的请求,才由DispatcherServlet继续处理。 如果Web应用服务器默认Servlet名称不是"default",则需要通过default-servlet-name属性指定 但是只配置该标签和RequestMapping注解会产生冲突,导致静态资源可以被访问,但是动态资源无法被访问 此时需要配置<mvc:annotation-driven/>来开启注解驱动,该标签功能强大,但在此处的作用只是使注解生效 对于<mvc:resources/>标签可以使任何地方的静态资源都被访问 在之前客户端只能访问webapp目录下的静态资源,而无法访问WEB-INF目录下的静态资源 通过mapping属性配置映射路径,"/"代表webapp目录,如果想要访问该目录下的所有子目录中的静态资源,可配置"/**",如果只想访问webapp目录下的WEB-INF目录,可配置"/WEB-INF/";location代表访问路径,"/"为最大范围。
所以,通常情况下只需要配置<mvc:default-servlet-handler/>标签和<mvc:annotation-driven/>标签即可
<mvc:default-servlet-handler/>
<mvc:annotation-driven/>
二、拦截器interceptor
跟常规一样,都是实现 HandlerInterceptor
public class LoginInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println(">>>>>>>>>>>>>>>>"+o.getClass().getName()); String uri = httpServletRequest.getRequestURI(); if(!(uri.contains("/index") || uri.contains("/login"))) { User user = (User) httpServletRequest.getSession().getAttribute("loginUser"); if (user == null) { httpServletResponse.sendRedirect("index"); return false; } } return true; } public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
上述代码主要实现了session验证拦截,如果除了访问index和login两个uri 之外都需要验证session。如果验证失败则redirect跳转到index当中。
同样的我们在SpringMVC配置上添加interceptor也是通过在配置类中复写WebMvcConfigurerAdapter的方法,这次我们是重写addInterceptors方法,将我们写的LoginInterceptor添加到registry当中。
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor()); }
三、addViewControllers方法
我们已经写了很多个没有任何业务的controller方法了,事实上我们可以在WebMvcConfigurerAdapter重写addViewControllers方法。
@Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName("/index"); registry.addViewController("/userinfo").setViewName("userinfo"); }
等价于
@Controller public class IndexController { @RequestMapping("/index") public String index(){ return "index"; }
四、全局配置
在我之前的关于SpringMVC的笔记当中写了很多关于controller类的全局配置,例如:
1、@ExceptionHandler 异常处理
2、@ModelAttribute 我都不知道怎么描述这个ModelAttribute?标记了@ModelAttribute的方法会为每一个属于同一个Controller类的方法添加添加属性
3、@InitBinder 这个我也不熟悉,我也是看书看到的。标记了@InitBinder的方法会在参数中注入一个WebDateBinder对象,详细可以看看这个类的API,(随后我还是看看WebDateBinder和DataBinder的源码发现内容还是很多的,里面有很多不少的功能,有时间的同学可以研究一下,并在评论分享出一些有用的方法~)
但是这次比较特别,我们通过@ControllerAdvice,为所有controller添加全局的配置。
@ControllerAdvice public class GlobalHandlerAdvice { //当前我选择拦截所有Exception @ExceptionHandler(value = Exception.class) public ModelAndView exception(Exception exception, WebRequest request){ //全局异常配置,如果有controller发生了异常。跳转到error 并添加errorMessage的VieModel属性 ModelAndView modelAndView = new ModelAndView("error"); modelAndView.addObject("errorMessage",exception.getMessage()); return modelAndView; } @ModelAttribute public void addAttributes(Model model){ //为全部controller的ViewModel添加msg参数 model.addAttribute("msg","other message from GlobalHandlerAdvice !!!"); } @InitBinder public void initBinder(WebDataBinder webDataBinder){ //拦截otherMsg的request参数,在任何controller方法中读取otherMsg参数将为null webDataBinder.setDisallowedFields("otherMsg"); } }
五、文件上传
说到文件上传,就是创建一个CommonsMultipartResolver。和我们以前配置XML一样,在配置类创建一个multipartResolver的bean。
@Bean public MultipartResolver multipartResolver(){ CommonsMultipartResolver resolver = new CommonsMultipartResolver(); resolver.setMaxUploadSize(10000000); return resolver; }
然后就没有什么好说的就是写个controller的方法接收multipartFile参数就可以了,还是贴一下代码吧···
@RequestMapping("/uploadAction") public ModelAndView uploadFile(@RequestParam(name = "file") MultipartFile file,HttpSession session) throws IOException, URISyntaxException { ModelAndView modelAndView = new ModelAndView(); if (file == null || file.isEmpty()) { String fileType = file.getContentType().toLowerCase(); if (!(fileType.contains("jpg") || fileType.contains("jpeg"))) { modelAndView.setViewName("redirect:upload"); } } else { //获得classPath String targetFilePath = ResourceUtils.getFile("classpath:assets/").getPath(); //获得项目路径 // String targetFilePath = session.getServletContext().getRealPath("/"); System.out.println(">>>>>>>"+targetFilePath); File targetFile = new File(targetFilePath+File.separator+file.getOriginalFilename()); FileCopyUtils.copy(file.getBytes(), targetFile); modelAndView.setViewName("redirect:assets/" + file.getOriginalFilename()); } return modelAndView; }
六、HttpMessageConvert
emm......,默认Spring已经将我们常用的HttpMessageConvert都加载了,但是在使用Spring4.X这种无配置文件的SpringMVC下 使用我之前笔记上的com.fasterxml.jackson.core 的2.1.4版本时,出现 java.lang.ClassNotFoundException: com.fasterxml.jackson.core.util.DefaultIndenter 的错误。这个错误是由于jackson的版本问题所导致的,所以我这里使用了Jackson的2.5.1版本
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.5.1</version> </dependency>
当然这个Json输入输出没有什么好说的,因为本来SpringMVC已经帮我们加载了非常多的Convert了。其实我也没有怎么在工作中定义convert,老说我也不是很记得怎么定义了。所以在回忆一下吧····
主要我们需要继承AbstractHttpMessageConverter 然后实现下面的方法,主要是三个方法
1、支持那些class的转换
2、输入转换(String->User)
3、输出转换 (User - > String)
注意:这里是inputstream就是说可以二进制流转换都可以,我是转成String再去转换而已。你完全可以通过序列化的方式去转换~
public class MyConvert extends AbstractHttpMessageConverter<User> { public MyConvert() { super(new MediaType("application","x-gamer")); } //检查是否支持目标转换,这里只支持User类型 protected boolean supports(Class<?> clazz) { return User.class.isAssignableFrom(clazz); } //转换成对象 protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { String inputString = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("utf-8")); String[] targetArray = inputString.split("#"); User user = new User(targetArray[0],targetArray[1]); return user; } //对象转成字符串 protected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { String outputString = user.getUsername() + "#" + user.getUserpwd(); outputMessage.getBody().write(outputString.getBytes()); } }
当然也和我们平时使用XML配置一样,也需要添加我们自定义的MessageConvert,在我们的配置类当中重写extendMessageConverters,并定义我们MyConvert的bean
@Bean public MyConvert myConvert(){ return new MyConvert(); } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(myConvert()); }
最后添加一个测试的Controller方法:
@RequestMapping(value = "/testConvert.gamer",produces = {"application/x-gamer"}) public @ResponseBody User testMessageConvert(@RequestBody User user){ System.out.println("user:"+user); return user; }
需要注意在测试的时候一定要将content-type改成你们定义的,我这里不要脸的定义成application/x-gamer,请求的http 头为:
POST /AnnotationSpringmvc/testConvert.gamer HTTP/1.1
Content-Type: application/x-gamer
Host: localhost:8080
Connection: close
User-Agent: Paw/3.0.12 (Macintosh; OS X/10.12.5) GCDHTTPRequest
Content-Length: 12
user#gamerpwd