SpringMVC处理流程:
(1)DispatcherServlet截获客户端请求。
(2)DispatcherServlet解析URL,通过查找处理器映射器(HandlerMapping)找到对应的处理器对象(Handler),以及这个对象关联的拦截器(Interceptor),这些对象被封装在一个HandlerExecutionChain对象里面。
(3)DispatcherServlet将请求请求交给合适的适配器处理。适配器的主要功能是帮助DispatcherServlet调用Handler实际的处理请求的方法。Spring官方文档中是这样说的:
Helps the DispatcherServlet to invoke a handler mapped to a request regardless of the handler is actually invoked. For example, invoking an annotated controller requires resolving various annotations. Thus the main purpose of a HandlerAdapter is to shield the DispatcherServlet from such details.
为了调用指定的请求处理方法,还需要对客户端发送的数据进行转换,比如转换为Java的类型以及POJO。
(4)Handler处理完请求后向DispatcherServlet返回一个ModelAndView对象,这个对象包含视图名和模型数据。
(5)DispatcherServlet根据ModelAndView对象,选择一个合适的视图解析器(ViewResolver)渲染视图。
(6)DispatcherServlet将渲染的视图结果返回给客户端。
1. 开发一个控制器
1.1 配置springmvc.xml
(1)打开Spring Bean的自动扫描;
(2)配置视图解析器:JSP页面的话使用InternalResourceViewResolver,Velocity模板使用VelocityViewResolver;并定义好前缀和后缀;
<!-- spring可以自动去扫描base-pack下面的包或者子包下面的java文件,
如果扫描到有Spring的相关注解的类,则把这些类注册为Spring的bean -->
<context:component-scan base-package="org.fkit.controller"/>
<!-- 视图解析器 -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 前缀 -->
<property name="prefix">
<value>/WEB-INF/content/</value>
</property>
<!-- 后缀 -->
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
1.2 定义控制器
@Controller
public class DeptController {
/**
* 自动注入UserService
* */
@Autowired
@Qualifier("hrmService")
private HrmService hrmService;
/**
* 处理/login请求
* */
@RequestMapping(value="/dept/selectDept")
public String selectDept(Model model,Integer pageIndex,
@ModelAttribute Dept dept){
PageModel pageModel = new PageModel();
if(pageIndex != null){
pageModel.setPageIndex(pageIndex);
}
/** 查询用户信息 */
List<Dept> depts = hrmService.findDept(dept, pageModel);
model.addAttribute("depts", depts);
model.addAttribute("pageModel", pageModel);
return "dept/dept";
}
/**
* 处理删除部门请求
* @param String ids 需要删除的id字符串
* @param ModelAndView mv
* */
@RequestMapping(value="/dept/removeDept")
public ModelAndView removeDept(String ids,ModelAndView mv){
// 分解id字符串
String[] idArray = ids.split(",");
for(String id : idArray){
// 根据id删除部门
hrmService.removeDeptById(Integer.parseInt(id));
}
// 设置客户端跳转到查询请求
mv.setViewName("redirect:/dept/selectDept");
// 返回ModelAndView
return mv;
}
}
(1)@Controller声明一个控制器类。
(2)@RequestMapping标识请求路径对应的处理方法。
(3)一个Controller往往需要调用一个Service对象来完成业务逻辑,所以使用@Autowired + @Qualifier(“hrmService”)的方式注入进来。
(4)处理方法参数中既可以使用ModelAndView也可以使用Model对象。
(5)@ModelAttribute注解的用法有多种,最常见的就是用于注释一个参数:
public String selectDept(Model model,Integer pageIndex,
@ModelAttribute Dept dept){
Spring框架会将页面提交的表达数据按照名称封装到Dept对象中,@ModelAttribute的作用就是以””dept”为key,将dept这个对象添加到Model对象中。
1.2.1 接受请求的参数
@RequestParam
按名称接收Query Parameters或表单数据,required默认是true,也就是如果没有这个参数会报错。
@RequestMapping(value="/login")
public ModelAndView login(@RequestParam("loginname") String loginname,
@RequestParam("password") String password,
HttpSession session,
ModelAndView mv){
@RequestParam(value="password", required=true, defaultValue="admin")
@PathVariable
和@RequestMapping结合使用,接收Path变量:
@RequestMapping(value="/{formName}")
public String loginForm(@PathVariable String formName){
// 动态跳转页面
return formName;
}
@SessionAttributes
只能声明在类上,不能声明在方法上。
标识将Model中的特定属性放入到HttpSession对象中。默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据复制一份到session域中。
@SessionAttributes(value={"names"},types={Integer.class})
@Controller
public class Test {
@RequestMapping("/test")
public String test(Map<String,Object> map){
map.put("names", Arrays.asList("caoyc","zhh","cjx"));
map.put("age", 18);
return "hello";
}
}
1.2.2 处理表单数据
application/x-www-form-urlencoded
HTML中的简单表单控件如下:
<form action="user/login.do" method="get" >
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="登录"/>
</form>
application/x-www-form-urlencoded 是常用的表单发包方式,普通的表单提交,或者js发包,默认都是通过这种方式。
(1)GET请求发送表单
当HTTP Method为GET时候,浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串(name1=value1&name2=value2…),然后把这个字串append到url后面,用?分割,加载这个新的url:
http://localhost:8080/springmvc/user/login.do?username=xiaoming&password=123456789
(2)POST请求发送表单
当HTTP Method为POST时,浏览器把form数据封装到http body中,然后发送到服务器:
POST http://localhost:8080/springmvc/user/login.do HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 33
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost:8080/springmvc/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
username=xiaoming&password=123456789
@RequestParam和@ModelAttribute,还有@RequestBody都可以处理application/x-www-form-urlencoded类型的表单。
multipart/form-data
如果没有 type=file 的控件,form表单会自动form的enctype属性为编码方式默认的 application/x-www-form-urlencoded。
如果有 type=file 的话,就要用到 multipart/form-data 了。浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition(form-data或者file)、Content-Type(默认为text/plain)、name(控件name)等信息,并加上分割符(boundary)。
需要上传附件时,必须为”multipart/form-data”,而且Method一定是POST:
<form action="user/login.do" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
上传文件:<input type="file" name="uploadFile"/><br>
<input type="submit" value="登录"/>
</form>
提交表单时,Http请求协议如下:
POST http://localhost:8080/springmvc/user/login.do HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 400
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykALcKBgBaI9xA79y
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost:8080/springmvc/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
------WebKitFormBoundarykALcKBgBaI9xA79y
Content-Disposition: form-data; name="username"
xiaoming
------WebKitFormBoundarykALcKBgBaI9xA79y
Content-Disposition: form-data; name="password"
123456789
------WebKitFormBoundarykALcKBgBaI9xA79y
Content-Disposition: form-data; name="uploadFile"; filename="file.txt"
Content-Type: text/plain
文件中的内容
------WebKitFormBoundarykALcKBgBaI9xA79y--
——WebKitFormBoundarykALcKBgBaI9xA79y– 是分割分。
对于有上传文件的multipart/form-data表单数据,@RequestParam和@ModelAttribute都可以处理,但是@RequestBody不能处理。
1.2.3 处理上传文件
(1)使用@RequestParam绑定MultipartFile文件:
// 上传文件会自动绑定到MultipartFile中
@RequestMapping(value="/upload",method=RequestMethod.POST)
public String upload(HttpServletRequest request,
@RequestParam("description") String description,
@RequestParam("file") MultipartFile file) throws Exception{
......
}
(2)希望整个表单数据直接被Spring转换为一个对象:
User(domain):
public class User implements Serializable{
private String username;
private MultipartFile image;
// 省略构造器和setter/getter
}
FileUplodController:
@RequestMapping(value="/register")
public String register(HttpServletRequest request,
@ModelAttribute User user,
Model model)throws Exception{
System.out.println(user.getUsername());
// 如果文件不为空,写入上传路径
if(!user.getImage().isEmpty()){
// 上传文件路径
String path = request.getServletContext().getRealPath(
"/images/");
// 上传文件名
String filename = user.getImage().getOriginalFilename();
File filepath = new File(path,filename);
// 判断路径是否存在,如果不存在就创建一个
if (!filepath.getParentFile().exists()) {
filepath.getParentFile().mkdirs();
}
// 将上传文件保存到一个目标文件当中
user.getImage().transferTo(new File(path+File.separator+ filename));
// 将用户添加到model
model.addAttribute("user", user);
return "userInfo";
}else{
return "error";
}
}
@RequestParam只能获取表单数据中的一条属性,@ModelAttribute既可以获取一个属性,也可以获取整个对象。
1.2.4 @RequestBody
处理POST请求时,请求中的Body一般有一下几种格式:
- application/x-www-form-urlencoded:@RequestParam、@ModelAttribute、@RequestBody都可以处理;
- multipart/form-data:@RequestParam、@ModelAttribute可以,@RequestBody不行;
- application/json和application/xml:必须使用@RequestBody;
前面已经介绍过了,目前最常用和高效的开发模型下,我们会尽量使用Spring的注解和默认配置。在配置处理器适配器的时候,选择RequestMappingHandlerAdapter一般就够用了。在数据转换方法,这个适配器默认装载了一下HTTPMessageConverter:
- StringHTTPMessageConverter:字符串,响应媒体类型是text/plain;
- ByteArrayHTTPMessageConverter:读写二进制数据,application/octet-stream;
- SourceHTTPMessageConverter
- XmlAwareHTTPMessageConverter:xml;
尤其在使用mvc配置的时候,<mvc:annotation-driven>会自动注册RequestMappingHandlerMapping与RequestMappingHandlerAdapter两个Bean,这是Spring MVC为@Controller分发请求所必须的,并提供以下支持:
- 数据绑定
- @NumberFormat、@DateTimeFormat
- @Valid
- JAXB读写XML
- JSON(默认Jackson)
如果要处理Json数据格式,需要先取消message-converters的默认配置,然后在converters列表中加入MappingJackson2HTTPMessageConverter。这个转换器是Spring提供的,看名字就知道依赖的是Jackson库,但是现在用得更普遍的是fastjson,如下配置:
<!-- spring可以自动去扫描base-pack下面的包或者子包下面的java文件,
如果扫描到有Spring的相关注解的类,则把这些类注册为Spring的bean -->
<context:component-scan base-package="org.fkit.controller"/>
<!-- 使用默认的Servlet来响应静态文件 -->
<mvc:default-servlet-handler/>
<!-- 设置配置方案 -->
<mvc:annotation-driven>
<!-- 设置不使用默认的消息转换器 -->
<mvc:message-converters register-defaults="false">
<!-- 配置Spring的转换器 -->
<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
<bean class="org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter"/>
<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
<bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"/>
<!-- 配置fastjson中实现HttpMessageConverter接口的转换器 -->
<bean id="fastJsonHttpMessageConverter"
class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<!-- 加入支持的媒体类型:返回contentType -->
<property name="supportedMediaTypes">
<list>
<!-- 这里顺序不能反,一定先写text/html,不然ie下会出现下载提示 -->
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
@Controller
@RequestMapping("/json")
public class BookController {
private static final Log logger = LogFactory.getLog(BookController.class);
// @RequestBody根据json数据,转换成对应的Object
@RequestMapping(value="/testRequestBody")
public void setJson(@RequestBody Book book,
HttpServletResponse response) throws Exception{
// JSONObject-lib包是一个beans,collections,maps,java arrays和xml和JSON互相转换的包。
// 使用JSONObject将book对象转换成json输出
logger.info(JSONObject.toJSONString(book));
book.setAuthor("肖文吉");
response.setContentType("text/html;charset=UTF-8");
// 将book对象转换成json写出到客户端
response.getWriter().println(JSONObject.toJSONString(book));
}
}
上面Controller的代码注意三点:
(1)@RequestBody 是Jason数据;
(2)JSONObject.toJSONString(book)是fastjson中的方法;
(3)response.getWriter().println(JSONObject.toJSONString(book)) 写出json到客户端;
2. 数据转换与校验
2.1 数据转换与格式化
客户端提交的数据往往是字符串,Spring框架需要将这些数据转换与绑定为对应的Java参数,比如日期Date、货币等。Spring早期提供了转换器实现这一功能,用户自定义的转化器只需要实现Converter<T>,然后在ConversionServiceFactoryBean中注册这个转化器即可。
但是转换器不提供输入输出的信息格式化工作,所以Spring 3.0以后引入了格式化器Formatter<T>,使用方法和转换器类似。既可以转换输入的数据,也可以对输出格式化。用户自定义的转化器只需要实现Formatter<T>,然后在FormattingConversionServiceFactoryBean中注册这个转化器即可。更方便的是,我们需要数据转化的场景多是针对日期或数字,所以Spring提供@DataTimeFormatter和@NumberFormatter注解,可以标注在领域模型需要的属性上。
public class User implements Serializable{
// 日期类型
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birthday;
// 正常数字类型
@NumberFormat(style=Style.NUMBER, pattern="#,###")
private int total;
// 百分数类型
@NumberFormat(style=Style.PERCENT)
private double discount;
// 货币类型
@NumberFormat(style=Style.CURRENCY)
private double money;
.....
}
2.2 数据校验
2.2.1 Validation框架
/ 实现Spring的Validator接口
@Repository("userValidator")
public class UserValidator implements Validator {
// 该校验器能够对clazz类型的对象进行校验。
@Override
public boolean supports(Class<?> clazz) {
// User指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。
return User.class.isAssignableFrom(clazz);
}
// 对目标类target进行校验,并将校验错误记录在errors当中
@Override
public void validate(Object target, Errors errors) {
/**
使用ValidationUtils中的一个静态方法rejectIfEmpty()来对loginname属性进行校验,
假若'loginname'属性是 null 或者空字符串的话,就拒绝验证通过 。
*/
ValidationUtils.rejectIfEmpty(errors, "loginname", null, "登录名不能为空");
ValidationUtils.rejectIfEmpty(errors, "password", null, "密码不能为空");
User user = (User)target;
if(user.getLoginname().length() > 10){
// 使用Errors的rejectValue方法验证
errors.rejectValue("loginname", null, "用户名不能超过10个字符");
}
if(user.getPassword() != null
&& !user.getPassword().equals("")
&& user.getPassword().length() < 6){
errors.rejectValue("password", null, "密码不能小于6位");
}
}
}
@Controller
public class UserController{
private static final Log logger = LogFactory.getLog(UserController.class);
// 注入UserValidator对象
@Autowired
@Qualifier("userValidator")
private UserValidator userValidator;
@RequestMapping(value="/{formName}")
public String loginForm(
@PathVariable String formName,
Model model){
User user = new User();
model.addAttribute("user",user);
// 动态跳转页面
return formName;
}
@RequestMapping(value="/login",method=RequestMethod.POST)
public String login(
@ModelAttribute User user,
Model model,
Errors errors) {
logger.info(user);
model.addAttribute("user", user);
// 调用userValidator的验证方法
userValidator.validate(user, errors);
// 如果验证不通过跳转到loginForm视图
if(errors.hasErrors()){
return "loginForm";
}
return "success";
}
}
2.2.2 JSR 303注解
JSR 303标准有两个实现,一个是Hibernate Validator,一个是Apache Bval,我们推荐前者。
领域对象上加注解:
public class User implements Serializable{
@NotBlank
private String loginname;
@NotBlank
@Length(min=6,max=8)
private String password;
@NotBlank
private String username;
@Range(min=15, max=60)
private int age;
@Email
private String email;
@DateTimeFormat(pattern="yyyy-MM-dd")
@Past
private Date birthday;
@Pattern(regexp="[1][3,8][3,6,9][0-9]{8}")
private String phone;
....
}
Controller上使用@Valid注解对提交的数据进行校验,后面跟着Errors对象保存校验信息:
@RequestMapping(value="/login",method=RequestMethod.POST)
public String login(
@Valid @ModelAttribute User user,
Model model,
Errors errors) {
logger.info(user);
// 如果验证不通过跳转到loginForm视图
if(errors.hasErrors()){
return "loginForm";
}
return "success";
}
3. 拦截器
开发一个拦截器的步骤很简单:
(1)实现HandlerInterceptor接口,或者继承HandlerInterceptorAdapter;
- preHandle:返回false则请求结束;先声明的Interceptor先执行它的preHandle;
- postHandle:可以对Controller返回的ModelAndView进行处理;先声明的Interceptor后执行它的postHandle;
- afterCompletion:主要作用是资源清理;
(2)在Springmvc.xml文件中声明这个拦截器并指明拦截路径
拦截器:
public class AuthorizationInterceptor implements HandlerInterceptor {
// 不拦截"/loginForm"和"/login"请求
private static final String[] IGNORE_URI = {"/loginForm", "/login"};
/**
* 该方法将在整个请求完成之后执行, 主要作用是用于清理资源的,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception exception)
throws Exception {
System.out.println("AuthorizationInterceptor afterCompletion --> ");
}
/**
* 该方法将在Controller的方法调用之后执行, 方法中可以对ModelAndView进行操作 ,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView mv) throws Exception {
System.out.println("AuthorizationInterceptor postHandle --> ");
}
/**
* preHandle方法是进行处理器拦截用的,该方法将在Controller处理之前进行调用,
* 该方法的返回值为true拦截器才会继续往下执行,该方法的返回值为false的时候整个请求就结束了。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
System.out.println("AuthorizationInterceptor preHandle --> ");
// flag变量用于判断用户是否登录,默认为false
boolean flag = false;
//获取请求的路径进行判断
String servletPath = request.getServletPath();
// 判断请求是否需要拦截
for (String s : IGNORE_URI) {
if (servletPath.contains(s)) {
flag = true;
break;
}
}
// 拦截请求
if (!flag){
// 1.获取session中的用户
User user = (User) request.getSession().getAttribute("user");
// 2.判断用户是否已经登录
if(user == null){
// 如果用户没有登录,则设置提示信息,跳转到登录页面
System.out.println("AuthorizationInterceptor拦截请求:");
request.setAttribute("message", "请先登录再访问网站");
request.getRequestDispatcher("loginForm").forward(request, response);
}else{
// 如果用户已经登录,则验证通过,放行
System.out.println("AuthorizationInterceptor放行请求:");
flag = true;
}
}
return flag;
}
}
springmvc.xml:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*"/>
<!-- 使用bean定义一个Interceptor,直接定义在mvc:interceptors根下面的Interceptor将拦截所有的请求 -->
<bean class="org.fkit.interceptor.AuthorizationInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
553

被折叠的 条评论
为什么被折叠?



