Ajax请求合并

线上项目 首页加载时 请求了9个接口 刷新首页 请求数刷刷的往上涨

没几下就被我的后台限流策略屏蔽了 为了限流能正常工作 便需要把首页的请求进行合并

首先考虑到要不影响线上功能并且改动较小

对原接口不做任何改动

思路

前端拦截所有Ajax请求 合并统一发送 存储各自的 promise 后端统一返回后再 各自回调

后端提供一个通用接口 /index/wilful 返回多个返回值

请求参数为: 路径和原来的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
"path": "/mp/content/detail", //原请求路径
"param": { //原请求参数
"mallId": 48,
"sectionKey": "home_banner"
}
},
{ //多个同理
"path": "/game/turntable/details",
"param": {
"mallId": "55",
"itemId": "111",
"activityId": "222"
}
}
]

返回值:

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
{
"msg": "success",
"ver": "1.1.1",
"code": 0,
"t": 1618451056360,
"data": {
"/game/turntable/details": { //对应多个请求的原始url 内部为原始请求的返回值
"msg": "success", //只代表此接口的成功失败
"code": 0,
"turntable": {
"id": 3,
...
},
"prizes": [
{
"id": 12,
"turntableId": 3,
"type": "0",
...
},
...
]
},
"/mp/content/detail": {
"msg": "success",
"total": 3,
"code": 0,
"rows": [
{
...
},
...
],
"pageNum": 1
}
},
"url": "/index/wilful"
}

实现

前端

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
//把全局 request 全部转发给
export function request(options) {
//可以判断一下 只有需要整合的接口才进 group
if (options.url != '/index/wilful') {
return uni.$httpGroup.request({
path: options.url,
param: {
...options.data
}
})
}
//原有逻辑
...
}


//合并Ajax请求
const $httpGroup = {
paramList: [],
resolveList: [],
fireRequest (data) { //真正的请求
return request({
url: '/index/wilful',
header: {
'Content-Type': 'application/json'
},
method: 'POST',
data
})
},
request (data) {
return new Promise((resolve, reject) => {
//获得到第一个请求之后进行定时
if (this.paramList.length < 1) {
//收集范围 下面有说明
Promise.resolve().then(()=> {
let copyParamList = [...this.paramList];
let copyResolveList = [...this.resolveList];
//清空待请求
this.paramList = [];
this.resolveList = [];
//请求合并接口
this.fireRequest([
...copyParamList
]).then(res => {
if (res && res.data) {
//成功回调所有的接口
copyParamList.forEach((item, index) => {
copyResolveList[index].resolve(res.data[item.path]);
})
} else {
//异常回调
copyParamList.forEach((item, index) => {
copyResolveList[index].resolve({
code: res.code || 500,
data: undefined,
msg: res.msg || ''
});
})
}
})
})
}
this.resolveList.push({
resolve,
});
this.paramList.push({
...data
});
})
}
}

收集范围:

1
2
3
4
5
6
7
8
//在本轮“事件循环”结束时执行 	JS stack级别
Promise.resolve()

//在有实现Microtasks的浏览器是Microtasks级别 没有就是 Task级别
Vue.prototype.$nextTick(()=>{})

//在下一轮“事件循环”开始时执行 Tasks级别
setTimeout(fn, 0)

更详细可以看 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

小结:

大致上就是 把Ajax异步请求收集成一个数组 把整个数组全部提交到后端接口 再全部回调

后端

后端用 SpringMVC

先写一个工具类 把项目中所有的 Controller 和 Controller 中的 @RequestMapping 整理出来 (不管是 @GetMapping 还是 @GetMapping 都是实现了 @RequestMapping )

再用反射注入请求参数 并执行 获得返回结果 合并接口后返回

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.context.annotation.Lazy;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.lqs1848.common.exception.CustomException;
import com.lqs1848.common.utils.AopTargetUtils;
import com.lqs1848.common.utils.ServletUtils;
import com.lqs1848.common.utils.bean.BeanUtils;
import com.lqs1848.common.utils.bean.StringToClass;
import com.lqs1848.common.utils.spring.SpringContextHolder;
import com.lqs1848.common.web.domain.R;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
* 1 不考虑 同路径有 get又有post 2 先不考虑路径 {xxx} p 3 只装配基础数据
*/
@Lazy(true)
@Component
public class WebWilfulUtils {

ParameterNameDiscoverer parameterNameDiscoverer;
Map<String, Box> map = null;

@PostConstruct
public void init() throws Exception {
parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
map = new HashMap<>();

Map<String, Object> dealers = SpringContextHolder.getApplicationContext()
.getBeansWithAnnotation(Controller.class);
for (Object controller : dealers.values()) {
List<String> basePaths = new ArrayList<>();
Object original = AopTargetUtils.getTarget(controller);
Class<?> cla = original.getClass();
RequestMapping anno = AnnotatedElementUtils.getMergedAnnotation(cla, RequestMapping.class);
if (anno != null) {
for (String path : anno.value()) {
basePaths.add(getPath(path));
//System.out.println("controller path:" + path);
}
}

for (Method m : cla.getMethods()) {

if (m.getReturnType() == null || !m.getReturnType().getName().equals(R.class.getName()))
continue;

// 待添加
// ++++权限校验 feign 专用的方法不能被调用
List<StringBuffer> msbs = new ArrayList<>();
RequestMapping manno = (RequestMapping) getMethodAnno(m, RequestMapping.class);
if (manno != null) {
if (!basePaths.isEmpty()) {
for (String basePath : basePaths) {
for (String path : manno.value()) {
msbs.add(new StringBuffer(basePath).append(getPath(path)));
} //
} //
} else {
for (String path : manno.value()) {
msbs.add(new StringBuffer(getPath(path)));
} //
} //
} //

for (StringBuffer sb : msbs) {
map.put(sb.toString(), new Box(m, controller));
} //

} // for method

} // for class
}// init

public R getR(String path, Map<String, String> paramMap) {

Box b = matchingPath(path);
if (b == null)
return R.error("404 Url is Not Find");

Class<?>[] clas = b.getM().getParameterTypes();
String[] paramStr = parameterNameDiscoverer.getParameterNames(b.getM());

List<Object> args = new ArrayList<>(clas.length);
for (int x = 0; x < clas.length; x++) {
args.add(getParam(paramStr[x], clas[x], paramMap));
}
try {
return (R) b.getM().invoke(b.getO(), args.toArray());
} catch (CustomException e) {
return R.error(e.getMessage());
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
return R.error("参数不正确");
}// method

private Object getParam(String par, Class<?> cla, Map<String, String> paramMap) {

// by Servlet
if (cla.isInstance(HttpServletRequest.class))
return ServletUtils.getRequest();
if (cla.isInstance(HttpServletResponse.class))
return ServletUtils.getResponse();
if (cla.isInstance(HttpSession.class))
return ServletUtils.getSession();

// by Name
Object nameParam = StringToClass.call(paramMap.get(par), cla);
if (nameParam != null)
return nameParam;

// by POJO by Name
if (!cla.getPackageName().startsWith("java.lang")) {
// 不是基础类型 可以顺便判断一下是不是自己自定义包名下面的类
/*
* try { Object obj = cla.getDeclaredConstructor().newInstance(); BeanMap
* beanMap = BeanMap.create(obj); beanMap.putAll(paramMap); return obj; } catch
* (InstantiationException | IllegalAccessException | IllegalArgumentException |
* InvocationTargetException | NoSuchMethodException | SecurityException e) {
* return null; }
*/
try {
Object pojo = mapToBean(paramMap, cla);
return pojo;
} catch (Exception e) {
e.printStackTrace();
}
}

return null;
}

public static Object mapToBean(Map<String, String> map, Class<?> beanClass) throws Exception {
Object object = beanClass.getDeclaredConstructor().newInstance();
List<Field> fields = BeanUtils.getThisToObjectFies(beanClass);
for (Field f : fields) {
Object value = StringToClass.call(map.get(f.getName()), f.getType());
if (value != null) {
f.setAccessible(true);
f.set(object, value);
}
}
return object;
}

// 路径匹配
private Box matchingPath(String path) {
// 更复杂的 比如 /xxx/{路径参数} 这种的待添加
return map.get(path);
}

private String getPath(String path) {
if (path == null || path.isEmpty())
return "";
if (path.startsWith("/") || path.startsWith("\\"))
return path;
return "/" + path;
}

private Annotation getClassAnno(Class<?> cla, Class<? extends Annotation> annotationType) {
return AnnotatedElementUtils.getMergedAnnotation(cla, annotationType);
/*
* Annotation res = cla.getAnnotation(annotationType); if (res != null) return
* res; for (Annotation a : cla.getAnnotations()) { res =
* getClassAnno(a.getClass(), annotationType); if (res != null) return res; } //
* for return null;
*/
}// method

private Annotation getMethodAnno(Method method, Class<? extends Annotation> annotationType) {
return AnnotatedElementUtils.getMergedAnnotation(method, annotationType);
/*
* Annotation res = method.getAnnotation(annotationType); if (res != null)
* return res; for (Annotation a : method.getAnnotations()) { res =
* getClassAnno(a.getClass(), annotationType); if (res != null) return res; } //
* for return null;
*/
}// method

@Data
@AllArgsConstructor
class Box {
Method m;
Object o;
}

}// class

其他工具类

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
public class StringToClass {

public static Object call(String param,Class<?> cla) {

if(StringUtils.isEmpty(param)) return null;

if (cla.isAssignableFrom(String.class))
return param;
if (cla.isAssignableFrom(Integer.class) || cla.isAssignableFrom(int.class))
return Integer.valueOf(param);
if (cla.isAssignableFrom(Short.class) || cla.isAssignableFrom(short.class))
return Short.valueOf(param);
if (cla.isAssignableFrom(Long.class) || cla.isAssignableFrom(long.class))
return Long.valueOf(param);
if (cla.isAssignableFrom(Float.class) || cla.isAssignableFrom(float.class))
return Float.valueOf(param);
if (cla.isAssignableFrom(Double.class) || cla.isAssignableFrom(double.class))
return Double.valueOf(param);
if (cla.isAssignableFrom(BigDecimal.class))
return BigDecimal.valueOf(Double.valueOf(param));
if (cla.isAssignableFrom(Boolean.class) || cla.isAssignableFrom(boolean.class))
return Boolean.valueOf(param);
if (cla.isAssignableFrom(Byte.class) || cla.isAssignableFrom(byte.class))
return Byte.valueOf(param);

return null;
}
}// class

public final class BeanUtils extends org.springframework.beans.BeanUtils {
public static List<Field> getThisToObjectFies(Class<?> cls) {
List<Field> res = new ArrayList<Field>();
if (cls == null)
return res;
do {
res.addAll(Arrays.asList(cls.getDeclaredFields()));
} while ((cls = cls.getSuperclass()) != null);
return res;
}// getThisToObjectFies
}

Controller:

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
@RestController
@RequestMapping("index")
public class IndexController extends GlobalController {

@Lazy(true)
@Autowired
private WebWilfulUtils wilfulUtils;

@PostMapping("wilful")
public R test(@RequestBody List<WilfulParam> params) {
Map<String,R> data = new HashMap<>();
params.forEach(p->{
R mr = wilfulUtils.getR(p.getPath(), p.getParam());
data.put(p.getPath(), mr.pure());
});
return R.data(data);
}

}// class IndexController


@Data
public class WilfulParam {
private String path;
private Map<String,String> param;
}


小结:

麻烦的几个点是

​ 组合注解的识别 最早是自己实现的 后修改为 使用Spring的AnnotatedElementUtils进行识别

​ 方法参数注入 这个真的是特别麻烦 现在的实现 支持注入的参数其实不够多

​ 方法参数名的识别也是个大坑 现在先用 Spring的ParameterNameDiscoverer 去识别

还有些路径是不能识别的比如 /xxx/{id}/{xxxid} 这样的路径参数 的 @RequestMapper

暂时没有这样的接口需要合并就先不实现了

花点时间 基本上 所有的接口都可以被反射合并调用