一、搭建 Hello Spring 工程
1.1 项目搭建
- 新建 maven Web 工程,并引入相应的依赖:
<properties> <spring-base-version>5.1.3.RELEASE</spring-base-version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring-base-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring-base-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring-base-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring-base-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring-base-version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>
- 得益于 servlet3.0 和 Spring 的共同支持,我们可以在没有
web.xml
的情况下完成关于 servlet 配置。新建 DispatcherServletInitializer,这个类的作用相当于我们在 XML 方式下 web.xml 中配置的 DispatcherServlet:public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[0]; } protected Class<?>[] getServletConfigClasses() { return new Class[]{ServletConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } }
- 新建 ServletConfig ,文件内容如下 (这个类相当于我们在 XML 配置方式中的
springApplication.xml
):@Configuration @EnableWebMvc @ComponentScan(basePackages = {"com.heibaiying.controller"}) public class ServletConfig implements WebMvcConfigurer { /** * 配置视图解析器 */ @Bean public ViewResolver viewResolver() { InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver(); internalResourceViewResolver.setPrefix("/WEB-INF/jsp/"); internalResourceViewResolver.setSuffix(".jsp"); internalResourceViewResolver.setExposeContextBeansAsAttributes(true); return internalResourceViewResolver; } /** * 配置静态资源处理器 */ public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }
- 新建 Controller 用于测试整体配置是否成功:
@Controller @RequestMapping("mvc") public class HelloController { @RequestMapping("hello") private String hello() { return "hello"; } }
- 在 WEB-INF 下新建 JSP 文件夹 ,并创建一个简单的 hello.jsp 文件:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> Hello Spring MVC! </body> </html>
- 启动 Tomcat 服务,访问的服务地址为:localhost:8080/mvc/hello
1.2 相关注解说明
- @Configuration:@Configuration 用于配置类的定义,等价于XML 配置文件,配置类内部可以包含一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,方法返回的实体会被直接注册为容器内的 Bean。
- @EnableWebMvc:简单的说就是提供了部分 SpringMVC 的功能,例如格式转换和参数绑定。
二、配置自定义拦截器
- 创建自定义拦截器,实现接口 HandlerInterceptor(这里我们创建两个拦截器,用于测试拦截器的执行顺序):
public class MyFirstInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { System.out.println("进入第一个拦截器 preHandle"); return true; } // 需要注意的是,如果对应的程序报错,不一定会进入这个方法 但一定会进入 afterCompletion 这个方法 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { System.out.println("进入第一个拦截器 postHandle"); } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("进入第一个拦截器 afterCompletion"); } }
public class MySecondInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { System.out.println("进入第二个拦截器 preHandle"); return true; } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { System.out.println("进入第二个拦截器 postHandle"); } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("进入第二个拦截器 afterCompletion"); } }
- 在 ServletConfig 中注册自定义拦截器:
public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/mvc/**").excludePathPatterns("mvc/login"); registry.addInterceptor(new MySecondInterceptor()).addPathPatterns("/mvc/**"); }
- 关于多个拦截器方法执行顺序的说明
拦截器的执行顺序是按声明的先后顺序执行的,先声明的拦截器中的 preHandle 方法会先执行,然而它的 postHandle 方法和 afterCompletion 方法却会后执行。
三、全局异常处理
- 定义自定义异常:
public class NoAuthException extends RuntimeException { public NoAuthException() { super(); } public NoAuthException(String message) { super(message); } public NoAuthException(String message, Throwable cause) { super(message, cause); } public NoAuthException(Throwable cause) { super(cause); } }
- 实现自定义异常处理器:
public class NoAuthExceptionResolver implements HandlerExceptionResolver { public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (ex instanceof NoAuthException && !isAjax(request)) { return new ModelAndView("NoAuthPage"); } return new ModelAndView(); } // 判断是否是 Ajax 请求 private boolean isAjax(HttpServletRequest request) { return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With")); } }
- 在 ServletConfig 中注册自定义异常处理器:
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { resolvers.add(new NoAuthExceptionResolver()); }
- 定义测试 controller,抛出自定义异常:
@Controller @RequestMapping("mvc") public class HelloController { @RequestMapping("hello") private String hello() { return "hello"; } @RequestMapping("auth") private void auth() { throw new NoAuthException("没有对应的访问权限!"); } }
调用这个 Controller 时,同时可以验证在拦截器部分提到的:如果对应的程序报错,拦截器不一定会进入 postHandle 这个方法 但一定会进入 afterCompletion 这个方法。
四、参数绑定
4.1 参数绑定
- 新建 Programmer.java 作为测试实体类:
@Data public class Programmer { private String name; private int age; private float salary; private String birthday; }
- 新建 ParamBindController.java 文件,跳转到指定视图:
@Controller public class ParamBindController { @InitBinder protected void initBinder(WebDataBinder binder) { binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss")); } // 参数绑定与日期格式转换 @RequestMapping("param") public String param(String name, int age, double salary, @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date birthday, Model model) { model.addAttribute("name", name); model.addAttribute("age", age); model.addAttribute("salary", salary); model.addAttribute("birthday", birthday); return "param"; } @RequestMapping("param2") public String param2(String name, int age, double salary, Date birthday, Model model) { model.addAttribute("name", name); model.addAttribute("age", age); model.addAttribute("salary", salary); model.addAttribute("birthday", birthday); return "param"; } @PostMapping("param3") public String param3(Programmer programmer, String extendParam, Model model) { System.out.println("extendParam" + extendParam); model.addAttribute("p", programmer); return "param"; } }
- 新建 param.jsp 文件,用于测试数据在视图中的绑定情况:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Restful</title> </head> <body> <ul> <li>姓名:${empty name ? p.name : name}</li> <li>年龄:${empty age ? p.age : age}</li> <li>薪酬:${empty salary ? p.salary : salary}</li> <li>生日:${empty birthday ? p.birthday : birthday}</li> </ul> </body> </html>
- 启动 Tomcat,接着可以使用 postman 等接口测试软件发送测试请求。
4.2 日期格式转换
Spring 支持使用以下三种方法来对参数中的日期格式进行转换:
- 方法一 :如上面的实例代码所示,在对应的 Controller 中初始化绑定:
@InitBinder protected void initBinder(WebDataBinder binder) { binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss")); }
- 方法二 :利用 @DateTimeFormat 注解,如果是用实体类去接收参数,则在对应的实体类的属性上用 @DateTimeFormat 和 @JsonFormat 进行声明:
public String param(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date birthday)
- 方法三 :使用全局的日期格式绑定,新建自定义日期格式转化类,之后在
springApplication.xml
中进行注册,采用这种方式会对全局范围内的日期格式转换生效:public class CustomDateConverter implements Converter<String, Date> { public Date convert(String s) { try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return simpleDateFormat.parse(s); } catch (Exception e) { e.printStackTrace(); } return null; } }
在 ServletConfig 中进行注册:
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new CustomDateConverter());
}
五、数据校验
- Spring 支持 JSR303 标准的校验,需要引入相关的依赖:
<!-- 数据校验依赖包 --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.13.Final</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
- 新建测试类 ParamValidController.java,在需要校验的参数前加上 @Validated 注解,表明该参数需要被校验。同时在方法声明中加上 bindingResult 参数,可以从这个参数中获取最终校验的结果:
@RestController public class ParamValidController { @PostMapping("validate") public void valid(@Validated Programmer programmer,BindingResult bindingResult) { List<ObjectError> allErrors = bindingResult.getAllErrors(); for (ObjectError error : allErrors) { System.out.println(error.getDefaultMessage()); } } }
- 在 Programmer.java 的对应属性上加上注解约束,用于声明每个参数的校验规则:
@Data public class Programmer { @NotNull private String name; @Min(value = 0,message = "年龄不能为负数!" ) private int age; @Min(value = 0,message = "薪酬不能为负数!" ) private float salary; private String birthday; }
其他支持的注解可以到 javax.validation.constraints 包下进行查看。
六、文件上传与下载
6.1 文件上传
- 在 ServletConfig 中进行配置,开启文件上传:
@Bean public CommonsMultipartResolver multipartResolver(){ CommonsMultipartResolver resolver = new CommonsMultipartResolver(); resolver.setMaxUploadSize(1024*1000*10); resolver.setMaxUploadSizePerFile(1024*1000); resolver.setDefaultEncoding("utf-8"); return resolver; }
- 新建上传测试类:
@Controller public class FileController { @GetMapping("file") public String filePage() { return "file"; } /*** * 单文件上传 */ @PostMapping("upFile") public String upFile(MultipartFile file, HttpSession session) { //保存在项目根目录下 image 文件夹下,如果文件夹不存在则创建 FileUtil.saveFile(file, session.getServletContext().getRealPath("/image")); // success.jsp 就是一个简单的成功页面 return "success"; } /*** * 多文件上传 多个文件用同一个名字 */ @PostMapping("upFiles") public String upFiles(@RequestParam(name = "file") MultipartFile[] files, HttpSession session) { for (MultipartFile file : files) { FileUtil.saveFile(file, session.getServletContext().getRealPath("images")); } return "success"; } /*** * 多文件上传方式 2 分别为不同文件指定不同名字 */ @PostMapping("upFiles2") public String upFile(String extendParam, @RequestParam(name = "file1") MultipartFile file1, @RequestParam(name = "file2") MultipartFile file2, HttpSession session) { String realPath = session.getServletContext().getRealPath("images2"); FileUtil.saveFile(file1, realPath); FileUtil.saveFile(file2, realPath); System.out.println("extendParam:" + extendParam); return "success"; } }
- 其中工具类 FileUtil.java 代码如下:
public class FileUtil { public static String saveFile(MultipartFile file, String path) { InputStream inputStream = null; FileOutputStream outputStream = null; String fullPath = path + File.separator + file.getOriginalFilename(); try { File saveDir = new File(path); if (!saveDir.exists()) { saveDir.mkdirs(); } outputStream = new FileOutputStream(new File(fullPath)); inputStream = file.getInputStream(); byte[] bytes = new byte[1024 * 1024]; int read; while ((read = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, read); } } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return fullPath; } }
- 新建用于上传的 JSP 页面,上传文件时表单必须声明
enctype="multipart/form-data"
:<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>文件上传</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/file.css"> </head> <body> <form action="${pageContext.request.contextPath }/upFile" method="post" enctype="multipart/form-data"> 请选择上传文件:<input name="file" type="file"><br> <input type="submit" value="点击上传文件"> </form> <form action="${pageContext.request.contextPath }/upFiles" method="post" enctype="multipart/form-data"> 请选择上传文件 (多选):<input name="file" type="file" multiple><br> <input type="submit" value="点击上传文件"> </form> <form action="${pageContext.request.contextPath }/upFiles2" method="post" enctype="multipart/form-data"> 请选择上传文件 1:<input name="file1" type="file"><br> 请选择上传文件 2:<input name="file2" type="file"><br> 文件内容额外备注: <input name="extendParam" type="text"><br> <input type="submit" value="点击上传文件"> </form> </body> </html>
6.2 文件下载
- 在 fileController.java 中增加下载方法:
/*** * 上传用于下载的文件 */ @PostMapping("upFileForDownload") public String upFileForDownload(MultipartFile file, HttpSession session, Model model) throws UnsupportedEncodingException { String path = FileUtil.saveFile(file, session.getServletContext().getRealPath("/image")); model.addAttribute("filePath", URLEncoder.encode(path,"utf-8")); model.addAttribute("fileName", file.getOriginalFilename()); return "fileDownload"; } /*** * 下载文件 */ @GetMapping("download") public ResponseEntity<byte[]> downloadFile(String filePath) throws IOException { HttpHeaders headers = new HttpHeaders(); File file = new File(filePath); // 解决文件名中文乱码 String fileName=new String(file.getName().getBytes("UTF-8"),"iso-8859-1"); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); headers.setContentDispositionFormData("attachment", fileName); return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers, HttpStatus.CREATED); }
- 其中 fileDownload.jsp 如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>文件下载</title> </head> <body> <a href="${pageContext.request.contextPath}/download?filePath=${filePath}">${fileName}</a> </body> </html>
七、RESTFul 风格的请求
- 新建测试实体类:
@Data public class Pet { private String ownerId; private String petId; }
- 新建 RestfulController.java,用 @PathVariable 和 @ModelAttribute 注解进行参数绑定:
@RestController public class RestfulController { @GetMapping("restful/owners/{ownerId}/pets/{petId}") public void get(@PathVariable String ownerId, @PathVariable String petId) { System.out.println("ownerId:" + ownerId); System.out.println("petId:" + petId); } @GetMapping("restful2/owners/{ownerId}/pets/{petId}") public void get(@ModelAttribute Pet pet) { System.out.println("ownerId:" + pet.getOwnerId()); System.out.println("petId:" + pet.getPetId()); } }
在 RESTFul 风格的请求中,资源通过 URL 进行标识和定位,而操作行为是通过 HTTP 方法进行定义,在进行不同行为时对应 HTTP 方法和 Spring 注解分别如下:
- 创建资源时:POST(@PostMapping)
- 读取资源时:GET( @GetMapping)
- 更新资源时:PUT 或 PATCH(@PutMapping、@PatchMapping)
- 删除资源时:DELETE(@DeleteMapping)