Spring

Spring-Framework-4-架构图

1629720945720-2

核心层

  • Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块

AOP层

  • AOP:面向切面编程,它依赖核心层容器,目的是在不改变原有代码的前提下对其进行功能增强
  • Aspects:AOP是思想,Aspects是对AOP思想的具体实现

数据层

  • Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术
  • Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis
  • Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现,也是后期学习的重点内容

Web层

  • 这一层的内容将在SpringMVC框架具体学习

Test层

  • Spring主要整合了Junit来完成单元测试和集成测试

Spring与IOC、DI之间关系

IOC(Inversion of Control)控制反转

  • 使用对象时,由主动new产生对象转换为由”外部“提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。

Spring和IOC之间的关系

  • Spring技术对IOC思想进行了实现

  • Spring提供了一个容器,称为IOC容器,用来充当IOC思想中的”外部“

  • IOC容器负责对对象的创建、初始化等一系列工作,被创建或被管理的对象统称为Bean。

DI(Dependency Injection)依赖注入

  • 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入

1629735078619-2

Bean销毁时机

容器关闭前触发bean的销毁

关闭容器方式

  1. 手工关闭容器:ConfigurableApplicationContext接口close()操作
  2. 注册关闭钩子,在虚拟机退出前先关闭容器再退出虚拟机:ConfigurableApplicationContext接口registerShutdownHook()方法
public class AppForLifeCycle{
public static void main(String[] args){
ConfigurableApplicationContext ctx = new ConfigurableApplicationContext("applicationContext.xml");
// 在容器未关闭之前,提前设置好回调函数,让JVM在退出之前回调此函数来关闭容器
ctx.registerShutdownHook();
// 业务结束,调佣close
ctx.close();
}
}

ConfigurableApplicationContext是ApplicationContext的子类

XML版开发

IOC入门案例

public interface BookDao {}
public class BookDaoImpl implements BookDao {}
public interface BookService {}
public class BookServiceImpl implements BookService {}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--bean标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.zx.service.impl.BookServiceImpl"/>
</beans>
public class App {
public static void main(String[] args) {
// 获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取实例对象
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
BookService bookService = (BookService) ctx.getBean("bookService");
}
}

DI入门案例

去除代码中的new,为属性提供setter方法

public class BookServiceImpl implements BookService {
// 删除new方式创建的对象
private BookDao bookDao;

// 提供对应的set方法
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
}

修改配置完成注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--bean标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl"/>

<bean id="bookService" class="com.zx.service.impl.BookServiceImpl">
<!--配置server与dao的关系-->
<!--property标签表示配置当前bean的属性
name属性表示配置哪一个具体的属性
ref属性表示参照哪一个bean
-->
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>

对应关系:name的值对应类成员变量,ref指向需要注入的bean

name属性、scope属性

bean别名配置-name属性

定义bean的别名,可定义多个,使用逗号(,)分号(;)空格( )分隔。

<bean id="bookDao" name="dao bookDaoImpl" class='com.zx.dao.impl.BookDaoImpl'/>
<bean name="service,bookServiceImpl" class="com.zx.service.impl.BookServiceImpl"/>

注意:无法获取到,将抛出异常:NoSuchBeanDefinitionException

bean作用范围-scope属性

默认单例singleton、非单例prototype

<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl" scope="prototype" />

bean作用范围说明

  • 为什么bean默认为单例?
  • 适合交给容器进行管理的bean
    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 不适合交给容器进行管理的bean
    • 封装实体的域对象

实例化bean的3种方式

1.构造方法

提供可访问的构造方法

public class BookDaoImpl implements BookDao {
// 默认空参构造,可不写
public BookDaoImpl() {
System.out.println("book dao constructor is running ....");
}
public void save() {
System.out.println("book dao save ...");
}
}

配置

<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl"/>

注意:无参构造方法如果不存在,将抛出异常BeanCreationException

2.静态工厂(了解)

//静态工厂创建对象
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
return new OrderDaoImpl();
}
}

配置

<bean id="orderDao" class="com.zx.factory.OrderDaoFactory" factory-method="getOrderDao"/>

3.实例工厂

public class UserDaoFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}

配置

<bean id = "userDaoFactory" class = "com.zx.factory.UserDaoFactory" />
<bean id = "userDao" factory-method = "getUserDao" factory-bean = "userDaoFactory" />

注意:第一个bean配合使用的,实际无意义。工厂方法名不固定,每次需要配置。

实例工厂改良(实现FactoryBean)

public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
//返回所创建类的Class对象
public Class<?> getObjectType() {
return UserDao.class;
}
// 默认为true,单例,可以不重写
default boolean isSingleton() {
return true;
}
}

配置

<bean id="userDao" class="com.zx.factory.UserDaoFactoryBean"/>

bean生命周期控制

bean生命周期

  • 初始化容器
    1. 创建对象(内存分配)
    2. 执行构造方法
    3. 执行属性注入(set操作)
    4. 执行bean初始化方法
  • 使用bean
    1. 执行业务操作
  • 关闭/销毁容器
    1. 执行bean销毁方法

提供生命周期控制方法

public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
//表示bean初始化对应的操作
public void init(){
System.out.println("init...");
}
//表示bean销毁前对应的操作
public void destory(){
System.out.println("destory...");
}
}

配置生命周期控制方法

<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>

接口控制(了解)

实现InitializingBean,DisposableBean接口

public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
public void destroy() throws Exception {
System.out.println("service destroy");
}
public void afterPropertiesSet() throws Exception {
System.out.println("service init");
}
}

依赖注入的2种方式

setter注入-引用类型

在bean中定义引用类型,并提供可访问的setter方法

public class BookServiceImpl implements BookService {
private UserDao userDao;

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
<bean id="userDao" class="com.zx.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.zx.service.impl.BookServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>

setter注入-简单类型(了解)

在bean中声明简单数据类型,并提供对应的setter方法

public class BookDaoImpl implements BookDao {
private String databaseName;
private int connectionNum;

public void setConnectionNum(int connectionNum) {
this.connectionNum = connectionNum;
}

public void setDatabaseName(String databaseName) {
this.databaseName = databaseName;
}
}
<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl">
<property name="databaseName" value="mysql"/>
<property name="connectionNum" value="10"/>
</bean>

构造器注入-引用类型

public class BookServiceImpl implements BookService{
private BookDao bookDao;

// 添加需要注入的引用类型变量的构造方法
public BookServiceImpl(BookDao bookDao) {
this.bookDao = bookDao;
}
}
<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.zx.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>

name属性对应的值为构造函数中方法形参的参数名,必须要保持一致。

ref属性指向的是spring的IOC容器中其他bean对象。

构造器注入-简单数据类型

public class BookDaoImpl implements BookDao {
private String databaseName;
private int connectionNum;

public BookDaoImpl(String databaseName, int connectionNum) {
this.databaseName = databaseName;
this.connectionNum = connectionNum;
}
}
<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl">
<constructor-arg name="databaseName" value="mysql"/>
<constructor-arg name="connectionNum" value="666"/>
</bean>

构造器注入-综合配置示例

<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl">
<constructor-arg name="databaseName" value="mysql"/>
<constructor-arg name="connectionNum" value="666"/>
</bean>

<bean id="userDao" class="com.zx.dao.impl.UserDaoImpl"/>

<bean id="bookService" class="com.zx.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="userDao" ref="userDao"/>
</bean>

构造器注入-参数适配(了解)

配置中使用constructor-arg标签type属性设置按形参类型注入

<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl">
<constructor-arg type="int" value="10"/>
<constructor-arg type="java.lang.String" value="mysql"/>
</bean>
<bean id="bookDao" class="com.zx.dao.impl.BookDaoImpl">
<constructor-arg index="1" value="100"/>
<constructor-arg index="0" value="mysql"/>
</bean>

方式选择

  • 使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
  • 使用setter注入有概率不进行注入导致null对象出现
  • Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  • 实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
  • 自己开发的模块推荐使用setter注入

依赖自动装配

定义:IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配

自动装配方式

  • 按类型(常用)
  • 按名称
  • 按构造方法
  • 不启用自动装配

autowire属性设置自动装配的类型

<!--autowire属性:开启自动装配,通常使用按类型装配-->
<bean id="bookService" class="com.zx.service.impl.BookServiceImpl" autowire="byType"/>

依赖自动装配特征

  1. 自动装配用于引用类型依赖注入,不能对简单类型进行操作
  2. 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
  3. 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
  4. 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效

集合注入

注入数组类型数据

<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
// 引用类型
<ref bean="填引用类型的beanId"/>
</array>
</property>

注入List类型数据

<property name="list">
<list>
<value>itcast</value>
<value>zx</value>
<value>boxuegu</value>
<value>chuanzhihui</value>
</list>
</property>

注入Set类型数据

<property name="set">
<set>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>boxuegu</value>
</set>
</property>

注入Map类型数据

<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="henan"/>
<entry key="city" value="kaifeng"/>
</map>
</property>

注入Properties类型数据

<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">henan</prop>
<prop key="city">kaifeng</prop>
</props>
</property>

Spring管理第三方数据源

导入druid的依赖

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>

加载properties文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<!-- 开启context命名空间 -->
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
<!-- 开启context命名空间 -->
http://www.springframework.org/schema/context
<!-- 开启context命名空间 -->
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 使用context命名空间,加载指定properties文件 -->
<context:property-placeholder location="jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!-- 使用${}读取加载的属性值 -->
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>

jdbc.properties文件:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db
jdbc.username=root
jdbc.password=root

各种加载写法

<!-- 不加载系统属性 -->
<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER" />

<!-- 加载多个properties文件 -->
<context:property-placeholder location="jdbc.properties, msg.properties" />

<!-- 加载所有properties文件 -->
<context:property-placeholder location="*.properties" />

<!-- 加载properties文件标准格式 -->
<context:property-placeholder location="classpath:*.properties" />

<!-- 从类路径或jar包中搜索并加载properties文件 -->
<context:property-placeholder location="classpath*:*.properties" />

创建容器的方式

// 方式1:类路径加载配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

// 方式2:文件路径加载配置文件
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\applicationContext.xml");

// 加载多个配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean1.xml", "bean2.xml");

获取bean的方式

// 方式1:bean名称
BookDao bookDao = (BookDao)ctx.getBean("bookDao");

// 方式2:bean名称+指定类型
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);

// 方式3:bean类型
BookDao bookDao = ctx.getBean(BookDao.class);

容器类层次结构图

1629984980781

spring扩展思维:增加子接口加入新功能

从图中可以看出,容器类也是从无到有根据需要一层层叠加上来的,大家重点理解下这种设计思想。

最早是使用beanfactory初始化

类路径加载配置文件

Resource resources = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(resources);
BookDao bookDao = bf.getBean(BookDao.class);
bookDao.save();

BeanFactory创建完毕后,所有的bean均为延迟加载

bean、依赖、容器总结

Bean相关总结

1629986510487-2

依赖注入相关总结

1629986848563-2

容器相关总结

  • BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
  • ApplicationContext接口是Spring容器的核心接口,初始化时bean立即加载
  • ApplicationContext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
  • ApplicationContext接口常用初始化类
    • ClassPathXmlApplicationContext(常用)
    • FileSystemXmlApplicationContext

注解开发

定义bean

使用@Component定义bean

@Component("bookDao")
public class BookDaoImpl implements BookDao {
}
@Component
public class BookServiceImpl implements BookService {
}

核心配置文件中通过组件扫描加载bean

<context:component-scan base-package="com.zx"/>

Spring提供@Component注解的三个衍生注解

  • @Controller:用于表现层bean定义
  • @Service:用于业务层bean定义
  • @Repository:用于数据层bean定义
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
}
@Service
public class BookServiceImpl implements BookService {
}

配置类

Spring3.0开启了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道

Java类代替Spring核心配置文件

<context:component-scan base-package="com.zhangxin"/>

TO:

@Configuration
@ComponentScan("com.zhangxin")
public class SpringConfig {
}

@Configuration注解用于设定当前类为配置类

@ComponentScan注解用于设定扫描路径,此注解只能添加一次,多个数据用数组格式

@ComponentScan({"com.zx.service","com.zx.dao"})

bean生命周期

使用@PostConstruct@PreDestroy 定义初始化方法、销毁方法

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
// 在构造方法之后执行,替换 init-method
@PostConstruct
public void init() {
System.out.println("init ...");
}
// 在销毁方法之前执行,替换 destroy-method
@PreDestroy
public void destroy() {
System.out.println("destroy ...");
}
}

依赖注入

使用@Autowired注解开启自动装配模式(按类型)

@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
// public void setBookDao(BookDao bookDao) {
// this.bookDao = bookDao;
// }
}

自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值,因此无需提供setter方法

自动装配建议使用无参构造方法创建对象(默认),如果不提供对应构造方法,请提供唯一的构造方法

使用@Qualifier注解指定名称装配(必须配合@Autowired使用)

@Autowired
@Qualifier("bookDao1")
private BookDao bookDao;

加载properties文件

使用@PropertySource注解加载properties文件

@Configuration
@ComponentScan("com.zx")
@PropertySource("classpath:jdbc.properties")
public class SpringConfig {
}

路径仅支持单一文件配置,多文件请使用数组格式配置,不允许使用通配符*

第三方bean管理

使用@Bean配置第三方bean

@Configuration
public class SpringConfig {
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}

使用独立的配置类管理第三方bean

public class JdbcConfig {
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}

将独立的配置类加入核心配置

方式1:导入式

public class JdbcConfig{
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
// 相关配置
return ds;
}
}

使用@Import注解手动加入配置类到核心配置,多数据用数组格式

@Configuration
@Import(JdbcConfig.class)
public class SpringConfig{
}

方式2:扫描式

@Configuration
public class JdbcConfig{
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
// 相关配置
return ds;
}
}

使用@ComponentScan注解扫描配置类所在的包,加载对应的配置类信息

@Configuration
@ComponentScan({"com.zx.config","com.zx.service","com.zx.dao"})
public class SpringConfig{
}

简单类型依赖注入

使用@Value注解引入值

public class JdbcConfig {
@Value("com.mysql.jdbc.Driver")
private String driver;
@Value("jdbc:mysql://localhost:3306/spring_db")
private String url;
@Value("root")
private String userName;
@Value("password")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

引用类型依赖注入

引用类型注入只需要为bean定义方法设置行参即可,容器会根据类型自动装配对象

步骤1:在SpringConfig中扫描BookDao

扫描的目的是让Spring能管理到BookDao,也就是说要让IOC容器中有一个bookDao对象

@Configuration
@ComponentScan("com.zx.dao")
@Import({JdbcConfig.class})
public class SpringConfig {
}

步骤2:在JdbcConfig类的方法上添加参数

@Bean
public DataSource dataSource(BookDao bookDao){
System.out.println(bookDao);
DruidDataSource ds = new DruidDataSource();
// 属性设置
return ds;
}

xml配置和注解配置比较

功能 xml配置 注解
定义bean bean标签
- id属性
- class属性
@Component
- @Controller
- @Service
- @Repository
@ComponentScan
设置依赖注入 setter注入(set方法)
- 引用/简单
构造器注入(构造方法)
- 引用/简单
自动装配
@Autowired
- @Qualifier
@Value
配置第三方bean bean标签
静态工厂、实例工厂、FactoryBean
@Bean
作用范围 - scope属性 @Scope
生命周期 标准接口
- init-method
- destory-method
@PostConstructor
@PreDestory

Spring整合

整合MyBatis

依赖:

<dependency>
<!--Spring操作数据库需要该jar包-->
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<!--
Spring与Mybatis整合的jar包
这个jar包mybatis在前面,是Mybatis提供的
-->
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

数据源配置类

public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

Mybatis配置类

public class MybatisConfig {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
//设置模型类的别名扫描
ssfb.setTypeAliasesPackage("com.zx.domain");
//设置数据源
ssfb.setDataSource(dataSource);
return ssfb;
}
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.zx.dao");
return msc;
}
}

SpringConfig主配置类,并且import数据源配置类、Mybatis配置类

//配置类注解
@Configuration
//包扫描
@ComponentScan("com.zx")
// 读properties
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

主程序

public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

AccountService accountService = ctx.getBean(AccountService.class);

Account ac = accountService.findById(1);
System.out.println(ac);
}
}

拓展:配置演变

从xml文件 –> 注解+配置类

1630138835057

1630138916939

xml版本:

create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
// 初始化属性数据
<!--读取外部properties配置文件-->
<properties resource="jdbc.properties"></properties>
// 初始化类型别名
<!--别名扫描的包路径-->
<typeAliases>
<package name="com.zx.domain"/>
</typeAliases>
// 初始化dataSource
<!--数据源-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
// 初始化映射配置
<!--映射文件扫描包路径-->
<mappers>
<package name="com.zx.dao"></package>
</mappers>
</configuration>
public class App {
public static void main(String[] args) throws IOException {
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 加载SqlMapConfig.xml配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak");
// 3. 创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);

// 获取连接,获取实现
// 4. 获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 5. 执行SqlSession对象执行查询,获取结果User
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);

// 获取数据层接口
Account ac = accountDao.findById(1);
System.out.println(ac);

// 关闭接口
// 6. 释放资源
sqlSession.close();
}
}

整合JUnit

导Jar包

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

test\java下创建一个xxxTest

// 设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
// 设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
// @ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {
// 支持自动装配注入bean
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
System.out.println(accountService.findById(1));
}
}

知识点1:@RunWith

名称 @RunWith
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit运行器
属性 value(默认):运行所使用的运行期

知识点2:@ContextConfiguration

名称 @ContextConfiguration
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit加载的Spring核心配置
属性 classes:核心配置类,可以使用数组的格式设定加载多个配置类
locations:配置文件,可以使用数组的格式设定加载多个配置文件名称

AOP

AOP简介

AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。

  • OOP(Object Oriented Programming)面向对象编程

作用:在不惊动原始设计的基础上为其进行功能增强

Spring理念:无入侵式/无侵入式

1630144353462-2

AOP核心理念

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • 在SpringAOP中,理解为方法的执行
  • 切入点(Pointcut):匹配连接点的式子
    • 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
      • 一个具体的方法:如com.zx.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
    • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
  • 通知(Advice):在切入点处执行的操作,也就是共性功能
    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:定义通知的类
  • 切面(Aspect):描述通知与切入点的对应关系。

AOP核心概念

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

AOP入门案例(注解版)

案例设定:测算接口执行效率.

简化设定:在方法执行前输出当前系统时间。

开发模式:XML or 注解

思路分析:

1.导入坐标(pom.xml)

2.制作连接点(原始操作,Dao接口与实现类)

3.制作共性功能(通知类与通知)

4.定义切入点

5.绑定切入点与通知关系(切面)

实现:

<dependencies>
<!--spring-context依赖spring-aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
public interface BookDao {
public void save();
public void update();
}

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}

public void update(){
System.out.println("book dao update ...");
}
}
// 定义bean,被spring容器管理
@Component
// 定义当前类为切面类
@Aspect
public class MyAdvice {
// 定义切入点,切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
@Pointcut("execution(void com.zx.dao.BookDao.update())")
private void pt(){}

// 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
@Configuration
@ComponentScan("com.zx")
// 开启Spring对AOP注解驱动支持
@EnableAspectJAutoProxy
public class SpringConfig {
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}

知识点1:@EnableAspectJAutoProxy

名称 @EnableAspectJAutoProxy
类型 配置类注解
位置 配置类定义上方
作用 开启注解格式AOP功能

知识点2:@Aspect

名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类

知识点3:@Pointcut

名称 @Pointcut
类型 方法注解
位置 切入点方法定义上方
作用 设置切入点方法
属性 value(默认):切入点表达式

知识点4:@Before

名称 @Before
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

AOP工作流程

1.spring启动

2.读取所有切面配置中的切入点

@Component
@Aspect
public class MyAdvice{
@Pointcut("execution(void com.zx.dao.BookDao.save())")
private void ptx(){}

@Pointcut("execution(void com.zx.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}

3.初始化bean,判定bean对应的类中的方法是否匹配到任意切入点

  • 第一种:匹配失败,创建对象
  • 第二种:匹配成功,创建原始对象(目标对象)的代理对象

4.获取bean

  • 第一种:获取bean,调用方法并执行,完成操作

  • 第二种:获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

AOP切入点表达式

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式
package com.zx.dao;
public interface BookDao{
public void update();
}

public class BookDaoImpl implements BookDao{
public void update(){
System.out.println("book dao update");
}
}

描述方式一:执行com.zx.dao包下的BookDao接口中的无参数update方法

execution(void com.zx.dao.BookDao.update())

描述方式二:执行com.zx.dao.impl包下的BookDaoImpl类中的无参数update方法

execution(void com.zx.dao.impl.BookDaoImpl.update())

切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

execution(public User com.zx.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • com.zx.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

通配符描述切入点,快速描述

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    execution(public * com.zx.*.UserService.find*(*))

    匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    execution(public User com..UserService.findById(..))

    匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

  • +:专用于匹配子类类型

    execution(* *..*Service+.*(..))

    这个使用率较低,描述子类的,咱们做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。

示例:

execution(void com.zx.dao.BookDao.update())
匹配接口,能匹配到
execution(void com.zx.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* com.zx.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.zx.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.update())
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.zx.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.zx.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

后面两种更符合我们平常切入点表达式的编写规则

书写技巧

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用\*匹配,例如UserService书写成*Service,绑定业务层接口名
  • 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则

AOP通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置

AOP通知共分为5种类型

  • 前置通知@Before
  • 后置通知@After
  • 环绕通知(重点)@Around
  • 返回后通知(了解)@AfterReturning
  • 抛出异常后通知(了解)@AfteThrowing

@Before:当前通知方法在原始切入点方法运行

@Before("pt()")
//此处也可以写成 @Before("MyAdvice.pt()"),不建议
public void before() {
System.out.println("before advice ...");
}

@After:当前通知方法在原始切入点方法运行

@After("pt()")
public void after() {
System.out.println("after advice ...");
}

@Around:当前通知方法在原始切入点方法前后运行

@Around("pt()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}

@Around注意事项:

  1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

@AfterReturning:当前通知方法在原始切入点方法正常执行完毕运行

@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}

@AfteThrowing:当前通知方法在原始切入点方法运行抛出异常后执行

@AfteThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}

测试业务层接口万次执行效率

@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.zx.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(Procee dingJoinPoint pjp){
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行操作名称(接口名)
String className = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String methodName = signature.getName();

long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}

AOP通知获取数据

获取切入点方法的参数:所有通知

  • JoinPoint:适用于@Before@After@AfterReturning@AfterThrowing

    // JoinPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
    @Before("pt()")
    public void before(JoinPoint jp){
    Object[] args = jp.getArgs();
    System.out.println(Arrays.toString(args));
    }
  • ProceedingJoinPoint:适用于@Around

    // ProceedJointPoint是JoinPoint的子类
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp)throws Throwable {
    Object[] args = pjp.getArgs();
    System.out.println(Arrays.toString(args));
    Object ret = pjp.proceed();
    return ret;
    }

获取切入点方法返回值:@Before@AfterThrowing是没有返回值,@After可有可无,所以不做研究

  • @AfterReturning

    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(Object ret) {
    System.out.println("afterReturning advice ..."+ret);
    }
  • @Around

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
    Object ret = pjp.proceed();
    return ret;
    }

获取切入点方法运行异常信息:@Before@AfterReturning是不会有,@After可有可无,所以不做研究

  • @AfterThrowing

    @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t){
    System.out.println("afterThrowing advice ..." + t);
    }
  • @Around

    @Around("pt()")
    public Object around(ProceedingJointPoint pjp){
    Object ret = null;
    try {
    ret = pjp.proceed();
    } catch (Throwable t) {
    t.printStackTrace();
    }
    return ret;
    }

百度网盘密码数据兼容处理

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
public interface ResourcesDao {
boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
public boolean readResources(String url, String password) {
//模拟校验
return password.equals("root");
}
}
public interface ResourcesService {
public boolean openURL(String url ,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao;

public boolean openURL(String url, String password) {
return resourcesDao.readResources(url,password);
}
}
@Configuration
@ComponentScan("com.zx")
@EnableAspectJAutoProxy
public class SpringConfig {
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
System.out.println(flag);
}
}
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.zx.service.*Service.*(*,*))")
private void servicePt(){}

@Around("DataAdvice.servicePt()")
// @Around("servicePt()")这两种写法都对
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
//获取原始方法的参数
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
//将修改后的参数传入到原始方法的执行中
Object ret = pjp.proceed(args);
return ret;
}

}

Spring事务

简介

事务作用:在数据层保障一系列的数据库操作同成功同失败

Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager

public interface PlatformTransactionManager{
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}

转账案例

1.在业务层接口上添加Spring事务管理

// 加在接口表示所有方法都开启事务
// @Transactional
public interface AccountService {
// 加在方法上指定某一方法
@Transactional
public void transfer(String out,String in ,Double money);
}

注解通常加在接口而非实现类,降低耦合。

2.设置事务管理器

//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}

事务管理器要根据使用技术进行选择,

Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

3.开启注解式事务驱动

@Configuration
@ComponentScan("com.zx")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
// 开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}

Spring事务角色

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

image-20220811202615572

Spring事务属性

image-20220811204624726

Spring事务传播行为

image-20220811205826949

image-20220811210251515

转账业务追加日志

在业务层接口上添加Spring事务,设置事务传播行为为REQUIRES_NEW(需要新事务)

@Service
public class LogServiceImpl implements LogService {

@Autowired
private LogDao logDao;
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}

spring事务方法的同步问题

一、事务方法的同步问题

例如以下这段代码

@RequestMapping("/test")
@Transactional
public int test(){
Test test = testMapper.selectById(1);
int max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
return max;
}

当用jmeter并发访问1000次这个接口,发现max的值为532,跟预期的1000不一样。

img

这是因为有些请求查询不到数据库最新的值,导致的同步问题。

这里我使用的项目是springboot+mybatisplus+mysql。

二、解决方法

1.使用select for update解决
@Select("select * from test where id = #{id} for update")
Test selectForUpdate(@Param("id") long id);

for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则也必须对该表施加行级锁.即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。

只有当出现如下之一的条件,才会释放共享更新锁:
1、执行提交(COMMIT)语句
2、退出数据库(LOG OFF)
3、程序停止运行

2.使用手动事务+同步锁synchronized
@Autowired
private PlatformTransactionManager platformTransactionManager;

@Autowired
private TransactionDefinition transactionDefinition;

@RequestMapping("/test")
public synchronized int test(){
int max = 0;
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
try{
Test test = testMapper.selectById(1);
max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
platformTransactionManager.commit(transactionStatus);
}catch (Exception e){
platformTransactionManager.rollback(transactionStatus);
}
return max;
}

这里使用手动事务的原因是,spring aop中,在方法执行前开启事务,方法执行后提交事务,也就是说事务的开启和提交过程是没有加锁的,因此无法实现同步。

以下方式是无法实现同步的:

@RequestMapping("/test")
@Transactional
public synchronized int test(){
Test test = testMapper.selectById(1);
int max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
return max;
}

将数据库值清0,再次使用jmeter测试,发现max的值为647。

img

说明依然没有解决同步问题。

就算把synchronized放到方法体里也是一样无法实现同步的,锁住的只是方法里的代码块,没有锁住事务的开启和提交过程。

private Object object = new Object();

@RequestMapping("/test")
@Transactional
public int test(){
int max = 0;
synchronized (object) {
Test test = testMapper.selectById(1);
max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
}
return max;
}
3.使用乐观锁

数据库添加version字段。

public class Test {

private Long id;

private Integer max;

@Version
private Integer version;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Integer getMax() {
return max;
}

public void setMax(Integer max) {
this.max = max;
}

public Integer getVersion() {
return version;
}

public void setVersion(Integer version) {
this.version = version;
}
}
@RequestMapping("/test")
@Transactional(isolation = Isolation.READ_COMMITTED)
public int test(){
int i = 0;
Test test = null;
while(i<1){
test = testMapper.selectById(1);
test.setMax(test.getMax()+1);
i = testMapper.updateById(test);
}

System.out.println(test.getMax());
return test.getMax();
}

这里因为使用了mybatisplus框架,只需要在bean类version属性添加@Version注释,然后使用框架提供的方法查询和修改,默认支持乐观锁。

需要注意的是,这里@Transactional注释中标明了隔离级别为读取已提交。

否则会导致在事务中查询不到数据库最新的数据,导致一直无法更新成功,一直回旋。

原因是@Transactional默认的隔离级别是根据数据库的隔离级别,由于我用的是mysql的innodb引擎,默认的隔离级别是支持可重复读的,导致在事务中,查询数据库的值都是一致的可重复读的,导致无法获取数据库最新的值。

因此将事务隔离级别改为读取已提交就可以解决。

三、总结

事务方法的同步可以使用以下方法解决:

select for update

手动事务+同步锁synchronized(可将锁细化)

乐观锁

并发量不是特别高的话,使用乐观锁的效率最高。