SpringMVC框架围绕 DispatcherServlet(前端控制器)这个核心展开、DispatcherServlet 是SpringMVC框架的总导演、总策划,它负责截获请求并将其分派给相应的处理器处理。传统的基于Spring Framework的web开发需要大量的 xml 配置,在有SpringBoot以后,Web开发的效率得到了很大的提升,几乎大部分配置可以使用默认约定的规则。我们基于SpringBoot的项目来进行SpringMVC的学习。
MVC设计模式
MVC(Model View Controller)是软件工程中的一种软件架构模式,它把软件系统分为模型、视图和控制器三个基本部分。
-
Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
-
View(视图)是应用程序中处理数据显示的部分。
通常视图是依据模型数据创建的。 -
Controller(控制器)是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。说明:一般现在主流的前后端分离的开发模式,后端人员不再写前端代码,我们也不学习前端,对应MVC中View的部分,我们也就不关心了。
工作流程
1、用户发送请求至前端控制器DispatcherServlet。
2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、Controller执行完成返回ModelAndView。
7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、ViewReslover解析后返回具体View.
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、DispatcherServlet响应用户。
请求转发与重定向
-
重定向会改变URL的路径,可以拿到页面外的资源,
-
请求转发的URL地址不会改变,有可能请求外部资源访问不到,因为是以当前页面路径为根路径开始查找的,而外部资源不在当前路径下,所以找不到。
@Controller
@RequestMapping("/mvc")
@Slf4j
// @Slf4j用来记录日志
public class Mvccontroller {
@RequestMapping("index")
public String getIndex(){
log.error("mvc哈哈");
return "redirect:/index.html";
//重定向,redirect实现的是临时重定向
}
@RequestMapping("index2")
public String getIndex2(HttpServletResponse resp){
resp.setStatus(301);
resp.setHeader("location","/index.html");
return null;
//301永久重定向,redirect实现的是临时重定向
}
@RequestMapping("index1")
public String getIndex1(){
log.error("mvc哈哈");
//请求转发
return "forward:/index.html";
}
}
Mvc常用的注解
-
1、@Controller注解标注是一个类是Web控制器,其和@Component注解等价,只不过在Web层使用,其便于区分类的作用。
-
2、 @RequestMapping是Spring Web应用程序中最常被用到的注解之一。在对SpringMVC进行配置的时候,需要指定请求与处理方法之间的映射关系,这时候就需要使用@RequestMapping注解。该注解可以在控制器类的级别和其方法级别上使用。默认返回的是一个view试图(或者是页面),如果要返回一个JSON字符串需要用到 @ResponseBody作用在类或者方法上。
@RequestMapping("/3")
@ResponseBody
public String test3(){
return "好了,已经知道了"; } 12345
@RequestMapping注解能够处理的HTTP请求方法有: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE 。为了能够将一个请求映射到一个特定的HTTP方法,你需要在@RequestMapping中使用method参数声明HTTP请求所使用的方法类型
//指定服务提供的请求方法
@RequestMapping(value = "/2", method = RequestMethod.GET)
public String test2(){
return "forward:/home.html"; }
- 3、@ResponseBody
表示将控制器方法的返回序列化作为响应体内容返回前端。
- 返回类型为String,表示响应Content-Type: text/plain,且响应体为控制器方法的字符串返回值
- 返回类型为普通Java类型,表示响应Content-Type: application/json,以返回对象序列化为json后作为响应体。
- @ResponseBody可以使用在类上,表示该类中所有方法都是默认以返回值作为响应体,也就是所有方法都使用@ResponseBody。如果返回值为null,表示响应体内容为空
@RequestMapping("/5")
@ResponseBody
public String test5(){
return null; }
@RequestMapping("/6")
@ResponseBody
public Object test6(){
return null; }
-
4、组合注解
@RestController注解使用在类上,和使用两个注解@Controller,@ResponseBody在类上意思一样
@GetMapping即是:@RequestMapping(method = RequestMethod.GET)
@PostMapping即是:@RequestMapping(method = RequestMethod.POST) -
5、方法的参数注解
(1)@PathVariable注解。一般的 URI 服务路径都是固定的该注解可以直接从URL中获得参数,URL中名字要与参数名对应相同。url 语义很明确,搜索引擎获取的权限更高。
@GetMapping("/owners/{ownerId}/pets/{petId}")
public String findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
return "主人id:"+ownerId+", 宠物id:"+petId; }
{}是将服务路径 URI 中的部分定义为变量,之后在方法参数中获取该路径变量。更多格式可参考
URI格式。
请求 /arg/owners/1/pets/2 ,显示的网页内容为: 主人id:1, 宠物id:2 。
变量已经定义了为Long长整形,所以不能转换为Long的 URI 都会报错,如请求
/arg/owners/abc/pets/2 就会报错,响应状态码400。
变量名ownerId,petId必须和 URI 中的定义名称一致
(2)RequestParam
需要注意@RequestParam注解参数默认为 required=true ,如果不传该参数就会报错,需要指定为: @RequestParam(required = false) 。
//public Object param2(@RequestParam(required = false) Integer count)
//以上代码使用包装类型 Integer ,如果使用 int ,不传入键为 count 的请求数据就会报错,
//因为null 无法转换为 int.建议是:必填的请求数据可以使用基本数据类型,可以不传的参数,
//都要使用包装类型
//public Object param3(@RequestParam MultipartFile file) throws IOException {
//传输文件
@PostMapping("/param1")
public Object param1(@RequestParam String username, @RequestParam String
password){
Map<String, String> map = new HashMap<>();
map.put("用户名", username);
map.put("密码", password);
return map; }
@RequestParam默认必须传入该请求数据,而 POJO 对象是根据请求数据来填充属性,如果请求数据没有,则属性就是默认值(new对象时每个属性的默认值)。
@Getter
@Setter
@ToString
public class User {
private String username;
private String password;
}
@PostMapping("/pojo2")
public Object pojo2(User user){
Map<String, String> map = new HashMap<>();
map.put("用户名", user.getUsername());
map.put("密码", user.getPassword());
return map;
}
(3)@RequestBody
当请求的数据类型Content-Type为 application/json 时,需要显示的使用@RequestBody注解。请求参数需要到body中去拿。
(4)@RequestPart获得二进制文件
@RequestMapping("/reg")
public String regin(String username, String password,
@RequestPart MultipartFile file) throws IOException {
// 1.动态获取当前项目运行的static路径,将文件保存在static目录下
String path = ClassUtils.getDefaultClassLoader().
getResource("static").getPath();
// 2.文件名(全局唯一id【UUID】)+文件的原始类型
String fileType = file.getOriginalFilename(); // img.png
fileType = fileType.substring(fileType.lastIndexOf("."));
// 文件名
String fileName = UUID.randomUUID().toString() + fileType;
// 将文件保存到服务器
file.transferTo(new File(path + fileName));
}
Servlet API
在控制器方法参数中,可以使用Servlet相关API,SpringMVC会自动将相关Servlet对象装配到方法参数中,如 HttpServletRequest 、 HttpServletResponse 、 HttpSession 等
@GetMapping("/servlet")
public void servlet(HttpServletRequest req, HttpServletResponse resp) throws
IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
String username = req.getParameter("username");
String password = req.getParameter("password");
PrintWriter pw = resp.getWriter();
pw.println("接收到的请求为:用户名="+username+",密码:"+password);
@RequestHeader 绑定请求头Header信息
@GetMapping("/header")
public String header(@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("User-Agent") String userAgent) {
return String.format("<p>Accept-Encoding: %s</p><p>User-Agent: %s</p>",
encoding, userAgent);
}
@CookieValue 绑定请求头Cookie里边的信息
@GetMapping("/cookie")
public String cookie(@CookieValue("SESSIONID") String cookie) {
return String.format("JSESSIONID: %s", cookie);
}
SpringMVC自定义配置
SpringBoot中使用SpringMVC非常方便,SpringBoot提供了大部分的MVC默认功能,并且需要自定义某部分功能也非常方便,在配置类中实现 WebMvcConfigurer 接口,根据需要重写方法即可:
自定义后端路径映射
在配置类中实现 WebMvcConfigurer 接口,重写 configurePathMatch 方法,实现时,可以添加统一的服务路径前缀
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
//Controller路径,统一添加请求的路径前缀,第二个参数,c是Controller类,
//返回boolean表示是否添加前缀
//所有Controller请求路径,都要带/api的前缀
configurer.addPathPrefix("api", c->true);
}
}
自定义Controller拦截器
先 实现HandlerInterceptor接口类,即自定义拦截器。 在配置类中重写 addInterceptors 方法,就是将自己的拦截器添加到拦截器链中。实现时,需要指定拦截器,并配置需要拦截、不拦截的路径。在客户端发起请求时,如果路径最终匹配该规则,则执行拦截器中的接口方法。以下示例,实现用户统一的会话管理:
首先定义一个用户登录接口
定义拦截器
public class LoginInterceptor implements HandlerInterceptor {
// 自定义拦截方法,返回结果是 boolean,当
// 为 true 表示可以方法后端接口,为 false 表示无权访问后端接口
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 判断 session 是否有值
HttpSession session = request.getSession(false);
if (session != null &&
session.getAttribute(AppFinal.USERINFO_SESSIONKEY) != null) {
// 用户已经登录
return true;
}
return false;
}
}
在配置类中进行配置
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
// 配置拦截规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有的接口
.excludePathPatterns("/api/user/login") // 不拦截登录接口
.excludePathPatterns("/api/user/reg") // 不拦截注册接口
.excludePathPatterns("/login.html") // 不拦截登录页面
.excludePathPatterns("/regin.html") // 不拦截注册页面
.excludePathPatterns("/**/**.html") // 不拦截注册页面
}
}
@ControllerAdvice(全局统一)
@ControllerAdvice注解(控制器通知、控制器增强)定义的类,会自动注册为一个Bean对象,将扫描指定包中带@Controller注解的类:在客户端发起请求,映射到控制器方法时,结合其他注解或接口完成统一的增强功能。
注:可以不指定扫描的包,对容器中所有@Controller生效。
应用一:统一异常处理
此时需要结合@ExceptionHandler使用,可以实现控制器方法中出现异常后的统一异常处理。先定义一个抛异常的服务,在 org.example.demo.controller.TestRestController 中,定义如下
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public Object err2(Exception e) {
HashMap<String, Object> map = new HashMap<>();
map.put("status", "-2");
map.put("data", "");
map.put("msg", e.getMessage());
return map;
}
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public Object err3(Exception e) {
HashMap<String, Object> map = new HashMap<>();
map.put("status", "-3");
map.put("data", "");
map.put("msg", "空指针异常");
return map;
}
@ExceptionHandler
@ResponseBody
public Object err(Exception e) {
HashMap<String, Object> map = new HashMap<>();
map.put("status", "-1");
map.put("data", "");
map.put("msg", e.getMessage());
return map;
}
}
应用二:响应数据格式的统一封装
此时需要结合ResponseBodyAdvice接口,可以实现对控制器方法返回数据的统一封装。
@ControllerAdvice
//实现ResponseBodyAdvice接口,表示可以根据条件对返回的数据重写
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper; //注入容器中的ObjectMapper,SpringBoot默
//认使用jackson框架中的ObjectMapper完成json的序列化
//方法参数可以获取请求调用的控制器类及方法,再决定是否要执行响应内容重写
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
//响应的内容在返回客户端之前,会执行本方法,重写之后在返回
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o,
MethodParameter methodParameter,
MediaType mediaType,
Class aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
HashMap<String, Object> map = new HashMap<>();
map.put("status", 0);
map.put("data", o);
map.put("msg", "");
if (o instanceof String) {
// 当前方法要给前端返回一个 json 类型的字符串
serverHttpResponse.getHeaders().setContentType(
MediaType.APPLICATION_JSON);
// 返回一个 json 字符串
return objectMapper.writeValueAsString(map);
}
return map;
}
}