1. AOP
1.1 介绍
- Spring AOP => 底层是基于动态代理的
- AOP不是Spring独有的,AOP是Spring的核心之一
- 功能 : 在不修改原有类的基础上去对目标方法进行增强
- AOP实现必须要有接口,动态代理就是为了对功能进行扩展
1.2 AOP核心关键词
原始对象 target 要被增强的方法的对象
代理对象 proxy 方法被增强后的对象
连接点 joinpoint 可能被增强的方法
切点 pointcut 一定被增强的方法
增强/通知 advice 具体增强的逻辑
切面 aspect 切点+通知
1.3 入门
// 1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
// 2.
// 接口
public interface Person {
void eat(int a);
void shit();
}
//切面类
@Aspect
@Component
public class Advice {
//切点 => 指向的 Person的eat方法
@Pointcut("execution(* com.itheima.bootaop.aop.Person.shit(..))")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object result = null;
try {
//前置通知
System.out.println("环绕通知===前置通知");
result = pjp.proceed(); // 对标的是jdk动态代理的 method.invoke(对象,args) 对象.方法()
//后置通知
System.out.println("环绕通知===后置通知");
}catch (Throwable e) {
System.out.println("环绕通知===异常通知");
//异常通知的逻辑
throw new RuntimeException(e);
} finally {
System.out.println("环绕通知===最终通知");
//最终通知的逻辑
}
return result;
}
// 前置通知
@Before("pt()")
public void before(){
System.out.println("吃饭前洗手");
}
//后置通知
// @AfterReturning("pt()")
// public void afterReturning(){
// System.out.println("拉完屎忘带纸了");
// }
//异常通知
// 出了异常才会执行,异常和后置是互斥的!!!
// @AfterThrowing("pt()")
// public void afterThrowing(){
// System.out.println("看看有水么");
// }
//最终通知
// @After("pt()")
// public void after(){
// System.out.println("看看周围有人么?直接跑");
// }
}
// target实现类
@Component
public class ZhangSan implements Person {
@Override
public void eat(int a) {
System.out.println("张三在吃饭");
}
@Override
public void shit() {
System.out.println("张三拉屎");
//int i = 1 / 0;
}
}
注意
jdk的动态代理和cglib的区别
- jdk => target和proxy是兄弟关系,jdk是通过接口来创建的代理对象
- cglib => target 和 proxy是父子关系,这种情况下我们是不需要去使用接口来接收的
1.4 通知类型
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :最终通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行,与异常通知互斥
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
1.5 切入点表达式
形式 : “execution(* 切点方法名全路径(…))”
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.delete(..))")
//delete方法中参数变成..代表任意参数
1.6 切入点表达式的语法规则:
-
- 方法的访问修饰符可以省略
-
- 返回值可以使用*号代替(任意返回值类型)
-
- 包名可以使用号代替,代表任意包(一层包使用一个)
-
- 使用…配置包名,标识此包以及此包下的所有子包
-
- 类名可以使用*号代替,标识任意类
-
- 方法名可以使用*号代替,表示任意方法
-
- 可以使用 * 配置参数,一个任意类型的参数
-
- 可以使用… 配置参数,任意个任意类型的参数
2. JWT
JWT全称:JSON Web Token )
- 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。
简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。
JWT是如何将原始的JSON格式数据,转变为字符串的呢?
其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号
需要注意的是Base64是编码方式,而不是加密方式。
2.1 生成和校验
1.引入依赖
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. 生成
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","Tom");
String jwt = Jwts.builder()
.setClaims(claims) //自定义内容(载荷)
.signWith(SignatureAlgorithm.HS256, "itheima") //签名算法
.setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期
.compact();
System.out.println(jwt);
}
3. 解析
public void parseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("itheima")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
.getBody();
System.out.println(claims);
}
3. 全局异常处理
3.1 引入
三层架构中出现异常该如何处理?
全局异常处理器
3.2 定义
我们该怎么样定义全局异常处理器?
- 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
- 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result handleException(Exception ex){
return Result.ERR(ex.getMesssage());
}
}
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
3.3 自定义异常
@RestControllerAdvice
public class GlobalExceptionProcessor {
//===================下面2个异常输入自定义异常===================
//业务异常 => 程序不是真的报错,我们是用过手动控制的方式来控制程序的走向和返回结果
//这里的 @ExceptionHandler 注解需要指定要处理的是什么异常
@ExceptionHandler(UserNotExistsException.class)
public Result handleException(UserNotExistsException ex){
return Result.ERR(ex.getMessage());
}
@ExceptionHandler(PasswordErrException.class)
public Result handleException(PasswordErrException ex){
return Result.ERR(ex.getMessage());
}
//===================下面这个异常是系统异常===================
// 数据出现了重复,在sql直接报错,我们也是需要把结果处理成对用户比较友好的格式才能返回
//系统异常处理起来一定要慎重!!!
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result handleException(SQLIntegrityConstraintViolationException ex){
String message = ex.getMessage();
//Duplicate entry 'tom' for key 'username'
String errMsg = message.split(" ")[2] + "已存在";
return Result.ERR(errMsg);
}
}