概述
Spring基于MVC设计模式的轻量级web框架。
与Spring关系
SpringMVC是基于Spring的开源项目。以web项目为例:
servletContext容器是web项目根本容器。
Spring容器 会通过ServletContextListener监听类与context-param配置的xml文件创建。
SpringMVC容器 则通过 DispatcherServlet创建,并将Spring容器设为Parent。
- 为什么不用spring容器去扫描所有的Bean?
不可能,请求达到服务端后,找 DispatcherServlet 去处理,只会去 SpringMVC 容器中找,这就意味着 Controller 必须在 SpringMVC 容器中扫描。
- 为什么不在 SpringMVC 容器中扫描所有 Bean?
这个是可行的,但是为了配置文件的管理,以及在 Spring+SpringMVC+Hibernate 组合中,实际上也不支持这种写法。
工作流程及组件
- DispatcherServlet:前端控制器
是整个流程控制的中心,相当于是 SpringMVC 的大脑,由它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。
- HandlerMapping:处理器映射器
负责根据用户请求找到 Handler 即处理器(也就是我们所说的 Controller)。
- Handler:处理器
前端控制器的后端控制器,在DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。(这里所说的 Handler 就是指我们的 Controller)
4.HandlAdapter:处理器适配器
通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。
5.ViewResolver:视图解析器
ViewResolver 负责将处理结果生成 View 视图,ViewResolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。
1 2 3 4 5 6 7
| @Controller public class MyController3 { @RequestMapping("/hello3") public ModelAndView hello() { return new ModelAndView("hello3"); } }
|
要能够访问到这个接口,我们需要 RequestMappingHandlerMapping 才能定位到需要执行的方法,需要 RequestMappingHandlerAdapter,所以配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.javaboy.helloworld"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" id="handlerMapping"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" id="handlerAdapter"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver"> <property name="prefix" value="/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
|
当然我们还有简化写法:
@RequestMapping
这个注解用来标记一个接口, 表示我们用的是 RequestMappingHandlerMapping 这个处理器映射器。
请求url
标记请求 URL 很简单,只需要在相应的方法上添加该注解即可:
1 2 3 4 5 6 7
| @Controller public class HelloController { @RequestMapping("/hello") public ModelAndView hello() { return new ModelAndView("hello"); } }
|
当请求地址为 /hello 的时候,这个方法会被触发。其中,地址可以是多个,就是可以多个地址映射到同一个方法。
1 2 3 4 5 6 7
| @Controller public class HelloController { @RequestMapping({"/hello","/hello2"}) public ModelAndView hello() { return new ModelAndView("hello"); } }
|
/hello 和 /hello2 都可以访问到该方法。
请求窄化
同一个项目中,会存在多个接口,例如订单相关的接口都是 /order/xxx 格式的,用户相关的接口都是 /user/xxx 格式的。为了方便处理,这里的前缀(就是 /order、/user)可以统一在 Controller 上面处理。
1 2 3 4 5 6 7 8
| @Controller @RequestMapping("/user") public class HelloController { @RequestMapping({"/hello","/hello2"}) public ModelAndView hello() { return new ModelAndView("hello"); } }
|
当类上加了 @RequestMapping 注解之后,此时,要想访问到 hello ,地址就应该是 /user/hello 或者 /user/hello2。
请求方法限定
使用 @RequestMapping 注解定义好的方法,可以被 GET 请求访问到,也可以被 POST 请求访问到,但是 DELETE 请求以及 PUT 请求不可以访问到。
当然,我们也可以指定具体的访问方法:
1 2 3 4 5 6 7 8
| @Controller @RequestMapping("/user") public class HelloController { @RequestMapping(value = "/hello",method = RequestMethod.GET) public ModelAndView hello() { return new ModelAndView("hello"); } }
|
通过 @RequestMapping 注解,指定了该接口只能被 GET 请求访问到,此时,该接口就不可以被 POST 以及请求请求访问到了。
当然,限定的方法也可以有多个:
1 2 3 4 5 6 7 8
| @Controller @RequestMapping("/user") public class HelloController { @RequestMapping(value = "/hello",method = {RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT,RequestMethod.DELETE}) public ModelAndView hello() { return new ModelAndView("hello"); } }
|
此时,这个接口就可以被 GET、POST、PUT、以及 DELETE 访问到了。
视图解析器
配置:
1 2 3 4 5
| <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>
|
对于/hello请求,DispatcherServlet会将请求转发到前缀+ view-name + suffix = /WEB-INF/jsp/hello.jsp。
Controller 方法的返回值
返回 ModelAndView
如果是前后端不分的开发,大部分情况下,我们返回 ModelAndView,即数据模型+视图:
1 2 3 4 5 6 7 8 9 10
| @Controller @RequestMapping("/user") public class HelloController { @RequestMapping("/hello") public ModelAndView hello() { ModelAndView mv = new ModelAndView("hello"); mv.addObject("username", "javaboy"); return mv; } }
|
Model 中,放我们的数据,然后在 ModelAndView 中指定视图名称。
返回 Void
没有返回值。没有返回值,并不一定真的没有返回值,只是方法的返回值为 void,我们可以通过其他方式给前端返回。实际上,这种方式也可以理解为 Servlet 中的那一套方案。
注意,由于默认的 Maven 项目没有 Servlet,因此这里需要额外添加一个依赖:
1 2 3 4 5
| <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency>
|
服务端跳转:
1 2 3 4
| @RequestMapping("/hello2") public void hello2(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("/jsp/hello.jsp").forward(req,resp); }
|
重定向:
1 2 3 4
| @RequestMapping("/hello3") public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.sendRedirect("/hello.jsp"); }
|
返回字符串
返回逻辑视图
1 2 3 4 5
| @RequestMapping("/hello5") public String hello5(Model model) { model.addAttribute("username", "javaboy"); return "hello"; }
|
服务端跳转
1 2 3 4
| @RequestMapping("/hello5") public String hello5() { return "forward:/jsp/hello.jsp"; }
|
客户端跳转
1 2 3 4
| @RequestMapping("/hello5") public String hello5() { return "redirect:/user/hello"; }
|
真的返回字符串
1 2 3 4 5
| @RequestMapping("/hello5") @ResponseBody public String hello5() { return "redirect:/user/hello"; }
|
上面代码表示就是想返回一段内容为 redirect:/user/hello 的字符串,他没有特殊含义。注意,这里如果单纯的返回一个中文字符串,是会乱码的,可以在 @RequestMapping 中添加 produces 属性来解决:
1 2 3 4 5
| @RequestMapping(value = "/hello5",produces = "text/html;charset=utf-8") @ResponseBody public String hello5() { return "Java 语言程序设计"; }
|
参数绑定
默认支持的参数类型
默认支持的参数类型,就是可以直接写在 @RequestMapping 所注解的方法中的参数类型,一共有四类:
- HttpServletRequest
- HttpServletResponse
- HttpSession
- Model/ModelMap
简单数据类型
Integer、Boolean、Double 等等简单数据类型也都是支持的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Controller public class BookController { @RequestMapping("/book") public String addBook() { return "addbook"; }
@RequestMapping(value = "/doAdd",method = RequestMethod.POST) @ResponseBody public void doAdd(String name,String author,Double price,Boolean ispublic) { System.out.println(name); System.out.println(author); System.out.println(price); System.out.println(ispublic); } }
|
POST 请求传上来的中文会乱码,所以,我们在 web.xml 中再额外添加一个编码过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <filter> <filter-name>encoding</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>forceRequestEncoding</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>forceResponseEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
|
在上面的绑定中,有一个要求,表单中字段的 name 属性要和接口中的变量名一一对应,才能映射成功,否则服务端接收不到前端传来的数据。
如果前后端不一致,这个时候我们可以通过 @RequestParam 注解来解决。
这个注解的的功能主要有三方面:
1 2 3 4 5 6 7 8
| @RequestMapping(value = "/doAdd",method = RequestMethod.POST) @ResponseBody public void doAdd(@RequestParam("name") String bookname, String author, Double price, Boolean ispublic) { System.out.println(bookname); System.out.println(author); System.out.println(price); System.out.println(ispublic); }
|
注解中的 “name” 表示给 bookname 这个变量取的别名,也就是说,bookname 将接收前端传来的 name 这个变量的值。在这个注解中,还可以添加 required 属性和 defaultValue 属性,如下:
1 2 3 4 5 6 7 8
| @RequestMapping(value = "/doAdd",method = RequestMethod.POST) @ResponseBody public void doAdd(@RequestParam(value = "name",required = true,defaultValue = "三国演义") String bookname, String author, Double price, Boolean ispublic) { System.out.println(bookname); System.out.println(author); System.out.println(price); System.out.println(ispublic); }
|
required 属性默认为 true,即只要添加了 @RequestParam 注解,这个参数默认就是必填的,如果不填,请求无法提交,会报 400 错误,如果这个参数不是必填项,可以手动把 required 属性设置为 false。但是,如果同时设置了 defaultValue,这个时候,前端不传该参数到后端,即使 required 属性为 true,它也不会报错。
实体类
参数除了是简单数据类型之外,也可以是实体类。实际上,在开发中,大部分情况下,都是实体类。
1 2 3 4 5
| @RequestMapping(value = "/doAdd",method = RequestMethod.POST) @ResponseBody public void doAdd(Book book) { System.out.println(book); }
|
前端页面传值的时候和上面的一样,只需要写属性名就可以了,不需要写 book 对象名。
自定义参数转换
特殊的数据类型,系统无法自动转换,例如日期。例如前端传一个日期到后端,后端不是用字符串接收,而是使用一个 Date 对象接收,这个时候就会出现参数类型转换失败。这个时候,需要我们手动定义参数类型转换器,将日期字符串手动转为一个 Date 对象。
1 2 3 4 5 6 7 8 9 10 11 12
| @Component public class DateConverter implements Converter<String, Date> { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); public Date convert(String source) { try { return sdf.parse(source); } catch (ParseException e) { e.printStackTrace(); } return null; } }
|
在自定义的参数类型转换器中,将一个 String 转为 Date 对象,同时,将这个转换器注册为一个 Bean。
1 2 3 4 5 6 7 8
| <mvc:annotation-driven conversion-service="conversionService"/> <bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="conversionService"> <property name="converters"> <set> <ref bean="dateConverter"/> </set> </property> </bean>
|
配置完成后,在服务端就可以接收前端传来的日期参数了。
集合类的参数
String数组
String 数组可以直接用数组去接收,前端传递的时候,数组的传递其实就多相同的 key,这种一般用在 checkbox 中较多。
前端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <form action="/doAdd" method="post"> <table> <tr> <td>书名:</td> <td><input type="text" name="name"></td> </tr> <tr> <td>作者姓名:</td> <td><input type="text" name="author.name"></td> </tr> <tr> <td>作者年龄:</td> <td><input type="text" name="author.age"></td> </tr> <tr> <td>出生日期:</td> <td><input type="date" name="author.birthday"></td> </tr> <tr> <td>兴趣爱好:</td> <td> <input type="checkbox" name="favorites" value="足球">足球 <input type="checkbox" name="favorites" value="篮球">篮球 <input type="checkbox" name="favorites" value="乒乓球">乒乓球 </td> </tr> <tr> <td>价格:</td> <td><input type="text" name="price"></td> </tr> <tr> <td>是否上架:</td> <td> <input type="radio" value="true" name="ispublic">是 <input type="radio" value="false" name="ispublic">否 </td> </tr> <tr> <td colspan="2"> <input type="submit" value="添加"> </td> </tr> </table> </form>
|
1 2 3 4 5 6
| @RequestMapping(value = "/doAdd",method = RequestMethod.POST) @ResponseBody public void doAdd(Book book,String[] favorites) { System.out.println(Arrays.toString(favorites)); System.out.println(book); }
|
注意,前端传来的数组对象,服务端不可以使用 List 集合去接收。
list集合
如果需要使用 List 集合接收前端传来的数据,List 集合本身需要放在一个封装对象中,这个时候,List 中,可以是基本数据类型,也可以是对象。例如有一个班级类,班级里边有学生,学生有多个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public class MyClass { private Integer id; private List<Student> students;
@Override public String toString() { return "MyClass{" + "id=" + id + ", students=" + students + '}'; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public List<Student> getStudents() { return students; }
public void setStudents(List<Student> students) { this.students = students; } } public class Student { private Integer id; private String name;
@Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + '}'; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
|
添加班级的时候,可以传递多个 Student,前端页面写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <form action="/addclass" method="post"> <table> <tr> <td>班级编号:</td> <td><input type="text" name="id"></td> </tr> <tr> <td>学生编号:</td> <td><input type="text" name="students[0].id"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="students[0].name"></td> </tr> <tr> <td>学生编号:</td> <td><input type="text" name="students[1].id"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="students[1].name"></td> </tr> <tr> <td colspan="2"> <input type="submit" value="提交"> </td> </tr> </table> </form>
|
1 2 3 4 5
| @RequestMapping("/addclass") @ResponseBody public void addClass(MyClass myClass) { System.out.println(myClass); }
|
Map
相对于实体类而言,Map 是一种比较灵活的方案,但是,Map 可维护性比较差,因此一般不推荐使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class MyClass { private Integer id; private List<Student> students; private Map<String, Object> info;
@Override public String toString() { return "MyClass{" + "id=" + id + ", students=" + students + ", info=" + info + '}'; }
public Map<String, Object> getInfo() { return info; }
public void setInfo(Map<String, Object> info) { this.info = info; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public List<Student> getStudents() { return students; }
public void setStudents(List<Student> students) { this.students = students; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <form action="/addclass" method="post"> <table> <tr> <td>班级编号:</td> <td><input type="text" name="id"></td> </tr> <tr> <td>班级名称:</td> <td><input type="text" name="info['name']"></td> </tr> <tr> <td>班级位置:</td> <td><input type="text" name="info['pos']"></td> </tr> <tr> <td>学生编号:</td> <td><input type="text" name="students[0].id"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="students[0].name"></td> </tr> <tr> <td>学生编号:</td> <td><input type="text" name="students[1].id"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="students[1].name"></td> </tr> <tr> <td colspan="2"> <input type="submit" value="提交"> </td> </tr> </table> </form>
|
服务端请求参数校验
普通校验
首先,我们需要加入校验需要的依赖:
1 2 3 4 5
| <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.0.Final</version> </dependency>
|
1 2 3 4
| <bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> </bean> <mvc:annotation-driven validator="validatorFactoryBean"/>
|
假设学生编号不能为空,学生姓名长度不能超过 10 且不能为空,邮箱地址要合法,年龄不能超过 150。那么在定义实体类的时候,就可以加入这个判断条件了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| public class Student { @NotNull private Integer id; @NotNull @Size(min = 2,max = 10) private String name; @Email private String email; @Max(150) private Integer age;
public String getEmail() { return email; }
@Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + ", age=" + age + '}'; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
|
定义完成后,接下来,在 Controller 中定义接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller public class StudentController { @RequestMapping("/addstudent") @ResponseBody public void addStudent(@Validated Student student, BindingResult result) { if (result != null) { List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage()); } } } }
|
在这里:
- @Validated 表示 Student 中定义的校验规则将会生效
- BindingResult 表示出错信息,如果这个变量不为空,表示有错误,否则校验通过。
分组校验
由于校验规则都是定义在实体类上面的,但是,在不同的数据提交环境下,校验规则可能不一样。例如,用户的 id 是自增长的,添加的时候,可以不用传递用户 id,但是修改的时候则必须传递用户 id,这种情况下,就需要使用分组校验。
分组校验,首先需要定义校验组,所谓的校验组,其实就是空接口:
1 2 3 4
| public interface ValidationGroup1 { } public interface ValidationGroup2 { }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| public class Student { @NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class) private Integer id; @NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class}) @Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class}) private String name; @Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class}) private String email; @Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class}) private Integer age;
public String getEmail() { return email; }
@Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + ", age=" + age + '}'; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
|
在 group 中指定每一个校验规则所属的组,一个规则可以属于一个组,也可以属于多个组。
最后,在接收参数的地方,指定校验组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller public class StudentController { @RequestMapping("/addstudent") @ResponseBody public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null) { List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage()); } } } }
|
配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。
校验注解
校验注解,主要有如下几种:
- @Null 被注解的元素必须为 null
- @NotNull 被注解的元素必须不为 null
- @AssertTrue 被注解的元素必须为 true
- @AssertFalse 被注解的元素必须为 false
- @Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max=, min=) 被注解的元素的大小必须在指定的范围内
- @Digits (integer, fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
- @Past 被注解的元素必须是一个过去的日期
- @Future 被注解的元素必须是一个将来的日期
- @Pattern(regex=,flag=) 被注解的元素必须符合指定的正则表达式
- @NotBlank(message =) 验证字符串非 null,且长度必须大于0
- @Email 被注解的元素必须是电子邮箱地址
- @Length(min=,max=) 被注解的字符串的大小必须在指定的范围内
- @NotEmpty 被注解的字符串的必须非空
- @Range(min=,max=,message=) 被注解的元素必须在合适的范围内