SSM

Mybatis

1.1 Mybatis概述

1.1.1 Mybatis概念

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

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

  • 官网:https://mybatis.org/mybatis-3/zh/index.html

持久层:

  • 负责将数据保存到数据库的那一层代码。

    会将操作数据库的Java代码作为持久层。而Mybatis就是对jdbc代码进行了封装。

  • JavaEE三层架构:表现层、业务层、持久层

数据持久化

  • 持久化就是将程序的数九在持久状态和瞬间状态转化的过程

框架:

  • 框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型
  • 在框架的基础之上构建软件编写更加高效、规范、通用、可扩展

优点

  • 简单易学
  • 灵活
  • sql和代码的分离,提高了可维护性
  • 提供映射标签,支持对象与数据库的orm字段关系映射
  • 提供对象关系映射标签,支持对象关系组建维护
  • 提供xml标签,支持编写动态sql

1.1.2 JDBC 缺点

下面是 JDBC 代码,它存在几个缺点:

image-20210726203656847
  • 硬编码

    • 注册驱动、获取连接

      上图标1的代码有很多字符串,而这些是连接数据库的四个基本信息,以后如果要将Mysql数据库换成其他的关系型数据库的话,这四个地方都需要修改,如果放在此处就意味着要修改源代码。

    • SQL语句

      上图标2的代码。如果表结构发生变化,SQL语句就要进行更改。这也不方便后期的维护。

  • 操作繁琐

    • 手动设置参数

    • 手动封装结果集

      上图标4的代码是对查询到的数据进行封装,而这部分代码是没有什么技术含量,而且特别耗费时间的。

1.1.3 Mybatis 优化

  • 硬编码可以配置到配置文件
  • 操作繁琐的地方mybatis都自动完成

如图所示

image-20210726204849309

1.2 Mybatis快速入门

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

1.2.1 环境准备

  • 创建一个maven项目,并创建实体类,在 com.duan.pojo 包下创建 Brand类

    public class Brand {
        // id 主键
        private Integer id;
        // 品牌名称
        private String brandName;
        // 企业名称
        private String companyName;
        // 排序字段
        private Integer ordered;
        // 描述信息
        private String description;
        // 状态:0:禁用  1:启用
        private Integer status;
        
        //省略了 setter 和 getter
    }
    
  • 创建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', '男', '西安');
    
  • 导入依赖

    在创建好的模块中的 pom.xml 配置文件中添加依赖的坐标

        <dependencies>
            <!--mybatis 依赖-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.6</version>
            </dependency>
    
            <!--mysql 驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.33</version>
            </dependency>
    
            <!--junit 单元测试-->
            <!-- https://mvnrepository.com/artifact/junit/junit -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
        
        <build>
            <resources>
                <resource>
                    <directory>src/main/resources</directory>
                    <includes>
                        <include>**/*.properties</include>
                        <include>**/*.xml</include>
                    </includes>
                    <filtering>true</filtering>
                </resource>
                <resource>
                    <directory>src/main/java</directory>
                    <includes>
                        <include>**/*.properties</include>
                        <include>**/*.xml</include>
                    </includes>
                    <filtering>true</filtering>
                </resource>
            </resources>
        </build>
    </project>
    
  • 编写 MyBatis 核心配置文件 -- > 替换连接信息 解决硬编码问题

    在模块下的 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>
    
        <!--类型别名-->
        <typeAliases>
            <package name="com.duan.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://localhost:3306/mybatis?serverTimezone=Asia/Shanghai"/>
                    <property name="username" value="root"/>
                    <property name="password" value="1234"/>
                </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">
    
    <mapper namespace="com.duan.dao.BrandDao">  
        <select id="getAllBrand" resultType="com.duan.pojo.Brand">
            select * from tb_brand;
        </select>
    </mapper>
    
  • 安装 MyBatisX 插件

    • MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。

    • 主要功能

      • XML映射配置文件 和 接口方法 间相互跳转
      • 根据接口方法生成 statement
    • 安装方式

      点击 file ,选择 settings ,就能看到如下图所示界面

      image-20210729113304743

      注意:安装完毕后需要重启IDEA

    • 插件效果

      红色头绳的表示映射配置文件,蓝色头绳的表示mapper接口。在mapper接口点击红色头绳的小鸟图标会自动跳转到对应的映射配置文件,在映射配置文件中点击蓝色头绳的小鸟图标会自动跳转到对应的mapper接口。也可以在mapper接口中定义方法,自动生成映射配置文件中的 statement

  • 编写BrandDaoUtils工具类

    private static SqlSessionFactory sqlSessionFactory;
        private static InputStream inputStream;
    
         static {
             try {
                 String resource = "mybatis-config.xml";
                 inputStream = Resources.getResourceAsStream(resource);
                 sqlSessionFactory =  new SqlSessionFactoryBuilder().build(inputStream);
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     inputStream.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
    
         }
    
         public static SqlSession getSqlSession(){
             return sqlSessionFactory.openSession();
         }
    
  • 解决SQL映射文件的警告提示:

  • 在入门案例映射配置文件中存在报红的情况。

    • 产生的原因:Idea和数据库没有建立连接,不识别表信息。但是它并不影响程序的执行。

    • 解决方式:在Idea中配置MySQL数据库连接。

1.2.2 查询所有数据

  • 编写测试类testGetAllBrand
@Test
     void testGetAllBrand(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);
        List<Brand> allBrand = mapper.getAllBrand();
        System.out.println(allBrand);
        sqlSession.close();
    }

执行测试方法结果如下:image-20230615155952237

从上面结果看到了问题,有些数据封装成功了,而有些数据并没有封装成功。为什么这样呢?

这个问题可以通过两种方式进行解决(详情见1.3.5)

  • 给字段起别名
  • 使用resultMap定义字段和属性的映射关系

1.2.3 查询单条数据

有些数据的属性比较多,在页面表格中无法全部实现,而只会显示部分,而其他属性数据的查询可以通过 查看详情 来进行查询。

查看详情功能实现步骤:

  • 编写接口方法:Mapper接口

    image-20210729180604529
    • 参数:id

      查看详情就是查询某一行数据,所以需要根据id进行查询。而id以后是由页面传递过来。

    • 结果:Brand

      根据id查询出来的数据只要一条,而将一条数据封装成一个Brand对象即可

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

    image-20210729180709318
  • 执行方法、进行测试

  • 编写接口方法

BrandMapper 接口中定义根据id查询数据的方法

/**
  * 查看详情:根据Id查询
  */
Brand getById(int id);
  • 编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

<select id="getById"  resultMap="brandResultMap">
    select *
    from tb_brand where id = #{id};
</select>
  • 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

@Test
    public void testGetById(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);
        Brand byId = mapper.getById(1);
        System.out.println(byId);
        sqlSession.close();
    }

执行测试方法结果如下:

Brand


  • 参数占位符

查询到的结果很好理解就是id为1的这行数据。而这里我们需要看控制台显示的SQL语句,能看到使用?进行占位。说明在映射配置文件中的写的 #{id} 最终会被?进行占位。

mybatis提供了两种参数占位符:

  • #{} :执行SQL时,会将 #{} 占位符替换为?,将来自动设置参数值。从上述例子可以看出使用#{} 底层使用的是 PreparedStatement

  • ${} :拼接SQL。底层使用的是 Statement,会存在SQL注入问题。如下图将 映射配置文件中的 #{} 替换成 ${} 来看效果

    <select id="selectById"  resultMap="brandResultMap">
        select *
        from tb_brand where id = ${id};
    </select>
    

    重新运行查看结果如下:

    image-20210729184156019

注意:从上面两个例子可以看出,以后开发要使用 #{} 参数占位符。

  • parameterType使用

对于有参数的mapper接口方法,在映射配置文件中应该配置 ParameterType 来指定参数类型。只不过该属性都可以省略。

<select id="selectById" parameterType="int" resultMap="brandResultMap">
    select *
    from tb_brand where id = ${id};
</select>
  • SQL语句中特殊字段处理

以后肯定会在SQL语句中写一下特殊字符,比如某一个字段大于某个值,如下图

image-20210729184756094

可以看出报错了,因为映射配置文件是xml类型的问题,而 > < 等这些字符在xml中有特殊含义,所以此时我们需要将这些符号进行转义,可以使用以下两种方式进行转义

  • 转义字符

    下图的 &lt; 就是 < 的转义字符。

    image-20210729185128686
  • 或者是

    image-20210729185030318

1.2.4 多条件查询

image-20210729203804276

经常会遇到如上图所示的多条件查询,将多条件查询的结果展示在下方的数据列表中。而做这个功能需要分析最终的SQL语句应该是什么样,思考两个问题

  • 条件表达式
  • 如何连接

条件字段 企业名称品牌名称 需要进行模糊查询,所以条件应该是:

image-20210729204458815

简单的分析后,具体的步骤为:

  • 编写接口方法

    • 参数:所有查询条件
    • 结果:List
  • 在映射配置文件中编写SQL语句

  • 编写测试方法并执行


  1. 编写接口方法

BrandMapper 接口中定义多条件查询的方法。

而该功能有三个参数,就需要考虑定义接口时,参数应该如何定义。Mybatis针对多参数有多种实现

  • 使用 @Param("参数名称") 标记每一个参数,在映射配置文件中就需要使用 #{参数名称} 进行占位

    List<Brand> getByCondition(@Param("status") int status, @Param("companyName") String companyName,@Param("brandName") String brandName);
    
  • 将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和实体类属性名保持一致。

    List<Brand> getByCondition(Brand brand);
    
  • 将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和map集合中键的名称一致。

    List<Brand> getByCondition(Map map);
    
  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

<select id="getByCondition" resultMap="brandResultMap">
    select *
    from tb_brand
    where status = #{status}
    and company_name like #{companyName}
    and brand_name like #{brandName}
</select>
  1. 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

@Test
    public void testGetByCondition(){
        //接收参数
        int status = 1 ;
        String companyName = "";
        String brandName = "";

        // 处理参数
        companyName = "%" + companyName + "%";
        brandName = "%" + brandName + "%";

        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);


       /* //执行方法
        //方式一 :接口方法参数使用 @Param 方式调用的方法
        List<Brand> brands = mapper.getByCondition(status, companyName, brandName);*/

        //方式二 :接口方法参数是 实体类对象 方式调用的方法
        //封装对象
         Brand brand = new Brand();
        brand.setStatus(status);
        brand.setCompanyName(companyName);
        brand.setBrandName(brandName);
        List<Brand> brands = mapper.getByCondition(brand);


        /*//方式三 :接口方法参数是 map集合对象 方式调用的方法
        Map map = new HashMap();
        map.put("status" , status);
        map.put("companyName", companyName);
        map.put("brandName" , brandName);
        List<Brand> brands = mapper.getByCondition(map);*/
        System.out.println(brands);
    }

测试结果为:

[Brand{id=2, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华而不为', status=1}
, Brand{id=3, brandName='小米', companyName='小米科技有限公司', ordered=50, description='are you ok', status=1}
]


1.2.4.1 动态SQL

上述功能实现存在很大的问题。用户在输入条件时,肯定不会所有的条件都填写,这个时候的SQL语句就不能那样写

例如用户只输入 当前状态 时,SQL语句就是

select * from tb_brand where status = #{status}

而用户如果只输入企业名称时,SQL语句就是

select * from tb_brand where company_name like #{companName}

而用户如果输入了 当前状态企业名称 时,SQL语句又不一样

select * from tb_brand where status = #{status} and company_name like #{companName}

针对上述的需要,Mybatis对动态SQL有很强大的支撑:

  • if

  • choose (when, otherwise)

  • trim (where, set)

  • foreach

if 标签和 where 标签:

  • if 标签:条件判断

    • test 属性:逻辑表达式
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        where
            <if test="status != null">
                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>
    </select>
    

    如上的这种SQL语句就会根据传递的参数值进行动态的拼接。如果此时status和companyName有值那么就会值拼接这两个条件。

    但是它也存在问题,如果此时给的参数值是

    Map map = new HashMap();
    // map.put("status" , status);
    map.put("companyName", companyName);
    map.put("brandName" , brandName);
    

    拼接的SQL语句就变成了

    select * from tb_brand where and company_name like ? and brand_name like ?
    

    而上面的语句中 where 关键后直接跟 and 关键字,这就是一条错误的SQL语句。这个就可以使用 where 标签解决

  • where 标签

    • 作用:
      • 替换where关键字
      • 会动态的去掉第一个条件前的 and
      • 如果所有的参数没有值则不加where关键字
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        <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>
    

    注意:需要给每个条件前都加上 and 关键字。

1.2.4.2 单个条件(动态SQL)

image-20210729213613029

如上图所示,在查询时只能选择 品牌名称当前状态企业名称 这三个条件中的一个,但是用户到底选择哪儿一个,并不能确定。这种就属于单个条件的动态SQL语句。

这种需求需要使用到 choose(when,otherwise)标签 实现, 而 choose 标签类似于Java 中的switch语句。

通过一个案例来使用这些标签

  1. 编写接口方法

BrandMapper 接口中定义单条件查询的方法。

/**
  * 单条件动态查询
  * @param brand
  * @return
  */
List<Brand> getByConditionSingle(Brand brand);
  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

<select id="getByConditionSingle" resultMap="brandResultMap">
    select *
    from tb_brand
    <where>
        <choose><!--相当于switch-->
            <when test="status != null"><!--相当于case-->
                status = #{status}
            </when>
            <when test="companyName != null and companyName != '' "><!--相当于case-->
                company_name like #{companyName}
            </when>
            <when test="brandName != null and brandName != ''"><!--相当于case-->
                brand_name like #{brandName}
            </when>
        </choose>
    </where>
</select>
  1. 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

@Test
public void testSelectByConditionSingle() throws IOException {
    //接收参数
    int status = 1;
    String companyName = "华为";
    String brandName = "华为";

    // 处理参数
    companyName = "%" + companyName + "%";
    brandName = "%" + brandName + "%";

    //封装对象
    Brand brand = new Brand();
    //brand.setStatus(status);
    brand.setCompanyName(companyName);
    //brand.setBrandName(brandName);

    //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.selectByConditionSingle(brand);
    System.out.println(brands);

    //5. 释放资源
    sqlSession.close();
}

执行测试方法结果如下:

[Brand{id=2, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华而不为', status=1}]


1.2.5 添加数据

  1. 编写接口方法

BrandMapper 接口中定义添加方法。

 /**
   * 添加
   */
void add(Brand brand);
  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写添加数据的 statement

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

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

	@Test
    public void testAdd() throws IOException {
        //接收参数
        int status = 0;
        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);


        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);
        //4. 执行方法
        mapper.add(brand);
        //提交事务
        sqlSession.commit();
        //5. 释放资源
        sqlSession.close();
    }
  1. 添加-主键返回

在数据添加成功后,有时候需要获取插入数据库数据的主键(主键是自增长)。

明白了什么时候 主键返回 。接下来简单模拟一下,在添加完数据后打印id属性值,能打印出来说明已经获取到了。

将上面添加品牌数据的案例中映射配置文件里 statement 进行修改,如下

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

在 insert 标签上添加如下属性:

  • useGeneratedKeys:是够获取自动增长的主键值。true表示获取
  • keyProperty :指定将获取到的主键值封装到哪儿个属性里

1.2.6 修改

注意:如果有哪儿个输入框没有输入内容,是将表中数据对应字段值替换为空白还是保留字段之前的值?答案肯定是保留之前的数据。

  1. 编写接口方法

BrandMapper 接口中定义修改方法。

 /**
   * 修改
   */
void update(Brand brand);

上述方法参数 Brand 就是封装了需要修改的数据,而id肯定是有数据的,这也是和添加方法的区别。

  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写修改数据的 statement

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

set 标签可以用于动态包含需要更新的列,忽略其它不更新的列。

  1. 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

	@Test
    public void testUpdate() throws IOException {
        //接收参数
        int status = 0;
        String companyName = "波导手机";
        String brandName = "波导";
        String description = "波导手机,手机中的战斗机";
        int ordered = 200;
        int id = 5;

        //封装对象
        Brand brand = new Brand();
        brand.setStatus(status);
        brand.setCompanyName(companyName);
        brand.setBrandName(brandName);
        brand.setDescription(description);
        brand.setOrdered(ordered);
        brand.setId(id);

        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);
        mapper.update(brand);
        sqlSession.commit();
        sqlSession.close();
    }

1.2.7 删除单个数据

  1. 编写接口方法

BrandMapper 接口中定义根据id删除方法。

/**
  * 根据id删除
  */
void deleteById(int id);
  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写删除一行数据的 statement

<delete id="deleteById">
    delete from tb_brand where id = #{id};
</delete>
  1. 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

 @Test
public void testDeleteById() throws IOException {
    //接收参数
    int id = 6;

    //1. 获取SqlSessionFactory
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    //2. 获取SqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //SqlSession sqlSession = sqlSessionFactory.openSession(true);
    //3. 获取Mapper接口的代理对象
    BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
    //4. 执行方法
    brandMapper.deleteById(id);
    //提交事务
    sqlSession.commit();
    //5. 释放资源
    sqlSession.close();
}

运行过程只要没报错,直接到数据库查询数据是否还存在。

1.2.8 批量删除

  1. 编写接口方法

BrandMapper 接口中定义删除多行数据的方法。

/**
  * 批量删除
  */
void deleteByIds(int[] ids);

参数是一个数组,数组中存储的是多条数据的id

  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写删除多条数据的 statement

编写SQL时需要遍历数组来拼接SQL语句。Mybatis 提供了 foreach 标签使用

foreach 标签

用来迭代任何可迭代的对象(如数组,集合)。

  • collection 属性:
    • mybatis会将数组参数,封装为一个Map集合。
      • 默认:array = 数组
      • 使用@Param注解改变map集合的默认key的名称
  • item 属性:本次迭代获取到的元素。
  • separator 属性:集合项迭代之间的分隔符。foreach 标签不会错误地添加多余的分隔符。也就是最后一次迭代不会加分隔符。
  • open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
  • close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
<delete id="deleteByIds">
    delete from tb_brand where id
    in
    <foreach collection="array" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
    ;
</delete>

假如数组中的id数据是{1,2,3},那么拼接后的sql语句就是:

delete from tb_brand where id in (1,2,3);
  1. 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

	@Test
    public void testDeleteByIds(){
        int[] ids = {7,8};
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);
        mapper.deleteByIds(ids);
        sqlSession.commit();
        sqlSession.close();
    }

1.2.9 Mybatis参数传递

Mybatis 接口方法中可以接收各种各样的参数,如下:

  • 多个参数
  • 单个参数:单个参数又可以是如下类型
    • POJO 类型
    • Map 集合类型
    • Collection 集合类型
    • List 集合类型
    • Array 类型
    • 其他类型

1.2.9.1 多个参数

如下面的代码,就是接收两个参数,而接收多个参数需要使用 @Param 注解,那么为什么要加该注解呢?这个问题要弄明白就必须来研究Mybatis 底层对于这些参数是如何处理的。

User select(@Param("username") String username,@Param("password") String password);
<select id="select" resultType="user">
	select *
    from tb_user
    where 
    	username=#{username}
    	and password=#{password}
</select>

在接口方法中定义多个参数,Mybatis 会将这些参数封装成 Map 集合对象,值就是参数值,而键在没有使用 @Param 注解时有以下命名规则:

  • 以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:

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

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

  • 以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:

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

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

代码验证:

  • UserMapper 接口中定义如下方法

    User select(String username,String password);
    
  • UserMapper.xml 映射配置文件中定义SQL

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

    或者

    <select id="select" resultType="user">
    	select *
        from tb_user
        where 
        	username=#{param1}
        	and password=#{param2}
    </select>
    
  • 运行代码结果如下

    image-20210805230303461

    在映射配合文件的SQL语句中使用用 arg 开头的和 param 书写,代码的可读性会变的特别差,此时可以使用 @Param 注解。

在接口方法参数上使用 @Param 注解,Mybatis 会将 arg 开头的键名替换为对应注解的属性值。

代码验证:

  • UserMapper 接口中定义如下方法,在 username 参数前加上 @Param 注解

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

    Mybatis 在封装 Map 集合时,键名就会变成如下:

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

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

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

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

  • UserMapper.xml 映射配置文件中定义SQL

    <select id="select" resultType="user">
    	select *
        from tb_user
        where 
        	username=#{username}
        	and password=#{param2}
    </select>
    
  • 运行程序结果没有报错。而如果将 #{} 中的 username 还是写成 arg0

    <select id="select" resultType="user">
    	select *
        from tb_user
        where 
        	username=#{arg0}
        	and password=#{param2}
    </select>
    
  • 运行程序则可以看到错误

    image-20210805231727206

结论:以后接口参数是多个时,在每个参数上都使用 @Param 注解。这样代码的可读性更高。

1.2.9.2 单个参数

  • POJO 类型

    直接使用。要求 属性名参数占位符名称 一致

  • Map 集合类型

    直接使用。要求 map集合的键名参数占位符名称 一致

  • Collection 集合类型

    Mybatis 会将集合封装到 map 集合中,如下:

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

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

    可以使用 @Param 注解替换map集合中默认的 arg 键名。

  • List 集合类型

    Mybatis 会将集合封装到 map 集合中,如下:

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

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

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

    可以使用 @Param 注解替换map集合中默认的 arg 键名。

  • Array 类型

    Mybatis 会将集合封装到 map 集合中,如下:

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

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

    可以使用 @Param 注解替换map集合中默认的 arg 键名。

  • 其他类型

    比如int类型,参数占位符名称 叫什么都可以。尽量做到见名知意

1.3 配置解析

核心配置文件中还可以配置很多内容。在需要时可以通过查询官网看可以配置的内容

image-20210726221454927

1.3.1 多环境配置

在核心配置文件的 environments 标签中其实是可以配置多个 environment ,使用 id 给每段环境起名,在 environments 中使用 default='环境id' 来指定使用哪儿段配置。我们一般就配置一个 environment 即可。

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <!--数据库连接信息-->
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
            <property name="username" value="root"/>
            <property name="password" value="1234"/>
        </dataSource>
    </environment>

    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <!--数据库连接信息-->
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
            <property name="username" value="root"/>
            <property name="password" value="1234"/>
        </dataSource>
    </environment>
</environments> 

1.3.2 类型别名

在映射配置文件中的 resultType 属性需要配置数据封装的类型(类的全限定名)。而每次这样写是特别麻烦的,Mybatis 提供了 类型别名(typeAliases) 可以简化这部分的书写。

首先需要现在核心配置文件中配置类型别名,也就意味着给pojo包下所有的类起了别名(别名就是类名),不区分大小写。内容如下:

<typeAliases>
    <!--name属性的值是实体类所在包-->
    <package name="com.duan.pojo"/> 
</typeAliases>

通过上述的配置,就可以简化映射配置文件中 resultType 属性值的编写

<mapper namespace="com.duan.mapper.UserMapper">
    <select id="selectAll" resultType="user">
        select * from tb_user;
    </select>
</mapper>

1.3.3 设置

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true | false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 true | false false
aggressiveLazyLoading 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。 true | false false (在 3.4.1 及之前的版本中默认为 true)
multipleResultSetsEnabled 是否允许单个语句返回多结果集(需要数据库驱动支持)。 true | false true
useColumnLabel 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 true | false true
useGeneratedKeys 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 true | false False
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 NONE, PARTIAL, FULL PARTIAL
autoMappingUnknownColumnBehavior 指定发现自动映射目标未知列(或未知属性类型)的行为。NONE: 不做任何反应WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARNFAILING: 映射失败 (抛出 SqlSessionException) NONE, WARNING, FAILING NONE
defaultExecutorType 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 任意正整数 未设置 (null)
defaultFetchSize 为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 任意正整数 未设置 (null)
defaultResultSetType 指定语句默认的滚动策略。(新增于 3.5.2) FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置) 未设置 (null)
safeRowBoundsEnabled 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 true | false False
safeResultHandlerEnabled 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 true | false True
mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 true | false False
localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 SESSION | STATEMENT SESSION
jdbcTypeForNull 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 OTHER
lazyLoadTriggerMethods 指定对象的哪些方法触发一次延迟加载。 用逗号分隔的方法列表。 equals,clone,hashCode,toString
defaultScriptingLanguage 指定动态 SQL 生成使用的默认脚本语言。 一个类型别名或全限定类名。 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
defaultEnumTypeHandler 指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5) 一个类型别名或全限定类名。 org.apache.ibatis.type.EnumTypeHandler
callSettersOnNulls 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 true | false false
returnInstanceForEmptyRow 当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2) true | false false
logPrefix 指定 MyBatis 增加到日志名称的前缀。 任何字符串 未设置
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置
proxyFactory 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 CGLIB (3.5.10 起废弃) | JAVASSIST JAVASSIST (MyBatis 3.3 以上)
vfsImpl 指定 VFS 的实现 自定义 VFS 的实现的类全限定名,以逗号分隔。 未设置
useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) true | false true
configurationFactory 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) 一个类型别名或完全限定类名。 未设置
shrinkWhitespacesInSql 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5) true | false false
defaultSqlProviderType 指定一个拥有 provider 方法的 sql provider 类 (新增于 3.5.6). 这个类适用于指定 sql provider 注解上的type(或 value) 属性(当这些属性在注解中被忽略时)。 (e.g. @SelectProvider) 类型别名或者全限定名 未设置
nullableOnForEach 为 'foreach' 标签的 'nullable' 属性指定默认值。(新增于 3.5.9) true | false false
argNameBasedConstructorAutoMapping 当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。(新增于 3.5.10) true | false false

一个配置完整的 settings 元素的示例如下:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="safeResultHandlerEnabled" value="true"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
  <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
  <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
  <setting name="callSettersOnNulls" value="false"/>
  <setting name="returnInstanceForEmptyRow" value="false"/>
  <setting name="logPrefix" value="exampleLogPreFix_"/>
  <setting name="logImpl" value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>
  <setting name="proxyFactory" value="CGLIB | JAVASSIST"/>
  <setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/>
  <setting name="useActualParamName" value="true"/>
  <setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/>
</settings>

1.3.4 生命周期和作用域

作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。

提示:对象生命周期和依赖注入框架

依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。image-20230616145418892

  1. SqlSessionFactoryBuilder
  • 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。

  • 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。


  1. SqlSessionFactory
  • SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。(可以想象为数据库连接池)

  • 因此 SqlSessionFactory 的最佳作用域是应用作用域。

  • 最简单的就是使用 单例模式或者静态单例模式。


  1. SqlSession
  • 连接到线程池的一个请求!
  • SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
  • 用完之后需要关闭,否则资源被占用。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
  // 应用逻辑代码
}

在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。

1.3.5 解决属性名和字段名不一致的问题

  1. 起别名解决上述问题

在查询时 brandNamecompanyName 这两个属性的数据没有封装成功,查询 实体类 和 表中的字段 发现,在实体类中属性名是 brandNamecompanyName ,而表中的字段名为 brand_namecompany_name,如下图所示 。

image-20210729173210433

可以在写sql语句时给这两个字段起别名,将别名定义成和属性名一致即可。

<select id="selectAll" resultType="brand">
    select
    id, brand_name as brandName, company_name as companyName, ordered, description, status
    from tb_brand;
</select>

而上面的SQL语句中的字段列表书写麻烦,如果表中还有更多的字段,同时其他的功能也需要查询这些字段时就显得的代码不够精炼。Mybatis提供了sql 片段可以提高sql的复用性。

SQL片段:

  • 将需要复用的SQL片段抽取到 sql 标签中

    <sql id="brand_column">
    	id, brand_name as brandName, company_name as companyName, ordered, description, status
    </sql>
    

    id属性值是唯一标识,引用时也是通过该值进行引用。

  • 在原sql语句中进行引用

    使用 include 标签引用上述的 SQL 片段,而 refid 指定上述 SQL 片段的id值。

    <select id="selectAll" resultType="brand">
        select
        <include refid="brand_column" />
        from tb_brand;
    </select>
    
  1. 使用resultMap解决上述问题

起别名 + sql片段的方式可以解决上述问题,但是它也存在问题。如果还有功能只需要查询部分字段,而不是查询所有字段,那么就需要再定义一个 SQL 片段,这就显得不是那么灵活。

这时可以使用resultMap来定义字段和属性的映射关系的方式解决上述问题。

  • 在映射配置文件中使用resultMap定义 字段 和 属性 的映射关系

    <resultMap id="brandResultMap" type="brand">
        <!--
                id:完成主键字段的映射
                    column:表的列名
                    property:实体类的属性名
                result:完成一般字段的映射
                    column:表的列名  
                    property:实体类的属性名
            -->
        <result column="brand_name" property="brandName"/>
        <result column="company_name" property="companyName"/>
    </resultMap>
    

    注意:在上面只需要定义 字段名 和 属性名 不一样的映射,而一样的则不需要专门定义出来。

  • SQL语句正常编写

    <select id="selectAll" resultMap="brandResultMap">
        select *
        from tb_brand;
    </select>
    

1.4,注解实现CRUD

使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发

@Select(value = "select * from tb_user where id = #{id}")
public User select(int id);

注意:

  • 注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的 statement

Mybatis 针对 CURD 操作都提供了对应的注解,已经做到见名知意。如下:

  • 查询 :@Select
  • 添加 :@Insert
  • 修改 :@Update
  • 删除 :@Delete*

代码实现:

  • 将之前案例中 UserMapper.xml 中的 根据id查询数据 的 statement 注释掉

    image-20210805235229938
  • UserMapper 接口的 selectById 方法上添加注解

    image-20210805235405070
  • 运行测试程序也能正常查询到数据

注意:在官方文档中 入门 中有这样的一段话:

image-20210805234302849

所以,注解完成简单功能,配置文件完成复杂功能。

动态 SQL 就是复杂的功能,如果用注解使用的话,就需要使用到 Mybatis 提供的SQL构建器来完成,而对应的代码如下:

image-20210805234842497

上述代码将java代码和SQL语句融到了一块,使得代码的可读性大幅度降低。

1.5 日志

1.5.1 日志工厂

日志的作用:如果一个数据库的操作出现了异常,就需要排错,而日志就可以记录异常的信息。

mybatis中的日志设置如下:

设置名 描述 有效值 默认值
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置
  • **SLF4J(掌握) **

  • LOG4J(3.5.9 起废弃)

  • LOG4J2

  • JDK_LOGGING

  • COMMONS_LOGGING
  • STDOUT_LOGGING (掌握)

  • NO_LOGGING

方式

在mybatis配置文件mybatis-config.xml配置日志

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

输出结果就变为:

image-20230616225919300

1.6 分页

为什么要分页?

  • 为了减少数据的处理量

1.6.1使用limit分页

  1. 编写接口方法

BrandMapper 接口中定义删除多行数据的方法。

/**
  * 分页
  */
list<Brand> GetBrandByLImit(Map<String,Integer> map)
  1. 编写SQL语句

BrandMapper.xml 映射配置文件中编写分页数据。

<select id="getBrandByLimit" resultType="com.duan.pojo.Brand" >
        select *
        from tb_brand limit #{startIndex},#{pageSize};
    </select>
  1. 编写测试方法

test/java 下的 com.duan.mapper 包下的 MybatisTest类中 定义测试方法

 	@Test
    public void testGetBrandByLimit(){
        Map<String, Integer> map = new HashMap<>();
        map.put("startIndex",0);
        map.put("pageSize",2);
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        BrandDao mapper = sqlSession.getMapper(BrandDao.class);
        List<Brand> brands =  mapper.getBrandByLimit(map);
        System.out.println(brands);
        sqlSession.close();
    }

输出结果为id为1和2的数据就说明编写成功

1.6.2 使用RowBound分页(不建议在开发中使用)

1.6.3 分页插件mybatisPageHelper(了解既可)

官网:https://pagehelper.github.io/

Spring

1,简介

  • 从使用和占有率看

    • Spring在市场的占有率与使用率高

    • Spring在企业的技术选型命中率高

    • 所以说,Spring技术是JavaEE开发必备技能,企业开发技术选型命中率>90%

      image-20210729171139088

      说明:对于未使用Spring的项目一般都是些比较老的项目,大多都处于维护阶段。

  • 从专业角度看

    • 随着时代发展,软件规模与功能都呈几何式增长,开发难度也在不断递增,该如何解决?
      • Spring可以简化开发,降低企业级开发的复杂性,使开发变得更简单快捷
    • 随着项目规模与功能的增长,遇到的问题就会增多,为了解决问题会引入更多的框架,这些框架如何协调工作?
      • Spring可以框架整合,高效整合其他技术,提高企业级应用开发与运行效率

Spring框架主要的优势是在简化开发框架整合上,Spring框架的主要内容:

  • 简化开发: Spring框架中提供了两个大的核心技术,分别是:

    • IOC
    • AOP
      • 事务处理

    1.Spring的简化操作都是基于这两块内容,所以这也是Spring学习中最为重要的两个知识点。

    2.事务处理属于Spring中AOP的具体应用,可以简化项目中的事务管理,也是Spring技术中的一大亮点。

  • 框架整合: Spring在框架整合这块已经做到了极致,它可以整合市面上几乎所有主流框架,比如:

    • MyBatis
    • MyBatis-plus
    • Struts
    • Struts2
    • Hibernate
    • ……

    在Spring框架的学习中,主要是学习如何整合MyBatis。

    综上所述,对于Spring的学习,主要学习四块内容:

    (1)IOC,(2)整合Mybatis(IOC的具体应用),(3)AOP,(4)声明式事务(AOP的具体应用)

学习思路

  • 学习Spring框架设计思想
    • 对于Spring来说,它能迅速占领全球市场,不只是说它的某个功能比较强大,更重要是在它的思想上。
  • 学习基础操作,思考操作与思想间的联系
    • 掌握了Spring的设计思想,然后就需要通过一些基础操作来思考操作与思想之间的关联关系
  • 学习案例,熟练应用操作的同时,体会思想
    • 会了基础操作后,就需要通过大量案例来熟练掌握框架的具体应用,加深对设计思想的理解。

2,Spring相关概念

  • 官网:https://spring.io,从官网可以大概了解到:

    • Spring能做什么:用以开发web、微服务以及分布式系统等,光这三块就已经占了JavaEE开发的九成多。
    • Spring并不是单一的一个技术,而是一个大家族,可以从官网的Projects中查看其包含的所有技术。
  • Spring发展到今天已经形成了一种开发的生态圈,Spring提供了若干个项目,每个项目用于完成特定的功能。

    • Spring已形成了完整的生态圈,也就是说可以完全使用Spring技术完成整个项目的构建、设计与开发。

    • Spring有若干个项目,可以根据需要自行选择,把这些个项目组合起来,起了一个名称叫全家桶,如下图所示

      image-20210729171850181

      说明:

      图中的图标都代表什么含义,可以进入https://spring.io/projects网站进行对比查看。

      这些技术并不是所有的都需要学习,额外需要重点关注Spring FrameworkSpringBootSpringCloud:

      1629714811435

      • Spring Framework:Spring框架,是Spring中最早最核心的技术,也是所有其他技术的基础。
      • SpringBoot:Spring是来简化开发,而SpringBoot是来帮助Spring在简化的基础上能更快速进行开发。
      • SpringCloud:这个是用来做分布式之微服务架构的相关开发。

      除了上面的这三个技术外,还有很多其他的技术,也比较流行,如SpringData,SpringSecurity等。Spring其实指的是Spring Framework

Spring发展史:

image-20210729171926576

Spring发展史

  • IBM(IT公司-国际商业机器公司)在1997年提出了EJB思想,早期的JAVAEE开发大都基于该思想。
  • Rod Johnson(Java和J2EE开发领域的专家)在2002年出版的Expert One-on-One J2EE Design and Development,书中有阐述在开发中使用EJB该如何做。
  • Rod Johnson在2004年出版的Expert One-on-One J2EE Development without EJB,书中提出了比EJB思想更高效的实现方案,并且在同年将方案进行了具体的落地实现,这个实现就是Spring1.0。
  • 随着时间推移,版本不断更新维护,目前最新的是Spring5
    • Spring1.0是纯配置文件开发
    • Spring2.0为了简化开发引入了注解开发,此时是配置文件加注解的开发方式
    • Spring3.0已经可以进行纯注解开发,使开发效率大幅提升,的课程会以注解开发为主
    • Spring4.0根据JDK的版本升级对个别API进行了调整
    • Spring5.0已经全面支持JDK8

2.2 Spring系统架构

2.2.1 系统架构图

  • Spring Framework是Spring生态圈中最基础的项目,是其他项目的根基。

  • Spring Framework的发展也经历了很多版本的变更,每个版本都有相应的调整

    image-20210729172153796

  • 主要研究的是4的架构图

    1629720945720

    (1)核心层

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

    (2)AOP层

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

    (3)数据层

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

    (4)Web层

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

    (5)Test层

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

2.2.2 课程学习路线

Spring的学习主要包含四部分内容,下图所示:

1629722300996

2.3 Spring核心概念

在Spring核心概念这部分内容中主要包含IOC/DIIOC容器Bean,那么问题就来了,这些都是什么呢?

2.3.1 目前项目中的问题

1629723232339

(1)业务层需要调用数据层的方法,就需要在业务层new数据层的对象

(2)如果数据层的实现类发生变化,那么业务层的代码也需要跟着改变,发生变更后,都需要进行编译打包和重部署

(3)所以,现在代码在编写的过程中存在的问题是:耦合度偏高

针对这个问题,该如何解决呢?

1629724206002

如果能把框中的内容给去掉,不就可以降低依赖了么,但是又会引入新的问题,去掉以后程序能运行么?

答案肯定是不行,因为UserDao没有赋值为Null,强行运行就会出空指针异常。

所以现在的问题就是,业务层不想new对象,运行的时候又需要这个对象,该咋办呢?

针对这个问题,Spring就提出了一个解决方案:

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

这种实现思就是Spring的一个核心概念

2.3.2 IOC、IOC容器、Bean、DI

  1. IOC(Inversion of Control)控制反转

(1)什么是控制反转呢?

  • 使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。
    • 业务层要用数据层的类对象,以前是自己new
    • 现在自己不new了,交给外部来创建对象
    • 外部就反转控制了数据层对象的创建权
    • 这种思想就是控制反转

这种思想,从本质上解决了问题,让程序员不用在去管理对象的创建。系统的耦合性大大降低,可以更专注在业务的实现上

(2)Spring和IOC之间的关系是什么呢?

  • Spring技术对IOC思想进行了实现
  • Spring提供了一个容器,称为IOC容器,用来充当IOC思想中的"外部"
  • IOC思想中的别人[外部]指的就是Spring的IOC容器

(3)IOC容器的作用以及内部存放的是什么?

  • IOC容器负责对象的创建、初始化等一系列工作,其中包含了数据层和业务层的类对象
  • 被创建或被管理的对象在IOC容器中统称为Bean
  • IOC容器中放的就是一个个的Bean对象

(4)当IOC容器中创建好service和dao对象后,程序能正确执行么?

  • 不行,因为service运行需要依赖dao对象
  • IOC容器中虽然有service和dao对象
  • 但是service对象和dao对象没有任何关系
  • 需要把dao对象交给service,也就是说要绑定service和dao对象之间的关系

像这种在容器中建立对象与对象之间的绑定关系就要用到DI:

  1. DI(Dependency Injection)依赖注入

(1)什么是依赖注入呢?

  • 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入
    • 业务层要用数据层的类对象,以前是自己new
    • 现在自己不new了,靠外部其实指的就是IOC容器来给注入进来
    • 这种思想就是依赖注入

(2)IOC容器中哪些bean之间要建立依赖关系呢?

  • 这个需要程序员根据业务需求提前建立好关系,如业务层需要依赖数据层,service就要和dao建立依赖关系

Spring的IOC和DI的最终目标就是:充分解耦,具体实现靠:

  • 使用IOC容器管理bean(IOC)
  • 在IOC容器内将有依赖关系的bean进行关系绑定(DI)
  • 最终结果为:使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系.

小结:

这节比较重要,重点要理解什么是IOC/DI思想什么是IOC容器什么是Bean

(1)什么IOC/DI思想?

  • IOC:控制反转,控制反转的是对象的创建权
  • DI:依赖注入,绑定对象与对象之间的依赖关系

(2)什么是IOC容器?

Spring创建了一个容器用来存放所创建的对象,这个容器就叫IOC容器

(3)什么是Bean?

容器中所存放的一个个对象就叫Bean或Bean对象

2.3.4 优点

  • spring是一个开源免费的框架
  • spring是一个轻量级的、非入侵式的框架
  • 控制反转(IOC),面向切面编程(AOP)
  • 支持事务处理,对框架整合的支持

2.3.5 弊端

  • 发展太久以后违背了原来的理念!配置十分繁琐,被称为”配置地狱!“。

3,入门案例

3.1 思路分析

(1)Spring是使用容器来管理bean对象的,那么管什么?

  • 主要管理项目中所使用到的类对象,比如(Service和Dao)

(2)如何将被管理的对象告知IOC容器?

  • 使用配置文件

(3)被管理的对象交给IOC容器,要想从容器中获取对象,就先得思考如何获取到IOC容器?

  • Spring框架提供相应的接口

(4)IOC容器得到后,如何从容器中获取bean?

  • 调用Spring框架提供对应接口中的方法

(5)使用Spring导入哪些坐标?

  • 用别人的东西,就需要在pom.xml添加对应的依赖

3.2 入门案例代码实现

步骤1:创建Maven项目

步骤2:添加Spring的依赖jar包

pom.xml

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

步骤3:添加案例中需要的类

创建UserService,UserServiceImpl,UserDao、UserDaoMysqlImpl、UserDaoImpl和测试类

public interface UserDao {
    void getUser();
}

----------------------------------
public class UserDaoImpl implements UserDao {
    @override
    public void getUser(){
        System.out.print("默认获取用户的数据!")
    }
}
----------------------------------
public class UserDaoMysqlImpl implements UserDao {
    @override
    public void getUser(){
        System.out.print("获取mysql的数据!")
    }
}
----------------------------------    
    
public interface UserService {
    public void getUser();
}
----------------------------------
public class UserServiceImpl implements UserService {
    private UserDao dao;
    
    //利用set进行动态实现值的注入!
    public void setUserDao(UserDao userDao){
        this.dao = userDao;
    }
    
    @override
    public void getUser(){
        dao.getUser();
    }
}

测试类

@test
public void testUserService(){
    //用户实际调用的是业务层,dao层用户不需要接触
    UserService userService = new UserServiceImpl();
    
    ((UserServiceImpl) userService).setUserDao(new UserDaoImpl);
    ((UserServiceImpl) userService).setUserDao(new UserDaoMysqlImpl);
}

输出结果为:

默认获取用户的数据!

获取mysql的数据!

在之前的代码中,用户的需求可能会影响原来的代码,需要根据用户的需求来修改原来的代码!

现在使用set接口来实现发生了革命性的变化,在用户增加需求时就不需要更改原来的代码,只需要在原来的基础上增加业务

private UserDao dao;
//利用set进行动态实现值的注入!
public void setUserDao(UserDao userDao){
    this.dao = userDao;
}

3.3 IOC本质:

IOC(inversion of control)是一种设计思想,DI(依赖注入)是实现IOC的一种方法。在没有IOC的程序中,使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中。在使用控制反转后将对象的创建转移给第三方,个人认为所谓的控制反转就是:获得依赖对象的创建方式由内转外了。

采用xml方式配置bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,bean的定义信息直接以注解的形式定义在实现类中,从而达到零配置的目的。

控制反转是一种通过描述(xml或注解)并通过第三方去生产或获取特定对象的方式。在spring中实现控制反转的是IOC容器,其实现方法是依赖注入(Dependency Injection , DI)

3.4 IOC创建对象的方式

  1. 使用无参构造创建对象,默认

  2. 如果要使用有参构造创建对象

    1. 在配置文件的使用下标赋值

      <!--第一种,下标赋值-->
      <bean id="user" class=“com.duan.pojo.User">
        <constructor-arg index="0" value="Java"/>                                          
      </bean>
      
    2. 在配置文件中使用类型创建(不建议使用)

      <!--第二种,类型创建-->
      <bean id="user" class=“com.duan.pojo.User">
        <constructor-arg type=“java.lang.String” value="Java"/>                                          
      </bean>
      
    3. 在配置文件中使用参数名创建

      <!--第三种,参数名创建-->
      <bean id="user" class=“com.duan.pojo.User">
        <constructor-arg name=“name” value="Java"/>                                          
      </bean>
      

4,Spring配置

4.1 bean基础配置

对于bean的配置中,主要是bean基础配置,bean的别名配置,bean的作用范围配置(重点),这三部分内容:

4.1.1 bean基础配置(id与class)

对于bean的基础配置,在前面的案例中已经使用过:

<!--id是bean的唯一标识符,class是bean对象所对应的全限定名 包名+类型-->
<bean id="" class=""/>

其中,bean标签的功能、使用方式以及id和class属性的作用。

这其中需要重点掌握的是:bean标签的id和class属性的使用

思考:

  • class属性能不能写接口如UserkDao的类全名呢?

答案肯定是不行,因为接口是没办法创建对象的。

  • 前面提过为bean设置id时,id必须唯一,但是如果由于命名习惯而产生了分歧后,该如何解决?

在解决这个问题之前,需要准备下开发环境,对于开发环境可以有两种解决方案:

  • 使用前面IOC和DI的案例

  • 重新搭建一个新的案例环境,目的是方便查阅代码

4.1.2 bean的name属性

环境准备好后,接下来就可以在这个环境的基础上来进行bean的别名配置,

步骤1:配置别名

打开spring的配置文件applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--name:为bean指定别名,别名可以有多个,使用逗号,分号,空格进行分隔-->
    <bean id="userService" name="service service4 UserEbi" class="com.duan.service.impl.UserServiceImpl">
        <property name="UserDao" ref="UserDao"/>
    </bean>

    <!--scope:bean设置作用范围,可选值为单例singloton,非单例prototype-->
    <bean id="UserDao" name="dao"class="com.duan.dao.impl. UserDaoImpl"/>
  </bean>

说明:Ebi全称Enterprise Business Interface,翻译为企业业务接口

步骤2:根据名称容器中获取bean对象

public class AppForName {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        //此处根据bean标签的id属性和name属性的任意一个值来获取bean对象
        userService userService = (userService) ctx.getBean("service4");
        userService.save();
    }
}
注意事项:
  • bean依赖注入的ref属性指定bean,必须在容器中存在。

  • 如果不存在,则会报错。

    bean无论是通过id还是name获取,如果无法获取到,将抛出异常NoSuchBeanDefinitionException

4.1.3 bean作用范围scope配置

关于bean的作用范围是bean属性配置的一个重点内容。

bean作用范围的配置属性:

  • 名称:scope

  • 功能:singleton:单例(默认)

    prototype:非单例

  • 结论:默认情况下,Spring创建的bean对象都是单例的

获取到结论后,问题就来了,那如果想创建出来非单例的bean对象,该如何实现呢?

1 . 配置bean为非单例

在Spring配置文件中,配置scope属性来实现bean的非单例创建

  • 在Spring的配置文件中,修改<bean>的scope属性

    <bean id="UserDao" name="dao" class="com.duan.dao.impl.UserDaoImpl" scope=""/>
    
  • 将scope设置为singleton

    <bean id="UserDao" name="dao" class="com.duan.dao.impl.UserDaoImpl" scope="singleton"/>
    

    运行AppForScope,打印结果一致

  • 将scope设置为prototype

    <bean id="UserDao" name="dao" class="com.duan.dao.impl.UserDaoImpl" scope="prototype"/>
    

    运行AppForScope,打印结果不一致

  • 结论,使用bean的scope属性可以控制bean的创建是否为单例:

    • singleton默认为单例
    • prototype为非单例
思考
  • 为什么bean默认为单例?
    • bean为单例的意思是在Spring的IOC容器中只会有该类的一个对象
    • bean对象只有一个就避免了对象的频繁创建与销毁,达到了bean对象的复用,性能高
  • bean在容器中是单例的,会不会产生线程安全问题?
    • 如果对象是有状态对象,即该对象有成员变量可以用来存储数据的,
    • 因为所有请求线程共用一个bean对象,所以会存在线程安全问题。
    • 如果对象是无状态对象,即该对象没有成员变量没有进行数据存储的,
    • 因方法中的局部变量在方法调用完成后会被销毁,所以不会存在线程安全问题。
  • 哪些bean对象适合交给容器进行管理?
    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 哪些bean对象不适合交给容器进行管理?
    • 封装实例的域对象,因为会引发线程安全问题,所以不适合。

4.14 bean基础配置小结

关于bean的基础配置中,需要掌握以下属性:

1631529887695

4.2 bean实例化

对象已经能交给Spring的IOC容器来创建了,但是容器是如何来创建对象的呢?

就需要研究下bean的实例化过程,在这块内容中主要解决两部分内容,分别是

  • bean是如何创建的

4.2.1 环境准备

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件applicationContext.xml

4.2.2 构造方法实例化

步骤1:准备需要被创建的类

准备一个UserDao和UserDaoImpl类

public interface UserDao {
    public void save();
}

public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("userDao被加载");
    }

}

步骤2:将类配置到Spring容器

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>

</beans>

步骤3:编写运行程序

public class AppForInstanceuser {
    public static void main(String[] args) {
        ApplicationContext ctx = new 
            ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao UserDao = (UserDao) ctx.getBean("UserDao");
        UserDao.save();

    }
}

步骤4:类中提供构造函数测试

在UserDaoImpl类中添加一个无参构造函数,并打印一句话,方便观察结果。

public class UserDaoImpl implements UserDao {
    public UserDaoImpl() {
        System.out.println("user dao constructor is running ....");
    }
    public void save() {
        System.out.println("user dao save ...");
    }

}

运行程序,如果控制台有打印构造函数中的输出,说明Spring容器在创建对象的时候也走的是构造函数

1629775972507

步骤5:将构造函数改成private测试

public class UserDaoImpl implements UserDao {
    private UserDaoImpl() {
        System.out.println("user dao constructor is running ....");
    }
    public void save() {
        System.out.println("user dao save ...");
    }

}

运行程序,能执行成功,说明内部走的依然是构造函数,能访问到类中的私有构造方法,显而易见Spring底层用的是反射

步骤6:构造函数中添加一个参数测试

public class UserDaoImpl implements UserDao {
    private UserDaoImpl(int i) {
        System.out.println("user dao constructor is running ....");
    }
    public void save() {
        System.out.println("user dao save ...");
    }

}

运行程序,

程序会报错,说明Spring底层使用的是类的无参构造方法。

因为每一个类默认都会提供一个无参构造函数,所以其实真正在使用这种方式的时候,什么也不需要做。这也是以后开发时比较常用的一种方式。

4.2.3 静态工厂实例化(了解)

一般是用来兼容早期的一些老系统,所以了解为主

4.2.3.1 工厂方式创建bean

使用工厂来创建对象的方式:

(1)准备一个OrderDao和OrderDaoImpl类

public interface OrderDao {
    public void save();
}

public class OrderDaoImpl implements OrderDao {
    public void save() {
        System.out.println("order dao save ...");
    }
}

(2)创建一个工厂类OrderDaoFactory并提供一个静态方法

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

(3)编写AppForInstanceOrder运行类,在类中通过工厂获取对象

public class AppForInstanceOrder {
    public static void main(String[] args) {
        //通过静态工厂创建对象
        OrderDao orderDao = OrderDaoFactory.getOrderDao();
        orderDao.save();
    }
}

如果代码中对象是通过上面的这种方式来创建的,如何将其交给Spring来管理呢?

4.2.3.2 静态工厂实例化

这就要用到Spring中的静态工厂实例化的知识了,具体实现步骤为:

(1)在spring的配置文件application.properties中添加以下内容:

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

class:工厂类的类全名

factory-mehod:具体工厂类中创建对象的方法名

(2)在AppForInstanceOrder运行类,使用从IOC容器中获取bean的方法进行运行测试

public class AppForInstanceOrder {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        OrderDao orderDao = (OrderDao) ctx.getBean("orderDao");
        orderDao.save();

    }
}

为什么在工厂类中也是直接new对象的,和直接new没什么太大的区别,而且静态工厂的方式反而更复杂,这种方式的意义是什么?

主要的原因是:

  • 在工厂的静态方法中,除了new对象还可以做其他的一些业务操作,这些操作必不可少,如:
public class OrderDaoFactory {
    public static OrderDao getOrderDao(){
        System.out.println("factory setup....");//模拟必要的业务操作
        return new OrderDaoImpl();
    }
}

4.2.4 实例工厂与FactoryBean

4.2.4.1 环境准备

(1)准备一个UserDao和UserDaoImpl类

public interface UserDao {
    public void save();
}

public class UserDaoImpl implements UserDao {

    public void save() {
        System.out.println("userDao被加载");
    }
}

(2)创建一个工厂类OrderDaoFactory并提供一个普通方法,注意此处和静态工厂的工厂类不一样的地方是方法不是静态方法

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

(3)编写AppForInstanceUser运行类,在类中通过工厂获取对象

public class AppForInstanceUser {
    public static void main(String[] args) {
        //创建实例工厂对象
        UserDaoFactory userDaoFactory = new UserDaoFactory();
        //通过实例工厂对象创建对象
        UserDao userDao = userDaoFactory.getUserDao();
        userDao.save();
}

对于上面这种实例工厂的方式如何交给Spring管理呢?

4.2.4.2 实例工厂实例化

具体实现步骤为:

(1)在spring的配置文件中添加以下内容:

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

实例化工厂运行的顺序是:

  • 创建实例化工厂对象,对应的是第一行配置

  • 调用对象中的方法来创建bean,对应的是第二行配置

    • factory-bean:工厂的实例对象

    • factory-method:工厂对象中的具体创建对象的方法名

factory-mehod:具体工厂类中创建对象的方法名

(2)在AppForInstanceUser运行类,使用从IOC容器中获取bean的方法进行运行测试

public class AppForInstanceUser {
    public static void main(String[] args) {
        ApplicationContext ctx = new 
            ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao userDao = (UserDao) ctx.getBean("userDao");
        userDao.save();
    }
}

实例工厂实例化的方式就已经介绍完了,配置的过程还是比较复杂,所以Spring为了简化这种配置方式就提供了一种叫FactoryBean的方式来简化开发。

4.2.4.3 FactoryBean的使用

具体的使用步骤为:·

(1)创建一个UserDaoFactoryBean的类,实现FactoryBean接口,重写接口的方法

public class UserDaoFactoryBean implements FactoryBean<UserDao> {
    //代替原始实例工厂中创建对象的方法
     @override
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }
    //返回所创建类的Class对象
     @override
    public Class<?> getObjectType() {
        return UserDao.class;
    }
}

(2)在Spring的配置文件中进行配置

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

(3)AppForInstanceUser运行类不用做任何修改,直接运行

这种方式在Spring去整合其他框架的时候会被用到。

查看源码会发现,FactoryBean接口其实会有三个方法,分别是:

T getObject() throws Exception;

Class<?> getObjectType();

default boolean isSingleton() {
    return true;
}

方法一:getObject(),被重写后,在方法中进行对象的创建并返回

方法二:getObjectType(),被重写后,主要返回的是被创建类的Class对象

方法三:isSingleton() ,没有被重写,因为它已经给了默认值,从方法名中可以看出其作用是设置对象是否为单例,默认true,默认是单例。

如果要改为非单例,只需要将isSingleton()方法进行重写,修改返回为false即可

//FactoryBean创建对象
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
    //代替原始实例工厂中创建对象的方法
     @override
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }
   @override
    public Class<?> getObjectType() {
        return UserDao.class;
    }

    @override
    public boolean isSingleton() {
        return false;
    }

一般情况下都会采用单例,也就是采用默认即可。

4.2.5 bean实例化小结

(1)bean是如何创建的呢?

  • 通过构造方法

(2)Spring的IOC实例化对象的三种方式分别是:

  • 构造方法(常用)
  • 静态工厂(了解)
  • 实例工厂(了解)
    • FactoryBean(实用)

这些方式中,重点掌握构造方法FactoryBean即可。

需要注意的一点是,构造方法在类中默认会提供,但是如果重写了构造方法,默认的就会消失,在使用的过程中需要注意,如果需要重写构造方法,最好把默认的构造方法也重写下。

4.3 bean的生命周期

  • 什么是生命周期?
    • 从创建到消亡的完整过程。
  • bean生命周期是什么?
    • bean对象从创建到销毁的整体过程。
  • bean生命周期控制是什么?
    • 在bean创建后到销毁前做一些事情。

现在面临的问题是如何在bean的创建之后和销毁之前把需要添加的内容添加进去。

4.3.1 环境准备

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件applicationContext.xml

(1)项目中添加UserDao、UserDaoImpl、userService和userServiceImpl类

public interface UserDao {
    void getUser();
}

----------------------------------
public class UserDaoImpl implements UserDao {
    @override
    public void getUser(){
        System.out.print("获取用户的数据!")
    }
}
----------------------------------
public interface UserService {
    public void getUser();
}
----------------------------------
public class UserServiceImpl implements UserService {
    private UserDao dao;
    
    //利用set进行动态实现值的注入!
    public void setUserDao(UserDao userDao){
        this.dao = userDao;
    }
    
    @override
    public void getUser(){
        dao.getUser();
    }
}

(2)resources下提供spring的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
</beans>

(3)编写AppForLifeCycle运行类,加载Spring的IOC容器,并从中获取对应的bean对象

public class AppForLifeCycle {
    public static void main( String[] args ) {
        ApplicationContext ctx = new 
          ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao UserDao = (UserDao) ctx.getBean("UserDao");
        UserDao.getUser();
    }
}

4.3.2 生命周期设置

接下来,在上面这个环境中来为UserDao添加生命周期的控制方法,具体的控制有两个阶段:

  • bean创建之后,想要添加内容,比如用来初始化需要用到资源
  • bean销毁之前,想要添加内容,比如用来释放用到的资源
步骤1:添加初始化和销毁方法

针对这两个阶段,在UserDaoImpl类中分别添加两个方法,方法名任意

public class UserDaoImpl implements UserDao {
    public void getUser() {
        System.out.print("获取用户的数据!")
    }
    //表示bean初始化对应的操作
    public void init(){
        System.out.println("init...");
    }
    //表示bean销毁前对应的操作
    public void destory(){
        System.out.println("destory...");
    }
}
步骤2:配置生命周期

在配置文件添加配置,如下:

<bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl" init-method="init" destroy-method="destory"/>
步骤3:运行程序

运行AppForLifeCycle打印结果为:

init...

获取用户的数据!

从结果中可以看出,init方法执行了,但是destroy方法却未执行,这是为什么呢?

  • Spring的IOC容器是运行在JVM中
  • 运行main方法后,JVM启动,Spring加载配置文件生成IOC容器,从容器获取bean对象,然后调方法执行
  • main方法执行完后,JVM退出,这个时候IOC容器中的bean还没有来得及销毁就已经结束了
  • 所以没有调用对应的destroy方法

知道了出现问题的原因,具体该如何解决呢?

4.3.3 close关闭容器

  • ApplicationContext中没有close方法

  • 需要将ApplicationContext更换成ClassPathXmlApplicationContext

    ClassPathXmlApplicationContext ctx = new 
        ClassPathXmlApplicationContext("applicationContext.xml");
    
  • 调用ctx的close()方法

    ctx.close();
    
  • 运行程序,就能执行destroy方法的内容

    init...

    获取用户的数据!

    destory...

4.3.4 注册钩子关闭容器

  • 在容器未关闭之前,提前设置好回调函数,让JVM在退出之前回调此函数来关闭容器

  • 调用ctx的registerShutdownHook()方法

    ctx.registerShutdownHook();
    

    注意:registerShutdownHook在ApplicationContext中也没有

  • 运行后,查询打印结果

    init...

    获取用户的数据!

    destory...

close和registerShutdownHook选哪个?

相同点:这两种都能用来关闭容器

不同点:close()是在调用的时候关闭,registerShutdownHook()是在JVM退出前调用关闭。

分析上面的实现过程,会发现添加初始化和销毁方法,即需要编码也需要配置,实现起来步骤比较多也比较乱。

Spring提供了两个接口来完成生命周期的控制,好处是可以不用再进行配置init-methoddestroy-method

接下来在userServiceImpl完成这两个接口的使用:

修改userServiceImpl类,添加两个接口InitializingBeanDisposableBean并实现接口中的两个方法afterPropertiesSetdestroy

public class userServiceImpl implements userService, InitializingBean, DisposableBean {
    private UserDao UserDao;
    public void setUserDao(UserDao UserDao) {
        this.UserDao = UserDao;
    }
    public void getUser() {
        System.out.print("获取用户的数据!")
        UserDao.getUser(); 
    }
    public void afterPropertiesSet() throws Exception {
        System.out.println("service init");
    }
    public void destroy() throws Exception {
        System.out.println("service destroy");
    }
}

重新运行AppForLifeCycle类,

init...

service init

获取用户的数据!

service destroy!

destory...

小细节

  • 对于InitializingBean接口中的afterPropertiesSet方法,翻译过来为属性设置之后

  • 对于userServiceImpl来说,UserDao是它的一个属性

  • setUserDao方法是Spring的IOC容器为其注入属性的方法

  • 思考:afterPropertiesSet和setUserDao谁先执行?

    • 从方法名分析,猜想应该是setUserDao方法先执行

    • 验证思路,在setUserDao方法中添加一句话

      public void setUserDao(UserDao UserDao) {
              System.out.println("set .....");
              this.UserDao = UserDao;
          }
      
      
    • 重新运行AppForLifeCycle,打印结果如下:

      init...

      set.....

      service init

      获取用户的数据!

      service destroy

      destory...

      初始化方法会在类中属性设置之后执行。

4.3.5 bean生命周期小结

(1)关于Spring中对bean生命周期控制提供了两种方式:

  • 在配置文件中的bean标签中添加init-methoddestroy-method属性
  • 类实现InitializingBeanDisposableBean接口,这种方式了解下即可。

(2)对于bean的生命周期控制在bean的整个生命周期中所处的位置如下:

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

(3)关闭容器的两种方式:

  • ConfigurableApplicationContext是ApplicationContext的子类
    • close()方法
    • registerShutdownHook()方法

5,DI相关内容

思考

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

Spring就是基于上面这些知识点,提供了两种注入方式,分别是:

  • setter注入
    • 简单类型
    • 引用类型
  • 构造器注入
    • 简单类型
    • 引用类型

5.1 setter注入

  1. 对于setter方式注入引用类型的方式之前已经学习过,快速回顾下:
  • 在bean中定义引用类型属性,并提供可访问的set方法
public class userServiceImpl implements userService {
    private UserDao UserDao;
    public void setUserDao(UserDao UserDao) {
        this.UserDao = UserDao;
    }
}
  • 配置中使用property标签ref属性注入引用类型对象
<bean id="userService" class="com.duan.service.impl.userServiceImpl">
  <property name="UserDao" ref="UserDao"/>
</bean>

<bean id="UserDao" class="com.duan.dao.imipl.UserDaoImpl"/>

5.1.1 环境准备

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件

(1)项目中添加UserDao、UserDaoImpl、UserDaoImpl、userService和userServiceImpl类

public interface UserDao {
    void getUser();
}

----------------------------------
public class UserDaoImpl implements UserDao {
    @override
    public void getUser(){
        System.out.print("获取用户的数据!")
    }
}
----------------------------------
public interface UserService {
    public void getUser();
}
----------------------------------
public class UserServiceImpl implements UserService {
    private UserDao dao;
    
    //利用set进行动态实现值的注入!
    public void setUserDao(UserDao userDao){
        this.dao = userDao;
    }
    
    @override
    public void getUser(){
        dao.getUser();
    }
}

(2)resources下提供spring的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <property name="UserDao" ref="UserDao"/>
    </bean>
</beans>

(3)编写AppForDISet运行类,加载Spring的IOC容器,并从中获取对应的bean对象

public class AppForDISet {
    public static void main( String[] args ) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        userService userService = (userService) ctx.getBean("userService");
        userService.getUser();
    }
}

5.1.2 注入引用数据类型

需求:在userServiceImpl对象中注入userDao

1.在userServiceImpl中声明userDao属性

2.为userDao属性提供setter方法

3.在配置文件中使用property标签注入

步骤1:声明属性并提供setter方法

在userServiceImpl中声明userDao属性,并提供setter方法

public class userServiceImpl implements userService{
    private UserDao UserDao;
    
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void getUser() {
        System.out.print("获取用户的数据2!")
        UserDao.getUser();
    }
}

步骤2:配置文件中进行注入配置

在applicationContext.xml配置文件中使用property标签注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <property name="UserDao" ref="UserDao"/>
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>

5.1.3 注入简单数据类型

需求:给UserDaoImpl注入一些简单数据类型的数据

参考引用数据类型的注入,可以推出具体的步骤为:

1.在UserDaoImpl类中声明对应的简单数据类型的属性

2.为这些属性提供对应的setter方法

3.在applicationContext.xml中配置

思考:

引用类型使用的是<property name="" ref=""/>,简单数据类型还是使用ref么?

ref是指向Spring的IOC容器中的另一个bean对象的,对于简单数据类型,没有对应的bean对象,该如何配置?

步骤1:声明属性并提供setter方法

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

public class UserDaoImpl implements UserDao {

    private String databaseName;
    private int connectionNum;

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

    public void setDatabaseName(String databaseName) {
        this.databaseName = databaseName;
    }

    public void save() {
        System.out.println("userDao被加载"+databaseName+","+connectionNum);
    }
}

步骤2:配置文件中进行注入配置

在applicationContext.xml配置文件中使用property标签注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl">
        <property name="databaseName" value="mysql"/>
      <property name="connectionNum" value="10"/>
    </bean>
    <bean id="userDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <property name="UserDao" ref="UserDao"/>
    </bean>
</beans>

说明:

value:后面跟的是简单数据类型,对于参数类型,Spring在注入的时候会自动转换,但是不能写成,这样的话,spring在将abc转换成int类型的时候就会报错。

<property name="connectionNum" value="abc"/>

注意:两个property注入标签的顺序可以任意。

小结
  • 对于引用数据类型使用的是<property name="" ref=""/>
  • 对于简单数据类型使用的是<property name="" value=""/>

5.2 构造器注入

5.2.1 环境准备

(1)项目中添加UserDao、UserDaoImpl、UserDao、UserDaoImpl、userService和userServiceImpl类

public interface UserDao {
    public void save();
}

public class UserDaoImpl implements UserDao {
    
    private String databaseName;
    private int connectionNum;
    
    public void save() {
        System.out.println("userDao被加载");
    }
}


public interface userService {
    public void save();
}

public class userServiceImpl implements userService{
    private UserDao UserDao;

    public void setUserDao(UserDao UserDao) {
        this.UserDao = UserDao;
    }

    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

(2)resources下提供spring的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <property name="UserDao" ref="UserDao"/>
    </bean>
</beans>

(3)编写AppForDIConstructor运行类,加载Spring的IOC容器,并从中获取对应的bean对象

public class AppForDIConstructor {
    public static void main( String[] args ) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        userService userService = (userService) ctx.getBean("userService");
        userService.save();
    }
}

5.2.2 构造器注入引用数据类型

接下来,在上面这个环境中来完成构造器注入的学习:

需求:将userServiceImpl类中的UserDao修改成使用构造器的方式注入。

1.将UserDao的setter方法删除掉

2.添加带有UserDao参数的构造方法

3.在applicationContext.xml中配置

步骤1:删除setter方法并提供构造方法

在userServiceImpl类中将UserDao的setter方法删除掉,并添加带有UserDao参数的构造方法

public class userServiceImpl implements userService{
    private UserDao UserDao;

    public userServiceImpl(UserDao userDao) {
        this.UserDao = UserDao;
    }

    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

步骤2:配置文件中进行配置构造方式注入

在applicationContext.xml中配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <constructor-arg name="UserDao" ref="UserDao"/>
    </bean>
</beans>

说明:

标签

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

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

步骤3:运行程序

运行AppForDIConstructor类,查看结果,说明UserDao已经成功注入。

1629802656916

5.2.3 构造器注入多个引用数据类型

需求:在userServiceImpl使用构造函数注入多个引用数据类型,比如userDao

1.声明userDao属性

2.生成一个带有UserDao和userDao参数的构造函数

3.在applicationContext.xml中配置注入

步骤1:提供多个属性的构造函数

在userServiceImpl声明userDao并提供多个参数的构造函数

public class userServiceImpl implements userService{
    private UserDao UserDao;

    public userServiceImpl(UserDao UserDao) {
        this.UserDao = UserDao;
    }

    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

步骤2:配置文件中配置多参数注入

在applicationContext.xml中配置注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <constructor-arg name="UserDao" ref="UserDao"/>
        <constructor-arg name="userDao" ref="userDao"/>
    </bean>
</beans>

说明:这两个<contructor-arg>的配置顺序可以任意

5.2.4 构造器注入多个简单数据类型

需求:在UserDaoImpl中,使用构造函数注入databaseName和connectionNum两个参数。

参考引用数据类型的注入可以推出具体的步骤为:

1.提供一个包含这两个参数的构造方法

2.在applicationContext.xml中进行注入配置

步骤1:添加多个简单属性并提供构造方法

修改UserDaoImpl类,添加构造方法

public class UserDaoImpl implements UserDao {
    private String databaseName;
    private int connectionNum;

    public UserDaoImpl(String databaseName, int connectionNum) {
        this.databaseName = databaseName;
        this.connectionNum = connectionNum;
    }

    public void save() {
        System.out.println("userDao被加载"+databaseName+","+connectionNum);
    }
}

步骤2:配置完成多个属性构造器注入

在applicationContext.xml中进行注入配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl">
        <constructor-arg name="databaseName" value="mysql"/>
        <constructor-arg name="connectionNum" value="666"/>
    </bean>
    <bean id="userDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <constructor-arg name="UserDao" ref="UserDao"/>
        <constructor-arg name="userDao" ref="userDao"/>
    </bean>
</beans>

说明:这两个<contructor-arg>的配置顺序可以任意

问题:

  • 当构造函数中方法的参数名发生变化后,配置文件中的name属性也需要跟着变
  • 这两块存在紧耦合,具体该如何解决?

在解决这个问题之前,需要了解的是,这个参数名发生变化的情况并不多。

方式一:删除name属性,添加type属性,按照类型注入

<bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl">
    <constructor-arg type="int" value="10"/>
    <constructor-arg type="java.lang.String" value="mysql"/>
</bean>
  • 这种方式可以解决构造函数形参名发生变化带来的耦合问题
  • 但是如果构造方法参数中有类型相同的参数,这种方式就不太好实现了

方式二:删除type属性,添加index属性,按照索引下标注入,下标从0开始

<bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl">
    <constructor-arg index="1" value="100"/>
    <constructor-arg index="0" value="mysql"/>
</bean>
  • 这种方式可以解决参数类型重复问题
  • 但是如果构造方法参数顺序发生变化后,这种方式又带来了耦合问题

具体该如何选择呢?

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

小结

Spring的依赖注入的实现方式:

  • setter注入

    • 简单数据类型

      <bean ...>
        <property name="" value=""/>
      </bean>
      
    • 引用数据类型

      <bean ...>
        <property name="" ref=""/>
      </bean>
      
  • 构造器注入

    • 简单数据类型

      <bean ...>
        <constructor-arg name="" index="" type="" value=""/>
      </bean>
      
    • 引用数据类型

      <bean ...>
        <constructor-arg name="" index="" type="" ref=""/>
      </bean>
      
  • 依赖注入的方式选择上

    • 建议使用setter注入
    • 第三方技术根据情况选择

5.3 自动配置

5.3.1 什么是依赖自动装配?

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

5.3.2 自动装配方式有哪些?

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

5.3.3 准备下案例环境

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件

(1)项目中添加UserDao、UserDaoImpl、userService和userServiceImpl类

public interface UserDao {
    public void save();
}

public class UserDaoImpl implements UserDao {
    
    private String databaseName;
    private int connectionNum;
    
    public void save() {
        System.out.println("userDao被加载");
    }
}
public interface userService {
    public void save();
}

public class userServiceImpl implements userService{
    private UserDao UserDao;

    public void setUserDao(UserDao UserDao) {
        this.UserDao = UserDao;
    }

    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

(2)resources下提供spring的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.duan.service.impl.userServiceImpl">
        <property name="UserDao" ref="UserDao"/>
    </bean>
</beans>

(3)编写AppForAutoware运行类,加载Spring的IOC容器,并从中获取对应的bean对象

public class AppForAutoware {
    public static void main( String[] args ) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        userService userService = (userService) ctx.getBean("userService");
        userService.save();
    }
}

5.3.4 完成自动装配的配置

自动装配只需要修改applicationContext.xml配置文件即可:

(1)将<property>标签删除

(2)在<bean>标签中添加autowire属性

首先来实现按照类型注入的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

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

</beans>

注意事项:

  • 需要注入属性的类中对应属性的setter方法不能省略
  • 被注入的对象必须要被Spring的IOC容器管理
  • 按照类型在Spring的IOC容器中如果找到多个对象,会报NoUniqueBeanDefinitionException

一个类型在IOC中有多个对象,还想要注入成功,这个时候就需要按照名称注入,配置方式为:

注意事项:

  • 按照名称注入中的名称指的是什么?

    public class UserServiceImpl implements UserService{
        private UserDao userDao;
        
        public void setUserDao(UserDao userDao){
            this.userDao = userDao;
        }
    }
    
    • UserDao是private修饰的,外部类无法直接方法
    • 外部类只能通过属性的set方法进行访问
    • 对外部类来说,setUserDao方法名,去掉set后首字母小写是其属性名
      • 为什么是去掉set首字母小写?
      • 这个规则是set方法生成的默认规则,set方法的生成是把属性名首字母大写前面加set形成的方法名
    • 所以按照名称注入,其实是和对应的set方法有关,但是如果按照标准起名称,属性名和set对应的名是一致的
  • 如果按照名称去找对应的bean对象,找不到则注入Null

  • 当某一个类型在IOC容器中有多个对象,按照名称注入只找其指定名称对应的bean对象,不会报错

两种方式介绍完后,以后用的更多的是按照类型注入。

最后对于依赖注入,需要注意一些其他的配置特征:

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

5.4 集合注入

还有一种数据类型集合,集合中既可以装简单数据类型也可以装引用数据类型,对于集合,在Spring中该如何注入呢?

先来回顾下,常见的集合类型有哪些?

  • 数组
  • List
  • Set
  • Map
  • Properties

针对不同的集合类型,该如何实现注入呢?

5.4.1 环境准备

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件applicationContext.xml

(1)项目中添加添加UserDao、UserDaoImpl类

public interface UserDao {
    public void save();
}
  
public class UserDaoImpl implements UserDao {

    private int[] array;

    private List<String> list;

    private Set<String> set;

    private Map<String,String> map;

    private Properties properties;

     public void save() {
        System.out.println("userDao添加数据");

        System.out.println("遍历数组:" + Arrays.toString(array));

        System.out.println("遍历List" + list);

        System.out.println("遍历Set" + set);

        System.out.println("遍历Map" + map);

        System.out.println("遍历Properties" + properties);
    }
  //setter....方法省略,自己使用工具生成
}

(2)resources下提供spring的配置文件,applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
</beans>

(3)编写AppForDICollection运行类,加载Spring的IOC容器,并从中获取对应的bean对象

public class AppForDICollection {
    public static void main( String[] args ) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao UserDao = (UserDao) ctx.getBean("UserDao");
        UserDao.save();
    }
}

下面的所有配置方式,都是在UserDao的bean标签中使用进行注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl">
        <!--注入数组类型数据-->
        <property name="array">
        <array>
            <value>100</value>
            <value>200</value>
            <value>300</value>
        </array>
    </property>
        
        <!--注入List类型数据-->
        <property name="list">
        <list>
            <value>duan</value>
            <value>thirty</value>
        </list>
    </property>
        
        <!--注入Set类型数据-->
        <property name="set">
        <set>
            <value>duan</value>
            <value>thirty</value>
        </set>
    </property>
        
        <!--注入Map类型数据-->
        <property name="map">
        <map>
            <entry key="country" value="china"/>
            <entry key="province" value="yunnan"/>
            <entry key="city" value="jianshui"/>
        </map>
    </property>
        
        <!--注入Properties类型数据-->
        <property name="properties">
        <props>
            <prop key="country">china</prop>
            <prop key="province">henan</prop>
            <prop key="city">kaifeng</prop>
        </props>
    </property>
    </bean>
</beans>

配置完成后,运行结果为:

userDao添加数据
遍历数组[100, 200, 300]
遍历List[duan, thirty]
遍历Set[duan, thirty]
遍历Map{[country=china], [province=yunnan], [city=jianshui]}
遍历Properties

说明:

  • property标签表示setter方式注入,构造方式注入constructor-arg标签内部也可以写<array><list><set><map><props>标签
  • List的底层也是通过数组实现的,所以<list><array>标签是可以混用
  • 集合中要添加引用类型,只需要把<value>标签改成<ref>标签,这种方式用的比较少

6,第三方bean

内容

  • IOC/DI配置管理第三方bean
  • IOC/DI的注解开发
  • IOC/DI注解管理第三方bean
  • Spring与Mybatis及Junit的整合开发

6.1,IOC/DI配置管理第三方bean

6.1.1 案例:数据源对象管理

6.1.1.1 环境准备
  • 创建一个Maven项目

  • pom.xml添加依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • resources下添加spring的配置文件applicationContext.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
                http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd">
    </beans>
    
  • 编写运行类

    public class Application {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        }
    }
    
6.1.1.2 思路分析

需求:使用Spring的IOC容器来管理Druid连接池对象

1.使用第三方的技术,需要在pom.xml添加依赖

2.在配置文件中将【第三方的类】制作成一个bean,让IOC容器进行管理

3.数据库连接需要基础的四要素驱动连接用户名密码,【如何注入】到对应的bean中

4.从IOC容器中获取对应的bean对象,将其打印到控制台查看结果

6.1.1.3 实现Druid管理

步骤1:导入druid的依赖

pom.xml中添加依赖

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

步骤2:配置第三方bean

在applicationContext.xml配置文件中添加DruidDataSource的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
  <!--管理DruidDataSource对象-->
    <bean class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring_db?serviceTimezone=Asia/Shanghai?serviceTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="1234"/>
    </bean>
</beans>

说明:

  • 数据库连接的四要素要和自己使用的数据库信息一致。

步骤3:从IOC容器中获取对应的bean对象

public class Application {
    public static void main(String[] args) {
       ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
       DataSource dataSource = (DataSource) ctx.getBean("dataSource");
       System.out.println(dataSource);
    }
}

步骤4:运行程序

打印结果显示Druid连接池对象: 说明第三方bean对象已经被spring的IOC容器进行管理

6.1.1.4 实现C3P0管理

需求:使用Spring的IOC容器来管理C3P0连接池对象

实现方案和上面基本一致,重点要关注管理的是哪个bean对象`?

步骤1:导入C3P0的依赖

pom.xml中添加依赖

<dependency>
    <groupId>c3p0</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.1.2</version>
</dependency>

步骤2:配置第三方bean

在applicationContext.xml配置文件中添加配置

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/spring_db?serviceTimezone=Asia/Shanghai?serviceTimezone=Asia/Shanghai"/>
    <property name="user" value="root"/>
    <property name="password" value="1234"/>
    <property name="maxPoolSize" value="1000"/>
</bean>

注意:

  • ComboPooledDataSource的属性是通过setter方式进行注入
  • 想注入属性就需要在ComboPooledDataSource类或其上层类中有提供属性对应的setter方法
  • C3P0的四个属性和Druid的四个属性是不一样的

步骤3:运行程序

程序会报错,报的错为ClassNotFoundException,翻译出来是类没有发现的异常,具体的类为com.mysql.cj.jdbc.Driver。错误的原因是缺少mysql的驱动包。

分析出错误的原因,只需要在pom.xml把驱动包引入即可。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.24</version>
</dependency>

添加完mysql的驱动包以后,再次运行启动类,就可以打印出结果:

注意
  • 数据连接池在配置属性的时候,除了可以注入数据库连接四要素外还可以配置很多其他的属性,具体都有哪些属性用到的时候再去查,一般配置基础的四个,其他都有自己的默认值
  • Druid和C3P0在没有导入mysql驱动包的前提下,一个没报错一个报错,说明Druid在初始化的时候没有去加载驱动,而C3P0刚好相反
  • Druid程序运行虽然没有报错,但是当调用DruidDataSource的getConnection()方法获取连接的时候,也会报找不到驱动类的错误

6.1.2 加载properties文件

上面实现第三方bean包含了一些问题:

  • 这两个数据源中都使用到了一些固定的常量如数据库连接四要素,把这些值写在Spring的配置文件中不利于后期维护
  • 需要将这些值提取到一个外部的properties配置文件中
  • Spring框架如何从配置文件中读取属性值来配置就是接下来要解决的问题。
6.1.2.1 第三方bean属性优化

实现思路

需求:将数据库连接四要素提取到properties配置文件,spring来加载配置信息并使用这些信息来完成属性注入。

1.在resources下创建一个jdbc.properties(文件的名称可以任意)

2.将数据库连接四要素配置到配置文件中

3.在Spring的配置文件中加载properties文件

4.使用加载到的值实现属性注入

6.1.2.1.2 实现步骤

步骤1:准备properties配置文件

resources下创建一个jdbc.properties文件,并添加对应的属性键值对

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

步骤2:在applicationContext.xml中完成属性注入

使用${key}来读取properties配置文件中的内容并完成属性注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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">
    
    <context:property-placeholder location="jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

至此,读取外部properties配置文件完成。

注意事项

至此,读取properties配置文件中的内容就已经完成,但是在使用的时候,有些注意事项:

  • 问题一:键值对的key为username引发的问题

    1.在properties中配置键值对的时候,如果key设置为username

    username=root
    

    2.在applicationContext.xml注入该属性

    <?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"
           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">
        
        <context:property-placeholder location="jdbc.properties"/>
        
        <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl">
            <property name="name" value="${username}"/>
        </bean>
    </beans>
    

    3.运行后,username却不是root,而是自己电脑的用户名

    4.出现问题的原因是<context:property-placeholder/>标签会加载系统的环境变量,而且环境变量的值会被优先加载,如何查看系统的环境变量?

    public static void main(String[] args) throws Exception{
        Map<String, String> env = System.getenv();
        System.out.println(env);
    }
    

    运行打印出来的结果(计算机当前用户名称)

    USERNAME=duan

    5.解决方案

    <?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"
           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">
        
        <context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
    </beans>
    

    system-properties-mode:设置为NEVER,表示不加载系统属性,就可以解决上述问题。

    当然还有一个解决方案就是避免使用username作为属性的key

  • 问题二:当有多个properties配置文件需要被加载,该如何配置?

    1.内容如下:

    jdbc.properties

    jdbc.driver=com.mysql.jdbc.cj.Driver
    jdbc.url=jdbc:mysql://localhost/spring_db?serviceTimezone=Asia/Shanghai
    jdbc.username=root
    jdbc.password=root
    

    jdbc2.properties

    username=root666
    

    2.修改applicationContext.xml

    <?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"
           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">
        <!--方式一 -->
        <context:property-placeholder location="jdbc.properties,jdbc2.properties" system-properties-mode="NEVER"/>
        <!--方式二-->
        <context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
        <!--方式三 -->
        <context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>
        <!--方式四-->
        <context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
    </beans>  
    

    说明:

    • 方式一:可以实现,如果配置文件多的话,每个都需要配置
    • 方式二:*.properties代表所有以properties结尾的文件都会被加载,可以解决方式一的问题,但是不标准
    • 方式三:标准的写法,classpath:代表的是从根路径下开始查找,但是只能查询当前项目的根路径
    • 方式四:不仅可以加载当前项目还可以加载当前项目所依赖的所有项目的根路径下的properties配置文件

小结

  • 如何开启context命名空间

    1629980280952

  • 如何加载properties配置文件

    <context:property-placeholder location="" system-properties-mode="NEVER"/>
    
  • 如何在applicationContext.xml引入properties配置文件中的值

    ${key}
    

6.2,核心容器

6.2.1 环境准备

  • 创建一个Maven项目

  • pom.xml添加Spring的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • resources下添加applicationContext.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    </beans>
    
  • 添加UserDao和UserDaoImpl类

    public interface UserDao {
        public void save();
    }
    public class UserDaoImpl implements UserDao {
        public void save() {
            System.out.println("userDao被加载" );
        }
    }
    
  • 创建运行类App

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserDao UserDao = (UserDao) ctx.getBean("UserDao");
            UserDao.save();
        }
    }
    

6.2.2 容器

6.2.2.1 容器的创建方式

案例中创建ApplicationContext的方式为:

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

这种方式翻译为:类路径下的XML配置文件

除了上面这种方式,Spring还提供了另外一种创建方式为:

ApplicationContext ctx = new FileSystemXmlApplicationContext("applicationContext.xml");

这种方式翻译为:文件系统下的XML配置文件

使用这种方式,运行,会出现如下错误:

1629983245121

从错误中能发现,这种方式是从项目路径下开始查找配置文件的,所以需要将其修改为绝对路径:

ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\workspace\\spring\\src\\main\\resources\\applicationContext.xml"); 

这种方式虽能实现,但是当项目的位置发生变化后,代码也需要跟着改,耦合度较高,不推荐使用。

6.2.2.2 Bean的三种获取方式

方式一,就是目前案例中获取的方式:

UserDao UserDao = (UserDao) ctx.getBean("UserDao");

这种方式存在的问题是每次获取的时候都需要进行类型转换,有没有更简单的方式呢?

方式二:

UserDao UserDao = ctx.getBean("UserDao",UserDao.class);

这种方式可以解决类型强转问题,但是参数又多加了一个,相对来说没有简化多少。

方式三:

UserDao UserDao = ctx.getBean(UserDao.class);

这种方式就类似按类型注入。必须要确保IOC容器中该类型对应的bean对象只能有一个。

6.2.2.3 容器类层次结构

(1)在IDEA中双击shift,输入BeanFactory

1629985148294

(2)点击进入BeanFactory类,ctrl+h,就能查看到如下结构的层次关系

1629984980781

6.2.2.4 BeanFactory的使用

使用BeanFactory来创建IOC容器的具体实现方式为:

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

为了更好的看出BeanFactoryApplicationContext之间的区别,在UserDaoImpl添加如下构造函数:

public class UserDaoImpl implements UserDao {
    public UserDaoImpl() {
        System.out.println("constructor");
    }
    public void save() {
        System.out.println("userDao被加载" );
    }
}

如果不去获取bean对象,打印会发现:

  • BeanFactory是延迟加载,只有在获取bean对象的时候才会去创建

  • ApplicationContext是立即加载,容器加载的时候就会创建bean对象

  • ApplicationContext要想成为延迟加载,只需要按照如下方式进行配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"  lazy-init="true"/>
    </beans>
    

小结

  • 容器创建的两种方式

    • ClassPathXmlApplicationContext[掌握]
    • FileSystemXmlApplicationContext[了解]
  • 获取Bean的三种方式

    • getBean("名称"):需要类型转换
    • getBean("名称",类型.class):多了一个参数
    • getBean(类型.class):容器中不能有多个该类的bean对象

    上述三种方式,各有各的优缺点,用哪个都可以。

  • 容器类层次结构

    • 只需要知晓容器的最上级的父接口为 BeanFactory即可
  • BeanFactory

    • 使用BeanFactory创建的容器是延迟加载
    • 使用ApplicationContext创建的容器是立即加载
    • 具体BeanFactory如何创建只需要了解即可。

总结

1.容器相关
  • BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
  • ApplicationContext接口是Spring容器的核心接口,初始化时bean立即加载
  • ApplicationContext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
  • ApplicationContext接口常用初始化类
    • ClassPathXmlApplicationContext(常用)
    • FileSystemXmlApplicationContext
2.bean相关
<bean 
    //bean的id
    id = "userDao"
    //bean的别名
    name = "dao"
    //bean的类型
    class = "com.duan.dao.impl.UserDaoImpl"
    //bean的实例数量
    scope = "singleton"
    //生命周期的初始化方法
    init-method = "init"
    //生命周期销毁方法 
    destroy-method = "destory"
    //自动装配类型
    autowire = "byType"
    //bean的工厂方法,应用于静态工厂和实例工厂
    factory-method = "getInstance" 
    //实例工厂bean
    factory-bean = "com.duan.factory.UserDaoFactory"
    //控制bean的延迟加载
    lazy-init = "true"
/>

其实整个配置中最常用的就两个属性idclass

3.依赖注入相关
<bean id="UserService" class="com.duan.service.impl.userServiceImpl">
    //构造器注入引用类型,一般用户第三方技术整合
    <constructor-arg name="userDao" ref="userDao" />
    <constructor-arg name="userDao" ref="userDao" />
    //构造器注入简单类型
    <constructor-arg name="msg" value="WARN " />
    //类型匹配与索引匹配
    <constructor-arg type="java.lang.String" index="3" value="WARN"/>
    //setter注入引用类型
    <property name="userDao" ref="userDao" />
    <property name="userDao" ref="userDao" />  
    //setter注入简单类型    
    <property name="msg" value="WARN"/>
    //setter注入集合类型
    <property name="names">
        //list集合
        <List>
            //集合注入简单类型
            <value>duan</value>
            //集合注入引用类型
            <ref bean="dataSource" />
        </List>
    </ property>
</bean>

6.3,IOC/DI注解开发

Spring的IOC/DI对应的配置开发使用起来相对来说还是比较复杂的,复杂的地方在配置文件

Spring可以简化代码的开发,到现在并没有体会到。

所以Spring到底是如何简化代码开发的呢?

要想真正简化开发,就需要用到Spring的注解开发,Spring对注解支持的版本历程:

  • 2.0版开始支持注解
  • 2.5版注解功能趋于完善
  • 3.0版支持纯注解开发

6.3.1 环境准备

  • 创建一个Maven项目

  • pom.xml添加Spring的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • resources下添加applicationContext.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>
    </beans>
    
  • 添加UserDao、UserDaoImpl、userService、userServiceImpl类

    public interface UserDao {
        public void save();
    }
    public class UserDaoImpl implements UserDao {
        public void save() {
            System.out.println("userDao被加载" );
        }
    }
    public interface userService {
        public void save();
    }
    
    public class userServiceImpl implements userService {
        public void save() {
            System.out.println("userService被加载");
        }
    }
    
    
  • 创建运行类App

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserDao UserDao = (UserDao) ctx.getBean("UserDao");
            UserDao.save();
        }
    }
    

6.3.2 注解开发定义bean

步骤1:删除原XML配置

将配置文件中的<bean>标签删除掉

<bean id="UserDao" class="com.duan.dao.impl.UserDaoImpl"/>

步骤2:Dao上添加注解

在UserDaoImpl类上添加@Component注解

//相当于给bean起一个id为UserDao
@Component("UserDao")
public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("userDao被加载" );
    }
}

注意:@Component注解不可以添加在接口上,因为接口是无法创建对象的。

步骤3:配置Spring的注解包扫描

为了让Spring框架能够扫描到写在类上的注解,需要在配置文件上进行包扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <context:component-scan base-package="com.duan"/>
</beans>

说明:

component-scan

  • component:组件,Spring将管理的bean视作自己的一个组件
  • scan:扫描

base-package指定Spring框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。

  • 包路径越多[如:com.duan.dao.impl],扫描的范围越小速度越快
  • 包路径越少[如:com.duan],扫描的范围越大速度越慢
  • 一般扫描到项目的组织名称即Maven的groupId下[如:com.duan]即可。

步骤4:Service上添加注解

在userServiceImpl类上也添加@Component交给Spring框架管理

@Component
public class userServiceImpl implements userService {
    private UserDao UserDao;

    public void setUserDao(UserDao UserDao) {
        this.UserDao = UserDao;
    }

    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

步骤5:运行程序

在App类中,从IOC容器中获取userServiceImpl对应的bean对象,打印

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao UserDao = (UserDao) ctx.getBean("UserDao");
        System.out.println(UserDao);
        //按类型获取bean
        userService userService = ctx.getBean(userService.class);
        System.out.println(userService);
    }
}

打印观察结果,两个bean对象都已经打印到控制台

com.duan.dao.impl.UserDaoImpl@57f23557
com.duan.service.impl.UserServiceImpl@3d7741f


说明:

  • userServiceImpl类没有起名称,所以在App中是按照类型来获取bean对象

  • @Component注解如果不起名称,会有一个默认值就是当前类名首字母小写,所以也可以按照名称获取,如

    userService userService = (userService)ctx.getBean("userServiceImpl");
    System.out.println(userService);
    

对于@Component注解,还衍生出了其他三个注解@Controller@Service@Repository

通过查看源码会发现:

1630028345074

这三个注解和@Component注解的作用是一样的,为什么要衍生出这三个呢?

方便后期在编写类的时候能很好的区分出这个类是属于表现层业务层还是数据层的类。

6.3.3 纯注解开发模式

Spring在3.0版已经支持纯注解开发

  • Spring3.0开启了纯注解开发模式,使用Java类替代配置文件。
6.3.3.1 思路分析

实现思路为:

  • 将配置文件applicationContext.xml删除掉,使用类来替换。
6.3.3.2 实现步骤

步骤1:创建配置类

创建一个配置类SpringConfig

步骤2:声明该类为配置类

在配置类上添加@Configuration注解,将其标识为一个配置类

步骤3:用注解替换包扫描配置

在配置类上添加包扫描注解@ComponentScan

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

步骤4:创建运行类并执行

创建一个新的运行类AppForAnnotation

public class AppForAnnotation {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserDao UserDao = (UserDao) ctx.getBean("UserDao");
        System.out.println(UserDao);
        userService userService = ctx.getBean(userService.class);
        System.out.println(userService);
    }
}

运行AppForAnnotation,两个对象输出成功

至此,纯注解开发的方式就已经完成了,主要内容包括:

  • Java类替换Spring核心配置文件

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

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

    @ComponentScan({com.duan.service","com.duan.dao"})
    
  • 读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象

    //加载配置文件初始化容器
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    //加载配置类初始化容器
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    
小结
  • @Component、@Controller、@Service、@Repository这四个注解
  • applicationContext.xml中<context:component-san/>的作用是指定扫描包路径,注解为@ComponentScan
  • @Configuration标识该类为配置类,使用类替换applicationContext.xml文件
  • ClassPathXmlApplicationContext是加载XML配置文件
  • AnnotationConfigApplicationContext是加载配置类

6.3.4 注解开发bean作用范围与生命周期管理

6.3.4.1 环境准备
  • 创建一个Maven项目

  • pom.xml添加Spring的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • 添加一个配置类SpringConfig

    @Configuration
    @ComponentScan("com.duan")
    public class SpringConfig {
    }
    
  • 添加UserDao、UserDaoImpl类

    public interface UserDao {
        public void save();
    }
    @Repository
    public class UserDaoImpl implements UserDao {
        public void save() {
            System.out.println("userDao被加载" );
        }
    }
    
  • 创建运行类App

    public class App {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            UserDao UserDao1 = ctx.getBean(UserDao.class);
            UserDao UserDao2 = ctx.getBean(UserDao.class);
            System.out.println(UserDao1);
            System.out.println(UserDao2);
        }
    }
    
6.3.4.2 Bean的作用范围

(1)运行App类,在控制台打印两个一摸一样的地址,说明默认情况下bean是单例

(2)要想将UserDaoImpl变成非单例,只需要在其类上添加@scope注解

@Repository
//@Scope设置bean的作用范围
@Scope("prototype")
public class UserDaoImpl implements UserDao {

    public void save() {
        System.out.println("userDao被加载");
    }
}
名称 @Scope
类型 类注解
位置 类定义上方
作用 设置该类创建对象的作用范围
可用于设置创建出的bean是否为单例对象
属性 value(默认):定义bean作用范围,
默认值singleton(单例),可选值prototype(非单例)
6.3.4.3 Bean的生命周期

(1)在UserDaoImpl中添加两个方法,initdestroy,方法名可以任意

@Repository
public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("userDao被加载");
    }
    public void init() {
        System.out.println("init ...");
    }
    public void destroy() {
        System.out.println("destroy ...");
    }
}

(2)如何对方法进行标识,哪个是初始化方法,哪个是销毁方法?

只需要在对应的方法上添加@PostConstruct@PreDestroy注解即可。

@Repository
public class UserDaoImpl implements UserDao {
    @override
    public void save() {
        System.out.println("userDao被加载");
    }
    @override
    @PostConstruct //在构造方法之后执行,替换 init-method
    public void init() {
        System.out.println("init ...");
    }
    @override
    @PreDestroy //在销毁方法之前执行,替换 destroy-method
    public void destroy() {
        System.out.println("destroy ...");
    }
}

(3)需要注意的是destroy只有在容器关闭的时候,才会执行,所以需要手动关闭容器

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserDao UserDao1 = ctx.getBean(UserDao.class);
        UserDao UserDao2 = ctx.getBean(UserDao.class);
        System.out.println(UserDao1);
        System.out.println(UserDao2);
        ctx.close(); //关闭容器
    }
}

注意:@PostConstruct和@PreDestroy注解如果找不到,需要导入下面的jar包

<dependency>
  <groupId>javax.annotation</groupId>
  <artifactId>javax.annotation-api</artifactId>
  <version>1.3.2</version>
</dependency>

找不到的原因是,从JDK9以后jdk中的javax.annotation包被移除了,这两个注解刚好就在这个包中。

@PostConstruct
名称 @PostConstruct
类型 方法注解
位置 方法上
作用 设置该方法为初始化方法
属性
@PreDestroy
名称 @PreDestroy
类型 方法注解
位置 方法上
作用 设置该方法为销毁方法
属性

小结

1630033039358

6.3.5 注解开发依赖注入

Spring为了使用注解简化开发,并没有提供构造函数注入setter注入对应的注解,只提供了自动装配的注解实现。

6.3.5.1 环境准备

在学习之前,把案例环境介绍下:

  • 创建一个Maven项目

  • pom.xml添加Spring的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • 添加一个配置类SpringConfig

    @Configuration
    @ComponentScan("com.duan")
    public class SpringConfig {
    }
    
  • 添加UserDao、UserDaoImpl、userService、userServiceImpl类

    public interface UserDao {
        public void save();
    }
    @Repository
    public class UserDaoImpl implements UserDao {
        public void save() {
            System.out.println("userDao被加载" );
        }
    }
    public interface userService {
        public void save();
    }
    @Service
    public class userServiceImpl implements userService {
        private UserDao UserDao;
      public void setUserDao(UserDao UserDao) {
            this.UserDao = UserDao;
        }
        @override
        public void save() {
            System.out.println("userService被加载");
            UserDao.save();
        }
    }
    
  • 创建运行类App

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

环境准备好后,运行后报错:NullPointerException空指针异常

出现问题的原因是,在userServiceImpl类中添加了UserDao的属性,并提供了setter方法,但是目前是没有提供配置注入UserDao的,所以UserDao对象为Null,调用其save方法就会报控指针异常

6.3.5.2 注解实现按照类型注入

对于这个问题使用注解该如何解决?

(1) 在userServiceImpl类的UserDao属性上添加@Autowired注解

@Service
public class userServiceImpl implements userService {
    @Autowired
    private UserDao UserDao;
    
//    public void setUserDao(UserDao UserDao) {
//        this.UserDao = UserDao;
//    }
    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

注意:

  • @Autowired可以写在属性上,也可也写在setter方法上,最简单的处理方式是写在属性上并将setter方法删除掉
  • 为什么setter方法可以删除呢?
    • 自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值
    • 普通反射只能获取public修饰的内容
    • 暴力反射除了获取public修饰的内容还可以获取private修改的内容
    • 所以此处无需提供setter方法

(2)@Autowired是按照类型注入,那么对应UserDao接口如果有多个实现类,比如添加UserDaoImpl2

@Repository
public class UserDaoImpl2 implements UserDao {
    public void save() {
        System.out.println("userDao被加载2");
    }
}

这个时候再次运行App,就会报错NoUniqueBeanDefinitionException

此时,按照类型注入就无法区分到底注入哪个对象,解决方案:按照名称注入

  • 先给两个Dao类分别起个名称

    @Repository("UserDao")
    public class UserDaoImpl implements UserDao {
        public void save() {
            System.out.println("userDao被加载" );
        }
    }
    @Repository("UserDao2")
    public class UserDaoImpl2 implements UserDao {
        public void save() {
            System.out.println("userDao被加载2" );
        }
    }
    

    此时就可以注入成功,但是得思考个问题:

    • @Autowired是按照类型注入的,给UserDao的两个实现类起名称,它还是有两个bean对象,为什么不报错?

    • @Autowired默认按照类型自动装配,如果IOC容器中同类的Bean找到多个,就按照变量名和Bean的名称匹配。因为变量名叫UserDao而容器中也有一个userDao,所以可以成功注入。

    • 如果将两个dao类改为下面 代码,是否能注入成功?

      @Repository("UserDao1")
      public class UserDaoImpl implements UserDao {
          public void save() {
              System.out.println("userDao被加载" );
          }
      }
      @Repository("UserDao2")
      public class UserDaoImpl2 implements UserDao {
          public void save() {
              System.out.println("userDao被加载2" );
          }
      }
      

      不行,因为按照类型会找到多个bean对象,此时会按照UserDao名称去找,因为IOC容器只有名称叫UserDao1UserDao2,所以找不到,会报NoUniqueBeanDefinitionException

6.3.5.3 注解实现按照名称注入

当根据类型在容器中找到多个bean,注入参数的属性名又和容器中bean的名称不一致,这个时候该如何解决,就需要使用到@Qualifier来指定注入哪个名称的bean对象。

@Service
public class userServiceImpl implements userService {
    @Autowired
    @Qualifier("UserDao1")
    private UserDao UserDao;
    
    public void save() {
        System.out.println("userService被加载");
        UserDao.save();
    }
}

@Qualifier注解后的值就是需要注入的bean的名称。

注意:@Qualifier不能独立使用,必须和@Autowired一起使用

6.3.5.4 简单数据类型注入

引用类型看完,简单类型注入就比较容易懂了。简单类型注入的是基本数据类型或者字符串类型,下面在UserDaoImpl类中添加一个name属性,用其进行简单类型注入

@Repository("UserDao")
public class UserDaoImpl implements UserDao {
    private String name;
    public void save() {
        System.out.println("userDao被加载" + name);
    }
}

数据类型换了,对应的注解也要跟着换,这次使用@Value注解,将值写入注解的参数中就行了

@Repository("UserDao")
public class UserDaoImpl implements UserDao {
    @Value("duan")
    private String name;
    public void save() {
        System.out.println("userDao被加载" + name);
    }
}

注意数据格式要匹配,如将"abc"注入给int值,这样程序就会报错。

为什么这个注解好像没什么用,跟直接赋值是一个效果,还没有直接赋值简单,所以这个注解存在的意义是什么?

6.3.5.5 注解读取properties配置文件

@Value一般会被用在从properties配置文件中读取内容进行使用,具体如何实现?

步骤1:resource下准备properties文件

jdbc.properties

name=duan888

步骤2: 使用注解加载properties配置文件

在配置类上添加@PropertySource注解

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

步骤3:使用@Value读取配置文件中的内容

@Repository("UserDao")
public class UserDaoImpl implements UserDao {
    @Value("${name}")  //读取文件中名称为name属性的值
    private String name;
    public void save() {
        System.out.println("userDao被加载" + name);
    }
}

注意:

  • 如果读取的properties配置文件有多个,可以使用@PropertySource的属性来指定多个

    @PropertySource({"jdbc.properties","xxx.properties"})
    
  • @PropertySource注解属性中不支持使用通配符*,运行会报错

    @PropertySource({"*.properties"})
    
  • @PropertySource注解属性中可以把classpath:加上,代表从当前项目的根路径找文件

    @PropertySource({"classpath:jdbc.properties"})
    
@Autowired
名称 @Autowired
类型 属性注解 或 方法注解(了解) 或 方法形参注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方 或 方法形参前面
作用 为引用类型属性设置值
属性 required:true/false,定义该属性是否允许为null
@Qualifier
名称 @Qualifier
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方
作用 为引用类型属性指定注入的beanId
属性 value(默认):设置注入的beanId
@Value
名称 @Value
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方
作用 为 基本数据类型 或 字符串类型 属性设置值
属性 value(默认):要注入的属性值
@PropertySource
名称 @PropertySource
类型 类注解
位置 类定义上方
作用 加载properties文件中的属性值
属性 value(默认):设置加载的properties文件对应的文件名或文件名组成的数组

6.4,IOC/DI注解开发管理第三方bean

如果是第三方的类,这些类都是在jar包中,没法在类上面添加注解,就要用到了注解@Bean

这个注解该如何使用呢?

6.4.1 环境准备

学习@Bean注解之前先来准备环境:

  • 创建一个Maven项目

  • pom.xml添加Spring的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • 添加一个配置类SpringConfig

    @Configuration
    public class SpringConfig {
    }
    
  • 添加UserDao、UserDaoImpl类

    public interface UserDao {
        public void save();
    }
    @Repository
    public class UserDaoImpl implements UserDao {
        public void save() {
            System.out.println("userDao被加载" );
        }
    }
    
  • 创建运行类App

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

6.4.2 注解开发管理第三方bean

在上述环境中完成对Druid数据源的管理,具体的实现步骤为:

步骤1:导入对应的jar包

步骤2:在配置类中添加一个方法

注意该方法的返回值就是要创建的Bean对象类型

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

步骤3:在方法上添加@Bean注解

@Bean注解的作用是将方法的返回值制作为Spring管理的一个bean对象

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

注意:不能使用DataSource ds = new DruidDataSource()

因为DataSource接口中没有对应的setter方法来设置属性。

步骤4:从IOC容器中获取对象并打印

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        DataSource dataSource = ctx.getBean(DataSource.class);
        System.out.println(dataSource);
    }
}

6.4.3 引入外部配置类

如果把所有的第三方bean都配置到Spring的配置类SpringConfig中,虽然可以,但是不利于代码阅读和分类管理,所有得按照类别将这些bean配置到不同的配置类中?

对于数据源的bean,新建一个JdbcConfig配置类,并把数据源配置到该类下。

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

现在的问题是,这个配置类如何能被Spring配置类加载到,并创建DataSource对象在IOC容器中?

针对这个问题,有两个解决方案:

6.4.3.1 使用包扫描引入

步骤1:在Spring的配置类上添加包扫描

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

步骤2:在JdbcConfig上添加配置注解

JdbcConfig类要放入到com.duan.config包下,被Spring的配置类扫描到即可

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

步骤3:运行程序

依然能获取到bean对象并打印控制台。

这种方式虽然能够扫描到,但是不能很快的知晓都引入了哪些配置类,所有这种方式不推荐使用。

6.4.3.2 使用@Import引入

方案一实现起来有点小复杂,Spring提供了第二种方案。

这种方案可以不用加@Configuration注解,但是必须在Spring配置类上使用@Import注解手动引入需要加载的配置类

步骤1:去除JdbcConfig类上的注解

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

步骤2:在Spring配置类中引入

@Configuration
//@ComponentScan("com.duan.config")
@Import({JdbcConfig.class})
public class SpringConfig {
  
}

注意:

  • 扫描注解可以移除

  • @Import参数需要的是一个数组,可以引入多个配置类。

  • @Import注解在配置类中只能写一次,下面的方式是不允许的

    @Configuration
    //@ComponentScan("com.duan.config")
    @Import(JdbcConfig.class)
    @Import(Xxx.class)
    public class SpringConfig {
      
    }
    

步骤3:运行程序

依然能获取到bean对象并打印控制台

@Bean
名称 @Bean
类型 方法注解
位置 方法定义上方
作用 设置该方法的返回值作为spring管理的bean
属性 value(默认):定义bean的id
@Import
名称 @Import
类型 类注解
位置 类定义上方
作用 导入配置类
属性 value(默认):定义导入的配置类类名,
当配置类有多个时使用数组格式一次性导入多个配置类

6.4.4 注解开发实现为第三方bean注入资源

在使用@Bean创建bean对象的时候,如果方法在创建的过程中需要其他资源该怎么办?

这些资源会有两大类,分别是简单数据类型引用数据类型

6.4.4.1 简单数据类型

对于下面代码关于数据库的四要素不应该写死在代码中,应该是从properties配置文件中读取。

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

步骤1:类中提供四个属性

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

步骤2:使用@Value注解引入值

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

现在的数据库连接四要素还是写在代码中,需要做的是将这些内容提取到jdbc.properties配置文件

1.resources目录下添加jdbc.properties

2.配置文件中提供四个键值对分别是数据库的四要素

3.使用@PropertySource加载jdbc.properties配置文件

4.修改@Value注解属性的值,将其修改为${key},key就是键值对中的键的值

6.4.4.2 引用数据类型

步骤1:在SpringConfig中扫描UserDao

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

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

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

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

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

总结

XML配置和注解的开发实现以及它们之间的差异:

1630134786448

6.5. Spring整合Mybatis

6.5.1 环境准备

在准备环境的过程中,回顾下Mybatis开发的相关内容:

步骤1:准备数据库表

Mybatis是来操作数据库表,所以先创建一个数据库及表

create database spring_db character set utf8;
use spring_db;
create table tbl_account(
    id int primary key auto_increment,
    name varchar(35),
    money double
);

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

项目的pom.xml添加相关依赖

<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.24</version>
    </dependency>
	<dependency>
    	<groupId>org.projectlombok</groupId>
    	<artifactId>lombok</artifactId>
    	<version>1.18.12</version>
	</dependency>
</dependencies>

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

@Date
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;
  //toString...    
}

步骤4:创建Dao接口

public interface AccountDao {

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

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

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

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

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

步骤5:创建Service接口和实现类

public interface AccountService {

    void save(Account account);

    void delete(Integer id);

    void update(Account account);

    List<Account> findAll();

    Account findById(Integer id);

}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
        accountDao.save(account);
    }

    public void update(Account account){
        accountDao.update(account);
    }

    public void delete(Integer id) {
        accountDao.delete(id);
    }

    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    public List<Account> findAll() {
        return accountDao.findAll();
    }
}

步骤6:添加jdbc.properties文件

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

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?serviceTimezone=Asia/Shanghai?useSSL=false
jdbc.username=root
jdbc.password=root

useSSL:关闭MySQL的SSL连接

步骤7:添加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>
    <!--读取外部properties配置文件-->
    <properties resource="jdbc.properties"></properties>
    <!--别名扫描的包路径-->
    <typeAliases>
        <package name="com.duan.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>
    <!--映射文件扫描包路径-->
    <mappers>
        <package name="com.duan.dao"></package>
    </mappers>
</configuration>

步骤8:编写应用程序

public class App {
    public static void main(String[] args) throws IOException {
        // 1. 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 2. 加载SqlMapConfig.xml配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak");
        // 3. 创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // 4. 获取SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 5. 执行SqlSession对象执行查询,获取结果User
        AccountDao accountDao = sqlSession.getMapper(AccountDao.class);

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

        // 6. 释放资源
        sqlSession.close();
    }
}

6.5.2 整合思路

Spring与Mybatis的整合,大体需要做两件事,

第一件事是:Spring要管理MyBatis中的SqlSessionFactory

第二件事是:Spring要管理Mapper接口的扫描

具体的步骤为:

步骤1:项目中导入整合需要的jar包

<dependency>
    <!--Spring操作数据库需要该jar包-->
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<dependency>
    <!--
    Spring与Mybatis整合的jar包
    这个jar包mybatis在前面,是Mybatis提供的
  -->
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
</dependency>

步骤2:创建Spring的主配置类

//配置类注解
@Configuration
//包扫描,主要扫描的是项目中的AccountServiceImpl类
@ComponentScan("com.duan")
public class SpringConfig {
}

步骤3:创建数据源的配置类

在配置类中完成数据源的创建

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

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

步骤4:主配置类中读properties并引入数据源配置类

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

步骤5:创建Mybatis配置类并配置SqlSessionFactory

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

说明:

  • 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息

    1630138835057

    • SqlSessionFactoryBean是FactoryBean的一个子类,在该类中将SqlSessionFactory的创建进行了封装,简化对象的创建,只需要将其需要的内容设置即可。
    • 方法中有一个参数为dataSource,当前Spring容器中已经创建了Druid数据源,类型刚好是DataSource类型,此时在初始化SqlSessionFactoryBean这个对象的时候,发现需要使用DataSource对象,而容器中刚好有这么一个对象,就自动加载了DruidDataSource对象。
  • 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IOC容器中

    1630138916939

    • 这个MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类,用来处理原始配置文件中的mappers相关配置,加载数据层的Mapper接口类
    • MapperScannerConfigurer有一个核心属性basePackage,就是用来设置所扫描的包路径

步骤6:主配置类中引入Mybatis配置类

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

步骤7:编写运行类

在运行类中,从IOC容器中获取Service对象,调用方法获取结果

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

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

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

支持Spring与Mybatis的整合就已经完成了,其中主要用到的两个类分别是:

  • SqlSessionFactoryBean
  • MapperScannerConfigurer

6.6. Spring整合Junit

整合Junit与整合Druid和MyBatis差异比较大,Junit是一个搞单元测试用的工具,它不是程序的主体,也不会参加最终程序的运行,从作用上来说就和之前的东西不一样,它不是做功能的,看做是一个辅助工具就可以了。

6.6.1 环境准备

这块环境,直接使用Spring与Mybatis整合的环境即可。

6.6.2 整合Junit步骤

步骤1:引入依赖

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

步骤2:编写测试类

在test\java下创建一个AccountServiceTest,这个名字任意

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

注意:

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

@RunWith

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

@ContextConfiguration

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

7,Aop

7.1,AOP简介

Spring有两个核心的概念,一个是IOC/DI,一个是AOP

AOP是在不改动原有代码的前提下对其进行增强。

7.1.1 什么是AOP?

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

7.1.2 AOP作用

  • 作用:在不惊动原始设计的基础上为其进行功能增强,有技术就可以实现这样的功能即代理模式

7.1.3 AOP核心概念

为了能更好的理解AOP的相关概念,需要准备一个环境,整个环境的内容暂时可以不用关注,最主要的类为:UserDaoImpl

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

当在App类中从容器中获取UserDao对象后,分别执行其save,delete,updateselect方法后会有如下的打印结果:

这个时候,就会发现

  • 对于计算万次执行消耗的时间只有save方法有,为什么delete和update方法也会有呢?
  • delete和update方法有,那什么select方法为什么又没有呢?

其实就使用了Spring的AOP,在不惊动(改动)原有设计(代码)的前提下,想给谁添加功能就给谁添加。这个也就是Spring的理念:

  • 无入侵式/无侵入式,Spring到底是如何实现的呢?

1630144353462

(1)Spring的AOP是对一个类的方法在不进行任何修改的前提下实现增强。userServiceImpl中有save,update,deleteselect方法,这些方法叫连接点

(2)在userServiceImpl的四个方法中,updatedelete只有打印没有计算万次执行消耗时间,但是在运行的时候已经有该功能,那也就是说updatedelete方法都已经被增强,所以对于需要增强的方法叫切入点

(3)执行userServiceImpl的update和delete方法的时候都被添加了一个计算万次执行消耗时间的功能,将这个功能抽取到一个方法中,就是存放共性功能的方法叫通知

(4)通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,对于通知和切入点之间的关系描述叫切面

(5)通知是一个方法,方法不能独立存在需要被写在一个类中,这个类叫通知类

总结:

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

小结

这一节中主要讲解了AOP的概念与作用,以及AOP中的核心概念,学完以后大家需要能说出:

  • 什么是AOP?
  • AOP的作用是什么?
  • AOP中核心概念都有什么?
    • 连接点
    • 切入点
    • 通知
    • 通知类
    • 切面

7.2,AOP入门案例

7.2.1 需求分析

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

对于SpringAOP的开发有两种方式,XML 和 注解,因为现在注解使用的比较多,所以就采用注解完成AOP的开发。

总结需求为:使用SpringAOP的注解方式完成在方法执行的前打印出当前系统时间。

7.2.2 思路分析

步骤:

1.导入坐标(pom.xml)

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

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

4.定义切入点

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

7.2.3 环境准备

  • 创建一个Maven项目

  • pom.xml添加Spring依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • 添加UserDao和UserDaoImpl类

    public interface UserDao {
        public void save();
        public void update();
    }
    
    @Repository
    public class UserDaoImpl implements UserDao {
    
        public void save() {
            System.out.println(System.currentTimeMillis());
            System.out.println("userDao save被加载");
        }
    
        public void update(){
            System.out.println("userDao update 被加载...");
        }
    }
    
  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.duan")
    public class SpringConfig {
    }
    
  • 编写App运行类

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

说明:

  • 目前打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间
  • 对于update方法来说,就没有该功能
  • 我们要使用SpringAOP的方式在不改变update方法的前提下让其具有打印系统时间的功能。

7.2.4 AOP实现步骤

步骤1:添加依赖

pom.xml

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>
  • 因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
  • 导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。

步骤2:定义接口与实现类

环境准备的时候,UserDaoImpl已经准备好,不需要做任何修改

步骤3:定义通知类和通知

通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。

public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

类名和方法名没有要求,可以任意。

步骤4:定义切入点

UserDaoImpl中有两个方法,分别是save和update,我们要增强的是update方法,该如何定义呢?

public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

说明:

  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
  • execution及后面编写的内容,后面会有章节专门去学习。

步骤5:制作切面

切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?

public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}

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

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型。

步骤6:将通知类配给容器并标识其为切面类

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤7:开启注解格式AOP功能

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

步骤8:运行程序

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

输出结果如下,说明对原始方法进行了增强,AOP编程成功。

1638076434580
userDao save被加载
1638076434584
userDao update 被加载...

@EnableAspectJAutoProxy

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

@Aspect

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

@Pointcut

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

@Before

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

7.3,AOP工作流程

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

流程1:Spring容器启动

  • 容器启动就需要去加载bean,哪些类需要被加载呢?
  • 需要被增强的类,如:userServiceImpl
  • 通知类,如:MyAdvice
  • 注意此时bean对象还没有创建成功

流程2:读取所有切面配置中的切入点

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Pointcut("execution(void com.duan.dao.UserDao.save())")
    private void ptx(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
  • 上面这个例子中有两个切入点的配置,但是第一个ptx()并没有被使用,所以不会被读取。

流程3:初始化bean,

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

  • 注意第1步在容器启动的时候,bean对象还没有被创建成功。

  • 要被实例化bean对象的类中的方法和切入点进行匹配

    • 匹配失败,创建原始对象,如UserDao
      • 匹配失败说明不需要增强,直接调用原始对象的方法即可。
    • 匹配成功,创建原始对象(目标对象)的代理对象,如:UserDao
      • 匹配成功说明需要对其进行增强
      • 对哪个类做增强,这个类对应的对象就叫做目标对象
      • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
      • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

流程4:获取bean执行方法

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

验证容器中是否为代理对象

  • 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
  • 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。

需要注意的是:

不能直接打印对象,直接打印对象走的是对象的toString方法,不管是不是代理对象打印的结果都是一样的,原因是内部对toString方法进行了重写。

AOP核心概念

AOP的工作流程中,有两个核心概念,分别是:

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

上面这两个概念比较抽象,简单来说,

目标对象就是要增强的类[如:userServiceImpl类]对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。

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

小结

  • AOP的工作流程
  • AOP的核心概念
    • 目标对象、连接点、切入点
    • 通知类、通知
    • 切面
    • 代理
  • SpringAOP的本质或者可以说底层实现是通过代理模式。

7.4,AOP配置管理

7.4.1 AOP切入点表达式

7.4.1.1 语法格式

首先要明确两个概念:

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

对于切入点的描述,有两种方式

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

execution(void com.duan.dao.UserDao.update())

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

execution(void com.duan.dao.impl.UserDaoImpl.update())

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

对于切入点表达式的语法为:

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

使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?

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

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

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

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

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

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

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

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

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

接下来,我们把案例中使用到的切入点表达式来分析下:

public interface UserDao {
    void save();
    void update();
}
-----------------------------------------
@Repository
public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("userDao save被加载");
    }
    
    @Override
    public void update() {
        System.out.println("userDao update 被加载...");
    }
}
execution(void com.duan.dao.UserDao.update())
匹配接口,能匹配到
execution(void com.duan.dao.impl.UserDaoImpl.update())
匹配实现类,能匹配到
execution(* com.duan.dao.impl.UserDaoImpl.update())
返回值任意,能匹配到
execution(* com.duan.dao.impl.UserDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.update())
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.duan.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.duan.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

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

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

7.4.2 AOP通知类型

@Bofore("pt()")

它所代表的含义是将通知添加到切入点方法执行的前面

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

通知具体要添加到切入点的哪里?

共提供了5种通知类型:

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知(了解)
  • 抛出异常后通知(了解)
public class ClassName {
    public int methodName() {
        //代码1
        try {
            //代码2
            //原始的业务操作(可以理解为我们要增强的方法)
            //代码3
        }catch (Exception e){
            //代码4
        }
        //代码5
    }
}

(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容

(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容

(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加

(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加

(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能。

7.4.2.2 环境准备
  • 创建一个Maven项目

  • pom.xml添加Spring依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.6</version>
        </dependency>
    </dependencies>
    
  • 添加UserDao和UserDaoImpl类

    public interface UserDao {
        public void update();
        public int select();
    }
    
    @Repository
    public class UserDaoImpl implements UserDao {
        public void update(){
            System.out.println("user dao update ...");
        }
        public int select() {
            System.out.println("user dao select is running ...");
            return 100;
        }
    }
    
  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.duan")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
  • 创建通知类

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.duan.dao.UserDao.update())")
        private void pt(){}
    
        public void before() {
            System.out.println("before advice ...");
        }
    
        public void after() {
            System.out.println("after advice ...");
        }
    
        public void around(){
            System.out.println("around before advice ...");
            System.out.println("around after advice ...");
        }
    
        public void afterReturning() {
            System.out.println("afterReturning advice ...");
        }
        
        public void afterThrowing() {
            System.out.println("afterThrowing advice ...");
        }
    }
    
  • 编写App运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            UserDao UserDao = ctx.getBean(UserDao.class);
            UserDao.update();
        }
    }
    
7.4.2.3 通知类型的使用
前置通知

修改MyAdvice,在before方法上添加@Before注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Before("pt()")
    //此处也可以写成 @Before("MyAdvice.pt()"),不建议
    public void before() {
        System.out.println("before advice ...");
    }
}
  • 输出结果为:

    before advice ...

    user dao update ...


后置通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }
}

输出结果为:

before advice ...

user dao update ...

after advice ...


环绕通知
基本使用
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

输出结果为:

around before advice ...

around after advice ...


运行结果中,通知的内容打印出来,但是原始方法的内容却没有被执行。

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

说明:proceed()为什么要抛出异常?下图为proceed()源码

1630168248052

再次运行,程序原始方法已经执行

输出结果为:

around before advice ...

user dao update ...

around after advice ...

注意事项

(1)原始方法有返回值的处理

  • 修改MyAdvice,对UserDao中的select方法添加环绕通知,
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.duan.dao.UserDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}
  • 修改App类,调用select方法
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserDao UserDao = ctx.getBean(UserDao.class);
        int num = UserDao.select();
        System.out.println(num);
    }
}

运行后会报错,错误内容为:

Exception in thread "main" org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.duan.dao.UserDao.select()
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:226)
at com.sun.proxy.$Proxy19.select(Unknown Source)
at com.duan.App.main(App.java:12)

错误大概的意思是:空的返回不匹配原始方法的int返回

  • void就是返回Null
  • 原始方法就是UserDao下的select方法

所以使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.duan.dao.UserDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
}

说明:

  • 为什么返回的是Object而不是int的主要原因是Object类型更通用。
  • 在环绕通知中是可以对原始方法返回值就行修改的。
返回后通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.duan.dao.UserDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}

1630169124446

user dao select is running ...

afterReturning advice ...

100


注意:返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。这个案例大家下去可以自己练习验证下。

异常后通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.duan.dao.UserDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.duan.dao.UserDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

注意:异常后通知是需要原始方法抛出异常,可以在select()方法中添加一行代码int i = 1/0即可。如果没有抛异常,异常后通知将不会被执行。

思考:
  • 环绕通知是如何实现其他通知类型的功能的?
    • 因为环绕通知是可以控制原始方法执行的,所以把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能,如:

1630170090945

通知类型总结
知识点1:@After
名称 @After
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
知识点2:@AfterReturning
名称 @AfterReturning
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行
知识点3:@AfterThrowing
名称 @AfterThrowing
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
知识点4:@Around
名称 @Around
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

环绕通知注意事项

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

7.4.3 业务层接口执行效率

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

目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来方便优化。

具体实现的思路:

(1) 开始执行方法之前记录一个时间

(2) 执行方法

(3) 执行完方法之后记录一个时间

(4) 用后一个时间减去前一个时间的差值,就是我们需要的结果。

所以要在方法执行的前后添加业务,经过分析将采用环绕通知

说明:原始方法如果只执行一次,时间太快,两个时间差可能为0,所以要执行万次来计算时间差。

7.4.3.2 环境准备
  • 创建一个Maven项目

  • pom.xml添加Spring依赖

    <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
        </dependency>
        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.47</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>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>1.3.0</version>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    
  • 添加AccountService、AccountServiceImpl、AccountDao与Account类

    public interface AccountService {
        void save(Account account);
        void delete(Integer id);
        void update(Account account);
        List<Account> findAll();
        Account findById(Integer id);
    }
    
    @Service
    public class AccountServiceImpl implements AccountService {
    
        @Autowired
        private AccountDao accountDao;
    
        public void save(Account account) {
            accountDao.save(account);
        }
    
        public void update(Account account){
            accountDao.update(account);
        }
    
        public void delete(Integer id) {
            accountDao.delete(id);
        }
    
        public Account findById(Integer id) {
            return accountDao.findById(id);
        }
    
        public List<Account> findAll() {
            return accountDao.findAll();
        }
    }
    public interface AccountDao {
    
        @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
        void save(Account account);
    
        @Delete("delete from tbl_account where id = #{id} ")
        void delete(Integer id);
    
        @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
        void update(Account account);
    
        @Select("select * from tbl_account")
        List<Account> findAll();
    
        @Select("select * from tbl_account where id = #{id} ")
        Account findById(Integer id);
    }
    
    public class Account implements Serializable {
    
        private Integer id;
        private String name;
        private Double money;
        //setter..getter..toString方法省略
    }
    
  • resources下提供一个jdbc.properties

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring_db?serviceTimezone=Asia/Shanghai?useSSL=false
    jdbc.username=root
    jdbc.password=root
    
  • 创建相关配置类

    //Spring配置类:SpringConfig
    @Configuration
    @ComponentScan("com.duan")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class,MybatisConfig.class})
    public class SpringConfig {
    }
    //JdbcConfig配置类
    public class JdbcConfig {
        @Value("${jdbc.driver}")
        private String driver;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String userName;
        @Value("${jdbc.password}")
        private String password;
    
        @Bean
        public DataSource dataSource(){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(userName);
            ds.setPassword(password);
            return ds;
        }
    }
    //MybatisConfig配置类
    public class MybatisConfig {
    
        @Bean
        public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
            SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
            ssfb.setTypeAliasesPackage("com.duan.domain");
            ssfb.setDataSource(dataSource);
            return ssfb;
        }
    
        @Bean
        public MapperScannerConfigurer mapperScannerConfigurer(){
            MapperScannerConfigurer msc = new MapperScannerConfigurer();
            msc.setBasePackage("com.duan.dao");
            return msc;
        }
    }
    
    
  • 编写Spring整合Junit的测试类

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountServiceTestCase {
        @Autowired
        private AccountService accountService;
    
        @Test
        public void testFindById(){
            Account ac = accountService.findById(2);
        }
    
        @Test
        public void testFindAll(){
            List<Account> all = accountService.findAll();
        }
    
    }
    
7.4.3.3 功能开发

步骤1:开启SpringAOP的注解功能

在Spring的主配置文件SpringConfig类中添加注解

@EnableAspectJAutoProxy

步骤2:创建AOP的通知类

  • 该类要被Spring管理,需要添加@Component

  • 要标识该类是一个AOP的切面类,需要添加@Aspect

  • 配置切入点表达式,需要添加一个方法,并添加@Pointcut

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.duan.service.*Service.*(..))")
    private void servicePt(){}
    
    public void runSpeed(){
        
    } 
}

步骤3:添加环绕通知

在runSpeed()方法上添加@Around

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.duan.service.*Service.*(..))")
    private void servicePt(){}
    
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public Object runSpeed(ProceedingJoinPoint pjp){
        Object ret = pjp.proceed();
        return ret;
    } 
}

注意:目前并没有做任何增强

步骤4:完成核心业务,记录万次执行的时间

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.duan.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
    } 
}

步骤5:运行单元测试类

注意:因为程序每次执行的时长是不一样的,所以运行多次最终的结果是不一样的。

步骤6:程序优化

目前程序所面临的问题是,多个方法一起执行测试的时候,控制台都打印的是:

业务层接口万次执行时间:xxxms

没有办法区分到底是哪个接口的哪个方法执行的具体时间,具体如何优化?

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.duan.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        //获取执行签名信息
        Signature signature = pjp.getSignature();
        //通过签名获取执行操作名称(接口名)
        String className = signature.getDeclaringTypeName();
        //通过签名获取执行操作名称(方法名)
        String methodName = signature.getName();
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    } 
}

步骤7:运行单元测试类

结果为:

万次执行:com.duan.service.AccountService.findAll---->1461ms

万次执行:com.duan.service.AccountService.findById---->920ms

补充说明

当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程。

这块只是通过该案例把AOP的使用进行了学习,具体的实际值是有很多因素共同决定的。

7.4.4 AOP通知获取数据

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

  • pom.xml添加Spring依赖

    <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
        </dependency>
      </dependencies>
    
  • 添加UserDao和UserDaoImpl类

    public interface UserDao {
        public String findName(int id);
    }
    @Repository
    public class UserDaoImpl implements UserDao {
    
        public String findName(int id) {
            System.out.println("id:"+id);
            return "duan";
        }
    }
    
  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.duan")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
  • 编写通知类

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
        private void pt(){}
    
        @Before("pt()")
        public void before() {
            System.out.println("before advice ..." );
        }
    
        @After("pt()")
        public void after() {
            System.out.println("after advice ...");
        }
    
        @Around("pt()")
        public Object around() throws Throwable{
            Object ret = pjp.proceed();
            return ret;
        }
        @AfterReturning("pt()")
        public void afterReturning() {
            System.out.println("afterReturning advice ...");
        }
    
    
        @AfterThrowing("pt()")
        public void afterThrowing() {
            System.out.println("afterThrowing advice ...");
        }
    }
    
  • 编写App运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            UserDao UserDao = ctx.getBean(UserDao.class);
            String name = UserDao.findName(100);
            System.out.println(name);
        }
    }
    
7.4.4.2 获取参数

非环绕通知获取方式

在方法上添加JoinPoint,通过JoinPoint来获取参数

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint jp) 
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ..." );
    }
  //...其他的略
}

输出结果为:

[100]

before advice ...

id:100

duan


思考:方法的参数只有一个,为什么获取的是一个数组?

因为参数的个数是不固定的,所以使用数组更通配些。

如果将参数改成两个会是什么效果呢?

(1)修改UserDao接口和UserDaoImpl实现类

public interface UserDao {
    public String findName(int id,String password);
}
@Repository
public class UserDaoImpl implements UserDao {

    public String findName(int id,String password) {
        System.out.println("id:"+id);
        return "duan";
    }
}

(2)修改App类,调用方法传入多个参数

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserDao UserDao = ctx.getBean(UserDao.class);
        String name = UserDao.findName(100,"duan");
        System.out.println(name);
    }
}

输出结果为:

[100,duan]

before advice ...

id:100

duan


说明:

使用JoinPoint的方式获取参数适用于前置后置返回后抛出异常后通知。

环绕通知获取方式

环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中有对应的getArgs()方法,

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
    private void pt(){}

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

运行App后查看运行结果,说明ProceedingJoinPoint也是可以通过getArgs()获取参数

输出结果为:

[100,duan]

id:100

duan


注意:

  • pjp.proceed()方法是有两个构造方法,分别是:

    1630234756123

    • 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数

    • 所以调用这两个方法的任意一个都可以完成功能

    • 但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
          private void pt(){}
      
          @Around("pt()")
          public Object around(ProceedingJoinPoint pjp) throws Throwable{
              Object[] args = pjp.getArgs();
              System.out.println(Arrays.toString(args));
              args[0] = 666;
              Object ret = pjp.proceed(args);
              return ret;
          }
        //其他的略
      }
      

      有了这个特性后,就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。

7.4.4.3 获取返回值

对于返回值,只有返回后AfterReturing和环绕Around这两个通知类型可以获取,具体如何获取?

环绕通知获取返回值

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);
        return ret;
    }
  //其他的略
}

上述代码中,ret就是方法的返回值,不但可以获取,如果需要还可以进行修改。

返回后通知获取返回值

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
    private void pt(){}

    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..."+ret);
    }
  //其他的略
}

注意:

(1)参数名的问题

1630237320870

(2)afterReturning方法参数类型的问题

参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型

(3)afterReturning方法参数的顺序问题

1630237586682

输出结果为:

id:100

afterReturning advice ...duan

duan


7.4.4.4 获取异常

环绕通知获取异常

这块比较简单,抛出异常,现在只需要将异常捕获,就可以获取到原始方法的异常信息了

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try{
            ret = pjp.proceed(args);
        }catch(Throwable throwable){
            t.printStackTrace();
        }
        return ret;
    }
  //其他的略
}

在catch方法中就可以获取到异常。

抛出异常后通知获取异常

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.duan.dao.UserDao.findName(..))")
    private void pt(){}

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

如何让原始方法抛出异常,方式有很多,

@Repository
public class UserDaoImpl implements UserDao {

    public String findName(int id,String password) {
        System.out.println("id:"+id);
        if(true){
            throw new NullPointerException();
        }
        return "itcast";
    }
}

注意:

1630239939043

运行App后,查看控制台,就能看的异常信息被打印到控制台

1630239997560

总结:

AOP通知,数据中包含参数返回值异常(了解)

7.4.5 密码数据兼容处理

7.4.5.1 需求分析

需求: 对分享链接输入密码时尾部多输入的空格做兼容处理,如下所示。

1630240203033

问题描述:

  • 当复制提取密码的时候,有时候会多复制到一些空格,直接粘贴到提取密码输入框
  • 如果不做处理,直接对比的话,就会引发提取码不一致,导致访问失败
  • 所以多输入一个空格可能会导致项目的功能无法正常使用。

解决方案:

  • 只需要在业务方法执行之前对所有的输入参数进行格式处理——trim()
  • 一般只需要针对字符串处理即可。

  • 业务中需要去除前后空格的业务可能会有很多,这个时候可以使用AOP来统一处理。

  • 需求是将原始方法的参数处理后在参与原始方法的调用,能做这件事的就只有环绕通知。

综上所述:
①:在业务方法执行之前对所有的输入参数进行格式处理——trim()
②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用

7.4.5.2 环境准备
  • 创建一个Maven项目

  • pom.xml添加Spring依赖

    <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
        </dependency>
      </dependencies>
    
  • 添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl类

    public interface ResourcesDao {
        boolean readResources(String url, String password);
    }
    @Repository
    public class ResourcesDaoImpl implements ResourcesDao {
        public boolean readResources(String url, String password) {
            //模拟校验
            return password.equals("root");
        }
    }
    public interface ResourcesService {
        boolean openURL(String url ,String password);
    }
    @Service
    public class ResourcesServiceImpl implements ResourcesService {
        @Autowired
        private ResourcesDao resourcesDao;
    
        @override
        public boolean openURL(String url, String password) {
            return resourcesDao.readResources(url,password);
        }
    }
    
    
  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.duan")
    public class SpringConfig {
    }
    
  • 编写App运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
            boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
            System.out.println(flag);
        }
    }
    

现在项目的效果是,当输入密码为"root"控制台打印为true,如果密码改为"root "控制台打印的是false

需求是使用AOP将参数进行统一处理,不管输入的密码root前后包含多少个空格,最终控制台打印的都是true。

7.4.5.3 具体实现

步骤1:开启SpringAOP的注解功能

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

步骤2:编写通知类

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.duan.service.*Service.*(*,*))")
    private void servicePt(){}
    
}

步骤3:添加环绕通知

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.duan.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")这两种写法都对
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        Object ret = pjp.proceed();
        return ret;
    }
}

步骤4:完成核心业务,处理参数中的空格

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

步骤5:运行程序

不管密码root前后是否加空格,最终控制台打印的都是true

步骤6:优化测试

为了能更好的看出AOP已经生效,可以修改ResourcesImpl类,在方法中将密码的长度进行打印

@Repository
public class ResourcesDaoImpl implements ResourcesDao {
    public boolean readResources(String url, String password) {
        System.out.println(password.length());
        //模拟校验
        return password.equals("root");
    }
}

再次运行成功,就可以根据最终打印的长度来看看,字符串的空格有没有被去除掉。

注意:

1630242491831

7.5,AOP总结

7.5.1 AOP的核心概念

  • 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能增强
  • 核心概念
    • 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
    • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象成为目标对象

7.5.2 切入点表达式

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

    execution(* com.duan.service.*Service.*(..))
    
  • 切入点表达式描述通配符:

    • 作用:用于快速描述,范围描述
    • *:匹配任意符号(常用)
    • .. :匹配多个连续的任意符号(常用)
    • +:匹配子类类型
  • 切入点表达式书写技巧

    1.按标准规范开发
    2.查询操作的返回值建议使用*匹配
    3.减少使用..的形式描述包
    4.对接口进行描述,使用*表示模块名,例如UserService的匹配描述为*Service
    5.方法名书写保留动词,例如get,使用*表示名词,例如getById匹配描述为getBy*
    6.参数根据实际情况灵活调整

7.5.3 五种通知类型

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
    • 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
    • 环绕通知可以隔离原始方法的调用执行
    • 环绕通知返回值设置为Object类型
    • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
  • 返回后通知
  • 抛出异常后通知

7.5.4 通知中获取参数

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

7.6,AOP事务管理

7.6.1 Spring事务简介

7.6.1.1 相关概念介绍
  • 事务作用:在数据层保障一系列的数据库操作同成功同失败
  • Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

为什么业务层也需要处理事务?

  • 转账业务会有两次数据层的调用,一次是加钱一次是减钱
  • 把事务放在数据层,加钱和减钱就有两个事务
  • 没办法保证加钱和减钱同时成功或者同时失败
  • 这个时候就需要将事务放在业务层进行处理。

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

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

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

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

public class DateSourceTransactionMagager{
    ......
}

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

7.6.1.2 转账案例-需求分析

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

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

步骤实现:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)

②:业务层提供转账操作(transfer),调用减钱与加钱的操作

③:提供2个账号和操作金额执行转账操作

④:基于Spring整合MyBatis环境搭建上述操作

7.6.1.3 转账案例-环境搭建

步骤1:准备数据库表

create database spring_db;
use spring_db;
create table tbl_account(
    id int primary key auto_increment,
    name varchar(35),
    money double
);
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);

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

<dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
    </dependency>
  </dependencies>

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Integer id;
    private String name;
    private Double money;
}

步骤4:创建Dao接口

public interface AccountDao {

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

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

步骤5:创建Service接口和实现类

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    public void transfer(String out,String in ,Double money);

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @override
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(in,money);
        accountDao.inMoney(out,money);
    }

}

步骤6:添加jdbc.properties文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?serviceTimezone=Asia/Shanghai?useSSL=false
jdbc.username=root
jdbc.password=root

步骤7:创建JdbcConfig配置类

public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    
    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(this.driver);
        dataSource.setUrl(this.url);
        dataSource.setUsername(this.username);
        dataSource.setPassword(this.password);
        return dataSource;
    }
}

步骤8:创建MybatisConfig配置类

public class MybatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setTypeAliasesPackage("com.duan.pojo");
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
    
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapper = new MapperScannerConfigurer();
        mapper.setBasePackage("com.duan.dao");
        return mapper;
    }
}

步骤9:创建SpringConfig配置类

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

步骤10:编写测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
        accountService.transfer("Tom","Jerry",100D);
    }

}
7.6.1.4 事务管理

运行单元测试类,会执行转账操作,Tom的账户会减少100,Jerry的账户会加100。

这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

   	@Override
    @Transactional
    public int transfer(int out, int in, Double money) {
        accountDao.inMoney(in, money);
        int i = 1/0;
        accountDao.outMoney(out, money);

}

这个时候就模拟了转账过程中出现异常的情况,正确的操作应该是转账出问题了,Tom应该还是900,Jerry应该还是1100,运行后会发现,Tom账户为900而Jerry变成1200。

分析:

①:程序正常执行时,账户金额A减B加,没有问题

②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而Spring的事务管理就是用来解决这类问题的。

Spring事务管理具体的实现步骤为:

步骤1:在需要被事务管理的方法上添加注解

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    
    @override
  	@Transactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }

}

  

注意:

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

  • 写在接口类上,该接口的所有实现类的所有方法都会有事务
  • 写在接口方法上,该接口的所有实现类的该方法都会有事务
  • 写在实现类上,该类中的所有方法都会有事务
  • 写在实现类方法上,该方法上有事务
  • 建议写在实现类或实现类的方法上

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

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

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

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

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

步骤3:开启事务注解

在SpringConfig的配置类中开启

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

步骤4:运行测试类

会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。

@EnableTransactionManagement
名称 @EnableTransactionManagement
类型 配置类注解
位置 配置类定义上方
作用 设置当前Spring环境中开启注解式事务支持
@Transactional
名称 @Transactional
类型 接口注解 类注解 方法注解
位置 业务层接口上方 业务层实现类上方 业务方法上方
作用 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)

7.6.2 Spring事务角色

重点是事务管理员事务协调员

  1. 未开启Spring事务之前:
  • AccountDao的outMoney因为是修改操作,会开启一个事务T1
  • AccountDao的inMoney因为是修改操作,会开启一个事务T2
  • AccountService的transfer没有事务,
    • 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
    • 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
    • 就会导致数据出现错误
  1. 开启Spring的事务管理后
  • transfer上添加了@Transactional注解,在该方法上就会有一个事务T
  • AccountDao的outMoney方法的事务T1加入到transfer的事务T中
  • AccountDao的inMoney方法的事务T2加入到transfer的事务T中
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
  1. 概念:
  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

注意:

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

7.6.3 Spring事务属性

7.6.3.1 事务配置

1630250069844

上面这些属性都可以在@Transactional注解的参数上进行设置。

  • readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。

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

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

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

    • 思考:

      • rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?
        • 并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
      public interface AccountService {
          /**
           * 转账操作
           * @param out 传出方
           * @param in 转入方
           * @param money 金额
           */
          //配置当前接口方法具有事务
          public void transfer(String out,String in ,Double money) throws IOException;
      }
      
      @Service
      public class AccountServiceImpl implements AccountService {
      
          @Autowired
          private AccountDao accountDao;
        @Transactional
          public void transfer(String out,String in ,Double money) throws IOException{
              accountDao.outMoney(out,money);
              //int i = 1/0; //这个异常事务会回滚
              if(true){
                  throw new IOException(); //这个异常事务就不会回滚
              }
              accountDao.inMoney(in,money);
          }
      
      }
      
  • 出现这个问题的原因是,Spring的事务只会对Error异常RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚

    • 此时就可以使用rollbackFor属性来设置出现IOException异常回滚

      @Service
      public class AccountServiceImpl implements AccountService {
      
          @Autowired
          private AccountDao accountDao;
          
          @override
         	@Transactional(rollbackFor = {IOException.class})
          public void transfer(String out,String in ,Double money) throws IOException{
              accountDao.outMoney(out,money);
              //int i = 1/0; //这个异常事务会回滚
              if(true){
                  throw new IOException(); //设置这个异常事务也会回滚
              }
              accountDao.inMoney(in,money);
          }
      
      }
      
  • rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串

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

  • isolation设置事务的隔离级别

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

介绍完上述属性后,还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例。

7.6.3.2 转账业务追加日志案例

需求:实现任意两个账户间转账操作,并对每次转账操作在数据库记录日志

实现:

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

②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能,预期效果为:

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

环境准备

步骤1:创建日志表

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

步骤2:添加LogDao接口

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

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

public interface LogService {
    void log(Integer out, Integer in, Double money);
}
@Service
public class LogServiceImpl implements LogService {

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

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

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
    
  	@Transactional
    public void transfer(String out,String in ,Double money) {
        try{
            accountDao.outMoney(out,money);
            accountDao.inMoney(in,money);
        }finally {
            logService.log(out,in,money);
        }
    }

}

步骤5:运行程序

  • 当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
  • 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
  • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
  • 最终效果:无论转账操作是否成功,日志必须保留
7.6.3.3 事务传播行为

分析:

  • log方法、inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
  • transfer因为加了@Transactional注解,开启了事务T
  • Spring事务会把T1,T2,T3都加入到事务T中
  • 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
  • 这导致了业务与需求不符

要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:

事务传播行为:事务协调员对事务管理员所携带事务的处理态度。

具体如何解决,就需要用propagation属性

  • 修改logService改变事务的传播行为
@Service
public class LogServiceImpl implements LogService {

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

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。

  • 事务传播行为的可选值

1630254257628

对于实际中使用的话,因为默认值需要事务是常态的。根据开发过程选择其他的就可以了置。其实入账和出账操作上也有事务,采用的就是默认值。

SpringMVC

1,SpringMVC概述

三层架构:model、view、controller(模型、视图、控制器)

1630427303762

  • 浏览器发送一个请求给后端服务器,后端服务器使用Servlet来接收请求和数据

  • 如果所有的处理都交给Servlet来处理的话,所有的东西都耦合在一起,对后期的维护和扩展极为不利

  • 将后端服务器Servlet拆分成三层,分别是webservicedao

    • web层主要由servlet来处理,负责页面请求和数据的收集以及响应结果给前端
    • service层主要负责业务逻辑的处理
    • dao层主要负责数据的增删改查操作
  • servlet处理请求和数据的时候,存在的问题是一个servlet只能处理一个请求

  • 针对web层进行了优化,采用了MVC设计模式,将其设计为controllerviewModel

    • controller负责请求和数据的接收,接收后将其转发给service进行业务处理
    • service根据需要会调用dao对数据进行增删改查
    • dao把数据处理完后将结果交给service,service再交给controller
    • controller根据需求组装成Model和View,Model和View组合起来生成页面转发给前端浏览器
    • 这样做的好处就是controller可以处理多个请求,并对请求进行分发,执行不同的业务操作。

随着互联网的发展,上面的模式因为是同步调用,性能慢慢的跟不是需求,所以异步调用慢慢的走到了前台,是现在比较流行的一种处理方式。

1630427769938

  • 因为是异步调用,所以后端不需要返回view视图,将其去除
  • 前端如果通过异步调用的方式进行交互,后台就需要将返回的数据转换成json格式进行返回
  • SpringMVC主要负责的就是
    • controller如何接收请求和数据
    • 如何将请求和数据转发给业务层
    • 如何将响应数据转换成json发回到前端

SpringMVC进行一个定义

  • SpringMVC是一种基于Java实现MVC模型的轻量级Web框架

  • 优点

    • 使用简单、开发便捷(相比于Servlet)
    • 灵活性强

2,SpringMVC入门案例

因为SpringMVC是一个Web框架,是替换Servlet的,所以先来回顾下Servlet是如何进行开发的?

1.创建web工程(Maven结构)

2.设置tomcat服务器,加载web工程(tomcat插件)

3.导入坐标(Servlet)

4.定义处理请求的功能类(UserServlet)

5.设置请求映射(配置映射关系)

SpringMVC的制作过程和上述流程几乎是一致的,具体的实现流程是什么?

1.创建web工程(Maven结构)

2.设置tomcat服务器,加载web工程(tomcat插件)

3.导入坐标(SpringMVC+Servlet)

4.定义处理请求的功能类(UserController)

5.设置请求映射(配置映射关系)

6.将SpringMVC设定加载到Tomcat容器中

2.1 需求分析

2.2 案例制作

步骤1:创建Maven项目

步骤2:补全目录结构

步骤3:导入jar包

将pom.xml中多余的内容删除掉,再添加SpringMVC需要的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.duan</groupId>
  <artifactId>springmvc_01_quickstart</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.1</version>
        <configuration>
          <port>80</port>
          <path>/</path>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

说明:servlet的坐标为什么需要添加<scope>provided</scope>?

  • scope是maven中jar包依赖作用范围的描述,

  • 如果不设置默认是compile在在编译、运行、测试时均有效

  • 如果运行有效的话就会和tomcat中的servlet-api包发生冲突,导致启动报错

  • provided代表的是该包只在编译和测试的时候用,运行的时候无效直接使用tomcat中的,就避免冲突

步骤4:创建配置类

@Configuration
@ComponentScan("com.duan.controller")
public class SpringMvcConfig {
}

步骤5:创建Controller类

@Controller
public class UserController {
    @RequestMapping("/save")
    public void save(){
        System.out.println("user save ...");
    }
}

步骤6:使用配置类替换web.xml

将web.xml删除,换成ServletContainersInitConfig

public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
    //加载springmvc配置类
    protected WebApplicationContext createServletApplicationContext() {
        //初始化WebApplicationContext对象
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        //加载指定配置类
        ctx.register(SpringMvcConfig.class);
        return ctx;
    }

    //设置由springmvc控制器处理的请求映射路径
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    //加载spring配置类
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }
}

步骤7:配置Tomcat环境

步骤8:启动项目

步骤9:浏览器访问

浏览器输入http://localhost/save进行访问,会报如下错误:

1630430401561

页面报错的原因是后台没有指定返回的页面,目前只需要关注控制台看user save ...有没有被执行即可。

步骤10:修改Controller返回值解决上述问题

现在是前端发送异步请求,后台响应json数据,所以把Controller类的save方法进行修改

@Controller
public class UserController {
    
    @RequestMapping("/save")
    public String save(){
        System.out.println("user save ...");
        return "{'info':'springmvc'}";
    }
}

再次重启tomcat服务器,然后重新通过浏览器测试访问,会发现还是会报错,这次的错是404

1630430658028

出错的原因是,如果方法直接返回字符串,springmvc会把字符串当成页面的名称在项目中进行查找返回,因为不存在对应返回值名称的页面,所以会报404错误,找不到资源。

而直接返回的是json数据,具体如何修改呢?

步骤11:设置返回数据为json

@Controller
public class UserController {
    
    @RequestMapping("/save")
    @ResponseBody
    public String save(){
        System.out.println("user save ...");
        return "{'info':'springmvc'}";
    }
}

再次重启tomcat服务器,然后重新通过浏览器测试访问,就能看到返回的结果数据

1630430835628

注意事项

  • SpringMVC是基于Spring的,在pom.xml只导入了spring-webmvcjar包的原因是它会自动依赖spring相关坐标
  • AbstractDispatcherServletInitializer类是SpringMVC提供的快速初始化Web3.0容器的抽象类
  • AbstractDispatcherServletInitializer提供了三个接口方法供用户实现
    • createServletApplicationContext方法,创建Servlet容器时,加载SpringMVC对应的bean并放入WebApplicationContext对象范围中,而WebApplicationContext的作用范围为ServletContext范围,即整个web容器范围
    • getServletMappings方法,设定SpringMVC对应的请求映射路径,即SpringMVC拦截哪些请求
    • createRootApplicationContext方法,如果创建Servlet容器时需要加载非SpringMVC对应的bean,使用当前方法进行,使用方式和createServletApplicationContext相同。
    • createServletApplicationContext用来加载SpringMVC环境
    • createRootApplicationContext用来加载Spring环境

@Controller

名称 @Controller
类型 类注解
位置 SpringMVC控制器类定义上方
作用 设定SpringMVC的核心控制器bean

@RequestMapping

名称 @RequestMapping
类型 类注解或方法注解
位置 SpringMVC控制器类或方法定义上方
作用 设置当前控制器方法请求访问路径
相关属性 value(默认),请求访问路径

@ResponseBody

名称 @ResponseBody
类型 类注解或方法注解
位置 SpringMVC控制器类或方法定义上方
作用 设置当前控制器方法响应内容为当前返回值,无需解析

2.3 入门案例总结

  • 一次性工作
    • 创建工程,设置服务器,加载工程
    • 导入坐标
    • 创建web容器启动类,加载SpringMVC配置,并设置SpringMVC请求拦截路径
    • SpringMVC核心配置类(设置配置类,扫描controller包,加载Controller控制器bean)
  • 多次工作
    • 定义处理请求的控制器类
    • 定义处理请求的控制器方法,并配置映射路径(@RequestMapping)与返回json数据(@ResponseBody)

2.4 工作流程解析

为了更好的使用SpringMVC,可以将SpringMVC的使用过程总共分两个阶段来分析,分别是启动服务器初始化过程单次请求过程

1630432494752

2.4.1 启动服务器初始化过程

  1. 服务器启动,执行ServletContainersInitConfig类,初始化web容器

    • 功能类似于以前的web.xml
  2. 执行createServletApplicationContext方法,创建了WebApplicationContext对象

    • 该方法加载SpringMVC的配置类SpringMvcConfig来初始化SpringMVC的容器
  3. 加载SpringMvcConfig配置类

    @Configuration
    @ComponentScan("com.duan.controller")
    public class SpringMvcConfig(){}
    
  4. 执行@ComponentScan加载对应的bean

    • 扫描指定包及其子包下所有类上的注解,如Controller类上的@Controller注解
  5. 加载UserController,每个@RequestMapping的名称对应一个具体的方法

    @Controller
    public class UserController{
        @RequestMapping("/save")
        @ResponseBody
        public String save(){
            System.out.print("user save ...");
            return"{'info':'springmvc'}";
        }
    }
    
    • 此时就建立了 /save 和 save方法的对应关系
  6. 执行getServletMappings方法,设定SpringMVC拦截请求的路径规则

    1630433510528

    • /代表所拦截请求的路径规则,只有被拦截后才能交给SpringMVC来处理请求

2.4.2 单次请求过程

  1. 发送请求http://localhost/save
  2. web容器发现该请求满足SpringMVC拦截规则,将请求交给SpringMVC处理
  3. 解析请求路径/save
  4. 由/save匹配执行对应的方法save()
    • 上面的第五步已经将请求路径和方法建立了对应关系,通过/save就能找到对应的save方法
  5. 执行save()
  6. 检测到有@ResponseBody直接将save()方法的返回值作为响应体返回给请求方

2.5 bean加载控制

2.5.1 问题分析

前面Spring也创建过一个配置类SpringConfig。这两个配置类都需要加载资源,那么它们分别都需要加载哪些内容?

  • config目录存入的是配置类,写过的配置类有:

    • ServletContainersInitConfig
    • SpringConfig
    • SpringMvcConfig
    • JdbcConfig
    • MybatisConfig
  • controller目录存放的是SpringMVC的controller类

  • service目录存放的是service接口和实现类

  • dao目录存放的是dao/Mapper接口

controller、service和dao这些类都需要被容器管理成bean对象,那么到底是该让SpringMVC加载还是让Spring加载呢?

  • SpringMVC加载其相关bean(表现层bean),也就是controller包下的类
  • Spring控制的bean
    • 业务bean(Service)
    • 功能bean(DataSource,SqlSessionFactoryBean,MapperScannerConfigurer等)

分析清楚谁该管哪些bean以后,接下来要解决的问题是如何让Spring和SpringMVC分开加载各自的内容。

在SpringMVC的配置类SpringMvcConfig中使用注解@ComponentScan,只需要将其扫描范围设置到controller即可,如

@ConfigUration
@ComponentScan("com.duan.controller")
public class SpringMvcConfig{}

在Spring的配置类SpringConfig中使用注解@ComponentScan,当时扫描的范围中其实是已经包含了controller,如:

@CompoentScan(Value="com.duan")
public class SpringConfig{}

从包结构来看的话,Spring已经把SpringMVC的controller类也给扫描到。

现在的问题就是因为功能不同,如何避免Spring错误加载到SpringMVC的bean?

2.5.2 思路分析

针对上面的问题,解决方案也比较简单,就是:

  • 加载Spring控制的bean的时候排除掉SpringMVC控制的bean

具体该如何排除:

  • 方式一:Spring加载的bean设定扫描范围为精准范围,例如service包、dao包等
  • 方式二:Spring加载的bean设定扫描范围为com.duan,排除掉controller包中的bean
  • 方式三:不区分Spring与SpringMVC的环境,加载到同一个环境中[了解即可]

2.5.4 环境准备

  • 创建一个Web的Maven项目

  • pom.xml添加Spring依赖

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.duan</groupId>
      <artifactId>springmvc_02_bean_load</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <dependencies>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.16</version>
        </dependency>
    
        <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.6</version>
        </dependency>
    
        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.47</version>
        </dependency>
    
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
    
        <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>1.3.0</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.1</version>
            <configuration>
              <port>80</port>
              <path>/</path>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
    
  • 创建对应的配置类

    public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
        protected WebApplicationContext createServletApplicationContext() {
            AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
            ctx.register(SpringMvcConfig.class);
            return ctx;
        }
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
        protected WebApplicationContext createRootApplicationContext() {
          return null;
        }
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan("com.duan.controller")
    public class SpringMvcConfig {
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan("com.duan")
    public class SpringConfig {
    }
    
    
  • 编写Controller,Service,Dao,Domain类

    @Controller
    public class UserController {
    
        @RequestMapping("/save")
        @ResponseBody
        public String save(){
            System.out.println("user save ...");
            return "{'info':'springmvc'}";
        }
    }
    --------------------------------------------------------------------------
    public interface UserService {
        public void save(User user);
    }
    --------------------------------------------------------------------------
    @Service
    public class UserServiceImpl implements UserService {
        public void save(User user) {
            System.out.println("user service ...");
        }
    }
    --------------------------------------------------------------------------
    public interface UserDao {
        @Insert("insert into tbl_user(name,age)values(#{name},#{age})")
        public void save(User user);
    }
    --------------------------------------------------------------------------
    public class User {
        private Integer id;
        private String name;
        private Integer age;
        //setter..getter..toString略
    }
    

2.5.5 设置bean加载控制

方式一:修改Spring配置类,设定扫描范围为精准范围。

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

说明:

上述只是通过例子说明可以精确指定让Spring扫描对应的包结构,真正在做开发的时候,因为Dao最终是交给MapperScannerConfigurer对象来进行扫描处理的,只需要将其扫描到service包即可。

方式二:修改Spring配置类,设定扫描范围为com.duan,排除掉controller包中的bean

@Configuration
@ComponentScan(value="com.duan",
    excludeFilters=@ComponentScan.Filter(
    	type = FilterType.ANNOTATION,
        classes = Controller.class
    )
)
public class SpringConfig {
}
  • excludeFilters属性:设置扫描加载bean时,排除的过滤规则

  • type属性:设置排除规则,当前使用按照bean定义时的注解类型进行排除

    • ANNOTATION:按照注解排除
    • ASSIGNABLE_TYPE:按照指定的类型过滤
    • ASPECTJ:按照Aspectj表达式排除,基本上不会用
    • REGEX:按照正则表达式排除
    • CUSTOM:按照自定义规则排除

    只需要知道第一种ANNOTATION即可

  • classes属性:设置排除的具体注解类,当前设置排除@Controller定义的bean

如何测试controller类已经被排除掉了?

public class App{
	public static void main (String[] args){
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        System.out.println(ctx.getBean(UserController.class));
    }
}

如果被排除了,该方法执行就会报bean未被定义的错误

1630462200947

注意:测试的时候,需要把SpringMvcConfig配置类上的@ComponentScan注解注释掉,否则不会报错

出现问题的原因是,

  • Spring配置类扫描的包是com.duan
  • SpringMVC的配置类,SpringMvcConfig上有一个@Configuration注解,也会被Spring扫描到
  • SpringMvcConfig上又有一个@ComponentScan,把controller类又给扫描进来了
  • 所以如果不把@ComponentScan注释掉,Spring配置类将Controller排除,但是因为扫描到SpringMVC的配置类,又将其加载回来,演示的效果就出不来
  • 解决方案,也简单,把SpringMVC的配置类移出Spring配置类的扫描范围即可。

有了Spring的配置类,要想在tomcat服务器启动将其加载,需要修改ServletContainersInitConfig

public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMvcConfig.class);
        return ctx;
    }
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
    protected WebApplicationContext createRootApplicationContext() {
      AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(SpringConfig.class);
        return ctx;
    }
}

对于上述的配置方式,Spring还提供了一种更简单的配置方式,可以不用再去创建AnnotationConfigWebApplicationContext对象,不用手动register对应的配置类,如何实现?

public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

@ComponentScan

名称 @ComponentScan
类型 类注解
位置 类定义上方
作用 设置spring配置类扫描路径,用于加载使用注解格式定义的bean
相关属性 excludeFilters:排除扫描路径中加载的bean,需要指定类别(type)和具体项(classes)
includeFilters:加载指定的bean,需要指定类别(type)和具体项(classes)

3,ApiPost工具的使用

3.1 ApiPost简介

代码编写完后,只需要打开浏览器直接输入地址发送请求即可。发送的是GET请求可以直接使用浏览器,但是如果要发送的是POST请求呢?

如果要求发送的是post请求,就得准备页面在页面上准备form表单,测试起来比较麻烦。就需要借助一些第三方工具,如ApiPost.

  • 支持模拟POST、GET、PUT等常见请求,是一个国产的、用来测试Web API的软件,提供window、mac、linux版本下载,对于有在开发Web API的开发者相当有用,同时由于它是国产软件,完全中文界面更加友好,也符合中国开发者的使用体验。另外支持一键生成api接口文档,省掉不少开发者的工作。

  • 作用:常用于进行接口测试

  • 特征

    • 简单
    • 实用
    • 美观
    • 大方

3.2 ApiPost安装

官网:https://doc.apipost.cn/

3.3 ApiPost使用

使用文档:https://v7-wiki.apipost.cn/docs/1

4,请求与响应

4.1 设置请求映射路径

4.1.1 环境准备

  • 创建一个Web的Maven项目

  • pom.xml添加Spring依赖

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.duan</groupId>
      <artifactId>springmvc_03_request_mapping</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <dependencies>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.1</version>
            <configuration>
              <port>80</port>
              <path>/</path>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
    
  • 创建对应的配置类

    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
        protected Class<?>[] getRootConfigClasses() {
            return new Class[0];
        }
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan("com.duan.controller")
    public class SpringMvcConfig {
    }
    
    
  • 编写BookController和UserController

    @Controller
    public class UserController {
    
        @RequestMapping("/save")
        @ResponseBody
        public String save(){
            System.out.println("user save ...");
            return "{'module':'user save'}";
        }
        
        @RequestMapping("/delete")
        @ResponseBody
        public String save(){
            System.out.println("user delete ...");
            return "{'module':'user delete'}";
        }
    }
    --------------------------------------------------------------------------
    @Controller
    public class BookController {
        @RequestMapping("/save")
        @ResponseBody
        public String save(){
            System.out.println("book save ...");
            return "{'module':'book save'}";
        }
    }
    

最终创建好的项目结构如下:

把环境准备好后,启动Tomcat服务器,后台会报错,原因为:

  • UserController有一个save方法,访问路径为http://localhost/save
  • BookController也有一个save方法,访问路径为http://localhost/save
  • 当访问http://localhost/saved的时候,到底是访问UserController还是BookController?

4.1.2 问题分析

团队多人开发,每人设置不同的请求路径,冲突问题该如何解决?

解决思路:为不同模块设置模块名作为请求路径前置

对于Book模块的save,将其访问路径设置http://localhost/book/save

对于User模块的save,将其访问路径设置http://localhost/user/save

这样在同一个模块中出现命名冲突的情况就比较少了。

4.1.3 设置映射路径

步骤1:修改Controller
@Controller
public class UserController {

    @RequestMapping("/user/save")
    @ResponseBody
    public String save(){
        System.out.println("user save ...");
        return "{'module':'user save'}";
    }
    
    @RequestMapping("/user/delete")
    @ResponseBody
    public String save(){
        System.out.println("user delete ...");
        return "{'module':'user delete'}";
    }
}
--------------------------------------------------------------------------
@Controller
public class BookController {

    @RequestMapping("/book/save")
    @ResponseBody
    public String save(){
        System.out.println("book save ...");
        return "{'module':'book save'}";
    }
}

问题是解决了,但是每个方法前面都需要进行修改,写起来比较麻烦而且还有很多重复代码,如果/user后期发生变化,所有的方法都需要改,耦合度太高。

步骤2:优化路径配置

优化方案:

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/save")
    @ResponseBody
    public String save(){
        System.out.println("user save ...");
        return "{'module':'user save'}";
    }
    
    @RequestMapping("/delete")
    @ResponseBody
    public String save(){
        System.out.println("user delete ...");
        return "{'module':'user delete'}";
    }
}
--------------------------------------------------------------------------
@Controller
@RequestMapping("/book")
public class BookController {

    @RequestMapping("/save")
    @ResponseBody
    public String save(){
        System.out.println("book save ...");
        return "{'module':'book save'}";
    }
}

注意:

  • 当类上和方法上都添加了@RequestMapping注解,前端发送请求的时候,要和两个注解的value值相加匹配才能访问到。
  • @RequestMapping注解value属性前面加不加/都可以

4.2 请求参数

请求路径设置好后,只要确保页面发送请求地址和后台Controller类中配置的路径一致,就可以接收到前端的请求,接收到请求后,如何接收页面传递的参数?

关于请求参数的传递与接收是和请求方式有关系的,目前比较常见的两种请求方式为:

  • GET
  • POST

针对于不同的请求前端如何发送,后端如何接收?

4.2.1 环境准备

  • 创建一个Web的Maven项目

  • pom.xml添加Spring依赖

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.duan</groupId>
      <artifactId>springmvc_03_request_mapping</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <dependencies>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.1</version>
            <configuration>
              <port>80</port>
              <path>/</path>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
    
  • 创建对应的配置类

    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
        protected Class<?>[] getRootConfigClasses() {
            return new Class[0];
        }
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan("com.duan.controller")
    public class SpringMvcConfig {
    }
    
    
  • 编写UserController

    @Controller
    public class UserController {
    
        @RequestMapping("/commonParam")
        @ResponseBody
        public String commonParam(){
            return "{'module':'commonParam'}";
        }
    }
    
  • 编写实体类,User和Address

    public class Address {
        private String province;
        private String city;
        //setter...getter...略
    }
    --------------------------------------------------------------------------
    public class User {
        private String name;
        private int age;
        //setter...getter...略
    }
    

4.2.2 参数传递

GET发送单个参数

发送请求与参数:

localhost/commonParam?name=duan

image-20230622150908701

接收参数:

@Controller
public class UserController {

    @RequestMapping("/commonParam")
    @ResponseBody
    public String commonParam(String name){
        System.out.println("普通参数传递 name ==> "+name);
        return "{'module':'commonParam'}";
    }
}
GET发送多个参数

发送请求与参数:

http://localhost/commonParam?name=duan&age=15

接收参数:

@Controller
public class UserController {

    @RequestMapping("/commonParam")
    @ResponseBody
    public String commonParam(String name,int age){
        System.out.println("普通参数传递 name ==> "+name);
        System.out.println("普通参数传递 age ==> "+age);
        return "{'module':'commonParam'}";
    }
}
GET请求中文乱码

如果传递的参数中有中文,接收到的参数会出现中文乱码问题。

发送请求:http://localhost/commonParam?name=张三&age=18

控制台:

1630480536510

Tomcat8.5以后的版本已经处理了中文乱码的问题,但是IDEA中的Tomcat插件目前只到Tomcat7,所以需要修改pom.xml来解决GET请求中文乱码问题

<build>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.1</version>
        <configuration>
          <port>80</port><!--tomcat端口号-->
          <path>/</path> <!--虚拟目录-->
          <uriEncoding>UTF-8</uriEncoding><!--访问路径编解码字符集-->
        </configuration>
      </plugin>
    </plugins>
  </build>
POST发送参数

发送请求与参数:image-20230622151416896

和GET一致,不用做任何修改

@Controller
public class UserController {

    @RequestMapping("/commonParam")
    @ResponseBody
    public String commonParam(String name,int age){
        System.out.println("普通参数传递 name ==> "+name);
        System.out.println("普通参数传递 age ==> "+age);
        return "{'module':'commonParam'}";
    }
}
POST请求中文乱码

发送请求与参数:image-20230622151515113

控制台打印的内容,会发现有中文乱码问题。

解决方案:配置过滤器

public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    //乱码处理
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding("UTF-8");
        return new Filter[]{filter};
    }
}

CharacterEncodingFilter是在spring-web包中,所以用之前需要导入对应的jar包。

4.3 五种类型参数传递

接下来来研究一些比较复杂的参数传递,常见的参数种类有:

  • 普通参数
  • POJO类型参数
  • 嵌套POJO类型参数
  • 数组类型参数
  • 集合类型参数

这些参数如何发送,后台改如何接收?

4.3.1 普通参数

  • 普通参数:url地址传参,地址参数名与形参变量名相同,定义形参即可接收参数。

    localhost/commonParam?name=duan&age=20
    

如果形参与地址参数名不一致该如何解决?

发送请求与参数:

http://localhost/commonParamDifferentName?name=张三&age=18

后台接收参数:

@RequestMapping("/commonParamDifferentName")
@ResponseBody
public String commonParamDifferentName(String userName , int age){
    System.out.println("普通参数传递 userName ==> "+userName);
    System.out.println("普通参数传递 age ==> "+age);
    return "{'module':'common param different name'}";
}

因为前端给的是name,后台接收使用的是userName,两个名称对不上,导致接收数据失败:

1630481772035

解决方案:使用@RequestParam注解

	@RequestMapping("/commonParamDifferentName")
    @ResponseBody
    public String commonParamDifferentName(@RequestParam("name") String userName , int age){
        System.out.println("普通参数传递 userName ==> "+userName);
        System.out.println("普通参数传递 age ==> "+age);
        return "{'module':'common param different name'}";
    }

注意:写上@RequestParam注解框架就不需要自己去解析注入,能提升框架处理性能

4.3.2 POJO数据类型

简单数据类型一般处理的是参数个数比较少的请求,如果参数比较多,那么后台接收参数的时候就比较复杂,这个时候我们可以考虑使用POJO数据类型。

  • POJO参数:请求参数名与形参对象属性名相同,定义POJO类型形参即可接收参数

此时需要使用前面准备好的POJO类,先来看下User

public class User {
    private String name;
    private int age;
    //setter...getter...略
}

发送请求和参数:

localhost/pojoParam?name=duan&age=20

后台接收参数:

//POJO参数:请求参数与形参对象中的属性对应即可完成参数传递
@RequestMapping("/pojoParam")
@ResponseBody
public String pojoParam(User user){
    System.out.println("pojo参数传递 user ==> "+user);
    return "{'module':'pojo param'}";
}

注意:

  • POJO参数接收,前端GET和POST发送请求数据的方式不变。
  • 请求参数key的名称要和POJO中属性的名称一致,否则无法封装。

4.3.3 嵌套POJO类型参数

如果POJO对象中嵌套了其他的POJO类,如

public class Address {
    private String province;
    private String city;
    //setter...getter...略
}
public class User {
    private String name;
    private int age;
    private Address address;
    //setter...getter...略
}
  • 嵌套POJO参数:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数

发送请求和参数:

localhost/pojoParam?name=duan&age=20&address.city=yunnan&address.city=jianshui

后台接收参数:

//POJO参数:请求参数与形参对象中的属性对应即可完成参数传递
@RequestMapping("/pojoParam")
@ResponseBody
public String pojoParam(User user){
    System.out.println("pojo参数传递 user ==> "+user);
    return "{'module':'pojo param'}";
}

注意:

请求参数key的名称要和POJO中属性的名称一致,否则无法封装

4.3.4 数组类型参数

举个简单的例子,如果前端需要获取用户的爱好,爱好绝大多数情况下都是多个,如何发送请求数据和接收数据呢?

  • 数组参数:请求参数名与形参对象属性名相同且请求参数为多个,定义数组类型即可接收参数

发送请求和参数:

localhost/arrayParam?likes=game&likes=music&likes=football

后台接收参数:

  //数组参数:同名请求参数可以直接映射到对应名称的形参数组对象中
    @RequestMapping("/arrayParam")
    @ResponseBody
    public String arrayParam(String[] likes){
        System.out.println("数组参数传递 likes ==> "+ Arrays.toString(likes));
        return "{'module':'array param'}";
    }

4.3.5 集合类型参数

数组能接收多个值,那么集合是否也可以实现这个功能呢?

发送请求和参数:

localhost/listParam?likes=game&likes=music&likes=football

后台接收参数:

//集合参数:同名请求参数可以使用@RequestParam注解映射到对应名称的集合对象中作为数据
@RequestMapping("/listParam")
@ResponseBody
public String listParam(List<String> likes){
    System.out.println("集合参数传递 likes ==> "+ likes);
    return "{'module':'list param'}";
}

运行会报错,错误的原因是:

SpringMVC将List看做是一个POJO对象来处理,将其创建一个对象并准备把前端的数据封装到对象中,但是List是一个接口无法创建对象,所以报错。

解决方案是:使用@RequestParam注解

//集合参数:同名请求参数可以使用@RequestParam注解映射到对应名称的集合对象中作为数据
@RequestMapping("/listParam")
@ResponseBody
public String listParam(@RequestParam List<String> likes){
    System.out.println("集合参数传递 likes ==> "+ likes);
    return "{'module':'list param'}";
}
  • 集合保存普通参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam绑定参数关系
  • 对于简单数据类型使用数组会比集合更简单些。

@RequestParam

名称 @RequestParam
类型 形参注解
位置 SpringMVC控制器方法形参定义前面
作用 绑定请求参数与处理器方法形参间的关系
相关参数 required:是否为必传参数
defaultValue:参数默认值

4.4 JSON数据传输参数

现在比较流行的开发方式为异步调用。前后台以异步方式进行交换,传输的数据使用的是JSON,所以前端如果发送的是JSON数据,后端该如何接收?

对于JSON数据类型,常见的有三种:

  • json普通数组(["value1","value2","value3",...])
  • json对象({key1:value1,key2:value2,...})
  • json对象数组([{key1:value1,...},{key2:value2,...}])

对于上述数据,前端如何发送,后端如何接收?

4.4.1、JSON普通数组

步骤1:pom.xml添加依赖

SpringMVC默认使用的是jackson来处理json的转换,所以需要在pom.xml添加jackson依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>

步骤2:ApiPost发送JSON数据

image-20230622153146306

步骤3:开启SpringMVC注解支持

在SpringMVC的配置类中开启SpringMVC的注解支持,这里面就包含了将JSON转换成对象的功能。

@Configuration
@ComponentScan("com.duan.controller")
//开启json数据类型自动转换
@EnableWebMvc
public class SpringMvcConfig {
}

步骤4:参数前添加@RequestBody

//使用@RequestBody注解将外部传递的json数组数据映射到形参的集合对象中作为数据
@RequestMapping("/listParamForJson")
@ResponseBody
public String listParamForJson(@RequestBody List<String> likes){
    System.out.println("list common(json)参数传递 list ==> "+likes);
    return "{'module':'list common for json param'}";
}

4.4.2、JSON对象数据

请求数据的发送:

{
	"name":"duan",
	"age":15
}

image-20230622153512243

后端接收数据:

@RequestMapping("/pojoParamForJson")
@ResponseBody
public String pojoParamForJson(@RequestBody User user){
    System.out.println("pojo(json)参数传递 user ==> "+user);
    return "{'module':'pojo for json param'}";
}

说明:

address为null的原因是前端没有传递数据给后端。

如果想要address也有数据,需求修改前端传递的数据内容:

{
	"name":"duan",
	"age":15,
    "address":{
        "province":"beijing",
        "city":"beijing"
    }
}

再次发送请求,就能看到address中的数据

1630493450694

4.4.3、JSON对象数组

集合中保存多个POJO该如何实现?

请求和数据的发送:

[
    {"name":"duan","age":15},
    {"name":"duan","age":12}
]

image-20230622153733166

后端接收数据:

@RequestMapping("/listPojoParamForJson")
@ResponseBody
public String listPojoParamForJson(@RequestBody List<User> list){
    System.out.println("list pojo(json)参数传递 list ==> "+list);
    return "{'module':'list pojo for json param'}";
}

小结

SpringMVC接收JSON数据的实现步骤为:

(1)导入jackson包

(2)使用apipost发送JSON数据

(3)开启SpringMVC注解驱动,在配置类上添加@EnableWebMvc注解

(4)Controller方法的参数前添加@RequestBody注解

@EnableWebMvc

名称 @EnableWebMvc
类型 配置类注解
位置 SpringMVC配置类定义上方
作用 开启SpringMVC多项辅助功能

@RequestBody

名称 @RequestBody
类型 形参注解
位置 SpringMVC控制器方法形参定义前面
作用 将请求中请求体所包含的数据传递给请求参数,此注解一个处理器方法只能使用一次

4.4.5、@RequestBody与@RequestParam区别

  • 区别

    • @RequestParam用于接收url地址传参,表单传参
    • @RequestBody用于接收json数据
  • 应用

    • 后期开发中,发送json格式数据为主,@RequestBody应用较广
    • 如果发送非json格式数据,选用@RequestParam接收请求参数

4.5 日期类型参数传递

日期类型比较特殊,因为对于日期的格式有N多中输入方式,比如:

  • 2001-07-02
  • 2001/02/02
  • 07/02/2001
  • ......

步骤1:编写方法接收日期数据

在UserController类中添加方法,把参数设置为日期类型

@RequestMapping("/dataParam")
@ResponseBody
public String dataParam(Date date)
    System.out.println("参数传递 date ==> "+date);
    return "{'module':'data param'}";
}

步骤2:启动Tomcat服务器

查看控制台是否报错,如果有错误,先解决错误。

步骤3:使用apipost发送请求

使用apipost发送GET请求,并设置date参数

http://localhost/dataParam?date=2001/07/02

步骤4:查看控制台

通过打印,可以看到SpringMVC可以接收日期数据类型,并将其打印在控制台。

如果把日期参数的格式改成其他的,SpringMVC还能处理么?

步骤5:更换日期格式

为了能更好的看到程序运行的结果,我们在方法中多添加一个日期参数

@RequestMapping("/dataParam")
@ResponseBody
public String dataParam(Date date,Date date1)
    System.out.println("参数传递 date ==> "+date);
    return "{'module':'data param'}";
}

使用apipost发送请求,携带两个不同的日期格式,

http://localhost/dataParam?date=2001/07/02&date1=2001-07-02

发送请求和数据后,页面会报400,控制台会报出一个错误

Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Date'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2088-08-08'; nested exception is java.lang.IllegalArgumentException]

从错误信息可以看出,错误的原因是在将2001-07-02转换成日期类型的时候失败了,原因是SpringMVC默认支持的字符串转日期的格式为yyyy/MM/dd,而传递的不符合其默认格式,SpringMVC就无法进行格式转换,所以报错。

解决方案也比较简单,需要使用@DateTimeFormat

@RequestMapping("/dataParam")
@ResponseBody
public String dataParam(Date date,
                        @DateTimeFormat(pattern="yyyy-MM-dd") Date date1)
    System.out.println("参数传递 date ==> "+date);
	System.out.println("参数传递 date1(yyyy-MM-dd) ==> "+date1);
    return "{'module':'data param'}";
}

重新启动服务器,重新发送请求测试,SpringMVC就可以正确的进行日期转换了

步骤6:携带时间的日期

先修改UserController类,添加第三个参数

@RequestMapping("/dataParam")
@ResponseBody
public String dataParam(Date date,
                        @DateTimeFormat(pattern="yyyy-MM-dd") Date date1,
                        @DateTimeFormat(pattern="yyyy/MM/dd HH:mm:ss") Date date2)
    System.out.println("参数传递 date ==> "+date);
	System.out.println("参数传递 date1(yyyy-MM-dd) ==> "+date1);
	System.out.println("参数传递 date2(yyyy/MM/dd HH:mm:ss) ==> "+date2);
    return "{'module':'data param'}";
}

使用apipost发送请求,携带两个不同的日期格式,

http://localhost/dataParam?date=2001/07/02&date1=2001-07-02&date2=2001/07/02 8:08:08

1630495347289

重新启动服务器,重新发送请求测试,SpringMVC就可以将日期时间的数据进行转换

@DateTimeFormat

名称 @DateTimeFormat
类型 形参注解
位置 SpringMVC控制器方法形参前面
作用 设定日期时间型数据格式
相关属性 pattern:指定日期时间格式字符串

内部实现原理

思考题:

  • 前端传递字符串,后端使用日期Date接收
  • 前端传递JSON数据,后端使用对象接收
  • 前端传递字符串,后端使用Integer接收
  • 后台需要的数据类型有很多中
  • 在数据的传递过程中存在很多类型的转换

问:谁来做这个类型转换?

答:SpringMVC

问:SpringMVC是如何实现类型转换的?

答:SpringMVC中提供了很多类型转换接口和实现类

在框架中,有一些类型转换接口,其中有:

  • (1) Converter接口
/**
*	S: the source type
*	T: the target type
*/
public interface Converter<S, T> {
    @Nullable
    //该方法就是将从页面上接收的数据(S)转换成我们想要的数据类型(T)返回
    T convert(S source);
}

注意:Converter所属的包为org.springframework.core.convert.converter

Converter接口的实现类

1630496385398

框架中有提供很多对应Converter接口的实现类,用来实现不同数据类型之间的转换,如:

请求参数年龄数据(String→Integer)

日期格式转换(String → Date)

  • (2) HttpMessageConverter接口

该接口是实现对象与JSON之间的转换工作

注意:SpringMVC的配置类把@EnableWebMvc当做标配配置上去,不要省略

4.6 响应

SpringMVC接收到请求和数据后,进行一些了的处理,当然这个处理可以是转发给Service,Service层再调用Dao层完成的,不管怎样,处理完以后,都需要将结果告知给用户。

比如:根据用户ID查询用户信息、查询用户列表、新增用户等。

对于响应,主要就包含两部分内容:

  • 响应页面
  • 响应数据
    • 文本数据
    • json数据

因为异步调用是目前常用的主流方式,所以更关注的就是如何返回JSON数据,对于其他认识了解即可。

4.6.1 环境准备

  • 创建一个Web的Maven项目

  • pom.xml添加Spring依赖

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.duan</groupId>
      <artifactId>springmvc_05_response</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <dependencies>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.9.0</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.1</version>
            <configuration>
              <port>80</port>
              <path>/</path>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
    
  • 创建对应的配置类

    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
        protected Class<?>[] getRootConfigClasses() {
            return new Class[0];
        }
    
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
    
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
    
        //乱码处理
        @Override
        protected Filter[] getServletFilters() {
            CharacterEncodingFilter filter = new CharacterEncodingFilter();
            filter.setEncoding("UTF-8");
            return new Filter[]{filter};
        }
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan("com.duan.controller")
    //开启json数据类型自动转换
    @EnableWebMvc
    public class SpringMvcConfig {
    }
    
    
    
  • 编写模型类User

    public class User {
        private String name;
        private int age;
        //getter...setter...toString省略
    }
    
  • webapp下创建page.jsp

    <html>
    <body>
    <h2>Hello Spring MVC!</h2>
    </body>
    </html>
    
  • 编写UserController

    @Controller
    public class UserController {
    }
    

4.6.2 响应页面[了解]

步骤1:设置返回页面
@Controller
public class UserController {
    
    @RequestMapping("/toJumpPage")
    //注意
    //1.此处不能添加@ResponseBody,如果加了该注入,会直接将page.jsp当字符串返回前端
    //2.方法需要返回String
    public String toJumpPage(){
        System.out.println("跳转页面");
        return "page.jsp";
    }
    
}
步骤2:启动程序测试

此处涉及到页面跳转,所以不适合采用apipost进行测试,直接打开浏览器,输入

http://localhost/toJumpPage

1630497496785

4.6.3 返回文本数据[了解]

步骤1:设置返回文本内容
@Controller
public class UserController {
    
   	@RequestMapping("/toText")
	//注意此处该注解就不能省略,如果省略了,会把response text当前页面名称去查找
    @ResponseBody
    public String toText(){
        System.out.println("返回纯文本数据");
        return "response text";
    }
    
}
步骤2:启动程序测试

此处不涉及到页面跳转,因为发送的是GET请求,可以使用浏览器也可以使用apipost进行测试,输入地址http://localhost/toText访问

4.6.4 响应JSON数据

响应POJO对象
@Controller
public class UserController {
    @RequestMapping("/toJsonPOJO")
    @ResponseBody
    public User toJsonPOJO(){
        System.out.println("返回json对象数据");
        User user = new User();
        user.setName("duan");
        user.setAge(20);
        return user;
    }
    
}

返回值为实体类对象,设置返回值为实体类类型,即可实现返回对应对象的json数据,需要依赖@ResponseBody注解和@EnableWebMvc注解

重新启动服务器,访问http://localhost/toJsonPOJO

输出结果为:

{

​ "name": "duan",

​ "age": 15

}

响应POJO集合对象
@Controller
public class UserController {
    
    @RequestMapping("/toJsonList")
    @ResponseBody
    public List<User> toJsonList(){
        System.out.println("返回json集合数据");
        User user1 = new User();
        user1.setName("duan");
        user1.setAge(20);

        User user2 = new User();
        user2.setName("thirty");
        user2.setAge(12);

        List<User> userList = new ArrayList<User>();
        userList.add(user1);
        userList.add(user2);

        return userList;
    }
    
}

重新启动服务器,访问http://localhost/toJsonList

输出结果为:

{

​ "name": "duan",

​ "age": 20

}

{
"name": "thirty",
"age": 12

}

@ResponseBody

名称 @ResponseBody
类型 方法\类注解
位置 SpringMVC控制器方法定义上方和控制类上
作用 设置当前控制器返回值作为响应体,
写在类上,该类的所有方法都有该注解功能
相关属性 pattern:指定日期时间格式字符串

说明:

  • 该注解可以写在类上或者方法上
  • 写在类上就是该类下的所有方法都有@ReponseBody功能
  • 当方法上有@ReponseBody注解后
    • 方法的返回值为字符串,会将其作为文本内容直接响应给前端
    • 方法的返回值为对象,会将对象转换成JSON响应给前端

此处又使用到了类型转换,内部还是通过Converter接口的实现类完成的,所以Converter除了前面所说的功能外,它还可以实现:

  • 对象转Json数据(POJO -> json)
  • 集合转Json数据(Collection -> json)

5,Rest风格

5.1 REST简介

  • REST(Representational State Transfer),表现形式状态转换,它是一种软件架构风格

    表示一个网络资源的时候,可以使用两种方式:

    • 传统风格资源描述形式
      • http://localhost/user/getById?id=1 查询id为1的用户信息
      • http://localhost/user/saveUser 保存用户信息
    • REST风格描述形式
      • http://localhost/user/1
      • http://localhost/user

传统方式一般是一个请求url对应一种操作,这样做不仅麻烦,也不安全,因为会程序的人读取了你的请求url地址,就大概知道该url实现的是一个什么样的操作。

所以REST的优点有:

  • 隐藏资源的访问行为,无法通过地址得知对资源是何种操作
  • 书写简化

一个相同的url地址即可以是新增也可以是修改或者查询,那么该如何区分该请求是什么操作呢?

  • 按照REST风格访问资源时使用行为动作区分对资源进行了何种操作
    • http://localhost/users 查询全部用户信息 GET(查询)
    • http://localhost/users/1 查询指定用户信息 GET(查询)
    • http://localhost/users 添加用户信息 POST(新增/保存)
    • http://localhost/users 修改用户信息 PUT(修改/更新)
    • http://localhost/users/1 删除用户信息 DELETE(删除)

请求的方式比较多,但是比较常用的就4种,分别是GET,POST,PUT,DELETE

按照不同的请求方式代表不同的操作类型。

  • 发送GET请求是用来做查询
  • 发送POST请求是用来做新增
  • 发送PUT请求是用来做修改
  • 发送DELETE请求是用来做删除

注意:

  • 上述行为是约定方式,约定不是规范,可以打破,所以称REST风格,而不是REST规范
    • REST提供了对应的架构方式,按照这种架构设计项目可以降低开发的复杂性,提高系统的可伸缩性
    • REST中规定GET/POST/PUT/DELETE针对的是查询/新增/修改/删除,但是如果用GET请求做删除,这点在程序上运行是可以实现的。但是绝大多数人都遵循这种风格,写的让别人读起来就有点莫名其妙了。
  • 描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如:users、books、accounts......

什么是RESTful?

  • 根据REST风格对资源进行访问称为RESTful

5.2 RESTful入门案例

5.2.1 环境准备

  • 创建一个Web的Maven项目

  • pom.xml添加Spring依赖

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.duan</groupId>
      <artifactId>springmvc_06_rest</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <dependencies>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.9.0</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.1</version>
            <configuration>
              <port>80</port>
              <path>/</path>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
    
  • 创建对应的配置类

    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
        protected Class<?>[] getRootConfigClasses() {
            return new Class[0];
        }
    
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
    
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
    
        //乱码处理
        @Override
        protected Filter[] getServletFilters() {
            CharacterEncodingFilter filter = new CharacterEncodingFilter();
            filter.setEncoding("UTF-8");
            return new Filter[]{filter};
        }
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan("com.duan.controller")
    //开启json数据类型自动转换
    @EnableWebMvc
    public class SpringMvcConfig {
    }
    
    
    
  • 编写模型类User和Book

    public class User {
        private String name;
        private int age;
        //getter...setter...toString省略
    }
    --------------------------------------------------------------------------
    public class Book {
        private String name;
        private double price;
         //getter...setter...toString省略
    }
    
  • 编写UserController和BookController

    @Controller
    public class UserController {
    	@RequestMapping("/save")
        @ResponseBody
        public String save(@RequestBody User user) {
            System.out.println("user save..."+user);
            return "{'module':'user save'}";
        }
    
        @RequestMapping("/delete")
        @ResponseBody
        public String delete(Integer id) {
            System.out.println("user delete..." + id);
            return "{'module':'user delete'}";
        }
    
        @RequestMapping("/update")
        @ResponseBody
        public String update(@RequestBody User user) {
            System.out.println("user update..." + user);
            return "{'module':'user update'}";
        }
    
        @RequestMapping("/getById")
        @ResponseBody
        public String getById(Integer id) {
            System.out.println("user getById..." + id);
            return "{'module':'user getById'}";
        }
    
        @RequestMapping("/findAll")
        @ResponseBody
        public String getAll() {
            System.out.println("user getAll...");
            return "{'module':'user getAll'}";
        }
    }
    --------------------------------------------------------------------------
    @Controller
    public class BookController {
        
    	@RequestMapping(value = "/books",method = RequestMethod.POST)
        @ResponseBody
        public String save(@RequestBody Book book){
            System.out.println("book save..." + book);
            return "{'module':'book save'}";
        }
    
        @RequestMapping(value = "/books/{id}",method = RequestMethod.DELETE)
        @ResponseBody
        public String delete(@PathVariable Integer id){
            System.out.println("book delete..." + id);
            return "{'module':'book delete'}";
        }
    
        @RequestMapping(value = "/books",method = RequestMethod.PUT)
        @ResponseBody
        public String update(@RequestBody Book book){
            System.out.println("book update..." + book);
            return "{'module':'book update'}";
        }
    
        @RequestMapping(value = "/books/{id}",method = RequestMethod.GET)
        @ResponseBody
        public String getById(@PathVariable Integer id){
            System.out.println("book getById..." + id);
            return "{'module':'book getById'}";
        }
    
        @RequestMapping(value = "/books",method = RequestMethod.GET)
        @ResponseBody
        public String getAll(){
            System.out.println("book getAll...");
            return "{'module':'book getAll'}";
        }
        
    }
    

5.2.2 思路分析

需求:将之前的增删改查替换成RESTful的开发方式。

1.之前不同的请求有不同的路径,现在要将其修改为统一的请求路径

修改前: 新增: /save ,修改: /update,删除 /delete...

修改后: 增删改查: /users

2.根据GET查询、POST新增、PUT修改、DELETE删除对方法的请求方式进行限定

3.发送请求的过程中如何设置请求参数?

5.2.3 修改RESTful风格

新增
@Controller
public class UserController {
	//设置当前请求方法为POST,表示REST风格中的添加操作
    @RequestMapping(value = "/users",method = RequestMethod.POST)
    @ResponseBody
    public String save() {
        System.out.println("user save...");
        return "{'module':'user save'}";
    }
}
  • 将请求路径更改为/users

    • 访问该方法使用 POST: http://localhost/users
  • 使用method属性限定该方法的访问方式为POST

    • 如果发送的不是POST请求,比如发送GET请求,则会报错
删除
@Controller
public class UserController {
    //设置当前请求方法为DELETE,表示REST风格中的删除操作
	@RequestMapping(value = "/users",method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(Integer id) {
        System.out.println("user delete..." + id);
        return "{'module':'user delete'}";
    }
}
  • 将请求路径更改为/users
    • 访问该方法使用 DELETE: http://localhost/users

访问成功,但是删除方法没有携带所要删除数据的id,所以针对RESTful的开发,如何携带数据参数?

传递路径参数

前端发送请求的时候使用:http://localhost/users/1,路径中的1就是要传递的参数。

后端获取参数,需要做如下修改:

  • 修改@RequestMapping的value属性,将其中修改为/users/{id},目的是和路径匹配
  • 在方法的形参前添加@PathVariable注解
@Controller
public class UserController {
    //设置当前请求方法为DELETE,表示REST风格中的删除操作
	@RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(@PathVariable Integer id) {
        System.out.println("user delete..." + id);
        return "{'module':'user delete'}";
    }
}

思考如下两个问题:

(1)如果方法形参的名称和路径{}中的值不一致,该怎么办?

1630506231379

(2)如果有多个参数需要传递该如何编写?

前端发送请求的时候使用:http://localhost/users/1/tom,路径中的1tom就是要传递的两个参数。

后端获取参数,需要做如下修改:

@Controller
public class UserController {
    //设置当前请求方法为DELETE,表示REST风格中的删除操作
	@RequestMapping(value = "/users/{id}/{name}",method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(@PathVariable Integer id,@PathVariable String name) {
        System.out.println("user delete..." + id+","+name);
        return "{'module':'user delete'}";
    }
}
修改
@Controller
public class UserController {
    //设置当前请求方法为PUT,表示REST风格中的修改操作
    @RequestMapping(value = "/users",method = RequestMethod.PUT)
    @ResponseBody
    public String update(@RequestBody User user) {
        System.out.println("user update..." + user);
        return "{'module':'user update'}";
    }
}
  • 将请求路径更改为/users

    • 访问该方法使用 PUT: http://localhost/users
  • 访问并携带参数:

    1630506507096

根据ID查询
@Controller
public class UserController {
    //设置当前请求方法为GET,表示REST风格中的查询操作
    @RequestMapping(value = "/users/{id}" ,method = RequestMethod.GET)
    @ResponseBody
    public String getById(@PathVariable Integer id){
        System.out.println("user getById..."+id);
        return "{'module':'user getById'}";
    }
}

将请求路径更改为/users

  • 访问该方法使用 GET: http://localhost/users/1
查询所有
@Controller
public class UserController {
    //设置当前请求方法为GET,表示REST风格中的查询操作
    @RequestMapping(value = "/users" ,method = RequestMethod.GET)
    @ResponseBody
    public String getAll() {
        System.out.println("user getAll...");
        return "{'module':'user getAll'}";
    }
}

将请求路径更改为/users

  • 访问该方法使用 GET: http://localhost/users

小结

(1)设定Http请求动作(动词)

@RequestMapping(value="",method = RequestMethod.POST|GET|PUT|DELETE)

(2)设定请求参数(路径变量)

@RequestMapping(value="/users/{id}",method = RequestMethod.DELETE)

@ReponseBody

public String delete(@PathVariable Integer id){

}

@PathVariable

名称 @PathVariable
类型 形参注解
位置 SpringMVC控制器方法形参定义前面
作用 绑定路径参数与处理器方法形参间的关系,要求路径参数名与形参名一一对应

关于接收参数,@RequestBody@RequestParam@PathVariable,这三个注解之间的区别和应用分别是什么?

  • 区别
    • @RequestParam用于接收url地址传参或表单传参
    • @RequestBody用于接收json数据
    • @PathVariable用于接收路径参数,使用{参数名称}描述路径参数
  • 应用
    • 后期开发中,发送请求参数超过1个时,以json格式为主,@RequestBody应用较广
    • 如果发送非json格式数据,选用@RequestParam接收请求参数
    • 采用RESTful进行开发,当参数数量较少时,例如1个,可以采用@PathVariable接收请求路径变量,通常用于传递id值

5.3 RESTful快速开发

问题1:每个方法的@RequestMapping注解中都定义了访问路径/books,重复性太高。

问题2:每个方法的@RequestMapping注解中都要使用method属性定义请求方式,重复性太高。

问题3:每个方法响应json都需要加上@ResponseBody注解,重复性太高。

对于上面所提的这三个问题,具体该如何解决?

@RestController //@Controller + ResponseBody
@RequestMapping("/books")
public class BookController {
    
	//@RequestMapping(method = RequestMethod.POST)
    @PostMapping
    public String save(@RequestBody Book book){
        System.out.println("book save..." + book);
        return "{'module':'book save'}";
    }

    //@RequestMapping(value = "/{id}",method = RequestMethod.DELETE)
    @DeleteMapping("/{id}")
    public String delete(@PathVariable Integer id){
        System.out.println("book delete..." + id);
        return "{'module':'book delete'}";
    }

    //@RequestMapping(method = RequestMethod.PUT)
    @PutMapping
    public String update(@RequestBody Book book){
        System.out.println("book update..." + book);
        return "{'module':'book update'}";
    }

    //@RequestMapping(value = "/{id}",method = RequestMethod.GET)
    @GetMapping("/{id}")
    public String getById(@PathVariable Integer id){
        System.out.println("book getById..." + id);
        return "{'module':'book getById'}";
    }

    //@RequestMapping(method = RequestMethod.GET)
    @GetMapping
    public String getAll(){
        System.out.println("book getAll...");
        return "{'module':'book getAll'}";
    }
    
}

相应的解决方案:

问题1:每个方法的@RequestMapping注解中都定义了访问路径/books,重复性太高。

将@RequestMapping提到类上面,用来定义所有方法共同的访问路径。

问题2:每个方法的@RequestMapping注解中都要使用method属性定义请求方式,重复性太高。

使用@GetMapping  @PostMapping  @PutMapping  @DeleteMapping代替

问题3:每个方法响应json都需要加上@ResponseBody注解,重复性太高。

1.将ResponseBody提到类上面,让所有的方法都有@ResponseBody的功能
2.使用@RestController注解替换@Controller与@ResponseBody注解,简化书写

@RestController

名称 @RestController
类型 类注解
位置 基于SpringMVC的RESTful开发控制器类定义上方
作用 设置当前控制器类为RESTful风格,
等同于@Controller与@ResponseBody两个注解组合功能

@GetMapping @PostMapping @PutMapping @DeleteMapping

名称 @GetMapping @PostMapping @PutMapping @DeleteMapping
类型 方法注解
位置 基于SpringMVC的RESTful开发控制器方法定义上方
作用 设置当前控制器方法请求访问路径与请求动作,每种对应一个请求动作,
例如@GetMapping对应GET请求
相关属性 value(默认):请求访问路径

SSM整合

内容

  • 完成SSM的整合开发
  • 能够理解并实现统一结果封装与统一异常处理
  • 能够完成前后台功能整合开发
  • 掌握拦截器的编写

1,SSM整合

1.1 流程分析

(1) 创建工程

  • 创建一个Maven的web工程
  • pom.xml添加SSM需要的依赖jar包
  • 编写Web项目的入口配置类,实现AbstractAnnotationConfigDispatcherServletInitializer重写以下方法
    • getRootConfigClasses() :返回Spring的配置类->需要SpringConfig配置类
    • getServletConfigClasses() :返回SpringMVC的配置类->需要SpringMvcConfig配置类
    • getServletMappings() : 设置SpringMVC请求拦截路径规则
    • getServletFilters() :设置过滤器,解决POST请求中文乱码问题

(2)SSM整合[重点是各个配置的编写]

  • SpringConfig
    • 标识该类为配置类 @Configuration
    • 扫描Service所在的包 @ComponentScan
    • 在Service层要管理事务 @EnableTransactionManagement
    • 读取外部的properties配置文件 @PropertySource
    • 整合Mybatis需要引入Mybatis相关配置类 @Import
      • 第三方数据源配置类 JdbcConfig
        • 构建DataSource数据源,DruidDataSouroce,需要注入数据库连接四要素, @Bean @Value
        • 构建平台事务管理器,DataSourceTransactionManager,@Bean
      • Mybatis配置类 MybatisConfig
        • 构建SqlSessionFactoryBean并设置别名扫描与数据源,@Bean
        • 构建MapperScannerConfigurer并设置DAO层的包扫描
  • SpringMvcConfig
    • 标识该类为配置类 @Configuration
    • 扫描Controller所在的包 @ComponentScan
    • 开启SpringMVC注解支持 @EnableWebMvc

(3)功能模块[与具体的业务模块有关]

  • 创建数据库表
  • 根据数据库表创建对应的模型类
  • 通过Dao层完成数据库表的增删改查(接口+自动代理)
  • 编写Service层[Service接口+实现类]
    • @Service
    • @Transactional
    • 整合Junit对业务层进行单元测试
      • @RunWith
      • @ContextConfiguration
      • @Test
  • 编写Controller层
    • 接收请求 @RequestMapping @GetMapping @PostMapping @PutMapping @DeleteMapping
    • 接收数据 简单、POJO、嵌套POJO、集合、数组、JSON数据类型
      • @RequestParam
      • @PathVariable
      • @RequestBody
    • 转发业务层
      • @Autowired
    • 响应结果
      • @ResponseBody

1.2 整合配置

步骤1:创建Maven的web项目

步骤2:添加依赖

pom.xml添加SSM所需要的依赖jar包

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.duan</groupId>
  <artifactId>springmvc_08_ssm</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

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

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

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

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>

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

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

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.1</version>
        <configuration>
          <port>80</port>
          <path>/</path>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>


步骤3:创建项目包结构

image-20230622181455321

  • config目录存放的是相关的配置类
  • controller编写的是Controller类
  • dao存放的是Dao接口,因为使用的是Mapper接口代理方式,所以没有实现类包
  • service存的是Service接口,impl存放的是Service实现类
  • resources:存入的是配置文件,如Jdbc.properties
  • webapp:目录可以存放静态资源
  • test/java:存放的是测试类

步骤4:创建SpringConfig配置类

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

步骤5:创建JdbcConfig配置类

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

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

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager ds = new DataSourceTransactionManager();
        ds.setDataSource(dataSource);
        return ds;
    }
}

步骤6:创建MybatisConfig配置类

public class MyBatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setTypeAliasesPackage("com.duan.domain");
        return factoryBean;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.duan.dao");
        return msc;
    }
}

步骤7:创建jdbc.properties

在resources下提供jdbc.properties,设置数据库连接四要素

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db?serviceTimezone=Asia/Shanghai&useSSL=false
jdbc.username=root
jdbc.password=1234

步骤8:创建SpringMVC配置类

@Configuration
@ComponentScan("com.duan.controller")
@EnableWebMvc
public class SpringMvcConfig {
}

步骤9:创建Web项目入口配置类

public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    //加载Spring配置类
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }
    //加载SpringMVC配置类
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }
    //设置SpringMVC请求地址拦截规则
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
    //设置post请求中文乱码过滤器
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding("utf-8");
        return new Filter[]{filter};
    }
}

1.3 功能模块开发

需求:对表tbl_book进行新增、修改、删除、根据ID查询和查询所有

步骤1:创建数据库及表

create database ssm_db character set utf8;
use ssm_db;
create table tbl_book(
  id int primary key auto_increment,
  type varchar(20),
  name varchar(50),
  description varchar(255)
)

insert  into `tbl_book`(`id`,`type`,`name`,`description`) values (1,'计算机理论','Spring实战 第五版','Spring入门经典教程,深入理解Spring原理技术内幕'),(2,'计算机理论','Spring 5核心原理与30个类手写实践','十年沉淀之作,手写Spring精华思想'),(3,'计算机理论','Spring 5设计模式','深入Spring源码刨析Spring源码中蕴含的10大设计模式'),(4,'计算机理论','Spring MVC+Mybatis开发从入门到项目实战','全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'),(5,'计算机理论','轻量级Java Web企业应用实战','源码级刨析Spring框架,适合已掌握Java基础的读者'),(6,'计算机理论','Java核心技术 卷Ⅰ 基础知识(原书第11版)','Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新'),(7,'计算机理论','深入理解Java虚拟机','5个纬度全面刨析JVM,大厂面试知识点全覆盖'),(8,'计算机理论','Java编程思想(第4版)','Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉'),(9,'计算机理论','零基础学Java(全彩版)','零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术'),(10,'市场营销','直播就这么做:主播高效沟通实战指南','李子柒、李佳奇、薇娅成长为网红的秘密都在书中'),(11,'市场营销','直播销讲实战一本通','和秋叶一起学系列网络营销书籍'),(12,'市场营销','直播带货:淘宝、天猫直播从新手到高手','一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');

步骤2:编写模型类

public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;
    //getter...setter...toString省略
}

步骤3:编写Dao接口

public interface BookDao {

//    @Insert("insert into tbl_book values(null,#{type},#{name},#{description})")
    @Insert("insert into tbl_book (type,name,description) values(#{type},#{name},#{description})")
    public void save(Book book);

    @Update("update tbl_book set type = #{type}, name = #{name}, description = #{description} where id = #{id}")
    public void update(Book book);

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

    @Select("select * from tbl_book where id = #{id}")
    public Book getById(Integer id);

    @Select("select * from tbl_book")
    public List<Book> getAll();
}

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

@Transactional
public interface BookService {
    /**
     * 保存
     * @param book
     * @return
     */
    public boolean save(Book book);

    /**
     * 修改
     * @param book
     * @return
     */
    public boolean update(Book book);

    /**
     * 按id删除
     * @param id
     * @return
     */
    public boolean delete(Integer id);

    /**
     * 按id查询
     * @param id
     * @return
     */
    public Book getById(Integer id);

    /**
     * 查询全部
     * @return
     */
    public List<Book> getAll();
}
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    public boolean save(Book book) {
        bookDao.save(book);
        return true;
    }

    public boolean update(Book book) {
        bookDao.update(book);
        return true;
    }

    public boolean delete(Integer id) {
        bookDao.delete(id);
        return true;
    }

    public Book getById(Integer id) {
        return bookDao.getById(id);
    }

    public List<Book> getAll() {
        return bookDao.getAll();
    }
}

说明:

  • bookDao在Service中注入的会提示一个红线提示,为什么呢?

    • BookDao是一个接口,没有实现类,接口是不能创建对象的,所以最终注入的应该是代理对象
    • 代理对象是由Spring的IOC容器来创建管理的
    • IOC容器又是在Web服务器启动的时候才会创建
    • IDEA在检测依赖关系的时候,没有找到适合的类注入,所以会提示错误提示
    • 但是程序运行的时候,代理对象就会被创建,框架会使用DI进行注入,所以程序运行无影响。
  • 如何解决上述问题?

    • 可以不用理会,因为运行是正常的

    • 设置错误提示级别为警告

步骤5:编写Contorller类

@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

    @PostMapping
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }

    @PutMapping
    public boolean update(@RequestBody Book book) {
        return bookService.update(book);
    }

    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable Integer id) {
        return bookService.delete(id);
    }

    @GetMapping("/{id}")
    public Book getById(@PathVariable Integer id) {
        return bookService.getById(id);
    }

    @GetMapping
    public List<Book> getAll() {
        return bookService.getAll();
    }
}

1.4 单元测试

步骤1:新建测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookServiceTest {

}

步骤2:注入Service类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookServiceTest {

    @Autowired
    private BookService bookService;


}

步骤3:编写测试方法

我们先来对查询进行单元测试。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookServiceTest {

    @Autowired
    private BookService bookService;

    @Test
    public void testGetById(){
        Book book = bookService.getById(1);
        System.out.println(book);
    }

    @Test
    public void testGetAll(){
        List<Book> all = bookService.getAll();
        System.out.println(all);
    }

}

1.5 apipost测试

新增

http://localhost/books

{
	"type":"类别测试数据",
    "name":"书名测试数据",
    "description":"描述测试数据"
}

修改

http://localhost/books

{
    "id":13,
	"type":"类别测试数据",
    "name":"书名测试数据",
    "description":"描述测试数据"
}

删除

http://localhost/books/14

查询单个

http://localhost/books/1

查询所有

http://localhost/books

2,统一结果封装

2.1 表现层与前端数据传输协议定义

  • 在Controller层增删改返回给前端的是boolean类型数据

    1630653359533

  • 在Controller层查询单个返回给前端的是对象

    1630653385377

  • 在Controller层查询所有返回给前端的是集合对象

    1630653468887

目前就已经有三种数据类型返回给前端,如果随着业务的增长,返回的数据类型会越来越多。对于前端开发人员在解析数据的时候就比较凌乱了,所以对于前端来说,如果后台能够返回一个统一的数据结果,前端在解析的时候就可以按照一种方式进行解析。开发就会变得更加简单。

大体的思路为:

  • 为了封装返回的结果数据:创建结果模型类,封装数据到data属性中
  • 为了封装返回的数据是何种操作及是否操作成功:封装操作结果到code属性中
  • 操作失败后为了封装返回的错误信息:封装特殊消息到message(msg)属性中

1630654293972

根据分析,可以设置统一数据返回结果类

public class Result{
	private Object data;
	private Integer code;
	private String msg;
}

注意:Result类名及类中的字段并不是固定的,可以根据需要自行增减提供若干个构造方法,方便操作。

2.2 表现层与前端数据传输协议实现

2.2.1 环境准备

  • 创建一个Web的Maven项目
  • pom.xml添加SSM整合所需jar包
  • 创建对应的配置类
  • 编写Controller、Service接口、Service实现类、Dao接口和模型类
  • resources下提供jdbc.properties配置文件

2.2.2 结果封装

对于结果封装,在表现层进行处理,因此把结果类放在controller包下,具体的步骤为:

步骤1:创建Result类
public class Result {
    //描述统一格式中的数据
    private Object data;
    //描述统一格式中的编码,用于区分操作,可以简化配置0或1表示成功失败
    private Integer code;
    //描述统一格式中的消息,可选属性
    private String msg;

    public Result() {
    }
	//构造方法是方便对象的创建
    public Result(Integer code,Object data) {
        this.data = data;
        this.code = code;
    }
	//构造方法是方便对象的创建
    public Result(Integer code, Object data, String msg) {
        this.data = data;
        this.code = code;
        this.msg = msg;
    }
	//setter...getter...省略
}
步骤2:定义返回码Code类
//状态码
public class Code {
    public static final Integer SAVE_OK = 20011;
    public static final Integer DELETE_OK = 20021;
    public static final Integer UPDATE_OK = 20031;
    public static final Integer GET_OK = 20041;

    public static final Integer SAVE_ERR = 20010;
    public static final Integer DELETE_ERR = 20020;
    public static final Integer UPDATE_ERR = 20030;
    public static final Integer GET_ERR = 20040;
}

注意:code类中的常量设计也不是固定的,可以根据需要自行增减,例如将查询再进行细分为GET_OK,GET_ALL_OK,GET_PAGE_OK等。

步骤3:修改Controller类的返回值
//统一每一个控制器方法返回值
@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

    @PostMapping
    public Result save(@RequestBody Book book) {
        boolean flag = bookService.save(book);
        return new Result(flag ? Code.SAVE_OK:Code.SAVE_ERR,flag);
    }

    @PutMapping
    public Result update(@RequestBody Book book) {
        boolean flag = bookService.update(book);
        return new Result(flag ? Code.UPDATE_OK:Code.UPDATE_ERR,flag);
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        boolean flag = bookService.delete(id);
        return new Result(flag ? Code.DELETE_OK:Code.DELETE_ERR,flag);
    }

    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id) {
        Book book = bookService.getById(id);
        Integer code = book != null ? Code.GET_OK : Code.GET_ERR;
        String msg = book != null ? "" : "数据查询失败,请重试!";
        return new Result(code,book,msg);
    }

    @GetMapping
    public Result getAll() {
        List<Book> bookList = bookService.getAll();
        Integer code = bookList != null ? Code.GET_OK : Code.GET_ERR;
        String msg = bookList != null ? "" : "数据查询失败,请重试!";
        return new Result(code,bookList,msg);
    }
}

至此,返回结果就已经能以一种统一的格式返回给前端。前端根据返回的结果,先从中获取code,根据code判断,如果成功则取data属性的值,如果失败,则取msg中的值做提示。

3,统一异常处理

3.1 问题描述

在解决问题之前,先来看下异常的种类及出现异常的原因:

  • 框架内部抛出的异常:因使用不合规导致
  • 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
  • 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
  • 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
  • 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)

看完上面这些出现异常的位置,在开发的任何一个位置都有可能出现异常,而且这些异常是不能避免的。所以就得将异常进行处理。

思考

  1. 各个层级均出现异常,异常处理代码书写在哪一层?

    所有的异常均抛出到表现层进行处理

  2. 异常的种类很多,表现层如何将所有的异常都处理到呢?

    异常分类

  3. 表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不强,如何解决?

    AOP

对于上面这些问题及解决方案,SpringMVC提供了一套解决方案:

  • 异常处理器:

    • 集中的、统一的处理项目中出现的异常。

      1630657791653

3.2 异常处理器的使用

3.2.1 环境准备

  • 创建一个Web的Maven项目
  • pom.xml添加SSM整合所需jar包
  • 创建对应的配置类
  • 编写Controller、Service接口、Service实现类、Dao接口和模型类
  • resources下提供jdbc.properties配置文件

内容参考前面的项目或者直接使用前面的项目进行本节内容的学习。

最终创建好的项目结构如下:

3.2.2 使用步骤

步骤1:创建异常处理器类
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
    //除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
    @ExceptionHandler(Exception.class)
    public void doException(Exception ex){
      	System.out.println("嘿嘿,异常你哪里跑!")
    }
}

确保SpringMvcConfig能够扫描到异常处理器类

步骤2:让程序抛出异常

修改BookController的getById方法,添加int i = 1/0.

@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
  	int i = 1/0;
    Book book = bookService.getById(id);
    Integer code = book != null ? Code.GET_OK : Code.GET_ERR;
    String msg = book != null ? "" : "数据查询失败,请重试!";
    return new Result(code,book,msg);
}
步骤3:运行程序,测试

异常已经被拦截并执行了doException方法。

异常处理器类返回结果给前端
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
    //除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
    @ExceptionHandler(Exception.class)
    public Result doException(Exception ex){
      	System.out.println("嘿嘿,异常你哪里跑!")
        return new Result(666,null,"嘿嘿,异常你哪里跑!");
    }
}

启动运行程序,测试

1630658606549

至此,就算后台执行的过程中抛出异常,最终也能按照约定好的格式返回给前端。

@RestControllerAdvice

名称 @RestControllerAdvice
类型 类注解
位置 Rest风格开发的控制器增强类定义上方
作用 为Rest风格开发的控制器类做增强

说明:此注解自带@ResponseBody注解与@Component注解,具备对应的功能

1630659060451

@ExceptionHandler

名称 @ExceptionHandler
类型 方法注解
位置 专用于异常处理的控制器方法上方
作用 设置指定异常的处理方案,功能等同于控制器方法,
出现异常后终止原始控制器执行,并转入当前方法执行

说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常

3.3 项目异常处理方案

3.3.1 异常分类

那么在项目中该如何来处理异常呢?

因为异常的种类有很多,如果每一个异常都对应一个@ExceptionHandler,那得写多少个方法来处理各自的异常,所以处理异常之前,需要对异常进行一个分类:

  • 业务异常(BusinessException)

    • 规范的用户行为产生的异常

      • 用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串

        1630659599983

    • 不规范的用户行为操作产生的异常

      • 如用户故意传递错误数据

        1630659622958

  • 系统异常(SystemException)

    • 项目运行过程中可预计但无法避免的异常
      • 比如数据库或服务器宕机
  • 其他异常(Exception)

    • 编程人员未预期到的异常,如:用到的文件不存在

      1630659690341

将异常分类以后,针对不同类型的异常,要提供具体的解决方案:

3.3.2 异常解决方案

  • 业务异常(BusinessException)

    • 发送对应消息传递给用户,提醒规范操作

      常见的就是提示用户名已存在或密码格式不正确等

  • 系统异常(SystemException)

    • 发送固定消息传递给用户,安抚用户
      • 系统繁忙,请稍后再试
      • 系统正在维护升级,请稍后再试
      • 系统出问题,请联系系统管理员等
    • 发送特定消息给运维人员,提醒维护
      • 可以发送短信、邮箱或者是公司内部通信软件
    • 记录日志
      • 发消息和记录日志对用户来说是不可见的,属于后台程序
  • 其他异常(Exception)

    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给编程人员,提醒维护(纳入预期范围内)
      • 一般是程序没有考虑全,比如未做非空校验等
    • 记录日志

3.3.3 异常解决方案的具体实现

思路:

1.先通过自定义异常,完成BusinessException和SystemException的定义

2.将其他异常包装成自定义异常类型

3.在异常处理器类中对不同的异常进行处理

步骤1:自定义异常类
//自定义异常处理器,用于封装异常信息,对异常进行分类
public class SystemException extends RuntimeException{
    private Integer code;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public SystemException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public SystemException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

}

//自定义异常处理器,用于封装异常信息,对异常进行分类
public class BusinessException extends RuntimeException{
    private Integer code;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

}


说明:

  • 让自定义异常类继承RuntimeException的好处是,后期在抛出这两个异常的时候,就不用在try...catch...或throws了
  • 自定义异常类中添加code属性的原因是为了更好的区分异常是来自哪个业务的
步骤2:将其他异常包成自定义异常

假如在BookServiceImpl的getById方法抛异常了,该如何来包装呢?

public Book getById(Integer id) {
    //模拟业务异常,包装成自定义异常
    if(id == 1){
        throw new BusinessException(Code.BUSINESS_ERR,"请不要使用你的技术挑战我的耐性!");
    }
    //模拟系统异常,将可能出现的异常进行包装,转换成自定义异常
    try{
        int i = 1/0;
    }catch (Exception e){
        throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试!",e);
    }
    return bookDao.getById(id);
}

具体的包装方式有:

  • 方式一:try{}catch(){}在catch中重新throw自定义异常即可。
  • 方式二:直接throw自定义异常即可
//状态码
public class Code {
    public static final Integer SAVE_OK = 20011;
    public static final Integer DELETE_OK = 20021;
    public static final Integer UPDATE_OK = 20031;
    public static final Integer GET_OK = 20041;

    public static final Integer SAVE_ERR = 20010;
    public static final Integer DELETE_ERR = 20020;
    public static final Integer UPDATE_ERR = 20030;
    public static final Integer GET_ERR = 20040;
    public static final Integer SYSTEM_ERR = 50001;
    public static final Integer SYSTEM_TIMEOUT_ERR = 50002;
    public static final Integer SYSTEM_UNKNOW_ERR = 59999;

    public static final Integer BUSINESS_ERR = 60002;
}

步骤3:处理器类中处理自定义异常
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
    //@ExceptionHandler用于设置当前处理器类对应的异常类型
    @ExceptionHandler(SystemException.class)
    public Result doSystemException(SystemException ex){
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        return new Result(ex.getCode(),null,ex.getMessage());
    }

    @ExceptionHandler(BusinessException.class)
    public Result doBusinessException(BusinessException ex){
        return new Result(ex.getCode(),null,ex.getMessage());
    }

    //除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
    @ExceptionHandler(Exception.class)
    public Result doOtherException(Exception ex){
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        return new Result(Code.SYSTEM_UNKNOW_ERR,null,"系统繁忙,请稍后再试!");
    }
}
步骤4:运行程序

根据ID查询,

如果传入的参数为1,会报BusinessException

如果传入的是其他参数,会报SystemException

小结

以后项目中的异常处理方式为:

1630658821746

4,前后台协议联调

4.1 环境准备

  • 创建一个Web的Maven项目
  • pom.xml添加SSM整合所需jar包
  • 创建对应的配置类
  • 编写Controller、Service接口、Service实现类、Dao接口和模型类
  • resources下提供jdbc.properties配置文件

内容参考前面的项目或者直接使用前面的项目进行本节内容的学习。

  1. 将静态资源拷贝到webapp下。

1630663662691

  1. 因为添加了静态资源,SpringMVC会拦截,所有需要在SpringConfig的配置类中将静态资源进行放行。
  • 新建SpringMvcSupport

    @Configuration
    public class SpringMvcSupport extends WebMvcConfigurationSupport {
        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
            registry.addResourceHandler("/css/**").addResourceLocations("/css/");
            registry.addResourceHandler("/js/**").addResourceLocations("/js/");
            registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
        }
    }
    
  • 在SpringMvcConfig中扫描SpringMvcSupport

    @Configuration
    @ComponentScan({"com.duan.controller","com.duan.config"})
    @EnableWebMvc
    public class SpringMvcConfig {
    }
    

4.2 列表功能

1630670317859

需求:页面加载完后发送异步请求到后台获取列表数据进行展示。

1.找到页面的钩子函数,created()

2.created()方法中调用了this.getAll()方法

3.在getAll()方法中使用axios发送异步请求从后台获取数据

4.访问的路径为http://localhost/books

5.返回数据

返回数据res.data的内容如下:

{
    "data": [
        {
            "id": 1,
            "type": "计算机理论",
            "name": "Spring实战 第五版",
            "description": "Spring入门经典教程,深入理解Spring原理技术内幕"
        },
        {
            "id": 2,
            "type": "计算机理论",
            "name": "Spring 5核心原理与30个类手写实践",
            "description": "十年沉淀之作,手写Spring精华思想"
        },...
    ],
    "code": 20041,
    "msg": ""
}

发送方式:

getAll() {
    //发送ajax请求
    axios.get("/books").then((res)=>{
        this.dataList = res.data.data;
    });
}

1630666787456

4.3 添加功能

1630670332168

需求:完成图片的新增功能模块

1.找到页面上的新建按钮,按钮上绑定了@click="handleCreate()"方法

2.在method中找到handleCreate方法,方法中打开新增面板

3.新增面板中找到确定按钮,按钮上绑定了@click="handleAdd()"方法

4.在method中找到handleAdd方法

5.在方法中发送请求和数据,响应成功后将新增面板关闭并重新查询数据

handleCreate打开新增面板

handleCreate() {
    this.dialogFormVisible = true;
},

handleAdd方法发送异步请求并携带数据

handleAdd () {
    //发送ajax请求
    //this.formData是表单中的数据,最后是一个json数据
    axios.post("/books",this.formData).then((res)=>{
        this.dialogFormVisible = false;
        this.getAll();
    });
}

4.4 添加功能状态处理

基础的新增功能已经完成,但是还有一些问题需要解决下:

需求:新增成功是关闭面板,重新查询数据,那么新增失败以后该如何处理?

1.在handlerAdd方法中根据后台返回的数据来进行不同的处理

2.如果后台返回的是成功,则提示成功信息,并关闭面板

3.如果后台返回的是失败,则提示错误信息

(1)修改前端页面

handleAdd () {
    //发送ajax请求
    axios.post("/books",this.formData).then((res)=>{
        //如果操作成功,关闭弹层,显示数据
        if(res.data.code == 20011){
            this.dialogFormVisible = false;
            this.$message.success("添加成功");
        }else if(res.data.code == 20010){
            this.$message.error("添加失败");
        }else{
            this.$message.error(res.data.msg);
        }
    }).finally(()=>{
        this.getAll();
    });
}

(2)后台返回操作结果,将Dao层的增删改方法返回值从void改成int

public interface BookDao {

//    @Insert("insert into tbl_book values(null,#{type},#{name},#{description})")
    @Insert("insert into tbl_book (type,name,description) values(#{type},#{name},#{description})")
    public int save(Book book);

    @Update("update tbl_book set type = #{type}, name = #{name}, description = #{description} where id = #{id}")
    public int update(Book book);

    @Delete("delete from tbl_book where id = #{id}")
    public int delete(Integer id);

    @Select("select * from tbl_book where id = #{id}")
    public Book getById(Integer id);

    @Select("select * from tbl_book")
    public List<Book> getAll();
}

(3)在BookServiceImpl中,增删改方法根据DAO的返回值来决定返回true/false

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    public boolean save(Book book) {
        return bookDao.save(book) > 0;
    }

    public boolean update(Book book) {
        return bookDao.update(book) > 0;
    }

    public boolean delete(Integer id) {
        return bookDao.delete(id) > 0;
    }

    public Book getById(Integer id) {
        if(id == 1){
            throw new BusinessException(Code.BUSINESS_ERR,"请不要使用你的技术挑战我的耐性!");
        }
//        //将可能出现的异常进行包装,转换成自定义异常
//        try{
//            int i = 1/0;
//        }catch (Exception e){
//            throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试!",e);
//        }
        return bookDao.getById(id);
    }

    public List<Book> getAll() {
        return bookDao.getAll();
    }
}

(4)测试错误情况,将图书类别长度设置超出范围即可

1630668954348

处理完新增后,会发现新增还存在一个问题,

新增成功后,再次点击新增按钮会发现之前的数据还存在,这个时候就需要在新增的时候将表单内容清空。

resetForm(){
	this.formData = {};
}
handleCreate() {
    this.dialogFormVisible = true;
    this.resetForm();
}

4.5 修改功能

1630670367812

需求:完成图书信息的修改功能

1.找到页面中的编辑按钮,该按钮绑定了@click="handleUpdate(scope.row)"

2.在method的handleUpdate方法中发送异步请求根据ID查询图书信息

3.根据后台返回的结果,判断是否查询成功

如果查询成功打开修改面板回显数据,如果失败提示错误信息

4.修改完成后找到修改面板的确定按钮,该按钮绑定了@click="handleEdit()"

5.在method的handleEdit方法中发送异步请求提交修改数据

6.根据后台返回的结果,判断是否修改成功

如果成功提示错误信息,关闭修改面板,重新查询数据,如果失败提示错误信息

scope.row代表的是当前行的行数据,也就是说,scope.row就是选中行对应的json数据,如下:

{
    "id": 1,
    "type": "计算机理论",
    "name": "Spring实战 第五版",
    "description": "Spring入门经典教程,深入理解Spring原理技术内幕"
}

修改handleUpdate方法

//弹出编辑窗口
handleUpdate(row) {
    // console.log(row);   //row.id 查询条件
    //查询数据,根据id查询
    axios.get("/books/"+row.id).then((res)=>{
        if(res.data.code == 20041){
            //展示弹层,加载数据
            this.formData = res.data.data;
            this.dialogFormVisible4Edit = true;
        }else{
            this.$message.error(res.data.msg);
        }
    });
}

修改handleEdit方法

handleEdit() {
    //发送ajax请求
    axios.put("/books",this.formData).then((res)=>{
        //如果操作成功,关闭弹层,显示数据
        if(res.data.code == 20031){
            this.dialogFormVisible4Edit = false;
            this.$message.success("修改成功");
        }else if(res.data.code == 20030){
            this.$message.error("修改失败");
        }else{
            this.$message.error(res.data.msg);
        }
    }).finally(()=>{
        this.getAll();
    });
}

至此修改功能就已经完成。

4.6 删除功能

1630673984385

需求:完成页面的删除功能。

1.找到页面的删除按钮,按钮上绑定了@click="handleDelete(scope.row)"

2.method的handleDelete方法弹出提示框

3.用户点击取消,提示操作已经被取消。

4.用户点击确定,发送异步请求并携带需要删除数据的主键ID

5.根据后台返回结果做不同的操作

如果返回成功,提示成功信息,并重新查询数据

如果返回失败,提示错误信息,并重新查询数据

修改handleDelete方法

handleDelete(row) {
    //1.弹出提示框
    this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
        type:'info'
    }).then(()=>{
        //2.做删除业务
        axios.delete("/books/"+row.id).then((res)=>{
            if(res.data.code == 20021){
                this.$message.success("删除成功");
            }else{
                this.$message.error("删除失败");
            }
        }).finally(()=>{
            this.getAll();
        });
    }).catch(()=>{
        //3.取消删除
        this.$message.info("取消删除操作");
    });
}

接下来,下面是一个完整页面

<!DOCTYPE html>

<html>

    <head>

        <!-- 页面meta -->

        <meta charset="utf-8">

        <meta http-equiv="X-UA-Compatible" content="IE=edge">

        <title>SpringMVC案例</title>

        <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">

        <!-- 引入样式 -->

        <link rel="stylesheet" href="../plugins/elementui/index.css">

        <link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">

        <link rel="stylesheet" href="../css/style.css">

    </head>

    <body class="hold-transition">

        <div id="app">

            <div class="content-header">

                <h1>图书管理</h1>

            </div>

            <div class="app-container">

                <div class="box">

                    <div class="filter-container">

                        <el-input placeholder="图书名称" v-model="pagination.queryString" style="width: 200px;" class="filter-item"></el-input>

                        <el-button @click="getAll()" class="dalfBut">查询</el-button>

                        <el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>

                    </div>

                    <el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>

                        <el-table-column type="index" align="center" label="序号"></el-table-column>

                        <el-table-column prop="type" label="图书类别" align="center"></el-table-column>

                        <el-table-column prop="name" label="图书名称" align="center"></el-table-column>

                        <el-table-column prop="description" label="描述" align="center"></el-table-column>

                        <el-table-column label="操作" align="center">

                            <template slot-scope="scope">

                                <el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>

                                <el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>

                            </template>

                        </el-table-column>

                    </el-table>

                    <!-- 新增标签弹层 -->

                    <div class="add-form">

                        <el-dialog title="新增图书" :visible.sync="dialogFormVisible">

                            <el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right" label-width="100px">

                                <el-row>

                                    <el-col :span="12">

                                        <el-form-item label="图书类别" prop="type">

                                            <el-input v-model="formData.type"/>

                                        </el-form-item>

                                    </el-col>

                                    <el-col :span="12">

                                        <el-form-item label="图书名称" prop="name">

                                            <el-input v-model="formData.name"/>

                                        </el-form-item>

                                    </el-col>

                                </el-row>


                                <el-row>

                                    <el-col :span="24">

                                        <el-form-item label="描述">

                                            <el-input v-model="formData.description" type="textarea"></el-input>

                                        </el-form-item>

                                    </el-col>

                                </el-row>

                            </el-form>

                            <div slot="footer" class="dialog-footer">

                                <el-button @click="dialogFormVisible = false">取消</el-button>

                                <el-button type="primary" @click="handleAdd()">确定</el-button>

                            </div>

                        </el-dialog>

                    </div>

                    <!-- 编辑标签弹层 -->

                    <div class="add-form">

                        <el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit">

                            <el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px">

                                <el-row>

                                    <el-col :span="12">

                                        <el-form-item label="图书类别" prop="type">

                                            <el-input v-model="formData.type"/>

                                        </el-form-item>

                                    </el-col>

                                    <el-col :span="12">

                                        <el-form-item label="图书名称" prop="name">

                                            <el-input v-model="formData.name"/>

                                        </el-form-item>

                                    </el-col>

                                </el-row>

                                <el-row>

                                    <el-col :span="24">

                                        <el-form-item label="描述">

                                            <el-input v-model="formData.description" type="textarea"></el-input>

                                        </el-form-item>

                                    </el-col>

                                </el-row>

                            </el-form>

                            <div slot="footer" class="dialog-footer">

                                <el-button @click="dialogFormVisible4Edit = false">取消</el-button>

                                <el-button type="primary" @click="handleEdit()">确定</el-button>

                            </div>

                        </el-dialog>

                    </div>

                </div>

            </div>

        </div>

    </body>

    <!-- 引入组件库 -->

    <script src="../js/vue.js"></script>

    <script src="../plugins/elementui/index.js"></script>

    <script type="text/javascript" src="../js/jquery.min.js"></script>

    <script src="../js/axios-0.18.0.js"></script>

    <script>
        var vue = new Vue({

            el: '#app',
            data:{
                pagination: {},
				dataList: [],//当前页要展示的列表数据
                formData: {},//表单数据
                dialogFormVisible: false,//控制表单是否可见
                dialogFormVisible4Edit:false,//编辑表单是否可见
                rules: {//校验规则
                    type: [{ required: true, message: '图书类别为必填项', trigger: 'blur' }],
                    name: [{ required: true, message: '图书名称为必填项', trigger: 'blur' }]
                }
            },

            //钩子函数,VUE对象初始化完成后自动执行
            created() {
                this.getAll();
            },

            methods: {
                //列表
                getAll() {
                    //发送ajax请求
                    axios.get("/books").then((res)=>{
                        this.dataList = res.data.data;
                    });
                },

                //弹出添加窗口
                handleCreate() {
                    this.dialogFormVisible = true;
                    this.resetForm();
                },

                //重置表单
                resetForm() {
                    this.formData = {};
                },

                //添加
                handleAdd () {
                    //发送ajax请求
                    axios.post("/books",this.formData).then((res)=>{
                        console.log(res.data);
                        //如果操作成功,关闭弹层,显示数据
                        if(res.data.code == 20011){
                            this.dialogFormVisible = false;
                            this.$message.success("添加成功");
                        }else if(res.data.code == 20010){
                            this.$message.error("添加失败");
                        }else{
                            this.$message.error(res.data.msg);
                        }
                    }).finally(()=>{
                        this.getAll();
                    });
                },

                //弹出编辑窗口
                handleUpdate(row) {
                    // console.log(row);   //row.id 查询条件
                    //查询数据,根据id查询
                    axios.get("/books/"+row.id).then((res)=>{
                        // console.log(res.data.data);
                        if(res.data.code == 20041){
                            //展示弹层,加载数据
                            this.formData = res.data.data;
                            this.dialogFormVisible4Edit = true;
                        }else{
                            this.$message.error(res.data.msg);
                        }
                    });
                },

                //编辑
                handleEdit() {
                    //发送ajax请求
                    axios.put("/books",this.formData).then((res)=>{
                        //如果操作成功,关闭弹层,显示数据
                        if(res.data.code == 20031){
                            this.dialogFormVisible4Edit = false;
                            this.$message.success("修改成功");
                        }else if(res.data.code == 20030){
                            this.$message.error("修改失败");
                        }else{
                            this.$message.error(res.data.msg);
                        }
                    }).finally(()=>{
                        this.getAll();
                    });
                },

                // 删除
                handleDelete(row) {
                    //1.弹出提示框
                    this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
                        type:'info'
                    }).then(()=>{
                        //2.做删除业务
                        axios.delete("/books/"+row.id).then((res)=>{
                            if(res.data.code == 20021){
                                this.$message.success("删除成功");
                            }else{
                                this.$message.error("删除失败");
                            }
                        }).finally(()=>{
                            this.getAll();
                        });
                    }).catch(()=>{
                        //3.取消删除
                        this.$message.info("取消删除操作");
                    });
                }
            }
        })

    </script>

</html>

5,拦截器

5.1 拦截器概念

1630676280170

(1)浏览器发送一个请求会先到Tomcat的web服务器

(2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源

(3)如果是静态资源,会直接到Tomcat的项目部署目录下去直接访问

(4)如果是动态资源,就需要交给项目的后台代码进行处理

(5)在找到具体的方法之前,可以去配置过滤器(可以配置多个),按照顺序进行执行

(6)然后进入到到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截

(7)如果满足规则,则进行处理,找到其对应的controller类中的方法进行执行,完成后返回结果

(8)如果不满足规则,则不进行处理

(9)这个时候,如果需要在每个Controller方法执行的前后添加业务,具体该如何来实现?

这个就是拦截器要做的事。

  • 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行

  • 作用:

    • 在指定的方法调用前后执行预先设定的代码
    • 阻止原始方法的执行
    • 总结:拦截器就是用来做增强
  • 拦截器和过滤器在作用和执行顺序上也很相似

所以这个时候,就有一个问题需要思考:拦截器和过滤器之间的区别是什么?

  • 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
  • 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强

1630676903190

5.2 拦截器入门案例

5.2.1 环境准备

  • 创建一个Web的Maven项目

  • pom.xml添加SSM整合所需jar包

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.duan</groupId>
      <artifactId>springmvc_12_interceptor</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
    
      <dependencies>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.9.0</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.1</version>
            <configuration>
              <port>80</port>
              <path>/</path>
            </configuration>
          </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
      </build>
    </project>
    
    
  • 创建对应的配置类

    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
        protected Class<?>[] getRootConfigClasses() {
            return new Class[0];
        }
    
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
    
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
    
        //乱码处理
        @Override
        protected Filter[] getServletFilters() {
            CharacterEncodingFilter filter = new CharacterEncodingFilter();
            filter.setEncoding("UTF-8");
            return new Filter[]{filter};
        }
    }
    --------------------------------------------------------------------------
    @Configuration
    @ComponentScan({"com.duan.controller"})
    @EnableWebMvc
    public class SpringMvcConfig{
       
    }
    
  • 创建模型类Book

    public class Book {
        private String name;
        private double price;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Book{" +
                    "书名='" + name + '\'' +
                    ", 价格=" + price +
                    '}';
        }
    }
    
  • 编写Controller

    @RestController
    @RequestMapping("/books")
    public class BookController {
    
        @PostMapping
        public String save(@RequestBody Book book){
            System.out.println("book save..." + book);
            return "{'module':'book save'}";
        }
    
        @DeleteMapping("/{id}")
        public String delete(@PathVariable Integer id){
            System.out.println("book delete..." + id);
            return "{'module':'book delete'}";
        }
    
        @PutMapping
        public String update(@RequestBody Book book){
            System.out.println("book update..."+book);
            return "{'module':'book update'}";
        }
    
        @GetMapping("/{id}")
        public String getById(@PathVariable Integer id){
            System.out.println("book getById..."+id);
            return "{'module':'book getById'}";
        }
    
        @GetMapping
        public String getAll(){
            System.out.println("book getAll...");
            return "{'module':'book getAll'}";
        }
    }
    

5.2.2 拦截器开发

步骤1:创建拦截器类

让类实现HandlerInterceptor接口,重写接口中的三个方法。

@Component
//定义拦截器类,实现HandlerInterceptor接口
//注意当前类必须受Spring容器控制
public class ProjectInterceptor implements HandlerInterceptor {
    @Override
    //原始方法调用前执行的内容
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle...");
        return true;
    }

    @Override
    //原始方法调用后执行的内容
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...");
    }

    @Override
    //原始方法调用完成后执行的内容
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion...");
    }
}

注意:拦截器类要被SpringMVC容器扫描到。

步骤2:配置拦截器类
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    @Autowired
    private ProjectInterceptor projectInterceptor;

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
    }

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //配置拦截器
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books" );
    }
}
步骤3:SpringMVC添加SpringMvcSupport包扫描
@Configuration
@ComponentScan({"com.duan.controller","com.duan.config"})
@EnableWebMvc
public class SpringMvcConfig{
   
}
步骤4:运行程序测试

使用apipost发送http://localhost/books

如果发送http://localhost/books/100会发现拦截器没有被执行,原因是拦截器的addPathPatterns方法配置的拦截路径是/books,我们现在发送的是/books/100,所以没有匹配上,因此没有拦截,拦截器就不会执行。

步骤5:修改拦截器拦截规则
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    @Autowired
    private ProjectInterceptor projectInterceptor;

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
    }

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //配置拦截器
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*" );
    }
}

这个时候,如果再次访问http://localhost/books/100,拦截器就会被执行。

最后说一件事,就是拦截器中的preHandler方法,如果返回true,则代表放行,会执行原始Controller类中要请求的方法,如果返回false,则代表拦截,后面的就不会再执行了。

步骤6:简化SpringMvcSupport的编写
@Configuration
@ComponentScan({"com.duan.controller"})
@EnableWebMvc
//实现WebMvcConfigurer接口可以简化开发,但具有一定的侵入性
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private ProjectInterceptor projectInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置多拦截器
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*");
    }
}

此后咱们就不用再写SpringMvcSupport类了。

最后拦截器的执行流程:

1630679464294

当有拦截器后,请求会先进入preHandle方法,

如果方法返回true,则放行继续执行后面的handle[controller的方法]和后面的方法

如果返回false,则直接跳过后面方法的执行。

5.3 拦截器参数

5.3.1 前置处理方法

原始方法之前运行preHandle

public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler) throws Exception {
    System.out.println("preHandle");
    return true;
}
  • request:请求对象
  • response:响应对象
  • handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装

使用request对象可以获取请求数据中的内容,如获取请求头的Content-Type

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String contentType = request.getHeader("Content-Type");
    System.out.println("preHandle..."+contentType);
    return true;
}

使用handler参数,可以获取方法的相关信息

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HandlerMethod hm = (HandlerMethod)handler;
    String methodName = hm.getMethod().getName();//可以获取方法的名称
    System.out.println("preHandle..."+methodName);
    return true;
}

5.3.2 后置处理方法

原始方法运行后运行,如果原始方法被拦截,则不执行

public void postHandle(HttpServletRequest request,
                       HttpServletResponse response,
                       Object handler,
                       ModelAndView modelAndView) throws Exception {
    System.out.println("postHandle");
}

前三个参数和上面的是一致的。

modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整

因为咱们现在都是返回json数据,所以该参数的使用率不高。

5.3.3 完成处理方法

拦截器最后执行的方法,无论原始方法是否执行

public void afterCompletion(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            Exception ex) throws Exception {
    System.out.println("afterCompletion");
}

前三个参数与上面的是一致的。

ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理

因为现在已经有全局异常处理器类,所以该参数的使用率也不高。

这三个方法中,最常用的是preHandle,在这个方法中可以通过返回值来决定是否要进行放行,可以把业务逻辑放在该方法中,如果满足业务则返回true放行,不满足则返回false拦截。

5.4 拦截器链配置

5.4.1 配置多个拦截器

步骤1:创建拦截器类

实现接口,并重写接口中的方法

@Component
public class ProjectInterceptor2 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle...222");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...222");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion...222");
    }
}
步骤2:配置拦截器类
@Configuration
@ComponentScan({"com.duan.controller"})
@EnableWebMvc
//实现WebMvcConfigurer接口可以简化开发,但具有一定的侵入性
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private ProjectInterceptor projectInterceptor;
    @Autowired
    private ProjectInterceptor2 projectInterceptor2;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置多拦截器
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*");
        registry.addInterceptor(projectInterceptor2).addPathPatterns("/books","/books/*");
    }
}

步骤3:运行程序,观察顺序

1630680435269

拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。

  • 当配置多个拦截器时,形成拦截器链
  • 拦截器链的运行顺序参照拦截器添加顺序为准
  • 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
  • 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作

1630680579735

preHandle:与配置顺序相同,必定运行

postHandle:与配置顺序相反,可能不运行

afterCompletion:与配置顺序相反,可能不运行。

这个顺序不太好记,最终只需要把握住一个原则即可:以最终的运行结果为准

posted @ 2021-02-21 11:09  duanthirty  阅读(51)  评论(0)    收藏  举报