【整理分享】8種開發工具,提升工作效率,再也不做加班人!

2022-09-15 14:01:05
你還在因為加班熬夜而禿頭嗎?你還在因為奇葩需求而造輪子嗎?那你找對人了!!本文切身感受程式設計師之痛苦,背後默默吐血整理了一篇文章,希望對大家有所幫助。沖沖衝!!

寫爬蟲IP被封了怎麼解決?立即使用

工欲善其事 必先利其器

身為一個程式設計師,每天面對的事情就是寫程式碼和吹牛逼了。但是總是感覺自己這兩個事情沒有達到一個平衡點,總感覺每天寫程式碼的時間太多了,都沒有多少讓自己吹的時間了。不知道大家有沒有這些問題和疑惑呢?

我們已知程式設計師是最會偷懶的生物!哎!那麼問題來了,那怎麼摸魚時間還是這麼少呢?難道是我們太菜了嗎?不不不,可不要小瞧自己,那會是啥原因嘞?

答案就是,當然是你還沒看這篇文章唄,本文切身感受程式設計師之痛苦,背後默默吐血整理了一篇文章,現在分享給大家,希望對大家有所幫助。

目錄

  • 整體預覽圖

  • JSON解析工具

  • HTTP網路請求工具

  • 字串處理工具

  • 集合處理工具

  • 檔案流處理工具

  • 加解密工具

  • JAVA bean 物件轉換工具

  • 快取和限流工具


開始上手

整體預覽圖

本文會從圖中分類觸發,介紹相關工具包,並簡單介紹使用。因為本文篇幅有限,所以只當做是一個引子。具體細節還是都得大家在寫程式碼的時候慢慢體會。

1.png

JSON 解析工具

json 解析工具在開發中有多常用相信不用我多說了吧,可以說是程式設計師天天用到的工具,這也是我將它放到第一個來說的原因,下面我們來一起看一下,概括和使用吧,GO! 筆者我用的比較多的是 Fastjson ,它是阿里開源的一款進行 JSON 解析的工具,用法也是相當簡單。

2.png

1、maven 匯入 pom 座標

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

2、下面看怎麼使用

  • JSON 字串與實體物件互相轉化
// 字串轉物件
Studen student = JSON.parseObject("{"name":"小明","age":18}", Student.class);
// 物件轉字串
String str = JSON.toJSONString(student);
  • JSON 字串與 JSONObject 互相轉化

JSONObject只是一種資料結構,可以理解為JSON格式的資料結構(key-value 結構),可以使用put方法給json物件新增元素。JSONObject可以很方便的轉換成字串,也可以很方便的把其他物件轉換成JSONObject物件

// 字串轉JSONObject物件
JSONObject jsonObject = JSONObject.parseObject("{"name":"小明","age":18}");
// JSONObject物件轉字串
String str = jsonObject.toJSONString();
  • JSON 字串轉化為 集合類
// 定義解析字串
String studentListStr = "[{"name":"小明","age":18},{"name":"小牛","age":24}]";
// 解析為 List<Student>
List<Student> studentList = JSON.parseArray(studentListStr, Student.class);
// 定義解析字串
String studentMapStr = "{"name":"小明","age":18}";
// 解析為 Map<String,String>
Map<String, String> stringStringMap = 
JSONObject.parseObject(studentMapStr, new TypeReference<Map<String, String>>(){});

fastjson 就介紹到這裡,這裡只是介紹了簡單的使用,更加詳細的用法請參考官方的檔案,裡面還有更多的用法等著你的哦~~


HTTP 網路請求工具

除了 JSON 工具,作為一個優秀的網際網路打工人,不學會網路請求,怎麼能夠在這個行業佔有一席之地呢?HTTP 網路請求工具你值得擁有~~ 根據我的個人意願,我簡單介紹 httpclient的用法,因為我對這個比較熟悉

3.png

1、maven 匯入 pom 座標

2、如何使用

  • GET 請求(無參)
/**
* 無參的 GET 請求
*/
public static void noArgsGetHttp() throws IOException {
    // 定義 httpclient
    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
    // 建立 httpGet
    HttpGet httpGet = new HttpGet("http://www.baidu.com");
    // 定義返回結果
    CloseableHttpResponse execute = null;
    // 傳送執行
    execute = httpClient.execute(httpGet);
    // 獲取返回值
    HttpEntity entity = execute.getEntity();
    System.out.println("響應狀態為:" + execute.getStatusLine());
    if (Objects.nonNull(entity)) {
        System.out.println("響應內容長度為:" + entity.getContentLength());
        System.out.println("響應內容為:" + EntityUtils.toString(entity));
    }
    // 釋放資源
    if (httpClient != null) {
        httpClient.close();
    }
    if (execute != null) {
        execute.close();
    }
}

響應狀態為:HTTP/1.1 200 OK 響應內容長度為:-1 響應內容為:

  • GET 請求(有參)
 /**
* 有參的 GET 請求
*/
public static void haveArgsGetHttp() throws IOException, URISyntaxException {
    // 定義 httpclient
    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
    // 建立參數列
    List<NameValuePair> valueParamsList = new ArrayList<>();
    valueParamsList.add(new BasicNameValuePair("studentId","1"));
    // 建立對應請求 Uri
    URI uri = new URIBuilder()
        .setScheme("http")
        .setHost("localhost")
        .setPath("/getStudentInfo")
        .setParameters(valueParamsList)
        .build();
    // 根據 Uri 建立 httpGet
    HttpGet httpGet = new HttpGet(uri);
    // 定義返回結果
    CloseableHttpResponse execute = null;
    // 傳送執行
    execute = httpClient.execute(httpGet);
    // 獲取返回值
    HttpEntity entity = execute.getEntity();
    System.out.println("響應狀態為:" + execute.getStatusLine());
    if (Objects.nonNull(entity)) {
        System.out.println("響應內容長度為:" + entity.getContentLength());
        System.out.println("響應內容為:" + EntityUtils.toString(entity));
    }
    // 釋放資源
    if (httpClient != null) {
        httpClient.close();
    }
    if (execute != null) {
        execute.close();
    }
}
  • POST 請求(有引數)
/**
* post 有引數
*/
public static void haveArgsPosthttp() throws IOException {
    // 獲得Http使用者端(可以理解為:你得先有一個瀏覽器;注意:實際上HttpClient與瀏覽器是不一樣的)
    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
    // 建立Post請求
    HttpPost httpPost = new HttpPost("http://localhost:12345/posttest");
    JSONUtil.Student student = new JSONUtil.Student();
    student.setName("潘曉婷");
    student.setAge(18);

    String jsonString = JSON.toJSONString(student);
    StringEntity entity = new StringEntity(jsonString, "UTF-8");
    // post請求是將引數放在請求體裡面傳過去的;這裡將entity放入post請求體中
    httpPost.setEntity(entity);
    httpPost.setHeader("Content-Type", "application/json;charset=utf8");
    // 響應模型
    CloseableHttpResponse response = httpClient.execute(httpPost);
    // 從響應模型中獲取響應實體
    HttpEntity responseEntity = response.getEntity();
    System.out.println("響應狀態為:" + response.getStatusLine());
    if (responseEntity != null) {
        System.out.println("響應內容長度為:" + responseEntity.getContentLength());
        System.out.println("響應內容為:" + EntityUtils.toString(responseEntity));
    }
    // 釋放資源
    if (httpClient != null) {
        httpClient.close();
    }
    if (response != null) {
        response.close();
    }
}

啊!這樣一步一步總結下來好累啊,看到這裡的小夥伴們,點一下手裡的小贊。嘿嘿~~ 當然我只是簡單介紹一下 httpClient 的使用,具體高深的使用方法和設定可以參考其他博主的詳細介紹,讓我們介紹下一個常用的工具吧。


字串處理工具

字串使我們開發中最常見的型別,也是我們最常需要操作的型別了。如果不知道字串的常用工具,那在寫程式碼的時候簡直就是場災難!!!

字串判空,擷取字串、轉換大小寫、分隔字串、比較字串、去掉多餘空格、拼接字串、使用正規表示式等等。

StringUtils 給我們提供了非常豐富的選擇。我們著重以本類來介紹使用。

1、匯入maven座標

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

2、常用方法介紹

  • 字串判空 ⭐⭐⭐

(isNotBlankisBlanK)

String str = " ";
// 是否不為空
boolean res1 = StringUtils.isNotBlank(str);
// 是否為空
boolean res2 = StringUtils.isBlank(str);

(isNotEmptyisEmpty)

String str = " ";
// 是否不為空
boolean res1 = StringUtils.isNotEmpty(str);
// 是否為空
boolean res2 = StringUtils.isEmpty(str);

優先推薦使用 isBlankisNotBlank 方法,因為 它會把空字串也考慮進去

  • 分隔字串 —— split —— ⭐⭐⭐
String str2 = "1,2,3";
String[] split = StringUtils.split(str2);
System.out.println(Arrays.toString(split));

相較於 Stringsplit 方法來說,StringUtilssplit 方法不會有空指標異常

  • 判斷是否純數位 —— isNumeric —— ⭐⭐

給定一個字串,判斷它是否為純數位 可以使用isNumeric方法

String str3 = "1";
boolean numeric = StringUtils.isNumeric(str3);
System.out.println(numeric);

當然,這個工具類除了上面簡單的三個方法之外們還有其他很多對於我們來說很使用的方法,但是這裡就不一一舉例了, 有興趣的小夥伴們可以看原始碼統計一下


集合相關處理工具

哦吼~~看完了字串的常用工具,重中之重的集合它來了,如果說沒有字串,我們的程式就無法執行,那麼沒有集合,我們將會是每天在加班的中度過了,出去後將會自豪的說,老闆的車輪胎我也是做了貢獻的。

既然集合工具這麼重要,那麼當然要重點介紹。學會相關工具的使用,真的是能讓我們事半功倍的,真的是能讓摸魚時間大大增加的,不信你看看。

4.png

Collections

它是 JAVA 的集合工具包,內部包括了很多我們常用的方法,下圖展示了其中一些,方便食用!

5.png

  • 排序—— sort—— ⭐⭐⭐

在我們日常開發工作中,經常會遇到一些集合排序的需求。sort 就可以很好的幫助我們做好這一點

List<Integer> list = new ArrayList<>();
list.add(2);
list.add(1);
list.add(3);
//升序
Collections.sort(list);
System.out.println(list);
//降序
Collections.reverse(list);
System.out.println(list);

結果: [1, 2, 3] [3, 2, 1]

  • 獲取最大最小值 —— max / min ——⭐⭐⭐

最大值最小值是我們操作集合最常見的方法了吧!!Collections 就有現成的方法幫助我們實現

// 最大值最小值
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);

Integer max = Collections.max(intList);
Integer min = Collections.min(intList);

System.out.println("集合元素最大值:" + max);
System.out.println("集合元素最小值:" + min);

結果:集合元素最大值:3 集合元素最小值:1

  • 轉換執行緒安全集合—— synchronizedList ——⭐⭐⭐

在多執行緒狀態下,普通集合會產生並行問題,比如 ArrayList 並行新增會產生空值情況,這時我們又不想改動我們之前的集合怎麼辦? 我們簡單的通過Collections 的執行緒安全轉化就可以做到了,簡簡單單一行程式碼就可以做到!是不是方便的很!

List<Integer> synchronizedList = Collections.synchronizedList(intList);

當然,Collections 還有很多有用和有趣的方法等著我們去探索,只是只是作為了一個拋轉引玉的效果,就不過多的贅述了。

CollectionUtils

我最常用的便是 CollecionUtils,它是 apache 開源的工具包。它的功能可以說是相當強大的,不信你可以往下看看,反正你能想到的集合操作基本上它都有。

6.png

  • 集合判空—— isNotEmpty——⭐⭐⭐

我們最常用的集合方法,沒有之一,必須掌握它!!

List<String> stringList = new ArrayList<>();
boolean notEmpty = CollectionUtils.isNotEmpty(stringList);
System.out.println("集合不是空的嗎?"+ notEmpty);

集合不是空的嗎?false

  • 交集/並集/補集/差集——⭐⭐⭐

在開發中,經常需要將多集合進行交併補等的數學操作,不會還是傻傻的子集寫回圈處理吧!那樣還能有摸魚的時間嗎?下面就是大大提升效率的工具!!!

List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
List<Integer> list2 = new ArrayList<>();
list2.add(3);
list2.add(4);
// 獲取並集
Collection<Integer> union = CollectionUtils.union(list1, list2);
System.out.println(union);

// 獲取交集
Collection<Integer> intersection = CollectionUtils.intersection(list1, list2);
System.out.println(intersection);

//獲取交集的補集
Collection<Integer> disjunctionList = CollectionUtils.disjunction(list1, list2);
System.out.println(disjunctionList);

// 獲取差集
Collection<Integer> subtract = CollectionUtils.subtract(list1, list2);
System.out.println(subtract);

兩集合並集: [1, 2, 3, 4]

兩集合交集: [3]

兩集合交集的補集:[1, 2, 4]

兩集合差集: [1, 2]

  • 判斷兩集合是否相等——isEqualCollection——⭐⭐⭐
// 判斷兩集合是否相等
List<Integer> list3 = new ArrayList<>();
list1.add(3);
list1.add(4);
List<Integer> list4 = new ArrayList<>();
list2.add(3);
list2.add(4);
boolean equalCollection = CollectionUtils.isEqualCollection(list3, list4);
System.out.println("兩集合相等嗎?:" + equalCollection);

兩集合相等嗎?:true

Lists

最後在集合的工具類中再補充一個 google 官方的java包,裡面有很多我們想不到的超級方便的小工具,既然是說集合,我們說一下它裡面的 Lists 類,也是超級好用的!

7.png

  • 快速初始化集合——newArrayList——⭐⭐⭐

相信大家在開發中,都有過初始化集合的需要吧!那麼我們一般都是 新建一個 ArrayList 然後一個一個 add 進去,現在告訴大家,不用這麼麻煩,一個方法新建帶初始化全搞定,真香警告!!

// 快速初始化集合
ArrayList<Integer> integers1 = Lists.newArrayList(1, 2, 3);
System.out.println(integers1);

[1, 2, 3]

  • 集合分頁——partition——⭐⭐⭐

有時候我們想將我們的大集合分散成為一些小集合,我們又不想手動操作怎麼辦?這些痛點肯定已經有人幫助我們想好了!!來來來,介紹一下!!

// 陣列分頁
ArrayList<Integer> list7 = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<List<Integer>> partition = Lists.partition(list7, 5);
System.out.println(partition);

[ [1, 2, 3, 4, 5], [6, 7, 8, 9, 10] ]

介紹完了 集合相關的操作類,下面我們也要介紹一下檔案流相關操作,這個操作在開發中也是經常用到的。


檔案流處理工具

相信我們在工作已經厭煩了,經常寫** IO 流相關的類了吧。經常簡單的讀取和寫入一個檔案,我們需要大費周章的去定義一些 InputStream OutputStream **等,感覺有一種殺雞用牛刀的錯覺。 介紹一個檔案操作工具類,省去大量操作和關閉行為,超級好用,平常我可是經常用的。

8.png

  • 將資訊寫入檔案——writeByteArrayToFile——⭐⭐⭐

最最常用的操作,怎麼樣很簡單吧,就只用一行程式碼,秒殺!

// 將 test 寫入 test.txt 檔案
FileUtils.writeByteArrayToFile(new File("C:\Users\test.txt"), "test".getBytes());
  • 從檔案讀取資訊——readFileToByteArray——⭐⭐⭐

知道了怎麼往檔案裡寫東西,他的好兄弟讀取我們也得知道啊!

// 讀取 test.txt 檔案
byte[] bytes = FileUtils.readFileToByteArray(new File("D:\Users\test.txt"));
System.out.println("讀取到的文字為:" + new String(bytes));

讀取到的文字為:test

API 很多,我也不能一一為大家介紹,深入的瞭解還需要大家去,熟練地運用起來,並且是不是的去看看官方的檔案,查漏補缺,相信你也可以見識到很多讓你歎為觀止的方法。


加解密工具類

平常我們經常會遇到比如對使用者的密碼進行加密 (MD5) ,校驗介面的簽名 (sha256) 加密等等 用到加密的場景雖然不是很多,但是有這樣的工具何樂而不為呢?

因為常用的加密方法並不多,這裡介紹兩個方法給大家,想知道其他更多用法的夥伴們,去自己探索吧

9.png

// MD5 加密
String MD5 = DigestUtils.md2Hex("123");
System.out.println("加密後的結果:" + MD5);

//  sha(安全雜湊演演算法) 加密
String sha256Hex = DigestUtils.sha256Hex("123");
System.out.println("sha256加密後:" + sha256Hex);

MD5加密後的結果:ef1fedf5d32ead6b7aaf687de4ed1b71

sha256加密後:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e3


JAVA bean 物件轉換工具

說到這裡,我們來說一下物件轉化的工具,在開發中有些規範,比如DTO、DO、VO等等,之間,如果我們需要轉換,單純的我們要一個一個的 set 值,真是一項苦逼的活。

10.png

BeanUtils

java bean物件的相關轉化,我在這裡介紹兩個 ,一個 是大家都非常熟悉的 BeanUtils,還有一個就是我平常在開發中經常使用的 MapStructBeanUtils 最常用的莫過於物件的的拷貝了 。 不過面對需要深拷貝的物件大家要注意了,這裡並不推薦大家使用此工具去實現

Student student = new Student();
student.setName("小明");
student.setAge(18);
Student newStudent = new Student();
BeanUtils.copyProperties(student,newStudent);
System.out.println(newStudent);

Student(name=小明, age=18)


MapStruct

下面我們重點說一下 MapStruct 這個轉化,BeanUtils 就是一個大老粗,只能同屬性對映,或者在屬性相同的情況下,允許被對映的物件屬性少。

但當遇到被對映的屬性資料型別被修改或者被對映的欄位名被修改,則會導致對映失敗。而 mapstruct 就是一個巧媳婦兒了。

她心思細膩,把我們可能會遇到的情況都給考慮到了(要是我也能找一個這樣的媳婦兒該多好,內心笑出了豬聲)

1、首先啥都不用想上來我們先把該導的包導進去

<dependency>
	<groupId>org.mapstruct</groupId>
	<!-- jdk8以下就使用mapstruct -->
	<artifactId>mapstruct-jdk8</artifactId>
	<version>1.2.0.Final</version>
</dependency>
<dependency>
	<groupId>org.mapstruct</groupId>
	<artifactId>mapstruct-processor</artifactId>
	<version>1.2.0.Final</version>
</dependency>

2、用一下

  • 物件之間欄位完全相同

第一步:定義好我們的基礎類

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student{
	private String name;
	private Integer age ;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher{
	private String name;
	private Integer age ;
}

第二步:接下來定義一個介面,但是注意不需要實現,他就呢能夠幫我們轉化很神奇的

@Mapper
public interface UserCovertToTeacher {
    UserCovertToTeacher INSTANCE = Mappers.getMapper(UserCovertToTeacher.class);
    Teacher toCovert(Student student);
}

最後一步:在程式碼中呼叫,聰明的小夥伴看下面程式碼,一下就明白了,就是這麼簡單

Student student = new Student();
student.setName("小明");
student.setAge(18);
Teacher teacher = UserCovertToTeacher.INSTANCE.toCovert(student);
System.out.println(teacher);

Teacher(name=小明, age=18)

  • 物件之間欄位存在不相同情況

我們介紹了完全兩個類欄位相同的情況,那麼,更加令我們頭疼的 有多個欄位名字不同但是有對應關係應該怎麼搞呢?

我們進階介紹一下,如何處理這種引數名不同的對應關係 目前假設我們新定義一個微信類,我們的學生要註冊到微信上,我們就要將學生物件轉化為微信物件

@Data
@AllArgsConstructor
@NoArgsConstructor
static class WeiXin{
	private String username;
	private Integer age ;
}

但是我們發現和我們上面的學生類,的名字引數名不同,怎麼對應過去的? 答案就是在對方法上設定

@Mapper 
public interface UserCovertToWeinxin {
    UserCovertToWeinxin INSTANCE = Mappers.getMapper(UserCovertToWeinxin.class);
	// 設定欄位對映規則
    @Mapping(source = "name",target = "username")
    BeanUtilTest.WeiXin toCovert(BeanUtilTest.Student student);
}
Student student = new Student();
student.setName("小明");
student.setAge(18);
WeiXin weiXin = UserCovertToWeinxin.INSTANCE.toCovert(student);
System.out.println(weiXin);

WeiXin(username=小明, age=18)

這麼簡單的兩個小例子可包含不了 MapStruct這麼強大的功能,不管是日期格式化、還是表示式解析、還是深拷貝,都能一一搞定,只是限於本篇文章,無法跟大家說了。想想都很傷心呢! 但是還是那句話,拋磚引玉麼!剩下的就交給聰明的小夥伴們了!


快取和限流器工具

最後一小節,我給大家帶來我的珍藏,壓箱底的東西要拿出來了,大家還不快快點贊收藏,記好,錯過了,可就沒有下家店了。

我也是在 guava 中發現了很多好用的工具的 首先介紹快取工具,開發中,我們常用的記憶體快取,也就常常是定義一個 Map 去存放,但是單純的 Map 只能存和取,確無法做到,快取過期、快取淘汰,和相關通知等等複雜操作。

我們有必要學習掌握一種工具,能夠 Cover 上面所有情況的快取工具,有需求就有工具類,永遠記住。!!!,Cache 登場了,還是老規矩,先看看它怎麼用吧。

Cache

定義一個簡單定時過期的快取

Cache<String, String> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .build();
// 放入快取
cache.put("小明","活著");
Thread.sleep(10000);
// 從快取拿取值
System.out.println(cache.getIfPresent("小明"));

null

看到沒,結果顯而易見,超過了快取時間,就會自己釋放。嘿嘿。

定義一個快取符合以下限制:

  • 限制存取並行
  • 設定初始化容量
  • 限制快取數量上限
Cache<String, String> cache = CacheBuilder.newBuilder()、
		    // 最大容量,超過按照 LRU 淘汰
                .maximumSize(100)
		    // 初始容量
                .initialCapacity(10)
		    // 並行等級 10
                .concurrencyLevel(10)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .build();

兩個小例子,大家看明白了沒有,真正的乾貨,還不趕緊用起來。

除了這個,一個限流器也是常常需要的,我們總不能自記去寫一個限流器吧,需要考慮的太多,效能還不行哎!那就用接下來介紹的這個工具

RateLimiter

限流器大家在並行場景下經常會遇到,最簡單的實現限流就是令牌桶演演算法,原理很簡單,但是具體實現是很複雜的,RateLimiter 幫助我們解決這一點,只需要呼叫簡單的 API 就能實現並行限流

定義限流器,每1秒鐘通過 1 個請求

RateLimiter rateLimiter = RateLimiter.create(1,1,TimeUnit.SECONDS);

並行兩個去獲取,看一下結果吧,是不是符合我們的預期

new Thread(()->{
	System.out.println("執行緒 1 獲取到執行許可權了嗎?" + rateLimiter.tryAcquire());
}).start();

new Thread(()->{
	System.out.println("執行緒 2 獲取到執行許可權了嗎?" + rateLimiter.tryAcquire());
}).start();

執行緒 1 獲取到執行許可權了嗎?true

執行緒 2 獲取到執行許可權了嗎?false

怎麼樣,是不是隻能有一個通過,簡單例子說明問題。 具體用法還得大家在實際開發中具體體會,筆者在這裡就不多BB了!!留著時間大家趕快去練習吧。爭取成為一個 API 呼叫高手。

修煉完成

經過上面這麼多的講解、案例和對知識的思考,相信大家已經初步掌握了執行緒池的使用方法和細節,以及對原理執行流程的掌握, 如果你覺得本文對你有一定的啟發,引起了你的思考。 點贊、轉發、收藏,下次你就能很快的找到我嘍!

(學習視訊分享:)

以上就是【整理分享】8種開發工具,提升工作效率,再也不做加班人!的詳細內容,更多請關注TW511.COM其它相關文章!