有很多讀者留言希望鬆哥能好好聊聊 Spring Data Jpa!其實這個話題鬆哥以前零零散散的介紹過,在我的書裡也有介紹過,但是在公眾號中還沒和大夥聊過,因此本文就和大家來仔細聊聊 Spring Data 和 Jpa!
故事的主角
Jpa
1. JPA是什麼
- Java Persistence API:用於物件持久化的 API
- Java EE 5.0 平臺標準的 ORM 規範,使得應用程式以統一的方式訪問持久層
2. JPA和Hibernate的關係
- JPA 是 Hibernate 的一個抽象(就像JDBC和JDBC驅動的關係);
- JPA 是規範:JPA 本質上就是一種 ORM 規範,不是ORM 框架,這是因為 JPA 並未提供 ORM 實現,它只是制訂了一些規範,提供了一些程式設計的 API 介面,但具體實現則由 ORM 廠商提供實現;
- Hibernate 是實現:Hibernate 除了作為 ORM 框架之外,它也是一種 JPA 實現
- 從功能上來說, JPA 是 Hibernate 功能的一個子集
3. JPA的供應商
JPA 的目標之一是制定一個可以由很多供應商實現的 API,Hibernate 3.2+、TopLink 10.1+ 以及 OpenJPA 都提供了 JPA 的實現,Jpa 供應商有很多,常見的有如下四種:
- Hibernate
JPA 的始作俑者就是 Hibernate 的作者,Hibernate 從 3.2 開始相容 JPA。 - OpenJPA
OpenJPA 是 Apache 組織提供的開源專案。 - TopLink
TopLink 以前需要收費,如今開源了。 - EclipseLink
4. JPA的優勢
- 標準化: 提供相同的 API,這保證了基於JPA 開發的企業應用能夠經過少量的修改就能夠在不同的 JPA 框架下執行。
- 簡單易用,整合方便: JPA 的主要目標之一就是提供更加簡單的程式設計模型,在 JPA 框架下建立實體和建立 Java 類一樣簡單,只需要使用 javax.persistence.Entity 進行註解;JPA 的框架和介面也都非常簡單。
- 可媲美JDBC的查詢能力: JPA的查詢語言是物件導向的,JPA定義了獨特的JPQL,而且能夠支援批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能夠提供的高階查詢特性,甚至還能夠支援子查詢。
- 支援物件導向的高階特性: JPA 中能夠支援物件導向的高階特性,如類之間的繼承、多型和類之間的複雜關係,最大限度的使用物件導向的模型
5. JPA包含的技術
- ORM 對映後設資料:JPA 支援 XML 和 JDK 5.0 註解兩種後設資料的形式,後設資料描述物件和表之間的對映關係,框架據此將實體物件持久化到資料庫表中。
- JPA 的 API:用來操作實體物件,執行CRUD操作,框架在後臺完成所有的事情,開發者從繁瑣的 JDBC 和 SQL 程式碼中解脫出來。
- 查詢語言(JPQL):這是持久化操作中很重要的一個方面,通過物件導向而非面向資料庫的查詢語言查詢資料,避免程式和具體的 SQL 緊密耦合。
Spring Data
Spring Data 是 Spring 的一個子專案。用於簡化資料庫訪問,支援NoSQL 和 關係資料儲存。其主要目標是使資料庫的訪問變得方便快捷。Spring Data 具有如下特點:
- SpringData 專案支援 NoSQL 儲存:
MongoDB (文件資料庫)
Neo4j(圖形資料庫)
Redis(鍵/值儲存)
Hbase(列族資料庫) - SpringData 專案所支援的關係資料儲存技術:
JDBC
JPA - Spring Data Jpa 致力於減少資料訪問層 (DAO) 的開發量. 開發者唯一要做的,就是宣告持久層的介面,其他都交給 Spring Data JPA 來幫你完成!
- 框架怎麼可能代替開發者實現業務邏輯呢?比如:當有一個 UserDao.findUserById() 這樣一個方法宣告,大致應該能判斷出這是根據給定條件的 ID 查詢出滿足條件的 User 物件。Spring Data JPA 做的便是規範方法的名字,根據符合規範的名字來確定方法需要實現什麼樣的邏輯。
主角的故事
Jpa 的故事
為了讓大夥徹底把這兩個東西學會,這裡我就先來介紹單純的Jpa使用,然後我們再結合 Spring Data 來看 Jpa如何使用。
整體步驟如下:
- 使用 IntelliJ IDEA 建立專案,建立時選擇 JavaEE Persistence ,如下:
- 建立成功後,新增依賴jar,由於 Jpa 只是一個規範,因此我們說用Jpa實際上必然是用Jpa的某一種實現,那麼是哪一種實現呢?當然就是Hibernate了,所以新增的jar,實際上來自 Hibernate,如下:
- 新增實體類
接下來在專案中新增實體類,如下:
@Entity(name = "t_book")
public class Book {
private Long id;
private String name;
private String author;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
// 省略其他getter/setter
}
複製程式碼
首先@Entity註解表示這是一個實體類,那麼在專案啟動時會自動針對該類生成一張表,預設的表名為類名,@Entity註解的name屬性表示自定義生成的表名。@Id註解表示這個欄位是一個id,@GeneratedValue註解表示主鍵的自增長策略,對於類中的其他屬性,預設都會根據屬性名在表中生成相應的欄位,欄位名和屬性名相同,如果開發者想要對欄位進行定製,可以使用@Column註解,去配置欄位的名稱,長度,是否為空等等。
- 建立 persistence.xml 檔案
JPA 規範要求在類路徑的 META-INF 目錄下放置persistence.xml,檔案的名稱是固定的
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<persistence-unit name="NewPersistenceUnit" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>org.sang.Book</class>
<properties>
<property name="hibernate.connection.url"
value="jdbc:mysql:///jpa01?useUnicode=true&characterEncoding=UTF-8"/>
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
<property name="hibernate.connection.username" value="root"/>
<property name="hibernate.connection.password" value="123"/>
<property name="hibernate.archive.autodetection" value="class"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
複製程式碼
注意:
- persistence-unit 的name 屬性用於定義持久化單元的名字, 必填。
- transaction-type:指定 JPA 的事務處理策略。RESOURCE_LOCAL:預設值,資料庫級別的事務,只能針對一種資料庫,不支援分散式事務。如果需要支援分散式事務,使用JTA:transaction-type="JTA"
- class節點表示顯式的列出實體類
- properties中的配置分為兩部分:資料庫連線資訊以及Hibernate資訊
- 執行持久化操作
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("NewPersistenceUnit");
EntityManager manager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
transaction.begin();
Book book = new Book();
book.setAuthor("羅貫中");
book.setName("三國演義");
manager.persist(book);
transaction.commit();
manager.close();
entityManagerFactory.close();
複製程式碼
這裡首先根據配置檔案建立出來一個 EntityManagerFactory ,然後再根據 EntityManagerFactory 的例項建立出來一個 EntityManager ,然後再開啟事務,呼叫 EntityManager 中的 persist 方法執行一次持久化操作,最後提交事務,執行完這些操作後,資料庫中舊多出來一個 t_book 表,並且表中有一條資料。
關於 JPQL
- JPQL語言,即 Java Persistence Query Language 的簡稱。JPQL 是一種和 SQL 非常類似的中間性和物件化查詢語言,它最終會被編譯成針對不同底層資料庫的 SQL 查詢,從而遮蔽不同資料庫的差異。JPQL語言的語句可以是 select 語句、update 語句或delete語句,它們都通過 Query 介面封裝執行。
- Query介面封裝了執行資料庫查詢的相關方法。呼叫 EntityManager 的 createQuery、create NamedQuery 及 createNativeQuery 方法可以獲得查詢物件,進而可呼叫 Query 介面的相關方法來執行查詢操作。
- Query介面的主要方法如下:
- int executeUpdate(); | 用於執行update或delete語句。
- List getResultList(); | 用於執行select語句並返回結果集實體列表。
- Object getSingleResult(); | 用於執行只返回單個結果實體的select語句。
- Query setFirstResult(int startPosition); | 用於設定從哪個實體記錄開始返回查詢結果。
- Query setMaxResults(int maxResult); | 用於設定返回結果實體的最大數。與setFirstResult結合使用可實現分頁查詢。
- Query setFlushMode(FlushModeType flushMode); | 設定查詢物件的Flush模式。引數可以取2個列舉值:FlushModeType.AUTO 為自動更新資料庫記錄,FlushMode Type.COMMIT 為直到提交事務時才更新資料庫記錄。
- setHint(String hintName, Object value); | 設定與查詢物件相關的特定供應商引數或提示資訊。引數名及其取值需要參考特定 JPA 實現庫提供商的文件。如果第二個引數無效將丟擲IllegalArgumentException異常。
- setParameter(int position, Object value); | 為查詢語句的指定位置引數賦值。Position 指定引數序號,value 為賦給引數的值。
- setParameter(int position, Date d, TemporalType type); | 為查詢語句的指定位置引數賦 Date 值。Position 指定引數序號,value 為賦給引數的值,temporalType 取 TemporalType 的列舉常量,包括 DATE、TIME 及 TIMESTAMP 三個,,用於將 Java 的 Date 型值臨時轉換為資料庫支援的日期時間型別(java.sql.Date、java.sql.Time及java.sql.Timestamp)。
- setParameter(int position, Calendar c, TemporalType type); | 為查詢語句的指定位置引數賦 Calenda r值。position 指定引數序號,value 為賦給引數的值,temporalType 的含義及取捨同前。
- setParameter(String name, Object value); | 為查詢語句的指定名稱引數賦值。
- setParameter(String name, Date d, TemporalType type); | 為查詢語句的指定名稱引數賦 Date 值,用法同前。
- setParameter(String name, Calendar c, TemporalType type); | 為查詢語句的指定名稱引數設定Calendar值。name為引數名,其它同前。該方法呼叫時如果引數位置或引數名不正確,或者所賦的引數值型別不匹配,將丟擲 IllegalArgumentException 異常。
JPQL 舉例
和在 SQL 中一樣,JPQL 中的 select 語句用於執行查詢。其語法可表示為:
select_clause form_clause [where_clause] [groupby_clause] [having_clause] [orderby_clause]
其中:
- from 子句是查詢語句的必選子句。
- select 用來指定查詢返回的結果實體或實體的某些屬性。
- from 子句宣告查詢源實體類,並指定識別符號變數(相當於SQL表的別名)。
- 如果不希望返回重複實體,可使用關鍵字 distinct 修飾。select、from 都是 JPQL 的關鍵字,通常全大寫或全小寫,建議不要大小寫混用。
在 JPQL 中,查詢所有實體的 JPQL 查詢語句很簡單,如下:
select o from Order o 或 select o from Order as o
這裡關鍵字 as 可以省去,識別符號變數的命名規範與 Java 識別符號相同,且區分大小寫,呼叫 EntityManager 的 createQuery() 方法可建立查詢物件,接著呼叫 Query 介面的 getResultList() 方法就可獲得查詢結果集,如下:
Query query = entityManager.createQuery( "select o from Order o");
List orders = query.getResultList();
Iterator iterator = orders.iterator();
while(iterator.hasNext() ) {
// 處理Order
}
複製程式碼
其他方法的與此類似,這裡不再贅述。
Spring Data 的故事
在 Spring Boot 中,Spring Data Jpa 官方封裝了太多東西了,導致很多人用的時候不知道底層到底是怎麼配置的,本文就和大夥來看看在手工的Spring環境下,Spring Data Jpa要怎麼配置,配置完成後,用法和 Spring Boot 中的用法是一致的。
基本環境搭建
首先建立一個普通的Maven工程,並新增如下依賴:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.2.12.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.2.12.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.11.3.RELEASE</version>
</dependency>
</dependencies>
複製程式碼
這裡除了 Jpa 的依賴之外,就是Spring Data Jpa 的依賴了。
接下來建立一個 User 實體類,建立方式參考 Jpa中實體類的建立方式,這裡不再贅述。
接下來在resources目錄下建立一個applicationContext.xml檔案,並配置Spring和Jpa,如下:
<context:property-placeholder location="classpath:db.properties"/>
<context:component-scan base-package="org.sang"/>
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
<property name="driverClassName" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
<bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
<property name="packagesToScan" value="org.sang.model"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL57Dialect</prop>
</props>
</property>
</bean>
<bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- 配置jpa -->
<jpa:repositories base-package="org.sang.dao"
entity-manager-factory-ref="entityManagerFactory"/>
複製程式碼
這裡和 Jpa 相關的配置主要是三個,一個是entityManagerFactory,一個是Jpa的事務,還有一個是配置dao的位置,配置完成後,就可以在 org.sang.dao 包下建立相應的 Repository 了,如下:
public interface UserDao extends Repository<User, Long> {
User getUserById(Long id);
}
複製程式碼
getUserById表示根據id去查詢User物件,只要我們的方法名稱符合類似的規範,就不需要寫SQL,具體的規範一會來說。好了,接下來,建立 Service 和 Controller 來呼叫這個方法,如下:
@Service
@Transactional
public class UserService {
@Resource
UserDao userDao;
public User getUserById(Long id) {
return userDao.getUserById(id);
}
}
public void test1() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean(UserService.class);
User user = userService.getUserById(1L);
System.out.println(user);
}
複製程式碼
這樣,就可以查詢到id為1的使用者了。
Repository
上文我們自定義的 UserDao 實現了 Repository 介面,這個 Repository 介面是什麼來頭呢?
首先來看 Repository 的一個繼承關係圖:
可以看到,實現類不少。那麼到底如何理解 Repository 呢?
- Repository 介面是 Spring Data 的一個核心介面,它不提供任何方法,開發者需要在自己定義的介面中宣告需要的方法
public interface Repository<T, ID extends Serializable> { }
- 若我們定義的介面繼承了 Repository, 則該介面會被 IOC 容器識別為一個 Repository Bean,進而納入到 IOC 容器中,進而可以在該介面中定義滿足一定規範的方法。
- Spring Data可以讓我們只定義介面,只要遵循 Spring Data 的規範,就無需寫實現類。
- 與繼承 Repository 等價的一種方式,就是在持久層介面上使用 @RepositoryDefinition 註解,併為其指定 domainClass 和 idClass 屬性。像下面這樣:
@RepositoryDefinition(domainClass = User.class, idClass = Long.class)
public interface UserDao
{
User findById(Long id);
List<User> findAll();
}
複製程式碼
基礎的 Repository 提供了最基本的資料訪問功能,其幾個子介面則擴充套件了一些功能,它的幾個常用的實現類如下:
- CrudRepository: 繼承 Repository,實現了一組 CRUD 相關的方法
- PagingAndSortingRepository: 繼承 CrudRepository,實現了一組分頁排序相關的方法
- JpaRepository: 繼承 PagingAndSortingRepository,實現一組 JPA 規範相關的方法
- 自定義的 XxxxRepository 需要繼承 JpaRepository,這樣的 XxxxRepository 介面就具備了通用的資料訪問控制層的能力。
- JpaSpecificationExecutor: 不屬於Repository體系,實現一組 JPA Criteria 查詢相關的方法
方法定義規範
1.簡單條件查詢
- 按照 Spring Data 的規範,查詢方法以 find | read | get 開頭
- 涉及條件查詢時,條件的屬性用條件關鍵字連線,要注意的是:條件屬性以首字母大寫
例如:定義一個 Entity 實體類:
class User{
private String firstName;
private String lastName;
}
複製程式碼
使用And條件連線時,條件的屬性名稱與個數要與引數的位置與個數一一對應,如下:
findByLastNameAndFirstName(String lastName,String firstName);
複製程式碼
- 支援屬性的級聯查詢. 若當前類有符合條件的屬性, 則優先使用, 而不使用級聯屬性. 若需要使用級聯屬性, 則屬性之間使用 _ 進行連線.
查詢舉例:
- 按照id查詢
User getUserById(Long id);
User getById(Long id);
複製程式碼
- 查詢所有年齡小於90歲的人
List<User> findByAgeLessThan(Long age);
複製程式碼
- 查詢所有姓趙的人
List<User> findByUsernameStartingWith(String u);
複製程式碼
- 查詢所有姓趙的、並且id大於50的人
List<User> findByUsernameStartingWithAndIdGreaterThan(String name, Long id);
複製程式碼
- 查詢所有姓名中包含"上"字的人
List<User> findByUsernameContaining(String name);
複製程式碼
- 查詢所有姓趙的或者年齡大於90歲的
List<User> findByUsernameStartingWithOrAgeGreaterThan(String name, Long age);
複製程式碼
- 查詢所有角色為1的使用者
List<User> findByRole_Id(Long id);
複製程式碼
2.支援的關鍵字
支援的查詢關鍵字如下圖:
3.查詢方法流程解析
為什麼寫上方法名,JPA就知道你想幹嘛了呢?假如建立如下的查詢:findByUserDepUuid()
,框架在解析該方法時,首先剔除 findBy,然後對剩下的屬性進行解析,假設查詢實體為Doc:
- 先判斷 userDepUuid (根據 POJO 規範,首字母變為小寫)是否為查詢實體的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,繼續第二步;
- 從右往左擷取第一個大寫字母開頭的字串(此處為Uuid),然後檢查剩下的字串是否為查詢實體的一個屬性,如果是,則表示根據該屬性進行查詢;如果沒有該屬性,則重複第二步,繼續從右往左擷取;最後假設 user 為查詢實體的一個屬性;
- 接著處理剩下部分(DepUuid),先判斷 user 所對應的型別是否有depUuid屬性,如果有,則表示該方法最終是根據 “ Doc.user.depUuid” 的取值進行查詢;否則繼續按照步驟 2 的規則從右往左擷取,最終表示根據 “Doc.user.dep.uuid” 的值進行查詢。
- 可能會存在一種特殊情況,比如 Doc包含一個 user 的屬性,也有一個 userDep 屬性,此時會存在混淆。可以明確在屬性之間加上 "_" 以顯式表達意圖,比如 "findByUser_DepUuid()" 或者 "findByUserDep_uuid()"
- 還有一些特殊的引數:例如分頁或排序的引數:
Page<UserModel> findByName(String name, Pageable pageable);
List<UserModel> findByName(String name, Sort sort);
複製程式碼
@Query註解
有的時候,這裡提供的查詢關鍵字並不能滿足我們的查詢需求,這個時候就可以使用 @Query 關鍵字,來自定義查詢 SQL,例如查詢Id最大的User:
@Query("select u from t_user u where id=(select max(id) from t_user)")
User getMaxIdUser();
複製程式碼
如果查詢有引數的話,引數有兩種不同的傳遞方式,
- 利用下標索引傳參,索引引數如下所示,索引值從1開始,查詢中 ”?X” 個數需要與方法定義的引數個數相一致,並且順序也要一致:
@Query("select u from t_user u where id>?1 and username like ?2")
List<User> selectUserByParam(Long id, String name);
複製程式碼
- 命名引數(推薦):這種方式可以定義好引數名,賦值時採用@Param("引數名"),而不用管順序:
@Query("select u from t_user u where id>:id and username like :name")
List<User> selectUserByParam2(@Param("name") String name, @Param("id") Long id);
複製程式碼
查詢時候,也可以是使用原生的SQL查詢,如下:
@Query(value = "select * from t_user",nativeQuery = true)
List<User> selectAll();
複製程式碼
@Modifying註解
涉及到資料修改操作,可以使用 @Modifying 註解,@Query 與 @Modifying 這兩個 annotation一起宣告,可定義個性化更新操作,例如涉及某些欄位更新時最為常用,示例如下:
@Modifying
@Query("update t_user set age=:age where id>:id")
int updateUserById(@Param("age") Long age, @Param("id") Long id);
複製程式碼
注意:
- 可以通過自定義的 JPQL 完成 UPDATE 和 DELETE 操作. 注意: JPQL 不支援使用 INSERT
- 方法的返回值應該是 int,表示更新語句所影響的行數
- 在呼叫的地方必須加事務,沒有事務不能正常執行
- 預設情況下, Spring Data 的每個方法上有事務, 但都是一個只讀事務. 他們不能完成修改操作
說到這裡,再來順便說說Spring Data 中的事務問題:
- Spring Data 提供了預設的事務處理方式,即所有的查詢均宣告為只讀事務。
- 對於自定義的方法,如需改變 Spring Data 提供的事務預設方式,可以在方法上新增 @Transactional 註解。
- 進行多個 Repository 操作時,也應該使它們在同一個事務中處理,按照分層架構的思想,這部分屬於業務邏輯層,因此,需要在Service 層實現對多個 Repository 的呼叫,並在相應的方法上宣告事務。
好了,關於Spring Data Jpa 本文就先說這麼多,這一塊,鬆哥有一些私藏多年的筆記和視訊,如下圖:
那麼這些資料如何獲取呢?以下兩個條件滿足其一即可:
- 將本文分享到朋友圈,不可以設定分組,三天後截圖發給鬆哥,資料免費送你。
- 將本文分享到一個超過200人的微信群中(QQ群不算,鬆哥是群主的微信群也不算,群要為Java方向的群),如果沒有這麼大的群,也可以分享到多個Java方向的微信群,群累計人數達到200即可。
以上條件滿足其一,加鬆哥微信,給你資料。
更多微服務資料,請關注公眾號牧碼小子,關注後回覆 Java,領取鬆哥為大夥精心準備的 Java 乾貨!