Spring+SpringMvc+Mybatis整合

前言

Mybatis

Mybatis特性与优势

特性

image-20221111165647489

优势

image-20221111165433496

环境要求

  • mysql8.0 (编码默认支持中文,不用在另外设置字符集)

    • jdbc 驱动类使用 com.mysql.cj.jdbc.Driver

    • 连接地址 jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC

    没设置时区会报java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more

  • pom.xml中配置<packaging>jar</packaging>【jdbc不需要tomcat服务器支持】

  • 导入依赖

    <dependencies>
        <!-- Mybatis核心 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
    </dependencies>
  • 配置文件

    • 核心配置文件(放在resources下):设置如何连接数据库

      习惯命名为mybatis-config.xml(非强制)

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
          <!--设置连接数据库的环境-->
          <environments default="development">配置多个环境
              <environment id="development">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">数据源,作用:帮助管理数据库的连接
                      <property name="driver" value="com.mysql.cj.jdbc.Driver"/>驱动类
                      <property name="url" value="jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC"/>					连接数据库的地址
                      <property name="username" value="root"/>
                      <property name="password" value="密码"/>
                  </dataSource>
              </environment>
          </environments>
          <!--引入mybatis映射文件-->
          通过读取核心配置文件获取操作数据库的对象,sql语句在映射文件中
          <mappers>
              <package name="mappers/UserMapper.xml"/>
          </mappers>
      </configuration>
  • 映射文件:如何操作数据库(里面写sql)

创建mapper接口

  • 接口名:用实体类名+Mapper

  • 调用接口中的方法,执行该方法对应的sql

  • 不用创建其实现类,通过mybatis创建其代理实现类

创建mybatis映射文件

image-20221111194052315

  • 对应关系

    • 一个mapper接口

      • 一个映射文件

      • 数据库中的一张表

      • 一个实体类

    • 接口中的一个方法

      • 映射文件中的一条sql

  • 命名规则

    • 和mapper接口同名(建议)+.xml

  • 两个一致

    • 映射文件中的namespace要和mapper接口中的全类名保持一致namespace可以看作映射文件的唯一标识

    • 方法名要和sql的id保持一致sql的id可以看作一条sql语句的唯一标识

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.mybatis.mapper.UserMapper">
<!--int insertUser();-->
<insert id="insertUser">调用insertUser方法
	insert into t_user values(null,'admin','123456',23,'男','12345@qq.com')
</insert>
</mapper>

实现数据库相关操作

基操
public class MybatisTest {
        @Test
        public void testInsert() throws IOException {
            //获取核心配置文件输入流(resources选用org.apache.io)
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            //获取SqlSessionFactoryBuilder对象
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder=new SqlSessionFactoryBuilder();
            //获取SqlSessionFactory对象,通过输入流获取
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
            //获取sql的会话对象SqlSession,该对象用于操作数据库
            SqlSession sqlSession = sqlSessionFactory.openSession();
            //获取mapper接口的代理实现类对象
            以上过程是固定的,可以进行封装!!!!
            
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);//传入某类型的class对象,返回该类型的实例化对象
            /*接口不能直接创建对象*/
            //调用接口中的方法
            int result = mapper.insertUser();
            System.out.println("结果"+result);
            //提交事务
            sqlSession.commit();//mybatis 中不会自动提交事务
            sqlSession.close();//关闭会话对象
        }
}
过程详解,通过全类名--》找到映射文件--》找到执行sql的id
    又可以写成(只是为了理解,以后还是使用上述方法)
    //提供sql以及其唯一标识,通过namespace.id找到
    sqlSession.insert("mapper.UserMapper.insertUser")
封装工具类
封装工具类
    public class SqlSessionUtils {
    public static SqlSession getSession(){
        SqlSession sqlSession=null;
        try {
            //获取核心配置文件输入流(resources选用org.apache.io)
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            //获取SqlSessionFactoryBuilder对象
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder=new SqlSessionFactoryBuilder();
            //获取SqlSessionFactory对象,通过输入流获取
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
            //获取sql的会话对象SqlSession,该对象用于操作数据库
            sqlSession = sqlSessionFactory.openSession(true);//自动提交
            //获取mapper接口的代理实现类对象
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sqlSession;
    }
}
封装后的调用
    @Test
        public void testInsert() throws IOException {
            //调用工具类静态方法,获取SqlSession对象
            SqlSession sqlSession = SqlSessionUtils.getSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);         
            int result = mapper.insertUser();
            //sqlSession.commit();设置了自动提交
            System.out.println("结果"+result);
            sqlSession.close();
        }   
报错和注意

报错:org.apache.ibatis.binding.BindingException: Type interface XXX is not known to the MapperRegistry

image-20221111204629284

注意:mybatis 中不会自动提交事务,也就是默认开启事务

//开启自动提交(在openSession中设置参数)
SqlSession sqlSession = sqlSessionFactory.openSession(true);

image-20221112104123773

提交成功id为5的原因:之前没有提交,但是执行了id的自增

配置日志文件(log4j框架)
  • 导入依赖

<!-- log4j日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
  • 配置log4j日志文件(cv),名字必须为log4j.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS}
%m (%F:%L) \n" />
</layout>
</appender>
<logger name="java.sql">
<level value="debug" />
</logger>
<logger name="org.apache.ibatis"><level value="info" />
</logger>
<root>
<level value="debug" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>
配置日志,会打印大于等于当前日志级别的日志信息

image-20221112115931262

关于查询
  • 结果集映射:查询的结果要转化成实体类对象,两个映射只能设置一个

    • resultType:结果类型,设置查询结果要转化为java类型(根据查询的结果的类型,如果是实体类要写全类名

    • resultMap:自定义映射,应用场景如下

      • 字段名和属性名不一致

      • 一对多、多对多的映射

  • 查询实体,结果集映射都为该实体类的全类名

    • 单个返回值类型用改实体类类型

    • 多个就用容器封装,先转化成实体类类型,再放在集合中

将集合中的数据循环输出
List<User> user = mapper.selectAll();
user.forEach(System.out::println);

核心配置文件

Mybatis配置标签必须按照以下顺序

properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?

environments

  • 配置连接数据库的环境

<!--
environments:配置多个连接数据库的环境
属性:
default:设置默认使用的环境的id
开发中只会使用某个环境,所以default属性很重要
-->
<environments default="development">
    <!--
    environment:配置某个具体的环境
    属性:
    id:表示连接数据库的环境的唯一标识,不能重复
    -->
    <environment id="development">
        <!--
        transactionManager:设置事务管理方式
        属性:
        type="JDBC|MANAGED"
        JDBC:表示当前环境中,执行SQL时,使用的是JDBC中原生的事务管理方式,事务的提交或回滚需要手动处理
        MANAGED:被管理,例如Spring
        -->
        <transactionManager type="JDBC"/>
        <!--
        dataSource:配置数据源
        属性:
        type:设置数据源的类型
        type="POOLED|UNPOOLED|JNDI"
        POOLED:表示使用数据库连接池(帮助管理连接)缓存数据库连接,创建连接时直接从连接池中获取即可
        UNPOOLED:表示不使用数据库连接池,每次都是重新获取创建连接的
        JNDI:表示使用上下文中的数据源(了解)
        -->
        <dataSource type="POOLED">
            <!--设置连接数据库的驱动-->
            <property name="driver" value="${jdbc.driver}"/>
            <!--设置连接数据库的连接地址-->
            <property name="url" value="${jdbc.url}"/>
            <!--设置连接数据库的用户名-->
            <property name="username" value="${jdbc.username}"/>
            <!--设置连接数据库的密码-->
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url"
                      value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>

properties

  • 存放连接数据库的数据,以方便维护

  • properties使用步骤

  1. 先在resource中创建properties文件

    • idea创建properties文件的快捷方式

      • 右键项目文件--》new--》resource bundle

    设置键时加上一个表示其功能的前缀,防止重名

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
    jdbc.username=root
    jdbc.password=1212go12
  2. 在核心配置文件中引入properties文件:使用<properties>标签

    此后就可以在当前文件中使用${key}的方式访问value

<properties resource="properties的文件路径"/>

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

typeAliases

  • 类型别名(为某个具体的类型设置一个简短的名字,在mybatis的范围内都可以访问该别名)

typeAlias
<typeAliases>
<!--
typeAlias:设置某个类型的别名
属性:
type:设置需要设置别名的类型
alias:设置某个类型的别名,若不设置该属性,那么该类型拥有默认的别名,即类名且不区分大小写
-->
    <typeAlias type="类路径/全类名"></typeAlias>
</typeAliases>
package
  • 实体类过多的话就通过 实体类所在的包设置别名

  • 设置指定包,包中的所有类型都获得其默认别名【即类名】

  • 不是给该包设置别名

<!--以包为单位,将包下所有的类型设置默认的类型别名 ,即类名且不区分大小写-->
<package name="包路径"/>

mappers

  • 引入mybatis的映射文件

引入单个映射文件
<mapper resource="映射文件的路径"/>
以包的方式引入
要求
  1. mapper接口和映射文件所在的包必须一致

  2. 两者的名字一致

建议
  • 以后的映射文件中放在该包下

原因
  • 最终项目文件都会被加载在类路径中,映射文件和java代码都会被加载到包中同一位置

image-20221112170619331

Mybatis获取参数

两种方式

  • ${}

    • 本质就是字符串拼接

    • 若为字符串类型日期类型的字段进行赋值时,需要手动加单引号

  • #{}(推荐)

    • 本质就是占位符赋值

    image-20221113102715014

    • 字符串类型日期类型的字段进行赋值时, 可以自动添加单引号

    • 可以避免sql注入

    • 赋值后会自动在两侧添加单引号

分类

根据字面量类型
  • 字面量:字符串,基本数据类型以及所对应的包装类,字面量可以理解为要赋予的值

单个
  • 可以使用#{}以任意的名称获取参数的值

  • 建议和参数名保持一致,才知道取得是什么参数

image-20221113102927790

  • ${}相对任意的名称获取参数的值

    • 数字不能单独出现和打头

    • 不能出现特殊字符

    • 获取结果要手动加单引号,字符串没有加单引号会被当成字段(以下所有情况都相同)

    问题

    image-20221113103825060

    解决

    image-20221113104035033

多个

image-20221113104722335

  • 报错

    image-20221113104803259

    • 可用参数为[arg0, arg1, param1, param2],两两一组(两个参数的情况),组内两个参数分别对应传入的第一个参数和第二个参数

    • 有多个参数时,mybatis自动将这些参数放在map集合中,以两种方式存储,通过键名访问值

      • 两种方式

        • arg0,arg1....

        • param1,param2....

      • 可以混合使用

map集合类型的参数

  • 使用途径

    • 需要的参数为多个时,此时可以手动创建map集合,将这些数据放在 map中

    • 通过${}和#{}访问map集合的键就可以获取相对应的值

  • 代码

    • java

      接口中
      /**
       * 通过自定义集合的键访问
       * @param map
       * @return
       */
          User checkByMap(Map map); 
      
      @Test
          public void testCheckBymap(){
              SqlSession session = SqlSessionUtils.getSession();
              UserMapper mapper = session.getMapper(UserMapper.class);
              Map<String,Object>map=new HashMap<>();
              //值定义为Object是因为存入的数据既有字符串又有数字
              map.put("username","admin");
              map.put("password",123456);
              User user = mapper.checkByMap(map);
              System.out.println(user);
              session.close();
          }
    • 映射文件中

      username和password都为自定义map中的键名 
      <select id="checkByMap" resultType="User">
              select * from t_user where username=#{username} and password=#{password};
          </select>

!!!!实体类类型的参数!!!!

  • 通过实体对象的属性名获取属性值(类似键值对)

  • 属性名不等于成员变量名,它和get和set方法有关

  • 代码

    • java

     /**
         * 添加用户
         * @param user
         * @return
         */
        int insertUser(User user);
    
    public void testInsert(){
            SqlSession session = SqlSessionUtils.getSession();
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = new User(null, "zhangsan", "12131", 32, "m", "37701@qq.cmo");
            mapper.insertUser(user);
            session.close();
        }
    • 映射文件

     <insert id="insertUser">
            insert into t_user values(null,#{username},#{password},#{age},#{gender},#{email})
        </insert>

!!!!使用@Param标识参数(推荐)!!!!

  • 在mapper接口方法的参数类型前加@Param(命名参数)注解

  • mybatis中依旧会把参数放在一个map集合中,但是可以通过注解的value属性值访问,相当于map集合中的arg键名替换为value的属性值

User selectByParam(@Param(value = "username") String username, @Param("password")String password);
						//value可以省略
  • 好处

    • 相较于使用[arg0,arg1....],可以设置自己的键名

    • 相较于使用map集合类型,可以不用手动创建map

  • 取代的是arg的键名,依旧可以通过[param1....]方式访问

image-20221113130535229

  • 建议,以后不管是单个还是多个字面量类型,都使用注解

查询功能

根据查询的数据量

一条数据
  • 可以用集合封装

  • 可以返回具体类型(实体类,基本数据类型..)

多条数据
  • 使用集合封装

  • 不能用实体类作为返回值,否则抛出异常

image-20221113133143005

返回值和查询到的数据条数不匹配

根据查询的结果

这里讨论的都是没有对应的实体类类型的情况

单个结果的数据(单行单列)
  • mybatis为java中常用类型设置了类型别名

  • 返回类型可以写

    • 该类型的全类名 如java.lang.Integer

    • 或者该类型的别名 int/Integer(别名不区分大小写)

单条数据用map
<select id="getUserByidtomap" resultType="map">
    select * from t_user where id=#{id}
</select>
Map<String,Object> getUserByidtomap(@Param("id") Integer id);
  • 与实体类相似,一个是通过属性名访问值,另一个是通过键访问值

  • 不同

    • 实体类中的属性是固定的,而map中的键是不固定的

      • 实体类要求字段名和属性名一致

      • map不做要求,查询结果会以字段名为键,以字段的值为值

    • 如果某个字段的值为null

      • 返回类型是map,该值不会放入该map集合中

      • 返回类型是实体类,则查询到的每个结果都会保留

多条数据

如果返回类型用map集合,会报下述错误,因为一条数据对应一个map

image-20221113140815640

解决:

  1. 泛型为map的list集合封装(!!使用较多!!

  2. 使用@Mapkey注解,把查询到的map结果放到一个大的map集合

    1. @Mapkey(value),value为大map集合的键,将查询的某个字段结果的值作为大map的键

    @MapKey("id")
        Map<String,Object> getAllusertomap();
    通过id来标识每一个map集合
    1. 该字段最好是唯一性字段

    2. 返回类型是map

模糊查询

  • 返回结果数量不确定,所以接口方法返回值用list集合封装

  • 错误情况

    image-20221114161409452

    • 报错

    image-20221114161520238

    image-20221114161538107

    • 原因

      image-20221114161622083

      • #{}被解析成占位符?,但在引号中被当成?字符,sql语句中就没有占位符了

      • 使用了#{},就要调用PreparedStatement(预编译)中的方法为占位符赋值,没找到占位符无法赋值

    • 解决

      1. ${}代替#{}

      	<select id="getUserLike" resultType="User">
              select * from t_user where username like '%${condition}%'
          </select>

      1. 利用mysql中字符串拼接函数concat

      image-20221114162934365

      1. 使用 “%”#{}“%”(必须是双引号)

       	<select id="getUserLike" resultType="User">
              select * from t_user where username like "%"#{condition}"%"
          </select>

特殊sql

批量删除(通过字符串拼接)

  • sql中实现方式

    1. where id in(删除的id序列)

    2. where id = id1 or id =id2....

  • java实现方式

    • 传入参数:以逗号为间隔的id字符串,如“id1,id2,id3”

  • 会遇到的错误

    • image-20221115092410476

    • image-20221115093605526

  • 原因

    • #{}会被解析成占位符?,然后给?赋值时会在两测添加单引号

  • 解决:使用${}(目前)

image-20221115093648833

动态设置表名

  • 只能使用${},因为#{}会为表名添加单引号(原因同上)

<select id="getList" resultType="User">
    select * from ${table_name}
</select>
/**
 * 动态设置表名,查询相应表的数据
 * @param table_name
 * @return
 */
List<User> getList(@Param("table_name") String table_name);

!!!添加功能获取自增的主键!!!

  • 多对一/一对多的关系,需要在多方设置一方的主键

  • 实例:添加班级信息

    1. 添加新班级

    2. 获取新班级的id

    3. 通过id为该班级分配学生,即这些学生的班级号为新班级id

  • 功能实现

    • 增删改的返回值都是受影响的行数,所以无法通过设置接口返回值类型实现

    • <insert id="insert" useGeneratedKeys="true" keyProperty="id">
              insert into t_user values(null,#{username},#{password},#{age},#{gender},#{email})
          </insert>
      • keyProperty 将获取到的主键信息存储到传输过来的参数的某个属性中

        • 上述将获取到的主键信息存储到传输过来的实体类对象的id属性

      • useGeneratedKeys 是否使用自增的主键

    • void insert(User user);
      
       @Test
          public void testinsert(){
              SqlSession session = SqlSessionUtils.getSession();
              SpecialSqlMapper mapper = session.getMapper(SpecialSqlMapper.class);
              User user=new User(null,"admin","123456",23,"男","12345@qq.com");
              mapper.insert(user);
              System.out.println(user);
              session.close();
          }
      • 传入一个实体类对象,此时id为null

      • 实体类对象的id属性被sql获取到的主键信息赋值

    • 运行结果

image-20221115184707707

自定义映射resultMap

字段名和属性名不一致

image-20221115200313804

  • 映射原理:把查询出来的字段名通过反射来作为的属性名,然后再获取相对应的属性进行赋值

  • 字段名和属性名不一致无法建立映射关系

    • 以上述为例,因为字段名是emp_id,实体类中没有对应属性名

    • 而实体类中的empId没有被赋值(被赋值的是emp_id这个属性【下面的emp_id仅用于测试】)

image-20221115202155912

  • 解决方式

    1. 给查询出来的字段起别名属性名保持一致

    <select id="selectByID" resultType="Emp">
            select emp_id empId from t_emp where emp_id=#{empId}
     </select>
    1. 在mybatis核心配置文件中设置一个全局配置setting,自动将下划线映射为驼峰

      前提是sql语句中的字段名为下划线,实体类中的属性名为驼峰

<settings>				自动将下划线映射为驼峰             true表示执行,默认为false
        <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings> 

3.使用resultMap自定义映射(推荐)

<resultMap id="empResult" type="Emp">
        <id column="emp_id" property="empID"></id>
        <result column="emp_name" property="empName"></result>
    </resultMap>

<select id="selectByID" resultMap="对应的resultMap的id">
        select * from t_emp where emp_id=#{empId}
    </select>
  • resultMap属性

    • id:用于标识resultMap,供sql语句使用

    • type:返回类型,即查询出的字段要映射的实体类类型的属性名

  • 标签

    • id:设置主键的映射关系

    • result:设置普通字段的映射关系

多对一映射处理

  • 实体类中对一:对应一个对象

private Integer empID;
    private String empName;
    private Integer age;
    private String gender;
    //表示当前员工所对应的部门
    private Dept dept;
  • 处理多对一关系方式

1.级联处理

级联(cascade)在计算机科学里指多个对象之间的映射关系,建立数据之间的级联关系提高管理效率

  • 查询出的dept_id和dept_name字段对应的是实体类对象dept对象的属性

  • 通过对象名.属性名的方式为其赋值

   <resultMap id="edMapOne" type="Emp">
        <id column="emp_id" property="empID"></id>
        <result column="emp_name" property="empName"></result>
<!--        级联处理:emp对象的dept属性对应一个dept对象-->
        <result column="dept_id" property="dept.deptId"></result>
        <result column="dept_name" property="dept.deptName"></result>
    </resultMap>
2.association标签
  • 用于处理多对一的映射关系,即处理实体类类型的属性

<resultMap id="edMap" type="Emp">
    <id column="emp_id" property="empID"></id>
    <result column="emp_name" property="empName"></result>
    <association property="dept" javaType="Dept">
        <id column="dept_id" property="deptId"></id>
        <result column="dept_name" property="deptName"></result>
    </association>
</resultMap>
  • association属性

    • proerty:需要处理的实体类中的属性的属性名

    • javaType:设置该属性的类型,即该属性对应的对象类型

  • 标签

    • id:设置主键的映射关系

    • result:设置普通字段的映射

3.分步查询
  • 以员工表和部门表为例,多对一

    • 先查出员工信息以及其对应的部门id(下一步的查询条件

    • 通过部门id查询部门信息

    • 将查询的部门信息结果再赋值给emp中的dept对象

  • association属性

    • select:下一步sql的唯一标识(namespace.sql的id)

      • 先找到对应的映射文件

      • 再去找相应的sql语句

      • 快捷方式:在对应的接口方法中--》方法名点击右键--》copy reference

    • proerty:当前需要处理的实体类中的属性的属性名

    • column:把当前查询出的某个字段作为下一步的查询条件

<!--分步查询-->
    <resultMap id="edStepMap" type="Emp">
        <id column="emp_id" property="empID"></id>
        <result column="emp_name" property="empName"></result>
        <association property="dept" 
                     select="mapper.DeptMapper.selectEmpAndDeptStep2" 
                     column="dept_id">
        </association>
    </resultMap>
    <select id="selectEmpAndDeptStep1" resultMap="edStepMap">
        /*分步查询,只要查询员工表*/
        select * from t_emp where emp_id=#{ID}
    </select>
EmpMapper中
/**
     * 分步查询员工及其对应部门的第一步
     * @param id
     * @return
     */
    Emp selectEmpAndDeptStep1(@Param("ID") int id);

DeptMapper中
/**
     * 分步查询员工及其对应部门的第二步
     * @return
     */
    Dept selectEmpAndDeptStep2(@Param("deptID") int deptID);
  • 分步查询的优势:延迟加载(懒加载)

    • 好处

      • 获取什么数据,就只会执行对应sal语句

        image-20221117120014618

        没有获取部门相关信息,因此没有执行部门查询的sql

      • 减少内存的消耗

    • 开启延迟加载,在核心配置文件中设置全局配置

      • lazyLoadingEnabled:延迟加载的全局开关。默认为false(不开启)

      • aggressiveLazyLoading:默认为false

        • false按需加载:只执行需要查询出来的属性对应的sql语句

        • true完整加载:全部属性都查询出来,执行所有有关的sql语句

      • association的属性fetchType:在开启延迟加载的环境条件下,设置当前分步查询是否执行延迟加载

        • 没有设置该属性且在开启延迟加载的环境条件下,就执行延迟加载

        • eager:立即加载

        • lazy:延迟加载

一对多映射处理

  • 实体类中对多:对应一个集合

private  Integer deptId;
    private  String deptName;
    private List<Emp> emps;
1.connection标签
  • 处理一对多的映射关系,即处理实体对象集合的属性

  • 属性

    • property

    • ofType:处理集合中存储的数据类型(即相应的实体类类型

  • 标签

    • id

    • result

 <resultMap id="deptAndEmpMap" type="dept">
     <id column="dept_id" property="deptId"></id>
     <result column="dept_name" property="deptName"></result>
     
     <collection property="emps" ofType="Emp">
         <id column="emp_id" property="empID"></id>
         <result column="emp_name" property="empName"></result>
         <result column="age" property="age"></result>
         <result column="gender" property="gender"></result>
     </collection>
 </resultMap>
2.分步查询

以员工表和部门表为例,一对多

  • 先查出部门信息

  • 通过部门id(下一步的查询条件)查询到对应的员工集合

  • 将查询的员工集合再赋值给dept中的emps集合

DeptMapper中
	<resultMap id="deptAndEmpStep" type="dept">
        <id column="dept_id" property="deptId"></id>
        <result column="dept_name" property="deptName"></result>
        <collection property="emps"
                    select="mapper.EmpMapper.selectDeptAndEmpStep2"
                    column="dept_id"></collection>
    </resultMap>

 	<select id="getDeptAndEmpByDeptId" resultMap="deptAndEmpMap">
        select * from t_dept d left join
        t_emp e on d.dept_id=e.dept_id
        where d.dept_id=#{deptId}
    </select>

EmpMapper中
<select id="selectDeptAndEmpStep2" resultMap="empResult">
        select * from t_emp where dept_id=#{deptId}
    </select>

小结

实体类中一对多/多对一处理
  • 对一:对应一个对象

  • 对多:对应一个集合

association
用途
  • 多对一映射处理

属性
  • proerty:需要处理的实体类中的属性的属性名

  • javaType:设置该属性的类型,即该属性对应的对象类型

  • 分步查询中

    • fetchType:在开启延迟加载的环境条件下,设置当前分步查询是否执行延迟加载

    • select:下一步sql的唯一标识(namespace.sql的id)

    • column:把当前查询出的某个字段作为下一步的查询条件

标签
  • id:设置主键的映射关系

  • result:设置普通字段的映射

注意connection用于一对多映射处理,除了ofType和association中的javaType属性不同之外,其他的属性和子标签基本相同

  • 也就是connection返回类型是ofType,association返回类型是javaType,写错会报如下异常

image-20230331175123687

分步查询
  • 一对多和多对一的column属性(查询条件)都为一的主键

  • 可以延迟加载,节省内存

动态sql

  • 用于动态拼接sql语句

  • 解决特定的参数值

    • null没有向服务器传输某个请求参数,而去获取这个参数,获取结果为null

      • 比方说没有选中单选框,就没有向服务器传输相应的value

    • 空字符串:提交空文本框

if

  • 属性

    • test:判断条件

  • 作用:判断标签中的内容是否有效,有效则拼接到sql语句中

<select id="getEmpByCondition" resultMap="empMap">
    select * from t_emp
    /*xml文件中不支持&&;,用and或者是&amp;&amp;代替*/
    where <if test="age!='' and age!=null">age=#{age}</if>
     <if test="age!='' and age!=null">and gender=#{gender}</if>
     <if test="age!='' and age!=null">and emp_name=#{empName}</if>
</select>

where

  • 问题

    • sql语句中where直接连接and报错(第一个if条件不满足而后续的if条件满足)

    image-20221118201737860

    • sql语句中只有where没有条件(所有的if条件不成立)

image-20221118201912622

  • 解决

1.使用恒成立条件
  • where后接上类似于1=1的恒成立条件

  • 其他的拼接条件要加上and

<select id="getEmpByCondition" resultMap="empMap">
    select * from t_emp
    where 1=1 <if test="age!='' and age!=null">age=#{age}</if>
     <if test="age!='' and age!=null">and gender=#{gender}</if>
     <if test="age!='' and age!=null">and emp_name=#{empName}</if>
</select>
2.使用where标签
  • 动态生成where关键字:如果where标签中

    • 有条件成立,会自动生成where关键字

    • 没有条件成立,不生成where关键字

  • 可以去掉内容多余的and,但是内容后多余的and无法去掉

<select id="getEmpByCondition" resultMap="empMap">
    select * from t_emp
    <where>
    <if test="age!='' &amp;&amp; age!=null">and age=#{age}</if>
     <if test="gender!='' and gender!=null">and gender=#{gender}</if>
     <if test="empName!='' and empName!=null">and emp_name=#{empName}</if>
    </where>
</select>
3.使用trim标签
  • 截取:标签中有内容,在标签中的内容前/后 添加/删除内容

  • 属性

    • prefix:在内容加上内容

    • prefixOverrides:在内容去掉某些内容

    • suffix:在内容加上内容

    • suffixOverrides:在内容去掉某些内容

image-20221118204353565

  • 没有条件成立的情况下,没有添加where关键字

image-20221118204727325

choose(父标签)、when、otherwise

  • 整体是switch结构

  • when

    • 类似于case,where后至多有一个when条件成立

    • 条件前不需要写and,因为最多只有一个when条件成立

  • otherwise

    • 类似于default,前面的when都不成立,则该条件成立

    • 最多设置一个(在最后)

<select id="getEmpByChoose" resultMap="empMap">
    select * from t_emp
   <where>
   <choose>
        <when test="age!='' &amp;&amp; age!=null"> age=#{age} </when>
        <when test="gender!='' and gender!=null"> gender=#{gender} </when>
        <when test="empName!='' and empName!=null"> emp_name=#{empName}</when>
        <otherwise>emp_id=2</otherwise>
   </choose>
   </where>
</select>

foreach

批量添加(利用集合)
  • 传入参数集合/数组

    • 如果没有给参数设置注解,mybatis依旧把它放在一个map集合中

      • 集合以list为键,数组以array为键

      • 以当前参数为值

    • 设置了注解直接用注解名访问

  • 属性

    • collection:用于设置当前需要循环的集合或数组,名称要么为@Param的内容要么就是默认值

      image-20230330220356564

    • item:表示数组/集合中的每个元素

    • separator:设置每次循环数据之间的分隔符,而且会自动在分隔符前后加空格

  • 注意事项

    • sql中的条件不是#{实体类属性名},而是#{元素名.属性名}

    • 如果没有设置分隔符,数据库会把多个参数当作是一个整体

image-20230330220707753

批量删除(利用数组)
  • sql中

    • 利用in

      <delete id="deleteMore">
          delete from t_emp 
          where emp_id in <foreach collection="ids" item="id" open="(" close=")" separator=",">
          #{id}
      </foreach>
      </delete>
      
      <delete id="deleteMore">
          delete from t_emp 
          where emp_id in (<foreach collection="ids" item="id" separator=",">
          #{id}
      </foreach>)
      </delete>
    • 利用or

    <delete id="deleteMore">
        delete from t_emp 
        where  
        <foreach collection="ids" item="id"  separator="or">
        emp_id=#{id}
    	</foreach>
    </delete>
  • 属性补充

    • open:当前数据以什么开始

    • close: 结束

    • index:当前循环的索引(目前没什么用)

sql标签

  • 用于记录常用的sql片段,然后通过include引入该片段

  • 属性

    • id:该sql标签的唯一标识

  • include

    • 属性:refid,值为需要应用的sql标签id

Mybatis的缓存

  • 将查询出来的数据进行缓存,便于下次查询相同数据

一级缓存

  • 默认开启

  • SqlSession级别

    • 通过同一个SqlSession查询的数据会被缓存

    • 下次通过同一个SqlSession查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问

    image-20221120115842203

  • 上图案例:

    • 同一个SqlSession对象获取的mapper

      • 获取相同数据总共只执行了一次sql

      • 获取同数据执行了多次sql

    • 不同SqlSession对象获取相同数据要执行多次sql

  • 一级缓存失效的四种情况

    1. 不同的sqlSession对象

    2. 获取不同数据/查询条件不同

    3. 同一个sqlsession中,两次相同查询之间

      1. 进行了数据更新(dml)

      2. 手动清空了缓存:Sqlsession对象调用clearCache方法(手动清空一级缓存)

二级缓存

  • 是SqlSessionFactory级别

    • 通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存

    • 此后若再次执行相同的查询语句,结果就会从缓存中获取

  • 开启条件

    1. 在核心配置文件中,设置全局配置属性cacheEnabled="true",默认为true,所以不需要手动设置

    2. 在映射文件中设置标签 <cache/>

      image-20221120144343910

    3. 二级缓存必须在SqlSession关闭或提交之后有效

      1. 未保存之前的数据会先存在一级缓存(Sqlsession对象中)

      2. 关闭或提交后数据才由一级缓存传到二级缓存

    4. 查询的数据所转换的实体类类型必须实现序列化的接口,否则会报异常

    image-20221120143617808

  • 缓存命中率

    image-20221120143845367

    • 只有二级缓存才会在控制台输出

    • 与数据量有关

    • 只要不为零就说明当前查询的数据有缓存

      • 手动提交/关闭前一个SQL session对象不为零

      • 自动提交为零(测试后发现的)

  • 失效情况:两次相同查询中进行数据更新

二级缓存相关配置(了解)

在mapper配置文件中添加的cache标签可以设置一些属性:

  1. eviction属性:缓存回收策略,默认的是 LRU。

    1. LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象

    2. FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。

    3. SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

    4. WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

  2. flushInterval属性:刷新间隔,单位毫秒 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新

  3. size属性:引用数目,正整数代表缓存最多可以存储多少个对象,太大容易导致内存溢出

  4. readOnly属性:

    1. true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了 很重要的性能优势。

    2. false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。

MyBatis缓存查询的顺序

  • 大范围到小范围

  1. 先查询二级缓存,因为二级缓存中可能会有其他程序(其它sqlSession)已经查出来的数据,可以拿来直接使用。

  2. 如果二级缓存没有命中,再查询一级缓存(如果没有关闭和提交sqlsession,则该一级缓存的数据在对应的二级缓存中不存在)

  3. 如果一级缓存也没有命中,则查询数据库

顺序:查询二级缓存--》查询没有提交或者关闭的sqlsession对象对应的一级缓存--》查询数据库

SqlSession关闭之后,一级缓存中的数据会写入二级缓存

整合第三方缓存(了解)

  • 针对于二级缓存

  • 有兴趣去看ssm文档

Mybatis逆向工程

image-20221121201448900

  • 只能支持单表操作,多表联查需要的实体类对象属性或者实体类集合属性需要自己创建

①添加依赖和插件
  • 驱动在依赖和插件中都要设置,如果依赖中为配置则会报一下异常

image-20221123151843744

.

类未找到--》实际上就是少了jar包--》可能是没有添加相应的maven依赖

  • 报如下错误是因为核心配置文件中,mapper映射文件的路径配错了

image-20221123152729798

image-20221123152919011

.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>mybatis_mbg</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- log4j日志 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
    </dependencies>
    <!-- 控制Maven在构建过程中相关配置 -->
    <build>
        <!-- 构建过程中用到的插件 -->
        <plugins>
            <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.0</version>
                <!-- 插件的依赖 -->
                <dependencies>
                    <!-- 逆向工程的核心依赖 -->
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.3.2</version>
                    </dependency>
                    <!-- MySQL驱动 -->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.29</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>
②创建MyBatis的核心配置文件
③创建逆向工程的配置文件
  • 文件名必须是:generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">爆红勿理			
<generatorConfiguration>
    <!--
    targetRuntime: 执行生成的逆向工程的版本
    	MyBatis3Simple: 生成基本的CRUD(清新简洁版)
    	MyBatis3: 生成带条件的CRUD(奢华尊享版):可以生成对单表的所有操作
    -->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 数据库的连接信息 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mybatis?
serverTimezone=UTC"
                        userId="root"
                        password="1212go12">
        </jdbcConnection>
        <!-- javaBean的生成策略-->
        <javaModelGenerator targetPackage="com.study.pojo"实体类包的位置,我的是pojo
                            targetProject=".\src\main\java">
            .表示访问当前目录
            ..表示访问上级目录
            <property name="enableSubPackages" value="true" />如果为false的话com.atguigu.mybatis.pojo就被解析成单个目录,不会被解析成多个子目录
            <property name="trimStrings" value="true" />把当前字段前后的空格去掉来生成实体类的属性
        </javaModelGenerator>
        <!-- SQL映射文件的生成策略 -->
        <sqlMapGenerator targetPackage="com.study.mapper"创建mapper接口包的路径
                         targetProject=".\src\main\resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>
        <!-- Mapper接口的生成策略 -->
        <javaClientGenerator type="XMLMAPPER"
                             targetPackage="com.atguigu.mybatis.mapper" targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>
        <!-- 逆向分析的表 -->
        <!-- tableName设置表名,如果设置为*号,可以对应所有表,此时不写domainObjectName -->
        <!-- domainObjectName属性指定生成出来的实体类的类名 -->
        <table tableName="t_emp" domainObjectName="Emp"/>
        <table tableName="t_dept" domainObjectName="Dept"/>
    </context>
</generatorConfiguration>
④执行MBG插件的generate目标
  • 双击该插件执行,相应的包会生成

  • 多次生成之后

image-20221123121200512

奢华版注意事项
  • example为条件

  • 插入详解

    • int insert(Emp record);
      如果没有给某个字段赋值,则为其赋值为null(会覆盖其默认值)
    • int insertSelective(Emp record);
      如果没有给某个字段赋值,则该字段赋予默认值
  • 修改详解

    • int updateByExample(@Param("record") Emp record, @Param("example") EmpExample example);
      int updateByPrimaryKey(Emp record);
      如果某个属性值为null或者没有设置该属性,则会将对应字段修改为null
    • int updateByExampleSelective(@Param("record") Emp record, @Param("example") EmpExample example);
      int updateByPrimaryKeySelective(Emp record);
      如果某个属性值为null或者没有设置该属性,则不会修改该字段
  • 查询注意事项

    • 这里的查询遵循qbc(Query By Criteria根据条件查询)

    • 查询所有(没有任何限制条件)

    //查询所有
    List<Emp> emps = mapper.selectByExample(null);
    • 根据主键查询

    Emp emp = mapper.selectByPrimaryKey(2);
    • 根据条件查询

      • 先创建条件对象的执行对象example

      • 然后通过该执行对象创建条件对象createCriteria

      • 调用相应方法(见名知意)

        • 方法名都是以and开头,紧跟着字段名

        • 然后见名知意

          • greater大于less小于equalto等于

          • like,between等等同sql含义

      • 查询多个and条件就example.createCriteria.方法1.方法2

      example.createCriteria().andAgeBetween(20,30).andEmpNameLike("%三%");

      image-20221123154914982

      • 而查询or条件就另外调用or方法然后后面跟多个条件

        1. example.createCriteria.方法1.方法2....

        2. examp.or.方法1.方法2....

      example.createCriteria().andAgeBetween(20,30).andEmpNameLike("%三%");
      example.or().andEmpNameLike("李四");

      image-20221123155350163

注意(b站评论)

逆向工程,如果生成的pojo实体类和mapper出现了多个重复的文件(如xx.java.1文件),原因是:其他数据库中存在相同的表名,mybatis自动生成时串库了。 解决方法:在逆向工程的配置文件中添加<property name="nullCatalogMeansCurrent" value="true"/>

分页插件

  • sql实现分页功能

    • limit index,pageSize

      • index:当前页的起始索引【(pageSize-1)* pageNum

        • 忘记了的话自己枚举

      • pageSize:每页显示条数(已知)

      • pageNum:当前页的页码(已知)

      • count:总记录数(通过数据库查询)

      • totalPage:总页数

      totalPage=count/pageSize
      if(count%pageSize!=0){
          totalPage+=1;
      }
  • 大致效果(中间的是导航分页)

    首页 上一页 2 3 4 5 6下一页尾页

    • 分为 首页和上一页 导航分页 下一页和尾页三部分

      • 页码为1首页和上一页不展示

      • 尾页下一页和尾页不展示

        • 获取尾页totalPage.

      • 导航分页要判断头两页和尾两页等等情况

  • 分页插件实现

    • 不需要手动加limit

    • 自动完成分页功能

环境准备

①添加依赖
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>
②配置分页插件(核心配置文件中)
<plugins>
<!--设置分页插件-->
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>

具体使用

  • 查询功能之前开启分页功能

PageHelper.startPage(2,4);
List<Emp> emps = mapper.selectByExample(null);
emps.forEach(System.out::println);
  • 会执行两条sql

    • 先查出总数

    • 再进行分页查询

    image-20221125162114853

  • 获取分页相关的数据

    1. PageHelper.startPage(int pageNum, int pageSize))的返回值为page对象,存放一部分数据

      1. image-20221125162702192

      2. 以及当前展示的分页数据

      3. 参数

        1. pageNum:当前页的页码

        2. pageSize:每页显示的条数

    2. PageInfo<T> pageInfo = new PageInfo<>(List<T> list, int navigatePages)获取PageInfo对象查询功能之后获取相关数据)

      1. 参数

        1. list:查询出来的集合,即分页之后展示的数据

        2. navigatePages:导航分页的页码数

      2. 数据

        image-20230513201402644

        1. pageNum:当前页的页码 pageSize:每页显示的条数

        2. size:当前页显示的真实条数 total:总记录数 pages:总页数

        3. prePage/nextPage:上/下一页的页码 isFirstPage/isLastPage:是否为第一页/最后一页 hasPreviousPage/hasNextPage:是否存在上一页/下一页

        4. navigatePages:导航分页的页码数(有几页) navigatepageNums:导航分页的页码,[1,2,3,4,5](哪些页)

        5. 以及page对象的数据 list:当前页的数据

Spring

Spring基础知识

  • 是最受欢迎的企业级 Java 应用程序开发框架,使用Spring 框架来创建性能好、易于测试、可重用的代码

  • 轻量级的框架,其基础版本只有 2 MB 左右的大小。

  • Spring 框架的核心特性是可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO编程模型来促进良好的编程实践

SpringFramework相关

概念
  • Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都是以 Spring Framework为基础的

五大功能模块

image-20230227213147872

特性
  • 非侵入式:使用 Spring Framework 开发应用程序时,Spring对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序时结构清晰、简洁优雅

  • 控制反转IOC——Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源变成环境将资源准备好 ,我们享受资源注入(把对象的创建权、控制权反转给资源本身,程序员被动获取该对象)

    • 配置文件配置的是什么对象,spring就为我们提供什么对象,降低对象和对象之间的依赖关系

  • 面向切面编程AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功能

    • 面向对象是纵向继承机制,可以封装连续执行的代码

    • 面向切面是横向抽取,是对面向对象的补充(例子:将非连续的事务执行操作横向抽取)

  • 容器:Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率

  • 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭建超大型复杂应用系统

  • 声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现(我要干什么告诉框架就行了,具体实现过程由框架实现)

    • 编程式:我想干什么那这个过程就需要自己实现

  • 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(跟spring框架不同源的,如mybatis)。而且Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现

IOC

IOC容器相关概念

IOC思想
  • IOC:Inversion of Control反转控制

①获取资源的传统方式

  • 必须清楚了解资源创建整个过程中的全部细节且熟练掌握

  • 在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率

②反转控制方式获取资源

  • 不必关心资源创建过程的所有细节

  • 反转控制的思想完全颠覆了应用程序组件获取资源的传统方式

    • 反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率,这种行为也称为查找的被动形式

③DI

  • Dependency Injection依赖注入(当前需要/依赖什么对象,spring就帮助我们注入什么对象)

    • 注入:为所依赖的对象进行赋值

    • 依赖注入:为spring中所管理的对象中的属性进行赋值

  • DI 是 IOC 的另一种表述方式

    • 即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入

    • 相对于IOC而言,这种表述更直接

  • 结论:IOC 就是一种反转控制的思想, 而DI是对 IOC 的一种具体实现

IOC容器在Spring中的实现
  • Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件叫做 bean。在创建bean 之前,首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式:

①BeanFactory

  • 这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用

  • 工厂设计模式:将创建对象的过程进行隐藏,直接提供需要的对象(如获取数据库连接对象)

  • ioc容器通过反射工厂设计模式帮助我们创建对象

    • 大部分通过反射创建对象都需要无参构造,如果只有有参构造而没有无参构造会报异常

    org.springframework.beans.BeanInstantiationException: Failed to instantiate [pojo.student]: No default constructor found; nested exception is java.lang.NoSuchMethodException:pojo.student.<init>()【指的就是该类型的无参构造】

②ApplicationContext

  • BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用ApplicationContext 而不是底层的 BeanFactory

    image-20230228152228394

  • ClassPathXmlApplicationContext

    • 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象

  • FileSystemXmlApplicationContext

    • 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象

    • 用的较少,因为java工程以后打成jar包,发送给其他的电脑,磁盘路径对应的路径会找不到

  • ConfigurableApplicationContext

    • ApplicationContext 的子接口

    • 包含一些扩展方法refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力

  • WebApplicationContext

    • 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域

基于xml管理bean(组件)

环境准备与测试
  1. 创建Maven Module

  2. 引入依赖

    <dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
    <groupId>org.springframework</groupId>项目组织的标识符
    <artifactId>spring-context</artifactId>项目的标识符
    <version>5.3.1</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
        
    <version>4.12</version>
    <scope>test</scope>
    </dependency>
    </dependencies>
  3. 创建类

  4. 创建spring的配置文件

image-20230228154116774

  1. 在Spring的配置文件中配置bean

    • 一个bean对应一个唯一的对象

    • 通过bean标签配置IOC容器所管理的bean

    • 属性

      • id:设置bean的唯一标识

      • class:设置bean所对应类型的全类名

    • 配置HelloWorld所对应的bean,即将HelloWorld的对象交给Spring的IOC容器管理

<bean id="helloworld" class="com.atguigu.spring.bean.HelloWorld"></bean>
  1. 创建测试类测试

    • resource和java目录最终会被加载到类路径下,所以可以直接访问相关资源

@Test
public void test(){
    //获取ioc容器
    ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
    //获取容器中的bean
    helloword bean = (helloword)ioc.getBean("helloFirst");
    bean.sayHello();
}
!!!获取bean的三种方式!!!
  1. 通过类型获取bean

    • ioc.getBean(student.class)

    • 注意:根据类型获取bean时

      • 如果设置两个同类型的bean,会报错

        image-20230301212452196

      image-20230301212549742

      • 没有相应类型的bean也会报错

        image-20230301212815107

      • 所以这种方式要求ioc容器中有且仅有一个类型匹配的bean

  2. 根据id获取

  3. 根据类型和id

    • ioc.getBean(id,类型)

    • 同一类型的多个也可以获取

  • 开发中用1比较多,一个类型的bean只用配置一次,因为同一类型可以配置单例也可以多例(通过scope来设置)

  • !!!配置文件中bean的class属性只能写有无参构造的具体的类型,因为反射需要用到无参构造,没有无参构造的类接口等无法实现!!!

  • 扩展

    • 如果bean的类型继承了父类或者实现了接口,可以通过父类型或接口类型获取对象

      • 前提bean的类型唯一,一个接口实现了多个类就无法获取

    • 根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到

!!!常用的依赖注入!!!
  • 依赖注入实际上就是为类中的属性进行赋值(类依赖于属性)

!!!setter注入!!!
  • 为bean属性赋值

    <bean id="studentOne" class="com.atguigu.spring.bean.Student">
        <property name="id" value="1001"></property>
        <property name="name" value="张三"></property>
        <property name="age" value="23"></property>
        <property name="sex" value="男"></property>
    </bean>
    • property标签:通过组件类的setXxx()方法给组件对象设置属性

    • name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关)

    • value属性:指定属性值

构造器注入
<bean id="studentTwo" class="com.atguigu.spring.bean.Student">
    <constructor-arg value="1002"></constructor-arg>
    <constructor-arg value="李四"></constructor-arg>
    <constructor-arg value="33"></constructor-arg>
    <constructor-arg value="女"></constructor-arg>
</bean>
  • 有多少给constructor-arg标签就对应多少个参数的有参构造

  • constructor-arg标签还有两个属性可以进一步描述构造器参数:

    • index属性:指定参数所在位置的索引(从0开始)

    • name属性:指定参数名

!!!赋值相关!!!
特殊值处理
  • 字面量赋值

    • 字面量:基本数据类型以及其包装类,还有String类型

    • 直接使用value属性赋值即可(常用)

      • 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量

    • 也可以使用value子标签

image-20230303214209475

  • null值

    • 不能用value赋值,因为value是用于给字面量赋值,会被null当作字符串"null"

    • 在property标签中嵌套一个null标签

    <property name="name">
    	<null />
    </property>

  • xml实体

    • 小于号在XML文档中用来定义标签的开始,不能随便使用

    • 解决方案一:使用XML实体来代替

      • 场景的xml实体

        • <: &lt;

        • >:&gt;

    <property name="expression" value="a &lt; b"/>
  • CDATA节/区

    • 解决方案二:使用CDATA节

      • <![CDATA[内容]]>

      • CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据,当中的内容会原样解析

      • XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析,CDATA节中写什么符号都随意

      • cdata是xml中特殊的标签,不能直接写在属性中,只能以标签的形式编写,所以这里要采用value标签赋值

    <property name="expression">
        <value><![CDATA[a < b]]></value>
    </property>
类类型属性赋值
  • 也包括接口类型

  • 引用外部的bean

    • 使用ref,用途:引用ioc容器中的某个bean的id,找到该id对应的bean所对应的对象进行赋值

      <bean id="studentFour" class="com.atguigu.spring.bean.Student">
          <property name="id" value="1004"></property>
          <property name="name" value="赵六"></property>
          <property name="age" value="26"></property>
          <property name="sex" value="女"></property>
          <!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 -->
          <property name="clazz" ref="clazzOne"></property>
      </bean>
      <bean id="clazzOne" class="com.atguigu.spring.bean.Clazz">
          <property name="clazzId" value="1111"></property>
          <property name="clazzName" value="财源滚滚班"></property>
      </bean>
      • 如果写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException:Cannot convert value of type 'java.lang.String' to required type'com.atguigu.spring.bean.Clazz' for property 'clazz': no matching editors or conversionstrategy found

      • 意思是不能把String类型转换成我们要的Clazz类型,说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值

  • 级联属性赋值

    • 不能直接通过类类型.属性的方式进行赋值,否则会报错,因为该对象还没有被创建出来

    image-20230304211847020

    • 有两种方式

      1. 先引用外部bean,然后通过级联的方式(成员对象.属性)赋值(相当于先创建对象,然后通过级联对该对象的属性赋值)

      2. 直接先在实体类中把类类型的成员实例化

    • 级联方式不常用,使用的时候要保证该类类型的对象已经被创建了

  • 内部bean

    <bean id="studentFour" class="com.atguigu.spring.bean.Student">
    <property name="clazz">
        <bean id="clazzInner" class="com.atguigu.spring.bean.Clazz">
            <property name="clazzId" value="2222"></property>
            <property name="clazzName" value="远大前程班"></property>
        </bean>
    </property>
    </bean>
    • 在一个bean中再声明一个bean就是内部bean

    • 内部bean只能用于给属性赋值不能在外部通过IOC容器获取,因此可以省略id属性

数组类型赋值
  • 通过array标签,然后为数组中的各个元素赋值

  • 根据数组存储元素的类型

    • 字面量类型,使用value赋值

    • 类类型,使用ref赋值

<bean id="studentFour" class="com.atguigu.spring.bean.Student">
    <property name="hobbies">
    <array>
        <value>抽烟</value>
        <value>喝酒</value>
        <value>烫头</value>
    </array>
    </property>
</bean>
集合类型属性赋值
list集合
  1. 内部list集合

    • 通过list标签,然后为集合中的各个元素赋值(和数组相似)

    • 根据集合中存储元素的类型

      • 字面量类型,使用value赋值

      • 类类型,使用ref赋值

  2. 引用list集合类型的bean,需要引入util约束

    <bean id="clazzTwo" class="com.atguigu.spring.bean.Clazz">
        <property name="clazzId" value="4444"></property>
        <property name="clazzName" value="Javaee0222"></property>
        <property name="students" ref="students"></property>
    </bean>
    <!--list集合类型的bean-->
    <util:list id="students">
        <ref bean="studentOne"></ref>
        <ref bean="studentTwo"></ref>
        <ref bean="studentThree"></ref>
    </util:list>
    • 不能用以下形式,因为只是给ArrayList这个赋值了,不能作为集合类型给集合对象赋值

      <bean class="ArrayList">
          <ref bean="studentOne"></ref>
          <ref bean="studentTwo"></ref>
          <ref bean="studentThree"></ref>
      </bean>
map集合
  1. 使用map标签(内部map),map标签属性

    • key(字面量),key-ref(引用bean)为map的键赋值

    • value,value-ref为map键赋值

    image-20230305193713766

    <property name="teacherMap">
        <map>
            <entry>
                <key>
                	<value>10010</value>
                </key>
                <ref bean="teacherOne"></ref>
            </entry>
            <entry>
                <key>
                	<value>10086</value>
                </key>
                <ref bean="teacherTwo"></ref>
            </entry>
        </map>
    </property>
  2. 引入外部map,使用util:map标签

    image-20230305194150087

    <!--map集合类型的bean-->
    <util:map id="teacherMap">
        <entry>
            <key>
                <value>10010</value>
            </key>
        <ref bean="teacherOne"></ref>
        </entry>
        <entry>
            <key>
            	<value>10086</value>
            </key>
        <ref bean="teacherTwo"></ref>
        </entry>
    </util:map>
p命名空间
  • 使用p时必需要有p命名空间,即当前对应的约束(alt+insert就可以引入了)

  • 是通过bean标签中的属性来为来为bean赋值

    • p:属性名 可以为属性赋值字面量

    • p:属性名-ref 为属性赋值一个bean

    <bean id="studentSix" class="com.atguigu.spring.bean.Student"
    p:id="1006" p:name="小明" p:clazz-ref="clazzOne" p:teacherMap-ref="teacherMap"></bean>
  • 循环注入会报错,也就是一个bean引入另一个bean,而被引用的这个bean又引用了当前的bean(闭环)

管理数据源和引入外部属性文件
管理数据源
  • 数据源是管理数据库连接

  • 管理数据源对象直接使用bean标签即可,dataSource是一个接口,bean中class属性要求是有无参构造的类才能创建对应的对象,所以class属性填写dataSource的实现类DruidDataSource

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"></bean>
  • 如果遇到这个异常,就说明数据库连接的密码写错了

image-20230305203755467

!!!引入外部属性文件!!!
  • 使用context约束,引入外部属性文件

    • context:property-placeholder专门用于引入properties文件

    • 引入之后就可以通过${key}来访问对应的value

<context:property-placeholder location="jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
</bean>
bean的作用域和生命周期
作用域
  • 通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表

    image-20230305211556050

    • singleton就是单例,表示获取该bean对应的对象都是同一个,大部分情况使用单例

    • prototype(原型),指多例,表示每次获取该bean对应的对象都是一个新的对象

  • 在WebApplicationContext环境下还有另外两个作用域(但不常用)

image-20230305211620929

!!!生命周期
具体的生命周期过程
  1. bean对象创建(调用无参构造器)

  2. 给bean对象设置属性(依赖注入)

  3. bean对象初始化之前操作(由bean的后置处理器负责)

  4. bean对象初始化(需在配置bean时使用init-method属性指定初始化方法)

    public void initMethod(){
    System.out.println("生命周期:3、初始化");
    }
    public void destroyMethod(){
    System.out.println("生命周期:5、销毁");
    }
    <bean class="com.atguigu.bean.User" scope="prototype" init-method="initMethod"
    destroy-method="destroyMethod">
    	....
    </bean>
  5. bean对象初始化之后操作(由bean的后置处理器负责)

  6. bean对象就绪可以使用

  7. bean对象销毁,只有在ioc容器关闭的时候才会调用destory方法(需在配置bean时使用destroy-method属性指定销毁方法)

  8. IOC容器关闭

    • ApplicationContext接口中无close方法

    • 可以使用ConfigurableApplicationContext接口或者是ApplicationContext接口的实现类调用close

不同作用域对声明周期的影响
  • 单例:在获取ioc容器时就创建好对象(对应声明周期的创建、设置属性、初始化步骤),之后获取对象直接从容器中取

  • 多例:获取ioc容器并不会创建对象(对应声明周期的创建、设置属性、初始化步骤),而是在获取对象的时候创建,因为每次要获取新的对象,关闭容器时,bean的销毁不执行

后置处理器
  • bean初始化前后添加额外的操作

  • 需要实现BeanPostProcessor接口配置到IOC容器

    • 接口方法中的bean对象就是ioc容器所管理的bean

package com.atguigu.spring.process;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
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;
        }
}
<!-- bean的后置处理器要放入IOC容器才能生效 -->
<bean id="myBeanProcessor" class="com.atguigu.spring.process.MyBeanProcessor"/>
  • bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行

FactoryBean
  • 是一个接口,需要有实现类,其中有三个方法

    • getObject()将一个对象交给ioc管理

    • getObjectType()设置提供对象的类型

    • isSingleton()所提供的对象是否单例

  • 配置一个FactoryBean类型的bean,通过getObject()方法的返回值类型获取bean时,可以获取到该方法返回的对象

    • 通过FactoryBean类型获取bean会获取FactoryBean对象

    • 没配置getObect方法的返回类型对应的bean,也可以通过该返回类型去获取到返回类型对象

      • 实际上是将FactoryBean中getObject()方法的返回值交给ioc容器管理

    • 如果配置了该返回类型的bean,通过该类型去获取该bean会报错

image-20230310220653758

public class UserFactoryBean implements FactoryBean<student> {
    @Override
    public student getObject() throws Exception {
        return new student();
    }
    @Override
    public Class<?> getObjectType() {
        return student.class;
    }
}
<bean id="user" class="com.atguigu.bean.UserFactoryBean"></bean>
<!--获取该bean获取到的不是UserFactoryBean类型的对象,而是该类中getObject()方法返回的结果对象 -->
public void testFactoryBean(){
    ApplicationContext ioc=new ClassPathXmlApplicationContext("factoryBeanTest.xml");
    student bean = ioc.getBean(student.class);
    //没有配置student的bean也能获取该对象,因为student以及通过工厂bean的getObject方法交给ioc容器管理了
    System.out.println(bean.toString());
}
  • 通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们

  • 将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的

基于xml的自动装配
  • 自动装配:根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值

  • 使用bean标签的autowire属性设置自动装配效果,其可选值如下

    • byType

      • 根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值

      • 在IOC中

        • 没有任何兼容类型的bean能够为属性赋值,则该属性不装配(没找到就不赋值,值为默认值null),报空指针异常

        • 多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException

      • 相当于根据类型获取bean

    • byName

      • 将自动装配的属性的属性名当作bean的id在IOC容器中匹配相对应的bean进行赋值

      • ioc容器中

        • 没有任何兼容类型的bean就和byType一样

        • 不可能有多个兼容类型的情况(因为id是唯一标识,多个同id的bean会报配置异常)

      image-20230311102345725

      • 相当于根据id获取bean

      • 当byType有多个时,就是用byName

    • no和default都是不使用自动装配

基于注解管理bean

标记与扫描
注解
  • 和 XML 配置文件一样,注解本身并不能执行,注解本身只是一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作

  • 本质上:所有一切的操作都是Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行

  • bean的类型必须是有无参构造的类类型,所以注解要加在有无参构造的类上

标识组件的常用注解

@Component:将类标识为普通组件

@Controller:将类标识为控制层组件

@Service:将类标识为业务层组件

@Repository:将类标识为持久层组件

  • 四者关系和区别

    image-20230311110128725

    .

    • @Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字

    • 对于Spring使用IOC容器管理这些组件来说没有区别。@Controller、@Service、@Repository这三个注解只是让我们能够便于分辨组件的作用(虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记)

    • 可用于排除扫描和包含扫描

扫描
  • spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测,然后根据注解进行后续操作

  • 在xml文件中使用context约束的context:componet-scan标签,其中有个base-package属性指定要扫描的包

    • 注意创建包名时不要和其它的文件的包名重合(如org等等)否则所以和org有关的目录都会被扫描

    • context:componet-scan标签中有两个子标签

      • context:exclude-filter排除某些组件的扫描(不扫描谁)

        • 属性type:设置排除的依据,其常用值为

          • type="annotation",根据注解排除,expression中设置要排除的注解的全类名

          <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
          • type="assignable",根据类型排除,expression中设置要排除的类的全类名

      • context:include-filter包含某些组件的扫描(只扫描谁)

        • type:设置包含的依据

          • type="annotation",根据注解保留,expression中设置要保留的注解的全类名

          • type="assignable",根据类型保留,expression中设置要保留的类型的全类名

        • 如果要让该属性起作用,要将context:componet-sc标签中的use-default-filters属性设置为false

          • use-default-filters取值false表示关闭默认扫描规则(全扫描)

          • 除了使用包含扫描的情况下,其它情况都使用全扫描(即不设置该属性)

注解加扫描
  • 扫描的包中加了注解的类作为组件进行管理

  • 获取bean的方式和之前一样

  • bean的id

    • 通过注解加扫描配置的bean的id默认值类名的小驼峰(即把类名的第一个字母小写)

    @Component
    public class UserController {
    }

    image-20230311130313437

    .

    • 为注解的value属性赋值就可以自定义bean的id

    @Component("controller")
    public class UserController {
    }

image-20230311130340387

.

基于注解的自动装配
  • 使用@Autowired(实现自动装配的注解)

    • 标识的位置

      • 成员变量上(不需要设置该变量的set方法)

      • set方法上

      • 为当前成员变量赋值的有参构造上

    • 实现原理/过程

      1. 默认通过byType的方式,在ioc容器中通过类型匹配某个bean为属性赋值

      2. 多个类型匹配的bean,会自动转换成byName的方式,将要赋值的属性的属性名作为bean的id来匹配某个bean进行赋值

      3. 如果有多个类型匹配的bean并且通过属性名找不到对应的bean(两种方式都无法进行自动装配),会报错

        image-20230311132945757

      4. 这时就要使用@Qualifier注解,其属性值是指定要引用的bean的id

        @Autowired
        @Qualifier("userServiceImpl")
        private UserService userService;
    • required属性

      • 默认为true,表示必须完成自动装配,如果没有类型匹配的bean会报异常

        • 以后使用注解发现这个错误,去找对应的类上有没有注解,如果有注解就是没扫描到

      image-20230311134213640

      • 设置为false,如果没有类型匹配的bean,会赋予默认值null

xml和注解的选择
  • 第三方类库只能用xml,因为jar放的是class文件,我们获取到的是反编译的结果并且只读,无法使用直接

  • 自己创建的类可以使用注解的方式

AOP

当前缺陷

  • 针对带日志功能的实现类,有如下缺陷

    • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力

    • 附加功能分散在各个业务功能方法中,不利于统一维护

    • 解决思路

      • 解决这两个问题,核心就是:解耦合(需要把附加功能从业务功能代码中抽取出来

    • 困难

      • 要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决,所以需要引入新的技术

      • 因为面向对象是纵向继承机制,没办法把非连续的代码封装

代理模式

概念
  • 二十三种设计模式中的一种,属于结构型模式

  • 作用

    • 通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦

    • 调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护

原来

image-20230312144045609

使用代理模式

image-20230312144101595

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法

  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法(目标的核心功能保留,额外功能交给代理对象)

静态代理
  • 特点是一对一(一个目标对应一个代理)

  • 代理类要调用目标类相应的方法,就必须实现和目标类相同的接口

    • 通过代理对象间接访问目标对象

  • 静态代理实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性

    • 静态代理类实现了目标类同样的接口

    • 静态代理类对应的目标对象也是写死的(一个代理一个目标)

    • 日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理

动态代理
  • 进一步需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术

  • 动态指的是动态生成目标类对应的代理类

  • 动态代理有两种

    • jdk动态代理

      • 要求必须有接口,最终生成的代理类和目标实现相同接口

      • 生成的类在com.sum.proxy包下,类名为$proxy加上一个数字

    • cglib动态代理:最终生成的代理类会继承目标类,并且和目标类在相同的包

  • jdk动态代理实现

    • 创建代理工厂类

      • 声明一个Object类型(因为不知道目标类型)的成员变量,用于存放传入的目标对象

      • 有参构造接收目标对象

      • getProxy方法获取代理对象,通过Proxy.newProxyInstance(classLoader,interfaces,h)创建代理实例,其三个参数分别为

        • classLoader:加载动态生成的代理类的类加载器(类想被执行就必须先被加载)!!!反射相关内容,后续学习!!!

          • 根类加载器:底层C实现的,主要加载核心类库

          • 扩展类加载器:加载扩展类库

          • 应用类加载器:加载自己写的类或者引入的第三方jar包的类

          • 自定义类加载器

        • interfaces:目标对象实现的所有接口的class对象所组成的数组,动态生成的类必须要和目标对象实现相同接口,才能保证代理对象和目标对象实现功能一致

        • invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法

      public class ProxyFactory {
          private Object target; //不知道目标类的类型,所以声明为Object类型
          public ProxyFactory(Object target) {
              this.target = target;
          }
          public Object getProxy(){
              // newProxyInstance():创建一个代理实例
        		ClassLoader classLoader = this.getClass().getClassLoader();
              Class<?>[] interfaces = target.getClass().getInterfaces();
              //某个类型所实现的所有接口的Class对象所组成的数组
              InvocationHandler h=new InvocationHandler() {
                  @Override
                  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                      //来设置代理类的方法该如何重写
                      //proxy代理对象,method要重写/执行的抽象方法,要执行的方法的参数列表
                      System.out.println("日志,方法:"+method.getName()+",参数:"+ Arrays.toString(args));
                      Object result = method.invoke(target, args);
                      //invoke有两个参数:当前要使用的对象,参数列表(因为代理方法要实现目标方法,所以要传入目标对象)
                      System.out.println("日志,方法:"+method.getName()+",结果:"+result);       
                      return result;
                  }
              };
              return Proxy.newProxyInstance(classLoader,interfaces,h);
          }
      }
    • 获取动态代理对象

      • 由于代理工厂获取到的是Object类型的对象,不具有目标对象的方法,所以要将其转型成目标对象所实现的接口类型

      ProxyFactory proxyFactory=new ProxyFactory(new CalculatorPureImpl());
      Calculator proxy = (Calculator) proxyFactory.getProxy();
      proxy.add(1,3);

aop相关概念

概述
  • AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术

  • aop是横向抽取机制,功能就是把非核心代码封装到切面类进行管理(抽),把抽取出来的代码套用到抽取出来的位置(套)

相关术语
  • 横切关注点

    • 从每个方法中抽取出来的同一类非核心业务。在同一个项目中,可以使用多个横切关注点对相关方法进行多个不同方面的增强

    • 这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点

  • 通知每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法

    • 前置通知:在被代理的目标方法执行

    • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝

    • 异常通知:在被代理的目标方法异常结束后执行(死于非命

    • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论

    • 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

  • 切面:封装横切关注点,封装通知方法的类

  • 目标:被代理的目标对象

  • 代理:向目标对象应用通知之后创建的代理对象

  • 连接点:就是抽取横切关注点的位置

  • 这也是一个纯逻辑概念,不是语法定义的

    • 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点

image-20230316210309589

.

  • 切入点定位连接点的方式

    • 本质是一个表达式,连接点只是逻辑概念,要通过切入点来定位

    • 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)

    • 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句

    • Spring 的 AOP 技术可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件

作用
  • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性

  • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了

基于注解的AOP

  • 技术说明

    image-20230316211747707

    • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口兄弟两个拜把子模式)

    • cglib:通过继承被代理的目标类认干爹模式)实现代理,所以不需要目标类实现接口

    • AspectJ:本质上是静态代理,将代理逻辑“织入”到被代理的目标类编译得到的字节码文件,所以最终效果是动态的weaver就是织入器。Spring只是借用了AspectJ中的注解

准备工作
①添加依赖
  • 在IOC所需依赖基础上再加入下面依赖即可

<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
</dependency>
②准备被代理的目标资源
  • 创建接口和实现类

③创建切面类
  • 为目标类和切面类添加注解

  • 创建通知方法,声明相应注解,定位到目标类中的方法

  • 通知方法声明JoinPoint的形参

④创建核心配置文件
  • 包扫描

  • 开启AspectJ的自动代理,为目标对象自动生成代理

注意事项
  • 目标类和切面类都需要交给IOC容器管理(注解/xml的方式)

  • 切面类必须通过@Aspect标识为切面

  • 在spring配置文件中设置<aop:aspectj-autoproxy/>标签开启基于注解的aop

    • 无法通过ioc获取到目标对象,只能获取代理对象(通过共同实现的接口来获取

  • 通知方法要用注解声明,其格式为@通知类型("execution(修饰符 返回值 包.目标类.方法(形参类型)")

    • 要指定参数类型是因为了区分不同的重载方法

各种通知
  • 切面类中的方法要通过注解声明为通知方法

    • 前置通知:使用@Before注解标识,在被代理的目标方法执行

    • 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝

      • 其中有returning属性,用于指定方法中的一个参数来接受目标方法的返回值信息

    • 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命

      • 其中有throwing属性,用于指定方法中的一个参数来接受异常信息

    • 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行,也就是finally子句中执行(盖棺定论

    • 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

  • 各种通知的执行顺序

    • Spring版本5.3以前

      前置通知

      目标操作

      后置通知

      返回通知或异常通知

    • Spring版本5.3以后

      前置通知

      目标操作

      返回通知或异常通知

      后置通知

切入点表达式
语法

image-20230318161021897

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限

  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的

    • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello

  • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意

  • 在类名的部分

    • 类名的整体用*号代替,表示类名任意

    • 类名的一部分可以使用*号代替

      • 例如:*Service匹配所有名称以Service结尾的类或接口

  • 在方法名部分

    • 方法名的整体可以使用*号表示方法名任意

    • 方法名的一部分可以使用*号代替

      • 例如:*Operation匹配所有方法名以Operation结尾的方法

  • 在方法参数列表部分

    • 使用(..)表示参数列表任意

    • 使用(int,..)表示参数列表以一个int类型的参数开头

    • 基本数据类型和对应的包装类型是不一样的

      • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的

  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符

    • 也就是返回值类型和权限修饰符是打包的

    • 例如

      • execution(public int ..Service.*(.., int)) 正确

      • execution(* int ..Service.*(.., int)) 错误

切入点表达式重用
  • 创建一个声明公共的切入点表达式的方法

    • 该方法无参数、返回值

    • 在方法上加@Pointcut注解,参数写公共的切入点表达式(就是上述的语法)

    @Pointcut("execution(* aop.annotation.CalculatorPureImpl.*(..))")
    public void point(){ }
  • 使用:通知方法在注解的参数位置写上该方法名

    • 找到该方法--》找到注解--》找到切入点表达式--》作为自己的表达式

    @Before("point()")
    public void beforeActive(){
        System.out.println("前置通知");
    }
获取连接点信息
①获取连接点信息
  • 获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参

    • 获取连接点的签名方法的名称参数类型以及修饰符返回值)信息

    String methodName = joinPoint.getSignature().getName();

    • 获取目标方法接收到的实参

    String args = Arrays.toString(joinPoint.getArgs())

②获取目标方法的返回值
  • @AfterReturning中的属性returning,用来将通知方法的某个形参指定为接收目标方法的返回值的参数

  • 就是将通知方法的一个参数用来接收目标方法的返回值,所以该属性填写的是参数名

@AfterReturning(value = "point()",returning = "result")
public void afterReturningActive(JoinPoint joinPoint,Object result){
    Signature signature = joinPoint.getSignature();
    System.out.println(signature.getName()+"方法的返回值为"+result);
}
  • 不过奇怪的是,JoinPoint参数和接受返回值的参数调换顺序就报错了...

③获取目标方法的异常
  • @AfterThrowing中的属性throwing,用来将通知方法的某个形参指定为接收目标方法的异常的参数

④环绕通知
  • 要在通知方法之上声明@Around注解

  • 要设置ProceedingJoinPoint类型的参数,用于获取目标方法的执行进程

  • 类似于动态代理

    • 该方法必须有返回值将目标方法的返回结果返回

    • 通过joinPoint.proceed()获取到目标方法的执行结果(相当于调用了目标方法)

@Around("execution(* com.atguigu.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;
}
!!!切面的优先级!!!
  • 相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序

    • 优先级高的切面:外面;优先级低的切面:里面

image-20230319111316860

  • 使用@Order注解可以控制切面的优先级(值越小优先级越高)

    • @Order(较小的数):优先级高

    • @Order(较大的数):优先级低

    • 默认值为Integer的最大值

基于xml的AOP

  • 配置文件中设置aop:config标签,不用设置aop:aspectj-autoproxy/标签

  • aop:config标签的子标签

    • aop:aspect,将一个类指定为切面类()

      • 子标签

        • aop:各种通知类型,其属性

          • method,作用于哪个方法

          • pointcut,指定切入点表达式

          • pointcut-ref,引用切入点表达式

        • 通知子标签中

          • aop:after-returning还有returning属性,用于指定方法的一个参数来接受目标方法的返回值

          • aop:after-throwing有throwing属性,指定参数接受目标对象抛出的异常

      • 属性

        • ref,指定一个类作为切面类

        • order,设置优先级(参数为数字,值越小优先级越高

    • aop:pointcut,声明公共的切入点表达式

    • aop:advisor,和事务相关,后续学习

声明式事务

JdbcTemplate
  • 导入依赖

    • spring-test:Spring整合junit的依赖 ,在测试文件中不用获取ioc容器,直接获取ioc管理的bean

      • 测试类要设置两个注解

        • @RunWith(SpringJUnit4ClassRunner.class)

          • 指定当前测试类在spring测试环境下运行,就可以直接通过注入的方式获取ioc容器的bean

        • @ContextConfiguration("Spring-JdbcTemplate.xml")

          • 设置Spring测试环境的配置文件(即找到管理的ioc容器)

<dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- Spring 持久化层支持jar包 -->
    <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
    <!-- 导入 orm(对象关系映射) 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- Spring 测试相关-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
</dependencies>
  • spring配置文件

    • location中使用"classpath:",即类路径,web工程就必须指定使用的是类路径(web工程有资源路径和类路径)

<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="${atguigu.url}"/>
    <property name="driverClassName" value="${atguigu.driver}"/>
    <property name="username" value="${atguigu.username}"/>
    <property name="password" value="${atguigu.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!-- 装配数据源 -->
    <property name="dataSource" ref="druidDataSource"/>
</bean>
声明式事务概念
编程式事务
  • 编程式自己写代码实现功能

  • 编程式的实现方式存在缺陷

    • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐

    • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用

声明式事务
  • 声明式:通过一些配置或者一些代码既可以实现具体的功能(通过配置框架实现功能)

  • 既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装

  • 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作,优点:

    • 提高开发效率

    • 消除了冗余的代码

    • 框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化

基于注解的声明式事务
用户买书案例
  • 创建表

    • stock和balance设置无符号是为了保证库存和余额不能小于0

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_user1` (
`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_user1`(`user_id`,`username`,`balance`) values (1,'admin',50);
  • 配置文件中

    • 配置事务(数据源)管理器

      • <bean  id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="druidDataSource"></property>
        </bean>
      • 事务管理依赖于数据源,所以必须设置dataSource属性

      • 事务管理器要声明id才能和transaction-manager属性使用

    • 开启事务注解驱动

      • 引入tx:annotation-driven的命名空间(要选tx命名空间 )

      image-20230326205937029

      <tx:annotation-driven transaction-manager="transactionManager"/>
      • transaction-manager属性来设置事务管理器的id,默认值是transactionManager,如果事务管理器bean的id是默认值,则可以省略这个属性

      • 作用:把事务管理器作用于连接点,通过@Transactional标识的方法标识的类中所有的方法,都会被事务管理器管理

        • 三层架构中service需要实现事务操作,所以该注解可以加在service对应的类或者方法

事务属性
只读
  • @Transactional(readOnly = true)

  • readOnly属性,默认取值为false,事务中只有查询操作的时候才可以使用

  • 增删改操作设置只读会抛出下面异常:

    Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

超时
  • 事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源,这时候就需要超时机制释放资源

  • 执行:规定时间内事务未完成就强制回滚,释放资源

  • timeout属性,默认值是-1(即一直等待直至完成),设置超时时间,单位和下面的TimeUnit设置的一致

  • 测试超时功能,通过sleep模拟占用资源

    • TimeUnit.SECONDS.sleep(5);//休眠五秒

    • TimeUnit可以设置休眠时间的单位,DAYS天,HOURS小时....

    • 抛出异常:org.springframework.transaction.TransactionTimedOutException: Transaction timed out,然后强制回滚

回滚策略
  • 声明式事务默认只针对运行时异常回滚(任何运行时异常都会造成回滚),编译时异常不回滚

  • 可以通过@Transactional中相关属性设置回滚策略

    因为什么而回滚(设置会造成回滚的异常,一般不设置,因为默认是所以运行异常都会回滚)

    rollbackFor属性:需要设置一个Class类型的对象(异常对象)

    rollbackForClassName属性:需要设置一个字符串类型的全类名(异常全称)

    不因什么而回滚(设置不造成回滚的异常)

    noRollbackFor属性:需要设置一个Class类型的对象

    noRollbackFor = ArithmeticException.class//注意ArithmeticException.class返回的是一个数组,但是因为只有一个元素所以noRollBackFor属性值的{}可以省略

    rollbackFor属性:需要设置一个字符串类型的全类名

    noRollbackForClassName = "java.lang.ArithmeticException"
隔离级别
  • 数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱

  • 隔离级别一共有四种

    • 读未提交:READ UNCOMMITTED允许Transaction01读取Transaction02未提交的修改

      • 问题——脏读:读到2未提交、被撤销的数据

    • 读已提交:READ COMMITTED、要求Transaction01只能读取Transaction02已提交的修改

      • 问题——不可重复读:两次读取数据不一致

    • 可重复读:REPEATABLE READ确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新(mysql默认)

      • 问题——幻读:只是对某些数据加锁,并没有对表加锁,其它事务依旧可以对表执行增删改,1可能会读到其它事务修改的数据(mysql的可重复读避免了幻读的情况,每个事务中都只能读到当前事务的操作)

    • 串行化/可序列化:SERIALIZABLE确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下

  • 各个隔离级别解决并发问题的能力见下表(mysql中可重复读解决了幻读)

image-20230326212650399

  • 各种数据库产品对事务隔离级别的支持程度

image-20230326212705635

  • isolation属性设置隔离级别,该属性是枚举类型,可以通过Iosalation.枚举项设置隔离级别,默认隔离级别与数据库中使用的默认级别一致

事务的传播行为
  • propagation属性设置事务传播行为(也是枚举类型)

  • @Transactional(propagation = Propagation.REQUIRED)默认情况,当前线程上有已经开启的事务可用,就在该事务中运行

    • 购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。第一本书可以购买成功,购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了

  • @Transactional(propagation = Propagation.REQUIRES_NEW),不管当前线程上是否有已经开启的事务,都要开启新事务

    • 每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本

基于xml的声明式事务
  • 基于xml实现的声明式事务,必须引入aspectJ的依赖

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
    </dependency>
  • 配置通知的切入点表达式

    <aop:config>
        <aop:advisor advice-ref="txAdvice" pointcut="execution(*com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
    </aop:config>
  • tx:advice标签:配置事务通知

    • 属性

      • id属性:给事务通知标签设置唯一标识,便于引用

      • transaction-manager属性:关联事务管理器(同注解)

    • 子标签tx:attributes可以设置哪些方法的具体的事务属性

      • tx:method标签:配置具体的事务方法

        • name属性:指定方法名

          • 可以使用*代表多个字符get*就是所有以get开头的方法)

          • 也可以直接使用*来代表该切入点表达式对应连接点的所有方法(name="*")

        • read-only属性:设置只读属性

        • rollback-for属性:设置回滚的异常

        • no-rollback-for属性:设置不回滚的异常

        • isolation属性:设置事务的隔离级别

        • timeout属性:设置事务的超时属性

        • propagation属性:设置事务的传播行为

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="find*" read-only="true"/>        
        <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
    </tx:attributes>
</tx:advice>

SpringMVC

相关概念

MVC

  • MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分

    • M:Model,模型层,指工程中的JavaBean,作用是处理数据

      • JavaBean分为两类

        • 实体类Bean:专门存储业务数据的,如 Student、User 等

        • 业务处理 Bean:指ServiceDao对象,专门用于处理业务逻辑数据访问

    • V:View,视图层,指工程中的html或jsp等页面,作用是与用户进行交互,展示数据

    • C:Controller,控制层,指工程中的servlet,作用是接收请求和响应浏览器

  • MVC的工作流程: 用户通过视图层发送请求到服务器,在服务器中请求被Controller接收Controller调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller再根据请求处理的结果找到相应的View视图渲染数据后最终响应给浏览器

SpringMVC

  • SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案

    • 三层架构分为表述层(或表示层)业务逻辑层数据访问层,表述层表示前台页面和后台servlet

    • SpringMVC封装的就是servlet

  • 特点

    • Spring 家族原生产品,与 IOC 容器等基础设施无缝对接

    • 基于原生的Servlet,通过了功能强大的前端控制器DispatcherServlet(把servlet封装成前端控制器,无需自己创建servlet),对请求和响应进行统一处理

    • 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案

    • 代码清新简洁,大幅度提升开发效率

    • 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可

    • 性能卓著,尤其适合现代大型、超大型互联网项目要求

入门

创建web工程

  • 可以先创建普通的java工程,然后在pom.xml中将打包方式设置成war包,在项目结构中添加web.xml文件即可

    image-20230401132334664

image-20230401132535666

  • 注意,web-inf目录要在项目文件的src的main目录下

添加依赖

<dependencies>
    <!-- SpringMVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    <!-- ServletAPI -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- Spring5和meleaf整合包 -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.12.RELEASE</version>
    </dependency>
</dependencies>

配置web.xml

默认配置
  • 注册springmvc封装的前端控制器

    • servlet标签的servlet-name可以随意配置

    • servlet-mapping中的servlet-name要和servlet标签的一致,servlet-name写的是default可用于处理静态资源

    image-20230401133849748

    • url-pattern:路径模型,浏览器发送的请求符合url-pattern,就会被前端控制器处理

      • 后缀匹配<url-pattern>*.do</url-pattern>

      • <url-pattern>/</url-pattern>可以匹配浏览器向服务器发送的所有请求,但不能匹配以.jsp结尾的请求

        • 这里使用/是因为tomcat中配置过JspServlet专门用于处理.jsp的请求,前端控制器应该.jsp的请求交给jspServlet处理

        image-20230401134811035

        image-20230401134925779

      • <url-pattern>/*</url-pattern>可以匹配浏览器向服务器发送的所有请求,也包括jsp请求

      • 可以设置多个url-pattern,表示同一个servlet可以处理多个不同的请求

    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
!!!扩展配置!!!
  • <init-param>:通过初始化参数指定SpringMVC配置文件的位置和名称 

    • <param-name>contextConfigLocation</param-name>contextConfigLocation为固定值

      • contextConfigLocation用于设置前端控制器要加载的springMVC的配置文件的路径

    • 使用classpath:表示从类路径查找配置文件,如果没写默认从web-inf目录

    • 如果报如下异常说明没有找到对应文件,可能是未加载到target

      • 可以去target目录中的web-inf目录下(java下面和resource下的内容最终会放在该目录下)找有没有

      • 没有的话

        • 可以先clean,然后package

        • 或者重新运行

    image-20230402185255912

  • <load-on-startup>:作为框架的核心组件,在启动过程中有大量的初始化操作要做,而这些操作放在第一次请求时才执行会严重影响访问速度,因此需要通过此标签将启动控制器DispatcherServlet的初始化时间提前到服务器启动时

<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name><!-- contextConfigLocation为固定值 -->
        <param-value>classpath:springMVC.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>springMVC</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>

创建请求控制器

  • 由于前端控制器对浏览器发送的请求进行了统一的处理,但是具体的请求有不同的处理过程,因此需要创建处理具体请求的类,即请求控制器

  • 请求控制器中每一个处理请求的方法称为控制器方法,因为SpringMVC的控制器由一个POJO(普通的Java类)担任,因此需要通过@Controller注解将其标识为一个控制层组件,交给Spring的IOC容器管理,此时SpringMVC才能够识别控制器的存在

@Controller
public class HelloController {
}

!!!创建SpringMVC的配置文件!!!

  • 默认配置方式:SpringMVC的配置文件是前端控制器初始化时完成加载的,所以配置文件时位置和名称是固定的

    • 位置:web-inf下,在web-inf下有下面的命名要求

    • 名称:servlet-name标签中的名字加上-servlet.xml,如servlet-name为springMVC,则配置文件名称为springMVC-servlet.xml

  • 物理视图=视图前缀+逻辑视图+视图后缀

<!-- 自动扫描包 -->
<context:component-scan base-package="controller"></context:component-scan>

<!-- 配置Thymeleaf视图解析器 -->
<bean id="viewResolver"
      class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    <property name="order" value="1"/>优先级
    <property name="characterEncoding" value="UTF-8"/>
    <property name="templateEngine">模板引擎
        <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
            <property name="templateResolver">模板解析器 
                <bean
                        class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
                    <!-- 视图前缀 -->
                    <property name="prefix" value="/WEB-INF/templates/"/>
                    <!-- 视图后缀 -->
                    <property name="suffix" value=".html"/>
                    <property name="templateMode" value="HTML5"/>
                    <property name="characterEncoding" value="UTF-8"/>
                </bean>
            </property>
        </bean>
    </property>
</bean>

测试

配置tomcat
  • application context:应用上下文路径即当前工程的路径,可以通过该路径找到对应项目

image-20230402134024152

image-20230402134302152

  • on update action:点击刷新按钮执行的操作

    image-20230402171142273

    image-20230402134348509

    • redeploy重新部署,restart server重新启动

处理器类中写方法
  • 在对应方法上声明@RequestMapping注解,可以把浏览器发送的请求映射到该方法,处理该请求

  • /对应的是项目路径

  • 逻辑视图=物理视图去掉视图前缀和视图后缀,返回的逻辑视图会被视图解析器解析,拼接之后得到物理视图

  • 如果没有设置返回值的话,会把RequestMapping中的请求地址当成逻辑视图解析

@Controller
public class HelloController {
@RequestMapping("/")
    public String protal(){
		return "index";//将逻辑视图返回
    }
}
创建资源
  • web-inf目录下创建templates目录(“/WEB-INF/templates/” 为视图前缀)

  • templates目录中创建index.html资源

  • 使用th相关标签要先引入命名空间

    • <html lang="en" xmlns:th="http://www.thymeleaf.org">

    • 创建html时自动引入th命名空间的步骤

      • 将上行代码放入html创建模板中

    image-20230402170414054

  • th:href的作用

    • 如果是<a href="/hello">,以斜线开头的代表绝对路径

    • 使用了th:href,会将@{}中的绝对路径渲染,补充项目路径

    <a th:href="@{/hello}">HelloWorld</a>

总结

  1. 浏览器发送请求,若请求地址符合前端控制器的url-pattern,该请求就会被前端控制器DispatcherServlet处理

  2. 前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器

  3. 将请求地址和控制器中@RequestMapping注解的value属性值进行匹配

  4. 若匹配成功,该注解所标识的控制器方法就是处理请求的方法

  5. 处理请求的方法需要返回一个字符串类型的视图名称(逻辑视图),该视图名称会被视图解析器解析加上前缀和后缀组成视图的路径

  6. 通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面,转发是因为

    • web-inf下的页面无法通过重定向访问

    • Thymeleaf是服务器中的视图渲染技术,重定向的地址会改变

@RequestMapping

基础知识

  • 功能

    • @RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系

    • SpringMVC 接收到指定的请求,就会去找在映射关系中对应的控制器方法来处理这个请求

  • @RequestMapping注解的位置

    • 标识一个类:设置映射请求的请求路径的初始信息

    • 标识一个方法:设置映射请求请求路径的具体信息

    • 加到类上相当于加了一个父目录,该类标识了注解的方法为子目录

属性

value
  • value属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求

    • 一个控制器方法可以处理多个不同的请求

  • @RequestMapping注解的value属性必须设置,至少通过请求地址匹配请求映射

method
  • 设置当前控制器方法处理请求的请求方式

    • 请求路径匹配的前提下,属性通过请求的请求方式(get或post)匹配请求映射

    • 请求路径和请求方法都匹配才会处理请求

  • method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配多种请求方式的请求

    • 枚举类型,提供了GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE请求方式

    @RequestMapping(value = {"abc","hello"},method = RequestMethod.GET)
    • 使用post的请求的情况

      • 表单中的method方法设置为post

      • ajax把请求方式设为post

    • 其它都是get请求

  • 若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错

    • 405:Request method 'XXX' not supported(就是浏览器发送的请求的请求方式和控制器要求的请求方式不一致

  • 对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解

处理get请求的映射-->@GetMapping

处理post请求的映射-->@PostMapping

处理put请求的映射-->@PutMapping

处理delete请求的映射-->@DeleteMapping

params(了解)
  • 通过请求的请求参数匹配请求映射

  • @RequestMapping注解的params属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系

    • "param":要求请求映射所匹配的请求必须携带param请求参数

    • "!param":要求请求映射所匹配的请求必须不能携带param请求参数

    • "param=value":要求请求映射所匹配的请求必须携带param请求参数且param=value

    • "param!=value":要求请求映射所匹配的请求可以不携带param,如果携带param请求参数就必须满足param!=value

@RequestMapping(
value = {"/testRequestMapping", "/test"},method = {RequestMethod.GET, RequestMethod.POST}
,params = {"username","!password","age=23","gender!=女"}//必须有年龄和用户名,不能有密码,而且年龄必须为23
    //性别可有可不有,有的话必须不能为女
)
  • 使用th:href携带参数

    • <a th:href="@{/hello?username=admin}">这种方式问号可能会报红,但是可以正常运行

    • <a th:href="@{/hello(username='admin')}"

header(了解)
  • @RequestMapping注解的headers属性通过请求的请求头信息匹配请求映射,和params的用法类似

  • headers属性是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系

    • "header":要求请求映射所匹配的请求必须携带header请求头信息

    • "!header":要求请求映射所匹配的请求必须不能携带header请求头信息

    • "header=value":要求请求映射所匹配的请求必须携带header请求头信息且header=value

    • "header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value

  • 请求头和响应头的键都不区分大小写(值是区分大小写的)

  • 若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面显示404错误,即资源未找到

  • 请求头信息中的referer指的是来源,表示从哪个路径跳转到当前路径的

    image-20230417185319601

springmvc支持的路径风格

ant风格
  • ?:表示任意的单个字符

  • *:表示任意个数的任意字符

  • **:表示任意层数任意目录

    • 在使用时,只能使用/**/xxx的方式,**前后不能有其它字符

      • /a**a/是错误的,因为会把**当作单个的字符

  • 这些符号都不能表示某些特殊字符('?','/'),比如说'?'会被当成请求参数和路径间的分隔符

@RequestMapping("/a*a/test")
可以接收aaaaa/test
    	aa/test
但是不能接收 a?a/test
!!!路径中的占位符!!!
  • 原始方式:/deleteUser?id=1

  • rest方式:/user/delete/1

  • SpringMVC路径中的占位符常用于RESTful风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的@RequestMapping注解的value属性中通过占位符{xxx}表示传输的数据,再通过@PathVariable注解将占位符所表示的数据赋值给控制器方法的形参

  • 语法@RequestMapping(路径/{参数1}/{参数2}....)

    • 方法中的形参用@PathVariable()标识,括号里面填写的是把路径的哪个变量赋值给该参数

      • 形参和RequestMapping路径中的参数名匹配

    • 请求路径的参数要和requestMapping的接收路径参数按顺序对应

控制器方法   
@RequestMapping("test/rest/{id}/{userName}")
    public String testRest(@PathVariable("id") Integer userId,@PathVariable("userName")String userName) {
        System.out.println("id为"+userId+"的用户是"+userName);
        return "success";
    }
index文件
<a th:href="@{/test/rest/1/张三}">测试占位符</a>
//发送的路径为http://localhost:8080/springMvc/demo/test/rest/1/%E5%BC%A0%E4%B8%89

SpringMVC获取请求参数

通过servletApi获取

  • HttpServletRequest作为控制器方法的形参,此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象

  • 因为控制器方法是由前端控制器调用,调用时会检查参数类型,如果是requset类型就会把当前的请求对象为该参数赋值

通过控制器方法的形参获取

  • 在控制器方法的形参位置,设置和请求参数同名的形参,当浏览器发送请求,匹配到请求映射时,在DispatcherServlet中就会将请求参数赋值给相应的形参

@RequestMapping("/Param")
public String testParam(String username){
    System.out.println("username:"+username);
    return "success";
}
index文件
  • 若请求所传输的请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置

    • 字符串数组接收此请求参数,此参数的数组中包含了每一个数据

    • 字符串类型的形参接收此请求参数,此参数的值为每个数据中间使用逗号拼接的结果

获取浏览器数据的相关注解

@RequestParam
  • 如果请求参数和控制器方法形参名字不匹配,可以在形参前设置@RequestParam

    • 作用是将请求参数和控制器方法形参进行绑定

    • 属性

      • value/name 指定为形参赋值的请求参数的参数名

      • required:设置是否必须传输此请求参数,默认值为true,若设置

        • 为true时,则当前请求必须传输value所指定的请求参数,若没有传输该请求参数,且没有设置defaultValue属性,则页面报错400:Required String parameter 'xxx' is not present

        • 为false时,则当前请求不是必须传输value所指定的请求参数,若没有传输,则注解所标识的形参的值为默认值

      • defaultValue:不管required属性值为true或false,当value所指定的请求参数没有传输或传输的值为""时,则使用默认值为形参赋值

@RequestHeader
  • @RequestHeader是将请求头信息和控制器方法的形参创建映射关系

  • 在构造器方法中声明形参,在之前用上该注解,如果没有使用注解默认用于根据形参名获取对应请求参数

@RequestMapping("/Param")
public String testParam(@RequestParam(defaultValue = "张三") String username,@RequestHeader("referer")String refer){
    System.out.println("username:"+username);
    System.out.println(refer);
    return "success";
}
  • @RequestHeader注解一共有三个属性:value、required、defaultValue,用法同@RequestParam

@CookieValue
  • @CookieValue是将cookie数据和控制器方法的形参创建映射关系

    • 原生的获取Cookie的方式是getCookies获取Cookie类型的数组,然后获取键和值

    • 使用注解的方式用value匹配对应的key,就可以获取到cookie的值

  • @CookieValue注解一共有三个属性:value、required、defaultValue,用法同@RequestParam

通过POJO获取请求参数

  • 可以在控制器方法的形参位置设置一个实体类类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值

    • 和成员变量名无关,只和get、set方法有关

!!!解决中文乱码问题!!!

  • tomcat8之后只有post请求有中文乱码问题

  • 无法使用形参接收的request对象来解决乱码,因为不接收request对象也可以获取到参数,说明该request对象之前就已经获取了参数了,request设置编码集有要求:在设置编码之前不能获取任意请求参数

    • 解决乱码问题必须在获取参数前处理

  • 使用SpringMVC提供的编码过滤器CharacterEncodingFilter,但是必须在web.xml中进行注册

    • SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效(过滤器的执行顺序只和web.xml的配置的filter-mapping的顺序有关)

    • 只设置encoding只会处理请求的编码,加上forceEncoding可以处理响应的编码

<!--配置springMVC的编码过滤器-->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
    <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

域对象共享数据

向request域对象共享数据

使用servletapi
@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
request.setAttribute("testScope", "hello,servletAPI");
return "success";
}
使用ModelAndView
  • 官方推荐

    • 不管以什么方式进行页面跳转和往请求域存储数据,最终都会被封装到modelAndView中

  • 如果使用这种方式,返回值就必须为ModelAndView

  • 包含model和view功能

    • model:向请求域中共享数据

    • view:设置逻辑视图实现页面跳转

  • 使用步骤

    1. new ModelAndView()对象

    2. 对象调用addObject("属性名","值”)方法,向请求域中共享数据

    3. 对象调用setViewName("跳转的逻辑视图")方法,设置逻辑视图

    4. 返回该对象

@RequestMapping("test/mav")
public ModelAndView testMav(){
    //获取modelAndview对象
    ModelAndView mav=new ModelAndView();
    mav.addObject("testMAV","恭喜你入门了ModelAndView");//往请求域中存储数据
    mav.setViewName("success");//指定跳转的逻辑视图
    return mav;
}
三种相似的方式
  • Model、ModelMap、Map类型的参数其实本质上都是BindingAwareModelMap类型的

  • 实际上只需要创建BindingAwareModelMap类型继承的类或者实现的接口就可以往请求域中共享数据

public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}
使用model
  • model是个接口,放在控制器方法的形参中去接收前端控制台提供的对象

  • 对象.addAttribute("属性名","属性值")向请求域共享数据

  • 返回逻辑视图

@RequestMapping("test/model")
public String testModel(Model model){
    model.addAttribute("testMAV","恭喜你入门了Model");
    return "success";
}
使用modelMap
  • 和model的方式基本相同(就只是换了一个名字...)

    @RequestMapping("test/modelMap")
    public String testModel(ModelMap model){
        model.addAttribute("testMAV","恭喜你入门了ModelMap");
        return "success";
    }
使用map
  • 声明一个map类型的形参,key为String类型(表示属性名),值为Object类型(表示属性值)

  • map.put()

@RequestMapping("test/map")
public String testModel(Map<String,Object> map){
    map.put("testMAV","恭喜你入门了Map");
    return "success";
}

向session域和application域中共享数据

  • 建议使用servletapi的方式,其它方式没有拓展

  • session

    • 控制器方法

      @RequestMapping("/test/session")
      public String testSession(HttpSession session){
          session.setAttribute("testMAV","session域");
          return "session";
      }
    • th中获取session域数据要通过session.属性名获取<p th:text="${session.testMAV}"></p>

    • 钝化和活化

      • 钝化:服务器关闭时,session对象会被保存到一个磁盘文件(tomcat的work目录中)中

      • 活化:服务器重启时会将钝化的文件中的数据重新加载到session中

      • 实现钝化,要在tomcat中设置

        image-20230421221700942

        即tomcat重启之后,不清空session对象

      • 如果共享的是实体类的数据,要实现钝化,该类就必须实现序列化接口

  • application

    • 不能直接通过形参获取到,但是可以通过其它域对象获取

    • 控制器方法

      @RequestMapping("/test/application")
      public String testApplication(HttpSession session){
          ServletContext context = session.getServletContext();
          context.setAttribute("testMAV","application域");
          return "success";
      }
    • 展示数据<p th:text="${application.testMAV}"></p>

SpringMvc视图

  • SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户

  • SpringMVC视图的种类很多,默认有转发视图重定向视图

    • 当工程引入jstl的依赖,转发视图会自动转换为JstlView

    • 若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView

  • !!!断点调试!!!

    • 方法栈中,越往上的方法里当前断点所在位置越近(最上方为当前栈顶)

    image-20230423202545029

    image-20230423202804389

    返回上一个断点

    image-20230423202820679

    跳过断点

ThymeleafView

  • 创建视图的不同取决于视图名称(视图只和视图名称有关系),返回类型为

    • String就取决于返回值

    • ModelAndView就取决于setViewName的属性值

  • 当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析,视图名称拼接视图前缀和视图后缀所得到的最终路径,会通过转发的方式实现跳转(创建的就是ThymeleafView)

转发视图

  • SpringMVC中默认的转发视图是InternalResourceView(网络资源视图)

  • SpringMVC中创建转发视图的情况

    • 当控制器方法中所设置的视图名称以"forward:"为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"forward:"去掉剩余部分作为最终路径通过转发的方式实现跳转

    • 例如"forward:/","forward:/employee"

  • 使用的不多,因为ThymeleafView也是转发,而且使用这种方式的转发只是普通转发不会被Thymeleaf渲染,以下报错就是因为没有用Thymeleaf渲染而导致找不到路径,解决方式①可以先转发到内部的用Thymeleaf渲染的控制器方法代为转发.....②写完整路径

  • 在Thymeleaf环境中基本不使用

image-20230424122046485

image-20230424121903106

image-20230424122154862

重定向视图

  • SpringMVC中默认的重定向视图是RedirectView

  • 当控制器方法中所设置的视图名称以"redirect:"为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转

  • 重定向视图在解析时,会先将redirect:前缀去掉,然后会判断剩余部分是否以/开头(以/开头为绝对路径),若则会自动拼接上下文路径

@RequestMapping("view/redirect")
public String testRedirect(){
    return "redirect:/test/mav";
}

!!!视图控制器!!!

  • 当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用view-controller标签进行表示

    • path:设置处理的请求地址

    • view-name:设置请求地址所对应的视图名称

<mvc:view-controller path="/testView" view-name="success"></mvc:view-controller>
  • 注意:当SpringMVC中设置view-controller时,其他控制器中的请求映射将全部失效,此时需要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签:<mvc:annotation-driven />

restful

简介(了解)

  • REST:Representational State Transfer,表现层资源状态转移,是一种开发风格

  1. 资源

    • 资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解

    • 与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互

  2. 资源的表述

    • 资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移(交换)。资源的表述可以有多种格式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等。资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式

  3. 状态转移

    • 状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述。通过转移和操作资源的表述,来间接实现操作资源的目的

增删改查的实现

  • HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE

    • 它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE用来删除资源

  • REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性

image-20230424131950232

  • 查询

    • 使用占位符来接收数据

    @RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getAllUser(){
        System.out.println("查询到所有用户");
        return "success";
    }
    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    public String getAllUser(@PathVariable("id") Integer id){
        System.out.println("查询到id为"+ id+"的用户");
        return "success";
    }
    
    <a th:href="@{/user}">查询所有用户</a>
    <a th:href="@{/user/1}">根据id查询用户</a>
  • 新增

    • 可以通过一个实体类来接收参数

    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String insert(User user){
        System.out.println(user);
        return "success";
    }
    
    <form th:action="@{/user}" method="post">
        用户名<input type="text" name="username">
        密码<input type="password" name="password">
        <input type="submit">
    </form>
  • 删除和修改

    • 配置处理请求方式的过滤器HiddenHttpMethodFilter(将POST请求转换为DELETE或PUT 请求)

    • 使用要求

      1. 当前请求的请求方式必须为post

      2. 当前请求必须传输请求参数_method,其值为真正的请求方式

        • 最好设置为隐藏域发送

          //修改
          <form th:action="@{/user/1}" method="post">
              <input type="hidden" name="_method" value="put">
              <input type="submit">
          </form>
          //删除   
          <form th:action="@{/user/1}" method="post">
              <input type="hidden" name="_method" value="delete">
              <input type="submit">
          </form>
      <filter>
          <filter-name>HiddenHttpMethodFilter</filter-name>
          <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
      </filter>
      <filter-mapping>
          <filter-name>HiddenHttpMethodFilter</filter-name>
          <url-pattern>/*</url-pattern>
      </filter-mapping>

!!!案例练习!!!

功能清单

image-20230425215718397

访问首页,获取员工列表
  • 通过model将员工列表共享到请求域

@Autowired
EmployeeDao dao;
@RequestMapping(value = "/employee",method = RequestMethod.GET)
public String getAllEmployees(Model model){
    //获取员工列表
    Collection<Employee> all = dao.getAll();
    model.addAttribute("employeeList",all);
    return "employee_list";
}
展示员工数据
  • th:each 循环遍历集合(有点类似jstl)

  • 语法 th:each="用于指代当前遍历到的集合元素:${集合}"

<table border="1" cellpadding="0" cellspacing="0" style="text-align:
center;" id="dataTable">
    <tr>
        <th colspan="5">Employee Info</th>
    </tr>
    <tr>
        <th>id</th>
        <th>lastName</th>
        <th>email</th>
        <th>gender</th>
        <th>options(<a th:href="@{/toAdd}">add</a>)</th>
    </tr>
    <tr th:each="employee : ${employeeList}">
        <td th:text="${employee.id}"></td>
        <td th:text="${employee.lastName}"></td>
        <td th:text="${employee.email}"></td>
        <td th:text="${employee.gender}"></td>
        <td>
            <a class="deleteA" @click="deleteEmployee"
               th:href="@{'/employee/'+${employee.id}}">delete</a>
            <a th:href="@{'/employee/'+${employee.id}}">update</a>
        </td>
    </tr>
</table>
  • 引入静态资源

    • 引入css资源<link rel="stylesheet" th:href="@{/static/css/index_work.css}">

    • 可能会出现的问题:处理静态资源要用默认的servlet,前端控制器处理不了,如果无法引入静态资源,可能是其url-pattern和其他的servlet的冲突了(默认为/),多个相同的配置以当前工程的配置为准

    • 解决:配置默认的servlet来处理静态资源<mvc:default-servlet-handler/>

      • 注意,配置这个标签之后一定要配置注解驱动,否则所有请求都会被默认servlet处理

添加员工
  • 配置视图控制器

<mvc:view-controller path="/toAdd" view-name="employee_add"/>
  • 添加页面

    • form里面可以有table,但是table里面不能有form

<form th:action="@{/employee}" method="post">
    <table>
        <tr>
            <th colspan="2">add</th>
        </tr>
        <tr>
            <td>lastName</td>
            <td>
                <input name="lastName" type="text">
            </td>
        </tr>
        <tr>
            <td>email</td>
            <td>
                <input name="email" type="text">
            </td>
        </tr>
        <tr>
            <td>gender</td>
            <td>
                <input name="lastName" type="radio" value="1">male
                <input name="lastName" type="radio" value="0">female
            </td>
        </tr>
        <tr>
            <td colspan="2">  <input type="submit"></td>
        </tr>
    </table>
</form>
  • 控制器方法

    • 实体类接收数据

    • 跳转到展示列表的控制器方法(直接跳转到列表页面是没有数据的,要交给列表控制器方法处理请求)

    • 建议使用重定向

@RequestMapping(value = "/employee",method = RequestMethod.POST)
public String addEmployee(Employee employee){
    //保存员工信息
    dao.save(employee);
    return "redirect:/employee";
}
修改数据
  • 回显数据的超链接

    • <a th:href="@{'/employee/'+${employee.id}}">update</a>

    • 不能写成<a th:href="@{/employee/${employee.id}}">,表达式会被当成普通字符串解析

  • 修改数据的表单

    • 使用th:value就可以获取到域中的数据

    • 单选框数据用th:field,当数据的值和value一致时,就会被选中

    • 使用隐藏域来设置请求方式put

    • id信息也用隐藏域(表单必须要有id信息,否则用实体类接收请求参数的时候会缺少id信息)

<form th:action="@{/employee}" method="post">
    <input type="hidden" name="_method" value="put">
    <input type="hidden" name="id" th:value="${employee.id}">
    <table>
        <tr>
            <th colspan="2">update</th>
        </tr>
        <tr>
            <td>lastName</td>
            <td>
                <input name="lastName" type="text" th:value="${employee.lastName}">
            </td>
        </tr>
        <tr>
            <td>email</td>
            <td>
                <input name="email" type="text" th:value="${employee.email}">
            </td>
        </tr>
        <tr>
            <td>gender</td>
            <td>
                <input name="lastName" type="radio" value="1" th:field="${employee.gender}">male
                <input name="lastName" type="radio" value="0" th:field="${employee.gender}">female
            </td>
        </tr>
        <tr>
            <td colspan="2">  <input type="submit" value="修改"></td>
        </tr>
    </table>
</form>
  • 回显和修改数据的控制器方法

@RequestMapping(value = "/employee/{id}",method = RequestMethod.GET)
public String showEmployee(@PathVariable("id") Integer id, Model model){
    Employee emp = dao.get(id);//获取到该id的员工信息
    model.addAttribute("employee",emp);//将该员工信息存入请求域
    return "employee_update";//转发到修改页面
}
@RequestMapping(value = "/employee",method = RequestMethod.PUT)
public String showEmployee(Employee employee, Model model){
    dao.save(employee);
    return "redirect:/employee";
}
删除数据
  • 删除要使用delete的请求参数,因此要写一个表单来传输

    <form id="delete_form" method="post">
        <input type="hidden" name="_method" value="delete">
    </form>
  • 将某个员工的信息给表单的链接赋值

    <a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>
  • 使用vue

    • 引入vue.js<script type="text/javascript" th:src="@{/static/js/vue.js}"></script>

    • event.target表示当前触发事件的标签

    • 阻止超链接的默认行为

<script type="text/javascript" >
    var vue = new Vue({
        el: "#dataTable",
        methods: {
            //event表示当前事件
            deleteEmployee:function (event) {
                //通过id获取表单标签
                var delete_form = document.getElementById("delete_form");
                // document.getElementsByTagName("form")[0]只有一个表单就可以通过这种方式获取
                //将触发事件的超链接的href属性为表单的action属性赋值
                delete_form.action = event.target.href;
                //提交表单
                delete_form.submit();
                //阻止超链接的默认跳转行为
                event.preventDefault();
            }
        }
    });
</script>

处理ajax请求

axios的基本使用(具体用法可以去官网学习)

  • 引入axios<script type="text/javascript" th:src="@{/js/axios.min.js}"></script>

  • 参数

    • url——请求路径

      • 请求路径要加上上下文路径,因为ajax的请求是直接被浏览器解析的(js代码用不了thymeleaf)

    • method——请求方式

    • params——请求参数

      1. name=value&name=value的方式发送的请求参数

      2. 无论什么请求方式,请求参数都会被拼接到请求地址(post也会)

      3. 可以通过request.getParameter获取,控制器方法中可以直接用形参接收

    • data——请求参数

      1. json格式发送的请求参数

      2. 请求参数会被保存到请求报文的请求体传输到浏览器【请求方式只能用post,put,patch,因为get没有请求体

      3. 不能通过request.getParameter获取,可以先读取请求体中的数据,再通过处理json数据的jar包将数据转化为java对象

  • 处理响应结果

    • axios.then()——处理ajax请求成功之后服务器响应过来的结果

      • 服务器响应的数据被封装到response.data

    • axios.catch()——处理请求失败的结果

  • axios.post(url,data[,config])和axios.get(url,[,config])【[]中的配置是可以省略的】

    • 如果要传递param方式的参数可以拼接到url中(推荐)或者写到config中

    • 不使用axios是因为axios要写键值对,而axios.post等等的只用写参数就行了

  • consle.log(数据) 将其中的数据以日志的形式输出到控制台

  • 注意

    • vue中要用methods,用method会报错(暂时不知道原因)

    • 按钮的事件的方法的()如果没有参数就可写可不写,有参数就必须要写

      • @click="testRequestBody()"和@click="testRequestBody"都可以

image-20230504170907209

<div id="app">
    <h1>index.html</h1>
    <input type="button" value="测试springmvc处理ajax" @click="testRequestBody()">
</div>
<script type="text/javascript" th:src="@{/js/vue.js}"></script>
<script type="text/javascript" th:src="@{/js/axios.min.js}"></script>
<script type="text/javascript">
    var vue=new Vue({
        el:"#app",
        methods:{
            testRequestBody(){
                axios.post(
                    "/springmvc_ajax/testAjax?id=1001",
                    {username:"admin",password:"123456"}
                ).then(response=>{
                    console.log(response.data);
                });
            }
        }
    });
</script>
  • 处理ajax请求的控制器方法

    • 处理响应结果

      • response.getWriter().print()/response.getWriter().write()

      @RequestMapping(value = "/testAjax" ,method = RequestMethod.POST)
      public void testAjax(Integer id, HttpServletResponse response) throws IOException {
          System.out.println("id:" + id);
          response.getWriter().write("hello");
      }

处理json格式的请求参数

  • 方式一

    • 使用@RequestBody注解绑定请求体信息

      • @RequestBody可以获取请求体信息,使用@RequestBody注解标识控制器方法的形参,当前请求的请求体就会为当前注解所标识的形参赋值

  • 方式二

    1. 导入jackson依赖,将json格式的请求参数转化为Java对象

      <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.12.1</version>
      </dependency>
    2. 在配置文件中开启注解驱动

    3. 在控制器方法的形参位置,直接设置json格式参数要转换成的java类型的形参,使用@RequestBody注解标识

      • 使用实体类

      @RequestMapping(value = "/testRequestBody" ,method = RequestMethod.POST)
      public void testAjax(@RequestBody User user, HttpServletResponse response) throws IOException {
          System.out.println(user);
          response.getWriter().write("requestBody");
      }
      • 使用map(key为String类型,value为Object类型)

      @RequestMapping(value = "/testRequestBody" ,method = RequestMethod.POST)
      public void testAjax(@RequestBody Map<String,Object> user, HttpServletResponse response) throws IOException {
          System.out.println(user);
          response.getWriter().write("requestBody");
      }
      • 有对应的实体类就用实体类,没有就用map,实体类输出的键值对数量固定,而map是不固定的

响应json数据

  • !!!@ResponseBody注解!!!

  • 用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器(就可以不用response对象了)

  • 响应json格式的数据

    • 使用response.getWriter().print(java对象),响应结果为该对象toString()之后的结果

    • 不能直接响应java对象,要将其先转换成json字符串

    • 步骤

      1. 导入jackson依赖

      2. 注解驱动

      3. 使用@ResponseBody注解标识控制器方法,将需要转换为json字符串并响应到浏览器的java对象作为控制器方法的返回值,此时SpringMVC就可以将此对象直接转换为json字符串并响应到浏览器

      @RequestMapping(value = "/testResponseBody" ,method = RequestMethod.POST)
      @ResponseBody
      public User testResponseBody() throws IOException {
          User user=new User("邻家大姐哦","dasdad");
          return user;
      }
      1. 前端响应到控制台

      testResponseBody(){
          axios.post(
              "/springmvc_ajax/testResponseBody"
          ).then(response=>{
              console.log(response.data
          })
      }
  • 常用的java对象转化为json的结果

    • 实体类->json对象

    • map->json对象

    image-20230504210558327

    image-20230504210535296

    • list->json数组

image-20230504210758010

image-20230504210815828

  • @RestController注解是springMVC提供的一个复合注解标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解

拓展功能

文件的下载和上传

文件下载
  • ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文,使用ResponseEntity实现下载文件的功能

    • 参数

      • body响应体

      • headers响应头

      • status响应状态

  • !!!文件下载模板!!!

@RequestMapping("/testDown")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws
IOException {
    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取服务器中文件的真实路径
    String realPath = servletContext.getRealPath("项目路径后面的需要获取的资源路径");
    //创建输入流
    InputStream is = new FileInputStream(realPath);
    //创建字节数组
    byte[] bytes = new byte[is.available()];
    //将流读到字节数组中
    is.read(bytes);
    //创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    //设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=文件名称");
    //设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    //创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers,
    statusCode);
    //关闭输入流
    is.close();
    return responseEntity;
}
  • getRealPath如果参数为空字符串,则获取项目所在的真实路径

    • 如果是普通工程--》getRealPath获取到的是out目录下的路径

    • maven工程就在--》target目录下的路径

    • getRealPath参数如果为非空字符串,则会把该字符串拼接到项目所在真实路径后面(不管该路径存不存在)

    • File.separator可以获取当前系统适用的文件分割符(当搞不清该用"\还是/"的时候可以使用)

  • 输入流.available()获取当前输入流所对应的文件的所有的字节数(当前的文件有多少字节就创建多大的数组)

  • Content-Disposition来指定下载方式的请求头参数(头信息不区分大小写)

    • attachment;filename=为固定格式,attachment表示以附件的形式下载,filename指定下载的文件默认名字

文件上传
  • 文件上传要求form表单的请求方式必须为post,并且添加属性enctype="multipart/form-data"(以二进制的形式提交到服务器),表单中要声明文件域

  • SpringMVC中将上传的文件封装到MultipartFile对象中(要求该对象的命名要和文件域的name一致),通过此对象可以获取文件相关信息,该对象方法

    • transferTo(指定路径位置) 将文件上传到指定位置

    • getName 获取文件域的name

    • getOriginalFileName 上传文件的文件名(包含后缀名)

  • 配置文件解析器才能获取到MultipartFile对象

    • 属性

      • defaultEncoding 设置默认编码

      • maxUploadSize 设置最大上传尺寸

    • 注意事项

      • 获取该解析器的bean是通过id获取的,而不是类型,id必须为multipartResolver

      • 要导入jar包才能启动服务器

    <bean id="multipartResolver"class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="utf-8"></property>
    </bean>
    • 相应jar包

    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
  • 控制器方法

@RequestMapping("/test/up")
public String testUp(MultipartFile photo,HttpSession session) throws IOException {
    //获取上传的文件名
    String filename = photo.getOriginalFilename();
    //获取上下文对象
    ServletContext context = session.getServletContext();
    //获取当前工程下photo目录的真实路径
    String realPath = context.getRealPath("photo");
    //创建对应路径的文件对象,判断该路径是否存在
    File file=new File(realPath);
    if(!file.exists()){
        file.mkdirs();//不存在就创建一个目录
    }
    //文件上传路径
    String finalPath=realPath+File.separator+filename;
    //上传文件
    photo.transferTo(new File(finalPath));
    return "success";
}
  • 文件重名问题

    • 文件重名会导致原来文件的内容被覆盖了

    • 解决:对文件名进行重写

      1. 先获取文件后缀名(截取文件名中最后一个‘.’之后的字符串)

        String suffix = filename.substring(filename.lastIndexOf('.'));

      2. 获取uuid( 通用唯一标识码)

        String uuid = UUID.randomUUID().toString();

      3. 拼接成新的且唯一的文件名filename=uuid+suffix;

//获取上传的文件名
String filename = photo.getOriginalFilename();
//获取上次文件的后缀名
String  suffix = filename.substring(filename.lastIndexOf('.'));
String uuid = UUID.randomUUID().toString();
filename=uuid+suffix;
//获取上下文对象
ServletContext context = session.getServletContext();
//获取当前工程下photo目录的真实路径
String realPath = context.getRealPath("photo");
//创建对应路径的文件对象,判断该路径是否存在
File file=new File(realPath);
if(!file.exists()){
    file.mkdirs();//不存在就创建一个目录
}
//文件上传路径
String finalPath=realPath+File.separator+filename;
//上传文件
photo.transferTo(new File(finalPath));
return "success";

拦截器

SpringMVC中的拦截器
  • 用于拦截控制器方法的执行

  • 需要实现HandlerInterceptor接口

  • 必须在SpringMVC的配置文件中进行配置

    • 以下三种配置方式都是对DispatcherServlet所处理的所有请求进行拦截

方式一
<bean id="firstInterceptor" class="com.atguigu.interceptor.FirstInterceptor"></bean>
<mvc:interceptor>
    <ref bean="firstInterceptor"></ref>
</mvc:interceptor>
方式二
<mvc:interceptor>
    <bean class="com.atguigu.interceptor.FirstInterceptor"></bean>
    <!--由springmvc加载的,不需要设置id-->
</mvc:interceptor>
方式三:对拦截器的类加上注解
<context:component-scan base-package="interceptor"></context:component-scan>
<mvc:interceptor>
    <ref bean="标识的类名的小驼峰"></ref>
    <!--通过注解管理的bean的id默认值为该类名的小驼峰 -->
</mvc:interceptor>
  • 拓展配置

    • <mvc:mapping path="拦截路径"/>设置拦截路径

      • /* 表示上下文路径下的一层目录的请求(可以拦截 项目路径/test ,无法拦截 项目路径/test/hello)

      • /** 表示所有请求

    • <mvc:exclude-mapping path=""/>设置不拦截的资源的路径

<mvc:interceptor>
    <mvc:mapping path="/**"/>
    <mvc:exclude-mapping path="/testRequestEntity"/>
    <ref bean="firstInterceptor"></ref>
</mvc:interceptor>
<!--
以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过
mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求
-->
拦截器的三个抽象方法
  • 不管请求资源是否存在,这三个方法都会执行

  • preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法

  • postHandle:控制器方法执行之后执行postHandle()

  • afterCompletion:处理完视图和模型数据,渲染视图完毕之后执行afterCompletion()

多个拦截器的执行顺序
  1. 若每个拦截器的preHandle()都返回true

    • 此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:

      • preHandle()会按照配置的顺序执行

      • postHandle()和afterCompletion()会按照配置的反序执行

  2. 若某个拦截器的preHandle()返回了false

    • 该拦截器和它之前的拦截器的preHandle()都会执行postHandle()都不执行,返回false的拦截器之前的拦截器的afterCompletion()会执行

    • 源码中有个属性存储的是拦截到的位置(图一),在该位置之后的拦截器的afterCompletion()就不会执行了(因为图二源码是从拦截到的位置逆序从前遍历的)

      image-20230509144833650

image-20230509144928348

异常处理器

基于配置的异常处理
  • SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver

    • 实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver

  • SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver,使用方式

    • prop的键表示处理器方法执行过程中出现的异常

    • prop的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面的逻辑视图

    • exceptionAttribute属性设置异常信息的属性名将出现的异常信息在请求域中进行共享,页面中可以通过属性名获取异常信息

<bean
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
        <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <property name="exceptionAttribute" value="ex"></property>
</bean>
基于注解
  • @ControllerAdvice将当前类标识为异常处理的组件

  • 控制器方法中

    • @ExceptionHandler用于设置所标识方法处理的异常,该注解接收throwable及其子类的类型所组成的数组

    • 声明一个异常类型的形参来接收异常信息

    • 返回需要展示异常信息的页面

@ControllerAdvice
public class Exception {
    @ExceptionHandler({ArithmeticException.class,NullPointerException.class})
    public String solve(Throwable ex, Model model){
        model.addAttribute("ex",ex);
        return "error";
    }
}

注解配置springmvc

  • 在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。 Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成

  • Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展/继承了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文

  • 使用配置类和注解代替web.xml和SpringMVC配置文件的功能

配置类(代替web.xml)使用

  • 继承AbstractAnnotationConfigDispatcherServletInitializer ,重写抽象方法

    • getRootConfigClasses 指定一个配置类代替spring的配置文件

    • getServletConfigClasses 指定一个配置类代替springmvc的配置文件

    • getServletMappings 指定DispatcherServlet需要处理的请求路径,即url-pattern

    • getServletFilters 添加过滤器

      • 创建编码过滤器

      CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
      encodingFilter.setEncoding("UTF-8");
      encodingFilter.setForceRequestEncoding(true);
      • 创建处理请求参数的过滤器,要选用

        image-20230511151628841

      HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();

SpringConfig配置类

  • 要用@Configuration标识为配置类

WebConfig配置类

  • 需要实现WebMvcConfigurer接口

  • 扫描组件

    • 使用@ComponentScan(),括号里面写需要扫描的包

  • 视图解析器

    • 这里需要的各个形参的名字和@Bean标识的方法的方法名一致

    //配置生成模板解析器
    @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;
    }
  • 默认servlet处理静态资源

    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();//配置可用
    }
  • mvc的注解驱动

    • 使用@EnableWebMvc,开启mvc的注解驱动

  • 视图控制器

    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("需要处理的请求路径").setViewName("跳转到的逻辑视图");
    }
  • 文件上传解析器

    • @Bean作用:将标识方法的返回值作为bean交给ioc容器管理,bean的id为该方法的方法名

    @Bean
    public CommonsMultipartResolver multipartResolver(){
    	return new CommonsMultipartResolver();
    }
  • 拦截器

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        FirstInterceptor firstInterceptor=new FirstInterceptor();//自定义的拦截器
       registry.addInterceptor(拦截器对象).addPathPatterns("拦截的路径").excludePathPatterns("排除的目标");
    }
  • 异常解析器

        SimpleMappingExceptionResolver exceptionResolver=new SimpleMappingExceptionResolver();//spring提供的异常处理器对象
            Properties prop=new Properties();
            prop.setProperty("需要处理的异常","跳转到的逻辑视图");
            exceptionResolver.setExceptionMappings(prop);
            exceptionResolver.setExceptionAttribute("ex");//设置异常信息对象名
            resolvers.add(exceptionResolver);//添加异常处理器

SpringMvc的执行流程

springmvc的常用组件

  • DispatcherServlet:前端控制器,不需要工程师开发,由框架提供

    • 统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求

  • HandlerMapping:处理器映射器,不需要工程师开发,由框架提供

    • 根据请求的url、method等信息查找Handler,即控制器方法

  • Handler:处理器需要工程师开发

    • 在DispatcherServlet的控制下Handler对具体的用户请求进行处理

  • HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供

    • 通过HandlerAdapter对处理器(控制器方法)进行执行

  • ViewResolver:视图解析器,不需要工程师开发,由框架提供

    • 进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView

  • View:视图需要工程师开发

    • 将模型数据通过页面展示给用户

!!!SpringMVC的执行流程!!!

  1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获

  2. DispatcherServlet对请求URL进行解析(可以理解成资源在网络中的地址),得到请求资源标识符URI【不带协议、端口号、ip地址】(可以理解成资源在服务器中的地址),判断请求URI对应的映射:

    • 不存在

      • 再判断是否配置了mvc:default-servlet-handler(注意:如果没有配置注解驱动,则所有请求都被默认servlet处理,否则就先被前端控制器处理)

        • 如果没配置,则控制台报映射查找不到,客户端展示404错误

        image-20230511195302588

        • 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误

          image-20230511195636148

    • 存在则执行以下流程

  3. 根据该URI,调用HandlerMapping(匹配控制器方法)获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回

  4. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter(调用控制器方法)

  5. 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】

  6. 提取Request中的模型数据(请求报文),填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

    • HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息

    • 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等

    • 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等

    • 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中

  7. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象

  8. 此时将开始执行拦截器的postHandle(...)方法【逆向】

  9. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理,然后返回modelAndView对象)选择一个适合的ViewResolver进行视图解析,根据Model(请求域共享数据)和View(跳转的逻辑视图),来渲染视图

  10. 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】

  11. 将渲染结果返回给客户端

前端控制器生命周期

  • 以下内容有兴趣的话去资料里头搜索(源码什么的)

    • DispatcherServlet初始化过程

    • DispatcherServlet调用组件处理请求

ssm整合

  • springmvc和spring可以不整合,创建同一个ioc容器

  • 整合就各自管理各自的组件(springmvc的容器是子容器,spring的容器是父容器,子容器可以访问父容器的bean,反之不行)

    • springmvc的ioc容器是在前端控制器初始化时创建的,springmvc的ioc容器管理控制层组件

    • spring的ioc容器管理业务层(service)层组件以及除控制层组件以外的组件

    • 控制层组件依赖于业务层组件(控制层需要创建一个service的成员变量【需要自动装配】),spring的ioc容器创建时间一定要优先于springmvc的ioc容器的创建时间

    • 父子关系底层原理

    image-20230513143032680

  • 如果管理service组件的ioc容器不存在的话,为controller类执行自动装配时会报错

image-20230513142556215

ContextLoaderListener

  • 过滤器和监听器都是在前端控制器初始化前执行的,而过滤器又是用于过滤请求,所以采用监听器初始化时(只执行一次)创建spring的ioc容器即可

  • servletContextListener可以监听servletContext的状态(可以监听servleContext的启动和关闭)

  • Spring提供了监听器ContextLoaderListener,实现ServletContextListener接口,可监听ServletContext的状态,在web服务器的启动时,读取Spring的配置文件,创建Spring的IOC容器,web应用中必须在web.xml中配置

<listener>
    <!--
    配置Spring的监听器,在服务器启动时加载Spring的配置文件
    Spring配置文件默认位置和名称:/WEB-INF/applicationContext.xml
    可通过上下文参数自定义Spring配置文件的位置和名称
    -->
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--自定义Spring配置文件的位置和名称-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring.xml</param-value>
</context-param>

!!!准备工作(众神归位)!!!

导入依赖

  • 使用spring.version的方式可以统一设置spring导入依赖的版本号

  • 引入spring-jdbc是为了使用事务管理器,不是要用它作为持久层

<properties>
    <spring.version>5.3.1</spring.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!--springmvc-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- Mybatis核心 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    <!--mybatis和spring的整合包-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.6</version>
    </dependency>
    <!-- 连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.9</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    <!-- log4j日志 -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!-- 分页插件 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.2.0</version>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    <!-- ServletAPI -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
     <!-- 处理json数据 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.12.1</version>
    </dependency>
     <!-- 文件上传 -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
    <!-- Spring5和Thymeleaf整合包 -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.12.RELEASE</version>
    </dependency>
</dependencies>

配置web.xml

  • 一定要先配置编码过滤器,防止其它web对象提前获取参数而无法设置编码

<filter>
    <filter-name>CharacterEncoding</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncoding</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
    <!--
    配置Spring的监听器,在服务器启动时加载Spring的配置文件
    Spring配置文件默认位置和名称:/WEB-INF/applicationContext.xml
    可通过上下文参数自定义Spring配置文件的位置和名称
    -->
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--自定义Spring配置文件的位置和名称-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring.xml</param-value>
</context-param>
<servlet>
    <servlet-name>springMvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springMVC.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springMvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

配置springmvc

<!-- 自动扫描包 -->
<context:component-scan base-package="controller"></context:component-scan>
<!-- 配置Thymeleaf视图解析器 -->
<bean id="viewResolver"
      class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    <property name="order" value="1"/>
    <property name="characterEncoding" value="UTF-8"/>
    <property name="templateEngine">
        <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
            <property name="templateResolver">
                <bean
                        class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
                    <!-- 视图前缀 -->
                    <property name="prefix" value="/WEB-INF/templates/"/>
                    <!-- 视图后缀 -->
                    <property name="suffix" value=".html"/>
                    <property name="templateMode" value="HTML5"/>
                    <property name="characterEncoding" value="UTF-8"/>
                </bean>
            </property>
        </bean>
    </property>
</bean>
<!--视图控制器-->
<mvc:view-controller path="/" view-name="index"/>
<!--
处理静态资源,例如html、js、css、jpg
若只设置该标签,则只能访问静态资源,其他请求则无法访问
此时必须设置<mvc:annotation-driven/>解决问题
-->
<mvc:default-servlet-handler/>
<mvc:annotation-driven/>
<!--文件上传解析器-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8"></property>
</bean>
异常解析器和拦截器有需要的话就配置

配置spring

  • 配置文件中需要扫描除控制层以外的所有组件(可以使用以注解的方式进行排除)

  • 配置数据源注意,驱动那块要用driverClassName,如果用driver会报以下错误

    image-20230513192101746

<!--    扫描除控制层以外的组件-->
<context:component-scan base-package="ssm">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--    引入jdbc参数文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--    配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

配置mybatis环境

  • 配置mapper接口

  • 配置映射文件(要求所在包以及文件名要和接口的一致,还要保证命名空间和mapper接口的全类名一致)

  • 在spring的配置文件中实现spring整合mybatis

    • 配置SqlsessionFactoryBean(不需要设置id),可以获取SqlsessionFactory的对象

      • 在该bean中可以进行mybatis的核心配置,其常用配置有

        • configLocation设置mybatis的核心配置文件的路径(可以不设置,设置的话要求里面的配置和spring中的不重复)

        • dataSource配置连接数据库的环境

        • typeAliasesPackage设置类型别名所对应的包

        • plugins配置插件(其类型是一个长度可变的参数,可以用数组配置)

        • configurationProperties设置全局配置

        • mapperLocations配置映射文件的路径(在配置了basePackage标签的情况下,只有mapper接口和xml文件所在包不一致才需要设置该标签;如果没有配置basePackage,就一定要配置mapperLocations)

      <bean class="org.mybatis.spring.SqlSessionFactoryBean">
      <!--<property name="configLocation" value="classpath:mybatis-config.xml"/>-->
          <property name="dataSource" ref="dataSource"/>
          <property name="typeAliasesPackage" value="ssm/pojo"/>
          <property name="plugins">
              <array>
      <!--分页插件引入-->
                  <bean class="com.github.pagehelper.PageInterceptor"/>
              </array>
          </property>
      <!--<property name="mapperLocations" value="classpath:ssm/mapper/*.xml"/>-->
      </bean>
    • 配置MapperScannerConfigurer(不需要设置id),用于扫描mapper接口,来创建这些接口的代理实现类对象交给ioc管理

      • 就可以不用通过sqlsession获取mapper对象那一系列繁琐操作啦~

      <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
          <property name="basePackage" value="ssm.mapper"/>
      </bean>

配置事务

  • 事务管理是针对于连接对象的,连接对象又是数据源管理的,所有要引用数据源

  • 如果事务管理器的id为transactionManager,开启事务的注解驱动可以不配置transaction-manager属性

    • 开启事务的注解驱动将@Transactional标识的方法或类中的所有方法(连接点 )进行事务管理

  • 在spring配置文件中配置

    <!--配置事务管理器-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    <!--开启事务的注解驱动-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

测试整合(rest风格)

列表功能

  • html(视图层)

    • 设置流水号/序号,在循环中设置status数据(当前循环的状态对象,其属性count表示当前循环的次数,first/last是否为第一次/最后一次循环,index当前循环的索引,even/odd判断当前循环是否为偶/奇数,size获取集合的长度)

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>员工列表</title>
    </head>
    <link rel="stylesheet" th:href="@{/static/css/index_work.css}"/>
    <body>
    <table>
      <tr>
        <th colspan="6">员工列表</th>
      </tr>
      <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年龄</th>
        <th>性别</th>
        <th>邮箱</th>
        <th>操作</th>
      </tr>
      <tr th:each="employee,status :${list}">
        <td th:text="${status.count}"></td>
        <td th:text="${employee.empName}"></td>
        <td th:text="${employee.age}"></td>
        <td th:text="${employee.gender}"></td>
        <td th:text="${employee.email}"></td>
        <td>
          <a th:href="@{#}">删除</a>
          <a th:href="@{#}">修改</a>
        </td>
      </tr>
    </table>
    </body>
    </html>
  • 控制层

    @Autowired
    private EmployeeService employeeService;
    @RequestMapping(value = "/employee",method = RequestMethod.GET)
    public String getAllEmployee(Model model){
        List<Employee> list = employeeService.getAll();
        model.addAttribute("list",list);
        return "employee_list";
    }
  • 业务层

    @Autowired
    private EmployeeMapper employeeMapper;
    @Override
    public List<Employee> getAll() {
        List<Employee> list = employeeMapper.getAllEmployee();
        return list;
    }
  • 持久层

    <select id="getAllEmployee" resultType="Employee">
        select * from t_emp;
    </select>

分页功能

  • 根据传输的当前页的页码,来获取分页相关信息(获取分页信息也是get,防止路径和查询某个员工信息相同要加 page加以区分)

    @RequestMapping(value = "/employee/page/{pageNum}",method = RequestMethod.GET)
    public String getAllEmployeeByPageNum(@PathVariable("pageNum") Integer pageNum, Model model){
        PageInfo<Employee> page = employeeService.getPage(pageNum);
        model.addAttribute("page",page);
        return "employee_list";
    }
  • 在查询所有员工之前开启分页功能

    • 注意开启分页功能相当于给下面的查询语句拼接了一个limit,如果该查询语句以;结尾将会报以下错误

    image-20230513202810826

    //开启分页功能
    PageHelper.startPage(pageNum,4);
    List<Employee> list = employeeMapper.getAllEmployee();
    PageInfo<Employee> pageInfo = new PageInfo<>(list, 5);
    return pageInfo;
  • 分页数据存在pageInfo的list元素中(和查询列表跳转的页面是不一样的)

    <tr th:each="employee,status :${page.list}">
      <td th:text="${status.count}"></td>
      <td th:text="${employee.empName}"></td>
      <td th:text="${employee.age}"></td>
      <td th:text="${employee.gender}"></td>
      <td th:text="${employee.email}"></td>
      <td>
        <a th:href="@{#}">删除</a>
        <a th:href="@{#}">修改</a>
      </td>
  • 分页导航栏

    • pageInfo中的

      • hasPreviousPage可以判断是否存在上一页

      • prePage/nextPage表示上/下一页的页码(th:href的@{}中不能使用${page.prePage},因为会把该表达式当成字符串处理,可以使用拼接处理)

      • 首页可以用1表示,尾页可以用总页数表示

    • 标识当前所在页码,导航分页中判断当前循环的页码是否为当前页的页码,是就特殊标记

<div style="text-align: center">
<!--  分页导航栏-->
  <a th:if="${page.hasPreviousPage}" th:href="@{'/employee/page/'+${page.pageNum}}">首页</a>
  <a th:if="${page.hasPreviousPage}" th:href="@{'/employee/page/'+${page.prePage}}">上一页</a>
  <span th:each="num:${page.navigatepageNums}">
    <a th:if="${page.pageNum==num}" style="color:red" th:href="@{'/employee/page/'+${page.pageNum}}" th:text="'['+${num}+']'"></a>
    <a th:if="${page.pageNum!=num}" th:href="@{'/employee/page/'+${num}}" th:text="${num}"></a>
  </span>
  <a th:if="${page.hasNextPage}" th:href="@{'/employee/page/'+${page.nextPage}}">下一页</a>
  <a th:if="${page.hasNextPage}" th:href="@{'/employee/page/'+${page.pages}}">尾页</a>
</div>

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值