CategoryJava article

MyBatis

  1. MyBatis 开发步骤:

    • 添加 MyBatis 的坐标

      <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
      </dependency>
    • 创建 user 数据表
    • 创建 User 实体类
    • 编写映射文件 UserMapper.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="userMapper">
      
          <select id="findAll" resultType="com.domain.User">
              select * from user
          </select>
      
      </mapper>
    • 编写核心文件 SqlMapConfig.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/test"/>
                      <property name="username" value="root"/>
                      <property name="password" value="jie13727507037"/>
                  </dataSource>
              </environment>
          </environments>
      
          <!-- 加载映射文件 -->
          <mappers>
              <mapper resource="com.mapper\UserMapper.xml"/>
          </mappers>
      
      </configuration>
    • 编写测试类

      public void test1() throws IOException {
          // 加载核心配置文件
          InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
          // 获得 SQLSession 工厂对象
          SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
          // 获得 session 会话对象
          SqlSession sqlSession = sqlSessionFactory.openSession();
          // 执行操作     参数:namespace+语句id
          List<User> userList = sqlSession.selectList("userMapper.findAll");
          // 打印数据
          System.out.println(userList);
          // 释放资源
          sqlSession.close();
      }
  2. 映射文件概述

    image-20220630141304364

  3. MyBatis 的插入操作注意问题

    • 插入语句使用 insert 标签
    • 在映射文件中使用 parameterType 属性指定要插入的数据类型
    • Sql 语句中使用 #{实例属性名} 的方式引用实体中的属性值
    • 插入操作使用的 API 是 sqlSession.insert("namespace.id", 实体对象);
    • 插入操作涉及数据库数据变化,所以要使用 sqlSession对象显示的提交事务,即 sqlSession.commit()
    <?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="userMapper">
    
        <!-- 查询操作 -->
        <select id="findAll" resultType="com.domain.User">
            select * from user
        </select>
    
        <!-- 插入操作 -->
        <insert id="save" parameterType="com.domain.User">
            insert into user values(#{id}, #{username}, #{password})
        </insert>
    
    </mapper>
    public void test2() throws IOException {
    
        // 模拟 user 对象
        User user = new User();
        user.setUsername("tom");
        user.setPassword("abc");
    
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得 SQLSession 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获得 session 会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行操作     参数:namespace+语句id
        sqlSession.insert("userMapper.save", user);
    
        // mybatis 执行更新操作  需要提交事务
        sqlSession.commit();
    
        // 释放资源
        sqlSession.close();
    }
  4. MyBatis 修改操作注意问题

    • 修改语句使用 update 标签
    • 修改操作使用的 API 是 sqlSession.update("namespace.id", 实体对象);
    <!-- 修改操作 -->
    <update id="update" parameterType="com.domain.User">
        update user set username=#{username}, password=#{password} where id=#{id}
    </update>
    public void test3() throws IOException {
    
        // 模拟 user 对象
        User user = new User();
        user.setId(7);
        user.setUsername("lucy");
        user.setPassword("123");
    
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得 SQLSession 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获得 session 会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行操作     参数:namespace+语句id
        sqlSession.update("userMapper.update", user);
    
        // mybatis 执行更新操作  需要提交事务
        sqlSession.commit();
    
        // 释放资源
        sqlSession.close();
    }
  5. MyBatis 删除操作注意问题

    • 删除语句使用 delete 标签
    • Sql 语句中使用 #{任意字符串} 方式引用传递的单个参数
    • 删除操作使用的 API 是 sqlSession.delete("namespace.id", Object);
    <!-- 删除操作 -->
    <delete id="delete" parameterType="java.lang.Integer">
        delete from user where id=#{id}
    </delete>
    public void test4() throws IOException {
    
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 获得 SQLSession 工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        // 获得 session 会话对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 执行操作     参数:namespace+语句id
        sqlSession.delete("userMapper.delete", 7);
    
        // mybatis 执行更新操作  需要提交事务
        sqlSession.commit();
    
        // 释放资源
        sqlSession.close();
    }
  6. MyBatis 核心配置文件常用配置

    • properties 标签:改标签可以加载外部的 properties 文件

      <!-- 加载外部 properties 文件 -->
      <properties resource="jdbc.properties"/>
    • typeAliases 标签:设置类型别名

      <!-- 定义别名 -->
      <typeAliases>
          <typeAlias type="com.domain.User" alias="user"/>
      </typeAliases>
    • mappers 标签:加载映射配置

      <!-- 加载映射文件 -->
      <mappers>
          <mapper resource="com.mapper/UserMapper.xml"/>
      </mappers>
    • environments 标签:数据源环境配置标签

      <!-- 配置数据源环境 -->
      <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>
  7. MyBatis 相应 API

    • SqlSession 工厂构造器 SqlSessionFactory build(InputStream inputstream)

      // 加载核心配置文件
      InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
      // 获得 SQLSession 工厂对象
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    • SqlSession 工厂对象 SqlSessionFactory

      • openSession():会默认开启一个事务,但事务不会自动提交,也就是一位置需要手动提交该事务,更新操作数据才会持久化到数据库中
      • openSession(boolean autoCommit):参数为是否自动提交,如果设置为 true,那么不需要手动提交事务
      // 获得 session 会话对象
      SqlSession sqlSession = sqlSessionFactory.openSession();
    • SqlSession 会话对象

      • 执行语句的方法

        // 查询单个对象
        T selectOne(String statement, Object parameter);
        // 查询全部
        List<E> selectList(String statement, Object parameter);
        // 插入
        int insert(String statement, Object parameter);
        // 修改
        int update(String statement, Object parameter);
        // 删除
        int delete(String statement, Object parameter);
      • 操作事务的方法

        void.commit();     // 事务提交
        void rollback(); // 事务回滚
  8. MyBatis DAO 层实现

    • 代理开发方式

      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      • Mapper 接口开发需要遵循以下规范

        1. Mapper.xml 文件中的 namespace 与 mapper 接口的全限定名相同
        2. Mapper 接口方法名和 Mapper.xml 中定义的每个 statement 的 id 相同
        3. Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同
        4. Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同
  9. MyBatis 映射文件深入

    • 动态 sql —— if

      <select id="findByCondition" resultType="user" parameterType="user">
          select * from user
          <where>
              <if test="id!=0">
                  id=#{id}
              </if>
              <if test="username!=null">
                  and username=#{username}
              </if>
              <if test="password!=null">
                  and password=#{password}
              </if>
          </where>
      </select>
    • 动态 sql —— foreach

      <select id="findByIds" parameterType="list" resultType="user">
          select * from user
          <where>
              <foreach collection="list" open="id in(" close=")" item="id" separator=",">
                  #{id}
              </foreach>
          </where>
      </select>
    • 动态 sql —— sql 语句的抽取

      <!-- sql 语句的抽取 -->
      <sql id="selectUser">select * from user</sql>
      
          <include refid="selectUser"></include>
  10. MyBatis 核心配置文件深入

    • typeHandlers 标签

      • 开发步骤

        1. 定义转换类继承类 BaseTypeHandler<T> 
        2. 覆盖 4 个未实现的方法,其中 setNonNullParameter 为 java 程序设置数据到数据库的回调方法,getNullableResult 为查询时 mysql 的字符串类型转换成 java 的 Type 类型的方法
        3. 在 MyBatis 核心配置文件中个进行注册
        4. 测试转换是否正确
    • plugins 标签

      • 开发步骤

        1. 导入通用 PageHelper 的坐标
        2. 在 mybatis 核心配置文件中配置 PageHelper 插件
        3. 测试分页数据获取
  11. MyBatis 多表操作

    1. 一对一

      • 第一种配置
     

     ```xml
     <resultMap id="orderMap" type="order">
         <!-- 手动指定字段与实体的映射关系
              column: 数据表的字段名称
              property: 实体的属性名称
          -->
         <id column="oid" property="id"/>
         <result column="ordertime" property="orderTime"/>
         <result column="total" property="total"/>
         <result column="uid" property="user.id"/>
         <result column="username" property="user.username"/>
         <result column="password" property="user.password"/>
         <result column="birthday" property="user.birthday"/>
     </resultMap>
     
     <select id="findAll" resultMap="orderMap">
         select *, o.id oid from orders o, user u where o.id=u.id
     </select>
     ```

   - 第二种配置

     ```xml
     <resultMap id="orderMap" type="order">
         <!-- 手动指定字段与实体的映射关系
              column: 数据表的字段名称
              property: 实体的属性名称
          -->
         <id column="oid" property="id"/>
         <result column="ordertime" property="orderTime"/>
         <result column="total" property="total"/>
         <!--<result column="uid" property="user.id"/>
         <result column="username" property="user.username"/>
         <result column="password" property="user.password"/>
         <result column="birthday" property="user.birthday"/>-->
     
         <!--
             property: 当前实体的属性名称
             javaType: 当前实体中的属性的类型
         -->
         <association property="user" javaType="user">
             <id column="uid" property="id"/>
             <result column="username" property="user.username"/>
             <result column="password" property="user.password"/>
             <result column="birthday" property="user.birthday"/>
         </association>
     </resultMap>
     
     <select id="findAll" resultMap="orderMap">
         select *, o.id oid from orders o, user u where o.id=u.id
     </select>
     ```

2. 一对多

   - ```xml
     <resultMap id="userMap" type="user">
         <id column="id" property="id"/>
         <result column="username" property="username"/>
         <result column="password" property="password"/>
         <result column="birthday" property="birthday"/>
     
         <!-- 配置集合信息
              property: 集合名称
              ofType: 当前集合中的数据类型
          -->
         <collection property="orderList" ofType="order">
             <id column="oid" property="id"/>
             <result column="ordertime" property="orderTime"/>
             <result column="total" property="total"/>
         </collection>
     </resultMap>
     
     <select id="findAll" resultMap="userMap">
         select *, o.id oid from user u, order o where u.id=o.uid
     </select>
     ```

3. 多对多

   - ```xml
     <resultMap id="userRoleMap" type="user">
         <id column="userId" property="id"/>
         <result column="username" property="username"/>
         <result column="password" property="password"/>
         <result column="birthday" property="birthday"/>
         
         <collection property="roleList" ofType="role">
             <id column="roleId" property="id"/>
             <result column="roleName" property="roleName"/>
             <result column="roleDesc" property="roleDesc"/>
         </collection>
     </resultMap>
     
     <select id="findUserAndRoleAll" resultMap="userRoleMap">
         select * from user u, sys_user_role ur, sys_role r where u.id=ur.userId and ur.roleId
     </select>
     ```
  1. MyBatis 注解开发

    • 常用注解

      • @Insert:实现新增
      • @Update:实现更新
      • @Delete:实现删除
      • @Select:实现查询
      • @Result:实现结果集封装
      • @Results:可以与 @Result 一起使用,封装多个结果集
      • @One:实现一对一结果封装
      • @Many:实现一对多结果集封装
    • 简单查询

      package com.mapper;
      
      import com.domain.User;
      import org.apache.ibatis.annotations.Delete;
      import org.apache.ibatis.annotations.Insert;
      import org.apache.ibatis.annotations.Select;
      import org.apache.ibatis.annotations.Update;
      
      import java.util.List;
      
      public interface UserMapper {
      
          @Insert("insert into user values(#{id}, #{username}, #{password}, #{birthday})")
          void save(User user);
      
          @Delete("delete from user where id=#{id}")
          void delete(int id);
      
          @Update("update user set username=#{username}, password=#{password} where id=#{id}")
          void update(User user);
      
          @Select("select * from user where id=#{id}")
          User findById(int id);
      
          @Select("select * from user")
          List<User> findAll();
      
      }
      <!-- 加载映射关系 -->
      <mappers>
          <!-- 指定一个包下的接口 -->
          <package name="com.mapper"/>
      </mappers>
    • 复杂查询 —— 一对一

      两种方式:

      • package com.mapper;
        
        import com.domain.Order;
        import org.apache.ibatis.annotations.Result;
        import org.apache.ibatis.annotations.Results;
        import org.apache.ibatis.annotations.Select;
        
        import java.util.List;
        
        public interface OrderMapper {
        
            @Select("select *, o.id oid from orders o, user u where o.id=u.id")
            @Results({
                    @Result(column = "oid", property = "id"),
                    @Result(column = "ordertime", property = "orderTime"),
                    @Result(column = "total", property = "total"),
                    @Result(column = "uid", property = "user.id"),
                    @Result(column = "username", property = "user.username"),
                    @Result(column = "password", property = "user.password")
            })
            List<Order> findAll();
        
        }
      • @Select("select * from orders")
        @Results({
                @Result(column = "id", property = "id"),
                @Result(column = "ordertime", property = "orderTime"),
                @Result(column = "total", property = "total"),
                @Result(
                        javaType = User.class,  // 要封装的实体类型
                        column = "uid",            // 根据哪个字段去查询 user 表的数据
                        property = "user",       // 要封装的属性名
                        // select 属性 代表查询哪个接口的方法获得数据
                        one = @One(select = "com.mapper.UserMapper.findById")
                )
        })
        List<Order> findAll();
    • 复杂查询——一对多

      @Select("select * from orders where uid=#{uid}")
      List<Order> findByUid(int uid);
      @Select("select * from user")
      @Results({
              @Result(id = true, column = "id", property = "id"),
              @Result(column = "username", property = "username"),
              @Result(column = "password", property = "password"),
              @Result(
                      javaType = List.class,  // 要封装的实体类型
                      column = "id",            // 根据哪个字段去查询 user 表的数据
                      property = "orderList",       // 要封装的属性名
                      // select 属性 代表查询哪个接口的方法获得数据
                      many = @Many(select = "com.mapper.OrderMapper.findByUid")
              )
      })
      List<User> findUserAndOrderAll();
    • 复杂查询 —— 多对多

Spring Boot 与 Web 开发

  1. SpringMVC 快速使用

    • 基于 restful http 接口的 CURD

      @RestController
      @RequestMapping("/user")
      public class UserController {
      
          @Autowired
          UserService userService;
      
          @GetMapping("/{id}")
          public Result getUser(@PathVariable Integer id) {
              User user = userService.getUserById(id);
              return new Result<>(200, "查询成功", user);
          }
      
          @PostMapping("/add")
          public Result getUser(User user) {
              userService.add(user);
              return new Result<>(200, "添加成功");
          }
      
          @PutMapping("/{id}")
          public Result editUser(User user) {
              userService.update(user);
              return new Result(200, "修改成功");
          }
      
          @DeleteMapping("/{id}")
          public Result deleteUser(@PathVariable Integer id) {
              userService.delete(id);
              return new Result<>(200, "删除成功");
          }
      }
    • 调用 rest http 接口

      • 通过 restTemplate 调用

        RestTemplate 类可用于在应用中调用 rest 服务,它简化了与 http 服务的通信方式,统一了 RESTful 的标准,封装了 http 链接,我们只需要传入 url 及返回值类型即可。

        适用于微服务架构下,服务之间的远程调用。 ps:以后使用微服务架构,使用 spring cloud feign 组件

        restTemplate 和 webClient 都可以调用远程服务,区别:webclient 依赖 webflux,webclient 请求远程服务是无阻塞的,响应的。RestTemplate 是阻塞的,需要等待请求响应后才能执行下一句代码。

        DELETEdelete
        GETgetForObject 按照指定 Class 返回对象
        getForEntity 返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
        HEADheadForHeaders
        OPTIONSoptionForAllow
        POSTpostForLocaltion
        postForObject
        PUTput
        any
        支持任何请求方法类型
        exchange
        execute
        • 基于 restTemplate 调用查询

          // 声明了 RestTemplate
          private final RestTemplate restTemplate;
          
          // 当 Bean 没有无参构造函数的时候, spring 将自动拿到有参的构造函数,参数进行自动注入
          public OrderController(RestTemplateBuilder restTemplateBuilder) {
              this.restTemplate = restTemplateBuilder.build();
          }
          
          @RequestMapping("/order")
          public String order() {
              Result forObject = restTemplate.getForObject("http://localhost:8080/user/{id}", Result.class, 1);
          
              return forObject.toString();
          }
        • 基于 restTemplate 调用查询

          @RequestMapping("/order")
          public String order() {
              //基于 restTemplate 调用查询
              User user = new User("AZhang", "wyu");
              // url: 请求的远程 rest url
              // object: post 请求的参数
              // Class<T>: 返回的类型
              // ...Object: 是 @PathVariable 占位符的参数
              ResponseEntity<Result> resultResponseEntity = restTemplate.postForEntity("http://localhost:8080/user/add", user, Result.class);
              System.out.println(resultResponseEntity);
              return resultResponseEntity.getBody().toString();
          }
        • 基于 restTemplate 调用修改

          @RequestMapping("/order")
          public String order() {
              //基于 restTemplate 调用修改
              User user = new User(1, "AZhang", "wyu");
              //restTemplate.put("http://localhost:8080/user/{id}", user, Result.class);
              HttpEntity<User> httpEntity = new HttpEntity<>(user);
              ResponseEntity<Result> resultResponseEntity = restTemplate.exchange("http://localhost:8080/user/{id}", HttpMethod.PUT, httpEntity, Result.class, 1);
              System.out.println(resultResponseEntity);
              return resultResponseEntity.getBody().toString();
          }
        • 基于 restTemplate 调用删除

          @RequestMapping("/order")
          public String order() {
              //基于 restTemplate 调用删除
              ResponseEntity<Result> resultResponseEntity = restTemplate.exchange("http://localhost:8080/user/{id}", HttpMethod.DELETE, null, Result.class, 1);
              System.out.println(resultResponseEntity);
              return resultResponseEntity.getBody().toString();
          }
     

 - 通过 postman 调用

 - 通过 MockMvc 测试

   MockMvc 是由 spring-test 包提供,实现了对 Http 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
  1. SpringMVC 自动配置原理分析

    ​ Spring Boot 为 Spring MVC 提供了自动配置,可与大多数应用程序完美配合。

    自动配置在 Spring 的默认值之上添加了以下功能:

    • 包含 ContentNegotiatingViewResolverBeanNameViewResolver

      • ViewResolver 都是 SpringMVC 内置的视图解析器

        • ContentNegotiatingViewResolver

          • 不会解析视图,而是委派给其他视图解析器进行解析。
          • 所有的视图解析器,都会根据返回的视图名称进行解析视图 resolveViewName

            public View resolveViewName(String viewName, Locale locale) throws Exception {
                RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
                Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
                List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
                if (requestedMediaTypes != null) {
                    // 获得所有匹配的视图
                    List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
                    // 获取最终的这个
                    View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
                    if (bestView != null) {
                        return bestView;
                    }
                }

            委派给其他视图解析器进行解析:

            @Override
            protected void initServletContext(ServletContext servletContext) {
                Collection<ViewResolver> matchingBeans =
                        BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
                if (this.viewResolvers == null) {
                    this.viewResolvers = new ArrayList<>(matchingBeans.size());
                    for (ViewResolver viewResolver : matchingBeans) {
                        if (this != viewResolver) {
                            this.viewResolvers.add(viewResolver);
                        }
                    }
                }
        • BeanNameViewResolver

          • 会根据 handler 方法返回的视图名称,去 IOC 容器中找到名字叫 azhang 的一个 Bean,并且这个 Bean要实现了 View接口。
          • 示例:

            @GetMapping("/test")
            public String getUser() {
                return "azhang";
            }

            可以配置一个名字叫 azhang 的视图(View)

            @Component
            public class Azhang implements View {
                @Override
                public String getContentType() {
                    return "text/html";
                }
            
                @Override
                public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
                    response.getWriter().print("Welcome to AzhangView");
                }
            }
       

   - 由以上代码可以得出结论,它是从 Spring IOC 容器获得 ViewResolve 类型 Bean,name我们可以自己定制一个 ViewResolver,ContentNegotiatingViewResolver 也会帮我们委派解析:

     ```java
     @Bean
     public ViewResolver AZhangViewResolve() {
         InternalResourceViewResolver resolver = new InternalResourceViewResolver();
         resolver.setPrefix("/");
         resolver.setSuffix(".html");
         return resolver;
     }
     ```

     ![image-20220506164908963](https://static-upyun.hackerjk.top/markdown/image-20220506164908963.png!)
  • 支持提供静态资源,包括对 WebJars 的支持。

    • 以前要访问 jpg / css / js 等这些静态资源文件,需要在 web.xml 配置,但现在在 SpringBoot 中不需要配置,只需要放在约定文件夹中就可以(约定大于配置)
    • 原理:

      • WebJars:就是将静态资源放在 jar 包中进行访问
      • WebJars 官网:https://www.webjars.org/
      • @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
                return;
            }
            addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
            addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
                registration.addResourceLocations(this.resourceProperties.getStaticLocations());
                if (this.servletContext != null) {
                    ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
                    registration.addResourceLocations(resource);
                }
            });
        }
        
        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
            addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
        }
        
        private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
                Consumer<ResourceHandlerRegistration> customizer) {
            if (registry.hasMappingForPattern(pattern)) {
                return;
            }
            ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
            customizer.accept(registration);
            registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
            registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
            registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
            customizeResourceHandlerRegistration(registration);
        }
      • 当访问 /webjars/** 时,就会去 classpath:/META-INF/resources/webjars/ 对应进行映射

        当访问 http://localhost:8080/webjars/jquery/3.5.1/jquery.js 对应映射到 /META-INF/resources/webjars/jquery/3.5.1/jquery.js

        image-20220506171959211

        • 在 static 文件中访问的静态资源,默认的四个静态资源路径 (对应的映射地址) :
        { "classpath:/META-INF/resources/",
                "classpath:/resources/",
                "classpath:/static/",
    • 配置首页欢迎页:

      private Resource getWelcomePage() {
          for (String location : this.resourceProperties.getStaticLocations()) {    // 拿到上面静态资源地址
              Resource indexHtml = getIndexHtml(location);
              if (indexHtml != null) {
                  return indexHtml;
              }
          }
          ServletContext servletContext = getServletContext();
          // 去里面找一个 index.html 的首页文件
          if (servletContext != null) {
              return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));
          }
          return null;
      }
      
      private Resource getIndexHtml(String location) {
          return getIndexHtml(this.resourceLoader.getResource(location));
      }
      
      private Resource getIndexHtml(Resource location) {
          try {
              Resource resource = location.createRelative("index.html");
              if (resource.exists() && (resource.getURL() != null)) {
                  return resource;
              }
          }
          catch (Exception ex) {
          }
          return null;
      }
    • 也可以通过配置文件指定具体的静态资源地址:

  • 自动注册 ConverterGenericConverterFormatter Bean 类。

    • 使用方式
  • 支持 HttpMessageConverters

    • HttpMessageConverters 负责 http 请求和响应的报文处理。
    • image-20220507011511095
  • 自动注册 MessageCodesResolver

    • 修改 4xx 错误下格式转换出错、类型转换出错的错误代码
    • 以前的格式是 errorCode + "." + object name + "." + field

      例:typeMismatch.user.birthday

    • 可以通过 spring.mvc.message-codes-resolver-format=postfix_error_code 将格式修改为:object name + "." + field + "." + errorCode
  • 静态 index.html 支持。

    • 在 springboot 中可以直接返回 html 的视图,因为在 WebMvcAutoConfiguration 配置类配置了:

      @Bean
      @ConditionalOnMissingBean
      public InternalResourceViewResolver defaultViewResolver() {
          InternalResourceViewResolver resolver = new InternalResourceViewResolver();
          resolver.setPrefix(this.mvcProperties.getView().getPrefix());
          resolver.setSuffix(this.mvcProperties.getView().getSuffix());
          return resolver;
      }

      所以可以通过在配置文件中完成:

      spring.mvc.view.prefix=/pages/
      spring.mvc.view.suffix=.html
  • 自动使用 ConfigurableWebBindingInitializer bean。

    1. 定制 SpringMVC 的自动配置

      SpringMVC 的自动配置类:WebMvcAutoConfiguration

  • 在大多数情况下,SpringBoot 在自动配置类中标记了很多 @ConditionalOnMissingBean(xxxxxxxxxx.class) ;(意思就是如果容器中没有,当前的 Bean 才会生效)。只需要在自己的配置类中配置对应的一个 @Bean 就可以覆盖默认自动配置。
  • 通过 WebMvcConfigurer 进行拓展

    • 扩展视图控制器
    • 扩展拦截器
    • 扩展全局 CORS
    • @Configuration
      public class MyWebMvcConfigurer implements WebMvcConfigurer {
          /*@Override
          public void configurePathMatch(PathMatchConfigurer configurer) {
              configurer.setUseTrailingSlashMatch(false);
          }*/
      
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(new TimeInterceptor())      // 添加拦截器
                  .addPathPatterns("/**")      // 拦截映射规则  /**拦截所有的请求
                  .excludePathPatterns("/pages/**");        // 排除 pages 下面所有的请求
          }
      
          /**
           *  CORS 配置
           *  全局跨域请求配置
           */
          /*@Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/user/*")       // 映射服务器中哪些 http 接口允许运行跨域访问
                      .allowedOrigins("http://localhost:8081")                // 配置哪些来源有权限跨域
                      .allowedMethods("GET", "POST", "PUT", "DELETE");        // 配置允许跨域访问的请求方法
          }*/
      
          @Bean
          public InternalResourceViewResolver AZhangViewResolver() {
              InternalResourceViewResolver resolver = new InternalResourceViewResolver();
              resolver.setPrefix("/pages");
              resolver.setSuffix(".html");
              return resolver;
          }
      }
    • WebMvcConfigurer 原理

      实现 WebMvcConfigurer 接口可以扩展 Mvc 实现,又既保留了 SpringBoot 的自动配置

      • WebMvcAutoConfiguration 也有一个实现了 WebMvcConfigurer 的配置类
      • WebMvcAutoConfigurationAdapter 它也是利用这种方式去进行扩展,所以我们通过查看这个类我们发现它帮我们实现了其他不常用的方法,帮助我们进行自动配置,我们只需要定制(拦截器、视图控制器、CORS 等在开发中需要额外定制的功能)

        @Configuration(proxyBeanMethods = false)
        @Import(EnableWebMvcConfiguration.class)
        @EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
        @Order(0)
        public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
      • 导入了 EnableWebMvcConfiguration

        @Import(EnableWebMvcConfiguration.class)

        EnableWebMvcConfiguration 它的父类上 setConfigurers 使用了 @AutoWired 注解

        • 它会去容器中将所有实现了 WebMvcConfigurer 接口的 Bean 都自动注入进来,添加到 configurers 变量中
      public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
      
          private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
      
          @Autowired(required = false)
          public void setConfigurers(List<WebMvcConfigurer> configurers) {
              if (!CollectionUtils.isEmpty(configurers)) {
                  this.configurers.addWebMvcConfigurers(configurers);
              }
          }
      ```
    
    - 添加到 ``delegates`` 委派器中
    
      ```java
      public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
          if (!CollectionUtils.isEmpty(configurers)) {
              this.delegates.addAll(configurers);
          }
      }
      ```
    
    - 底层调用 WebMvcConfigurer 对应的方法时,就是去拿到之前注入到 delegates 的 WebMvcConfigurer,依次调用
    
      ```java
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          for (WebMvcConfigurer delegate : this.delegates) {
              delegate.addInterceptors(registry);
          }
      }
      ```
    
    • 当添加了 @EnableWebMvc 就不会使用 SpringMVC 自动配置类的默认配置了

      原理:

      • 在 EnableMvc 中 @Import(DelegatingWebMvcConfiguration.class)

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(DelegatingWebMvcConfiguration.class)
        public @interface EnableWebMvc {
        }
      • 在 DelegatingWebMvcConfiguration 中继承了 WebMvcConfigurationSupport

        @Configuration(proxyBeanMethods = false)
        public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
      • 在 WebMvcAutoConfiguration 中 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

        ​ 当容器中不存在 WebMvcConfigurationSupport 这个 Bean 的时候自动配置类才会生效

        ​ 正因为通过 @EnableWebMvc 导入了 DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport 从而才使自动配置类失效

        @Configuration(proxyBeanMethods = false)
        @ConditionalOnWebApplication(type = Type.SERVLET)
        @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
        @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
        @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
        @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
                ValidationAutoConfiguration.class })
        public class WebMvcAutoConfiguration {
  • Json 开发

    • Spring Boot 提供了与三个 JSON 映射库的集成:

      • Gson

        • Jackson 性能最好
      • JSON-B

      Jackson 是我们使用的默认 json 库

    • jackson 的使用

      • @JsonIgnore

        • 进行排除 json 序列化,将它标注在属性上将不会进行 json 格式化
  • 国际化
  • 统一异常处理
  1. SpringBoot 的嵌入式 Servlet 容器

热部署与日志

  1. springboot 中 devtools 热部署

    1. 1 引言

      ​ 为了进一步提高开发效率,springboot 为我们提供了全局项目热部署,日后在开发过程中修改部分代码以及相关配置文件后,不需要每次重启使修改生效,在项目中开启了 springboot 全局热部署之后只需要在修改之后等待几秒即可使修改生效。

    2. 2 开启热部署

      1. 21 项目中引入依赖

        <!-- 1. devtools 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
      2. 22 当我们修改了类文件后,idea 不会自动编译,得修改 idea 设置。

        • File-Setting-Compiler-Build Project automatically

          image-20220505164635699

        • ctrl + shift + alt + /,选择 Registry,勾上 Compiler autoMake allow when app running

          image-20220505164651913

          image-20220505164728718

  2. 带你弄清混乱的 JAVA 日志体系

    1. 1 日志框架

      日志实现日志门面
      log4jJCL
      jul java.util.loggingSJF4J
      log4j2
      logback(推荐使用)
    2. 2 代码实现

      1. 21 日志实现

        1. 211 jul(注意包名)

          package com.springboot;
          
          import java.util.logging.Logger;
          
          public class JulMain {
              public static void main(String[] args) {
                  Logger logger = Logger.getLogger(JulMain.class.getName());
                  logger.info("崇尚官方开发组:Jul");
              }
          }

          image-20220428214936443

        2. 212 log4j(注意包名)

          导包:

          <!-- log4j 的核心依赖 -->
          <dependency>
              <groupId>log4j</groupId>
              <artifactId>log4j</artifactId>
              <version>1.2.17</version>
          </dependency>

          添加 log4j.properties 文件:

          log4j.rootLogger=trace, stdout
          log4j.appender.stdout=org.apache.log4j.ConsoleAppender
          log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
          log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

          java 代码实现:

          package com.springboot;
          
          import org.apache.log4j.Logger;
          
          public class Log4jMain {
              public static void main(String[] args) {
                  Logger logger = Logger.getLogger(Log4jMain.class.getName());
                  logger.info("崇尚开源开发组:Log4j");
              }
          }

          image-20220428214918691

      2. 22 日志及日志门面实现

        开发标准:记录日志不能直接使用日志实现框架,必须通过日志门面来实现。

        1. 221 Jul (实现) + JCL (门面)

          导包:

          <!-- 引入 JCL 门面依赖 -->
          <dependency>
              <groupId>commons-logging</groupId>
              <artifactId>commons-logging</artifactId>
              <version>1.2</version>
          </dependency>

          添加 commons-logging.properties 文件:

          org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger

          java 实现:

          package com.springboot;
          
          import org.apache.commons.logging.Log;
          import org.apache.commons.logging.LogFactory;
          
          public class JulMain {
              public static void main(String[] args) {
                  Log log = LogFactory.getLog(JulMain.class.getName());
                  System.out.println(log.getClass());
                  log.info("崇尚官方开发组:Jul");
              }
          }
        2. 222 log4j (实现) + slf4j (门面)

          导包:

          <!-- slf4j 的核心依赖 -->
          <dependency>
              <groupId>org.slf4j</groupId>
              <artifactId>slf4j-api</artifactId>
          </dependency>
          <!-- 加上桥接器 -->
          <dependency>
              <groupId>org.slf4j</groupId>
              <artifactId>slf4j-log4j12</artifactId>
          </dependency>

          java 实现:

          package com.springboot;
          
          import org.slf4j.Logger;
          import org.slf4j.LoggerFactory;
          
          public class Log4jMain {
              public static void main(String[] args) {
                  Logger logger = LoggerFactory.getLogger(Log4jMain.class);
                  System.out.println(logger.getClass());
                  logger.info("崇尚开源开发组:Log4j");
              }
          }
  1. 3 桥接器

    image-20220428220818406

  2. 4 将 JCL 转化到 slf4j

    使用 jcl-over-slf4j 或者 jul-to-slf4j

    <!-- 为了日志统一实现,将 JCL 转化到 slf4j
         添加 JCL-slf4j 的适配器
    -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
    </dependency>
  1. logback 日志的集成

    image-20220428232620876

    1. SpringBoot 底层也是使用 slf4j + logback 的方式进行日志记录

      logback 桥接:logback-calssic

    2. SpringBoot 也把其他的日志都替换成了 slf4j;

      • log4j 适配:log4j-over-slf4j
      • jul 适配:jul-to-slf4j
      • jcl-over-slf4j
  2. SpringBoot 日志使用

    • 日志级别

      可以设置 TRACE,DEBUG,INFO,WARN,ERROR 或 OFF 之一

      logging:
        level:
          root: "warn"
          org.springframework.web: "debug"
          org.hibernate: "error"
  • 日志格式

    2022-04-29 16:18:09.667 TRACE 78860 --- [           main] com.springboot.Application               : 跟踪
    2022-04-29 16:18:09.667 DEBUG 78860 --- [           main] com.springboot.Application               : 调试
    2022-04-29 16:18:09.667  INFO 78860 --- [           main] com.springboot.Application               : 信息
    2022-04-29 16:18:09.667  WARN 78860 --- [           main] com.springboot.Application               : 警告
    2022-04-29 16:18:09.667 ERROR 78860 --- [           main] com.springboot.Application               : 异常

    详细介绍:

    可以使用 logging.pattern.console 修改默认的控制台的日志格式:

    %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
    • 2022-04-29 16:18:09.667

      • 日期和时间:毫秒精度,易于排序。
      • %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint}

        • %clr 代表当前内容的颜色 {faint}
        • (%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}) 括号中表示显示的内容

          • ${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}

            使用了 ${value:value2} springboot 的占位符 + null 条件的表达式(如果 value 为 null 则使用 value2)

            LOG_DATEFORMAT_PATTERN:系统环境变量中的值,springboot 底层会根据对应的配置项将值设置到对应的环境变量中 LOG_DATEFORMAT_PATTERN=logging.pattern.dateformat 可通过 dateformat: -yyyy-MM-dd 设置

          • %d{-yyyy-MM-dd HH:mm:ss.SSS}

            %d 表示 logback 的日期显示方式

            {-yyyy-MM-dd HH:mm:ss.SSS} 表示日期的格式

    • TRACE

      • 日志级别。
      • %clr(${LOG_LEVEL_PATTERN:-%5p})

        • %clr 表示内容的颜色,会根据不同的日志级别输出对应的颜色
        • ${LOG_LEVEL_PATTERN:-%5p}

          • %5 代表当前内容所占字符长度
          • p 表示输出日志的级别
    • 78860

      • 进程 ID。
      • %clr(${PID:- }){magenta}

        • %clr 表示当前内容的颜色为 {magenta}
        • ${PID:- } springboot 的占位符 + null 条件的表达式(如果 value 为 null 则使用 value2)
        • PID 是系统环境变量中的进程 ID (由系统分配)
    • ---

      • 一个 --- 分离器来区分实际日志消息的开始。
    • [ main]

      • 线程名称:用方括号括起来(对于控制台输出可能会被截断)。
    • com.springboot.Application

      • 记录日志的类。
    • 跟踪

      • 日志消息
  • 文件输出

    默认情况下,Spring Boot 仅记录到控制台,不写日志文件。如果除了控制台输出外还想写日志文件,则需要设置一个 logging.file.name 或 logging.file.path 属性(例如,在中 application.properties)。

    下表显示了如何 logging.* 一起使用这些属性:

    logging.file.namelogging.file.path实例描述
    (没有)(没有) 仅控制台记录
    指定文件名(没有)my.log写入指定的日志文件
    (没有)具体目录/var/log写入 spring.log 指定的目录
    • logging.file.name

      • 可以设置文件的名称,如果没有设置路径会默认在项目的相对路径下创建
      • 还可以指定路径 + 文件名: D:/my.log
    • logging.file.path

      • 不可以指定文件的名称,必须要指定一个物理文件夹路径,会默认使用 spring.log 为文件名称
  • Spring Boot 的默认日志级别是 INFO

    @SpringBootApplication
    public class Application {
    
        //1. 声明日志记录器
        static Logger logger = LoggerFactory.getLogger(Application.class);
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
    
            logger.trace("跟踪");
            logger.debug("调试");
            //Spring Boot 的默认日志级别是 INFO
            logger.info("信息");
            logger.warn("警告");
            logger.error("异常");
        }
    
    }

    image-20220429155655577

  • 日志迭代(轮转)

    如果您使用的是 Logback,则可以使用 application.properties 或 application.yaml 文件微调日志轮播设置。对于所有其他日志记录系统,您需要直接配置轮转设置(例如,如果使用 Log4J2,则可以添加 log4j.xml 文件)。

    名称描述
    logging.logback.rollingpolicy.file-name-pattern归档的文件名
    logging.logback.rollingpolicy.clean-history-on-start如果应在应用程序启动时进行日志归档清理
    logging.logback.rollingpolicy.max-file-size归档前日志文件的最大大小
    logging.logback.rollingpolicy.total-size-cap删除日志档案之前可以使用的最大大小
    logging.logback.rollingpolicy.max-history保留日志存档的天数(默认为7)
    • logging.logback.rollingpolicy.file-name-pattern

      • ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz

        • ${LOG_FILE} 对应 logging.file.name
        • %d{yyyy-MM-dd} 日期 年-月-日
        • %i 索引,当文件超出指定大小后进行的文件索引递增
  • 自定义日志配置文件

    可以通过在类路径中包含适日志配置文件来激活各种日志记录系统或使用 logging.config

    Logging SystemCustomization
    Logbacklogback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
    log4j2log4j2-spring.xml or log4j2.xml
    JDK(Java Util Logging)logging.properties

    注意:

    • 如果使用自定义日志配置文件,会使 springboot 中全局配置文件的 logging 相关配置失效。
    • 可以结合 springboot 提供的 Profile 来控制日志的生效

      • 要注意的是,一定要将日志配置文件的文件名改成 logback-spring.xml, 因为 logback.xml 会在 springboot 容器加载之前先被 logback 给加载到,那么由于 logback 无法解析 springProfile 将会报错

        image-20220429181012835

      image-20220429181043577

  • 切换日志框架

    • 将 logback 切换成 log4j2

      添加 spring-boot-starter-log4j2 依赖(场景启动器)

      <!-- log4j2 的场景启动器 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-log4j2</artifactId>
      </dependency>

      排除 logback 的场景启动器

      <!-- starter-web 里面自动添加了 starter-logging 也就是 logback 的依赖 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <exclusions>
              <!-- 排除 starter-logging 也就是 logback 的依赖 -->
              <exclusion>
                  <groupId>org.apache.logging.log4j</groupId>
                  <artifactId>log4j-slf4j-impl</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>org.apache.logging.log4j</groupId>
                  <artifactId>log4j-jul</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>org.slf4j</groupId>
                  <artifactId>jul-to-slf4j</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-logging</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>*</groupId>
                  <artifactId>*</artifactId>
              </exclusion>
          </exclusions>
      </dependency>

      添加 log4j2.xml 文件

      <?xml version="1.0" encoding="UTF-8" ?>
      <configuration status="OFF">
          <appenders>
              <Console name="Console" target="SYSTEM_OUT">
                  <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} +++++++ %msg%n"/>
              </Console>
          </appenders>
          <loggers>
              <root level="info">
                  <appender-ref ref="Console"/>
              </root>
          </loggers>
      </configuration>
 

 - 将 logback 切换成 log4j

   1. 首先将 logback 的桥接器排除

      ```xml
      <!-- starter-web 里面自动添加了 starter-logging 也就是 logback 的依赖 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>ch.qos.logback</groupId>
                  <artifactId>logback-classic</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>org.apache.logging.log4j</groupId>
                  <artifactId>log4j-slf4j-impl</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>org.apache.logging.log4j</groupId>
                  <artifactId>log4j-jul</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>org.slf4j</groupId>
                  <artifactId>jul-to-slf4j</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-logging</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>*</groupId>
                  <artifactId>*</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      ```

   2. 添加 log4j 的桥接器

      ```xml
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
      </dependency>
      ```

   3. 添加 log4j 的配置文件

      ```properties
      log4j.rootLogger=trace, stdout
      log4j.appender.stdout=org.apache.log4j.ConsoleAppender
      log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
      log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] ======== %m%n
      ```

Spring Boot 的配置文件和自动配置原理

  1. 使用 Spring Initializr 快速创建 Spring Boot 项目

    • IDEA:使用 Spring Initializr 快速创建项目

      ​ 继承关系-springboot 的 maven 项目

      • 使用 spring initializr 新建一个父 maven,type 选择 POM
      • 在父 maven 项目中新建模块,使用 spring initializr 新建一个子 maven,type选择 maven project
      • 修改子项目中的继承方式:

        之前:

        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.6.6</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>

        修改后:

        <parent>
            <groupId>com.SpringBoot</groupId>
            <artifactId>springboot_parent</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </parent>

        spring_initializr 继承 springboot_parent 继承 spring-boot-starter-parent

    • 默认生成的 Spring Boot 项目:

      resource文件夹中的目录结构

      • static:保存所有的静态资源,js css images;
      • template:保存所有的模板页面;(Spring Boot 默认 jar 包使用嵌入式的 Tomcat,默认不支持 JSP 页面);可以使用模板引擎(freemarker、thymeleaf)
      • application.properties:Spring Boot 应用的配置文件;可以修改一下默认设置;
  2. 自定义 SpringApplication

    其他的上官网看。

    • lazy-initialization:懒加载

      # 懒加载设置,要在使用 bean 的时候才会去创建 bean 实例,否则不创建
      spring.main.lazy-initialization=true
  3. 配置文件的使用

    SpringBoot 使用一个全局的配置文件或者叫核心配置文件,配置文件名在约定的情况下 名字是固定的

    配置文件的作用:修改 SpringBoot 自动配置的默认值;SpringBoot 在底层都给我们自动配置好;

    两种配置文件的格式:

    1. application.properties 的用法:扁平的 k/v 格式。

      server.port=8081
      server.servlet.context-path=/wyu
    2. application.yml 的用法:树型结构。

      server:
          port: 8081
          servlet:
              context-path: /wyu

      建议使用 yml,因为它的可读性更强。

  • YAML语法:

    k:(空格)v:表示一对键值对(空格必须有);

    以空格的缩进来空值层级关系;只要是左对齐的一列数据,都是同一个层级的;

    属性和值也是大小写敏感;

  • 配置文件加载顺序:

    • application.yml
    • application.yaml
    • application.properties
  • 外部约定配置文件加载顺序:

    springboot 启动还会扫描以下位置的 application.properties 或者 application.yml 文件作为 Spring Boot 的默认配置文件

    下面无序列表优先级从低到高

    • classpath 根目录下的

      image-20220412141330192

    • classpath 根 config/

      image-20220412141257617

    • 项目根目录

      如果当前项目是继承/耦合关系 maven 项目的话,项目根目录为父 maven 项目的根目录。

      image-20220412141847161

    • 项目根目录 /config

      image-20220412142104682

    • 直接子目录 /config

      image-20220412143826880

  • 配置文件值注入

    image-20220425024303286

    可以使用 @Value 标签一个一个属性绑定,也可以使用 @ConfigurationProperties 标签根据 yml 或者 properties 文件一次性绑定

    • 松散绑定

      user:
        user-name: 啊章
      user:
        userName: 啊章
      user:
        user_name: 啊章
      user:
        USERNAME: 啊章

      以上四种命名是可以自动绑定 bean 属性 User.username

    • 如果输出中文乱码,则需要调一下setting:

    image-20220425024608880

    • @Value@ConfigurationProperties 的获取值比较:
    @ConfigurationProperties@Value
    功能批量注入配置文件中的属性一个个指定
    松散绑定支持支持,有限
    SpEL不支持支持
    JSR303数据校验支持不支持
    复杂类型封装支持不支持
    自动提示支持不支持
    • 导入配置文件处理器并且勾选如图设置,以后编写配置就有提示了:
    <!-- 会生成 META-INF 元数据  用于提供 idea 自动提示配置文件的 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <!-- option: 如果依赖不会传播  就是说别的项目依赖本项目,那么本项目的这个依赖不会被别的项目依赖 -->
        <optional>true</optional>
    </dependency>

    image-20220425025520512

    • 配置文件赋值:

      • 对象、Map(属性和值)(键值对):

        • 对象是 k : v 的方式

            girl-friend:
              18 : IU
              20 : 庄达菲
        • 行内写法:

            girl-friend: {18: IU,20: 庄达菲}
      • 数组(List、Set):

        • 用 - 值 表示数组中的一个元素

            hobbies:
              - 唱
              - 跳
              - rap
              - 篮球
        • 行内写法

            hobbies: [唱,跳,rap,篮球]
      • JSR303数据校验

        image-20220425033655210

        <!-- 数据校验 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        @NotNull 要使用 javax.validation 包下面的验证注解

  • 配置文件占位符

    • 随机数

      #随机值
      ${random.value}
      #随机 int 值
      ${random.int}
      #随机值
      ${random.long}
      #随机值
      ${random.uuid}
      # 10 以内的随机 int 值
      ${random.int(10)}
      #范围为 1024 到 65536 的随机 int 值
      ${random.}int[1024,65536]}
    • 占位符获取之前配置的值,如果没有可以使用:指定默认值

  • Profile文件的加载

    对于应用程序来说,不同的环境需要不同的配置。

    1. 多 Profile 文件

      • Spring 官方给出的语法规则是 application-{profile}.properties(.yaml/.yml)
      • 如果需要创建自定义的 properties 文件时,可以用 application-xxx.properties 的命名方式。

        • 开发环境下:

          image-20220412154909418

        • 生产环境下:

          image-20220412154920749

      • 若我们需要在两种环境下进行切换,只需要在 application.properties 中加入如下内容即可。

        spring.profiles.active=prod
    2. 激活指定 profile

      • 在配置文件中指定 spring.profiles.active=dev
      • 命令行:

        java -jar configuration_file-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;
        • 还可以通过 spring.config.location 来改变默认的配置文件

          使用 spring.config.location 指定的配置文件,是不会进行互补。

          java -jar configuration_file-0.0.1-SNAPSHOT.jar --spring.profiles.location=D:/application.properties
        • 还可以通过 spring.config.name 来改变默认的配置文件

          java -jar configuration_file-0.0.1-SNAPSHOT.jar --spring.profiles.name=application-prod;
    3. 所有配置文件按以下顺序考虑:优先级从低到高

      1. 打包在 jar 中配置文件
      2. 打包在 jar 中 profile
      3. 打包在 jar 之外的配置文件
      4. 打包在 jar 之外的 profile
    4. 通配符位置

      如果具有某些 Redis 配置和某些 mysql 配置,则可能需要将这两个配置分开,同时要求这两个配置都存在于 application.properties 文件中。这可能会导致两个不同的 application.properties 文件安装在诸如不同的位置 /config/redis/application.properties 和 /config/mysql/application.properties。在这种情况下,通配符位置为 config/*/,将导致两个文件都被处理。

    5. 配置文件读取方式:优先级从低到高

      • @PropertySource@Configuration 类上的注解。请注意,Environment 在刷新应用程序上下文之前,不会将此类属性源添加到中。现在配置某些属性(如 logging. 和 spring.main. 在刷新开始之前先读取)为时已晚。

        • 会和约定的配置文件形成互补
        • 一定要指定 .properties 配置
        @SpringBootApplication
        @PropertySource("classpath:appSource.properties")
        public class Application {
        
            public static void main(String[] args) throws IOException {}
      

    - 默认属性(通过设置指定 SpringApplication.setDefaultProperties)

      - 会和约定的配置文件形成互补

      ```java
      @SpringBootApplication
      public class Application {
      
          public static void main(String[] args) throws IOException {
              SpringApplication springApplication = new SpringApplication(Application.class);
      
              // 创建 properties
              Properties properties = new Properties();
              // 通过当前类的 ClassLoader
              InputStream is = Application.class.getClassLoader().getResourceAsStream("app.properties");
              // 将输入流读取成 properties 对象
              properties.load(is);
      
              springApplication.setDefaultProperties(properties);
              springApplication.run(args);
          }
      
      }
      ```

      

    - 配置数据(例如 application.properties 文件)

      - 约定配置文件

    - 操作系统环境变量。

      - 会使约定配置文件失效

      - idea 

        ![image-20220422040551533](https://static-upyun.hackerjk.top/markdown/image-20220422040551533.png!)

      - windows

        ![image-20220422040837561](https://static-upyun.hackerjk.top/markdown/image-20220422040837561.png!)

    - Java 系统属性(System.getProperties())。

      - 会使约定配置文件失效

      - idea

        ![image-20220422041130067](https://static-upyun.hackerjk.top/markdown/image-20220422041130067.png!)

      - 命令行 java 属性

        ![image-20220422041323887](https://static-upyun.hackerjk.top/markdown/image-20220422041323887.png!)

    - JNDI 属性 java:comp/env。

    - ServletContext 初始化参数。

      ![image-20220422041429270](https://static-upyun.hackerjk.top/markdown/image-20220422041429270.png!)

    - ServletConfig 初始化参数。

      ![image-20220422041509283](https://static-upyun.hackerjk.top/markdown/image-20220422041509283.png!)

    - 来自的属性 SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的嵌入式 JSON)。

    - 命令行参数。

      - 会使约定配置文件失效

      ```cmd
      java -jar configuration_file-0.0.1-SNAPSHOT.jar --spring.profiles.location=D:/application.properties
      ```

      

    - properties 测试中的属性。可用于测试应用程序的特定部分 @SpringBootTest 的测试注释和注释。

    - @TestPropertySource 测试中的注释。

      - 用在单元测试上的

        ![image-20220422043022209](https://static-upyun.hackerjk.top/markdown/image-20220422043022209.png!)

    - $HOME/.config/spring-boot 当 devtools 处于活动状态时,目录中的 Devtools全局设置属性。
  • 配置文件加载位置
  1. Spring Boot 的配置和自动配置原理

    • @SpringBootApplication:Spring Boot 应用标注在某个类上说明这个类是 SpringBoot 的主配置类,SpringBoot 需要运行这个类的 main 方法来启动 SpringBoot 应用;

      image-20220427150357061

      @Target(ElementType.TYPE)    //设置当前注解可以标记在哪
      @Retention(RetentionPolicy.RUNTIME)    //当注解标注的类编译以什么方式保留
          RetentionPolicy.RUNTIME 会被 jvm 加载 | 反射的方式:class.getAnnotionbytype(Autowreid.class)
      @Documented        //java doc 会生成注解信息
      @Inherited        //是否会被继承
      
      @SpringBootConfiguration    //Spring Boot 的配置类;标注在某个类上,表示这是一个 Spring Boot 的配置类;
      
      @Configuration    //配置类上来标注这个注解;配置类——配置文件;配置类也是同期中的一个组件;@Component
      
      @EnableAutoConfiguration    //开启自动配置功能;以前我们需要配置的东西,Spring Boot 帮我们自动配置;@EnableAutoConfiguration 告诉 SpringBoot 开启自动配置功能;这样自动配置才能生效;
      
      @ComponentScan    //扫描包 相当于在 spring.xml 配置中 <context:component-scan>    但是没有指定 basepackage,如果没有指定,spring 底层会自动扫描当前配置类所有在的包
      
      TypeExcludeFilter:springboot    //对外提供的扩展类,可以供我们去按照我们的方式进行排除
      
      AutoConfigurationExcludeFilter    //排除所有配置类并且是自动配置类中里面的其中一个
      
      @AutoConfigurationPackage    //将当前配置类所在包保存在 BasePackages 的 Bean 中。供 spring 内部使用
    • 以 HttpEncodingAutoConfiguration (Http 编码自动配置)为例解释自动配置原理;

      @Configuration(proxyBeanMethods = false)
      @EnableConfigurationProperties(ServerProperties.class)
      @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
      @ConditionalOnClass(CharacterEncodingFilter.class)
      @ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
      public class HttpEncodingAutoConfiguration {
      
          private final Encoding properties;
      
          public HttpEncodingAutoConfiguration(ServerProperties properties) {
              this.properties = properties.getServlet().getEncoding();
          }
      
          @Bean
          @ConditionalOnMissingBean
          public CharacterEncodingFilter characterEncodingFilter() {
              CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
              filter.setEncoding(this.properties.getCharset().name());
              filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
              filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
              return filter;
          }
      • @Configuration(proxyBeanMethods = false)

        标记了 @Configuration,Spring 底层会给配置创建 cglib 动态代理。

        作用:防止每次调用本类的 Bean 方法而重新创建对象,Bean 是默认单例的。

      • @EnableConfigurationProperties(ServerProperties.class)

        启用可以再配置类设置的属性 对应的类

      • @xxxConditional 根据当前不同的条件判断,决定这个配置类是否生效?
      • @Conditional 派生注解(Spring 注解版原生的 @Conditional 作用)

        作用:必须是 @Conditional 指定的条件成立,才给容器中添加组件,配置配里面的所有内容才能生效;

        @Conditional 扩展注解作用(判断是否满足当前指定条件)
        @ConditionalOnJava系统的 java 版本是否符合要求
        @ConditionalOnBean容器中存在指定的 Bean;
        @ConditionalOnMissingBean容器中不存在指定的 Bean;
        @ConditionalOnExpression满足 SpEL 表达式指定
        @ConditionalOnClass系统中有指定的类
        @ConditionalOnOnMissingClass系统中没有指定的类
        @ConditionalOnSingleCandidate容器中只有一个指定的 Bean,或者这个 Bean 是首选 Bean
        @ConditionalOnProperty系统中指定的属性是否有指定的值
        @ConditionalOnResource类路径下是否存在指定资源文件
        @ConditionalOnWebApplication当前是 Web 环境
        @ConditionalOnNotWebApplication当前不是 Web 环境
        @ConditionalOnJndiJNDI 存在指定项
      • 可以通过设置配置文件中:启用 debug=true 属性来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效

        ============================
        CONDITIONS EVALUATION REPORT
        ============================
        
        
        Positive matches:    **表示自动配置类启用的**
        -----------------
        ......
        
        
        Negative matches:    **没有匹配成功的自动配置类**
        -----------------
        ......

Spring Boot 简介及快速搭建

  1. 导入依赖:

    <!-- 继承 SpringBoot 的父项目 -->
    <!-- spring boot 要加上下面的 parent 代码才能将当前项目标记为 spring boot 项目 -->
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.6.6</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    
    <!-- 下面的 plugin 中的 spring-boot-maven-plugin 可以告诉服务器 SpringBoot 的启动类在哪 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  2. 建包并创建控制器

    @RestController  // 相当于 @Controller + @ResponseBody
    @RequestMapping("/hello")
    public class HelloController {
    
        @RequestMapping("/world")
        public String sayHi() {
            return "hello world!";
        }
    
    }
  3. 编写启动类

    @SpringBootApplication      // 标记为 SpringBoot 的启动类  整个工程只有一个
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
  4. 修改端口及添加项目名

    在resource文件夹中创建一个 application.properties 文件(文件名只能是这个)

    # 端口
    server.port=8080
    # 项目虚拟路径
    server.servlet.context-path=/wyu

    ​ server.port 是用来选用端口的

    ​ server.servlet.context-path 则是用来添加项目名,注意:没有使用 server.servlet.context-path 之前的访问地址为localhost:8080/hello/world,使用之后地址变为 localhost:8080/wyu/hello/world

  5. 部署服务器

    在 pom.xml 文件中加入下面的代码

    <!-- 下面的 plugin 中的 spring-boot-maven-plugin 可以告诉服务器 SpringBoot 的启动类在哪 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  6. 代码讲解

    <!-- 引入父 Maven 项目,继承父 Maven 项目中所有的配置信息
        spring-boot-starter-parent: 又引入一个父 Maven 项目
        <parent>
            <artifactId>spring-boot-dependencies</artifactId>
            <groupId>org.springframework.boot</groupId>
            <version>2.6.6</version>
        </parent>
        spring-boot-dependencies 帮我们管理了 SpringBoot 应用中所有的依赖和版本
                以后我们导入已有依赖就不需要写版本号了,它帮我们解决了第三方库之间的版本冲突问题
                名次:SpringBoot 的版本仲裁中心
    -->
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.6.6</version>
    </parent>
    
    <!--
        starter 场景启动器:不同场景启动器维护了所对应的所有依赖,从而简化 maven 文件书写。
        spring-boot-starter-web:使用 Spring MVC 构建 Web (包括RESTful)应用程序。
            使用 Tomcat 作为默认的嵌入式容器
    -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    
    <!-- 下面的 plugin 中的 spring-boot-maven-plugin 可以告诉服务器 SpringBoot 的启动类在哪 -->
    <!-- 部署 SpringBoot 的插件,只有加了这个插件,当运行 java -jar xxx.jar 才能正常启动 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  7. 面试题:

    • 描述一下 SpringBoot 的作用
    • SpringBoot 有哪些特性?

Spring 中的事务管理

1、事务简介
  • 事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性。
  • 事务就是一系列的动作,它们被当做一个单独的工作单元。这些动作要么全部完成,要么全部不起作用
  • 事务的四个关键属性 (ACID)

    • 原子性 (atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成要么完全不起作用。
    • 一致性 (consistency):一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中。
    • 隔离性 (isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
    • 持久性 (durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该收到影响。通常情况下,事务的结果被写到持久化存储器中。
2、Spring 中的事务管理
  • 作为企业级应用程序框架,Spring 在不同的事务管理 API 之上定义了一个抽象层。而应用程序开发人员不必了解底层的事务管理 API,就可以使用 Spring 的事务管理机制。
  • Spring 既支持编程式的事务管理,也支持声明式的事务管理。
  • 编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码。
  • 声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过 AOP 方法模块化。Spring通过 Spring AOP 框架支持声明式事务管理。
3、Spring 中的事务管理器
  • Spring 从不同的事务管理 API 中抽象了一整套的事务机制。开发人员不必了解底层的事务 API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了。
  • Spring 的核心事务管理抽象是 Interface PlatformTransactionManager 管理封装了一组独立于技术的方法。无论使用 Spring 的哪种事务管理策略 (编程式或声明式),事务管理器都是必须的。
  • Spring 中的事务管管理器的不同实现

    • Class DataSourceTransactionManager:在应用程序中主需要处理一个数据源,而且通过 JDBC 存取

      配置事务管理器:

      <!-- 配置事务管理器 -->
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"/>
      </bean>
      
      <!-- 启用事务注解 -->
      <tx:annotation-driven transaction-manager="transactionManager"/>

      添加事务注解 @Transactional:

      @Repository("bookShopService")
      public class BookShopServiceImpl implements BookShopService {
      
          @Autowired
          private BookShopDao bookShopDao;
      
          //添加事务注解
          @Transactional
          @Override
          public void purchase(String isbn, String username) {
              //1.获取书的单价
              int price = bookShopDao.findBookPriceByIsbn(isbn);
      
              //2.更新书的库存
              bookShopDao.updateBookStock(isbn);
      
              //3.更新用户余额
              bookShopDao.updateUserAccount(username, price);
      
          }
      
      }
    • Class JtaTransactionManager:在 JavaEE 应用服务器上用 JTA (Java Transaction API) 进行事务管理
    • Class HibernateTransactionManager:用 Hibernate 框架存取数据库
    • ……
    • 事务管理器以普通的 Bean 形式声明在 Spring IOC 容器中
4、事务的传播属性
  • 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新的事务,并在自己的事务中运行。
  • 事务的传播行为可以由 传播属性指定。Spring 定义了 7 中类传播行为。
  • 传播属性描述
    REQUIRED如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行。
    REQUIRED_NEW当前的方法必须启动新事务,并在它自己的事务给运行。如果有事务正在运行,应该将它挂起。
    SUPPORTS如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中。
    NOT_SUPPORTED当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。
    MANDATORY当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常。
    NEVER当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。
    NESTED如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行。
    • REQUIRED 传播行为

      当 bookService 的 purchase() 方法被另一个事务方法 checkout() 调用时,它默认会在现有的事务内运行。这个默认的传播行为就是 REQUIRED。 因此在 checkout() 方法的开始和终止边界内只有一个事务。这个事务只在 checkout() 方法结束的时候被提交,结果用户一本书都买不了。

      事务传播属性可以在 @Transactional 注解的 propagation 属性中定义。

    • REQUIRES_NEW 传播行为

      另一种常见的传播行为是 REQUIRES_NEW。它表示该方法必须启动一个新事物,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。

5、事务的其他属性
  • 隔离级别

    • 使用 ioslation 指定事务的隔离级别,最常用的取值为 READ_COMMITTED。
  • 回滚

    • 默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置。通常情况下取默认值即可。
  • 只读

    • 使用 readOnly 指定事务是否为只读。
    • 表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。
    • 若真的只是一个只读取数据库值的方法,应设置 readOnly = true。
  • 过期

    • 使用 timeout 指定强制回滚之前事务可以占用的时间。
    • 单位是 秒(s)。
6、基于 xml 文件的方式配置事务管理器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <!-- 导入资源文件 -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!-- 配置 c3p0 数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="driverClass" value="${jdbc.driverClass}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>

        <property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
    </bean>

    <!-- 配置 Spring 的 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置 bean -->
    <bean id="bookShopDao" class="com.spring.tx.xml.BookShopDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
    <bean id="bookShopService" class="com.spring.tx.xml.service.impl.BookShopServiceImpl">
        <property name="bookShopDao" ref="bookShopDao"/>
    </bean>
    <bean id="cashier" class="com.spring.tx.xml.service.impl.CashierImpl">
        <property name="bookShopService" ref="bookShopService"/>
    </bean>

    <!-- 1.配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 2.配置事务属性 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 根据方法名指定事务的属性 -->
            <tx:method name="purchase" propagation="REQUIRES_NEW"/>
            <!-- 通常情况下,对于 get 或者 find 开头的方法 read-only 的值为 true -->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 3.配置事务切入点,以及把事务切入点和事务属性关联起来 -->
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.spring.tx.xml.service.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>

</beans>
7、Spring 整合 Hibernate
  • Spring 整合 Hibernate 整合什么?

    • 由 IOC 容器来管理 Hibernate 的 SessionFactory
    • 让 Hibernate 使用上 Spring 的声明式事务
  • 整合步骤:

    • 加入 hibernate

      • 导入 jar 包

        <dependency>
                <groupId>org.jboss.logging</groupId>
                <artifactId>jboss-logging</artifactId>
                <version>3.1.0.GA</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.spec.javax.transaction</groupId>
                <artifactId>jboss-transaction-api_1.1_spec</artifactId>
                <version>1.0.1.Final</version>
            </dependency>
            <dependency>
                <groupId>org.javassist</groupId>
                <artifactId>javassist</artifactId>
                <version>3.15.0-GA</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate.javax.persistence</groupId>
                <artifactId>hibernate-jpa-2.0-api</artifactId>
                <version>1.0.1.Final</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-core</artifactId>
                <version>4.2.4.Final</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate.common</groupId>
                <artifactId>hibernate-commons-annotations</artifactId>
                <version>4.0.2.Final</version>
            </dependency>
            <dependency>
                <groupId>dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>1.6.1</version>
            </dependency>
            <dependency>
                <groupId>antlr</groupId>
                <artifactId>antlr</artifactId>
                <version>2.7.7</version>
            </dependency>
      • 添加 hibernate 的配置文件:hibernate.cfg.xml

        <?xml version='1.0' encoding='utf-8'?>
        <!DOCTYPE hibernate-configuration PUBLIC
            "-//Hibernate/Hibernate Configuration DTD//EN"
            "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
        <hibernate-configuration>
          <session-factory>
            <!-- 配置 hibernate 的基本信息 -->
            <!-- 1. 数据源需配置到 IOC 容器中,所以在此处不需要再配置数据源 -->
            <!-- 2. 关联的 .hbm.xml 也在 IOC 容器配置 SessionFactory 实例时再进行配置 -->
            <!-- 3. 配置 hibernate 的基本属性:方言,SQL 显示及格式化,生成数据表的策略以及二级缓存等 -->
            <property name="hibernate.dialect"> org.hibernate.dialect.MySQL5InnoDBDialect </property>
        
            <property name="hibernate.show_sql"> true </property>
            <property name="hibernate.format_sql"> true </property>
        
            <property name="hibernate.hbm2ddl.auto"> update </property>
        
            <!-- 配置 hibernate 二级缓存相关的属性 -->
        
            
          </session-factory>
        </hibernate-configuration>
  • 加入 Spring
  • 整合

Spring 对 JDBC 的支持

  1. 创建资源文件 db.properties
    jdbc.user=root
    jdbc.password=jie13727507037
    jdbc.driverClass=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql:///spring4?serverTimezone=UTC
    
    jdbc.initPoolSize=5
    jdbc.maxPoolSize=10
  2. 在 xml 文件中导入资源文件
    <!-- 导入资源文件 -->
    <context:property-placeholder location="classpath:db.properties"/>
    
    <!-- 配置 c3p0 数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="driverClass" value="${jdbc.driverClass}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
    
        <property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
    </bean>
  3. 配置 JdbcTemplate
    <!-- 配置 Spring 的 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
  4. java 代码
    package com.spring.jdbc;
    
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    
    import javax.sql.DataSource;
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.List;
    
    public class JDBCTest {
    
        private ApplicationContext app = null;
        private JdbcTemplate jdbcTemplate;
        private EmployeeDao employeeDao;
        private DepartmentDao departmentDao;
    
        {
            app = new ClassPathXmlApplicationContext("applicationContext.xml");
            jdbcTemplate = (JdbcTemplate) app.getBean("jdbcTemplate");
            employeeDao = (EmployeeDao) app.getBean("employeeDao");
            departmentDao = (DepartmentDao) app.getBean("departmentDao");
        }
    
        @Test
        public void testDepartmentDao() {
            System.out.println(departmentDao.get(2));
        }
    
        @Test
        public void testEmployeeDao() {
            System.out.println(employeeDao.get(1));
        }
    
        /**
         * 获取单个列的值,或做统计查询
         * 使用 queryForObject(String sql, Class<Long> requiredType) 方法
         */
        @Test
        public void testQueryForObject2() {
            String sql = "SELECT count(id) FROM employees";
            long count = jdbcTemplate.queryForObject(sql, Long.class);
    
            System.out.println(count);
        }
    
        /**
         * 查到实体类的集合
         * 注意调用的不是 queryForList 方法
         */
        @Test
        public void testQueryForList() {
            String sql = "SELECT id, last_name lastName, email FROM employees WHERE id > ?";
            RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
            List<Employee> employees = jdbcTemplate.query(sql, rowMapper, 5);
    
            System.out.println(employees);
        }
    
        /**
         * 从数据库中获取一条记录,实际得到对应的一个对象
         * 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法
         * 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
         * 1.其中的 RowMapper 指定如何去映射结果集的行,常用的实现类为 BeanPropertyRowMapper
         * 2.使用 SQL 中的别名完成列名和类的属性名的映射,例如 last_name laseName
         * 3.不支持级联属性,JdbcTemplate 到底是一个 JDBC 的小工具,而不是 ORM 框架
         */
        @Test
        public void testQueryForObject() {
            String sql = "SELECT id, last_name lastName, email FROM employees WHERE id = ?";
            RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
            Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
    
            System.out.println(employee);
        }
    
        /**
         * 执行批量更新:批量 INSERT,UPDATE,DELETE
         * 最后一个参数是 Object[] 的 List 类型:因为修改一条记录需要一个 Object 的数组,那么多条就需要多个 Object 的数组
         */
        @Test
        public void testBatchUpdate() {
            String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(?,?,?)";
            List<Object[]> batchArgs = new ArrayList<Object[]>();
    
            batchArgs.add(new Object[]{"AA", "aa@123.com", 1});
            batchArgs.add(new Object[]{"BB", "bb@123.com", 2});
            batchArgs.add(new Object[]{"CC", "cc@123.com", 3});
            batchArgs.add(new Object[]{"DD", "cc@123.com", 3});
            batchArgs.add(new Object[]{"EE", "ee@123.com", 2});
    
            jdbcTemplate.batchUpdate(sql, batchArgs);
        }
    
        /**
         * 执行 INSERT,UPDATE,DELETE
         */
        @Test
        public void testUpdate() {
            String sql = "UPDATE employees SET last_name = ? WHERE id = ?";
            jdbcTemplate.update(sql, "Jack", 5);
        }
    
        @Test
        public void testDataSource() throws SQLException {
    
            DataSource dataSource = (DataSource) app.getBean("dataSource");
            System.out.println(dataSource.getConnection());
    
        }
    
    }
    
    package com.spring.jdbc;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.core.support.JdbcDaoSupport;
    import org.springframework.stereotype.Repository;
    
    import javax.sql.DataSource;
    
    /**
     * 不推荐使用 JdbcDaoSupport,而推荐使用 JdbcTemplate 作为 Dao 类的成员变量
     */
    
    @Repository
    public class DepartmentDao extends JdbcDaoSupport {
    
        @Autowired
        public void setDataSource2(DataSource dataSource) {
            setDataSource(dataSource);
        }
    
        public Department get(Integer id) {
            String sql = "SELECT id, dept_name name FROM departments WHERE id = ?";
            RowMapper<Department> rowMapper = new BeanPropertyRowMapper<>(Department.class);
            Department department = getJdbcTemplate().queryForObject(sql, rowMapper, id);
    
            return department;
        }
    
    }
    
    package com.spring.jdbc;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class EmployeeDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public Employee get(Integer id) {
            String sql = "SELECT id, last_name lastName, email FROM employees WHERE id = ?";
            RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
            Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, id);
    
            return employee;
        }
    }
    
  5. 在 JDBC 模板中使用具名参数
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate = (NamedParameterJdbcTemplate) app.getBean("namedParameterJdbcTemplate");
    /**
     * 可以为参数起名字,
     * 1.好处:若有多个参数则不用再去对应位置,直接对应参数名,便于维护
     * 2.缺点:较为麻烦
     */
    @Test
    public void testNamedParameterJdbcTemplate() {
        String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(:ln, :email, :deptid)";
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("ln", "FF");
        paramMap.put("email", "ff@123.com");
        paramMap.put("deptid", 2);
        namedParameterJdbcTemplate.update(sql, paramMap);
    }
    /**
     * 使用具名参数时,可以使用 update(String sql, SqlParameterSource paramSource) 方法进行更新操作
     * 1. sql 语句中的参数名和类的属性一致
     * 2.使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数
     */
    @Test
    public void testNamedParameterJdbcTemplate2() {
        String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(:lastName, :email, :deptId)";
    
        Employee employee = new Employee();
        employee.setLastName("XZJ");
        employee.setEmail("xzj@95.com");
        employee.setDeptId(1);
    
        SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(employee);
    
        namedParameterJdbcTemplate.update(sql, parameterSource);
    }

Spring AOP (面向切面编程)

1、AOP 前奏
package com.spring.aop.helloworld;

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    public int add(int i, int j) {
        System.out.println("The method add begins with[" + i + "," + j + "]");
        int result = i + j;
        System.out.println("The method add ends with " + result);
        return result;
    }

    public int sub(int i, int j) {
        System.out.println("The method sub begins with[" + i + "," + j + "]");
        int result = i - j;
        System.out.println("The method sub ends with " + result);
        return result;
    }

    public int mul(int i, int j) {
        System.out.println("The method mul begins with[" + i + "," + j + "]");
        int result = i * j;
        System.out.println("The method mul ends with " + result);
        return result;
    }

    public int div(int i, int j) {
        System.out.println("The method div begins with[" + i + "," + j + "]");
        int result = i / j;
        System.out.println("The method div ends with " + result);
        return result;
    }

}

问题:

  • 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
  • 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。
2、使用动态代理解决上述问题
  • 代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始的对象上。

    public class ArithmeticCalculatorLoggingProxy {
    
        //要代理的对象
        private ArithmeticCalculator target;
    
        public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target) {
            this.target = target;
        }
    
        public ArithmeticCalculator getLoggingProxy() {
            ArithmeticCalculator proxy = null;
    
            //代理对象由哪一个类加载器负责加载
            ClassLoader loader = target.getClass().getClassLoader();
            //代理对象的类型,即其中有哪些方法
            Class [] interfaces = new Class[]{ArithmeticCalculator.class};
            //当调用代理对象其中的方法时,该执行的代码
            InvocationHandler h = new InvocationHandler() {
                /**
                 * @param proxy:正在返回的那个代理对象,一般情况下,在 invoke 方法中都不使用该对象
                 * @param method:正在被调用的方法
                 * @param args:调用方法时传入的参数
                 * @return
                 * @throws Throwable
                 */
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    String methodName = method.getName();
                    //日志
                    System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
                    //执行方法
                    Object result = method.invoke(target, args);
                    //日志
                    System.out.println("The method " + methodName + " ends with " + result);
                    return result;
                }
            };
    
            proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
    
            return proxy;
        }
    
    }
    
    
    
    public class Main {
    
        public static void main(String[] args) {
    
            //ArithmeticCalculator arithmeticCalculator = new ArithmeticCalculatorLoggingImpl();
    //        arithmeticCalculator = new ArithmeticCalculatorImpl();
    
            ArithmeticCalculator target = new ArithmeticCalculatorImpl();
            ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();
    
            int result = proxy.add(1, 2);
            System.out.println("-->" + result);
            result = proxy.div(4, 2);
            System.out.println("-->" + result);
    
        }
    
    }
3、AOP 简介
  • AOP (Aspect-OrientedProgramming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-OrientedProgramming,面向对象编程)的补充。
  • AOP 的主要编程对象是切面(aspect),而切面模块化横切关注点
  • 在应用 AOP 编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的对象(切面)里。
  • AOP 的好处:

    • 每个事物逻辑位于一个位置,代码不分散,便于维护和升级。
    • 业务模块更简洁,只包含核心业务代码。
  • image-20220216005157686
4、AOP 术语
  • 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
  • 通知(Advice):切面必须要完成的工作
  • 目标(Target):被通知的对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象
  • 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmeticCalculator#add() 方法执行前的连接点,执行点为 ArithmeticCalculator#add();方位为该方法执行前的位置。
  • 切点(pointcut):每个类都拥有多个连接点;例如:ArithmeticCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
5、在 Spring 中启用 AspectJ 注解支持
  • 要在 Spring 应用中使用 Aspect 注解,必须在 classpath 下包含 AspectJ 类库:aopalliance.jar、aspectj.weaver,jar 和 spring-aspects.jar
  • 将 aopSchema 添加到 <beans> 根元素中。
  • 要在 Spring IOC 容器中启用 AspectJ注解支持,只要在 Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy> 
  • 当 Spring IOC 容器侦测到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素时,会自动为与 AspectJ 切面匹配的 Bean 创建代理。
6、使用 SpringAOP 步骤
  1. 加入 jar 包:

    <dependencies>
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
    </dependencies>
  2. 在配置文件中加入 aop 的命名空间

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
  3. 基于注解的方式

    • 在配置文件中加入如下配置

      <!-- 使 AspectJ 注解起作用:自动为匹配的类生成代理对象-->
      <aop:aspectj-autoproxy/>
    • 把横切关注点的代码抽象到切面的类中。

      • 切面首先是一个 IOC 中的 bean ,即加入 @Component 注解
      • 切面还需要加入 @Aspect 注解
      • //把这个类声明为一个切面:需要把该类放入到 IOC 容器中、再声明为一个切面
        @Aspect
        @Component
        public class LoggingAspect {
        
            //声明该方法是一个前置通知:在目标方法开始之前执行
            @Before("execution(public int com.spring.aop.impl.ArithmeticCalculator.*(int, int))")
            public void beforeMethod(JoinPoint joinPoint) {
                String methodName = joinPoint.getSignature().getName();
                List<Object> args = Arrays.asList(joinPoint.getArgs());
        
                System.out.println("The method " + methodName + " begins with " + args);
            }
        
    • 在类中声明各种通知

      AspectJ 支持 5 种类型的通知注解:

      • @Before:前置通知,在方法执行之前执行
      • @After:后置通知,在方法执行之后执行
      • @AfterRunning:返回通知,在方法返回结果之后执行

        @AfterThrowing:异常通知,在方法抛出异常之后执行

      • @Around:环绕通知,围绕着方法执行
    • 可以在通知方法中声明一个类型为 JoinPoint 的参数,然后就能访问链接细节,如方法名称和参数值。

      //声明该方法是一个前置通知:在目标方法开始之前执行
      @Before("execution(public int com.spring.aop.impl.ArithmeticCalculator.*(int, int))")
      public void beforeMethod(JoinPoint joinPoint) {
          String methodName = joinPoint.getSignature().getName();
          List<Object> args = Arrays.asList(joinPoint.getArgs());
      
          System.out.println("The method " + methodName + " begins with " + args);
    
    ##### 7、AspectJ 支持 5 种类型的通知注解:
  4. @Before:前置通知,在方法执行之前执行

    /**
    * 在 com.spring.aop.ArithmeticCalculator 接口的每一个实现类的每一个方法开始之前执行一段代码
    * @param joinPoint
    */
    @Before("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
     String methodName = joinPoint.getSignature().getName();
     List<Object> args = Arrays.asList(joinPoint.getArgs());
    
     System.out.println("The method " + methodName + " begins with " + args);
    }
  5. @After:后置通知,在方法执行之后执行,即连接点返回结果或者抛出异常的时候,下面的后置通知记录了方法的终止。

    /**
    * 在方法执行之后执行的代码,无论该方法是否出现异常
    * @param joinPoint
    */
    @After("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
     String methodName = joinPoint.getSignature().getName();
    
     System.out.println("The method " + methodName + " ends.");
    }
  6. @AfterRunning:返回通知,在方法返回结果之后执行

    /**
    * 在方法正常结束后执行的代码
    * 返回通知是可以访问到方法的返回值的
    */
    @AfterReturning(value = "execution(public int com.spring.aop.ArithmeticCalculator.*(..))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
     String methodName = joinPoint.getSignature().getName();
    
     System.out.println("The method " + methodName + " ends with " + result);
    }
  7. @AfterThrowing:异常通知,在方法抛出异常之后执行

    /**
    * 在目标方法出现异常时会执行的代码
    * 可以访问到异常对象;且可以指定在出现特定异常时再执行通知代码
    * @param joinPoint
    * @param ex
    */
    @AfterThrowing(value = "execution(public int com.spring.aop.ArithmeticCalculator.*(..))", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
     String methodName = joinPoint.getSignature().getName();
    
     System.out.println("The method " + methodName + " occurs exception: " + ex);
    }
  8. @Around:环绕通知,围绕着方法执行

    /**
    * 环绕通知需要携带 ProceedingJoinPoint 类型的参数
    * 环绕通知类似于动态代理的全过程:ProceedingJoinPoint 这个类型的参数可以决定是否执行目标方法
    * 且环绕通知必须有返回值,返回值即为目标方法的返回值
    * @param pjd
    */
    @Around("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
    public Object aroundMethod(ProceedingJoinPoint pjd) {
     Object result = null;
     String methodName = pjd.getSignature().getName();
    
     //执行目标方法
     try {
         //前置通知
         System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
         result = pjd.proceed();
         //返回通知
         System.out.println("The method " + methodName + " ends with " + result);
     } catch (Throwable ex) {
         //异常通知
         System.out.println("The method " + methodName + " occurs exception: " + ex);
     }
     //后置通知
     System.out.println("The method " + methodName + " ends.");
    
     return result;
    }
    8、指定切面的优先级
  9. 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
  10. 切面的优先级可以通过实现 Ordered 接口或者利用 @Order 注解指定。
  11. 实现 Ordered 接口,getOrder() 方法的返回值越小,优先级越高。
  12. 若使用 @Order 注解,序号出现在注解中。

    /**
    * 可以使用 @Order 注解指定切面的优先级,值越小优先级越高
    */
    
    @Order(1)
    @Aspect
    @Component
    public class VlidationAspect {
    
     @Before("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
     public void validateArgs(JoinPoint joinPoint) {
         System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
     }
    
    }
    
    9、重用切点表达式

    使用 @PointCut 来声明切入点表达式。

    后面的其他通知直接使用方法名来引用当前的切入点表达式。

    /**
     * 定义一个方法,用于声明切入点表达式。一般地,该方法不再需要舔入其他代码
     */
    @Pointcut("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
    public void declareJoinPointExpression() {}

@Before("declareJoinPointExpression()")
public void validateArgs(JoinPoint joinPoint) {

System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));

}


##### 10、基于配置文件的方法来配置 AOP

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"

   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:aop="http://www.springframework.org/schema/aop"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置 bean -->
<bean id="arithmeticCalculator" class="com.spring.xml.ArithmeticCalculatorImpl"/>

<!-- 配置切面的 bean -->
<bean id="loggingAspect" class="com.spring.xml.LoggingAspect"/>
<bean id="vlidationAspect" class="com.spring.xml.VlidationAspect"/>

<!-- 配置 AOP -->
<aop:config>
    <!-- 配置切点表达式 -->
    <aop:pointcut id="pointcut" expression="execution(public int com.spring.xml.ArithmeticCalculator.*(..))"/>
    <!-- 配置切面及通知 -->
    <aop:aspect ref="loggingAspect" order="2">
        <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
        <aop:after method="afterMethod" pointcut-ref="pointcut"/>
        <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/>
        <aop:around method="aroundMethod" pointcut-ref="pointcut"/>
    </aop:aspect>

    <aop:aspect ref="vlidationAspect" order="1">
        <aop:before method="validateArgs" pointcut-ref="pointcut"/>
    </aop:aspect>
</aop:config>