Spring5从入门到精通(下篇)

191 阅读18分钟

Spring5从入门到精通

[TOC]

本篇承接自上篇

4.JdbcTemplate

概念引入

JDBC已经能够满足大部分用户最基本的需求,但是在使用JDBC时,必须自己来管理数据库资源如:获取PreparedStatement,设置SQL语句参数,关闭连接等步骤。

JdbcTemplate是Spring对JDBC的封装,目的是使JDBC更加易于使用。JdbcTemplate是Spring的一部分。JdbcTemplate处理了资源的建立和释放。他帮助我们避免一些常见的错误,比如忘了总要关闭连接。他运行核心的JDBC工作流,如Statement的建立和执行,而我们只需要提供SQL语句和提取结果。

在使用JdbcTemplate之前,必须要先引入几个jar包。

在上面引入的jar包的基础上,还需要引入mysql的连接包以及spring中相关的依赖。

对应的maven依赖如下:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.3.15</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>

有了这些相应的依赖,我们就可以开始我们的工作了。

基础环境配置

既然是操作数据库,肯定首先要先连接到数据库,在IOC章节已经模拟演示过了。这里再操作一下,这次我选用外部属性文件导入的方式。

  • 在开始之前,先创建一个数据库,这里我命名为user_soberw

  • 创建一个外部properties属性文件

    prop.driverClassName=com.mysql.cj.jdbc.Driver
    prop.url=jdbc:mysql://localhost:3306/localhost?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT
    prop.username=root
    prop.password=123456
    
  • 在spring配置文件中引入外部文件,并配置数据库连接池

    <!--引入外部文件-->
    <context:property-placeholder location="classpath:MyJDBC.properties" />
    
    <!--配置数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="driverClassName" value="${prop.driverClassName}"/>
        <property name="url" value="${prop.url}"/>
        <property name="username" value="${prop.username}"/>
        <property name="password" value="${prop.password}"/>
    </bean>
    
  • 配置JdbcTemplate对象,注入DataSource

    <!--创建JdbcTemplate对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--set注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--组件扫描-->
    <context:component-scan base-package="com.soberw.spring"/>
    
  • 创建dao类接口BookDao以及实现类BookDaoImpl,在dao中注入jdbcTemplate

  • 创建service类BookService,在service中注入BookDao

    package com.soberw.spring.dao;
    
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author soberw
     * @Classname BookDao
     * @Description
     * @Date 2022-02-15 10:20
     */
    public interface BookDao {
    }
    
    package com.soberw.spring.dao;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author soberw
     * @Classname BookDaoImpl
     * @Description
     * @Date 2022-02-15 10:20
     */
    @Repository
    public class BookDaoImpl implements BookDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    }
    
    package com.soberw.spring.service;
    
    import com.soberw.spring.dao.BookDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @author soberw
     * @Classname BookService
     * @Description
     * @Date 2022-02-15 10:21
     */
    @Service
    public class BookService {
        @Autowired
        private BookDao bookDao;
    }
    
  • 在数据库中创建一个表t_book

    image-20220215143201919

  • 相应的,我们要在项目中创建一个对相应的bean实体类Book,对应表中的字段

    package com.soberw.spring.bean;
    
    /**
     * @author soberw
     * @Classname Book
     * @Description
     * @Date 2022-02-15 14:32
     */
    public class Book {
        private String bookId;
        private String bookname;
        private String bstatus;
    
        public String getBookId() {
            return bookId;
        }
    
        public void setBookId(String bookId) {
            this.bookId = bookId;
        }
    
        public String getBookname() {
            return bookname;
        }
    
        public void setBookname(String bookname) {
            this.bookname = bookname;
        }
    
        public String getBstatus() {
            return bstatus;
        }
    
        public void setBstatus(String bstatus) {
            this.bstatus = bstatus;
        }
    }
    
  • 前期准备就绪,接下来就可以进行数据库的操作了。

JdbcTemplate操作数据库

操作数据库无外乎就是对数据的增删改查操作,接下来逐一进行演示。

添加修改和删除

在开始之前,先引入一个函数JdbcTemplate.update(),对数据库的增删改操作都是使用这个函数

public int update(String sql, Object... args)

  • 参数一:传入预编译SQL语句,参数用占位符?代替
  • 参数二:可变参数,设置SQL语句的参数
  • BookService类中添加一个方法addBook(),用来完成添加操作。

    image-20220215151916708

  • 对应的,在BookDao中声明add()方法并在实现类中实现:

    image-20220215152051945

    @Override
    public void add(Book book) {
        //创建一个SQL语句
        String sql = "insert into t_book values(?,?,?)";
        //参数
        Object[] args = {book.getBookId(), book.getBookname(), book.getBstatus()};
        //调用方法实现
        int update = jdbcTemplate.update(sql, args);
        System.out.println(update);
    }
    
  • 进行测试:

    @Test
    public void testAdd(){
        ApplicationContext app = new ClassPathXmlApplicationContext("ApplicationContext.xml");
        BookService bookService = app.getBean("bookService", BookService.class);
        Book book = new Book();
        book.setBookId("1");
        book.setBookname("java");
        book.setBstatus("soberw");
        bookService.addBook(book);
    }
    

    image-20220215152210494

    image-20220215152228903

  • 添加成功。

  • BookService类中添加两个方法updateBook()delete(),用来完成修改删除操作。

    //修改的方法
    public void updateBook(Book book){
        bookDao.updateBook(book);
    }
    //删除的方法
    public void delete(String id){
        bookDao.delete(id);
    }
    
  • 对应的,在BookDao中声明方法并在实现类中实现:

    @Override
    public void updateBook(Book book) {
        String sql = "update t_book set bookname=?,bstatus=? where book_id=?";
        Object[] args = {book.getBookname(), book.getBstatus(), book.getBookId()};
        int update = jdbcTemplate.update(sql, args);
        System.out.println(update);
    }
    
    @Override
    public void delete(String id) {
        String sql = "delete from t_book where book_id=?";
        int update = jdbcTemplate.update(sql,id);
        System.out.println(update);
    }
    
  • 测试:

    • 修改上面添加的那条数据:

      //修改
      Book book = new Book();
      book.setBookId("1");
      book.setBookname("java入门到精通");
      book.setBstatus("soberw");
      bookService.updateBook(book);
      

      image-20220215153741937

      image-20220215153804733

    • 删除数据:

      //删除
      bookService.delete("1");
      

      image-20220215153929182

      image-20220215153945321

查询操作

一般我们开发中使用量最大的就是查询操作了,也是比较复杂的,我们需要对查询的返回值进行处理。而根据对查询返回集的不同,我这里将查询分为一下三部分。

在开始之前,先在表中添加若干数据。

image-20220215160717759

查询返回某个值

此类针对的是查询结果返回的是某一个值的情况,例如,查询员工的平均工资,查询某个部门一共多少人,查询单日客流量等...

返回的都只是一个单一值。这就用到了一个函数:JdbcTemplate.queryForObject()

T queryForObject(String sql, Class requiredType)

第一个参数:SQL语句

第二个参数:返回值类型Class

  • 这里以查询book表总个数为例:

  • 同样的添加方法:

    //查询返回某个值
    public int findCount(){
        return bookDao.selectCount();
    }
    
    @Override
    public int selectCount() {
        String sql = "select count(*) from t_book";
        return jdbcTemplate.queryForObject(sql, Integer.class);
    }
    
  • 测试:

    image-20220215161133177

查询返回对象

比如当用户点击商品详情页面的时候,页面中就会显示商品的详细信息,而此信息一般都是对应的数据库的一张表,或者视图,在项目中也一般会对应一个bean类。

因此返回的是一个对象,所用到的函数是:JdbcTemplate.queryForObject()

public T queryForObject(String sql, RowMapper rowMapper, Object... args)

  • 第一个参数:SQL语句
  • 第二个参数:RowMapper是接口,针对返回不同类型数据,使用这个接口里面实现类完成数据封装
  • 第三个参数:SQL语句参数值
  • 这里以查询返回一个book对象为例:

  • 添加查询方法

    //查询返回对象
    public Book findOne(String id){
        return bookDao.findBookInfo(id);
    }
    
    @Override
    public Book findBookInfo(String id) {
        String sql = "select book_id,bookname,bstatus from t_book where book_id=?";
        Book book = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Book>(Book.class), id);
        return book;
    }
    
  • 以查询book_id为5的数据为例:

    image-20220215163537096

查询返回集合

那么实际开发中使用最多的还是这个操作了,因为上面的都是返回某一个值的情况,但是实际情况中我们往往需要返回的是一堆数据。

例如商品列表显示,分页等操作,返回的都是一个集合。

这里用到的方法是JdbcTemplate.query()

List query(String sql, RowMapper rowMapper, @Nullable Object... args)

参数与之前的一样

  • 这里以查询表中所有的记录为例

  • 添加查询方法

    //查询返回集合
    public List<Book> findAll(){
        return bookDao.findAllBook();
    }
    
    @Override
    public List<Book> findAllBook() {
        String sql = "select * from t_book";
        List<Book> bookList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class));
        return bookList;
    }
    
  • 测试:

    image-20220215165102919

批量操作

单条记录的操作一般都过于繁琐,大多时候使用的是批量操作。

执行批量操作这里用到的方法是:JdbcTemplate.bacthUpdate()

public int[] batchUpdate(String sql, List<Object[]> batchArgs)

第一个参数:SQL语句

第二个参数:List集合,添加多条记录

  • 批量添加:

    //批量添加操作
    public void batchAdd(List<Object[]> batchArgs){
        bookDao.batchAdd(batchArgs);
    }
    
    @Override
    public void batchAdd(List<Object[]> batchArgs) {
        String sql = "insert into t_book values (?,?,?)";
        int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
        System.out.println(Arrays.toString(ints));
    }
    
    • 测试:

      //批量添加
      List<Object[]> batchArgs = new ArrayList<>();
      batchArgs.add(new Object[]{"8","spring","adgh"});
      batchArgs.add(new Object[]{"9","springmvc","afgdf"});
      batchArgs.add(new Object[]{"10","springboot","adagtrghgh"});
      batchArgs.add(new Object[]{"11","springcloud","adggujrty"});
      bookService.batchAdd(batchArgs);
      

      image-20220215172136384

      image-20220215172205383

  • 批量修改

    //批量修改操作
    public void batchUpdate(List<Object[]> batchArgs){
        bookDao.batchUpdate(batchArgs);
    }
    
    @Override
    public void batchUpdate(List<Object[]> batchArgs) {
        String sql = "update t_book set bookname=?,bstatus=? where book_id=?";
        int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
        System.out.println(Arrays.toString(ints));
    }
    
    • 测试

      //批量修改
      List<Object[]> batchArgs = new ArrayList<>();
      batchArgs.add(new Object[]{"spring","soberw","8"});
      batchArgs.add(new Object[]{"springmvc","soberw","9"});
      batchArgs.add(new Object[]{"springboot","soberw","10"});
      batchArgs.add(new Object[]{"springcloud","soberw","11"});
      bookService.batchUpdate(batchArgs);
      

      image-20220215172559314

      image-20220215172635808

  • 批量删除

    //批量删除操作
    public void batchDelete(List<Object[]> batchArgs){
        bookDao.batchDelete(batchArgs);
    }
    
    @Override
    public void batchDelete(List<Object[]> batchArgs) {
        String sql = "delete from t_book where book_id=?";
        int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
        System.out.println(Arrays.toString(ints));
    }
    
    • 测试:

      //批量删除
      List<Object[]> batchArgs = new ArrayList<>();
      batchArgs.add(new Object[]{"8"});
      batchArgs.add(new Object[]{"9"});
      batchArgs.add(new Object[]{"10"});
      batchArgs.add(new Object[]{"11"});
      bookService.batchDelete(batchArgs);
      

      image-20220215172837329

      image-20220215172945216

至此,使用JdbcTemplate操作数据库已经基本实现了,可以发现,相比于我们自己封装,确实方便了不少,而在类中提供的方法还远不止这些,针对不同场景,JdbcTemplate都给我们提供了相应的重载方法供我们使用,由于篇幅原因,这里不在演示。

5.事务管理

事务概念

事务是数据库操作最基本单元,逻辑上的一组操作,要么都成功,如果有一个失败所有操作都失败,用来确保数据的完整性和一致性。

了解数据库的都知道,事务有四个特性(ACID):

  • 原子性(Atomicity) : 原子性是指事务是一个不可分割的工作单位,事务中的操作 要么都发生,要么都不发生。

  • 一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

  • 隔离性(Isolation): 隔离性是指当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事物,不能被其他事物的操作干扰,多个并发事物之间要互相隔离,对任何两个并发的事物T1和T2,T1看来,T2要么在T1之前就结束,要么在T1结束之后才开始,这样每个事物都感觉不到有其他事物在并发执行。

  • 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变是永久性的,即便在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

典型的场景就是银行转账操作,一个人给另一个人转账,中间出现任何变故(比如断网)转账都会失败。

引入案例

接下来就以转账操作为例,演示如果没有事务,会是怎么样。

  • 业务分析

    image-20220215205219914

  • 创建数据表t_account,并添加记录

    image-20220215205606936

    image-20220215205626870

  • 其他配置以及相关的jar包的导入完全与JdbcTemplate操作一致。

  • 创建dao类接口UserDao以及实现类UserDaoImpl,在dao中注入jdbcTemplate

  • 创建service类UserService,在service中注入UserDao

    package com.soberw.spring.dao;
    
    /**
     * @author soberw
     * @Classname UserDao
     * @Description
     * @Date 2022-02-15 21:02
     */
    public interface UserDao {
    }
    
    package com.soberw.spring.dao;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    /**
     * @author soberw
     * @Classname UserDaoImpl
     * @Description
     * @Date 2022-02-15 21:10
     */
    @Repository
    public class UserDaoImpl implements UserDao{
        @Autowired
        private JdbcTemplate jdbcTemplate;
    }
    
    package com.soberw.spring.service;
    
    import com.soberw.spring.dao.UserDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @author soberw
     * @Classname UserService
     * @Description
     * @Date 2022-02-15 21:09
     */
    @Service
    public class UserService {
        @Autowired
        private UserDao userDao;
    }
    
  • 在dao创建两个方法:多钱的方法addMoney()以及少钱的方法reduceMoney(),实现Lucy给Mary转账100元。

    @Override
    public void addMoney() {
        String sql = "update t_account set money=money-? where username=?";
        jdbcTemplate.update(sql,100,"lucy");
    }
    
    @Override
    public void reduceMoney() {
        String sql = "update t_account set money=money+? where username=?";
        jdbcTemplate.update(sql,100,"mary");
    }
    
  • 在 service 创建方法(转账的方法)

    //转账的方法
    public void accountMoney(){
        //Lucy少100
        userDao.reduceMoney();
        //Mary多100
        userDao.addMoney();
    }
    
  • 测试:

    @Test
    public void testAccount(){
        ApplicationContext app = new ClassPathXmlApplicationContext("ApplicationContext.xml");
        UserService userService = app.getBean("userService", UserService.class);
        userService.accountMoney();
    }
    

    image-20220215212802809

思考一下:上面的案例代码,如果正常执行,那肯定是没有问题的,但是如果出现了异常,就会存在严重问题,比如在Lucy在少100后,,突然断网了,这时候会怎样呢?这里简单模拟一下。给程序手动加一个异常。

//转账的方法
public void accountMoney(){
    //Lucy少100
    userDao.reduceMoney();
    //模拟异常
    int i = 10/0;
    //Mary多100
    userDao.addMoney();
}

image-20220216084927535

image-20220216085138197

上面的问题应该如何解决呢?我们可以利用事务的特性,通过事务管理解决。

基本流程

try {
    //第一步  开启事务
    
    //第二步  进行业务操作
    //Lucy少100
    userDao.reduceMoney();
    
    //模拟异常
    int i = 10 / 0;
    
    //Mary多100
    userDao.addMoney();
    
    //第三步 没有发生异常,提交事务
}catch (Exception e){
    //第四步 出现异常,事务回滚
}

但是不同的数据库访问技术,处理操作事务是不同的,例如:

  • 使用jdbc访问数据库, 事务处理。

    public void updateAccount(){
        Connection conn = ...
        conn.setAutoCommit(false);
        stat.insert()
        stat.update();
        conn.commit();
        con.setAutoCommit(true)
    }
    
  • mybatis执行数据库,处理事务

    public void updateAccount(){
        SqlSession session = SqlSession.openSession(false);
        try{
            session.insert("insert into student...");
            session.update("update school ...");
            session.commit(); 
        }catch(Exception e){
            session.rollback();
        } 
    }
    

spring事务管理

而在spring里面,事务的操作就更简单了,使用spring的事务管理器,管理不同数据库访问技术的事务处理。 开发人员只需要掌握spring的事务处理一个方案, 就可以实现使用不同数据库访问技术的事务管理。管理事务面向的是spring, 有spring管理事务,做事务提交,事务回顾。

事务管理介绍

  1. 我们一般推荐将事务添加到JavaEE三层结构上面的service层(业务逻辑层)上

  2. 使用spring进行事务管理操作,一般有两种方式:

  • 编程式事务管理

    • 使用硬编码的方式,将代码嵌入到我们的方法逻辑中,比如上面的流程。

    • 实际开发中我们一般不用,因为难以维护,并且臃肿。

  • 声明式事务管理【常用】

    • 在spring进行声明式事务管理,其底层使用的是AOP
  1. 在spring中进行声明式事务管理,有两种方式:
  • 基于注解方式【常用】
  • 基于xml配置文件方式【了解即可】
  1. spring框架提供了一个API接口,代表事务管理器,这个接口针对于不同的框架,提供了不同的实现类

    image-20220216093809787

    如图:事务管理器有很多实现类: 一种数据库的访问技术有一个实现类。 由实现类具体完成事务的提交,回顾。

    例如:jdbc或者mybatis访问数据库有自己的事务管理器实现类DataSourceTranactionManager

    ​ hibernate框架,他的事务管理器实现类HibernateTransactionManager,这就增加了代码的通用性。

注解方式实现声明式事务管理

  • 在spring配置文件中创建事务管理器对象

    <!--创建事务管理器对象-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
  • 在spring配置文件中开启事务注解

    • 引入命名空间tx

      image-20220216094839821

    • 开启事务注解

      <!--开启事务注解-->
      <tx:annotation-driven transaction-manager="transactionManager"/>
      
    • 在service类上面或者service类方法上加入事务注解

        1. @Transactional,这个注解添加在类或者方法上
        1. 如果把这个注解添加在类上,这个类里面的所有方法都添加了事务
        1. 如果把这个注解添加在方法上,则只为这个方法添加事务

      image-20220216095750361

    • 测试一下,现在我还模拟一下异常情况:

      //转账的方法
      public void accountMoney(){
              //Lucy少100
              userDao.reduceMoney();
      
              //模拟异常
              int i = 10 / 0;
      
              //Mary多100
              userDao.addMoney();
      }
      

      image-20220216100019163

      image-20220216100120842

注解参数说明

注解@Transactional中可以传入这些参数

  • 我们主要配置这些参数:

image-20220216100823594

  • propagation事务传播行为
  • isolation事务隔离级别
  • timeout超时时间
  • readOnly是否只读
  • rollbackFor回滚
  • noRollbackFor不回滚
事务传播行为
  • 传播行为:业务方法在调用时,事务在方法之间的,传递和使用。使用传播行为,标识方法有无事务。

  • 事务传播行为:在多事务方法直接进行调用时,这个过程中,事务是如何进行管理的

  • 有七种事务传播行为:

    image-20220216103934111

    image-20220216105127379

  • 重点掌握三个:

    • PROPAGATION_REQUIRED

      • spring默认传播行为,方法在调用的时候,如果存在事务就是使用当前的事务,如果没有事务,则新建事务,方法在新事务中执行。

      • 举个例子,李四发现张三开着热点,李四就连上了张三的热点,实际上他们使用的就是一个网络了,也就是当前存在事务时,就加入到这个事务中运行;后来张三发现有人蹭网就把热点关了,李四发现没热点了,就打开了自己的数据,也就是如果没有事务,则新建事务

    • PROPAGATION_SUPPORTS

      • 支持的意思,方法有事务可以正常执行,没有事务也可以正常执行。
      • 一般多用在查询操作上
    • PROPAGATION_REQUIRES_NEW

      • 方法需要一个新事务。如果调用方法时,存在一个事务,则原来的事务暂停。直到新事务执行完毕。如果方法调用时,没有事务,则新建一个事务,在新事务执行代码。
事务隔离级别

事务的特性称为隔离性,多事务操作之间不会产生影响。

不考虑隔离性会产生很多问题。

有三个读问题:脏读、不可重复读、幻读

  • 脏读:一个未提交事务读取到另一个未提交事务的数据。

    image-20220216113353194

  • 不可重复读:一个未提交事务读取到另一个已提交事务的数据。

    image-20220216113803266

  • 幻读(虚读):一个未提交事务读取到了另一个提交事务添加的数据。

通过设置事务的隔离级别,就能解决读问题。

  • 1)DEFAULT:采用 DB 默认的事务隔离级别。MySql的默认为REPEATABLE_READ;Oracle 默认为READ_COMMITTED。

  • 2)READ_UNCOMMITTED:读未提交。未解决任何并发问题。

  • 3)READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。

  • 4)REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读

  • 5)SERIALIZABLE:串行化。不存在并发问题。

对比图:

image-20220216122513981

事务其他参数
  • timeout:超时时间

    • 事务需要在一定时间内进行提交,如果不提交或者执行超时则回滚
    • 默认值为-1,设置时间以秒为单位计算
  • readOnly:是否只读

    • 读:查询操作;写:添加修改删除操作
    • 默认值为false,表示可以读写操作
    • 可以设置为true,表示只能进行查询操作
  • rollbackFor:回滚

    • 设置出现哪些异常进行事务回滚
  • noRollbackFor:不回滚

    • 设置出现哪些异常不进行事务回滚

    • 这两个方法传入的都是对应的异常类

    • 例如:rollbackFor = {Exception.class}

xml方式实现声明式事务管理(了解)

使用配置文件实现,主要有三个步骤:

    1. 配置事务管理器
    1. 配置通知
    1. 配置切入点和切面
  • 完整配置如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!--引入外部文件-->
        <context:property-placeholder location="classpath:MyJDBC.properties"/>
    
        <!--配置数据库连接池-->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
            <property name="driverClassName" value="${prop.driverClassName}"/>
            <property name="url" value="${prop.url}"/>
            <property name="username" value="${prop.username}"/>
            <property name="password" value="${prop.password}"/>
        </bean>
    
        <!--创建JdbcTemplate对象-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!--set注入dataSource-->
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!--组件扫描-->
        <context:component-scan base-package="com.soberw.spring"/>
    
        <!--======================================以下是配置事务管理部分====================================================-->
    
        <!--1.创建事务管理器对象-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!--注入数据源-->
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!--开启事务注解-->
        <!--    <tx:annotation-driven transaction-manager="transactionManager"/>-->
    
        <!--2.配置通知,即声明业务方法的事务属性(隔离级别,传播行为,超时等)
                 id:给业务方法配置事务段代码起个名称,唯一值
                transaction-manager:事务管理器的id
        -->
        <tx:advice id="txadvice" transaction-manager="transactionManager">
            <!--配置事务参数,即给具体的业务方法增加事务的说明-->
            <tx:attributes>
                <!--指定哪种规则,哪个方法上添加事务
                   给具体的业务方法,说明他需要的事务属性
                   name: 业务方法名称。
                         配置name的值: 1. 业务方法的名称; 2. 带有部分通配符(*)的方法名称; 3 使用*
                   propagation:指定传播行为的值
                   isolation:隔离级别
                   read-only:是否只读,默认是false
                   timeout:超时时间
                   rollback-for:指定回滚的异常类列表,使用的异常全限定名称
                -->
                <tx:method name="accountMoney" propagation="REQUIRED" isolation="DEFAULT"
                read-only="false" timeout="20" rollback-for="java.lang.NullPointerException"/>
    
                <!--在业务方法有命名规则后, 可以对一些方法使用事务-->
                <tx:method name="add*" propagation="REQUIRES_NEW"
                           rollback-for="java.lang.Exception" />
                <tx:method name="modify*"
                           propagation="REQUIRED" rollback-for="java.lang.Exception" />
                <tx:method name="remove*"
                           propagation="REQUIRED" rollback-for="java.lang.Exception" />
    
                <!--以上方法以外的 * :querySale, findSale, searchSale -->
                <tx:method name="*" propagation="SUPPORTS" read-only="true" />
            </tx:attributes>
        </tx:advice>
        <!--3.配置切入点和切面,即声明切入点表达式: 表明那些包中的类,类中的方法参与事务-->
        <aop:config>
            <!--配置切入点,即声明切入点表达式
                expression:切入点表达式, 表示那些类和类中的方法要参与事务
                id:切入点表达式的名称,唯一值
            -->
            <aop:pointcut id="pt" expression="execution(* com.soberw.spring.service.UserService.*(..))"/>
            <!--配置切面,即关联切入点表达式和事务通知-->
            <aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
        </aop:config>
    </beans>
    

完全注解方式

即完全使用注解来实现,包括连接数据库连接池,配置事务管理等....

  • 创建一个配置类:

    package com.soberw.spring.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    /**
     * @author soberw
     * @Classname SpringConfig
     * @Description
     * @Date 2022-02-16 14:12
     */
    @Configuration  //配置类
    @ComponentScan(basePackages = "com.soberw.spring")  //组件扫描
    @EnableTransactionManagement  //开启事务
    public class SpringConfig {
        //创建数据库连接池
        @Bean
        public DruidDataSource getDruidDataSource(){
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/user_soberw?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT");
            dataSource.setUsername("root");
            dataSource.setPassword("123456");
            return dataSource;
        }
        //创建JdbcTemplate对象
        @Bean
        public JdbcTemplate getJdbcTemplate(DataSource dataSource){
            //到ioc容器中根据类型找到dataSource
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            //注入
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
    
        //创建事务管理器
        @Bean
        public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            dataSourceTransactionManager.setDataSource(dataSource);
            return dataSourceTransactionManager;
        }
    }
    
  • 测试一下:

    @Test
    public void testAnno(){
        ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserService userService = app.getBean("userService", UserService.class);
        userService.accountMoney();
    }
    

    image-20220216143337011

    image-20220216143428040

6.Spring5新特性

查看中文文档请点击,里面标注了spring5框架完整的新功能。

这里对几个重大的变动进行简单说明:

    1. 整个 Spring5 框架的代码基于 Java8,运行时兼容 JDK9,许多不建议使用的类和方法在代码库中删除
    1. Spring 5.0 框架自带了通用的日志封装
    1. Spring5 框架核心容器支持@Nullable注解
    1. Spring5 核心容器支持函数式风格GenericApplicationContext
    1. Spring5 支持整合 JUnit5
    1. 新增模块Webflux,用于web开发

日志封装

Spring 5.0 框架自带了通用的日志封装。

  • Spring5 已经移除 Log4jConfigListener,官方建议使用Log4j2
  • Spring5 框架整合 Log4j2

  • 第一步:导入jar包,或者引入maven依赖

    <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.17.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.17.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.17.1</version>
        <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.30</version>
    </dependency>
    
  • 第二步:创建固定名字的配置文件log4j2.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
    日志级别以及优先级排序:
            OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL
    -->
    
    <!--Configuration后面的status用于设置log4j2自身内部的信息输出,可以不设置, 当设置成trace时,可以看到log4j2内部各种详细输出-->
    <configuration status="INFO">
        <!--先定义所有的appender-->
        <appenders>
            <!--输出日志信息到控制台-->
            <console name="Console" target="SYSTEM_OUT">
                <!--控制日志输出的格式-->
                <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
            </console>
        </appenders>
        <!--然后定义logger,只有定义logger并引入的appender,appender才会生效-->
        <!--root:用于指定项目的根日志,如果没有单独指定Logger,则会使用root作为默认的日志输出-->
        <loggers>
            <root level="info">
                <appender-ref ref="Console"/>
            </root>
        </loggers>
    </configuration>
    
  • 这就实现了日志信息的输出,在不做任何操作的情况下,运行上面的测试代码

    image-20220216171515262

  • 当然我们也可以手动输出日志,新建一个类UserLog模拟一下:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * @author soberw
     * @Classname UserLog
     * @Description
     * @Date 2022-02-16 17:18
     */
    public class UserLog {
        private static final Logger logger = LoggerFactory.getLogger(UserLog.class);
    
        public static void main(String[] args) {
            logger.info("hello log4j2");
            logger.warn("hello log4j2");
        }
    }
    

    image-20220216172227802

支持@Nullable注解

Spring5 框架核心容器开始支持@Nullable 注解

  • @Nullable 注解可以使用在方法上面,属性上面,参数上面
  • 注解用在方法上面,方法返回值可以为空
  • 注解使用在方法参数里面,方法参数可以为空
  • 注解使用在属性上面,属性值可以为空

支持函数式风格

java8新增的一个很重大的内容就是lambda表达式了,在spring5中,也提供了对lambda的支持,用的是GenericApplicationContext实现类

使用函数式风格创建对象,并交给spring进行管理。

以上面的类UserLog为例:

//函数式风格创建对象,交给spring进行管理
@Test
public void testGeneric(){
    //1. 创建GenericApplicationContext对象
    GenericApplicationContext context = new GenericApplicationContext();
    //2. 调用方法注册
    context.refresh();
    context.registerBean("userLog",UserLog.class, UserLog::new);
    //3. 获取在spring注册的对象
    UserLog ul = context.getBean("userLog",UserLog.class);
    System.out.println(ul);
}

image-20220216183512156

整合JUnit5

配置文件一次获取,多次调用,我们不再需要像上面那样,每次测试都要重新读取一次配置文件。

  1. 整合JUnit4实现

    第一步:导入Spring相关针对测试依赖

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.15</version>
        <scope>test</scope>
    </dependency>
    

    第二步:创建测试类,使用注解完成

    package com.soberw.spring.test;
    
    import com.soberw.spring.service.UserService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    /**
     * @author soberw
     * @Classname JTest4
     * @Description
     * @Date 2022-02-16 18:45
     */
    @RunWith(SpringJUnit4ClassRunner.class)//单元测试框架
    @ContextConfiguration("classpath:ApplicationContext.xml")//加载配置文件
    public class JTest4 {
        @Autowired
        private UserService userService;
    
        @Test
        public void test1(){
            userService.accountMoney();
        }
    }
    

    image-20220216185639998

  2. 上面这个功能我们使用JUnit4、5都能实现,但是在JUnit5中对这一写法做出了改动,使其更加简洁。

  3. 先引入JUnit5的jar包、其实这里我们可以借助我们的高级工具自动引入:

    image-20220216190115228

    编写测试类:

    package com.soberw.spring.test;
    
    import com.soberw.spring.service.UserService;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    
    /**
     * @author soberw
     * @Classname JTest5
     * @Description
     * @Date 2022-02-16 19:03
     */
    @ExtendWith(SpringExtension.class)//单元测试框架
    @ContextConfiguration("classpath:ApplicationContext.xml")
    public class JTest5 {
        @Autowired
        private UserService userService;
    
        @Test
        public void test1(){
            userService.accountMoney();
        }
    }
    

    image-20220216190611422

  4. 上面这样做其实和4版本没区别,只是导入的包不同而已。

  5. 5版本支持使用一个复合注解替代上面两个注解完成整合。

    image-20220216190824835

另外,spring还推出了一个全新的模块Webflux,用于 web 开发的,功能和 SpringMVC 类似的,Webflux 使用 当前一种比较流程响应式编程出现的框架。此模块需要基于SpringMVC,SpringBoot等知识进行实现,篇幅原因,这里不在赘述,感兴趣的同学可以点击此处了解