二、Java框架之Spring注解开发

news2025/1/11 0:53:07

文章目录

  • 1. IOC/DI注解开发
    • 1.1 Component注解
      • @Component
      • @Controller @Service @Repository
    • 1.2 纯注解开发模式
    • 1.3 注解开发bean管理
      • @Scope
      • @PostConstruct @PreDestroy
    • 1.4 注解开发依赖注入
      • @Autowired @Qualifier
      • @Value
      • @PropertySource
    • 1.5 第三方bean管理
      • @Bean
      • @import(多个Config类)
      • 引用类型的注入
      • 总结
    • 1.6 XML配置和注解配置对比
  • 2. Spring整合MyBatis
    • 2.1 mybatis写法回顾
    • 2.2 整合:导入依赖:pom.xml
    • 2.3 整合:环境准备
    • 2.4 整合:Spring核心配置文件
      • SpringConfig.java
      • JdbcConfig.java
      • MybatisConfig.java
    • 2.5 运行和说明
  • 3. Spring整合JUnit
  • 4. AOP
    • 4.1 AOP核心概念
    • 4.2 AOP入门案例
      • @EnableAspectJAutoProxy @Aspect @Pointcut @Before
    • 4.3 AOP原理
      • AOP工作流程
      • AOP核心概念 - 代理
    • 4.4 AOP切入点表达式
    • 4.5 AOP通知类型
    • 4.6 案例:业务层接口执行效率
    • 4.7 AOP通知获取数据
  • 5. AOP事务管理
    • 5.1 Spring事务简介
    • 5.2 Spring事务案例
      • 无事务管理情况
      • 开启事务处理
    • 5.3 Spring事务角色
    • 5.4 Spring事务属性
      • 事务配置
      • 案例:转账业务追加案例
      • 事务传播行为

从Spring2开始引入注解,Spring3已经可以纯注解开发,以避免使用复杂的配置文件

1. IOC/DI注解开发

1.1 Component注解

@Component

  • 在对应类上添加Component注解

    在这里插入图片描述

  • 在applicationContext.xml指定要扫描的路径
    在这里插入图片描述

    注意:这里首先创建了context命名空间,然后使用了component-scan base-package,之后就可以正常获取bean了

    • 扫描的范围是 base-package 指定的范围
  • 测试BookService

    //BookServiceImpl.java
    @Component
    //可以不添加名称,之后按类型获取
    
    //applicationContext.xml
    <context:component-scan base-package="org.example"/>
    
    //App.java
    BookService bookService = ctx.getBean(BookService.class);
    bookService.save();
    

@Controller @Service @Repository

这三个注解是Component的衍生注解,作用和Component相同,只是为了区分某个类是属于表现层业务层还是数据层的类

在这里插入图片描述

  • Controller注解:表现层,例如BooServlet.java
  • Service注解:业务层,例如BookServiceImpl.java
  • Repository注解:数据层,例如BookDaoImpl.java(代表mybatis里面的mapper部分)

1.2 纯注解开发模式

不再写applicationContext.xml配置文件,而是用Config类替代

  • 创建Config类
    在这里插入图片描述

    @Configuration//表示这是个配置类,相当于applicationContext.xml默认部分,如命令空间xmlns那一块内容
    @ComponentScan("org.example.dao")//相当于设置了<bean>标签
    public class SpringConfig {
    }
    

    之前的applicationContext.xml已经可以删除了

    • @Configuration:设置该类为spring配置类

    • @ComponentScan:设置spring配置类扫描路径,此注解只能添加一次,多个数据用{}格式,如

      @ComponentScan({"org.example.dao", "org.example.service"})
      
  • BookDaoImpl.java

    package org.example.dao.impl;
    
    @Repository("bookDao")
    public class BookDaoImpl implements BookDao {
        public void save() {
            System.out.println("book dao save ...");
        }
    }
    
    
  • 使用SpringConfig:AnnotationConfigApplicationContext

    public class APP {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = (BookDao) ctx.getBean("bookDao");
            System.out.println(bookDao);
            BookService bookService = ctx.getBean(BookService.class);
            System.out.println(bookService);
        }
    }
    

1.3 注解开发bean管理

@Scope

设置是否为单例模式

在这里插入图片描述

@PostConstruct @PreDestroy

管理生命周期 init() 和 destroy()

在这里插入图片描述

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao);
ctx.close();//关闭容器,从而可以看到destroy()的信息

1.4 注解开发依赖注入

@Autowired @Qualifier

在这里插入图片描述

如果只有一个实现类implements BookDao时,仅需@Autowired即可自动注入
如果只有多个实现类implements BookDao时,还需@Qualifier(“name”)指定哪一个实现类

使用@Autowired可以省略setter方法

@Value

在这里插入图片描述

name变量被注入了值 “example”
这样单纯使用@Value是没有意义的,注解主要是为了加载properties文件,使得变量值可更改

@PropertySource

读取Properties配置文件

  • 新建jdbc.properties

    name=example
    
  • 配置Config类

    package org.example.config;
    
    @Configuration
    @ComponentScan({"org.example.dao", "org.example.service"})
    @PropertySource("classpath:jdbc.properties")
    public class SpringConfig {
    }
    
  • 注入

    package org.example.dao.impl;
    
    @Repository("bookDao")
    public class BookDaoImpl implements BookDao {
        @Value("${name}")
        private String name;
        public void save() {
            System.out.println("book dao save ..."+name);
        }
    }
    

注意事项:(1)多个properties配置文件同样使用{}格式;(2)不支持通配符

1.5 第三方bean管理

@Bean

  • 导入依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.16</version>
    </dependency>
    
  • 配置Config文件

    @Configuration
    public class SpringConfig {
        //1. 定义一个方法获得要管理的对象
        //2. 添加@Bean,表示当前方法的返回值是一个bean
        @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并运行

    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    DataSource dataSource = ctx.getBean(DataSource.class);
    System.out.println(dataSource);
    

@import(多个Config类)

像是上面的dataSource()这类的通常会专门创建一个Config类,如JdbcConfig,现在需要使其生效
在这里插入图片描述

方法一(不推荐)

  • JdbcConfig.java

    @Configuration
    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;
        }
    }
    
  • 还需要配置SpringConfig.java

    @Configuration
    @ComponentScan("org.example.config")
    public class SpringConfig {
    }
    

方法二(推荐)

  • JdbcConfig.java

    public class JdbcConfig {//注意,没再使用@Configuration
        @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;
        }
    }
    
  • 配置SpringConfig.java

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

练习:使用@Value和properties文件修改上述代码

引用类型的注入

  • BookDaoImpl.java

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

    @Configuration
    @ComponentScan("org.example.dao")//关联到BookDaoImpl
    @Import(JdbcConfig.class)
    public class SpringConfig {
    }
    
  • JdbcConfig.java

    @PropertySource("classpath:jdbc.properties")
    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(BookDao bookDao){
            System.out.println(bookDao);
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            return ds;
        }
    }
    
  • 自动装配
    上面仅提供了一个形参bookDao,即可自动注入
    这是因为@Bean使其认为形参应当被自动提供,于是将自动寻找相应的类,并注入到形参中

总结

  • 1.第三方Bean管理
    • @Bean
  • 2.第三方依赖注入
    • 引用类型:方法形参
    • 简单类型:成员变量

1.6 XML配置和注解配置对比

在这里插入图片描述

2. Spring整合MyBatis

2.1 mybatis写法回顾

在这里插入图片描述

  1. 创建javaweb项目,在pom.xml添加<packaging>war</packaging>

  2. 配置pom.xml依赖和插件

    <dependencies>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!--Tomcat插件,非必要 -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
            </plugin>
        </plugins>
    </build>
    
  3. 编写mybatis-config.xml配置文件

    <?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 resource="jdbc.properties"/>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <!--数据库连接信息-->
                    <property name="driver" value="${jdbc.driver}"/>
                    <property name="url" value="${jdbc.url}"/>
                    <property name="username" value="${jdbc.username}"/>
                    <property name="password" value="${jdbc.password}"/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
        	<package name="org.example.mapper"/>
    	</mappers>
    </configuration>
    

    jdbc.properties

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql:///spring_db?useSSL=false&amp;useServerPrepStmts=true
    jdbc.username=root
    jdbc.password=123456
    
  4. 创建AcccountMapper.xml和AccontMapper接口
    AccountMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="org.example.mapper.AccountMapper">
    
    </mapper>
    

    AccontMapper接口

    package org.example.mapper;
    
    public interface AccountMapper {
        @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
        void save(Account account);
    
        @Delete("delete from tbl_account where id = #{id} ")
        void delete(Integer id);
    
        @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
        void update(Account account);
    
        @Select("select * from tbl_account")
        List<Account> findAll();
    
        @Select("select * from tbl_account where id = #{id} ")
        Account findById(Integer id);
    }
    

    在这一部分定义sql语句

  5. 编写service方法负责业务逻辑层,主要是调用数据库
    准备工具类:SqlSessionFactoryUtils

    package org.example.util;
    
    public class SqlSessionFactoryUtils {
        private static SqlSessionFactory sqlSessionFactory;
        static{
            try {
                String resource = "mybatis-config.xml";
                InputStream inputStream = Resources.getResourceAsStream(resource);
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public static SqlSessionFactory getSqlSessionFactory(){
            return sqlSessionFactory;
        }
    }
    

    编写AccountService接口

    public interface AccountService {
        List<Account> findAll();
    }
    

    AccountService.java

    package org.example.service.impl;
    
    public class AccountServiceImpl implements AccountService {
        private SqlSessionFactory factory = SqlSessionFactoryUtils.getSqlSessionFactory();
    
        @Override
        public List<Account> findAll() {
            SqlSession session = factory.openSession();
            AccountMapper mapper = session.getMapper(AccountMapper.class);
            List<Account> accounts = mapper.findAll();
            session.close();
            return accounts;
        }
    }
    
  6. 接下来应该是在servlet类里面调用service方法,这里写在main函数里面

    package org.example;
    
    public class Main {
        public static void main(String[] args) {
            AccountService service = new AccountServiceImpl();
            List<Account> accounts = service.findAll();
            System.out.println(accounts);
        }
    }
    

即可成功获取到数据库数据
当Spring需要整合mybatis时,真正需要交给Spring管理的是SqlSessionFactory

2.2 整合:导入依赖:pom.xml

<dependencies>
    <!-- spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!-- druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.16</version>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.5</version>
    </dependency>
    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.46</version>
    </dependency>
    <!-- spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!-- mybatis-spring -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.0</version>
    </dependency>
</dependencies>

2.3 整合:环境准备

步骤1:准备数据库表

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
);
insert into tbl_account values (null, 'zhangsan', 1999.10);
insert into tbl_account values (null, '张三', 32.43);

步骤2:创建基础文件

在这里插入图片描述

Account.java

package org.example.domain;

public class Account{
    private Integer id;
    private String name;
    private Double money;
}
//省略getter, setter, toString

AccountDao接口

这里的AccountDao就是AccountMapper的作用,需要加上注解

package org.example.dao;

@Repository("accountDao")
public interface AccountDao {
    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
    void save(Account account);

    @Delete("delete from tbl_account where id = #{id} ")
    void delete(Integer id);

    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    @Select("select * from tbl_account")
    List<Account> findAll();

    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
}

Service接口和实现类

接口是没有变化的

package org.example.service;

public interface AccountService {
    void save(Account account);
    void delete(Integer id);
    void update(Account account);
    List<Account> findAll();
    Account findById(Integer id);
}

实现类变化很大,和之前相比,spring会接管SqlSessionFactory对象的创建,因此这次不需要创建了
重点:@Service和自动注入

package org.example.service.impl;

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired//自动注入
    @Qualifier("accountDao")
    private AccountDao accountDao;
    public void save(Account account) {
        accountDao.save(account);
    }
    public void update(Account account){
        accountDao.update(account);
    }
    public void delete(Integer id) {
        accountDao.delete(id);
    }
    public Account findById(Integer id) {
        return accountDao.findById(id);
    }
    public List<Account> findAll() {
        return accountDao.findAll();
    }
}

jdbc.properties

resources目录下添加,用于配置数据库连接四要素

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=123456

useSSL:关闭MySQL的SSL连接

2.4 整合:Spring核心配置文件

在没有整合之前,mybatis的service类里面会创建SqlSessionFactory对象,来与数据库互通
在整合后,可以看到新的service类里面不再具备这样的功能
spring核心配置文件就是用来设置配置信息的,用以替代mybatis-config.xml等配置文件 ,并管理bean之间的依赖关系

SpringConfig.java

主配置类,推荐在这个配置类里面import其他配置类

package org.example.config;

@Configuration//说明这是一个配置类
@ComponentScan("org.example")//定义扫描路径
@PropertySource("classpath:jdbc.properties")//引入连接信息资源文件
@Import({JdbcConfig.class, MybatisConfig.class})//要么这里导入,要么在 JdbcConfig 前面加 @Configuration
public class SpringConfig {
}

JdbcConfig.java

package org.example.config;

//定义数据源	
//本来是需要引入jdbc.properties的,但这里选择将所有文件都放在SpringConfig里面引入
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(BookDao bookDao){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }
}

MybatisConfig.java

SqlSessionFactoryBean来源于org.mybatis.spring,可以直接获取SqlSessionFactory

package org.example.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {
    //sqlSessionFactoryBean完成了mybatis-config里面的<environment>部分
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        //dataSource也是一个Bean,所以这里能够自动注入
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("org.example.domain");//取别名,domain是实体类包,相当于之前的pojo包
        ssfb.setDataSource(dataSource);//设置数据源,即连接相关信息
        return ssfb;
    }
    //mapperScannerConfigurer完成了mybatis-config里面的<mappers>部分
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("org.example.dao");//这里的dao包实际上就是之前学mybatis里面的mapper包
        return msc;
    }
}

2.5 运行和说明

App.java

package org.example;

public class App {
    public static void main(String[] args){
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        AccountService accountService = ctx.getBean(AccountService.class);//@Service标注会自动生成bean
        Account account = accountService.findById(1);//即使后面AccountServiceImpl修改,也不影响这里的代码
        System.out.println(account);
    }
}

说明

  • 运行流程

    - 程序启动时候检测使用了@Configuration注解的配置类SpringConfig
    - SpringConfig中引入了MybatisConfig和JdbcConfig,相当于这三个文件都成为一个配置文件
    - MybatisConfig通过JdbcConfig获取到了dataSource,里面带有配置数据库连接的信息,从而成功创建 SqlSessionFactory
    - 由于AccountServiceImpl.java上使用了注解@Service,且配置类SpringConfig定义了扫描路径"org.example",于是它将被纳入bean管理
    - 执行ctx.getBean(AccountService.class),这里实际上是以接口类去接实现类,类似于Father father = new Son();
    - 调用实现类的findById方法
    
  • 关于Spring注入的是接口还是实现类?
    参考:https://blog.csdn.net/m0_51697147/article/details/126802648

    • 在配置文件模式中,配置bean

      <bean id="bookService" class="org.example.service.BookServiceImpl">
          <property name="bookDao" ref="bookDao"/>
      </bean>
      

      获取bean

      BookService bookService = ctx.getBean(BookService.class);
      
    • 在注解开发模式中,配置bean

      @Service
      public class AccountServiceImpl implements AccountService {
          @Autowired
          @Qualifier("accountDao")
          private AccountDao accountDao;
          ...
      }
      

      获取bean

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

    从spring容器中获取一个类,如果这个类实现了一个接口并且该类存在一个AOP的切入点方法,那么通过getBean()获取到的bean类型只能是这个类的接口类型,不能是具体实现

    getBean()必须面向接口,这是因为底层实现用了代理,并由Proxy的内部实现决定

    优点:如果之后实现类发生改变,例如修改为AccountServiceImpl2.java,那么App.java里面的内容不必修改

    思考:如果有多个实现类继承了AccountService,这也写将会报错,那么如何处理?

3. Spring整合JUnit

1.导入依赖

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- spring test -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

2.编写测试类

package org.example.service;

@RunWith(SpringJUnit4ClassRunner.class)//设定类运行器
@ContextConfiguration(classes = SpringConfig.class)//加载配置类
//@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {
    //支持自动装配注入bean
    @Autowired
    private  AccountService accountService;

    @Test
    public void testFindById(){
        System.out.println(accountService.findById(2));
    }

    @Test
    public void testFindAll(){
        System.out.println(accountService.findAll());
    }
}

要测试哪个方法,就在哪个方法那里点击执行

  • 单元测试,如果测试的是注解配置类,则使用@ContextConfiguration(classes = 配置类.class)
  • 单元测试,如果测试的是配置文件,则使用@ContextConfiguration(locations={配置文件名,...})
  • Junit运行后是基于Spring环境运行的,所以Spring提供了一个专用的类运行器,这个务必要设置,这个类运行器就在Spring的测试专用包中提供的,导入的坐标就是这个东西SpringJUnit4ClassRunner
  • 上面两个配置都是固定格式,当需要测试哪个bean时,使用自动装配加载对应的对象

知识点1:@RunWith

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

知识点2:@ContextConfiguration

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

4. AOP

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

Spring有两个核心的概念,一个是IOC/DI,一个是AOP
作用:AOP是在不改原有代码的前提下对其进行增强
Spring理念:无入侵时/无侵入式

4.1 AOP核心概念

package org.example.dao.impl;

import org.example.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        //记录程序当前执行执行(开始时间)
        Long startTime = System.currentTimeMillis();
        //业务执行万次
        for (int i = 0;i<10000;i++) {
            System.out.println("book Dao");
        }
        //记录程序当前执行时间(结束时间)
        Long endTime = System.currentTimeMillis();
        //计算时间差
        Long totalTime = endTime-startTime;
        //输出信息
        System.out.println("执行万次消耗时间:" + totalTime + "ms");
    }
    public void update(){
        System.out.println("book dao update ...");
    }
    public void delete(){
        System.out.println("book dao delete ...");
    }
    public void select(){
        System.out.println("book dao select ...");
    }
}

需求:希望对update、delete函数执行和save一样的流程,即执行10000次,然后打印时间差

在这里插入图片描述

AOP中的核心概念

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等

    • 在SpringAOP中,理解为方法的执行
    • AOP将每一个方法调用,即连接点作为编程的入口,针对方法调用进行编程
  • 切入点(Pointcut):匹配连接点的式子

    • 指需要被增强的方法
    • 切入点是连接点,但连接点不一定是切入点
  • 通知(Advice):在切入点处执行的操作,也就是共性功能

    • 如上面的计算万次执行消耗时间作为共性功能,被抽取到一个方法中,这个方法就是通知
    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:定义通知的类

  • 切面(Aspect):描述通知与切入点的对应关系。

    • 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,通知与切入点的对应关系叫切面

4.2 AOP入门案例

需求:在方法执行前输出当前系统时间。

开发模式:XML 和 注解

步骤:

  1. 导入坐标(pom.xml)

    <!-- spring-context里面包含了aop -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!-- aspectjweaver -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    
  2. 制作连接点(原始操作,Dao接口与实现类)

    package org.example.dao;
    
    public interface BookDao {
        public void save();
        public void update();
    }
    
    package org.example.dao.impl;
    
    @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 ...");
        }
    }
    
  3. 制作共性功能(通知类与通知)

    新建包aop,新建MyAdvice通知类,printTime即为通知方法

  4. 定义切入点

    切入点即 pt() ,需要注解@Pointcut注明哪些方法需要被增强

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

    @Before说明切入点与通知的关系

  6. 配置Spring环境

    package org.example.aop;
    
    //6. 配置Spring环境
    @Component//需要将其交给Spring管理
    @Aspect//告诉Spring当作AOP处理,而非Bean
    public class MyAdvice {
        //4. 定义切入点
        @Pointcut("execution(void org.example.dao.BookDao.update())")
        private void pt(){}
        //5. 绑定切入点与通知关系(切面)
        @Before("pt()")//在pt()方法前执行
        //3. 制作共性功能(通知类与通知)
        public void printTime(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
    package org.example.config;
    
    @Configuration
    @ComponentScan("org.example")
    @EnableAspectJAutoProxy//开启Spring对AOP注解驱动支持
    public class SpringConfig {
    }
    
  7. 运行

    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = ctx.getBean(BookDao.class);
    bookDao.update();
    

@EnableAspectJAutoProxy @Aspect @Pointcut @Before

名称@EnableAspectJAutoProxy
类型配置类注解
位置配置类定义上方
作用开启注解格式AOP功能
名称@Aspect
类型类注解
位置切面类定义上方
作用设置当前类为AOP切面类
名称@Pointcut
类型方法注解
位置切入点方法定义上方
作用设置切入点方法
属性value(默认):切入点表达式
名称@Before
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

4.3 AOP原理

AOP工作流程

由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起

工作流程

  1. 流程1:Spring容器启动

    容器启动就需要去加载bean,带有@Component,@Service ,@Controller 的类都是spring 要创建的bean对象

    • 需要被增强的类BookDaoImpl,通知类MyAdvice
    • 注意此时bean对象还没有创建成功
  2. 流程2:读取所有切面配置中的切入点

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void org.example.dao.BookDao.save())")
        private void ptx(){}
    
        @Pointcut("execution(void org.example.dao.BookDao.update())")
        private void pt(){}
    
        @Before("pt()")
        public void printTime(){
            System.out.println(System.currentTimeMillis());
        }
    }
    

    有两个切入点,其中切入点ptx()并没有被使用,所以不会被读取

  3. 流程3:初始化bean

    在容器启动的时候,bean对象还没有被创建成功
    在创建bean对象时,需要判定bean对应的类中的方法是否匹配到任意切入点,以BookDao为例

    • 匹配失败,创建原始对象,即BookDao本身的对象
      • 匹配失败,即该类中没有一个方法能匹配上切入点,说明不需要增强,直接调用原始对象的方法即可
    • 匹配成功,创建原始对象(目标对象)的代理对象
      • 匹配成功说明需要对其进行增强
      • 对哪个类做增强,这个类对应的对象就叫做目标对象
      • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
      • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
  4. 流程4:获取bean并执行方法

    • 获取的bean是原始对象时,调用方法并执行,完成操作
    • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

验证代理

System.out.println(bookDao);
System.out.println(bookDao.getClass());

在这里插入图片描述

打印bookDao时,由于代理里面重写了toString,所以看到的是BookDaoImpl
打印Class就可以看到,最终生成的是目标对象的代理对象

AOP核心概念 - 代理

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

SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知[如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。

SpringAOP的本质或者可以说底层实现是通过代理模式

4.4 AOP切入点表达式

切入点:要进行增强的方法

切入点表达式:要进行增强的方法的描述方式

  • 接口描述

    execution(void org.example.dao.BookDao.update())
    
  • 实现类描述

    execution(void org.example.dao.impl.BookDaoImpl.update())
    

因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的

切入点表达式标准格式

动作关键字(访问修饰符  返回值  包名.类/接口名.方法名(参数) 异常名)
execution(public User org.example.service.UserService.findById(int))

切入点通配符

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

    execution(public * org.example.*.UserService.find*(*))
    

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

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

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

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

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

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

    很少使用。*Service+,表示所有以Service结尾的接口的子类。

切入点表达式练习

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

书写技巧

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

4.5 AOP通知类型

5种通知类型

  • 前置通知

    @Before("pt()")
    
  • 后置通知

    @After("pt()")
    
  • 环绕通知(重点)

    package org.example.aop;
    
    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void org.example.dao.BookDao.update())")
        private void pt(){}
    
        @Around("pt()")
        public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
            //前置
            System.out.println("before advice");
            //原始操作
            pjp.proceed();
            //后置
            System.out.println("after advice");
        }
    }
    

    有返回值的情况

    package org.example.aop;
    
    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(int org.example.dao.BookDao.select())")
        private void pt(){}
    
        @Around("pt()")
        public Object aroundUpdate(ProceedingJoinPoint pjp) throws Throwable {
            //前置
            System.out.println("before advice");
            //原始操作
            Object ret = pjp.proceed();
            //后置
            System.out.println("after advice");
            return ret;
        }
    }
    
  • 返回后通知(了解)

    @AfterReturning("pt()")
    

    返回后通知是需要在原始方法select正常执行后才会被执行,如果过程中出现了异常,那么返回后通知是不会被执行
    后置通知是不管原始方法有没有抛出异常都会被执行

  • 抛出异常后通知(了解)

    @AfterThrowing("pt()")
    

    如果有异常才会执行

注意事项

  • 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用
  • 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
  • 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

4.6 案例:业务层接口执行效率

需求:任意业务层接口执行均可显示其执行效率(执行时长)
环境准备:使用前面整合MyBatis和Junit之后的项目

  1. 添加pom.xml依赖

    <!-- spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!-- aspectjweaver -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    
  2. 配置SpringConfig环境

    package org.example.config;
    
    @Configuration
    @ComponentScan("org.example")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class, MybatisConfig.class})
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
  3. 创建通知类 org.example.aop.ProjectAdvice

  4. 编写通知方法

    package org.example.aop;
    
    @Component
    @Aspect
    public class ProjectAdvice {
        //1. 切入点:匹配业务层的所有方法
        @Pointcut("execution(* org.example.service.*Service.*(..))")
        private void servicePt(){}
    
        //2. 环绕方法
        @Around("ProjectAdvice.servicePt()")
        public void runSpeed(ProceedingJoinPoint pjp) throws Throwable{
            //ProceedingJoinPoint:连接点,携带原始方法信息
            Signature signature = pjp.getSignature();
            String className = signature.getDeclaringTypeName();
            String methodName = signature.getName();
            //前置:获取开始时间
            long start  = System.currentTimeMillis();
            for(int i=0; i<10000; ++i){
                //调用原始方法
                Object ret = pjp.proceed();
            }
            //后置:获取结束时间
            long end = System.currentTimeMillis();
    
            System.out.println("万次执行:"+className+"."+methodName+" 时间为:"+(end-start)+"ms");
        }
    }
    
  5. 测试类

    package org.example.service;
    
    @RunWith(SpringJUnit4ClassRunner.class)//设定类运行器
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountServiceTest {
        @Autowired
        private  AccountService accountService;
    
        @Test
        public void testFindById(){
            accountService.findById(2);
        }
    
        @Test
        public void testFindAll(){
            accountService.findAll();
        }
    }
    

4.7 AOP通知获取数据

  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
    • 抛出异常后通知
    • 环绕通知

获取参数

package org.example.aop;

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* org.example.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint jp){
        Object[] args = jp.getArgs();
    }
    @After("pt()")
    public void after(JoinPoint jp){
        Object[] args = jp.getArgs();
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        args[0] = 666;//可以中途修改参数
        Object ret = pjp.proceed(args);
        return ret;
    }
}

环绕方法可以修改传递过来的参数,有时可以用作对参数清洗

返回值

@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(JoinPoint jp, Object ret){//注意如果有JoinPoint参数,它必须得在第一位
    System.out.println("afterReturning advice ..."+ret);//ret即为返回值
}

获取异常

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

5. AOP事务管理

5.1 Spring事务简介

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

Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager
在这里插入图片描述

commit是用来提交事务,rollback是用来回滚事务

PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现

只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。其内部采用的是JDBC的事务
所以如果你持久层采用的是JDBC相关的技术,就可以采用这个事务管理器来管理事务。而Mybatis内部采用的就是JDBC的事务,所以后期Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。

5.2 Spring事务案例

无事务管理情况

需求: 实现任意两个账户间转账操作,A账户减钱和B账户加钱必须是同成功或同失败
准备工作:第2节中整合MyBatis中的spring-mybatis项目

步骤1:准备数据库表

含有 id name money 三个属性的数据库表

步骤2:创建项目导入jar包

步骤3:根据表创建模型类

即Account类

步骤4:创建Dao接口

在AccountDao.java中加入

@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);

@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);

步骤5:编写Service接口和实现类

package org.example.service;

public interface AccountService {
    /**
     * 转账
     * @param out:转出账户
     * @param in:转入账户
     * @param money:转账金额
     */
    public void transfer(String out, String in, Double money);
}
package org.example.service.impl;

@Service
public class AccountServiceImpl implements AccountService {
    //自动注入accountDao
    @Autowired
    @Qualifier("accountDao")
    private AccountDao accountDao;
    
    @Override
    public void transfer(String out, String in, Double money) {
        accountDao.outMoney(out, money);
        accountDao.inMoney(in, money);
    }
}

步骤6:编写配置类

SpringConfig,JdbcConfig,MybatisConfig,jdbc.properties

步骤7:编写测试类

package org.example.service;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
    @Autowired
    private  AccountService accountService;

    @Test
    public void testTransfer() throws IOException{
        accountService.transfer("zhangsan", "lisi", 100D);
    }
}

问题

当增加和修改两个操作中间出现异常时,会出现一个账户减少了,而另一个账户却没增加的错误!,如:

public void transfer(String out, String in, Double money) {
    accountDao.outMoney(out, money);
    int i = 1/0;
    accountDao.inMoney(in, money);
}

开启事务处理

步骤1:添加@Transactional注解

可以写在接口类上、接口方法上、实现类上和实现类方法上

  • 写在接口类上,该接口的所有实现类的所有方法都会有事务
  • 写在接口方法上,该接口的所有实现类的该方法都会有事务
  • 写在实现类上,该类中的所有方法都会有事务
  • 写在实现类方法上,该方法上有事务
  • 常写在方法前
package org.example.service;

public interface AccountService {
    @Transactional
    public void transfer(String out, String in, Double money);
}

步骤2:在JdbcConfig类中配置事务管理器

package org.example.config;

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;
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){//自动注入dataSource
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

步骤3:在SpringConfig中开启事务注解

package org.example.config;

import org.springframework.context.annotation.*;
import org.springframework.transaction.annotation.EnableTransactionManagement;

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

至此即可实现transfer函数的同成功或同失败

5.3 Spring事务角色

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法,如transfer()

  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法,如outMoney()inMoney

  1. 未开启Spring事务之前

在这里插入图片描述

  • AccountDao的outMoney因为是修改操作,会开启一个事务T1
  • AccountDao的inMoney因为是修改操作,会开启一个事务T2
  • AccountService的transfer没有事务,
    • 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
    • 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
    • 就会导致数据出现错误
  1. 开启Spring的事务管理后

在这里插入图片描述

  • transfer上添加了@Transactional注解,在该方法上就会有一个事务T
  • AccountDao的outMoney方法的事务T1加入到transfer的事务T中
  • AccountDao的inMoney方法的事务T2加入到transfer的事务T中
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。

目前的事务管理是基于DataSourceTransactionManagerSqlSessionFactoryBean使用的是同一个数据源

5.4 Spring事务属性

事务配置

在这里插入图片描述

@Transactional(readOnly = true, timeout = -1)
  • rollbackFor(重点)

    当transfer()的代码如下时,先前的事务管理失效,仍然导致一方改变了,而另一方未改变

    @Override
    public void transfer(String out, String in, Double money) throws IOException {
        accountDao.outMoney(out, money);
        if(true) throw new IOException();
        accountDao.inMoney(in, money);
    }
    

    原因:Spring的事务只会对Error异常RuntimeException异常及其子类进行事务回滚,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚

    修改:设置rollbackFor

    @Transactional(rollbackFor = {IOException.class})
    public void transfer(String out, String in, Double money) throws IOException;
    
  • readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。

  • timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。

  • noRollbackFor:当出现指定异常不进行事务回滚

  • rollbackForClassName:等同于rollbackFor,只不过属性为异常的类全名字符串

  • noRollbackForClassName:等同于noRollbackFor,只不过属性为异常的类全名字符串

  • isolation设置事务的隔离级别(见MySQL数据库相关知识)

    • DEFAULT:默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED : 读未提交
    • READ_COMMITTED : 读已提交
    • REPEATABLE_READ : 重复读取
    • SERIALIZABLE: 串行化

案例:转账业务追加案例

需求:无论转账操作是否成功,均进行转账操作的日志留痕
准备工作:基于前面5.2节的案例

步骤1:添加数据库表

create table tbl_log(
   id int primary key auto_increment,
   info varchar(255),
   createDate datetime
)

步骤2:添加LogDao接口

package org.example.dao;

@Repository
public interface LogDao {
    @Insert("insert into tbl_log (info, createDate) values (#{info}, now())")
    void log(String info);
}

步骤3:添加LogService接口与实现类

package org.example.service;

public interface LogService {
    @Transactional
    public void log(String out, String in, Double money);
}
package org.example.service.impl;

@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;
    @Override
    public void log(String out, String in, Double money) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}

步骤4:在转账的业务中添加记录日志

package org.example.service;

public interface AccountService {    
    @Transactional    
    public void transfer(String out, String in, Double money);
}
package org.example.service.impl;

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    @Qualifier("accountDao")
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
    
    @Override
    public void transfer(String out, String in, Double money) {
        try {
            accountDao.outMoney(out, money);
            int i = 1/0;
            accountDao.inMoney(in, money);
        } finally {
            logService.log(out, in, money);
        }
    }
}

注意:结果如果报异常,记录不会被写入tbl_log表中去,
因为此时日志记录和转账操作隶属于一个事务,同成功同失败,那么转账被回滚了失败了,日志记录自然也失败了

但是需求是:无论转账是否成功,都记录日志
此时需要:转账的两个操作inMoney和outMoney加入到transfer事务中,但记录日志的log操作单独启动一个事务

事务传播行为

修改日志的事务属性:propagation

package org.example.service;

public interface LogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)//开启新事物
    public void log(String out, String in, Double money);
}
package org.example.service.impl;

@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;

    @Override
    public void log(String out, String in, Double money) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}

此时即可实现失败转账操作回滚,但日志仍被记录
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/179579.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redisson 完成分布式锁

1、简介 Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格&#xff08;In-Memory Data Grid&#xff09;。充分 的利用了 Redis 键值数据库提供的一系列优势&#xff0c;基于 Java 实用工具包中常用接口&#xff0c;为使用者 提供了一系列具有分布式特性的常用工具类…

JavaWeb | 揭开SQL注入问题的神秘面纱

本专栏主要是记录学习完JavaSE后学习JavaWeb部分的一些知识点总结以及遇到的一些问题等&#xff0c;如果刚开始学习Java的小伙伴可以点击下方连接查看专栏 本专栏地址&#xff1a;&#x1f525;JDBC Java入门篇&#xff1a; &#x1f525;Java基础学习篇 Java进阶学习篇&#x…

MyEclipse提示过期,MyEclipse Subscription Expired解决方案

一、错误描述 某一天打开MyEclipse&#xff0c;突然发现出现如下提示框&#xff1a; 1.错误日志 Thank you for choosing MyEclipse Your license expired 1091 days ago. To continue use of MyEclipse please choose "Buy" to purchase a MyEclipse license. I…

离散系统的数字PID控制仿真-3

离散PID控制的封装界面如图1所示&#xff0c;在该界面中可设定PID的三个系数、采样时间及控制输入的上下界。仿真结果如图2所示。图1 离散PID控制的封装界面图2 阶跃响应结果仿真图&#xff1a;离散PID控制的比例、积分和微分三项分别由Simulink模块实现。离散PID控制器仿真图&…

【servlet篇】servlet相关类介绍

目录 servlet对象什么时候被创建&#xff1f; 2.servlet接口中各个方法的作用 3.相关类和接口介绍 GenericServlet ServletConfig ServletContext HttpServlet servlet对象什么时候被创建&#xff1f; 1&#xff0c;通常情况下&#xff0c;tomcat启动时&#xff0c;并没有…

高阶数据结构 位图的介绍

作者&#xff1a;学习同学 专栏&#xff1a;数据结构进阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;简单介绍高阶数据结构位图 位图的介绍bitset的介绍位图的引入位图的概念位图的引用bitset的使用bitset定义方式方式一 默认初…

基于BGP技术和防火墙双机热备技术的校园网设计与实现

规划设计描述网络拓扑设计分为三部分进行设计&#xff1a;主校区网络、 运营商骨干网络、分校区网络。总公司网络设计&#xff1a;划分&#xff1a;教学楼区域、宿舍区域、办公楼区域、行政楼区域&#xff0c;图书馆区域、数据中心。并且设有web服务器。出口设置双机热备技术&a…

人工智能在网络犯罪中的应用:5个最重要的趋势

在当今的数字世界中&#xff0c;网络威胁不断演变。 人工智能的使用虽然在网络犯罪中还不是必须的&#xff0c;但无疑是我们将在未来几年看到的具有重大发展的最有前途的技术之一。 随着 AI 技术的进步&#xff0c;攻击者开始尝试新的、越来越复杂和有效的攻击模式和技术。 …

PCL OcTree(二)——点云压缩

文章目录 一、应用背景二、代码解读1、官方源码2、代码解释与扩展3、完整代码三、参考文献一、应用背景 点云由庞大的数据集组成,这些数据集通过距离、颜色、法线等附加信息来描述空间三维点。此外,点云能以非常高的速率被创建出来,因此需要占用相当大的存储资源,一旦点云…

【信管9.3】项目干系人管理

项目干系人管理干系人&#xff0c;这三个字我们已经很早就见过了&#xff0c;相信你对它一定不会陌生。在我们的教材中&#xff0c;它是和项目沟通管理放在一起的&#xff0c;在同一个章节中讲完了&#xff0c;我们也遵循教材的顺序&#xff0c;将它和沟通放在一起。其实&#…

【计算机网络(考研版)】第一站:计算机网络概述(二)

目录 四、OSI参考模型和TCP/IP模型 1.ISO/0SI参考模型 2.TCP/IP模型 3.OSI/RM参考模型和TCP/IP参考模型的区别和联系 4.五层教学模型 5.数据流动示意图 四、OSI参考模型和TCP/IP模型 前面我们已经讨论了体系结构的基木概念&#xff0c;在具体的实施中有两个重要的网络体系…

Qt扫盲-QNetworkReply理论总结

QNetworkReply理论总结一、概述二、使用1. 读取body内容2. 获取head属性值3. 错误处理一、概述 QNetworkReply类包含了与QNetworkAccessManager发送的请求回来的相关的数据和元数据。与QNetworkRequest类似&#xff0c;它包含一个URL和头部(包括解析的和原始的形式)&#xff0…

Java基础语法——运算符与表达式

目录 Eclipse下载 安装 使用 运算符 键盘录入 Eclipse下载 安装 使用 Eclipse的概述(磨刀不误砍柴工)——是一个IDE(集成开发环境)Eclipse的特点描述&#xff08;1&#xff09;免费 &#xff08;2&#xff09;纯Java语言编写 &#xff08;3&#xff09;免安装 &#xff08…

【自然语言处理】情感分析(二):基于 scikit-learn 的 Naive Bayes 实现

情感分析&#xff08;二&#xff09;&#xff1a;基于 scikit-learn 的 Naive Bayes 实现在上一篇博客 情感分析&#xff08;一&#xff09;&#xff1a;基于 NLTK 的 Naive Bayes 实现 中&#xff0c;我们介绍了基于 NLTK 实现朴素贝叶斯分类的方法&#xff0c;本文将基于 sci…

阿里云效git仓库的创建与使用

一、为何选用阿里仓库为什么要让同学们什么阿里云git仓库呢&#xff1f;主要是考虑速度、容量、人数限制、功能等因素。阿里的速度较快。代码库不限&#xff0c;人数不限制。gitee等仓库要求人员在5名以下&#xff0c;不方便实操练习。云效的功能还强大。有阿里做后盾&#xff…

微服务必经之路,企业应用架构蓝图,有状态和无状态组件之分

微服务如火如荼&#xff0c;但很多时候是事倍功半&#xff0c;花了大力气&#xff0c;收获很少。怎样实现一键扩展&#xff0c;负载量自然伸缩&#xff0c;高可用呢&#xff1f; 一般公司都有了企业级的应用&#xff0c;我们通常所说的三层架构&#xff0c;即用户界面或者说前台…

“华为杯”研究生数学建模竞赛2005年-【华为杯】D题:仓库容量有限条件下的随机存贮管理问题(附获奖论文和matlab代码)

赛题描述 工厂生产需定期地定购各种原料,商家销售要成批地购进各种商品。无论是原料或商品,都有一个怎样存贮的问题。存得少了无法满足需求,影响利润;存得太多,存贮费用就高。因此说存贮管理是降低成本、提高经济效益的有效途径和方法。 问题2 以下是来自某个大型超市的…

【Android】开发一个简单时钟应用每天看时间起床

有没有想过&#xff0c;家里闲置不用的旧手机可以当时钟闹钟来用&#xff0c;觉得这个想法可以吧&#xff0c;把闲置手机充分利用起来了呢&#xff0c;接下来给大家讲一下如何实现&#xff0c;功能简单&#xff0c;可以当作编程入门课。 在电脑上&#xff0c;打开我们熟悉的An…

【设计模式】结构型模式·享元模式

学习汇总入口【23种设计模式】学习汇总(数万字讲解体系思维导图) 写作不易&#xff0c;如果您觉得写的不错&#xff0c;欢迎给博主来一波点赞、收藏~让博主更有动力吧&#xff01; 一.概述 定义&#xff1a;运用共享技术来有效地支持大量细粒度对象的复用。通过共享已经存在的对…

厚积薄发打卡Day115:Debug设计模式<简单工厂、工厂方法、抽象工厂>

厚积薄发打卡Day115&#xff1a;Debug设计模式<简单工厂、工厂方法、抽象工厂> 简单工厂 定义 由一个工厂对象决定创建出哪一种产品类的实例&#xff08;严格意义并不是设计模式&#xff0c;更是一种风格&#xff09; 类型&#xff1a;创建型&#xff0c;但不属于GOF…