一. 背景:

最近小熙在写对接,涉及到一些远程调用,用的是httpclient实现的,但是觉得有些麻烦。有没有封装过的框架,让操作更方便呢,有的比如:Forset

  1. 介绍:
    Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。

  2. 好处:
    使用 Forest 就像使用类似 Dubbo 那样的 RPC 框架一样,只需要定义接口,调用接口即可,不必关心具体发送 HTTP 请求的细节。同时将 HTTP 请求信息与业务代码解耦,方便您统一管理大量 HTTP 的 URL、Header 等信息。而请求的调用方完全不必在意 HTTP 的具体内容,即使该 HTTP 请求信息发生变更,大多数情况也不需要修改调用发送请求的代码。

  3. 原理:
    Forest 会将您定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API(httpclient 等 API)执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类。

  4. 架构:

二: 依赖引入和配置yml:

这里小熙使用的项目是 springboot 的,所以是基于此演示的
坐标版本引用的是 1.5.0-RC2 的,所以以下演示都是基于此版本的

  1. 引入坐标:

    <dependency><groupId>com.dtflys.forest</groupId><artifactId>spring-boot-starter-forest</artifactId><version>1.5.0-RC2</version>
    </dependency>
    

    辅助 json 解析工具坐标(Fastjson依赖:版本 >= 1.2.48):

       <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.48</version></dependency>
    
  2. yml中的配置:

       forest:bean-id: forestConfiguration # 在spring上下文中bean的id, 默认值为forestConfigurationbackend: okhttp3             # 后端HTTP API: 默认使用:okhttp3,也可以替换成:httpclientmax-connections: 1000        # 连接池最大连接数,默认值为500max-route-connections: 500   # 每个路由的最大连接数,默认值为500timeout: 3000                # 请求超时时间,单位为毫秒, 默认值为3000connect-timeout: 3000        # 连接超时时间,单位为毫秒, 默认值为2000retry-count: 1               # 请求失败后重试次数,默认为0次不重试ssl-protocol: SSLv3          # 单向验证的HTTPS的默认SSL协议,默认为SSLv3logEnabled: true             # 打开或关闭日志,默认为true,也可以在单个方法上加注解是否开启:@LogEnabled
    

    获取Forest在spring上下文中的bean

    @Resource(name = "forestConfiguration")
    private ForestConfiguration forestConfiguration;
    

三. 使用介绍:

1. 请求类型:可支持(GET, POST, PUT, HEAD, OPTIONS, DELETE)

     /*** 测试访问百度* 这里使用的是Get请求,你也可以简写为:@Get* * 下面的请求同理* * @return*/@GetRequest(url = "http://www.baidu.com")String accessBaiDu();/*** 也可以在请求里面指定类型* * 下面的请求同理* * @return*/@Request(url = "http://www.baidu.com", type = "get")String accessBaiDuRequestType();@Post(url = "http://www.baidu.com")String accessBaiDuPost();@Put(url = "http://www.baidu.com")String accessBaiDuPut();@DeleteRequest(url = "http://www.baidu.com")String accessBaiDuDelete();

2. 动态替换请求中变量(可传入):

     /*** post请求* @param domainName 参数传递domainName(变量绑定是@DataVariable,视情况而定可以不出现在url上)* @param port 参数传递 port(变量绑定是@DataVariable)* @return*/@PostRequest(url = "http://${domainName}:${port}")String testDataVariable(@DataVariable("domainName") String domainName, @DataVariable("port") Integer port);

3. 动态替换请求变量的优化(对模板替换和批量参数等不够优雅和繁杂)

     /*** Query的时候需要注意的点:* (1) 需要单个单个定义 参数名=参数值 的时候,@Query注解的value值一定要有,比如 @Query("name") String name** (2) 需要绑定对象的时候,@Query注解的value值一定要空着,比如 @Query User user 或 @Query Map map*//*** post请求* @param domainName (@Query修饰的参数一定会出现在url中)* @param port* @return*/@PostRequest(url = "http://domainName:port")String testQuery(@Query("domainName") String domainName, @Query("port") Integer port);/*** post请求* 使用 @Query 注解,可以修饰 Map 类型的参数* 很自然的,Map 的 Key 将作为 URL 的参数名, Value 将作为 URL 的参数值* 这时候 @Query 注解不定义名称* @param map* @return*/@PostRequest(url = "http://domainName:port")String testQuery(@Query Map<String, Object> map);/*** @Query 注解也可以修饰自定义类型的对象参数* 依据对象类的 Getter 和 Setter 的规则取出属性* 其属性名为 URL 参数名,属性值为 URL 参数值* 这时候 @Query 注解不定义名称* @param urlClass 这是你的入参对象* @return*/@PostRequest(url = "http://domainName:port")String testQuery(@Query UrlClass urlClass);

4. 关于请求头的设置(以下例子中依次递增的是优化方案):

         /**  @Header使用注意事项* (1) 需要单个单个定义请求头的时候,@Header注解的value值一定要有,比如 @Header("Content-Type") String contentType** (2) 需要绑定对象的时候,@Header注解的value值一定要空着,比如 @Header MyHeaders headers 或 @Header Map headerMap*//*** 默认get请求,在head中也可以使用@Query设置值同理@DataVariable* @param encoding* @return*/@Request(url = "http://www.baidu.com",headers = {"Accept-Charset: ${encoding}","Content-Type: text/plain"})String testheaders(@Query("encoding") String encoding);/*** post请求* 使用 @Header 注解将参数绑定到请求头上* @Header 注解的 value 指为请求头的名称,参数值为请求头的值* @Header("Accept") String accept将字符串类型参数绑定到请求头 Accept 上* @Header("accessToken") String accessToken将字符串类型参数绑定到请求头 accessToken 上* @param accept* @param accessToken*/@Post("http://www.baidu.com")void testHead(@Header("Accept") String accept, @Header("accessToken") String accessToken);/*** 使用 @Header 注解可以修饰 Map 类型的参数* Map 的 Key 指为请求头的名称,Value 为请求头的值* 通过此方式,可以将 Map 中所有的键值对批量地绑定到请求头中* @param headerMap*/@Post("http://www.baidu.com")void testHead(@Header Map<String, Object> headerMap);/*** 使用 @Header 注解可以修饰自定义类型的对象参数* 依据对象类的 Getter 和 Setter 的规则取出属性* 其属性名为 URL 请求头的名称,属性值为请求头的值* 以此方式,将一个对象中的所有属性批量地绑定到请求头中*/@Post("http://localhost:8080/hello/user?username=foo")void testHead(@Header HeaderInfo headersInfo);

5. 请求中的附加参数(head和body中):

     /*** delete请求* @param url* @param param 这是数据参数,附加在请求上,get在head,post在body*              此方法@DataParam已过时* @return*/@DeleteRequest(url = "${url}")String testVariableAndParam(@DataVariable("url") String url, @DataParam("param") String param);/*** @Body注解修饰的参数一定会绑定到请求体中* 默认body格式为 application/x-www-form-urlencoded,即以表单形式序列化数据* @param username* @param password* @return*    username=xxx&password=xxx (这是默认格式,如果要转为json格式,在请求头中设置即可Content-Type: application/json)*/@Post(url = "http://www.baidu.com",headers = {"Accept:text/plain"})String testBody(@Body("username") String username,  @Body("password") String password);/*** 也可以使用@Body修饰,将整个类以表单方式传输* @param item* @return*/@Post(url = "http://www.baidu.com",headers = {"Accept:text/plain"})String testBody(@Body Item item);/*** 被 @JSONBody注解修饰的参数会根据其类型被自定解析为JSON字符串, (也可以拆分开,修饰单个参数,如 @JSONBody(string) String string)*  修饰map等亦可以,当修饰 list的时候,结果类型可为["A", "B", "C"]* 使用 @JSONBody注解时可以省略 contentType = "application/json"属性设置* @param item* @return*    {"username": "xx", "password": "xxx"}*/@Post("http://www.baidu.com")String testJSONBody(@JSONBody Item item);

6. 基础请求(提取封装请求相关属性,避免重复编写):

    /*** @BaseRequest 为配置接口层级请求信息的注解,* 其属性会成为该接口下所有请求的默认属性,* 但可以被方法上定义的属性所覆盖** @BaseRequest注解中的所有字符串属性都可以通过模板表达式引用全局变量或方法中的参数。* 若全局变量中已定义 baseUrl* 便会将全局变量中的值绑定到 @BaseRequest 的属性中*/@BaseRequest(baseURL = "${baseUrl}",     // 默认域名headers = {"Accept:text/plain"                // 默认请求头},sslProtocol = "sslType"                    // 默认单向SSL协议(TLS))interface MyClient {/*** 在 @BaseRequest 中的属性亦可以引用方法中的绑定变量名的参数* @param baseUrl 在BaseRequest注解中被引用* @return*/@Get("/hello/user")String testOne(@DataVariable("baseUrl") String baseUrl);/*** 方法的URL不必再写域名部分* @param sslType 在BaseRequest注解中被引用* @return*/@Get("/hello/user")String testTwo(@Query("sslType") String sslType);/*** 若方法的URL是完整包含http://开头的,那么会以方法的URL中域名为准,不会被接口层级中的baseUrl属性覆盖* @param username* @return*/@Get("http://www.xxx.com/hello/user")String testThree(@Query("username") String username);/*** 覆盖请求头中信息* @param username* @return*/@Get(url = "/hello/user",headers = {"Accept:application/json"      // 覆盖接口层级配置的请求头信息})String testFour(@Query("username") String username);}

7. 接收响应封装数据:

    /*** dataType属性指定了该请求响应返回的数据类型,目前可选的数据类型有三种: text, json, xml* Forest请求会自动将响应的返回数据反序列化成您要的数据类型。想要接受指定类型的数据需要完成两步操作:* 第一步:定义dataType属性* 第二步:指定反序列化的目标类型* 从1.4.0版本开始,dataType 属性默认为 auto(自动判断数据类型), 也就是说 dataType 属性可以完全省略不填,Forest会自行判断返回的数据类型是哪种格式。*//*** dataType为text或不填时,请求响应的数据将以文本字符串的形式返回回来* @return*/@Request(url = "http://localhost:8080/text/data",dataType = "text")String testQueryStringData();/*** dataType属性指明了返回的数据类型为JSON* 从1.4.0版本开始,dataType 属性默认为 auto(自动判断数据类型),* 也就是说 dataType 属性可以完全省略不填,Forest会自行判断返回的数据类型是哪种格式。* @param id* @return*/@Get(url = "http://localhost:8080/user?id=${0}",dataType = "json"   // 注意这里从1.4开始可以不写,自动判断)User testQueryUserData(Integer id);/*** ForestResponse 可以获取响应内容,也可以得到响应头等信息* ForestResponse 可以作为请求方法的返回类型* ForestResponse 为带泛型的类,其泛型参数中填的类作为其响应反序列化的目标类型* @param item* @return*/@Post("http://localhost:8080/user")ForestResponse<String> testResponseString(@JSONBody Item item);/*** 响应封装在对象中*  日志打印默认为true* @param item* @return*/@LogEnabled(value = true)@Post("http://localhost:8080/user")ForestResponse<Item> testResponseItem(@JSONBody Item item);

其中用ForestResponse对象接到请求响应数据后便可以获取响应内容:

 // 以 ForestResponse类型变量接受响应数据ForestResponse<String> response = client.postUser(user);// 用 isError方法去判断请求是否失败if (response.isError()) {... ...}// 用isSuccess方法去判断请求是否成功if (response.isSuccess()) {... ...}// 以字符串方式读取请求响应内容String text = response.readAsString();// getContent方法可以获取请求响应内容文本// 和 readAsString方法不同的地方在于,getContent方法不会读取二进制形式数据内容,// 而 readAsString方法会将二进制数据转换成字符串读取String content = response.getContent();// 获取反序列化成对象类型的请求响应内容// 因为返回类型为 ForetReponse<String>, 其泛型参数为String// 所以这里也用 String类型获取结果        String result = response.getResult();// 以字节数组的形式获取请求响应内容byte[] byteArray = response.getByteArray();// 以输入流的形式获取请求响应内容InputStream in = response.getInputStream();// 根据响应头名称获取单个请求响应头ForestHeader header = response.getHeader("Content-Type");// 响应头名称String headerName = header.getName();// 响应头值String headerValue = header.getValue();// 根据响应头名称获取请求响应头列表List<ForestHeader> heaers = response.getHeaders("Content-Type");// 根据响应头名称获取请求响应头值String val = response.getHeaderValue("Content-Type");// 根据响应头名称获取请求响应头值列表List<String> vals = response.getHeaderValues("Content-Type");

8. 回调函数(可用于成功和失败默认回调):

    /*** 在异步请求中只能通过OnSuccess<T>回调函数接或Future<T>返回值接受数据。* 而在同步请求中,OnSuccess<T>回调函数和任何类型的返回值都能接受到请求响应的数据。* OnError回调函数可以用于异常处理,一般在同步请求中使用try-catch也能达到同样的效果。*//*** 请求使用成功和失败回调** 如这两个回调函数的类名所示的含义一样,OnSuccess<T>在请求成功调用响应时会被调用,而OnError在失败或出现错误的时候被调用。* 其中OnSuccess<T>的泛型参数T定义为请求响应返回结果的数据类型。* @param username* @param onSuccess* @param onError* @return*/@Request(url = "http://localhost:8080/hello/user",headers = {"Accept:text/plain"},data = "username=${username}")String sendAndCallback(@DataVariable("username") String username, OnSuccess<String> onSuccess, OnError onError);

回调使用演示:

 testInterface.sendAndCallback("小熙", (String resultString, ForestRequest request, ForestResponse response) -> {// 成功响应回调(如果是异步,这里可以操作响应数据封装)System.out.println(resultString);},(ForestRuntimeException ex, ForestRequest request, ForestResponse response) -> {// 异常回调System.out.println(ex.getMessage());});

9. 异步请求:

     /*** 在Forest使用异步请求,可以通过设置@Request注解的async属性为true实现,不设置或设置为false即为同步请求。*//*** 异步请求,成功回调(在异步请求中只能通过OnSuccess<T>回调函数接或Future<T>返回值接受数据,所以返回值为void)* @param username* @param onSuccess*/@Request(url = "http://localhost:8080/hello/queryUserByName/username",async = true,headers = {"Accept:text/plain"})void testAsyncGet(@Query("username") String username, OnSuccess<String> onSuccess);/*** 异步请求,Future封装接收* @param username* @return*/@Request(url = "http://localhost:8080/hello/queryUserByName/username",async = true,headers = {"Accept:text/plain"})Future<String> testAsyncGet(@Query("username") String username);

调用演示:

         // 回调异步testInterface.testAsyncGet("小熙", (String resultString, ForestRequest request, ForestResponse response) -> {// 打印成功请求返回的结果System.out.println(resultString);});// future异步Future<String> stringFuture = testInterface.testAsyncGet("小熙");// 获取future异步成功调用的返回结果String string = stringFuture.get();

10. 文件上传:

     /*** 上传下载* Forest从 1.4.0 版本开始支持多种形式的文件上传和文件下载功能*//*** 用@DataFile注解修饰要上传的参数对象* OnProgress 参数为监听上传进度的回调函数* @param filePath* @param onProgress* @return*/@Post(url = "/upload")Map testUpload(@DataFile("file") String filePath, OnProgress onProgress);/*** File类型对象*/@Post(url = "/upload")Map testUpload(@DataFile("file") File file, OnProgress onProgress);/*** byte数组* 使用byte数组和Inputstream对象时一定要定义fileName属性*/@Post(url = "/upload")Map testUpload(@DataFile(value = "file", fileName = "${1}") byte[] bytes, String filename);/*** Inputstream 对象* 使用byte数组和Inputstream对象时一定要定义fileName属性*/@Post(url = "/upload")Map testUpload(@DataFile(value = "file", fileName = "${1}") InputStream in, String filename);/*** Spring Web MVC 中的 MultipartFile 对象*/@PostRequest(url = "/upload")Map testUpload(@DataFile(value = "file") MultipartFile multipartFile, OnProgress onProgress);/*** Spring 的 Resource 对象*/@Post(url = "/upload")Map testUpload(@DataFile(value = "file") Resource resource);

调用演示:

 Map result = testInterface.testUpload("D:\\TestUpload\\xxx.jpg", progress -> {System.out.println("total bytes: " + progress.getTotalBytes());   // 文件大小System.out.println("current bytes: " + progress.getCurrentBytes());   // 已上传字节数System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%");  // 已上传百分比if (progress.isDone()) {   // 是否上传完成System.out.println("--------   Upload Completed!   --------");}});

批量上传:

    /*** 批量上传*//*** 上传Map包装的文件列表* 其中 ${_key} 代表Map中每一次迭代中的键值* @param byteArrayMap* @return*/@PostRequest(url = "/upload")ForestRequest<Map> uploadByteArrayMap(@DataFile(value = "file", fileName = "${_key}") Map<String, byte[]> byteArrayMap);/*** 上传List包装的文件列表* 其中 ${_index} 代表每次迭代List的循环计数(从零开始计)* @param byteArrayList* @return*/@PostRequest(url = "/upload")ForestRequest<Map> uploadByteArrayList(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") List<byte[]> byteArrayList);/*** 上传数组包装的文件列表* 其中 ${_index} 代表每次迭代List的循环计数(从零开始计)* @param byteArrayArray* @return*/@PostRequest(url = "/upload")ForestRequest<Map> uploadByteArrayArray(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") byte[][] byteArrayArray);

11. 文件下载:

         /*** 在方法上加上@DownloadFile注解* dir属性表示文件下载到哪个目录* filename属性表示文件下载成功后以什么名字保存,如果不填,这默认从URL中取得文件名* OnProgress参数为监听上传进度的回调函数* @param dir* @param filename* @param onProgress* @return*/@Get(url = "http://localhost:8080/images/xxx.jpg")@DownloadFile(dir = "${0}", filename = "${1}")File testDownloadFile(String dir, String filename, OnProgress onProgress);/*** 如果您不想将文件下载到硬盘上,而是直接在内存中读取,可以去掉@DownloadFile注解,并且用以下几种方式定义接口:*//*** 返回类型用byte[],可将下载的文件转换成字节数组* @return*/@GetRequest(url = "http://localhost:8080/images/test-img.jpg")byte[] downloadImageToByteArray();/*** 返回类型用InputStream,用流的方式读取文件内容* @return*/@GetRequest(url = "http://localhost:8080/images/test-img.jpg")InputStream downloadImageToInputStream();

调用演示:

     File file = testInterface.testDownloadFile("D:\\TestDownload", "", progress -> {System.out.println("total bytes: " + progress.getTotalBytes());   // 文件大小System.out.println("current bytes: " + progress.getCurrentBytes());   // 已下载字节数System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%");  // 已下载百分比if (progress.isDone()) {   // 是否下载完成System.out.println("--------   Download Completed!   --------");}});

四: HTTPS请求:

为保证网络访问安全,现在大多数企业都会选择使用SSL验证来提高网站的安全性。

1. 单向认证:

全局配置可以配置一个全局统一的SSL协议,但现实情况是有很多不同服务(尤其是第三方)的API会使用不同的SSL协议,这种情况需要针对不同的接口设置不同的SSL协议。

    /*** 在某个请求接口上通过 sslProtocol 属性设置单向SSL协议* @return*/@Get(url = "https://localhost:5555/hello/user",sslProtocol = "SSL")ForestResponse<String> testTruestSSLGet();/*** 在一个个方法上设置太麻烦,也可以在 @BaseRequest 注解中设置一整个接口类的SSL协议*/@BaseRequest(sslProtocol = "TLS")public interface SSLClient {/*** 类中使用* @return*/@Get("https://localhost:5555/hello/user")String testTruestSSLGet();}

2. 双向认证

在yml中添加证书等配置:

     forest:...ssl-key-stores:- id: keystore1                      # id为该keystore的名称,必填。你可以配置多个key,调用不同key即可file: test.keystore             # 公钥文件地址keystore-pass: 123456           # keystore秘钥cert-pass: 123456               # cert秘钥protocols: SSLv3                # SSL协议  - id: keystore2                      # 第二个keystorefile: test2.keystore    keystore-pass: abcdef  cert-pass: abcdef      protocols: SSLv3       ...

接下来在代码中直接引用这个 id 对应的key:

    @Request(url = "https://localhost:5555/hello/user",keyStore = "keystore1")String testTruestkeystoreGet();

五. 后语:

Forest 作为远程之间调用的封装,还有很多功能,这里小熙只是列出了大部分基础的功能,感兴趣的可以去官网看看哟。

Forest 使用简介相关推荐

  1. 机器学习-Random Forest算法简介

    Random Forest是加州大学伯克利分校的Breiman Leo和Adele Cutler于2001年发表的论文中提到的新的机器学习算法,可以用来做分类,聚类,回归,和生存分析,这里只简单介绍该 ...

  2. Random Forest算法简介

    转自JoinQuant量化课堂 一.相关概念 分类器:分类器就是给定一个样本的数据,判定这个样本属于哪个类别的算法.例如在股票涨跌预测中,我们认为前一天的交易量和收盘价对于第二天的涨跌是有影响的,那么 ...

  3. 孤立森林算法 python_孤立森林(isolation forest)

    1.简介 孤立森林(Isolation Forest)是另外一种高效的异常检测算法,它和随机森林类似,但每次选择划分属性和划分点(值)时都是随机的,而不是根据信息增益或者基尼指数来选择. 在建树过程中 ...

  4. 机器学习算法总结--提升方法

    参考自: <统计学习方法> 浅谈机器学习基础(上) Ensemble learning:Bagging,Random Forest,Boosting 简介 提升方法(boosting)是一 ...

  5. 碳足迹分析软件市场现状研究分析报告-

    辰宇信息咨询市场调研公司最近发布-<2022-2028中国碳足迹分析软件市场现状研究分析与发展前景预测报告 > 内容摘要 本文研究中国市场碳足迹分析软件现状及未来发展趋势,侧重分析在中国市 ...

  6. Machine Learning | (8) Scikit-learn的分类器算法-随机森林(Random Forest)

    Machine Learning | 机器学习简介 Machine Learning | (1) Scikit-learn与特征工程 Machine Learning | (2) sklearn数据集 ...

  7. mSystems:南京土壤所禇海燕组受邀发表微生物生物地理学综述(官方配视频简介)

    文章目录 瞬息万变的世界中的土壤微生物生物地理学:最新进展和未来展望 写在前面 视频介绍 快讯 图 1 长期以来在微生物生物地理学领域已发表文章的数量 图 2 土壤微生物生物地理学中需要进行时空研究的 ...

  8. USEARCH11新功能简介

    USEARCH是继Mothur.QIIME后的第三大流行扩增子分析流程,目前引用11588次.由Robert Edgar大神独立编写.官方网址:http://www.drive5.com/usearc ...

  9. USEARCH11发布,新功能简介

    USEARCH是继Mothur.QIIME后的第三大流行扩增子分析流程,目前引用7296次.由Robert Edgar大神独立编写.官方网址:http://www.drive5.com/usearch ...

最新文章

  1. 执行计划组件、组件、老化
  2. 不懂AI的我,是如何搞开发的?
  3. js错误:对象不支持此属性或方法
  4. cloud 部署_使用Google Cloud AI平台开发,训练和部署TensorFlow模型
  5. Java基础-方法(method)的应用
  6. 中国双色向滤光镜行业市场供需与战略研究报告
  7. java ilvmanagerview_创建一个多行的tooltip
  8. 测试用例方法--等价类划分法
  9. 非线性光纤光学——光孤子
  10. c语言逻辑运算符用法大全,【学习笔记】【C语言】逻辑运算符
  11. 搜索引擎优化、常用SEO优化方法总结
  12. 2017年数据库技术盘点
  13. 判断和循环——实战收尾篇1(二分法、抛硬币等)
  14. 凯文·凯利:流动、共享、颠覆,未来20年的 12大技术趋势
  15. 计算机网络云怎么连接网络,华为云电脑如何连网 华为云电脑使用方法介绍
  16. 运营商大数据在不同行业的利用
  17. 【Linux初阶】操作系统概念与定位 | 操作系统管理硬件方法、系统调用和库函数概念
  18. php水解蛋白技术,乳蛋白部分水解配方奶粉:美赞臣亲舒
  19. 360 新推出的搜索会成功吗?
  20. Android 四大组件之广播(Broadcast)

热门文章

  1. 审稿专家们在审博士论文时最看重什么
  2. 技术分享 | 咬文嚼字之驱动表 outer表
  3. 互联旅馆项目的经历路线
  4. 用一个项目讲解网页设计流程
  5. Python之append和extent的区别
  6. OTC非处方药是什么意思?
  7. Elasticsearch7.X-IK分词器
  8. 伸缩自如的时光轴实现
  9. MATLAB中果蝇味道浓度判定函数,果蝇优化算法的加权策略研究
  10. 端口扫描与拒绝服务总结