Spring Web MVC知识点
官方文档
使用SpringMVC意味这你的项目是个Web项目!
导入依赖包
我使用maven创建项目,在已经导入spring相关的包的基础上,还需导入:
<!-- Spring Web MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0-b05</version>
</dependency>
tips:maven根据依赖情况还导入了speing-web这个jar包。
web.xml配置
实际上是为了配置dispatchServlet(前端控制器),他是SpringMVC中的一个类,直接配置上即可。
DispatchServlet会接收前端的所有请求,然后进行相应的操作。
处理字符串乱码
允许接收PUT、DELETE请求
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<!--web项目都有这个web.xml配置文件-->
<!--在这里我们可以配置springMVC自带的DispatcherServlet作为一个servlet-->
<!--同时可以初始化参数:MVC的配置文件路径-->
<!--最后针对该servlet做一个mapping, / 表示接受所有路径的请求-->
<!--配置dispatchServlet-->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--spring mvc默认配置文件:<servlet-name>-servlet.xml-->
<init-param>
<!--指定mvc配置文件-->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:mvc-config.xml</param-value>
</init-param>
<!--自动启动-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--/* 是过滤所有请。/是除了.jsp不过滤,其他都过滤-->
<!--解决POST方式的乱码-->
<filter>
<filter-name>CharacterEncodingFilter</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>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--支持GET、POST、PUT与DELETE请求 restful请求-->
<!--浏览器只支持post和get-->
<!--如何发出PUT或DELETE请求?-->
<!--使用post表单,在提交参数中加上 _method 字段,其值为PUT-->
<!--该过滤器接收到参数后会将该方法解析为PUT-->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
MVC配置文件
放在resources文件夹下
处理静态文件
看配置文件中配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--mvc配置文件-->
<!--开启对controller层的扫描,接受请求后,dispatchServlet就会去对应的包查看是否有对应的路径,然后执行相应的函数-->
<!--扫描controller层-->
<context:component-scan base-package="controller"/>
<!-- 视图解析器 -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--设置了视图路径的前后缀-->
<!--在tomcat启动后,WEB-INF就是当前项目下的文件夹,该路径是启动后的相对路径,而不是项目内的-->
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--开启mvc的注解驱动:视频讲解时,是为了处理静态资源-->
<mvc:annotation-driven/>
<!--处理静态资源-->
<!--静态资源存放在webapp目录下且不能放在WEB-INF中-->
<!--在处理 /static/**的请求时,不去找dispatchServlet,而是转去location下找对应资源-->
<!--/** 表示任意子目录的文件-->
<mvc:resources mapping="/static/**" location="/static/"/>
</beans>
建立Controller文件夹和Controller类
别忘了添加包扫描。
@Controller
@RequestMapping("/Hello")
public class HelloController {
@RequestMapping(value = "/World",method = RequestMethod.GET)
public String helloWorld(){
System.out.println("HelloWorld");
return "login";//跳转到/WEB-INF/jsp/login.jsp, 前缀和后缀是由配置文件配置得来的
}
@RequestMapping("/jump")
public String jumpToOtherMethod(){
return "redirect:World"; //redirect:表示跳转到方法,而不是jsp界面
}
@RequestMapping("/login")
public String login(String username, String password){//参数接受只要参数名符合即可(区分大小写)
//没传参会自动赋值为null,对于参数是基本类型:int i = null,显然会出错
System.out.println(username); //参数都写成包装类类型,不然会产生奇怪的错误
System.out.println(password);
return "helloWorld";
}
}
restful风格url
要在web.xml中添加过滤器HiddenHttpMethodFilter。
/**
* Restful风格url
* /user/1 get 获取id=1的user
* /user post 新增user
* /user/1 put 更新id=1的user
* /user/1 delete 删除id=1的user
*
* Post方法提交,在表单数据中添加 _method字段,其值为DELETE
*/
@RequestMapping(value = "/{id}",method = RequestMethod.DELETE)
public String delete(@PathVariable("id")Integer id){
System.out.println(id);
return "success";
}
<form action="/user/1" method="post">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">提交</button>
</form>
Servlet原生API
/**
* 原生API
* servlet提供的一些有用的接口
* 直接写在方法参数中,就可以使用
* 以下列举了常用的接口
*/
public String api(HttpSession session, HttpServletResponse response, HttpServletRequest request){
return "success";
}
如何接收普通参数
//参数接受只要参数名符合即可(区分大小写)
//没传参会自动赋值为null,对于参数是基本类型:int i = null,显然会出错
//参数都写成包装类类型,不然会产生奇怪的错误
@RequestMapping("/login")
public String login(String username, String password){
System.out.println(username);
System.out.println(password);
return "helloWorld";
}
解决参数名不一致
//使用@RequestParam注解,本例中接收前端参数名为cardId的参数到id中
//还可以设置默认值以及该参数是否必须
@RequestMapping("/selectUser3")
public String selectUser3(@RequestParam("cardId") Integer id){
System.out.println(id);
return "success";
}
如何接收模型参数
public class Person {
private String name;
private Integer age;
private Address address;
}
class Address {
private String province;
private String city;
}
/**
* 如何接受模型参数:如一个复合类
*
* 这里就不再使用表单发送post请求了,而是用postman发送请求
*
* 赋值流程:以本register方法为例
* TODO 还是要看源码才能掌握其执行流程
* 1、进入类寻找无参构造函数,若找到则实列化该类。若没有,则执行其他构造函数?(这里不清楚到底会执行哪一个)
* 2、使用set方法设置参数(按参数名匹配,不包括自定义类)
* 3、若有自定义类,则进入其内部,执行1
*
* 设参数名为:param,依赖对象为 inner
* 则:
* param表示是给主类的参数
* inner.param表示是给inner对象的参数
*
* 以下是都只有默认构造器时,前端界面传参的参数名
* name
* age
* address.province
* address.city
*/
@RequestMapping("/register")
public String register(Person person){
System.out.println(person);
return "success";
}
接收数组或集合
这种复杂数据类型用json传输更好
public class UserVO {
private List<User> users;
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}
@RequestMapping("/editUsers")
public String editUsers(UserVO userList){
List<User> users = userList.getUsers();
System.out.println(users);
return "success";
}
<table width="30%" border="1">
<tr>
<td>选择</td>
<td>用户名</td>
</tr>
<tr>
<td>
<input name="users[0].id" value="1" type="checkbox">
</td>
<td>
<input name="users[0].username" value="tome" type="text">
</td>
</tr>
<tr>
<td>
<input name="users[1].id" value="1" type="checkbox">
</td>
<td>
<input name="users[1].username" value="tome" type="text">
</td>
</tr>
<tr>
<td>
<input name="users[2].id" value="1" type="checkbox">
</td>
<td>
<input name="users[2].username" value="tome" type="text">
</td>
</tr>
</table>
如何接收json参数
在参数前加上@RequestBody注解。必须得有无参的构造函数,不然会报400。
/**
* 接收JSON类型数据
*/
@RequestMapping("/receive-json")
@ResponseBody
public void reJson(@RequestBody Address address){
System.out.println(address);
}
枚举类型转json
现有一枚举类
public enum SexEnum {
MALE("男",1),FEMALE("女",2);
private String msg;
private int code;
SexEnum(String msg, int code){
this.msg = msg;
this.code = code;
}
//setter getter toString
}
将其转化为JSON(fastJSON)
System.out.println(JSON.toJSONString(SexEnum.MALE));
得到结果为:
"MALE"
可能想象中的为:
{
"code":1,
"msg":"男"
}
但事实不是这样,而且“MALE”是可以转回原来的枚举类型的
JSON.parseObject("\"MALE\"", SexEnum.class)
所以后端某方法参数为(@RequestBody SexEnum sex)
则前端传值为“MALE”或“FEMALE”
若SexEnum为Person类的属性:sex,后端某方法参数为(@RequesBody Person p)
则前端传来的json为:
{
"id":2,
"note":"张三是个好同志",
"userName":"张三",
"sex":"MALE"
}
如何向页面传值
/**
* controller向前端页面传递数据
* 1、ModelAndView
* 2、Map
* 3、Model
* jsp使用el表达式读取数据
*/
@RequestMapping("/info")
public ModelAndView getInfo(){
Person person = new Person("吴家俊", 18, "湖北", "潜江");
ModelAndView mv = new ModelAndView();
mv.addObject("person",person);
mv.setViewName("user/info"); //设置返回视图
return mv;
}
@RequestMapping("/info1")
public String getInfo1(Map<String,Object> map){
Person person = new Person("吴家俊", 18, "湖北", "潜江");
map.put("person",person);
return "user/info";
}
@RequestMapping("/info2")
public String getInfo2(Model model){
Person person = new Person("吴家俊", 18, "湖北", "潜江");
model.addAttribute("person",person);
return "user/info";
}
如何跳转到方法/页面
@RequestMapping(value = "/World",method = RequestMethod.GET)
public String helloWorld(){
System.out.println("HelloWorld");
return "login";//跳转到/WEB-INF/jsp/login.jsp, 前缀和后缀是由配置文件配置得来的
}
@RequestMapping("/jump")
public String jumpToOtherMethod(){
return "redirect:World"; //redirect:表示跳转到方法,而不是jsp界面
}
SpringMVC运行流程
DispatchServlet默认配置文件
路径:spring-web-mvc/springframework/web/servlet/DispatchServlet.properties
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
可以在springmvc的配置文件中,配置相应的bean来替换默认的配置。
如何接受日期类型参数
有以下四种做法(推荐使用第三种):
- 根据SpringMVC模型参数赋值流程,可以做如下编码:
public class BigDate {
private Date date;
//控制器接收参数后,会调用该set方法
public void setDate(String date) throws ParseException {
this.date = (new SimpleDateFormat("yyyy-MM-dd")).parse(date);
}
}
- 使用注解@DateTimeFormat(pattern = “…”)
public class BigDate {
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date date;
public void setDate(Date date) {
this.date = date;
}
public Date getDate(){
return date;
}
}
- 将日期类型以字符串形式存储。减少不必要的麻烦。
public class BigDate {
private String date;
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
}
- 自定义类型转换器。
自定义类型转换器
首先新建自定义转换类
public class AddressConverter implements Converter<String, Address> {
@Override
public Address convert(String s) {//传入 "湖北-潜江"
String[] split = s.split("-");
if (split.length==2){
return new Address(split[0],split[1]);
}
return null;
}
}
然后在springMVC配置文件中配置相应的bean
<!--conversion-service属性是为了配置自定义类型转换器-->
<mvc:annotation-driven conversion-service="MyConverter"/>
<!--配置自定义转换器-->
<bean id="MyConverter" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<!--这里可以添加多个转换器-->
<bean class="converter.AddressConverter"/>
</set>
</property>
</bean>
这样配置的转换器会在接收参数时自动调用。
/**
* 自定义请求转换器
* String -> Address
*/
@RequestMapping("/address")
public String getDate(Address address){
System.out.println(address);
return "success";
}
前端请求:“湖北-潜江”,会被转换成Address对象。
这样配置,难以让人了解完整流程。因为转换器配置在xml文件,你不会想在看方法前,先浏览一遍xml文件。其实可以手动将“转换”这一操作写在代码逻辑中。如下:
@RequestMapping("/address")
public String getDate(String address){
System.out.println(new AddressConverter().convert(address));
return "success";
}
这样流程更加清晰。他人在浏览你代码时,会很清楚的知道你接收了字符串类型,并且使用转换器将其变为Address类型。
返回JSON类型的数据
导入对应jar包,有好几种都可以实现转换功能(谷歌出的、阿里出的)。
<!--返回json数据时所需要的转换包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
在需要返回json数据的方法上加上@ResponseBody
@RequestMapping("/json")
@ResponseBody
public Address getJson(){
return new Address("湖北","潜江");
}
异常处理
- 可以定义作用于某个Controller的处理器。方法写在Controller类里。
/**
* 配置异常处理器
* 该处理器只处理本Controller的异常
*/
@ExceptionHandler
public String handler(Exception ex){
return "fail";
}
/**
*只处理特定的异常
*/
@ExceptionHandler(ArrayIndexOutOfBoundsException.class)
public String handler2(Exception ex){
return "fail";
}
- 定义作用于全局的异常处理器。
@ControllerAdvice
public class ExHandler {
@ExceptionHandler(ArrayIndexOutOfBoundsException.class)
public String handler2(Exception ex){
return "fail";
}
}
还要在SpringMVC配置文件中添加注解扫描,扫描到ExHandler。
<context:component-scan base-package="handler"/>
自定义拦截器
创建拦截器。
静态资源和内部路径跳转不会通过拦截器。
public class MyHandler implements HandlerInterceptor {
//在执行handler(controller)前调用,用于认证
//返回true表示放行
//返回false表示拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("pre");
return true;
}
/**
*在执行handler时,返回ModelAndView前,执行
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("post");
}
/**
* 在执行完handler之后执行,用于日志和性能分析
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("after");
}
}
在SpringMVC配置文件中配置拦截器。
<!--配置拦截器,多个拦截器顺序执行-->
<mvc:interceptors>
<!--全局拦截器-->
<bean class="com.interceptor.interceptor1"/>
<!--局部拦截器-->
<mvc:interceptor>
<!--拦截器处理的路径-->
<mvc:mapping path="/**"/>
<!--放行指定的目录,拦截器不会进行处理。这里将静态资源排除-->
<mvc:exclude-mapping path="/static/**"/>
<bean class="interceptor.MyHandler"/>
</mvc:interceptor>
</mvc:interceptors>
上传文件
- 导入jar包。
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
- 在SpringMVC配置文件中配置文件解析器。
<!--文件上传配置-->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="utf-8"/>
<property name="maxUploadSize" value="-1"/><!--最大文件大小,单位是字节,-1表示无限制-->
<property name="maxInMemorySize" value="10240"/><!--最大内存大小,单位是字节-->
<property name="uploadTempDir" value="/upload/"/> <!--文件临时目录-->
</bean>
- 编写文件上传表单。
<form action="/user/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" multiple="multiple">
<input type="text" name="desc">
<button type="submit">submit</button>
</form>
- 编写方法。
@RequestMapping(value = "/upload",method = RequestMethod.POST)
public String upload(MultipartFile file, String desc) throws IOException {
System.out.println(desc);
if (file == null || StringUtils.isEmpty(file.getName())){
throw new IOException("没有上传实际文件或者文件名被删除");
}
//指定存放上传文件的目录
String fileDir = "C:\\ftpfile\\img3";
File dir = new File(fileDir);
//判断目录是否存在,不存在则创建目录
if (!dir.exists()) {
dir.mkdirs();
}
//生成新文件名,防止文件名重复而导致文件覆盖
//1、获取原文件后缀名 .img .jpg ....
String originalFileName = file.getOriginalFilename();
String suffix = originalFileName.substring(originalFileName.lastIndexOf('.'));
//2、使用UUID生成新文件名
String newFileName = UUID.randomUUID() + suffix;
//生成文件
//这个时候文件就已经生成,但是并没有内容
File newFile = new File(dir, newFileName);
//传输文件内容
file.transferTo(newFile);
return "success";
}
使用tomcat可以配置虚拟路径,将项目的某个路径指向系统文件夹。就没有必要使用nginx重定向。许多使用nginx的功能都可以使用tomcat实现。具体见tomcat详解。
文件下载
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<a href="/download?filename=wjj_a7736a77-f411-4ce7-9b80-109ef7c3571c_0ca4bd73-c6b5-4b97-bb89-81a48e2baf50.jpg">点击下载文件</a>
@RequestMapping("/download")
public ResponseEntity<byte[]> fileDownload(HttpServletRequest request,
String filename) throws IOException {
String path = request.getServletContext().getRealPath("/upload");
File file = new File(path + File.separator +filename);
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment",filename);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),headers,HttpStatus.OK);
}