开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring Boot 无侵入式 实现RESTful API

发表于 2021-07-05

前言

现在我们做项目基本上中大型项目都是选择前后端分离,前后端分离已经成了一个趋势了,所以总这样·我们就要和前端约定统一的api 接口返回json 格式,

这样我们需要封装一个统一通用全局 模版api返回格式,下次再写项目时候直接拿来用就可以了

约定JSON格式

一般我们和前端约定json格式是这样的

1
2
3
4
5
6
7
json复制代码{
"code": 200,
"message": "成功",
"data": {

}
}
  • code: 返回状态码
  • message: 返回信息的描述
  • data: 返回值

封装java bean

定义状态枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码package cn.soboys.core.ret;


import lombok.Data;
import lombok.Getter;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 15:35
* 响应码枚举,对应HTTP状态码
*/
@Getter
public enum ResultCode {

SUCCESS(200, "成功"),//成功
//FAIL(400, "失败"),//失败
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "认证失败"),//未认证
NOT_FOUND(404, "接口不存在"),//接口不存在
INTERNAL_SERVER_ERROR(500, "系统繁忙"),//服务器内部错误
METHOD_NOT_ALLOWED(405,"方法不被允许"),

/*参数错误:1001-1999*/
PARAMS_IS_INVALID(1001, "参数无效"),
PARAMS_IS_BLANK(1002, "参数为空");
/*用户错误2001-2999*/


private Integer code;
private String message;

ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
}

定义返回状态码,和信息一一对应,我们可以约定xxx~xxx 为什么错误码,防止后期错误码重复,使用混乱不清楚,

定义返回体结果体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码package cn.soboys.core.ret;

import lombok.Data;

import java.io.Serializable;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 15:47
* 统一API响应结果格式封装
*/
@Data
public class Result<T> implements Serializable {

private static final long serialVersionUID = 6308315887056661996L;
private Integer code;
private String message;
private T data;


public Result setResult(ResultCode resultCode) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
return this;
}

public Result setResult(ResultCode resultCode,T data) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
this.setData(data);
return this;
}


}

code,和message都从定义的状态枚举中获取

这里有两个需要注意地方我的数据类型T data返回的是泛型类型而不是object类型而且我的结果累实现了Serializable接口

我看到网上有很多返回object,最后返回泛型因为泛型效率要高于object,object需要强制类型转换,还有最后实现了Serializable接口因为通过流bytes传输方式web传输,速率更块

定义返回结果方法

一般业务返回要么是 success成功,要么就是failure失败,所以我们需要单独定义两个返回实体对象方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码package cn.soboys.core.ret;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 16:30
* 响应结果返回封装
*/
public class ResultResponse {
private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";

// 只返回状态
public static Result success() {
return new Result()
.setResult(ResultCode.SUCCESS);
}

// 成功返回数据
public static Result success(Object data) {
return new Result()
.setResult(ResultCode.SUCCESS, data);


}

// 失败
public static Result failure(ResultCode resultCode) {
return new Result()
.setResult(resultCode);
}

// 失败
public static Result failure(ResultCode resultCode, Object data) {
return new Result()
.setResult(resultCode, data);
}



}

注意这里我定义的是静态工具方法,因为使用构造方法进行创建对象调用太麻烦了, 我们使用静态方法来就直接类调用很方便

这样我们就可以在controller中很方便返回统一api格式了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码 package cn.soboys.mallapi.controller;

import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultResponse;
import cn.soboys.mallapi.bean.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author kenx
* @version 1.0
* @date 2021/7/2 20:28
*/
@RestController //默认全部返回json
@RequestMapping("/user")
public class UserController {
@GetMapping("/list")
public Result getUserInfo(){
User u=new User();
u.setUserId("21");
u.setUsername("kenx");
u.setPassword("224r2");
return ResultResponse.success(u);
}
}

返回结果符合我们预期json格式
但是这个代码还可以优化,不够完善,比如,每次controller中所有的方法的返回必须都是要Result类型,我们想返回其他类型格式怎么半,还有就是不够语义化,其他开发人员看你方法根本就不知道具体返回什么信息

如果改成这个样子就完美了如

1
2
3
4
5
6
7
8
java复制代码 @GetMapping("/list")
public User getUserInfo() {
User u = new User();
u.setUserId("21");
u.setUsername("kenx");
u.setPassword("224r2");
return u;
}

其他开发人员一看就知道具体是返回什么数据。但这个格式要怎么去统一出来?

其实我们可以这么去优化,通过SpringBoot提供的ResponseBodyAdvice进行统一响应处理

  1. 自定义注解@ResponseResult来拦截有此controller注解类的代表需要统一返回json格式,没有就安照原来返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package cn.soboys.core.ret;

import java.lang.annotation.*;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 16:43
* 统一包装接口返回的值 Result
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseResult {
}
  1. 定义请求拦截器通过反射获取到有此注解的HandlerMethod设置包装拦截标志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码package cn.soboys.core.ret;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 17:10
* 请求拦截
*/
public class ResponseResultInterceptor implements HandlerInterceptor {

//标记名称
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//请求方法
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
//判断是否在对象上加了注解
if (clazz.isAnnotationPresent(ResponseResult.class)) {
//设置此请求返回体需要包装,往下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
//方法体上是否有注解
} else if (method.isAnnotationPresent(ResponseResult.class)) {
//设置此请求返回体需要包装,往下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
}
}
return true;
}
}
  1. 实现ResponseBodyAdvice<Object> 接口自定义json返回解析器根据包装拦截标志判断是否需要自定义返回类型返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
java复制代码package cn.soboys.core.ret;

import cn.soboys.core.utils.HttpContextUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


import javax.servlet.http.HttpServletRequest;

/**
* @author kenx
* @version 1.0
* @date 2021/6/17 16:47
* 全局统一响应返回体处理
*/
@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

/**
* @param methodParameter
* @param aClass
* @return 此处如果返回false , 则不执行当前Advice的业务
* 是否请求包含了包装注解 标记,没有直接返回不需要重写返回体,
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
HttpServletRequest request = HttpContextUtil.getRequest();
//判断请求是否有包装标志
ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
return responseResultAnn == null ? false : true;
}

/**
* @param body
* @param methodParameter
* @param mediaType
* @param aClass
* @param serverHttpRequest
* @param serverHttpResponse
* @return 处理响应的具体业务方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (body instanceof Result) {
return body;
} else if (body instanceof String) {
return JSON.toJSONString(ResultResponse.success(body));
} else {
return ResultResponse.success(body);
}
}
}

注意这里string类型返回要单独json序列化返回一下,不然会报转换异常

这样我们就可以在controler中返回任意类型,了不用每次都必须返回 Result 如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码package cn.soboys.mallapi.controller;

import cn.soboys.core.ret.ResponseResult;
import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultResponse;
import cn.soboys.mallapi.bean.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author kenx
* @version 1.0
* @date 2021/7/2 20:28
*/
@RestController //默认全部返回json
@RequestMapping("/user")
@ResponseResult
public class UserController {
@GetMapping("/list")
public User getUserInfo() {
User u = new User();
u.setUserId("21");
u.setUsername("kenx");
u.setPassword("224r2");
return u;
}

@GetMapping("/test")
public String test() {
return "ok";
}
@GetMapping("/test2")
public Result test1(){
return ResultResponse.success();
}

}

这里还有一个问题?正常情况返回成功的话是统一json 格式,但是返回失败,或者异常了,怎么统一返回错误json 格式,sprinboot有自己的错误格式?

请参考我上一篇,SpringBoot优雅的全局异常处理

扫码关注公众号猿人生了解更多好文

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《设计模式系列》 - 代理模式

发表于 2021-07-05

有情怀,有干货,微信搜索【三太子敖丙】关注这个有一点点东西的程序员。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

设计模式已经跟大家分享很多了常见的模式了,感兴趣的小伙伴可以再回顾一下,巩固一下理解。

这次要跟大家分享的是设计模式中三大类创建型中的代理模式,代理模式在业务场景上我们可能不会经常用到,但是面试官却会经常问一个问题?

请你跟我讲讲Spring里面AOP的代理模式?jdk的代理模式和cglib的代理模式又啥区别?

清楚和不清楚的同学都可以接着向下看,一定会有收获。

言归正传,接下来开始一步一步分析一下代理模式。

定义以及目的

首先代理模式可以分为多种类型

  • 远程代理:就是将工作委托给远程对象(不同的服务器,或者不同的进程)来完成。常见的是用在web Service中。还有就是我们的RPC调用也可以理解为一种远程代理。
  • 保护代理:该模式主要进行安全/权限检查。(接触很少)
  • 缓存代理:这个很好理解,就是通过存储来加速调用,比如Sping中的@Cacheable方法,缓存特定的参数获取到的结果,当下次相同参数调用该方法,直接从缓存中返回数据。
  • 虚拟代理:这种代理主要是为方法增加功能,比如记录一些性能指标等,或进行延迟初始化

上面只是我们作为了解的概念,接下来再看看代理模式有哪些部分构成。

  • Subject(共同接口):客户端使用的现有接口
  • RealSubject(真实对象):真实对象的类
  • ProxySubject(代理对象):代理类

从图中可以看出其实整个接口还是很简单,就是一个真实对象以及代理对象。

目的:提供一个实际代理对象,以便更好的控制实际对象。 以上定义来自《设计模式之美》

代码举例实现

为了方便理解,还是举一个例子,不知道大家在读初中或者高中是否经历过传小纸条的过程,假如现在同学A 对同学C有一些话想聊(比如放学相约一起打游戏)但是因为现在是上课时间,又不能大声说,同学A和同学C之间坐了一个同学B,所以现在同学A只能是先找到同学B把纸条给它,让他转告同学C,但是去玩还是不是不去玩,那还是只能真正的同学C自己才能决定。

所以代理模式可以理解为 同学B是同学的C的代理,同学A要找同学C,只能找到同学B,通过同学B转达同学C,同时将同学的C的执行结果反馈给同学A。

说完了例子还是具体看看代码的实现吧

1
2
3
4
java复制代码public interface Subject {
// 共同的接口
void doSomething();
}

定义一个共同的接口(即大家要做的事请:放学一起打游戏)

1
2
3
4
5
6
7
java复制代码public class RealSubject implements Subject {
// 真实对象
@Override
public void doSomething() {
System.out.println("放学去打游戏");
}
}

构建一个真实对象,即例子中的同学C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public class ProxySubject implements Subject {

private RealSubject realSubject;

public ProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}

public ProxySubject() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
this.realSubject = (RealSubject) this.getClass().getClassLoader().loadClass("com.ao.bing.demo.proxyPattern.RealSubject").newInstance();
}

@Override
public void doSomething() {
realSubject.doSomething();
}


public static void main(String[] args) {

try {
// 第一种方式
new ProxySubject().doSomething();
// 打印结果: 放学去打游戏
} catch (Exception e) {
// 异常情况,代理失败,
// 传纸条的被老师抓了。或者同学C不在座位上了 等异常情况
}

// 第二种方式
new ProxySubject(new RealSubject()).doSomething();
// 打印结果: 放学去打游戏
}
}

构建代理对象,即同学B,那么可以看到同学A并没有真实接触到同学C,通过同学B对同学C的代理就能知道同学C放学能不能跟他一起去打游戏

在Main方法里面,有两种方式来调用真实对象

  • 第一种:采用类加载器形式,去加载实列对象,这样我们就不同关心到底什么时候需要真实的实列化对象
  • 第二种:通过传值的形式,把实列化对象传过来。(理解为装饰器模式了)

这里大家要区别一下,代理模式是提供完全相同的接口,而装饰器模式是为了增强接口。

静态代理、动态代理和cglib代理分析

静态代理

在上面的举的列子实现其实就是静态代理,大家可以看到整体也比较简单。但是它的缺点也很明显

静态代理需要为每一个对象都创建一个代理类,增加了维护成本以及开发成本,那么为了解决这个问题,动态代理就出来了,不要再固定为每一个需要代理的类而创建一个代理类

动态代理

动态代理合理的避免了静态代理的那种方式,不用事先为要代理的类而构建好代理类。而是在运行时通过反射机制创建。

在写动态代理事需要理解两个东西:Proxy 可以理解为就是调度器,InvocationHandler 增强服务接口可以理解为代理器。 所以我个人理解动态代理其实就是一种行为的监听。

具体的代码实现举一个例子:螳螂捕蝉,通过通过螳螂监听到蝉的动作。方便后面讲到多级代理模式。

1
2
3
4
5
6
7
8
9
10
java复制代码public interface BaseService {
void mainService();
}

public class Cicada implements BaseService {
@Override
public void mainService() {
System.out.println("主要业务,以蝉为例,当蝉出现业务调用时,螳螂监听到");
}
}

创建共同接口,以及真实对象蝉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class PrayingMantis implements InvocationHandler {

private BaseService baseService;

// 这里采用的是构建传参数,可以用反射,举的第一个例子有样式代码
public PrayingMantis(BaseService baseService) {
this.baseService = baseService;
}

// 螳螂主要业务,也就是监听对象
@Override
public Object invoke(Object listener, Method method, Object[] args) throws Throwable {
method.invoke(baseService,args);
secondaryMain();
return null;
}
// 这里理解增强业务,即我们可以在实现InvocationHandler里面添加其他的业务,比如日志等等。
private void secondaryMain(){
System.out.println("螳螂捕蝉 - 次要业务");
}
}

创建螳螂类,监听着蝉的类的动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class BeanFactory {

public static BaseService newInstanc(Class classFile) {
// 1. 创建蝉,真实类对象
BaseService trueCicada = new Cicada();
// 2.创建代理类 螳螂
InvocationHandler prayingMantis = new PrayingMantis(trueCicada);
// 3.向Jvm索要代理对象 其实就是监听的对象,
Class classArray[] = {BaseService.class};
BaseService baseService = (BaseService) Proxy.newProxyInstance(classFile.getClassLoader(), classArray, prayingMantis);
return baseService;
}

// 测试Demo
public static void main(String[] args) {
BaseService baseService = newInstanc(Cicada.class);
baseService.mainService();
// 测试结果 :主要业务
// 螳螂捕蝉 - 次要业务
}
}

通过结果可以看出当蝉主要业务发生调用时,螳螂能监听到蝉的业务并且能处理其他业务逻辑,这也就是Spring里面AOP为什么能处理日志切面等。

代理的本质:

我认为其实就是一种行为的监听,对代理对象($proxy InvocationHandler)的一种监听行为。

代理模式组成:

  • 接口:声明需要被监听行为
  • 代理实现类(InvocationHandler):次要业务,次要业务和主要业务绑定执行
  • 代理对象(监听对象)

Cglib动态代理

cglib动态代理其实和jdk的动态代理是很相似的,都是要去实现代理器接口完成。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public class PrayingMantis implements MethodInterceptor {

private Cicada cicada;// 代理对象

public Cicada getInstance(Cicada cicada) {
this.cicada = cicada;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.cicada.getClass());
enhancer.setCallback(this);
return (Cicada) enhancer.create();
}

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object object = methodProxy.invokeSuper(o, objects);
secondaryMain();
return object;
}

private void secondaryMain() {
System.out.println("螳螂捕蝉 - 次要业务");
}

public static void main(String[] args) {
PrayingMantis prayingMantis = new PrayingMantis();
Cicada instance = prayingMantis.getInstance(new Cicada());
instance.mainService();
// 结果:主要业务
// 螳螂捕蝉 - 次要业务
}

因为蝉类都是一样的所以我就不单独这里再贴出来。

细心的同学已经发现,Cglib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。

Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因为是采用继承方式,所以代理类不能加final修饰,否则会报错。

final类:类不能被继承,内部的方法和变量都变成final类型

JDK和Cglib的区别:

jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理

cglib动态代理是利用ASM开源包,对被代理对象类的class文件加载进来,通过修改其字节码生成子类来处理

ASM: 一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。 – 以上ASM解释来自简书

多级动态代理

看完上面的动态代理,不知道大家有没有想法,实现一个多级动态代理。

还是以螳螂捕蝉为例子,再加上一个黄雀在后,实现多级动态代理模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class Cardinal implements InvocationHandler {
// 监听代理代理对象
private Object proxyOne;

public Cardinal(Object proxyOne) {
this.proxyOne = proxyOne;
}

// 螳螂主要业务,也就是监听对象
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
method.invoke(proxyOne, args);
secondaryMain();
return null;
}
private void secondaryMain() {
System.out.println("黄雀吃螳螂 - 次要业务");
}
}

创建一个黄雀代理对象,那作为他的真实对象就变成螳螂了,当螳螂对象发生调用时,黄雀就能坚挺到,同时作出对应业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public class BeanFactory {

public static BaseService newInstanc(Class classFile) {
// 1. 创建蝉,真实类对象
BaseService trueCicada = new Cicada();

// 2.创建代理类 螳螂
InvocationHandler prayingMantis = new PrayingMantis(trueCicada);

// 3.向Jvm索要代理对象 其实就是坚挺的对象
Class classArray[] = {BaseService.class};
BaseService baseService = (BaseService) Proxy.newProxyInstance(classFile.getClassLoader(), classArray, prayingMantis);

// 4.创建代理实现类 黄雀 二级代理
InvocationHandler cardinal = new Cardinal(baseService);
BaseService secondBaseService = (BaseService) Proxy.newProxyInstance(classFile.getClassLoader(), classArray, cardinal);

// 假设要实现三级,四级代理,则在黄雀类上再加一层代理即可实现。
// 省略其他的更多级代理对象
return secondBaseService;
}

// 测试demo
public static void main(String[] args) {
BaseService baseService = BeanFactory.newInstanc(Cicada.class);
baseService.mainService();
// 结果:主要业务
// 螳螂捕蝉 - 次要业务
// 黄雀吃螳螂 - 次要业务
}
}

根据这个代码基本就实现多级代理过程。螳螂监听着蝉类的动作,黄雀监听着螳螂类的动作。

同样的如果要实现三级代理,四级代理也就不是什么难事了,在每一层的上面再加一个代理对象就可以了。

动态代理本质还是可以理解为将“次要业务”与“主要业务”解耦合,让开发者能更加专注于主要业务,提升开发效率,以及维护成本。

总结

代理模式在业务代码上我个人认为是比较少见的,特别是动态代理基本上是没有见过。但是代理模式也是我们必须要理解的一种模式,因为学习好代理模式有助于我们去读一些源码,排查一些更深层次的问题,或者面对一些业务场景问题,也能有一个很大的提升,设计模式本身也就是为了解决问题而创建出来的。

理解完动态代理现在对我们来说AOP的实现原理也就不言而喻了。

详细的设计模式到这里就结束了,后面针对一些不常见设计模式我还是会给大家做一个总结吧。

我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞、收藏和评论,我们下期见!


文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务:剖析一下源码,Nacos的健康检查竟如此简单

发表于 2021-07-05

前言

前面我们多次提到Nacos的健康检查,比如《微服务之:服务挂的太干脆,Nacos还没反应过来,怎么办?》一文中还对健康检查进行了自定义调优。那么,Nacos的健康检查和心跳机制到底是如何实现的呢?在项目实践中是否又可以参考Nacos的健康检查机制,运用于其他地方呢?

这篇文章,就带大家来揭开Nacos健康检查机制的面纱。

Nacos的健康检查

Nacos中临时实例基于心跳上报方式维持活性,基本的健康检查流程基本如下:Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。

原理很简单,关于代码层的实现,下面来就逐步来进行解析。

客户端的心跳

实例基于心跳上报的形式来维持活性,当然就离不开心跳功能的实现了。这里以客户端心跳实现为基准来进行分析。

Spring Cloud提供了一个标准接口ServiceRegistry,Nacos对应的实现类为NacosServiceRegistry。Spring Cloud项目启动时会实例化NacosServiceRegistry,并调用它的register方法来进行实例的注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码@Override
public void register(Registration registration) {
// ...
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();

Instance instance = getNacosInstanceFromRegistration(registration);

try {
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}catch (Exception e) {
// ...
}
}

在该方法中有两处需要注意,第一处是构建Instance的getNacosInstanceFromRegistration方法,该方法内会设置Instance的元数据(metadata),通过源元数据可以配置服务器端健康检查的参数。比如,在Spring Cloud中配置的如下参数,都可以通过元数据项在服务注册时传递给Nacos的服务端。

1
2
3
4
5
6
7
8
9
10
yaml复制代码spring:
application:
name: user-service-provider
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
heart-beat-interval: 5000
heart-beat-timeout: 15000
ip-delete-timeout: 30000

其中的heart-beat-interval、heart-beat-timeout、ip-delete-timeout这些健康检查的参数,都是基于元数据上报上去的。

register方法的第二处就是调用NamingService#registerInstance来进行实例的注册。NamingService是由Nacos的客户端提供,也就是说Nacos客户端的心跳本身是由Nacos生态提供的。

在registerInstance方法中最终会调用到下面的方法:

1
2
3
4
5
6
7
8
9
10
ini复制代码@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
serverProxy.registerService(groupedServiceName, groupName, instance);
}

其中BeatInfo#addBeatInfo便是进行心跳处理的入口。当然,前提条件是当前的实例需要是临时(瞬时)实例。

对应的方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
//fix #1733
if ((existBeat = dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
dom2Beat.put(key, beatInfo);
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

在倒数第二行可以看到,客户端是通过定时任务来处理心跳的,具体的心跳请求有BeatTask完成。定时任务的执行频次,封装在BeatInfo,回退往上看,会发现BeatInfo的Period来源于Instance#getInstanceHeartBeatInterval()。该方法具体实现如下:

1
2
3
csharp复制代码public long getInstanceHeartBeatInterval() {
return this.getMetaDataByKeyWithDefault("preserved.heart.beat.interval", Constants.DEFAULT_HEART_BEAT_INTERVAL);
}

可以看出定时任务的执行间隔就是配置的metadata中的数据preserved.heart.beat.interval,与上面提到配置heart-beat-interval本质是一回事,默认是5秒。

BeatTask类具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
ini复制代码class BeatTask implements Runnable {

BeatInfo beatInfo;

public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}

@Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.get("clientBeatInterval").asLong();
boolean lightBeatEnabled = false;
if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.has(CommonParams.CODE)) {
code = result.get(CommonParams.CODE).asInt();
}
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
Instance instance = new Instance();
instance.setPort(beatInfo.getPort());
instance.setIp(beatInfo.getIp());
instance.setWeight(beatInfo.getWeight());
instance.setMetadata(beatInfo.getMetadata());
instance.setClusterName(beatInfo.getCluster());
instance.setServiceName(beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ex) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());

}
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}

在run方法中通过NamingProxy#sendBeat完成了心跳请求的发送,而在run方法的最后,再次开启了一个定时任务,这样周期性的进行心跳请求。

NamingProxy#sendBeat方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {

if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
}
Map<String, String> params = new HashMap<String, String>(8);
Map<String, String> bodyMap = new HashMap<String, String>(2);
if (!lightBeatEnabled) {
bodyMap.put("beat", JacksonUtils.toJson(beatInfo));
}
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
params.put("ip", beatInfo.getIp());
params.put("port", String.valueOf(beatInfo.getPort()));
String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);
return JacksonUtils.toObj(result);
}

实际上,就是调用了Nacos服务端提供的”/nacos/v1/ns/instance/beat”服务。

在客户端的常量类Constants中定义了心跳相关的默认参数:

1
2
3
4
5
ini复制代码static {
DEFAULT_HEART_BEAT_TIMEOUT = TimeUnit.SECONDS.toMillis(15L);
DEFAULT_IP_DELETE_TIMEOUT = TimeUnit.SECONDS.toMillis(30L);
DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5L);
}

这样就呼应了最开始说的Nacos健康检查机制的几个时间维度。

服务端接收心跳

分析客户端的过程中已经可以看出请求的是/nacos/v1/ns/instance/beat这个服务。Nacos服务端是在Naming项目中的InstanceController中实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
less复制代码@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {

// ...
Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);

if (instance == null) {
// ...
instance = new Instance();
instance.setPort(clientBeat.getPort());
instance.setIp(clientBeat.getIp());
instance.setWeight(clientBeat.getWeight());
instance.setMetadata(clientBeat.getMetadata());
instance.setClusterName(clusterName);
instance.setServiceName(serviceName);
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(clientBeat.isEphemeral());

serviceManager.registerInstance(namespaceId, serviceName, instance);
}

Service service = serviceManager.getService(namespaceId, serviceName);
// ...
service.processClientBeat(clientBeat);
// ...
return result;
}

服务端在接收到请求时,主要做了两件事:第一,如果发送心跳的实例不存在,则将其进行注册;第二,调用其Service的processClientBeat方法进行心跳处理。

processClientBeat方法实现如下:

1
2
3
4
5
6
java复制代码public void processClientBeat(final RsInfo rsInfo) {
ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();
clientBeatProcessor.setService(this);
clientBeatProcessor.setRsInfo(rsInfo);
HealthCheckReactor.scheduleNow(clientBeatProcessor);
}

ClientBeatProcessor同样是一个实现了Runnable的Task,通过HealthCheckReactor定义的scheduleNow方法进行立即执行。

scheduleNow方法实现:

1
2
3
typescript复制代码public static ScheduledFuture<?> scheduleNow(Runnable task) {
return GlobalExecutor.scheduleNamingHealth(task, 0, TimeUnit.MILLISECONDS);
}

再来看看ClientBeatProcessor中对具体任务的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码@Override
public void run() {
Service service = this.service;
// logging
String ip = rsInfo.getIp();
String clusterName = rsInfo.getCluster();
int port = rsInfo.getPort();
Cluster cluster = service.getClusterMap().get(clusterName);
List<Instance> instances = cluster.allIPs(true);

for (Instance instance : instances) {
if (instance.getIp().equals(ip) && instance.getPort() == port) {
// logging
instance.setLastBeat(System.currentTimeMillis());
if (!instance.isMarked()) {
if (!instance.isHealthy()) {
instance.setHealthy(true);
// logging
getPushService().serviceChanged(service);
}
}
}
}
}

在run方法中先检查了发送心跳的实例和IP是否一致,如果一致则更新最后一次心跳时间。同时,如果该实例之前未被标记且处于不健康状态,则将其改为健康状态,并将变动通过PushService提供事件机制进行发布。事件是由Spring的ApplicationContext进行发布,事件为ServiceChangeEvent。

通过上述心跳操作,Nacos服务端的实例的健康状态和最后心跳时间已经被刷新。那么,如果没有收到心跳时,服务器端又是如何判断呢?

服务端心跳检查

客户端发起心跳,服务器端来检查客户端的心跳是否正常,或者说对应的实例中的心跳更新时间是否正常。

服务器端心跳的触发是在服务实例注册时触发的,同样在InstanceController中,register注册实现如下:

1
2
3
4
5
6
7
8
9
10
less复制代码@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// ...
final Instance instance = parseInstance(request);

serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}

ServiceManager#registerInstance实现代码如下:

1
2
3
4
5
arduino复制代码public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {

createEmptyService(namespaceId, serviceName, instance.isEphemeral());
// ...
}

心跳相关实现在第一次创建空的Service中实现,最终会调到如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
scss复制代码public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
Service service = getService(namespaceId, serviceName);
if (service == null) {

Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();

putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}

在putServiceAndInit方法中对Service进行初始化:

1
2
3
4
5
6
7
8
9
10
scss复制代码private void putServiceAndInit(Service service) throws NacosException {
putService(service);
service = getService(service.getNamespaceId(), service.getName());
service.init();
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}

service.init()方法实现:

1
2
3
4
5
6
7
scss复制代码public void init() {
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
entry.getValue().setService(this);
entry.getValue().init();
}
}

HealthCheckReactor#scheduleCheck方法实现:

1
2
3
4
scss复制代码public static void scheduleCheck(ClientBeatCheckTask task) {
futureMap.computeIfAbsent(task.taskKey(),
k -> GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
}

延迟5秒执行,每5秒检查一次。

在init方法的第一行便可以看到执行健康检查的Task,具体Task是由ClientBeatCheckTask来实现,对应的run方法核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
scss复制代码@Override
public void run() {
// ...
List<Instance> instances = service.allIPs(true);

// first set health status of instances:
for (Instance instance : instances) {
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
if (!instance.isMarked()) {
if (instance.isHealthy()) {
instance.setHealthy(false);
// logging...
getPushService().serviceChanged(service);
ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
}
}
}
}

if (!getGlobalConfig().isExpireInstance()) {
return;
}

// then remove obsolete instances:
for (Instance instance : instances) {

if (instance.isMarked()) {
continue;
}

if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
// delete instance
deleteIp(instance);
}
}
}

在第一个for循环中,先判断当前时间与上次心跳时间的间隔是否大于超时时间。如果实例已经超时,且为被标记,且健康状态为健康,则将健康状态设置为不健康,同时发布状态变化的事件。

在第二个for循环中,如果实例已经被标记则跳出循环。如果未标记,同时当前时间与上次心跳时间的间隔大于删除IP时间,则将对应的实例删除。

小结

通过本文的源码分析,我们从Spring Cloud开始,追踪到Nacos Client中的心跳时间,再追踪到Nacos服务端接收心跳的实现和检查实例是否健康的实现。想必通过整个源码的梳理,你已经对整个Nacos心跳的实现有所了解。关注我,持续更新Nacos的最新干货。

Nacos系列

  • 《Spring Cloud集成Nacos服务发现源码解析?翻了三套源码,保质保鲜!》
  • 《要学习微服务的服务发现?先来了解一些科普知识吧》
  • 《微服务的灵魂摆渡者——Nacos,来一篇原理全攻略》
  • 《你也对阅读源码感兴趣,说说我是如何阅读Nacos源码的》
  • 《学习Nacos?咱先把服务搞起来,实战教程》
  • 《微服务之:服务挂的太干脆,Nacos还没反应过来,怎么办?》
  • 《微服务之吐槽一下Nacos日志的疯狂输出》
  • 《一个实例,轻松演示Spring Cloud集成Nacos实例》

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【redis前传】redis字典快速映射+hash釜底抽薪 

发表于 2021-07-05

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

前言

  • 相信你一定使用过新华字典吧!小时候不会读的字都是通过字典去查找的。在Redis中也存在相同功能叫做字典又称为符号表!是一种保存键值对的抽象数据结构
  • 本篇仍然定位在【redis前传】系列中,因为本篇仍然是在解析redis数据结构!当你尝试去了解redis时才能明白其中原理!才能明白为什么redis被大家吹捧速度快,而不是被告知redis很快!

应用场景

  • 在Redis中有很多场景都是用了字典作为底层数据结构!我们使用最多的应该是redis的库的设置和五种基本数据类型的Hash结构数据!
  • 在上一篇【redis前传】中我们学习了list数据结构。今天我们继续学习主流数据结构Hash。
  • 在redis内部有字典结构、hash结构但是这里的hash和我们平时熟知的redis基础数据的hash并不是一个意思!我们简单的将字典结构、hash结构理解成redis更加底层的一种抽象结构。平时我们使用的hash基础数据结构理解成hash工具

image-20210624161020745

  • 而今天我们的主角就是五种数据结构的Hash分析。他的底层使用了字典这个结构。字典结构内部使用的是底层的hash结构。有点绕!好好理解你行的

哈希表

image-20210624164553947

  • 上面这张图诠释了作为redis底层结构的Hash。在内部redis称之为dictht 。 后面我们为什么和之前的hash结构冲突我们都已类名为准叫做dictht类。
  • 在hictht类中有四个属性分别是table 、 size 、 sizemask 、 used ; 其中table就是一个数组;数组中元素是另外一个类叫做dictEntry类。
  • dictEntry就是真正存储数据的。内部是key、value存储结构。一个简单的哈希表就如图所示。数据最终会存储在table中的dictEntry对象中。
  • 至于为什么sizemask = size -1 ; 这个是为了在计算hash索引时需要用到的。那为什么不直接使用size-1而是通过一个变量来承接呢?这个吧!!!我也不知道。容我去百度百度。

数组节点

  • 上面的哈希表是不是很熟悉,这不和我们Java中的Map数据结构如出一辙吗。可以说是也可以说不是,两者很相似但也有区别的。
  • 在上面中我们提到数据最终是存储在哈希表里table数组里的元素。该元素叫dictEntry 。 下面我们看看dictEntry结构如何吧!

image-20210624165611646

  • 通过左侧对dictEntry的定义我们可以看出。dictEntry存储的值可以是指针、正数、浮点数各种数据类型!类似于Java中的Object属性。 对于上述的key没有啥真意的就是一个键。
  • 既然是数组那么索引就是固定长度的,那么在有限的长度中肯定会出现经典问题就是【hash冲突】。在Java中我们是通过链表和红黑树来解决冲突的问题!在redis中是通过链表解决的。在dictEntry中通过next指针将冲突元素连接。
  • 这里我们就可以和Java中的Map结构进行理解。他们内部很是相似!!!
  • 这里需要注意下在hash冲突时redis的确是通过链表进行存储的,但是由于哈希表(dictht)中没有记录每个索引未中链表的尾部节点只有头结点信息所以。而且我们也知道链表在查询上效率不佳,所以当发生哈希冲突时redis是将新加入的节点加入在链表的头部!

image-20210625113012772

字典

多态字典

  • 字典是本文开头提出的结构!也是redis中大量使用的一种底层数据结构。在redis中名叫做dict类。

image-20210625110556458

  • 通过图示我们明确的看出内部是包含哈希表的。其实从名字上我们也可以看出哈希表为什么叫dictht 。 笔者这里认为是dicthashcodetable 。 意思就是字典表内部的一个hash相关的数组(仅个人理解)
  • 之前也提到过redis内部很多地方会使用到字典!就好比我们上学是用到【新华字典】、【成语词典】、【歇后语词典】等等。虽然名字叫法不一样但是内部结构都是一部字典供我们快速定位。而redis中dict内部就是通过type字段进行区分每个字典的。而privdata是每部字典需要的特定参数。通过type和privdata就可以轻松实现各种功能不同的字典,他有个专有名词叫多态字典

rehash

  • 除了type 、 privdata以外剩下的就是ht 、 rehashidx了。其中ht是一个长度为2的数组。数组里元素就是我们之前提到了哈希表(dictht) 。 ht为什么长度为2 这就需要我们了解下redis的rehash过程了。而rehashidx就是记录rehash的进度!在没有发生rehash的时候rehashidx=-1;
  • 在实际使用过程中在字典中我们所有的数据都会存储在ht[0]对应的哈希表中。ht[1]永远都是一个空数组。这些都是为什么rehash做准备,在正式开始之前我们先来了解下redis为什么需要rehash这个动作
  • 首先我们在哈希表中是一个定长数组发生冲突时内部是通过链表解决的。理论上一个哈希表可以存储足够的数据,这里的足够就是指空间允许的范围有多少存多少。但是我们知道链表的特点就是新增、删除很快但是查询很慢,尤其是当链表很长的时候就会出现查询效率低下的问题!为了避免链表过长redis就会在一定条件下对哈希表中数组长度的扩展从而解决局部链表过长的问题!
  • 每次数组发生长度变化时,那么之前的hash值就需要重新经历一遍hash然后寻址index的过程。这个过程就叫做rehash 。

image-20210625133555602

  • 关于rehash和Java中Map的resize是一样的功能!Java中resize是直接new 出一片内存进行复制的而且他是每次进行2倍扩展。而redis的rehash稍微不同基本上我们也可以理解成2倍扩展!关于两块内存复制有点类似于JVM中垃圾回收有点类似。有时间我们可以一起研究下JVM章节。
  • 那么啥时候需要进行rehash呢?这里和Java的负载因子一样;但是除了负载因子这个空间考核以外redis还考虑一个性能的问题。因为在单线程的前提下我们还要考虑客户端使用的感知性!单线程的意思就是执行命令是顺序执行的。总不能在我们rehash的过程中全部阻塞客户端的使用这对于操作体验上稳定性来说是不友好的。

image-20210625140300363

  • 涉及到上述两个命令的我们称之为后台命令结合负载因子产生如下条件

image-20210625140528097

image-20210625142224557

image-20210625142326375

渐进式rehash

  • 一直强调redis是单线程。那么什么叫单线程模型?就是对于redis服务来说执行命令是线性操作!但是每个客户端的命令是无序的,先到的就先进入队列redis服务从队列一次取出命令进行执行。除了客户端的命令还有一些系统生成的命令比如说我们上面提到的rehash操作!
  • ①、首先为了避免阻塞客户端或者说尽量控制阻塞的时间在客户端感知范围内,redis内部的rehash并不是一次性操作而是一个循序渐进的过程。一次仅复制一部分
  • ②、还记得之前我们提到dict中rehashidx这个属性吗,他是记录rehash的进度。因为哈希表内部是一个数组而rehashidx就是记录这个数组的索引。从而我们也可以知道每次rehash复制的时候是已一个索引完整链表为单元进行复制的。
  • ③、除了新增以外的其他操作都会同时影响到ht[0]、ht[1] 因为在rehash过程中两个数组都是在使用状态的
  • ④、新增值的时候就只需要新增到ht[1]中。因为最终的目的就是将所有值同步到ht[1]中。而ht[0]的值会慢慢的变少;没必要新增到ht[0]
  • ⑤、在rehash过程中查找元素时会查找两个数组中的并集元素。这也就也是了为什么再rehash过程新增元素只需要新增到ht[1]的原因

总结

①、字典表在redis被广泛使用,基于字典表优秀的设计解决redis单线程问题

②、字典里包含哈希表,哈希表内部使用节点负责存储key、value

③、字典type实现多态字典用于多场景!

④、渐进式rehash解决服务卡顿问题

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Join查询联表的数量最大到底不要超过多少,有人说5,有人说

发表于 2021-07-05

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

导读

从今天开始,小k将使用掘金社区作为MySQL案例的应用场景,详细剖析MySQL的实现细节。

相信掘友们都知道掘金社区个人主页上有一个“关注了”和“关注者”的功能,尤其是“关注者”功能,当你看到消息提醒,又有人关注你啦,你可能会点进关注者列表瞅瞅:咦,今天又有哪位大佬关注我啦!有点小激动,哈哈哈!

今天小k就以“关注者”这个功能为例,看看我们是怎么用SQL实现该功能的,语句背后的执行原理又是怎样的?

我们先来看看如何用SQL实现“关注者”功能:

1
sql复制代码SELECT user.user_name, user.avatar, user.position, user.company FROM user LEFT JOIN t_user_relation ON user.user_id = t_user_relation.follow_user_id WHERE t_user_relation.followed_user_id = 10008

上面这条SQL表示查询user_id=10008的关注列表,SELECT字段包含用户名user_name、头像avatar、职位position和公司company,where条件的字段为followed_user_id,即被关注者用户id,从上面的语句看,这是一个left join查询,当然因为“关注者”列表这个功能是一个在线功能,一般我们通过先查t_user_relation表,根据被关注者用户id得到关注者用户id,后拿关注者用户id查user表来完成这个功能。不过,这里,我只是拿这个案例来开启今天联表查询这个主题。

在《Join查询深度优化 - 不为人知的新方法》一文中我讲解了Join查询的几种策略,通过这篇文章,你应该对Join查询的基本过程有了一定的了解,那么,结合上面这个案例,那么,只要让MySQL使用Index Nested-Loop Join策略来执行这条语句,性能上还是可以有所保证的,因为查询能命中索引。

但是,这个案例中的SQL只关联了两张表user和t_user_relation,如果在有些场景下,比如,报表查询,由于跨业务域的原因,这时候可能不得不关联5张、6张、10张甚至更多的表来获取想要的数据,那么,此时的联表查询可能就会变得很慢,那么,我到底在语句中关联几张表才能保证语句执行不至于慢得离谱呢?

在《MySQL为什么选择执行计划A而不选择B(上)》和《MySQL为什么选择执行计划A而不选择B(下)》两篇文章中,我详细讲解了基于MySQL成本模型的优化策略:MySQL会针对多个潜在的QEP(查询执行计划),比较其执行成本,最后选择执行成本最低的来执行语句。

而联表查询,比如上面的案例,既可以用user表来驱动t_user_relation表,也可以反过来,用t_user_relation表来驱动user表,那么,MySQL同样会基于成本模型,比较这两种方案的执行成本,最后,选择成本最低的来执行语句,即选择最低成本的驱动关系。

既然MySQL是通过比较不同的驱动关系的执行成本来选择驱动表的,那么,表关联越多,意味着各种驱动关系组合就越多(最坏的情况有2的n次方-1种组合,n为关联表数量),比较各种组合的执行成本的代价也就越高,进而相应的查询语句执行就会变慢。

既然语句执行慢的原因之一是花在了驱动关系的成本分析上,那么,我们就来看一下这个驱动关系成本分析的过程是什么样的?

在讲解成本分析的过程之前,我们先来看下成本分析相关的几个核心结构,因此,在这里,我以《导读》中的语句为例来讲解这些结构:

image.png

Join

一条语句的结构解析后就会存放在Join这个结构里,包含select列、from表、where、groupby、orderby等信息。

  • best_ref:比较执行计划成本过程中,当前最低成本的执行计划。包含了该计划中每个阶段的执行信息,如上图绿色箭头指向的JOIN_TAB数组就是这个最低成本执行计划中每个阶段的执行情况。假设《导读》中语句当前最低成本的执行计划是user -> t_user_relation,即先查询user表,后查询t_user_relation,我们来看下这个JOIN_TAB:
+ JOIN\_TAB:包含一个阶段执行使用的相关表及该阶段的执行成本情况。主要包含下面几个核心属性:


    - table\_ref:一个阶段使用的表信息。如上图,由于执行计划为`user -> t_user_relation`,因此,第一个JOIN\_TAB中的table\_ref表示查询user表这个阶段,user表的相关信息。第二个JOIN\_TAB中的table\_ref表示查询t\_user\_relation表阶段,t\_user\_relation表的相关信息。
    - dependent:一个阶段使用的表,其依赖的表。比如《导读》中的语句中where条件的字段使用右表t\_user\_relation的字段followed\_user\_id,MySQL可以单独使用该条件查询t\_user\_relation,这时候,user和t\_user\_relation表之间就没有依赖关系。


    但是,如果我把语句改成这样:



    
1
sql复制代码SELECT user.user_name, user.avatar, user.position, user.company FROM user LEFT JOIN t_user_relation ON user.user_id = t_user_relation.follow_user_id WHERE user.user_id = 10008
那么,我们发现where条件的字段使用左表user的字段user\_id,因此,使用该条件查询t\_user\_relation表时依赖user表的user\_id字段,因此,这时候,上图中第二个JOIN\_TAB中的dependent就是user表,表示t\_user\_relation表查询依赖user表中的字段。 - read\_time:一个阶段使用的表读取时间。如上图,由于执行计划为`user -> t_user_relation`,因此,第一个JOIN\_TAB中的read\_time表示查询user表这个阶段,读取user表的时间。第二个JOIN\_TAB中的read\_time表示查询t\_user\_relation表阶段,读取t\_user\_relation表的时间。
  • positions:表示比较执行计划成本过程中,当前在计算成本的执行计划。如上图中的positions为user -> t_user_relation,表示先查询user表,后查询t_user_relation表。其内部由多个POSITION组成一个数组。每个POSITION按执行计划顺序,表示该执行计划中的某一个阶段。
+ POSITION:如上图,第二个POSITION表示查询t\_user\_relation表这个阶段,该阶段用st\_position结构描述。我们来看下这个st\_position结构:
    - prefix\_rowcount:当前阶段之前(包含当前阶段)的总扫描行数。比如,上图第二个POSITION为查询t\_user\_relation表阶段,该阶段之前一共只有查询user表这一个阶段,因此,该POSITION中的prefix\_rowcount为查询t\_user\_relation扫描行数+查询user扫描行数=8。
    - prefix\_cost:当前阶段之前(包含当前阶段)的总执行成本。比如,上图第二个POSITION为查询t\_user\_relation表阶段,该阶段之前一共只有查询user表这一个阶段,因此,该POSITION中的prefix\_cost为查询t\_user\_relation成本+查询user成本=12.2。
    - read\_cost:当前阶段的执行成本。比如,上图第二个POSITION为查询t\_user\_relation表阶段,因此,该POSITION中的read\_cost为9.6,表示查询t\_user\_relation表的成本为9.6。
    - rows\_fetched:当前阶段的扫描行数。比如,上图第二个POSITION为查询t\_user\_relation表阶段,由于条件`t_user_relation.followed_user_id = 10008`查询命中索引,因此,该POSITION中的rows\_fetched为0,表示根据该条件查询t\_user\_relation表扫描了0行。
    - JOIN\_TAB:同上面的JOIN\_TAB结构。如上图,st\_position中的JOIN\_TAB指向JOIN\_TAB数组中的第二个JOIN\_TAB,表示查询t\_user\_relation表阶段执行使用的相关表及该阶段的执行成本情况。
  • best_read:比较执行计划成本过程中,整个语句当前的最低执行成本。如上图,假设《导读》中语句当前的最低执行成本为12.2。

驱动表选择

讲解完核心数据结构之后,你可能有个疑问:既然st_position中的JOIN_TAB和JOIN_TAB数组中的JOIN_TAB的关系是1:1,为什么MySQL不把st_position放到JOIN_TAB数组中的JOIN_TAB中呢?先别急,我们先来看下这张图:

image.png

有没有发现,这不就是一个图的深度遍历嘛!没错!

  • t1 -> t2 -> t4:表示依次查询t1、t2和t4表:
    • t1 -> t2
      • 分析t1,得到t1成本为2.6,将该成本带入t2,即图中第一个红色箭头上的2.6。
      • 分析t2,得到t2成本为9.6,带入成本2.6 + t2成本9.6,得到t1 -> t2总成本12.2。将12.2带入t4。即图中第二个红色箭头上的12.2。
    • t2 -> t4
      • 分析t4,得到t4成本为1.6,带入成本12.2 + t4成本1.6,得到t1 -> t2 -> t4总成本13.8。
  • t1 -> t3 -> t4:表示依次查询t1、t3和t4表,同理,得到t1 -> t3 -> t4总成本12.8。

我们拿这个过程对比上面的Join结构,是不是发现,刚好st_position这个结构可以描述上面这个深度遍历过程中,两个节点的关系,因此,MySQL单独设计了st_position来表示执行计划遍历路径中两张表的成本关系。其中,st_position中的prefix_cost就是当前节点之前(包含当前节点)的总成本,read_cost就是当前节点的查询成本。

在讲完核心结构之后,我们可以看看MySQL是如何比较驱动关系的成本的,上面我提到了深度遍历,聪明的小伙伴已经猜到了,没错!MySQL在比较不同的驱动关系成本的时候,也使用了深度遍历,这种遍历方式在算法上叫做贪婪搜索。因此,《导读》中的语句,其贪婪搜索的过程就变成这样:

image.png

如上图,MySQL分别计算了user表驱动t_user_relation表和t_user_relation表驱动user表的成本:

  1. user -> t_user_relation:user表查询成本为2.6,t_user_relation查询成本为9.6,因此,总成本为2.6 + 9.6 = 12.2。
  2. t_user_relation -> user:t_user_relation表查询成本为2.0127,user表查询成本为6,因此,总成本为2.0127 + 6 = 8.0127。

由于8.0127 < 12.2,因此,MySQL选择t_user_relation表来驱动user表。关于具体的查询成本分析过程,可以阅读这2篇文章《MySQL为什么选择执行计划A而不选择B(上)?》和《MySQL为什么选择执行计划A而不选择B(下)?》。

那么,现在我们了解了MySQL比较驱动关系成本的过程,回到关联表过多,导致查询变慢的问题,我们知道如果关联表很多,那么,MySQL处理查询语句时,不得不做更多的表顺序组合,对各种组合进行贪婪搜索,来比较它们的成本。这对MySQL而言,势必影响查询的性能。

剪枝过程

因此,MySQL引入了prune_level这个变量,来减少搜索遍历的次数。我们来看下这个减少的过程,假设一条语句关联的表有4张:t1、t2、t3、t4:

image.png

在此,假设MySQL先选择t1表作为驱动表来计算查询成本,那么,这个过程如上图:

  1. t1 - > t2,即t1表先驱动t2表,得到t1的查询成本为2.6,t2的查询成本为1.6,两者总和4.2。
  2. t1 -> t3,即t1表驱动t3表,得到t1的查询成本为2.6,t3的查询成本为8.6,两者总和11.2,由于prune_level变量设置为1,表示对路径剪枝,因此,MySQL发现t1 -> t3成本11.2大于t1 -> t2成本4.2,t1 -> t3遍历分支结束,从t1开始不再遍历其他表,只关注t1 -> t2的遍历分支。如上图,step1中的t3打了叉,表示t1 -> t3遍历分支结束。将4.2带入t3和t4。即图中第二个红色箭头上的4.2和图中第二个绿色箭头上的4.2。
  3. t2 -> t3,即开始从t2驱动t3,得到t3的查询成本为5.6,加上带入成本4.2,4.2 + 5.6 = 9.8。
  4. t2 -> t4,即开始从t2驱动t4,得到t4的查询成本为1.6,加上带入成本4.2,4.2 + 1.6 = 5.8。由于prune_level变量设置为1,表示对路径剪枝,因此,MySQL发现t1 -> t2 -> t3成本9.8大于t1 -> t2 -> t4成本5.8,t1 -> t2 -> t3遍历分支结束,从t2开始不再遍历其他表,只关注t1 -> t2 -> t4的遍历分支。如上图,step2中的t3打了叉,表示t1 -> t2 -> t3遍历分支结束。将5.8带入t3。即图中第三个绿色箭头上的5.8。
  5. t4 -> t3,即开始从t4驱动t3,得到t3的查询成本为5.6,加上带入成本5.8,5.8 + 5.6 = 11.4。
  6. 因此,得到驱动关系:t1 -> t2 -> t4 -> t3,总成本11.4。

上面的过程演示了prune_level变量减少贪婪搜索遍历次数的过程,我们发现通过该变量剪枝遍历分支,可能会造成有些分支成本很低,但是,没有再去遍历的情况,比如,下面这张图:

image.png

图中,t1 -> t2 -> t3 -> t4这条遍历分支,其成本总和为10.4,明显比t1 -> t2 -> t4 -> t3的成本总和11.4要小,因此,prune_level变量为1,开启剪枝功能,可能会忽略成本更低的分支,因此,在联表查询时,如果表的数量很少,prune_level变量为1就不那么合适了。

剪枝调优

好在MySQL给我提供了参数,可以调整这个变量的值。prune_level变量默认为1,我们只需执行下面的命令,就可以将prune_level变量置成0,表示关闭剪枝功能。

1
sql复制代码set optimizer_prune_level = 0;

遍历深度调优

MySQL在实现贪婪搜索时,使用递归的方法做深度遍历,那么,如果联表的数量非常大,递归调用产生的临时内存空间就会非常大,对于内存敏感的MySQL而言,是不太能接受的,因此,MySQL对遍历的最大深度做了限制,默认为62。

随之而来的问题出现了,如果连接的表数为100,那么,以一张表开始深度遍历其他表,就会触达MySQL的最大深度限制,我们可以想象,这个O(62)的空间复杂度对查询性能的影响是巨大的,因此,我们肯定希望可以调小这个遍历最大深度的阈值。

那么,我们将这个阈值调整到多少合适呢?MySQL很聪明,它给我们提供了一个阈值0,这表示什么含义呢?0表示MySQL自身给我们动态计算了最大遍历深度,计算规则如下:

  1. 如果表数量小于等于7,那么,最大遍历深度为表数量+1
  2. 如果表数量大于7,那么,最大遍历深度为7

发现没,MySQL将7作为动态计算最大遍历深度的阈值,你可以想想为什么用7?但是,从这个动态计算得到的阈值7来看,MySQL可能还是建议我们联表遍历的深度不要超过7。PS:当然最合理的方式是动态统计遍历深度性能,然后,根据统计结果确定最大遍历深度,MySQL源码里做了这样的TODO,这是给我发挥的机会吗,哈哈哈!开玩笑的。

因此,这就回答了我文章标题的问题:Join查询联表的数量最大不要超过多少?答案是7。因为如果我们联表的数量小于等于7,那么,势必从一张表开始遍历其他表的深度不会超过7。

既然可以将最大遍历深度的阈值调整为0,那么,我们该如何调整呢?具体调整方法如下:

1
sql复制代码set optimizer_search_depth = 0;

总结

最后,我们来总结一下今天的内容:联表成本分析的核心数据结构Join以及驱动表选择的过程。

同时,我还提供了2个参数调优:

场景 参数调优
如果表连接数不多 建议set optimizer_prune_level = 0; 关闭剪枝功能
如果表连接数小于等于7 建议set optimizer_search_depth = 0; 让MySQL自身动态计算最大遍历深度

还回答了标题的问题:Join查询联表的数量最大到底不要超过多少?

MySQL给到的建议是:Join查询联表的数量最大不要超过7。

我是小k,如果你觉得这篇文章不错,记得点赞 + 关注哦!

当然,如果你对本文的内容还有疑惑,也欢迎在评论区提问,知无不言,言无不尽!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

10jedis连接redis

发表于 2021-07-04

环境准备

jdk + 启动redis服务 + idea(或eclipse)+ jedis所需jar包

jedis所需jar包:jedis-3.3.0.jar + commons-pool2-2.6.2.jar

或maven依赖:

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>

启动redis服务时先修改redis配置文件和linux防火墙,否则将导致连接redis失败。

  • 注释bind的绑定ip。
1
2
yaml复制代码#指定redis只接收来自于该IP地址的请求,如果不进行设置,那么将处理所有请求,在生产环境中最好设置该项
# bind 127.0.0.1
  • 关闭保护模式(或设置密码)
1
2
yaml复制代码#是否启动redis保护模式,默认开启,启动之后远程服务需要密码才能连接,如果没有设置密码又需要远程连接,则需要把保护模式关闭
protected-mode no

或者不关闭保护模式,则需要设置密码,jedis需要带密码连接redis。

设置密码:config set requirepass 123456

清空密码(需要重启):config set requirepass ''

获取密码:config get requirepass

  • 关闭防火墙(或开放redis服务端口)

关闭防火墙:systemctl stop firewalld.service

开启防火墙:systemctl start firewalld.service

不推荐关闭防火墙,则需要开放redis服务端口(需重启防火墙)。

开放端口:firewall-cmd --add-port=6379/tcp

关闭端口:firewall-cmd --remove-port=6379/tcp

重启防火墙:systemctl restart firewalld.service 或 firewall-cmd --reload

测试连接

  • ifconfig查看linux的ip地址,比如我的是:192.168.64.129,我的redis服务启动端口是:6379

在这里插入图片描述

  • 无密码(关闭保护模式)
1
2
3
4
5
6
7
8
java复制代码public class PingTest {
public static void main(String[] args) {
// 参数:redis服务所在机器的ip地址 + redis启动端口
Jedis jedis = new Jedis("192.168.64.129", 6379);
// 结果:PONG
System.out.println(jedis.ping());
}
}
  • 有密码
1
2
3
4
5
6
7
8
9
10
11
java复制代码public class PingTest {
public static void main(String[] args) {
// 参数:redis服务所在机器的ip地址 + redis启动端口
JedisShardInfo jedisShardInfo = new JedisShardInfo("192.168.64.129", 6379);
// 设置redis密码
jedisShardInfo.setPassword("123456");
Jedis jedis = jedisShardInfo.createResource();
// 结果:PONG
System.out.println(jedis.ping());
}
}

当调用ping()方法返回结果为PONG则表示连接成功。

jedis常用api

只列举部分,剩下的同学们自己尝试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码public class Test01 {
public static void main(String[] args) {
// 获取jedis连接对象
JedisShardInfo jedisShardInfo = new JedisShardInfo("192.168.64.129", 6379);
jedisShardInfo.setPassword("123456");
Jedis jedis = jedisShardInfo.createResource();

// keys *
Set<String> keys = jedis.keys("*");
// 遍历并输出说有key
for (Iterator iterator = keys.iterator(); iterator.hasNext(); ) {
String key = (String) iterator.next();
System.out.println(key);
}
System.out.println("************************************************");
// exists k2
System.out.println("jedis.exists====>" + jedis.exists("k2"));
System.out.println(jedis.ttl("k1"));
// get k1
System.out.println(jedis.get("k1"));
// set k4 k4_redis
jedis.set("k4", "k4_redis");
// mset str1 v1 str2 v2 str3 v3
jedis.mset("str1", "v1", "str2", "v2", "str3", "v3");
System.out.println("************************************************");
// lpush mylist v1 v2 v3 v4 v5
jedis.lpush("mylist","v1","v2","v3","v4","v5");
// lrange mylist 0 -1
List<String> list = jedis.lrange("mylist", 0, -1);
// 遍历输出mylist列表
for (String element : list) {
System.out.println(element);
}
System.out.println("************************************************");
// sadd orders jd001
jedis.sadd("orders", "jd001");
// smembers orders
Set<String> set1 = jedis.smembers("orders");
// 遍历输出set无序集合
for (Iterator iterator = set1.iterator(); iterator.hasNext(); ) {
String string = (String) iterator.next();
System.out.println(string);
}
// srem orders jd002
jedis.srem("orders", "jd002");
System.out.println("************************************************");
// hset hash1 username lisi
jedis.hset("hash1", "userName", "lisi");
// hget hash1 username
String hget = jedis.hget("hash1", "userName");
Map<String, String> map = new HashMap<String, String>();
map.put("telphone", "11012011933");
map.put("address", "China");
map.put("email", "abc@163.com");
// hmset hash2 telphone 11012011933 address China email 163
jedis.hmset("hash2", map);
// hmget hash2 telphone email
List<String> result = jedis.hmget("hash2", "telphone", "email");
System.out.println("************************************************");
// zadd zset01 60 v1
jedis.zadd("zset01", 60d, "v1");
// zrange zset01 0 -1
Set<String> s1 = jedis.zrange("zset01", 0, -1);
// 遍历输出zset有序集合
for (Iterator iterator = s1.iterator(); iterator.hasNext(); ) {
String string = (String) iterator.next();
System.out.println(string);
}
}
}

jedis事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public class TXTest01 {
public static void main(String[] args) {
JedisShardInfo jedisShardInfo = new JedisShardInfo("192.168.64.129", 6379);
jedisShardInfo.setPassword("123456");
Jedis jedis = jedisShardInfo.createResource();

//监控key,如果该动了事务就被放弃
// watch serialNum
jedis.watch("serialNum");
// set serialNum 1000
jedis.set("serialNum", "1000");
// unwatch
// jedis.unwatch();

// multi
Transaction transaction = jedis.multi();//被当作一个命令进行执行
// set serialNum 1001
transaction.set("serialNum", "1001");
Response<String> response = transaction.get("serialNum");
// lpush list01 a
transaction.lpush("list01", "a");

// exec
transaction.exec();
// discard
//2 transaction.discard();

// serialNum:1000(被回滚了)
System.out.println("serialNum:" + jedis.get("serialNum"));
}
}

jedis主从复制

首先开启6379和6380端口的redis服务。

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class MSTest {
public static void main(String[] args) {
// 连接6379和6380,此处使用关闭保护模式方式
Jedis jedisM6379 = new Jedis("192.168.64.129",6379);
Jedis jedisS6380 = new Jedis("192.168.64.129",6380);

// slaveof 192.168.64.129 6379
jedisS6380.slaveof("192.168.64.129",6379);
jedisM6379.set("k1","v1");
String result = jedisS6380.get("k1");
// v1
System.out.println(result);
}
}

jedis连接池

类似数据库连接池对象,可以减少频繁的创建或销毁数据库连接对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
java复制代码public class JedisPoolUtil {
/**
* 数据库连接对象
*/
private static volatile JedisPool jedisPool = null;

private JedisPoolUtil() {
}

/**
* 双重锁检查单例模式
*
* @return
*/
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 控制一个pool最多有多少个状态为idle(空闲)的jedis实例
poolConfig.setMaxIdle(32);
// 表示当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛JedisConnectionException
poolConfig.setMaxWaitMillis(1000);
// 获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "192.168.64.129", 6379, 2000, "123456");
}
}
}
return jedisPool;
}

/**
* 关闭连接池
*
* @param jedisPool
*/
public static void close(JedisPool jedisPool) {
if (!jedisPool.isClosed()) {
jedisPool.destroy();
}
}

public static void main(String[] args) {
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
JedisPool jedisPool2 = JedisPoolUtil.getJedisPoolInstance();

// 测试是否为单例模式:true
System.out.println(jedisPool == jedisPool2);

Jedis jedis = null;
try {
jedis = jedisPool.getResource();
// PONG
System.out.println(jedis.ping());
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭连接池对象
JedisPoolUtil.close(jedisPool);
}
}
}

连接池配置信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
markup复制代码JedisPool的配置参数大部分是由JedisPoolConfig的对应项来赋值的。

maxActive:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted。

maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;

whenExhaustedAction:表示当pool中的jedis实例都被allocated完时,pool要采取的操作;默认有三种。

WHEN_EXHAUSTED_FAIL --> 表示无jedis实例时,直接抛出NoSuchElementException;

WHEN_EXHAUSTED_BLOCK --> 则表示阻塞住,或者达到maxWait时抛出JedisConnectionException;

WHEN_EXHAUSTED_GROW --> 则表示新建一个jedis实例,也就说设置的maxActive无用;

maxWait:表示当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛JedisConnectionException;

testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;

testOnReturn:return 一个jedis实例给pool时,是否检查连接可用性(ping());

testWhileIdle:如果为true,表示有一个idle object evitor线程对idle object进行扫描,如果validate失败,此object会被从pool中drop掉;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;

timeBetweenEvictionRunsMillis:表示idle object evitor两次扫描之间要sleep的毫秒数;

numTestsPerEvictionRun:表示idle object evitor每次扫描的最多的对象数;

minEvictableIdleTimeMillis:表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;

softMinEvictableIdleTimeMillis:在minEvictableIdleTimeMillis基础上,加入了至少minIdle个对象已经在pool里面了。如果为-1,evicted不会根据idle time驱逐任何对象。如果minEvictableIdleTimeMillis>0,则此项设置无意义,且只有在timeBetweenEvictionRunsMillis大于0时才有意义;

lifo:borrowObject返回对象时,是采用DEFAULT_LIFO(last in first out,即类似cache的最频繁使用队列),如果为False,则表示FIFO队列;

其中JedisPoolConfig对一些参数的默认设置如下:
testWhileIdle=true
minEvictableIdleTimeMills=60000
timeBetweenEvictionRunsMillis=30000
numTestsPerEvictionRun=-1

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

nginx配置静态资源

发表于 2021-07-04

业务场景:

app端的同事想要隐私协议和用户协议的h5静态资源,以方便用户点进去查看。

解决办法:

当时我有两个想法,一个是用web服务器配置静态资源,另外一个用nginx配置静态资源。

评估了一下,因当前项目用的技术架构前后端分离,用nginx做的反向代理和webpack打包的前端静态页面,并没有用到web服务器。所以最后选择了用nginx。

具体的操作:

进入nginx软件目录下,找到nginx.conf 文件,

vi命令打开此文件。

添加如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码server {
listen 8091;
server_name localhost;

location /private {
alias html;
index index.html index.htm;
}

location /user {
alias test;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

}

这里解释一下,上面配置的含义:

listen 代表监听的端口

server_name 代表监听的域名,没有的话一般默认localhost

location /private 代表拦截url为http://ip:8091/private/的请求

alias 为别名 也可以设置为root其区别是root相当于default

alias 后面的html代表与nginx.conf 同一级别的静态资源目录

index 代表html文件的index.html作为首页。

location /use同理

重启nginx的坑:

部署时,先停止nginx进程再进行启动nginx。若直接执行./sbin/nginx -s reload 进行重启,亲测一般不会有效果

先执行停止nginx进程,再进行启动:

ps -ef|grep nginx 查询nginx的进程号,一般有两条,选以maste开头的那条。

kill -TERM xxx 杀死进程xxx,比暴力的命令“kill -9”稍微好些。

./sbin/nginx -c /usr/local/nginx/nginx.conf 启动nginx的命令。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 AOP AOP 的拦截与方法调用

发表于 2021-07-04

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

一 . 前言

之前说了 AOP初始化 和 AOP 代理类的创建 , 这一篇来看一下 AOP 对请求的拦截

AOP 拦截的起点是 DynamicAdvisedInterceptor , 该对象在 CglibAopProxy -> getProxy -> getCallbacks

二 . 拦截的发起

2.1 CglibAopProxy 的拦截开始

1
2
3
4
5
6
7
8
9
JAVA复制代码C- DynamicAdvisedInterceptor # intercept
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

// 仅保留核心方法 , 可以看到这里构建了 CglibMethodInvocation用于调用
Object retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
retVal = processReturnType(proxy, target, method, retVal);
return retVal;

}

2.2 Interceptor 执行 Proceed

上一节创建后 CglibMethodInvocation 后 , 会执行 proceed , 以此调用切面类 , 此处先以 Around 为例 , 后面再看一下其他的几种调用

Step 1 : Proceed 调用逻辑 (ReflectiveMethodInvocation)

主要是调用父类的逻辑 CglibMethodInvocation extends ReflectiveMethodInvocation 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}

// 2.2.1 获取具体的 Advice , 此处通过自增完成拦截链的切换
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// 动态方法匹配器-> Pro22101
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
} else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}else {
// 2.2.2 调用 Advice 的 invoke 方法
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

// Pro22101 : InterceptorAndDynamicMethodMatcher 的作用
class InterceptorAndDynamicMethodMatcher {
// 方法拦截器
final MethodInterceptor interceptor;
// 方法匹配器
final MethodMatcher methodMatcher;
}

MethodMatcher 对象体系 :

System-MethodMatcher.png

Step 2 : ExposeInvocationInterceptor

这里是通过 ExposeInvocationInterceptor(CglibMethodInvocation) 调用 , 该拦截器的作用为 将当前MethodInvocation公开为本地线程对象的拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private static final ThreadLocal<MethodInvocation> invocation =new NamedThreadLocal<>("Current AOP method invocation");

public Object invoke(MethodInvocation mi) throws Throwable {
MethodInvocation oldInvocation = invocation.get();
// 线程设置
invocation.set(mi);
try {
return mi.proceed();
}
finally {
invocation.set(oldInvocation);
}
}

Step 3 : 调用不同的拦截链

从 2.2 步骤中 , 会通过自增的方式迭代拦截器 :
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

image.png

PS : 通常链式结构是递归执行的 , 通常最先执行的在列表最后

1
java复制代码// 调用对应的 xxxAdviceInterceptor -> [Pro25001]

[Pro25001] : Advice 拦截链的调用

1
2
3
4
java复制代码// 此处会对不同的 Advice 进行处理 , 核心带入如下 : 
Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

从 interceptorsAndDynamicMethodMatchers 可以看到 , 如果存在 Advice 就会存在相关对象

Step 4 : 反射到 Advice Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// 调用实际业务方法
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
} catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}



// 拦截适配器用于把通知转换为拦截器

// 主要的适配器 :
C- MethodBeforeAdviceAdapter


// 补充 invokeAdviceMethodWithGivenArgs 带参数访问
protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// TODO AopUtils.invokeJoinpointUsingReflection
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
}


PS: 至此就完成了 Advice 方法的调用

2.3 补充其他通知的调用方式

AspectJAfterAdvice 的调用

1
2
3
4
5
6
7
8
9
10
java复制代码
public Object invoke(MethodInvocation mi) throws Throwable {
try {
// Step 1 : 链式调用走流程
return mi.proceed();
} finally {
// Step 2 : finally 完成最终调用
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}

AfterReturningAdviceInterceptor 的调用

1
2
3
4
5
6
7
java复制代码public Object invoke(MethodInvocation mi) throws Throwable {
// Step 1 : 链式调用走流程
Object retVal = mi.proceed();
// Step 2 : 调用 afterReturning
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}

三 . 方法的调用

以 Around 为例 , 当执行了 ProceedingJoinPoint.proceed() 方法后 , 即开始了实际方法的调用

  • Step 1 : 切面调用 pj.proceed()
  • Step 2 : MethodInvocationProceedingJoinPoint 发起 proceed 操作
  • Step 3 : 循环完成 , 发起方法调用
  • Step 4 : 实际方法调用

3.1 MethodInvocationProceedingJoinPoint 发起 proceed 操作

PS : 此处还没有完成 , 这是构建了一个新的对象 , 从上一个链表图中可以看到 , 还要执行后续的 Before Advice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
JAVA复制代码public Object proceed() throws Throwable {
return this.methodInvocation.invocableClone().proceed();
}

// PS : invocableClone 的作用
// 解答 : 此处创建了一个浅克隆 , 用于构建一个独立的拦截器 , 并且用于后续索引 , 但是其中的引用被保持
public MethodInvocation invocableClone() {
Object[] cloneArguments = this.arguments;
if (this.arguments.length > 0) {
// 虽然没有传入参数, 但是实际上构建函数中已经传入了
cloneArguments = this.arguments.clone();
}
return invocableClone(cloneArguments);
}

// C- MethodInvocationProceedingJoinPoint
public MethodInvocation invocableClone(Object... arguments) {
// 强制初始化用户属性Map,以便在克隆中有一个共享的Map引用
if (this.userAttributes == null) {
this.userAttributes = new HashMap<>();
}

// 创建MethodInvocation克隆对象
try {
ReflectiveMethodInvocation clone = (ReflectiveMethodInvocation) clone();
clone.arguments = arguments;
return clone;
} catch (CloneNotSupportedException ex) {
throw new IllegalStateException(...);
}
}

3.2 Interceptor 循环完成 , 发起方法调用

补充 ReflectiveMethodInvocation 对象结构 >>

1
2
3
4
5
java复制代码- ReflectiveMethodInvocation : 基于反射的方式,代理方法调用实现类。
I- ProxyMethodInvocation : 代理方法调用接口
M- #proceed() : 执行方法。基于递归的方式,调用每个拦截器链中的拦截器,最后调用真正的方法。
M- #invokeJoinpoint() : 执行真正的方法,即切点的方法。
M- CglibMethodInvocation : 基于CGLIB 的方式,进一步优化调用的实现类。

当循环迭代完成后(currentInterceptorIndex匹配完成) :

1
2
3
4
5
6
7
java复制代码public Object proceed() throws Throwable {
// 我们从一个索引-1开始,并提前递增 , 当全部递增完成后 , 意味着切面全部完成
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
// ..........
}

3.3 发起实际方法调用

1
2
3
4
5
6
7
8
9
10
11
java复制代码// Step 3 : 实际方法调用 (2.1 中初始化了该属性)
private final MethodProxy methodProxy;

// 代理方法
protected Object invokeJoinpoint() throws Throwable {
if (this.methodProxy != null) {
return this.methodProxy.invoke(this.target, this.arguments);
} else {
return super.invokeJoinpoint();
}
}

3.4 补充 : 带参数的请求

在 MethodInvocationProceedingJoinPoint 中 ,存在一个带参数的 proceed 方法 , 用于构建带参数的 clone 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 除了无参的请求 , 实际上还有个带参的请求 , 他们的请求方式是不一样的
Object proceed(Object[] args)

public Object proceed(Object[] arguments) throws Throwable {

// 首先会校验参数是否存在和长度是否一致 , 这里省略
this.methodInvocation.setArguments(arguments);
return this.methodInvocation.invocableClone(arguments).proceed();
}

// 回顾clone 方法 , 有个变长参数
public MethodInvocation invocableClone(Object... arguments) {
//.............
}

// 这里和上面一样最终是 clone 了一个 ReflectiveMethodInvocation 出来 , 但是为其设置了独立的参数

直到这里 , AOP 的方法调用就完全完成了 >>

四 . 补充 AOP 的拦截链构建

这里补充看一下 AOP 链的调用逻辑

C- AdvisorChainFactory : 通知链工厂

C- DefaultAdvisorChainFactory : 实现类

4.1 DynamicAdvisedInterceptor # intercept 发起拦截链构建

1
2
3
4
java复制代码public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//... 省略其他的逻辑
List<?> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
}

4.2 从缓存中获取拦截器

这里会优先从缓存中获取 , 缓存没有会先创建 ,再放入缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 缓存集合
private transient Map<MethodCacheKey, List<Object>> methodCache;

C- AdvisedSupport # getInterceptorsAndDynamicInterceptionAdvice
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
MethodCacheKey cacheKey = new MethodCacheKey(method);
List<Object> cached = this.methodCache.get(cacheKey);
// 缓存没有则直接创建 , 并且放入缓存
if (cached == null) {
cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this, method, targetClass);
this.methodCache.put(cacheKey, cached);
}
return cached;
}

4.3 构建通知链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
java复制代码C- DefaultAdvisorChainFactory
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {

// 注册Advisor适配器的接口 , 该接口是一个 SPI 接口
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
// 从 Advised 中获取通知者
Advisor[] advisors = config.getAdvisors();
List<Object> interceptorList = new ArrayList<>(advisors.length);
Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
Boolean hasIntroductions = null;

for (Advisor advisor : advisors) {
// 如果为切点则执行
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
// 校验是否匹配目标类
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
boolean match;
if (mm instanceof IntroductionAwareMethodMatcher) {
if (hasIntroductions == null) {
hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
}
match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
}
else {
match = mm.matches(method, actualClass);
}
if (match) {
// 通过切点获取拦截器
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
if (mm.isRuntime()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
}
else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
// IntroductionAdvisor 通过AOP通知实现额外的接口
else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
// org.springframework.aop.interceptor.ExposeInvocationInterceptor
return interceptorList;
}

Advisor 体系结构 :
Advisor-system.png

总结

到了这一篇 AOP 的主要逻辑就全部完成了 , 后续准备说说AOP 的性能分析以及补充知识点 , 等全部完成后 , 再对 AOP 逻辑进行一遍打磨

现阶段程度只是读懂了代码 , 只能看懂为什么这么用 . 等打磨的时候 ,期望能从中学到一些代码的设计精髓 , 以及写一套出来

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式之责任链模式

发表于 2021-07-04

话不多说,开始今天的文章

责任链模式的定义:

责任链是一种行为性的设计模式
通俗点讲:责任链其实就是将连续做处理的单元串成一条链,从链头一直到链尾执行下去或者中途不符合条件跳出

以下将通过登陆需求引出责任链模式,但并不是说这种情况下责任链模式会更加好

用户登陆时的需求:

1.判断用户是否存在

2.判断用户状态是否正常

3.判断密码是否正确

解决方案:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码MemberDO memberDO = memberMapper.selectByEmail(email);
//1.判断用户是否存在
ObjectUtil.isNullToMessage(memberDO, ResultEnum.USER_NO_EXIT);
//2.判断用户状态是否正常
if (!Objects.equals(UserConstant.USER_STATUS_NORMAL,memberDO.getStatus())){
throw new ResultException(ResultEnum.USER_DISABLE);
}
//3.判断密码是否正确
if (!Objects.equals(memberDO.getPassword(),EncryptionUtil.EncoderPwdByMd5(loginDTO.getPassword(),null))){
throw new ResultException(ResultEnum.USER_PASSWORD_ERROR);
}

责任链模式解决方案

1
2
3
4
5
6
7
8
9
10
11
scss复制代码MemberDO memberDO = memberMapper.selectByEmail(email);
loginDTO.setMemberDO(memberDO);
AbstractHandler.Builder builder = new AbstractHandler.Builder();
//1.判断用户是否存在
builder.addHandler(new UserHandler())
//2.判断用户状态是否正常
.addHandler(new VerifyPermissionHandler())
//3.判断密码是否正确
.addHandler(new AuthHandler())
//添加入参
.build().doHandler(loginDTO);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
kotlin复制代码public abstract class AbstractHandler<T> {

protected AbstractHandler<T> next = null;

/**
* 责任链入参
* @param loginDTO 登陆实体类
*/
public abstract void doHandler(LoginDTO loginDTO);

public void next(AbstractHandler handler) {
this.next = handler;
}

public static class Builder<T> {
private AbstractHandler<T> head;
private AbstractHandler<T> tail;

public Builder<T> addHandler(AbstractHandler handler) {
if (this.head == null) {
this.head = handler;
this.tail = handler;
} else {
this.tail.next(handler);
this.tail = handler;
}
return this;
}

public AbstractHandler build() {
return this.head;
}
}
}

具体处理者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
scala复制代码public class UserHandler extends AbstractHandler {

@Override
public void doHandler(LoginDTO loginDTO) {
MemberDO memberDO = loginDTO.getMemberDO();
//判断用户是否存在
ObjectUtil.isNullToMessage(memberDO, ResultEnum.USER_NO_EXIT);
if (next != null){
next.doHandler(loginDTO);
}
}
}

public class VerifyPermissionHandler extends AbstractHandler {

@Override
public void doHandler(LoginDTO loginDTO) {
MemberDO memberDO = loginDTO.getMemberDO();
//判断用户状态
if (Objects.equals(UserConstant.USER_STATUS_NORMAL,memberDO.getStatus())){
throw new ResultException(ResultEnum.USER_DISABLE);
}
if (next != null){
next.doHandler(loginDTO);
}
}
}


public class AuthHandler extends AbstractHandler {

@Override
public void doHandler(LoginDTO loginDTO) {
MemberDO memberDO = loginDTO.getMemberDO();
//查看密码是否正确
if (!Objects.equals(memberDO.getPassword(),EncryptionUtil.EncoderPwdByMd5(loginDTO.getPassword(),null))){
throw new ResultException(ResultEnum.USER_PASSWORD_ERROR);
}
return;
}
}

优点:

  1. 降低耦合度
  2. 增加新的处理类方便
  3. 允许动态的修改处理类顺序以及个数

缺点:

  1. 链路太长性能受到影响
  2. 容易造成循环调用

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 AOP AOP 的拦截对象的创建

发表于 2021-07-04

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

一 . 前言

之前说了 AOP初始化 和 AOP 代理类的创建 , 这一篇来看一下 AOP 拦截对象的创建

二 . CglibAopProxy 代理模块

AOP 的主要代理类还是 CglibAopProxy , 所以整体流程还是以该对象为例 :

2.1 拦截对象创建的入口

在 CglibAopProxy # getProxy 主流程中 , 有几个比较重要的逻辑 , 把这几个流程整合后看一下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码// 代码做过一定程度的省略和魔改
public Object getProxy(@Nullable ClassLoader classLoader) {

//... 省略主要逻辑
Enhancer enhancer = createEnhancer();

// rootClass => com.gang.aop.demo.service.StartService
// 获取所有的回调对象 -> PIC21001 : Callback[] 系列参数
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}

enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap,
this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);

// Generate the proxy class and create a proxy instance.
// return createProxyClassAndInstance(enhancer, callbacks);
enhancer.setInterceptDuringConstruction(false);
enhancer.setCallbacks(callbacks);
return (this.constructorArgs != null && this.constructorArgTypes != null ?
enhancer.create(this.constructorArgTypes, this.constructorArgs) :
enhancer.create());
}

PIC21001 : Callback[] 系列参数

image.png

2.1.1 创建 Callback 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
// 是否为每次调用公开AOP代理
boolean exposeProxy = this.advised.isExposeProxy();
// 配置是否被冻结,并且不能进行通知更改
boolean isFrozen = this.advised.isFrozen();
// 是否所有对#getTarget()的调用都返回同一个对象
boolean isStatic = this.advised.getTargetSource().isStatic();

// 选择一个 AOP 拦截器 -> 此处创建的是 DynamicAdvisedInterceptor , 这是一个重要的类 -> 2.1.2
// advised -> PIC21101
Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

// 选择性创建 StaticUnadvisedInterceptor / DynamicUnadvisedInterceptor
Callback targetInterceptor;
if (exposeProxy) {
targetInterceptor = (isStatic ?
new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) :
new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()));
}
else {
targetInterceptor = (isStatic ?
new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
new DynamicUnadvisedInterceptor(this.advised.getTargetSource()));
}

// 选择一个目标分派器
// StaticDispatcher : 静态目标的调度器 , Dispatcher比Interceptor快得多
Callback targetDispatcher = (isStatic ?
new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp());

// PICPIC21102
Callback[] mainCallbacks = new Callback[] {
aopInterceptor, // for normal advice
targetInterceptor, // invoke target without considering advice, if optimized
new SerializableNoOp(), // no override for methods mapped to this
targetDispatcher, this.advisedDispatcher,
new EqualsInterceptor(this.advised),
new HashCodeInterceptor(this.advised)
};

Callback[] callbacks;

// 如果目标是静态的,并且通知链是冻结的,那么我们可以通过使用该方法的固定链将AOP调用直接发送到目标
if (isStatic && isFrozen) {
Method[] methods = rootClass.getMethods();
Callback[] fixedCallbacks = new Callback[methods.length];
this.fixedInterceptorMap = new HashMap<>(methods.length);

for (int x = 0; x < methods.length; x++) {
Method method = methods[x];
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, rootClass);
fixedCallbacks[x] = new FixedChainStaticTargetInterceptor(
chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass());
this.fixedInterceptorMap.put(method, x);
}

// 从mainCallbacks和fixedCallbacks复制回调函数到callbacks数组中
callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length];
System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length);
System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length);
this.fixedInterceptorOffset = mainCallbacks.length;
}
else {
callbacks = mainCallbacks;
}
// 返回所有的回调对象
return callbacks;
}

PIC21101 advised 中已经包含目标对象和通知面

image.png

PIC21102 : mainCallbacks 结构

image.png

扩展 : ProxyCallbackFilter 的作用

作用: CallbackFilter为方法分配回调

介绍: 该类实现了一个 Accept 方法 , 用于返回回调函数的索引

1
2
3
4
5
6
7
8
9
java复制代码private static class ProxyCallbackFilter implements CallbackFilter {

private final AdvisedSupport advised;

private final Map<Method, Integer> fixedInterceptorMap;

private final int fixedInterceptorOffset;

}

2.1.2 DynamicAdvisedInterceptor

作用 : 当目标是动态的或代理未被冻结时使用 , 会使用该通用的AOP回调

发起 : 从 2.1.1 中 getCallbacks 中可以看到创建流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
java复制代码// 那么来看一下这个对象是如何实现方法的拦截的
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

// PRO212001 : AdvisedSupport 的作用
private final AdvisedSupport advised;

public DynamicAdvisedInterceptor(AdvisedSupport advised) {
this.advised = advised;
}

@Override
@Nullable
// proxy -> EnhancerBySpringCGLIB 代理
// method -> 实际业务方法
// args : 方法参数
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
// 当前调用的目标 ,
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
// 根据此配置确定给定方法的MethodInterceptor对象列表
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// 检查我们是否只有一个InvokerInterceptor (没有通知 , 只有反射调用)
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// 方法代理直接调用
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// 核心逻辑 , 此处创建了一个方法调用
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
// 处理返回类型
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// 恢复旧的代理
AopContext.setCurrentProxy(oldProxy);
}
}
}
}



// SingletonTargetSource 对象结构
public class SingletonTargetSource implements TargetSource, Serializable {

// 使用反射缓存和调用的目标
private final Object target;
}

// CglibMethodInvocation 结构
private static class CglibMethodInvocation extends ReflectiveMethodInvocation {

@Nullable
private final MethodProxy methodProxy;

public CglibMethodInvocation(Object proxy, @Nullable Object target, Method method,
Object[] arguments, @Nullable Class<?> targetClass,
List<Object> interceptorsAndDynamicMethodMatchers, MethodProxy methodProxy) {

super(proxy, target, method, arguments, targetClass, interceptorsAndDynamicMethodMatchers);

// 仅对非java.lang.Object派生的公共方法使用方法代理
this.methodProxy = (Modifier.isPublic(method.getModifiers()) &&
method.getDeclaringClass() != Object.class && !AopUtils.isEqualsMethod(method) &&
!AopUtils.isHashCodeMethod(method) && !AopUtils.isToStringMethod(method) ?
methodProxy : null);
}

可以看到 , DynamicAdvisedInterceptor 是 MethodInterceptor 的实现类 , 该接口对方法进行拦截

MethodInterceptor 的继承体系 :
Systerm-DynamicAdvisedInterceptor.png

PRO212001 : AdvisedSupport 的作用

作用 : AOP代理配置管理器的基类。其本身不是AOP代理,但是该类的子类通常是工厂,从这些工厂中直接获得AOP代理实例

特点 : 这个类是可序列化的 , 用于保存代理的快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码
public class AdvisedSupport extends ProxyConfig implements Advised {

// 通知链工厂 -> Pro
AdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();

// key 为方法缓存 , value 为 advisor 链
private transient Map<MethodCacheKey, List<Object>> methodCache;

// 代理要实现的接口
private List<Class<?>> interfaces = new ArrayList<>();

// Advisor 对象集合 , 每一个通知会包装为通知者放在该集合中
private List<Advisor> advisors = new ArrayList<>();

// 用于内部操作 , advisor 改变后会刷新该对象
private Advisor[] advisorArray = new Advisor[0];

}

// AdvisorChainFactory 的作用

System-AdvisedSupport.png

代理的核心逻辑 :

对应的方法已经在前面通过 Enhancer 进行 CGLIB 代理了 , 调用时实际调用的是 DynamicAdvisedInterceptor , 再由 DynamicAdvisedInterceptor 完成 AOP 类的调用

PS : 这里涉及到动态代理的相关概念 ,会生成 xxx$$EnhancerBySpringCGLIB$$… 的实际类 , 进行 Class 层面的调用 , 这些后面单独说说

总结

请求方DynamicAdvisedInterceptorCglibMethodInvocation切面请求方调用 Interceptor 对象调用 CglibMethodInvocation 发起切面的调用实际调用到切面对象 , 执行对应的通知方法请求方DynamicAdvisedInterceptorCglibMethodInvocation切面
后续就是 ReflectiveMethodInvocation 调用具体的方法 ,这里先不说

还是感觉没说清楚 , 整个条理不是很清楚 , 考虑整个系列完成后进行一次深入打磨 , 欢迎关注 👉

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…621622623…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%