Filter 过滤器
对所有请求都可以过滤。
实现Filter接口,重写几个方法,加上@WebFilter注解,表示拦截哪些路由,如上是所有请求都会拦截。
然后还需要在入口处加上@SvlterComponentScan注解,因为Filter是javaweb三大组件之一,并不是springboot的内容。
chain.doFilter是放行该请求的意思,如果没有将会卡在当前过滤器上。
过滤器链
一个服务可以配置多个过滤器,多个过滤器形成过滤器链。有点像koa的洋葱模型。
过滤器1执行 -> 过滤器2执行 -> 执行主要逻辑 -> 过滤器2放行后的逻辑执行 -> 过滤器1放行后的逻辑执行
那么怎么区分那个过滤器先执行呢?通过过滤器首字母排序来决定。
Interceptor拦截器
拦截器是Spring框架提供的,跟filter不一样。
使用:
实现HandlerInterceptor
接口,重写方法,其中preHanldel是在controller执行前执行,返回值作为放行的条件。
postHandle是在controller执行后执行。
实现拦截器之后,还需要配置才能生效。
实现WebMvcConfigurer接口,然后使用@Configuration,这样springboot启动的时候会自动扫描该注解,生效该配置。
如上就是将拦截器注册,并指定其拦截的接口。
拦截器-拦截路径
可以通过addPathPatterns指定哪些路径需要拦截,通过excludePathPatterns指定哪些路径不需要拦截。
执行时机
上面说过,拦截器是spring框架提供的,而过滤器是tomcat框架提供的,如图。
如果都存在的话,会先执行过滤器,再执行拦截器的逻辑。
过滤器会拦截所有的请求资源,而拦截器只会拦截Spring环境中的资源。
案例 实现jwt登陆验证
安装对应依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
实现jwtUtils类
package com.example.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String singKey = "test1234"; // 密钥
private static long expire = 432000L; //过期时间
public static String generatorToken(Map<String, Object> data) {
String jwt = Jwts.builder()
.addClaims(data)
.signWith(SignatureAlgorithm.HS256, JwtUtils.singKey)
.setExpiration(new Date(System.currentTimeMillis() + JwtUtils.expire))
.compact();
return jwt;
}
public static Claims parseToken(String token) {
Claims calims = Jwts.parser()
.setSigningKey(singKey)
.parseClaimsJws(token)
.getBody();
return calims;
}
}
使用jwt生成token
@Slf4j
@RestController
public class LoginController {
@PostMapping("/login")
public Result login(@RequestBody LoginUser body){
Map<String, Object> data = new HashMap<>();
log.info("{},{}", body.getName(),body.getPassword());
data.put("name", body.getName());
data.put("password", body.getPassword());
String token = JwtUtils.generatorToken(data);
return Result.success(token);
};
}
使用对应的拦截器进行校验
@Component("loginInterceptor")
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
System.out.println("preHandle....." + req.getRequestURL());
String url = req.getRequestURL().toString();
if(url.contains("login")){
// 登陆接口不需要校验
return true;
}
String token = req.getHeader("token");
try {
if (token != null) {
Claims data = JwtUtils.parseToken(token);
req.setAttribute("user", data);
return true;
} else {
throw new Exception("token不存在或者过期");
}
} catch (Exception e) {
Result error = Result.error("token is not exists or expire");
res.addHeader("Content-Type", "application/json");
res.getWriter().write(JSONObject.toJSONString(error));
return false;
}
}
}
@Configuration
public class SpringMvcConfigure implements WebMvcConfigurer {
@Resource(name="loginInterceptor")
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); //该拦截器拦截所有请求
}
}
登陆接口不需要验证,让该拦截器拦截对应请求,拿到token,进行解析,解析到用户数据就塞入req中,后续controller就可以拿到该用户信息。
异常处理
- 程序开发过程中不可避免会碰到异常,有时候返回的信息并不是后端统一的信息。
像nest可以配置全局异常过滤器,会俘获所有的异常然后统一信息返回。
方案
java也可以定义全局异常处理器。
通过注解@RestControllerAdvice指定这是一个controller异常处理器,@RestControllerAdvicd中也包含@ResponseBody,表示所有的方法返回的值会被转为json传给前端(实际上所有的请求传输都是字符串,只不过设置了contentType为json,浏览器会自动识别contentType处理。)。通过@ExceptionHandler(Exception.class)指定拦截什么类型的异常,Exception.class就是指拦截所有的异常。
import com.example.demo.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("服务器报错: " + ex.getMessage());
}
}
事务管理
springboot提供了@Transactional注解来开启事务。
回滚异常
@Transactional注解,可以传入值,可以控制出现什么异常的情况下,回滚事务。默认只有运行时异常才会处理。
事务传播行为
事务a方法调用事务b方法的时候
比如在a中调用b的方法,当a失败后,事务回滚,会导致b方法执行的逻辑也会回滚,事务传播行为默认是有则加入,也就是b方法会加入当前a方法的事务中。
将其改为REQUIRES_NEW,在a中调用b的时候,会先挂起a的事务,然后起一个b的事务,当a失败后,a执行的逻辑会回滚,但是b方法执行的逻辑如果没报错,会保留。
案例 删除一个部门,并将该部门下所有员工删除,不管删除失败成功都需要记录日志
如下,启用Propagation.REQUIRES_NEW,在调用创建日志方法的时候并不会受原本事务的影响
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public Result deleteDepts(Integer id) {
try {
this.dept.deleteDept(id);
this.emp.deleteEmpByJobId(id); //删除员工表下该部门的员工
return Result.success(0);
} catch (Exception e) {
return Result.error(e.getMessage());
} finally {
// 记录日志 也是一个事务方法,用REQUIRES_NEW则不会被上述逻辑影响,会正常记录日志
logger.create(new Date(), "删除")
}
}
AOP
实现
编写AOP类,交给IOC容器管理。
通过joinPoint可以拿到原始方法。
AOP核心概念
- 连接点,joinPoint,可以被AOP控制的方法,比如上图的list,delete,save等方法都属于连接点。
- 通知,Advice,重复逻辑,比如上图的recordTime方法,共性逻辑(所有连接点都会执行)
- 切入点:pointCut,顾名思义就是在哪里切入这个类,匹配连接点的条件。
- 切面:描述通知与切入点的对应关系,如上图的@Around(切入点表达式)+recordTIme(通知)方法就是切面。
- 目标对象:Target,AO类所应用的对象。
AOP的执行流程
AOP类是如何作用于目标对象的,
上述说过,AOP是通过动态代理实现的,如上,DeptServiceImpl类,是AOP的目标对象,他会根据通知
,生成一个代理类,DeptServiceProxy,然后重新list方法,执行AOP类的逻辑,最后如上,通过@Autowired注入的deptService就不是DeptServiceImpl,而是DeptSerivceProxy
这个代理对象,所以执行list方法的时候,就会执行代理对象list,从而执行通知
的逻辑
通俗的说,AOP类会在不影响目标对象代码的基础上,基于目标对象,新增一些其他的逻辑,通过代理的形式生成一个新的类,交给IOC容器.
案例 记录每个controller的耗费时间
可以用拦截器做到,也可以用过滤器做到,也可以用AOP做到。
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable{
long begin = System.currentTimeMillis();
Object object = joinPoint.proceed(); //调用原方法
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "执行耗时: {}ms", end- begin);
return object; //返回
}
}
AOP高阶
通知类型
上述我们使用的@Around就是通知类型。他的功能最强大,可以编写目标方法执行前的逻辑,也可以编写目标方法执行后的逻辑(可以拿到目标方法)
@Pointcut
注解可以抽离公共的切入点表达式,服用切入点表达式
通知的执行顺序
当有多个通知
都匹配到同一个切入点时,目标方法执行,多个通知方法都会执行。
跟过滤器一样,其实是根据AOP的类名字母排序有关,且@before和@after的执行顺序就跟洋葱模型一样,也是234 -> 432这样执行。
除此之外,还可以通过@Order注解来标记执行顺序。
切入点表达式
通配符号 * 和 …
如
@Around("execution(* com.*.demo.controller.*.update*(*))")
* com表示 匹配任何返回值
com.*.demo表示二级包是任意的
controller.*表示controller下的类或者接口是任意的
update*表示以update开头的类或者接口
(*)表示匹配一个参数
上述表示 任何的返回值,com下任意的二级包里含有的demo三级包下的controller目录下的任意的以类或者接口下以update开头,且只能有一个参数的方法
如果换成…
即
@Around("execution(* com..controller.*.update*(..))")
com…可以匹配任意层级的包
update*(…)表示匹配update开头的方法,且可以有任意参数。
上述表示 任何的返回值,com下任意的二级包里含有controller的目录,下的任意的以类或者接口下以update开头,且只能有一个参数的方法
多个切入点表达式可以用 || & 等组合。
@annotation
匹配标识有特定注解的方法
定义一个注解用来标识
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 Mylog {
}
将execution切换成@annotation
public class TimeAspect {
//@Around("execution(* com.example.demo.controller.*.*(..))")
@Around("@annotation(com.example.demo.aop.Mylog)")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable{
long begin = System.currentTimeMillis();
Object object = joinPoint.proceed(); //调用原方法
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "执行耗时: {}ms", end- begin);
return object; //返回
}
}
连接点
对于@Around,只能通过ProceddingJoinPoint获取连接点信息,如下
对于其他四种通知
只能通过JoinPoint获取连接点信息。
@Aspect
@Component
@Slf4j
public class TimeAspect {
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable{
long begin = System.currentTimeMillis();
String className = joinPoint.getTarget().getClass().getName();
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
log.info("打印结果==={},{},{},{}", className, signature, methodName, Arrays.toString(args));
Object object = joinPoint.proceed(); //调用原方法
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "执行耗时: {}ms", end- begin);
return object; //返回
}
}
案例 对增删改操作进行日志写入操作
使用AOP完成该功能。
定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperatorLog {
}
在增删改方法上加上该注解
编写AOP代码
@Component
@Aspect
@Slf4j
public class OperatorLog {
// 通过注解获取request对象
@Autowired
private HttpServletRequest request;
@Around("@annotation(com.example.demo.aop.OperatorLog)")
public Object createLog(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Claims user = (Claims) request.getAttribute("user");
String className = joinPoint.getTarget().getClass().getName();
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
Object object = joinPoint.proceed(); //调用原方法
long time = begin - System.currentTimeMillis();
Log newLog = new Log((String) user.get("name"),time, methodName, Arrays.toString(args),className);
log.info("插入一条日志: {}",newLog);
Log.insertLogs(newLog);
return object; //返回
}
}
通过注解获取request对象,在拦截器的时候将user注入到req里面去,所以这里可以获取得到。然后插入一条日志。