# SSM实战 **Repository Path**: zhuang-kang/ssm-practice ## Basic Information - **Project Name**: SSM实战 - **Description**: 用来记录学习实战SSM的代码,多思考,多学习 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-09-26 - **Last Updated**: 2023-04-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一、MyBatis ## 1、MyBatis简介 ### 1.1、MyBatis历史 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁 移到了Google Code。随着开发团队转投Google Code旗下, iBatis3.x正式更名为MyBatis。代码于 2013年11月迁移到Github。 iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。 iBatis提供的持久层框架 包括SQL Maps和Data Access Objects(DAO)。 ### 1.2、MyBatis特性 1) MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架 2) MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集 3) MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录 4) MyBatis 是一个 半自动的ORM(Object Relation Mapping)框架 ### 1.3、MyBatis下载 MyBatis下载地址:https://github.com/mybatis/mybatis-3 ![在这里插入图片描述](https://img-blog.csdnimg.cn/3b90ad82da5649648969e1b0144d4973.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/d03ecee337ed427ba2b016a461930490.png) ### 1.4、和其它持久化层技术对比 - JDBC - SQL 夹杂在Java代码中耦合度高,导致硬编码内伤 - 维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见 - 代码冗长,开发效率低 - Hibernate 和 JPA - 操作简便,开发效率高 - 程序中的长难复杂 SQL 需要绕过框架 - 内部自动生产的 SQL,不容易做特殊优化 - 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。 - 反射操作太多,导致数据库性能下降 - MyBatis - 轻量级,性能出色 - SQL 和 Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据 - 开发效率稍逊于HIbernate,但是完全能够接受 # 2、搭建MyBatis ## 2.1、开发环境 IDE:idea 2021.2 构建工具:maven 3.5.4 MySQL版本:MySQL 5.7 MyBatis版本:MyBatis 3.5.7 > MySQL不同版本的注意事项 > 1、驱动类driver-class-name > MySQL 5版本使用jdbc5驱动,驱动类使用:com.mysql.jdbc.Driver > MySQL 8版本使用jdbc8驱动,驱动类使用:com.mysql.cj.jdbc.Driver > 2、连接地址url > MySQL 5版本的url: > jdbc:mysql://localhost:3306/ssm > MySQL 8版本的url: > jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC > 否则运行测试用例报告如下错误: > java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or > represents more ## 2.2、创建maven工程 ①打包方式:jar ②引入依赖 ```xml org.mybatis mybatis 3.5.7 junit junit 4.12 test mysql mysql-connector-java 8.0.16 org.projectlombok lombok 1.18.16 log4j log4j 1.2.17 ``` **sql语句** ```sql CREATE TABLE ssm.t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(20) DEFAULT NULL, password varchar(20) DEFAULT NULL, age int(11) DEFAULT NULL, gender varchar(255) DEFAULT NULL, email varchar(50) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE = INNODB, AUTO_INCREMENT = 4, AVG_ROW_LENGTH = 8192, CHARACTER SET utf8, COLLATE utf8_general_ci; ``` ## 2.3、创建MyBatis的核心配置文件 > 习惯上命名为mybatis-config.xml,这个文件名仅仅只是建议,并非强制要求。将来整合Spring > 之后,这个配置文件可以省略,所以大家操作时可以直接复制、粘贴。 > 核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息 > 核心配置文件存放的位置是src/main/resources目录下 ```xml ``` ## 2.4、创建mapper接口 MyBatis中的mapper接口相当于以前的dao。但是区别在于,mapper仅仅是接口,我们不需要提供实现类 ```java package com.zhuang.mybatis.mapper; import com.zhuang.mybatis.pojo.User; import java.util.List; /** * description: UserMapper * date: 2022/9/26 22:28 * author: Zhuang * version: 1.0 */ public interface UserMapper { /** * 添加用户信息 */ int insertUser(); /** * 删除用户信息 */ int deleteUser(); /** * 更新用户信息 */ int updateUser(); /** * 查询一个实体类对象 */ User getUserById(); /** * 查询list集合 * @return List */ List getUserList(); } ``` ## 2.5、创建MyBatis的映射文件 相关概念:ORM(Object Relationship Mapping)对象关系映射。 - 对象:Java的实体类对象 - 关系:关系型数据库 - 映射:二者之间的对应关系 | Java概念 | 数据库概念 | | -------- | ---------- | | 类 | 表 | | 属性 | 字段/列 | | 对象 | 记录/行 | > 1、映射文件的命名规则: > 表所对应的实体类的类名+Mapper.xml > 例如:表t_user,映射的实体类为User,所对应的映射文件为UserMapper.xml > 因此一个映射文件对应一个实体类,对应一张表的操作 > MyBatis映射文件用于编写SQL,访问以及操作表中的数据 > MyBatis映射文件存放的位置是src/main/resources/mappers目录下 > 2、 MyBatis中可以面向接口操作数据,要保证两个一致: > a>mapper接口的全类名和映射文件的命名空间(namespace)保持一致 > b>mapper接口中方法的方法名和映射文件中编写SQL的标签的id属性保持一致 ```xml insert into t_user values (null, 'jack', '123321666', 21, '男', '12zkk5@qq.com') delete from t_user where id = 3 update t_user set username='zk', password='123' where id = 2 ``` ## 2.6、通过junit测试功能 ```java //读取MyBatis的核心配置文件 InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); //创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); //通过核心配置文件所对应的字节输入流创建工厂类SqlSessionFactory,生产SqlSession对象 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); //创建SqlSession对象,此时通过SqlSession对象所操作的sql都必须手动提交或回滚事务 //SqlSession sqlSession = sqlSessionFactory.openSession(); //创建SqlSession对象,此时通过SqlSession对象所操作的sql都会自动提交 SqlSession sqlSession = sqlSessionFactory.openSession(true); //通过代理模式创建UserMapper接口的代理实现类对象 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //调用UserMapper接口中的方法,就可以根据UserMapper的全类名匹配元素文件,通过调用的方法名匹配 映射文件中的SQL标签,并执行标签中的SQL语句 int result = userMapper.insertUser(); //sqlSession.commit(); System.out.println("结果:"+result); ``` > SqlSession:代表Java程序和数据库之间的会话。(HttpSession是Java程序和浏览器之间的会话) > SqlSessionFactory:是“生产”SqlSession的“工厂”。 > 工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象。 ## 2.7、加入log4j日志功能 log4j的配置文件名为log4j.xml,存放的位置是src/main/resources目录下 ```xml ``` > 日志的级别 > FATAL(致命)>ERROR(错误)>WARN(警告)>INFO(信息)>DEBUG(调试) > 从左到右打印的内容越来越详细 # 3、核心配置文件详解 > 核心配置文件中的标签必须按照固定的顺序: > properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,refl > ectorFactory?,plugins?,environments?,databaseIdProvider?,mappers? ```xml ``` # 4、MyBatis的增删改查 ## 4.1、新增 ```xml insert into t_user values(null,'admin','123456',23,'男') ``` ## 4.2、删除 ```xml delete from t_user where id = 7 ``` ## 4.3、修改 ```xml update t_user set username='zk',password='123' where id = 6 ``` ## 4.4、查询一个实体类对象 ```xml ``` ## 4.5、查询list集合 ```xml ``` > 注意: > 1、查询的标签select必须设置属性resultType或resultMap,用于设置实体类和数据库表的映射 > 关系 > resultType:自动映射,用于属性名和表中字段名一致的情况 > resultMap:自定义映射,用于一对多或多对一或字段名和属性名不一致的情况 # 5、MyBatis获取参数值的两种方式 > **MyBatis获取参数值的两种方式:${}和#{}** > > ${}的本质就是字符串拼接,#{}的本质就是占位符赋值 > ${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号; > > 但是#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段进行赋值时,可以自动添加单引号 ## 5.1、单个字面量类型的参数 > 若mapper接口中的方法参数为单个的字面量类型 > 此时可以使用${}和#{}以任意的名称获取参数的值,注意${}需要手动加单引号 ## 5.2、多个字面量类型的参数 > 若mapper接口中的方法参数为多个时 > 此时MyBatis会自动将这些参数放在一个map集合中,以arg0,arg1...为键,以参数为值;以 > param1,param2...为键,以参数为值;因此只需要通过${}和#{}访问map集合的键就可以获取相 > 对应的值,注意${}需要手动加单引号 ## 5.3、map集合类型的参数 > 若mapper接口中的方法需要的参数为多个时,此时可以手动创建map集合,将这些数据放在 > map中只需要通过${}和#{}访问map集合的键就可以获取相对应的值,注意${}需要手动加单引号 ## 5.4、实体类类型的参数 > 若mapper接口中的方法参数为实体类对象时 > 此时可以使用${}和#{},通过访问实体类对象中的属性名获取属性值,注意${}需要手动加单引号 ## 5.5、使用@Param标识参数 > 可以通过@Param注解标识mapper接口中的方法参数 > 此时,会将这些参数放在map集合中,以@Param注解的value属性值为键,以参数为值;以 > param1,param2...为键,以参数为值;只需要通过${}和#{}访问map集合的键就可以获取相对应 > 的值,注意${}需要手动加单引号 # 6、MyBatis的各种查询功能 ## 6.1、查询一个实体类对象 ```java /** * 根据用户id查询用户信息 * @param id * @return */ User getUserById(@Param("id") int id); ``` ```xml ``` ## 6.2、查询一个list集合 ```java /** * 查询所有用户信息 * @return */ List getUserList(); ``` ```xml ``` > 当查询的数据为多条时,不能使用实体类作为返回值,否则会抛出异常 > TooManyResultsException;但是若查询的数据只有一条,可以使用实体类或集合作为返回值 ## 6.3、查询单个数据 ```java /** * 查询用户的总记录数 * @return * 在MyBatis中,对于Java中常用的类型都设置了类型别名 * 例如: java.lang.Integer-->int|integer * 例如: int-->_int|_integer * 例如: Map-->map,List-->list */ int getCount(); ``` ```xml ``` ## 6.4、查询一条数据为map集合 ```java /** * 根据用户id查询用户信息为map集合 * @param id * @return */ Map getUserToMap(@Param("id") int id); ``` ```xml ``` ## 6.5、查询多条数据为map集合 ### ①方式一 ```java /** * 查询所有用户信息为map集合 * @return * 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,此 时可以将这些map放在一个list集合中获取 */ List> getAllUserToMap(); ``` ```xml ``` ### ②方式二 ```java /** * 查询所有用户信息为map集合 * @return * 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,并 且最终要以一个map的方式返回数据,此时需要通过@MapKey注解设置map集合的键,值是每条数据所对应的 map集合 */ @MapKey("id") Map getAllUserToMap(); ``` ```xml ``` # 7、特殊SQL的执行 ## 7.1、模糊查询 ```java /** * 测试模糊查询 * @param mohu * @return */ List testMohu(@Param("mohu") String mohu); ``` ```xml ``` ## 7.2、批量删除 ```java /** * 批量删除 * @param ids * @return */ int deleteMore(@Param("ids") String ids); ``` ```xml delete from t_user where id in (${ids}) ``` ## 7.3、动态设置表名 ```java /** * 动态设置表名,查询所有的用户信息 * @param tableName * @return */ List getAllUser(@Param("tableName") String tableName); ``` ```xml ``` ## 7.4、添加功能获取自增的主键 > 场景模拟: > t_clazz(clazz_id,clazz_name) > t_student(student_id,student_name,clazz_id) > 1、添加班级信息 > 2、获取新添加的班级的id > 3、为班级分配学生,即将某学的班级id修改为新添加的班级的id ```java /** * 添加用户信息 * @param user * @return * useGeneratedKeys:设置使用自增的主键 * keyProperty:因为增删改有统一的返回值是受影响的行数,因此只能将获取的自增的主键放在传输的参 数user对象的某个属性中 */ int insertUser(User user); ``` ```java insert into t_user values(null,#{username},#{password},#{age},#{sex}) ``` # 8、自定义映射resultMap ## 8.1、resultMap处理字段和属性的映射关系 > 若字段名和实体类中的属性名不一致,则可以通过resultMap设置自定义映射 ```xml ``` > 若字段名和实体类中的属性名不一致,但是字段名符合数据库的规则(使用_),实体类中的属性 > 名符合Java的规则(使用驼峰) > 此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系 > a>可以通过为字段起别名的方式,保证和实体类中的属性名保持一致 > b>可以在MyBatis的核心配置文件中设置一个全局配置信息mapUnderscoreToCamelCase,可 > 以在查询表中数据时,自动将_类型的字段名转换为驼峰 > 例如:字段名user_name,设置了mapUnderscoreToCamelCase,此时字段名就会转换为 > userName ## 8.2、多对一映射处理 > 场景模拟: > 查询员工信息以及员工所对应的部门信息 ### 8.2.1、级联方式处理映射关系 ```xml ``` ### 8.2.2、使用association处理映射关系 ```xml ``` ### 8.2.3、分步查询 #### ①查询员工信息 ```java /** * 通过分步查询查询员工信息 * @param eid * @return */ Emp getEmpByStep(@Param("eid") int eid); ``` ```xml ``` #### ②根据员工所对应的部门id查询部门信息 ```java /** * 分步查询的第二步: 根据员工所对应的did查询部门信息 * @param did * @return */ Dept getEmpDeptByStep(@Param("did") int did); ``` ```xml ``` ## 8.3、一对多映射处理 ### 8.3.1、collection ```java /** * 根据部门id查新部门以及部门中的员工信息 * @param did * @return */ Dept getDeptEmpByDid(@Param("did") int did); ``` ```java ``` ### 8.3.2、分步查询 #### ①查询部门信息 ```java /** * 分步查询部门和部门中的员工 * @param did * @return */ Dept getDeptByStep(@Param("did") int did); ``` ```xml ``` #### ②根据部门id查询部门中的所有员工 ```java /** * 根据部门id查询员工信息 * @param did * @return */ List getEmpListByDid(@Param("did") int did); ``` ```xml ``` > 分步查询的优点:可以实现延迟加载 > > 但是必须在核心配置文件中设置全局配置信息: > lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载 > aggressiveLazyLoading:当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载 > 此时就可以实现按需加载,获取的数据是什么,就只会执行相应的sql。此时可通过association和 > collection中的fetchType属性设置当前的分步查询是否使用延迟加载, fetchType="lazy(延迟加 > 载)|eager(立即加载)" # 9、动态SQL Mybatis框架的动态SQL技术是一种根据特定条件动态拼装SQL语句的功能,它存在的意义是为了解决拼接SQL语句字符串时的痛点问题。 ## 9.1、if > if标签可通过test属性的表达式进行判断,若表达式的结果为true,则标签中的内容会执行;反之标签中的内容不会执行 ```xml ``` ## 9.2、where > where和if一般结合使用: > a>若where标签中的if条件都不满足,则where标签没有任何功能,即不会添加where关键字 > b>若where标签中的if条件满足,则where标签会自动添加where关键字,并将条件最前方多余的and去掉 > 注意:where标签不能去掉条件最后多余的and ```xml ``` ## 9.3、trim > trim用于去掉或添加标签中的内容 > 常用属性: > prefix:在trim标签中的内容的前面添加某些内容 > prefixOverrides:在trim标签中的内容的前面去掉某些内容 > suffix:在trim标签中的内容的后面添加某些内容 > suffixOverrides:在trim标签中的内容的后面去掉某些内容 ```java ``` ## 9.4、choose、when、otherwise > choose、when、 otherwise相当于if...else if..else ```xml ``` ## 9.5、foreach ```xml insert into t_emp values (null,#{emp.ename},#{emp.age},#{emp.sex},#{emp.email},null) delete from t_emp where eid = #{eid} delete from t_emp where eid in #{eid} ``` ## 9.6、SQL片段 > sql片段,可以记录一段公共sql片段,在使用的地方通过include标签进行引入 ```java eid,ename,age,sex,did select from t_emp ``` # 10、MyBatis的缓存 ## 10.1、MyBatis的一级缓存 一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就 会从缓存中直接获取,不会从数据库重新访问 使一级缓存失效的四种情况: 1) 不同的SqlSession对应不同的一级缓存 2) 同一个SqlSession但是查询条件不同 3) 同一个SqlSession两次查询期间执行了任何一次增删改操作 4) 同一个SqlSession两次查询期间手动清空了缓存 ## 10.2、MyBatis的二级缓存 二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被 缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取 二级缓存开启的条件: a>在核心配置文件中,设置全局配置属性cacheEnabled="true",默认为true,不需要设置 b>在映射文件中设置标签 c>二级缓存必须在SqlSession关闭或提交之后有效 d>查询的数据所转换的实体类类型必须实现序列化的接口 使二级缓存失效的情况: 两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效 ## 10.3、二级缓存的相关配置 在mapper配置文件中添加的cache标签可以设置一些属性: ①eviction属性:缓存回收策略,默认的是 LRU。 LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。 FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。 SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。 WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。 ②flushInterval属性:刷新间隔,单位毫秒 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新 ③size属性:引用数目,正整数 代表缓存最多可以存储多少个对象,太大容易导致内存溢出 ④readOnly属性:只读, true/false true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了 很重 要的性能优势。 false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。 ## 10.4、MyBatis缓存查询的顺序 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。 如果二级缓存没有命中,再查询一级缓存 如果一级缓存也没有命中,则查询数据库 SqlSession关闭之后,一级缓存中的数据会写入二级缓存 ## 10.5、整合第三方缓存EHCache ### 10.5.1、添加依赖 ```xml org.mybatis.caches mybatis-ehcache 1.2.1 ch.qos.logback logback-classic 1.2.3 ``` ### 10.5.2、各jar包功能 | jar包名称 | 作用 | | --------------- | ------------------------------- | | mybatis-ehcache | Mybatis和EHCache的整合包 | | ehcache | EHCache核心包 | | slf4j-api | SLF4J日志门面包 | | logback-classic | 支持SLF4J门面接口的一个具体实现 | ### 10.5.3、创建EHCache的配置文件ehcache.xml ```xml ``` ### 10.5.4、设置二级缓存的类型 ```xml ``` ### 10.5.5、加入logback日志 > 存在SLF4J时,作为简易日志的log4j将失效,此时我们需要借助SLF4J的具体实现logback来打印日 > 志。 创建logback的配置文件logback.xml ```java [%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n ``` ### 10.5.6、EHCache配置文件说明 ![在这里插入图片描述](https://img-blog.csdnimg.cn/78bf33fe59074328950679244da416f9.png) # 11、MyBatis的逆向工程 > 正向工程:先创建Java实体类,由框架负责根据实体类生成数据库表。 Hibernate是支持正向工 > 程的。 > 逆向工程:先创建数据库表,由框架负责根据数据库表,反向生成如下资源: > > - Java实体类 > - Mapper接口 > - Mapper映射文件 ## 11.1、创建逆向工程的步骤 ### ①添加依赖和插件 ```xml org.mybatis mybatis 3.5.7 junit junit 4.12 test log4j log4j 1.2.17 mysql mysql-connector-java 8.0.16 org.projectlombok lombok 1.18.16 com.github.pagehelper pagehelper 5.2.0 org.mybatis.generator mybatis-generator-maven-plugin 1.3.0 org.mybatis.generator mybatis-generator-core 1.3.2 mysql mysql-connector-java 8.0.16 ``` ### ②创建MyBatis的核心配置文件 ### ③创建逆向工程的配置文件 > 文件名必须是:generatorConfig.xml ```xml
``` ### ④执行MBG插件的generate目标 ![在这里插入图片描述](https://img-blog.csdnimg.cn/a48ff95f0068493fb4c743877a98fb05.png) ### ⑤效果 ![在这里插入图片描述](https://img-blog.csdnimg.cn/ae02265e32b44ee4bf59ff3bf87cb0e3.png) ## 11.2、QBC查询 ```java public class MGBTest { @Test public void testMBG() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); Page page = PageHelper.startPage(1, 4); List emps = mapper.selectByExample(null); emps.forEach(System.out::println); System.out.println("page = " + page); PageInfo pageInfo = new PageInfo<>(emps, 5); System.out.println("pageInfo = " + pageInfo); } } ``` # 12、分页插件 limit index,pageSize pageSize:每页显示的条数 pageNum:当前页的页码 index:当前页的起始索引,index=(pageNum-1)*pageSize count:总记录数 totalPage:总页数 totalPage = count / pageSize; if(count % pageSize != 0){ totalPage += 1; } pageSize=4,pageNum=1,index=0 limit 0,4 pageSize=4,pageNum=3,index=8 limit 8,4 pageSize=4,pageNum=6,index=20 limit 8,4 首页 上一页 2 3 4 5 6 下一页 末页 ## 12.1、分页插件的使用步骤 ### ①添加依赖 ```xml com.github.pagehelper pagehelper 5.2.0 ``` ### ②配置分页插件 在MyBatis的核心配置文件中配置插件 ```xml ``` ## 12.2、分页插件的使用 a>在查询功能之前使用PageHelper.startPage(int pageNum, int pageSize)开启分页功能 > pageNum:当前页的页码 > pageSize:每页显示的条数 b>在查询获取list集合之后,使用PageInfo pageInfo = new PageInfo<>(List list, int navigatePages)获取分页相关数据 > list:分页之后的数据 > navigatePages:导航分页的页码数 c>分页相关数据 > PageInfo{ > pageNum=8, pageSize=4, size=2, startRow=29, endRow=30, total=30, pages=8, > list=Page{count=true, pageNum=8, pageSize=4, startRow=28, endRow=32, total=30, > pages=8, reasonable=false, pageSizeZero=false}, > prePage=7, nextPage=0, isFirstPage=false, isLastPage=true, hasPreviousPage=true, > hasNextPage=false, navigatePages=5, navigateFirstPage4, navigateLastPage8, > navigatepageNums=[4, 5, 6, 7, 8] > } > pageNum:当前页的页码 > pageSize:每页显示的条数 > size:当前页显示的真实条数 > total:总记录数 > pages:总页数 > prePage:上一页的页码 > nextPage:下一页的页码 > isFirstPage/isLastPage:是否为第一页/最后一页 > hasPreviousPage/hasNextPage:是否存在上一页/下一页 > navigatePages:导航分页的页码数 > navigatepageNums:导航分页的页码,[1,2,3,4,5] # 二、Spring # 1、Spring简介 ## 1.1、Spring概述 官网地址:https://spring.io/ > Spring 是最受欢迎的企业级 Java 应用程序开发框架,数以百万的来自世界各地的开发人员使用 > Spring 框架来创建性能好、易于测试、可重用的代码。 > Spring 框架是一个开源的 Java 平台,它最初是由 Rod Johnson 编写的,并且于 2003 年 6 月首 > 次在 Apache 2.0 许可下发布。 > Spring 是轻量级的框架,其基础版本只有 2 MB 左右的大小。 > Spring 框架的核心特性是可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应 > 用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO > 编程模型来促进良好的编程实践。 ## 1.2、Spring家族 项目列表:https://spring.io/projects ## 1.3、Spring Framework Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都是以 Spring Framework 为基础的。 ### 1.3.1、Spring Framework特性 - 非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常 小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会 破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序 时结构清晰、简洁优雅。 - 控制反转:IOC——Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源 变成环境将资源准备好,我们享受资源注入。 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功 能。 - 容器:Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化 的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发 效率。 - 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭 建超大型复杂应用系统。 - 声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且 Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基 础上全部使用 Spring 来实现。 ### 1.3.2、Spring Framework五大功能模块 | 功能模块 | 功能介绍 | | ----------------------- | ----------------------------------------------------------- | | Core Container | 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 | | AOP&Aspects | 面向切面编程 | | Testing | 提供了对 junit 或 TestNG 测试框架的整合 | | Data Access/Integration | 提供了对数据访问/集成的功能。 | | Spring MVC | 提供了面向Web应用程序的集成功能。 | # 2、IOC ## 2.1、IOC容器 ### 2.1.1、IOC思想 IOC:Inversion of Control,翻译过来是**反转控制。** #### ①获取资源的传统方式 自己做饭:买菜、洗菜、择菜、改刀、炒菜,全过程参与,费时费力,必须清楚了解资源创建整个过程中的全部细节且熟练掌握。 在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。 #### ②反转控制方式获取资源 点外卖:下单、等、吃,省时省力,不必关心资源创建过程的所有细节。 反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。 #### ③DI DI:Dependency Injection,翻译过来是**依赖注入。** DI 是 IOC 的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。 所以结论是:IOC 就是一种反转控制的思想, 而 DI 是对 IOC 的一种具体实现。 ### 2.1.2、IOC容器在Spring中的实现 Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件也叫做 bean。在创建bean 之前,首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式: #### ①BeanFactory 这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。 #### ②ApplicationContext BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用ApplicationContext 而不是底层的 BeanFactory。 #### ③ApplicationContext的主要实现类 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5ef22ed9768f4b3ba2a252f498f6d35b.png) | 类型名 | 简介 | | ------------------------------- | ------------------------------------------------------------ | | ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 | | FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 | | ConfigurableApplicationContext | ApplicationContext 的子接口,包含一些扩展方法refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。 | | WebApplicationContext | 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 | ## 2.2、基于XML管理bean ### 2.2.1、实验一:入门案例 #### ①创建Maven Module #### ②引入依赖 ```xml org.springframework spring-context 5.3.1 junit junit 4.12 test org.projectlombok lombok 1.18.16 junit junit 4.12 test mysql mysql-connector-java 8.0.16 com.alibaba druid 1.0.31 ``` ![在这里插入图片描述](https://img-blog.csdnimg.cn/e6e60986a567486a9fddb13a90aebd66.png) #### ③创建类HelloWorld ```java public class HelloWorld { public void sayHello(){ System.out.println("helloworld"); } } ``` #### ④创建Spring的配置文件 ![在这里插入图片描述](https://img-blog.csdnimg.cn/1d45ecb0f8444dd687e9c2f4c10852d1.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/ccfb51180f9b49b88d20c3f6bf481270.png) #### ⑤在Spring的配置文件中配置bean ```xml ``` #### ⑥创建测试类测试 ```java public class SpringTest { @Test public void testHelloWorld() { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Hello helloworld = context.getBean(Hello.class); helloworld.sayHello(); } } ``` #### ⑦思路 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2bd487425aaa47d8b65a29ba4a713663.png) #### ⑧注意 Spring 底层默认通过反射技术调用组件类的无参构造器来创建组件对象,这一点需要注意。如果在需要无参构造器时,没有无参构造器,则会抛出下面的异常: > org.springframework.beans.factory.BeanCreationException: Error creating bean with name > 'helloworld' defined in class path resource [applicationContext.xml]: Instantiation of bean > failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed > to instantiate [com.zhuang.spring.bean.HelloWorld]: No default constructor found; nested > exception is java.lang.NoSuchMethodException: com.zhuang.spring.bean.HelloWorld. > () ### 2.2.2、实验二:获取bean #### ①方式一:根据id获取 由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。上个实验中我们使用的就是这种方式。 #### ②方式二:根据类型获取 ```java @Test public void testHelloWorld(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); HelloWorld bean = ac.getBean(HelloWorld.class); bean.sayHello(); } ``` #### ③方式三:根据id和类型 ```java @Test public void testHelloWorld(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); HelloWorld bean = ac.getBean("helloworld", HelloWorld.class); bean.sayHello(); } ``` #### ④注意 当根据类型获取bean时,要求IOC容器中指定类型的bean有且只能有一个 当IOC容器中一共配置了两个: ```java ``` 根据类型获取时会抛出异常: > org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying beanof type 'com.zhuang.spring.bean.HelloWorld' available: expected single matching bean but found 2: helloworldOne,helloworldTwo #### ⑤扩展 如果组件类实现了接口,根据接口类型可以获取 bean 吗? > 可以,前提是bean唯一 如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗? > 不行,因为bean不唯一 #### ⑥结论 根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 **instanceof** 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到。 ### 2.2.3、实验三:依赖注入之setter注入 #### ①创建学生类Student ```java @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Student { private Integer id; private String name; private Integer age; private String sex; private Clazz clazz; private String[] hobbies; private Map teacherMap; } ``` #### ②配置bean时为属性赋值 ```xml ``` #### ③测试 ```java @Test public void testDIBySet(){ ApplicationContext ac = new ClassPathXmlApplicationContext("spring-di.xml"); Student studentOne = ac.getBean("studentOne", Student.class); System.out.println(studentOne); } ``` ### 2.2.4、实验四:依赖注入之构造器注入 #### ①在Student类中添加有参构造 ```java public Student(Integer id, String name, Integer age, String sex) { this.id = id; this.name = name; this.age = age; this.sex = sex; } ``` #### ②配置bean ```java ``` > 注意: > constructor-arg标签还有两个属性可以进一步描述构造器参数: > > - index属性:指定参数所在位置的索引(从0开始) > - name属性:指定参数名 #### ③测试 ```java @Test public void testDIBySet(){ ApplicationContext ac = new ClassPathXmlApplicationContext("spring-di.xml"); Student studentOne = ac.getBean("studentTwo", Student.class); System.out.println(studentOne); } ``` ### 2.2.5、实验五:特殊值处理 #### ①字面量赋值 > 什么是字面量? > int a = 10; > 声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a > 的时候,我们实际上拿到的值是10。 > 而如果a是带引号的:'a',那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面 > 量。所以字面量没有引申含义,就是我们看到的这个数据本身。 ```xml ``` #### ②null值 ```xml ``` 注意: ```xml ``` 以上写法,为name所赋的值是字符串null #### ③xml实体 ```xml ``` #### ④CDATA节 ```xml ``` ### 2.2.6、实验六:为类类型属性赋值 #### ①创建班级类Clazz ```java @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Clazz { private Integer clazzId; private String clazzName; private List students; } ``` #### ②修改Student类 在Student类中添加以下代码: ```java private Clazz clazz; public Clazz getClazz() { return clazz; } public void setClazz(Clazz clazz) { this.clazz = clazz; } ``` #### ③方式一:引用外部已声明的bean 配置Clazz类型的bean: ```xml ``` 为Student中的clazz属性赋值: ```xml ``` 错误演示: ```xml ``` > 如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException: > Cannot convert value of type 'java.lang.String' to required type > 'com.zhuang.spring.bean.Clazz' for property 'clazz': no matching editors or conversion > strategy found > 意思是不能把String类型转换成我们要的Clazz类型,说明我们使用value属性时,Spring只把这个 > 属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值 #### ④方式二:内部bean ```xml ``` #### ③方式三:级联属性赋值 ```xml ``` ### 2.2.7、实验七:为数组类型属性赋值 #### ①修改Student类 在Student类中添加以下代码: ```java private String[] hobbies; public String[] getHobbies() { return hobbies; } public void setHobbies(String[] hobbies) { this.hobbies = hobbies; } ``` #### ②配置bean ```xml 抽烟 喝酒 烫头 ``` ### 2.2.8、实验八:为集合类型属性赋值 #### ①为List集合类型属性赋值 在Clazz类中添加以下代码: ```java private List students; public List getStudents() { return students; } public void setStudents(List students) { this.students = students; } ``` 配置bean: ```xml ``` > 若为Set集合类型属性赋值,只需要将其中的list标签改为set标签即可 #### ②为Map集合类型属性赋值 创建教师类Teacher: ```java @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Teacher { private Integer teacherId; private String teacherName; } ``` 在Student类中添加以下代码: ```java private Map teacherMap; public Map getTeacherMap() { return teacherMap; } public void setTeacherMap(Map teacherMap) { this.teacherMap = teacherMap; } ``` 配置bean: ```xml 抽烟 喝酒 烫头 10010 10086 ``` #### ③引用集合类型的bean ```xml 10010 10086 抽烟 喝酒 烫头 ``` > 使用util:list、util:map标签必须引入相应的命名空间,可以通过idea的提示功能选择 ### 2.2.9、实验九:p命名空间 引入p命名空间后,可以通过以下方式为bean的各个属性赋值 ```xml ``` ### 2.2.10、实验十:引入外部属性文件 #### ①加入依赖 ```xml mysql mysql-connector-java 8.0.16 com.alibaba druid 1.0.31 ``` #### ②创建外部属性文件 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5d087d99d7cd4792b793e28df1511ce8.png) ```properties jdbc.user=root jdbc.password=root jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver ``` #### ③引入属性文件 ```xml ``` #### ④配置bean ```xml ``` #### ⑤测试 ```java @Test public void testDataSource() throws SQLException { ApplicationContext ac = new ClassPathXmlApplicationContext("springdatasource. xml"); DataSource dataSource = ac.getBean(DataSource.class); Connection connection = dataSource.getConnection(); System.out.println(connection); } ``` ### 2.2.11、实验十一:bean的作用域 #### ①概念 在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表: | 取值 | 含义 | 创建对象的时机 | | ----------------- | --------------------------------------- | --------------- | | prototype | 这个bean在IOC容器中有多个实例 | 获取bean时 | | singleton(默认) | 在IOC容器中,这个bean的对象始终为单实例 | IOC容器初始化时 | 如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用): | 取值 | 含义 | | ------- | -------------------- | | request | 在一个请求范围内有效 | | session | 在一个会话范围内有效 | #### ②创建类User ```java @AllArgsConstructor @ToString public class User { private Integer id; private String username; private String password; private Integer age; } ``` #### ③配置bean ```xml ``` #### ④测试 ```java @Test public void testBeanScope(){ ApplicationContext ac = new ClassPathXmlApplicationContext("springscope. xml"); User user1 = ac.getBean(User.class); User user2 = ac.getBean(User.class); System.out.println(user1==user2); } ``` ### 2.2.12、实验十二:bean的生命周期 #### ①具体的生命周期过程 - bean对象创建(调用无参构造器) - 给bean对象设置属性 - bean对象初始化之前操作(由bean的后置处理器负责) - bean对象初始化(需在配置bean时指定初始化方法) - bean对象初始化之后操作(由bean的后置处理器负责) - bean对象就绪可以使用 - bean对象销毁(需在配置bean时指定销毁方法) - IOC容器关闭 #### ②修改类User ```java @AllArgsConstructor @ToString public class User { private Integer id; private String username; private String password; private Integer age; public User() { System.out.println("生命周期:1、创建对象"); } public void initMethod() { System.out.println("生命周期:3、初始化"); } public void destroyMethod() { System.out.println("生命周期:5、销毁"); } public Integer getId() { return id; } public void setId(Integer id) { System.out.println("生命周期:2、依赖注入"); this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } } ``` > 注意其中的initMethod()和destroyMethod(),可以通过配置bean指定为初始化和销毁的方法 #### ③配置bean ```xml ``` #### ④测试 ```java @Test public void testLife(){ ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-lifecycle.xml"); User bean = ac.getBean(User.class); System.out.println("生命周期:4、通过IOC容器获取bean并使用"); ac.close(); } ``` #### ⑤bean的后置处理器 bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行创建bean的后置处理器: ```java public class MyBeanProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("☆☆☆" + beanName + " = " + bean); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("★★★" + beanName + " = " + bean); return bean; } } ``` 在IOC容器中配置后置处理器: ```java ``` ### 2.2.13、实验十三:FactoryBean #### ①简介 FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。 将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。 #### ②创建类UserFactoryBean ```java public class UserFactoryBean implements FactoryBean { @Override public User getObject() throws Exception { return new User(); } @Override public Class getObjectType() { return User.class; } } ``` #### ③配置bean ```xml ``` #### ④测试 ```java @Test public void testUserFactoryBean(){ //获取IOC容器 ApplicationContext ac = new ClassPathXmlApplicationContext("springfactorybean.xml"); User user = (User) ac.getBean("user"); System.out.println(user); } ``` ### 2.2.14、实验十四:基于xml的自动装配 > 自动装配: > 根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值 #### ①场景模拟 创建类UserController ```java public class UserController { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } public void saveUser(){ userService.saveUser(); } } ``` 创建接口UserService ```java public interface UserService { void saveUser(); } ``` 创建类UserServiceImpl实现接口UserService ```java @Data public class UserServiceImpl implements UserService { private UserDao userDao; @Override public void saveUser() { userDao.saveUser(); } } ``` 创建接口UserDao ```java public interface UserDao { void saveUser(); } ``` 创建类UserDaoImpl实现接口UserDao ```java public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println("保存成功"); } } ``` #### ②配置bean ``` 使用bean标签的autowire属性设置自动装配效果 自动装配方式:byType byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值 若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值 null 若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常 NoUniqueBeanDefinitionException ``` ```xml ``` > 自动装配方式:byName > byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值 ```xml ``` #### ③测试 ```java @Test public void testAutoWireByXML(){ ApplicationContext ac = new ClassPathXmlApplicationContext("autowirexml.xml"); UserController userController = ac.getBean(UserController.class); userController.saveUser(); } ``` ## 2.3、基于注解管理bean ### 2.3.1、实验一:标记与扫描 #### ①注解 和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。 本质上:所有一切的操作都是Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行。 #### ②扫描 Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作 #### ③新建Maven Module ```xml org.springframework spring-context 5.3.1 junit junit 4.12 test ``` #### ④创建Spring配置文件 ![在这里插入图片描述](https://img-blog.csdnimg.cn/90c5a70f3e0a494c83cccc0c3de9a71f.png) #### ⑤标识组件的常用注解 > - @Component:将类标识为普通组件 > - @Controller:将类标识为控制层组件 > - @Service:将类标识为业务层组件 > - @Repository:将类标识为持久层组件 问:以上四个注解有什么关系和区别? ![在这里插入图片描述](https://img-blog.csdnimg.cn/820463852eb34fbab8ad05dfabf48dc1.png) 通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。 对于Spring使用IOC容器管理这些组件来说没有区别。 所以@Controller、@Service、@Repository这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。 注意:虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记。 #### ⑥创建组件 创建控制层组件 ```java @Controller public class UserController { } ``` 创建接口UserService ```java public interface UserService { } ``` 创建业务层组件UserServiceImpl ```java @Service public class UserServiceImpl implements UserService { } ``` 创建接口UserDao ```java public interface UserDao { } ``` 创建持久层组件UserDaoImpl ```java @Repository public class UserDaoImpl implements UserDao { } ``` #### ⑦扫描组件 情况一:最基本的扫描方式 ```xml ``` 情况二:指定要排除的组件 ```xml ``` 情况三:仅扫描指定组件 ```xml ``` #### ⑧测试 ```java @Test public void testAutowireByAnnotation(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserController userController = ac.getBean(UserController.class); System.out.println(userController); UserService userService = ac.getBean(UserService.class); System.out.println(userService); UserDao userDao = ac.getBean(UserDao.class); System.out.println(userDao); } ``` #### ⑨组件所对应的bean的id 在我们使用XML方式管理bean的时候,每个bean都有一个唯一标识,便于在其他地方引用。现在使用注解后,每个组件仍然应该有一个唯一标识。 > 默认情况 > 类名首字母小写就是bean的id。例如:UserController类对应的bean的id就是userController。 > 自定义bean的id > 可通过标识组件的注解的value属性设置自定义的bean的id > @Service("userService")//默认为userServiceImpl public class UserServiceImpl implements > UserService {} ### 2.3.2、实验二:基于注解的自动装配 #### ①场景模拟 > 参考基于xml的自动装配 > 在UserController中声明UserService对象 > 在UserServiceImpl中声明UserDao对象 #### ②@Autowired注解 在成员变量上直接标记@Autowired注解即可完成自动装配,不需要提供setXxx()方法。以后我们在项 目中的正式用法就是这样。 ```java @Controller public class UserController { @Autowired private UserService userService; public void saveUser(){ userService.saveUser(); } } ``` ```java public interface UserService { void saveUser(); } ``` ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public void saveUser() { userDao.saveUser(); } } ``` ```java public interface UserDao { void saveUser(); } ``` ```java @Repository public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println("保存成功"); } } ``` #### ③@Autowired注解其他细节 > @Autowired注解可以标记在构造器和set方法上 ```java @Controller public class UserController { private UserService userService; @Autowired public UserController(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } } ``` ```java @Controller public class UserController { private UserService userService; @Autowired public void setUserService(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } } ``` #### ④@Autowired工作流程 ![在这里插入图片描述](https://img-blog.csdnimg.cn/c1c4e58318af4c38bda4938f06eee4b2.png) - 首先根据所需要的组件类型到IOC容器中查找 - 能够找到唯一的bean:直接执行装配 - 如果完全找不到匹配这个类型的bean:装配失败 - 和所需类型匹配的bean不止一个 - 没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行 匹配 - 能够找到:执行装配 - 找不到:装配失败 - 使用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配 - 能够找到:执行装配 - 找不到:装配失败 ```java @Controller public class UserController { @Autowired @Qualifier("userServiceImpl") private UserService userService; public void saveUser(){ userService.saveUser(); } } ``` > @Autowired中有属性required,默认值为true,因此在自动装配无法找到相应的bean时,会装配失败 > 可以将属性required的值设置为true,则表示能装就装,装不上就不装,此时自动装配的属性为默认值 > 但是实际开发时,基本上所有需要装配组件的地方都是必须装配的,用不上这个属性。 # 3、AOP ## 3.1、场景模拟 ### 3.1.1、声明接口 声明计算器接口Calculator,包含加减乘除的抽象方法 ``` public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } ``` ### 3.1.2、创建实现类 ![在这里插入图片描述](https://img-blog.csdnimg.cn/cf3eb7d21ca94b468980b5a00acac4fa.png) ```java public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println("方法内部 result = " + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println("方法内部 result = " + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println("方法内部 result = " + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println("方法内部 result = " + result); return result; } } ``` ### 3.1.3、创建带日志功能的实现类 ![在这里插入图片描述](https://img-blog.csdnimg.cn/d212801d50f04ac4ab35b8049bd775b9.png) ```java public class CalculatorLogImpl implements Calculator { @Override public int add(int i, int j) { System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); int result = i + j; System.out.println("方法内部 result = " + result); System.out.println("[日志] add 方法结束了,结果是:" + result); return result; } @Override public int sub(int i, int j) { System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j); int result = i - j; System.out.println("方法内部 result = " + result); System.out.println("[日志] sub 方法结束了,结果是:" + result); return result; } @Override public int mul(int i, int j) { System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j); int result = i * j; System.out.println("方法内部 result = " + result); System.out.println("[日志] mul 方法结束了,结果是:" + result); return result; } @Override public int div(int i, int j) { System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j); int result = i / j; System.out.println("方法内部 result = " + result); System.out.println("[日志] div 方法结束了,结果是:" + result); return result; } } ``` ### 3.1.4、提出问题 #### ①现有代码缺陷 针对带日志功能的实现类,我们发现有如下缺陷: - 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力 - 附加功能分散在各个业务功能方法中,不利于统一维护 #### ②解决思路 解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。 #### ③困难 解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。 ## 3.2、代理模式 ### 3.2.1、概念 #### ①介绍 二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/842f552ec50f4a6daab3ecf04e78cdf3.png) 使用代理后: ![在这里插入图片描述](https://img-blog.csdnimg.cn/47e086af6d004beea4647be9776332d2.png) #### ②生活中的代理 - 广告商找大明星拍广告需要经过经纪人 - 合作伙伴找大老板谈合作要约见面时间需要经过秘书 - 房产中介是买卖双方的代理 #### ③相关术语 - 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。 - 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。 ### 3.2.2、静态代理 创建静态代理类: ```java public class CalculatorStaticProxy implements Calculator { // 将被代理的目标对象声明为成员变量 private Calculator target; public CalculatorStaticProxy(Calculator target) { this.target = target; } @Override public int add(int i, int j) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); // 通过目标对象来实现核心业务逻辑 int addResult = target.add(i, j); System.out.println("[日志] add 方法结束了,结果是:" + addResult); return addResult; } } ``` > 静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来 > 说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代 > 码,日志功能还是分散的,没有统一管理。 > 提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理 > 类来实现。这就需要使用动态代理技术了。 ### 3.2.3、动态代理 ![在这里插入图片描述](https://img-blog.csdnimg.cn/591201e0433d43daa6e57fd40c54b863.png) 生产代理对象的工厂类: ```java public class ProxyFactory { private Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxy() { /** * newProxyInstance():创建一个代理实例 * 其中有三个参数: * 1、classLoader:加载动态生成的代理类的类加载器 * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组 * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接 口中的抽象方法 */ ClassLoader classLoader = target.getClass().getClassLoader(); Class < ?>[] interfaces = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler() {@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /** * proxy:代理对象 * method:代理对象需要实现的方法,即其中需要重写的方法 * args:method所对应方法的参数 */ Object result = null; try { System.out.println("[动态代理][日志] " + method.getName() + ",参数:" + Arrays.toString(args)); result = method.invoke(target, args); System.out.println("[动态代理][日志] " + method.getName() + ",果:" + result); } catch(Exception e) { e.printStackTrace(); System.out.println("[动态代理][日志] " + method.getName() + ",异常:" + e.getMessage()); } finally { System.out.println("[动态代理][日志] " + method.getName() + ",方法执行完毕"); } return result; } }; return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler); } } ``` ### 3.2.4、测试 ```java @Test public void testDynamicProxy() { ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl()); Calculator proxy = (Calculator) factory.getProxy(); proxy.div(1, 0); //proxy.div(1,1); } ``` ## 3.3、AOP概念及相关术语 ### 3.3.1、概述 AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。 ### 3.3.2、相关术语 #### ①横切关注点 从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。 这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/06aba67acef143b19a72879f1a123200.png) #### ②通知 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。 - 前置通知:在被代理的目标方法前执行 - 返回通知:在被代理的目标方法成功结束后执行(寿终正寝) - 异常通知:在被代理的目标方法异常结束后执行(死于非命) - 后置通知:在被代理的目标方法最终结束后执行(盖棺定论) - 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置 ![在这里插入图片描述](https://img-blog.csdnimg.cn/4bd302d573114b0e8585306bff0141f5.png) #### ③切面 封装通知方法的类。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/09848acebf7f4989bb92165c7aee6648.png) #### ④目标 被代理的目标对象。 #### ⑤代理 向目标对象应用通知之后创建的代理对象。 #### ⑥连接点 这也是一个纯逻辑概念,不是语法定义的。 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉 点就是连接点。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/1969a3c641204c08b3765bf8676866fe.png) #### ⑦切入点 定位连接点的方式。 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。 Spring 的 AOP 技术可以通过切入点定位到特定的连接点。 切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件 ### 3.3.3、作用 - 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。 - 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。 ## 3.4、基于注解的AOP ### 3.4.1、技术说明 ![在这里插入图片描述](https://img-blog.csdnimg.cn/791cb8e16bf84db9a0dc6c2aa29e0733.png) - 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因 为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。 - cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。 - AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最 终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解 ### 3.4.2、准备工作 #### ①添加依赖 在IOC所需依赖基础上再加入下面依赖即可: ```xml org.springframework spring-aspects 5.3.1 ``` #### ②准备被代理的目标资源 接口: ```java public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } ``` 实现类: ```java @Component public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println("方法内部 result = " + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println("方法内部 result = " + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println("方法内部 result = " + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println("方法内部 result = " + result); return result; } } ``` ### 3.4.3、创建切面类并配置 ```java // @Aspect表示这个类是一个切面类 @Aspect // @Component注解保证这个切面类能够放入IOC容器 @Component public class LogAspect {@Before("execution(public int com.zhuang.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:" + methodName + ",参数:" + args); }@After("execution(* com.zhuang.aop.annotation.CalculatorImpl.*(..))") public void afterMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->后置通知,方法名:" + methodName); }@AfterReturning(value = "execution(* com.zhuang.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:" + methodName + ",结果:" + result); }@AfterThrowing(value = "execution(* com.zhuang.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:" + methodName + ",异常:" + ex); }@Around("execution(* com.zhuang.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println("环绕通知-->目标对象方法执行之前"); //目标对象(连接点)方法的执行 result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后"); } catch(Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时"); } finally { System.out.println("环绕通知-->目标对象方法执行完毕"); } return result; } } ``` 在Spring的配置文件中配置: ```xml ``` ### 3.4.4、各种通知 - 前置通知:使用@Before注解标识,在被代理的目标方法前执行 - 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝) - 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命) - 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论) - 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置 > 各种通知的执行顺序: > > - Spring版本5.3.x以前: > - 前置通知 > - 目标操作 > - 后置通知 > - 返回通知或异常通知 > - Spring版本5.3.x以后: > - 前置通知 > - 目标操作 > - 返回通知或异常通知 > - 后置通知 ### 3.4.5、切入点表达式语法 #### ①作用 ![在这里插入图片描述](https://img-blog.csdnimg.cn/4f88e41de3bd4ad581052ebcfffdda03.png) #### ②语法细节 - 用`*号`代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限 - 在包名的部分,一个`“*”`号只能代表包的层次结构中的一层,表示这一层是任意的。 - 例如`*.Hello`匹配com.Hello,不匹配com.zhuang.Hello - 在包名的部分,使用`“*..”`表示包名任意、包的层次深度任意 - 在类名的部分,类名 部分整体用`*`号代替,表示类名任意 - 在类名的部分,可以使用`*`号代替类名的一部分 - 例如:`*Service`匹配所有名称以Service结尾的类或接口 在方法名部分,可以使用`*`号表示方法名任意 - 在方法名部分,可以使用`*`号代替方法名的一部分 例如:`*Operation`匹配所有方法名以Operation结尾的方法 - 在方法参数列表部分,使用(..)表示参数列表任意 - 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头 - 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的 - 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的 - 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符 - 例如:execution(`public int ..Service.*(.., int)`) 正确 例如:execution(` *int ..Service.*(.., int)`) 错误* ![在这里插入图片描述](https://img-blog.csdnimg.cn/57395238ce914c1e9c067915262b0609.png) ### 3.4.6、重用切入点表达式 #### ①声明 ```java @Pointcut("execution(* com.zhuang.aop.annotation.*.*(..))") public void pointCut(){} ``` #### ②在同一个切面中使用 ```java @Before("pointCut()") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); } ``` #### ③在不同切面中使用 ```java @Before("com.zhuang.aop.CommonPointCut.pointCut()") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); } ``` ### 3.4.7、获取通知的相关信息 #### ①获取连接点信息 获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参 ```java @Before("execution(public int com.zhuang.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod(JoinPoint joinPoint){ //获取连接点的签名信息 String methodName = joinPoint.getSignature().getName(); //获取目标方法到的实参信息 String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); } ``` #### ②获取目标方法的返回值 @AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值 ```java @AfterReturning(value = "execution(* com.zhuang.aop.annotation.CalculatorImpl.* (..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result); } ``` #### ③获取目标方法的异常 @AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常 ```java @AfterThrowing(value = "execution(* com.zhuang.aop.annotation.CalculatorImpl.* (..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex); } ``` ### 3.4.8、环绕通知 ```java @Around("execution(* com.zhuang.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println("环绕通知-->目标对象方法执行之前"); //目标方法的执行,目标方法的返回值一定要返回给外界调用者 result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时"); } finally { System.out.println("环绕通知-->目标对象方法执行完毕"); } return result; } ``` ### 3.4.9、切面的优先级 相同目标方法上同时存在多个切面时,切面的优先级控制切面的**内外嵌套**顺序。 - 优先级高的切面:外面 - 优先级低的切面:里面 使用@Order注解可以控制切面的优先级: - @Order(较小的数):优先级高 - @Order(较大的数):优先级低 ![在这里插入图片描述](https://img-blog.csdnimg.cn/7f59b32343d04e0faf1741cfc31611be.png) ## 3.5、基于XML的AOP(了解) ### 3.5.1、准备工作 参考基于注解的AOP环境 ### 3.5.2、实现 ```xml ``` # 4、声明式事务 ## 4.1、JdbcTemplate ### 4.1.1、简介 Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作 ### 4.1.2、准备工作 #### ①加入依赖 ```xml org.springframework spring-context 5.3.1 org.springframework spring-orm 5.3.1 org.springframework spring-test 5.3.1 junit junit 4.12 test mysql mysql-connector-java 8.0.16 com.alibaba druid 1.0.31 ``` #### ②创建jdbc.properties ```properties jdbc.user=root jdbc.password=root jdbc.url=jdbc:mysql://localhost:3306/ssm jdbc.driver=com.mysql.cj.jdbc.Driver ``` #### ③配置Spring的配置文件 ```xml ``` ### 4.1.3、测试 #### ①在测试类装配 JdbcTemplate ```java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring-jdbc.xml") public class JDBCTemplateTest { @Autowired private JdbcTemplate jdbcTemplate; } ``` #### ②测试增删改功能 ```java @Test //测试增删改功能 public void testUpdate() { String sql = "insert into t_emp values(null,?,?,?)"; int result = jdbcTemplate.update(sql, "张三", 23, "男"); System.out.println(result); } ``` #### ③查询一条数据为实体类对象 ```java @Test //查询一条数据为一个实体类对象 public void testSelectEmpById() { String sql = "select * from t_emp where id = ?"; Emp emp = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper < >(Emp.class), 1); System.out.println(emp); } ``` #### ④查询多条数据为一个list集合 ```java @Test //查询多条数据为一个list集合 public void testSelectList() { String sql = "select * from t_emp"; List < Emp > list = jdbcTemplate.query(sql, new BeanPropertyRowMapper < >(Emp.class)); list.forEach(emp - >System.out.println(emp)); } ``` #### ⑤查询单行单列的值 ```java @Test //查询单行单列的值 public void selectCount(){ String sql = "select count(id) from t_emp"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); } ``` ## 4.2、声明式事务概念 ### 4.2.1、编程式事务 事务功能的相关操作全部通过自己编写代码来实现: ```java Connection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 提交事务 conn.commit(); } catch(Exception e) { // 回滚事务 conn.rollBack(); } finally { // 释放数据库连接 conn.close(); } ``` 编程式的实现方式存在缺陷: - 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。 - 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。 ### 4.2.2、声明式事务 既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。 - 好处1:提高开发效率 - 好处2:消除了冗余的代码 - 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化 所以,我们可以总结下面两个概念: - 编程式:自己写代码实现功能 - 声明式:通过配置让框架实现功能 ## 4.3、基于注解的声明式事务 ### 4.3.1、准备工作 #### ①加入依赖 ```xml org.springframework spring-context 5.3.1 org.springframework spring-orm 5.3.1 org.springframework spring-test 5.3.1 junit junit 4.12 test mysql mysql-connector-java 8.0.16 com.alibaba druid 1.0.31 ``` #### ②创建jdbc.properties ```properties jdbc.user=root jdbc.password=root jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver ``` #### ③配置Spring的配置文件 ```xml ``` #### ④创建表 ```sql CREATE TABLE `t_book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称', `price` int(11) DEFAULT NULL COMMENT '价格', `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)', PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍 穹',80,100),(2,'斗罗大陆',50,100); CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(20) DEFAULT NULL COMMENT '用户名', `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50); ``` #### ⑤创建组件 创建BookController: ```java @Controller public class BookController {@Autowired private BookService bookService; public void buyBook(Integer bookId, Integer userId) { bookService.buyBook(bookId, userId); } } ``` 创建接口BookService: ```java public interface BookService { void buyBook(Integer bookId, Integer userId); } ``` 创建实现类BookServiceImpl: ```java @Service public class BookServiceImpl implements BookService {@Autowired private BookDao bookDao;@Override public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); } } ``` 创建接口BookDao: ```java public interface BookDao { Integer getPriceByBookId(Integer bookId); void updateStock(Integer bookId); void updateBalance(Integer userId, Integer price); } ``` 创建实现类BookDaoImpl: ```java @Repository public class BookDaoImpl implements BookDao {@Autowired private JdbcTemplate jdbcTemplate;@Override public Integer getPriceByBookId(Integer bookId) { String sql = "select price from t_book where book_id = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock(Integer bookId) { String sql = "update t_book set stock = stock - 1 where book_id = ?"; jdbcTemplate.update(sql, bookId); } @Override public void updateBalance(Integer userId, Integer price) { String sql = "update t_user set balance = balance - ? where user_id =?"; jdbcTemplate.update(sql, price, userId); } } ``` ### 4.3.2、测试无事务情况 #### ①创建测试类 ```java @RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration("classpath:txannotation.xml") public class TxByAnnotationTest {@Autowired private BookController bookController;@Test public void testBuyBook() { bookController.buyBook(1, 1); } } ``` ### ②模拟场景 用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额 假设用户id为1的用户,购买id为1的图书 用户余额为50,而图书价格为80 购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段 此时执行sql语句会抛出SQLException #### ③观察结果 因为没有添加事务,图书的库存更新了,但是用户的余额没有更新 显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败 ### 4.3.3、加入事务 #### ①添加事务配置 在Spring的配置文件中添加配置: ```xml ``` 注意:导入的名称空间需要 **tx 结尾**的那个。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/fe0bb35f326a4e83979657ba826611fd.png) #### ②添加事务注解 因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理在BookServiceImpl的buybook()添加注解@Transactional #### ③观察结果 由于使用了Spring的声明式事务,更新库存和更新余额都没有执行 ### 4.3.4、@Transactional注解标识的位置 @Transactional标识在方法上,咋只会影响该方法 @Transactional标识的类上,咋会影响类中所有的方法 ### 4.3.5、事务属性:只读 #### ①介绍 对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。 #### ②使用方式 ```java @Transactional(readOnly = true) public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); } ``` #### ③注意 对增删改操作设置只读会抛出下面异常: > Caused by: java.sql.SQLException: Conn ection is read-only. Queries leading to data modification > are not allowed ### 4.3.6、事务属性:超时 #### ①介绍 事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。 概括来说就是一句话:超时回滚,释放资源。 #### ②使用方式 ```java @Transactional(timeout = 3) public void buyBook(Integer bookId, Integer userId) { try { TimeUnit.SECONDS.sleep(5); } catch(InterruptedException e) { e.printStackTrace(); } //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); } ``` #### ③观察结果 执行过程中抛出异常: > org.springframework.transaction.TransactionTimedOutException: Transaction timed out: > deadline was Fri Jun 04 16:25:39 CST 2022 ### 4.3.7、事务属性:回滚策略 #### ①介绍 声明式事务默认只针对运行时异常回滚,编译时异常不回滚。 可以通过@Transactional中相关属性设置回滚策略 - rollbackFor属性:需要设置一个Class类型的对象 - rollbackForClassName属性:需要设置一个字符串类型的全类名 - noRollbackFor属性:需要设置一个Class类型的对象 - rollbackFor属性:需要设置一个字符串类型的全类名 ##### ②使用方式 ```java @Transactional(noRollbackFor = ArithmeticException.class) //@Transactional(noRollbackForClassName = "java.lang.ArithmeticException") public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); System.out.println(1 / 0); } ``` #### ③观察结果 虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当 出现ArithmeticException不发生回滚,因此购买图书的操作正常执行 ### 4.3.8、事务属性:事务隔离级别 #### ①介绍 数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事 务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同 的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。 隔离级别一共有四种: - 读未提交:READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。 - 读已提交:READ COMMITTED、 要求Transaction01只能读取Transaction02已提交的修改。 - 可重复读:REPEATABLE READ 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它 事务对这个字段进行更新。 - 串行化:SERIALIZABLE 确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它 事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。 各个隔离级别解决并发问题的能力见下表: ![在这里插入图片描述](https://img-blog.csdnimg.cn/f2e0182b07af4094969a321c73de9256.png) 各种数据库产品对事务隔离级别的支持程度: ![在这里插入图片描述](https://img-blog.csdnimg.cn/6b7c35f477f44be6aad8cfeb22068965.png) #### ②使用方式 ```java @Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化 ``` ### 4.3.9、事务属性:事务传播行为 #### ①介绍 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。 #### ②测试 创建接口CheckoutService: ```java public interface CheckoutService { void checkout(Integer[] bookIds, Integer userId); } ``` 创建实现类CheckoutServiceImpl: ```java @Service public class CheckoutServiceImpl implements CheckoutService { @Autowired private BookService bookService; @Override @Transactional //一次购买多本图书 public void checkout(Integer[] bookIds, Integer userId) { for (Integer bookId: bookIds) { bookService.buyBook(bookId, userId); } } } ``` 在BookController中添加方法: ```java @Autowired private CheckoutService checkoutService; public void checkout(Integer[] bookIds, Integer userId) { checkoutService.checkout(bookIds, userId); } ``` 在数据库中将用户的余额修改为100元 #### ③观察结果 可以通过@Transactional中的propagation属性设置事务传播行为 修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性 @Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开 启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调 用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余 额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不 了,就都买不了 @Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启 的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图 书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受 影响,即能买几本就买几本 ## 4.4、基于XML的声明式事务 ### 4.4.1、场景模拟 参考基于注解的声明式事务 ### 4.4.2、修改Spring配置文件 将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置: ``` ``` **注意:基于xml实现的声明式事务,必须引入aspectJ的依赖** ```xml org.springframework spring-aspects 5.3.1 ``` # 三、SpringMVC # 1、SpringMVC简介 ## 1.1、什么是MVC MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分M:Model,模型层,指工程中的JavaBean,作用是处理数据 JavaBean分为两类: - 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等 - 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。 V:View,视图层,指工程中的html或jsp等页面,作用是与用户进行交互,展示数据 C:Controller,控制层,指工程中的servlet,作用是接收请求和响应浏览器 MVC的工作流程: 用户通过视图层发送请求到服务器,在服务器中请求被Controller接收,Controller 调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller再根据请求处理的结果 找到相应的View视图,渲染数据后最终响应给浏览器 ## 1.2、什么是SpringMVC SpringMVC是Spring的一个后续产品,是Spring的一个子项目 SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、 WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 Java EE 项目 表述层开发的**首选方案**。 > 注:三层架构分为表述层(或表示层)、业务逻辑层、数据访问层,表述层表示前台页面和后台 > servlet ## 1.3、SpringMVC的特点 - Spring 家族原生产品,与 IOC 容器等基础设施无缝对接 - 基于原生的Servlet,通过了功能强大的前端控制器DispatcherServlet,对请求和响应进行统一处理 - 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案 - 代码清新简洁,大幅度提升开发效率 - 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可 - 性能卓著,尤其适合现代大型、超大型互联网项目要求 # 2、入门案例 ## 2.1、开发环境 IDE:idea 2021.2 构建工具:maven3.5.4 服务器:tomcat9.0 Spring版本:5.3.1 ## 2.2、创建maven工程 ### ①添加web模块 ### ②打包方式:war ### ③引入依赖 ```xml org.springframework spring-webmvc 5.3.1 ch.qos.logback logback-classic 1.2.3 javax.servlet javax.servlet-api 3.1.0 provided org.thymeleaf thymeleaf-spring5 3.0.12.RELEASE ``` 注:由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/fe1d3247f613434d8c78f07d17145ed7.png) ## 2.3、配置web.xml 注册SpringMVC的前端控制器DispatcherServlet ### ①默认配置方式 此配置作用下,SpringMVC的配置文件默认位于WEB-INF下,默认名称为- servlet.xml,例如,以下配置所对应SpringMVC的配置文件位于WEB-INF下,文件名为springMVCservlet.xml ```xml springMVC org.springframework.web.servlet.DispatcherServlet springMVC / ``` ### ②扩展配置方式 可通过init-param标签设置SpringMVC配置文件的位置和名称,通过load-on-startup标签设置SpringMVC前端控制器DispatcherServlet的初始化时间 ```xml springMVC org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:springMVC.xml 1 springMVC / ``` > 标签中使用/和/*的区别: > /所匹配的请求可以是/login或.html或.js或.css方式的请求路径,但是/不能匹配.jsp请求路径的请 > 求 > 因此就可以避免在访问jsp页面时,该请求被DispatcherServlet处理,从而找不到相应的页面 > /*则能够匹配所有请求,例如在使用过滤器时,若需要对所有请求进行过滤,就需要使用/*的写 > 法 ## 2.4、创建请求控制器 由于前端控制器对浏览器发送的请求进行了统一的处理,但是具体的请求有不同的处理过程,因此需要 创建处理具体请求的类,即请求控制器 请求控制器中每一个处理请求的方法成为控制器方法 因为SpringMVC的控制器由一个POJO(普通的Java类)担任,因此需要通过@Controller注解将其标识 为一个控制层组件,交给Spring的IoC容器管理,此时SpringMVC才能够识别控制器的存在 ```java @Controller public class HelloController { } ``` ## 2.5、创建SpringMVC的配置文件 ```xml text/html application/json ``` ## 2.6、测试HelloWorld ### ①实现对首页的访问 在请求控制器中创建处理请求的方法 ```java // @RequestMapping注解:处理请求和控制器方法之间的映射关系 // @RequestMapping注解的value属性可以通过请求地址匹配请求,/表示的当前工程的上下文路径 // localhost:8080/springMVC/ @RequestMapping("/") public String index() { //设置视图名称 return "index"; } ``` ### ②通过超链接跳转到指定页面 在主页index.html中设置超链接 ```html 首页

首页

HelloWorld
``` 在请求控制器中创建处理请求的方法 ```java @RequestMapping("/hello") public String HelloWorld() { return "target"; } ``` ## 2.7、总结 浏览器发送请求,若请求地址符合前端控制器的url-pattern,该请求就会被前端控制器DispatcherServlet处理。前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器,将请求地址和控制器中@RequestMapping注解的value属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面 # 3、@RequestMapping注解 ## 3.1、@RequestMapping注解的功能 从注解名称上我们可以看到,@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联 起来,建立映射关系。 SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。 ## 3.2、@RequestMapping注解的位置 @RequestMapping标识一个类:设置映射请求的请求路径的初始信息 @RequestMapping标识一个方法:设置映射请求请求路径的具体信息 ```java @Controller@RequestMapping("/test") public class RequestMappingController { //此时请求映射所映射的请求的请求路径为:/test/testRequestMapping @RequestMapping("/testRequestMapping") public String testRequestMapping() { return "success"; } } ``` ## 3.3、@RequestMapping注解的value属性 @RequestMapping注解的value属性通过请求的请求地址匹配请求映射 @RequestMapping注解的value属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求 @RequestMapping注解的value属性必须设置,至少通过请求地址匹配请求映射 ```html 测试@RequestMapping的value属性-- >/testRequestMapping
测试@RequestMapping的value属性-->/test
``` ```java @RequestMapping( value = {"/testRequestMapping", "/test"} ) public String testRequestMapping(){ return "success"; } ``` ## 3.4、@RequestMapping注解的method属性 @RequestMapping注解的method属性通过请求的请求方式(get或post)匹配请求映射 @RequestMapping注解的method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配 多种请求方式的请求 若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错 405:Request method 'POST' not supported ```html 测试@RequestMapping的value属性-->/test
``` ```java @RequestMapping( value = {"/testRequestMapping", "/test"}, method = {RequestMethod.GET, RequestMethod.POST} ) public String testRequestMapping(){ return "success"; } ``` > 注: > 1、对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解 > 处理get请求的映射-->@GetMapping > 处理post请求的映射-->@PostMapping > 处理put请求的映射-->@PutMapping > 处理delete请求的映射-->@DeleteMapping > 2、常用的请求方式有get,post,put,delete > 但是目前浏览器只支持get和post,若在form表单提交时,为method设置了其他请求方式的字符 > 串(put或delete),则按照默认的请求方式get处理 > 若要发送put和delete请求,则需要通过spring提供的过滤器HiddenHttpMethodFilter,在 > RESTful部分会讲到 ## 3.5、@RequestMapping注解的params属性(了解) @RequestMapping注解的params属性通过请求的请求参数匹配请求映射 @RequestMapping注解的params属性是一个字符串类型的数组,可以通过四种表达式设置请求参数 和请求映射的匹配关系 "param":要求请求映射所匹配的请求必须携带param请求参数 "!param":要求请求映射所匹配的请求必须不能携带param请求参数 "param=value":要求请求映射所匹配的请求必须携带param请求参数且param=value "param!=value":要求请求映射所匹配的请求必须携带param请求参数但是param!=value ```html 测试@RequestMapping的 params属性-->/test
``` ```java @RequestMapping( value = {"/testRequestMapping", "/test"} ,method = {RequestMethod.GET, RequestMethod.POST} ,params = {"username","password!=123456"} ) public String testRequestMapping(){ return "success"; } ``` > 注: > 若当前请求满足@RequestMapping注解的value和method属性,但是不满足params属性,此时 > 页面回报错400:Parameter conditions "username, password!=123456" not met for actual > request parameters: username={admin}, password={123456} ## 3.6、@RequestMapping注解的headers属性(了解) @RequestMapping注解的headers属性通过请求的请求头信息匹配请求映射 @RequestMapping注解的headers属性是一个字符串类型的数组,可以通过四种表达式设置请求头信 息和请求映射的匹配关系 "header":要求请求映射所匹配的请求必须携带header请求头信息 "!header":要求请求映射所匹配的请求必须不能携带header请求头信息 "header=value":要求请求映射所匹配的请求必须携带header请求头信息且header=value "header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value 若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面 显示404错误,即资源未找到 ## 3.7、SpringMVC支持ant风格的路径 ?:表示任意的单个字符 *:表示任意的0个或多个字符 **:表示任意层数的任意目录 注意:在使用**时,只能使用/**/xxx的方式 ## 3.8、SpringMVC支持路径中的占位符(重点) 原始方式:/deleteUser?id=1 rest方式:/user/delete/1 SpringMVC路径中的占位符常用于RESTful风格中,当请求路径中将某些数据通过路径的方式传输到服 务器中,就可以在相应的@RequestMapping注解的value属性中通过占位符{xxx}表示传输的数据,在 通过@PathVariable注解,将占位符所表示的数据赋值给控制器方法的形参 ```html 测试路径中的占位符-->/testRest
``` ```java @RequestMapping("/testRest/{id}/{username}") public String testRest(@PathVariable("id") String id, @PathVariable("username") String username){ System.out.println("id:"+id+",username:"+username); return "success"; } //最终输出的内容为-->id:1,username:admin ``` # 4、SpringMVC获取请求参数 ## 4.1、通过ServletAPI获取 将HttpServletRequest作为控制器方法的形参,此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象 ```java @RequestMapping("/testParam") public String testParam(HttpServletRequest request) { String username = request.getParameter("username"); String password = request.getParameter("password"); System.out.println("username:" + username + ",password:" + password); return "success"; } ``` ## 4.2、通过控制器方法的形参获取请求参数 在控制器方法的形参位置,设置和请求参数同名的形参,当浏览器发送请求,匹配到请求映射时,在DispatcherServlet中就会将请求参数赋值给相应的形参 ```html 测试获取请求参数-- >/testParam
``` ```java @RequestMapping("/testParam") public String testParam(String username, String password) { System.out.println("username:" + username + ",password:" + password); return "success"; } ``` > 注: > 若请求所传输的请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置字符串 > 数组或者字符串类型的形参接收此请求参数 > 若使用字符串数组类型的形参,此参数的数组中包含了每一个数据 > 若使用字符串类型的形参,此参数的值为每个数据中间使用逗号拼接的结果 ## 4.3、@RequestParam @RequestParam是将请求参数和控制器方法的形参创建映射关系 @RequestParam注解一共有三个属性: value:指定为形参赋值的请求参数的参数名 required:设置是否必须传输此请求参数,默认值为true 若设置为true时,则当前请求必须传输value所指定的请求参数,若没有传输该请求参数,且没有设置 defaultValue属性,则页面报错400:Required String parameter 'xxx' is not present;若设置为 false,则当前请求不是必须传输value所指定的请求参数,若没有传输,则注解所标识的形参的值为 null defaultValue:不管required属性值为true或false,当value所指定的请求参数没有传输或传输的值 为""时,则使用默认值为形参赋值 ## 4.4、@RequestHeader @RequestHeader是将请求头信息和控制器方法的形参创建映射关系 @RequestHeader注解一共有三个属性:value、required、defaultValue,用法同@RequestParam ## 4.5、@CookieValue @CookieValue是将cookie数据和控制器方法的形参创建映射关系 @CookieValue注解一共有三个属性:value、required、defaultValue,用法同@RequestParam ## 4.6、通过POJO获取请求参数 可以在控制器方法的形参位置设置一个实体类类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值 ```html
用户名:
密码:
性别:
年龄:
邮箱:
``` ```java @RequestMapping("/testpojo") public String testPOJO(User user){ System.out.println(user); return "success"; } //最终结果-->User{id=null, username='张三', password='123', age=23, sex='男',email='123@qq.com'} ``` ## 4.7、解决获取请求参数的乱码问题 解决获取请求参数的乱码问题,可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter,但是 必须在web.xml中进行注册 ```xml CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true CharacterEncodingFilter /* ``` > 注: > SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效 # 5、域对象共享数据 ## 5.1、使用ServletAPI向request域对象共享数据 ```java @RequestMapping("/testServletAPI") public String testServletAPI(HttpServletRequest request) { request.setAttribute("testScope", "hello,servletAPI"); return "success"; } ``` ## 5.2、使用ModelAndView向request域对象共享数据 ```java @RequestMapping("/testModelAndView") public ModelAndView testModelAndView() { /** * ModelAndView有Model和View的功能 * Model主要用于向请求域共享数据 * View主要用于设置视图,实现页面跳转 */ ModelAndView mav = new ModelAndView(); //向请求域共享数据 mav.addObject("testScope", "hello,ModelAndView"); //设置视图,实现页面跳转 mav.setViewName("success"); return mav; } ``` ## 5.3、使用Model向request域对象共享数据 ```java @RequestMapping("/testModel") public String testModel(Model model) { model.addAttribute("testScope", "hello,Model"); return "success"; } ``` ## 5.4、使用map向request域对象共享数据 ```java @RequestMapping("/testMap") public String testMap(Map < String, Object > map) { map.put("testScope", "hello,Map"); return "success"; } ``` ## 5.5、使用ModelMap向request域对象共享数据 ```java @RequestMapping("/testModelMap") public String testModelMap(ModelMap modelMap) { modelMap.addAttribute("testScope", "hello,ModelMap"); return "success"; } ``` ## 5.6、Model、ModelMap、Map的关系 Model、ModelMap、Map类型的参数其实本质上都是 BindingAwareModelMap 类型的 ```java public interface Model{} public class ModelMap extends LinkedHashMap {} public class ExtendedModelMap extends ModelMap implements Model {} public class BindingAwareModelMap extends ExtendedModelMap {} ``` ## 5.7、向session域共享数据 ```java @RequestMapping("/testSession") public String testSession(HttpSession session) { session.setAttribute("testSessionScope", "hello,session"); return "success"; } ``` ## 5.8、向application域共享数据 ```java @RequestMapping("/testApplication") public String testApplication(HttpSession session) { ServletContext application = session.getServletContext(); application.setAttribute("testApplicationScope", "hello,application"); return "success"; } ``` # 6、SpringMVC的视图 SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户 SpringMVC视图的种类很多,默认有转发视图和重定向视图 当工程引入jstl的依赖,转发视图会自动转换为JstlView 若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视 图解析器解析之后所得到的是ThymeleafView ## 6.1、ThymeleafView 当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析,视图名称拼接视图前缀和视图后缀所得到的最终路径,会通过转发的方式实现跳转 ```java @RequestMapping("/testHello") public String testHello(){ return "hello"; } ``` ## 6.2、转发视图 SpringMVC中默认的转发视图是InternalResourceView SpringMVC中创建转发视图的情况: 当控制器方法中所设置的视图名称以"forward:"为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"forward:"去掉,剩余部分作为最终路径通过转发的方式实现跳转 例如"forward:/","forward:/employee" ```java @RequestMapping("/testForward") public String testForward(){ return "forward:/testHello"; } ``` ## 6.3、重定向视图 SpringMVC中默认的重定向视图是RedirectView 当控制器方法中所设置的视图名称以"redirect:"为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转 例如"redirect:/","redirect:/employee" ```java @RequestMapping("/testRedirect") public String testRedirect(){ return "redirect:/testHello"; } ``` > 注: > 重定向视图在解析时,会先将redirect:前缀去掉,然后会判断剩余部分是否以/开头,若是则会自 > 动拼接上下文路径 ## 6.4、视图控制器view-controller 当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用viewcontroller标签进行表示 ```xml ``` > 注: > 当SpringMVC中设置任何一个view-controller时,其他控制器中的请求映射将全部失效,此时需 > 要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签: > # 7、RESTful ## 7.1、RESTful简介 REST:Representational State Transfer,表现层资源状态转移。 ### ①资源 资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互。 ### ②资源的表述 资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移(交换)。资源的表述可以有多种格式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等。资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式。 ### ③状态转移 状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述。通过转移和操作资源的表述,来间接实现操作资源的目的。 ## 7.2、RESTful的实现 具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE用来删除资源。 REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方 式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/689e8cf583314193865d8b6ec2f86c7c.png) ## 7.3、HiddenHttpMethodFilter 由于浏览器只支持发送get和post方式的请求,那么该如何发送put和delete请求呢? SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求 HiddenHttpMethodFilter 处理put和delete请求的条件: a>当前请求的请求方式必须为post b>当前请求必须传输请求参数_method 满足以上条件,HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数 _method的值,因此请求参数_method的值才是最终的请求方式 在web.xml中注册HiddenHttpMethodFilter ```java HiddenHttpMethodFilter org.springframework.web.filter.HiddenHttpMethodFilter HiddenHttpMethodFilter /* ``` > 目前为止,SpringMVC中提供了两个过滤器:CharacterEncodingFilter和 > HiddenHttpMethodFilter > 在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter > 原因: > 在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字 > 符集的request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作 > 而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作: > > String paramValue = request.getParameter(this.methodParam); # 8、RESTful案例 ## 8.1、准备工作 和传统 CRUD 一样,实现对员工信息的增删改查。 ```java @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Employee { private Integer id; private String lastName; private String email; //1 male, 0 female private Integer gender; } ``` - 准备dao模拟数据 ```java @Repository public class EmployeeDao { private static Map employees = null; static { employees = new HashMap(); employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1)); employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1)); employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0)); employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0)); employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1)); } private static Integer initId = 1006; public void save(Employee employee) { if (employee.getId() == null) { employee.setId(initId++); } employees.put(employee.getId(), employee); } public Collection getAll() { return employees.values(); } public Employee get(Integer id) { return employees.get(id); } public void delete(Integer id) { employees.remove(id); } } ``` ## 8.2、功能清单 | 功能 | 功能 | 请求方式 | | ------------------- | ----------- | -------- | | 访问首页√ | / | GET | | 查询全部数据√ | /employee | GET | | 删除√ | /employee/2 | DELETE | | 跳转到添加数据页面√ | /toAdd | GET | | 执行保存√ | /employee | POST | | 跳转到更新数据页面√ | /employee/2 | GET | | 执行更新√ | /employee | PUT | ## 8.3、具体功能:访问首页 ### ①配置view-controller ```xml ``` ### ②创建页面 ```html Title

首页

访问员工信息 ``` ## 8.4、具体功能:查询所有员工数据 ### ①控制器方法 ```java @GetMapping(value = "/employee") public String getAllEmployee(Model model) { //获取所有的员工信息 Collection allEmployee = employeeDao.getAll(); //将所有的员工信息在请求域中共享 model.addAttribute("allEmployee", allEmployee); //跳转到列表页面 return "employee_list"; } ``` ### ②创建employee_list.html ```html employee list
employee list
id lastName email gender options(add
delete update
``` ## 8.5、具体功能:删除 ### ①创建处理delete请求方式的表单 ```html
``` ### ②删除超链接绑定点击事件 引入vue.js ```html ``` 删除超链接 ```javascript delete ``` 通过vue处理点击事件 ```javascript ``` ### ③控制器方法 ```java @DeleteMapping(value = "/employee/{id}") public String deleteEmployee(@PathVariable("id") Integer id) { //删除员工信息 employeeDao.delete(id); //重定向到列表功能:/employee return "redirect:/employee"; } ``` ## 8.6、具体功能:跳转到添加数据页面 ### ①配置view-controller ```xml ``` ### ②创建employee_add.html ```html add employee
add employee
lastName
email
gender male female
``` ## 8.7、具体功能:执行保存 ### ①控制器方法 ```java @PostMapping(value = "/employee") public String addEmployee(Employee employee) { //保存员工信息 employeeDao.save(employee); //重定向到列表功能:/employee return "redirect:/employee"; } ``` ## 8.8、具体功能:跳转到更新数据页面 ### ①修改超链接 ```html update ``` ### ②控制器方法 ```java @GetMapping(value = "/employee/{id}") public String toUpdate(@PathVariable("id") Integer id, Model model) { //根据id查询员工信息 Employee employee = employeeDao.get(id); //将员工信息共享到请求域中 model.addAttribute("employee", employee); //跳转到employee_update.html return "employee_update"; } ``` ### ③创建employee_update.html ```html update employee
update employee
lastName
email
gender male female
``` ## 8.9、具体功能:执行更新 ### ①控制器方法 ```java @PutMapping(value = "/employee") public String updateEmployee(Employee employee) { //修改员工信息 employeeDao.save(employee); //重定向到列表功能:/employee return "redirect:/employee"; } ``` # 9、SpringMVC处理ajax请求 ## 9.1、@RequestBody @RequestBody可以获取请求体信息,使用@RequestBody注解标识控制器方法的形参,当前请求的请求体就会为当前注解所标识的形参赋值 ```html
用户名:
密码:
``` ```java @RequestMapping("/test/RequestBody") public String testRequestBody(@RequestBody String requestBody){ System.out.println("requestBody:"+requestBody); return "success"; } ``` 输出结果: requestBody:username=admin&password=123456 ## 9.2、@RequestBody获取json格式的请求参数 > 在使用了axios发送ajax请求之后,浏览器发送到服务器的请求参数有两种格式: > 1、name=value&name=value...,此时的请求参数可以通过request.getParameter()获取,对应 > SpringMVC中,可以直接通过控制器方法的形参获取此类请求参数 > > 2、{key:value,key:value,...},此时无法通过request.getParameter()获取,之前我们使用操作 > json的相关jar包gson或jackson处理此类请求参数,可以将其转换为指定的实体类对象或map集 > 合。在SpringMVC中,直接使用@RequestBody注解标识控制器方法的形参即可将此类请求参数 > 转换为java对象 使用@RequestBody获取json格式的请求参数的条件: 1、导入jackson的依赖 ```xml com.fasterxml.jackson.core jackson-databind 2.12.1 ``` 2、SpringMVC的配置文件中设置开启mvc的注解驱动 ```xml ``` 3、在控制器方法的形参位置,设置json格式的请求参数要转换成的java类型(实体类或map)的参数,并使用@RequestBody注解标识 ```html ``` ```java @RequestMapping("/test/RequestBody/json") public void testRequestBody(@RequestBody Map map, HttpServletResponse response) throws IOException { System.out.println(map); response.getWriter().write("hello,RequestBody"); } public void testRequestBody(@RequestBody User user, HttpServletResponse response) throws IOException { System.out.println(user); response.getWriter().write("hello,RequestBody"); } ``` ## 9.3、@ResponseBody @ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器 ```java @RequestMapping("/test/ResponseBody") @ResponseBody public String testResponseBody() { return "success"; } ``` ## 9.4、@ResponseBody响应浏览器json数据 服务器处理ajax请求之后,大多数情况都需要向浏览器响应一个java对象,此时必须将java对象转换为json字符串才可以响应到浏览器,之前我们使用操作json数据的jar包gson或jackson将java对象转换为json字符串。在SpringMVC中,我们可以直接使用@ResponseBody注解实现此功能 @ResponseBody响应浏览器json数据的条件: 1、导入jackson的依赖 ```xml com.fasterxml.jackson.core jackson-databind 2.12.1 ``` 2、SpringMVC的配置文件中设置开启mvc的注解驱动 ```xml ``` 3、使用@ResponseBody注解标识控制器方法,在方法中,将需要转换为json字符串并响应到浏览器的java对象作为控制器方法的返回值,此时SpringMVC就可以将此对象直接转换为json字符串并响应到浏览器 ```javascript testResponseBody(){ axios.post("/SpringMVC/test/ResponseBody/json").then(response=>{ console.log(response.data); }); } ``` ```java @RequestMapping("/test/ResponseBody/json") @ResponseBody public List testResponseBodyJson() { User user1 = new User(1001, "admin1", "123456", 20, "男"); User user2 = new User(1002, "admin2", "123456", 20, "男"); User user3 = new User(1003, "admin3", "123456", 20, "男"); List list = Arrays.asList(user1, user2, user3); return list; } public Map testResponseBodyJson(){ User user1 = new User(1001, "admin1", "123456", 20, "男"); User user2 = new User(1002, "admin2", "123456", 20, "男"); User user3 = new User(1003, "admin3", "123456", 20, "男"); Map map = new HashMap<>(); map.put("1001", user1); map.put("1002", user2); map.put("1003", user3); return map; } public User testResponseBodyJson(){ User user = new User(1001, "admin", "123456", 20, "男"); return user; } ``` ## 9.5、@RestController注解 @RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解 # 10、文件上传和下载 ## 10.1、文件下载 ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文使用ResponseEntity实现下载文件的功能 ```java @RequestMapping("/test/down") public ResponseEntity testResponseEntity(HttpSession session) throws IOException { //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取服务器中文件的真实路径 String realPath = servletContext.getRealPath("img"); realPath = realPath + File.separator + "1.jpg"; //创建输入流 InputStream is = new FileInputStream(realPath); //创建字节数组,is.available()获取输入流所对应文件的字节数 byte[] bytes = new byte[is.available()]; //将流读到字节数组中 is.read(bytes); //创建HttpHeaders对象设置响应头信息 MultiValueMap headers = new HttpHeaders(); //设置要下载方式以及下载文件的名字 headers.add("Content-Disposition", "attachment;filename=1.jpg"); //设置响应状态码 HttpStatus statusCode = HttpStatus.OK; //创建ResponseEntity对象 ResponseEntity responseEntity = new ResponseEntity<>(bytes, headers, statusCode); //关闭输入流 is.close(); return responseEntity; } ``` ## 10.2、文件上传 文件上传要求form表单的请求方式必须为post,并且添加属性enctype="multipart/form-data"SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息 上传步骤: ### ①添加依赖: ```xml commons-fileupload commons-fileupload 1.3.1 ``` ### ②在SpringMVC的配置文件中添加配置: ```xml ``` ### ③控制器方法: ```java @RequestMapping("/test/up") public String testUp(MultipartFile photo, HttpSession session) throws IOException { //获取上传的文件的文件名 String fileName = photo.getOriginalFilename(); //获取上传的文件的后缀名 String hzName = fileName.substring(fileName.lastIndexOf(".")); //获取uuid String uuid = UUID.randomUUID().toString(); //拼接一个新的文件名 fileName = uuid + hzName; //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取当前工程下photo目录的真实路径 String photoPath = servletContext.getRealPath("photo"); //创建photoPath所对应的File对象 File file = new File(photoPath); //判断file所对应目录是否存在 if (!file.exists()) { file.mkdir(); } String finalPath = photoPath + File.separator + fileName; //上传文件 photo.transferTo(new File(finalPath)); return "success"; } ``` # 11、拦截器 ## 11.1、拦截器的配置 SpringMVC中的拦截器用于拦截控制器方法的执行 SpringMVC中的拦截器需要实现HandlerInterceptor SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置: ```xml ``` ## 11.2、拦截器的三个抽象方法 SpringMVC中的拦截器有三个抽象方法: preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返 回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法 postHandle:控制器方法执行之后执行postHandle() afterCompletion:处理完视图和模型数据,渲染视图完毕之后执行afterCompletion() ①若每个拦截器的preHandle()都返回true 此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关: preHandle()会按照配置的顺序执行,而postHandle()和afterCompletion()会按照配置的反序执行 ②若某个拦截器的preHandle()返回了false preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false 的拦截器之前的拦截器的afterCompletion()会执行 # 12、异常处理器 ## 12.1、基于配置的异常处理 SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver,使用方式: ```xml error ``` ## 12.2、基于注解的异常处理 ```java @ControllerAdvice public class ExceptionController { //设置要处理的异常信息 @ExceptionHandler(ArithmeticException.class) public String handleException(Throwable ex, Model model){ //ex表示控制器方法所出现的异常 model.addAttribute("ex", ex); return "error"; } } ``` # 13、注解配置SpringMVC 使用配置类和注解代替web.xml和SpringMVC配置文件的功能 ## 13.1、创建初始化类,代替web.xml 在Servlet3.0环境中,容器会在类路径中查找实现`javax.servlet.ServletContainerInitializer`接口的类, 如果找到的话就用它来配置Servlet容器。 Spring提供了这个接口的实现,名为 SpringServletContainerInitializer`,这个类反过来又会查找实现`WebApplicationInitializer`的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的`WebApplicationInitializer`基础实现,名为 `AbstractAnnotationConfigDispatcherServletInitializer`,当我们的类扩展了 `AbstractAnnotationConfigDispatcherServletInitializer`并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。 ## 13.2、创建SpringConfig配置类,代替spring的配置文件 ```java @Configuration public class SpringConfig { //ssm整合之后,spring的配置信息写在此类中 } ``` ## 13.3、创建WebConfig配置类,代替SpringMVC的配置文件 ```java @Configuration //扫描组件 @ComponentScan("com.zhuang.mvc.controller") //开启MVC注解驱动 @EnableWebMvc public class WebConfig implements WebMvcConfigurer { //使用默认的servlet处理静态资源 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } //配置文件上传解析器 @Bean public CommonsMultipartResolver multipartResolver() { return new CommonsMultipartResolver(); } //配置拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { FirstInterceptor firstInterceptor = new FirstInterceptor(); registry.addInterceptor(firstInterceptor).addPathPatterns("/**"); } //配置视图控制 /* * @Override public void addViewControllers(ViewControllerRegistry registry) { * registry.addViewController("/").setViewName("index"); } */ //配置异常映射 /* * @Override public void * configureHandlerExceptionResolvers(List resolvers) * { SimpleMappingExceptionResolver exceptionResolver = new * SimpleMappingExceptionResolver(); Properties prop = new Properties(); * prop.setProperty("java.lang.ArithmeticException", "error"); //设置异常映射 * exceptionResolver.setExceptionMappings(prop); //设置共享异常信息的键 * exceptionResolver.setExceptionAttribute("ex"); * resolvers.add(exceptionResolver); } */ //配置生成模板解析器 @Bean public ITemplateResolver templateResolver() { WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext(); // ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过 WebApplicationContext 的方法获得 ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver( webApplicationContext.getServletContext()); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); templateResolver.setCharacterEncoding("UTF-8"); templateResolver.setTemplateMode(TemplateMode.HTML); return templateResolver; } //生成模板引擎并为模板引擎注入模板解析器 @Bean public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver); return templateEngine; } //生成视图解析器并未解析器注入模板引擎 @Bean public ViewResolver viewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setCharacterEncoding("UTF-8"); viewResolver.setTemplateEngine(templateEngine); return viewResolver; } } ``` ## 13.4、测试功能 ```java @RequestMapping("/") public String index(){ return "index"; } ``` # 14、SpringMVC执行流程 ## 14.1、SpringMVC常用组件 - DispatcherServlet:**前端控制器**,不需要工程师开发,由框架提供 作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求 - HandlerMapping:**处理器映射器**,不需要工程师开发,由框架提供 作用:根据请求的url、method等信息查找Handler,即控制器方法 - Handler:**处理器**,需要工程师开发 作用:在DispatcherServlet的控制下Handler对具体的用户请求进行处理 - HandlerAdapter:**处理器适配器**,不需要工程师开发,由框架提供 作用:通过HandlerAdapter对处理器(控制器方法)进行执行 - ViewResolver:**视图解析器**,不需要工程师开发,由框架提供 作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView - View:**视图** 作用:将模型数据通过页面展示给用户 ## 14.2、DispatcherServlet初始化过程 DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet生命周期来进行调度。 ### ①初始化WebApplicationContext 所在类:org.springframework.web.servlet.FrameworkServlet ```java protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac; if (!cwac.isActive()) { if (cwac.getParent() == null) { cwac.setParent(rootContext); } this.configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { wac = this.findWebApplicationContext(); } if (wac == null) { // 创建WebApplicationContext wac = this.createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { synchronized(this.onRefreshMonitor) { this.onRefresh(wac); } } if (this.publishContext) { String attrName = this.getServletContextAttributeName(); this.getServletContext().setAttribute(attrName, wac); } return wac; } ``` ### ②创建WebApplicationContext 所在类:org.springframework.web.servlet.FrameworkServlet ```java protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { Class contextClass = this.getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } else { // 通过反射创建 IOC 容器对象 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass); wac.setEnvironment(this.getEnvironment()); // 设置父容器 wac.setParent(parent); String configLocation = this.getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } this.configureAndRefreshWebApplicationContext(wac); return wac; } } ``` ### ③DispatcherServlet初始化策略 `FrameworkServlet`创建`WebApplicationContext`后,刷新容器,调用onRefresh(wac),此方法在`DispatcherServlet`中进行了重写,调用了`initStrategies(context)`方法,初始化策略,即初始化`DispatcherServlet`的各个组件 所在类:org.springframework.web.servlet.DispatcherServlet ```java protected void initStrategies(ApplicationContext context) { this.initMultipartResolver(context); this.initLocaleResolver(context); this.initThemeResolver(context); this.initHandlerMappings(context); this.initHandlerAdapters(context); this.initHandlerExceptionResolvers(context); this.initRequestToViewNameTranslator(context); this.initViewResolvers(context); this.initFlashMapManager(context); } ``` ## 14.3、DispatcherServlet调用组件处理请求 ### ①processRequest() `FrameworkServlet`重写HttpServlet中的service()和doXxx(),这些方法中调用了`processRequest(request, response)` 所在类:org.springframework.web.servlet.FrameworkServlet ```java protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContext localeContext = this.buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor()); this.initContextHolders(request, localeContext, requestAttributes); try { // 执行服务,doService()是一个抽象方法,在DispatcherServlet中进行了重写 this.doService(request, response); } catch (IOException | ServletException var16) { failureCause = var16; throw var16; } catch (Throwable var17) { failureCause = var17; throw new NestedServletException("Request processing failed", var17); } finally { this.resetContextHolders(request, previousLocaleContext, previousAttributes); if (requestAttributes != null) { requestAttributes.requestCompleted(); } this.logResult(request, response, (Throwable)failureCause, asyncManager); this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause); } } ``` ### ②doService() 所在类:org.springframework.web.servlet.DispatcherServlet ```java protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { this.logRequest(request); Map attributesSnapshot = null; if (WebUtils.isIncludeRequest(request)) { attributesSnapshot = new HashMap(); Enumeration attrNames = request.getAttributeNames(); label120: while(true) { String attrName; do { if (!attrNames.hasMoreElements()) { break label120; } attrName = (String)attrNames.nextElement(); } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet")); attributesSnapshot.put(attrName, request.getAttribute(attrName)); } } request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource()); if (this.flashMapManager != null) { FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); } RequestPath requestPath = null; if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) { requestPath = ServletRequestPathUtils.parseAndCache(request); } try { this.doDispatch(request, response); } finally { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) { this.restoreAttributesAfterInclude(request, attributesSnapshot); } if (requestPath != null) { ServletRequestPathUtils.clearParsedRequestPath(request); } } } ``` ### ③doDispatch() 所在类:org.springframework.web.servlet.DispatcherServlet ```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { try { ModelAndView mv = null; Object dispatchException = null; try { processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; /* mappedHandler:调用链 包含handler、interceptorList、interceptorIndex handler:浏览器发送的请求所匹配的控制器方法 interceptorList:处理控制器方法的所有拦截器集合 interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行 */ mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. // 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法 HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { return; } } // 调用拦截器的preHandle() if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. // 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); // 调用拦截器的postHandle() mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } // 后续处理:处理模型数据和渲染视图 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } } ``` ### ④processDispatchResult() ```java private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { this.logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException)exception).getModelAndView(); } else { Object handler = mappedHandler != null ? mappedHandler.getHandler() : null; mv = this.processHandlerException(request, response, handler, exception); errorView = mv != null; } } // 处理模型数据和渲染视图 if (mv != null && !mv.wasCleared()) { this.render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else if (this.logger.isTraceEnabled()) { this.logger.trace("No view rendering, null ModelAndView returned."); } if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Exception (if any) is already handled.. // 调用拦截器的afterCompletion() if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, (Exception)null); } } } ``` ## 14.4、SpringMVC的执行流程 1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获。 2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射: a) 不存在 i. 再判断是否配置了mvc:default-servlet-handler ii. 如果没配置,则控制台报映射查找不到,客户端展示404错误 iii.如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404 错误 b) 存在则执行下面的流程 3) 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及 Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。 4) DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。 5) 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】 6) 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作: a) HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定 的响应信息 b) 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等 c) 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等 d) 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中 7) Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象。 8) 此时将开始执行拦截器的postHandle(...)方法【逆向】。 9) 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行 HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model 和View,来渲染视图。 10) 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。 11) 将渲染结果返回给客户端。 # 四、SSM整合 ## 4.1、ContextLoaderListener Spring提供了监听器`ContextLoaderListener`,实现`ServletContextListener`接口,可监听`ServletContext`的状态,在web服务器的启动,读取Spring的配置文件,创建Spring的IOC容器。web应用中必须在web.xml中配置 ```xml org.springframework.web.context.ContextLoaderListener contextConfigLocation classpath:spring.xml ``` ## 4.2、准备工作 ### ①创建Maven Module ### ②导入依赖 ```xml org.springframework spring-context ${spring.version} org.springframework spring-beans ${spring.version} org.springframework spring-web ${spring.version} org.springframework spring-webmvc ${spring.version} org.springframework spring-jdbc ${spring.version} org.springframework spring-aspects ${spring.version} org.springframework spring-test ${spring.version} org.mybatis mybatis 3.5.7 org.mybatis mybatis-spring 2.0.6 com.alibaba druid 1.0.31 junit junit 4.12 test mysql mysql-connector-java 8.0.16 log4j log4j 1.2.17 com.github.pagehelper pagehelper 5.2.0 ch.qos.logback logback-classic 1.2.3 javax.servlet javax.servlet-api 3.1.0 provided com.fasterxml.jackson.core jackson-databind 2.12.1 commons-fileupload commons-fileupload 1.3.1 org.thymeleaf thymeleaf-spring5 3.0.12.RELEASE org.projectlombok lombok 1.18.16 src/main/java **/*.properties **/*.xml false ``` ### ③创建表 ```sql CREATE TABLE `t_emp` ( `emp_id` int(11) NOT NULL AUTO_INCREMENT, `emp_name` varchar(20) DEFAULT NULL, `age` int(11) DEFAULT NULL, `sex` char(1) DEFAULT NULL, `email` varchar(50) DEFAULT NULL, PRIMARY KEY (`emp_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ``` ## 4.3、配置web.xml ```xml CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true CharacterEncodingFilter /* HiddenHttpMethodFilter org.springframework.web.filter.HiddenHttpMethodFilter HiddenHttpMethodFilter /* SpringMVC org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:springmvc.xml 1 SpringMVC / org.springframework.web.context.ContextLoaderListener contextConfigLocation classpath:spring.xml ``` ## 4.4、创建SpringMVC的配置文件并配置 ```xml ``` ## 4.5、搭建MyBatis环境 ### ①创建属性文件jdbc.properties ```properties jdbc.user=root jdbc.password=root jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver ``` ### ②创建MyBatis的核心配置文件mybatis-config.xml ```xml ``` ### ③创建Mapper接口和映射文件 ```java public interface EmployeeMapper { /** * 查询所有的员工信息 * @return */ List getAllEmployee(); } ``` ```xml ``` ### ④创建日志文件log4j.xml ```xml ``` ## 4.6、创建Spring的配置文件并配置 ```xml ``` ## 4.7、测试功能 ### ①创建组件 实体类Employee ```java @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Employee { private Integer empId; private String empName; private Integer age; private String gender; private String email; } ``` 创建控制层组件EmployeeController ```java /** * description: EmployeeController * date: 2022/10/11 20:07 * author: Zhuang * version: 1.0 * 查询所有的员工信息-->/employee-->get * 查询员工的分页信息-->/employee/page/1-->get * 根据id查询员工信息-->/employee/1-->get * 跳转到添加页面-->/to/add-->get * 添加员工信息-->/employee-->post * 修改员工信息-->/employee-->put * 删除员工信息-->/employee/1-->delete */ @Controller public class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping(value = "/employee/page/{pageNum}", method = RequestMethod.GET) public String getEmployeePage(@PathVariable("pageNum") Integer pageNum, Model model) { //获取员工的分页信息 PageInfo page = employeeService.getEmployeePage(pageNum); //将分页数据共享到请求域中 model.addAttribute("page", page); //跳转到employee_list.html return "employee_list"; } @RequestMapping(value = "/employee", method = RequestMethod.GET) public String getAllEmployee(Model model) { //查询所有的员工信息 List list = employeeService.getAllEmployee(); //将员工信息在请求域中共享 model.addAttribute("list", list); //跳转到employee_list.html return "employee_list"; } } ``` 创建接口EmployeeService ```java public interface EmployeeService { PageInfo getEmployeeList(Integer pageNum); } ``` 创建实现类EmployeeServiceImpl ```java @Service @Transactional public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override public List getAllEmployee() { return employeeMapper.getAllEmployee(); } @Override public PageInfo getEmployeePage(Integer pageNum) { //开启分页功能 PageHelper.startPage(pageNum, 4); //查询所有的员工信息 List list = employeeMapper.getAllEmployee(); //获取分页相关数据 PageInfo page = new PageInfo<>(list, 5); return page; } } ``` ### ②创建页面 ```html 员工列表
员工列表
流水号 员工姓名 年龄 性别 邮箱 操作
删除 修改
``` ### ③访问测试分页功能 localhost:8080/employee/page/1