目录
9.Spring&SpringMVC&SpringBoot常用注解
1.Spring中Bean的线程安全问题
问题原因
再明白Spring中Bean是否为线程安全问题前我们先补习一些其他知识
Spring中Bean分为单例singleton和多例prototype,其中单例是唯一的就是说IOC容器中就这一个全局共享Bean,多例是指我们每次去IOC容器获取Bean时IOC容器都会创建一个新的Bean给你,具体可以在代码中修改单例还是多例。
@Service
@Scope("singleton")
public class UserServiceImpl implements UserService {
}
由于Spring中没有对于单例bean进行任何多线程的封装处理,所以它是非线程安全的,但是分为有状态和无状态,我们通常在项目中使用的Spring bean都是不可变的状态(比如Service类和DAO类)这类Bean是线程安全的,那么有状态是指如下这种
import org.springframework.stereotype.Service;
@Service
public class NonThreadSafeStatefulService {
private static String state; // 使用静态变量,可以被多个线程共享
public static void setState(String state) {
NonThreadSafeStatefulService.state = state; // 修改静态变量
}
public static String getState() {
return NonThreadSafeStatefulService.state; // 读取静态变量
}
}
state
是一个静态变量,可以被多个线程共享。如果多个线程同时调用 setState
和 getState
方法,就可能导致线程安全问题。因此,非线程安全的。
解决方案
1)最粗暴的办法就是将Bean修改为多例
@Service
@Scope("prototype")
public class UserServiceImpl implements UserService {
}
2)第二种方式就是通过ThreadLocal解决,ThreadLocal本身就具备线程安全的隔离性,将可变的成员变量保存在ThreadLocal中,为每个线程提供一个独立的变量副本,不同线程只能操作自己的副本变量
2.Spring中AOP原理
AOP是“面向切面编程”的缩写。其中Aop是通过动态代理实现的,就是不改变你原有业务逻辑的基础上进行一个增强。在Spring框架中,AOP用来处理那些与具体业务无关,但对多个对象都有影响的公共操作和逻辑。比如,记录日志或管理事务这样的操作,它们在多个地方都可能用到。AOP能帮你把这些公共的部分抽取出来,变成可复用的模块,这样就可以减少代码之间的耦合性。你说就好比ServiceImpl就负责写业务逻辑而你却写了一堆公共业务逻辑这不是把代码都耦合在一起提高复杂度嘛?所以简单来说,AOP就像一个工具箱,里面装满了各种常用的工具和零件,让你在需要的时候随时取用,而不需要每次都重新制作。
- 切面(Aspect): 切面是关注点的模块化,它包含一组跨越多个类的行为。例如,一个日志记录切面可以定义在每个方法执行前后记录日志。切面本身并不包含完整的业务逻辑,而是定义了如何在关注点处执行逻辑。
- 连接点(Join Point): 连接点是在应用执行过程中可以插入切面的点,通常是方法调用、对象初始化等。切面可以在连接点前、后、环绕等不同的时机执行。
- 通知(Advice): 通知是切面在连接点处执行的具体行为,可以是方法调用、异常处理、环绕逻辑等。主要的通知类型有前置通知(Before)、后置通知(After)、环绕通知(Around)、返回通知(AfterReturning)和异常通知(AfterThrowing)。
- 切点(Pointcut): 切点定义了哪些连接点会触发切面的通知。它通过表达式或规则来匹配一组连接点。
- 织入(Weaving): 织入是将切面应用到目标代码的过程,即将切面的通知插入到连接点处。织入可以在编译时、类加载时、运行时等不同阶段进行。
- 引入(Introduction): 引入是一种动态为类添加新方法或属性的机制,允许切面向现有类添加新功能。这可以用来实现类似于多继承的效果。
动态代理
上面提到一个词语动态代理,再说这个知识点之前我跟大家分享一下我以前老师教给我的一句话,‘’对象如何嫌身上干的事太对的话,可以通过代理来转移部分职责‘’而Java中动态代理分为两种,静态代理、动态代理,其中动态代理分为两种,JDK动态代理、CGlib动态代理,其中静态代理一般通过我们手写出代理Class比较麻烦而且比较冗余,我主要说动态代理。
JDK动态代理
JDK动态代理:基于接口的动态代理技术,就是说当目标对象实现了接口时,默认情况下会采用JDK的动态代理实现AOP。这种方式只代理目标接口方法。
CGLIB动态代理
CGLIB代理:基于父类的动态代理技术,就是说如果目标对象没有实现接口,必须采用CGLIB库来实现AOP。这种方式通过为目标对象生成的子类来实现代理,对于final修饰的方法没有作用。
AOP记录日志方式
开发中我们通常使用AOP来进行日志记录,主要思路是获取请求的用户名、请求方式、访问地址、模块名称、登录ip、操作时间,记录到数据库的日志表中。
AOP实现事物原理
Spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务控制:需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用
声明式事务管理:声明式事务管理建立在AOP之上的也就是直接使用@Transactionno注解。其本质是通过AOP功能,对方法前后进行拦截,当某个方法被调用时,AOP会捕获该方法的事务信息,然后进行事务控制。例如,当方法执行成功时,会自动提交事务;当方法执行失败时,会自动回滚事务。
3. 事物失效的场景及原因
在日常开发当中我们对数据库进行操作都会使用事物@Transactional来保证事物不会出现脏数据,数据不一致情况,但是使用事物的同时也要注意事物失效的场景。
场景一:异常捕获处理
此代码中我们可以看到我们手动添加了一个异常 int a = 1 / 0并且也添加了事物@Transactional,但是这个代码是事物失效的,因为我们对事物进行了捕获处理,但是却没有抛出,因为异常处理机制是通过try-catch语句块来实现的。当程序在try部分遇到一个异常时,控制流会立即转移到相应的catch部分,并执行其代码。如果在try-catch块中捕获异常但选择不抛出(即不使用throw语句),那么该异常会被“隐藏”,则AOP即事物无法感知异常的存在,因此会默认为操作都成功了,解决方案就是,在catch块添加throw new RuntimeException(e)抛出
@Transactional
public void update(Integer from, Integer to, Double money) {
try {
转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
异常 int a = 1 / 0;
被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
} catch (Exception e) {
e.printStackTrace();
}
}
场景二:抛出检查异常
我们可以看到读取文件IO流是一个编译时异常我们选择了抛出,那么这种情况事物就失效了,由于IO流找不到dddd文件位置会抛出异常,此时事物是无法捕获到的,就会造成IO流场面的数据库操作成功,IO流下面的代码失败,造成这样的原因就是因为,事物@Transactional只会对RunTimeExction运行时进行事物处理,而非运行时异常即编译时抛出的异常就无法处理,解决方案就是,最简单的办法就是改成@Transactional(robackfor = Excption.class)就是说不管发生什么异常我都进行捕获进行处理。
@Transactional
public void update(Integer from, Integer to, Double money) throws FileNotFoundException {
//转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
读取文件 new FileInputStream("dddd");
被转账的用户 Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
}
场景三:非public修饰的方法
这个很简单非public修饰的方法会导致事物失效,原因是Spring为方法创建代理,添加事物通知,前提条件都是该方法时public公共的,不是public就无法创建代理。
@Transactional
void update(Integer from, Integer to, Double money) throws FileNotFoundException {
//转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
读取文件 new FileInputStream("dddd");
被转账的用户 Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
}
场景四:本类方法调用
如果同一个类中的两个方法分别为A和B,方法A上没有添加事务注解,方法B上添加了@Transactional事务注解,方法A调用方法B,那么方法B的事务会失效,因为事物是基于AOP实现的,那么就相当于把本类调用就相当于把方法B的代码粘过来了,不是代理对象,解决方案就是通过OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();转成自己的本类再去调用这样就属于代理对象了,还要加上@EnableAspectJAutoProxy(exposeProxy = true) //对外暴露代理对象,也要引入AOP的依赖
4. Spring事物传播行为
事务传播行为决定了一个方法执行时如何参与到已有的事务中,或者如何创建新的事务。需要根据实际的业务场景和要求来选择合适的事务传播行为。总共七种我列举一下,然后我主要说前两个最常用的。
使用示例
使用事务传播行为需要在方法上加上 @Transactional
注解,并指定传播行为,例如如下。
@Transactional(propagation=Propagation.REQUIRED)
public void doSomething() {
// ...
}
A类有一个a方法,B类有一个b方法,如果在A的方法逻辑里面调用了b方法,那么B是用A的事务还是不用A的事务
1)REQUIRED:A调用了B方法,这个时候会判断一下A上有没有事务,如果A里面有事务,那么B就用你A里面的事务,如果A里面没有事务,我自己重新写一份。
2)REQUIRES_NEW:比如a有事务,那么a在调这个b的时候,那么b会把你这个a的事物挂起来,然后自己再创建一个事物,假如说你a没有事物,那么你a在调我b的时候,那么我也会创建一个事物,就是说跟你不发发生关系
3)SUPPORTS
:支持当前事务,如果当前不存在事务,则以非事务方式执行。
4)NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,则挂起该事务。
5)NEVER
:以非事务方式执行操作,如果当前存在事务,则抛出异常。
6)NESTED
:如果当前存在事务,则嵌套事务中执行。嵌套事务是相对于外部事务而言的,它可以独立提交或回滚,但是嵌套事务的提交或回滚并不会对外部事务产生影响。如果外部事务不存在,那么 NESTED
与 REQUIRED
的效果是一样的。该传播行为只有在使用 JDBC 事务时才有效。
7)MANDATORY
:强制要求当前存在事务,如果不存在事务,则抛出异常。(mandatory:adj.强制的)
5. Spring事物隔离级别
事务隔离级别是用来控制多个并发事务之间如何相互“看到”或“影响”彼此的数据的一种设置,它决定了事务的“独立”程度。
1)脏读:当一个事务正在访问数据并且对数据进行了修改,而这种修改
还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”。
2)不可重复度:事务A查询ID为1的数据,事务B修改了ID为1的数据并且提交了,这时事务A在此去查询ID为1的数据就跟上次查询的不一致,因为查询的是事务B提交的数据,也就是一次事物内查询到了两种不同的数据。
3)幻读:事物A读取了几行数据,接着另一个并发事务B插入了一些数据。在随后的查询中,第一个事务A就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读
解决方案
(未提交读READ UNCOMMITTAD) 什么也解决不了
(读已提交READ COMMITTED) 脏读
(可重复读REPEATABLE READ) 脏读,不可重复读
(串行化 SERIALIZABLE) 什么都可以解决
为什么不用串行化因为一个事物执行下个事物才可以执行,相当于放弃了并发事物,在开发中不可能会用。
使用方式
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void save(){
System.out.println("hello word");
}
6. Spring中的IOC容器
首先Spring IOC 它全称叫这个价控制反转
我们都知道在传统的 Java 程序开发里面,我们只能通过 new 关键字来去创建一个对象,那么这种方式会导致程序里面对象的依赖关系比较复杂,而且耦合度比较高。而 IOC 的主要作用就是实现了对象的管理,也就是我们把设计好的对象交给 IOC 容器来控制,然后在需要用到目标对象的时候,直接从容器里面去获取,有了 IOC 容器来管理Bean以后,相当于把对象的创建和查找依赖对象的控制交给了容器。这种设计理念使得对象和对象之间是一种松耦合状态,极大的提升了程序的灵活性以及功能的复用性。
7. SpringMVC执行流程
第一,用户发起请求,全部到spring MVC 里面的前端控制器DispatcherServlet中,然后将接收到的请求转发给处理器映射器 handler mapping
第二, 处理器映射器负责解析请求,根据请求信息和配置信息找到匹配的 controller 类。
第三,找到匹配的 controller 以后,会把请求参数传递给 controller 里面的方法。
第四, controller 里面的方法执行完成以后,会返回一个 mode and view,这里面会包括视图名称和需要传递给视图的模型数据。
第五,然后视图解析器会根据名字找到视图,然后把数据模型填充到视图里面,再渲染成 HTML 内容返回给客户端。
8.SpringBoot自动装配
1,在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
2, 其中@EnableAutoConfiguration是实现自动化配置的核心注解。 该注解通过@Import注解导入对应的配置选择器。 内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。 在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。
3, 条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
9.Spring&SpringMVC&SpringBoot常用注解
Spring
SpringMVC
SpringBoot