乾貨|一文讀懂 Spring Data Jpa!

有不少讀者留言但願鬆哥能好好聊聊 Spring Data Jpa!其實這個話題鬆哥之前零零散散的介紹過,在個人書裏也有介紹過,可是在公衆號中還沒和大夥聊過,所以本文就和你們來仔細聊聊 Spring Data 和 Jpa!java

故事的主角

Jpa

1. JPA是什麼
  1. Java Persistence API:用於對象持久化的 API
  2. Java EE 5.0 平臺標準的 ORM 規範,使得應用程序以統一的方式訪問持久層

圖片描述

2. JPA和Hibernate的關係
  1. JPA 是 Hibernate 的一個抽象(就像JDBC和JDBC驅動的關係);
  2. JPA 是規範:JPA 本質上就是一種 ORM 規範,不是ORM 框架,這是由於 JPA 並未提供 ORM 實現,它只是制訂了一些規範,提供了一些編程的 API 接口,但具體實現則由 ORM 廠商提供實現;
  3. Hibernate 是實現:Hibernate 除了做爲 ORM 框架以外,它也是一種 JPA 實現
  4. 從功能上來講, JPA 是 Hibernate 功能的一個子集
3. JPA的供應商

JPA 的目標之一是制定一個能夠由不少供應商實現的 API,Hibernate 3.2+、TopLink 10.1+ 以及 OpenJPA 都提供了 JPA 的實現,Jpa 供應商有不少,常見的有以下四種:mysql

  1. Hibernate

JPA 的始做俑者就是 Hibernate 的做者,Hibernate 從 3.2 開始兼容 JPA。spring

  1. OpenJPA

OpenJPA 是 Apache 組織提供的開源項目。sql

  1. TopLink

TopLink 之前須要收費,現在開源了。數據庫

  1. EclipseLink
4. JPA的優點
  1. 標準化: 提供相同的 API,這保證了基於JPA 開發的企業應用可以通過少許的修改就可以在不一樣的 JPA 框架下運行。
  2. 簡單易用,集成方便: JPA 的主要目標之一就是提供更加簡單的編程模型,在 JPA 框架下建立實體和建立 Java 類同樣簡單,只須要使用 javax.persistence.Entity 進行註解;JPA 的框架和接口也都很是簡單。
  3. 可媲美JDBC的查詢能力: JPA的查詢語言是面向對象的,JPA定義了獨特的JPQL,並且可以支持批量更新和修改、JOIN、GROUP BY、HAVING 等一般只有 SQL 纔可以提供的高級查詢特性,甚至還可以支持子查詢。
  4. 支持面向對象的高級特性: JPA 中可以支持面向對象的高級特性,如類之間的繼承、多態和類之間的複雜關係,最大限度的使用面向對象的模型
5. JPA包含的技術
  1. ORM 映射元數據:JPA 支持 XML 和 JDK 5.0 註解兩種元數據的形式,元數據描述對象和表之間的映射關係,框架據此將實體對象持久化到數據庫表中。
  2. JPA 的 API:用來操做實體對象,執行CRUD操做,框架在後臺完成全部的事情,開發者從繁瑣的 JDBC 和 SQL 代碼中解脫出來。
  3. 查詢語言(JPQL):這是持久化操做中很重要的一個方面,經過面向對象而非面向數據庫的查詢語言查詢數據,避免程序和具體的 SQL 緊密耦合。

Spring Data

Spring Data 是 Spring 的一個子項目。用於簡化數據庫訪問,支持NoSQL 和 關係數據存儲。其主要目標是使數據庫的訪問變得方便快捷。Spring Data 具備以下特色:express

  1. SpringData 項目支持 NoSQL 存儲:
    MongoDB (文檔數據庫)
    Neo4j(圖形數據庫)
    Redis(鍵/值存儲)
    Hbase(列族數據庫)
  2. SpringData 項目所支持的關係數據存儲技術:
    JDBC
    JPA
  3. Spring Data Jpa 致力於減小數據訪問層 (DAO) 的開發量. 開發者惟一要作的,就是聲明持久層的接口,其餘都交給 Spring Data JPA 來幫你完成!
  4. 框架怎麼可能代替開發者實現業務邏輯呢?好比:當有一個 UserDao.findUserById() 這樣一個方法聲明,大體應該能判斷出這是根據給定條件的 ID 查詢出知足條件的 User 對象。Spring Data JPA 作的即是規範方法的名字,根據符合規範的名字來肯定方法須要實現什麼樣的邏輯。

主角的故事

Jpa 的故事

爲了讓大夥完全把這兩個東西學會,這裏我就先來介紹單純的Jpa使用,而後咱們再結合 Spring Data 來看 Jpa如何使用。 編程

總體步驟以下:微信

  1. 使用 IntelliJ IDEA 建立項目,建立時選擇 JavaEE Persistence ,以下:

圖片描述

  1. 建立成功後,添加依賴jar,因爲 Jpa 只是一個規範,所以咱們說用Jpa實際上必然是用Jpa的某一種實現,那麼是哪種實現呢?固然就是Hibernate了,因此添加的jar,實際上來自 Hibernate,以下:

圖片描述

  1. 添加實體類

接下來在項目中添加實體類,以下:架構

@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註解,去配置字段的名稱,長度,是否爲空等等。app

  1. 建立 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&amp;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信息
  1. 執行持久化操做
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
  1. JPQL語言,即 Java Persistence Query Language 的簡稱。JPQL 是一種和 SQL 很是相似的中間性和對象化查詢語言,它最終會被編譯成針對不一樣底層數據庫的 SQL 查詢,從而屏蔽不一樣數據庫的差別。JPQL語言的語句能夠是 select 語句、update 語句或delete語句,它們都經過 Query 接口封裝執行。
  2. Query接口封裝了執行數據庫查詢的相關方法。調用 EntityManager 的 createQuery、create NamedQuery 及 createNativeQuery 方法能夠得到查詢對象,進而可調用 Query 接口的相關方法來執行查詢操做。
  3. 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]

其中:

  1. from 子句是查詢語句的必選子句。
  2. select 用來指定查詢返回的結果實體或實體的某些屬性。
  3. from 子句聲明查詢源實體類,並指定標識符變量(至關於SQL表的別名)。
  4. 若是不但願返回重複實體,可以使用關鍵字 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 呢?

  1. Repository 接口是 Spring Data 的一個核心接口,它不提供任何方法,開發者須要在本身定義的接口中聲明須要的方法 public interface Repository<T, ID extends Serializable> { }
  2. 若咱們定義的接口繼承了 Repository, 則該接口會被 IOC 容器識別爲一個 Repository Bean,進而歸入到 IOC 容器中,進而能夠在該接口中定義知足必定規範的方法。
  3. Spring Data可讓咱們只定義接口,只要遵循 Spring Data 的規範,就無需寫實現類。
  4. 與繼承 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);
  • 支持屬性的級聯查詢. 若當前類有符合條件的屬性, 則優先使用, 而不使用級聯屬性. 若須要使用級聯屬性, 則屬性之間使用 _ 進行鏈接.

查詢舉例:

  1. 按照id查詢
User getUserById(Long id);
User getById(Long id);
  1. 查詢全部年齡小於90歲的人
List<User> findByAgeLessThan(Long age);
  1. 查詢全部姓趙的人
List<User> findByUsernameStartingWith(String u);
  1. 查詢全部姓趙的、而且id大於50的人
List<User> findByUsernameStartingWithAndIdGreaterThan(String name, Long id);
  1. 查詢全部姓名中包含"上"字的人
List<User> findByUsernameContaining(String name);
  1. 查詢全部姓趙的或者年齡大於90歲的
List<User> findByUsernameStartingWithOrAgeGreaterThan(String name, Long age);
  1. 查詢全部角色爲1的用戶
List<User> findByRole_Id(Long id);
2.支持的關鍵字

支持的查詢關鍵字以下圖:

圖片描述

3.查詢方法流程解析

爲何寫上方法名,JPA就知道你想幹嗎了呢?假如建立以下的查詢:findByUserDepUuid(),框架在解析該方法時,首先剔除 findBy,而後對剩下的屬性進行解析,假設查詢實體爲Doc:

  1. 先判斷 userDepUuid (根據 POJO 規範,首字母變爲小寫)是否爲查詢實體的一個屬性,若是是,則表示根據該屬性進行查詢;若是沒有該屬性,繼續第二步;
  2. 從右往左截取第一個大寫字母開頭的字符串(此處爲Uuid),而後檢查剩下的字符串是否爲查詢實體的一個屬性,若是是,則表示根據該屬性進行查詢;若是沒有該屬性,則重複第二步,繼續從右往左截取;最後假設 user 爲查詢實體的一個屬性;
  3. 接着處理剩下部分(DepUuid),先判斷 user 所對應的類型是否有depUuid屬性,若是有,則表示該方法最終是根據 「 Doc.user.depUuid」 的取值進行查詢;不然繼續按照步驟 2 的規則從右往左截取,最終表示根據 「Doc.user.dep.uuid」 的值進行查詢。
  4. 可能會存在一種特殊狀況,好比 Doc包含一個 user 的屬性,也有一個 userDep 屬性,此時會存在混淆。能夠明確在屬性之間加上 "_" 以顯式表達意圖,好比 "findByUser_DepUuid()" 或者 "findByUserDep_uuid()"
  5. 還有一些特殊的參數:例如分頁或排序的參數:
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. 利用下標索引傳參,索引參數以下所示,索引值從1開始,查詢中 」?X」 個數須要與方法定義的參數個數相一致,而且順序也要一致:
@Query("select u from t_user u where id>?1 and username like ?2")
List<User> selectUserByParam(Long id, String name);
  1. 命名參數(推薦):這種方式能夠定義好參數名,賦值時採用@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);

注意:

  1. 能夠經過自定義的 JPQL 完成 UPDATE 和 DELETE 操做. 注意: JPQL 不支持使用 INSERT
  2. 方法的返回值應該是 int,表示更新語句所影響的行數
  3. 在調用的地方必須加事務,沒有事務不能正常執行
  4. 默認狀況下, Spring Data 的每一個方法上有事務, 但都是一個只讀事務. 他們不能完成修改操做

說到這裏,再來順便說說Spring Data 中的事務問題:

  1. Spring Data 提供了默認的事務處理方式,即全部的查詢均聲明爲只讀事務。
  2. 對於自定義的方法,如需改變 Spring Data 提供的事務默認方式,能夠在方法上添加 @Transactional 註解。
  3. 進行多個 Repository 操做時,也應該使它們在同一個事務中處理,按照分層架構的思想,這部分屬於業務邏輯層,所以,須要在Service 層實現對多個 Repository 的調用,並在相應的方法上聲明事務。

好了,關於Spring Data Jpa 本文就先說這麼多,這一塊,鬆哥有一些私藏多年的筆記和視頻,以下圖:

圖片描述

圖片描述

那麼這些資料如何獲取呢?如下兩個條件知足其一便可:

  1. 關注牧碼小子公衆號,將本文對應的公衆號文章分享到朋友圈,不能夠設置分組,三天後截圖發給鬆哥,資料免費送你。
  2. 關注牧碼小子公衆號,將本文對應的公衆號文章分享到一個超過200人的微信羣中(QQ羣不算,鬆哥是羣主的微信羣也不算,羣要爲Java方向的羣),若是沒有這麼大的羣,也能夠分享到多個Java方向的微信羣,羣累計人數達到200便可。

以上條件知足其一,加鬆哥微信,給你資料。

圖片描述

更多微服務資料,請關注公衆號牧碼小子,關注後回覆 Java,領取鬆哥爲大夥精心準備的 Java 乾貨!

圖片描述

相關文章
相關標籤/搜索