一、背景

某个服务或前端依赖一个服务接口,该接口可能依赖多个低层服务或模块,或第三方接口,这种情况下需要搭建多个底层模块多套测试环境,比较痛苦,如果mock掉第一级的服务接口,可以节约不少人力,同时规避了可能由第三方服务导致的问题。

mockJs拦截axios mock拦截请求原理_数据

mockJs拦截axios mock拦截请求原理_spring_02

目前常见服务或接口协议主要两种,一种是RPC,另一种是HTTP/HTTPS,mock原理都类似,要么是修改原服务地址为Mock服务地址,要么是拦截原服务的请求Mock返回值,总之就是构造一个假的服务,替代原有服务。

二、Mock实现方式

只介绍下HTTP/HTTPS协议的mock实现,RPC不做深究,原理都类似。Mock实现常见两种方式,一种是通过代理抓取待测服务请求并控制返回值;另一种是直接将待测服务指向Mock服务地址,替代下游原始真实服务。

第一种通过替换待测服务为Mock Gateway地址抓取请求并控制返回值来实现,(或者简单点,直接用Mock Service地址来替换待测服务地址也可以,更简单)通过Gateway网关转发请求,下游再设具体mock服务,可以是一个mock服务直接返回预期的mock值,也可以是proxy服务继续代理请求到其他地址,或redirect服务转发到某个特殊的地址等等方式。

如果自己搭建,建议使用java技术栈,Mock Gateway和Mock Service可以使用springcloud或springboot来实现,比较简单,mock策略和数据可以使用mysql或redis或es来存储,或者放到内存中也是可以的。

mockJs拦截axios mock拦截请求原理_数据_03

第二种直接使用正向代理代理请求,拦截请求,常用工具有charles、fiddler,mitmproxy等,工具比较成熟,直接使用即可,不做深入讨论,如果持久化Mock数据,建议使用mitmproxy,技术栈是python,可自行研究

三、Mock实践实例

介绍下第一种mock方案实践方案,技术栈选用springcloud+zuul+mybatis+mysql+keytools

  • 工程1:mock-gateway用于实现拦截请求,接受到请求后,充当路由功能
  • 工程2:mock-service用于接受mock-gateway转发来的请求,指定mock规则和mock数据返回
  • 工程3:proxy-service用于接受mock-gateway转发来的请求,继续透传请求或其他mock规则改变请求

 

  • 创建数据库和表
#存储mock规则,包含所有mock、proxy、redirect等规则数据
-- auto-generated definition
create table mock_app
(
  id               int auto_increment,
  name             varchar(200) null,
  description      varchar(200) null,
  request_type     varchar(20)  null,
  request_uri      varchar(200) null,
  request_query    longtext     null,
  request_body     longtext     null,
  request_header   longtext     null,
  mock_data        longtext     null,
  redirect_type    varchar(200) null,
  redirect_url     varchar(200) null,
  redirect_query   longtext     null,
  redirect_body    longtext     null,
  redirect_header  longtext     null,
  proxy_url_perfix varchar(200) null,
  is_active        tinyint(1)   null,
  create_time      timestamp    not null,
  update_time      timestamp    not null,
  approver         varchar(200) not null,
  constraint mock_app_id_uindex
  unique (id)
);

alter table mock_app
  add primary key (id);
#存储mock策略,mock_app表中同一条mock数据根据mock策略返回不同的值
-- auto-generated definition
create table mock_strategy
(
  id                     int auto_increment,
  name                   varchar(200) not null,
  description            varchar(200) not null,
  mock_response_strategy int          not null,
  create_time            timestamp    not null,
  update_time            timestamp    not null,
  approver               varchar(200) not null,
  constraint mock_strategy_id_uindex
  unique (id)
);

alter table mock_strategy
  add primary key (id);
  • mock-gateway
  • 直接创建一个springcloud工程,不会的自己搜索下,很简单,官网直接可以生产工程目录
  • 添加springcloud主类
package com.personal.mock;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;


@SpringBootApplication
@MapperScan(basePackages = "com.personal.mock.dao")
@EnableZuulProxy
public class MockApplication {

	public static void main(String[] args) {
		SpringApplication.run(MockApplication.class, args);
	}
}
#properties配置文件

#gateway服务端口
server.port=10086
#添加zuul拦截条件
zuul.routes.root.path=/*
zuul.routes.root.url=http://127.0.0.1:10086/

#数据库相关配置
spring.datasource.url = jdbc:mysql://localhost:3306/mock?characterSet=utf8mb4&useSSL=false
spring.datasource.username = root
spring.datasource.password = QWER1234qwer

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.filters=stat
spring.datasource.maxActive=20
spring.datasource.initialSize=1
spring.datasource.maxWait=60000
spring.datasource.minIdle=1
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=true
spring.datasource.testOnReturn=true
spring.datasource.poolPreparedStatements=true
spring.datasource.maxOpenPreparedStatements=20

#下游服务配置
mock.request.address=http://127.0.0.1:10010/mock/request
mock.response.address=http://127.0.0.1:10010/mock/response
proxy.address=http://127.0.0.1:10011/proxy
redirect.address=http://127.0.0.1:10012/redirect
  • 添加过滤器,过滤请求,过滤规则也在这里实现即可
package com.personal.mock.filter;

import com.alibaba.fastjson.JSONObject;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.personal.mock.common.MockStrategyEnum;
import com.personal.mock.po.MockApp;
import com.personal.mock.route.MockZuulRoute;
import com.personal.mock.route.MockZuulRouteLocator;
import com.personal.mock.service.FilterService;
import com.personal.mock.service.RequestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
import java.util.Map;

import static com.personal.mock.common.Constant.*;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

/**
 * author: zhaoxu
 * date: 2019/4/30 上午10:07
 */
@Component
public class MockPreFilter extends ZuulFilter {

    private static Logger logger = LoggerFactory.getLogger(MockPreFilter.class);

    @Value(value = "${mock.request.address}")
    private String mockRequestAddress;
    @Value(value = "${mock.response.address}")
    private String mockResponseAddress;
    @Value(value = "${proxy.address}")
    private String proxyAddress;
    @Value(value = "${redirect.address}")
    private String redirectAddress;


    @Autowired
    FilterService filterService;

    @Autowired
    RequestService requestService;

    @Autowired
    MockZuulRouteLocator mockZuulRouteLocator;

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 4;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest httpServletRequest = ctx.getRequest();

        logger.info(String.valueOf(requestService.getRequestHeader()));
        logger.info(requestService.getMethod());
        logger.info(requestService.getQueryString());
        logger.info(requestService.getRequestURI());
        logger.info(String.valueOf(requestService.getRequestBody()));

        logger.info("request uri: "+httpServletRequest.getRequestURI());
        Map<MockApp, Integer> map = filterService.getFilterInfo(httpServletRequest);
        if (null != map){
            Iterator <MockApp>iterator = map.keySet().iterator();
            MockApp mockApp = iterator.next();
            Integer mockStrategyId = map.get(mockApp);
            String path = mockApp.getRequestUri();
            Integer mockAppId = mockApp.getId();
            String url = null;
            try{
                MockStrategyEnum mockStrategyEnum = MockStrategyEnum.getMockStrategyByStrategyId(mockStrategyId);
                switch (mockStrategyEnum) {
                    case MOCK_RESPONSE_DIRECT:
                        url = mockResponseAddress;
                        logger.info("【正常,gateway转发给下级服务处理】" +url);
                        break;
                    case MOCK_REQUEST_RETURN:
                        url = mockRequestAddress;
                        logger.info("【正常,gateway转发给下级服务处理】" +url);
                        break;
                    case REQUEST_REDIRECT:
                        url = redirectAddress;
                        logger.info("【正常,gateway转发给下级服务处理】" +url);
                        break;
                    case PROXY:
                        url = proxyAddress;
                        logger.info("【正常,gateway转发给下级服务处理】" +url);
                        break;
                }
            } catch(NullPointerException e){
                logger.error("【异常,没有匹配到策略,详情如下:】");
                logger.error("【mock_app.id=%d 对应的mock_strategy.mock_response_strategy配置异常,请检查】");
            }
            MockZuulRoute mockZuulRoute = new MockZuulRoute(mockAppId.toString(),path,url);
            mockZuulRouteLocator.updateRoutes(mockZuulRoute);
            ctx.addZuulRequestHeader(MOCK_STRATEGY_ID,mockStrategyId.toString());
            ctx.addZuulRequestHeader(MOCK_APP_ID,mockAppId.toString());
            ctx.addZuulRequestHeader(REQUEST_URI,httpServletRequest.getRequestURI());
        } else {
            logger.error("【异常,没有匹配到mock数据,请检查】");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("errorMessage","【呃呃呃, 你的请求没有匹配到数据库的mock数据,请检查】");
            ctx.setResponseBody(jsonObject.toJSONString());
            ctx.getResponse().setContentType("application/json;charset=UTF-8");
        }
        return null;
    }
}
  • mock-service、mock-proxy服务与mock-gateway代码结构类似,去掉请求过滤器即可

po层和dao层的代码就不粘贴了,类似与mybatis,后续我会把代码放到github上