{"id":19633989,"url":"https://github.com/bilibili/brouter","last_synced_at":"2025-04-09T20:14:02.253Z","repository":{"id":45224254,"uuid":"289221560","full_name":"bilibili/BRouter","owner":"bilibili","description":null,"archived":false,"fork":false,"pushed_at":"2021-05-31T10:15:27.000Z","size":357,"stargazers_count":300,"open_issues_count":7,"forks_count":24,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-04-09T20:13:52.064Z","etag":null,"topics":["android","component","gateway","injection","module","router"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bilibili.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-21T08:43:36.000Z","updated_at":"2025-03-31T10:01:34.000Z","dependencies_parsed_at":"2022-08-04T14:45:38.089Z","dependency_job_id":null,"html_url":"https://github.com/bilibili/BRouter","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bilibili%2FBRouter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bilibili%2FBRouter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bilibili%2FBRouter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bilibili%2FBRouter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bilibili","download_url":"https://codeload.github.com/bilibili/BRouter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248103872,"owners_count":21048245,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","component","gateway","injection","module","router"],"created_at":"2024-11-11T12:19:15.463Z","updated_at":"2025-04-09T20:14:02.228Z","avatar_url":"https://github.com/bilibili.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 简介\n\nBRouter 是一个 Kotlin 编写的面向模块 Android 路由库，支持：\n\n* 路由\n    * 泛平台路由定义\n    * 自定义启动流程\n    * 统一路由协议\n    * 全局/模块/路由三级拦截器\n    * 灵活的路由匹配/捕获规则\n* 服务\n    * 服务发现\n    * 依赖注入\n* 任务\n    * 初始化去中心化\n    * 自动解析任务 / 服务依赖图\n    * 可选执行线程\n* 模块\n    * 模块生命周期\n* 数据\n    * 可扩展 APT\n    * 元数据描述\n    \n## 接入\n\n* Android Gradle Plugin \u003e= 3.3.0\n* Gradle \u003e= 5.3\n* Java \u003e= 1.8\n* Kotlin 可选\n\n\n```gradle\nbrouter_version = \"1.0.0\"\n\ndependencies {\n\n    // androidx\n    annotationProcessor \"com.bilibili.android:brouter-apt:$brouter_version\" // or kapt for Kotlin\n    implementation \"com.bilibili.android:brouter-api:$brouter_version\" // API 模块，通常仅需要依赖该模块，不要依赖 core\n    implementation \"com.bilibili.android:brouter-core:$brouter_version\" // 核心模块，仅需要配置初始化时用到\n    \n    // appcompat\n    nnotationProcessor \"com.bilibili.android:brouter-apt-appcompat:$brouter_version\"\n    implementation \"com.bilibili.android:brouter-core-appcompat:$brouter_version\"\n    implementation \"com.bilibili.android:brouter-api-appcompat:$brouter_version\"\n}\n\n```\n\nApplication 模块需要 apply 路由插件有 2 种方式：\n\n```gradle\nbuildscript {\n    dependencies {\n        classpath \"com.bilibili.android:brouter-gradle-plugin:$brouter_version\"\n    }\n}\n\napply plugin: 'com.bilibili.brouter'\n\n```\n\n或\n\n```\nplugins {\n  id \"com.bilibili.brouter\" version brouter_version\n}\n```\n\n## 使用\n\n### 初始化\n\n```kotlin\nBRouterCore.setUp(Application) { builder : GlobalConfiguration.Builder -\u003e\n    // 自定义初始化配置，可选\n}\n\n// 初始化配置 API，每个配置项的含义请参考接口文档\ninterface GlobalConfiguration.Builder {\n    val preMatchInterceptors: MutableList\u003cRouteInterceptor\u003e\n    val postMatchInterceptors: MutableList\u003cRouteInterceptor\u003e\n    val attributeSchema: AttributeSchema\n    fun defaultScheme(defaultScheme: String): Builder\n    fun logger(logger: SimpleLogger): Builder\n    fun emptyRouteTypeHandler(handler: EmptyTypeHandler): Builder\n    fun servicesMissFactory(factory: OnServicesMissFactory): Builder\n    fun authenticator(authenticator: RouteAuthenticator): Builder\n    fun addPreMatchInterceptor(interceptor: RouteInterceptor): Builder\n    fun addPostMatchInterceptor(interceptor: RouteInterceptor): Builder\n    fun routeListener(listener: RouteListener): Builder\n    fun routeListenerFactory(factory: RouteListener.Factory): Builder\n    fun executor(executor: ExecutorService): Builder\n    fun attributeSchema(action: (AttributeSchema) -\u003e Unit): Builder\n    fun globalLauncher(globalLauncher: GlobalLauncher): Builder\n    fun taskExecutionListener(taskExecutionListener: TaskExecutionListener): Builder\n    fun taskComparator(comparator: Comparator\u003cTask\u003e): Builder\n}\n\n```\n\n### 路由\n\n##### 声明一个简单路由\n\n```kotlin\n@Routes(\n    value = [ // 匹配规则，必填，其他均可选\n        \"(http|https)://www.bilibili.com/BV{bvid}\",\n        \"bilibili://video/BV{bvid}\"，\n    ], \n    exported = true, // 自动生成相关的 Manifest Deep Link 信息，仅 Library 模块有效\n    routeName = \"VideoDetails\",\n    desc = \"这是视频详情播放页\",\n    routeType = StandardRouteType.NATIVE, \n    interceptors = [CheckLoginInterceptor::class],\n    launcher = CustomLauncher::class\n)\n@Attribute(\"player\", \"main\") // 页面特性，可选\nclass VideoFragment\n```\n\n路由规则中末尾的`/`会被忽略，如果没有 scheme 则会使用上面初始化配置中的 defaultScheme。\n\n这里支持 3 类路由规则，请注意，前两类路由规则不能跨越`/`，且不同路由规则是互斥的，每个段内只能应用一类，优先级从高到低分别是\n\n1. **复合** ```(?\u003ccapture_name\u003epath1|path2|pathN)```，其中```?\u003ccapture_name\u003e```是可选的，括号是可嵌套的，如```(bilibili|http|https)```与```(bilibili|htt(p|ps))```匹配规则是一样的，注意括号中的字符串不能为空，即暂不支持```http(|s)```这样的格式，段内可以有多个\n2. **通配** `{capture_name}` 或者 `*`, 其中`*`等价`{}`，即匹配但是不捕获，段内只能有一个\n3. **前缀** ```**``` ，只能存在末尾，如```https://bilibili.com/**```，会匹配```https://bilibili.com/a/b/n```，但是不能匹配```https://bilibili.com```，一定会产生捕获，capture_name 为空字符串\n\n上述中所有的捕获会置于匹配后的路由信息中的，通常最终会被启动器作为参数带入页面中。\n\n##### 拦截器\n\n拦截器分为三个级别，依次是 全局 / 模块 / 路由，其中全局又分为匹配前与匹配后，继续上面的示例\n\n```kotlin\n@Singleton\n@Services\nclass CheckLoginInterceptor(private val loginService: LoginService) : RouteInterceptor {\n    override fun intercept(chain: RouteInterceptor.Chain): RouteResponse {\n        return if (!loginService.isLogin) {\n            RouteResponse(RouteResponse.Code.UNAUTHORIZED, chain.request)\n        } else {\n            chain.proceed(chain.request)\n        }\n    }\n}\n```\n\n路由内拦截器实例化的方式有2种，一种是尝试去取对应类的默认服务，即上面的示例，\n如果不存在则调用```OnServicesMissFactory.create```，默认实现是用无参构造函数反射。\n\n该拦截器做了登陆校验，如果未登录会返回未认证，如果上面初始化时配置了 ```RouteAuthenticator```，则会自动重定向到认证页，且认证后自动转发请求，具体用法见工程内示例。\n\n\n##### 路由请求\n\n###### RouteRequest\n\nBRouter 通过```RouteRequest```这个类来启动路由或者获取路由信息，示例：\n\n```kotlin\n\n// 启动\nval response = BRouter.routeTo(\"bilibili://video/BV1tC4y1H7yz\".toRouteRequest(), this)\n\n// 获取路由信息\nval response = \"bilibili://video/BV1tC4y1H7yz\".toRouteRequest().newCall(CallParams(RequestMode.ROUTE, this)).execute()\n\n```\n###### RouteResponse\n\n路由请求的结果是```RouteResponse```\n\n```kotlin\ninterface RouteResponse {\n    val code: Code\n    val request: RouteRequest\n    val message: String\n    val routeInfo: RouteInfo?\n    val obj: Any?\n    val redirect: RouteRequest?\n    val flags: Int\n    val priorFailureResponse: RouteResponse?\n    val priorRouteTypeResponse: RouteResponse?\n    val prevRequestResponse: RouteResponse?\n    enum class Code {\n        OK,\n        REDIRECT,\n        BAD_REQUEST,\n        UNAUTHORIZED,\n        FORBIDDEN,\n        NOT_FOUND,\n        ERROR,\n        FOUND_STUB,\n        UNSUPPORTED,\n    }\n}\n```\n其中```RouteInfo```是路由匹配的结果，在拦截器中也可以获取到\n\n```\ninterface RouteInfo : HasAttributes {\n    val routeName: String\n    val routeRule: String\n    val routeType: String\n    val captures: Map\u003cString, String\u003e\n    val target: Class\u003c*\u003e\n    val interceptors: Array\u003cout Class\u003cout RouteInterceptor\u003e\u003e\n    val module: Module\n}\n```\n其中每个字段的含义请参阅接口文档。\n\n###### 统一路由协议\n\n```RouteRequest```是统一路由协议的超集\n\n* 任意一条统一路由协议的字符串均可转化为一个 RouteRequest 对象\n* 任意一个 RouteRequest 对象也可以转化为一条统一路由协议，但是部分字段可能会丢失\n\n举例：\n\n```kotlin\n\"bilibili://video/BV1tC4y1H7yz\".toBuilder()\n    .routeTypes(\"native\", \"web\")\n    .flags(1)\n    .prev(\"https://www.bilibili.com\".toRouteRequest())\n    .forward(\"https://baidu.com\".toRouteRequest())\n    .props {\n        it.append(\"prop1\", \"1\")\n            .append(\"prop1\", \"2\")\n            .append(\"prop2\", \"2\")\n    }\n    .params {\n        it.append(\"param1\", \"1\")\n            .append(\"param1\", \"2\")\n    }\n    .attributes {\n        it.attribute(\"player\", \"main\")\n    }\n    .build()\n// 等价于\n(\"bilibili://video/BV1tC4y1H7yz?\" +\n        \"-Bpt.types=native\u0026\" +\n        \"-Bpt.types=web\u0026\" +\n        \"-Bpt.flags=1\u0026\" +\n        \"-Bpt.prev=${Uri.encode(\"https://bilibili.com\")}\u0026\" +\n        \"-Bpt.forward=${Uri.encode(\"https://baidu.com\")}\u0026\" +\n        \"-Bprop1=1\u0026\" +\n        \"-Bprop1=2\u0026\" +\n        \"-Bprop2=2\u0026\" +\n        \"param1=1\u0026\" +\n        \"param1=2\u0026\" +\n        \"-Aplayer=main\").toRouteRequest()\n```\n\n```RouteRequest```中，下面的几个字段是其独享的，无法与统一路由协议互转\n\n* extras: Bundle，额外参数\n* options: 参见```startActivity(Intent intent,Bundle options)```\n* animIn, animOut: 参见```Activity.overridePendingTransition(int enterAnim, int exitAnim)```\n\n其他均可用统一路由协议表示，具体参数参见```UniformProtol```这个类。\n\n一个最佳实践：\n\n* 页面应当支持只需要 params 中的参数就可以正常启动，这样可以直接被 URI 唤起\n* props 作为属性参数，通常用于 容器 / UI 的控制参数，而非页面的本身的逻辑参数\n* attributes 用于描述页面的一些性质，当多页面路径匹配且优先级一致时才会发生匹配\n* 可以认为 RouteType 是一个特殊的特质，但是发生在页面路径匹配之前，请求存在多 RouteType 时，会依次查询直到第一个成功为止\n\n##### 更多路由功能\n\n请参阅示例，文档待补齐。\n\n### 服务\n\n#### 声明服务\n\nBRouter 支持在类上或者静态方法上声明服务\n\n##### 注解在类上\n\n在这种情况，BRouter会查找公开构造函数来创建服务的实例，如果有多个，则寻找```@Inject```注解的构造函数\n\n```kotlin\ninterface LoginService\n\n@Singleton // 会缓存第一个实例作为单例\n@Named(\"v1\") // 默认为 \"default\"\n@Services(LoginService::class) // 暴露的服务类，不填则为被注解类本身\nclass LoginManager1 : LoginService\n```\n\n##### 注解在静态方法\n```java\nclass LoginManager2 implements LoginService  {\n    \n    @Named(\"v2\")\n    @Services(LoginService.class)\n    synchronized public static LoginManager getInstance(@Named(\"app\") Application app， Provider\u003cStorageService\u003e storage) {\n        ...\n    } \n}\n```\n\n注意，无论是注解在类上还是静态方法上，其目标方法中的参数均会作为被依赖的服务来查找，通过添加```@Named(\"service_name\")```来表示服务的名字，默认为```\"default\"```\n\n#### 消费服务\n\n##### 声明在服务的函数参数中\n\n如上所示，服务是可以依赖服务的，通过声明对应的参数即可获取对应依赖的服务\n\n##### 通过 @Inject 注解\n\n```kotlin\nclass Consumer {\n    @Inject\n    @Named(\"v2\")\n    lateinit var service1: LoginService? // ? 表示 Nullable\n    \n    @Inject\n    @Named(\"v2\")\n    lateinit var service2: Provider\u003cLoginService\u003e\n    \n    @Inject\n    @Named(\"v2\")\n    lateinit var service3: Provider\u003cout LoginService\u003e\n\n    init {\n        BRouter.inject(this)\n    }\n}\n```\n\n服务的消费不单单可以直接获取实例，也可以以```Provider\u003cT\u003e```或 ```Provider\u003c? extends T\u003e```来获取一个延时创建实例的 Provider\n\n##### 直接获取\n```kotlin\nval service = BRouter.services.getService(LoginService::class.java, \"v1\") \n```\n\n值得注意的是，通过注解静态注册的方式，可以获得编译期的检查，确保被依赖的服务均存在且不存在循环依赖，另外可通过添加```@Nullable```注解或者 Kotlin 的 ? 语法表示服务是可空的\n\n\n### 模块\n\n##### 声明模块\n\n所有的路由与服务均归属于模块，通过注解```@ModuleOptions```来显示声明一个模块，如果不声明模块则会隐式的创建\n\n```java\n@ModuleOptions(name = \"test\")\npublic class TestModule extends ModuleActivator {\n\n    private final TeenagerService service;\n\n    public TestModule(TeenagerService service) {\n        this.service = service;\n    }\n\n    @Override\n    public void onCreate(@NotNull ModuleContext context, @NotNull ModuleConfigurationModifier modifier) {\n        super.onCreate(context, modifier);\n        modifier.addModuleInterceptors(chain -\u003e {\n            if (service.isEnabled()) {\n                return RouteResponseKt.redirectTo(chain.getRequest(),\n                        RouteRequestKt.toRouteRequest(\"bilibili://teenager\"));\n            } else {\n                return chain.proceed(chain.getRequest());\n            }\n        });\n    }\n}\n```\n\n如果模块注解的类可选继承```ModuleActivator```，以处理对应的回调\n```onCreate``` ```onPostCreate```，注意以下几点：\n\n1. 所有归属这个模块的服务，均隐式的依赖了这个模块的```onCreate```\n2. 可以构造函数在构造函数中声明对其他模块服务的依赖\n3. ```onCreate``` 与 ```onPostCreate``` 其实是特殊的 Task，因此可以在上面加```TaskOptions```注解来配置任务\n4. ON_DEMAND 模块的 ```onCreate``` 在模块的路由或者服务第一次被获取时触发或者手动触发\n5. ON_INIT 则在```BRouterCore.setUp```触发，均为异步\n\n##### 模块 API\n\n```kotlin\nval module = BRouter.module(\"test\")\n\n// 模块 API\ninterface Module : HasAttributes {\n    val name: String\n    val mode: BootStrapMode\n    val status: ModuleStatus\n    val moduleInterceptors: List\u003cRouteInterceptor\u003e\n    val tasks: TaskContainer\n    fun moveStatusTo(status: ModuleStatus) // 手动触发状态转移\n    @Throws(InterruptedException::class)\n    fun awaitAtLeast(status: ModuleStatus) // 同步等待状态\n    fun whenStatusAtLeast(status: ModuleStatus, action: (Module) -\u003e Unit) // 异步监听状态\n}\n```\n\n##### 多模块\n\nBRouter 支持在一个 Gradle Project 内声明多个模块\n\n```kotlin \nannotation class ModuleOptions(\n    val name: String,\n    val mode: BootStrapMode = BootStrapMode.ON_INIT,\n    val desc: String = \"\",\n    val defaultModule: Boolean = true\n)\n```\n\n但是请注意，路由 / 服务 / 任务 在未指定所属模块时，会属于默认模块，即当存在多个模块时，应当只能有一个模块为 defaultModule = true。\n\n另外声明路由 / 服务 / 任务 时，可通过注解```@BelongsTo```来指定所属的模块\n\n``` koltin \nannotation class BelongsTo(val value: String)\n```\n\n### 任务\n\nBRouter 的任务是一次性的，用完即毁，仅适用于启动任务，通过```@TaskOptions```，标注一个实现```TaskAction```的类，即可声明一个任务：\n\n```kotlin\n@TaskOptions(\"task2\", dependencies = [\"lib2.task1\"], threadMode = ThreadMode.ANY)\npublic class Task2(@Named(\"v1\") service: LoginService) : TaskAction {\n    override fun execute(task: Task) {\n        executor = Executors.newSingleThreadExecutor()\n    }\n\n    @Named(\"cached\")\n    @TaskOutput(ExecutorService::class, Executor::class)\n    lateinit var executor: ExecutorService\n}\n```\n\n同样，构造函数可依赖服务，比较特别的是，```@TaskOutput```会产生服务，而且因为 Task 一次性的性质，```@TaskOutput```产生的服务是单例的，所以慎用。\n\nTask 也是归属一个模块的，因此在```dependencies```里声明对其他 Task 名的依赖时，要注意如果是其他模块的 Task 要加上模块名字与点，如果是同模块的可以只写 Task 名。\n\n\n### 数据\n\nBRouter 是数据驱动的，所有的 Route / Service / Task / Module 均由 JSON 文件描述，因此拥有了强大的扩展性。\n\n##### 自定义 APT\n\n众所周知 APT 是通过实现 Processor 这个接口并注册到 SPI 中来实现的，BRouter 借鉴了这个思想，将 Processor 中的回调做二次转发给 MetaProcessor 给来收集元信息，并在最后一轮生成元数据文件与 Java 代码。\n\n包括 BRouter 自身的注解也是通过 MetaProcessor 来收集的，有兴趣的同学可以一阅源码，这里给一个简单的示例，我们以 ARouter 中的 Route 注解为例：\n\n```kotlin\n@AutoService(MetaProcessor::class)\nclass CompatRouteProcessor : MetaProcessor {\n    override val supportedAnnotations: Set\u003cString\u003e\n        get() = setOf(Route::class.java.name)\n\n    override fun process(\n        annotations: Set\u003cTypeElement\u003e,\n        roundEnv: RoundEnvironment,\n        processingEnv: ProcessingEnvironment,\n        collector: MetaCollector\n    ) {\n        roundEnv.getElementsAnnotatedWith(Route::class.java).forEach { element -\u003e\n            element as TypeElement\n            collector.addRoute { builder -\u003e\n                val route = element.getAnnotation(Route::class.java)\n                builder.className(element.javaClassName)\n                    .routeRules(listOf(route.path.also {\n                        require(it.isNotEmpty()) {\n                            \"Path is empty.\"\n                        }\n                    }))\n\n                route.name.let {\n                    if (it.isNotEmpty()) {\n                        builder.routeName(it)\n                    }\n                }\n\n                builder.attributes(\n                    listOf(\n                        \"group\" to route.group,\n                        \"extras\" to route.extras.toString()\n                    )\n                )\n            }\n        }\n    }\n}\n```\n\n示例里用了 Google 的 AutoService 来自动生成服务描述文件，也可以替换成手写。将其注册到```annotationProcessor```或```kapt```中即可实现对 ARouter 中```@Route```注解的兼容。\n\n通过类似上面的方式，可以很方便的将其他版本的路由迁移到 BRouter 中。\n\n##### 数据模型\n\n编译 Application 模块后，BRouter 会在```build/outputs/${variant_name}/brouter``` 下产生描述所有上述功能的元数据 JSON。\n\n通过解析该文件，可用于 CI / CD / 自动化测试 / 数据可视化等。\n\n### 路由插件\n\n插件的所有产物均在 build/intermediates/brouter 与 build/outputs/brouter 目录下，可自行查找。\n\n路由插件只有一个 id，但是在不同的模块下有不同的效果\n\n```groovy\napply plugin: 'com.bilibili.brouter'\n```\n\n#### Application \n\nApplication 模块必须要 apply 路由插件来生成必要的类，否则无法使用。\n\n另外可配置导出 Deep Link 的目标类，所有要导出的路由会生成对应的 AndroidManifest 信息，如果不配置类名则忽略。\n\n```kotlin\nbrouter {\n    exportedActivityClass(\"com.bilibili.brouter.example.EntranceActivity\")\n}\n```\n\n#### Library\n\nLibrary 模块下插件有 2 个功能\n\n##### Android Test\n插件同样可以为 Android Test 生成对应的类，因此当模块 A 依赖模块 B 的暴露服务时，在构建测试 APK 时可以选择依赖模块 B 的实现或者在 A 的 androidTest 下用注解 mock B 的实现。\n\n##### 导出模块服务\n\n当模块需要暴露服务时，可使用路由插件，将需要暴露的 API 定义在 src/${sourceSetName}/api 目录下，目前仅支持 Java。\n\n\n```groovy\n\n// A\ndependencies {\n    // API 编译需要依赖的外部类使用 serviceApi 来依赖，例如暴露的接口用到了 RxJava\n    serviceApi \"io.reactivex.rxjava3:rxjava:3.0.0\"\n    \n    // 只为 release 依赖\n    releaseServiceApi ...\n    \n    // 依赖另一个模块导出的 API\n    serviceOnly project(\":B\")\n}\n\n// B\ndependencies {\n    serviceOnly project(\":A\")\n    // 如果 Library 打包时 apply 了路由插件，远程依赖也可以通过这种方式来获取暴露的服务\n    serviceOnly \"com.bilibili:x:1.0.0\"\n}\n\n// 支持模块之间互相依赖彼此导出的 API\n```\n\n简单的原理图\n![](media/configurations_for_service.jpg)\n\n## Java\n\n所有的 Kotlin 示例或者 API 均支持 Java，请自行查阅。\n\n## 混淆\n\n*brouter-core* 中自带了混淆规则，无需特别配置\n\n## 沟通\n\n* 推荐在 Issues 下留言沟通\n* 联系作者 dieyidezui@gmail.com\n\n## 致谢\n\n* [Gradle](https://github.com/gradle/gradle) \n* [OkHttp](https://github.com/square/okhttp)\n\n## License\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbilibili%2Fbrouter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbilibili%2Fbrouter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbilibili%2Fbrouter/lists"}