MyBatis&Spring Framrwork

1. MyBatis

1.1 概述

  • MyBatis是一款优秀的持久层框架,用于简化JDBC开发

  • MyBatis本是Apache的一个开源项目iBatis,2010年这个项目迁移到了google code,并改名为MyBatis。2013年迁移到Github

  • 官网:https://mybatis.net.cn

  • 持久层

    • 负责将数据保存到数据库的那一层代码
    • JavaEE三层架构:表现层、业务层、持久层
  • MyBatis简化JDBC

    • 硬编码:---->配置文件
      • 注册驱动,获取连接
      • SQL语句
    • 操作繁琐:---->自动完成
      • 手动设置参数
      • 手动封装给结果集

    MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作

1.2 MyBatis快速入门

  • 需求:查询user表中所有的数据

    • 创建user表,添加数据

      create database mybatis;
      use mybatis;
      drop table if exists tb_user;
      create table tb_user(
      id int primary key auto_increment,
      username varchar(20),
      password varchar(20),
      gender char(1),
      addr varchar(30)
      );
      INSERT INTO tb_user VALUES (1, 'zhangsan', '123', '男', '北京');
      INSERT INTO tb_user VALUES (2, '李四', '234', '女', '天津');
      INSERT INTO tb_user VALUES (3, '王五', '11', '男', '西安');
      
    • 创建模块,导入坐标

      <dependencies>
              <!--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>8.0.29</version>
              </dependency>
      
              <!--junit单元测试-->
              <dependency>
                  <groupId>junit</groupId>
                  <artifactId>junit</artifactId>
                  <version>4.11</version>
                  <scope>test</scope>
              </dependency>
      
              <!-- 添加slf4j日志api -->
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>1.7.20</version>
              </dependency>
              <!-- 添加logback-classic依赖 -->
              <dependency>
                  <groupId>ch.qos.logback</groupId>
                  <artifactId>logback-classic</artifactId>
                  <version>1.2.3</version>
              </dependency>
              <!-- 添加logback-core依赖 -->
              <dependency>
                  <groupId>ch.qos.logback</groupId>
                  <artifactId>logback-core</artifactId>
                  <version>1.2.3</version>
              </dependency>
          </dependencies>
      
    • 编写 MyBatis 核心配置文件 --> 替换连接信息 解决硬编码问题

      从 XML 中构建 SqlSessionFactory

      在模块下的 resources 目录下创建mybatis的配置文件 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>
          <environments default="development">
              <environment id="development">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                      <!--数据库连接信息-->
                      <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                      <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                      <property name="username" value="root"/>
                      <property name="password" value="123"/>
                  </dataSource>
              </environment>
          </environments>
          <mappers>
              <!--加载sql的映射文件-->
              <mapper resource="UserMapper.xml"/>
          </mappers>
      </configuration>
      
    • 编写 SQL 映射文件 --> 统一管理sql语句,解决硬编码问题

      在模块的 resources 目录下创建映射配置文件 UserMapper.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">
      <!--
          namespace:名称空间
      -->
      <mapper namespace="test">
          <select id="selectAll" resultType="com.mark.pojo.User">
              select * from tb_user;
          </select>
      
      </mapper>
      
    • 编码

      • 定义pojo类

        package com.mark.pojo;
        
        /**
         * @ClassName User
         * @Description TODO
         * @Author Mark
         * @Date 2022/8/29 16:04
         * @Version 1.0
         */
        public class User {
            //alt + 拖动鼠标左键 整列编辑
            private Integer id;
            private String username;
            private String password;
            private String gender;
            private String addr;
        
            public Integer getId() {
                return id;
            }
        
            public void setId(Integer id) {
                this.id = id;
            }
        
            public String getUsername() {
                return username;
            }
        
            public void setUsername(String username) {
                this.username = username;
            }
        
            public String getPassword() {
                return password;
            }
        
            public void setPassword(String password) {
                this.password = password;
            }
        
            public String getGender() {
                return gender;
            }
        
            public void setGender(String gender) {
                this.gender = gender;
            }
        
            public String getAddr() {
                return addr;
            }
        
            public void setAddr(String addr) {
                this.addr = addr;
            }
        
            @Override
            public String toString() {
                return "User{" +
                        "id=" + id +
                        ", username='" + username + '\'' +
                        ", password='" + password + '\'' +
                        ", gender='" + gender + '\'' +
                        ", addr='" + addr + '\'' +
                        '}';
            }
        }
        
        
      • 加载核心配置文件,获取SqlSessionFactory对象

      • 获取SqlSession对象,执行SQL语句

      • 释放资源

        package com.mark;
        
        import com.mark.pojo.User;
        import org.apache.ibatis.io.Resources;
        import org.apache.ibatis.session.SqlSession;
        import org.apache.ibatis.session.SqlSessionFactory;
        import org.apache.ibatis.session.SqlSessionFactoryBuilder;
        
        import java.io.IOException;
        import java.io.InputStream;
        import java.util.List;
        
        /**
         * @ClassName MyBatisDemo
         * @Description TODO MyBatis快速入门
         * @Author Mark
         * @Date 2022/8/29 16:13
         * @Version 1.0
         */
        public class MyBatisDemo {
            public static void main(String[] args) throws IOException {
                //1.加载Mybatis核心配置文件,获取SqlSessionFactory
                String resource = "mybatis-config.xml";
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                //2.获取SqlSession对象,执行SQL语句
                SqlSession sqlSession = sqlSessionFactory.openSession();
                List<User> users = sqlSession.selectList("test.selectAll");
                System.out.println(users);
                //3.释放资源
                sqlSession.close();
            }
        }
        
        
  • 在IDEA中配置MySql数据库连接

    • 在IDEA右侧点击Database
    • 左边“+”--->Data Source--->MySQL
    • 输入配置

1.3 Mapper代理开发

  • 目的:

    • 解决原生方式中的硬编码
    • 简化后期执行SQL
  • 步骤

    • 定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下

      image

      修改mybatis-config.xml中sql的映射文件位置

      <mappers>
          <!--加载sql的映射文件-->
          <mapper resource="com/mark/mapper/UserMapper.xml"/>
      </mappers>
      
    • 设置SQL映射文件的namespace属性为Mapper接口全限定名

      <mapper namespace="com.mark.mapper.UserMapper">
          <select id="selectAll" resultType="com.mark.pojo.User">
              select * from tb_user;
          </select>
      
      </mapper>
      
    • 在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致

      package com.mark.mapper;
      
      import com.mark.pojo.User;
      
      import java.util.List;
      
      public interface UserMapper {
          List<User> selectAll();
      }
      
    • 编码

      • 通过SqlSession的getMapper方法获取Mapper接口的代理对象

      • 调用对应方法完成sql的执行

        package com.mark;
        
        import com.mark.mapper.UserMapper;
        import com.mark.pojo.User;
        import org.apache.ibatis.io.Resources;
        import org.apache.ibatis.session.SqlSession;
        import org.apache.ibatis.session.SqlSessionFactory;
        import org.apache.ibatis.session.SqlSessionFactoryBuilder;
        
        import java.io.IOException;
        import java.io.InputStream;
        import java.util.List;
        
        /**
         * @ClassName MyBatisDemo
         * @Description TODO Mapper代理开发
         * @Author Mark
         * @Date 2022/8/29 16:13
         * @Version 1.0
         */
        public class MyBatisDemo2 {
            public static void main(String[] args) throws IOException {
                //1.加载Mybatis核心配置文件,获取SqlSessionFactory
                String resource = "mybatis-config.xml";
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                //2.获取SqlSession对象,执行SQL语句
                SqlSession sqlSession = sqlSessionFactory.openSession();
        
        //        List<User> users = sqlSession.selectList("test.selectAll");
        //        System.out.println(users);
        
                //⭐2.1获取UserMapper接口的代理对象
                UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
                //⭐2.2执行对应方法
                List<User> users = userMapper.selectAll();
              
                System.out.println(users);
                //3.释放资源
                sqlSession.close();
            }
        }
        

如果Mapper接口名称和SQL映射文件名称相同,在同一目录下,则可以使用包扫描的方式简化SQL映射文件的加载

<mappers>
    <!--加载sql的映射文件-->
    <!--<mapper resource="com/mark/mapper/UserMapper.xml"/>-->
    <package name="com.mark.mapper"/>
</mappers>

1.4 MyBatis核心配置文件

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>
    <!--类型别名,在SQL映射文件中不再需要写com.mark.pojo.User,可以直接写user,无大小写区分-->
    <typeAliases>
        <package name="com.mark.pojo"/>
    </typeAliases>

    <!--environments:配置数据库连接环境信息,可以配置多个environment,通过default属性来切换不同的environment-->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!--数据库连接信息-->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!--加载sql的映射文件-->
        <!--        <mapper resource="com/mark/mapper/UserMapper.xml"/>-->
        <package name="com.mark.mapper"/>
    </mappers>
</configuration>

在配置标签时应当注意顺序

1.5 配置文件完成增删改查

  • 创建tb_brand表并添加数据

    -- 删除tb_brand表
    drop table if exists tb_brand;
    -- 创建tb_brand表
    create table tb_brand
    (
    -- id 主键
    id int primary key auto_increment,
    -- 品牌名称
    brand_name varchar(20),
    -- 企业名称
    company_name varchar(20),
    -- 排序字段
    ordered int,
    -- 描述信息
    description varchar(100),
    -- 状态:0:禁用 1:启用
    status int
    );
    -- 添加数据
    insert into tb_brand (brand_name, company_name, ordered, description, status)
    values ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
    ('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
    ('小米', '小米科技有限公司', 50, 'are you ok', 1);
    
  • 创建测试类MyBatisTest

  • 创建Brand类

    package com.mark.pojo;
    
    /**
     * @ClassName Brand
     * @Description TODO
     * @Author Mark
     * @Date 2022/8/29 18:10
     * @Version 1.0
     */
    public class Brand {
        // id 主键
        private Integer id;
        // 品牌名称
        private String brandName;
        // 企业名称
        private String companyName;
        // 排序字段
        private Integer ordered;
        // 描述信息
        private String description;
        // 状态:0:禁用 1:启用
        private Integer status;
        
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getBrandName() {
            return brandName;
        }
    
        public void setBrandName(String brandName) {
            this.brandName = brandName;
        }
    
        public String getCompanyName() {
            return companyName;
        }
    
        public void setCompanyName(String companyName) {
            this.companyName = companyName;
        }
    
        public Integer getOrdered() {
            return ordered;
        }
    
        public void setOrdered(Integer ordered) {
            this.ordered = ordered;
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    
        public Integer getStatus() {
            return status;
        }
    
        public void setStatus(Integer status) {
            this.status = status;
        }
    
        @Override
        public String toString() {
            return "Brand{" +
                    "id=" + id +
                    ", brandName='" + brandName + '\'' +
                    ", companyName='" + companyName + '\'' +
                    ", ordered=" + ordered +
                    ", description='" + description + '\'' +
                    ", status=" + status +
                    '}';
        }
    }
    
    
  • 创建BrandMapper接口

    package com.mark.mapper;
    
    import com.mark.pojo.Brand;
    
    import java.util.List;
    
    public interface BrandMapper {
        public List<Brand> selectAll();
    }
    
  • 创建BrandMapper.xmlSQL映射文件

    <?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="com.mark.mapper.BrandMapper">
    
    </mapper>
    

1.5.1 查询所有数据

  • 在BrandMapper接口中定义方法selectAll()

    public interface BrandMapper {
        public List<Brand> selectAll();
    }
    
  • alt+enter生成映射文件中的select标签,并编写查询语句

    <mapper namespace="com.mark.mapper.BrandMapper">
    
        <select id="selectAll" resultType="brand">
            select *
            from tb_brand;
        </select>
    
    </mapper>
    
  • 编写测试类执行方法查询

    public class MyBatisTest {
        @Test
        public void testSelectAll() throws IOException {
            //1.获取SqlSessionFactory
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            //2.获取sqlSession对象
            SqlSession sqlSession = sqlSessionFactory.openSession();
            //3.获取Mapper接口的代理对象
            BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
            //4.执行方法
            List<Brand> brands = brandMapper.selectAll();
            System.out.println(brands);
            //5.释放资源
            sqlSession.close();
        }
    }
    

结果映射时数据库表的字段名称和实体类的属性名称不一样,则不能自动封装数据,解决方式有两种:

  • 给数据库中字段名称不一样的起别名,让数据库中字段与实体类相同

    <select id="selectAll" resultType="brand">
        select  id, brand_name as brandName, company_name as companyName, ordered, description, status
        from tb_brand;
    </select>
    
    • 缺点:每次查询都要定义一次别名

      • sql片段

        <sql id="brand_column">
            id, brand_name as brandName, company_name as companyName, ordered, description, status
        </sql>
        
        <select id="selectAll" resultType="brand">
            select
                <include refid="brand_column"/>
            from tb_brand;
        </select>
        
        • 缺点:不灵活
  • ⭐resultMap:

    • 定义<resultMap>标签
    • 在<select>标签中使用resultMap属性替换resultType属性
    <!--id:唯一标识-->
        <!--type:映射的类型,支持别名-->
    <resultMap id="brandResultMap" type="brand">
      	<!--
          id:主键字段的映射
             column:表的列名
             property:实体类的属性名
          result:一般字段的映射
             column:表的列名
             property:实体类的属性名
        -->      
        <result column="brand_name" property="brandName"/>
        <result column="company_name" property="companyName"/>
    </resultMap>
    
    <select id="selectAll" resultMap="brandResultMap">
        select *
        from tb_brand;
    </select>
    
    

两处细节

  • 参数占位符:

    • #{}:会将其替换为?,防止SQL注入
    • ${}:拼sql。会存在sql注入问题

    使用时机:

    • 参数传递的时候用#{}
    • 表名或者列名不固定时用${}
    <select id="selectById" resultMap="brandResultMap">
        select *
        from tb_brand where id = #{id};
    </select>
    
    Brand selectById(int id);
    
    //接收参数
    int id = 1;
    //4.执行方法
    Brand brand = brandMapper.selectById(id);
    System.out.println(brand);
    
  • 参数类型:parameterType

  • 特殊字符处理:

    • 转义字符 如<的转义字符:&lt;
    • CDATA区(大写CD回车即可):<![CDATA[ ]]>

条件查询

  • 多条件查询

    • 散装参数:如果方法中有多个参数,需要使用@Param("SQL参数占位符名称")

      List<Brand> selectByCondition(@Param("status")int status,@Param("companyName")String companyName,@Param("brandName")String brandName);

      <select id="selectByCondition" resultMap="brandResultMap">
          select *
          from tb_brand
          where
              status = #{status}
              and company_name like #{companyName}
              and brand_name like #{brandName}
          ;
      </select>
      
      //接收参数
      int status = 1;
      String companyName = "华为";
      String brandName = "华为";
      
      //处理参数
      companyName = "%" + companyName + "%";
      brandName = "%" + brandName + "%";
      
      //4.执行方法
      List<Brand> brands = brandMapper.selectByCondition(status, companyName, brandName);
      System.out.println(brands);
      
    • 对象参数:对象的属性名称要和参数占位符名称一致

      List<Brand> selectByCondition(Brand brand);

      //接收参数
      int status = 1;
      String companyName = "华为";
      String brandName = "华为";
      
      //处理参数
      companyName = "%" + companyName + "%";
      brandName = "%" + brandName + "%";
      
      Brand brand = new Brand();
      brand.setStatus(status);
      brand.setCompanyName(companyName);
      brand.setBrandName(brandName);
      
      //4.执行方法
      List<Brand> brands = brandMapper.selectByCondition(brand);
      System.out.println(brands);
      
    • map集合参数

      List<Brand> selectByCondition(Map map);

      //接收参数
      int status = 1;
      String companyName = "华为";
      String brandName = "华为";
      
      //处理参数
      companyName = "%" + companyName + "%";
      brandName = "%" + brandName + "%";
      
      Map map =new HashMap();
      map.put("status",status);
      map.put("companyName",companyName);
      map.put("brandName",brandName);
      
      //4.执行方法
      List<Brand> brands = brandMapper.selectByCondition(map);
      System.out.println(brands);
      

1.5.2 添加

添加步骤:

  • 编写接口方法:Mapper接口

    • 参数:除了id之外的所有数据
    • 结果:void

    void add(Brand brand);

  • 编写SQL语句:SQL映射文件

    <insert id="add">
        insert into tb_brand(brand_name, company_name, ordered, description, status)
        values(#{brandName},#{companyName},#{ordered},#{description},#{status});
    </insert>
    
  • 执行方法,测试

    • MyBatis事务:
      • openSession():默认开启事务,进行增删改操作后需要使用sqlSession.commit(),手动提交事务
      • openSession(true):可以设置为自动提交事务(关闭事务)
    @Test
    public void testAdd() throws IOException {
        //接收参数
        int status = 1;
        String companyName = "菠萝手机";
        String brandName = "菠萝";
        String description = "菠萝手机就是牛";
        int ordered = 100;
        Brand brand = new Brand();
        brand.setStatus(status);
        brand.setCompanyName(companyName);
        brand.setBrandName(brandName);
        brand.setDescription(description);
        brand.setOrdered(ordered);
        //1.获取SqlSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //2.获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);
        //3.获取Mapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
        //4.执行方法
        brandMapper.add(brand);
    
        //提交事务
        //sqlSession.commit();
        //5.释放资源
        sqlSession.close();
    }
    

主键返回:

在数据添加成功后,需要获取插入数据库的主键的值,如订单和订单项

  • 添加订单
  • 添加订单项,订单项中需要设置所属订单的id
<insert id="add" useGeneratedKeys="true" keyProperty="id">
    insert into tb_brand(brand_name, company_name, ordered, description, status)
    values(#{brandName},#{companyName},#{ordered},#{description},#{status});
</insert>

这时调用方法的getId即可取出id值

alter table tablename auto_increment = 新数字;可以重新设置自增id开始数字

1.5.3 修改

  • 修改全部字段

    • 编写接口方法

      • 参数:所有数据
      • 结果:void

      int update(Brand brand);

    • 编写SQL语句:SQL映射文件

      <update id="update">
          update tb_brand
          set brand_name   = #{brandName},
              company_name = #{companyName},
              ordered      = #{ordered},
              description  = #{description},
              status       = #{status}
          where id = #{id};
      </update>
      
    • 执行方法,测试

      @Test
      public void testupdate() throws IOException {
          //接收参数
          //接收参数
          int status = 1;
          String companyName = "菠萝手机";
          String brandName = "菠萝";
          String description = "菠萝手机,手机中的战斗机";
          int ordered = 200;
          int id = 4;
      
          Brand brand = new Brand();
          brand.setStatus(status);
          brand.setCompanyName(companyName);
          brand.setBrandName(brandName);
          brand.setDescription(description);
          brand.setOrdered(ordered);
          brand.setId(id);
      
          //1.获取SqlSessionFactory
          String resource = "mybatis-config.xml";
          InputStream inputStream = Resources.getResourceAsStream(resource);
          SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
          //2.获取sqlSession对象
          SqlSession sqlSession = sqlSessionFactory.openSession(true);
          //3.获取Mapper接口的代理对象
          BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
          //4.执行方法
          int change = brandMapper.update(brand);
          System.out.println(change);
          //5.释放资源
          sqlSession.close();
      }
      
  • 修改动态字段

    <update id="update">
        update tb_brand
        <set>
            <if test="brandName != null and brandName != ''">
                brand_name = #{brandName},
            </if>
            <if test="companyName != null and companyName != ''">
                company_name = #{companyName},
            </if>
            <if test="ordered != null">
                ordered = #{ordered},
            </if>
            <if test="description != null and description != ''">
                description = #{description},
            </if>
            <if test="status != null">
                status = #{status}
            </if>
        </set>
        where id = #{id};
    </update>
    

1.5.4 删除

  • 删除一个

    void deleteById(int id);

    <delete id="deleteById">
        delete from tb_brand where id = #{id};
    </delete>
    
  • 批量删除

    • 编写接口方法:Mapper数组

      • 参数:id数组
      • 结果:void

      void deleteByIds(@Param("ids")int[] ids);

    • 编写SQL语句:SQL映射文件

      <!--MyBatis会将数组封装为Map集合
          默认情况下key = array,value = 数组
          可以使用param注解来改变map集合的默认key的名称
      -->
      <delete id="deleteByIds">
          delete from tb_brand where id
          in
          <foreach collection="ids" item="id" separator="," open="(" close=")">
              #{id}
          </foreach>
          ;
      </delete>
      
    • 执行方法,测试

      @Test
      public void testDeleteByIds() throws IOException {
          int ids[] = {1,3};
          //1.获取SqlSessionFactory
          String resource = "mybatis-config.xml";
          InputStream inputStream = Resources.getResourceAsStream(resource);
          SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
          //2.获取sqlSession对象
          SqlSession sqlSession = sqlSessionFactory.openSession();
          //3.获取Mapper接口的代理对象
          BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
          //4.执行方法
          brandMapper.deleteByIds(ids);
      
          sqlSession.commit();
          //5.释放资源
          sqlSession.close();
      }
      

1.5.5 参数传递

MyBatis接口方法中可以接收各种各样的参数,MyBatis底层对这些数据进行不同的封装处理方式

  • 单个参数

    • POJO类型:可以直接使用,属性名要和参数占位符名称一致,否则要用<resultMap>的<result>修改

    • Map集合:可以直接使用,键名要和参数占位符名称一致 ,否则要用<resultMap>的<result>修改

    • Collection:最终封装为Map集合,可以使用@Param注解,替换Map集合中默认的arg0键名

      map.put("collection",collection集合);

      map.put("arg0",collection集合);

    • List:最终封装为Map集合,可以使用@Param注解,替换Map集合中默认的arg0键名

      map.put("collection",List集合);

      map.put("list",List集合);

      map.put("arg0",List集合);

    • Array:最终封装为Map集合,可以使用@Param注解,替换Map集合中默认的arg0键名

      map.put("array",数组);

      map.put("arg0",数组);

    • 其他类型:直接使用,如int id,而且这类参数名称叫什么都无所谓,#{}中写什么都能接收

      <select id="selectById" resultType="user">
          select * from tb_user where id = #{xxx};
      </select>
      
  • 多个参数

    有多个参数时,MyBatis自动会将他们封装为Map集合

    User select(String username,String password);

    底层会这样封装

    map.put("arg0",参数值1);

    map.put("param1",参数值1);

    map.put("param2",参数值2);

    map.put("arg1",参数值2);

    如果不写@param注解可以使用arg或者param获取值

    <select id="select" resultType="user">
        select *
        from tb_user
        where
            username = #{arg0}
            and password = #{arg1}
        ;
    </select>
    

    而@param注解就是替换默认的arg键名

    User select(@param("username")String username,@param("password")String password);

    map.put("username",参数值1);

    map.put("param1",参数值1);

    map.put("param2",参数值2);

    map.put("password",参数值2);

    <select id="select" resultType="user">
        select *
        from tb_user
        where
            username = #{username}
            and password = #{password}
        ;
    </select>
    

MyBatis提供了ParamNamePesolver类来进行参数封装

总结:Collection、List、Array、多个参数要加@Param注解修改Map中的键名,并用修改后的名称来获取值,这样可读性更高

1.6 注解完成增删改查

使用注解开发会比配置文件开发更为方便

@Select("select * from tn_user where id = #{id}")
public User selectById(int id);
  • 查询:@Select
  • 添加:@Insert
  • 修改:@Update
  • 删除:@Delete

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。

注解开发一般完成比较简单的功能,而配置文件用来完成复杂功能

1.7 动态SQL

SQL语句会随着用户的输入和外部条件的变化而变化,我们称之为动态SQL

MyBatis对动态SQL有强大的支撑:

  • if:条件判断 test:逻辑表达式 <where>替换where关键字

    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        /*where 1 = 1*/
        <where>
        <if test="status != null">
            and status = #{status}
        </if>
        <if test="companyName != null and companyName != ''">
            and company_name like #{companyName}
        </if>
        <if test="brandName != null and brandName != ''">
            and brand_name like #{brandName}
        </if>
        </where>
    </select>
    
  • choose(when):选择,类似于Java中的switch语句

    • choose标签相当于switch
    • when标签相当于case
    • otherwise标签相当于default
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        /*where 1 = 1*/
        <where>
            <choose>
                <when test="status != null">
                    status = #{status}
                </when>
                <when test="companyName != null and companyName != ''">
                    company_name like #{companyName}
                </when>
                <when test="brandName != null and brandName != ''">
                    brand_name like #{companyName}
                </when>
                <!--<otherwise>
                    1 = 1
                </otherwise>-->
            </choose>
        </where>
    </select>
    

2. Spring

2.1 初识Spring

官网:https://spring.io

Spring发展到今天已经形成了一种开发生态圈,Spring提供了若干个项目,每个项目完成特定的功能

  • Spring家族
    • Spring Framework:Spring所有的技术都依赖它执行,是底层设计型的框架
    • Spring Boot:可以再Spring简化开发的基础上加速开发
    • Spring Cloud:分布式开发
  • Spring发展史
    • 1997年出现了EJB思想
    • 2004年Rod Johnson写了《J2EE Development without EJB》,Spring1.0问世,使用纯配置开发
    • 2006年Spring2.0 引入了注解功能
    • 2009年Spring3.0 已经演化成了可以不写配置的开发模式
    • 2013年Spring4.0 Spring紧跟JDK版本升级,对个别API进行了些许调整
    • 2017年,Spring到达了Spring5.0版本,已经全面支持JDK1.8

2.2 Spring Framework系统架构

Spring Framework是Spring生态圈最基础的项目,是其他项目的根基,其他所有的项目都在他的基础上运行

  • Spring Framework系统架构图

    image

    • ⭐Core Container:核心容器:对对象进行管理
    • AOP:面向切面编程:在不修改源码的情况下,对源码进行增强
    • Aspects:AOP思想实现
    • Data Access:数据访问
    • Data Integration:数据集成:支持Spring技术与其他技术整合使用(包容其他技术)
      • ⭐Transactions:提供了一种开发起来效率非常高的事务控制方案
    • Web:Web开发,在学习SpringMVC时进一步探讨
    • Test:单元测试与集成测试
  • Spring Framework学习路线

    • 核心容器Core Container
      • 核心概念(IOC/DI)
      • 容器基本操作
    • 数据访问/数据集成Data Access/Integration
      • 整合数据层技术MyBatis
    • 面向切面编程AOP、Aspects
      • 核心概念
      • AOP基础操作
      • AOP实用开发
    • 事务Transactions
      • 事务实用开发

2.3 核心概念

如今JavaWeb代码书写现状耦合度偏高

//业务层
public class BookServiceImpl implements BookService{
  	private BookDao bookDao = new BookDaoImpl();
  
  	public void save(){
    	bookDao.save();
  	}
}
//数据层
public class BookDaoImpl implements BookDao{
  	piblic void save(){
    	System.out.println("book dao save...");
  	}
}

当我们想要修改数据层代码

//数据层
public class BookDaoImpl2 implements BookDao{
  	piblic void save(){
    	System.out.println("book dao save...2");
  	}
}

这时业务层也需要修改,然后源码就要重新编译、测试...

解决方案

使用对象时,在程序中不要主动使用new产生对象,转为由外部提供对象,即IoC

  • IoC(Inversion of control):控制反转

    • 使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象的创建控制权由程序转移到外部,这种思想称为控制反转
  • Spring技术对IoC思想进行了实现

    • Spring提供了一个容器,称为IoC容器,用来充当IoC思想中的“外部
  • Spring框架中,由主动new产生对象转换为由Ioc容器提供对象

    //业务层
    public class BookServiceImpl implements BookService{
      	private BookDao bookDao;
      
      	public void save(){
        	bookDao.save();
      	}
    }
    
    //数据层
    public class BookDaoImpl implements BookDao{
      	piblic void save(){
        	System.out.println("book dao save...");
      	}
    }
    

    image

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

  • Service要依赖dao运行,IoC将会绑定两个对象,这种思想称作DI(Dependency Injection)依赖注入

    • 在容器中建立bean和bean之间的依赖关系的整个过程,称为依赖注入
  • IoC的目标:充分解耦

    • 使用IoC容器管理bean(IoC)
    • 在IoC容器内将有依赖关系的bean进行关系绑定(DI)
  • 最终效果:

    • 使用对象时不仅可以直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系

2.4 IoC入门案例(XML版)

案例基础:

image

其中两个实现类分别为

public class BookServiceImpl implements BookService {
    private BookDao bookDao = new BookDaoImpl();

    @Override
    public void save() {
        System.out.println("book service save...");
        bookDao.save();
    }
}
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("book dao save...");
    }
}
  • 思路分析

    • IoC管什么?
      • Bean(Service和Dao)
    • 如何将被管理的对象告知(交给)IoC容器?
      • 配置
    • 被管理的对象交给IoC容器,如何获取到IoC容器?
      • 接口
    • IoC容器得到后,如何从容器中获取Bean?
      • 接口方法
    • 使用Spring导入哪些坐标?
      • pom.xml
  • 代码实现

    • pom.xml引入Spring坐标

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.2.10.RELEASE</version>
      </dependency>
      
    • 在resource中创建配置文件applicationContext.xml

    • 配置Bean

      • bean表示配置bean
      • id属性表示给bean起名字
      • class属性表示给bean定义类型
      <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
      
      <bean id="bookService" class="com.mark.service.impl.BookServiceImpl"/>
      
    • 获取IOC容器

    • 获取Bean

    • 调用实现类的方法

      public class App2 {
          public static void main(String[] args) {
              //获取IoC容器
              ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
              //获取Bean
      //        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
      //        bookDao.save();
      
              BookService bookService = (BookService) ctx.getBean("bookService");
              bookService.save();
          }
      }
      
      //book service save...
      //book dao save...
      

2.5 DI入门案例

  • 思路分析

    • 基于IoC管理bean
    • Service中使用new形式创建的Dao对象是否保留?
    • Service中需要的Dao对象如何进入到Service中?
      • 提供方法
    • Service与Dao间的关系如何描述?
      • 配置
  • 案例实现

    • 删除业务层中使用new的方式创建的dao对象

      private BookDao bookDao;

    • 提供对应的set方法

      public void setBookDao(BookDao bookDao) {
          this.bookDao = bookDao;
      }
      
    • 配置Service与Dao的关系

      • property标签表示配置当前bean的属性
      • name属性表示配置实现类哪一个具体的属性
      • ref属性表示参照哪一个bean
      <bean id="bookService" class="com.mark.service.impl.BookServiceImpl">
          <property name="bookDao" ref="bookDao"/>
      </bean>
      

2.6 bean配置

  • bean基础配置

    类别 描述
    名称 bean
    类型 标签
    所属 beans标签
    功能 定义Spring核心容器管理的对象
    格式 <beans> <bean/> <bean> </bean> </beans>
    属性列表 id:Bean的id,使用容器可以通过id值获取对应的bean,在一个容器中id值唯一
    class:bean的类型,即配置bean的全路径类名
    范例 <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.mark.service.impl.BookServiceImpl"></bean>
  • bean别名配置

    • name标签 可以配置多个别名 用空格、逗号、或者分号隔开

      <bean id="bookDao" name="dao" class="com.mark.dao.impl.BookDaoImpl"/>
      
      <bean id="bookService" name="service service2 bookEbi" class="com.mark.service.impl.BookServiceImpl">
          <property name="bookDao" ref="bookDao"></property>
      </bean>
      
    • 在获取bean时可以使用别名获取,在注入依赖关系时也可以使用别名,但推荐使用id注入ref

      <bean id="bookService" name="service service2 bookEbi" class="com.mark.service.impl.BookServiceImpl">
          <property name="bookDao" ref="dao"></property>
      </bean>
      
      BookService bookService = (BookService) ctx.getBean("service");
      bookService.save();
      
    • 获取bean无论时通过id获取还是name获取,如果无法获取到,抛出异常:

      NoSuchBeanDefinitionException: No bean named 'service4' available

      这时要检查配置bean的名字和获取bean时的名字是否对应

  • bean作用范围配置

    public static void main(String[] args) {
        ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
    
        BookDao bookDao1 = (BookDao) ctx.getBean("dao");
        BookDao bookDao2 = (BookDao) ctx.getBean("dao");
        System.out.println(bookDao1);
        System.out.println(bookDao2);
    }
    
    //com.mark.dao.impl.BookDaoImpl@37918c79
    //com.mark.dao.impl.BookDaoImpl@37918c79
    

    可以发现Spring默认创建的bean是一个单例

    当我们想要创建非单例时就需要在bean标签中添加一个属性:scope

    • scope即作用范围

      • singleton:不写scope时的默认值
      • prototype:此时Spring造出的对象并不是同一个对象,即非单例对象
      <bean id="bookDao" name="dao" class="com.mark.dao.impl.BookDaoImpl" scope="prototype"/>
      
      public static void main(String[] args) {
          ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
      
          BookDao bookDao1 = (BookDao) ctx.getBean("dao");
          BookDao bookDao2 = (BookDao) ctx.getBean("dao");
          System.out.println(bookDao1);
          System.out.println(bookDao2);
      }
      //com.mark.dao.impl.BookDaoImpl@37918c79
      //com.mark.dao.impl.BookDaoImpl@78e94dcf
      
    • 为什么bean默认为单例?

      • 当默认不是单例,每次使用对象都会创建一个新的对象,这对内存会造成影响。而使用单例并不会对业务实现造成影响。
      • Spring就是管理那些可以复用的对象
    • 哪些bean适合造单例呢?

      • 表现层对象:如Servlet
      • 业务层对象:如Service
      • 数据层对象:如Dao
      • 工具对象
    • 不适合交给容器进行管理的bean:

      • 封装实体的域对象:如domain/pojo

2.7 bean的实例化

  • bean是如何创建的

    • bean本质上就是对象,创建bean使用构造方法完成

      private BookDaoImpl() {
          System.out.println("book dao constructor is running...");
      }
      

      在创建bookDao时会调用此构造方法,并且无论构造方法是私有还是公共的都可以调用成功

      但是,当构造方法有参数时,就无法调用成功。抛出异常从最后一个异常往上看

      NoSuchMethodException: com.mark.dao.impl.BookDaoImpl.<init>():没有无参构造方法

      Failed to instantiate:错误的实例化 BookDaoImpl]: No default constructor found:该类没有发现默认的构造方法

      BeanCreationException:创建Bean异常 Error creating bean with name 'bookDao' defined in class path resource [applicationContext.xml]: Instantiation of bean failed:错误地创建了一个在applicationContext.xml中定义的一个名叫bookDao的bean,实例化失败

  • 实例化bean的三种方式

    • ⭐使用构造方法实例化bean(常用)

      • 提供可访问的构造方法(写不写都可以)

        public class BookDaoImpl implements BookDao {
        
            /*
            private BookDaoImpl() {
                System.out.println("book dao constructor is running...");
            }
            */
        
            @Override
            public void save() {
                System.out.println("book dao save ...");
            }
        }
        
      • 配置

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

      • 无参构造方法如果不存在,将排除异常BeanCreationException

    • 静态工厂实例化bean(一般用于早期遗留的系统,了解即可)

      • 静态工厂

        public static OrderDao getOrderDao(){
            System.out.println("factory setup...");
            return new OrderDaoImpl();
        }
        
      • 配置

        <!--class配置工程类名 添加factory-method,配置工厂里造对象的方法名-->
        <bean id="orderDao" class="com.mark.factory.OrderDaoFactory" factory-method="getOrderDao"/>
        
    • 实例工厂初始化bean(了解)

      • 实例工厂

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

        <bean id="userFactory" class="com.mark.factory.UserDaoFactory"/>
        
        <bean id="userDao" factory-bean="userFactory" factory-method="getUserDao"/>
        
    • ⭐使用FactoryBean实例化bean(第三种初始化的变种、实用

      • 创建FactoryBean

        public class UserDaoFactoryBean implements FactoryBean<UserDao> {
            /**
             * 代替原始实例工厂中创建对象的方法
             * @return new 对象
             * @throws Exception
             */
            @Override
            public UserDao getObject() throws Exception {
                return new UserDaoImpl();
            }
        
            /**
             * 对象的类型
             * @return 对象.class
             */
            @Override
            public Class<?> getObjectType() {
                return UserDao.class;
            }
        
            /**
             * 设置非单例
             * @return false
             */
            @Override
            public boolean isSingleton() {
                return false;
            }
        }
        
        
      • 配置

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

2.8 bean的生命周期控制

  • 生命周期:从创建到消亡的完整过程

  • bean生命周期:bean从创建到销毁的整体过程

    • 初始化容器
      1. 创建对象(内存分配)
      2. 执行构造方法
      3. 执行属性注入(set操作)
      4. 执行bean初始化方法
    • 使用bean
      • 执行业务操作
    • 关系/销毁容器
      • 执行bean销毁方法
  • bean生命周期控制:在bean创建后到销毁前做一些事情

  • 如何配置bean生命周期的方法?方式一:配置方式

    • 在实现类创建两个方法

      public class BookDaoImpl implements BookDao {
          @Override
          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.mark.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>
      
  • 在程序运行结束后就会关闭虚拟机,导致销毁的方法并没有机会执行

    • 在虚拟机退出之前,关闭IoC容器(暴力方式)

      public class AppForLifeCycle {
          public static void main( String[] args ) {
      //        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
              ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
      
              BookDao bookDao = (BookDao) ctx.getBean("bookDao");
      
              bookDao.save();
      
              ctx.close();
          }
      }
      
    • 设置关闭钩子

      public class AppForLifeCycle {
          public static void main(String[] args) {
              ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
      
              //注册关闭钩子(任何时刻都可以)
              ctx.registerShutdownHook();
      
              BookDao bookDao = (BookDao) ctx.getBean("bookDao");
      
              bookDao.save();
      
              //ctx.close();
          }
      }
      
  • 使用接口配置bean生命周期的方法:不需要配置初始化和销毁方法

    public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
        private BookDao bookDao;
    
        public void setBookDao(BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        @Override
        public void save() {
            System.out.println("book service save ...");
            bookDao.save();
        }
    
        /**
         * bean销毁前执行
         * @throws Exception
         */
        @Override
        public void destroy() throws Exception {
            System.out.println("Service destory...");
        }
    
        /**
         * 在属性设置完之后初始化bean
         * @throws Exception
         */
        @Override
        public void afterPropertiesSet() throws Exception {
            System.out.println("Service init...");
        }
    }
    

2.9 依赖注入方式

  • 思考:

    • 向一个类中传递数据的方式有几种?
      • 普通方法(set方法)
      • 构造方法
    • 依赖注入描述了在容器中建立bean与bean之间依赖关系的过程,如果bean运行需要的是数字或字符串呢?
      • 引用类型
      • 简单类型(基本数据类型与String)
  • 依赖注入方式

    • setter注入

      • 简单类型

        • 在bean中定义简单类型属性并提供可访问的set方法

          public class BookDaoImpl implements BookDao {
          
              private int connectionNum;
              private String dataBaseName;
          
              public void setConnectionNum(int connectionNum) {
                  this.connectionNum = connectionNum;
              }
          
              public void setDataBaseName(String dataBaseName) {
                  this.dataBaseName = dataBaseName;
              }
          
              @Override
              public void save() {
                  System.out.println("book dao save ..."+connectionNum+","+dataBaseName);
              }
          }
          
        • 配置中使用property标签value属性注入简单类型数据

          <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl">
              <property name="connectionNum" value="500"></property>
              <property name="dataBaseName" value="mysql"></property>
          </bean>
          
      • 引用类型

        • 在bean中定义引用类型属性并提供可访问的set方法

          public class BookServiceImpl implements BookService {
              private BookDao bookDao;
          
              @Override
              public void save() {
                  System.out.println("book service save...");
                  bookDao.save();
              }
          
              public void setBookDao(BookDao bookDao) {
                  this.bookDao = bookDao;
              }
          }
          
        • 配置中使用property标签的ref属性注入引用类型对象

          <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
          
          <bean id="bookService" class="com.mark.service.impl.BookServiceImpl">
              <property name="bookDao" ref="bookDao"></property>
          </bean>
          
    • 构造器注入

      • 简单类型

        • 在bean中定义简单类型属性并提供可访问的构造方法

          public class BookDaoImpl implements BookDao {
              private int connectionNum;
              private String dataBaseName;
          
              public BookDaoImpl(int connectionNum, String dataBaseName) {
                  this.connectionNum = connectionNum;
                  this.dataBaseName = dataBaseName;
              }
          
              @Override
              public void save() {
                  System.out.println("book dao save ...");
              }
          }
          
        • 配置中使用constructor-arg标签value属性注入简单类型数据

          <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl">
              <constructor-arg name="connectionNum" value="500"></constructor-arg>
              <constructor-arg name="dataBaseName" value="mysql"></constructor-arg>
          </bean>
          
          • 解决形参会变名的问题:与形参名不耦合

            <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl">
                <constructor-arg type="int" value="500"></constructor-arg>
                <constructor-arg type="java.lang.String" value="mysql"></constructor-arg>
            </bean>
            
          • 根据参数位置匹配注入简单类型数据:解决参数的类型重复问题

            <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl">
                <constructor-arg index="0" value="500"></constructor-arg>
                <constructor-arg index="1" value="mysql"></constructor-arg>
            </bean>
            
      • 引用类型

        • 在bean中定义引用类型属性并提供可访问的构造方法

          public class BookServiceImpl implements BookService{
              private BookDao bookDao;
              private UserDao userDao;
          
              public BookServiceImpl(BookDao bookDao, UserDao userDao) {
                  this.bookDao = bookDao;
                  this.userDao = userDao;
              }
          
              @Override
              public void save() {
                  System.out.println("book service save ...");
                  bookDao.save();
                  userDao.save();
              }
          }
          
        • 配置中使用constructor-arg标签的ref属性注入引用类型对象

          <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"></bean>
          
          <bean id="userDao" class="com.mark.dao.impl.UserDaoImpl"></bean>
          
          <bean id="bookService" class="com.mark.service.impl.BookServiceImpl">
              <!--这里的name是构造方法的形参名-->
              <constructor-arg name="bookDao" ref="bookDao"></constructor-arg>
              <constructor-arg name="userDao" ref="userDao"></constructor-arg>
          </bean>
          
  • 两种依赖注入方式的选择

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

2.10 依赖自动装配

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

  • 自动装配方式

    • 按类型(常用)

      <bean class="com.mark.dao.impl.BookDaoImpl"/>
      
      <bean id="bookService" class="com.mark.service.impl.BookServiceImpl" autowire="byType"/>
      

      前提是需要提供set方法

    • 按名称

      <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
      
      <!--使用bean中set方法的属性名和配置中bean的id名进行匹配-->
      <bean id="bookService" class="com.mark.service.impl.BookServiceImpl" autowire="byName"/>
      
    • 按构造方法

    • 不启用自动装配

  • 依赖自动装配特征

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

2.11 集合注入

  • 数组

    <property name="array">
        <array>
            <value>100</value>
            <value>120</value>
            <value>140</value>
        </array>
    </property>
    
  • List

    <property name="list">
        <list>
            <value>王昭君</value>
            <value>嫦娥</value>
            <value>李元芳</value>
        </list>
    </property>
    
  • Set

    <property name="set">
        <set>
            <value>王昭君</value>
            <value>王昭君</value>
            <value>嫦娥</value>
            <value>嫦娥</value>
        </set>
    </property>
    
  • Map

    <property name="map">
        <map>
            <entry key="country" value="China"/>
            <entry key="province" value="Shaanxi"/>
            <entry key="city" value="Xian"/>
        </map>
    </property>
    
  • Properties

    <property name="properties">
        <props>
            <prop key="country">China</prop>
            <prop key="province">Shaanxi</prop>
            <prop key="city">Baoji</prop>
        </props>
    </property>
    

2.12 案例:数据源对象管理

  • 第三方资源配置管理

    • 导入druid坐标

      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.16</version>
      </dependency>
      
    • 配置数据源对象作为spring管理的bean

      <bean class="com.alibaba.druid.pool.DruidDataSource">
          <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
          <property name="url" value="jdbc:mysql:////spring_db"/>
          <property name="username" value="root"/>
          <property name="password" value="123456"/>
      </bean>
      

2.13 加载properties文件

  1. 开启context命名空间

    <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"
           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
                ">
    </beans>
    
  2. 使用context空间加载properties文件

    <context:property-placeholder location="jdbc.properties"/>
    
  3. 使用属性占位符${}读取文件中的属性

    <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>
    
  • 注意:

    • 当配置文件的属性名与系统的环境变量值相同时,获取到的是系统变量值

      <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl">
          <property name="name" value="${username}"/>
      </bean>
      

      上述配置读出的username一定是电脑用户名

      因此想要避免读取系统变量值,可以在加载文件时添加system-properties-mode属性

      <context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
      
    • 当要加载多个配置文件时可以使用逗号隔开,也可以使用*.properties加载全部properties文件

      <context:property-placeholder location="jdbc.properties,jdbc2.properties" system-properties-mode="NEVER"/>
      
      <context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
      

      标准格式

      <!--添加classpath只读取当前模块的文件-->
      <context:property-placeholder location="classpath*.properties" system-properties-mode="NEVER"/>
      

      从类路径或jar包中搜索并加载properties文件:

      <context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
      

2.14 容器

  • 创建容器

    1. 加载类路径下的配置文件

      ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
      
    2. 从文件系统下加载配置文件

      ApplicationContext ctx2 = new FileSystemXmlApplicationContext("D:\\Code\\IJavaMySpring\\Spring_10_Container\\src\\main\\resources\\applicationContext.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);
      

      对应的Bean只能有一个

  • 容器类层次结构

    image

    image

  • BeanFactory

    public class AppForBeanFactory {
        public static void main(String[] args) {
            Resource resources = new ClassPathResource("applicationContext.xml");
            BeanFactory bf = new XmlBeanFactory(resources);
            BookDao bookDao = bf.getBean(BookDao.class);
            bookDao.save();
        }
    }
    

    BeanFactory完毕后,所有的bean均为延迟加载,而ApplicationContext是全部立即加载

2.15 注解开发

  • 注解开发定义bean

    • 使用@Component定义bean

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

      Component是组件的意思,该注解可以代替配置

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

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

      <!--扫描com.mark.dao.impl下的组件-->
      <context:component-scan base-package="com.mark.dao.impl"/>
      
  • Spring提供了@Component注解的三个衍生注解

    • @Controller:用于表现层bean定义

    • @Service:用于业务层bean定义

      @Service
      public class BookServiceImpl implements BookService {
        	
      }
      
    • @Repository:用于数据层bean定义

      @Repository("bookDao")
      //Repository是仓库的意思,这里译为数据仓库
      public class BookDaoImpl implements BookDao {
        
      }
      
    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            BookDao bookDao = (BookDao) ctx.getBean("bookDao");
            System.out.println(bookDao);
            BookService bookService = ctx.getBean(BookService.class);
            System.out.println(bookService);
        }
    }
    //com.mark.dao.impl.BookDaoImpl@459e9125
    //com.mark.service.impl.BookServiceImpl@692f203f
    
  • 纯注解开发

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

    • 在config包下创建类SpringConfig添加注解@Configuration,用于设定当前类为配置类,这里的@Configuration就代替了配置文件的

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:context="http://www.springframework.org/schema/context"
             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
                  http://www.springframework.org/schema/context
                  http://www.springframework.org/schema/context/spring-context.xsd
          ">
      </beans>
      
    • 添加注解@ComponentScan("com.mark"),用于设定扫描路径,此注解只能添加一次,多个数据需要用数组格式,这个注解就代替了

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

      @Configuration
      //@ComponentScan("com.mark")
      @ComponentScan({"com.mark.dao","com.mark.service"})
      public class SpringConfig {
      
      }
      
    • 创建容器时使用注解配置ctx:AnnotationConfigApplicationContext

      public class AppForAnnotation {
          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);
          }
      }
      

2.16 bean管理

  • bean作用范围

    • 注解@Scope
      • singleton:单例模式
      • prototype:非单例模式
  • bean生命周期

    • @PostConstruct注解:初始化方法
    • @PreDestroy注解:摧毁方法

    两个注解添加前要增加jsr-api坐标

    @Repository
    @Scope("singleton")
    public class BookDaoImpl implements BookDao {
        @Override
        public void save() {
            System.out.println("book dao save ...");
        }
    
        @PostConstruct
        public void init() {
            System.out.println("book dao init ...");
        }
    
        @PreDestroy
        public void destory() {
            System.out.println("book dao destory ...");
        }
    }
    

2.17 依赖注入

  • 自动装配

    • 在bean中添加@Autowired注解

      @Service
      public class BookServiceImpl implements BookService {
          @Autowired
          private BookDao bookDao;
      
        	/*
          public void setBookDao(BookDao bookDao) {
              this.bookDao = bookDao;
          }
          */
      
          @Override
          public void save() {
              System.out.println("book service save ...");
              bookDao.save();
          }
      }
      

      @Autowired注解根据类型装配,使用暴力反射注入,即使没有set方法依旧可以成功

    • 当有两个相同类型的类时,就不能用类型装配,需要使用名字注入,使用注解@Qualifier,而且@Autowired不可以去掉,@Qualifier无法单独使用

      @Autowired
      @Qualifier("bookDao2")
      private BookDao bookDao;
      
      public void setBookDao(BookDao bookDao) {
          this.bookDao = bookDao;
      }
      
      @Override
      public void save() {
          System.out.println("book service save ...");
          bookDao.save();
      }
      
      
    • 使用@Value实现简单类型注入

      @Repository("bookDao")
      public class BookDaoImpl implements BookDao {
          @Value("Mark")
          private String name;
      
          @Override
          public void save() {
              System.out.println("book dao save ..."+name);
          }
      }
      
    • 将配置文件的数据注入简单类型

      • 在配置类添加注解@PropertySource

        @Configuration
        @ComponentScan("com.mark")
        @PropertySource("jdbc.properties")
        public class SpringConfig {
        
        }
        
      • 获取值直接使用占位符${},里面写属性名即可

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

      如果配置文件有多个,可以使用数组形式:

      @PropertySource("{jdbc.properties,jdbc.propertiesjdbc.properties}")

      这里不支持使用通配符:*.properties

2.18 第三方bean管理

  • 使用@Bean配置第三方bean

    @Configuration
    public class SpringConfig {
        //1.定义一个方法,获得要管理的对象
        //2.添加@Bean,表示当前方法的返回值是一个bean
    
        @Bean
        public DataSource dataSource(){
            DruidDataSource ds =new DruidDataSource();
    
            ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
            ds.setUrl("jdbc:mysql:///spring_db");
            ds.setUsername("root");
            ds.setPassword("123");
    
            return ds;
        }
    }
    
    • 不建议将该@Bean写到SpringConfig中,应当使用第三方的配置类管理第三方bean

    • 方式一:导入式

      public class JDBCConfig {
          @Bean
          public DataSource dataSource(){
              DruidDataSource ds =new DruidDataSource();
      
              ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
              ds.setUrl("jdbc:mysql:///spring_db");
              ds.setUsername("root");
              ds.setPassword("123");
      
              return ds;
          }
      }
      
      • 使用@Import注解手动加入配置到核心配置,此注解只添加一次,多个配置类要用数组格式

        @Configuration
        @Import(JDBCConfig.class)
        public class SpringConfig {
            
        }
        
    • 方式二:扫描式(不推荐)

      • @Configuration
        public class JDBCConfig {
            @Bean
            public DataSource dataSource(){
                DruidDataSource ds =new DruidDataSource();
        
                ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
                ds.setUrl("jdbc:mysql:///spring_db");
                ds.setUsername("root");
                ds.setPassword("123");
        
                return ds;
            }
        }
        
      • 使用@ComponentScan注解扫描配置类所在的包,加载对应的配置类信息

        @Configuration
        @ComponentScan("com.mark.config")
        public class SpringConfig {
           
        }
        
  • 为第三方bean注入资源

    • 简单类型依赖注入

      public class JDBCConfig {
          @Value("com.mysql.cj.jdbc.Driver")
          private String driver;
          @Value("jdbc:mysql:///spring_db")
          private String url;
          @Value("root")
          private String username;
          @Value("123")
          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;
          }
      }
      
    • 引用类型注入

      public class JDBCConfig {
          @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;
          }
      }
      
      • 引用类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象

2.19 XML配置和注解配置对比

image

2.20 Spring整合MyBatis

  • MyBatis程序核心对象分析

    image

    image

  • 步骤:

    • 导入所需坐标

      <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>8.0.29</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>
      
    • 添加MyBatisConfig配置类

      public class MyBatisConfig {
          @Bean
          public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
              SqlSessionFactoryBean ssfb =new SqlSessionFactoryBean();
              ssfb.setTypeAliasesPackage("com.mark.domain");
              ssfb.setDataSource(dataSource);
              return ssfb;
          }
      
          @Bean
          public MapperScannerConfigurer mapperScannerConfigurer(){
              MapperScannerConfigurer msc =new MapperScannerConfigurer();
              msc.setBasePackage("com.mark.dao");
              return msc;
          }
      }
      

      第一个bean代替了

      <typeAliases>
          <package name="com.itheima.domain"/>
      </typeAliases>
      <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>
      

      第二个bean代替了

      <mappers>
          <package name="com.itheima.dao"></package>
      </mappers>
      

2.21 Spring整合Junit

  • 添加依赖

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    
  • 给测试类添加注解

    //使用Spring整合Junit专用的类加载器
    @RunWith(SpringJUnit4ClassRunner.class)
    //指定Spring的配置
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountServiceTest {
        @Autowired
        private AccountService accountService;
    
        @Test
        public void testFindById(){
            System.out.println(accountService.findById(2));
        }
    }
    

2.22 AOP简介

  • AOP核心概念

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

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

  • AOP作用

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

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

    image

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

2.23 AOP入门案例

要求:在接口执行前输出当前系统时间

开发模式:XML or 注解

步骤:

  • 导入坐标

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
      	<!--aspect用于实现AOP切面-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
    </dependencies>
    
  • 制作连接点方法(原始操作,Dao接口与实现类)

    @Repository
    public class BookDaoImpl implements BookDao {
    
        @Override
        public void save() {
            System.out.println(System.currentTimeMillis());
            System.out.println("book dao save ...");
        }
    
        @Override
        public void update(){
            System.out.println("book dao update ...");
        }
    }
    
  • 制作共性功能(通知类与通知)

  • 定义切入点

    • 切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
  • 绑定切入点与通知关系(切面)

    //通知类必须配置成Spring管理的bean
    @Component
    //1.告诉Spring,MyAdvice是用来做AOP的
    @Aspect
    public class MyAdvice {
        //3.定义切入点
        @Pointcut("execution(void com.mark.dao.BookDao.update())")
        private void pt(){}
    
        //4.绑定切入点和通知
        @Before("pt()")
        //2.定义通知
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
    
  • Spring核心配置中标明使用注解开发AOP

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

2.24 AOP工作流程

  • Spring容器启动

  • 读取所有切面配置中的切入点,即已经在@Before中表明的切入点

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

    • 匹配失败,创建对象
    • 匹配成功,创建原始对象(目标对象)的代理对象
  • 获取bean执行方法

    • 获取bean,调用方法并执行,完成操作
    • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的

  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

2.25 AOP切入点表达式

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

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

  • 切入点表达式描述方式:

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

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

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

      execution(void com.mark.dao.BookDaoImpl.update())

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

    • 动作关键字:描述切入点的行为动作,例如execution表示执行到指定的切入点
    • 访问修饰符:public,private等,可省略
    • 返回值
    • 包名
    • 类/接口名
    • 方法名
    • 参数
    • 异常名:方法定义中抛出指定异常,可以省略
  • 通配符

    可以使用通配符描述切入点,快速描述

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

      @Pointcut("execution( * com.mark.*.BookDao.find*(*))")

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

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

      @Pointcut("execution(public User com..BookDao.findById(..))")

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

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

      @Pointcut("execution(* *..*Service+.*(..))")

      表示匹配任意返回值任意包下的以Service结尾的类或者接口的子类的任意参数的任意方法

  • 书写技巧

    • 所有代码按照标准规范开发,否则以下技巧全部失效

    • 描述切入点通常描述接口,而不描述实现类

    • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述

    • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述

    • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配

    • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名

    • 方法名书写以动词进行精准匹配名词采用*匹配,例如getByld书写成getBy*,selectAll书写成selectAll

    • 参数规则较为复杂,根据业务方法灵活调整

    • 通常不使用异常作为匹配规则

      @Pointcut("execution(* com.mark.*.*Service.find*(..))")

2.26 AOP通知类型

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

AOP通知共分为5种类型

  • 前置通知

    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    
  • 后置通知

    @After("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    
  • 环绕通知(重点、常用)

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

    @Around注意事项

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

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

    只有方法没有抛出异常正常结束时才会运行

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

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

    只有方法抛出异常后才会运行

2.27 案例:测控业务层接口万次执行效率

  • 需求:任意业务层接口执行均可显示其执行效率(执行时长)

  • 分析:

    • 业务功能:业务层接口执行前后分别记录时间
    • 通知类型选择前后均可增强的类型:环绕通知
  • 实现:

    @Component
    @Aspect
    public class AppAdvice {
        //匹配业务层所有方法
        @Pointcut("execution(* com.mark.service.*Service.*(..))")
        private void servicePt() {
        }
    
        @Around("AppAdvice.servicePt()")
        public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
            //代表一次执行的签名信息
            Signature signature = pjp.getSignature();
            //类型
            //System.out.println(signature.getDeclaringType());
            //类型名
            //System.out.println(signature.getDeclaringTypeName());
            //方法名
            //System.out.println(signature.getName());
            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");
        }
    }
    ​```
    
    

2.28 AOP通知获取数据

  • 获取切入点方法的参数

    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知

      @Before("pt()")
      public void before(JoinPoint jp) {
          Object[] args = jp.getArgs();
          System.out.println(Arrays.toString(args));
          System.out.println("before advice ..." );
      }
      
      @After("pt()")
      public void after(JoinPoint jp) {
          Object[] args = jp.getArgs();
          System.out.println(Arrays.toString(args));
          System.out.println("after advice ...");
      }
      
      
    • ProceedJointPoint:适用于环绕通知

      @Around("pt()")
      public Object around(ProceedingJoinPoint pjp) throws Throwable {
          Object[] args = pjp.getArgs();
          args[0]=666;
          //System.out.println(Arrays.toString(args));
          Object ret = pjp.proceed(args);
          return ret;
      }
      
  • 获取切入点方法的返回值

    • 返回后通知

      //ret作为返回值 returning要和形参对应
      @AfterReturning(value = "pt()",returning = "ret")
      public void afterReturning(Object ret) {
          System.out.println("afterReturning advice ..."+ret);
      }
      

      如果要获取参数添加JoinPoint,JoinPoint jp必须在前

      @AfterReturning(value = "pt()",returning = "ret")
      public void afterReturning(JoinPoint jp, Object ret) {
          Object[] args = jp.getArgs();
          System.out.println(Arrays.toString(args));
          System.out.println("afterReturning advice ..."+ret);
      }
      
    • 环绕通知

      @Around("pt()")
      public Object around(ProceedingJoinPoint pjp) throws Throwable {
          Object ret = pjp.proceed();
          return ret;
      }
      
  • 获取切入点方法的异常

    • 抛出异常后通知

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

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

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

  • 需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理

  • 分析:

    • 在业务方法执行之前对所有的输入参数进行格式处理:trim()
  • 实现:

    @Component
    @Aspect
    public class DataAdvice {
        @Pointcut("execution(boolean com.mark.service.ResourcesService.openURL(*,*))")
        private void servicePt() {
        }
    
        @Around("DataAdvice.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;
        }
    }
    
    

2.29 Spring事务简介

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

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

image

  • 案例:模拟银行账户间转账业务

    • 需求:实现任意两个账户间转账操作

    • 需求微缩:A账户减钱、B账户加钱

    • 分析:

      • 数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
      • 业务层提供转账操作(transfer),调用减钱与加钱的操作
      • 提供两个账号和操作金额执行转账操作
      • 基于Spring整合MyBatis环境搭建上述操作
    • 结果分析:

      • 程序正常执行时,账户金额A减B加
      • 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
    • 实现:

      • 给要添加事务的实现类的接口添加注解@Transactional

        @Transactional
        public void transfer(String out,String in ,Double money) ;
        

        Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合

        注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务

      • 在JDBC配置类中添加事务管理器配置PlatformTransactionManager

        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource){
            DataSourceTransactionManager ptm = new DataSourceTransactionManager();
            ptm.setDataSource(dataSource);
            return ptm;
        }
        
      • 在Spring配置类中打开使用注解式事务驱动@EnableTransactionManagement

        @Configuration
        @ComponentScan("com.mark")
        @PropertySource("classpath:jdbc.properties")
        @Import({JdbcConfig.class,MybatisConfig.class})
        @EnableTransactionManagement
        public class SpringConfig {
        }
        

2.30 Spring事务角色

image

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

2.31 事务相关配置

  • 事务配置

    image

    //开启只读、永不超时
    @Transactional(readOnly = true,timeout = -1)
    public void transfer(String out,String in ,Double money) ;
    

    除了运行时异常和Error系异常会回滚,除此之外都不回滚,即便出现异常也不回滚,如IOException()

    为了保证正确回滚,需要添加属性rollbackFor

    @Transactional(rollbackFor = {IOException.class})
    public void transfer(String out,String in ,Double money) throws IOException;
    
  • 案例:转账业务追加日志

    • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕

    • 需求微缩:A账户减钱、B账户加钱,数据库记录日志

    • 分析:

      • 基于转账操作案例添加日志模块,实现数据库中记录日志

      • 业务层转账操作(transfer),调用减钱、加钱与记录日志功能

        public interface LogDao {
            @Insert("insert into tnl_log (info,createDate) values(#{info},now())")
            void log(String info);
        }
        
        public interface LogService {
        
            @Transactional
            void log(String out, String in, Double money);
        }
        
        @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);
            }
        }
        
    • 实现效果预期:

      • 无论转账操作是否成功,均进行转账操作的日志留痕

        @Override
        public void transfer(String out,String in ,Double money) throws IOException{
            try {
                accountDao.outMoney(out,money);
                int i = 1/0;
                accountDao.inMoney(in,money);
            }finally {
                logService.log(out,in,money);
            }
        }
        
    • 存在的问题

      • 日志的记录与转账操作隶属于同一个事务,同成功同失败
    • 实现效果预期改进:

      • 无论转账操作是否成功,日志必须保留
  • 事务传播行为

    事务协调员对事务管理员所携带事务的处理态度

    image

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

      public interface LogService {
          @Transactional(propagation = Propagation.REQUIRES_NEW)
          void log(String out, String in, Double money);
      }
      

    几种事务传播行为:

    image

posted @   风吹头蛋凉OvO  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示