本篇文章主要对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
Cookie
优点 : HTTP协议中支持的技术
缺点 : 移动端APP无法使用Cookie , 不安全,用户可以自己禁用 , Cookie不能跨域
Session
优点 : 存储在服务端, 安全
缺点 : 服务器集群环境下无法直接使用Session , 而且还有Cookie的缺点
Token令牌技术
优点 : 支持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框架中提供的,用来动态拦截控制器当中方法的执行
作用 : 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
使用 :
- 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法
- 注册拦截器 , 配置拦截器
定义拦截器 , 新建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 :
- /* ,拦截一级路径,不能拦截 /emps/1 这种
- /** ,match any path
- /depts/* ,能匹配/depts/1 , 不能匹配/depts/1/2
- 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"
}
那么我们该如何进行处理呢? 有下面几种方案 :
- 在Controller的方法中进行 try…catch处理 (代码臃肿 , 不推荐)
- 全局异常处理器 (简单 , 推荐) 即:面向切面编程 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 时 , 我们就了解过事务 , 事务 是一组操作的集合 , 这些操作 要么同时成功 , 要么同时失败
操作 :
- 开启事务 , start transaction / begin
- 提交事务 , commit
- 回滚事务 , 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.通知类型 :
- @Around : 环绕通知 , 此注解标注的通知方法在目标前 , 后都被执行环绕通知需要自己调用 ProceedingJoinPoint.proceed()方法
- @Before : 前置通知 , 此注解标注的通知方法在目标方法前被执行
- @After: 后置(最终)通知 , 此注解标注的通知方法在目标方法后被执行(不论是否有异常)
- @AfterReturning: 返回后通知 , 此注解标注的通知方法在目标方法后被执行 , 有异常不会执行
- @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 根据注解匹配
其中 ? 代表可省略的部分, 写几个例子 :execution(访问修饰符? 返回值 包名.类名.方法名(参数) throws 异常?)execution(* com.kitten.service.*Service.delete*(*)) //service包下所有以 Service结尾的类中 所有以delete开头,返回值任意,必须有一个形参的方法 //测试 : 写一个切入点表达式用于匹配 Listlist() 和 void delete(Integer id) execution(* com.kitten.service.DeptService.list()) || execution(* com.kitten.service.DeptService.delete(Integer)) @annotation
我们在aop下定义一个 MyLog接口 :package com.kitten.aop; import ... @Retention(RetentionPolicy.RUNTIME) //表示注解在 运行时生效 的时间 @Target(ElementType.METHOD) //表示注解作用在方法上 public @interface MyLog {}aop/myAspect
在使用 @annotation注解修饰的方法上加上 annotation指定的接口即可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..."); } }com.kitten.service.impl.DeptService.DeptServiceImplpackage com.kitten.service.impl; import ... @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; private DeptLogServiceImpl deptLogService; /** * 获取部门信息 */ @MyLog public ListqueryDept(){ 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案例
记录操作日志 : 将案例中增,删,改相关接口的操作日志记录在数据库表中
日志信息有 : 操作人,操作时间,执行方法的类名,方法名,方法参数,返回值,执行时长
- 准备 : 引入 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; //操作耗时 } - 编码 : 自定义注解@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/OperateLogMapperpackage 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 注解分为三个
- @SpringBootConfiguration这个注解包含@Configuration , 表明我们springboot项目的入口类也是一个配置类 , 我们可以在里面声明 @Bean
- @ComponentScan(…)进行组件扫描 , 默认扫描的是当前包及其子包。(扫描我们定义的@Component ,就是 bean对象)
- @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 容器管理
- 创建aliyun-oss-spring-boot-starter 模块
- 创建aliyun-oss-spring-boot-autoconfigure 模块 , 在 starter中引入该模块
- 在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依赖
然后在模块中建立两个类 : AliOSSUtils 和 AliOSSProperties
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

Comments NOTHING