基於Ant+Velocity的簡單代碼生成器的思路與實現

(原文:http://www.javaeye.com/topic/30893
在SSH項目中,我們應用了service layer模式,所以針對一個模塊,它就存在pojo、dao、daoImpl、service、serviceImpl,再到struts中的action、form。假設設計是面向數據庫的,針對一個數據庫表,那麼就要產生7個java文件,如果還要做異常處理,那麼就是8個java文件。如果數據庫有50個表,那麼就是50*8=400個java文件。工程不小。

至於爲什麼要用service layer模式,論壇上已有討論http://www.javaeye.com/topic/29867

然而我們都知道,web中出現最多的操作是CURD,這400個java文件中有多少代碼是重複的?幾乎佔了80%甚至更多。編寫這樣重複的代碼是很枯燥無味的,而且如果是由不同人負責不同的模塊的分工方式,程序員編碼的風格是各不相同(雖然可能有規範約束,但是最後出來的東西還是避免不了的帶有程序員個人風格的)。

所以爲了節省時間和精力,便做一個程序來生成程序。
只要配置好你的項目名,你的模塊名,模塊路徑,就可以在幾秒之內完成一個模塊的CURD代碼,同時你可以自定義模板。

這是工具的大概設計思路:

由ant處理編譯、生成目錄的工作,velocity處理程序模板,contentEngine爲核心處理程序。

產生的目錄結構和代碼路徑:
模塊名
--子模塊1
----model
------businessobject
------dao
--------hibernate
----service
------impl
----view
------action
------form
----Exception
--子模塊2
...
其中model/businessobject中是pojo和hbm.xml,這個由hibernate工具根據數據庫表產生。

我們假設模塊名爲course,子模塊名爲table,類名爲CourseMember。因篇幅問題,我們只看一個daoImpl的例子。

首先我們利用建立一個daoImpl的模板
ObjectDaoHibernateImpl.vm

代碼
  1. ${package_Hibernate}   
  2. ${import_SQLException}   
  3. ${import_List}   
  4. ${import_HibernateCallback}   
  5. ${import_HibernateObjectRetrievalFailureException}   
  6. ${import_HibernateDaoSupport}   
  7. ${import_HibernateException}   
  8. ${import_Query}   
  9. ${import_Session}   
  10. ${import_ObjectNameDao}   
  11. ${import_ObjectName}   
  12. ${import_Finder}   
  13. ${import_Page}   
  14. ${import_Criteria}   
  15. ${import_Projections}   
  16.   
  17. /**   
  18.  * The Hibernate implementation of the <code>${ObjectName}Dao</code>.   
  19.  *    
  20.  * @author ${Author}   
  21.  * @see ${ObjectName}Dao   
  22.  */   
  23. public class ${ObjectName}DaoHibernateImpl extends HibernateDaoSupport implements ${ObjectName}Dao {   
  24.     /**   
  25.      * Default constructor.   
  26.      */   
  27.     public ${ObjectName}DaoHibernateImpl() {   
  28.         super();   
  29.     }   
  30.        
  31.     /**   
  32.      * @see ${ObjectName}Dao#save${ObjectName}(${ObjectName})   
  33.      */   
  34.     public ${ObjectName} save${ObjectName}(${ObjectName} ${objectname}) {   
  35.         this.getHibernateTemplate().save(${objectname});   
  36.         return ${objectname};   
  37.     }   
  38.        
  39.     /**   
  40.      * @see ${ObjectName}Dao#get${ObjectName}(String)   
  41.      */   
  42.     public ${ObjectName} get${ObjectName}(String id) {   
  43.         return (${ObjectName})this.getHibernateTemplate().load(${ObjectName}.class, id);   
  44.     }   
  45.        
  46.     /**   
  47.      * @see ${ObjectName}Dao#update${ObjectName}(${ObjectName})   
  48.      */   
  49.     public void update${ObjectName}(${ObjectName} ${objectname}) {   
  50.         this.getHibernateTemplate().update(${objectname});   
  51.     }   
  52.        
  53.     /**   
  54.      * @see ${ObjectName}Dao#delete${ObjectName}(${ObjectName})   
  55.      */   
  56.     public void delete${ObjectName}(${ObjectName} ${objectname}) {   
  57.         this.getHibernateTemplate().delete(${objectname});   
  58.     }   
  59.        
  60.     /**   
  61.      * @see ${ObjectName}Dao#getAll${ObjectName}s()   
  62.      */   
  63.     public List getAll${ObjectName}s() {   
  64.         return getHibernateTemplate().executeFind(new HibernateCallback() {   
  65.             public Object doInHibernate(Session session)   
  66.                 throws HibernateException, SQLException {   
  67.   
  68.                 StringBuffer sb = new StringBuffer(100);   
  69.                 //sb.append("select distinct ${objectname} ");   
  70.                 sb.append("SELECT  ${objectname} ");   
  71.                 sb.append("FROM  ${ObjectName} ${objectname} ");   
  72.                 sb.append("order by ${objectname}.id");   
  73.   
  74.                 Query query = session.createQuery(sb.toString());   
  75.                 List list = query.list() ;   
  76.   
  77.                 return list;   
  78.             }   
  79.         });        
  80.     }   
  81.        
  82.        
  83.        
  84.     public Object query(final ${ObjectName} ${objectname},   
  85.             final int pageNo, final int maxResult) {   
  86.         return getHibernateTemplate().execute(new HibernateCallback() {   
  87.             public Object doInHibernate(Session session)   
  88.                     throws HibernateException, SQLException {   
  89.                 Criteria criteria=session.createCriteria(${ObjectName}.class);   
  90.                 Criteria anothercriteria=session.createCriteria(${ObjectName}.class);   
  91.                 criteria.setProjection(Projections.rowCount());    
  92.                
  93.     //          if (!${objectname}.get${objectname}Name().equals("")   
  94.     //                  && ${objectname}.get${objectname}Name() != null) {   
  95.     //              criteria.add(Expression.ilike("contactName","%"+customerContactForm.getContactName()+"%"));   
  96.     //              anothercriteria.add(Expression.ilike("contactName","%"+customerContactForm.getContactName()+"%"));   
  97.     //          }   
  98.                 Integer count=(Integer)criteria.uniqueResult();   
  99.                 List list=anothercriteria.setFirstResult((pageNo-1)*maxResult).setMaxResults(maxResult).list();   
  100.                 Page page=new Page(count.intValue(), maxResult, pageNo);   
  101.                 return new Finder(list, page);   
  102.             }   
  103.         });   
  104.     }   
  105.        
  106.     public boolean deleteBybatch(final String[] chxSong) {   
  107.     StringBuffer cusIdList = new StringBuffer(200);   
  108.     cusIdList.append("delete from ${ObjectName} where ${objectName}No=");   
  109.         for (int i = 0; i < chxSong.length; i++) {   
  110.             if (i == 0)   
  111.                 cusIdList.append(chxSong[i]);   
  112.             else   
  113.                 cusIdList.append(" or ${objectName}No=" + chxSong[i]);   
  114.         }   
  115.         this.getSession().createQuery(cusIdList.toString()).executeUpdate();   
  116.         return true;   
  117.     }   
  118.        
  119. }   
<script type="text/javascript">render_code();</script>
聲明:
1)其中${}是模板語言中的變量,變量的來源一是通過對應的.properties文件,另外是通過參數傳遞。
2)註釋部分因是分頁查詢條件,這個涉及到具體字段,無法預知,所以需要在產生代碼之後程序員根據查詢條件自行修改。另外也涉及到個人項目的分頁方法,這個根據具體情況自定義模板。

 

template.properties
公共屬性文件,是所有template文件(.vm)的變量聲明處,這個會在後面代碼中進行設置。
對於屬性文件,可有兩種方式:
一是針對每一個template模板文件都建立一個屬性文件,優點是在後面ant中設置的參數就少了,而且方便修改。缺點是模板文件數量增多,另外公共部分聲明重複。
二是設定一個公共屬性文件,將特定的變量交給參數傳遞。
我們這裏先用公共屬性文件的方式。

代碼
  1. Author = Cmas R&D Team   
  2. import_Arraylist = import java.util.ArrayList;   
  3. import_List = import java.util.List;   
  4. import_Set = import java.util.Set;   
  5. import_FacesException = import javax.faces.FacesException;   
  6. import_BeanUtils = import org.apache.commons.beanutils.BeanUtils;   
  7. import_Log = import org.apache.commons.logging.Log;   
  8. import_LogFactory = import org.apache.commons.logging.LogFactory;   
  9. import_SQLException = import java.sql.SQLException;   
  10. import_HibernateCallback = import org.springframework.orm.hibernate3.HibernateCallback;   
  11. import_HibernateObjectRetrievalFailureException = import org.springframework.orm.hibernate3.HibernateObjectRetrievalFailureException;   
  12. import_HibernateDaoSupport = import org.springframework.orm.hibernate3.support.HibernateDaoSupport;   
  13. import_HibernateException = import org.hibernate.HibernateException;   
  14. import_Query = import org.hibernate.Query;   
  15. import_Session = import org.hibernate.Session;   
  16. import_Map = import java.util.Map;   
  17. import_HashMap = import java.util.HashMap;   
  18. import_Iterator = import java.util.Iterator;   
  19. import_Criteria=import org.hibernate.Criteria;   
  20. import_Projections=import org.hibernate.criterion.Projections;   
  21. import_DispatchActionSupport=import org.springframework.web.struts.DispatchActionSupport;   
  22. import_Action=import org.apache.struts.action.*;   
  23. import_HttpServletRequest=import javax.servlet.http.HttpServletRequest;   
  24. import_HttpServletResponse=import javax.servlet.http.HttpServletResponse;   
  25. import_BeanUtils=import org.apache.commons.beanutils.BeanUtils;   
  26. import_DataIntegrity=import org.springframework.dao.DataIntegrityViolationException;   

接下來是ant部分,我們編寫build.xml

build.xml

代碼
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <project name="cmas" basedir="../" default="all">  
  3.   
  4.     <!-- Project settings -->  
  5.     <property name="project.distname" value="cmas" /><!-- 設定項目名 -->  
  6.     <property name="project/operationName" value="course/table" /><!-- 設定模塊名,如果有多層以“/”方式擴充,此爲目錄結構變量設定 -->  
  7.     <property name="project.operationName" value="course.table" /><!-- 設定模塊名,如果有多層以“.”方式擴充,此爲包結構變量設定 -->  
  8.     <property name="ObjectName" value="CourseMember" /><!-- 模塊名類名,大寫 -->  
  9.     <property name="objectName" value="courseMember" /><!-- 模塊名變量名,小寫 -->  
  10.   
  11.     <!-- Local system paths -->  
  12.     <property file="${basedir}/ant/build.properties" /><!-- 設定ant的一些屬性,這裏我們沒有額外的設置,使用默認 -->  
  13.     <property file="${basedir}/${webroot.dir}/template/build.properties" />  
  14.   
  15.     <!--Save_path-->  
  16.         <!-- 建立目錄結構 -->  
  17.     <mkdir dir="${basedir}/JavaSource/com/bnu/${project.distname}/${project/operationName}/model" />  
  18.     <mkdir dir="${basedir}/JavaSource/com/bnu/${project.distname}/${project/operationName}/service" />  
  19.     <mkdir dir="${basedir}/JavaSource/com/bnu/${project.distname}/${project/operationName}/view" />  
  20.         <!-- 聲明目錄結構變量 -->  
  21.     <property name="model.src.dir" location="${basedir}/JavaSource/com/bnu/${project.distname}/${project/operationName}/model" />  
  22.     <property name="service.src.dir" location="${basedir}/JavaSource/com/bnu/${project.distname}/${project/operationName}/service" />  
  23.     <property name="view.src.dir" location="${basedir}/JavaSource/com/bnu/${project.distname}/${project/operationName}/view" />  
  24.   
  25.     <property name="overwrite" value="false" />  
  26.     <property name="debug" value="true" />  
  27.     <property name="webroot.dir" value="${basedir}/WebContent" />  
  28.     <property name="webinf.dir" value="${webroot.dir}/WEB-INF" />  
  29.     <property name="build.dir" value="build" />  
  30.   
  31.     <!-- 模板文件的聲明,這裏暫時只寫ObjectDaoHibernateImpl -->  
  32.     <property name="template.dir" value="${webroot.dir}/template" />  
  33.     <property name="ObjectDaoHibernateImpl.template" value="./ObjectDaoHibernateImpl.vm" />  
  34.     <property name="template.properties" value="${template.dir}/template.properties" />  
  35.         <!--設定classpath,這些包不能少-->  
  36.     <property name="classpath" value="${webinf.dir}/classes/" />  
  37.     <!-- classpath for JSF 1.1.01 -->  
  38.     <path id="compile.classpath">  
  39.         <pathelement path="${webinf.dir}/lib/hibernate3.jar" />  
  40.         <pathelement path="${webinf.dir}/lib/log4j-1.2.9.jar" />  
  41.         <pathelement path="${webinf.dir}/lib/commons-beanutils.jar" />  
  42.         <pathelement path="${webinf.dir}/lib/commons-collections.jar" />  
  43.         <pathelement path="${webinf.dir}/lib/commons-digester.jar" />  
  44.         <pathelement path="${webinf.dir}/lib/commons-logging.jar" />  
  45.         <pathelement path="${webinf.dir}/lib/jsf-api.jar" />  
  46.         <pathelement path="${webinf.dir}/lib/jsf-impl.jar" />  
  47.         <pathelement path="${webinf.dir}/lib/jstl.jar" />  
  48.         <pathelement path="${webinf.dir}/lib/standard.jar" />  
  49.         <pathelement path="${webinf.dir}/lib/log4j.jar" />  
  50.         <pathelement path="${webinf.dir}/lib/velocity-1.4.jar" />  
  51.         <pathelement path="${webinf.dir}/lib/velocity-1.4-dev.jar" />  
  52.         <pathelement path="${webinf.dir}/classes" />  
  53.         <pathelement path="${classpath.external}" />  
  54.         <pathelement path="${classpath}" />  
  55.     </path>  
  56.   
  57.     <!--*****************Build_Dao_Hibernate_Impl*開始創建daoImpl**********************-->  
  58.     <!-- define your folder for deployment -->  
  59.     <property name="build_daoimpl.dir" value="build_daoimpl" />  
  60.   
  61.     <!-- Check timestamp on files -->  
  62.     <target name="build_daoimpl_prepare">  
  63.         <tstamp />  
  64.     </target>  
  65.   
  66.     <!-- Copy any resource or configuration files -->  
  67.     <target name="build_daoimpl_resources">  
  68.         <copy todir="${webinf.dir}/classes" includeEmptyDirs="no">  
  69.             <fileset dir="JavaSource">  
  70.                 <patternset>  
  71.                     <include name="**/*.conf" />  
  72.                     <include name="**/*.properties" />  
  73.                     <include name="**/*.xml" />  
  74.                 </patternset>  
  75.             </fileset>  
  76.         </copy>  
  77.     </target>  
  78.   
  79.     <target name="build_daoimpl_init">  
  80.         <!-- Create the time stamp -->  
  81.         <tstamp />  
  82.         <!-- Create the build directory structure used by compile -->  
  83.         <mkdir dir="${model.src.dir}/dao/hibernate" />  
  84.     </target>  
  85.   
  86.     <!-- Normal build of application -->  
  87.     <target name="build_daoimpl_compile" depends="build_daoimpl_prepare,build_daoimpl_resources,build_daoimpl_init">  
  88.         <javac srcdir="${basedir}/JavaSource/com/bnu/exception/" destdir="${webinf.dir}/classes/">  
  89.             <classpath refid="compile.classpath" />  
  90.         </javac>  
  91.   
  92.         <!--編譯核心java文件contentEngine,這個路徑根據具體情況設定,也可以在前面對其進行統一聲明-->  
  93.         <javac srcdir="${basedir}/JavaSource/com/bnu/tools" destdir="${webinf.dir}/classes/">  
  94.             <classpath refid="compile.classpath" />  
  95.         </javac>  
  96.                 <!--運行contentEngine,參數設定-->  
  97.         <java classname="com.bnu.tools.ContentEngine">  
  98.             <classpath refid="compile.classpath" />  
  99.             <arg value="DaoImpl" />  
  100.             <arg value="${template.dir}" />  
  101.             <arg value="${template.properties}" />  
  102.             <arg value="${ObjectDaoHibernateImpl.template}" />  
  103.             <arg value="package com.bnu.${project.distname}.${project.operationName}.model.dao.hibernate;" />  
  104.             <arg value="import com.bnu.${project.distname}.${project.operationName}.model.dao.${ObjectName}Dao;" />  
  105.             <arg value="import com.bnu.${project.distname}.${project.operationName}.model.businessobject.${ObjectName};" />  
  106.             <arg value="${objectName}" />  
  107.             <arg value="${ObjectName}" />  
  108.             <arg value="${model.src.dir}/dao/hibernate" />  
  109.             <arg value="${ObjectName}DaoHibernateImpl.java" />  
  110.         </java>  
  111.     </target>  
  112.   
  113.     <!-- Remove classes directory for clean build -->  
  114.     <target name="build_daoimpl_clean" description="Prepare for clean build">  
  115.         <delete dir="${webinf.dir}/classes" />  
  116.         <mkdir dir="${webinf.dir}/classes" />  
  117.     </target>  
  118.   
  119.     <!-- Build entire project -->  
  120.     <target name="build_daoimpl_build" depends="build_daoimpl_prepare,build_daoimpl_compile" />  
  121.     <target name="build_daoimpl_rebuild" depends="build_daoimpl_clean,build_daoimpl_prepare,build_daoimpl_compile" />  
  122.   
  123.     <target name="build_daoimpl" depends="build_daoimpl_build">  
  124.         <delete file="${build_daoimpl.dir}/${project.distname}.war" />  
  125.         <delete dir="${build_daoimpl.dir}/${project.distname}" />  
  126.     </target>  
  127.   
  128.     <target name="clean" description="clean">  
  129.         <delete dir="${build.dir}" />  
  130.         <delete dir="${webinf.dir}/classes" />  
  131.         <delete dir="${dist.dir}" />  
  132.     </target>  
  133.        
  134.     <target name="all" description="build all" depends="clean,build_daoimpl">  
  135.     </target>  
  136. </project>  

<script type="text/javascript">render_code();</script>
這裏摘取了daoImpl的聲明段,重要部分已經做了註釋。

核心代碼部分,contentEngine文件。

代碼
  1. package com.bnu.tools;   
  2.   
  3. import org.apache.velocity.Template;   
  4. import org.apache.velocity.VelocityContext;   
  5. import org.apache.velocity.app.Velocity;   
  6. import org.apache.velocity.exception.ParseErrorException;   
  7. import org.apache.velocity.exception.ResourceNotFoundException;   
  8.   
  9. import com.bnu.exception.AppException;   
  10.   
  11. import java.io.FileInputStream;   
  12. import java.io.FileOutputStream;   
  13. import java.io.PrintWriter;   
  14. import java.io.StringWriter;   
  15. import java.util.Iterator;   
  16. import java.util.Properties;   
  17.   
  18. /**  
  19.  *   
  20.  * To change the template for this generated type comment go to  
  21.  * Window>Preferences>Java>Code Generation>Code and Comments  
  22.  */  
  23. public class ContentEngine {   
  24.     private VelocityContext context = null;   
  25.   
  26.     private Template template = null;   
  27.   
  28.     // private String properties = null ;   
  29.   
  30.     /**  
  31.      *   
  32.      * @param properties  
  33.      * @throws Exception  
  34.      */  
  35.     public void init(String properties) throws Exception {   
  36.         if (properties != null && properties.trim().length() > 0) {   
  37.             Velocity.init(properties);   
  38.         } else {   
  39.             Velocity.init();   
  40.         }   
  41.         context = new VelocityContext();   
  42.     }   
  43.   
  44.     public void init(Properties properties) throws Exception {   
  45.   
  46.         Velocity.init(properties);   
  47.         context = new VelocityContext();   
  48.     }   
  49.   
  50.     /**  
  51.      *   
  52.      * @param key  
  53.      * @param value  
  54.      */  
  55.     public void put(String key, Object value) {   
  56.         context.put(key, value);   
  57.     }   
  58.   
  59.     /**  
  60.      * 設置模版  
  61.      *   
  62.      * @param templateFile  
  63.      *            模版文件  
  64.      * @throws AppException  
  65.      */  
  66.     public void setTemplate(String templateFile) throws AppException {   
  67.         try {   
  68.             template = Velocity.getTemplate(templateFile);   
  69.         } catch (ResourceNotFoundException rnfe) {   
  70.             rnfe.printStackTrace();   
  71.             throw new AppException(" error : cannot find template "  
  72.                     + templateFile);   
  73.         } catch (ParseErrorException pee) {   
  74.             throw new AppException(" Syntax error in template " + templateFile   
  75.                     + ":" + pee);   
  76.         } catch (Exception e) {   
  77.             throw new AppException(e.toString());   
  78.         }   
  79.   
  80.     }   
  81.   
  82.     /**  
  83.      * 設置模版  
  84.      *   
  85.      * @param templateFile  
  86.      *            模版文件  
  87.      * @throws AppException  
  88.      */  
  89.     public void setTemplate(String templateFile, String characterSet)   
  90.             throws AppException {   
  91.         try {   
  92.             template = Velocity.getTemplate(templateFile, characterSet);   
  93.         } catch (ResourceNotFoundException rnfe) {   
  94.             rnfe.printStackTrace();   
  95.             throw new AppException(" error : cannot find template "  
  96.                     + templateFile);   
  97.         } catch (ParseErrorException pee) {   
  98.             throw new AppException(" Syntax error in template " + templateFile   
  99.                     + ":" + pee);   
  100.         } catch (Exception e) {   
  101.             throw new AppException(e.toString());   
  102.         }   
  103.   
  104.     }   
  105.   
  106.     /**  
  107.      * 轉換爲文本文件  
  108.      */  
  109.     public String toText() throws AppException {   
  110.         StringWriter sw = new StringWriter();   
  111.         try {   
  112.             template.merge(context, sw);   
  113.         } catch (Exception e) {   
  114.             throw new AppException(e.toString());   
  115.         }   
  116.         return sw.toString();   
  117.     }   
  118.   
  119.     /**  
  120.      *   
  121.      * @param fileName  
  122.      */  
  123.     public void toFile(String fileName) throws AppException {   
  124.         try {   
  125.             StringWriter sw = new StringWriter();   
  126.             template.merge(context, sw);   
  127.   
  128.             PrintWriter filewriter = new PrintWriter(new FileOutputStream(   
  129.                     fileName), true);   
  130.             filewriter.println(sw.toString());   
  131.             filewriter.close();   
  132.         } catch (Exception e) {   
  133.             throw new AppException(e.toString());   
  134.         }   
  135.   
  136.     }   
  137.   
  138.     public static void main(String[] args) {   
  139.         ContentEngine content = new ContentEngine();   
  140.         try {   
  141.             Properties p = new Properties();   
  142.   
  143.             Properties varp = new Properties();   
  144.   
  145.             String path = args[1];   
  146.   
  147.             p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, path);   
  148.             p.setProperty(Velocity.RUNTIME_LOG, path + "velocity.log");   
  149.   
  150.             content.init(p);   
  151.   
  152.             FileInputStream in = new FileInputStream(args[2]);   
  153.             varp.load(in);   
  154.   
  155.             content.setTemplate(args[3], "gb2312");   
  156.   
  157.             Iterator it = varp.keySet().iterator();   
  158.             String key = "";   
  159.             String value = "";   
  160.             while (it.hasNext()) {   
  161.                 key = (String) it.next();   
  162.                 value = varp.getProperty(key);   
  163.                 content.put(key, value);   
  164.             }   
  165.   
  166.             if (args[0].equals("DaoImpl")) {   
  167.                 content.put("package_Hibernate", args[4]);   
  168.                 content.put("import_ObjectNameDao", args[5]);   
  169.                 content.put("import_ObjectName", args[6]);   
  170.                 content.put("objectname", args[7]);   
  171.                 content.put("ObjectName", args[8]);   
  172.                 content.toFile(args[9] + '/' + args[10]);//導出的路徑,由參數傳遞。   
  173.             }   
  174. //else 其他情況處理部分,這裏省略。              
  175.         } catch (AppException ae) {   
  176.             ae.printStackTrace();   
  177.         } catch (Exception e) {   
  178.             e.printStackTrace();   
  179.         }   
  180.   
  181.     }   
  182.   
  183. }   
<script type="text/javascript">render_code();</script>

 

至此,這個簡單的代碼生成器的代碼就結束了。很顯然它還很弱小,充其量也只是半自動。離完善的代碼生成器還差很遠。拿出來希望對大家有點用處,另外也希望得到各位的指導,大家討論一下代碼生成器的話題。

 
發佈了20 篇原創文章 · 獲贊 1 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章