如果你想開發(fā)一個(gè)應(yīng)用(1-11)

這一章開始的時(shí)候,先拿一個(gè)廣告圖鎮(zhèn)樓:

圖是網(wǎng)上隨便找的,哈哈好希望真的有路虎

這句廣告此很有意思,雖然腳踏實(shí)地的走路是最踏實(shí)的(jdbc),如果可以,當(dāng)然有輛自行車(JdbcTemplate)就更好了.但我相信,一輛能裝載,速度快,安全性高的路虎,是每個(gè)人心中的夢(mèng)想。

路虎

我們想要這樣一些能力:

  • 對(duì)象可以和數(shù)據(jù)庫(kù)字段自動(dòng)進(jìn)行映射
  • 自動(dòng)生成sql語(yǔ)句
  • 自動(dòng)完成查詢條件
  • 自動(dòng)生成級(jí)聯(lián)關(guān)系
  • 自動(dòng)管理數(shù)據(jù)庫(kù)緩存和延遲加載等

這些能力可以使我們從無(wú)休止的?中解脫出來(lái),那么有沒(méi)有這樣一種既簡(jiǎn)單,又方便的工具呢?Spring集成的JPA功能登場(chǎng)了。

JPA(Java Persistence API)Java持久性API,是用于對(duì)象/關(guān)系映射(ORM)的Java API,其中Java對(duì)象映射到數(shù)據(jù)庫(kù)工件,以便在java應(yīng)用程序中管理數(shù)據(jù)關(guān)系。JPA包括Java持久性查詢語(yǔ)言(JPQL),Java持久性標(biāo)準(zhǔn)API以及用于定義對(duì)象/關(guān)系映射元數(shù)據(jù)的Java API和XML模式。

需要再次強(qiáng)調(diào)一下,JPA不是orm,他僅僅是一套API標(biāo)準(zhǔn)。

Spring2開始集成了JPA功能,就像有一輛車之前需要駕照,使用JPA之前同樣需要引入JPA所依賴的包:

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>2.0.2.RELEASE</version>
</dependency>

然后我們就可以去4S點(diǎn)去試駕,或者選車,出去飛了。

試駕

引入JPA的依賴包之后開始對(duì)JPA進(jìn)行配置,而配置JPA的第一步就是要配置實(shí)體管理工廠的Bean,以獲取實(shí)體管理器,在JPA中定義了兩種實(shí)體管理工廠:

  • 應(yīng)用程序管理類型:程序向管理器工廠直接請(qǐng)求時(shí),會(huì)創(chuàng)建一個(gè)管理器,適合不在JavaEE容器中的應(yīng)用程序,需配置persistence.xml文件
  • 容器管理類型:應(yīng)用程序不和管理器工廠打交道,它的創(chuàng)建由容器負(fù)責(zé)。適合運(yùn)行在容器中的程序,可不需要配置persistence.xml文件

我們的程序即在JavaEE容器中運(yùn)行,有極力的想要全java配置,所以當(dāng)然選擇容器管理類型了,在Spring中使用LocalContainerEntityManagerFactoryBean的FactoryBean來(lái)配置實(shí)體管理器:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                   JpaVendorAdapter jpaVendorAdapter){
    LocalContainerEntityManagerFactoryBean lcemf=new LocalContainerEntityManagerFactoryBean();
    lcemf.setDataSource(dataSource);
    lcemf.setJpaVendorAdapter(jpaVendorAdapter);
    lcemf.setPackagesToScan("com.niufennan.jtodos.models");
    return lcemf;
}

注意這個(gè)Bean需要兩個(gè)參數(shù),分別為數(shù)據(jù)源和Jpa實(shí)現(xiàn)適配器,然后分別set到對(duì)象里,并且通過(guò)'setPackagesToScan'方法設(shè)置默認(rèn)掃描的實(shí)體包。

在這個(gè)bean的參數(shù)里,數(shù)據(jù)源即上一章設(shè)置的數(shù)據(jù)源,這里不在敘述,而JpaVendorAdapter是針對(duì)JPA不同的實(shí)現(xiàn),目前JPA的實(shí)現(xiàn)有很多種,主要有Hibernate,OpenJpa,EclipseJpa等,對(duì)于Spring-jpa的用戶來(lái)說(shuō),使用哪種實(shí)現(xiàn)在代碼上都無(wú)所謂,因?yàn)橐呀?jīng)在容器中透明了,這里我選擇了EclipseLinkJPA的實(shí)現(xiàn),首先還是引入依賴:

<dependency>
  <groupId>org.eclipse.persistence</groupId>
  <artifactId>org.eclipse.persistence.jpa</artifactId>
  <version>2.7.0</version>
</dependency>

然后增加jpaVendorAdapter的Bean:

@Bean
public JpaVendorAdapter jpaVendorAdapter(){
    EclipseLinkJpaVendorAdapter adapter=new EclipseLinkJpaVendorAdapter();
    adapter.setDatabase(Database.MYSQL);  //1
    adapter.setShowSql(true);             //2   
    adapter.setGenerateDdl(false);        //3   
    adapter.setDatabasePlatform(MySQLPlatform.class.getName());                                  //4
    return adapter;
}

1 設(shè)置訪問(wèn)的數(shù)據(jù)庫(kù)類型
2 設(shè)置在日志中輸出生成的SQL
3 設(shè)置是否根據(jù)數(shù)據(jù)實(shí)體生成修改數(shù)據(jù)庫(kù)結(jié)構(gòu),這里不修改
4 設(shè)置sql方言

然后,根據(jù)JPA實(shí)際的需求,我們還需要對(duì)實(shí)體類進(jìn)行一些改造,這里以Todo類為例,改造方式如下:

  1. 增加JPA所需的一些注解
  2. 將基本數(shù)據(jù)類型換成包裝類形式

改造完后代碼如下:

@Entity(name = "todos")
public class Todo {
    @Id
    private Integer id;
    private String item;
    private Date createTime=new Date();
    private Integer userId;
    get... set...
}

現(xiàn)在挑選完成,準(zhǔn)備起飛。

低配版##

為了和上一章的dao類區(qū)分,我們新創(chuàng)建一個(gè)persistence包,用來(lái)存放基于JPA實(shí)現(xiàn)的持久層類,首先,創(chuàng)建一個(gè)TodoRepository類,并在里定義三個(gè)方法,即將TodoDao接口的方法拷貝入內(nèi):

public interface TodoRepository {
    public List<Todo> getAll();
    public List<Todo> getTodoByUserId(int userId);
    public void save(Todo todo);
}

然后統(tǒng)一創(chuàng)建impl,作為接口的實(shí)現(xiàn),這里創(chuàng)建一個(gè)基于jpa實(shí)現(xiàn)的類:

public class JpaTodoRepository implements TodoRepository {
    public List<Todo> getAll() {
        return null;
    }
    public List<Todo> getTodoByUserId(int userId) {
        return null;
    }
    public void save(Todo todo) {

    }
}

下面完成這個(gè)類:

@Transactional
@Repository
public class JpaTodoRepository implements TodoRepository {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;
    public List<Todo> getAll() {
        CriteriaQuery<Todo> criteriaQuery=entityManagerFactory.createEntityManager().getCriteriaBuilder().createQuery(Todo.class);
        return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList();
    }

    public List<Todo> getTodoByUserId(int userId) {
        CriteriaBuilder builder=entityManagerFactory.createEntityManager().getCriteriaBuilder();
        CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class);
        Root<Todo> todoRoot = criteriaQuery.from(Todo.class);
        Predicate predicate = builder.equal(todoRoot.get("userId"), userId);
        criteriaQuery.where(predicate);
        return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList();
    }

    public void save(Todo todo) {
        entityManagerFactory.createEntityManager().persist(todo);
    }
}

我知道你想說(shuō)什么,看上去代碼好復(fù)雜,尤其是條件查詢的部分,這里先對(duì)條件查詢進(jìn)行一下說(shuō)明:

 CriteriaBuilder builder=entityManagerFactory.createEntityManager().getCriteriaBuilder();  //基于建造模式,構(gòu)建一個(gè)Criteria構(gòu)建器對(duì)象(基于Criteria模式進(jìn)行條件查詢)

 CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class);  //為Todo對(duì)象創(chuàng)建一個(gè)基礎(chǔ)查詢

 Root<Todo> todoRoot = criteriaQuery.from(Todo.class); //為基礎(chǔ)查詢?cè)O(shè)置一個(gè)查詢條件列表

 Predicate predicate = builder.equal(todoRoot.get("userId"), userId);  //通過(guò)userId進(jìn)行查詢

 criteriaQuery.where(predicate);

 return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList();  //設(shè)置 查詢條件并返回

其余的代碼很簡(jiǎn)單就不在敘述,接下來(lái)使用土土的測(cè)試方式,運(yùn)行一下,阿啊哦,報(bào)錯(cuò)了,查看一下報(bào)錯(cuò)信息(復(fù)制其中的一句):

Failed to load class "org.slf4j.impl.StaticLoggerBinder".

這是因?yàn)镋clipseLink默認(rèn)使用了slf4j的API記錄日志,所以之類需要添加對(duì)它的引用即可:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j-impl</artifactId>
  <version>2.9.1</version>
</dependency>

然后土土的跑起來(lái),測(cè)試一下,啊哦,還是有錯(cuò)誤,查看一下報(bào)錯(cuò)信息:

16:55:30.136 [RMI TCP Connection(5)-127.0.0.1] ERROR org.springframework.web.context.ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [com/niufennan/jtodos/config/DataBaseConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Cannot apply class transformer without LoadTimeWeaver specified

提示對(duì)LoadTimeWeaver的調(diào)用失敗,那么LoadTimeWeaver又是做什么用的呢?LoadTimeWeaver顧名思義,就是使用AspectJ提供在Aop中類加載時(shí)織入切片的能力。

那么如何使用LoadTimeWeaver呢?首先,需要通過(guò)JVM的-javaagent參數(shù)設(shè)置LTW的織入器類包,以代理JVM默認(rèn)的類加載器;第二,LTW織入器需要一個(gè) aop.xml文件,在該文件中指定切面類和需要進(jìn)行切面織入的目標(biāo)類。簡(jiǎn)單說(shuō),就是提供動(dòng)態(tài)代理的能力。我們可以使用注解:

@EnableLoadTimeWeaving( aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.DISABLED)

對(duì)他進(jìn)行關(guān)閉。

這時(shí)候運(yùn)行,ok 成功出現(xiàn)了我們需要的頁(yè)面。

但這樣顯然不是什么好主意,因?yàn)镾pring現(xiàn)在就是基于注解在使用的,而基于注解,肯定會(huì)不可避免的使用到動(dòng)態(tài)代理的織入,所以,將LTW禁用顯然是不合理的。所以,最簡(jiǎn)單的方法是,既然entityManagerFactory需要,那么給它就好了,修改entityManagerFactory的Bean:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                   JpaVendorAdapter jpaVendorAdapter){
    LocalContainerEntityManagerFactoryBean lcemf=new LocalContainerEntityManagerFactoryBean();
    lcemf.setDataSource(dataSource);
    lcemf.setJpaVendorAdapter(jpaVendorAdapter);
    lcemf.setPackagesToScan("com.niufennan.jtodos.models");
    lcemf.setLoadTimeWeaver(new InstrumentationLoadTimeWeaver());
    return lcemf;
}

最后將LoadTimeWeaver給set進(jìn)去,在運(yùn)行一下,還是報(bào)錯(cuò),查看一下報(bào)錯(cuò)信息:

Must start with Java agent to use InstrumentationLoadTimeWeaver

難道一定要修改java的啟動(dòng)參數(shù)么?當(dāng)然不是,進(jìn)入源碼看一看(此源碼為在Idea環(huán)境下直接雙擊進(jìn)入):

public void addTransformer(ClassFileTransformer transformer) {
    Assert.notNull(transformer, "Transformer must not be null");
    InstrumentationLoadTimeWeaver.FilteringClassFileTransformer actualTransformer = new InstrumentationLoadTimeWeaver.FilteringClassFileTransformer(transformer, this.classLoader);
    List var3 = this.transformers;
    synchronized(this.transformers) {
        Assert.state(this.instrumentation != null, "Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.");
        this.instrumentation.addTransformer(actualTransformer);
        this.transformers.add(actualTransformer);
    }
}

可以看到,這個(gè)錯(cuò)誤是在判斷儀表盤是否為空的時(shí)候產(chǎn)生的,而我們現(xiàn)在不需要這個(gè),所以完全可以把這個(gè)錯(cuò)誤隱藏掉,因此,創(chuàng)建一個(gè)擴(kuò)展類,覆蓋這點(diǎn)代碼:

public class ExtInstrumentationLoadTimeWeaver extends
        InstrumentationLoadTimeWeaver {
    @Override
    public void addTransformer(ClassFileTransformer transformer) {
        try {
            super.addTransformer(transformer);
        } catch (Exception e) {}
    }
}

然后修改setLoadTimeWeaver方法:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                   JpaVendorAdapter jpaVendorAdapter){
    LocalContainerEntityManagerFactoryBean lcemf=new LocalContainerEntityManagerFactoryBean();
    lcemf.setLoadTimeWeaver(new ExtInstrumentationLoadTimeWeaver( ));
    lcemf.setDataSource(dataSource);
    lcemf.setJpaVendorAdapter(jpaVendorAdapter);
    lcemf.setPackagesToScan("com.niufennan.jtodos.models");
    return lcemf;
}

這時(shí),土土的運(yùn)行一下,完全ok。

當(dāng)然,還可以在tomcat配置的地方為VM options設(shè)置參數(shù),-javaagent:spring-agent.jar的絕對(duì)路徑,因?yàn)樗褂昧私^對(duì)路徑,所以我很不喜歡。故不采用這種方法。

還可以使用一個(gè)更簡(jiǎn)單的方法,即換一個(gè)JavaEE的容器,如Jetty,因?yàn)檫@個(gè)Bug只在Tomcat中會(huì)出現(xiàn)(至少目前我只在Tomcat中發(fā)現(xiàn))

截止目前源代碼v1-11_1

中配版

折騰半天,終于開著低配版的路虎起飛了,但你可能也發(fā)現(xiàn)了:

  1. 代碼并沒(méi)有減少,甚至更加復(fù)雜
  2. 每次都調(diào)用entityManagerFactory.createEntityManager(),看著很不爽
  3. 同2,這意味著會(huì)創(chuàng)建很多EntityManager對(duì)象。

那么有沒(méi)有更方便的方法呢,就像換一輛中配的汽車?

當(dāng)然可以,可是有個(gè)大問(wèn)題就是EntityManager不是線程安全的,一般來(lái)說(shuō),不適合作為共享bean注入到Repository中,但是好在Spring依然為我們提供了方法:

@Transactional
@Repository
public class JpaTodoRepository implements TodoRepository {

    @PersistenceContext
    private EntityManager entityManager;
    public List<Todo> getAll() {
        CriteriaQuery<Todo> criteriaQuery=entityManager.getCriteriaBuilder().createQuery(Todo.class);
        return entityManager.createQuery(criteriaQuery).getResultList();
    }
    public List<Todo> getTodoByUserId(int userId) {
        CriteriaBuilder builder=entityManager.getCriteriaBuilder();
        CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class);
        Root<Todo> todoRoot = criteriaQuery.from(Todo.class);
        Predicate predicate = builder.equal(todoRoot.get("userId"), userId);
        criteriaQuery.where(predicate);
        return entityManager.createQuery(criteriaQuery).getResultList();
    }
    public void save(Todo todo) {
        entityManager.persist(todo);
    }
}

這里的關(guān)鍵就是@PersistenceContext,它的精彩之處是不沒(méi)有真的注入EntityManager,而是產(chǎn)生了一個(gè)代理(貌似Spring大量的使用了代理模式),然后真正的實(shí)體管理器始終是與當(dāng)前事物相關(guān)聯(lián)的那一個(gè),當(dāng)然如果不存在,則會(huì)重新創(chuàng)建一個(gè),這樣的話,就能始終保持他是線程安全的。

@PersistenceContext與@PersistenceUnit均不是Spring的注解,他是jpa的注解。

好,現(xiàn)在土土的運(yùn)行一下,發(fā)現(xiàn)中配版的路虎也可以起飛了。

截止目前源代碼v1-11_2

高配版路虎

中配版升級(jí)了實(shí)體管理器,實(shí)現(xiàn)了由容器自動(dòng)管理實(shí)體管理器的創(chuàng)建和使用,那么接下來(lái)看一下代碼,能不能升級(jí)一下查詢的方法體呢?

答案是當(dāng)然可以,甚至我們都可以只寫一個(gè)Repository的接口就可以了,繼續(xù)修改TodoRepository:

public interface TodoRepository extends JpaRepository<Todo,Integer> {
    public List<Todo> getTodoByUserId(int userId);
}

然后我們將此接口的實(shí)現(xiàn)刪除,土土的運(yùn)行一下,完全Ok。

這很令人驚訝,為什么,完全沒(méi)有實(shí)現(xiàn)類和任何的注解!實(shí)際上,因?yàn)門odoRepository繼承JpaRepository,而JpaRepository經(jīng)過(guò)一系列的繼承,最終繼承并擴(kuò)展了Repository接口,于是,Spring-Data框架會(huì)掃描定義包內(nèi)所有的Repository的子接口,并在應(yīng)用啟動(dòng)的時(shí)候創(chuàng)建他的實(shí)現(xiàn)類,而且實(shí)現(xiàn)類中會(huì)默認(rèn)包含CurdRepository等父接口所包含的18個(gè)方法。

一個(gè)非常令人驚嘆的技術(shù)。

通過(guò)JpaRepository提供的18個(gè)方法,幾乎可以進(jìn)行任何通用的操作,那么我的需求超過(guò)這些方法了怎么辦,比如getTodoByUserId方法

這里就牽扯到Spring-Data的另一個(gè)令人驚嘆的技術(shù),根據(jù)方法名與實(shí)體對(duì)像推斷方法的目的:

動(dòng)詞(get)--主題(Todo)--關(guān)鍵詞(by)--斷言(UserId)

根據(jù)這種組合,我們幾乎可以實(shí)現(xiàn)任何功能,如根據(jù)User獲取todo列表并更加創(chuàng)建時(shí)間排序:

getTodoByUserIdOrderByCreateTime

Spring-Data允許的動(dòng)詞:

get,read,find,count等

get,read,find沒(méi)有明顯差別。

由于此實(shí)現(xiàn)是基于泛型的,所以主題可以省略。

而斷言部分則是精華所在,非常的繁復(fù),靈活,幾乎支持所有的sql語(yǔ)句關(guān)鍵字,具體可以根據(jù)日志打印的sql語(yǔ)句與斷言匹配以練習(xí)。

截止目前源代碼v1-11_3

改裝車

一個(gè)無(wú)法改裝的越野車不是好越野車,當(dāng)我發(fā)現(xiàn)這些均無(wú)法滿足要求怎么辦?我查詢的sql語(yǔ)句無(wú)比復(fù)雜,斷言幾乎無(wú)法完成,那怎么辦呢?

這時(shí)候我們可以部分退化到中配版,但依然使用高配版的全自動(dòng)化,機(jī)創(chuàng)建一個(gè)實(shí)現(xiàn)類,但這個(gè)實(shí)現(xiàn)類按照約定命名,即Repository接口加impl后綴,(此類僅為舉例):

public class TodoRepositoryImpl implements ExtTodoRepository {
    @PersistenceContext
    private EntityManager entityManager;
    public List<Todo> getTodoByUserId(int userId) {
        String sql="select t from  com.niufennan.jtodos.models.Todo t where  t.userId=:userId";
        Query query= entityManager.createQuery(sql);
        query.setParameter("userId",userId);
        return query.getResultList();
    }
}

這里使用ExtTodoRepository接口是因?yàn)槿绻褂肨odoRepository接口的話,會(huì)要求實(shí)現(xiàn)所有的18個(gè)方法,ExtTodoRepository的代碼如下:

public interface ExtTodoRepository {
    public List<Todo> getTodoByUserId(int userId);
}

最后,還要讓TodoRepository知道ExtTodoRepository定義的方法:

public interface TodoRepository extends JpaRepository<Todo,Integer> ,ExtTodoRepository{
}

這樣,就可以靈活的使用hql(?)來(lái)進(jìn)行查詢了,甚至可以直接使用createSqlQuery來(lái)直接使用SQL進(jìn)行查詢。

截止目前源代碼v1-11_4

這部分內(nèi)容提交后刪除

行車記錄儀

整理代碼,將不需要的,如Dao和impl包下的內(nèi)容全部刪除,并允許,同時(shí)添加一條新的todo記錄,留個(gè)紀(jì)念吧:

很完美,不是么,但是,控制臺(tái)有這樣一條輸出缺引起了我的注意:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.

沒(méi)有找到日志的配置項(xiàng),所以就輸出到控制臺(tái)了,當(dāng)然我們能從控制臺(tái)看到好多東西,比如生成的sql語(yǔ)句:

SELECT ID, CREATETIME, ITEM, USERID FROM TODOS WHERE (USERID = ?)

但是,就像是開車一樣,沒(méi)有任何人喜歡碰撞,但是如果真的出現(xiàn)了,緊靠研究記錄肯定是不行的,這時(shí)候需要一個(gè)行車記錄儀就方便多了,而日志也起了同樣的作用,就是將程序中任何的問(wèn)題,輸出均記錄下來(lái)。而Spring其實(shí)已經(jīng)將日志的一切都自動(dòng)化執(zhí)行了,我們所需要的,僅僅是配置一個(gè)日志配置文件即可.

Log4j2不支持properties文件,只可以使用xml,yaml和json,下面是一個(gè)xml配置的例子:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <properties>
        <property name="LOG_HOME">${sys:catalina.home}/WEB-INF/logs</property>
        <property name="FILE_NAME">jtodos_log</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
        <RollingFile name="RollingFile" fileName="${LOG_HOME}/${FILE_NAME}.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz"
                     immediateFlush="true">
            <PatternLayout
                    pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 M" />
            </Policies>
            <DefaultRolloverStrategy max="20" />
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <!-- 這里是輸入到文件-->
            <AppenderRef ref="RollingFile" />
            <!-- 這里是輸入到控制臺(tái)-->
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>

運(yùn)行后,到日志路徑去看,日志書寫完成:

里邊內(nèi)容可以自行查看。

不知不覺(jué),寫了這么多字,看來(lái)能開上路虎真的不容易呀:)
11章最終版代碼 v1-11_5
謝謝觀看

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容