Android 自定义注解处理器并生成json文件

最近在慕课网上学习Navigation相关的时候,一般Navigation的底部导航菜单设置是直接写xml文件,也通过自定义注解动态生成json文件然后去解析定制底部导航栏。由于这些东西都是比较新的东西之前没怎么接触过,所以特地写个demo来记录一下。

这个Demo中以新建项目中的 Bottom Navigation Activity 模板为例,实现三个Fragment实现对应json文件的解析。

初始化环境

  1. app下的build.gradle中统一JDK版本
    android {
         ...
     
         compileOptions {
             sourceCompatibility JavaVersion.VERSION_1_8
             targetCompatibility JavaVersion.VERSION_1_8
         }
     }
    
    
  2. 创建一个新的moudle名为libmyannotationprocessor,记得选择类型的时候选择Java Library
  3. 统一JDK环境,将moudle的build.gradle中的sourceCompatibilitytargetCompatibility改为8
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        ...
    }
    
    sourceCompatibility = "8"
    targetCompatibility = "8"
    
  4. build.gradle中引入所需资源
    这里用到了fastjson用作json转换处理,auto-service辅助实现注解处理器,最新版本可至git仓库中获取。
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        //fastjson
        implementation 'com.alibaba:fastjson:1.2.59'
        
        //auto service
        implementation 'com.google.auto.service:auto-service:1.0-rc6'
        annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    }
    

自定义注解

先自定义两个注解分别用于给ActivityFragment标记。

//public @interface FragmentDestination {
@Target(ElementType.TYPE) //标记在类、接口上
public @interface FragmentDestination {

    String pageUrl();  //url

    boolean needLogin() default false;  //是否需要登录 默认false

    boolean asStarter() default false;  //是否是首页  默认false
}

//ActivityDestination
@Target(ElementType.TYPE) //标记在类、接口上
public @interface ActivityDestination {

    String pageUrl();  //url

    boolean needLogin() default false;  //是否需要登录 默认false

    boolean asStarter() default false;  //是否是首页  默认false
}

自定义注解处理器

/**
 * Created by Hy on 2020/01/15 21:27
 * <p>
 * SupportedSourceVersion  源码类型  也可通过{@link #getSupportedSourceVersion()}设置
 * SupportedAnnotationTypes 设定需要处理的注解 也可通过{@link #getSupportedAnnotationTypes()}设置
 **/
@SuppressWarnings("unused")
@AutoService(Processor.class)  //auto-service
@SupportedSourceVersion(SourceVersion.RELEASE_8)  //源码类型 1.8
@SupportedAnnotationTypes({"com.yu.hu.libmyannotationprocessor.annotation.ActivityDestination", "com.yu.hu.libmyannotationprocessor.annotation.FragmentDestination"})
public class NavProcessor extends AbstractProcessor {

    private static final String OUTPUT_FILE_NAME = "destination.json";

    private Messager messager; //使用日志打印

    private Filer filer;  //用于文件处理

    //该方法再编译期间会被注入一个ProcessingEnvironment对象,该对象包含了很多有用的工具类。
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //日志打印,在java环境下不能使用android.util.log.e()
        this.messager = processingEnvironment.getMessager();
        //文件处理工具
        this.filer = processingEnvironment.getFiler();
    }

    /**
     * 该方法将一轮一轮的遍历源代码
     *
     * @param set              该方法需要处理的注解类型
     * @param roundEnvironment 关于一轮遍历中提供给我们调用的信息.
     * @return 改轮注解是否处理完成 true 下轮或者其他的注解处理器将不会接收到次类型的注解.用处不大.
     */
    @SuppressWarnings("ResultOfMethodCallIgnored")
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        //通过处理器环境上下文roundEnv分别获取 项目中标记的FragmentDestination.class 和ActivityDestination.class注解。
        //此目的就是为了收集项目中哪些类 被注解标记了
        Set<? extends Element> fragmentElements = roundEnvironment.getElementsAnnotatedWith(FragmentDestination.class);
        Set<? extends Element> activityElements = roundEnvironment.getElementsAnnotatedWith(ActivityDestination.class);

        if (!fragmentElements.isEmpty() || !activityElements.isEmpty()) {
            HashMap<String, JSONObject> destMap = new HashMap<>();
            //分别 处理FragmentDestination  和 ActivityDestination 注解类型
            //并收集到destMap 这个map中。以此就能记录下所有的页面信息了
            handleDestination(fragmentElements, FragmentDestination.class, destMap);
            handleDestination(activityElements, ActivityDestination.class, destMap);

            // app/src/assets
            FileOutputStream fos = null;
            OutputStreamWriter writer = null;
            try {
                //filer.createResource()意思是创建源文件
                //我们可以指定为class文件输出的地方,
                //StandardLocation.CLASS_OUTPUT:java文件生成class文件的位置,/app/build/intermediates/javac/debug/classes/目录下
                //StandardLocation.SOURCE_OUTPUT:java文件的位置,一般在/ppjoke/app/build/generated/source/apt/目录下
                //StandardLocation.CLASS_PATH 和 StandardLocation.SOURCE_PATH用的不多,指的了这个参数,就要指定生成文件的pkg包名了
                FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_FILE_NAME);
                String resourcePath = resource.toUri().getPath();
                messager.printMessage(Diagnostic.Kind.NOTE, "resourcePath: " + resourcePath);

                //由于我们想要把json文件生成在app/src/main/assets/目录下,所以这里可以对字符串做一个截取,
                //以此便能准确获取项目在每个电脑上的 /app/src/main/assets/的路径
                String appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4);
                String assetsPath = appPath + "src/main/assets";

                File file = new File(assetsPath);
                if (!file.exists()) {
                    file.mkdir();
                }

                //写入文件
                File outputFile = new File(file, OUTPUT_FILE_NAME);
                if (outputFile.exists()) {
                    outputFile.delete();
                }
                outputFile.createNewFile();

                //利用fastjson把收集到的所有的页面信息 转换成JSON格式的。并输出到文件中
                String content = JSON.toJSONString(destMap);
                fos = new FileOutputStream(outputFile);
                writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
                writer.write(content);
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (writer != null) {
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return true;
    }

    private void handleDestination(Set<? extends Element> elements, Class<? extends Annotation> annotationClass, HashMap<String, JSONObject> destMap) {
        for (Element element : elements) {
            //TypeElement是Element的一种。
            //如果我们的注解标记在了类名上。所以可以直接强转一下。使用它得到全类名
            TypeElement typeElement = (TypeElement) element;
            //全类名
            String className = typeElement.getQualifiedName().toString();
            //页面的id.此处不能重复,使用页面的类名做hascode即可
            int id = Math.abs(className.hashCode());
            //是否需要登录
            boolean needLogin = false;
            //页面的pageUrl相当于隐式跳转意图中的host://schem/path格式
            String pageUrl = null;
            //是否作为首页的第一个展示的页面
            boolean asStarter = false;
            //标记该页面是fragment 还是activity类型的
            boolean isFragment = false;

            Annotation annotation = typeElement.getAnnotation(annotationClass);
            if (annotation instanceof FragmentDestination) {
                FragmentDestination dest = (FragmentDestination) annotation;
                pageUrl = dest.pageUrl();
                needLogin = dest.needLogin();
                asStarter = dest.asStarter();
                isFragment = true;
            } else if (annotation instanceof ActivityDestination) {
                ActivityDestination dest = (ActivityDestination) annotation;
                pageUrl = dest.pageUrl();
                needLogin = dest.needLogin();
                asStarter = dest.asStarter();
            }

            if (destMap.containsKey(pageUrl)) {
                messager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl" + className);
            } else {
                JSONObject object = new JSONObject();
                object.put("id", id);
                object.put("needLogin", needLogin);
                object.put("asStarter", asStarter);
                object.put("pageUrl", pageUrl);
                object.put("className", className);
                object.put("isFragment", isFragment);
                destMap.put(pageUrl, object);
            }
        }
    }

    /**
     * 返回我们Java的版本.
     * <p>
     * 也可以通过{@link SupportedSourceVersion}注解来设置
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        //return SourceVersion.latest();
        return super.getSupportedSourceVersion();
    }

    /**
     * 返回我们将要处理的注解
     * <p>
     * 也可通过{@link SupportedAnnotationTypes}注解来设置
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
//        Set<String> annotataions = new LinkedHashSet<>();
//        annotataions.add(ActivityDestination.class.getCanonicalName());
//        annotataions.add(FragmentDestination.class.getCanonicalName());
//        return annotataions;
        return super.getSupportedAnnotationTypes();
    }

}

使用

  1. appbuild.gradle中引入自定义注解处理器
    dependencies {
         ...
         
         //引入项目已使用注解 如果觉得跟下面的合起来有点怪可以单独再创建一个moudle,把注解的定义放到那里面去
         implementation project(path: ':libmyannotationprocessor')
         //配置注解处理器
         annotationProcessor project(':libmyannotationprocessor')
     
     
     }
    
  2. 使用自定义注解,给默认生成的三个fragment加上自定义的注解:
    @FragmentDestination(pageUrl = "tabs/dashboard")
    public class DashboardFragment extends Fragment {
        ...
    }
    
    @FragmentDestination(pageUrl = "tabs/home")
    public class HomeFragment extends Fragment {
        ...
    }
    
    
    @FragmentDestination(pageUrl = "tabs/notification")
    public class NotificationsFragment extends Fragment {
        ...
    }
    
  3. 标记好注解后,重新Make Project即可在app/src/main/assets/生成对应的destination.json文件:
    {
        "tabs/dashboard":{
            "isFragment":true,
            "asStarter":false,
            "needLogin":false,
            "pageUrl":"tabs/dashboard",
            "className":"com.yu.hu.annotationprocessortest.ui.dashboard.DashboardFragment",
            "id":1222462875
        },
        "tabs/notification":{
            "isFragment":true,
            "asStarter":false,
            "needLogin":false,
            "pageUrl":"tabs/notification",
            "className":"com.yu.hu.annotationprocessortest.ui.notifications.NotificationsFragment",
            "id":2030977523
        },
        "tabs/home":{
            "isFragment":true,
            "asStarter":false,
            "needLogin":false,
            "pageUrl":"tabs/home",
            "className":"com.yu.hu.annotationprocessortest.ui.home.HomeFragment",
            "id":1050166585
        }
    }
    

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271