mybatis-generator-plugin:mybatis逆向工程插件開發實踐

項目地址:https://gitee.com/chuyunfei/mybatis-generator-plugin.git
下面的是教程,上面的是成品,但是需要改東西才能在你的電腦上跑,下面教程有,希望可以幫到你。

一、我爲什麼要用 mybatis-generator ?

1、我不想寫SQL語句

在使用mybatis進行開發的時候,所有的SQL語句是需要我們自己手動去寫的,哪怕是:select * from table_name; 這樣子簡單的語句。這些簡單、繁複、枯燥、無聊的工作佔據了我們很大的一部分開發時間,所以我不想寫這些簡單的SQL,想讓別人把這些簡單的給完成了,我再去寫那些關鍵的SQL語句。

2、數據表結構改變比較頻繁

很少有什麼項目的需求可以在開始開發前就直接確定完的,難免會在開發的過程中進行需求的更改而導致數據表的更改,而mybatis的映射文件是與結構表高度相關的,如果在大量的映射文件中找到指定的配置難免會出現不必要的錯誤,所以對於數據表的映射文件的管理最好是自動化的,減少人爲造成bug。在我開發的過程中,項目的數據表習慣於寫在一個SQL文件裏面,變換數據表結構時修改這個文件而不是直接修改數據庫,可以保證數據庫裏面的數據表結構和這個SQL文件裏面所呈現的一致。

3、穩定Dao層的接口,避免非必要的對以前的業務代碼進行重構

在自己使用mybatis進行開發時,mapper的接口及其xml配置都是個人進行命名的,很難保證數據表結構或者需求更改後仍然保持穩定,特別是在沒有一個強制規範的時候,mapper的接口會寫的亂七八糟,帶有很強烈的個性,這種個性對於非第一代開發人員的程序維護將造成很大的障礙,所以最好可以將Dao層的API接口穩定,便於程序的維護,也可以明確API含義,避免API含糊其辭,比如這個方法名在一個mapper標識的是一個意思,在另外一個mapper裏面標識的卻是完全風馬牛不相及的意思,這會增加理解的成本,增加維護的難度。

4、我懶

就是懶,不想做哪些明明別人都已經給我解決的問題,不想動,躺着不好嗎?

二、搭建自己的mybatis-generator工程

最開始我在想到底要不要寫這個,畢竟網上一搜一大堆,但是根據我搭建的慘痛教訓,我覺得還是寫一下,但是事先聲明環境,不是這個環境的後面的各個階段都要找到對應環境的相應解決方案,好了,我的開發環境如下:

  1. JDK8
  2. mybatis:3.4.5
  3. mybatis-generator-core:1.3.2
  4. maven 3 (IDEA插件)
  5. IDEA 企業版
  6. MySQL 5.7 (高版本MySQL這裏需要注意配置 jdbc.url 的附帶參數的問題,比如什麼useSSL=false,什麼時區,什麼字符編碼什麼的)
  7. mybatis-generator-maven-plugin:1.3.2

1、搭建一個maven項目

記得改maven的三座標,後面關於maven的座標的信息記得換成你自己的座標

搭建完成了的樣子是這個樣子的:
基本項目結構
然後添加一下項目依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cqupt.mislab</groupId>
    <artifactId>generator</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

依賴添加後會出現一個maven插件,也就是我們的依賴,如下:
依賴的配置圖
再來一個逆向工程的配置文件:generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration>

    <!--這個需要配置一個jar包的位置,一般在maven長褲裏面就直接找到了-->
    <classPathEntry location="C:\Users\chuyunfei\.m2\repository\mysql\mysql-connector-java\5.1.6\mysql-connector-java-5.1.6.jar"/>

    <!--生成Example類-->
    <context id="mysql" defaultModelType="flat" targetRuntime="MyBatis3">

        <!-- 生成的Java文件的編碼 -->
        <property name="javaFileEncoding" value="UTF-8"/>
        <!-- 格式化java代碼 -->
        <property name="javaFormatter" value="org.mybatis.generator.api.dom.DefaultJavaFormatter"/>
        <!-- 格式化XML代碼 -->
        <property name="xmlFormatter" value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>

        <!--MySQL風格的數據庫分割符-->
        <property name="autoDelimitKeywords" value="true"/>
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 數據庫連接 -->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/erpm"
                        userId="root" password="root" />

        <!--類處理器-->
        <javaTypeResolver type="org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl">
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- java模型生成配置 -->
        <javaModelGenerator targetPackage="edu.cqupt.mislab.erpm.user.model.entity" targetProject="src/main/java">
            <property name="enableSubPackages" value="false"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--xml生成配置-->
        <sqlMapGenerator targetPackage="mybatis.mapper.user" targetProject="src/main/resources">
            <property name="enableSubPackages" value="false"/>
        </sqlMapGenerator>

        <!--接口生成配置-->
        <javaClientGenerator targetPackage="edu.cqupt.mislab.erpm.user.dao" targetProject="src/main/java/"
                             type="XMLMAPPER">
            <property name="enableSubPackages" value="false"/>
        </javaClientGenerator>

        <!-- 數據庫表:% 表示所有的表名 -->
        <table schema="" tableName="%"
               enableInsert="true"
               enableDeleteByPrimaryKey="true" enableSelectByPrimaryKey="true" enableUpdateByPrimaryKey="true"
               delimitIdentifiers="true"
               enableCountByExample="false" enableSelectByExample="false" enableDeleteByExample="false"
               enableUpdateByExample="false" selectByExampleQueryId="false" selectByPrimaryKeyQueryId="false">
            <generatedKey column="id" sqlStatement="MySql" type="post" identity="true"/>
        </table>
    </context>
</generatorConfiguration>

檢驗成果的時候到了:
插件運行示意
看見我們的成果了嗎?
運行結果

當然前提是你正確配置了相關信息,在這裏有一篇我不知道哪裏偷來的詳細解釋,在此感謝那位匿名的仁兄!(後來我找到了,在這裏:https://www.cnblogs.com/xiaocao1434/p/8797636.html ,感謝 小草1434!)

配置文件完整的解釋:generatorConfigComment.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<!--配置節點的順序不能變-->
<generatorConfiguration>
    <!--參考資料:https://www.cnblogs.com/xiaocao1434/p/8797636.html-->

    <!--加載mybatis逆向工程需要的jar包位置,比如說需要數據庫驅動包,需要指定一個驅動包的完整路徑(必須)-->
    <classPathEntry location="C:\Users\chuyunfei\.m2\repository\mysql\mysql-connector-java\5.1.6\mysql-connector-java-5.1.6.jar"/>

    <!--
        context:生成一組對象的環境
            id:必選,上下文id,用於在生成錯誤時提示
            defaultModelType:指定生成對象的樣式
                1,conditional:類似hierarchical;
                2,flat:所有內容(主鍵,blob)等全部生成在一個對象中;
                3,hierarchical:主鍵生成一個XXKey對象(key class),Blob等單獨生成一個對象,其他簡單屬性在一個對象中(record class)
            targetRuntime:
                1,MyBatis3:默認的值,生成基於MyBatis3.x以上版本的內容,包括XXXBySample;
                2,MyBatis3Simple:類似MyBatis3,只是不生成XXXBySample;
            introspectedColumnImpl:類全限定名,用於擴展MBG
    -->
    <context id="mysql" defaultModelType="hierarchical" targetRuntime="MyBatis3Simple">

        <!--
            自動識別數據庫關鍵字,默認false。
            設置爲true時,根據SqlReservedWords中定義的關鍵字列表。
            一般保留默認值,遇到數據庫關鍵字(Java關鍵字),使用columnOverride覆蓋
         -->
        <property name="autoDelimitKeywords" value="false"/>

        <!-- 生成的Java文件的編碼 -->
        <property name="javaFileEncoding" value="UTF-8"/>

        <!-- 格式化java代碼 -->
        <property name="javaFormatter" value="org.mybatis.generator.api.dom.DefaultJavaFormatter"/>

        <!-- 格式化XML代碼 -->
        <property name="xmlFormatter" value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>

        <!--
            beginningDelimiter和endingDelimiter:指明數據庫的用於標記數據庫對象名的符號。
            比如:ORACLE就是雙引號,MYSQL默認是`反引號;
        -->
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!--配置數據庫的連接:必須有!!-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql:///service_system"
                        userId="root" password="root">
            <!--設置Driver的屬性,裏面的設置是Driver的屬性,並不是連接屬性-->
            <!--<property name="" value=""/>-->
        </jdbcConnection>

        <!--
            java類型處理器
                用於處理DB中的類型到Java中的類型,默認使用JavaTypeResolverDefaultImpl;
                注意:默認會先嚐試使用Integer,Long,Short等來對應DECIMAL和 NUMERIC數據類型;
        -->
        <javaTypeResolver type="org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl">
            <!--
                true:使用BigDecimal對應DECIMAL和 NUMERIC數據類型
                false:默認,
                    scale>0;length>18:使用BigDecimal;
                    scale=0;length[10,18]:使用Long;
                    scale=0;length[5,9]:使用Integer;
                    scale=0;length<5:使用Short;
             -->
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>


        <!--
            java模型創建器,是必須要的元素,負責:
                1,key類(見context的defaultModelType);
                2,java類;
                3,查詢類
            targetPackage:生成的類要放的包,真實的包受enableSubPackages屬性控制;
            targetProject:目標項目,指定一個存在的目錄下,生成的內容會放到指定目錄中,如果目錄不存在,MBG不會自動建目錄
         -->
        <javaModelGenerator targetPackage="com.copm.model" targetProject="src/main/java">

            <!--自動爲每一個生成的類創建一個構造方法,構造方法包含了所有的field;而不是使用setter-->
            <property name="constructorBased" value="false"/>

            <!-- 在targetPackage的基礎上,根據數據庫的schema再生成一層package,最終生成的類放在這個package下,默認爲false -->
            <property name="enableSubPackages" value="true"/>

            <!--
                是否創建一個不可變的類。
                如果爲true,那麼MBG會創建一個沒有setter方法的類,取而代之的是類似constructorBased的類
             -->
            <property name="immutable" value="false"/>

            <!--
                設置一個根對象。
                如果設置了這個根對象,那麼生成的keyClass或者recordClass會繼承這個類;在Table的rootClass屬性中可以覆蓋該選項
                注意:如果在key class或者record class中有root class相同的屬性,MBG就不會重新生成這些屬性了,包括:
                    1,屬性名相同,類型相同,有相同的getter/setter方法;
             -->
            <!--<property name="rootClass" value="com.copm.domain"/>-->

            <!-- 設置是否在getter方法中,對String類型字段調用trim()方法 -->
            <property name="trimStrings" value="false"/>
        </javaModelGenerator>


        <!--
            生成SQL map的XML文件生成器。
            注意:在Mybatis3之後,我們可以使用mapper.xml文件+Mapper接口(或者不用mapper接口),或者只使用Mapper接口+Annotation。
                所以,如果 javaClientGenerator配置中配置了需要生成XML的話,這個元素就必須配置
            targetPackage/targetProject:同javaModelGenerator
         -->
        <sqlMapGenerator targetPackage="com.copm.xml" targetProject="src/main/resources">
            <!-- 在targetPackage的基礎上,根據數據庫的schema再生成一層package,最終生成的類放在這個package下,默認爲false -->
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>


        <!--
            對於mybatis來說,即生成Mapper接口。
            注意:如果沒有配置該元素,那麼默認不會生成Mapper接口
            targetPackage/targetProject:同javaModelGenerator
            type:選擇怎麼生成mapper接口(在MyBatis3/MyBatis3Simple下):
                1,ANNOTATEDMAPPER:會生成使用Mapper接口+Annotation的方式創建(SQL生成在annotation中),不會生成對應的XML;
                2,MIXEDMAPPER:使用混合配置,會生成Mapper接口,並適當添加合適的Annotation,但是XML會生成在XML中;
                3,XMLMAPPER:會生成Mapper接口,接口完全依賴XML;
            注意,如果context是MyBatis3Simple:只支持ANNOTATEDMAPPER和XMLMAPPER
        -->
        <javaClientGenerator targetPackage="com.copm.mapper" type="XMLMAPPER" targetProject="src/main/java">
            <!-- 在targetPackage的基礎上,根據數據庫的schema再生成一層package,最終生成的類放在這個package下,默認爲false -->
            <property name="enableSubPackages" value="true"/>

            <!-- 可以爲所有生成的接口添加一個父接口,但是MBG只負責生成,不負責檢查-->
            <!--<property name="rootInterface" value=""/>-->

        </javaClientGenerator>

        <!--
            選擇一個table來生成相關文件,可以有一個或多個table,必須要有table元素:多個table標籤生成多個對應產物
            選擇的table會生成以下文件:
                1,SQL map文件
                2,生成一個主鍵類;
                3,除了BLOB和主鍵的其他字段的類;
                4,包含BLOB的類;
                5,一個用戶生成動態查詢的條件類(selectByExample, deleteByExample),可選;
                6,Mapper接口(可選)

            tableName(必要):要生成對象的表名;
            注意:大小寫敏感問題。正常情況下,MBG會自動的去識別數據庫標識符的大小寫敏感度,在一般情況下,MBG會
                根據設置的schema,catalog或tablename去查詢數據表,按照下面的流程:
                1,如果schema,catalog或tablename中有空格,那麼設置的是什麼格式,就精確的使用指定的大小寫格式去查詢;
                2,否則,如果數據庫的標識符使用大寫的,那麼MBG自動把表名變成大寫再查找;
                3,否則,如果數據庫的標識符使用小寫的,那麼MBG自動把表名變成小寫再查找;
                4,否則,使用指定的大小寫格式查詢;
            另外:如果在創建表的時候,使用的""把數據庫對象規定大小寫,就算數據庫標識符是使用的大寫,在這種情況下也會使用給定的大小寫來創建表名;
                這個時候,請設置delimitIdentifiers="true"即可保留大小寫格式;

            可選:
                1,schema:數據庫的schema;
                2,catalog:數據庫的catalog;
                3,alias:爲數據表設置的別名,如果設置了alias,那麼生成的所有的SELECT SQL語句中,列名會變成:alias_actualColumnName
                4,domainObjectName:生成的domain類的名字,如果不設置,直接使用表名作爲domain類的名字;可以設置爲somepck.domainName,那麼會自動把domainName類再放到somepck包裏面;
                5,enableInsert(默認true):指定是否生成insert語句;
                6,enableSelectByPrimaryKey(默認true):指定是否生成按照主鍵查詢對象的語句(就是getById或get);
                7,enableSelectByExample(默認true):MyBatis3Simple爲false,指定是否生成動態查詢語句;
                8,enableUpdateByPrimaryKey(默認true):指定是否生成按照主鍵修改對象的語句(即update);
                9,enableDeleteByPrimaryKey(默認true):指定是否生成按照主鍵刪除對象的語句(即delete);
                10,enableDeleteByExample(默認true):MyBatis3Simple爲false,指定是否生成動態刪除語句;
                11,enableCountByExample(默認true):MyBatis3Simple爲false,指定是否生成動態查詢總條數語句(用於分頁的總條數查詢);
                12,enableUpdateByExample(默認true):MyBatis3Simple爲false,指定是否生成動態修改語句(只修改對象中不爲空的屬性);
                13,modelType:參考context元素的defaultModelType,相當於覆蓋;
                14,delimitIdentifiers:參考tableName的解釋,注意,默認的delimitIdentifiers是雙引號,如果類似MYSQL這樣的數據庫,使用的是`(反引號,那麼還需要設置context的beginningDelimiter和endingDelimiter屬性)
                15,delimitAllColumns:設置是否所有生成的SQL中的列名都使用標識符引起來。默認爲false,delimitIdentifiers參考context的屬性

            注意,table裏面很多參數都是對javaModelGenerator,context等元素的默認屬性的一個複寫;
         -->
        <table tableName="userinfo" >

            <!-- 參考 javaModelGenerator 的 constructorBased屬性-->
            <property name="constructorBased" value="false"/>

            <!-- 默認爲false,如果設置爲true,在生成的SQL中,table名字不會加上catalog或schema; -->
            <property name="ignoreQualifiersAtRuntime" value="false"/>

            <!-- 參考 javaModelGenerator 的 immutable 屬性 -->
            <property name="immutable" value="false"/>

            <!-- 指定是否只生成domain類,如果設置爲true,只生成domain類,如果還配置了sqlMapGenerator,那麼在mapper XML文件中,只生成resultMap元素 -->
            <property name="modelOnly" value="false"/>

            <!-- 參考 javaModelGenerator 的 rootClass 屬性
            <property name="rootClass" value=""/>
             -->

            <!-- 參考javaClientGenerator 的  rootInterface 屬性
            <property name="rootInterface" value=""/>
            -->

            <!-- 如果設置了runtimeCatalog,那麼在生成的SQL中,使用該指定的catalog,而不是table元素上的catalog
            <property name="runtimeCatalog" value=""/>
            -->

            <!-- 如果設置了runtimeSchema,那麼在生成的SQL中,使用該指定的schema,而不是table元素上的schema
            <property name="runtimeSchema" value=""/>
            -->

            <!-- 如果設置了runtimeTableName,那麼在生成的SQL中,使用該指定的tablename,而不是table元素上的tablename
            <property name="runtimeTableName" value=""/>
            -->

            <!--
                注意,該屬性只針對MyBatis3Simple有用;
                如果選擇的runtime是MyBatis3Simple,那麼會生成一個SelectAll方法,如果指定了selectAllOrderByClause,那麼會在該SQL中添加指定的這個order條件;
             -->
            <!--<property name="selectAllOrderByClause" value="age desc,username asc"/>-->

            <!-- 如果設置爲true,生成的model類會直接使用column本身的名字,而不會再使用駝峯命名方法,比如BORN_DATE,生成的屬性名字就是BORN_DATE,而不會是bornDate -->
            <property name="useActualColumnNames" value="false"/>

            <!-- generatedKey用於生成生成主鍵的方法,
                如果設置了該元素,MBG會在生成的**<insert>**元素中生成一條正確的<selectKey>元素,該元素可選
                column:主鍵的列名;
                sqlStatement:要生成的selectKey語句,有以下可選項:
                    Cloudscape:相當於selectKey的SQL爲: VALUES IDENTITY_VAL_LOCAL()
                    DB2       :相當於selectKey的SQL爲: VALUES IDENTITY_VAL_LOCAL()
                    DB2_MF    :相當於selectKey的SQL爲:SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1
                    Derby      :相當於selectKey的SQL爲:VALUES IDENTITY_VAL_LOCAL()
                    HSQLDB      :相當於selectKey的SQL爲:CALL IDENTITY()
                    Informix  :相當於selectKey的SQL爲:select dbinfo('sqlca.sqlerrd1') from systables where tabid=1
                    MySql      :相當於selectKey的SQL爲:SELECT LAST_INSERT_ID()
                    SqlServer :相當於selectKey的SQL爲:SELECT SCOPE_IDENTITY()
                    SYBASE      :相當於selectKey的SQL爲:SELECT @@IDENTITY
                    JDBC      :相當於在生成的insert元素上添加useGeneratedKeys="true"和keyProperty屬性
            <generatedKey column="" sqlStatement=""/>
             -->

            <!--
                該元素會在根據表中列名計算對象屬性名之前先重命名列名,非常適合用於表中的列都有公用的前綴字符串的時候,
                比如列名爲:CUST_ID,CUST_NAME,CUST_EMAIL,CUST_ADDRESS等;
                那麼就可以設置searchString爲"^CUST_",並使用空白替換,那麼生成的Customer對象中的屬性名稱就不是
                custId,custName等,而是先被替換爲ID,NAME,EMAIL,然後變成屬性:id,name,email;

                注意,MBG是使用java.util.regex.Matcher.replaceAll來替換searchString和replaceString的,
                如果使用了columnOverride元素,該屬性無效;

            <columnRenamingRule searchString="" replaceString=""/>
             -->

            <!-- 用來修改表中某個列的屬性,MBG會使用修改後的列來生成domain的屬性;
                column:要重新設置的列名;
                注意,一個table元素中可以有多個columnOverride元素哈~
             -->
            <columnOverride column="username">

                <!-- 使用property屬性來指定列要生成的屬性名稱 -->
                <property name="property" value="userName"/>

                <!-- javaType用於指定生成的domain的屬性類型,使用類型的全限定名
                <property name="javaType" value=""/>
                 -->

                <!-- jdbcType用於指定該列的JDBC類型
                <property name="jdbcType" value=""/>
                 -->

                <!-- typeHandler 用於指定該列使用到的TypeHandler,如果要指定,配置類型處理器的全限定名
                    注意,mybatis中,不會生成到mybatis-config.xml中的typeHandler
                    只會生成類似:where id = #{id,jdbcType=BIGINT,typeHandler=com._520it.mybatis.MyTypeHandler}的參數描述
                <property name="jdbcType" value=""/>
                -->

                <!-- 參考table元素的delimitAllColumns配置,默認爲false
                <property name="delimitedColumnName" value=""/>
                 -->
            </columnOverride>

            <!-- ignoreColumn設置一個MGB忽略的列,如果設置了改列,那麼在生成的domain中,生成的SQL中,都不會有該列出現
                column:指定要忽略的列的名字;
                delimitedColumnName:參考table元素的delimitAllColumns配置,默認爲false

                注意,一個table元素中可以有多個ignoreColumn元素
            <ignoreColumn column="deptId" delimitedColumnName=""/>
            -->
        </table>

    </context>

</generatorConfiguration>

三、逆向工程插件開發——重點

爲什麼糾結這個插件開發,當然不是頭腦發熱什麼追求技術什麼的,當然是開發了插件可以讓我更舒服、更快、更靈活、更有效的開發。

1、爲什麼我要來寫這個逆向工程插件開發博客

每一次我搜索 mybatis-generator 插件開發 的時候,出來的都是教我怎麼用mybatis-generator的,對,沒錯,以插件的方式使用mybatis-generator,也就是我們到目前爲止已經實現的部分,嗯…,每次我都想換Google再來搜一次。

2、初探逆向工程插件

沒辦法,查不到資料只有自己動手豐衣足食了,想到mybatis的插件開發,我試着在剛纔的項目中 double shift and input plugin !!!!一個大大的驚喜:
在這裏插入圖片描述
秉承着打破砂鍋問到底的小強精神,我點了進去,於是徹底對其宣判死刑:
接口類層次圖
沒錯,真相只有一個,就是這個關鍵類。
到目前爲止我發現自己其實是走了彎路的,回想起配置文件裏面是不是有可以配置插件的配置?還說不是有默認的幾個插件嘛?不就是我瞎貓碰到死耗子的那幾個插件嗎?然後順藤摸瓜,依然可以找到這個關鍵接口。

3、準備依葫蘆畫瓢開發插件

先選一個插件來進行研究,就拿帶toString那個插件嘛,就決定是它了:

在這裏插入圖片描述
繼續瞭解這個方法:

    private void generateToString(IntrospectedTable introspectedTable,
            TopLevelClass topLevelClass) {
        //創建了一個方法,叫toString: method.setName("toString"); //$NON-NLS-1$
        Method method = new Method();
        //public訪問權限
        method.setVisibility(JavaVisibility.PUBLIC);
        //返回值爲String
        method.setReturnType(FullyQualifiedJavaType.getStringInstance());
        method.setName("toString"); //$NON-NLS-1$
        //如果支持Java5就加一個註解@Override
        if (introspectedTable.isJava5Targeted()) {
            method.addAnnotation("@Override"); //$NON-NLS-1$
        }

        context.getCommentGenerator().addGeneralMethodComment(method,
                introspectedTable);

        //爲方法添加一行代碼體
        method.addBodyLine("StringBuilder sb = new StringBuilder();"); //$NON-NLS-1$
        method.addBodyLine("sb.append(getClass().getSimpleName());"); //$NON-NLS-1$
        method.addBodyLine("sb.append(\" [\");"); //$NON-NLS-1$
        method.addBodyLine("sb.append(\"Hash = \").append(hashCode());"); //$NON-NLS-1$
        StringBuilder sb = new StringBuilder();
        //將類裏面的字段都給拼湊起來
        for (Field field : topLevelClass.getFields()) {
            String property = field.getName();
            sb.setLength(0);
            sb.append("sb.append(\"").append(", ").append(property) //$NON-NLS-1$ //$NON-NLS-2$
                    .append("=\")").append(".append(").append(property) //$NON-NLS-1$ //$NON-NLS-2$
                    .append(");"); //$NON-NLS-1$
            method.addBodyLine(sb.toString());
        }

        method.addBodyLine("sb.append(\"]\");"); //$NON-NLS-1$
        method.addBodyLine("return sb.toString();"); //$NON-NLS-1$
		//給這個類添加這個toString方法
        topLevelClass.addMethod(method);
    }

en…,感覺這個也太…簡單了吧?咋這麼簡單呢?還以爲要代理要探針要什麼高深莫測的技術,結果我褲子都脫了,給我看這個!!!
其實仔細、冷靜、慢慢的分析還是有道理的,逆向工程生成文件其實就是根據數據庫和配置文件裏面的元數據,生成對需要生成的文件的元數據描述,最後纔會通過這些元數據描述來進行文件的生成。也就是說我們現在修改的其實只是這個類的描述數據而已,也就解釋了爲什麼這麼簡單的原因,因爲到插件這裏壓根就還沒有生成文件!!

4、繼續深入的發散思考

  1. 如果對方法都可以進行描述,那麼字段、接口、參數、返回值、註解、註釋這些應該也是通過一定的數據結構來進行描述的,嗯,一定是。
  2. 如果我操作的數據僅僅是對於文件的描述數據,那麼我將可以控制整個要被生成出來的類的一切東西,因爲這個類的一切東西應該都是通過描述數據來描述的,簡直可以爲所欲爲啊,顛倒黑白什麼的簡直不要太容易。
  3. 既然開放了插件接口,那麼肯定是可以影響其底層邏輯的,也就是說我們應該可以通過插件來影響model、mapper、xml三個模塊的行爲。

5、對插件思考的驗證

先查看插件接口裏面有哪些方法:
在這裏插入圖片描述
仔細分析上面的接口和查看接口註釋可以發現接口的方法被分爲五個部分:

  1. 以init*、set*、context*開頭的方法,註釋多爲設置基本信息,也就是爲插件設置元數據的,就是配置在配置文件裏面插件的配置,可以用來支持插件行爲的配置化。同時還有一些上下文信息,多數都是隻調用一遍。
  2. 以client*開頭的方法,註釋發現都是對mapper接口的插件方法接口。
  3. 以model*開頭的方法,註釋多是對model實體類的插件方法接口。
  4. 以sqlMap*開頭的方法,註釋多是對xml配置文件的插件方法接口。
  5. 以provider*開頭的方法,註釋多是對於SQL provider數據庫廠商的插件方法接口。

看着這些接口的名字再看看配置文件的結構:
在這裏插入圖片描述
是不是突然恍然大悟!!!原來一切都是安排。

6、開始進行插件開發

特別注意:雖然我很想把我知道的都給你說,都是事實是根本就沒有辦法說清楚,但是如果你按照下面的步驟做,你將能夠爲你的應用開發無論是xml、mapper、model那個方面的。

  1. 大膽猜測,信心求證
  2. 多看源碼,有一點點想法就去看看源碼再去實踐。
  3. 不要慫。

①、開發準備

由於是以插件的方式運行,所以需要把自己開發的插件加入插件依賴,更改pom文件,將本項目添加到插件依賴:

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <dependency>
                        <!--這個座標記得改成你自己的,由於是倉庫依賴,需要將本項目安裝到本地倉庫-->
                        <groupId>cqupt.mislab</groupId>
                        <artifactId>generator</artifactId>
                        <version>1.0-SNAPSHOT</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

爲了方便測試,寫一個maven命令:

clean:clean
compiler:compile 
jar:jar 
install:install-file -Dfile=F:\JAVA\Projects\Main\generator\target\generator-1.0-SNAPSHOT.jar 
-DgroupId=cqupt.mislab 
-DartifactId=generator 
-Dversion=1.0-SNAPSHOT 
-Dpackaging=jar 
mybatis-generator:generate

註釋:

  1. 清空當前項目的編譯結果
  2. 編譯當前項目
  3. 打包當前項目
  4. 將當前項目的jar包安裝到本地倉庫:打包後jar包的絕對路勁,jar包的三座標,安裝的是jar包
  5. 調用mybatis-generator插件進行工程生成

注意更改你的jar包位置和maven三座標,然後將這個命令搞到這裏來:
在這裏插入圖片描述
記得點右下角的apply,然後將看見這個:
在這裏插入圖片描述
現在點擊右邊那個綠色三角形執行一下,可以看見和剛剛一樣的結果就是成功了。

②、第一個插件:爲mapper添加@Repository註解

根據我們前面的分析和自帶插件的分析,首先一個插件需要事先Plugin接口,當然爲了更方便都是直接繼承PluginAdapter,然後由於我們是想寫修改Mapper接口行爲的插件(爲mapper填加註解),所以挑選以client*開頭的方法,最終得到:

package plugins;

import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.TopLevelClass;

import java.util.List;

public class MapperAnnotationPlugin extends PluginAdapter {

    @Override
    public boolean clientGenerated(Interface interfaze,TopLevelClass topLevelClass,IntrospectedTable introspectedTable){

        //首先要導入這個類型撒,addImportedType,生動形象,還有一些什麼:addMethod、addAnnotation什麼的,見名知意
        interfaze.addImportedType(new FullyQualifiedJavaType("org.springframework.stereotype.Repository"));

        //給它加一頂註解
        interfaze.addAnnotation("@Repository");

        return true;
    }

    @Override//每個插件必須實現,具體的就是插件執行中的警告信息,該插件的配置信息是否完備等
    public boolean validate(List<String> warnings){

        return true;
    }
}

再把我們的插件配置進去:
在這裏插入圖片描述
執行,點擊配置的那個maven命令:
在這裏插入圖片描述
現在我們要讓這個插件更加智能,我可以讓他在某些表裏面生效,某些表不失效,可以添加多個註解,擼起袖子加油幹:
首先爲插件配置屬性:

        <plugin type="plugins.MapperAnnotationPlugin">
            <!--全局的插件開關,true的時候啓用-->
            <property name="globalEnable" value="true"/>
            <!--多個需要加的註解之間的分隔符-->
            <property name="globalAnnotationSeparator" value=","/>
            <!--需要加的註解,如果多個就用分隔符分開-->
            <property name="globalAnnotationClasses" value="org.springframework.stereotype.Repository"/>
        </plugin>

其次爲表配置是否啓用插件:

        <table schema="" tableName="user_student_basic_info"
               enableInsert="true"
               enableDeleteByPrimaryKey="true" enableSelectByPrimaryKey="true" enableUpdateByPrimaryKey="true"
               delimitIdentifiers="true"
               enableCountByExample="false" enableSelectByExample="false" enableDeleteByExample="false"
               enableUpdateByExample="false" selectByExampleQueryId="false" selectByPrimaryKeyQueryId="false">
            <!--這張表是否開啓這個插件-->
            <property name="enableMapperAnnotation" value="true"/>
            <!--這張表的分隔符,優先級比全局的高-->
            <property name="annotationSeparator" value=","/>
            <!--除了全局需要加的註解外這個mapper額外需要加的註解-->
            <property name="annotationClasses" value="org.springframework.stereotype.Repository"/>
            <generatedKey column="id" sqlStatement="MySql" type="post" identity="true"/>
        </table>

現在去改造我們原來的簡單的插件:

package plugins;

import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.TopLevelClass;

import java.util.List;
import java.util.Properties;

/**
 * 0、全局在插件配置屬性裏面使用:globalEnable = true 開啓
 * 1、實現爲Mapper文件添加指定註解的功能
 * 2、單表開啓參數:enableMapperAnnotation = true
 * 3、單表註解的分隔符,配置參數爲:annotationSeparator,默認爲:,
 * 4、單表註解類,使用配置:annotationClasses
 * 5、全局分隔符使用:globalAnnotationSeparator 配置
 * 6、全局註解使用:globalAnnotationClasses 配置
 */
public class MapperAnnotationPlugin extends PluginAdapter {

    @Override
    public boolean clientGenerated(Interface interfaze,TopLevelClass topLevelClass,IntrospectedTable introspectedTable){

        //判斷插件是否啓用
        if(!isPluginEnable(introspectedTable)){
            return true;
        }

        final Properties tableProperties = getTableProperties(introspectedTable);

        //獲取表裏面配置的分隔符,優先級比全局的高,默認爲:,
        String separator = tableProperties.getProperty("annotationSeparator");

        if(separator == null){

            //獲取全局分隔符
            separator = properties.getProperty("globalAnnotationSeparator",",");
        }

        String annotationClass = null;

        //獲取這張表需要添加的註解
        String tableAnnotationClass = tableProperties.getProperty("annotationClasses");

        if(tableAnnotationClass!=null){
            annotationClass = tableAnnotationClass;
        }

        //全局需要配置註解
        String globalAnnotationClass = properties.getProperty("globalAnnotationClasses");

        if(globalAnnotationClass != null){

            if(tableAnnotationClass == null){

                annotationClass = globalAnnotationClass;
            }else {

                annotationClass = tableAnnotationClass + separator + globalAnnotationClass;
            }
        }

        if(annotationClass != null){

            String[] annotationClazz = annotationClass.split(separator);

            if(annotationClazz.length > 0){

                for(String clazz : annotationClazz){

                    //導入註解
                    interfaze.addImportedType(new FullyQualifiedJavaType(clazz));
                    //添加註解
                    interfaze.addAnnotation("@" + clazz.substring(clazz.lastIndexOf(".")+1));
                }
            }
        }

        return true;
    }

    @Override
    public boolean validate(List<String> warnings){

        return true;
    }

    /**
     * 獲取表的配置數據
     */
    private Properties getTableProperties(IntrospectedTable introspectedTable){

        return introspectedTable.getTableConfiguration().getProperties();
    }

    /**
     * 判斷一個表是否開啓這個插件
     */
    private boolean isPluginEnable(IntrospectedTable introspectedTable){

        //獲取配置在插件裏面的屬性,全局開關
        final String globalEnable = properties.getProperty("globalEnable");

        //如果沒有1配置,默認爲全局開啓
        if(globalEnable == null || Boolean.valueOf(globalEnable)){

            //獲取配置在表裏面的屬性
            final String tableEnable = introspectedTable.getTableConfigurationProperty("enableMapperAnnotation");

            if(tableEnable != null){

                return Boolean.valueOf(tableEnable);
            }

            return true;
        }

        return false;
    }
}

注意重新測試的時候把你上一次測試的生成文件刪除掉

運行maven命令:
在這裏插入圖片描述
然後去測試開關這個插件吧。

③、開發自己的註釋生成器

在上一張圖片中你一定看見了那些令人感到厭煩的自帶的註釋,現在繼續自定義的註釋生成器,也是最常用的,可以將SQL裏面的comment自動的生成到model的字段上面去,但是這個有專門的方式,但是可以思考怎麼樣用插件實現,現在來進行常規的方式:自定義註釋生成器。

package plugins;

import org.mybatis.generator.api.*;
import org.mybatis.generator.api.dom.java.*;
import org.mybatis.generator.api.dom.xml.TextElement;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.config.MergeConstants;
import org.mybatis.generator.config.PropertyRegistry;

import java.io.Serializable;
import java.util.Date;
import java.util.Properties;

import static org.mybatis.generator.internal.util.StringUtility.isTrue;

/**
 * 1、將SQL的comment轉移到實體類裏面
 */
public class ConsumerCommentGenerator implements CommentGenerator , Serializable {

    private Properties properties;

    private boolean suppressDate;

    private boolean suppressAllComments;

    public ConsumerCommentGenerator() {
        super();
        properties = new Properties();
        suppressDate = false;
        suppressAllComments = false;
    }

    public void addJavaFileComment(CompilationUnit compilationUnit) {
        return;
    }

    /**
     * Adds a suitable comment to warn users that the element was generated, and
     * when it was generated.
     */
    public void addComment(XmlElement xmlElement) {
        if (suppressAllComments) {
            return;
        }
        xmlElement.addElement(new TextElement("<!--切勿修改-->"));
    }

    public void addRootComment(XmlElement rootElement) {

        rootElement.addElement(new TextElement(
                "<!--自定義SQL與逆向工程SQL分界線:上方爲自定義SQL區-->"));
    }

    @Override
    public void addConfigurationProperties(Properties properties) {
        this.properties.putAll(properties);

        suppressDate = isTrue(properties
                .getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_DATE));

        suppressAllComments = isTrue(properties
                .getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_ALL_COMMENTS));
    }

    /**
     * This method adds the custom javadoc tag for. You may do nothing if you do
     * not wish to include the Javadoc tag - however, if you do not include the
     * Javadoc tag then the Java merge capability of the eclipse plugin will
     * break.
     *
     * @param javaElement
     *            the java element
     */
    protected void addJavadocTag(JavaElement javaElement,boolean markAsDoNotDelete) {
        javaElement.addJavaDocLine(" *");
        StringBuilder sb = new StringBuilder();
        sb.append(" * ");
        sb.append(MergeConstants.NEW_ELEMENT_TAG);
        if (markAsDoNotDelete) {
            sb.append(" do_not_delete_during_merge");
        }
        String s = getDateString();
        if (s != null) {
            sb.append(' ');
            sb.append(s);
        }
        javaElement.addJavaDocLine(sb.toString());
    }

    /**
     * This method returns a formated date string to include in the Javadoc tag
     * and XML comments. You may return null if you do not want the date in
     * these documentation elements.
     *
     * @return a string representing the current timestamp, or null
     */
    protected String getDateString() {
        if (suppressDate) {
            return null;
        } else {
            return new Date().toString();
        }
    }

    public void addClassComment(InnerClass innerClass,IntrospectedTable introspectedTable) {
        if (suppressAllComments) {
            return;
        }

        StringBuilder sb = new StringBuilder();

        innerClass.addJavaDocLine("/**");
        innerClass.addJavaDocLine(" * This class was generated by MyBatis Generator."); //$NON-NLS-1$

        sb.append(" * This class corresponds to the database table "); //$NON-NLS-1$
        sb.append(introspectedTable.getFullyQualifiedTable());
        innerClass.addJavaDocLine(sb.toString());

        addJavadocTag(innerClass, false);

        innerClass.addJavaDocLine(" */"); //$NON-NLS-1$
    }

    public void addEnumComment(InnerEnum innerEnum,
                               IntrospectedTable introspectedTable) {
        return;
    }

    public void addFieldComment(Field field,
                                IntrospectedTable introspectedTable,
                                IntrospectedColumn introspectedColumn) {
        if (suppressAllComments) {
            return;
        }

        field.addJavaDocLine("/**");

        String remarkLine = introspectedColumn.getRemarks();

        if(remarkLine != null){

            String[] remarks = remarkLine.split(System.getProperty("line.separator"));

            for(String remark : remarks){

                field.addJavaDocLine(" * " + remark);
            }
        }
        field.addJavaDocLine("*/");
    }

    public void addFieldComment(Field field, IntrospectedTable introspectedTable) {
        if (suppressAllComments) {
            return;
        }

        field.addJavaDocLine("/**");
        field.addJavaDocLine(" * 類靜態字段");
        field.addJavaDocLine(" */");
    }

    public void addGeneralMethodComment(Method method,IntrospectedTable introspectedTable) {
        if (suppressAllComments) {
            return;
        }

        method.addJavaDocLine("/**");
        method.addJavaDocLine(" * 切勿修改");
        method.addJavaDocLine(" */");
    }

    public void addGetterComment(Method method,IntrospectedTable introspectedTable,IntrospectedColumn introspectedColumn) {
        if (suppressAllComments) {
            return;
        }

        method.addJavaDocLine("/**");
        method.addJavaDocLine(" * 切勿修改");
        method.addJavaDocLine(" */");
    }

    public void addSetterComment(Method method,
                                 IntrospectedTable introspectedTable,
                                 IntrospectedColumn introspectedColumn) {
        if (suppressAllComments) {
            return;
        }

        method.addJavaDocLine("/**");
        method.addJavaDocLine(" * 切勿修改");
        method.addJavaDocLine(" */");
    }

    public void addClassComment(InnerClass innerClass,
                                IntrospectedTable introspectedTable, boolean markAsDoNotDelete) {
        if (suppressAllComments) {
            return;
        }

        innerClass.addJavaDocLine("/**");
        innerClass.addJavaDocLine(" * 切勿修改");
        innerClass.addJavaDocLine(" * 數據表:" + introspectedTable.getFullyQualifiedTable());
        innerClass.addJavaDocLine(" */");
    }
}

現在來配置這個自定義的註釋生成器:

在這裏插入圖片描述
運行maven命令:
在這裏插入圖片描述

四、壓軸大戲——爲JPA服務的mybatis-generator

1、爲什麼要爲JPA開發mybatis-generator插件

這個是我當前項目的問題,我持久層採用JPA,但是IDEA自帶的JPA工具無法生成字段註釋,而且定數據表也是SQL,雖然最後都需要轉換爲JPA的ERP模型,但是自己寫Model和註釋簡直要把我逼瘋,所以想到了逆向工程插件來實現這個問題,具體的轉換思路是:

  1. 定數據表,寫成SQL文件
  2. 將SQL文件執行到數據庫
  3. 使用mybatis逆向工程將其轉換爲model
  4. 使用mybatis逆向工程插件將model轉換爲JPA的Entity
  5. 少量修改Entity將數據模型轉換爲對象模型

按照這個思路可以解決將數據錶轉換爲model所花的大量時間(因爲數據表多的令人絕望),還可以把數據表上面的註釋也給轉換過來,一舉多得。
在此之間,依賴 lombok 來保持Model的類定義的清爽。

2、開發JPA的Entity插件

由於我們修改的是model,所以選擇覆蓋model*的接口方法

package plugins;

import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.TopLevelClass;

import java.util.List;

public class JpaEntityPlugin extends PluginAdapter {

    /**
     * 爲JpaEntity的類級別增加註解的方法
     */
    @Override
    public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass,IntrospectedTable introspectedTable){

        //清空裏面所有的方法,所有的方法由lombok自動生成,保持文件的乾爽和清晰
        topLevelClass.getMethods().clear();

        //導入需要導入的類
        topLevelClass.addImportedType("lombok.*");
        topLevelClass.addImportedType("javax.persistence.*");

        final List<String> annotations = topLevelClass.getAnnotations();

        //添加lombok的相關注解
        annotations.add("@Data");
        annotations.add("@Builder");
        annotations.add("@NoArgsConstructor");
        annotations.add("@AllArgsConstructor");

        //添加JPA的相關注解
        annotations.add("@Entity");

        final String tableName = introspectedTable.getFullyQualifiedTable().getIntrospectedTableName();

        annotations.add("@Table(name = \"" + tableName + "\")");

        return super.modelBaseRecordClassGenerated(topLevelClass,introspectedTable);
    }

    /**
     * 爲JpaEntity的字段添加Jpa,注意,必須要有一個字段爲id的自增主鍵
     */
    @Override
    public boolean modelFieldGenerated(Field field,TopLevelClass topLevelClass,IntrospectedColumn introspectedColumn,IntrospectedTable introspectedTable,ModelClassType modelClassType){

        final String columnName = introspectedColumn.getActualColumnName();

        final List<String> annotations = field.getAnnotations();

        //添加Jpa的相關注解

        if(field.getName().equalsIgnoreCase("id")){
            annotations.add("@Id");
            annotations.add("@GeneratedValue(strategy = GenerationType.AUTO)");
        }else {
            annotations.add("@Basic");
        }

        annotations.add("@Column(name = \"" + columnName + "\")");

        return super.modelFieldGenerated(field,topLevelClass,introspectedColumn,introspectedTable,modelClassType);
    }

    @Override
    public boolean validate(List<String> warnings){
        return true;
    }
}

將其配置進入配置文件後,運行:
在這裏插入圖片描述

五、總結

我演示了model和mapper的插件開發,但是爲mapper增加方法的話需要修改xml配置文件,所以我分享一下我自己的工程:
https://gitee.com/chuyunfei/mybatis-generator-plugin.git
上面有這三個插件的源碼,還有其他三個自定義的mapper方法,上面演示的工程是我在寫博客的過程中一步步建立起來的,當然你可以直接運行我的git項目,不過記得改maven命令和那個jdbc驅動的位置。
如果有問題的話可以留言討論哦,萬一我不小心那個配置沒有正確而對你造成困擾就很不好了,歡迎留言,麼麼噠。

——傷心流淚的不一定就是受害者,也許是加害者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章