本篇记录SpringMVC的使用,初学Java后端,各种大佬轻拍。
配置工作
- 导入依赖
使用Spring5.0.2的版本,主要依赖是spring-web,其他都是学习过程中需要jar包,单纯只要SpringMVC不需要导入。
5.0.2.RELEASE
org.springframework
spring-context
${spring.version}
org.springframework
spring-web
${spring.version}
org.springframework
spring-webmvc
${spring.version}
javax.servlet
servlet-api
2.5
provided
javax.servlet.jsp
jsp-api
2.0
provided
com.fasterxml.jackson.core
jackson-databind
2.9.0
com.fasterxml.jackson.core
jackson-core
2.9.0
com.fasterxml.jackson.core
jackson-annotations
2.9.0
commons-fileupload
commons-fileupload
1.3.1
commons-io
commons-io
2.4
com.sun.jersey
jersey-core
1.18.1
com.sun.jersey
jersey-client
1.18.1
- 配置web.xml
不管SpringMVC还是其他MVC框架,都是封装servlet的API,而SpringMVC分发请求是通过一个DispatcherServlet,再转发请求到Controller,所以我们需要在web.xml中添加配置。
- 配置DispatcherServlet前端控制器,在初始化参数中,配置SpringMVC的配置文件为springmvc.xml。
- 配置解决中文乱码的过滤器,CharacterEncodingFilter,指定编码为utf-8。
Archetype Created Web Application
dispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:springmvc.xml
1
dispatcherServlet
/
characterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
characterEncodingFilter
/*
- 配置SpringMVC配置文件
上面DispatcherServlet中,我们指定SpringMVC的配置文件为springmvc.xml,所以我们需要在resources目录下,添加springmvc.xml文件。
- 配置spring创建容器时要扫描的包
- 配置视图解析器,指定jsp存放的配置,后缀等,这里指定为webapp下的WEB-INF里面的pages
- 配置静态资源不拦截,如html、css、js等
- 配置spring开启注解mvc的支持,开启后,就可以使用SpringMVC的注解
- 编写通用页面
一般项目都有通用的成功、失败页面。我们都放在webapp下的WEB-INF里面的pages下。我们就简单的写一些文字来代替。
- 通用错误页面
<%--
Created by IntelliJ IDEA.
User: wally
Date: 2020/6/23
Time: 2:51 下午
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
错误页面
友好的错误页面
${ errorMsg }
- 通用成功页面
<%--
Created by IntelliJ IDEA.
User: wally
Date: 2020/6/22
Time: 3:22 下午
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
成功页面
成功
- 编写Controller,输出HelloWord
- 使用@Controller注解,标识当前是一个Controller类
- 使用@RequestMapping注解,指定当前控制器管理的一级路径,这里为/user
- 使用@RequestMapping注解,指定sayHello()接口方法的响应的请求路径为/hello,完整路径为:http://localhost:8081/springmvc_sample_war_exploded/user/hello
- sayHello()接口方法,返回通用成功页面,直接返回success字符串,视图解析器会去pages下查找教success.jsp的文件
@Controller
@RequestMapping("/user")
public class HelloController {
/**
* 测试请求映射
*/
@RequestMapping(path = "/hello")
public String sayHello() {
System.out.println("Hello Spring MVC");
return "success";
}
}
- index.html中,添加请求跳转
a标签,只能发起GET请求,href超链接中指定请求url,点击就会跳转到success.jsp。
<%--
Created by IntelliJ IDEA.
User: wally
Date: 2020/6/22
Time: 3:05 下午
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
首页
入门案例
入门案例
表单数据绑定到JavaBean
一般前端页面会有表单提交,表单数据很多时,一般我们会封装到一个JavaBean,而SpringMVC可以将数据映射到JavaBean中,省去我们从Request对象中做很多次get操作来获取。
这里我们前端页面,请求保存账户信息以及它的用户信息
- 用户实体
/**
* 用户实体
*/
public class User {
private String uname;
private Integer age;
//省略get、set
}
- 账户实体
账户实体中,包含一个User实体
/**
* 账户实体
*/
public class Account implements Serializable {
private String username;
private String password;
private Double money;
/**
* 嵌套自定义引用类型
*/
private User user;
//省略get、set
}
- Controller
Controller类中添加一个saveAccount()方法,Account实体作为方法形参即可。
/**
* 测试数据绑定到JavaBean
*/
@RequestMapping("/saveAccount")
public String saveAccount(Account account) {
System.out.println("---- saveAccount() 调用成功 ----");
System.out.println(account);
return "success";
}
- index.jsp
index.jsp添加一个表单,由于我们的User实体是嵌入到Account实体的,所以input标签的name属性,如user.name、user.age,级联写即可。
自定义类型转换器
SpringMVC内置了一些转换器,例如时间转换,前端表单传递时间,使用字符串,也可以自动映射为实体的Date字段。
但例如有一些自定义格式,超过内置转换器的转换范围时,就需要我们自己自定义转换器了。
需求:例如我们使用2020-12-28这种格式,SpringMVC是不能帮我们转换的,这就需要我们自己提供转换器了。
- User实体增加date字段,表示出生日期
/**
* 用户实体
*/
public class User {
private String uname;
private Integer age;
private Date date;
//省略get、set
}
- 自定义转换器
自定义转换器,需要实现Converter接口,有2个泛型,第一个泛型为转换前类型,第二个泛型为转换后的类型,这里我们是从String转为Date类型。
复写convert()转换方法,处理年-月-日的日期格式,例如:2020-12-28
/**
* 字符串转日期类型
*/
public class StringToDateConverter implements Converter {
public Date convert(String source) {
try {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return format.parse(source);
} catch (ParseException e) {
e.printStackTrace();
throw new IllegalArgumentException("数据类型转换异常");
}
}
}
- 在SpringMVC配置文件中,配置转换器
所有自定义转换器,都要在ConversionServiceFactoryBean,这个转换服务下,通过set标签排列。并将服务命名为conversionService。
最后,将conversionService服务,交给mvc:annotation-driven标签的conversion-service属性。
//...省略其他配置
- Controller,添加接口
/**
* 测试自定义类型转换器
*/
@RequestMapping("/saveUser")
public String saveUser(User user) {
System.out.println("---- saveUser() 调用成功 ----");
System.out.println(user);
return "success";
}
- 前端页面index.jsp
添加表单,在date的input中填入:2020-12-28,点击提交,在后台能收到转成成功的Date即可。
保存用户
支持原生Servlet的API
虽然SpringMVC给我们提供了良好的封装,但如果我们需要用到原始的HttpServletRequest、HttpServletResponse对象时,要怎么做呢,同样很简单,只要在接口方法形参上写上即可。
- Controller添加接口方法
/**
* 测试获取Servlet原生API
*/
@RequestMapping("/testServlet")
public String testServlet(HttpServletRequest request, HttpServletResponse response) {
System.out.println("---- testServlet() 调用成功 ----");
System.out.println(request);
HttpSession session = request.getSession();
System.out.println(session);
ServletContext servletContext = session.getServletContext();
System.out.println(servletContext);
System.out.println(response);
return "success";
}
- index.jsp添加调用
测试原生Servlet的API
@RequestParam注解
上面讲到,表单请求,我们可以封装到一个JavaBean中,一般适用于参数很多和复杂的情况,如果参数很少,封装成JavaBean意义不大。那么我们就可以使用@RequestParam注解。
使用@RequestParam注解,注解内有一个value属性,标识表单字段变量,如果表单字段名和变量名一致,可以不写。
defaultValue属性,指定默认值,不传递参数或传null时生效。
/**
* 测试@RequestParam注解
*/
@RequestMapping("/testParam")
public String testParam(@RequestParam("name") String username,
@RequestParam(required = false, defaultValue = "18") Integer age) {
System.out.println("---- testParam() 调用成功 ----");
System.out.println("username:" + username);
System.out.println("age:" + age);
return "success";
}
- index.jsp增加调用
测试RequestParam注解
@RequestBody注解
除了上面讲参数对应到形参,SpringMVC还提供了@RequestBody注解,可以将表单的参数按:参数名1=值1&参数名2=值2,这样的格式获取,类似GET请求时,带参数的方式。
除了格式以外,还可以作为json上传的方式封装参数到JavaBean。
- 第一种:按格式获取,Controller添加方法
/**
* 测试@RequestBody注解,会将所有的表单数据按如下格式返回:username=hezihao&password=123
*/
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String body) {
System.out.println("---- testRequestBody() 调用成功 ----");
System.out.println(body);
return "success";
}
- index.jsp增加调用
测试RequestBody注解
- 第二种:json上传方式,将参数封装到JavaBean
/**
* 测试接收json数据请求
*/
@RequestMapping("/testAjax")
@ResponseBody
public User testAjax(@RequestBody User user) {
System.out.println("---- testAjax() 调用成功 ----");
System.out.println(user);
//模拟结果
user.setUname("haha");
user.setAge(40);
return user;
}
- index.jsp增加调用,这里我引入了jQuery,将jq放在webapp目录下的js目录下即可。
点击按钮时,会发起ajax请求,后端程序接收到请求后,返回数据,前端页面alter弹出数据。
首页
@PathVariable注解
GET请求除了表单提交外,还可以按Url的路径来请求,这时就需要用到@PathVariable注解。
- Controller增加接口
Url为user/testPathVariable/xxx,在Url后拼上id。
/**
* 测试@testPathVariable注解
*/
@RequestMapping("/testPathVariable/{sid}")
public String testPathVariable(@PathVariable(name = "sid") int id) {
System.out.println("---- testPathVariable() 调用成功 ----");
System.out.println("id:" + id);
return "success";
}
- index.jsp增加接口调用
测试PathVariable注解
@RequestHeader注解
当需要获取请求传递的Header请求头时,我们可以使用@RequestHeader注解来获取指定Key值的Header值。
- Controller增加方法
例如获取默认浏览器都会带的Accept请求头。
@RequestMapping("/testRequestHeader")
public String testRequestHeader(@RequestHeader(value = "Accept") String header) {
System.out.println("---- testRequestHeader() 调用成功 ----");
System.out.println("header => Accept:" + header);
return "success";
}
- index.jsp增加接口调用
测试RequestHeader注解
@CookieValue
需要获取请求发过来的Cookie时,可以使用@CookieValue注解。
- Controller添加方法
例如获取Session的ID,在前端浏览器上,这个id是保存在Cookie的,所以我们可以获取到。
@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue(value = "JSESSIONID") String cookieValue) {
System.out.println("---- testCookieValue() 调用成功 ----");
System.out.println("JSESSIONID:" + cookieValue);
return "success";
}
- index.jsp增加调用
测试CookieValue注解
SpringMVC的异常处理器
当我们的接口发生异常时,默认会返回异常信息的页面,这样返回给用户可不好,一般会返回一个漂亮又友好的页面。
SpringMVC给我们提供了全局异常处理器,我们可以捕获特定的异常,统一返回友好的错误页面。
- 自定义异常
SysException系统异常,附带的message为我们自己手动设置的,异常处理器可以拿到异常中的message,返回为用户。
/**
* 系统异常
*/
public class SysException extends Exception {
private String message;
public SysException(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
- 定义异常处理器
异常处理器需要实现HandlerExceptionResolver接口,复写resolveException()方法。
在这个方法里,我们需要判断异常对象的种类,如果是系统异常,则获取异常中的message,传递到到error错误页面。
如果是其他异常,统一返回请联系系统管理员的消息。
/**
* 异常处理器
*/
public class SysExceptionResolver implements HandlerExceptionResolver {
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
SysException ex;
if (e instanceof SysException) {
ex = (SysException) e;
} else {
ex = new SysException("请联系系统管理员");
}
//跳转到友好的错误页面
ModelAndView mv = new ModelAndView();
mv.setViewName("error");
mv.addObject("errorMsg", ex.getMessage());
return mv;
}
}
- 配置异常处理器到springmvc.xml
有了异常处理器类还不够,Spring还不知道我们的配置,所以需要在SpringMVC的配置文件springmvc.xml中进行配置。
//...省略其他配置
- Controller添加接口方法
例如我们故意制造一个算数异常,try-catch后手动抛出。
/**
* 测试异常处理器
*/
@RequestMapping("/testException")
public String testException() throws SysException {
try {
int result = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
throw new SysException(e.getMessage());
}
return "success";
}
- index.jsp增加接口调用
测试异常处理器
SpringMVC拦截器
SpringMVC还提供了拦截器,给我们在请求前和请求后(响应前)做一些处理。
- 自定义拦截器
拦截器需要实现HandlerInterceptor接口,提供以下方法进行复写。
- preHandle(),预处理,控制器方法执行前回调。如果返回true,则放行,执行下一个拦截器,如果没有下一个拦截器,则执行控制器中的方法。
- postHandle(),后处理方法,控制器方法执行后回调,jsp加载之前。
- afterCompletion(),jsp页面加载之后回调。
执行顺序:preHandle() => postHandle() => afterCompletion()。
我们定义了2个拦截器,当触发回调方法时,返回true,会执行下一个拦截器,返回false则相当于拦截了,后面的拦截器则不回调。
/**
* 自定义拦截器
*/
public class MyInterceptor1 implements HandlerInterceptor {
/**
* 预处理,控制器方法执行前回调
* @return 返回true,则放行,执行下一个拦截器,如果没有下一个拦截器,则执行控制器中的方法
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor1 => preHandle()执行了");
return true;
}
/**
* 后处理方法,控制器方法执行后回调,jsp加载之前
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor1 => postHandle()执行了");
}
/**
* jsp页面加载之后回调
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor1 => afterCompletion()执行了");
}
}
public class MyInterceptor2 implements HandlerInterceptor {
/**
* 预处理,控制器方法执行前回调
* @return 返回true,则放行,执行下一个拦截器,如果没有下一个拦截器,则执行控制器中的方法
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor2 => preHandle()执行了");
return true;
}
/**
* 后处理方法,控制器方法执行后回调,jsp加载之前
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor2 => postHandle()执行了");
}
/**
* jsp页面加载之后回调
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor2 => afterCompletion()执行了");
}
}
- springmvc.xml中配置拦截器
和异常处理器异常,我们还需要告知Spring我们的拦截器配置。
- mvc:interceptor标签,定义拦截器。
- mvc:mapping标签,配置拦截的具体方法,可用*作为通配符。
- mvc:exclude-mapping标签,表示不拦截某个方法,一般我们用上面的mapping标签比较多。
- bean标签,class属性指定拦截器的类名。
//...省略其他配置
SpringMVC文件上传
- 增加依赖
SpringMVC的文件上传依赖commons-fileupload,commons-fileupload又依赖了commons-io。io包可以不指定,Maven会帮我们依赖传递的导入。
commons-fileupload
commons-fileupload
1.3.1
commons-io
commons-io
2.4
先来复习一下,传统方式,使用HttpServletRequest文件上传
- index.jsp增加文件上传
1.form表单请求,请求方式必须为POST
- enctype,必须为multipart/form-data
- input标签为file类型
传统方式文件
s
- 新建UploadController控制器
- 先用HttpServletRequest对象,获取路径,我们将文件放到项目部署的根目录下的uploads文件夹内
- 创建FileItemFactory文件工厂,解析文件项
- 遍历文件项,判断到是文件项时,再通过io流写文件
@Controller
@RequestMapping("/upload")
public class UploadController {
/**
* 传统方式-文件上传
*/
@RequestMapping(value = "/testUpload1", method = RequestMethod.POST)
public String testUpload1(HttpServletRequest request) throws Exception {
//获取文件上传根目录
String path = request.getSession().getServletContext().getRealPath("/uploads/");
//创建文件夹
File pathDir = new File(path);
if (!pathDir.exists()) {
pathDir.mkdirs();
}
//创建文件工厂
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
//解析请求中的文件项
List items = upload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
//只是普通表单项,不是文件,忽略
} else {
//是文件,写文件
String fileName = item.getName();
//生成唯一id
String uuid = UUID.randomUUID().toString().replace("-", "");
fileName = fileName + "_" + uuid;
item.write(new File(pathDir, fileName));
//删除临时文件
item.delete();
}
}
return "success";
}
}
- SpringMVC方式
表单拷贝一份,请求testUpload2
SpringMVC方式文件
- Controller新增testUpload2方法
SpringMVC的方式相比传统方式就简单很多,SpringMVC给我们提供了MultipartFile对象,使用这个对象的transferTo即可实现文件复制。
/**
* SpringMVC方式-文件上传
*
* @param upload 多文件,变量名要和前端文件上传表单元素的name属性一致才行
*/
@RequestMapping(value = "/testUpload2", method = RequestMethod.POST)
public String testUpload2(HttpServletRequest request, @RequestParam("upload") MultipartFile upload) throws Exception {
System.out.println("SpringMVC方式文件上传...");
//获取文件上传路径,并创建目录
String path = request.getSession().getServletContext().getRealPath("/uploads/");
File pathDir = new File(path);
if (!pathDir.exists()) {
pathDir.mkdirs();
}
//重命名文件名
String originalFilename = upload.getOriginalFilename();
//生成唯一id
String uuid = UUID.randomUUID().toString().replace("-", "");
String fileName = originalFilename + "_" + uuid;
//上传文件
upload.transferTo(new File(pathDir, fileName));
return "success";
}
- 跨服务器上传
一般真实开发中,文件服务器是单独的一台服务器,不和业务服务器一起。我们可以使用jersey,来实现跨服务器上传。
- 同样,先拷贝一份表单,请求testUpload3
跨服务器上传文件
- Controller添加testUpload3方法
- 创建Client对象,调用resource方法,指定上传路径,一般是文件服务器的接口地址,返回WebResource对象。
- 使用WebResource对象的put()方法,获取MultipartFile对象的getBytes()获取Byte数据,再丢进去即可。
/**
* 跨服务器方式-文件上传
*
* @param upload 多文件,变量名要和前端文件上传表单元素的name属性一致才行
*/
@RequestMapping(value = "/testUpload3", method = RequestMethod.POST)
public String testUpload3(@RequestParam("upload") MultipartFile upload) throws Exception {
System.out.println("跨服务器方式文件上传...");
//文件服务器上传文件请求路径
String uploadPath = "http://localhost:9090/file_upload/uploads/";
//重命名文件名
String originalFilename = upload.getOriginalFilename();
//生成唯一id
String uuid = UUID.randomUUID().toString().replace("-", "");
String fileName = originalFilename + "_" + uuid;
//创建客户端对象
Client client = Client.create();
WebResource webResource = client.resource(uploadPath + fileName);
//上传文件
webResource.put(upload.getBytes());
return "success";
}
出现的问题总结
表单请求等一路顺利,但到了文件上传,启动一直报java.lang.NoClassDefFoundError: org/apache/commons/fileupload/FileItemFactory的错。
首先想到的是我们缺少jar包,就是上面的commons-fileupload包,但是我们Maven的pom文件已经添加了,而且这个类也是可以跳转过去的。
搜索资料了很久,最后发现是Tomcat部署的lib目录,WEB-INF/lib,没有添加上这2个jar包,添加上,再启动即可。
有同学可能不知道在哪里加,打开Project Structure,选择导出的war包,点开WEB-INF/lib,再点击上面的+号,选择fileupload和commons-io,确定就可以了。
项目地址
如果上面的描述不够清楚,我将代码都上传到了Github,有兴趣的同学可以clone。