Spring面试题总结

  1. 1. Spring
    1. 1.1 Spring 框架是什么?有什么优势?核心特性是什么?
    2. 1.2 什么是 Spring IoC 容器?
    3. 1.3 什么是依赖注入?
    4. 1.4 依赖注入有几种方式?
    5. 1.5 如何理解 IoC 和 DI?
    6. 1.6 什么是 AOP?
    7. 1.7 AOP 的实现方式有哪些?
    8. 1.8 Spring 框架中用到了哪些设计模式?
    9. 1.9 Spring 提供了哪些配置方式?
    10. 1.10 Spring 中的 Bean 是什么?
    11. 1.11 Bean 的作用域有哪些?
    12. 1.12 Bean 的生命周期有几个阶段?
    13. 1.13 详细讲一下 Bean 的生命周期是什么?
    14. 1.14 Bean 加载/销毁前后,如果想实现某些逻辑,可以怎么做?
    15. 1.15 BeanFactory 和 ApplicationContext 有什么区别?
    16. 1.16 什么是 Spring 事务管理模型?
    17. 1.17 Spring 的事务什么情况下会失效?
    18. 1.18 Spring 的事务,使用 this 调用是否生效?
  2. 2. Spring MVC
    1. 2.1 什么是 Spring MVC?工作原理是什么?
    2. 2.2 介绍一下 Spring MVC 的核心组件
    3. 2.3 HandlerMapping 和 HandlerAdapter 有了解吗?
  3. 3. Spring 注解
    1. 3.1 Spring 中常用的注解有哪些?
    2. 3.2 @Controller 和 @RestController 有什么区别?
    3. 3.3 @GetMapping、@PostMapping 和 @RequestMapping 有什么区别?
    4. 3.4 @RequestParam 和 @PathVariable 有什么区别?
    5. 3.5 详细讲一下 @Autowired 有什么用?
  4. 4. Spring Boot
    1. 4.1 为什么要用 Spring Boot?
    2. 4.2 Spring Boot 比 Spring 好在哪里?
    3. 4.3 Spring Boot 用到哪些设计模式?
    4. 4.4 怎么理解 Spring Boot 中的约定优于配置?
    5. 4.5 Spring Boot 的项目结构是怎么样的?
    6. 4.6 Spring Boot 自动装配原理是什么?
    7. 4.7 Spring Boot 中如何实现对不同环境的属性配置文件的支持?
    8. 4.8 如何理解 Spring Boot 中的 Starters?
    9. 4.9 Spring Boot Starters 的工作原理是什么?
    10. 4.10 介绍几个 Starter?
    11. 4.11 Spring Boot 的核心注解是什么?主要由哪几个注解组成?
    12. 4.12 Spring Boot 中如何使用 Bean?
    13. 4.13 RESTful 是什么?
  5. 5. Spring Cloud
    1. 5.1 什么是 Spring Cloud?和 Spring Boot 的区别是什么?
    2. 5.2 介绍一下用过哪些微服务组件?
    3. 5.3 详细介绍一下分布式项目与微服务架构
    4. 5.4 使用 Spring Cloud 有什么优势?
    5. 5.5 服务注册和发现是什么意思?Spring Cloud 如何实现?
    6. 5.6 负载均衡有什么作用?
    7. 5.7 负载均衡有哪些算法?
    8. 5.8 介绍一下服务熔断?
    9. 5.9 介绍一下服务降级?
    10. 5.10 Spring Cloud 和 Dubbo 有什么区别?
    11. 5.11 Sping Cloud 微服务之间如何通讯?
  6. 6. MyBatis
    1. 6.1 与传统的 JDBC 相比,MyBatis 的优点是什么?
    2. 6.2 MyBatis 在哪方面做的比较好?
    3. 6.3 JDBC 连接数据库的步骤是什么?
    4. 6.4 如果项目中要用到原生的 MyBatis 去查询,该怎样写?
    5. 6.5 MyBatis 里的 # 和 $ 区别是什么?
    6. 6.6 MyBatisPlus 和 MyBatis 的区别?
    7. 6.7 MyBatis 运用了哪些常见的设计模式?

Spring 面试题总结,涉及 Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis 等内容,文章将不断更新。

1. Spring

1.1 Spring 框架是什么?有什么优势?核心特性是什么?

Spring 框架是一个轻量级的 Java 开发框架,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的 JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发 Java 应用程序提供全面的基础架构支持。Spring 的最根本的使命是解决企业级应用开发的复杂性,即简化 Java 开发。

Spring 框架的优势主要有:

  • 方便解耦,简化开发:Spring 就是一个大工厂,可以将所有对象的创建以及依赖关系的维护交给 Spring 管理。
  • AOP 编程的支持:Spring 提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
  • 声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程。
  • 方便程序的测试:Spring 对 Junit4 支持,可以通过注解方便地测试 Spring 程序。
  • 方便集成各种优秀框架:Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis 等)的直接支持。
  • 降低 JavaEE API 的使用难度:Spring 对 JavaEE 开发中非常难用的一些 API(如 JDBC、JavaMail、远程调用等)都提供了封装,使这些 API 应用难度大大降低。

Spring 框架的核心特性为:

  • IoC 容器:Spring 通过控制反转实现了对象的创建和对象间的依赖关系管理,开发者只需要定义好 Bean 及其依赖关系,Spring 容器负责创建和组装这些对象。
  • AOP:面向切面编程,允许开发者定义横切关注点,例如事务管理、安全控制等独立于业务逻辑的代码。通过 AOP,可以将这些关注点模块化,提高代码的可维护性和可重用性。
  • 事务管理:Spring 提供了一致的事务管理接口,支持声明式和编程式事务,开发者可以轻松地进行事务管理,而无需关心具体的事务 API。
  • MVC 框架:Spring MVC 是一个基于 Servlet API 构建的 Web 框架,采用了模型-视图-控制器(MVC)架构。它支持灵活的 URL 到页面控制器的映射,以及多种视图技术。

1.2 什么是 Spring IoC 容器?

Spring IoC(Spring Inversion of Control)容器,是 Spring 框架的核心部分。IoC 即控制反转,是一种设计思想,在 Java 开发中,IoC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

Spring IoC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。具体来说,Spring IoC 容器负责创建对象、管理对象(通过依赖注入)、装配对象、配置对象,并且管理这些对象的整个生命周期。

使用 IoC 的目的,主要是为了降低类之间的耦合。通过控制反转,对象的创建和对象之间的依赖关系处理,交给 Spring 容器来管理,不用程序员自己创建和维护。这样,应用程序无需直接在代码中创建相关的对象,应用程序由 IoC 容器进行组装。这种方式不仅降低了类之间的耦合,也使得代码更加简洁,更易于测试和维护。

1.3 什么是依赖注入?

依赖注入(Dependency Injection,DI)是一种设计模式,也是 Spring 框架的核心概念之一。它的主要作用是去除 Java 类之间的依赖关系,实现松耦合,以便于开发测试。

在传统的程序设计过程中,当某个角色(可能是一个 Java 实例,调用者)需要另一个角色(另一个 Java 实例,被调用者)的协助时,通常由调用者来创建被调用者的实例。这种方式会导致调用者与被调用者之间产生紧密的耦合关系,使得代码难以修改和测试。

依赖注入的思想是,不在类内部直接创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给需要的类来使用。例如,A 类要依赖 B 类,A 类不再直接创建 B 类,而是把这种依赖关系配置在外部 XML(或 Java Config)文件中,然后由 Spring 容器根据配置信息创建、管理 bean 类。

这样,调用者不需要关心被调用者的创建和销毁,只需要关心如何使用被调用者,从而实现了调用者和被调用者之间的解耦。这种方式不仅降低了类之间的耦合,也使得代码更加简洁,更易于测试和维护。

举一个例子,加入我们有一个接口和两个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface MessageService {
String getMessage();
}

public class TextMessageService implements MessageService {
public String getMessage() {
return "Text Message";
}
}

public class EmailMessageService implements MessageService {
public String getMessage() {
return "Email Message";
}
}

然后,我们有一个使用 MessageServiceMessagePrinter 类:

1
2
3
4
5
6
7
8
9
10
11
12
public class MessagePrinter {
private MessageService service;

// 通过构造器注入依赖
public MessagePrinter(MessageService service) {
this.service = service;
}

public void printMessage() {
System.out.println(this.service.getMessage());
}
}

在 Spring 的配置文件中,我们可以定义 MessageServiceMessagePrinterbean,并通过构造器注入依赖:

1
2
3
4
5
6
7
<beans>
<bean id="messageService" class="com.example.TextMessageService"/>

<bean id="messagePrinter" class="com.example.MessagePrinter">
<constructor-arg ref="messageService"/>
</bean>
</beans>

在这个例子中,MessagePrinter 依赖于 MessageService。通过 Spring IoC 容器和依赖注入,我们可以在外部配置文件中定义这种依赖关系,而不需要在 MessagePrinter 类中硬编码依赖的实现类。这样,我们可以轻松地更改 MessageService 的实现,而无需修改 MessagePrinter 类的代码,这就是依赖注入的优势。

如果我们运行 MessagePrinterprintMessage() 方法,它将打印出 Text Message。这是因为我们在 Spring 的配置文件中将 TextMessageService 定义为 MessageService 的实现类,然后通过构造器注入的方式将其注入到了 MessagePrinter 中。所以,当我们调用 printMessage() 方法时,它会调用 TextMessageServicegetMessage() 方法。

总的来说,依赖注入是一种消除类之间依赖关系的设计模式,它使得对象之间的依赖关系更加清晰,代码更加灵活,更易于测试和维护。

1.4 依赖注入有几种方式?

在 Spring 中,有四种常见的依赖注入方式:

(1)属性注入(Field Injection):直接在需要注入的字段上使用 @Autowired@Resource 等注解:

1
2
3
4
5
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
}

(2)Setter 注入(Setter Injection):在 setter 方法上使用 @Autowired@Resource 等注解。在 SpringBoot 中,由于 WebSocket 的特殊性,它是由容器管理的,而不是由 Spring 管理的 Bean,每次 WebSocket 连接都会创建一个新的 WebSocket 实例(非单例模式),因此不能直接使用属性注入的方式来注入 WebSocket。假如我们要在 WebSocket 中使用 Mapper,那么我们就可以用 Setter 注入的方式:

1
2
3
4
5
6
7
8
9
10
@Component
@ServerEndpoint(value = "/websocket")
public class WebSocketServer {
private static UserMapper userMapper;

@Autowired
public void setUserMapper(UserMapper userMapper) {
WebSocketServer.userMapper = userMapper;
}
}

(3)构造器注入(Constructor Injection):在构造器上使用 @Autowired@Resource 等注解:

1
2
3
4
5
6
7
8
9
@Service
public class UserService {
private UserMapper userMapper;

@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
}

(4)静态工厂的方法注入:通过静态工厂方法创建 Bean,并在 Spring 配置文件中声明:

1
2
3
4
5
public class BeanFactory {
public static UserService createUserService() {
return new UserService();
}
}

在 Spring 配置文件中声明:

1
<bean id="userService" class="com.example.BeanFactory" factory-method="createUserService" />

1.5 如何理解 IoC 和 DI?

IoC(Inversion of Control,控制反转)和 DI(Dependency Injection,依赖注入)是 Spring 框架的核心概念,它们是面向对象编程的重要设计原则。IoC 和 DI 是同一个概念的不同角度描述,IoC 是一种设计思想,DI 是这种思想的一种实现方式。

IoC 是一种设计思想,不是一种技术。在 Java 开发中,IoC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。这意味着,所有的类都会在 Spring 容器中登记,告诉 Spring 你是什么,你需要什么,然后 Spring 会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 Spring 来控制,也就是说控制对象生存周期的不再是引用它的对象,而是 Spring。

DI 是 IoC 的一种实现方式。DI 是一种将调用者与被调用者分离的思想,组件之间的依赖关系由容器在运行时决定,形象的说,是由容器动态地将某个依赖关系注入到组件之中,这样你就可以使用 @Autowired@Resource 等注解来实现自动注入。

1.6 什么是 AOP?

AOP(Aspect Oriented Programming),即面向切面编程,是一种通过预编译方式和运行期动态代理实现程序功能的统一维护的技术。它是 OOP(面向对象编程)的延续,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。

AOP 的主要目标是将业务处理逻辑与系统服务分离开来,然后通过声明性的方式将系统服务应用到业务处理逻辑中。简单来说,AOP 就是把我们程序重复的代码抽取出来,在需要执行的时候使用动态代理技术在不修改源码的基础上,对我们的已有方法进行增强。

AOP 的主要术语包括:

  • 切面(Aspect):切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。
  • 连接点(JoinPoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
  • 切入点(PointCut):切入点是对连接点进行拦截的条件定义。
  • 通知(Advice):通知是指拦截到连接点之后要执行的代码,包括了 aroundbeforeafter 等不同类型的通知。
  • 目标对象(Target):目标对象指将要被增强的对象,即包含主业务逻辑的类对象。
  • 织入(Weaving):织入是将切面和业务逻辑对象连接起来,并创建通知代理的过程。

1.7 AOP 的实现方式有哪些?

  • 通过 Spring API 实现:这种方式的核心是通过编写增强类来继承 Spring API 提供的接口。例如,你可以编写业务接口和实现类,然后编写增强类,并实现 Spring API 相关接口的方法。然后在 resource 目录下新建 applicationContext 文件,实现 Java 类的创建和 AOP 的织入,最后编写测试类。
  • 通过自定义类来实现:这种方式比较推荐。你可以自定义切入类,然后在 Spring 中配置,最后编写测试类。
  • 使用注解实现:你可以自定义增强类(注解实现),然后在 Spring 配置文件中,注册 Bean,并增加支持注解的配置,最后编写测试类。
  • 使用 JDK 提供的代理方式:这种方式不依赖于 Spring。你可以使用 JDK 提供的代理方式来实现 AOP,包括静态和动态两种方式。
  • 使用 Spring 纯配置实现:你可以通过 Spring 的配置文件来实现 AOP。
  • 使用 Spring 注解:你可以通过 Spring 的注解来实现 AOP。
  • 动态代理和字节码增强:Spring AOP 的实现主要基于动态代理和字节码增强两种技术。动态代理是一种在运行时生成代理对象的技术,在代理对象中可以添加额外的逻辑,比如切面逻辑。Spring AOP 通过 JDK 动态代理和 CGLIB 动态代理两种方式实现代理对象的生成。

1.8 Spring 框架中用到了哪些设计模式?

(1)单例模式(Singleton):Spring 中的 Bean 默认都是单例的,这就是单例模式的应用。例如:

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}

在这个例子中,myService Bean 在整个应用中只有一个实例。在单例模式中,Spring容器会确保每个由 @Bean 注解定义的 Bean 在整个应用中只有一个实例。

(2)工厂模式(Factory):Spring 使用工厂模式通过 BeanFactoryApplicationContext 来创建 Bean。同单例模式的示例代码所示,在这个例子中,AppConfig 类就像一个工厂,myService() 方法就是工厂方法,用来创建 MyService 的实例。Spring 的 @Bean 注解同时实现了这两种模式。

(3)模板方法模式(Template Method):Spring 的 JdbcTemplateHibernateTemplate 等都是模板方法模式的应用。例如:

1
2
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.execute("CREATE TABLE CUSTOMERS (ID INTEGER, NAME VARCHAR(100))");

(4)代理模式(Proxy):Spring AOP 就是通过代理模式实现的。例如:

1
2
3
4
5
6
7
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.myapp.MyService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature());
}
}

在这个例子中,LoggingAspect 创建了一个代理,它在 MyService 的所有方法执行前打印日志。

(5)观察者模式(Observer):Spring 事件处理就是观察者模式的一个例子。当一个事件被发布时,所有注册的监听器都会收到通知。例如:

1
2
3
4
5
public class MyEventListener implements ApplicationListener<MyEvent> {
public void onApplicationEvent(MyEvent event) {
System.out.println("Received: " + event.getData());
}
}

(6)包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。

(7)适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller。

1.9 Spring 提供了哪些配置方式?

Spring 提供了以下三种主要的配置方式:

  • 基于 XML 的配置:在 Spring1.x 时代,都是基于 XML 来进行配置,用 XML 文件来管理 Bean 之间的关系。例如,你可以在 XML 文件中定义一个 Bean,然后在需要的地方引用这个 Bean。
  • 基于注解的配置:Spring2.5 以后开始支持用注解的方式来配置依赖注入。可以用注解的方式来替代 XML 方式的 Bean 描述,可以将 Bean 描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。例如,你可以使用 @Component 或其子类(@Repository@Service@Controller)来定义 Bean。
  • 基于 Java API 的配置:Spring3.0 以后,提供了 Java 配置的能力,Spring4.x和SpringBoot都推荐使用Java配置2。例如,你可以使用@Configuration和@Bean注解来定义和配置bean12。

注意 Spring 框架默认并不启用注解配置方式,你需要在配置文件中添加相应的配置才能启用注解配置方式:

1
2
3
4
5
6
<beans>
<!-- 启用注解配置 -->
<context:annotation-config/>

<!-- 其他bean的定义 -->
</beans>

<context:annotation-config/> 是 Spring 框架中的一个 XML 配置元素,用于启用注解驱动的 Spring 容器。它会自动扫描 Spring 容器中的所有组件,包括 @Service@Repository@Controller@Component 等注解标注的类,并将它们注册到 Spring 容器中。

也可以使用 <context:component-scan/>,如果使用了 <context:component-scan/>,那么 <context:annotation-config/> 就不再需要,因为 <context:component-scan/> 除了具有 <context:annotation-config/> 的功能之外,还可以在指定的 Package 下扫描以及注册 Java Bean。

1.10 Spring 中的 Bean 是什么?

在 Spring 中,构成应用程序主干并由 Spring IoC 容器管理的对象称为 Bean。Bean 是一个由 Spring IoC 容器实例化、组装和管理的对象,即 Spring 容器中存储的主要就是 Bean 对象。简而言之,Spring Bean 是 Spring 框架在运行时管理的对象,是任何 Spring 应用程序的基本构建块,我们编写的大多数应用程序逻辑代码都将放在 Spring Bean 中。Spring Bean 的管理包括:创建一个对象,提供依赖项(例如其他 Bean,配置属性),拦截对象方法调用以提供额外的框架功能,销毁一个对象。

1.11 Bean 的作用域有哪些?

  • singleton:单例模式,在整个 Spring IoC 容器中,使用 singleton 定义的 Bean 将只有一个实例。这是 Spring 的默认作用域。
  • prototype:原型模式,每次通过容器的 getBean() 方法获取 prototype 定义的 Bean 时,都将产生一个新的 Bean 实例。
  • request:对于每次 HTTP 请求,使用 request 定义的 Bean 都将产生一个新实例。只有在 Web 应用中使用 Spring 时,该作用域才有效。
  • session:对于每次 HTTP Session,使用 session 定义的 Bean 都将产生一个新实例。同样只有在 Web 应用中使用 Spring 时,该作用域才有效。
  • application:限定一个 Bean 的作用域为 ServletContext 的生命周期。该作用域仅适用于 Web 的 Spring WebApplicationContext 环境。
  • globalSession:全局 Session 作用域,仅在基于 portlet 的 Web 应用中才有意义,Spring5 已经没有了。

在 Spring 配置文件中,可以通过标签的 scope 属性来指定 Bean 的作用域。例如:

1
<bean id="myBean" class="com.example.MyBeanClass" scope="singleton"/>

在 Spring Boot 或基于 Java 的配置中,可以通过 @Scope 注解来指定 Bean 的作用域。例如:

1
2
3
4
5
@Bean
@Scope("prototype")
public MyBeanClass myBean() {
return new MyBeanClass();
}

1.12 Bean 的生命周期有几个阶段?

  • 实例化(Instantiation):Spring 根据配置文件或注解等方式创建 Bean 的实例。
  • 属性赋值(Populate):Spring 将实例化后的 Bean 的属性值设置到对应的属性中。
  • 初始化(Initialization):如果 Bean 实现了 InitializingBean 接口或在配置文件中通过 init-method 指定了初始化方法,则在 Bean 初始化完成后调用该方法。
  • 销毁(Destruction):如果 Bean 实现了 DisposableBean 接口或在配置文件中通过 destroy-method 指定了销毁方法,则在容器关闭时会调用该方法。

1.13 详细讲一下 Bean 的生命周期是什么?

Spring Bean 的生命周期是指一个 Bean 从被创建、初始化、使用到最终被销毁的整个过程。Spring 容器(ApplicationContextBeanFactory)负责管理这个生命周期。

Spring Bean 生命周期的详细阶段如下:

  1. Bean 定义加载与解析:Spring 容器启动时(如 ApplicationContext.refresh()),读取配置文件,将配置信息解析为内部的 BeanDefinition 对象,存储在 BeanFactory 的注册表中,BeanDefinition 包含了创建 Bean 所需的所有元数据(类名、作用域、初始化/销毁方法名、属性值、构造函数参数等)。

  2. 实例化(Instantiation):容器根据 BeanDefinition 的信息,创建 Bean 的实例。通常通过反射调用构造函数(Class.newInstance()Constructor.newInstance())来完成。对于工厂方法创建的 Bean,则调用指定的静态工厂方法或实例工厂方法。此时 Bean 只是一个“空壳”,属性未被设置,依赖未被注入。

  3. 属性赋值/依赖注入(Population of Properties):容器根据 BeanDefinition 中的配置,为 Bean 实例设置属性值,包括注入其他 Bean 的引用(@Autowired@Resource@Inject),注入基本类型或 String 等值,注入集合(ListSetMapProperties),解析和注入 @Autowired 标注的字段、Setter 方法或构造函数参数。

  4. Bean 后置处理器(BeanPostProcessor)- 初始化前(postProcessBeforeInitialization):如果容器中注册了实现了 BeanPostProcessor 接口的 Bean,容器会在每个 Bean 初始化之前调用其 postProcessBeforeInitialization(Object bean, String beanName) 方法,作用是在 Bean 初始化之前对 Bean 实例进行修改或包装(例如生成代理对象,如 AOP 代理通常在此阶段生成)。

  5. 初始化(Initialization):经过前置处理后,Bean 进入初始化阶段,容器会调用 Bean 上定义的初始化回调方法,作用是执行 Bean 创建后、使用前的自定义初始化逻辑,例如:建立数据库连接池、加载配置文件、验证依赖注入是否完整、启动后台线程、执行复杂的数据结构初始化。这些方法按固定顺序执行:

    • @PostConstruct 注解方法:由 JSR-250 规范定义。这是最常用、最推荐的方式。方法上标注 @PostConstruct
    • InitializingBean.afterPropertiesSet():Spring 特定接口。Bean 实现 InitializingBean 接口并覆写 afterPropertiesSet() 方法。不推荐使用,因为它将代码与 Spring 接口耦合。
    • 自定义 init-method:在配置中指定的方法(XML 的 init-method 属性,Java Config 的 @Bean(initMethod = "myInit"))。方法签名通常是 void xxx() 无参方法。
  6. Bean 后置处理器(BeanPostProcessor)- 初始化后(postProcessAfterInitialization):在 Bean 执行完自身的初始化方法后,容器会调用所有 BeanPostProcessorpostProcessAfterInitialization(Object bean, String beanName) 方法,作用是在 Bean 初始化之后对 Bean 实例进行最终的修改或增强,很多 Spring 的高级功能(如 AOP 的最终代理包装)在此阶段完成。

  7. Bean 就绪(Ready for Use):此时,Bean 已经完成了创建、依赖注入、初始化和所有后处理,完全初始化完毕,驻留在 Spring 容器(通常是单例池 singletonObjects)中,应用程序可以通过 getBean() 方法或依赖注入的方式获取并使用这个 Bean。

  8. 销毁(Destruction):当容器关闭时(例如调用 ApplicationContext.close() 或在 Web 应用中容器关闭),对于作用域为 singleton 且实现了销毁回调的 Bean,容器会触发销毁过程,作用是执行 Bean 销毁前的自定义清理逻辑,例如:关闭数据库连接池,释放连接、停止后台线程、保存状态到文件、释放文件句柄以及网络连接等资源。销毁回调的执行顺序与初始化相反:

    • @PreDestroy 注解方法:JSR-250 规范定义。最常用、最推荐。方法上标注 @PreDestroy
    • DisposableBean.destroy():Spring 特定接口。Bean 实现 DisposableBean 并覆写 destroy() 方法。不推荐使用(耦合)。
    • 自定义 destroy-method:配置中指定的方法(XML 的 destroy-method 属性,Java Config 的 @Bean(destroyMethod = "myDestroy"))。方法签名通常是 void xxx() 无参方法。

1.14 Bean 加载/销毁前后,如果想实现某些逻辑,可以怎么做?

在 Spring 框架中,如果希望在 Bean 加载(即实例化、属性赋值、初始化等过程完成后)或销毁前后执行某些逻辑,可以使用 Spring 的生命周期回调接口或注解。这些接口和注解允许你定义在 Bean 生命周期的关键点执行的代码。

(1)使用 init-methoddestroy-method

在 XML 配置中,你可以通过 init-methoddestroy-method 属性来指定 Bean 初始化后和销毁前需要调用的方法:

1
<bean id="myBean" class="com.example.MyBeanClass" init-method="init" destroy-method="destroy"/>

然后,在你的 Bean 类中实现这些方法:

1
2
3
4
5
6
7
8
public class MyBeanClass {
public void init() {
// 初始化逻辑
}
public void destroy() {
// 销毀逻辑
}
}

(2)实现 InitializingBeanDisposableBean 接口:

Bean 类可以实现 org.springframework.beans.factory.InitializingBeanorg.springframework.beans.factory.DisposableBean 接口,并分别实现 afterPropertiesSet()destroy() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class MyBeanClass implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 初始化逻辑
}

@Override
public void destroy() throws Exception {
// 销毀逻辑
}
}

(3)使用 @PostConstruct@PreDestroy 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class MyBeanClass {
@PostConstruct
public void init() {
// 初始化逻辑
}

@PreDestroy
public void destroy() {
// 销毀逻辑
}
}

(4)使用 @Bean 注解的 initMethoddestroyMethod 属性:

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "destroy")
public MyBeanClass myBean() {
return new MyBeanClass();
}
}

1.15 BeanFactory 和 ApplicationContext 有什么区别?

BeanFactoryApplicationContext 都是 Spring 框架中用于创建和管理 Bean 的容器。

BeanFactory 是 Spring 框架的最基本的容器,它是 Spring 的核心部分。BeanFactory 通过一个配置文件来管理和创建 Bean。BeanFactory 中的 Bean 是懒加载的,也就是说只有在调用 getBean() 方法去请求某个 Bean 时才会创建实例,这样可以提高程序的性能和启动速度,帮助我们节省资源。

ApplicationContextBeanFactory 的子接口,它是一个更加强大的容器。ApplicationContext 可以像 BeanFactory 一样创建和管理 Bean,但是它还可以提供其他的功能,比如支持国际化、事件传播、资源加载等。ApplicationContext 是在程序启动时就将所有的 Bean 全部实例化,因此在程序运行时可以直接获取已经创建好的 Bean,从而提高了程序的响应速度。

1.16 什么是 Spring 事务管理模型?

Spring 事务是 Spring 框架提供的一个核心功能,旨在简化在 Java 应用中管理数据库事务(以及扩展到其他支持事务的资源)的复杂性。它提供了一套声明式和编程式的事务管理模型,将事务管理逻辑从业务代码中分离出来,使开发者能更专注于核心业务逻辑。

事务(Transaction)是指一组数据库操作(如多个 SQL 语句)被视为一个单一的工作单元。这些操作要么全部成功提交(Commit),要么全部失败回滚(Rollback),这是保证数据一致性和完整性的关键机制。其具有 ACID 特性:

  • A(原子性,Atomicity):事务是不可分割的最小单元,要么全部执行,要么全部不执行。
  • C(一致性,Consistency):事务将数据库从一个一致状态转换到另一个一致状态。
  • I(隔离性,Isolation):并发执行的事务之间应该相互隔离,防止互相干扰。
  • D(持久性,Durability):一旦事务提交,它对数据库的改变就是永久性的,即使系统故障也不会丢失。

Spring 提供两种主要的事务管理方式:

  • 编程式事务管理(Programmatic Transaction Management):开发者直接在代码中通过 Spring 提供的 API 显式控制事务的边界(开始、提交、回滚)。优点为精细控制,事务边界清晰可见,缺点为事务管理代码侵入业务代码,代码重复度高,维护性稍差,适用于需要非常精细控制事务边界,或者声明式事务无法满足特定需求的情况。实现方式如下:
    • 使用 TransactionTemplate:这是最常用的编程方式。它封装了事务管理的样板代码(try-catch-finally),开发者只需在 execute() 方法内部实现需要事务的业务逻辑(通过 TransactionCallback 或 Lambda 表达式)。
    • 使用 PlatformTransactionManager:直接调用 getTransaction()commit()rollback() 方法,更底层,更灵活,但也更繁琐。
  • 声明式事务管理(Declarative Transaction Management):这是 Spring 推荐且最常用的方式,开发者通过配置(注解或 XML)来声明哪些方法(或类)需要事务支持以及事务的属性(传播行为、隔离级别等)。事务管理的具体操作(开启、提交、回滚)由 Spring 框架在运行时(通常基于 AOP 代理)自动完成。其具有非侵入性、事务管理与业务逻辑分离、配置灵活、易于维护、减少样板代码等优点,适用于绝大多数需要事务管理的情况,实现方式如下:
    • 基于 @Transactional 注解:将注解标注在方法或类上,标注在类上表示该类的所有 public 方法都应用该事务属性,方法上的注解会覆盖类上的注解,可以在注解中指定 propagationisolationtimeoutreadOnlyrollbackFornoRollbackFor 等属性。需要注意的是 @Transactional 生效依赖于 AOP 代理。这意味着调用必须是通过代理对象进行的(从 Spring 容器中获取的 Bean 已经是代理对象)。
    • 基于 XML 配置(<tx:advice><aop:config>):在 XML 文件中定义事务通知(<tx:advice>)并配置其属性(传播行为等),使用 AOP 切面配置(<aop:config>)将事务通知织入到指定的方法(通常通过切入点表达式 pointcut)。

1.17 Spring 的事务什么情况下会失效?

Spring Boot 通过 Spring 框架的事务管理模块来支持事务操作。事务管理在 Spring Boot 中通常是通过 @Transactional 注解来实现的。事务可能会失效的一些常见情况包括:

  1. 未捕获异常:如果一个事务方法中发生了未捕获的异常,并且异常未被处理或传播到事务边界之外,那么事务会失效,所有的数据库操作会回滚。
  2. 非受检异常:Spring 默认只在抛出 RuntimeException 及其子类或 Error 时回滚事务。如果方法抛出的是检查型异常(Checked Exception,如 IOExceptionSQLException)或其自定义非运行时异常,事务默认会提交!
  3. 事务传播属性设置不当:如果在多个事务之间存在事务嵌套,且事务传播属性配置不正确,可能导致事务失效。特别是在方法内部调用有 @Transactional 注解的方法时要特别注意。
  4. 多数据源的事务管理:如果在使用多数据源时,事务管理没有正确配置或者存在多个 @Transactional 注解时,可能会导致事务失效。
  5. 跨方法调用事务问题:如果一个事务方法内部调用另一个方法,而这个被调用的方法没有 @Transactional 注解,这种情况下外层事务可能会失效。
  6. 事务在非公开方法中失效:Spring 的事务管理(基于 AOP 代理或 AspectJ)默认只对 public 方法生效,如果 @Transactional 注解标注在私有方法上或者非 public 方法上,事务也会失效。

1.18 Spring 的事务,使用 this 调用是否生效?

在 Spring 中使用 this 关键字调用同一个类中的 @Transactional 方法,事务是不会生效的,这是 Spring 事务失效最常见的原因之一,被称为自调用问题。

Spring 的声明式事务管理 @Transactional 是通过 AOP(面向切面编程)实现的。当你在一个 Bean 上标注 @Transactional 时,Spring 容器会为该 Bean 创建一个代理对象(JDK 动态代理或 CGLIB 代理)。当外部代码(其他 Bean 的代码)调用这个 Bean 的事务方法时,实际上是调用了代理对象的方法。代理对象在调用目标方法(你写的原始方法)之前和之后,会插入事务管理的逻辑(开启事务、提交/回滚事务)。

当你在同一个 Bean 的一个非事务方法 A() 中,使用 this.事务方法B() 来调用本 Bean 的事务方法 B() 时,A() 在被调用时,确实是代理对象在拦截执行(如果 A() 本身有事务,事务会生效)。但在 A() 方法体内部,this 关键字指向的是目标对象本身(即你写的原始 Bean 实例),而不是 Spring 创建的代理对象。所以 this.B() 这个调用是直接发生在目标对象内部,完全绕过了代理对象。代理对象的事务拦截逻辑(TransactionInterceptor)根本没有机会介入到 B() 的调用过程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;

// 非事务方法
public void placeOrder(Order order) {
// ... 一些业务逻辑 ...
// 自调用事务方法 - 事务会失效!
this.deductInventory(order.getProductId(), order.getQuantity()); // 使用 this 调用
// ... 其他逻辑 ...
}

// 事务方法
@Transactional
public void deductInventory(Long productId, int quantity) {
// 更新库存 - 期望在事务中执行
productInventoryRepository.reduceStock(productId, quantity);
// 如果这里发生异常,期望事务回滚,库存恢复
}
}

如何解决自调用导致的事务失效呢?首选且推荐的最佳实践是将事务方法抽取到另一个独立的 Spring Bean 中,然后通过依赖注入进行调用。将需要事务管理的方法 B() 移动到另一个 Service 类中(例如 InventoryService),在原来的 Service(OrderService)中注入这个新的 InventoryService,然后在 A() 方法中调用 inventoryService.deductInventory(...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class InventoryService {
@Transactional
public void deductInventory(Long productId, int quantity) {
// ... 事务性库存扣减 ...
}
}

@Service
public class OrderService {
@Autowired
private InventoryService inventoryService; // 注入新 Bean

public void placeOrder(Order order) {
// ...
inventoryService.deductInventory(order.getProductId(), order.getQuantity()); // 调用其他Bean的方法
// ...
}
}

因为 inventoryService 是另一个 Spring Bean,对它方法的调用必然通过其代理对象,事务拦截器就能正常工作。

2. Spring MVC

2.1 什么是 Spring MVC?工作原理是什么?

Spring MVC 是 Spring 框架的一部分,它是一个基于 Java 的全功能 MVC Web 应用程序框架。MVC(Model-View-Controller)代表模型-视图-控制器,这是一种设计模式,用于将应用程序的数据访问、用户界面和业务逻辑分离开来。MVC 具体介绍如下:

  • 视图(View):为用户提供使用界面,与用户直接进行交互。
  • 模型(Model):代表一个存取数据的对象或 Java POJO(Plain Old Java Object,简单 Java 对象)。它也可以带有逻辑,主要用于承载数据,并对用户提交请求进行计算的模块。模型分为两类,一类称为数据承载 Bean,一类称为业务处理 Bean。所谓数据承载 Bean 是指实体类(如 User 类),专门为用户承载业务数据的;而业务处理 Bean 则是指 Service 或 Dao 对象,专门用于处理用户提交请求的。
  • 控制器(controller):用于将用户请求转发给相应的 Model 进行处理,并根据 Model 的计算结果向用户提供相应响应,它使视图与模型分离。

Spring MVC 的工作原理如下:

  • 用户发送请求:用户通过浏览器发送一个 HTTP 请求,直接请求到 DispatcherServlet
  • DispatcherServlet(前端控制器):请求被 Spring MVC 的 DispatcherServlet 捕获。DispatcherServlet 的作用类似于一个中央处理器,它会负责调用其他组件来处理请求。
  • HandlerMapping(控制器映射器):DispatcherServlet 会调用 HandlerMapping 解析请求对应的 Handler,即找出处理这个请求的 Controller
  • Controller(控制器):找到合适的 Controller 后,DispatcherServlet 会将请求交给它。Controller 是真正处理请求的地方,它会处理用户的请求,并返回一个 ModelAndView 对象。ModelAndView 包含了模型(Model)数据和视图(View)名称。
  • ViewResolver(视图解析器):DispatcherServlet 会把 ModelAndView 对象传给 ViewResolverViewResolver 会根据视图名称解析出真正的视图。
  • View(视图):最后,DispatcherServlet 会渲染视图,并把模型数据填充进去。这个视图就是最终呈现给用户的页面。

2.2 介绍一下 Spring MVC 的核心组件

Spring MVC 的核心组件主要包括以下几个:

  • DispatcherServlet(前端控制器):这是 Spring MVC 框架的核心,负责将请求路由到其他组件。它处理所有的 HTTP 请求和响应。
  • HandlerMapping(控制器映射器):它的任务是根据请求的 URL 找到正确的 Controller
  • Controller(控制器):这是应用程序的实际控制器,负责处理用户请求并返回一个模型和视图。
  • HandlerAdapter(控制器适配器):它负责调用 Controller 中的方法。
  • ViewResolver(视图解析器):它负责解析视图名并返回一个具体的视图对象。
  • View(视图):这是最终呈现给用户的页面。

除此之外,Spring MVC 还有一些其他的组件:

  • HandlerExceptionResolver(处理器异常解析器):它负责处理在 Controller 执行过程中抛出的异常。
  • LocaleResolver(区域解析器):它用于确定用户的区域,这对于国际化和本地化非常重要。
  • MultipartResolver(多部分解析器):它用于处理 multipart 请求,例如文件上传。
  • ThemeResolver(主题解析器):它用于确定应用程序的主题,这对于个性化布局非常有用。
  • RequestToViewNameTranslator(请求到视图名转换器):它用于在 Controller 没有明确返回视图名时,提供一个默认的视图名。
  • FlashMapManager(Flash 映射管理器):它用于存储和检索 FlashMap 模型,FlashMap 模型用于在重定向场景中存储属性。

2.3 HandlerMapping 和 HandlerAdapter 有了解吗?

(1)HandlerMapping

  • 作用:HandlerMapping 负责将请求映射到控制器(Controller)。
  • 功能:根据请求的 URL、请求参数等信息,找到处理请求的控制器。
  • 类型:Spring 提供了多种 HandlerMapping 实现,如 BeanNameUrlHandlerMapping、
    RequestMappingHandlerMapping 等。
  • 工作流程:根据请求信息确定要请求的控制器。HandlerMapping 可以根据 URL、请求参数等规则确定对应的控制器。

(2)HandlerAdapter

  • 作用:HandlerAdapter 负责调用控制器来处理请求。
  • 功能:控制器可能有不同的接口类型(Controller 接口、HttpRequestHandler 接口等),HandlerAdapter 根据接口类型来选择合适的方法调用控制器。
  • 类型:Spring 提供了多个 HandlerAdapter 实现,用于适配不同类型的控制器。
  • 工作流程:根据控制器的接口类型,选择相应的 HandlerAdapter 来调用控制器。

(3)工作流程

  1. 当客户端发送请求时,HandlerMapping 根据请求信息找到对应的控制器。
  2. HandlerAdapter 根据控制器接口的类型选择合适的方法来调用控制器。
  3. 控制器执行相应的业务逻辑,生成 ModelAndView。
  4. HandlerAdapter 将控制器的执行结果包装成 ModelAndView。
  5. 视图解析器根据 ModelAndView 找到对应的视图(View)进行渲染。
  6. 将渲染后的视图返回给客户端。

HandlerMapping 和 HandlerAdapter 协同工作,通过将请求映射到控制器,并调用控制器来处理请求,实现了请求处理的流程。它们的灵活性使得在 Spring MVC 中可以支持多种处理器和处理方式,提高了框架的扩展性和适应性。

3. Spring 注解

3.1 Spring 中常用的注解有哪些?

  • @Component:标注一个普通的 Spring Bean 类,当一个类被 @Component 注解标记时,Spring 会将其实例化为一个 Bean,并将其添加到 Spring 容器中。
  • @Configuration:用于标记一个类作为 Spring 的配置类。配置类可以包含 @Bean 注解的方法,用于定义和配置 Bean,作为全局配置:
1
2
3
4
5
6
7
@Configuration
public class MyConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
  • @Controller:标注一个控制器组件类,它是 @Component 注解的特例,用于标记控制层的 Bean。这是 MVC 结构的另一个部分,加在控制层:
1
2
@Controller
public class MyController {}
  • @Bean:用于标记一个方法作为 Spring 的 Bean 工厂方法。当一个方法被 @Bean 注解标记时,Spring 会将该方法的返回值作为一个 Bean,并将其添加到 Spring 容器中,如果自定义配置,经常用到这个注解。
  • @Service:标注一个业务逻辑层组件类,它也是 @Component 注解的特例,用于标记服务层的 Bean,一般标记在业务 Service 的实现类:
1
2
@Service
public class MyServiceImpl {}
  • @Repository:标注一个数据访问层组件类,它也是 @Component 注解的特例,用于标记数据访问层的 Bean。这个注解很容易被忽略,导致数据库无法访问。
  • @Autowired:由 Spring 提供的注解,用于自动装配 Bean,当 Spring 容器中存在与要注入的属性类型匹配的 Bean 时,它会自动将 Bean 注入到属性中。就跟我们 new 对象一样:
1
2
3
4
5
6
7
8
@Component
public class MyService {}

@Component
public class MyController {
@Autowired // Spring会自动将MyService类型的Bean注入到myService属性中
private MyService myService;
}
  • @RequestMapping:用于映射 Web 请求,包括访问路径和参数。
  • @ResponseBody:支持将返回值放在 response 内,而不是一个页面,通常用户返回 JSON 数据。
  • @RestController:该注解为一个组合注解,相当于 @Controller@ResponseBody 的组合,注解在类上,意味着,该 Controller 的所有方法都默认加上了 @ResponseBody
  • @ExceptionHandler:用于全局处理控制器里的异常。
  • @PathVariable:用于接收路径参数,比如 @RequestMapping("/hello/{name}") 申明的路径,将注解放在参数中前,即可获取该路径的 name 值,通常作为 RESTful 的接口实现方法。
  • @EnableAsync:在配置类中,通过此注解开启对异步任务的支持。
  • @Async:在实际执行的 Bean 方法使用该注解来申明其是一个异步任务。
  • @EnableScheduling:在配置类上使用,开启计划任务的支持。
  • @Scheduled:来申明这是一个任务,包括 cronfixDelayfixRate 等类型。

3.2 @Controller 和 @RestController 有什么区别?

(1)@Controller@Controller 注解表示该类是一个 Web 控制器,通常与 @RequestMapping 注解一起使用,用于处理 HTTP 请求。在 @Controller 中,我们可以返回一个视图(View),这在 Spring Web MVC 中非常常见。例如:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
@RequestMapping("/books")
public class BookController {
@GetMapping("/{id}")
public @ResponseBody Book getBook(@PathVariable int id) {
return findBookById(id);
}

private Book findBookById(int id) {
// ...
}
}

在这个例子中,BookController 类被标记为一个控制器,/books 是它的请求映射路径。getBook() 方法用于处理对 /books/{id} 路径的 GET 请求,其中 {id} 是路径变量。

(2)@RestController@RestController@Controller 的特化,它包含了 @Controller@ResponseBody 两个注解。这意味着,当一个类被 @RestController 注解标记后,该类的所有方法都会默认添加 @ResponseBody 注解。因此通常用于创建 RESTful Web 服务。例如:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/books")
public class BookRestController {
@GetMapping("/{id}")
public Book getBook(@PathVariable int id) {
return findBookById(id);
}

private Book findBookById(int id) {
// ...
}
}

在这个例子中,BookRestController 类被 @RestController 注解标记,因此不需要再每个请求处理方法上都添加 @ResponseBody 注解。

总的来说二者的主要区别在于,@Controller 通常用于处理返回视图的请求,而 @RestController 通常用于处理返回 JSON 或 XML 响应的请求。

3.3 @GetMapping、@PostMapping 和 @RequestMapping 有什么区别?

(1)@RequestMapping:这是一个通用的注解,可以处理所有类型的 HTTP 请求。你可以通过 method 属性来指定处理的 HTTP 方法类型(如 GET、POST 等)。例如:

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/users", method = RequestMethod.GET)
public Users getUsers() {
// ...
}

@RequestMapping(value = "/users", method = RequestMethod.POST)
public User createUser(User user) {
// ...
}

(2)@GetMapping:这是 @RequestMapping 的一个特化版本,用于处理 GET 请求,等价于 @RequestMapping(method = RequestMethod.GET)。例如:

1
2
3
4
@GetMapping("/users")
public Users getUsers() {
// ...
}

(3)@PostMapping:同样是 @RequestMapping 的一个特化版本,用于处理 POST 请求,等价于 @RequestMapping(method = RequestMethod.POST)。例如:

1
2
3
4
@PostMapping("/users")
public User createUser(User user) {
// ...
}

3.4 @RequestParam 和 @PathVariable 有什么区别?

(1)@RequestParam:用于从请求参数中提取值。例如,对于 URL:http://localhost:8080/books?id=1,你可以使用 @RequestParam 来获取 id 参数的值:

1
2
3
4
@GetMapping("/books")
public String getBook(@RequestParam("id") String id) {
// ...
}

(2)@PathVariable:用于从 URI 路径中提取值。例如,对于 URL:http://localhost:8080/books/1,你可以使用 @PathVariable 来获取 id

1
2
3
4
@GetMapping("/books/{id}")
public String getBook(@PathVariable("id") String id) {
// ...
}

3.5 详细讲一下 @Autowired 有什么用?

在 Spring 框架中,@Autowired 注解用于实现自动依赖注入。这意味着你不需要在代码中明确指定依赖关系,Spring 会自动为你完成这个工作,从而简化了代码并提高了可维护性。

@Autowired 注解可以应用于字段、构造器和方法:

(1)字段上的 @Autowired:当 @Autowired 注解应用于字段时,Spring 会在创建 Bean 时自动注入相应的依赖。在下面这个例子中,myService 字段会被自动注入一个 MyService 类型的 Bean:

1
2
@Autowired
private MyService myService;

(2)构造器上的 @Autowired:当 @Autowired 注解应用于构造器时,Spring 会在创建 Bean 时自动注入构造器的参数。在下面这个例子中,MyClass 的构造器会被自动注入一个 MyService 类型的 Bean:

1
2
3
4
5
6
7
8
class MyClass {
private MyService myService;

@Autowired
public MyClass(MyService myService) {
this.myService = myService;
}
}

(3)方法上的 @Autowired:当 @Autowired 注解应用于方法时,Spring 会在创建 Bean 时自动注入方法的参数。这通常用于 Setter 方法,但也可以用于其他任何方法。例如:

1
2
3
4
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}

4. Spring Boot

4.1 为什么要用 Spring Boot?

Spring Boot 是 Spring 框架的一个扩展,它的目标是简化 Spring 应用程序的配置和部署,Spring Boot 具有以下优势:

  • 快速开发:Spring Boot 通过提供一系列的开箱即用的组件和自动配置,简化了项目的配置和开发过程,开发人员可以更专注于业务逻辑的实现,而不需要花费过多时间在繁琐的配置上。例如,使用 Spring MVC 需要大量的 XML Bean 定义和自定义 servlet 类,但使用 Spring Boot 只需要添加一个 starter 依赖即可,无需任何代码生成或 XML 配置。
  • 快速启动:Spring Boot 提供了快速的应用程序启动方式,可通过内嵌的 Tomcat、Jetty 或 Undertow 等容器快速启动应用程序,无需额外的部署步骤,方便快捷。
  • 自动化配置:Spring Boot 通过自动配置功能,根据项目中的依赖关系和约定俗成的规则来配置应用程序,减少了配置的复杂性,使开发者更容易实现应用的最佳实践。
  • 有用的 Starters:Spring Boot Starters 是包含库和一些自动配置的 Maven 描述符。这些 Starters 可以为 Spring Boot 应用程序提供功能。例如,你想设置数据库连接,或者你想与消息队列进行通信或发送电子邮件,Spring Boot 都可以覆盖。
  • 嵌入式 Web 服务器:Spring Boot 提供了对嵌入式 Tomcat、Jetty 和 Undertow 服务器的开箱即用支持。这样,开发人员不必担心在传统的应用服务器中部署 Web 应用程序。
  • 丰富的 IDE 支持:所有主要的 IDE 都为 Spring Boot 提供了代码辅助支持。
  • 生产就绪的功能:Spring Boot 提供了开箱即用的监控、度量和日志记录功能。这些功能使开发人员可以避免额外的配置。

4.2 Spring Boot 比 Spring 好在哪里?

  • Spring Boot 提供了自动化配置,大大简化了项目的配置过程。通过约定优于配置的原则,很多常用的配置可以自动完成,开发者可以专注于业务逻辑的实现。
  • Spring Boot 提供了快速的项目启动器,通过引入不同的 Starter,可以快速集成常用的框架和库(如数据库、消息队列、Web 开发等),极大地提高了开发效率。
  • Spring Boot 默认集成了多种内嵌服务器(如 Tomcat、Jetty、Undertow),无需额外配置,即可将应用打包成可执行的 JAR 文件,方便部署和运行。

4.3 Spring Boot 用到哪些设计模式?

  • 代理模式:Spring 的 AOP 通过动态代理实现方法级别的切面增强,有静态和动态两种代理方式,采用动态代理方式。
  • 策略模式:Spring AOP 支持 JDK 和 CGLIB 两种动态代理实现方式,通过策略接口和不同策略类,运行时动态选择,其创建一般通过工厂方法实现。
  • 装饰器模式:Spring 用 TransactionAwareCacheDecorator 解决缓存与数据库事务问题增加对事务的支持。
  • 单例模式:Spring Bean 默认是单例模式,通过单例注册表(如 HashMap)实现。
  • 简单工厂模式:Spring 中的 BeanFactory 是简单工厂模式的体现,通过工厂类方法获取 Bean 实例。
  • 工厂方法模式:Spring 中的 FactoryBean 体现工厂方法模式,为不同产品提供不同工厂。
  • 观察者模式:Spring 观察者模式包含 Event 事件、Listener 监听者、Publisher 发送者,通过定义事件、监听器和发送者实现,观察者注册在 ApplicationContext 中,消息发送由 ApplicationEventMulticaster 完成。
  • 模板模式:Spring Bean 的创建过程涉及模板模式,体现扩展性,类似 Callback 回调实现方式。
  • 适配器模式:Spring MVC 中针对不同方式定义的 Controller,利用适配器模式统一函数定义,定义了统一接口 HandlerAdapter 及对应适配器类。

4.4 怎么理解 Spring Boot 中的约定优于配置?

约定优于配置是 Spring Boot 的核心设计理念,它通过预设合理的默认行为和项目规范,大幅减少开发者需要手动配置的步骤,从而提升开发效率和项目标准化程度。

理解 Spring Boot 中的“约定优于配置”原则,可以从以下几个方面来解释:

  • 自动化配置:Spring Boot 提供了大量的自动化配置,通过分析项目的依赖和环境,自动配置应用程序的行为。开发者无需显式地配置每个细节,大部分常用的配置都已经预设好了。例如,引入 spring-boot-starter-web 后,Spring Boot 会自动配置内嵌 Tomcat 和 Spring MVC,无需手动编写 XML。
  • 默认配置:Spring Boot 为诸多方面提供大量默认配置,如连接数据库、设置 Web 服务器、处理日志等。开发人员无需手动配置这些常见内容,框架已做好决策。例如,默认的日志配置可让应用程序快速输出日志信息,无需开发者额外繁琐配置日志级别、输出格式与位置等。
  • 约定的项目结构:Spring Boot 提倡特定项目结构,通常主应用程序类(含 Main 方法)置于根包,控制器类、服务类、数据访问类等分别放在相应子包,如 com.example.demo.controller 放控制器类,com.example.demo.service 放服务类等。此约定使团队成员更易理解项目结构与组织,新成员加入项目时能快速定位各功能代码位置,提升协作效率。

4.5 Spring Boot 的项目结构是怎么样的?

  • 开放接口层:定义系统对外暴露的协议(HTTP/RPC)和 API 规范,Controller 是其具体实现载体。
  • 终端显示层(半对应 Controller):处理前端交互(渲染 HTML/JSON),在前后端分离架构中弱化此层。当前主要是 velocity 渲染、JS 渲染、JSP 渲染、移动端展示等。
  • Web 层(直接对应 Controller):处理 HTTP 请求路由、参数校验、返回响应,主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层(直接对应 Service):业务逻辑层,实现核心业务逻辑,一般还会分为 Service 接口层和 Service 实现层,用面向接口的编程思想,为后续功能的解耦和扩展留下余地。
  • Manager 层:通用业务处理层,它有如下特征:
    • 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。
    • 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
    • 与 DAO 层交互,对多个 DAO 的组合复用。
  • DAO 层(直接对应 Mapper):数据访问(持久)层,与底层 MySQL、Oracle、Hbase、OceanBase 等进行数据交互。
  • Pojo 层(直接对应 Model):数据载体层,Entity 对应纯数据库映射对象(与 DB 强耦合),DTO/VO 用于传输,DTO 对应业务数据传输对象(包含业务字段),VO 对应前端展示对象(包含 UI 特定字段)。
  • 第三方服务:包括其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支付宝付款服务、高德地图服务等,通常作为 Service 的依赖组件。
  • 外部接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中。

4.6 Spring Boot 自动装配原理是什么?

Spring Boot 的自动装配原理是基于 Spring Framework 的条件化配置和 @EnableAutoConfiguration 注解实现的。这种机制允许开发者在项目中引入相关的依赖,Spring Boot 将根据这些依赖自动配置应用程序的上下文和功能。

Spring Boot 定义了一套接口规范,这套规范规定:Spring Boot 在启动时会扫描类路径下以及外部引用 Jar 包中的所有 META-INF/spring.factories 文件(Spring Boot 2.x)或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(Spring Boot 3.x+),将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 Jar 来说,只需要按照 Spring Boot 定义的标准,就能将自己的功能装置进 Spring Boot。

通俗来讲,自动装配就是通过注解或一些简单的配置就可以在 Spring Boot 的帮助下开启和配置各种功能,比如数据库访问、Web 开发。

点进 @SpringBootApplication 注解的内部可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}

这些注解的作用如下:

  • @Target({ElementType.TYPE}):该注解指定了这个注解可以用来标记在类上。在这个特定的例子中,这表示该注解用于标记配置类。
  • @Retention(RetentionPolicy.RUNTIME):这个注解指定了注解的生命周期,即在运行时保留。这是因为 Spring Boot 在运行时扫描类路径上的注解来实现自动配置,所以这里使用了 RUNTIME 保留策略。
  • @Documented:该注解表示这个注解应该被包含在 Java 文档中。它是用于生成文档的标记,使开发者能够看到这个注解的相关信息。
  • @Inherited:这个注解指示一个被标注的类型是被继承的。在这个例子中,它表明这个注解可以被继承,如果一个类继承了带有这个注解的类,它也会继承这个注解。
  • @SpringBootConfiguration:这个注解表明这是一个 Spring Boot 配置类。如果点进这个注解内部会发现与标准的 @Configuration 没啥区别,只是为了表明这是一个专门用于 Spring Boot 的配置。
  • @EnableAutoConfiguration:这个注解是 Spring Boot 自动装配的核心。它告诉 Spring Boot 启用自动配置机制,根据项目的依赖和配置自动配置应用程序的上下文。通过这个注解,Spring Boot 将尝试根据类路径上的依赖自动配置应用程序。
  • @ComponentScan:这个注解用于配置组件扫描的规则。在这里,它告诉 Spring Boot 在指定的包及其子包中查找组件,这些组件包括被注解的类、@Component 注解的类等。其中的 excludeFilters 参数用于指定排除哪些组件,这里使用了两个自定义的过滤器,分别是 TypeExcludeFilterAutoConfigurationExcludeFilter

4.7 Spring Boot 中如何实现对不同环境的属性配置文件的支持?

在 Spring Boot 中,你可以使用 Spring 的 Profile 功能来支持不同环境的属性配置文件。你可以为每个环境创建一个单独的配置文件,然后在运行应用程序时指定要使用的配置文件。

例如,假设你有开发环境(dev)、质量保证环境(qa)和生产环境(prod)。你可以在与 application.properties 文件相同的位置创建三个文件:

  • application-dev.properties:用于开发环境。
  • application-qa.properties:用于质量保证环境。
  • application-prod.properties:用于生产环境。

然后,你只需要在 application.properties 文件中设置 spring.profiles.active 属性,来指定当前的环境。例如,如果你想使用质量保证环境,你可以设置 spring.profiles.active=qa

此外,你还可以通过 JVM 参数来指定活动的配置文件。例如,你可以在启动应用程序时设置 -Dspring.profiles.active=dev

4.8 如何理解 Spring Boot 中的 Starters?

在 Spring Boot 中,Starters 可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包。你可以一站式集成 Spring 及其他技术,快速地添加和管理项目的依赖,而不需要到处找示例代码和依赖包。

例如,如果你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。同样,如果你想创建一个 RESTful 的 Web 应用,你可以添加 spring-boot-starter-web

4.9 Spring Boot Starters 的工作原理是什么?

Spring Boot Starters 的工作原理主要包括以下几个步骤:

  • 引入模块所需的相关 Jar 包:Spring Boot Starter 会将具备某种功能的 Jar 包打包到一起,可以简化依赖导入的过程。例如,我们导入 spring-boot-starter-web 这个 Starter,则和 Web 开发相关的 Jar 包都一起导入到项目中了。
  • 自动配置各个模块所需的属性:Spring Boot 在启动时会去依赖的 Starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包。接着根据 spring.factories 配置加载 AutoConfigure 类。根据 @Conditional 注解的条件,进行自动配置并将 Bean 注入 Spring Context。
  • Bean 的发现和加载:Spring Boot 默认扫描启动类所在的包下的主类与子类的所有组件,但并没有包括依赖包中的类,那么依赖包中的 Bean 是如何被发现和加载的?Spring Boot 在启动类上我们一般会加入 @SpringBootApplication 注解,此注解的源码中的 @EnableAutoConfiguration 注解引入了 @Import 这个注解,该注解导入自动配置功能类 AutoConfigurationImportSelector,主要方法 getCandidateConfigurations() 使用了 SpringFactoriesLoader.loadFactoryNames() 方法加载 META-INF/spring.factories 的文件(spring.factories 声明具体自动配置)。

4.10 介绍几个 Starter?

  • spring-boot-starter-web:这是最常用的起步依赖之一,它包含了 Spring MVC 和 Tomcat 嵌入式服务器,用于快速构建 Web 应用程序。
  • spring-boot-starter-security:提供了 Spring Security 的基本配置,帮助开发者快速实现应用的安全
    性,包括认证和授权功能。
  • mybatis-spring-boot-starter:这个 Starter 是由 MyBatis 团队提供的,用于简化在 Spring Boot 应用中集成 MyBatis 的过程。它自动配置了 MyBatis 的相关组件,包括 SqlSessionFactory、MapperScannerConfigurer 等,使得开发者能够快速地开始使用 MyBatis 进行数据库操作。
  • spring-boot-starter-data-jpaspring-boot-starter-jdbc:如果使用的是 Java Persistence API(JPA)进行数据库操作,那么应该使用 spring-boot-starter-data-jpa。这个 Starter 包含了 Hibernate 等 JPA 实现以及数据库连接池等必要的库,可以让你轻松地与 MySQL 数据库进行交互。你需要在 application.propertiesapplication.yml 中配置 MySQL 的连接信息。如果倾向于直接使用 JDBC 而不通过 JPA,那么可以使用 spring-boot-starter-jdbc,它提供了基本的 JDBC 支持。
  • spring-boot-starter-data-redis:用于集成 Redis 缓存和数据存储服务。这个 Starter 包含了与 Redis 交互所需的客户端(默认是 Jedis 客户端,也可以配置为 Lettuce 客户端),以及 Spring Data Redis 的支持,使得在 Spring Boot 应用中使用 Redis 变得非常便捷。同样地,需要在配置文件中设置 Redis 服务器的连接详情。
  • spring-boot-starter-test:包含了单元测试和集成测试所需的库,如 JUnit、Spring Test、AssertJ 等,便于进行测试驱动开发(TDD)。

4.11 Spring Boot 的核心注解是什么?主要由哪几个注解组成?

Spring Boot 的核心注解是 @SpringBootApplication。这个注解实际上是以下三个注解的组合:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。如关闭数据源的自动配置功能:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
  • @ComponentScan:Spring 组件扫描功能,让 Spring Boot 扫描到 Configuration 类并把它加入到程序上下文。

4.12 Spring Boot 中如何使用 Bean?

在 Spring Boot 中,你可以通过使用 @Bean 注解来声明一个 Bean。@Bean 注解告诉 Spring 一个方法会返回一个对象,这个对象应该被注册为 Spring 应用上下文中的 Bean。默认情况下,Bean 的名称是由方法名决定的,但你也可以在 @Bean 注解中通过 name 属性来设置 Bean 的名称。

例如,以下是一个简单的 @Bean 注解的使用示例:

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}

在这个例子中,myBean() 方法被注解为 @Bean,所以它会返回一个新的 MyBean 实例,这个实例将被注册为 Spring 应用上下文中的 Bean。

总的来说,你不需要在 Spring Boot 中手动配置 Bean,你只需要使用 @Bean 注解,Spring Boot 就会自动为你创建和管理 Bean。

4.13 RESTful 是什么?

RESTful 是一种软件架构风格,它主要用于客户端和服务器交互类的软件。在 RESTful 风格中,用户发起请求的发送方式有 GET、POST、DELETE、PUT 等方式对请求的处理方法进行区分。这样可以在前后端分离式的开发中使得前端开发人员不会对请求的资源地址产生混淆和大量的检查方法名的麻烦,形成一个统一的接口,使得 Web 服务变得更加简洁、有层次,易于实现缓存等机制。

在 Spring Boot 中,开发 RESTful 接口非常简单,通过不同的注解来支持前端的请求,除了经常使用的 @RestController 注解外,Spring Boot 还提供了一些组合注解。这些注解来帮助简化常用的 HTTP 方法的映射,并更好地表达被注解方法的语义。

例如,Spring Boot 提供了与 REST 操作方式(GET、POST、PUT、DELETE)对应的注解:

  • @GetMapping:处理 GET 请求。
  • @PostMapping:处理 POST 请求。
  • @PutMapping:用于更新资源。
  • @DeleteMapping:处理删除请求。
  • @PatchMapping:用于更新部分资源。

这些注解就是我们使用的 @RequestMapping 的简写版本:@GetMapping 其实就等于 @RequestMapping(value = "/xxx", method = RequestMethod.GET)

5. Spring Cloud

5.1 什么是 Spring Cloud?和 Spring Boot 的区别是什么?

Spring Boot 是用于构建单个 Spring 应用的框架,而 Spring Cloud 则是用于构建分布式系统中的微服务架构的工具。

Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发。Spring Cloud 为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理、服务注册与发现、断路器、负载均衡、智能路由、微代理、控制总线)。分布式系统的协调导致了样板模式,使用 Spring Cloud 开发人员可以快速地支持实现这些模式的服务和应用程序。

Spring Cloud 并不重复造轮子,而是将市面上开发得比较好的模块集成进去,进行封装,从而减少了各模块的开发成本。

总的来说,Spring Cloud 是微服务系统架构的一站式解决方案,是各个微服务架构落地技术的集合体,俗称微服务全家桶。

5.2 介绍一下用过哪些微服务组件?

  • 注册中心:注册中心是微服务架构最核心的组件。它起到的作用是对新节点的注册与状态维护,解决了如何发现新节点以及检查各节点运行状态的问题。微服务节点在启动时会将自己的服务名称、IP、端口等信息在注册中心登记,注册中心会定时检查该节点的运行状态。注册中心通常会采用心跳机制最大程度保证已登记过的服务节点都是可用的。
  • 负载均衡:负载均衡解决了如何发现服务及负载均衡如何实现的问题。通常微服务在互相调用时,并不是直接通过 IP、端口进行访问调用。而是先通过服务名在注册中心查询该服务拥有哪些节点,注册中心将该服务可用节点列表返回给服务调用者,这个过程叫服务发现,因服务高可用的要求,服务调用者会接收到多个节点,必须要从中进行选择。因此服务调用者一端必须内置负载均衡器,通过负载均衡策略选择合适的节点发起实质性的通信请求
  • 服务通信:服务通信组件解决了服务间如何进行消息通信的问题。服务间通信采用轻量级协议,通常是 HTTP RESTful 风格。但因为 RESTful 风格过于灵活,必须加以约束,通常应用时对其封装。例如在 Spring Cloud 中就提供了 Feign([fen])和 RestTemplate 两种技术屏蔽底层的实现细节,所有开发者都是基于封装后统一的 SDK 进行开发,有利于团队间的相互合作。
  • 配置中心:配置中心主要解决了如何集中管理各节点配置文件的问题。在微服务架构下,所有的微服务节点都包含自己的各种配置文件,如 JDBC 配置、自定义配置、环境配置、运行参数配置等。要知道有的微服务可能可能有几十个节点,如果将这些配置文件分散存储在节点上,发生配置更改就需要逐个节点调整,将给运维人员带来巨大的压力。配置中心便由此而生,通过部署配置中心服务器,将各节点配置文件从服务中剥离,集中转存到配置中心。一般配置中心都有 UI 界面,方便实现大规模集群配置调整。
  • 集中式日志管理:集中式日志主要是解决了如何收集各节点日志并统一管理的问题。微服务架构默认将应用日志分别保存在部署节点上,当需要对日志数据和操作数据进行数据分析和数据统计时,必须收集所有节点的日志数据。那么怎么高效收集所有节点的日志数据呢?业内常见的方案有 ELK、EFK。通过搭建独立的日志收集系统,定时抓取各节点增量日志形成有效的统计报表,为统计和分析提供数据支撑。
  • 分布式链路追踪:分布式链路追踪解决了如何直观的了解各节点间的调用链路的问题。系统中一个复杂的业务流程,可能会出现连续调用多个微服务,我们需要了解完整的业务逻辑涉及的每个微服务的运行状态,通过可视化链路图展现,可以帮助开发人员快速分析系统瓶颈及出错的服务。
  • 服务保护:服务保护主要是解决了如何对系统进行链路保护,避免服务雪崩的问题。在业务运行时,微服务间互相调用支撑,如果某个微服务出现高延迟导致线程池满载,或是业务处理失败。这里就需要引入服务保护组件来实现高延迟服务的快速降级,避免系统崩溃。

5.3 详细介绍一下分布式项目与微服务架构

分布式项目是指将一个大型的项目切割成多个小项目,每个小项目都是一套独立的系统。这些小项目被打成 Jar 包,然后通过互相引用(以 Jar 包的形式)来组装成原来的完整项目。每个子业务都是一套独立的系统,子业务之间相互协作,最终完成整体的大业务。这种方式可以提高系统的可扩展性高可用性,解决高并发的问题,并且可以利用分布式存储将数据分片到多个节点上,不仅可以提高性能,同时也可以使用多个节点对同一份数据进行备份。

微服务架构是一种软件开发框架,它将一个大型的应用程序划分为许多小的、独立的服务。每个服务都有自己的技术栈,包括数据库和数据管理模型。这些服务通常通过 REST API、事件流和消息代理进行通信,并按照业务能力进行组织。

微服务架构的主要优点包括:

  • 代码更容易更新:可以直接添加新特性或功能,而不必更新整个应用。
  • 团队可以对不同的组件使用不同的技术栈和不同的编程语言。
  • 组件可以相互独立地扩展,从而减少与必须扩展整个应用相关的浪费和成本。

然而,微服务架构也带来了一些挑战,例如管理复杂性的增加、日志记录数据的增加、新版本可能导致的向后兼容性问题、应用涉及更多网络连接可能导致的延迟和连接问题等。尽管如此,微服务架构仍然被广泛采用,因为它可以提高开发效率,使组织能够更快地响应业务需求。

微服务架构和分布式系统是两个不同的概念,它们的主要区别在于设计目标和实现方式:

  • 分布式系统:分布式系统的核心就是拆分,只要是将一个项目拆分成了多个模块,并将这些模块分开部署,那就算是分布式。分布式解决的是系统性能问题,即解决系统部署上单点的问题,尽量让组成系统的子系统分散在不同的机器上进而提高系统的吞吐能力。分布式是部署层面的东西,即强调物理层面的组成,系统的各子系统部署在不同计算机上。
  • 微服务架构:微服务架构通过更细粒度的服务切分,使得整个系统的迭代速度和并行程度更高,但是运维的复杂度和性能会随着服务的粒度更细而增加。微服务重在解耦合,使每个模块都独立。微服务是设计层面的东西,一般考虑如何将系统从逻辑上进行拆分,也就是垂直拆分。微服务可以是分布式的,即可以将不同服务部署在不同计算机上,当然如果量小也可以部署在单机上。

总的来说,分布式系统和微服务架构都是为了提高系统的可扩展性和可维护性,但它们的关注点和实现方式有所不同。

5.4 使用 Spring Cloud 有什么优势?

  • 约定优于配置:Spring Cloud 提供了一套默认的配置,使得开发人员可以更专注于业务逻辑的开发。
  • 适用于各种环境:无论是开发环境、部署 PC Server,还是各种云环境(例如阿里云、AWS 等),Spring Cloud 都可以适用。
  • 隐藏了组件的复杂性:Spring Cloud 提供了声明式、无 XML 的配置方式,隐藏了组件的复杂性。
  • 开箱即用、快速启动:Spring Cloud 提供了一套完整的微服务解决方案,使得开发人员可以快速启动项目。
  • 轻量级的组件:Spring Cloud 整合的组件大多比较轻量。
  • 组件丰富、选型中立、功能齐全:Spring Cloud 为微服务架构提供了非常完整的支持,有丰富的组件选择,开发人员可以根据需求选择合适的组件。
  • 灵活:Spring Cloud 的组成部分是解耦合的,开发人员可以按需灵活挑选技术选型。
  • 服务拆分粒度更细:有利于资源重复利用,提高开发效率。
  • 采用去中心化思想:服务之间采用轻量级通讯,适合互联网时代,产品迭代周期更短。

5.5 服务注册和发现是什么意思?Spring Cloud 如何实现?

服务注册是指将服务的元数据(例如服务名、IP 地址、端口号等)注册到注册中心中,以便其他服务可以发现它。例如,一个微服务启动后,会将自己的信息(通常是这个服务的 IP 和端口)注册到一个公共的组件上去(比如 ZooKeeper、Consul)。

服务发现是指客户端从注册中心中查找和选择可用的服务实例,并通过负载均衡策略来分配请求。也就是说,新注册的这个服务模块能够及时的被其他调用者发现。不管是服务新增和服务删减都能实现自动发现。

Spring Cloud 实现服务注册和发现的方式主要有以下几种:

  • Eureka([jʊ'ri:kə]):Eureka 是 Netflix 开源的一款提供服务注册和发现的产品。Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。在微服务应用启动后,Eureka Client 会向 Eureka Server 发送心跳。如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,则会将该节点移除。
  • Consul:Consul 是一种服务网格解决方案,提供了包括服务发现、配置和分段功能。这些功能中的每一个都可以根据需要独立使用,也可以一起使用以构建全堆栈服务网格。Consul 是适用于底层服务发现和配置的工具。
  • ZooKeeper:ZooKeeper 是一个开源的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

5.6 负载均衡有什么作用?

负载均衡是一种重要的网络技术,它可以有效地提高网络服务的性能、可用性和安全性。负载均衡的主要作用包括:

  • 解决并发压力:通过将客户端的请求分发到多个服务器,负载均衡可以有效地解决并发压力,提高应用处理性能,增加吞吐量,加强网络处理能力。
  • 提供故障转移:负载均衡可以检测后端服务的运行状况,自动检测异常实例,并快速实施故障转移;当实例恢复正常时,它将自动恢复负载。这样,即使某个服务器出现故障,负载均衡也可以保证服务的高可用性。
  • 提供网站伸缩性(扩展性):当业务压力增加时,可以通过将主机添加到后端服务器池来提高性能。当压力降低时,可以减少宿主。这样,负载均衡可以根据业务需求动态地添加或减少服务器数量,提供网站的伸缩性。
  • 安全防护:负载均衡设备上可以做一些过滤,黑白名单等处理,提供安全防护。

5.7 负载均衡有哪些算法?

  • 简单轮询:将请求按顺序分发给后端服务器上,不关心服务器当前的状态,比如后端服务器的性能、当前的负载。
  • 加权轮询:根据服务器自身的性能给服务器设置不同的权重,将请求按顺序和权重分发给后端服务器,可以让性能高的机器处理更多的请求。
  • 简单随机:将请求随机分发给后端服务器上,请求越多,各个服务器接收到的请求越平均。
  • 加权随机:根据服务器自身的性能给服务器设置不同的权重,将请求按各个服务器的权重随机分发给后端服务器。
  • 一致性哈希:根据请求的客户端 IP、或请求参数通过哈希算法得到一个数值,利用该数值取模映射出对应的后端服务器,这样能保证同一个客户端或相同参数的请求每次都使用同一台服务器
  • 最小活跃数:统计每台服务器上当前正在处理的请求数,也就是请求活跃数,将请求分发给活跃数最少的后台服务器。

5.8 介绍一下服务熔断?

服务熔断是应对微服务雪崩效应的一种链路保护机制,类似股市、保险丝。

比如说,微服务之间的数据交互是通过远程调用来完成的。服务 A 调用服务 B,服务 B 调用服务 C,某一时间链路上对服务 C 的调用响应时间过长或者服务 C 不可用,随着时间的增长,对服务 C 的调用也越来越多,然后服务 C 崩溃了,但是链路调用还在,对服务 B 的调用也在持续增多,然后服务 B 崩溃,随之 A 也崩溃,导致雪崩效应。

服务熔断是应对雪崩效应的一种微服务链路保护机制。例如在高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。

所以,服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。

在 Spring Cloud 框架里,熔断机制通过 Hystrix([hɪst'rɪks])实现。Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。

5.9 介绍一下服务降级?

服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略地不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行

服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理,可以理解为舍小保大。

服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的退路(fallback)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。

5.10 Spring Cloud 和 Dubbo 有什么区别?

Spring Cloud 和 Dubbo 都是现在主流的微服务框架,但它们之间存在一些主要的区别:

  • 初始定位不同:Spring Cloud 定位为微服务架构下的一站式解决方案,而 Dubbo 是 SOA(Service-Oriented Architecture,面向服务的架构)时代的产物,它的关注点主要在于服务的调用和治理。
  • 生态环境不同:Spring Cloud 依托于 Spring 平台,具备更加完善的生态体系;而 Dubbo 一开始只是做 RPC(Remote Procedure Call,远程过程调用)通信协议的远程调用,生态相对匮乏,现在才逐渐丰富起来。
  • 调用方式不同:Spring Cloud 是采用 HTTP 协议做远程调用,接口一般是 REST 风格,比较灵活;Dubbo 是采用 Dubbo 协议,接口一般是 Java 的 Service 接口,格式固定。
  • 服务网关:Dubbo 没有服务网关,而 Spring Cloud 使用的是 Spring Cloud Netflix Zuul。
  • 分布式配置:Dubbo 没有分布式配置,而 Spring Cloud 使用的是 Spring Cloud Config。
  • 服务跟踪:Dubbo 没有服务跟踪,而 Spring Cloud 使用的是 Spring Cloud Sleuth。
  • 消息总线:Dubbo 没有消息总线,而 Spring Cloud 使用的是 Spring Cloud Bus。
  • 数据流:Dubbo 没有数据流,而 Spring Cloud 使用的是 Spring Cloud Stream。

5.11 Sping Cloud 微服务之间如何通讯?

在 Spring Cloud 中,微服务之间的通信主要有两种方式:

  • 同步通信:Dubbo 通过 RPC 远程过程调用,而 Spring Cloud 通过 REST 接口 JSON 调用等(HTTP 通信)。
  • 异步通信:消息队列,如:RabbitMq、ActiveMq、Kafka 等。

6. MyBatis

6.1 与传统的 JDBC 相比,MyBatis 的优点是什么?

  • 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在 XML 里,解除 SQL 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
  • 与 JDBC 相比,减少了 50% 以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接。
  • 很好的与各种数据库兼容,因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持。
  • 能够与 Spring 很好的集成,开发效率高。
  • 提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

6.2 MyBatis 在哪方面做的比较好?

MyBatis 在 SQL 灵活性动态 SQL 支持结果集映射与 Spring 整合方面表现卓越,尤其适合重视 SQL 可控性的项目。

  • SQL 与代码解耦,灵活可控:MyBatis 允许开发者直接编写和优化 SQL,相比全自动 ORM(如 Hibernate),MyBatis 让开发者明确知道每条 SQL 的执行逻辑,便于性能调优。
1
2
3
4
5
6
7
8
<!-- 示例:XML 中定义 SQL -->
<select id="findUserWithRole" resultMap="userRoleMap">
SELECT u.*, r.role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
WHERE u.id = #{userId}
</select>
  • 动态 SQL 的强大支持:比如可以动态拼接 SQL,通过 <if><choose><foreach> 等标签动态生成 SQL,避免 Java 代码中繁琐的字符串拼接。
1
2
3
4
5
6
7
<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name LIKE #{name}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</select>

上面的例子中使用 MyBatis 的 <where><if> 标签实现动态 SQL,<where> 会自动去除多余的 AND/OR,且当内部条件为空时,不生成 WHERE 关键字,<if> 标签根据参数动态添加条件,例如 test="name != null" 表示当 name 参数非空时生效。根据传入参数不同可以生成不同的 SQL:

传入参数 生成的 SQL 说明
name="张%" SELECT * FROM user WHERE name LIKE '张%' 模糊查询姓张的用户
status=1 SELECT * FROM user WHERE status = 1 查询状态为1的用户
name="李%" status=0 SELECT * FROM user WHERE name LIKE '李%' AND status = 0 组合查询
无参数 SELECT * FROM user 查询所有用户
  • 自动映射与自定义映射结合:自动将查询结果字段名与对象属性名匹配(如驼峰转换)。例如在下面这个例子中,resultMap 实现了用户-角色一对多关系映射,<resultMap> 定义结果集映射规则,映射到 User 类;<id> 为主键映射(标识对象唯一性),将数据库的 user_id 字段映射到 User 对象的 id 属性;<result> 为普通字段映射,将数据库的 user_name 字段映射到 User 对象的 name 属性;<collection> 为一对多集合映射(用户对应多个角色),映射到 User 对象的 roles 属性,其中嵌套了对象的字段映射,将数据库的 role_name 字段映射到 Role 对象的 roleName 属性。
1
2
3
4
5
6
7
<resultMap id="userRoleMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<collection property="roles" ofType="Role">
<result property="roleName" column="role_name"/>
</collection>
</resultMap>
  • 插件扩展机制:可编写插件拦截 SQL 执行过程,实现分页、性能监控、SQL 改写等通用逻辑。
1
2
3
4
@Intercepts({@Signature(type=Executor.class, method="query", args={...})})
public class PaginationPlugin implements Interceptor {
// 实现分页逻辑
}
  • 与 Spring 生态无缝集成:通过 @MapperScan 快速扫描 Mapper 接口,结合 Spring 事务管理,配置简洁高效。
1
2
3
4
5
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
// 数据源和 SqlSessionFactory 配置
}

6.3 JDBC 连接数据库的步骤是什么?

使用 Java JDBC 连接数据库的一般步骤如下:

  1. 加载数据库驱动程序:在使用 JDBC 连接数据库之前,需要加载相应的数据库驱动程序。可以通过 Class.forName("com.mysql.jdbc.Driver") 来加载 MySQL 数据库的驱动程序。不同数据库的驱动类名会有所不同。
  2. 建立数据库连接:使用 DriverManager 类的 getConnection(url, username, password) 方法来连接数据库,其中 url 是数据库的连接字符串(包括数据库类型、主机、端口等),username 是数据库用户名,password 是密码。
  3. 创建 Statement 对象:通过 Connection 对象的 createStatement() 方法创建一个 Statement 对象,用于执行 SQL 查询或更新操作。
  4. 执行 SQL 查询或更新操作:使用 Statement 对象的 executeQuery(sql) 方法来执行 SELECT 查询操作,或者使用 executeUpdate(sql) 方法来执行 INSERTUPDATEDELETE 操作。
  5. 处理查询结果:如果是 SELECT 查询操作,通过 ResultSet 对象来处理查询结果。可以使用 ResultSetnext() 方法遍历查询结果集,然后通过 getXXX() 方法获取各个字段的值。
  6. 关闭连接:在完成数据库操作后,需要逐级关闭数据库连接相关对象,即先关闭 ResultSet,再关闭 Statement,最后关闭 Connection

以下是一个简单的示例代码,请注意在实际应用中,需要进行异常处理以确保资源的正确释放,以及使用 try-with-resources 来简化代码和确保资源的及时关闭:

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 java.sql.*;

public class Main {
public static void main(String[] args) {
try {
// 加载数据库驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");

// 建立数据库连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");

// 创建 Statement 对象
Statement statement = connection.createStatement();

// 执行 SQL 查询
ResultSet resultSet = statement.executeQuery("SELECT * FROM mytable");

// 处理查询结果
while (resultSet.next()) {
// 处理每一行数据
}

// 关闭资源
resultSet.close();
statement.close();
connection.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

6.4 如果项目中要用到原生的 MyBatis 去查询,该怎样写?

  1. 配置 MyBatis:在配置文件中配置数据源、MyBatis 的 Mapper 文件位置等信息。
  2. 创建实体类:创建与数据库表对应的实体类,字段名和类型需与数据库表保持一致:
1
2
3
4
5
6
7
8
package com.example.model.User;

public class User {
private Long id;
private String username;
private String email;
// Getters and setters
}
  1. 编写 SQL 映射文件:在 resources 目录下创建 XML 文件,定义 SQL 语句和映射关系:
1
2
3
4
5
6
<!-- userMapper.xml -->
<mapper namespace="com.example.dao.UserMapper">
<select id="selectUserById" resultType="com.example.model.User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
  1. 编写 DAO 接口:创建 DAO 接口,定义查询方法:
1
2
3
4
5
package com.example.dao.UserMapper;

public interface UserMapper {
User selectUserById(Long id);
}
  1. 编写具体的 SQL 查询语句:在 XML 文件中编写对应的 SQL 语句。
  2. 调用查询方法:在服务层或控制层中调用 DAO 接口中的方法进行查询:
1
2
// 在 Service 层中调用
User user = userMapper.selectUserById(1);

6.5 MyBatis 里的 # 和 $ 区别是什么?

  • MyBatis 在处理 #{} 时,会创建预编译的 SQL 语句,将 SQL 中的 #{} 替换为 ? 号(参数占位符),在执行 SQL 时会为预编译 SQL 中的占位符(?)赋值,调用 PreparedStatementset 方法来赋值,预编译的 SQL 语句执行效率高,并且可以防止 SQL 注入(SQL 注入是攻击者通过在输入参数中插入恶意 SQL 代码,篡改原始 SQL 语句逻辑的攻击方式),提供更高的安全性,适合传递参数值。
  • MyBatis 在处理 ${} 时,只是创建普通的 SQL 语句,然后在执行 SQL 语句时 MyBatis 将参数直接拼入到 SQL 里(简单的字符串替换),不能防止 SQL 注入,因为参数直接拼接到 SQL 语句中,如果参数未经过验证、过滤,可能会导致安全问题。

#{} 使用 JDBC 预编译机制,将参数值独立传输给数据库,数据库先编译 SQL 结构,再将参数值作为纯数据处理:恶意代码会被视为数据值非可执行代码

1
2
3
// MyBatis 底层实现
PreparedStatement ps = connection.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setInt(1, id); // 安全参数绑定

例如我们有以下两种写法:

1
2
3
4
5
6
7
8
9
<!-- 安全写法 -->
<select id="safeSearch">
SELECT * FROM user WHERE name = #{name}
</select>

<!-- 危险写法 -->
<select id="dangerSearch">
SELECT * FROM user WHERE name = '${name}'
</select>

不同输入的执行结果如下:

传入参数 safeSearch 生成的 SQL dangerSearch 生成的 SQL 结果
Alice SELECT * FROM user WHERE name = 'Alice' SELECT * FROM user WHERE name = 'Alice' 正常查询 Alice
' OR 1=1 -- SELECT * FROM user WHERE name = "\' OR 1=1 --" SELECT * FROM user WHERE name = '' OR 1=1 --' 前者查询 \' OR 1=1 -- 用户的信息,后者非法查询所有用户的信息

具体看一下这两条 SQL 语句的执行逻辑:

1
2
3
4
5
-- 直接将 "\' OR 1=1 --" 最为用户名进行查询
SELECT * FROM user WHERE name = "\' OR 1=1 --"

-- "--" 将后续的内容全部注释掉,然后通过永真条件 1=1 成功查询 user 表的所有用户
SELECT * FROM user WHERE name = '' OR 1=1 --'

6.6 MyBatisPlus 和 MyBatis 的区别?

MyBatisPlus 是一个基于 MyBatis 的增强工具库,旨在简化开发并提高效率。以下是 MyBatisPlus 和 MyBatis 之间的一些主要区别:

  • CRUD 操作:MyBatisPlus 通过继承 BaseMapper 接口,提供了一系列内置的快捷方法,使得 CRUD 操作更加简单,无需编写重复的 SQL 语句。
  • 代码生成器:MyBatisPlus 提供了代码生成器功能,可以根据数据库表结构自动生成实体类、Mapper 接口以及 XML 映射文件,减少了手动编写的工作量。
  • 通用方法封装:MyBatisPlus 封装了许多常用的方法,如条件构造器、排序、分页查询等,简化了开发过程,提高了开发效率。
  • 分页插件:MyBatisPlus 内置了分页插件,支持各种数据库的分页查询,开发者可以轻松实现分页功能,而在传统的 MyBatis 中,需要开发者自己手动实现分页逻辑。
  • 多租户支持:MyBatisPlus 提供了多租户的支持,可以轻松实现多租户数据隔离的功能。
  • 注解支持:MyBatisPlus 引入了更多的注解支持,使得开发者可以通过注解来配置实体与数据库表之间的映射关系,减少了 XML 配置文件的编写。

6.7 MyBatis 运用了哪些常见的设计模式?

  • 代理模式:MyBatis 实现的核心,比如 MapperProxyConnectionLogger,用的 JDK 的动态代理;还有 executor.loader 包使用了 CGLIB 或者 Javassist 达到延迟加载的效果。
  • 单例模式:例如 ErrorContextLogFactory
  • 工厂模式:例如 SqlSessionFactoryObjectFactoryMapperProxyFactory
  • 建造者模式(Builder):例如 SqlSessionFactoryBuilderXMLConfigBuilderXMLMapperBuilderXMLStatementBuilderCacheBuilder 等。
  • 组合模式:例如 SqlNode 和各个子类 ChooseSqlNode 等。
  • 模板方法模式:例如 BaseExecutorSimpleExecutor,还有 BaseTypeHandler 和所有的子类例如 IntegerTypeHandler
  • 适配器模式:例如 Log 的 MybBatis 接口和它对 JDBC、Log4j 等各种日志框架的适配实现。
  • 装饰者模式:例如 Cache 包中的 cache.decorators 子包中等各个装饰者的实现。
  • 迭代器模式:例如迭代器模式 PropertyTokenizer