文章目录
SpringMVC
1.简介与前言
前言:
该篇笔记是我在学习SpringMVC时所做的笔记。其目的是方便我查阅忘记的部分。所以是以应用为主的角度来进行书写的。并且由于是学习JavaWeb初期时的笔记所以理解可能会有问题。大家可以简单的做一个参考。虽然文中的代码都是亲自试过的。但是请务必独立思考,最好带着审视的目光来看。如果有错误,望指点一二。
简介
SpringMVC和Sturts2同为作为前端控制层的框架。但是SpirngMVC的优势却远大于Struts2.我认为最主要的优势有两点。第一就是解耦。可以灵活的选用适配器映射器等。第二就是方便集成Spring。
从学习的过程中能明显感受到SpringMVC的优越性。不过SpringMVC毕竟是后来出的。在前人的基础上做大量的优化是肯定的。
2.工作原理
1.客户端向前端控制器DispatcherServlet发送请求
2.前端控制器将请求发送给url映射器
3.由url映射器根据名字找到具体的控制器controller(不同的映射器有不同的匹配规则,后期一般用自动装配+注解的方式)
4.再由映射器将找到的控制器返还给前端控制器
5.前端控制器将找到的控制器交给适配器由适配器负责执行
6.适配器执行后将ModelAndView返还给前端控制器
7.前端控制器再将ModelAndView交给视图解析器
8.最后由视图解析器解析出目标页面
ModelAndView
里面可以封装两个信息,一个是视图信息,一个是模型信息
/*
目前为止,每一个控制器方法的返回值都是ModelAndView
ModelAndView顾名思义,其中封装了 模型 和 逻辑视图名
下面演示一下如何封装模型:
*/
@RequestMapping("save")
public ModelAndView save() {
// 重定向
// ModelAndView mav = new ModelAndView("redirect:/i.jsp");
// 转发
ModelAndView mav = new ModelAndView("i");
mav.addObject("x", "东方不败");
User user = new User();
user.setId(12);
user.setName("天山童姥");
user.setBirthday(new Date());
user.setMoney(330d);
// 在reuqest范围中,添加一个键值对,键值是“智能”生成的,值是user对象
// 键值默认就是类名首字母小写: user
mav.addObject(user);
// 在reuqest范围中,添加一个键值对,键值是user2,值是user对象
mav.addObject("user2", user);
mav.addObject("飞雪连天射白鹿");
List<User> list = new ArrayList<>();
list.add(new User(11,"韦小宝", new Date(), 1000d));
list.add(new User(12,"令狐冲", new Date(), 2000d));
list.add(new User(13,"张无忌", new Date(), 3000d));
// 在reuqest范围中,添加一个键值对,键值是userList,值是list对象,集合默认的是泛型+list
mav.addObject(list);
return mav;
}
/*
控制器的方法,返回类型除了可以是ModelAndView以外,也可以是String类型
此时String返回值的内容,表示的是“逻辑视图名”
此时,如果还想给跳转的目标页面传递参数,就要为控制器添加一个Model类型的参数
但凡要给页面传递什么参数,都是通过model的addAttribute方法实现的
*/
@RequestMapping("save2")
public String save2(Model model) {
System.out.println("UserController.save2()");
model.addAttribute("aa", "笑书神侠倚碧鸳");
model.addAttribute(new Date());
return "i";
}
/*
默认的跳转方式是转发,那么如何才能重定向呢?
注意,重定向时,前后缀必须自己手动添加
*/
@RequestMapping("save3")
public String save3() {
System.out.println("UserController.save3()");
return "redirect:/i.jsp";
}
3.配置文件
<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:com/woniuxy/o_rest/spring-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
4.注解配置controller
4.1Controller传入的参数值
4.2Controller获得三个作用域
SpringMVC的控制器方法中,如何获取web元素, 只能获取后面的这3个web元素:request,session,response
@RequestMapping("save4")
public String save4(HttpServletRequest request, HttpSession session, HttpServletResponse response) throws IOException {
// 获取发起当前请求的客户端的ip地址!
String ip = request.getRemoteAddr();
System.out.println("客户端ip地址:" + ip);
request.setAttribute("abc", "好好学习,天天向上,别浪!" + ip);
session.setAttribute("xyz", "日积月累,是一件很恐怖的事情!");
request.getServletContext().setAttribute("aaa", "哈哈哈");
// response.sendRedirect("http://www.163.com");
return "i";
4.3文件上传
首先需要导入jar包commons-fileupload
<!--
文件上传解析器
注意,必须给该文件上传解析器,定义id,
且该id的名字必须是multipartResolver!!!
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 限制文件上传的大小,单位是字节 -->
<property name="maxUploadSize" value="5242880"></property>
</bean>
然后在controller中添加
// springmvc在制作文件上传时,必须导入以下依赖:
// fileupload.
// 该方法,名字叫做upload,但是该方法并不会做“文件上传”的工作,
// 而是把已经上传到服务器内存中的图片的2进制,进行存盘!!
// 必须使用CommonsMultipartFile来接受客户端传来的图片的信息(名字和2进制),且在该参数前也必须加@RequestParam
// 还必须在spring-servlet中,配置一个文件上传解析器!!
@RequestMapping("upload")
public String upload(@RequestParam CommonsMultipartFile photo, HttpServletRequest request) throws IllegalStateException, IOException {
// 有3个地方不能写死:
// 1.后缀
String oldName = photo.getOriginalFilename();
int lastDot = oldName.lastIndexOf(".");
String ext = oldName.substring(lastDot);
// 2. 文件名
String newName = UUID.randomUUID().toString().replaceAll("-", "") + ext;
// 3.上传路径不能写死, 获取web应用在服务器中所在的真实目录!
String path = request.getServletContext().getRealPath("/images");
System.out.println("path:" + path);
// 存盘
photo.transferTo(new File(path, newName));
return "k";
}
5.@RequestPararm与类型转换器
控制器方法中,并不是所有参数都能直接接受请求参数的,比如下面的List类型的参数,
默认无法接收请求参数,为了让List也能接请求参数,需要在List前面加上一个注解:@RequestParam
Map类型的参数,与List和Set类型的参数一样,如果不在前面加@RequestParam,则map也无法接受请求参数!
不一样的是,Map类型的参数,在前面加上了@RequestParam以后,参数的名字可以与请求参数中的名字不一致!
需要继承Converter然后在泛型中添加需要转换的数据,然后框架会根据实体类与穿回来的数据自动转换
public class MyIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
System.out.println("原始数据:" + source);
int i = Integer.parseInt(source);
return i*10;
}
}
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.woniuxy.f_parameter.MyDateConverter"></bean>
<bean class="com.woniuxy.f_parameter.MyIntegerConverter"></bean>
</set>
</property>
</bean>
6.@RequestBody
0.http请求由几部分组成?
a. 请求行
b. 请求头
c. 请求体
-
http协议中的请求头:
content-type,客户端告诉服务器,我客户端给你发送的是什么格式的数据。
accept,客户端告诉服务器,我客户端想要什么格式的数据。 -
客户端要什么格式的数据,服务器未必就会给什么格式的数据,因为这只是一个http规范而已,
服务器最终返回什么格式的数据,取决于你所编写的代码。 -
我们都知道,springmvc框架是如何为控制器的参数注入值的:按照名称注入值(也就是拿着请求参数的名字,与目标方法的参数名匹配)。
-
当我们在控制器的方法参数上添加了@RequestBody注解,springmvc框架就不再按照名称自动注入参数的值,
而是使用HttpMessageConverter来为参数注入值。 -
SpringMVC中HttpMessageConverter的工作原理:
a. 客户端发起请求,并携带请求参数
b. SpringMVC根据映射器找到对应的handler,再把handler交给适配器
c. 适配器检测方法的参数上是否有@RequestBody
如果没有,则按照名称注入参数的值,然后流程就直接结束了。
如果有,则使用HttpMessageConverter注入参数的值,进入第4步
d. SpringMVC框架读取出请求头中的content-type的值,比如为text/html(也就是客户端告诉服务器我给你发的是html格式的数据)
SpringMVC根据获取到的content-type的值,去找出所有与此content-type的值匹配的HttpMessageConverter对象。如果一个都找不到,则抛出异常:HttpMediaTypeNotSupportedException 如果找到一个或一个以上,则进入第5步 (多个HttpMessageConveter的顺序就是配置的顺序)
e. SpringMVC拿着已经找到的一个或多个HttpMessageConverter对象,一一调用其canRead方法(canRead方法又回调了supports方法),
canRead方法中判断目标参数的类型是否归当前HttpMessageConverter管理:如果不是,则继续判断下一个; 如果是,则就确定了这唯一的一个HttpMessageConverter对象,然后进入第6步;(第一匹配者优先) 如果最终一个都找不到,则抛出异常: HttpMediaTypeNotSupportedException
f. SpringMVC调用HttpMessageConverter的read方法: StringHttpMessageConverter
// 参数clazz是要返回的对象的类型,也就是目标参数的类型
// 参数inputMessage中封装了请求参数,可以通过该inputMessage获取到请求中的参数信息
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOExceptiong. HttpMessageConverter的read方法执行完毕,把返回的对象注入给目标参数。
完成!h. 自定义HttpMessageConverter, 需要继承AbstractHttpMessageConverter,并实现相关方法。
然后需要在spring配置文件中,添加以下配置:<mvc:message-converters register-defaults="true"> <bean class="com.gao.g_httpmessageconverter.MyHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/user</value> </list> </property> </bean> </mvc:message-converters>
其中register-defaults用于告诉springmvc框架是否加载默认的HttpMessageConverter(包括StringHttpMessageConverter)
i. 可以查看AnnotationDrivenBeanDefinitionParser源码来分析HttpMessageConveter的底层原理。
j. 通过阅读AnnotationDrivenBeanDefinitionParser源码,我们得知,如果register-defaults的取值为ture,且当前项目的classpath中有
com.fasterxml.jackson.databind.ObjectMapper
com.fasterxml.jackson.core.JsonGenerator
这两个类的话,springmvc就会自动加载用于处理json格式的HttpMessageConverter。 只需要导入jackson-databind jar包即可
k. 参数配置代码如下:
```xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.gao.g_httpmessageconverter.MyHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/user</value>
</list>
</property>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg type="java.lang.String" value="yyyy/MM/dd" />
</bean>
</property>
<!-- 指定时区 -->
<property name="timeZone" value="GMT+8" />
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
```
============================================================================================================================
-
我们已经知道,控制器的返回值是String的时候,视图解析器会把这个返回值当做逻辑视图,然后再把逻辑视图解析为物理视图。
-
当我们在控制器的方法上添加@ResponseBody的时候,视图解析器就不会出来工作了。也就是说此时就没有视图解析器的什么事情了。
-
当我们在控制器的方法上添加@ResponseBody的时候,SpringMVC框架会先根据客户端请求头中的 “返回值类型”,再根据 “Accept的值” 来寻找一个最合适的HttpMessageConverter对象!
accept: “a/b”: StringHttpMessageConverter
supports: String.class = c
write, 最终write方法中的输入流写了什么,就会给客户端响应什么
-
SpringMVC会调用最合适的HttpMessageConverter对象的write方法:
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException完成。
============================================================================================================================
注意
1.并不是使用了某一个HttpMessageConverter对象的read方法后,就一定会使用同一个HttpMessageConverter对象的write方法。
2.注意本例的Jackson-databind的使用,以及其中的ObjectMapper的使用(在Test类中有演示)
6.1获取与发送json格式数据
后台需要在参数前加上@RequestBody,然后导入jar包jackson-databind
通过阅读AnnotationDrivenBeanDefinitionParser源码,我们得知,如果register-defaults的取值为ture,且当前项目的classpath中有
com.fasterxml.jackson.databind.ObjectMapper
com.fasterxml.jackson.core.JsonGenerator
这两个类的话,springmvc就会自动加载用于处理json格式的HttpMessageConverter。 只需要导入jackson-databind jar包即可
7.@ResponseBody
在方法执行完毕后,框架会检测方法上是否有@ResponseBody注解,如果有,就不会使用视图解析器来做响应,而会使用HttpMessageConverter来做响应.
// 有@ResponseBody --> 读取Accept请求头:text/user --> StringHttpMessageConverter, UserHttpMessageConverter
// supports: false true
// 最终找到了UserHttpMessageConverter,就会调用UserHttpMessageConverter的write方法
// 最终,该write方法中的输出流写出什么, 就会给客户端浏览器响应什么内容
8.拦截器
声明一个类继承HandlerInterceptor
然后在配置文件中添加
<mvc:interceptors>
<!--
<mvc:interceptor>
<mvc:mapping path="/user/save.do"/>
<bean class="com.woniuxy.l_interceptor.A"></bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/user/save.do"/>
<bean class="com.woniuxy.l_interceptor.B"></bean>
</mvc:interceptor>
-->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.woniuxy.l_interceptor.LogInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
9.异常拦截器
继承HandlerExceptionResolver.
然后在配置文件中添加
<!-- 配置异常解析器, id可以省略 -->
<bean class="com.woniuxy.m_exception.MyExceptionResolver"></bean>
10.校验框架
-
注意,不要使用tomcat7来运行该校验框架示例,会有jar包冲突问题:会说找不到ELManager类!
推荐使用tomcat9 -
导入依赖,注意只需要导入下面的第一个依赖即可把后面两个依赖也一起导入到项目中
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.7.Final</version> </dependency> <dependency> <groupId>org.jboss.logging</groupId> <artifactId>jboss-logging</artifactId> <version>3.3.1.Final</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
-
配置Hibernate校验器
<mvc:annotation-driven validator="validator" />
<bean name="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"></property>
</bean>
3. 在POJO中添加校验规则
```java
public class User implements Serializable {
private Integer id;
@Size(min = 2, max = 4, message = "名字必须在2到4列之间")
private String name;
private Date birthday;
private Double money;
private String cellphone;
/* getter and setter */
...
```
}
-
在controller中,在需要校验的POJO前加上@Validated注解,同时在需要校验的POJO后紧跟Errors
注意,测试时,要把日期也填上,否则birthday字段会报错!@Controller @RequestMapping("/user") public class UserController { @RequestMapping("/save") public String save(Model model, @Validated User user, Errors errors) { String path = ""; if(bindingResult.hasErrors()) { List<FieldError> list = errors.getFieldErrors(); for (FieldError fieldError : list) { model.addAttribute(fieldError.getField(), fieldError.getDefaultMessage()); } path = "h"; System.out.println("error: "+user); } else { path = "index"; System.out.println("success: "+user); } return path; } }
-
在jsp页面上显示错误信息
name:${name }
birthday:${birthday }
money: ${money }
GO -
数据回显
a. 被添加上@Validated注解的pojo参数,会被springmvc框架自动加入request范围中,key就是首字母小写的类名
可以通过@ModelAttribute注解手动指定加入request范围的key有了数据回显的form表单看起来是这个样子 <form action="${pageContext.request.contextPath }/user/save.do" method="post"> name:<input type="text" name="name" value="${user.name }" />${name } <br /> birthday:<input type="text" name="birthday" value="<fmt:formatDate value="${user.birthday }" pattern="yyyy/MM/dd" />" />${birthday } <br /> money: <input type="text" name="money" value="${user.money }" />${money } <br /> <button type="submit">GO</button> </form>
-
以上的错误信息展示的方式是“硬编码”实现的,下面讲解如何“软编码”
a. applicationContext.xml看起来是这个样子:<context:component-scan base-package="com.feicui.n_validation" /> <mvc:annotation-driven validator="validator" /> <!-- 配置校验框架 --> <bean name="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"></property> <!-- 注入资源文件 --> <property name="validationMessageSource" ref="messageSource"></property> </bean> <!-- 配置国际化 --> <bean name="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <!-- 注意,这里只配置资源文件的基本名称,所以在基本名称之后万万不可加properties这个后缀 --> <property name="basename" value="classpath:com/feicui/n_validation/ValidationMessages"></property> </bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"></property> <property name="suffix" value=".jsp"></property> </bean>
-
b. 在n_validation包下创建ValidationMessages.properties文件,编写键值对:validation.name.size = 姓名必须在{min}和{max}列之间
c. 在需要验证的POJO的属性上,添加的验证错误提示信息必须用{}括起来才可以使用资源文件中的信息。
@Size(min = 2, max = 4, message = "{validation.name.size}")
private String name;
-
JSR-303校验规则 (jsr是Java Specification Requests的缩写,意思是Java 规范提案) jsr300
@NotNull 被注解的属性不能为Null(但可以是空串或纯空格)
@NotBlank 被注解的属性不能为空串,也不能为纯空格
@Min(value) 被注解的属性必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注解的属性必须是一个数字,其值必须大于等于指定的最小值
@Digits(integer,fraction) 被注解的属性必须是一个数字,其值整数和小数部分必须是指定的精度(整数就占integer列, 小数就占frcation列)
@Past 被注解的日期必须是一个过去的日期
@Future 被注解的日期必须是一个未来的日期
@Pattern(regexp) 被注解的属性必须符合指定的正则表达式 -
hibernate validator附加的constraint
@Email 被注解的属性必须是电子邮箱格式
@Legnth 被注解的属性的长度必须在指定范围内
@NotEmpty 被注解的字符串不能为空串,但可以是纯空格
@Range 被注解的数字必须在指定的范围内 -
校验分组
a. 定义多个接口,一个接口就代表一个校验分组
b. 这些接口中不需要定义任何方法,这些接口仅仅是用来对校验规则进行分组的
interface A {}
interface B {}
c. 为验证添加了分组后,就必须使用,否则默认情况下不会直接使用分组中的校验规则
POJO中:@Size(min = 2, max = 4, message = "{validation.size}", groups = {A.class}) @Size(min = 3, max = 6, message = "{validation.size}", groups = {B.class}) private String name; Controller中: public String save(Model model, @Validated(value = A.class) User user, BindingResult bindingResult) { // ... }
11. 校验分组
a. 定义多个接口,一个接口就代表一个校验分组
b. 这些接口中不需要定义任何方法,这些接口仅仅是用来对校验规则进行分组的
interface A {}
interface B {}
c. 为验证添加了分组后,就必须使用,否则默认情况下不会直接使用分组中的校验规则
POJO中:
@Size(min = 2, max = 4, message = "{validation.size}", groups = {A.class})
@Size(min = 3, max = 6, message = "{validation.size}", groups = {B.class})
private String name;
Controller中:
public String save(Model model, @Validated(value = A.class) User user, BindingResult bindingResult) {
// ...
}