springBoot进阶

kitten 发布于 2025-02-08 370 次阅读


本篇文章主要对springboot基础内容进行拓展

1.登录认证

1.登录

请求路径: /login        请求方式: POST

请求参数: /application/json

返回参数: data:"...JWT令牌"

新建LoginController

package com.kitten.controller;
import ...
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("员工登录: {}",emp);
        Emp e = empService.login(emp);
        return e!=null ? Result.success() : Result.error("用户名或密码错误");
    }
}

EmpServiceImpl

...
/**
     * 处理员工登录
     * @param emp
     */
    @Override
    public Emp login(Emp emp) {
        return empMapper.getByUsernameAndPassword(emp);
    }

EmpMapper

/**
     * 员工登录
     * @param emp
     * @return
     */
    @Select("select * from tlias.emp where username=#{username} and password=#{password}")
    Emp getByUsernameAndPassword(Emp emp);

2.登录校验

员工登录后 , 服务端要对登录的员工进行一个 标记. 一般,标记后我们在实现emp管理功能时 , 都要进行 if 判断是否登录 , 这样就比较麻烦 , 所以我们要使用 统一拦截 , 拦截之后就可以对请求进行校验 . 所以登录校验分为两部分 :

1.标记 (会话技术)

2.统一拦截 (过滤器Filter , 拦截器Interceptor)

1.会话技术

浏览器和服务器的一次连接就是一次会话 , 一次会话可以包含多项请求

会话跟踪: 一种维护浏览器状态的方法 , 服务器需要识别多次请求是否来自于同一浏览器 , 以便在同一次会话的多次请求间共享数据 , 常用的会话跟踪技术 : 客户端会话跟踪技术:Cookie , 服务端~Session , 令牌技术token

优点 : HTTP协议中支持的技术

缺点 : 移动端APP无法使用Cookie , 不安全,用户可以自己禁用 , Cookie不能跨域

优点 : 存储在服务端, 安全

缺点 : 服务器集群环境下无法直接使用Session , 而且还有Cookie的缺点

优点 : 支持PC端,移动端 , 解决集群环境下的认证问题 , 减轻服务器端存储压力

缺点 : 需要自己实现


下面我们来介绍JWT

JWT (JSON Web Token https://jwt.io/ )

组成 :

第一部分 :Header , 记录令牌类型 , 签名算法等

第二部分 :Payload , 有效载荷 , 携带一些自定义信息

第三部分 :Signature , 签名 , 防止Token被篡改 , 确保安全性

生成和校验 :

样例 : springboot 单元测试类中 :


package com.kitten;

import ...

//@SpringBootTest
class TestManagementApplicationTests {
    @Test
    void contextLoads() {
    }
//    生成令牌
    @Test
    public void testGenJwt(){
        Map claims = new HashMap<>();
        claims.put("id",1);
        claims.put("name","tom");
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256,"kitten") //签名算法
                .setClaims(claims)       //自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis()+3600 * 1000))    //设置有效期 1h
                .compact();
        System.out.println(jwt);
    }
//    解析令牌
    @Test
    public void testParseJwt(){
        Claims claims = Jwts.parser()
                .setSigningKey("kitten")
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcxMzc2ODkxOH0.CqhOrAplEmgUW5_MaiUx9SwfnFMPjtYQSXdyhaRKI0I") //这里是设置了一段加密内容,可以尝试运行得到解密结果
                .getBody();
        System.out.println(claims);
    }
}

这个demo是 jwt 的基本使用说明 , 接下来我们在之前的项目中集成这个功能

新建 Utils/JwtUtils

package com.kitten.utils;
import ...
public class JwtUtils {
    private static String signKey = "kitten";
    private static Long expire = 43200000L;     // 12h
    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @return
     */
    public static String generateJwt(Map claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }
    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

修改 Controller/LoginController

...
@PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("员工登录: {}",emp);
        Emp e = empService.login(emp);
//        登录成功  就生成和下发令牌
        if(e != null){
            Map claims = new HashMap<>();
            claims.put("id",e.getId());
            claims.put("name",e.getName());
            claims.put("username",e.getUsername());
            String jwt = JwtUtils.generateJwt(claims);
            return Result.success(jwt);
        }
        return Result.error("用户名或密码错误");
    }

2.统一拦截技术

Filter入门

它是过滤器 , 是javaweb 三大组件之一 (Servlet , Filter , Listener)

可以把对资源的请求拦截下来 , 从而实现一些特殊的功能

一般用于一些通用的操作 , 比如 : 登录校验 , 统一编码处理 , 敏感字符处理等

入门 :

1.定义Filter : 定义类 , 实现接口 , 并重写方法

2.配置 : 加上@WebFilter注解 , 配置拦截资源的路径 , 引导类上加@ServletComponentScan 开启Servlet组件支持

新建 filter/DemoFilter

package com.kitten.filter;
import ...
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        //1.获取请求url
        String url = req.getRequestURL().toString();
        log.info("请求的url是: {}",url);
        //2.判断请求url里是否包含Login,如果包含,说明是登录操作,放行
        if(url.contains("login")){
            log.info("登录操作,放行...");
            filterChain.doFilter(servletRequest , servletResponse);
            return ;
        }
        //3.获取令牌
        String jwt = req.getHeader("token");
        //4.判断令牌是否存在 , 这里用maven导入一下 转JSON工具 fastjson
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }
        //5.解析Token
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) { //JWT解析失败 : 1.令牌异常2.时间超过
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }
        //6.放行
        log.info("令牌合法,放行");
    }
}

pom.xml

<dependency>
        <groupId>com.alibaba</groupId>
        <artifactd>fastjson</artifactId>
        <version>2.0.40</version>
    </dependency>
Interceptor入门

一种动态拦截方法调用的机制,类似于过滤器.Spring框架中提供的,用来动态拦截控制器当中方法的执行

作用 : 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码

使用 :

  1. 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法
  2. 注册拦截器 , 配置拦截器

定义拦截器 , 新建Interceptor/LoginCheckInterceptor Class

这个类实现springboot提供的接口 HandlerInteceptor , 并重写方法

package com.kitten.Interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override       //目标资源方法运行前运行 , 返回true:放行 false:不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle ...");
        return true;
    }

    @Override       //目标资源方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...");
    }

    @Override       //视图渲染完毕后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

配置拦截器

config/WebConfig

package com.kitten.config;

import com.kitten.Interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

//    注册拦截器方法
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        WebMvcConfigurer.super.addInterceptors(registry);
        // 拦截所有资源的拦截器
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

常见config :

  1. /*    ,拦截一级路径,不能拦截 /emps/1 这种
  2. /** ,match any path
  3. /depts/* ,能匹配/depts/1 , 不能匹配/depts/1/2
  4. depts/** ,能匹配/depts下的任意级路径

最后是集成

修改interceptor/LoginCheckInterceptor

package com.kitten.Interceptor;
import ...
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override       //目标资源方法运行前运行 , 返回true:放行 false:不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取请求url
        String url = request.getRequestURL().toString();
        log.info("请求的url是: {}",url);
        //2.判断请求url里是否包含Login,如果包含,说明是登录操作,放行
        if(url.contains("login")){
            log.info("登录操作,放行...");
            return true;
        }
        //3.获取令牌
        String jwt = request.getHeader("token");
        //4.判断令牌是否存在 , 这里用maven导入一下 转 JSON工具 fastjson
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }
        //5.解析Token
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) { //JWT解析失败 : 1.令牌异常2.时间超过
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result error = Result.error("TOKEN_invalidation");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }
        //6.放行
        log.info("令牌合法,放行");
        return true;
    }
    @Override       //目标资源方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...");
    }
    @Override       //视图渲染完毕后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

可以发起请求测试一波

3.异常处理

现在我们来看下异常处理 , 当发生一下情况时 :

现在数据库中有一个部门 人事部 , 前端如果发送了一条 添加部门 人事部 的请求 , 那么服务器端的数据库由于对部门名称添加了 unique关键字, 所以是会报错的 , 但是这条错误我们并没有做处理 , 所以前端页面中的 NETWORK 中 , 我们可以捕捉到下面消息 :

{
    "timestamp":"...",
    "status":500,
    "error":"Internal Server Error",
    "path":"/depts"
}

那么我们该如何进行处理呢? 有下面几种方案 :

  1. 在Controller的方法中进行 try…catch处理 (代码臃肿 , 不推荐)
  2. 全局异常处理器 (简单 , 推荐) 即:面向切面编程 AOP

全局异常处理器介绍

一般 , Mapper层出现问题 , 会往上抛出给调用者 Service , Service不会处理 , 继续上抛给Controller层 , 然后Controller 就会直接向前端响应 错误信息 , 所以我们可以定义一个 全局异常处理器 , 用来处理各个层上抛的异常信息 , 封装为Result , 并响应给前端

使用 :

新建 com.kitten/Exception/GlobalExceptionHandler

package com.kitten.exception;
import com.kitten.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)  //捕获所有的异常
    public Result ex(Exception ex){
        ex.printStackTrace();
        return Result.error("error! operation failed!");
    }
}

其中 @RestControllerAdvice 这个注解 等同于 @ControllerAdvice + @ResponseBody , @ResponseBody使用在控制层(controller)的方法上 , 能将方法的返回值,以特定的格式写入到response的body区域,进而将数据返回给客户端。如果返回值是字符串,那么直接将字符串写到客户端;如果是一个对象,会将对象转化为json串,然后写到客户端。

2.事务管理 & AOP 开发

1.事务管理

@Transactional 注解

在学 MySQL 时 , 我们就了解过事务 , 事务 是一组操作的集合 , 这些操作 要么同时成功 , 要么同时失败

操作 :

  1. 开启事务 , start transaction / begin
  2. 提交事务 , commit
  3. 回滚事务 , rollback

案例 : 解散部门

在之前写的案例中 , 我们删除部门后 , 属于该部门的员工是不会有任何改变的 , 这就是不合理的地方 , 部门删除后 , 部门中所有员工也应当删除 , 我们接下来来完善一下 :

EmpMapper

...
/**
     * 根据部门id 删除员工
     */
    @Delete("delete from tlias.emp where dept_id=#{deptId}")
    void deleteByDeptId(Integer deptId);

DeptServiceImpl

...
    @Autowired
    private EmpMapper empMapper;
/**
     * 2.根据id删除部门
     * //事务更新 : 还要删除部门员工
     */
    public void deleteDeptById(Integer id) {
        deptMapper.deleteDeptById(id);
        int i = 1/0;    //模拟异常
        empMapper.deleteByDeptId(id);
    }

上面代码中 , 我们完善了功能的逻辑 , 但是我们还模拟了一个异常 : 当执行删除部门功能后 , 部门确实会被删除 , 但是在删除员工之前 , 中间却抛出了一个异常 , 这就导致执行完删除部门请求后部门消失 , 但是部门对应员工依然存在 , 这就导致了数据的不一致,不完整

这里我们就需要通过事务来控制这两个功能(删除部门和删除部门员工)的 同一 , 即让它们同时成功或同时失败.这里需要 @Transactional 注解 , 这个注解可以作用在 接口 , 方法 , 类上 , 作用在接口上 , 就代表这个接口下所有的实现类 的所有方法都会有 事务控制 ; 作用在类上 就代表这个类的所有方法都有事务控制 ; 作用在方法上就代表 这个方法被事务管理

首先要进行配置:

application.yml

...
# spring transaction management
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

DeptServiceImpl

...
    @Transactional  //将该方法 定义为 事务管理 , 还要配置文件
    public void deleteDeptById(Integer id) {
        deptMapper.deleteDeptById(id);
        empMapper.deleteByDeptId(id);
    }

加上注解之后 , 就会在方法运行之前开启事务 , 运行完毕后提交事务 , 遇到错误会回滚

要注意一点 : 数据库的引擎 innodb 是支持事务的 , 所以创建数据库的时候记得配置一下 , 用别的引擎可能会有问题

rollbackFor 属性

默认情况下 , 只有出现RuntimeException 才回滚异常 , rollbackFor 属性用于控制出现何种异常类型 , 回滚事务

...
@Transactional(rollbackFor=Exception.class)
...

上面我们使用@Transactional注解完成事务控制, 这属于声明式事务, 之后我们在遇到 分布式 项目后, 可能会涉及到更复杂的事务控制, 小伙伴们可以自行了解 编程式事务

propagation 传播行为

什么事~传~播~行~为~呢~ , 比如说有俩方法 :

@Transactional
public void a(){
    .....
    useService.b();
}
@Transactional
public void b(){
    ....
}

那么在执行 a 方法的时候 , 会开启一个事务 , 在 方法体中我们又会执行b方法 , 那么是新开一个事务还是说让b方法的事务加入到a方法中呢 ? 这就涉及到了传播行为

我们可以手动指定事务的传播行为:

@Transactional(propagation=Propagation.REQUIRED)
public void a(){
    .....
    useService.b();
}

事务案例 : 在删除部门时 , 无论是操作成功还是失败 , 都要记录操作日志

新建表 dept_log

create table dept_log(
       id int auto_increment comment '主键ID' primary key,
    create_time datetime null comment '操作时间',
    description varchar(300) null comment '操作描述'
)comment '部门操作日志表';

创建对应的 DO实体类, 这里不赘述

创建修改数据库的方法

mapper/DeptLogMapper

package com.kitten.mapper;
import ...
@Mapper
public interface DeptLogMapper {
    @Insert("insert into tlias.dept_log(create_time,description) values(#{createTime},#{description})")
    void insert(DeptLog log);
}

Service/impl/DeptLogServiceImpl interface层不展示

package com.kitten.service.impl;
import ...
@Service
public class DeptLogServiceImpl implements DeptLogService {
    @Autowired
    private DeptLogMapper deptLogMapper;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

具体的sql这里不赘述, 下面我们模拟一下运行时异常

DeptServiceImpl

/**
     * 2.根据id删除部门
     * 事务更新 : 还要删除部门员工
     */
    @Transactional  
    public void deleteDeptById(Integer id) {
        try {
            deptMapper.deleteDeptById(id);
            int i = 1/0;
            empMapper.deleteByDeptId(id);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("执行了解散部门的操作,解散的是:"+id+"号部门");
            deptLogService.insert(deptLog);
        }
    }

这里我在 deptServiceImpl中 模拟一个异常 , 那么执行时就会 catch住 , 然后执行finally 块 , 会执行一个insert语句 , 这个语句 也有 @Transactional 注解 , 同样会开启一个 事务 , 那么在默认没给 insert方法的 @Transactional 配置 传递关系的话 , insert方法开启的事务就会 participate in (加入) 到 deleteDeptById方法开启的事务中 , 这样最后回滚的时候 , 就不会执行 insert 中的功能 , 也不会有日志生成 . 但是当给 insert 的方法的 @Transactional 配置 propagation = Propagation.REQUIRED_NEW , 那么在执行到insert 语句中就会将 deleteDeptById方法的 事务暂时挂起 , 新开一个事务 , 这个事务由于没有异常抛出 , 是不会回滚的 , 因此会在 dept_log数据库表中生成日志记录 , 然后事务结束 , 挂起的事务再运行 …

2.AOP基础

AOP , Aspect Oriented Programming

场景 : 对于一些执行较慢的功能 , 我们想统计其耗时 , 该怎么做

使用动态代理 , 它是 面向切面编程 最主流的实现 . SpringAOP 是Spring 框架的高级技术 , 旨在管理 bean对象的过程中 , 通过底层的代理机制 , 对特点的方法进行编程

快速入门 :

统计各个业务层方法执行耗时

1.导入依赖

pom.xml


<!--      导入AOP   -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.编写AOP程序 : 针对于特定方法根据业务需要进行编程

我们要做的就是定义一个模板方法 , 首先需要一个类 , 将这个类交给IOC容器管理 (@Component) , 要将这个类定义为AOP类, 需要@Aspect 注解 , 最后在这个AOP类中定义模板方法

新建 com.kitten/aop/TimeAspect

package com.kitten.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class TimeAspect {
    @Around("execution(* com.kitten.service.*.*(..))")  //切入点表达式
    // service下的任意类的任意方法(至少一个) ,而且任意形参(可以没有) 被AOP管理
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.记录开始时间
        long start = System.currentTimeMillis();
        //2.调用源方法
        Object result = joinPoint.proceed();
        //3.记录结束时间
        long end = System.currentTimeMillis();

        log.info(joinPoint.getSignature()+"方法执行耗时:{}ms",end-start);
        return result;
    }
}

简单说下 上面这段代码重要的点 : 切入点表达式 , 上面这个表达式表明会对service层中所有的接口的实现类的方法进行AOP管理 , 这就是为什么 service层下要创建 接口 和实现类包 impl 被AOP管理的方法的左边是会有 一个m小图标的

写完之后重启一下服务 , 测试方法执行耗时

AOP核心概念

  • 连接点 joinPoint , 指的是可以被AOP控制的方法
  • 通知 Advice , 指的是哪些重复的逻辑 , 也就是共性功能
  • 切入点 PointCut , 匹配连接点的条件 , 通知仅会在切入点方法执行时被使用
  • 切面 Aspect , 描述通知与切入点的对应关系 , 被AOP管理的类我们叫 切面类
  • 目标对象 Target , 通知所应用的对象

原理大概就是 AOP 会对执行的方法生成一个代理对象 : DeptService →DeptServiceProxy , 并且在调用的时候不再使用原来定义的方法 而是代理对象执行的方法 , 通过断点测试 , 我们可以知道 调用的是spring的CGLIB动态代理

AOP进阶

1.通知类型 :

  1. @Around : 环绕通知 , 此注解标注的通知方法在目标前 , 后都被执行环绕通知需要自己调用 ProceedingJoinPoint.proceed()方法
  2. @Before : 前置通知 , 此注解标注的通知方法在目标方法前被执行
  3. @After: 后置(最终)通知 , 此注解标注的通知方法在目标方法后被执行(不论是否有异常)
  4. @AfterReturning: 返回后通知 , 此注解标注的通知方法在目标方法后被执行 , 有异常不会执行
  5. @AfterThrowing : 异常后通知 , 此注解标注的通知方法在目标方法异常后被执行

aop/myAspect

package com.kitten.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class myAspect {
    @Pointcut("execution(* com.kitten.service.impl.DeptLogServiceImpl.*(..))")
    private void pt(){}
    @Before("pt()")
    public void Before(){
        log.info("before...");
    }
    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before...");
        Object proceed = proceedingJoinPoint.proceed();
        log.info("around after");
        return proceed;
    }
    @After("pt()")
    public void after(){
        log.info("after...");
    }
    @AfterReturning("pt()")
    public void afterReturning(){
        log.info("afterReturning...");
    }
    @AfterThrowing("pt()")
    public void afterThrow(){
        log.info("afterThrowing...");
    }

}

对于上面这个小案例 , 要注意的就是 @PointCut 这个注解 , 将公共的 切入点表达式 抽取出来 , 用到时引用即可 , 若是在不同包下使用这个切入点表达式 , 只用将这个公共方法的 访问层级改成 public 就行

2.通知顺序

当有多个切面的切入点都匹配到了目标方法 , 目标方法运行时,多个通知方法都会执行

比如说 , 我又定义了几个类 myAspect2 , myAspect3 , …. , 它们所包含的方法几乎与上面一致并且切入点表达式相同(说明管理的是同一套方法) , 但是分别输出 before…1 , before…2 , … , after…1 , after…2 , … ; 那么我在启动项目后 , 会根据类名顺序来输出 , 结果为 :

time -- INFO --- com.kitten.aop.myAspect2    : before...2
time -- INFO --- com.kitten.aop.myAspect3    : before...3
time -- INFO --- com.kitten.aop.myAspect3    : after...3
time -- INFO --- com.kitten.aop.myAspect2    : after...2

由此可知不同切面类中 , 默认按照切面类的类名字母排序 :

  • 目标方法前的通知方法 : 字母排名靠前的先执行
  • 目标方法后的通知方法 : 字母排名靠前的后执行

这样来说是比较不直观的 , 所以spring 给我们提供了一个 @Order(n) 注解来控制顺序 , n越小越先执行

aop/myAspect2

package com.kitten.aop;
import ...
@Order(3)
@Slf4j
@Component
@Aspect
public class myAspect2 {
    @Pointcut("execution(* com.kitten.service.impl.DeptServiceImpl.*(..))")
    private void pt(){}
    @Before("pt()")
    public void Before(){
        log.info("before...2");
    }
    @After("pt()")
    public void after(){
        log.info("after...2");
    }
}

aop/myAspect

package com.kitten.aop;
import ...
@Order(4)
@Slf4j
@Component
@Aspect
public class myAspect {
    @Pointcut("execution(* com.kitten.service.impl.DeptServiceImpl.*(..))")
    private void pt(){}
    @Before("pt()")
    public void Before(){
        log.info("before...");
    }
    @After("pt()")
    public void after(){
        log.info("after...");
    }
}

那么这样写的话 , 请求处理时 , 方法加工顺序为 :

time INFO nnnn --- [xxx] com.kitten.aop.myAspect2                 : before...2
time INFO nnnn --- [xxx] com.kitten.aop.myAspect                  : before...
...
time INFO nnnn --- [xxx] com.kitten.aop.myAspect                  : after...
time INFO nnnn --- [xxx] com.kitten.aop.myAspect2                 : after...2

切入点表达式

常见的有俩 : execution(…) 根据方法的签名来匹配 和 @annotation 根据注解匹配

  1. execution(访问修饰符? 返回值 包名.类名.方法名(参数) throws 异常?)
    其中 ? 代表可省略的部分, 写几个例子 :
    execution(* com.kitten.service.*Service.delete*(*))    //service包下所有以 Service结尾的类中 所有以delete开头,返回值任意,必须有一个形参的方法
    //测试 : 写一个切入点表达式用于匹配 List list() 和 void delete(Integer id)
    execution(* com.kitten.service.DeptService.list()) || execution(* com.kitten.service.DeptService.delete(Integer))
    
  2. @annotation
    我们在aop下定义一个 MyLog接口 :
    package com.kitten.aop;
    import ...
    @Retention(RetentionPolicy.RUNTIME) //表示注解在 运行时生效 的时间
    @Target(ElementType.METHOD)     //表示注解作用在方法上
    public @interface MyLog {}
    aop/myAspect
    package com.kitten.aop;
    import ...
    @Order(4)
    @Slf4j
    @Component
    @Aspect
    public class myAspect {  
        //1.基于 execution 来作用于不同类中的方法    
        @Pointcut("execution(* com.kitten.service.impl.DeptServiceImpl.queryDeptById(Integer) )")   
        private void pt(){}
        @Before("pt()")  
        public void Before(){       
            log.info("before..."); 
        }   
        //2. 基于 @annotation 来作用于 选定的方法 , 注解中跟的是注解方法的全类名   
        //只要在需要使用下面这个通知的方法上加上一个 @MyLog 注解,就能方便的操控作用范围(连接点)
        @Before("@annotation(com.kitten.aop.MyLog)")  
        public void BeforeByAnno(){       
           log.info("before...");   
        }
    }
    在使用 @annotation注解修饰的方法上加上 annotation指定的接口即可 com.kitten.service.impl.DeptService.DeptServiceImpl
    package com.kitten.service.impl;
    import ...
    @Service
    public class DeptServiceImpl implements DeptService {
        @Autowired
        private DeptMapper deptMapper;
        @Autowired
        private EmpMapper empMapper;
        private DeptLogServiceImpl deptLogService;
    /**
     * 获取部门信息
      */
        @MyLog
        public List queryDept(){
            return  deptMapper.queryDept();
        }
        ...
    }

连接点

在我们使用 @Around 注解时 , 被管理的方法里默认配置了一个参数类 ProceedingJoinPoint 类型 , 对于其他注解 , 我们其实是可以配置 JoinPoint 类型的参数的 , 它是 ProceedingJoinPoint类型的 父类 , 我们可以用于对原方法进行操作

aop/myAspect

//连接点 操作演示
// 就是 proceedingJoinPoint类的一系列API
//注意的点:@Around注解是可以拿到方法的返回值,但是@before这个注解不行
@Around("execution(* com.kitten.service.impl.DeptServiceImpl.*(..))") 
public Object round(ProceedingJoinPoint p) throws Throwable {
 log.info("around before ....");
 //3.1获取目标对象的类名
 String name = p.getTarget().getClass().getName(); 
 log.info("目标对象的类名: {}", name); 
 //3.2获取目标对象的方法名 
 String methodName = p.getSignature().getName(); 
 log.info("目标方法的方法名:{}", methodName); 
 //3.3获取目标方法传入的参数 
 Object[] args = p.getArgs(); 
 log.info("the params of the methods :{}", Arrays.toString(args)); 
 //3.4放行 , 目标方法执行 
 Object result = p.proceed(); 
 //3.5获取目标方法的返回值 
 log.info("around .... {}",result); 
 //可以对目标方法返回的结果进行修改 
 return result; 
}

aop案例

记录操作日志 : 将案例中增,删,改相关接口的操作日志记录在数据库表中

日志信息有 : 操作人,操作时间,执行方法的类名,方法名,方法参数,返回值,执行时长

  1. 准备 : 引入 aop 相关依赖 , 创建数据库表结构 , 并引入实体类
    
    -- 操作日志表
    create table operate_log(
        id int unsigned primary key auto_increment comment 'ID',
        operate_user int unsigned comment '操作人ID',
        operate_time datetime comment '操作时间',
        class_name varchar(100) comment '操作的类名',
        method_name varchar(100) comment '操作的方法名',
        method_params varchar(1000) comment '方法参数',
        return_value varchar(2000) comment '返回值',
        cost_time bigint comment '方法执行耗时, 单位:ms'
    ) comment '操作日志表';
    pojo/operate_log
    package com.kitten.pojo;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; //ID private Integer operateUser; //操作人ID private LocalDateTime operateTime; //操作时间 private String className; //操作类名 private String methodName; //操作方法名 private String methodParams; //操作方法参数 private String returnValue; //操作方法返回值 private Long costTime; //操作耗时 }
  2. 编码 : 自定义注解@Log , 定义切面类 , 完成记录日志逻辑
    com.kitten/anno/Log 接口
    
    package com.kitten.anno;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Log {}
    
    com.kitten/mapper/OperateLogMapper
    package com.kitten.mapper;
    import com.kitten.pojo.OperateLog;
    import org.apache.ibatis.annotations.Insert;
    import org.springframework.stereotype.Component;
    @Mapper
    public interface OperateLogMapper {
        @Insert("insert into tlias.operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) "+"values(#{operateUser},#{operateTime},#{className},#{methodName},#{methodParams},#{returnValue},#{costTime})")
        public void insert(OperateLog operateLog);
    }
    com.kitten/aop/LogAspect 切面类
    package com.kitten.aop;
    import ...
    @Slf4j
    @Component
    @Aspect
    public class LogAspect {
        @Autowired
        private HttpServletRequest httpServletRequest;
        @Autowired
        private OperateLogMapper operateLogMapper;
        @Around("@annotation(com.kitten.anno.Log)")
        public Object recordLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            //1.操作人id -> 当前登录员工id
            //获取jwt令牌,解析令牌,需要自动注入 httpServletRequest
            String token = httpServletRequest.getHeader("token");
            Claims claims = JwtUtils.parseJWT(token);
            Integer id = (Integer) claims.get("id");
            //2.操作时间
            LocalDateTime now = LocalDateTime.now();
            //3.操作类名
            String name = proceedingJoinPoint.getTarget().getClass().getName();
            //4.操作方法名
            String methodName = proceedingJoinPoint.getSignature().getName();
            //5.参数
            Object[] args = proceedingJoinPoint.getArgs();
            String methodParams = Arrays.toString(args);
            //计时
            long start = System.currentTimeMillis();
            //运行原始方法
            Object result = proceedingJoinPoint.proceed();
            long end = System.currentTimeMillis();
            //6.返回值
            String returnValue = JSONObject.toJSONString(result);
            //7.耗时
            long time = end - start;
            //记录操作日志
            OperateLog operateLog = new OperateLog(null,id,now,name,methodName,methodParams,returnValue,time);
            operateLogMapper.insert(operateLog);
            log.info("AOP记录操作日志");
            return result;
        }
    }
    

测试 :

先使用 emp 表中的随便一个用户进行登录 , 拿到token后 , 基于token发请求 (发送的请求 要注意在服务端是被 @Log 注解上的) 然后等待后端响应完成后 , 到operate_log数据库表中查看记录

3.底层原理

1.配置优先级

在前面我们知道 , 配置文件有三种格式 : properties , yml 和 yaml , 其优先级是 properties > yml > yaml

springboot除了支持配置文件属性 , 还支持 Java系统属性命令行参数 的方式进行配置

2022版idea中 , 点击导航栏项目名称 → Edit Configuration → modify options , 自行添加 VM options (java系统属性) 和 Program arguments (命令行参数)

VM option:
-Dserver.port=9000
Program arguments
--server.port=10010

运行项目后可以发现,项目端口号是 10010 , 所以优先级在这里就能看出来了

那么 , 项目打包之后 , 我们如何修改配置属性呢 ?

先使用 maven的 package 生命周期将项目打包 为 jar 包

找到 右侧maven , 选中项目 , 点击 lifestyle , 选择package并双击 , 等待打包完成

如果有报错 : Please refer to 路径 for the individual test results

Please refer to dump files …

pom中加上:

<plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <testFailureIgnore>true</testFailureIgnore>
            </configuration>
        </plugin>

运行 jar包 , 然后在 jar包的当前目录中运行cmd , 再运行下面的java指令 (直接运行 C:\x...\target java 可以查看文档)

java指令 : (按tab 键会自动提示项目名称)

java -Dserver.port=9000 -jar tliasManagementApplication-0.0.1-SNAPSHOT.jar --server.port=10010

注意 : Springboot项目进行打包时 , 需要引入插件 spring-boot-maven-plugin (基于骨架创建项目会自动添加)

基于上面测试 , 我们可知道 配置优先级 :

命令行参数 > java系统属性 > properties配置文件 > yml配置文件

2.bean管理

之前我们说过 , 可以通过 @Component 注解 以及它的三个衍生注解 (Repository、@Service和@Controller注解)

下面从三个方面讲解bean管理

1.获取bean

默认情况下 , spring项目启动时 , 会把bean都创建好都放在 IOC容器中 , 如果想要主动获取 bean , 可以通过下面方式:
根据name获取 :

Object getBean(String name)
根据类型获取 :
 T getBean(Class requiredType)
根据name(类型转换)获取
 T getBean(String name,Class requiredType)

在spring项目的测试类中我们来测试一下 :

test

package com.kitten;
import ...

@SpringBootTest
class TliasManagementApplicationTests {

    ...

    @Autowired
    private ApplicationContext applicationContext;  //IOC容器对象

    @Test
    public void testGetBean(){
        // 根据bean的名称获取
        DeptController bean1 =  (DeptController) applicationContext.getBean("deptController");
        System.out.println(bean1);
        // 根据bean的类型获取
        DeptController bean2 = applicationContext.getBean(DeptController.class);
        System.out.println(bean2);
        //根据bean的名称及类型获取
        DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
        System.out.println(bean3);
    }
}

上述代码我们获取的是 DeptController 这个类的 bean 对象

从打印结果我们可以看到 , 获取的三个 bean对象都是同一个 , 这就涉及到 bean的作用域

2.bean的作用域

Spring支持5种作用域 , 后三种在 Web 环境中才生效

作用域说明
singleton容器内同名称的 bean 只有一个实例 (单例) (spring的默认值)
prototype每次使用该 bean 时会创建新的实例
request每个请求范围内会创建新的实例 (web环境)
session每个会话范围内会创建新的实例 (web环境)
application每个应用范围内会创建新的实例 (web环境)

可以通过@Scope() 注解来配置作用域

示例 :

DeptController

package com.kitten.controller;
import ...
/**
 * 部门管理Controller
 */
//Slf4j注解为logback提供的注解,会自动为当前类提供日志log对象
//    RestController包含responseBody注解 , 会将响应对象转换为json
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {
    //构造器 , 获取 bean对象测试用
    public DeptController(){
        System.out.println("DeptController constructor.....");
    }
    ...
}

test

...
@Test
    public void soutBean(){
        for (int i = 0; i < 5; i++){
            DeptController bean = applicationContext.getBean(DeptController.class);
            System.out.println(bean);
        }
    }

已知Spring默认的bean对象的作用域是singleton , 由上面方法的断调试 (结果是 : 没有调用getBean方法时 , 却打印出了 DeptController constructor…..) 表明 在spring项目启动的时候 , DeptController 这个类已经实例化了 (是在容器启动的时候实例化的) , 并且交由 IOC 容器管理

我们可以在第一次使用的时候单例化 , 需要在 DeptController 这个类上加上 @Lazy 注解 , 表明延迟初始化 (延迟到第一次使用的时候) . 在上面这个案例中一旦启动 test方法 , 只有上面这个 for 循环第一次执行时 , 才会调用构造器函数 (而且,由于默认bean作用域为singleton , 所以 5次打印的bean都是同一个 )

接下来我们设置DeptController这个 bean 的作用域

package com.kitten.controller;
import ...
/**
 * 部门管理Controller
 */
//Slf4j注解为logback提供的注解,会自动为当前类提供日志log对象
//    RestController包含responseBody注解 , 会将响应对象转换为json
@Scope("prototype")
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {
    //构造器 , 获取 bean对象测试用
    public DeptController(){
        System.out.println("DeptController constructor.....");
    }
    ...
}

这样写后 , 再运行 test 方法 , 我们就会拿到 5个 不同的 bean 对象

重点 : 了解 bean 对象的创建时间 , 了解 @Lazy 注解的作用 , 明确如何设置 bean 对象的作用域

3.第三方bean

在项目中 , 我们自己会定义一些类 , 并给它们加上 @Component 及其衍生注解 , 将它们交给 IOC 容器管理 , 但是在一些第三方类中 , 我们想要直接注入该怎么办呢 ?

比如 , 我们之前是用到了 dom4j 这个工具包来读取xml文件 , 下面是一段示例

resources/test.xml

<root>
    <name>kitten</name>
    <password>12345678</password>
</root>

导入dom4j 这里不给出示例

test 测试类

//测试 第三方 bean
    @Test
    public void testThirdBean() throws DocumentException {
        SAXReader saxReader = new SAXReader();

        Document document = saxReader.read(this.getClass().getClassLoader().getResource("test.xml"));
        Element rootElement = document.getRootElement();
        String name = rootElement.element("name").getText();
        String password = rootElement.element("password").getText();

        System.out.println(name + "密码:"+password);
    }

在上面的test中 , 我们是直接 new 一个 SAXReader , 这样做是比较耗费资源的 , 我们该怎么对其进行 IOC 管理呢?

你可能会想到 , 我找到 SAXReader的 原类 , 在其上面加上 @Component 注解 , 但是你会发现我们引入的包是一个只读文件 , 无法对其进行修改 , 这时候就需要用到我们的 第三方 bean 管理方案

我们的解决方案是 : 使用 @Bean 注解

首先要在spring 的 入口方法 application类下 声明第三方类 :

TliasManagementApplication.java

package com.kitten;
import org.dom4j.io.SAXReader;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
    @Bean   //将当前方法的返回值对象交给IOC容器管理
    public SAXReader saxReader(){
        return new SAXReader();
    }
}

test

//测试 第三方 bean
    @Autowired
    private SAXReader saxReader;
    @Test
    public void testThirdBean() throws DocumentException {
//        SAXReader saxReader = new SAXReader();

        Document document = saxReader.read(this.getClass().getClassLoader().getResource("test.xml"));
        Element rootElement = document.getRootElement();
        String name = rootElement.element("name").getText();
        String password = rootElement.element("password").getText();

        System.out.println(name + "密码:"+password);
    }

上面的示例中 , 我们在 spring 的入口类中 声明了一个@Bean , 但是实际情况下, 如果有第三方 bean 对象需要 IOC 容器管理 , 我们建议在 config 包下 使用@Configuration 注解声明一个配置类 :

将上面示例中的 spring入口 的 saxReader() 注释掉 , 新建 config/CommonConfig

package com.kitten.config;
import ...
@Configuration
public class CommonConfig {
//可以通过 bean 注解的 value / name 属性指定bean名称
    @Bean   //将当前方法的返回值对象交给IOC容器管理
    public SAXReader saxReader(){
        return new SAXReader();
    }
}

可以通过 @Bean 注解的 value / name 属性指定bean名称

将上面代码的 @Bean 注解改为 @Bean(value = "reader")

test

@Test
    public void testGetBeanByName(){
        Object reader = applicationContext.getBean("reader");
        System.out.println(reader);
    }

获取第三方bean

在我们声明的方法中 , 如果第三方bean需要依赖其他bean对象 ,可以在参数中进行自动装配 , spring 将完成自动注入操作

@Bean   //将当前方法的返回值对象交给IOC容器管理
    public SAXReader saxReader(DeptService deptService){
        return new SAXReader();
    }

这样我们就成功的注入第三方Bean了, 并且不会在Aplication中声明大量的Bean导致冗余

以上就是我们的Bean管理的内容, 下面我们更深入的了解一下springboot

3.SpringBoot原理

依照原来的Spring进行开发,会比较繁琐 , 一个是 pom.xml 的配置 , 需要自己搜寻对应的包和相关的依赖 ; 二是需要配置大量的依赖

springBoot的好处就在于提供两个底层配置 : 起步依赖自动配置

1.起步依赖

如果我们构建一个spring 项目 , 那么就需要引入一些 依赖 :

  • spring-web-mvc , 这是spring项目进行web开发所必要的依赖
  • servlet-api , servlet的依赖
  • jackson-databind , 处理json的依赖
  • aspectjweaver , aop相关依赖

而且我们引入的这些依赖必须版本匹配 , 否则可能会有版本冲突问题 ; 而在springboot 项目中 ,我们只用引入一个 spring-boot-starter-web 就行 , 这是因为有一个 依赖传递 , 即它集成了我们开发的常见所有依赖 (而且还引入了某些依赖的依赖 , 这就是依赖传递)

2.自动配置

springboot的自动配置就是当 容器启动后 , 一些配置类,bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作

我们稍微操作下 : 启动springboot项目 , 在console中 , 可以查看 actuator ,这里可以看到项目状况 (beans , health等)

如果出现 failed to check aplication ready state ... 需要装依赖 :

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

可以尝试将所有 Java 程序关闭,然后将 C:\Users\{username}\AppData\Local\Temp\hsperfdata_{username} 这个文件夹中的内容删掉,username代表操作系统的用户名。

如果还不行 , 那么在之前说过的 配置优先级 中 , 我们配置了 VMOptions (Java系统属性) , 那么将配置内容改为 :

-Djava.rmi.server.hostname=localhost

这样我们就能看见 bean 对象 , 除了我们自己定义的 DeptController , DeptService , DeptMapper ….. 还有很多很多类. 这些配置类就是springboot启动时加载进来的配置项 , 提一下有一个gson (Google提供的) , 是用来处理json格式的数据的. 我们在下面测试一下

test

//Gson测试
    @Test
    private Gson gson;
    public void testGson(){
        String json = gson.toJson(Result.success());
        System.out.println(json);
    }

自动配置原理

我们准备了一个 kitten-utils 第三方依赖 , project中我们导入进来 , 然后在项目的 maven 中引入其坐标

先简单看下utils模块代码 , 代码中的几个类都被@Component注解及其衍生注解管理 , 这样我们在测试类中使用一下 , 看看情况

//kitten-utils测试
    @Test
    public void testTokenparser(){
        System.out.println(applicationContext.getBean(TokenParser.class));
    }
//获取HeaderParser
    @Test
    public void testHeaderParser(){
        System.out.println(applicationContext.getBean(HeaderParser.class));
    }
    //获取HeaderGenerator
    @Test
    public void testHeaderGenerator(){
        System.out.println(applicationContext.getBean(HeaderGenerator.class));
    }

然而运行结果是报错的。虽然 kitten-utils下的类都有 @Component注解及其衍生注解 , 但是它是不能被spring组件扫描 扫描到的。之前有说过 @SpringBootApplication 这个注解会扫描当前包及其子包 , 所以我们要考虑如何解决获取 bean 的问题

方案一 : @ComponentScan 组件扫描
@ComponentScan({"com.example","com.kitten"})
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}

这样配置并运行test 里的 testTokenparser() 后, 就能打印出bean对象了 (先不测试其他方法)

第一种方案使用比较繁琐 (当有多个第三方依赖要管理时,需要大量声明) , 而且这种大面积的扫描 , 性能也不高

方案二 : @Import 导入。使用@Import 导入的类会被 Spring加载到 IOC 容器中 , 导入形式有下面几种 :

普通类

TliasManagementApplication

@Import({TokenParser.class})    //2.1 获取普通类
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}

配置类

(关于配置类的使用和说明在 3.底层原理→2.bean管理→3.第三方bean 中)

com.example.headerConfig

package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HeaderConfig {
    @Bean
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    @Bean
    public HeaderGenerator headerGenerator(){
        return new HeaderGenerator();
    }
}

TliasManagementApplication

@Import({HeaderConfig.class})   //2.2 获取配置类
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}

导入 ImportSelector 接口实现类

com.example.myImportSelector

package com.example;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.example.HeaderConfig"};
    }
}

TliasManagementApplication

@Import({MyImportSelector.class})
...

第三方自己提供@EnableXxxx注解,封装 @Import注解

我们引入的 kitten-utils中除了上面的几个类 , 也提供了一个EnableHeaderConfig 接口用于我们测试

com.example.EnableHeaderConfig

package com.example;
import ...
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableHeaderConfig {}

TliasManagementApplication

@EnableHeaderConfig
...

3.源码跟踪

对springboot项目进行解读比较麻烦,我们从启动类开始

在@SpringBootApplication 这个注解里 , 进入到源码中我们可以看到里面也封装了一些注解 , 除了配置自定义注解的原注解 (@Target,@Retention,@Document,@Inherited) ,我们先关注 @SpringBootConfiguration 这个注解 。看这个注解的源码 , 我们可以看到它封装了 @Configuration 这个注解(用来声明配置类) , 这也是为什么之前在讲 第三方bean 的时候我们能直接在springboot的入口类中直接配置 @Bean 对象 , 因为springboot的入口类也是一个配置类。

我们再关注 @ComponentScan(xxx) 这个注解 , 我们之前在SpringBoot基础的 分层解耦→IOC-DI 模块简单提过 : @Component及其衍生注解 注解的bean对象想要生效,还需要被组件扫描注解@ComponentScan 扫描 , 那么它的作用我们就清楚了。

最后我们来看 @EnableAutoConfiguration 这个注解。从名称我们可以看出 , 它就是我们上面所讲的 自动配置 的核心注解。我们进入这个注解的源码中 , 确实可以看见它是封装了一个 @Import (AutoConfigurationImportSelector.class) 注解 , 也确实可以看见它是导入了一个 xxxImportSelector实现类 , 我们进入这个实现类 , 可以看到 它实现了一个 DeferredImportSelector 接口 , 在这个接口中有一个 非常重要的方法 selectImports , 它返回一个 String 数组 , 这个数组就存放了 哪些类我要导入到 IOC 容器当中 , 里面放的就是全类名。

我们回到AutoConfigurationImportSelector 这个实现类 , 找到它实现接口的 selectImports 方法 :

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

主要看这个方法的返回值 : StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()) 我们先看下 autoConfigurationEntry 这个 entry , 它是通过 this.getAutoConfigurationEntry(annotationMetadata) 这个方法得到的 , 我们进入 getAutoConfigurationEntry 这个方法 , 可以看到 :

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationEntry(configurations, exclusions);
        }
    }

还是先关注返回值 : new AutoConfigurationEntry(configurations, exclusions) , 查看参数 configurations , 它是由 this.getCandidateConfigurations(annotationMetadata, attributes) 得到的 , 那么我们再进入这个方法 :

protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

它返回一个 configurations , 这个configurations是由 load()这个方法返回的一个集合。我们不清楚这个load做了什么 , 但是我们可以看到下面有一个 Assert 断言 ,它用来判断 configurations 是否为空 , 如果为空 , 那么就会返回一个提示 : 没有一个自动配置的类在 META-INF/spring/... 这个配置文件中被发现 , 就是说 springboot 在启动的时候会自动加载这个文件中所配置的信息 , 加载出来之后就会封装到 configurations 这个List集合中 , 最终再将其封装到 String 数组中 , 而且这个数组中封装的就是 加载的 bean 或者 配置类

我们可以在项目的 External Libraries 中找到 org.springframework.boot:spring-boot-autoconfigure:3.2.4 ,并在里面发现这个META-INF文件夹 。在spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. 里,就是配置类的全类名(这些类的名称的最后面都是AutoConfiguration , 所以叫自动配置类) , 那么这份文件将会被读取出来并通过 @Import 注解加载到 IOC 容器中

在这个配置文件中 , 我们按下 ctrl+f ,搜索 gson , 可以看到里面有一条写的是 org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration 这就是我们之前在 3.SpringBoot原理→2.自动配置 中 演示的案例的bean对象 的全类名

就来说下我们为啥能在项目中直接使用@Autowired 自动注入 gson 这个东西 ? 首先springboot项目中有一个自动配置功能,可以将META-INF/spring这个文件夹中的 AutoConfigurations 用@Import注解装载到 IOC 容器中 , 使用 @Import 的条件是装载的是一个配置类 , 所以我们可以在springboot提供的 gson 的源码中看见 autoConfiguration注解 (这个注解中就包含@Configuration注解) ,所以我们可以直接注入使用

最后梳理一下 : @SpringBootConfiguration 注解分为三个

  1. @SpringBootConfiguration这个注解包含@Configuration , 表明我们springboot项目的入口类也是一个配置类 , 我们可以在里面声明 @Bean
  2. @ComponentScan(…)进行组件扫描 , 默认扫描的是当前包及其子包。(扫描我们定义的@Component ,就是 bean对象)
  3. @EnableAutoConfiguration实现自动化配置的核心注解 , 底层封装的有 @Import(AutoConfigurationImportSelector.class) 注解 , 指定了一个实现类 , 实现了一个 selectImport 方法 , 返回一个String类型的数组 , 里面放的是 要导入到 IOC 容器当中的全类名。这个方法中还加载了一个文件 , 指定了导入的类的全类名(大概140多个) , 最终会通过 @Import(…) 这个注解加载到 IOC 容器中

那么思考一下 , 这些bean都会放进IOC容器当中吗 ? 其实是不会的 , 在 gson 的例子中 , 我们看下它的源码 :

public class GsonAutoConfiguration{
    ...
    @Bean
    @ConditionalOnMissingBean
    public Gson gson(GsonBuilder gsonBuilder) {
        return gsonBuilder.create();
    }
}

这其中有一个 @ConditionalOnMissingBean注解 , 其实很多配置类中返回声明的bean的方法上都有这个注解 , 它就能根据某些状态来自动进行装配 , 下面我们来说下 @Conditional这个条件装配注解

4.@Conditional

作用 : 按照一定的条件进行判断 , 在满足给定条件后才会注册对应的bean对象到Spring IOC 容器中

位置 : 方法 或 类

本身是一个父注解 , 衍生出大量子注解 :

  • @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器
  • @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册到 IOC 容器
  • @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册到bean容器

kitten-utils 这个工具类为例 , 我们对 HeaderConfig 这个类中的 headerParser()方法进行条件装配 :

@Bean
    @ConditionalOnClass(name = "io.jsonwebtoken.Jwts")
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

上面这个例子就表明 , 当环境中存在 Jwts 这个类,才会将 这个方法返回的 bean 装载到 IOC 容器中 , 是根据指定 类型 (用value属性判断) 或 名称(用name属性判断)

@ConditionalOnMissingBean
...

这个例子表明 , 不存在该类型的bean (HeaderParser类型) , 就会把这个bean 加入到 IOC 容器中

@ConditionalOnproperty(name="name",havingValue = "kitten") 

application.yml

name: kitten

如果我们在配置文件中这样声明 , 那么在启动spring项目的时候 , 就会把 @ConditionalOnproperty 注解声明的类或方法返回的 对象 装载到 IOC容器中

5.案例

自定义 aliyun-oss-spring-boot-starter , 完成阿里云OSS 操作工具类 AliyunOSSUtils的自动配置

我们之前 , 是在 pom.xml中引入 阿里云OSS 相关依赖 , 然后 参照了官方 SDK 改造了 工具类 , 然后在 yml 配置文件中 配置了 阿里云 的参数 , 还在 Aliproperties 实体类中 加载 yml 文件中的配置选项 , 最后才能在 utils 类中获取到对象 , 并将 Utils 交给 IOC 容器管理

  1. 创建aliyun-oss-spring-boot-starter 模块
  2. 创建aliyun-oss-spring-boot-autoconfigure 模块 , 在 starter中引入该模块
  3. 在aliyun-oss-spring-boot-autoconfigure 模块中定义自动配置功能 , 并配置 META-INF/spring/xxxx.imports

file → new Module → 创建 aliyun-oss-spring-boot-starter 模块, 然后把里面的文件都删掉 , 注意要留下 iml 文件和 pom.xml文件

如果没有 iml 文件 , 需要自己生成 : 双按 ctrl , 然后选择模块 aliyun-xxx , 然后在输入框中运行 mvn idea:module , 等待安装完成后就出现 iml 文件了

对于 pom.xml 文件 , 我们可以删去里面的 test 单元测试依赖 , 然后删除 插件

然后创建 aliyun-oss-spring-boot-autoconfigure 模块 , 和上面 的 starter 模块进行一样的操作 , 但是要留下 src文件夹 , 并删除 入口类 和 单元测试类

目标是 : 当我们想使用 阿里云OSS 时 , 注入 AliyunOSSUtils 工具类就行

先在 autoconfigure 模块的pom.xml中引入 阿里云OSS 配置依赖 , 还有 MultipartFile 对应的springWeb依赖

然后在模块中建立两个类 : AliOSSUtilsAliOSSProperties

AliOSSProperties 是用来声明配置属性 endpoint 和 bucketName 的实体类 , 我们在之前的项目中 , 直接把它作为 Component 装配到 IOC 容器中 , 然后在 AliOSSUtils 中直接注入使用。但是注意这里 , aliyun-oss-spring-boot-autoconfigure这个模块是没有启动类的(我们删掉了) , 所以我们不能使用@Component注解 , 因为不会被 组件扫描。同样我们在AliOSSUtils这个类上也不能使用@Component , 也不能进行 @Autowired注入。这样我们的解决方案就是 : 写一个配置类 , 使用@Bean注解 管理 AliOSSUtils

com.aliyun.oss/AliOSSUtils

package com.aliyun.oss;
import ...
/**
 * 阿里云 OSS 工具类
 */
public class AliOSSUtils {
    private AliOSSproperties aliOSSproperties;
    public void setAliOSSproperties(AliOSSproperties aliOSSproperties) {
        this.aliOSSproperties = aliOSSproperties;
    }
    public AliOSSproperties getAliOSSproperties(){
        return aliOSSproperties;
    }
    //    private String accessKeyId = "xxxxxxxx";
//    private String accessKeySecret = "xxxxxxxx";
    EnvironmentVariableCredentialsProvider credentialsProvider; // 根据阿里提供的SDK, 需要自己在本机电脑中声明key和Secret
    {
        try {
            credentialsProvider = newEnvironmentVariableCredentialsProvider();
        } catch (ClientException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 实现上传图片到OSS
     */
    public String upload(MultipartFile file) throws IOException {
        String endpoint = aliOSSproperties.getEndpoint();
        String bucketName = aliOSSproperties.getBucketName();
        ...
    }
}

com.aliyun.oss/AliyunOSSProperties

package com.aliyun.oss;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSproperties {
    private String endpoint;
    private String bucketName;
    //get/set方法
    ...
}

com.aliyun.oss/AliOSSAutoConfiguration

package com.aliyun.oss;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(AliOSSproperties.class)
public class AliOSSAutoConfiguration {
    @Bean
    public AliOSSUtils aliOSSUtils(AliOSSproperties aliOSSproperties){
        //因为 Utils 里不能通过 Autowired 自动注入 
        AliOSSUtils aliOSSUtils = new AliOSSUtils();
        aliOSSUtils.setAliOSSproperties(aliOSSproperties);
        return aliOSSUtils;
    }
}

创建resources/META-INF/spring这个文件夹 , 然后在里面放一个 file : org.springframework.boot.autoconfigure.AutoConfiguration.imports

里面直接这样写上一行 :

org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.aliyun.oss.AliOSSAutoConfiguration

注意在测试的时候 , 要在测试工程里的[配置文件上配置 AliyunOSS 的配置信息 , 我们想要使用 , 在测试项目的 pom.xml 中引入 starter依赖 , 然后直接使用 @Autowired 注入 AliOSSUtils 就行

上面我们就讲完了进阶内容, 还额外包括如何自定义starter, 这些都是在实际开发中比较重要的操作, 可以加入相关社区多了解一下。

-- 补充: 最后的 kitten-utils 文件笔者这里是换新电脑之后找不到了, 大家可以理解文章内容就行

感谢大家的观看喵, 笔者还是小白一枚(*/ω\*), 哪里写的有问题欢迎联系笔者 qq: 1948677720

\u8bf4\u4e0d\u5b9a\u6709\u5973\u88c5