Spring MVC 基础

仅用作个人学习记录,无逻辑性,仅参考

前置知识

dao(mapper)层:数据访问层

  • dao层属于一种比较底层,比较基础的操作,具体到对于某个表的增删改查,也就是说某个DAO一定是和数据库的某一张表一 一对应的,其中封装了增删改查基本操作,建议DAO只做原子操作,增删改查。
  • 负责与数据库进行联络的一些任务都封装在此,dao层的设计首先是设计dao层的接口,然后在Spring的配置文件中定义此接口的实现类,然后就可以再模块中调用此接口来进行数据业务的处理,而不用关心此接口的具体实现类是哪个类,显得结构非常清晰,dao层的数据源配置,以及有关数据库连接参数都在Spring配置文件中进行配置。

service层:服务层

  • 粗略的理解就是对一个或多个DAO进行的再次封装,封装成一个服务,所以这里也就不会是一个原子操作了,需要事物控制。

  • service层主要负责业务模块的应用逻辑应用设计。同样是首先设计接口,再设计其实现类,接着再Spring的配置文件中配置其实现的关联。这样就可以在应用中调用service接口来进行业务处理。

  • service层的业务实,具体要调用已经定义的dao层接口,封装service层业务逻辑有利于通用的业务逻辑的独立性和重复利用性。程序显得非常简洁。

controller层

  • Controler负责请求转发,接受页面过来的参数,传给Service处理,接到返回值,再传给页面。

  • controller层负责具体的业务模块流程的控制,在此层要调用service层的接口来控制业务流程,控制的配置也同样是在Spring的配置文件里进行,针对具体的业务流程,会有不同的控制器。

  • 具体的设计过程可以将流程进行抽象归纳,设计出可以重复利用的子单元流程模块。这样不仅使程序结构变得清晰,也大大减少了代码量。

关系

  • Service层是建立在DAO层之上的,建立了DAO层后才可以建立Service层,而Service层又是在Controller层之下的,因而Service层应该既调用DAO层的接口,又要提供接口给Controller层的类来进行调用,它刚好处于一个中间层的位置。每个模型都有一个Service接口,每个接口分别封装各自的业务处理方法。

Servlet

  • Java Servlet 是运行在Web服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

Servlet主要执行以下任务

  • 读取读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。
  • 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等
  • 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。
  • 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
  • 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。

@Bean注解

  • Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理
  • @ComponentScan 是 Spring 框架中的一个注解,用于自动扫描并注册带有 @Component、@Service、@Repository 和 @Controller 等注解的类为 Spring Bean。这样可以避免手动在 XML 配置文件中声明 Bean,使配置更加简洁和灵活。

0x01 MVC

  • M 是指业务模型(Model):用于封装数据传递的实体类
  • V 是指用户界面(View):一般是指前端页面
  • C 是控制器(Controller):控制器相当于Servlet的基本功能,处理请求,返回响应

image-20241117204028989

Spring MVC是将三者之间进行解耦,实现职能划分,互不干扰,最后将View和Model进行渲染,得到最终的页面并返回给前端。

0x02 搭建项目

全注解配置形式

使用纯注解开发,可以直接添加一个类,Tomcat会在类路径中查找实现ServletContainerInitializer接口的类,如果发现就用该类来配置Servlet容器,Spring提供了这个接口的实现类 SpringServletContainerInitializer,通过@HandlesTypes(WebApplicationInitializer.class)设置,这个类会反过来查找实现WebApplicationInitializer的类,并将配置任务交给他们来实现,因此直接实现接口即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{WebConfiguration.class}; // 基本的Spring配置类,一般用于业务层配置
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[0]; // 配置DispatcherServlet的配置类,主要用于Controller等配置
}

@Override
protected String[] getServletMappings() {
return new String[]{"/"}; // 匹配路径,与上面一致
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// WebConfiguration 的实现
@EnableWebMvc // 快速配置SpringMVC注解,如果不添加此注解会导致后续无法实现WebMvcConfigurer接口进行自定义配置
@Configuration
@ComponentScan("com.demo.controller") // 扫描
//还可以这样使用
//@ComponentScans(value = {
// @ComponentScan("com.demo.controller"),
// @ComponentScan("com.demo.entity")
//})
public class WebConfiguration {

}
1
2
3
4
5
6
7
8
9
10
// Controller 
@Controller
public class HelloController {

@ResponseBody
@RequestMapping("/") // 请求映射
public String hello() {
return "Hello World";
}
}

0x03 Controller控制器

image-20241118215707486

当一个请求经过DispatcherServlet之后,会先走HandlerMapping,它会将请求映射为HandlerExecutionChain,依次经过HandlerInterceptor,类似于过滤器,在SpringMVC中使用的是拦截器,然后再交给HandlerAdapter,根据请求的路径选择合适的控制器进行处理,控制器处理完成后,会返回一个ModelAndView对象,包括数据模型和视图,即页面中的数据和页面本身。

返回ModelAndView之后,会交给ViewResolver(视图解析器)进行处理,视图解析器会对整个视图页面进行解析,SpringMVC自带一些视图解析器,也可以使用Thymeleaf作为视图解析器,就可以根据给定的视图名称直接读取HTML编写的页面解析为一个真正的View

解析完成后,需要将页面中的数据全部渲染到View中,最后返回给DispatcherServlet一个包含所有数据的成形页面,再响应给浏览器,完成整个过程。

配置视图解析器和控制器

首先需要实现最基本的页面解析并返回,第一步就是配置视图解析器(这里使用Thymeleaf提供视图解析器)

1
2
3
4
5
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 视图解析器配置
@EnableWebMvc // 快速配置SpringMVC注解,如果不添加此注解会导致后续无法实现WebMvcConfigurer接口进行自定义配置
@Configuration
@ComponentScan("com.demo.controller")
public class WebConfiguration {

// 需要使用ThymeleafViewResolver作为视图解析器,并解析HTML页面
@Bean
public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
thymeleafViewResolver.setOrder(1);
thymeleafViewResolver.setTemplateEngine(templateEngine);
thymeleafViewResolver.setCharacterEncoding("UTF-8");
return thymeleafViewResolver;
}

// 配置模板解析器
@Bean
public SpringResourceTemplateResolver springResourceTemplateResolver() {
SpringResourceTemplateResolver springResourceTemplateResolver = new SpringResourceTemplateResolver();
springResourceTemplateResolver.setSuffix(".html"); // 需要解析的后缀名称
springResourceTemplateResolver.setPrefix("classpath:");// 需要解析的HTML页面文件存放的位置,默认是webapp目录下, 如果是类路径下需要添加 classpath: 前缀
return springResourceTemplateResolver;
}


// 配置模板引擎Bean
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
}

现在完成了视图解析器的配置,然后创建一个Controller,只需在一个类上添加一个 @Controller注解即可,它会被Spring扫描并自动注册为Controller类型的Bean,只需在类中编写方法用于处理对应地址的请求即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
public class HelloController {

@ResponseBody
@RequestMapping("/")
public String hello() {
return "Hello World";
}

@RequestMapping("/index")
public ModelAndView index() {
ModelAndView modelAndView = new ModelAndView("index");
modelAndView.getModel().put("name", "soyo"); // 将name 传递给Model
return modelAndView;
}
public String index(Model model) { // 同样可以实现
model.addAttribute("name", "Hello World");
return "index";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<!--对应HTML-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>Nagasaki Never Regret</p>
<p th:text="${name}"></p>
</body>
</html>

此外,页面中可能还会包含一些静态资源,比如js,css。为了让静态资源通过Tomcat提供的默认Servlet进行解析,需要让配置类实现WebMvcConfigurer接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WebConfiguration implements WebMvcConfigurer {

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable(); // 开启默认的Servlet
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置静态资源的访问路径
registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/statics/");
}
}

image-20241118233542720

以上代码的项目结构

0x04 注解

4.1 请求映射

@RequestMapping 是将请求和处理请求的方法建立一个映射关系,当收到请求时就可以根据映射关系调用对应的请求处理方法,注解定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Reflective({ControllerMappingReflectiveProcessor.class})
// 以上注解为 元注解,制定了注解的适用范围、保留策略、是否包含在文档中以及映射处理器
public @interface RequestMapping {
String name() default "";

@AliasFor("path") // 提供别名的注解
String[] value() default {};

@AliasFor("value") // 提供别名的注解
String[] path() default {};

RequestMethod[] method() default {};

String[] params() default {};

String[] headers() default {};

String[] consumes() default {};

String[] produces() default {};
}

其中最关键的是path 属性(等价于value),它决定了当前方法处理的请求路径(路径必须全局唯一),任何路径只能有一个方法进行处理,它是一个数组,也即此方法不仅仅可以用于处理某一哦请求路径,可以使用此方法处理多个请求路径。

此外,还可以直接将@RequestMapping 添加到类名上,表示为此类中所有请求添加一个路径前缀,例如:

1
2
3
4
5
6
7
8
@Controller
@RequestMapping("/Nagasaki")
public class MyGoController {
@RequestMapping("/soyo")
public String name(){
return "soyo";
}
}

路径还支持使用通配符进行匹配:

  • ? :表示任意一个字符,比如 @RequestMapping("/index/?") 可以匹配 /index/a 等
  • * :表示任意0-n个字符,比如 @RequestMapping("/index/*") 可以匹配 /index/demo 等
  • ** :表示当前目录或基于当前目录的多级目录,比如@RequestMapping("/index/**") 可以匹配 /index/demo/a 等

设置请求方法:

  • @RequestMapping(value="/index", method=RequestMethod.POST)
  • @PostMapping@GetMapping

可以使用 params 属性来指定请求必须携带哪些参数

1
2
3
4
@RequestMapping(value = "/index", params = {"id"})
public String index() {
return "index";
}
  • 可以在参数之前添加 ! 表示请求不允许添加此参数

    1
    2
    3
    4
    5
    @RequestMapping(value = "/index", params = {"id", "!userid"})
    public String index() {
    return "index";
    }
    // 请求参数不能包含 userid
  • 也可以直接设定不允许一个固定值

    1
    2
    3
    4
    5
    @RequestMapping(value = "/index", params = {"id!=0", "token=1"})
    public String index() {
    return "index";
    }
    // 表示请求参数 id 不允许为 0 , 并且 token 必须为 1

header 属性用法与 params 一致,它要求请求头中必须携带什么内容,比如:

1
2
3
4
5
@RequestMapping(value = "/index", header = "!Connection")
public String index() {
return "index";
}
// 如果请求头中携带了 Connection 属性,则无法访问
  • consumes:指定处理请求的提交内容类型(Content-Type),例如 application/json
  • produces:指定返回的内容类型,仅当 request 请求头中的(Accept)类型中包含该指定类型才返回

4.2请求参数获取

@RequestParam@RequestHeader 用法基本一致 ,仅演示前者

获取到请求中的参数,只需为方法添加一个形式参数,并在形式参数前面添加@RequestParam注解即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("/test")
public ModelAndView test(@RequestParam("username") String username) {
System.out.println("接收到的参数为:" + username);
return new ModelAndView("test");
}
// 在@RequestParam 中填写参数名称,参数的值会自动传递给形式参数,可以直接在方法中使用
// 一旦添加 @RequestParam 那么此请求必须携带指定参数,也可以将require属性设定为false来将属性设定为非必须
@RequestMapping("/test")
public ModelAndView test(@RequestParam("username", required = false) String username) {
System.out.println("接收到的参数为:" + username);
return new ModelAndView("test");
}
// 还可以直接设定一个默认值,当请求参数缺失时,可以直接使用默认值
@RequestMapping("/test")
public ModelAndView test(@RequestParam("username", required = false, defaultValue = "soyo") String username) {
System.out.println("接收到的参数为:" + username);
return new ModelAndView("test");
}

@CookieValue@SessionValue

1
2
3
4
5
6
7
@RequestMapping(value = "cookie")
public ModelAndView cookie(HttpServletResponse response, @CookieValue(value = "test", required = false) String test) {
System.out.println("获取到的Cookie值为:" + test);
response.addCookie(new Cookie("test", "das"));
return new ModelAndView("index");
}
// session 用法一致

4.3重定向和请求转发

1
2
3
4
5
// 重定向
@GetMapping("ret")
public String ret() {
return "redirect:index";
}

image-20241120232315475

1
2
3
4
5
// 请求转发
@GetMapping("/ret2")
public String ret2() {
return "forward:/index";
}

image-20241120232640673

区别:

  • 重定向:302跳转到对应的映射
  • 请求转发:内部转发请求,交给其他映射处理,浏览器地址不变

4.4 Bean的web作用域

Spring MVC中,Bean的作用域被分为:

  • request:对于每次HTTP请求,使用request作用域定义的Bean都将产生一个新实例,请求结束后Bean也消失
  • session:对于每一个会话,使用session作用域定义的Bean都将产生一个新实例,会话过期后Bean消失
  • global session:。。。
1
2
3
4
5
@Component
@SessionScope
public class Test {

}
1
2
3
4
5
6
7
@Autowired
Test test;
@ResponseBody // 返回
@GetMapping("/bean")
public String bean() {
return test.toString();
}

image-20241120235617747

@Data 是 Lombok 库提供的一个非常有用的注解,它可以大大减少样板代码的编写。@Data 注解会自动生成以下内容:

  • Getter 和 Setter 方法:为类中的每个字段生成 getter 和 setter 方法。
  • toString 方法:生成一个 toString 方法,包含类的所有字段。
  • equals 和 hashCode 方法:生成 equals 和 hashCode 方法,基于类的所有字段。
  • requiredArgsConstructor:生成一个包含所有 final 字段和 @NonNull 字段的构造函数。

@Autowired 用于自动装配依赖。它可以在字段、构造函数、setter方法或者其他方法上使用,Spring容器会自动将匹配的 Bean 注入到标注了 @Autowired 的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 有一个 UserService 和一个 UserController,UserController 需要使用 UserService:

@Service
public class UserService {
public void getUserInfo() {
System.out.println("获取用户信息");
}
}

@Controller
public class UserController {

@Autowired
private UserService userService;

public void handleRequest() {
userService.getUserInfo();
}
}
  • Spring 容器初始化:Spring 容器启动时,会扫描带有 @Component, @Service, @Controller 等注解的类,并创建相应的 Bean
  • 创建 UserService 实例:Spring 容器根据 @Service 注解创建 UserService 实例
  • 创建 UserController 实例:Spring 容器根据 @Controller 注解创建 UserController 实例
  • 自动装配 UserService 到 UserController:Spring 容器通过 @Autowired 注解将 UserService 实例注入到 UserController 中
  • 结束:完成所有 Bean 的创建和依赖注入,Spring 容器准备就绪,可以处理请求

4.5 RestFul风格

一种设计风格,主要作用时充分并正确利用HTTP协议的特定,规范资源获取的URI路径。RESTful风格的设计允许将参数通过URL拼接传到服务端,目的是让URL看起来更简洁实用,并且让我们充分实用多种HTTP请求方式,来执行相同请求地址的不同操作类型

这种风格的链接,可以直接从请求路径中读取参数:

1
http://localhost:8080/mvc/index/123

可以直接将 index 的下一级路径作为请求参数进行处理,即现在的请求参数包含在了请求路径中:

1
2
3
4
5
@RequestMapping("index/{str}")
public String index(@PathVariable("str") String str) {
System.out.println(str);
return "Hello world";
}

0x05 Interceptor拦截器

拦截器在Servlet与RequestMapping之间,相当于DispatcherServlet在将请求交给对应Controller中的方法之前进行拦截处理,它只会拦截所有Controller中定义的请求映射对应的请求(不会拦截静态资源)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
1
2
3
4
5
6
7
8
9
//  注册拦截器
// WebConfiguration

@Override
public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
registry.addInterceptor(new MainInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/exclude"); // 忽视
}

image-20241121225506369

拦截器处理前和处理后,包含了真正的请求映射的处理,在整个流程结束后还执行了一次afterCompletion方法。

多级拦截器和 类 的处理方式类似,也可用 .order(Int) 来控制拦截器 优先级。

0x06 自定义异常处理

当请求映射方法中出现异常时,会直接展示在前端页面,这是因为Spring MVC提供了默认的异常处理页面,当出现异常时,请求会被直接转交给专门用于异常处理的控制器进行处理。

可以自定义异常处理器,一旦出现指定异常,就会直接转接到此控制器执行。

1
2
3
4
5
6
7
8
public class ErrorController {
@ExceptionHandler(Exception.class)
public String error(Exception e, Model model){
e.printStackTrace();
model.addAttribute("err", e);
return "500";
}
}

0x07 Axios请求

image-20241121232920665

前端异步请求,因为网页是动态的,点击按钮、跳转新的页面、更新页面数据都是通过JS完成异步请求实现。

前端异步请求是指在前端中发送请求至服务器或其他资源,并不阻塞用户界面或其他操作。在传统的同步请求中,当发送请求时,浏览器会等待服务器响应,期间用户无法进行其他操作。异步请求通过将请求发送到后台,等待响应时,允许用户进行其他操作。这种机制能够提升用户体验,并且允许页面进行实时更新。

常见的前端异步请求方式包括实用HMLHttpRequest对象,Fetch API以及实用jQuery中的AJAX方法以及目前最常用的Axios框架