# springmvc基础(基于注解) **开发环境:IDEA** **spring版本:5.1.3.RELEASE** **本部分内容官方文档链接:[Web Servlet](https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/web.html#spring-web)** ## 一、搭建hello spring工程 ### 1.1 项目搭建 1.新建maven web工程,并引入相应的依赖 ```xml 5.1.3.RELEASE org.springframework spring-context ${spring-base-version} org.springframework spring-beans ${spring-base-version} org.springframework spring-core ${spring-base-version} org.springframework spring-web ${spring-base-version} org.springframework spring-webmvc ${spring-base-version} javax.servlet javax.servlet-api 4.0.1 provided ``` 2.得益于servlet3.0和spring的支持,我们可以在没有web.xml的情况下完成关于servlet配置。 新建DispatcherServletInitializer.java文件,这个类的作用相当于我们在xml方式下web.xml中配置的DispatcherServlet ```java package com.heibaiying.config; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; /** * @author : heibaiying */ 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[]{"/"}; } } ``` 3.新建ServletConfig.java,文件内容如下(这个类相当于我们在xml配置方式中的springApplication.xml) ```java package com.heibaiying.config; import com.heibaiying.exception.NoAuthExceptionResolver; import com.heibaiying.interceptors.MyFirstInterceptor; import com.heibaiying.interceptors.MySecondInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; import java.util.List; /** * @author : heibaiying */ @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(); } } ``` 4.在src 下新建controller用于测试 ```java package com.heibaiying.controller; import com.heibaiying.exception.NoAuthException; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @author : heibaiying * @description : hello spring */ @Controller @RequestMapping("mvc") public class HelloController { @RequestMapping("hello") private String hello() { return "hello"; } } ``` 5.在WEB-INF 下新建jsp文件夹,新建hello.jsp 文件 ```jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> Title Hello Spring MVC! ``` 6.启动tomcat服务,访问localhost:8080/mvc/hello ### 1.2 相关注解说明 **1.@Configuration** @Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。 **2.@EnableWebMvc** 简单的说就是提供了部分springmvc的功能,例如格式转换和参数绑定。 ## 二、配置自定义拦截器 1.创建自定义拦截器,实现接口HandlerInterceptor(这里我们创建两个拦截器,用于测试拦截器方法的执行顺序) ```java package com.heibaiying.interceptors; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author : heibaiying * @description : spring5 中 preHandle,postHandle,afterCompletion 在接口中被声明为默认方法 */ 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"); } } ``` ```java package com.heibaiying.interceptors; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author : heibaiying * @description : spring5 中 preHandle,postHandle,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"); } } ``` 2.在ServletConfig.java中注册自定义拦截器 ```java /** * 添加自定义拦截器 */ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/mvc/**").excludePathPatterns("mvc/login"); registry.addInterceptor(new MySecondInterceptor()).addPathPatterns("/mvc/**"); } ``` 3.关于多个拦截器方法执行顺序的说明 拦截器的执行顺序是按声明的先后顺序执行的,先声明的拦截器中的preHandle方法会先执行,然而它的postHandle方法和afterCompletion方法却会后执行。 ## 三、全局异常处理 1.定义自定义异常 ```java package com.heibaiying.exception; /** * @author : heibaiying * @description : 自定义无权限异常 */ 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); } } ``` 2.实现自定义异常处理器 ```java package com.heibaiying.exception; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author : heibaiying * @description : 无权限异常处理机制 */ 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")); } } ``` 3.在ServletConfig.java注册自定义异常处理器 ```java /** * 添加全局异常处理器 */ public void configureHandlerExceptionResolvers(List resolvers) { resolvers.add(new NoAuthExceptionResolver()); } ``` 4.定义测试controller,抛出自定义异常 ```java @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 参数绑定 1.新建Programmer.java ```java package com.heibaiying.bean; import lombok.Data; /** * @author : heibaiying * @description : */ @Data public class Programmer { private String name; private int age; private float salary; private String birthday; } ``` 注:@Data 是lombok包下的注解,用来生成相应的set、get方法,使得类的书写更为简洁。 2.新建ParamBindController.java 文件 ```java package com.heibaiying.controller; import com.heibaiying.bean.Programmer; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.datetime.DateFormatter; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import java.util.Date; /** * @author : heibaiying * @description :参数绑定 */ @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"; } } ``` 3.新建param.jsp 文件 ```jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> Restful ``` 4.启动tomcat,用[postman](https://www.getpostman.com/)软件发送请求进行测试 ### 4.2 关于日期格式转换的三种方法 1.如上实例代码所示,在对应的controller中初始化绑定 ```java @InitBinder protected void initBinder(WebDataBinder binder) { binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss")); } ``` 2.利用@DateTimeFormat注解,如果是用实体类去接收参数,则在对应的属性上声明 ```java public String param(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date birthday) ``` 3.使用全局的日期格式绑定,新建自定义日期格式转化类,之后在ServletConfig.java中进行注册 ```java package com.heibaiying.convert; import org.springframework.core.convert.converter.Converter; import java.text.SimpleDateFormat; import java.util.Date; /** * @author : heibaiying * @description : */ public class CustomDateConverter implements Converter { 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.java ```java /** * 添加全局日期处理 */ public void addFormatters(FormatterRegistry registry) { registry.addConverter(new CustomDateConverter()); } ``` ## 五、数据校验 1.spring支持的数据校验是JSR303的标准,需要引入依赖的jar包 ```java org.hibernate.validator hibernate-validator 6.0.13.Final javax.validation validation-api 2.0.1.Final ``` 2.新建测试ParamValidController.java,主要是在需要校验的参数前加上@Validated,声明参数需要被校验,同时加上bindingResult参数,这个参数中包含了校验的结果 ```java package com.heibaiying.controller; import com.heibaiying.bean.Programmer; import org.hibernate.validator.constraints.Length; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.datetime.DateFormatter; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import java.util.Date; import java.util.List; /** * @author : heibaiying * @description :数据校验 */ @RestController public class ParamValidController { @PostMapping("validate") public void valid(@Validated Programmer programmer, BindingResult bindingResult) { List allErrors = bindingResult.getAllErrors(); for (ObjectError error : allErrors) { System.out.println(error.getDefaultMessage()); } } } ``` 3.在Programmer.java的对应属性上加上注解约束(支持的注解可以在javax.validation.constraints包中查看) ```java package com.heibaiying.bean; import lombok.Data; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; /** * @author : heibaiying * @description : */ @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; } ``` ## 六、文件上传与下载 #### 6.1 文件上传 1.在ServletConfig.java中进行配置,使之支持文件上传 ```java /** * 配置文件上传 */ @Bean public CommonsMultipartResolver multipartResolver(){ CommonsMultipartResolver resolver = new CommonsMultipartResolver(); resolver.setMaxUploadSize(1024*1000*10); resolver.setMaxUploadSizePerFile(1024*1000); resolver.setDefaultEncoding("utf-8"); return resolver; } ``` 2.新建测试上传的FileController.java ```java package com.heibaiying.controller; import com.heibaiying.utils.FileUtil; import org.apache.commons.io.FileUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpSession; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; /** * @author : heibaiying * @description : 文件上传 */ @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"; } } ``` 3.其中工具类FileUtil.java代码如下 ```java package com.heibaiying.utils; import org.springframework.web.multipart.MultipartFile; import java.io.*; /** * @author : heibaiying * @description : 文件上传工具类 */ 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; } } ``` 4.新建用于上传的jsp页面,上传文件时表单必须声明 enctype="multipart/form-data" ```jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> 文件上传
请选择上传文件:
请选择上传文件(多选):
请选择上传文件1:
请选择上传文件2:
文件内容额外备注:
``` #### 6.2 文件下载 1.在fileController.java中加上方法: ```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 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(FileUtils.readFileToByteArray(file), headers, HttpStatus.CREATED); } ``` 2.其中fileDownload.jsp 如下: ```jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> 文件下载 ${fileName} ``` ## 七、Restful风格的请求 1.新建Pet.java实体类 ```java package com.heibaiying.bean; import lombok.Data; /** * @author : heibaiying * @description :测试restful风格的实体类 */ @Data public class Pet { private String ownerId; private String petId; } ``` 2.新建RestfulController.java,用@PathVariable和@ModelAttribute注解进行参数绑定。 注: 在REST中,资源通过URL进行识别和定位。REST中的行为是通过HTTP方法定义的。在进行不同行为时对应HTTP方法和Spring注解分别如下: - 创建资源时:POST(PostMapping) - 读取资源时:GET( @GetMapping) - 更新资源时:PUT或PATCH(PutMapping、PatchMapping) - 删除资源时:DELETE(DeleteMapping) ```java package com.heibaiying.controller; import com.heibaiying.bean.Pet; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; /** * @author : heibaiying * @description : Restful 风格的请求 */ @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()); } } ```