mock平台支持三种方式(URI、方法、RPC)
URI:当前端需要根据接口的各种状态来进行不同的展示时,而后端还没有写完完整的代码时,可以在项目开发初期通过该方式进行自定义设置接口返回结果。
方法:当后端需要某个方法返回某些特定的值时,而这时真实的情况又很难构造这个值时,就可以采用该方式。
RPC:当后端要与别的服务进行合作时,而别的服务方接口提供较慢或有问题时,可以采用该方式进行自测。
mock平台的数据存储可以是数据库或者其他平台等等,主要存储对应关系,key可以为URI或方法的全限定名和参数,value为具体的json结构,为了防止影响别人,用用户ID进行区分,如用户ID和key组成唯一键,这些数据是从控制台进行设置。
下面是具体代码过程:
模拟上下文配置,包括当前所处环境,spring容器,存储当前用户登陆状态的ThreadLocal,类方法参数拼接key的方法,获取mock平台数据的方法,平台数据存储采用数据库,以及自动生成指定类方法所代表的key的方法,方便使用者在控制台进行key-value添加。
@Configuration
public class MockContext implements ApplicationContextAware {
private static final boolean online = !EnvUtils.isOffline();
private static FastThreadLocal<LoginUser> 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 Pair<String, MockDO> getMockConfigDO(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平台中获取对应的配置,有配置就返回,没有的话就继续正常流程。
@Configuration
public 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注解,生成代理。
@Configuration
public 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<String, MockDO> 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 <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new PH3636MockClusterInvoker<T>(directory, this.cluster.join(directory));
}
}
判断环境,组装key,获取配置,转化json为对应的对象
public class PH3636MockClusterInvoker<T> implements Invoker<T> {
private final Directory<T> directory;
private final Invoker<T> invoker;
public PH3636MockClusterInvoker(Directory<T> directory, Invoker<T> 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 Class<T> getInterface() {
return directory.getInterface();
}
@Override
public Result invoke(Invocation invocation) throws RpcException {
if (MockContext.online()) {
return invoker.invoke(invocation);
}
Pair<String, MockDO> 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()));
}
}