Spring
一. 控制反转(IoC)
1.手动
使用了Spring的@Configuration
和@Bean
注解来明确指定了哪些类需要被纳入容器的管理。在AppConfig
配置类中,通过@Bean
注解创建了Service
和Controller
的实例,Spring会自动将这些实例纳入容器的管理,并处理它们之间的依赖关系。
// 定义一个Service接口
public interface Service {
void doSomething();
}
// 实现Service接口的具体类
public class ServiceImpl implements Service {
@Override
public void doSomething() {
System.out.println("Service is doing something.");
}
}
// 定义一个Controller类,它依赖于Service接口
public class Controller {d
private Service service;
// 通过构造函数注入依赖
public Controller(Service service) {
this.service = service;
}
public void doAction() {
service.doSomething();
}
}
// 在应用的入口处,使用Spring容器创建实例并进行依赖关系的管理
public class Main {
public static void main(String[] args) {
// 创建Spring容器
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器中获取Controller实例
Controller controller = context.getBean(Controller.class);
// 调用Controller的方法,会自动调用Service的方法
controller.doAction();
}
}
// 定义一个配置类,用于告诉Spring容器如何创建和管理Bean
@Configuration
public class AppConfig {
@Bean
public Service service() {
return new ServiceImpl();
}
@Bean
public Controller controller(Service service) {
return new Controller(service);
}
}
2.自动
SpringApplication.run(CommunityApplication.class, args)
会扫描应用中的所有组件(包括被@Component
、@Service
、@Repository
等注解标记的类),并将它们纳入Spring容器的管理。通过调用SpringApplication.run
方法启动应用,会自动创建一个Spring应用上下文(ApplicationContext),并初始化整个应用的配置和组件。
public static void main(String[] args) {
SpringApplication.run(CommunityApplication.class, args);
}
@SpringBootApplication
实际上是一个组合注解,包含了@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
三个注解。其中,@SpringBootConfiguration
注解表示这是一个Spring Boot的配置类,@EnableAutoConfiguration
注解启用自动配置,@ComponentScan
注解指定要扫描的包路径,扫描被@Component
(@Repository
,@Service
,@Controller
)注解的 bean,注解默认会扫描该类所在的包下所有的类。
使用Spring Boot的自动化机制可以方便快捷地扫描和管理所有的Bean,而手动进行依赖注入可以更加精确地控制容器中的Bean。
Q1:使用@Repository注解时,怎么在Spring的配置文件中进行相关的配置?
1、Spring
在使用@Repository注解时,可以在Spring的配置文件中进行相关的配置。以下是配置步骤:
- 在Spring的配置文件中添加
context:component-scan
标签,用于开启组件扫描功能,扫描带有@Repository
注解的类。示例如下:
<context:component-scan base-package="com.example.repository" />
其中,base-package
属性指定了要扫描的包路径。
- 如果需要配置数据源和事务管理器,可以在Spring的配置文件中进行相关的配置。示例如下:
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<!-- 数据源相关配置 -->
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
其中,dataSource
是数据源的配置,transactionManager
是事务管理器的配置。
- 如果需要使用
@Repository
注解对应的数据访问异常转化功能,可以配置相关的异常转化器。示例如下:
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
这个配置会自动为带有@Repository
注解的类添加异常转化功能。
通过以上配置,就可以在Spring的配置文件中配置@Repository
注解相关的配置。注意,具体的配置内容根据实际需求和使用的技术选择可能会有所不同。
2、Spring Boot
在使用Spring Boot时,可以省略繁琐的配置过程,因为Spring Boot提供了自动配置的功能。对于@Repository注解的配置,可以按照以下步骤进行:
- 在Spring Boot主类上添加
@SpringBootApplication
注解,该注解包含了@EnableAutoConfiguration
注解,用于开启自动配置功能。 - 确保@Repository注解所在的包或子包在主类的扫描范围内,可以通过
@ComponentScan
注解进行配置,或者将主类放在包的顶层位置。 - 如果需要配置数据源,可以在
application.properties
或application.yml
配置文件中添加相关的配置项,如下所示:
propertiesCopy codespring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
这样,Spring Boot会根据配置文件的内容自动配置数据源。
- 如果需要事务管理器,可以在需要使用事务的类或方法上添加
@Transactional
注解,Spring Boot会自动配置事务管理器。 通过以上步骤,就可以在Spring Boot中使用@Repository注解,无需进行繁琐的配置,Spring Boot会根据约定和自动配置的机制自动完成相关的配置。
Q2:@Primary有什么用
@Primary注解是Spring框架中的一个注解,用于标识主要的Bean实例。当存在多个同类型的Bean实例时,通过使用@Primary注解,可以指定其中一个Bean实例为首选的主要实例。
Q3:@Repository有什么用
@Repository注解是Spring框架中的一个注解,用于标识持久层组件(如DAO类)。
@Repository的作用主要有以下几点:
- 标识持久层组件:通过在持久层的类上添加
@Repository
注解,可以告诉Spring容器该类是一个用于数据访问的组件。Spring在进行组件扫描时,会扫描带有@Repository
注解的类,并将其实例化为Bean
。 - 提供异常转化功能:
@Repository
注解还提供了数据访问异常的转化机制。当数据访问过程中发生异常时,Spring会将底层的数据访问异常(如JDBC异常)转化为Spring的统一异常体系,使得应用程序可以更方便地处理和捕获异常。 - 具备@Component的功能:
@Repository
注解是@Component
注解的派生注解,因此@Repository
注解具备@Component
的所有功能。它可以被Spring容器自动扫描并装配,可以使用@Autowired
或@Resource
等注解进行依赖注入,也可以使用@Qualifier注解进行指定具体的实现类。
Q4:谈谈对IoC的理解
控制:创建、实例化对象的权力
反转:将这些权力交给IoC容器和Spring框架
将对象之间的依赖关系交给IoC容器管理,由IoC容器完成对象的注入,简化开发,IoC容器像是一个工厂,创建一个对象时只配置好,不需要考虑怎么被创建出来的。Spring中用Xml文件配置bean,在Spring Boot中用注解配置。
Q5:@Component和@Bean的区别
-
@Component注解作用于类,而@Bean注解作用于方法
-
@Component通常是通过类路径扫描来自动侦察以及自动装配到Spring容器中;@Bean在标有该注解的方法中定义产生这个bean,@Bean告诉了Spring这是某个类的实例,当我需要的时候还给我。
-
@Bean比@Component的自定义性更强
Q6:注入Bean的注解
- @Autowired
- @Resource
- @Inject
Q7:@Autowired和@Resource的区别
-
@Autowired
属于Spring
内置的注解,默认的注入方式是byType(根据类型进行匹配)
,首先根据接口类型去匹配并注入Bean
。如果一个接口有多个实现类,注入方式就会变为byName(根据名称匹配)
举个例子,
SmsService
接口有两个实现类:SmsServiceImpl1
和SmsServiceImpl2
,且它们都已经被Spring
容器所管理。
// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;
@Resource
属于 JDK 提供的注解,默认注入方式为byName
。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType
。
@Resource
有两个比较重要且日常开发常用的属性:name
(名称)、type
(类型)。
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}
如果仅指定 name
属性则注入方式为byName
,如果仅指定type
属性则注入方式为byType
,如果同时指定name
和type
属性(不建议这么做)则注入方式为byType
+byName
。
// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;
总结一下:
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过@Qualifier
注解来显式指定名称,@Resource
可以通过name
属性来显式指定名称。 @Autowired
支持在构造函数、方法、字段和参数上使用。@Resource
主要用于字段和方法上的注入,不支持在构造函数或参数上使用
Q8:Bean的作用域
- Singleton:IoC容器中只有唯一的bean实例,Spring的bean默认都是单例的。在之后的每次请求或注入时,都会返回同一个实例,而不会重新创建新的实例。
- prototype:每次获取都会创建一个新的 bean 实例。与默认的单例模式不同,每次通过容器获取prototype作用域的Bean时,都会创建一个新的实例。原型模式的Bean不适合进行依赖注入,因为每次注入都会得到一个新的实例,无法保证依赖关系的一致性。
- request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
Q9:为什么在Spring boot没有配置 bean 的作用域
在Spring Boot中,为了简化配置和提高开发效率,通常不需要显式地配置Bean的作用域。Spring Boot默认使用单例模式来管理Bean,即每个Bean在容器中只会存在一个实例。 这是因为Spring Boot遵循"约定优于配置"的原则,通过自动配置和默认配置来简化开发过程。在大多数情况下,单例模式已经能够满足开发需求,因此默认使用单例模式可以减少不必要的配置。 如果需要使用其他作用域,如原型(prototype)、会话(session)、请求(request)等,可以在需要的地方使用特定的注解来标记,而不需要在配置文件中显式配置。例如,可以使用**@Scope("prototype")
**注解来将特定的Bean定义为原型模式。
Q10:区分 BeanFactory 和 ApplicationContext?
BeanFactory | ApplicationContext |
---|---|
它使用懒加载 | 它使用即时加载 |
它使用语法显式提供资源对象 | 它自己创建和管理资源对象 |
不支持国际化 | 支持国际化 |
不支持基于依赖的注解 | 支持基于依赖的注解 |
BeanFactory和ApplicationContext的优缺点分析:
BeanFactory的优缺点:
- 优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
- 缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创建的Bean生命周期会简单一些。
ApplicationContext的优缺点:
- 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题。
- 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大。
二. 面向切面编程(AOP)
术语 | 含义 |
---|---|
目标(Target) | 目标对象/被通知的对象 |
代理(Proxy) | 向目标对象应用通知之后创建的代理对象 |
连接点(JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点 |
切入点(Pointcut) | 被切面拦截/增强的连接点(切入点一定是连接点,连接点不一定是织入点) |
通知(Advice) | 增强的逻辑/代码,也是拦截到目标对象的连接点后应该做的事 |
织入(Weaving) | 将通知 应用到目标对象,产生代理对象的过程动作 |
方面主键(Aspect) | Pointcut+Advice |
Q1:谈谈对AOP的理解
能够将与业务无关,却为业务模块所共同调用的逻辑和责任所封装起来,例如:事务处理、权限控制,日志管理,减少重复代码,比较好维护。
Spring AOP
基于动态代理的,如果代理的对象实现了某个接口,那么Spring AOP
会使用JDK Proxy
,去创建代理对象;而对于没有实现接口的对象,Spring AOP
使用Cglib
生成一个被代理对象的子类作为代理。
Spring AOP
的工作原理:自动为目标对象生成代理,并在方法调用时织入切面逻辑。
代理对象是通过Spring AOP
自动生成的,并且已经被注入到ApplicationContext
中。具体来说,代理对象是在以下代码中获取的:
UserService userService = context.getBean(UserService.class);
// 主类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
UserService userService = context.getBean(UserService.class);
userService.addUser("john", "123456");
}
}
在这个示例中,通过ApplicationContext
的getBean
方法来获取代理对象。根据Spring AOP
的配置,如果UserService
接口被代理,那么获取到的userService
对象就是代理对象;如果UserServiceImpl
类被代理,那么获取到的userService
对象也是代理对象。
Q2:为什么要对Spring AOP进行代理,不代理会怎样?
在Spring AOP
中,代理是实现切面功能的关键。如果不进行代理,切面逻辑将无法被织入到目标对象的方法调用中,失去了AOP
的作用。 具体来说,代理对象在目标对象和调用方之间充当了一个中间层。当调用方调用代理对象的方法时,代理对象会在方法执行前后执行切面逻辑。这样,我们可以在切面中添加一些额外的逻辑,比如日志记录、事务管理、性能监控等。代理对象将切面逻辑织入到目标对象的方法调用中,从而实现了横切关注点的模块化。 如果不进行代理,切面逻辑将无法被自动应用到目标对象的方法调用中。这意味着我们需要在每个目标对象方法的调用处手动添加切面逻辑,这样会导致代码的重复和冗余,不利于代码的维护和扩展。而代理机制可以自动为目标对象生成代理,并在方法调用时织入切面逻辑,使得切面的应用更加便捷和灵活。 除了将切面逻辑织入到方法调用中,代理还可以实现其他功能,比如延迟加载、事务管理、缓存等。代理对象可以拦截方法调用,根据需要进行一些额外的处理,从而提供更多的功能。
Q3:那在以下代码中,谁是目标对象,谁是代理对象,谁是调用方?
public class AlphaAspect {
@Pointcut("execution(* com.newcoder.community.service.*.*(..))")//所有类、所有方法
public void pointcut(){}
@Before("pointcut()")//连接点开始记日志
public void before(){
System.out.println("before");
}
@After("pointcut()")//连接点后记日志
public void after(){
System.out.println("after");
}
@AfterReturning("pointcut()")//有返回值后记日志
public void afterReturn(){
System.out.println("afterReturn");
}
@AfterThrowing("pointcut()")//抛异常后记日志
public void afterThrowing(){
System.out.println("afterThrowing");
}
@Around("pointcut()")//前后都织入 执行代理对象,织入代理对象,用来代替原始对象
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{//ProceedingJoinPoint为连接点,织入部位
System.out.println("around before");
Object obj = joinPoint.proceed();//proceed调目标主键 调用原始对象方法
System.out.println("around after");
return obj;
}
在这个例子中,目标对象是com.newcoder.community.service包下的所有类的所有方法。 AlphaAspect类本身是一个切面类,其中包含了各种注解标记的通知方法。这些通知方法会在目标对象的方法调用前后执行相应的逻辑。因此,AlphaAspect类是代理对象。 调用方是在业务代码中调用目标对象的方法的地方,这个例子中没有给出具体的业务代码,所以无法确定调用方是什么。
所以,代理对象会在目标对象的方法调用前后执行相应的逻辑。
Q4:那为什么说向目标对象应用通知之后创建代理对象?
在AOP中,通知(Advice)是切面的核心逻辑,用于在目标对象的方法调用前后执行特定的操作。为了将通知应用到目标对象的方法调用中,需要创建代理对象来包装目标对象。代理对象负责拦截目标对象的方法调用,并在适当的时机执行通知。 代理对象的创建过程可以分为两种方式:静态代理和动态代理。
- 静态代理:在编译时就已经将切面逻辑与目标对象织入到了一起,生成了代理类的字节码文件。代理对象是在编译时生成的,不需要在运行时再去创建。(AspectJ)
- 动态代理:在运行时动态地生成代理对象。动态代理需要借助于Java的反射机制来实现。通过动态创建代理对象,可以根据切面的配置在运行时将通知应用到目标对象的方法调用中。 在Spring AOP中,默认使用的是动态代理。Spring使用JDK动态代理和CGLIB动态代理两种方式来创建代理对象。如果目标对象实现了接口,则使用JDK动态代理;如果目标对象没有实现接口,则使用CGLIB动态代理。 无论是静态代理还是动态代理,代理对象的创建都是在应用通知之后进行的。通过代理对象,切面逻辑可以被织入到目标对象的方法调用中,实现对目标对象的增强。
Q5:目标对象所有的方法就是连接点吗?
是的,目标对象的所有方法都可以作为连接点。连接点是程序执行过程中的特定点,包括方法的调用、异常的抛出、字段的访问等。在AOP中,最常见的连接点是方法的调用,因为我们通常希望在方法的执行前、执行后或发生异常时插入切面逻辑。 在AOP中,可以选择在目标对象的所有方法上织入切面逻辑,也可以选择只在特定的方法上织入切面逻辑,这取决于所定义的切点(Pointcut)。切点是定义在哪些连接点上织入切面逻辑的规则,可以使用表达式或注解来定义切点。
Q6:切入点是类路径吗?
不,切入点(Pointcut)并不是类路径。切入点是在AOP
中用于定义哪些连接点(JoinPoint)应该被织入切面逻辑的规则。 切入点可以使用表达式或注解来定义。使用表达式定义切入点时,可以使用AspectJ
风格的切入点表达式,如execution、within、args
等关键字,以及类、方法、参数等信息来描述需要织入切面逻辑的连接点。使用注解来定义切入点时,可以通过在目标对象的方法上添加特定的注解,然后通过切入点表达式来匹配这些注解来确定要织入切面逻辑的连接点。 切入点可以非常灵活地定义,可以选择在目标对象的所有方法上织入切面逻辑,也可以选择只在特定的方法或类上织入切面逻辑。切入点的定义是根据业务需求和设计目标来确定的,通常会根据实际情况来选择需要织入切面逻辑的连接点。
Q7:织入的过程是不是就相当于动态代理和静态代理过程?
是的,织入的过程可以类比为动态代理和静态代理的过程。
在静态代理中,代理对象和目标对象实现同一个接口,代理对象在调用目标对象方法的前后插入额外的逻辑。这种方式需要在编译时期就确定代理关系,并在代码中显式地指定代理对象。
在动态代理中,代理对象是在运行时动态生成的,无需事先编写代理类。通过Java的反射机制,动态代理可以在运行时拦截并处理目标对象的方法调用。在动态代理中,我们可以在目标对象方法的调用前后插入切面逻辑。
类似地,AOP
的织入过程也是在运行时动态生成的。AOP
框架会根据切入点的定义,对目标对象的方法调用进行拦截,并根据切面逻辑对其进行增强。这个过程可以看作是在运行时动态代理的过程。 不同的是,动态代理和静态代理通常是针对单个类或对象进行的,而**AOP
的织入可以同时作用于多个类和对象**,根据切入点的定义对满足条件的连接点进行增强。
三、Spring MVC
Q1:说说自己对Spring MVC的了解
MVC是模型(Model)、视图(View)、控制器(Controller)的简写,它是通过将业务逻辑、数据、显示分离组织代码。
Q2:Spring MVC的核心组件有哪些?
DispatcherServlet
:核心的中央处理器,负责接受请求、分发,并给予客户响应HandlerMapping
:处理器映射器,根据URL去匹配查找能处理的Handler
,并会将请求涉及到的拦截器和Handler
一起封装HandlerAdapter
:处理器适配器 ,根据HandlerMapping
找到的Handler
,适配执行对应的Handler
.Handler
:请求处理器,处理实际请求的处理器ViewResolver
:视图解析器,根据Handler
返回的逻辑视图,解析并渲染真正的视图,并传递给DispatcherServlet响应客户端
Q3:Spring MVC的工作原理
- 客户端发送请求,
DispatcherServlet
拦截请求 DispatcherServlet
根据请求信息调用HandlerMapping
。HandlerMapping
根据URL去匹配查找能处理的Handler
,并会将涉及到的拦截器和Handler
一起封装DispatcherServlet
调用HandlerAdapter
适配器执行Handler
Handler
完成对用户的请求的处理,返回一个ModelAndView
对象给DispatcherServlet
ViewResolver
根据逻辑View
查找实际的View
DispatcherServlet
把返回的Model
传给View
View
返回客户端
Q4:Handler 是什么?
Handler
是指在 Spring MVC
框架中用于处理用户请求的组件。Handler
可以是一个 Controller
类中的方法、一个 Servlet
、一个WebSocket
处理器或其他可处理请求的组件。
Q5:DispatcherServlet 为什么调用 HandlerAdapter适配器执行 Handler?
不同的 Handler
可能有不同的处理方式,例如一个 Controller
方法、一个 Servlet
、一个 WebSocket
处理器等。为了统一处理不同类型的 Handler
,需要使用适配器模式将不同类型的 Handler
适配为统一的处理方式。HandlerAdapter
适配器的作用就是根据不同类型的 Handler,将请求信息进行适配,使其能够被统一调用并执行。它根据 Handler 的类型,调用相应的适配方法,将请求信息传递给 Handler 进行处理,并获取处理结果返回给 DispatcherServlet
。 通过使用 HandlerAdapter
适配器,DispatcherServlet
不需要关心具体的Handler
类型,只需要调用适配器的方法即可,实现了对不同类型 Handler
的统一调用和处理。
Q5:为什么会将请求涉及到的拦截器和 Handler 一起封装?
将请求涉及到的拦截器和Handler一起封装的目的是为了在请求处理过程中能够方便地对请求进行拦截和处理。 拦截器是用于对请求进行预处理和后处理的组件,可以在请求到达Controller
之前和之后执行额外的逻辑。通过将拦截器和Handler一起封装,可以实现以下几个方面的功能:
- 统一管理拦截器:将拦截器和Handler一起封装,可以方便地统一管理拦截器的配置和使用。在配置文件或注解中指定拦截器的顺序和范围,可以灵活地控制拦截器的执行顺序和作用范围。
- 方便拦截器的调用:将拦截器和Handler一起封装后,可以在请求到达Controller之前和之后分别调用拦截器的预处理和后处理方法。这样可以方便地对请求进行拦截和处理,执行一些通用的操作,如权限验证、日志记录等。
- 提高代码的复用性:将一些通用的逻辑处理抽离出来作为拦截器,通过封装拦截器和Handler,可以在多个请求处理过程中共享这些逻辑,提高代码的复用性。例如,多个请求需要进行权限验证,可以将权限验证的逻辑抽象为一个拦截器,在多个请求处理过程中共享使用。
- 灵活控制请求处理流程:通过封装拦截器和Handler,可以灵活控制请求处理流程。在拦截器的预处理方法中,可以决定是否继续处理请求,或者进行一些重定向或错误处理操作。在拦截器的后处理方法中,可以对响应结果进行进一步的处理或修改。
四、设计模式
Q1:Spring框架中用到了哪些设计模式?
- 工厂模式:通过
beanFactory
、ApplicationContext
创建bean
对象 - 代理模式:
Spring AOP
功能的实现 - 单例模式:
Spring
的bean
默认都是单例的 - 适配器模式:
Spring MVC
用到了适配器模式适配handler、Spring AOP
的Advice
用到了适配器模式适配成MethodInterceptor
接口类型的对象(全都变成方法拦截器对象) - 模板模式:以
Template
结尾的对数据库操作的类 - 包装器模式:项目需要连接多个数据库,不同的客户每次访问可能会访问不同的数据库。包装器模式可动态的切换不同数据源。
五、Spring事务
- 声明式事务:通过
AOP
实现(基于@Transactional
的全注解) - 编程式事务:通过
TransactionTemplate
手动管理事务
Q1:事务的特性(ACID)了解吗?
- 原子性:事务是最小的执行单位,不允许分割,事物的原子性确保动作要么全部完成,要么不起作用
- 一致性:执行事务前后,数据保持一致,例如转账时,无论事务是否成功,转账人和收款人的总额应该是不变的
- 隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间的数据库是独立的
- 持久性:一个事务被提交以后,他对数据库的改变是持久的,数据库发生故障也不应该对它有任何影响
只有保证了原子性、隔离性、持久性以后,一致性才能得到保障:AID->C
Q2:什么时候才用事务的隔离性?
事务的隔离性是指多个事务并发执行时,各个事务之间的数据应该是相互隔离的,一个事务的操作不应该对其他事务的操作产生影响。在并发环境下,如果多个事务同时访问和修改共享数据,可能会导致数据不一致的问题。 举个例子来说明事务的隔离性的使用场景: 假设有一个电商平台,用户可以在平台上购买商品,而商品的库存是需要管理的。当用户购买商品时,会进行库存的扣减操作。在这个场景中,如果不考虑事务的隔离性,可能会出现以下问题:
- 非隔离性导致的库存错误:假设有两个用户 A 和 B 同时购买同一件商品,且库存只有一件。如果不使用事务的隔离性,A 和 B 同时发起购买请求时,在没有互斥控制的情况下,可能会导致库存扣减错误。例如,A 和 B 同时读取库存数量为 1,然后都进行库存扣减,最终库存可能会变成 -1。 为了解决这个问题,可以使用事务的隔离性。通过将库存扣减操作放在一个事务中,并使用合适的隔离级别,可以避免并发访问时的数据不一致问题。例如,使用数据库的隔离级别
SERIALIZABLE
,可以确保同一时间只有一个事务能够访问和修改库存数据,从而避免库存错误问题。
Q3:MySQL怎么保证原子性?
保证原子性,就要在异常发生的时候对已经执行的操作进行回滚,在MySQL
中,恢复机制是通过回滚日志实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中出现异常,就利用回滚日志中的信息将数据回滚到修改之前的样子。并且,回滚日志会先把数据持久化到硬盘上,这样就能保证数据库在事务执行过程中宕机,未完成的事务会自动回滚。当再次打开数据库时,数据库会通过回滚日志来查找未完成的事务,并对其进行回滚操作。
Q4:为什么报错直接自动回滚,而不需要调用setRollbackOnly方法?
public Object save2(){
//编程式事务
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {//回调方法
//新增用户
User user = new User();
user.setUsername("大姐");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("999"+user.getSalt()));
user.setEmail("aasss@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
//新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("大家好");
post.setContent("我来啦");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
// Integer.valueOf("abc");//报错回滚
return "ok";
}
});
}
在上述代码中,使用了编程式事务,通过事务模板(transactionTemplate)
来管理事务的隔离级别和传播行为。当代码执行到Integer.valueOf("abc")
这行时,会抛出异常,导致事务中断,此时事务模板会自动回滚事务。 事务模板内部使用了AOP
的方式来管理事务,它会在方法执行前开启事务,在方法执行后根据方法的执行结果(是否抛出异常)来决定是提交事务还是回滚事务。当方法抛出异常时,事务模板会捕捉到异常,并将事务标记为回滚状态,然后执行回滚操作。因此,在编程式事务中,不需要显式地调用setRollbackOnly()
方法来手动标记回滚。 需要注意的是,事务的回滚并不是发生在异常抛出的地方,而是在方法执行结束后的事务提交阶段。因此,如果在异常抛出之前有对数据库进行的修改操作,这些操作仍然会被提交到数据库中。只有在事务提交阶段时,发现事务标记为回滚状态,才会执行回滚操作,将之前的修改操作全部撤销。
Q5:Spring事务有哪几种传播行为?
事务传播是为了解决业务层方法之间互相调用的事务问题
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播,例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。以下默认外部方法的事务为Propagation.REQUIRED
:
TransactionDefinition.PROPAGATION_REQUIRED
:使用最多的事务传播行为,平时使用的@Transactional
注解默认使用这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,就创建一个新事务。- 如果外部方法没有开启事务的话,
Propagation.REQUIRED
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 - 如果外部方法开启事务并且被
Propagation.REQUIRED
的话,所有Propagation.REQUIRED
修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。
- 如果外部方法没有开启事务的话,
TransactionDefinition.PROPAGATION_REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,就把当前的事务挂起。也就是说不管外部方法是否开启事务,PROPAGATION_REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。- 如果我们上面的
内部方法
使用PROPAGATION_REQUIRES_NEW
事务传播行为修饰,外部方法
还是用PROPAGATION_REQUIRED
修饰的话。如果外部方法
发生异常回滚,内部方法
不会跟着回滚,因为内部方法
开启了独立的事务。但是,如果内部方法
抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,外部方法
同样也会回滚,因为这个异常被外部方法
的事务管理机制检测到了。
- 如果我们上面的
TransactionDefinition.PROPAGATION_NESTED
:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务运行;如果没有事务,则取TransactionDefinition.PROPAGATION_REQUIRED
- 如果
内部方法
回滚的话,外部方法
不会回滚。 - 如果
外部方法
回滚的话,内部方法
会回滚。
- 如果
Q6:如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。那如果内部出错,会等内部程序结束就回滚吗,还是说等外部结束才回滚?
是的,会等内部程序结束就回滚。当内部方法出现异常时,事务的回滚行为取决于异常的处理方式。如果内部方法的异常被捕获并处理,事务将根据异常处理的逻辑进行提交或回滚。如果内部方法的异常没有被捕获或者没有进行特殊处理,异常将会向上抛出,传递给外部方法。但由于外部方法没有事务,事务的回滚行为将由异常处理方式决定,而不会影响到外部方法的事务。
Q7:事务的隔离级别有哪些?
READ-UNCOMMITTED
(读未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致,脏读、不可重复读、幻读。READ-COMMITTED
(读已提交):允许读取已提交的数据,可以阻止脏读,但是还有幻读和不可重复读。REPEATABLE-READ
(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。在可重复读的隔离级别下,如果一个事务对数据进行了修改并提交,另一个事务在该事务提交之前不会看到被修改后的数据。只有在修改事务提交后,其他事务才能看到修改后的结果。SERIALIZABLE
(串行化):最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
Q8:并发事务带来了哪些问题?
- 脏读:一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据(读到脏数据)
- 不可重复读:指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读(多次读,数据不一样)
- 幻读:幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样(行数不一致)
六、Spring注解
Q1:@RestController与@Controller的区别
@RestController
注解是@Controller
和@ResponseBody
的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。
单独使用 @Controller
不加 @ResponseBody
的话一般是用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller
+@ResponseBody
返回 JSON 或 XML 形式数据。
- 示例1: @Controller+@ResponseBody 返回 JSON 格式数据
SpringBoot 默认集成了 jackson ,对于此需求你不需要添加任何相关依赖。
public class Person {
private String name;
private Integer age;
......
省略getter/setter ,有参和无参的construtor方法
}
@Controller
public class HelloController {
@PostMapping("/hello")
@ResponseBody
public Person greeting(@RequestBody Person person) {
return person;
}
}
使用 post 请求访问 http://localhost:8080/hello ,body 中附带以下参数,后端会以json 格式将 person 对象返回。
{
"name": "teamc",
"age": 1
}
- 示例2: @RestController 返回 JSON 格式数据
只需要将HelloController
改为如下形式:
@RestController
public class HelloController {
@PostMapping("/hello")
public Person greeting(@RequestBody Person person) {
return person;
}
Q2:@RequestMapping和@XxxMapping不同之处
@RequestMapping
:可注解在类和方法上;@XxxMapping
仅可注册在方法上@RequestMapping
:可进行 GET、POST、PUT、DELETE 等请求方法;@XxxMapping
是@RequestMapping
的 Xxx请求方法的特例,目的是为了提高清晰度。
Q3:@PathVariable 和 @RequestParam的区别
@PathVariable
用于获取路径参数,@RequestParam
用于获取查询参数。
举个简单的例子:
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
@PathVariable("klassId") Long klassId,
@RequestParam(value = "type", required = false) String type ) {
...
}
如果我们请求的 url 是:/klasses/123456/teachers?type=web
那么我们服务获取到的数据就是:klassId=123456,type=web
Q4:@RequestBody的用法
@RequestBody
注解用于将HTTP请求正文绑定到方法参数上,通常用于处理POST请求的JSON数据。下面是一个@RequestBody
注解的例子: 假设有一个用户管理系统,需要添加新用户,前端通过JSON数据提交用户信息,后端使用@RequestBody
注解将JSON数据绑定到User
对象上,代码如下:
javaCopy code@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<?> addUser(@RequestBody User user) {
userService.addUser(user);
return ResponseEntity.ok().build();
}
}
以上代码中,@PostMapping("/users")
指定了处理POST请求的/users
接口,@RequestBody
注解将HTTP请求正文绑定到User
对象上,将请求中的JSON数据转换成User
对象,可以直接使用User
对象进行业务处理。ResponseEntity.ok().build()
表示返回一个HTTP 200响应。 假如前端通过POST请求传递以下JSON数据:
jsonCopy code{
"name": "张三",
"age": 18,
"gender": "male"
}
那么后端会将这个JSON
数据转换成一个User
对象,其中name
属性为"张三"
,age
属性为18
,gender
属性为"male"
,然后将这个User
对象传递给addUser()
方法进行业务处理。
Q5:@Transactional事务注解原理
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。