├── fox-mock-agent ├── .DS_Store ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── dubbo │ │ │ │ └── org.apache.dubbo.rpc.Filter │ │ └── foxmock-logback.xml │ │ └── java │ │ ├── com │ │ └── cxytiandi │ │ │ └── foxmock │ │ │ └── agent │ │ │ ├── express │ │ │ ├── OgnlExpressException.java │ │ │ ├── ExpressFactory.java │ │ │ ├── Express.java │ │ │ └── OgnlExpress.java │ │ │ ├── utils │ │ │ ├── CollectionUtils.java │ │ │ ├── StringUtils.java │ │ │ ├── ClassUtils.java │ │ │ ├── IOUtils.java │ │ │ ├── HttpUtils.java │ │ │ ├── ReflectionUtils.java │ │ │ ├── JsonUtils.java │ │ │ └── MD5.java │ │ │ ├── storage │ │ │ ├── Storage.java │ │ │ ├── StorageHelper.java │ │ │ ├── HttpStorage.java │ │ │ └── LocalFileStorage.java │ │ │ ├── factory │ │ │ └── MockInfoFactory.java │ │ │ ├── constant │ │ │ └── FoxMockConstant.java │ │ │ ├── model │ │ │ ├── MockInfo.java │ │ │ ├── ClassInfo.java │ │ │ └── FoxMockAgentArgs.java │ │ │ ├── transformer │ │ │ ├── DubboInvokeFilter.java │ │ │ ├── MethodInvokeFilter.java │ │ │ └── MockClassFileTransformer.java │ │ │ └── FoxMockAgent.java │ │ └── feign │ │ └── FeignInvokeFilter.java └── pom.xml ├── fox-mock-boot ├── .DS_Store ├── src │ └── main │ │ └── java │ │ └── com │ │ └── cxytiandi │ │ └── foxmock │ │ └── boot │ │ ├── utils │ │ ├── PathUtils.java │ │ └── ProcessUtils.java │ │ ├── config │ │ └── Config.java │ │ └── Bootstrap.java └── pom.xml ├── fox-mock-example ├── src │ └── main │ │ ├── resources │ │ ├── mockdata │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getAge │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getAddrs │ │ │ ├── com.cxytiandi.foxmock.example.mybatis.UserMapper#findNameById │ │ │ ├── com.cxytiandi.foxmock.example.mybatis.UserMapper#updateNameById │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getUser │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getUsers │ │ │ ├── com.cxytiandi.foxmock.example.mybatis.UserMapper#findById │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getUsers2 │ │ │ ├── com.cxytiandi.foxmock.example.mybatis.UserMapper#find │ │ │ ├── com.cxytiandi.foxmock.example.UserService#mockException │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getName2 │ │ │ ├── com.cxytiandi.foxmock.example.dubbo.DubboApi#getUserInfo │ │ │ ├── com.cxytiandi.foxmock.example.UserService#getUserDetail │ │ │ └── com.cxytiandi.foxmock.example.dubbo.DubboApi#getUserDetail │ │ └── application.properties │ │ └── java │ │ └── com │ │ └── cxytiandi │ │ └── foxmock │ │ └── example │ │ ├── mybatis │ │ ├── User.java │ │ ├── UserQuery.java │ │ └── UserMapper.java │ │ ├── MockException.java │ │ ├── dubbo │ │ ├── DubboApi.java │ │ ├── DubboApiImpl.java │ │ └── DubboApiTestService.java │ │ ├── UserInfo.java │ │ ├── Result.java │ │ ├── feign │ │ ├── FeignApiImpl.java │ │ ├── FeignApiTestService.java │ │ └── FeignApi.java │ │ ├── ApplicationContextHelper.java │ │ ├── UserDetail.java │ │ ├── UserService.java │ │ └── FoxMockApp.java └── pom.xml ├── static └── mockdatafile.png ├── script ├── start.sh └── install.sh ├── .gitignore ├── agent.properties ├── README.md ├── pom.xml └── LICENSE /fox-mock-agent/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinjihuan/fox-mock/HEAD/fox-mock-agent/.DS_Store -------------------------------------------------------------------------------- /fox-mock-boot/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinjihuan/fox-mock/HEAD/fox-mock-boot/.DS_Store -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getAge: -------------------------------------------------------------------------------- 1 | 188 2 | -------------------------------------------------------------------------------- /static/mockdatafile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinjihuan/fox-mock/HEAD/static/mockdatafile.png -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getAddrs: -------------------------------------------------------------------------------- 1 | ["你好","大声道"] 2 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.mybatis.UserMapper#findNameById: -------------------------------------------------------------------------------- 1 | 猿天地 -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.mybatis.UserMapper#updateNameById: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getUser: -------------------------------------------------------------------------------- 1 | {"data":{"id":1000}} -------------------------------------------------------------------------------- /script/start.sh: -------------------------------------------------------------------------------- 1 | java -jar -Xbootclasspath/a:/${JAVA_HOME}/lib/tools.jar fox-mock-boot-jar-with-dependencies.jar 2 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getUsers: -------------------------------------------------------------------------------- 1 | [{"id":1001},{"id":1002}] 2 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.mybatis.UserMapper#findById: -------------------------------------------------------------------------------- 1 | {"id":11,"name":"yjhone"} -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getUsers2: -------------------------------------------------------------------------------- 1 | { 2 | "data":[{"id":1001},{"id":1002}] 3 | } 4 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.mybatis.UserMapper#find: -------------------------------------------------------------------------------- 1 | [{"id":11,"name":"yjh"},{"id":12,"name":"yjh2"}] -------------------------------------------------------------------------------- /fox-mock-agent/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter: -------------------------------------------------------------------------------- 1 | dubboInvokeFilter=com.cxytiandi.foxmock.agent.transformer.DubboInvokeFilter -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#mockException: -------------------------------------------------------------------------------- 1 | throw new com.cxytiandi.foxmock.example.MockException("mock exception"); -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getName2: -------------------------------------------------------------------------------- 1 | { 2 | "f_mock_data": "{\"id\":1001}", 3 | "f_ognl_express": "#p0.id.equals(2)" 4 | } -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.dubbo.DubboApi#getUserInfo: -------------------------------------------------------------------------------- 1 | { 2 | "f_mock_data": "{\"id\":9999}", 3 | "f_ognl_express": "#p0.equals(1L)" 4 | } -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.UserService#getUserDetail: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "name": "yinjihuan", 4 | "addressMap": { 5 | "上海": { 6 | "address": "虹口" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/mockdata/com.cxytiandi.foxmock.example.dubbo.DubboApi#getUserDetail: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "name": "yinjihuan", 4 | "addressMap": { 5 | "上海": { 6 | "address": "虹口" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/mybatis/User.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.mybatis; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class User implements Serializable { 9 | 10 | private Integer id; 11 | 12 | private String name; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # maven ignore 2 | target/ 3 | *.jar 4 | *.war 5 | *.zip 6 | *.tar 7 | *.tar.gz 8 | 9 | # eclipse ignore 10 | .settings/ 11 | .project 12 | .classpath 13 | 14 | # idea ignore 15 | .idea/ 16 | *.ipr 17 | *.iml 18 | *.iws 19 | 20 | # temp ignore 21 | *.log 22 | *.cache 23 | *.diff 24 | *.patch 25 | *.tmp 26 | *.java~ 27 | *.properties~ 28 | *.xml~ 29 | 30 | # system ignore 31 | .DS_Store 32 | Thumbs.db 33 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/MockException.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | /** 4 | * @作者 尹吉欢 5 | * @个人微信 jihuan900 6 | * @微信公众号 猿天地 7 | * @GitHub https://github.com/yinjihuan 8 | * @作者介绍 http://cxytiandi.com/about 9 | * @时间 2022-05-09 21:59 10 | */ 11 | public class MockException extends RuntimeException { 12 | 13 | public MockException(String msg) { 14 | super(msg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/express/OgnlExpressException.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.express; 2 | 3 | public class OgnlExpressException extends Exception { 4 | 5 | private final String express; 6 | 7 | public OgnlExpressException(String express, Throwable cause) { 8 | super(cause); 9 | this.express = express; 10 | } 11 | 12 | public String getExpress() { 13 | return express; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fox-mock-boot/src/main/java/com/cxytiandi/foxmock/boot/utils/PathUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.boot.utils; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * @作者 尹吉欢 7 | * @个人微信 jihuan900 8 | * @微信公众号 猿天地 9 | * @GitHub https://github.com/yinjihuan 10 | * @作者介绍 http://cxytiandi.com/about 11 | * @时间 2022-04-25 22:34 12 | */ 13 | public class PathUtils { 14 | 15 | public static String getAgentPath() { 16 | String path = new File("").getAbsolutePath(); 17 | return path; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/express/ExpressFactory.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.express; 2 | 3 | public class ExpressFactory { 4 | 5 | private static final ThreadLocal EXPRESS_THREAD_LOCAL = new ThreadLocal() { 6 | @Override 7 | protected Express initialValue() { 8 | return new OgnlExpress(); 9 | } 10 | }; 11 | 12 | public static Express getExpress(Object object) { 13 | return EXPRESS_THREAD_LOCAL.get().reset().bind(object); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /agent.properties: -------------------------------------------------------------------------------- 1 | # mock文件路径 2 | foxMockFilePath=/Users/yinjihuan/Documents/foxmockdata 3 | # foxmock的agent jar包路径,不填默认为当前文件夹 4 | foxMockAgentJarPath=fox-mock-agent/target/fox-mock-agent-6.0.jar 5 | # mock方法白名单,如果文件夹中有多个方法会被全部mock,如果指定了此配置将只会mock这里指定的方法 6 | #mockMethodWhiteList=com.cxytiandi.foxmock.example.UserService#getAge|com.cxytiandi.foxmock.example.UserService#getName2 7 | # httpn mock数据地址,可以对接配置中心,必须是get请求 8 | #mockDataHttpUrl=http%3A%2F%2F47.105.66.210%3A8848%2Fnacos%2Fv1%2Fcs%2Fconfigs%3FdataId%3Dfox-mock%26group%3DDEFAULT_GROUP%26tenant%3D9c9f5197-688c-4ef9-ae68-1ff28663301d -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/dubbo/DubboApi.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.dubbo; 2 | 3 | import com.cxytiandi.foxmock.example.Result; 4 | import com.cxytiandi.foxmock.example.UserDetail; 5 | import com.cxytiandi.foxmock.example.UserInfo; 6 | 7 | /** 8 | * @作者 尹吉欢 9 | * @个人微信 jihuan900 10 | * @微信公众号 猿天地 11 | * @GitHub https://github.com/yinjihuan 12 | * @作者介绍 http://cxytiandi.com/about 13 | * @时间 2022-05-18 22:54 14 | */ 15 | public interface DubboApi { 16 | 17 | UserInfo getUserInfo(Long id); 18 | 19 | Result getUserDetail(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/mybatis/UserQuery.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.mybatis; 2 | 3 | /** 4 | * @作者 尹吉欢 5 | * @个人微信 jihuan900 6 | * @微信公众号 猿天地 7 | * @GitHub https://github.com/yinjihuan 8 | * @作者介绍 http://cxytiandi.com/about 9 | * @时间 2022-05-16 22:32 10 | */ 11 | public class UserQuery { 12 | 13 | private Integer id; 14 | 15 | public UserQuery(Integer id) { 16 | this.id = id; 17 | } 18 | 19 | public void setId(Integer id) { 20 | this.id = id; 21 | } 22 | 23 | public Integer getId() { 24 | return id; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 2 | spring.datasource.url=jdbc:mysql://localhost:3306/test 3 | spring.datasource.username=root 4 | spring.datasource.password=123456 5 | 6 | mybatis.type-aliases-package=com.cxytiandi.foxmock.example.mybatis 7 | mybatis.configuration.cache-enabled=true 8 | 9 | dubbo.scan.base-packages=com.cxytiandi.foxmock.example.dubbo 10 | dubbo.application.name=foxmock-example 11 | dubbo.registry.address=47.105.66.210:8848 12 | dubbo.registry.protocol=nacos 13 | dubbo.protocol.name=dubbo 14 | dubbo.protocol.port=20880 15 | 16 | feign.sentinel.enabled=true -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @作者 尹吉欢 7 | * @个人微信 jihuan900 8 | * @微信公众号 猿天地 9 | * @GitHub https://github.com/yinjihuan 10 | * @作者介绍 http://cxytiandi.com/about 11 | * @时间 2022-04-20 00:15 12 | */ 13 | public class UserInfo implements Serializable { 14 | private int id; 15 | 16 | public void setId(int id) { 17 | this.id = id; 18 | } 19 | 20 | public int getId() { 21 | return id; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return String.valueOf(id); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/Result.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @作者 尹吉欢 9 | * @个人微信 jihuan900 10 | * @微信公众号 猿天地 11 | * @GitHub https://github.com/yinjihuan 12 | * @作者介绍 http://cxytiandi.com/about 13 | * @时间 2022-04-20 22:54 14 | */ 15 | public class Result implements Serializable { 16 | 17 | private T data; 18 | 19 | public void setData(T data) { 20 | this.data = data; 21 | } 22 | 23 | public T getData() { 24 | return data; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return new Gson().toJson(this); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/CollectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | 6 | /** 7 | * @作者 尹吉欢 8 | * @个人微信 jihuan900 9 | * @微信公众号 猿天地 10 | * @GitHub https://github.com/yinjihuan 11 | * @作者介绍 http://cxytiandi.com/about 12 | * @时间 2022-04-30 21:20 13 | */ 14 | public class CollectionUtils { 15 | 16 | public static boolean isEquals(List list, List list2) { 17 | if (Objects.isNull(list) || Objects.isNull(list2)) { 18 | return false; 19 | } 20 | 21 | if (list.size() != list2.size()) { 22 | return false; 23 | } 24 | 25 | return list.containsAll(list2); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | /** 4 | * 字符串工具类 5 | * 6 | * @作者 尹吉欢 7 | * @个人微信 jihuan900 8 | * @微信公众号 猿天地 9 | * @GitHub https://github.com/yinjihuan 10 | * @作者介绍 http://cxytiandi.com/about 11 | * @时间 2022-04-21 22:42 12 | */ 13 | public class StringUtils { 14 | public static boolean isBlank(final CharSequence cs) { 15 | int strLen; 16 | if (cs == null || (strLen = cs.length()) == 0) { 17 | return true; 18 | } 19 | for (int i = 0; i < strLen; i++) { 20 | if (Character.isWhitespace(cs.charAt(i)) == false) { 21 | return false; 22 | } 23 | } 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/dubbo/DubboApiImpl.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.dubbo; 2 | 3 | import com.cxytiandi.foxmock.example.Result; 4 | import com.cxytiandi.foxmock.example.UserDetail; 5 | import com.cxytiandi.foxmock.example.UserInfo; 6 | import org.apache.dubbo.config.annotation.Service; 7 | 8 | /** 9 | * @作者 尹吉欢 10 | * @个人微信 jihuan900 11 | * @微信公众号 猿天地 12 | * @GitHub https://github.com/yinjihuan 13 | * @作者介绍 http://cxytiandi.com/about 14 | * @时间 2022-05-18 22:56 15 | */ 16 | @Service 17 | public class DubboApiImpl implements DubboApi { 18 | @Override 19 | public UserInfo getUserInfo(Long id) { 20 | return new UserInfo(); 21 | } 22 | 23 | @Override 24 | public Result getUserDetail() { 25 | return new Result(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/storage/Storage.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.storage; 2 | 3 | import com.cxytiandi.foxmock.agent.model.FoxMockAgentArgs; 4 | 5 | import java.util.Set; 6 | 7 | /** 8 | * mock数据存储接口 9 | * 10 | * @作者 尹吉欢 11 | * @个人微信 jihuan900 12 | * @微信公众号 猿天地 13 | * @GitHub https://github.com/yinjihuan 14 | * @作者介绍 http://cxytiandi.com/about 15 | * @时间 2022-04-18 23:46 16 | */ 17 | public interface Storage { 18 | 19 | /** 20 | * 加载数据 21 | * @param request 22 | */ 23 | boolean loadData(FoxMockAgentArgs request); 24 | 25 | /** 26 | * 获取数据 27 | * @param key 28 | * @return 29 | */ 30 | String get(String key); 31 | 32 | /** 33 | * 获取mock的类名称 34 | * @return 35 | */ 36 | Set getMockClassNames(); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/mybatis/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.mybatis; 2 | 3 | import org.apache.ibatis.annotations.*; 4 | 5 | import java.util.List; 6 | 7 | //@CacheNamespace 8 | @Mapper 9 | public interface UserMapper { 10 | 11 | @Select("select * from t_user") 12 | List find(); 13 | 14 | @Select("select * from t_user where id = #{id}") 15 | User findById(@Param("id") Integer id); 16 | 17 | @Select("select name from t_user where id = #{id}") 18 | String findNameById(@Param("id") Integer id); 19 | 20 | @Update("update t_user set name = #{name} where id = #{id}") 21 | int updateNameById(@Param("id") Integer id, @Param("name") String name); 22 | 23 | @Select("select * from t_user where id = #{id}") 24 | List find2(UserQuery query); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/factory/MockInfoFactory.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.factory; 2 | 3 | import com.cxytiandi.foxmock.agent.model.MockInfo; 4 | import com.cxytiandi.foxmock.agent.utils.JsonUtils; 5 | 6 | /** 7 | * @作者 尹吉欢 8 | * @个人微信 jihuan900 9 | * @微信公众号 猿天地 10 | * @GitHub https://github.com/yinjihuan 11 | * @作者介绍 http://cxytiandi.com/about 12 | * @时间 2022-05-15 22:59 13 | */ 14 | public class MockInfoFactory { 15 | 16 | public static MockInfo create(String data) { 17 | MockInfo mockInfo = null; 18 | if (data.contains("f_mock_data") || data.contains("f_ognl_express")) { 19 | mockInfo = JsonUtils.fromJson(data, MockInfo.class); 20 | } else { 21 | mockInfo = new MockInfo(); 22 | mockInfo.setMockData(data); 23 | } 24 | return mockInfo; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /script/install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # default downloading url 4 | FOX_MOCK_FILE_URL="http://file.cxytiandi.com/foxmock.zip" 5 | 6 | # exit shell with err_code 7 | # $1 : err_code 8 | # $2 : err_msg 9 | exit_on_err() 10 | { 11 | [[ ! -z "${2}" ]] && echo "${2}" 1>&2 12 | exit ${1} 13 | } 14 | 15 | # check permission to download && install 16 | [ ! -w ./ ] && exit_on_err 1 "permission denied, target directory ./ was not writable." 17 | 18 | if [ $# -gt 1 ] && [ $1 = "--url" ]; then 19 | shift 20 | FOX_MOCK_FILE_URL=$1 21 | shift 22 | fi 23 | 24 | # download from cxytiandi 25 | echo "downloading... ${FOX_MOCK_FILE_URL}" 26 | curl \ 27 | -sLk \ 28 | --connect-timeout 5000 \ 29 | $FOX_MOCK_FILE_URL \ 30 | -o "foxmock.zip" \ 31 | || exit_on_err 1 "download failed!" 32 | 33 | 34 | unzip "foxmock.zip" 35 | chmod +x "foxmock/start.sh" 36 | 37 | # done 38 | echo "foxmock install successed." 39 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/constant/FoxMockConstant.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.constant; 2 | 3 | /** 4 | * @作者 尹吉欢 5 | * @个人微信 jihuan900 6 | * @微信公众号 猿天地 7 | * @GitHub https://github.com/yinjihuan 8 | * @作者介绍 http://cxytiandi.com/about 9 | * @时间 2022-05-14 22:06 10 | */ 11 | public class FoxMockConstant { 12 | 13 | public static final String IBATIS_BASE_EXECUTOR = "org.apache.ibatis.executor.BaseExecutor"; 14 | public static final String IBATIS_CACHING_EXECUTOR = "org.apache.ibatis.executor.CachingExecutor"; 15 | public static final String DUBBO_CONSUMER_FILTER = "org.apache.dubbo.rpc.filter.ConsumerContextFilter"; 16 | public static final String FEIGN_METHOD_HANDLER = "feign.SynchronousMethodHandler"; 17 | 18 | public static final String IBATIS_MOCK_QUERY_METHOD = "query"; 19 | public static final String IBATIS_MOCK_UPDATE_METHOD = "update"; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/model/MockInfo.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.model; 2 | 3 | /** 4 | * Mock信息 5 | * 6 | * @作者 尹吉欢 7 | * @个人微信 jihuan900 8 | * @微信公众号 猿天地 9 | * @GitHub https://github.com/yinjihuan 10 | * @作者介绍 http://cxytiandi.com/about 11 | * @时间 2022-05-11 21:47 12 | */ 13 | public class MockInfo { 14 | 15 | /** 16 | * Mock 数据 17 | */ 18 | private String f_mock_data; 19 | 20 | /** 21 | * ognl匹配表达式 22 | */ 23 | private String f_ognl_express; 24 | 25 | public void setMockData(String mockData) { 26 | this.f_mock_data = mockData; 27 | } 28 | 29 | public String getMockData() { 30 | return f_mock_data; 31 | } 32 | 33 | public void setOgnlExpress(String ognlExpress) { 34 | this.f_ognl_express = ognlExpress; 35 | } 36 | 37 | public String getOgnlExpress() { 38 | return f_ognl_express; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/dubbo/DubboApiTestService.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.dubbo; 2 | 3 | import com.cxytiandi.foxmock.example.Result; 4 | import com.cxytiandi.foxmock.example.UserDetail; 5 | import com.cxytiandi.foxmock.example.UserInfo; 6 | import org.apache.dubbo.config.annotation.Reference; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | * @作者 尹吉欢 11 | * @个人微信 jihuan900 12 | * @微信公众号 猿天地 13 | * @GitHub https://github.com/yinjihuan 14 | * @作者介绍 http://cxytiandi.com/about 15 | * @时间 2022-05-18 23:00 16 | */ 17 | @Service 18 | public class DubboApiTestService { 19 | 20 | @Reference(url="dubbo://localhost:20880") 21 | private DubboApi dubboApi; 22 | 23 | public UserInfo getUserInfo(Long id) { 24 | return dubboApi.getUserInfo(id); 25 | } 26 | 27 | public Result getUserDetail() { 28 | return dubboApi.getUserDetail(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/feign/FeignApiImpl.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.feign; 2 | 3 | import com.cxytiandi.foxmock.example.Result; 4 | import com.cxytiandi.foxmock.example.UserDetail; 5 | import com.cxytiandi.foxmock.example.UserInfo; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | /** 9 | * @作者 尹吉欢 10 | * @个人微信 jihuan900 11 | * @微信公众号 猿天地 12 | * @GitHub https://github.com/yinjihuan 13 | * @作者介绍 http://cxytiandi.com/about 14 | * @时间 2022-05-24 22:59 15 | */ 16 | @RestController 17 | public class FeignApiImpl { 18 | 19 | @RequestMapping(value = "/getUserInfo", method = RequestMethod.GET) 20 | public UserInfo getUserInfo(@RequestParam("id")Long id) { 21 | return new UserInfo(); 22 | } 23 | 24 | @RequestMapping(value = "/getUserDetail", method = RequestMethod.GET) 25 | public Result getUserDetail() { 26 | return new Result<>(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/ClassUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * @作者 尹吉欢 8 | * @个人微信 jihuan900 9 | * @微信公众号 猿天地 10 | * @GitHub https://github.com/yinjihuan 11 | * @作者介绍 http://cxytiandi.com/about 12 | * @时间 2022-05-14 22:09 13 | */ 14 | public class ClassUtils { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(ClassUtils.class); 17 | 18 | public static Class forNameByFormat(String className) { 19 | try { 20 | return Class.forName(formatClassName(className)); 21 | } catch (ClassNotFoundException e) { 22 | LOG.error("className {} not found", className, e); 23 | } 24 | return null; 25 | } 26 | 27 | public static String formatClassName(String className) { 28 | return className.replace('/', '.'); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/IOUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import java.io.*; 4 | 5 | public class IOUtils { 6 | 7 | static public String toString(InputStream input, String encoding) throws IOException { 8 | return (null == encoding) ? toString(new InputStreamReader(input, "UTF-8")) 9 | : toString(new InputStreamReader(input, encoding)); 10 | } 11 | 12 | static public String toString(Reader reader) throws IOException { 13 | CharArrayWriter sw = new CharArrayWriter(); 14 | copy(reader, sw); 15 | return sw.toString(); 16 | } 17 | 18 | static public long copy(Reader input, Writer output) throws IOException { 19 | char[] buffer = new char[1 << 12]; 20 | long count = 0; 21 | for (int n = 0; (n = input.read(buffer)) >= 0; ) { 22 | output.write(buffer, 0, n); 23 | count += n; 24 | } 25 | return count; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/express/Express.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.express; 2 | 3 | public interface Express { 4 | 5 | /** 6 | * 根据表达式获取值 7 | * 8 | * @param express 表达式 9 | * @return 表达式运算后的值 10 | * @throws OgnlExpressException 表达式运算出错 11 | */ 12 | Object get(String express) throws OgnlExpressException; 13 | 14 | /** 15 | * 根据表达式判断是与否 16 | * 17 | * @param express 表达式 18 | * @return 表达式运算后的布尔值 19 | * @throws OgnlExpressException 表达式运算出错 20 | */ 21 | boolean is(String express) throws OgnlExpressException; 22 | 23 | /** 24 | * 绑定对象 25 | * 26 | * @param object 待绑定对象 27 | * @return this 28 | */ 29 | Express bind(Object object); 30 | 31 | /** 32 | * 绑定变量 33 | * 34 | * @param name 变量名 35 | * @param value 变量值 36 | * @return this 37 | */ 38 | Express bind(String name, Object value); 39 | 40 | /** 41 | * 重置整个表达式 42 | * 43 | * @return this 44 | */ 45 | Express reset(); 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/ApplicationContextHelper.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class ApplicationContextHelper implements ApplicationContextAware { 11 | 12 | private static ApplicationContext applicationContext; 13 | 14 | @Override 15 | public void setApplicationContext(ApplicationContext context) throws BeansException { 16 | applicationContext = context; 17 | } 18 | 19 | public static Environment getEnvironment() { 20 | return applicationContext.getEnvironment(); 21 | } 22 | 23 | public static T getBean(Class requiredType) { 24 | if (applicationContext == null) { 25 | return null; 26 | } 27 | return applicationContext.getBean(requiredType); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/feign/FeignApiTestService.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.feign; 2 | 3 | import com.cxytiandi.foxmock.example.Result; 4 | import com.cxytiandi.foxmock.example.UserDetail; 5 | import com.cxytiandi.foxmock.example.UserInfo; 6 | import com.cxytiandi.foxmock.example.dubbo.DubboApi; 7 | import org.apache.dubbo.config.annotation.Reference; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | 12 | /** 13 | * @作者 尹吉欢 14 | * @个人微信 jihuan900 15 | * @微信公众号 猿天地 16 | * @GitHub https://github.com/yinjihuan 17 | * @作者介绍 http://cxytiandi.com/about 18 | * @时间 2022-05-18 23:00 19 | */ 20 | @Service 21 | public class FeignApiTestService { 22 | 23 | @Autowired 24 | private FeignApi feignApi; 25 | 26 | public UserInfo getUserInfo(Long id) { 27 | return feignApi.getUserInfo(id); 28 | } 29 | 30 | public Result getUserDetail() { 31 | return feignApi.getUserDetail(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/resources/foxmock-logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ${FOX_MOCK_LOG_FILE} 9 | 10 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} -%msg%n 11 | 12 | 13 | ${FOX_MOCK_LOG_FILE}.%d{yyyy-MM-dd}.%i.log 14 | 15 | 7 16 | 1MB 17 | 10MB 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/feign/FeignApi.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example.feign; 2 | 3 | import com.cxytiandi.foxmock.example.Result; 4 | import com.cxytiandi.foxmock.example.UserDetail; 5 | import com.cxytiandi.foxmock.example.UserInfo; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | 13 | /** 14 | * @作者 尹吉欢 15 | * @个人微信 jihuan900 16 | * @微信公众号 猿天地 17 | * @GitHub https://github.com/yinjihuan 18 | * @作者介绍 http://cxytiandi.com/about 19 | * @时间 2022-05-24 22:59 20 | */ 21 | @FeignClient(name = "fox-mock-example", url = "http://localhost:8080") 22 | public interface FeignApi { 23 | 24 | @RequestMapping(value = "/getUserInfo", method = RequestMethod.GET) 25 | UserInfo getUserInfo(@RequestParam("id")Long id); 26 | 27 | @RequestMapping(value = "/getUserDetail", method = RequestMethod.GET) 28 | Result getUserDetail(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/UserDetail.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | 6 | /** 7 | * @作者 尹吉欢 8 | * @个人微信 jihuan900 9 | * @微信公众号 猿天地 10 | * @GitHub https://github.com/yinjihuan 11 | * @作者介绍 http://cxytiandi.com/about 12 | * @时间 2022-04-28 21:54 13 | */ 14 | public class UserDetail implements Serializable { 15 | 16 | private String name; 17 | 18 | private Map addressMap; 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setAddressMap(Map addressMap) { 29 | this.addressMap = addressMap; 30 | } 31 | 32 | public Map getAddressMap() { 33 | return addressMap; 34 | } 35 | 36 | public static class UserAddress implements Serializable { 37 | private String address; 38 | 39 | public void setAddress(String address) { 40 | this.address = address; 41 | } 42 | 43 | public String getAddress() { 44 | return address; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import java.net.HttpURLConnection; 6 | import java.net.URL; 7 | import java.util.Objects; 8 | 9 | public class HttpUtils { 10 | 11 | private static final Logger LOG = LoggerFactory.getLogger(HttpUtils.class); 12 | 13 | public static String get(String url) { 14 | HttpURLConnection connection = null; 15 | try { 16 | URL getUrl = new URL(url); 17 | connection = (HttpURLConnection) getUrl.openConnection(); 18 | connection.setRequestMethod("GET"); 19 | connection.setRequestProperty("Accept", "*/*"); 20 | connection.setRequestProperty("User-Agent", 21 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; CIBA)"); 22 | connection.setRequestProperty("Accept-Language", "zh-cn"); 23 | connection.connect(); 24 | return IOUtils.toString(connection.getInputStream(), "UTF-8"); 25 | } catch (Exception e) { 26 | LOG.error("http request exception, url is {}", url, e); 27 | } finally { 28 | if (Objects.nonNull(connection)) { 29 | connection.disconnect(); 30 | } 31 | } 32 | 33 | return null; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Method; 5 | import java.lang.reflect.Type; 6 | import java.util.Objects; 7 | 8 | /** 9 | * @作者 尹吉欢 10 | * @个人微信 jihuan900 11 | * @微信公众号 猿天地 12 | * @GitHub https://github.com/yinjihuan 13 | * @作者介绍 http://cxytiandi.com/about 14 | * @时间 2022-05-14 22:37 15 | */ 16 | public class ReflectionUtils { 17 | 18 | public static Type getGenericReturnType(String className, String methodName) { 19 | Type genericReturnType = null; 20 | 21 | Class clazz = ClassUtils.forNameByFormat(className); 22 | if (Objects.isNull(clazz)) { 23 | return null; 24 | } 25 | 26 | Method[] declaredMethods = clazz.getDeclaredMethods(); 27 | for (Method m: declaredMethods) { 28 | if (m.getName().equals(methodName)){ 29 | genericReturnType = m.getGenericReturnType(); 30 | break; 31 | } 32 | } 33 | 34 | return genericReturnType; 35 | } 36 | 37 | public static Object getFieldValue(String fieldName, Class clz, Object target) throws Throwable { 38 | Field field = clz.getDeclaredField(fieldName); 39 | field.setAccessible(true); 40 | return field.get(target); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import com.google.gson.Gson; 4 | import java.lang.reflect.Type; 5 | 6 | /** 7 | * JSON转换工具类,由于javassist不支持泛型,所以采用了一个折中的方式,在这里处理好带泛型的对象 8 | * 9 | * @作者 尹吉欢 10 | * @个人微信 jihuan900 11 | * @微信公众号 猿天地 12 | * @GitHub https://github.com/yinjihuan 13 | * @作者介绍 http://cxytiandi.com/about 14 | * @时间 2022-05-06 21:20 15 | */ 16 | public class JsonUtils { 17 | 18 | private static Gson gson = new Gson(); 19 | 20 | private JsonUtils() {} 21 | 22 | public static Object parse(String data, String className, String methodName) { 23 | try { 24 | Type genericReturnType = ReflectionUtils.getGenericReturnType(className, methodName); 25 | Object value = gson.fromJson(data, genericReturnType); 26 | return value; 27 | } catch (Exception e) { 28 | throw new RuntimeException(e); 29 | } 30 | } 31 | 32 | public static Object parseByType(String data, Type genericReturnType) { 33 | try { 34 | Object value = gson.fromJson(data, genericReturnType); 35 | return value; 36 | } catch (Exception e) { 37 | throw new RuntimeException(e); 38 | } 39 | } 40 | 41 | public static String toJson(Object src) { 42 | return gson.toJson(src); 43 | } 44 | 45 | public static T fromJson(String json, Class classOfT) { 46 | return gson.fromJson(json, classOfT); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fox-mock-boot/src/main/java/com/cxytiandi/foxmock/boot/config/Config.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.boot.config; 2 | 3 | 4 | import com.cxytiandi.foxmock.boot.utils.PathUtils; 5 | 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.FileNotFoundException; 9 | import java.io.InputStreamReader; 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.Properties; 12 | 13 | public class Config { 14 | 15 | private static Properties config; 16 | 17 | public static void initializeCoreConfig() { 18 | config = new Properties(); 19 | 20 | try (final InputStreamReader configFileStream = loadConfig()) { 21 | config.load(configFileStream); 22 | } catch (Exception e) { 23 | e.printStackTrace(); 24 | } 25 | } 26 | 27 | private static InputStreamReader loadConfig() { 28 | File configFile = new File(PathUtils.getAgentPath(), "agent.properties"); 29 | 30 | if (configFile.exists() && configFile.isFile()) { 31 | try { 32 | return new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8); 33 | } catch (FileNotFoundException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | throw new RuntimeException("Failed to load agent.config"); 39 | } 40 | 41 | public static Properties getConfig() { 42 | if (null == config) { 43 | initializeCoreConfig(); 44 | } 45 | 46 | return config; 47 | } 48 | 49 | 50 | } -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/UserService.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * @作者 尹吉欢 8 | * @个人微信 jihuan900 9 | * @微信公众号 猿天地 10 | * @GitHub https://github.com/yinjihuan 11 | * @作者介绍 http://cxytiandi.com/about 12 | * @时间 2022-04-20 00:25 13 | */ 14 | public class UserService { 15 | public UserInfo getName2(UserReq req) { 16 | return new UserInfo(); 17 | } 18 | 19 | public Integer getAge() { 20 | return 0; 21 | } 22 | 23 | public List getAddrs() { 24 | return new ArrayList<>(); 25 | } 26 | 27 | public List getUsers() { 28 | return new ArrayList<>(); 29 | } 30 | 31 | public Result> getUsers2() { 32 | Result> result = new Result<>(); 33 | result.setData(new ArrayList<>()); 34 | return result; 35 | } 36 | 37 | public Result getUser() { 38 | Result result = new Result<>(); 39 | return result; 40 | } 41 | 42 | public Result getUserDetail() { 43 | Result result = new Result<>(); 44 | return result; 45 | } 46 | 47 | public void mockException(String name) { 48 | 49 | } 50 | 51 | public static class UserReq { 52 | private int id; 53 | 54 | public void setId(int id) { 55 | this.id = id; 56 | } 57 | 58 | public int getId() { 59 | return id; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/express/OgnlExpress.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.express; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import ognl.*; 6 | 7 | public class OgnlExpress implements Express { 8 | 9 | private static final MemberAccess MEMBER_ACCESS = new DefaultMemberAccess(true); 10 | private static final Logger logger = LoggerFactory.getLogger(OgnlExpress.class); 11 | private static final ObjectPropertyAccessor OBJECT_PROPERTY_ACCESSOR = new ObjectPropertyAccessor(); 12 | 13 | private Object bindObject; 14 | private final OgnlContext context; 15 | 16 | public OgnlExpress() { 17 | OgnlRuntime.setPropertyAccessor(Object.class, OBJECT_PROPERTY_ACCESSOR); 18 | context = new OgnlContext(); 19 | context.setMemberAccess(MEMBER_ACCESS); 20 | } 21 | 22 | @Override 23 | public Object get(String express) throws OgnlExpressException { 24 | try { 25 | return Ognl.getValue(express, context, bindObject); 26 | } catch (Exception e) { 27 | logger.error("Error during evaluating the expression:", e); 28 | throw new OgnlExpressException(express, e); 29 | } 30 | } 31 | 32 | @Override 33 | public boolean is(String express) throws OgnlExpressException { 34 | final Object ret = get(express); 35 | return ret instanceof Boolean && (Boolean) ret; 36 | } 37 | 38 | @Override 39 | public Express bind(Object object) { 40 | this.bindObject = object; 41 | return this; 42 | } 43 | 44 | @Override 45 | public Express bind(String name, Object value) { 46 | context.put(name, value); 47 | return this; 48 | } 49 | 50 | @Override 51 | public Express reset() { 52 | context.clear(); 53 | context.setMemberAccess(MEMBER_ACCESS); 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fox-mock 2 | 3 | fox-mock 是基于Java Agent实现的自测,联调Mock利器。能解决你的这些问题: 4 | 5 | - 开发过程中,依赖了下游多个接口,想跑个单测都必须得等下游把服务部署好 6 | - 联调过程中,下游某个接口出问题,阻塞了整个流程 7 | - 其他需要Mock方法返回值的场景 8 | 9 | 10 | 优点: 11 | 12 | - 无侵入式的Mock解决方案,支持应用启动前挂载和应用启动后attach挂载。 13 | - 支持本地文件mock 14 | - 支持对接配置中心管理mock数据 15 | 16 | # Quick start 17 | 18 | ## 下载包 19 | 下载fox-mock包,fox-mock 支持在 Linux/Unix/Mac 等平台上一键下载,请复制以下内容,并粘贴到命令行中,敲 回车 执行即可: 20 | 21 | ``` 22 | curl -L http://file.cxytiandi.com/install.sh | sh 23 | ``` 24 | 25 | 如果是Windows可以自行通过http://file.cxytiandi.com/foxmock.zip 进行下载,浏览器访问即可。 26 | 27 | 下载完成后会在当前目录有一个foxmock的文件夹,文件夹里面包含了fox-mock的包。文件夹内容如下: 28 | - fox-mock-agent-${version}.jar 核心代码 29 | - fox-mock-boot-jar-with-dependencies.jar attach启动程序 30 | - agent.properties 配置文件 31 | - start.sh attach启动脚本 32 | 33 | ## agent启动挂载使用 34 | 在服务器上,需要在程序启动参数中添加下面的参数: 35 | 36 | ``` 37 | java -javaagent:fox-mock包的路径/fox-mock-agent-${version}.jar=foxMockFilePath=mock数据文件的路径 -jar 你的jar 38 | ``` 39 | 40 | 如果是在开发工具中,也需要将-javaagent加入到启动类的vm options中。 41 | 42 | 接下来就是要创建mock文件了,mock数据文件格式为方法的全路径,格式为com.xx.xxService#getName 43 | 44 | 在IDEA中直接选中方法单击右键,选中Copy Reference即可,这个就是mock的文件名。 45 | 46 | ![](static/mockdatafile.png) 47 | 48 | 文件内容就是这个方法要返回的数据,基本类型直接写内容即可。如果是对象需要用json格式。可以参考fox-mock-example中的mockdata文件夹下的示列。 49 | 50 | 51 | ## attach挂载使用 52 | 53 | 进入到下载好的文件夹中,执行./start.sh,执行之前请确保存在JAVA_HOME的环境变量。 54 | 55 | 执行之后会提示选择要attach的进程ID, 输入数字按回车即可。然后就完成了mock动作。 56 | 57 | attach之前需要将mock的文件路径在agent.properties中指定。 58 | 59 | # 文档 60 | 61 | - [本地单测时的数据mock方式](http://cxytiandi.com/blog/detail/36611) 62 | - [测试环境联调时的数据mock方式](http://cxytiandi.com/blog/detail/36612) 63 | - [对接配置中心管理mock数据](http://cxytiandi.com/blog/detail/36614) 64 | - [mock指定的异常](http://cxytiandi.com/blog/detail/36617) 65 | - [使用ognl表达式mock指定场景](http://cxytiandi.com/blog/detail/36618) 66 | - [接口(interface)如何mock](http://cxytiandi.com/blog/detail/36621) 67 | 68 | # 视频讲解 69 | 70 | - [Java 程序员的福音,mock神器它来啦!再也不用为自测,联调而烦恼啦!](https://www.bilibili.com/video/BV1WS4y1h76q) 71 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/model/ClassInfo.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.model; 2 | 3 | import com.cxytiandi.foxmock.agent.utils.ClassUtils; 4 | import javassist.ClassPool; 5 | import javassist.CtClass; 6 | import javassist.LoaderClassPath; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.IOException; 10 | 11 | 12 | public class ClassInfo { 13 | private final String className; 14 | private final byte[] classFileBuffer; 15 | private final ClassLoader loader; 16 | 17 | 18 | public ClassInfo(String className, byte[] classFileBuffer, ClassLoader loader) { 19 | this.className = ClassUtils.formatClassName(className); 20 | this.classFileBuffer = classFileBuffer; 21 | this.loader = loader; 22 | } 23 | 24 | public String getClassName() { 25 | return className; 26 | } 27 | 28 | private CtClass ctClass; 29 | 30 | public CtClass getCtClass() throws IOException { 31 | if (ctClass != null) { 32 | return ctClass; 33 | } 34 | 35 | final ClassPool classPool = new ClassPool(true); 36 | if (loader == null) { 37 | classPool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader())); 38 | } else { 39 | classPool.appendClassPath(new LoaderClassPath(loader)); 40 | } 41 | 42 | final CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classFileBuffer), false); 43 | clazz.defrost(); 44 | 45 | this.ctClass = clazz; 46 | return clazz; 47 | } 48 | 49 | private boolean modified = false; 50 | 51 | public boolean isModified() { 52 | return modified; 53 | } 54 | 55 | public void setModified() { 56 | this.modified = true; 57 | } 58 | 59 | public ClassLoader getClassLoader() { 60 | return loader; 61 | } 62 | 63 | public byte[] getClassFileBuffer() { 64 | return classFileBuffer; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/transformer/DubboInvokeFilter.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.transformer; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.factory.MockInfoFactory; 6 | import com.cxytiandi.foxmock.agent.model.MockInfo; 7 | import com.cxytiandi.foxmock.agent.storage.StorageHelper; 8 | import com.cxytiandi.foxmock.agent.utils.JsonUtils; 9 | import com.cxytiandi.foxmock.agent.utils.ReflectionUtils; 10 | import org.apache.dubbo.rpc.*; 11 | 12 | import java.lang.reflect.Type; 13 | import java.util.Objects; 14 | 15 | /** 16 | * @作者 尹吉欢 17 | * @个人微信 jihuan900 18 | * @微信公众号 猿天地 19 | * @GitHub https://github.com/yinjihuan 20 | * @作者介绍 http://cxytiandi.com/about 21 | * @时间 2022-05-18 22:42 22 | */ 23 | public class DubboInvokeFilter { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(DubboInvokeFilter.class); 26 | 27 | public static Result invoke(Object[] args) throws RpcException { 28 | Invoker invoker = (Invoker) args[0]; 29 | Invocation invocation = (Invocation) args[1]; 30 | String className = invoker.getInterface().getName(); 31 | String methodName = invocation.getMethodName(); 32 | String key = String.format("%s#%s", className, methodName); 33 | String data = StorageHelper.get(key); 34 | if (Objects.nonNull(data)) { 35 | MockInfo mockInfo = MockInfoFactory.create(data); 36 | boolean filter = MethodInvokeFilter.filter(invocation.getArguments(), mockInfo.getOgnlExpress()); 37 | if (filter) { 38 | LOG.info(String.format("mock methods %s, mock data is %s", key, data)); 39 | Type genericReturnType = ReflectionUtils.getGenericReturnType(className, methodName); 40 | Object value = JsonUtils.parseByType(mockInfo.getMockData(), genericReturnType); 41 | return AsyncRpcResult.newDefaultAsyncResult(value, invocation); 42 | } 43 | } 44 | 45 | return null; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /fox-mock-boot/src/main/java/com/cxytiandi/foxmock/boot/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.boot; 2 | 3 | 4 | import com.cxytiandi.foxmock.boot.config.Config; 5 | import com.cxytiandi.foxmock.boot.utils.PathUtils; 6 | import com.cxytiandi.foxmock.boot.utils.ProcessUtils; 7 | import com.sun.tools.attach.VirtualMachine; 8 | 9 | import java.io.File; 10 | import java.util.InputMismatchException; 11 | import java.util.Objects; 12 | import java.util.Properties; 13 | 14 | /** 15 | * @作者 尹吉欢 16 | * @个人微信 jihuan900 17 | * @微信公众号 猿天地 18 | * @GitHub https://github.com/yinjihuan 19 | * @作者介绍 http://cxytiandi.com/about 20 | * @时间 2022-04-23 21:26 21 | */ 22 | public class Bootstrap { 23 | 24 | public static void main(String[] args) { 25 | try { 26 | Properties config = Config.getConfig(); 27 | 28 | String foxMockFilePath = config.getProperty("foxMockFilePath", ""); 29 | String foxMockAgentJarPath = config.getProperty("foxMockAgentJarPath"); 30 | String mockMethodWhiteList = config.getProperty("mockMethodWhiteList", ""); 31 | String mockDataHttpUrl = config.getProperty("mockDataHttpUrl", ""); 32 | 33 | if (Objects.isNull(foxMockAgentJarPath)) { 34 | foxMockAgentJarPath = PathUtils.getAgentPath() + File.separator + "fox-mock-agent-7.0.jar"; 35 | } 36 | 37 | VirtualMachine attach = VirtualMachine.attach(String.valueOf(getPid())); 38 | String agentArgs = String.format("%s=foxMockFilePath=%s,mockMethodWhiteList=%s,mockDataHttpUrl=%s", foxMockAgentJarPath, foxMockFilePath, mockMethodWhiteList, mockDataHttpUrl); 39 | attach.loadAgent(agentArgs); 40 | attach.detach(); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | System.out.println("attach exception"); 44 | } 45 | } 46 | 47 | private static long getPid() { 48 | long pid = -1; 49 | try { 50 | pid = ProcessUtils.select(false, 0, null); 51 | } catch (InputMismatchException e) { 52 | System.out.println("Please input an integer to select pid."); 53 | System.exit(1); 54 | } 55 | if (pid < 0) { 56 | System.out.println("Please select an available pid."); 57 | System.exit(1); 58 | } 59 | return pid; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/storage/StorageHelper.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.storage; 2 | 3 | import com.cxytiandi.foxmock.agent.constant.FoxMockConstant; 4 | import com.cxytiandi.foxmock.agent.model.FoxMockAgentArgs; 5 | 6 | import java.util.*; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 存储帮助类 11 | * 12 | * @作者 尹吉欢 13 | * @个人微信 jihuan900 14 | * @微信公众号 猿天地 15 | * @GitHub https://github.com/yinjihuan 16 | * @作者介绍 http://cxytiandi.com/about 17 | * @时间 2022-04-19 22:08 18 | */ 19 | public class StorageHelper { 20 | 21 | static List storageList = new ArrayList<>(); 22 | 23 | static { 24 | storageList.add(new LocalFileStorage()); 25 | storageList.add(new HttpStorage()); 26 | } 27 | 28 | static Set allMockClassNames = new HashSet<>(); 29 | 30 | /** 31 | * 加载所有数据 32 | * @param request 33 | * @return 34 | */ 35 | public static boolean loadAllData(FoxMockAgentArgs request) { 36 | int successCount = 0; 37 | for (Storage storage : storageList) { 38 | boolean result = storage.loadData(request); 39 | if (result) { 40 | successCount++; 41 | } 42 | } 43 | return successCount > 0; 44 | } 45 | 46 | /** 47 | * 获取mock数据 48 | * @param key 格式:方法全路径 com.xx.User#getName 49 | * @return 50 | */ 51 | public static String get(String key) { 52 | for (Storage storage : storageList) { 53 | String data = storage.get(key); 54 | if (Objects.nonNull(data)) { 55 | return data; 56 | } 57 | } 58 | return null; 59 | } 60 | 61 | /** 62 | * 获取mock的类名称 63 | * @return 64 | */ 65 | public static Set getMockClassNames() { 66 | Set mockClassNames = storageList.stream().map(storage -> { 67 | return storage.getMockClassNames(); 68 | }).flatMap(Set::stream).collect(Collectors.toSet()); 69 | 70 | allMockClassNames.addAll(mockClassNames); 71 | allMockClassNames.add(FoxMockConstant.IBATIS_BASE_EXECUTOR); 72 | allMockClassNames.add(FoxMockConstant.IBATIS_CACHING_EXECUTOR); 73 | allMockClassNames.add(FoxMockConstant.DUBBO_CONSUMER_FILTER); 74 | allMockClassNames.add(FoxMockConstant.FEIGN_METHOD_HANDLER); 75 | return allMockClassNames; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/feign/FeignInvokeFilter.java: -------------------------------------------------------------------------------- 1 | package feign; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.factory.MockInfoFactory; 6 | import com.cxytiandi.foxmock.agent.model.MockInfo; 7 | import com.cxytiandi.foxmock.agent.storage.StorageHelper; 8 | import com.cxytiandi.foxmock.agent.transformer.MethodInvokeFilter; 9 | import com.cxytiandi.foxmock.agent.utils.JsonUtils; 10 | import com.cxytiandi.foxmock.agent.utils.ReflectionUtils; 11 | import java.lang.reflect.Type; 12 | import java.util.Objects; 13 | 14 | /** 15 | * @作者 尹吉欢 16 | * @个人微信 jihuan900 17 | * @微信公众号 猿天地 18 | * @GitHub https://github.com/yinjihuan 19 | * @作者介绍 http://cxytiandi.com/about 20 | * @时间 2022-05-24 23:07 21 | */ 22 | public class FeignInvokeFilter { 23 | 24 | private static final Logger LOG = LoggerFactory.getLogger(FeignInvokeFilter.class); 25 | 26 | public static Object invoke(Object[] argsArray, Object methodHandler) throws Throwable { 27 | Object[] args = (Object[]) argsArray[0]; 28 | SynchronousMethodHandler handler = (SynchronousMethodHandler) methodHandler; 29 | Class handlerClass = handler.getClass(); 30 | 31 | MethodMetadata methodMetadata = (MethodMetadata) ReflectionUtils.getFieldValue("metadata", handlerClass, handler); 32 | Target target = (Target) ReflectionUtils.getFieldValue("target", handlerClass, handler);; 33 | 34 | String className = target.type().getName(); 35 | String ms = methodMetadata.configKey().split("#")[1]; 36 | String methodName = ms.substring(0, ms.indexOf("(")); 37 | 38 | String key = String.format("%s#%s", className, methodName); 39 | String data = StorageHelper.get(key); 40 | 41 | if (Objects.nonNull(data)) { 42 | MockInfo mockInfo = MockInfoFactory.create(data); 43 | boolean filter = MethodInvokeFilter.filter(args, mockInfo.getOgnlExpress()); 44 | if (filter) { 45 | LOG.info(String.format("mock methods %s, mock data is %s", key, data)); 46 | Type genericReturnType = methodMetadata.returnType(); 47 | Object value = JsonUtils.parseByType(mockInfo.getMockData(), genericReturnType); 48 | return value; 49 | } 50 | } 51 | 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/model/FoxMockAgentArgs.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.model; 2 | 3 | import com.cxytiandi.foxmock.agent.utils.StringUtils; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | import java.net.URLDecoder; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | /** 12 | * agent参数 13 | * 14 | * @作者 尹吉欢 15 | * @个人微信 jihuan900 16 | * @微信公众号 猿天地 17 | * @GitHub https://github.com/yinjihuan 18 | * @作者介绍 http://cxytiandi.com/about 19 | * @时间 2022-04-24 22:41 20 | */ 21 | public class FoxMockAgentArgs { 22 | 23 | 24 | /** 25 | * mock文件路径 26 | */ 27 | private String foxMockFilePath; 28 | 29 | /** 30 | * mock方法白名单,如果文件夹中有多个方法会被全部mock,如果指定了此配置将只会mock这里指定的方法 31 | * 格式: com.xx.xxService#getName|com.xx.xxService#getAge 32 | */ 33 | private List mockMethodWhiteList = new ArrayList<>(); 34 | 35 | /** 36 | * mock数据通过http请求获取 37 | */ 38 | private String mockDataHttpUrl; 39 | 40 | public FoxMockAgentArgs(String agentArgs) { 41 | parseArgs(agentArgs); 42 | } 43 | 44 | private void parseArgs(String agentArgs) { 45 | if (!StringUtils.isBlank(agentArgs)) { 46 | String[] args = agentArgs.split(","); 47 | for (String arg : args) { 48 | String[] values = arg.split("="); 49 | if (values.length != 2) { 50 | continue; 51 | } 52 | String key = values[0]; 53 | String value = values[1]; 54 | if ("foxMockFilePath".equals(key)) { 55 | this.foxMockFilePath = value; 56 | } 57 | if ("mockMethodWhiteList".equals(key)) { 58 | this.mockMethodWhiteList = new ArrayList<>(Arrays.asList(value.split("\\|"))); 59 | } 60 | if ("mockDataHttpUrl".equals(key)) { 61 | this.mockDataHttpUrl = decodeUrl(value); 62 | } 63 | } 64 | } 65 | } 66 | 67 | private String decodeUrl(String url) { 68 | try { 69 | return URLDecoder.decode(url, "utf-8"); 70 | } catch (UnsupportedEncodingException e) { 71 | throw new RuntimeException(e); 72 | } 73 | } 74 | 75 | public String getFoxMockFilePath() { 76 | return foxMockFilePath; 77 | } 78 | 79 | public List getMockMethodWhiteList() { 80 | return mockMethodWhiteList; 81 | } 82 | 83 | public String getMockDataHttpUrl() { 84 | return mockDataHttpUrl; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/storage/HttpStorage.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.storage; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.model.FoxMockAgentArgs; 6 | import com.cxytiandi.foxmock.agent.utils.HttpUtils; 7 | import com.cxytiandi.foxmock.agent.utils.MD5; 8 | import com.cxytiandi.foxmock.agent.utils.StringUtils; 9 | 10 | import java.io.IOException; 11 | import java.io.StringReader; 12 | import java.util.Map; 13 | import java.util.Properties; 14 | import java.util.Set; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * Http 请求获取mock数据 20 | * 21 | * @作者 尹吉欢 22 | * @个人微信 jihuan900 23 | * @微信公众号 猿天地 24 | * @GitHub https://github.com/yinjihuan 25 | * @作者介绍 http://cxytiandi.com/about 26 | * @时间 2022-04-29 21:31 27 | */ 28 | public class HttpStorage implements Storage { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(HttpStorage.class); 31 | 32 | private static Map mockData = new ConcurrentHashMap<>(); 33 | 34 | private static String cacheDataMD5 = ""; 35 | 36 | @Override 37 | public boolean loadData(FoxMockAgentArgs request) { 38 | if (StringUtils.isBlank(request.getMockDataHttpUrl())) { 39 | return false; 40 | } 41 | 42 | String mockDataText = HttpUtils.get(request.getMockDataHttpUrl()); 43 | 44 | if (StringUtils.isBlank(mockDataText)) { 45 | return false; 46 | } 47 | 48 | String dataMD5 = MD5.getInstance().getMD5String(mockDataText); 49 | if (StringUtils.isBlank(cacheDataMD5)) { 50 | cacheDataMD5 = dataMD5; 51 | } else { 52 | // 没有变动无需重新mock 53 | if (dataMD5.equals(cacheDataMD5)) { 54 | return false; 55 | } 56 | cacheDataMD5 = dataMD5; 57 | } 58 | 59 | // attach的场景会load多次,添加之前需要清空,否则如果文件有改动,则无法卸载掉之前的mock 60 | mockData.clear(); 61 | 62 | Properties properties = new Properties(); 63 | try { 64 | properties.load(new StringReader(mockDataText)); 65 | Set keys = properties.stringPropertyNames(); 66 | for (String key : keys) { 67 | String value = properties.getProperty(key); 68 | mockData.put(key, value); 69 | } 70 | } catch (IOException e) { 71 | LOG.error("loadData IOException, url is {}", request.getMockDataHttpUrl(), e); 72 | } catch (Exception e) { 73 | LOG.error("loadData Exception, url is {}", request.getMockDataHttpUrl(), e); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | @Override 80 | public String get(String key) { 81 | return mockData.get(key); 82 | } 83 | 84 | @Override 85 | public Set getMockClassNames() { 86 | return mockData.keySet().stream().map(key -> key.split("#")[0]).collect(Collectors.toSet()); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/utils/MD5.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.utils; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.security.MessageDigest; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | 10 | public class MD5 { 11 | private static int DIGITS_SIZE = 16; 12 | private static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 13 | 14 | private static Map rDigits = new HashMap<>(16); 15 | 16 | static { 17 | for (int i = 0; i < digits.length; ++i) { 18 | rDigits.put(digits[i], i); 19 | } 20 | } 21 | 22 | private static MD5 me = new MD5(); 23 | private MessageDigest mHasher; 24 | private ReentrantLock opLock = new ReentrantLock(); 25 | 26 | private MD5() { 27 | try { 28 | mHasher = MessageDigest.getInstance("md5"); 29 | } catch (Exception e) { 30 | throw new RuntimeException(e); 31 | } 32 | 33 | } 34 | 35 | public static MD5 getInstance() { 36 | return me; 37 | } 38 | 39 | public String getMD5String(String content) { 40 | return bytes2string(hash(content)); 41 | } 42 | 43 | public String getMD5String(byte[] content) { 44 | return bytes2string(hash(content)); 45 | } 46 | 47 | public byte[] getMD5Bytes(byte[] content) { 48 | return hash(content); 49 | } 50 | 51 | /** 52 | * 对字符串进行md5 53 | * 54 | * @param str 55 | * @return md5 byte[16] 56 | */ 57 | public byte[] hash(String str) { 58 | opLock.lock(); 59 | try { 60 | byte[] bt = mHasher.digest(str.getBytes("UTF-8")); 61 | if (null == bt || bt.length != DIGITS_SIZE) { 62 | throw new IllegalArgumentException("md5 need"); 63 | } 64 | return bt; 65 | } catch (UnsupportedEncodingException e) { 66 | throw new RuntimeException("unsupported utf-8 encoding", e); 67 | } finally { 68 | opLock.unlock(); 69 | } 70 | } 71 | 72 | /** 73 | * 对二进制数据进行md5 74 | * 75 | * @param data 76 | * @return md5 byte[16] 77 | */ 78 | public byte[] hash(byte[] data) { 79 | opLock.lock(); 80 | try { 81 | byte[] bt = mHasher.digest(data); 82 | if (null == bt || bt.length != DIGITS_SIZE) { 83 | throw new IllegalArgumentException("md5 need"); 84 | } 85 | return bt; 86 | } finally { 87 | opLock.unlock(); 88 | } 89 | } 90 | 91 | /** 92 | * 将一个字节数组转化为可见的字符串 93 | * 94 | * @param bt 95 | * @return 96 | */ 97 | public String bytes2string(byte[] bt) { 98 | int l = bt.length; 99 | 100 | char[] out = new char[l << 1]; 101 | 102 | for (int i = 0, j = 0; i < l; i++) { 103 | out[j++] = digits[(0xF0 & bt[i]) >>> 4]; 104 | out[j++] = digits[0x0F & bt[i]]; 105 | } 106 | 107 | return new String(out); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /fox-mock-example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fox-mock 7 | com.cxytiandi 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | fox-mock-example 13 | 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-dependencies 19 | 2.3.1.RELEASE 20 | pom 21 | import 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-dependencies 26 | Greenwich.SR2 27 | pom 28 | import 29 | 30 | 31 | 32 | 33 | 34 | 35 | com.google.code.gson 36 | gson 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | 45 | org.mybatis.spring.boot 46 | mybatis-spring-boot-starter 47 | 2.1.4 48 | 49 | 50 | mysql 51 | mysql-connector-java 52 | 53 | 54 | org.projectlombok 55 | lombok 56 | 57 | 58 | 59 | 60 | org.apache.dubbo 61 | dubbo-spring-boot-starter 62 | 2.7.1 63 | 64 | 65 | org.apache.dubbo 66 | dubbo 67 | 68 | 69 | org.apache.dubbo 70 | dubbo-registry-nacos 71 | 2.7.4.1 72 | 73 | 74 | com.alibaba.nacos 75 | nacos-client 76 | 1.1.4 77 | 78 | 79 | 80 | 81 | org.springframework.cloud 82 | spring-cloud-starter-openfeign 83 | 84 | 85 | com.alibaba.cloud 86 | spring-cloud-starter-alibaba-sentinel 87 | 2.1.0.RELEASE 88 | 89 | 90 | -------------------------------------------------------------------------------- /fox-mock-boot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fox-mock 7 | com.cxytiandi 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | fox-mock-boot 13 | 14 | 15 | 16 | com.alibaba.middleware 17 | cli 18 | 1.0.4 19 | 20 | 21 | com.taobao.arthas 22 | arthas-common 23 | 3.6.0 24 | 25 | 26 | 27 | com.github.olivergondza 28 | maven-jdk-tools-wrapper 29 | 0.1 30 | provided 31 | true 32 | 33 | 34 | 35 | 36 | fox-mock-boot 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-compiler-plugin 41 | 42 | 1.8 43 | 1.8 44 | UTF-8 45 | true 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-assembly-plugin 51 | 52 | 53 | 54 | single 55 | 56 | package 57 | 58 | 59 | jar-with-dependencies 60 | 61 | 62 | 63 | com.cxytiandi.foxmock.boot.Bootstrap 64 | 65 | 66 | yinjihuan 67 | ${project.name} 68 | ${project.version} 69 | ${project.name} 70 | ${project.version} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.cxytiandi 8 | fox-mock 9 | pom 10 | 1.0-SNAPSHOT 11 | 12 | 13 | fox-mock-agent 14 | fox-mock-example 15 | fox-mock-boot 16 | 17 | 18 | 19 | 3.28.0-GA 20 | 2.8.6 21 | 0.0.10 22 | 1.7.36 23 | 1.2.11 24 | 1.2.11 25 | 3.1.19 26 | 3.5.6 27 | 2.7.3 28 | 10.2.3 29 | 30 | 31 | 32 | 33 | 34 | org.javassist 35 | javassist 36 | ${javassist.version} 37 | 38 | 39 | 40 | com.google.code.gson 41 | gson 42 | ${gson.version} 43 | 44 | 45 | 46 | 47 | com.alibaba.arthas 48 | arthas-repackage-logger 49 | ${arthas-repackage-logger.version} 50 | 51 | 52 | org.slf4j 53 | slf4j-api 54 | ${slf4j-api.version} 55 | 56 | 57 | ch.qos.logback 58 | logback-classic 59 | ${logback-classic.version} 60 | 61 | 62 | ch.qos.logback 63 | logback-core 64 | ${logback-core.version} 65 | 66 | 67 | ognl 68 | ognl 69 | ${ognl.version} 70 | 71 | 72 | org.mybatis 73 | mybatis 74 | ${mybatis.version} 75 | 76 | 77 | 78 | 79 | org.apache.dubbo 80 | dubbo 81 | ${dubbo.version} 82 | 83 | 84 | 85 | 86 | io.github.openfeign 87 | feign-core 88 | ${feign.version} 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/transformer/MethodInvokeFilter.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.transformer; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.express.Express; 6 | import com.cxytiandi.foxmock.agent.express.OgnlExpressException; 7 | import com.cxytiandi.foxmock.agent.express.ExpressFactory; 8 | import com.cxytiandi.foxmock.agent.factory.MockInfoFactory; 9 | import com.cxytiandi.foxmock.agent.model.MockInfo; 10 | import com.cxytiandi.foxmock.agent.storage.StorageHelper; 11 | import com.cxytiandi.foxmock.agent.utils.JsonUtils; 12 | import com.cxytiandi.foxmock.agent.utils.ReflectionUtils; 13 | import com.cxytiandi.foxmock.agent.utils.StringUtils; 14 | import org.apache.ibatis.mapping.BoundSql; 15 | import org.apache.ibatis.mapping.MappedStatement; 16 | import org.apache.ibatis.mapping.ParameterMapping; 17 | 18 | import java.lang.reflect.Type; 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Objects; 23 | 24 | /** 25 | * 方法执行过滤 26 | *

采用ognl进行mock方法的匹配,如果只想对指定参数进行mock就配置ognl表达式

27 | * 28 | * @作者 尹吉欢 29 | * @个人微信 jihuan900 30 | * @微信公众号 猿天地 31 | * @GitHub https://github.com/yinjihuan 32 | * @作者介绍 http://cxytiandi.com/about 33 | * @时间 2022-05-10 21:54 34 | */ 35 | public class MethodInvokeFilter { 36 | 37 | private static final Logger LOG = LoggerFactory.getLogger(MethodInvokeFilter.class); 38 | 39 | public static boolean filter(Object[] args, String express) { 40 | if (args == null || args.length == 0 || StringUtils.isBlank(express)) { 41 | return true; 42 | } 43 | 44 | Express ex = ExpressFactory.getExpress(args); 45 | for (int i = 0; i < args.length; i++) { 46 | ex.bind("p" + i, args[i]); 47 | } 48 | 49 | try { 50 | return ex.is(express); 51 | } catch (OgnlExpressException e) { 52 | LOG.error("ognl filter exception", e); 53 | } 54 | 55 | return false; 56 | } 57 | 58 | public static Object filterAndConvertDataByMybatisQuery(Object[] args) { 59 | Object obj = filterAndConvertData(args); 60 | if (obj == null) { 61 | return null; 62 | } 63 | 64 | // 集合直接返回 65 | if (obj instanceof List) { 66 | return obj; 67 | } 68 | 69 | // 非集合包装成集合返回,因为org.apache.ibatis.executor.BaseExecutor.query返回的是集合 70 | List objs = new ArrayList<>(); 71 | objs.add(obj); 72 | return objs; 73 | } 74 | 75 | public static Object filterAndConvertDataByMybatisUpdate(Object[] args) { 76 | return filterAndConvertData(args); 77 | } 78 | 79 | private static Object filterAndConvertData(Object[] args) { 80 | MappedStatement ms = (MappedStatement) args[0]; 81 | Object parameterObject = args[1]; 82 | 83 | String msId = ms.getId(); 84 | String className = msId.substring(0, msId.lastIndexOf(".")); 85 | String methodName = msId.substring(msId.lastIndexOf(".") + 1); 86 | String key = String.format("%s#%s", className, methodName); 87 | 88 | String data = StorageHelper.get(key); 89 | if (Objects.isNull(data)) { 90 | return null; 91 | } 92 | 93 | List argsList = new ArrayList<>(); 94 | BoundSql boundSql = ms.getBoundSql(parameterObject); 95 | List parameterMappings = boundSql.getParameterMappings(); 96 | 97 | for (ParameterMapping mapping : parameterMappings) { 98 | if (parameterObject instanceof HashMap) { 99 | HashMap parameterMap = (HashMap) parameterObject; 100 | Object value = parameterMap.get(mapping.getProperty()); 101 | argsList.add(value); 102 | } else { 103 | argsList.add(parameterObject); 104 | } 105 | } 106 | 107 | MockInfo mockInfo = MockInfoFactory.create(data); 108 | boolean filter = filter(argsList.toArray(new Object[argsList.size()]), mockInfo.getOgnlExpress()); 109 | if (filter) { 110 | LOG.info(String.format("mock methods %s, mock data is %s", key, data)); 111 | Type genericReturnType = ReflectionUtils.getGenericReturnType(className, methodName); 112 | Object obj = JsonUtils.parseByType(mockInfo.getMockData(), genericReturnType); 113 | return obj; 114 | } 115 | 116 | return null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /fox-mock-example/src/main/java/com/cxytiandi/foxmock/example/FoxMockApp.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.example; 2 | 3 | import com.cxytiandi.foxmock.example.dubbo.DubboApiTestService; 4 | import com.cxytiandi.foxmock.example.feign.FeignApiTestService; 5 | import com.cxytiandi.foxmock.example.mybatis.UserMapper; 6 | import com.cxytiandi.foxmock.example.mybatis.UserQuery; 7 | import com.google.gson.Gson; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.cloud.openfeign.EnableFeignClients; 11 | import org.springframework.transaction.annotation.EnableTransactionManagement; 12 | 13 | import java.util.Map; 14 | import java.util.Objects; 15 | 16 | /** 17 | * @作者 尹吉欢 18 | * @个人微信 jihuan900 19 | * @微信公众号 猿天地 20 | * @GitHub https://github.com/yinjihuan 21 | * @作者介绍 http://cxytiandi.com/about 22 | * @时间 2022-04-18 22:11 23 | */ 24 | @EnableFeignClients(basePackages = "com.cxytiandi.foxmock.example.feign") 25 | @EnableTransactionManagement 26 | @SpringBootApplication 27 | public class FoxMockApp { 28 | public static void main(String[] args) throws Exception { 29 | SpringApplication.run(FoxMockApp.class); 30 | 31 | while (true) { 32 | /* UserService userService = new UserService(); 33 | UserService.UserReq userReq = new UserService.UserReq(); 34 | userReq.setId(2); 35 | System.out.println(String.format("你好 getName2 %s", userService.getName2(userReq).getId())); 36 | System.out.println(String.format("你好 getAge %s", userService.getAge())); 37 | System.out.println(String.format("你好 getAddrs %s", userService.getAddrs())); 38 | System.out.println(String.format("你好 getUsers %s", userService.getUsers())); 39 | System.out.println(String.format("你好 getUsers2 %s", userService.getUsers2())); 40 | System.out.println(String.format("你好 getUser %s", userService.getUser())); 41 | System.out.println(String.format("你好 getUserDetail %s", userService.getUserDetail())); 42 | // 泛型测试 43 | Result userDetailResult = userService.getUserDetail(); 44 | UserDetail userDetail = userDetailResult.getData(); 45 | if (Objects.nonNull(userDetail)) { 46 | Map addressMap = userDetail.getAddressMap(); 47 | addressMap.forEach((k,v) -> { 48 | System.out.println(k + "\t" + v.getAddress()); 49 | }); 50 | } 51 | try { 52 | userService.mockException("yjh"); 53 | } catch (Exception e) { 54 | e.printStackTrace(); 55 | } 56 | 57 | UserMapper mapper = ApplicationContextHelper.getBean(UserMapper.class); 58 | System.out.println("mapper find:" + new Gson().toJson(mapper.find())); 59 | System.out.println("mapper find2:" + new Gson().toJson(mapper.find2(new UserQuery(1)))); 60 | System.out.println("mapper findById:" + new Gson().toJson(mapper.findById(1))); 61 | System.out.println("mapper findNameById:" + mapper.findNameById(1)); 62 | System.out.println("mapper updateNameById:" + mapper.updateNameById(1, "张三")); 63 | */ 64 | DubboApiTestService dubboApiTestService = ApplicationContextHelper.getBean(DubboApiTestService.class); 65 | System.out.println("dubbo getUserInfo:" + new Gson().toJson(dubboApiTestService.getUserInfo(1L))); 66 | Result userDetailResult1 = dubboApiTestService.getUserDetail(); 67 | System.out.println("dubbo getUserDetail:" + new Gson().toJson(userDetailResult1)); 68 | UserDetail userDetail1 = userDetailResult1.getData(); 69 | if (Objects.nonNull(userDetail1)) { 70 | Map addressMap = userDetail1.getAddressMap(); 71 | addressMap.forEach((k,v) -> { 72 | System.out.println(k + "\t" + v.getAddress()); 73 | }); 74 | } 75 | 76 | FeignApiTestService feignApiTestService = ApplicationContextHelper.getBean(FeignApiTestService.class); 77 | System.out.println("feign getUserInfo:" + new Gson().toJson(feignApiTestService.getUserInfo(1L))); 78 | userDetailResult1 = feignApiTestService.getUserDetail(); 79 | System.out.println("feign getUserDetail:" + new Gson().toJson(userDetailResult1)); 80 | userDetail1 = userDetailResult1.getData(); 81 | if (Objects.nonNull(userDetail1)) { 82 | Map addressMap = userDetail1.getAddressMap(); 83 | addressMap.forEach((k,v) -> { 84 | System.out.println(k + "\t" + v.getAddress()); 85 | }); 86 | } 87 | 88 | System.out.println("----------------------------------------"); 89 | Thread.sleep(5000); 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/FoxMockAgent.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.constant.FoxMockConstant; 6 | import com.cxytiandi.foxmock.agent.model.FoxMockAgentArgs; 7 | import com.cxytiandi.foxmock.agent.storage.StorageHelper; 8 | import com.cxytiandi.foxmock.agent.transformer.MockClassFileTransformer; 9 | 10 | import java.lang.instrument.Instrumentation; 11 | import java.lang.instrument.UnmodifiableClassException; 12 | import java.util.Set; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.ThreadFactory; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | /** 20 | * Mock 入口 21 | * 22 | * @作者 尹吉欢 23 | * @个人微信 jihuan900 24 | * @微信公众号 猿天地 25 | * @GitHub https://github.com/yinjihuan 26 | * @作者介绍 http://cxytiandi.com/about 27 | * @时间 2022-04-18 22:17 28 | */ 29 | public class FoxMockAgent { 30 | 31 | static { 32 | System.setProperty("arthas.logback.configurationFile", "foxmock-logback.xml"); 33 | } 34 | 35 | private static final Logger LOG = LoggerFactory.getLogger(FoxMockAgent.class); 36 | 37 | private static ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, new ThreadFactory() { 38 | @Override 39 | public Thread newThread(Runnable r) { 40 | Thread t = new Thread(r); 41 | t.setName("fox-mock.refresh.worker"); 42 | t.setDaemon(true); 43 | return t; 44 | } 45 | }); 46 | 47 | private static AtomicBoolean isFirstLoad = new AtomicBoolean(true); 48 | 49 | private static String foxMockAgentArgs; 50 | 51 | public static void premain(final String agentArgs, final Instrumentation inst) { 52 | try { 53 | foxMockAgentArgs = agentArgs; 54 | LOG.info(String.format("[FoxMockAgent.premain] begin, agentArgs: %s, Instrumentation:%s", agentArgs, inst)); 55 | 56 | main(agentArgs, inst); 57 | 58 | if (isFirstLoad.get()) { 59 | isFirstLoad.compareAndSet(true, false); 60 | executor.scheduleAtFixedRate(() -> { 61 | main(foxMockAgentArgs, inst); 62 | }, 10, 10, TimeUnit.SECONDS); 63 | } 64 | 65 | } catch (Exception e) { 66 | LOG.error("FoxMockAgent.premain exception", e); 67 | } 68 | } 69 | 70 | public static void agentmain(final String agentArgs, final Instrumentation inst) { 71 | try { 72 | foxMockAgentArgs = agentArgs; 73 | LOG.info(String.format("[FoxMockAgent.agentmain] begin, agentArgs: %s, Instrumentation:%s", agentArgs, inst)); 74 | 75 | main(agentArgs, inst); 76 | 77 | if (isFirstLoad.get()) { 78 | isFirstLoad.compareAndSet(true, false); 79 | executor.scheduleAtFixedRate(() -> { 80 | main(foxMockAgentArgs, inst); 81 | }, 10, 10, TimeUnit.SECONDS); 82 | } 83 | } catch (Exception e) { 84 | LOG.error("FoxMockAgent.agentmain exception", e); 85 | } 86 | } 87 | 88 | private synchronized static void main(final String agentArgs, final Instrumentation inst) { 89 | LOG.info("start load data"); 90 | boolean success = StorageHelper.loadAllData(new FoxMockAgentArgs(agentArgs)); 91 | if (!success) { 92 | LOG.info("load data fail"); 93 | return; 94 | } 95 | 96 | if (isFirstLoad.get()) { 97 | MockClassFileTransformer mockClassFileTransformer = new MockClassFileTransformer(); 98 | inst.addTransformer(mockClassFileTransformer, true); 99 | } 100 | 101 | preLoadClass(); 102 | 103 | Set mockClassNames = StorageHelper.getMockClassNames(); 104 | Class[] allLoadedClasses = inst.getAllLoadedClasses(); 105 | for (Class clz : allLoadedClasses) { 106 | if (mockClassNames.contains(clz.getName())) { 107 | try { 108 | LOG.info("retransformClass {}", clz.getName()); 109 | inst.retransformClasses(clz); 110 | } catch (UnmodifiableClassException e) { 111 | LOG.error("retransformClasses exception", e); 112 | throw new RuntimeException("retransformClasses exception", e); 113 | } 114 | } 115 | } 116 | 117 | LOG.info("foxMock agent run completely"); 118 | } 119 | 120 | private static void preLoadClass() { 121 | try { 122 | Class.forName(FoxMockConstant.IBATIS_BASE_EXECUTOR); 123 | } catch (ClassNotFoundException e) { 124 | LOG.warn("{} ClassNotFoundException", FoxMockConstant.IBATIS_BASE_EXECUTOR); 125 | } 126 | 127 | try { 128 | Class.forName(FoxMockConstant.IBATIS_CACHING_EXECUTOR); 129 | } catch (ClassNotFoundException e) { 130 | LOG.warn("{} ClassNotFoundException", FoxMockConstant.IBATIS_CACHING_EXECUTOR); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/storage/LocalFileStorage.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.storage; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.model.FoxMockAgentArgs; 6 | import com.cxytiandi.foxmock.agent.utils.CollectionUtils; 7 | import com.cxytiandi.foxmock.agent.utils.StringUtils; 8 | 9 | import java.io.IOException; 10 | import java.nio.charset.Charset; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.*; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.concurrent.atomic.AtomicBoolean; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * 本地文件存储 22 | * 23 | * @作者 尹吉欢 24 | * @个人微信 jihuan900 25 | * @微信公众号 猿天地 26 | * @GitHub https://github.com/yinjihuan 27 | * @作者介绍 http://cxytiandi.com/about 28 | * @时间 2022-04-18 23:46 29 | */ 30 | public class LocalFileStorage implements Storage { 31 | 32 | private static final Logger LOG = LoggerFactory.getLogger(LocalFileStorage.class); 33 | 34 | private static Map mockData = new ConcurrentHashMap<>(); 35 | private static Map mockDataLastModified = new ConcurrentHashMap<>(); 36 | private static List mockMethodWhiteList = new ArrayList<>(); 37 | 38 | @Override 39 | public boolean loadData(FoxMockAgentArgs request) { 40 | try { 41 | String fileDirectory = request.getFoxMockFilePath(); 42 | if (StringUtils.isBlank(fileDirectory)) { 43 | LOG.info("Can not find foxMockFilePath"); 44 | return false; 45 | } 46 | 47 | if (!whiteListIsModified(request) & !fileDirectoryIsModified(fileDirectory)) { 48 | return false; 49 | } 50 | 51 | // attach的场景会load多次,添加之前需要清空,否则如果文件有改动,则无法卸载掉之前的mock 52 | mockData.clear(); 53 | 54 | Files.list(Paths.get(fileDirectory)).forEach(path -> { 55 | String key = path.getFileName().toString(); 56 | if (request.getMockMethodWhiteList() != null && !request.getMockMethodWhiteList().isEmpty()) { 57 | if (request.getMockMethodWhiteList().contains(key)) { 58 | mockData.put(key, readFileContent(path)); 59 | } 60 | } else { 61 | mockData.put(key, readFileContent(path)); 62 | } 63 | }); 64 | 65 | } catch (IOException e) { 66 | LOG.error("loadData IOException", e); 67 | } 68 | 69 | return true; 70 | } 71 | 72 | @Override 73 | public String get(String key) { 74 | return mockData.get(key); 75 | } 76 | 77 | @Override 78 | public Set getMockClassNames() { 79 | return mockData.keySet().stream().map(key -> key.split("#")[0]).collect(Collectors.toSet()); 80 | } 81 | 82 | private String readFileContent(Path path) { 83 | try { 84 | String content = new String(Files.readAllBytes(path), Charset.forName("UTF-8")); 85 | return content; 86 | } catch (IOException e) { 87 | LOG.error(String.format("readFileContent IOException, path is {}", path.toString()), e); 88 | } 89 | return null; 90 | } 91 | 92 | private void initMockDataLastModified(String fileDirectory) { 93 | try { 94 | mockDataLastModified.clear(); 95 | 96 | Files.list(Paths.get(fileDirectory)).forEach(path -> { 97 | String key = path.getFileName().toString(); 98 | mockDataLastModified.put(key, path.toFile().lastModified()); 99 | }); 100 | } catch (IOException e) { 101 | LOG.error("loadData IOException", e); 102 | } 103 | } 104 | 105 | private boolean fileDirectoryIsModified(String fileDirectory) { 106 | try { 107 | if (!mockDataLastModified.isEmpty()) { 108 | AtomicBoolean isModified = new AtomicBoolean(false); 109 | AtomicInteger fileCount = new AtomicInteger(); 110 | 111 | Files.list(Paths.get(fileDirectory)).forEach(path -> { 112 | fileCount.incrementAndGet(); 113 | 114 | String key = path.getFileName().toString(); 115 | Long lastModified = mockDataLastModified.get(key); 116 | // 文件有改变 117 | if (Objects.isNull(lastModified) || !lastModified.equals(path.toFile().lastModified())) { 118 | isModified.compareAndSet(false, true); 119 | } 120 | }); 121 | 122 | // 文件有新增或者删除 123 | if (fileCount.get() != mockDataLastModified.size()) { 124 | isModified.compareAndSet(false, true); 125 | } 126 | 127 | // 文件夹内没有变化 128 | if (!isModified.get()) { 129 | return false; 130 | } else { 131 | initMockDataLastModified(fileDirectory); 132 | } 133 | 134 | } else { 135 | initMockDataLastModified(fileDirectory); 136 | } 137 | } catch (IOException e) { 138 | LOG.error("loadData IOException", e); 139 | } 140 | return true; 141 | } 142 | 143 | private boolean whiteListIsModified(FoxMockAgentArgs request) { 144 | if (CollectionUtils.isEquals(mockMethodWhiteList, request.getMockMethodWhiteList())) { 145 | return false; 146 | } else { 147 | if (request.getMockMethodWhiteList() != null) { 148 | mockMethodWhiteList.clear(); 149 | mockMethodWhiteList.addAll(request.getMockMethodWhiteList()); 150 | } 151 | return true; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /fox-mock-agent/src/main/java/com/cxytiandi/foxmock/agent/transformer/MockClassFileTransformer.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.agent.transformer; 2 | 3 | import com.alibaba.arthas.deps.org.slf4j.Logger; 4 | import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; 5 | import com.cxytiandi.foxmock.agent.constant.FoxMockConstant; 6 | import com.cxytiandi.foxmock.agent.factory.MockInfoFactory; 7 | import com.cxytiandi.foxmock.agent.model.ClassInfo; 8 | import com.cxytiandi.foxmock.agent.model.MockInfo; 9 | import com.cxytiandi.foxmock.agent.storage.StorageHelper; 10 | import com.cxytiandi.foxmock.agent.utils.JsonUtils; 11 | import com.cxytiandi.foxmock.agent.utils.StringUtils; 12 | import javassist.CannotCompileException; 13 | import javassist.CtClass; 14 | import javassist.CtMethod; 15 | 16 | import java.lang.instrument.ClassFileTransformer; 17 | import java.lang.instrument.IllegalClassFormatException; 18 | import java.security.ProtectionDomain; 19 | import java.util.HashSet; 20 | import java.util.Map; 21 | import java.util.Objects; 22 | import java.util.Set; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | 25 | /** 26 | * Mock 字节码增强处理 27 | * 28 | * @作者 尹吉欢 29 | * @个人微信 jihuan900 30 | * @微信公众号 猿天地 31 | * @GitHub https://github.com/yinjihuan 32 | * @作者介绍 http://cxytiandi.com/about 33 | * @时间 2022-04-18 22:50 34 | */ 35 | public class MockClassFileTransformer implements ClassFileTransformer { 36 | 37 | private static final Logger LOG = LoggerFactory.getLogger(MockClassFileTransformer.class); 38 | 39 | private static Map CLASS_INFO = new ConcurrentHashMap<>(); 40 | 41 | private static Set MYBATIS_MOCK_CLASS = new HashSet() 42 | { 43 | { 44 | add(FoxMockConstant.IBATIS_BASE_EXECUTOR); 45 | add(FoxMockConstant.IBATIS_CACHING_EXECUTOR); 46 | } 47 | }; 48 | 49 | @Override 50 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 51 | try { 52 | if (StringUtils.isBlank(className)) { 53 | return null; 54 | } 55 | 56 | ClassInfo classInfo = new ClassInfo(className, classfileBuffer, loader); 57 | 58 | // 缓存中获取没有增强之前的类的信息 59 | ClassInfo classInfoCache = CLASS_INFO.get(className); 60 | if (Objects.nonNull(classInfoCache)) { 61 | classInfo = new ClassInfo(classInfoCache.getClassName(), classInfoCache.getClassFileBuffer(), classInfoCache.getClassLoader()); 62 | } 63 | 64 | CtClass ctClass = classInfo.getCtClass(); 65 | ctClass.defrost(); 66 | 67 | if (ctClass.isInterface()) { 68 | return ctClass.toBytecode(); 69 | } 70 | 71 | CtMethod[] declaredMethods = ctClass.getDeclaredMethods(); 72 | 73 | boolean match = false; 74 | for (CtMethod method : declaredMethods) { 75 | String methodName = method.getName(); 76 | String key = String.format("%s#%s", classInfo.getClassName(), methodName); 77 | String data = StorageHelper.get(key); 78 | if (Objects.nonNull(data)) { 79 | match = true; 80 | LOG.info(String.format("mock methods %s, mock data is %s", key, data)); 81 | MockInfo mockInfo = MockInfoFactory.create(data); 82 | updateMethod(mockInfo, method, className, methodName); 83 | } 84 | } 85 | 86 | if (MYBATIS_MOCK_CLASS.contains(classInfo.getClassName())) { 87 | for (CtMethod method : declaredMethods) { 88 | String methodName = method.getName(); 89 | if (FoxMockConstant.IBATIS_MOCK_QUERY_METHOD.equals(methodName)) { 90 | LOG.info("mock mybatis query method"); 91 | method.insertBefore("Object data = com.cxytiandi.foxmock.agent.transformer.MethodInvokeFilter.filterAndConvertDataByMybatisQuery($args);if(java.util.Objects.nonNull(data)){return ($r)data;}"); 92 | } 93 | if (FoxMockConstant.IBATIS_MOCK_UPDATE_METHOD.equals(methodName)) { 94 | LOG.info("mock mybatis update method"); 95 | method.insertBefore("Object data = com.cxytiandi.foxmock.agent.transformer.MethodInvokeFilter.filterAndConvertDataByMybatisUpdate($args);if(java.util.Objects.nonNull(data)){return ($r)data;}"); 96 | } 97 | } 98 | } 99 | 100 | if (FoxMockConstant.DUBBO_CONSUMER_FILTER.equals(classInfo.getClassName())) { 101 | for (CtMethod method : declaredMethods) { 102 | String methodName = method.getName(); 103 | if ("invoke".equals(methodName)) { 104 | LOG.info("mock dubbo ConsumerContextFilter invoke method"); 105 | method.insertBefore("Object data = com.cxytiandi.foxmock.agent.transformer.DubboInvokeFilter.invoke($args);if(java.util.Objects.nonNull(data)){return ($r)data;}"); 106 | } 107 | } 108 | } 109 | 110 | if (FoxMockConstant.FEIGN_METHOD_HANDLER.equals(classInfo.getClassName())) { 111 | for (CtMethod method : declaredMethods) { 112 | String methodName = method.getName(); 113 | if ("invoke".equals(methodName)) { 114 | LOG.info("mock feign FeignInvocationHandler invoke method"); 115 | method.insertBefore("Object data = feign.FeignInvokeFilter.invoke($args,this);if(java.util.Objects.nonNull(data)){return ($r)data;}"); 116 | } 117 | } 118 | } 119 | 120 | 121 | // 缓存没有增强之前的类的信息 122 | if (match && !CLASS_INFO.containsKey(className)) { 123 | CLASS_INFO.put(className, new ClassInfo(className, classfileBuffer, loader)); 124 | } 125 | 126 | return ctClass.toBytecode(); 127 | 128 | } catch (Exception e) { 129 | LOG.error("transform {} error", className, e); 130 | throw new RuntimeException("transform error " + className, e); 131 | } 132 | } 133 | 134 | private void updateMethod(MockInfo mockInfo, CtMethod method, String className, String methodName) throws CannotCompileException { 135 | if (mockInfo.getMockData().startsWith("throw new")) { 136 | String mockCode = "if(com.cxytiandi.foxmock.agent.transformer.MethodInvokeFilter.filter($args,%s)){%s}"; 137 | method.insertBefore(String.format(mockCode, JsonUtils.toJson(mockInfo.getOgnlExpress()), mockInfo.getMockData())); 138 | } else { 139 | String mockCode = "if(com.cxytiandi.foxmock.agent.transformer.MethodInvokeFilter.filter($args,%s)){" + 140 | "return ($r)com.cxytiandi.foxmock.agent.utils.JsonUtils.parse(%s,%s,%s);" + 141 | "}"; 142 | method.insertBefore(String.format(mockCode, JsonUtils.toJson(mockInfo.getOgnlExpress()), JsonUtils.toJson(mockInfo.getMockData()), "\""+className+"\"", "\""+methodName+"\"")); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /fox-mock-agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fox-mock 7 | com.cxytiandi 8 | 1.0-SNAPSHOT 9 | 10 | 7.0 11 | 4.0.0 12 | 13 | fox-mock-agent 14 | 15 | 16 | UTF-8 17 | 18 | 19 | 20 | 21 | org.javassist 22 | javassist 23 | 24 | 25 | 26 | com.google.code.gson 27 | gson 28 | 29 | 30 | 31 | 32 | com.alibaba.arthas 33 | arthas-repackage-logger 34 | 35 | 36 | org.slf4j 37 | slf4j-api 38 | provided 39 | true 40 | 41 | 42 | ch.qos.logback 43 | logback-classic 44 | provided 45 | true 46 | 47 | 48 | ch.qos.logback 49 | logback-core 50 | provided 51 | true 52 | 53 | 54 | 55 | ognl 56 | ognl 57 | 58 | 59 | 60 | org.mybatis 61 | mybatis 62 | provided 63 | true 64 | 65 | 66 | 67 | 68 | org.apache.dubbo 69 | dubbo 70 | provided 71 | true 72 | 73 | 74 | 75 | 76 | io.github.openfeign 77 | feign-core 78 | provided 79 | true 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-jar-plugin 87 | 3.1.0 88 | 89 | 90 | 91 | 92 | true 93 | 94 | 95 | com.cxytiandi.foxmock.agent.FoxMockAgent 96 | com.cxytiandi.foxmock.agent.FoxMockAgent 97 | true 98 | true 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-shade-plugin 106 | 3.3.0 107 | 108 | 109 | package 110 | 111 | shade 112 | 113 | 114 | 115 | 116 | javassist 117 | com.cxytiandi.foxmock.agent.javassist 118 | 119 | 120 | com.google.gson 121 | com.cxytiandi.foxmock.agent.gson 122 | 123 | 124 | org.slf4j 125 | com.alibaba.arthas.deps.org.slf4j 126 | 127 | 128 | ch.qos.logback 129 | com.alibaba.arthas.deps.ch.qos.logback 130 | 131 | 132 | ognl 133 | com.cxytiandi.foxmock.agent.ognl 134 | 135 | 136 | 137 | 138 | 139 | org.javassist:javassist 140 | com.google.code.gson:gson 141 | org.slf4j:slf4j-api 142 | ch.qos.logback:logback-classic 143 | ch.qos.logback:logback-core 144 | com.alibaba.arthas:arthas-repackage-logger 145 | ognl:ognl 146 | 147 | 148 | 149 | 150 | org.javassist:javassist 151 | 152 | META-INF/MANIFEST.MF 153 | 154 | 155 | 156 | true 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /fox-mock-boot/src/main/java/com/cxytiandi/foxmock/boot/utils/ProcessUtils.java: -------------------------------------------------------------------------------- 1 | package com.cxytiandi.foxmock.boot.utils; 2 | 3 | import com.taobao.arthas.common.*; 4 | 5 | import java.io.*; 6 | import java.lang.reflect.Method; 7 | import java.net.URL; 8 | import java.net.URLClassLoader; 9 | import java.util.*; 10 | import java.util.Map.Entry; 11 | 12 | /** 13 | * 14 | * @author hengyunabc 2018-11-06 15 | * 此代码来源于arthas https://github.com/alibaba/arthas 16 | * 17 | */ 18 | public class ProcessUtils { 19 | private static String FOUND_JAVA_HOME = null; 20 | 21 | //status code from com.taobao.arthas.client.TelnetConsole 22 | /** 23 | * Process success 24 | */ 25 | public static final int STATUS_OK = 0; 26 | /** 27 | * Generic error 28 | */ 29 | public static final int STATUS_ERROR = 1; 30 | /** 31 | * Execute commands timeout 32 | */ 33 | public static final int STATUS_EXEC_TIMEOUT = 100; 34 | /** 35 | * Execute commands error 36 | */ 37 | public static final int STATUS_EXEC_ERROR = 101; 38 | 39 | @SuppressWarnings("resource") 40 | public static long select(boolean v, long telnetPortPid, String select) throws InputMismatchException { 41 | Map processMap = listProcessByJps(v); 42 | // Put the port that is already listening at the first 43 | if (telnetPortPid > 0 && processMap.containsKey(telnetPortPid)) { 44 | String telnetPortProcess = processMap.get(telnetPortPid); 45 | processMap.remove(telnetPortPid); 46 | Map newProcessMap = new LinkedHashMap(); 47 | newProcessMap.put(telnetPortPid, telnetPortProcess); 48 | newProcessMap.putAll(processMap); 49 | processMap = newProcessMap; 50 | } 51 | 52 | if (processMap.isEmpty()) { 53 | AnsiLog.info("Can not find java process. Try to run `jps` command lists the instrumented Java HotSpot VMs on the target system."); 54 | return -1; 55 | } 56 | 57 | // select target process by the '--select' option when match only one process 58 | if (select != null && !select.trim().isEmpty()) { 59 | int matchedSelectCount = 0; 60 | Long matchedPid = null; 61 | for (Entry entry : processMap.entrySet()) { 62 | if (entry.getValue().contains(select)) { 63 | matchedSelectCount++; 64 | matchedPid = entry.getKey(); 65 | } 66 | } 67 | if (matchedSelectCount == 1) { 68 | return matchedPid; 69 | } 70 | } 71 | 72 | AnsiLog.info("Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER."); 73 | // print list 74 | int count = 1; 75 | for (String process : processMap.values()) { 76 | if (count == 1) { 77 | System.out.println("* [" + count + "]: " + process); 78 | } else { 79 | System.out.println(" [" + count + "]: " + process); 80 | } 81 | count++; 82 | } 83 | 84 | // read choice 85 | String line = new Scanner(System.in).nextLine(); 86 | if (line.trim().isEmpty()) { 87 | // get the first process id 88 | return processMap.keySet().iterator().next(); 89 | } 90 | 91 | int choice = new Scanner(line).nextInt(); 92 | 93 | if (choice <= 0 || choice > processMap.size()) { 94 | return -1; 95 | } 96 | 97 | Iterator idIter = processMap.keySet().iterator(); 98 | for (int i = 1; i <= choice; ++i) { 99 | if (i == choice) { 100 | return idIter.next(); 101 | } 102 | idIter.next(); 103 | } 104 | 105 | return -1; 106 | } 107 | 108 | private static Map listProcessByJps(boolean v) { 109 | Map result = new LinkedHashMap(); 110 | 111 | String jps = "jps"; 112 | File jpsFile = findJps(); 113 | if (jpsFile != null) { 114 | jps = jpsFile.getAbsolutePath(); 115 | } 116 | 117 | AnsiLog.debug("Try use jps to lis java process, jps: " + jps); 118 | 119 | String[] command = null; 120 | if (v) { 121 | command = new String[] { jps, "-v", "-l" }; 122 | } else { 123 | command = new String[] { jps, "-l" }; 124 | } 125 | 126 | List lines = ExecutingCommand.runNative(command); 127 | 128 | AnsiLog.debug("jps result: " + lines); 129 | 130 | long currentPid = Long.parseLong(PidUtils.currentPid()); 131 | for (String line : lines) { 132 | String[] strings = line.trim().split("\\s+"); 133 | if (strings.length < 1) { 134 | continue; 135 | } 136 | try { 137 | long pid = Long.parseLong(strings[0]); 138 | if (pid == currentPid) { 139 | continue; 140 | } 141 | if (strings.length >= 2 && isJpsProcess(strings[1])) { // skip jps 142 | continue; 143 | } 144 | 145 | result.put(pid, line); 146 | } catch (Throwable e) { 147 | // https://github.com/alibaba/arthas/issues/970 148 | // ignore 149 | } 150 | } 151 | 152 | return result; 153 | } 154 | 155 | /** 156 | *
157 |      * 1. Try to find java home from System Property java.home
158 |      * 2. If jdk > 8, FOUND_JAVA_HOME set to java.home
159 |      * 3. If jdk <= 8, try to find tools.jar under java.home
160 |      * 4. If tools.jar do not exists under java.home, try to find System env JAVA_HOME
161 |      * 5. If jdk <= 8 and tools.jar do not exists under JAVA_HOME, throw IllegalArgumentException
162 |      * 
163 | * 164 | * @return 165 | */ 166 | public static String findJavaHome() { 167 | if (FOUND_JAVA_HOME != null) { 168 | return FOUND_JAVA_HOME; 169 | } 170 | 171 | String javaHome = System.getProperty("java.home"); 172 | 173 | if (JavaVersionUtils.isLessThanJava9()) { 174 | File toolsJar = new File(javaHome, "lib/tools.jar"); 175 | if (!toolsJar.exists()) { 176 | toolsJar = new File(javaHome, "../lib/tools.jar"); 177 | } 178 | if (!toolsJar.exists()) { 179 | // maybe jre 180 | toolsJar = new File(javaHome, "../../lib/tools.jar"); 181 | } 182 | 183 | if (toolsJar.exists()) { 184 | FOUND_JAVA_HOME = javaHome; 185 | return FOUND_JAVA_HOME; 186 | } 187 | 188 | if (!toolsJar.exists()) { 189 | AnsiLog.debug("Can not find tools.jar under java.home: " + javaHome); 190 | String javaHomeEnv = System.getenv("JAVA_HOME"); 191 | if (javaHomeEnv != null && !javaHomeEnv.isEmpty()) { 192 | AnsiLog.debug("Try to find tools.jar in System Env JAVA_HOME: " + javaHomeEnv); 193 | // $JAVA_HOME/lib/tools.jar 194 | toolsJar = new File(javaHomeEnv, "lib/tools.jar"); 195 | if (!toolsJar.exists()) { 196 | // maybe jre 197 | toolsJar = new File(javaHomeEnv, "../lib/tools.jar"); 198 | } 199 | } 200 | 201 | if (toolsJar.exists()) { 202 | AnsiLog.info("Found java home from System Env JAVA_HOME: " + javaHomeEnv); 203 | FOUND_JAVA_HOME = javaHomeEnv; 204 | return FOUND_JAVA_HOME; 205 | } 206 | 207 | throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome 208 | + ", please try to start arthas-boot with full path java. Such as /opt/jdk/bin/java -jar arthas-boot.jar"); 209 | } 210 | } else { 211 | FOUND_JAVA_HOME = javaHome; 212 | } 213 | return FOUND_JAVA_HOME; 214 | } 215 | 216 | public static void startArthasCore(long targetPid, List attachArgs) { 217 | // find java/java.exe, then try to find tools.jar 218 | String javaHome = findJavaHome(); 219 | 220 | // find java/java.exe 221 | File javaPath = findJava(javaHome); 222 | if (javaPath == null) { 223 | throw new IllegalArgumentException( 224 | "Can not find java/java.exe executable file under java home: " + javaHome); 225 | } 226 | 227 | File toolsJar = findToolsJar(javaHome); 228 | 229 | if (JavaVersionUtils.isLessThanJava9()) { 230 | if (toolsJar == null || !toolsJar.exists()) { 231 | throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome); 232 | } 233 | } 234 | 235 | List command = new ArrayList(); 236 | command.add(javaPath.getAbsolutePath()); 237 | 238 | if (toolsJar != null && toolsJar.exists()) { 239 | command.add("-Xbootclasspath/a:" + toolsJar.getAbsolutePath()); 240 | } 241 | 242 | command.addAll(attachArgs); 243 | // "${JAVA_HOME}"/bin/java \ 244 | // ${opts} \ 245 | // -jar "${arthas_lib_dir}/arthas-core.jar" \ 246 | // -pid ${TARGET_PID} \ 247 | // -target-ip ${TARGET_IP} \ 248 | // -telnet-port ${TELNET_PORT} \ 249 | // -http-port ${HTTP_PORT} \ 250 | // -core "${arthas_lib_dir}/arthas-core.jar" \ 251 | // -agent "${arthas_lib_dir}/arthas-agent.jar" 252 | 253 | ProcessBuilder pb = new ProcessBuilder(command); 254 | try { 255 | final Process proc = pb.start(); 256 | Thread redirectStdout = new Thread(new Runnable() { 257 | @Override 258 | public void run() { 259 | InputStream inputStream = proc.getInputStream(); 260 | try { 261 | IOUtils.copy(inputStream, System.out); 262 | } catch (IOException e) { 263 | IOUtils.close(inputStream); 264 | } 265 | 266 | } 267 | }); 268 | 269 | Thread redirectStderr = new Thread(new Runnable() { 270 | @Override 271 | public void run() { 272 | InputStream inputStream = proc.getErrorStream(); 273 | try { 274 | IOUtils.copy(inputStream, System.err); 275 | } catch (IOException e) { 276 | IOUtils.close(inputStream); 277 | } 278 | 279 | } 280 | }); 281 | redirectStdout.start(); 282 | redirectStderr.start(); 283 | redirectStdout.join(); 284 | redirectStderr.join(); 285 | 286 | int exitValue = proc.exitValue(); 287 | if (exitValue != 0) { 288 | AnsiLog.error("attach fail, targetPid: " + targetPid); 289 | System.exit(1); 290 | } 291 | } catch (Throwable e) { 292 | // ignore 293 | } 294 | } 295 | 296 | public static int startArthasClient(String arthasHomeDir, List telnetArgs, OutputStream out) throws Throwable { 297 | // start java telnet client 298 | // find arthas-client.jar 299 | URLClassLoader classLoader = new URLClassLoader( 300 | new URL[]{new File(arthasHomeDir, "arthas-client.jar").toURI().toURL()}); 301 | Class telnetConsoleClass = classLoader.loadClass("com.taobao.arthas.client.TelnetConsole"); 302 | Method processMethod = telnetConsoleClass.getMethod("process", String[].class); 303 | 304 | //redirect System.out/System.err 305 | PrintStream originSysOut = System.out; 306 | PrintStream originSysErr = System.err; 307 | PrintStream newOut = new PrintStream(out); 308 | PrintStream newErr = new PrintStream(out); 309 | 310 | // call TelnetConsole.process() 311 | // fix https://github.com/alibaba/arthas/issues/833 312 | ClassLoader tccl = Thread.currentThread().getContextClassLoader(); 313 | try { 314 | System.setOut(newOut); 315 | System.setErr(newErr); 316 | Thread.currentThread().setContextClassLoader(classLoader); 317 | return (Integer) processMethod.invoke(null, new Object[]{telnetArgs.toArray(new String[0])}); 318 | } catch (Throwable e) { 319 | //java.lang.reflect.InvocationTargetException : java.net.ConnectException 320 | e = e.getCause(); 321 | if (e instanceof IOException || e instanceof InterruptedException) { 322 | // ignore connection error and interrupted error 323 | return STATUS_ERROR; 324 | } else { 325 | // process error 326 | AnsiLog.error("process error: {}", e.toString()); 327 | AnsiLog.error(e); 328 | return STATUS_EXEC_ERROR; 329 | } 330 | } finally { 331 | Thread.currentThread().setContextClassLoader(tccl); 332 | 333 | //reset System.out/System.err 334 | System.setOut(originSysOut); 335 | System.setErr(originSysErr); 336 | //flush output 337 | newOut.flush(); 338 | newErr.flush(); 339 | } 340 | } 341 | 342 | private static File findJava(String javaHome) { 343 | String[] paths = { "bin/java", "bin/java.exe", "../bin/java", "../bin/java.exe" }; 344 | 345 | List javaList = new ArrayList(); 346 | for (String path : paths) { 347 | File javaFile = new File(javaHome, path); 348 | if (javaFile.exists()) { 349 | AnsiLog.debug("Found java: " + javaFile.getAbsolutePath()); 350 | javaList.add(javaFile); 351 | } 352 | } 353 | 354 | if (javaList.isEmpty()) { 355 | AnsiLog.debug("Can not find java/java.exe under current java home: " + javaHome); 356 | return null; 357 | } 358 | 359 | // find the shortest path, jre path longer than jdk path 360 | if (javaList.size() > 1) { 361 | Collections.sort(javaList, new Comparator() { 362 | @Override 363 | public int compare(File file1, File file2) { 364 | try { 365 | return file1.getCanonicalPath().length() - file2.getCanonicalPath().length(); 366 | } catch (IOException e) { 367 | // ignore 368 | } 369 | return -1; 370 | } 371 | }); 372 | } 373 | return javaList.get(0); 374 | } 375 | 376 | private static File findToolsJar(String javaHome) { 377 | if (JavaVersionUtils.isGreaterThanJava8()) { 378 | return null; 379 | } 380 | 381 | File toolsJar = new File(javaHome, "lib/tools.jar"); 382 | if (!toolsJar.exists()) { 383 | toolsJar = new File(javaHome, "../lib/tools.jar"); 384 | } 385 | if (!toolsJar.exists()) { 386 | // maybe jre 387 | toolsJar = new File(javaHome, "../../lib/tools.jar"); 388 | } 389 | 390 | if (!toolsJar.exists()) { 391 | throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome); 392 | } 393 | 394 | AnsiLog.debug("Found tools.jar: " + toolsJar.getAbsolutePath()); 395 | return toolsJar; 396 | } 397 | 398 | private static File findJps() { 399 | // Try to find jps under java.home and System env JAVA_HOME 400 | String javaHome = System.getProperty("java.home"); 401 | String[] paths = { "bin/jps", "bin/jps.exe", "../bin/jps", "../bin/jps.exe" }; 402 | 403 | List jpsList = new ArrayList(); 404 | for (String path : paths) { 405 | File jpsFile = new File(javaHome, path); 406 | if (jpsFile.exists()) { 407 | AnsiLog.debug("Found jps: " + jpsFile.getAbsolutePath()); 408 | jpsList.add(jpsFile); 409 | } 410 | } 411 | 412 | if (jpsList.isEmpty()) { 413 | AnsiLog.debug("Can not find jps under :" + javaHome); 414 | String javaHomeEnv = System.getenv("JAVA_HOME"); 415 | AnsiLog.debug("Try to find jps under env JAVA_HOME :" + javaHomeEnv); 416 | for (String path : paths) { 417 | File jpsFile = new File(javaHomeEnv, path); 418 | if (jpsFile.exists()) { 419 | AnsiLog.debug("Found jps: " + jpsFile.getAbsolutePath()); 420 | jpsList.add(jpsFile); 421 | } 422 | } 423 | } 424 | 425 | if (jpsList.isEmpty()) { 426 | AnsiLog.debug("Can not find jps under current java home: " + javaHome); 427 | return null; 428 | } 429 | 430 | // find the shortest path, jre path longer than jdk path 431 | if (jpsList.size() > 1) { 432 | Collections.sort(jpsList, new Comparator() { 433 | @Override 434 | public int compare(File file1, File file2) { 435 | try { 436 | return file1.getCanonicalPath().length() - file2.getCanonicalPath().length(); 437 | } catch (IOException e) { 438 | // ignore 439 | } 440 | return -1; 441 | } 442 | }); 443 | } 444 | return jpsList.get(0); 445 | } 446 | 447 | private static boolean isJpsProcess(String mainClassName) { 448 | return "sun.tools.jps.Jps".equals(mainClassName) || "jdk.jcmd/sun.tools.jps.Jps".equals(mainClassName); 449 | } 450 | } 451 | --------------------------------------------------------------------------------