Android Studio Plugin 外掛開發教程(三) —— 製作一個自動生成資料庫程式碼的外掛

boredream發表於2017-08-31

專案原始碼

github.com/boredream/A…

系列教程

Android Studio Plugin 外掛開發教程(一) —— 開發你的第一個外掛

Android Studio Plugin 外掛開發教程(二) —— 外掛SDK中的常用物件介紹

Android Studio Plugin 外掛開發教程(三) —— 製作一個自動生成資料庫程式碼的外掛

Android Studio Plugin 外掛開發教程(四) —— 為自動生成資料庫程式碼的外掛新增UI


外掛介紹

本篇實戰擼個自動生成安卓Sqlite資料庫程式碼的外掛,先演示下最終效果
db資料夾下的都是外掛自動生成的,而MainActivity裡面的程式碼是我提前寫好的,用於實驗外掛生成的程式碼效果

DatabaseGenerator.gif
DatabaseGenerator.gif

簡單解釋下外掛功能
給定一個資料類,比如User。希望外掛能根據資料類自動生成對應的表結構,存在一個Column類裡。然後再生成對應的Dao類其中包含CRUD方法。

和網上常見的一些資料庫框架類似,只不過這裡是用外掛直接生成Android Sqlite原生程式碼

優點:

  • 無需額外依賴
  • 無學習成本
  • 便於自定義

缺點:

  • 原生程式碼量較多
  • 需要對安卓Sqlite原生程式碼有一定了解

開擼~
需要處理這麼幾個模組
SqliteOpenHelper類,其中包含create table的sql語句;
Columns欄位類,統一存在一個DataContract類中;
資料Dao類,包含CRUD的sql語句

幾個模組的處理步驟和邏輯都類似,這裡拿Columns類生成舉例。
其他可以下載原始碼參考 ,原始碼地址:
github.com/boredream/A…
歡迎star和follow~
下載原始碼後參考教程一先搭建環境,然後匯入專案


處理步驟如下:

一、定位到需要建立檔案的目錄

這裡希望把生成的類都存在包名目錄下的db包中(com.packagename.db)

首先要獲取到包名目錄路徑...app/src/main/java/包名,然後才能在它下面獲取或新建db資料夾。而獲取包名目錄又要先獲取Android專案的包名,想獲取這個又得先找到AndroidManifest檔案~

AndroidManifest.png
AndroidManifest.png

因為AndroidManifest檔案路徑是固定的,所以可以用上一篇教程中的LocalFileSystem.getInstance().findFileByPath(path);方法獲取檔案

public static PsiFile getManifestFile(Project project) {
    String path = project.getBasePath() + File.separator +
            "app" + File.separator +
            "src" + File.separator +
            "main" + File.separator +
            "AndroidManifest.xml";
    VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(path);
    if(virtualFile == null) return null;
    return PsiManager.getInstance(project).findFile(virtualFile);
}複製程式碼

project可以在Action中通過event.getData()獲取,參考上一篇教程
獲取到VirtualFile後再轉換成PsiFile,大部分操作都是針對Psi體系的

然後解析AndroidManifest檔案,獲取package屬性裡的包名
因為是Xml檔案,所以和Dom啥的解析差不多,獲取程式碼如下

public static String getAppPackageName(Project project) {
    PsiFile manifestFile = getManifestFile(project);
    XmlDocument xml = (XmlDocument) manifestFile.getFirstChild();
    return xml.getRootTag().getAttribute("package").getValue();
}複製程式碼

然後就可以根據包名獲取到包名目錄了

public static VirtualFile getAppPackageBaseDir(Project project) {
    String path = project.getBasePath() + File.separator +
            "app" + File.separator +
            "src" + File.separator +
            "main" + File.separator +
            "java" + File.separator +
            getAppPackageName(project).replace(".", File.separator);
    return LocalFileSystem.getInstance().findFileByPath(path);
}複製程式碼

project.getBasePath()是專案的根目錄,在其基礎上拼接後續路徑
然後,包名目錄下斷有沒有db資料夾,沒有就建立一個

// app包名根目錄 ...\app\src\main\java\PACKAGE_NAME\
VirtualFile baseDir = AndroidUtils.getAppPackageBaseDir(project);

// 判斷根目錄下是否有db資料夾
VirtualFile dbDir = baseDir.findChild("db");
if(dbDir == null) {
    // 沒有就建立一個
    try {
        dbDir = baseDir.createChildDirectory(null, "db");
    } catch (IOException e) {
        e.printStackTrace();
    }
}複製程式碼

這次我們用了 VirtualFile.FindChild(filename) 方法,獲取檔案子一級路徑中尋找檔案或資料夾
(LocalFileSystem.getInstance().findFileByPath(path); 只能獲取檔案不能獲取目錄,所以不用)
沒有db資料夾的話,就 VirtualFile.createChildDirectory(requestor, name) 建立一個
這個方法第一個引數是指定誰呼叫了它,一般傳null不做特殊處理

db目錄定位到了,然後就是在裡面建立DataContract類了,再在其中存放Columns類。
DataContact.java檔案其實也可以通過類似上面的 VirtualFile.createChildData 直接建立檔案,
但建立的是空的檔案,而我們需要的是有程式碼內容的java檔案,所以下面我們介紹另一個方法~


二、建立一個包含程式碼的檔案

按照我們的外掛需求,要建立一個DataContract類,然後把Columns類都存進去。
首先就是要生成這個作為殼子的類~ 我們先拼接出來類檔案的字串,程式碼如下

public static String genDataContractInitCode(VirtualFile dir) {
    return StringUtils.formatSingleLine(0, "package " + AndroidUtils.getFilePackagePath(dir) + ";") +
            "\n" +
            StringUtils.formatSingleLine(0, "import android.provider.BaseColumns;") +
            "\n" +
            StringUtils.formatSingleLine(0, "public final class DataContract {") +
            "\n" +
            StringUtils.formatSingleLine(1, "private DataContract() {") +
            StringUtils.formatSingleLine(2, "// private") +
            StringUtils.formatSingleLine(1, "}") +
            "\n" +
            "}";
}複製程式碼

其中getFilePackagePath是獲取當前檔案/資料夾對應包名 com.xxx.xxx 的,
邏輯是把當前檔案路徑的 / 替換成 . 然後擷取com.xxx.xxx以後的部分即可

public static String getFilePackageName(VirtualFile dir) {
    if(!dir.isDirectory()) {
        // 非目錄的取所在資料夾路徑
        dir = dir.getParent();
    }
    String path = dir.getPath().replace("/", ".");
    String preText = "src.main.java";
    int preIndex = path.indexOf(preText) + preText.length() + 1;
    path = path.substring(preIndex);
    return path;
}複製程式碼

獲取到程式碼字串以後,可以用createFileFromText建立有內容的檔案,如下

String name = "DataContract.java";
VirtualFile virtualFile = dbDir.findChild(name);
if(virtualFile == null) {
    // 沒有就建立一個,第一次使用程式碼字串建立個類
    PsiFile initFile = PsiFileFactory.getInstance(project).createFileFromText(
            name, JavaFileType.INSTANCE, CodeFactory.genDataContractInitCode(dbDir));
    // 加到db目錄下
    PsiManager.getInstance(project).findDirectory(dbDir).add(initFile);
    virtualFile = dbDir.findChild(name);
}複製程式碼

dbDir是步驟一中獲取到的db資料夾
genDataContractInitCode是上面拼接程式碼的方法,返回程式碼字串

注意,createFileFromText建立的檔案是一個無目錄的檔案,需要手動add到需要位置
這個add操作就會把檔案加到指定目錄下,新建一個檔案~


三、解析資料生成對應Columns類

上一篇介紹過,我們可以用action中的event獲取當前正在編輯的檔案,然後在file中獲取到PsiClass元素,最後遍歷Class獲取全部成員變數Field。PsiClass和Java中的Class相似,有一點反射姿勢的可以很快上手

下面就是根據資料類資訊,拼接程式碼字串的方法

public static String genBeanColumnsCode(PsiClass clazz) {
    StringBuilder sb = new StringBuilder();
    sb.append(StringUtils.formatSingleLine(0, "public interface " + clazz.getName() + " extends BaseColumns {"));
    sb.append(StringUtils.formatSingleLine(1, "String TABLE_NAME = \"" + StringUtils.camel2underline(clazz.getName()) + "\";"));
    for (PsiField field : clazz.getFields()) {
        String name = StringUtils.camel2underline(field.getName()).toUpperCase();
        String value = name.toLowerCase();
        sb.append(StringUtils.formatSingleLine(1, "String " + name + " = \"" + value + "\";"));
    }
    sb.append("}");
    return sb.toString().trim();
}複製程式碼

clazz.getField獲取到類的所有成員變數,然後拼接成需要的程式碼

其中的StringUtils是自己封裝的工具類
camel2underline是將駝峰命名轉換成下劃線風格的字串
formatStringLine是在前面加縮排符,後面加換行符
拼好程式碼後,就可以用它去生成類、檔案、方法等等

和之前生成檔案類似,也是 createXXXFromText一類的方法,
可以用程式碼生成類、方法、語句、變數等等。
這裡我們就要根據程式碼去生成Columns類,也就是PsiClass物件

上一步我們已經獲取到了DataContract類了,新建的Columns要儲存在它裡面

PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
// 用拼接的程式碼生成Columns Class
String beanColumnsCode = CodeFactory.genBeanColumnsCode(clazz);
PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
PsiClass beanColumnsClass = factory.createClassFromText(beanColumnsCode, psiFile);
// 將建立的class新增到DataContract Class中
PsiClass fileClass = PluginUtils.getFileClass(psiFile);
fileClass.add(beanColumnsClass.getInnerClasses()[0]);複製程式碼

這次用到了 PsiElementFactory 類,然後用它去 createClassFromText 建立類
方法的第二個引數是Context,傳入所在File或所在Class都可以

然後將這個生成的類新增到DataContract檔案類中
注意不能是新增到DataContract檔案上,而是新增到檔案裡的類上
獲取方法如下(應該有更好的方法吧,暫時沒找到)

public static PsiClass getFileClass(PsiFile file) {
    for (PsiElement psiElement : file.getChildren()) {
        if (psiElement instanceof PsiClass) {
            return (PsiClass) psiElement;
        }
    }
    return null;
}複製程式碼

class.getInnerClasses()[0] 這裡要單獨說明下

createFileFromText的時候我們拼接的字串是完整的程式碼,
但是在createClassFromText的時候比較特殊,codeText是作為類主體部分的

String classCode = "public class MyClass {\n" +
                                "\tprivate String a;\n" +
                            "}";
PsiClass newClass = factory.createClassFromText(classCode, null);複製程式碼

如果你這樣去生成,那麼最終程式碼會是

class _Dummy_ {
    public class MyClass {
        private String a;
    }
}複製程式碼

這不是我們想要的!!!
所以一般做法是隻用類 { } 裡面的程式碼去生成,比如"private String a;"
而類的public等資訊需要額外設定,如下

newClass.setName("User"); // 設定類名,預設名為_Dummy_
newClass.getModifierList().add(factory.createKeyword(PsiKeyword.PUBLIC)); // 定義列表裡新增關鍵字public
newClass.getImplementsList().add(factory.createReferenceElementByFQClassName(
                "android.provider.BaseColumns", clazz.getResolveScope())); // 實現介面列表裡新增BaseColumns類複製程式碼

這就比較麻煩了,所以介紹個良心小技巧!
還是用全部程式碼生成,然後再獲取這個類的innerClasses內部類裡面的第一個就行了!
所以才有了上面的 class.getInnerClasses()[0] 的處理


四、整合程式碼,執行

將之前的程式碼封裝到DatabaseGenerator類中的genCode方法中,然後在action裡呼叫
action的相關介紹參考教程一

public class DatabaseGenerateAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent e) {
        Project project = e.getData(PlatformDataKeys.PROJECT);
        PsiFile file = e.getData(PlatformDataKeys.PSI_FILE);
        PsiClass clazz = PluginUtils.getFileClass(file);
        WriteCommandAction.runWriteCommandAction(project, () -> {
            DatabaseGenerator.genCode(file, clazz);
        });
    }
}複製程式碼

注意,這裡有個特殊的處理 WriteCommandAction.runWriteCommandAction
在外掛中,如果是新建File等操作是可以直接進行的。
但在DataContract檔案類中新增個內部類,這種寫入檔案內容的操作是需要特殊處理的,需要放在 WriteCommandAction.runWriteCommandAction 第二個引數的runnable中執行

搞定,效果圖見文章開始動態圖。
原始碼部分見
github.com/boredream/A…
歡迎star和follow~
下載原始碼後參考教程一先搭建環境,然後匯入專案

相關文章