Spring

IOC原理

待填坑

AOP原理

待填坑

依赖注入的方式

对比三种依赖注入方式

特性 构造器注入 Setter 注入 字段注入
注入方式 通过构造器 通过 Setter 方法 直接注入到字段
依赖是否可选 必须注入,强制依赖 可选依赖 必须注入
是否可变 不可变,依赖一旦注入不可修改 可变,可以随时修改依赖 不可变
推荐程度 高,推荐的注入方式 中,适合可选依赖 低,不推荐使用
测试友好性 高,容易进行依赖替换和测试 高,容易进行依赖替换和测试 低,不容易进行依赖替换和测试
代码简洁性 需要构造器,代码稍多 需要额外的 Setter 方法 简洁,不需要构造器或 Setter

Spring中ApplicationContext和BeanFactory的区别

总结

特性 BeanFactory ApplicationContext
基础功能 提供基本的依赖注入功能和 Bean 管理 扩展了 BeanFactory,支持更多企业级功能
Bean 加载时机 按需加载(懒加载) 启动时预实例化单例 Bean
事件机制 不支持事件发布和监听 支持事件机制,例如 ApplicationEvent
国际化支持 不支持 支持国际化(MessageSource
资源访问 不统一 通过 getResource() 方法统一访问资源
生命周期管理 需要手动注册 BeanPostProcessor 自动处理 Bean 生命周期,自动注册处理器
扩展功能 功能较为基础,扩展性有限 支持 AOP、事务管理、扩展点更多

Autowired和Resource区别

@Autowired 注解

@Autowired 是 Spring 框架提供的注解,默认情况下它使用 byType 的方式进行依赖注入。

特点:

  • 默认按类型注入(byType)@Autowired 会根据类型查找匹配的 Bean。如果找到多个相同类型的 Bean,Spring 会抛出异常。
  • 可选按名称注入:可以配合 @Qualifier 注解使用,通过指定 Bean 的名称进行注入。
  • 可以用于构造器、字段、setter方法
  • 自动注入:如果没有找到对应的 Bean,且 required = true,程序会抛出异常。可以通过设置 required = false,避免异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class UserService {

// 按类型注入,若有多个同类型的Bean,可以使用@Qualifier指定
@Autowired(required = true)
private UserDao userDao;

// 也可以通过构造器注入
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void performService() {
System.out.println("Performing service with " + userDao.getData());
}
}

@Resource 注解

@Resource 是来自 Java EE(JDK) 的注解,它在 Spring 框架中也可以用来进行依赖注入。与 @Autowired 不同,@Resource 默认是 按名称(byName) 进行注入的。

特点:

  • 默认按名称注入(byName)@Resource 会根据属性名称进行注入。如果名称没有匹配的 Bean,则会再尝试按类型注入。
  • 可以用于字段、setter 方法,但不能用于构造器注入。
  • 不支持 @Qualifier,因此不能像 @Autowired 那样灵活地指定 Bean。
1
2
3
4
5
6
7
8
9
10
11
@Component
public class UserService {

// 默认按名称注入,如果找不到名称匹配的Bean,会按类型注入
@Resource(name = "userDao")
private UserDao userDao;

public void performService() {
System.out.println("Performing service with " + userDao.getData());
}
}

区别

特性 @Autowired @Resource
来源 Spring 框架 JSR-250 标准(Java 规范)
注入方式默认 按类型(byType) 按名称(byName)
注入方式可选 配合 @Qualifier 可按名称注入 默认按名称,找不到时按类型
使用场景 构造器、字段、setter 方法 字段、setter 方法
依赖是否必须 默认 required = true,可设置为 false 没有 required 属性,必须找到匹配的 Bean
灵活性 更灵活,支持按类型和按名称注入 更直接,主要按名称注入
支持构造器注入

Spring框架单例Bean是线程安全的?

Spring 框架中,默认的 Bean 作用域是 单例作用域singleton),这意味着在整个 Spring 容器内,一个 Bean 的实例只有一个,所有对该 Bean 的引用都指向同一个对象实例。然而,单例 Bean 并不是线程安全的。下面我们详细分析为什么单例 Bean 不是线程安全的,以及如何处理多线程环境中的单例 Bean。

1. 为什么 Spring 的单例 Bean 不是线程安全的?

单例作用域意味着 Spring 容器中每个 Bean 只有一个实例,所有对该 Bean 的调用都是共享同一个实例。在多线程环境下,多个线程可能会同时访问这个单例 Bean 的方法或共享的状态。这时,如果该单例类中包含了可变状态或者非线程安全的操作,就会导致线程安全问题。

1
2
3
4
5
6
7
8
9
10
@Component
public class NonThreadSafeService {

private int counter = 0;

public void incrementCounter() {
counter++;
System.out.println("Counter: " + counter);
}
}

在上面的 NonThreadSafeService 类中,counter 是一个共享的可变状态。如果单例 Bean 在多线程环境中被多个线程同时调用 incrementCounter() 方法,可能会出现并发访问问题,导致 counter 的值不准确或出现数据竞争。

问题原因:

  • 多个线程共享一个实例:由于单例模式的特性,多个线程会共享同一个 Bean 实例,如果该实例包含可变状态,不同线程对该状态的修改就可能产生冲突。
  • 非线程安全的操作:如果 Bean 的方法中存在对共享状态的修改操作,而没有适当的同步或并发控制,就会导致线程安全问题。

2. 什么时候单例 Bean 是线程安全的?

单例 Bean 只有在以下情况下是线程安全的:

  • 无状态:如果单例 Bean 是无状态的,也就是说它不包含任何可变的实例变量,那么它是线程安全的。无状态 Bean 通常是纯函数,即它们的行为完全取决于输入参数,而不依赖于类的内部状态。
  • 只读状态:如果单例 Bean 中的状态是不可变的(即只读),并且 Bean 的方法只执行读操作而不进行写操作,那么它也是线程安全的。
1
2
3
4
5
6
7
@Component
public class ThreadSafeService {

public void performTask() {
System.out.println("Performing a task in a thread-safe manner");
}
}

上面的 ThreadSafeService 类是线程安全的,因为它不包含任何可变的实例变量,也不依赖于共享状态。

3. 如何处理 Spring 单例 Bean 的线程安全问题?

如果单例 Bean 需要在多线程环境中共享数据或执行某些非线程安全的操作,开发者需要采取一些措施来确保线程安全。

  1. 避免共享可变状态

最简单的解决方案是避免在单例 Bean 中使用可变状态。如果某个 Bean 需要存储可变的状态,应该将这些状态限制在方法内部,而不是作为类的实例变量。

  1. 使用局部变量

局部变量是线程安全的,因为每个线程都有自己的方法栈,方法中的局部变量不会被其他线程共享。

1
2
3
4
5
6
7
8
9
@Component
public class SafeService {

public void performTask() {
int localCounter = 0; // 局部变量是线程安全的
localCounter++;
System.out.println("Local Counter: " + localCounter);
}
}
  1. 引入同步机制

如果必须在单例 Bean 中共享可变状态,可以使用同步机制来确保线程安全,例如 synchronized 关键字、ReentrantLock 等。

1
2
3
4
5
6
7
8
9
10
@Component
public class SynchronizedService {

private int counter = 0;

public synchronized void incrementCounter() {
counter++;
System.out.println("Synchronized Counter: " + counter);
}
}

上面的 incrementCounter() 方法使用了 synchronized 关键字,确保每次只有一个线程能够执行该方法,从而避免了并发问题。

  1. 使用并发工具类

Java 提供了许多并发工具类,比如 AtomicIntegerConcurrentHashMap 等,它们可以帮助你在多线程环境中安全地管理可变状态。

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class AtomicService {

private AtomicInteger counter = new AtomicInteger(0);

public void incrementCounter() {
int newValue = counter.incrementAndGet();
System.out.println("Atomic Counter: " + newValue);
}
}

在这个例子中,AtomicInteger 是线程安全的,并且提供了原子操作,避免了显式的同步。

  1. 使用 @Scope("prototype")

如果单例 Bean 中必须处理线程不安全的操作,或者 Bean 的某些方法中有大量的可变状态,可以考虑将 Bean 的作用域改为 prototype,这样每个线程都会获得该 Bean 的一个新的实例。

1
2
3
4
5
6
7
8
9
10
11
@Component
@Scope("prototype")
public class PrototypeService {

private int counter = 0;

public void incrementCounter() {
counter++;
System.out.println("Prototype Counter: " + counter);
}
}

prototype 作用域下,每次请求都会创建一个新的 PrototypeService 实例,因此不需要担心线程安全问题。

  1. 使用 ThreadLocal

ThreadLocal 是 Java 提供的一种机制,用于为每个线程提供独立的变量副本。通过使用 ThreadLocal,可以确保每个线程都有自己独立的变量,避免线程间共享数据导致的安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class ThreadLocalService {

private ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);

public void incrementCounter() {
int counter = threadLocalCounter.get();
counter++;
threadLocalCounter.set(counter);
System.out.println("ThreadLocal Counter: " + counter);
}
}

在这个例子中,threadLocalCounter 为每个线程维护了一个独立的 counter,因此不同线程不会共享这个变量。

总结

单例作用域singleton)是 Spring 的默认作用域,意味着在整个 Spring 容器中,只有一个 Bean 实例被共享。

单例 Bean 不是线程安全的,因为多个线程会共享同一个实例,如果该实例包含可变状态或非线程安全的操作,可能会导致并发问题。

线程安全的解决方案

  1. 避免共享可变状态。
  2. 使用局部变量。
  3. 使用同步机制(如 synchronizedReentrantLock)。
  4. 使用并发工具类(如 AtomicIntegerConcurrentHashMap)。
  5. 改变 Bean 的作用域为 prototype,保证每个线程都有自己的实例。
  6. 使用 ThreadLocal 为每个线程维护独立的变量。

Spring的Bean的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+--------------------+
| 创建Bean实例 | <-- 实例化 (Instantiation)
+--------------------+
|
v
+--------------------+
| 属性赋值 | <-- 属性赋值 (Populate Properties)
+--------------------+
|
v
+--------------------+
| 初始化操作 | <-- 初始化 (Initialization)
+--------------------+
|
v
+--------------------+
| 使用Bean | <-- 使用 (In Use)
+--------------------+
|
v
+--------------------+
| 销毁Bean | <-- 销毁 (Destruction)
+--------------------+

核心流程:

  1. 实例化:Spring 创建 Bean 实例,调用构造方法或工厂方法。
  2. 属性赋值:Spring 将配置的属性值或依赖注入到 Bean 中。
  3. 初始化:Spring 调用 Bean 的初始化方法,完成一些初始化操作。
  4. 使用:Bean 已经可以被应用程序使用,执行业务逻辑。
  5. 销毁:Spring 容器销毁 Bean,调用销毁方法,释放资源。

Spring支持Bean作用域

作用域 描述 适用场景
Singleton 在整个 Spring 容器中,Bean 实例是唯一的(默认作用域)。 适合共享状态的组件,如服务类、数据库连接池等。
Prototype 每次获取 Bean 时,都会创建一个新的实例。 适合无状态的对象或需要频繁创建销毁的对象。
Request 在一个 HTTP 请求范围内,Bean 实例是唯一的。 适合与 HTTP 请求相关的 Bean,如每个请求独立的业务逻辑处理类。
Session 在同一个 HTTP Session 范围内,Bean 实例是唯一的。 适合需要在会话期间保持状态的对象,如登录用户信息、购物车等。
Global Session 在全局 HTTP Session 中,Bean 实例是唯一的(主要用于 Portlet 应用)。 适合基于 Portlet 应用的全局 Session 共享对象。
1
2
3
4
5
@Bean
@Scope("singleton")
public MyService myService() {
return new MyService();
}

Spring事务

编程式事务

编程式事务需要开发人员手动编写代码来控制事务的开启、提交和回滚。

假设我们有一个简单的服务类 UserService,其中包含一个 saveUser 方法,用于保存用户信息。我们使用编程式事务来管理这个方法的事务。

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

public class UserService {

@Autowired
private DataSourceTransactionManager transactionManager;

@Autowired
private UserRepository userRepository;

public void saveUser(User user) {
// 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

// 开启事务
TransactionStatus status = transactionManager.getTransaction(def);

try {
userRepository.save(user);
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}

在这个示例中,我们手动获取了事务管理器 DataSourceTransactionManager,并在 saveUser 方法中手动开启了事务、提交事务或回滚事务。

声明式事务

声明式事务通过配置来控制事务的提交和回滚,开发人员只需使用注解或XML配置文件来声明事务的属性和行为即可。Spring框架会自动处理事务管理操作,这种方式减少了代码的侵入性,使代码更加简洁和易于维护。

示例:声明式事务(基于注解)

我们同样使用 UserService 类,但这次我们使用声明式事务来管理事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}

本质

  • 事务注解的本质是通过Spring AOP机制,在方法执行前后动态创建代理对象,将方法包装在一个事务中。事务注解在执行方法前开启一个事务,如果方法执行成功,则提交事务;如果方法执行过程中发生异常,则回滚事务。通过事务注解,我们可以在方法上标记事务的属性,方便灵活地控制事务的执行行为。同时,由于Spring框架在底层处理事务,因此可以避免一些常见的事务处理问题,比如并发访问、死锁等问题。事务注解的使用简单,可以提高开发效率,降低代码复杂度。

7 种事务传播级别

传播级别 描述 当前存在事务 当前不存在事务 适用场景
REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务(默认传播级别)。 加入现有事务 创建新事务 绝大多数场景,适合需要保证方法在事务中执行的情况。
SUPPORTS 支持当前事务,如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。 加入现有事务 非事务执行 适合对事务没有强制要求的情况。
MANDATORY 强制要求存在事务,如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 加入现有事务 抛出异常 适合必须在事务中执行的方法,如果没有事务则不允许执行。
REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则挂起当前事务,执行新事务。 挂起当前事务,创建新事务 创建新事务 适合需要独立于外部事务执行的方法,外部事务的回滚不会影响到该方法。
NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,则挂起当前事务。 挂起当前事务 非事务执行 适合不需要事务控制的操作,例如只读操作或性能要求高的操作。
NEVER 以非事务方式执行操作,如果当前存在事务,则抛出异常。 抛出异常 非事务执行 适合明确要求方法必须在非事务上下文中执行的场景。
NESTED 如果当前存在事务,则在嵌套事务内执行(可以独立回滚);如果当前没有事务,则类似于 REQUIRED 传播级别。 创建嵌套事务 创建新事务 适合需要部分回滚的场景,内外事务独立回滚,例如批处理操作。

Spring循环依赖及解决

例子:

1
2
3
4
5
6
7
8
9
10
11
@Component
class A {
@Autowired
private B b;
}

@Component
class B {
@Autowired
private A a;
}

1. Spring 的三级缓存机制

Spring 通过以下三个缓存来解决单例 Bean 的循环依赖:

  1. 一级缓存(singletonObjects):存储完全初始化好的 Bean。每当一个 Bean 完全构造好并完成依赖注入、初始化后,Spring 会将它放入一级缓存。
  2. 二级缓存(earlySingletonObjects):存储实例化了但还没有完全初始化的 Bean(即尚未完成依赖注入和初始化的 Bean)。这个缓存用于提前暴露 Bean 实例,防止循环依赖。
  3. 三级缓存(singletonFactories):存储 Bean 工厂(ObjectFactory),通过这个工厂,Spring 可以创建 Bean 实例的“引用”,并在需要时将半成品的 Bean 放入二级缓存。这也是三级缓存的主要作用,它允许在依赖注入过程中提前暴露 Bean 的引用,用于解决循环依赖。

工作流程

  • 当 Spring 容器在创建 Bean 时,如果发现这个 Bean 依赖另一个 Bean 还没有完全创建好,它会先在三级缓存中查找那个 Bean 的工厂,提前获取到 Bean 的引用,并把这个引用放到二级缓存中,从而避免循环依赖。

2. 通俗解释三级缓存的作用

可以把三级缓存的机制比作“造房子”的过程:

  • 一级缓存:房子完全建好了,可以住了。(Bean 已经完全初始化)
  • 二级缓存:房子正在装修,但已经有房子框架,可以暂时住人。(Bean 实例已经创建,但还没有完全初始化)
  • 三级缓存:有个房子设计图,虽然房子还没开始盖,但可以提前给别人看,别人知道这个房子以后会存在。(Bean 的工厂方法,能生成半成品 Bean)

当一个房子(Bean)A 在建造时,发现它需要另一个房子(Bean)B 的帮助,而 B 也在建造中。此时,A 可以先用 B 的设计图(三级缓存),然后等 B 搭好了框架(放到二级缓存),再继续建造。这就避免了循环等待。

3. 具体案例:Spring 如何处理循环依赖

让我们通过一个简单的代码示例来看看实际情况。

代码示例:

我们有两个 Bean,ServiceAServiceB,它们互相依赖。

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
@Component
public class ServiceA {

@Autowired
private ServiceB serviceB;

public ServiceA() {
System.out.println("ServiceA 构造方法");
}

public void doSomething() {
System.out.println("ServiceA 调用了 ServiceB");
serviceB.doSomething();
}
}

@Component
public class ServiceB {

@Autowired
private ServiceA serviceA;

public ServiceB() {
System.out.println("ServiceB 构造方法");
}

public void doSomething() {
System.out.println("ServiceB 调用了 ServiceA");
serviceA.doSomething();
}
}

在这个例子中,ServiceA 依赖 ServiceB,同时 ServiceB 也依赖 ServiceA,形成了循环依赖。

4. Spring 是如何解决这个问题的:

当 Spring 容器启动时,会按照以下步骤来实例化这些 Bean:

  1. 实例化 ServiceA
    • Spring 发现需要创建 ServiceA,于是调用 ServiceA 的构造方法(此时,ServiceA 还没有注入 ServiceB)。
    • Spring 将 ServiceA 的实例放入 三级缓存singletonFactories),就是说,Spring 暂时把 ServiceA 的设计图(它的工厂)放入缓存,表示虽然 ServiceA 还没完全构造好,但它将会存在。
  2. 实例化 ServiceB
    • 在创建 ServiceA 的过程中,Spring 发现 ServiceA 依赖 ServiceB,于是转而去创建 ServiceB
    • Spring 调用 ServiceB 的构造方法,创建 ServiceB 实例并将其放入 三级缓存,表示 ServiceB 也在建造过程中。
  3. 注入 ServiceAServiceB
    • 在构造 ServiceB 时,Spring 发现它依赖 ServiceA,于是去缓存中查找 ServiceA 的引用。
    • Spring 在三级缓存中找到 ServiceA 的工厂,并通过工厂获取到尚未完全初始化的 ServiceA 实例,将其放入 二级缓存earlySingletonObjects),表示 ServiceA 的框架已经搭好了,可以暂时使用这个半成品 ServiceA
  4. 注入 ServiceBServiceA
    • 接下来,Spring 继续完成 ServiceB 的创建,并将 ServiceB 的引用注入到 ServiceA 中。
    • 此时,ServiceB 已经完成创建,Spring 将完全实例化的 ServiceB 放入 一级缓存singletonObjects),表示 ServiceB 已经完全建造完成。
  5. 完成 ServiceA 的创建
    • Spring 回到 ServiceA 的创建过程中,发现 ServiceA 的依赖(ServiceB)已经准备好,于是将 ServiceB 的引用注入到 ServiceA 中。
    • 最后,ServiceA 也完成了初始化,Spring 将完全构造好的 ServiceA 放入 一级缓存,表示 ServiceA 也完成了建造。

5. 整个过程总结

  • 三级缓存的作用:当遇到循环依赖时,Spring 利用三级缓存机制,提前暴露 Bean 的引用(即半成品 Bean),避免了 Bean 之间的相互等待。
  • 一级缓存:存储完全构造好的 Bean。
  • 二级缓存:存储已经实例化但未完全初始化的 Bean。
  • 三级缓存:存储 Bean 的工厂,能够生成 Bean 的半成品引用,用来解决循环依赖。

通过这种机制,Spring 能够在单例模式下有效解决循环依赖的问题。注意,对于非单例(Prototype)Bean,Spring 无法自动解决循环依赖,因为每次依赖注入都会创建新的实例,无法提前暴露 Bean 的引用。

SpringMVC

流程

image-20241007215226501

(1)用户发送请求至前端控制器DispatcherServlet;
(2)DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handler;
(3)处理器映射器根据请求url找到具体的处理器Handler,生成处理器对象及处理器拦截器(如果有则生成),一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器,请求执行Handler;
(5)HandlerAdapter 经过适配调用 具体处理器进行处理业务逻辑;
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。

包括从 Servlet 接收到请求,到通过 Controller 处理请求,再到视图解析的过程。根据该流程,下面提供关键代码片段说明每个步骤。

1. Servlet 容器接收到请求

Servlet 容器首先会接收到 HTTP 请求,并交由 DispatcherServlet 处理。

1
2
3
4
5
6
7
8
@WebServlet("/")
public class MyDispatcherServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 请求交给 DispatcherServlet 处理
super.service(request, response);
}
}

2. DispatcherServlet 处理请求并找到对应的 Controller

DispatcherServlet 根据请求的路径,找到对应的 Controller 处理请求。Controller 会从请求中提取数据,然后执行业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
@RequestMapping("/user")
public class UserController {

@RequestMapping("/info")
public ModelAndView getUserInfo(@RequestParam("id") int userId) {
// 调用 Service 层获取用户数据
User user = userService.getUserById(userId);

// 返回 ModelAndView,包含视图名称和数据
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("userInfo");
modelAndView.addObject("user", user);

return modelAndView;
}
}

3. Controller 处理业务逻辑

在 Controller 中,通常会调用 Service 层来执行具体的业务逻辑,如数据库 CRUD 操作。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {

@Autowired
private UserRepository userRepository;

public User getUserById(int id) {
// 调用持久层,查询数据库
return userRepository.findById(id).orElse(null);
}
}

4. 返回 ModelAndView 供 View 解析

Controller 返回 ModelAndView,其中包含了视图的名称和模型数据。ViewResolver 会根据这个名称找到对应的视图模板进行渲染。

1
2
3
4
// ModelAndView 中包含视图名称 "userInfo" 和数据 "user"
ModelAndView modelAndView = new ModelAndView("userInfo");
modelAndView.addObject("user", user);
return modelAndView;

5. 视图解析并返回 Response

视图解析器根据返回的 ModelAndView 的视图名称来查找对应的视图(如 JSP 或 Thymeleaf 模板),渲染后生成 HTML 返回给客户端。

1
2
3
4
5
6
7
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}

通过上述代码,我们可以看到 Servlet 接收到请求后,经过 DispatcherServlet、Controller、Service 和 ViewResolver 的层层处理,最终产生 HTTP 响应并返回给客户端。

MyBatis

待填坑