mock平台支持三种方式(URI、方法、RPC)
URI:当前端需要根据接口的各种状态来进行不同的展示时,而后端还没有写完完整的代码时,可以在项目开发初期通过该方式进行自定义设置接口返回结果。
方法:当后端需要某个方法返回某些特定的值时,而这时真实的情况又很难构造这个值时,就可以采用该方式。
RPC:当后端要与别的服务进行合作时,而别的服务方接口提供较慢或有问题时,可以采用该方式进行自测。
mock平台的数据存储可以是数据库或者其他平台等等,主要存储对应关系,key可以为URI或方法的全限定名和参数,value为具体的json结构,为了防止影响别人,用用户ID进行区分,如用户ID和key组成唯一键,这些数据是从控制台进行设置。
下面是具体代码过程:
模拟上下文配置,包括当前所处环境,spring容器,存储当前用户登陆状态的ThreadLocal,类方法参数拼接key的方法,获取mock平台数据的方法,平台数据存储采用数据库,以及自动生成指定类方法所代表的key的方法,方便使用者在控制台进行key-value添加。
@Configurationpublic class MockContext implements ApplicationContextAware { private static final boolean online = !EnvUtils.isOffline(); private static FastThreadLocal fastThreadLocal = new FastThreadLocal<>(); private static ApplicationContext instance; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { instance = applicationContext; } public static ApplicationContext getInstance() { return instance; } /** * 是否是线上环境 */ public static boolean online() { return online; } public static void set(LoginUser loginUser) { fastThreadLocal.set(loginUser); } public static LoginUser get() { return fastThreadLocal.get(); } public static void remove() { fastThreadLocal.remove(); } /** * 唯一url(类,方法,参数拼接) */ public static String getUrl(String className, String methodName, Class>[] parameterTypes) { StringBuilder sb = new StringBuilder(className); sb.append(".").append(methodName); StringJoiner stringJoiner = new StringJoiner(",", "(", ")"); if (parameterTypes != null) { for (Class> parameterType : parameterTypes) { if (parameterType != null) { stringJoiner.add(parameterType.getName()); } } } return sb.append(stringJoiner.toString()).toString(); } /** * 数据库配置 * 可以替换为http形式等等 */ public static MockDO getMockConfigDO(String url) { LoginUser loginUser = MockContext.get(); if (loginUser == null) { return null; } return MockContext.getInstance().getBean(MockService.class).loadByUserIdUrl(loginUser.getUserId(), url); } public static PairgetMockConfigDO(String className, String methodName, Class>[] parameterTypes) { String url = getUrl(className, methodName, parameterTypes); return Pair.of(url, getMockConfigDO(url)); } public static boolean notContent(MockDO mockDO) { return mockDO == null; } public static boolean notReturnType(Class> returnType) { return returnType == null || returnType == void.class || returnType == Void.class; } /** * 获取唯一key * 第一个参数为类的全限定名(必填),第二个参数为该类的方法名(选填) * 控制台方式:用英文逗号分割 */ public static void main(String[] args) { Scanner input = new Scanner(System.in); String val; do { System.out.print("请输入:"); val = input.next(); print(val.split(",")); } while (!"exit".equals(val)); System.out.println("程序退出!"); input.close(); } public static void print(String[] args) { if (args == null || args.length < 1 || "exit".equals(args[0])) { return; } Class> c; try { c = Class.forName(args[0]); } catch (ClassNotFoundException ignore) { System.out.println("类不存在!" + ignore.fillInStackTrace()); return; } Method[] methods = c.getMethods(); String m = args.length > 1 ? args[1] : null; for (Method method : methods) { if (StringUtils.isBlank(m)) { System.out.println(getUrl(c.getName(), method.getName(), method.getParameterTypes())); } else if (method.getName().equals(m)) { System.out.println(getUrl(c.getName(), method.getName(), method.getParameterTypes())); } } }}
针对URI形式的拦截器,判断环境,设置登陆状态,根据uri从mock平台中获取对应的配置,有配置就返回,没有的话就继续正常流程。
@Configurationpublic class PH3636MockInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (MockContext.online()) { return true; } if (!(handler instanceof HandlerMethod)) { return false; } LoginUser loginUser = (LoginUser) request.getAttribute(Constants.LOGIN_USER); if (loginUser == null) { return true; } MockContext.set(loginUser); String uri = request.getRequestURI(); MockDO mockDO = MockContext.getMockConfigDO(uri); if (MockContext.notContent(mockDO)) { return true; } response.setContentType("application/json;charset=UTF-8"); response.setHeader("Cache-Control", "no-cache"); PrintWriter pw = response.getWriter(); try { pw.write(mockDO.getContent()); } finally { pw.flush(); pw.close(); } return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { if (MockContext.online()) { return; } MockContext.remove(); }}
针对方法形式的代理,判断环境,判断当前bean的方法中有没有mock注解,生成代理。
@Configurationpublic class PH3636MockBeanPostProcessor implements BeanPostProcessor { public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (MockContext.online()) { return bean; } Mock mock = null; for (Method method : bean.getClass().getMethods()) { mock = method.getAnnotation(Mock.class); if (mock != null) { break; } } if (mock == null) { return bean; } ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setProxyTargetClass(true); proxyFactory.setExposeProxy(true); proxyFactory.setTarget(bean); proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new PH3636MockMethodInterceptor())); return proxyFactory.getProxy(); }}
判断当前方法是否有mock注解,组装key,获取配置,转化json为对应的对象
public class PH3636MockMethodInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { Mock mock = invocation.getMethod().getAnnotation(Mock.class); if (mock == null) { return invocation.proceed(); } Pair pair = MockContext.getMockConfigDO(invocation.getThis().getClass().getName(), invocation.getMethod().getName(), invocation.getMethod().getParameterTypes()); if (MockContext.notContent(pair.getRight())) { return invocation.proceed(); } Class> returnType = invocation.getMethod().getReturnType(); if (MockContext.notReturnType(returnType)) { return invocation.proceed(); } return JsonUtil.fromJsonToType(pair.getRight().getContent(), invocation.getMethod().getGenericReturnType()); }}
针对RPC(DUBBO)形式的Cluster扩展wrapper形式,在无提供者的情况下正常起作用,不需要额外修改,其他的一些形式都各有弊端,比如filter形式必须要有提供者,ProxyFactory代理需要额外配置,ClusterInterceptor要求版本较高等等。
public class PH3636MockClusterWrapper implements Cluster { private Cluster cluster; public PH3636MockClusterWrapper(Cluster cluster) { this.cluster = cluster; } @Override public Invokerjoin(Directory directory) throws RpcException { return new PH3636MockClusterInvoker(directory, this.cluster.join(directory)); }}
判断环境,组装key,获取配置,转化json为对应的对象
public class PH3636MockClusterInvoker<T> implements Invoker<T> { private final Directory directory; private final Invoker invoker; public PH3636MockClusterInvoker(Directory directory, Invoker invoker) { this.directory = directory; this.invoker = invoker; } @Override public URL getUrl() { return directory.getUrl(); } @Override public boolean isAvailable() { return directory.isAvailable(); } @Override public void destroy() { this.invoker.destroy(); } @Override public ClassgetInterface() { return directory.getInterface(); } @Override public Result invoke(Invocation invocation) throws RpcException { if (MockContext.online()) { return invoker.invoke(invocation); } Pair pair = MockContext.getMockConfigDO(invoker.getInterface().getName(), invocation.getMethodName(), invocation.getParameterTypes()); if (MockContext.notContent(pair.getRight())) { return invoker.invoke(invocation); } Method method; try { method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); } catch (NoSuchMethodException e) { return invoker.invoke(invocation); } if (method == null) { return invoker.invoke(invocation); } Class> returnType = method.getReturnType(); if (MockContext.notReturnType(returnType)) { return invoker.invoke(invocation); } return new RpcResult((Object) JsonUtil.fromJsonToType(pair.getRight().getContent(), method.getGenericReturnType())); }}