线上项目 首页加载时 请求了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" : { "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 export function request (options ) { if (options.url != '/index/wilful' ) { return uni.$httpGroup.request({ path: options.url, param: { ...options.data } }) } ... } 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 Promise .resolve() Vue.prototype.$nextTick(()=> {}) 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;@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)); } } for (Method m : cla.getMethods()) { if (m.getReturnType() == null || !m.getReturnType().getName().equals(R.class.getName())) continue ; 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)); } } } } 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("参数不正确" ); } private Object getParam (String par, Class<?> cla, Map<String, String> paramMap) { if (cla.isInstance(HttpServletRequest.class)) return ServletUtils.getRequest(); if (cla.isInstance(HttpServletResponse.class)) return ServletUtils.getResponse(); if (cla.isInstance(HttpSession.class)) return ServletUtils.getSession(); Object nameParam = StringToClass.call(paramMap.get(par), cla); if (nameParam != null ) return nameParam; if (!cla.getPackageName().startsWith("java.lang" )) { 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) { 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); } private Annotation getMethodAnno (Method method, Class<? extends Annotation> annotationType) { return AnnotatedElementUtils.getMergedAnnotation(method, annotationType); } @Data @AllArgsConstructor class Box { Method m; Object o; } }
其他工具类
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 ; } } 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; } }
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); } } @Data public class WilfulParam { private String path; private Map<String,String> param; }
小结: 麻烦的几个点是
组合注解的识别 最早是自己实现的 后修改为 使用Spring的AnnotatedElementUtils进行识别
方法参数注入 这个真的是特别麻烦 现在的实现 支持注入的参数其实不够多
方法参数名的识别也是个大坑 现在先用 Spring的ParameterNameDiscoverer 去识别
还有些路径是不能识别的比如 /xxx/{id}/{xxxid} 这样的路径参数 的 @RequestMapper
暂时没有这样的接口需要合并就先不实现了
花点时间 基本上 所有的接口都可以被反射合并调用