注:阅读本文之前,建议先阅读一下:
ToyBricks简介以及原理分析

ToyBricks简介

ToyBricks是一个Android项目模块化的解决方案,主要包括四个部分,APT注解,APT注解处理器,ToyBricks插件(Gradle Plugin),ToyBricks库。

ToyBricks简介

其中:

  1. APT注解,主要定义了两个注解:Interface(接口,例如:IText),Implementation (实现,例如:TextImpl)
  2. APT注解处理器,在javac编译java源码之前。APT注解处理器会扫描Java源码中带有上面两个注解的接口和类,并且生成一个json文件, ToyBricks.json.
  3. ToyBricks插件(Gradle Plugin),负责ToyBricks.json的打包,合并,生成Java源文件等工作
  4. ToyBricks,提供对外调用方法。通过参数传入接口,返回相应的实现。

ToyBricks特性

  1. 同时支持Kotlin,Java
  2. 支持Android Build Variants
  3. Proguard免配置

ToyBricks局限性

ToyBricks具有传染性。
任何Android Application 或者 Android Library 使用包含ToyBricks.json的jar包或者aar包作为依赖,都必须继续使用ToyBricks,否则无法保证代码能正确运行。

ToyBricks规则

每个模块分为接口和实现两个部分。接口部分提供给模块外部调用,而实现部分则禁止来自外部的调用。

模块划分

接口 以@Interface进行注解,示例如下:

1
2
3
4
5
@Interface
public interface IText {
String getText();
}

实现 以@Implementation进行注解,示例如下:

1
2
3
4
5
6
7
@Implementation(IText.class)
public class NewTextImpl implements IText {
@Override
public String getText() {
return "NewTextImpl Implementation from "+ getClass().getCanonicalName();
}
}

@Implementation更详细的使用方法如下:

1
2
3
4
5
6
7
@Implementation(value = IText.class,global = true,singleton = true)
public class NewTextGobalImpl implements IText {
@Override
public String getText() {
return "NewTextImpl Implementation from "+ getClass().getCanonicalName() ;
}
}

参数说明如下:

  1. value后面应该填写接口的class
  2. global代表这个实现拥有高优先级。如果没有设置,默认取值为false。
  3. singleton代表这个实现将会以单例形式存在。如果没有设置,默认取值为false。由于单例会一直存在于APP的整个生命周期,因此,不应该滥用单例。

使用ToyBricks应该遵守以下规则:

  1. 接口命令强烈建议以I开头,例如:IText
  2. 每一个接口都必须至少有一个相应实现,否则编译出错。
  3. 每个实现类必须是public的。
  4. 每个实现类必须实现注解中标明的接口。
  5. 每个实现类必须拥有一个默认无参数的构造函数。
  6. 每个实现类不可以是abstract抽象类。
  7. 实现一共分成三类,全局实现(global=true),普通实现(global=false),替补实现(按照默认的包名和类名进行加载)。它们的优先级从前往后一直降低。每一类只能同时存在一个实现,否则编译出错。
  8. 替补实现遵循以下规则:
    假如接口为:com.github.snowdream.toybricks.app.IText,
    则替补实现为:com.github.snowdream.toybricks.app.impl.TextImpl
    实现的包名为接口的包名+”.impl”,实现的类名为接口名去掉第一个字母,第二个字母大写,然后加上”Impl”。
  9. 任何发布到Maven仓库的库,都应该将global设置为false。global设置为true,只适合Android Application测试新实现,或者替换默认实现。

ToyBricks使用方法

由于ToyBricks对Android的Gradle编译流程稍微进行了修改,因此,请注意按照下面的说明进行详细操作。
对于编译流程的修改如下:

  1. Android Application模块:首先按照上面的步骤生成ToyBricks.json,然后和依赖库中的ToyBricks.json进行合并,校验,最后按照预定规则,生成InterfaceLoaderImpl.java源代码,并参与编译。
  2. Android Library: 通过APT生成Json文件(ToyBricks.json,生成目录在“build/generated/source/apt”或者“build/generated/source/kapt”),然后打包到aar。如果你需要打包成jar包,并发布到maven仓库,请参考下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
task androidReleaseJar(type: Jar,dependsOn: "assembleRelease") {
from "$buildDir/intermediates/classes/release/"
from "$buildDir/generated/source/kapt/release/ToyBricks.json"
from "$buildDir/generated/source/apt/release/ToyBricks.json"
exclude '**/BuildConfig.class'
exclude '**/R.class'
exclude '**/R$*.class'
includeEmptyDirs false
}
task androidJavadocsJar(type: Jar) {
classifier = 'javadoc'
from "generateReleaseJavadoc.destinationDir"
includeEmptyDirs false
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
from "$buildDir/generated/source/kapt/release/ToyBricks.json"
from "$buildDir/generated/source/apt/release/ToyBricks.json"
includeEmptyDirs false
}

Gradle主工程

在主工程的build.gradle文件中添加ToyBricks的gradle插件。

1
2
3
4
5
6
7
8
9
10
11
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
classpath 'com.github.snowdream.toybricks:android-toybricks-gradle-plugin:0.9.10'
}
}

Android Library模块

在Library模块的build.gradle文件中添加ToyBricks的相关库依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
kapt {
generateStubs = true
}
dependencies {
compile "com.github.snowdream:toybricks:0.9.10@aar"
compile "com.github.snowdream.toybricks:annotation:0.9.10"
kapt "com.github.snowdream.toybricks:processor:0.9.10"
//annotationProcessor "com.github.snowdream.toybricks:processor:0.9.10"
}
apply plugin: 'com.github.snowdream.toybricks'

注: 有两种引用方式,一种是kotlin的kapt方式,需要配置上面的generateStubs。另外一种,是Android默认支持的annotationProcessor方式。

如果你的工程中包含任何Kotlin源文件,则必须选择kapt的方式,否则,可以选择annotationProcessor方式。

Android Application模块

在Application模块的build.gradle文件中添加ToyBricks的相关库依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kapt {
generateStubs = true
}
dependencies {
compile 'com.github.snowdream:annotation:0.7@aar'
compile "com.github.snowdream:toybricks:0.9.10@aar"
compile "com.github.snowdream.toybricks:annotation:0.9.10"
kapt "com.github.snowdream.toybricks:processor:0.9.10"
//annotationProcessor "com.github.snowdream.toybricks:processor:0.9.10"
}
apply plugin: 'com.github.snowdream.toybricks'

ToyBricks使用方法

通过上面的方式,开发好接口和实现模块后。只需要引用接口,就可以通过下面方式获取接口的实现:

1
IText text = ToyBricks.getImplementation(IText.class);

其中,IText为接口。

ToyBricks最佳实践

按照替补实现的命名规则,来开发接口的实现类。

如果您对ToyBricks有什么问题或者建议,欢迎通过后面的联系方式联系我。

参考资料:

  1. SnowdreamFramework/ToyBricks
  2. SnowdreamFramework/log

联系方式

sn0wdr1am

ToyBricks背景

我始终认为,在高内聚,低耦合的原则下,进行组件化,模块化,插件化都是移动应用开发的趋势。

为什么这么说呢?下面我们举个栗子:
大家都知道,以前Android应用开发中,可以使用HttpClient或者HttpUrlConnection来进行http访问。这里假设有一个耦合严重,但代码量巨大的项目,使用了基于HttpClient封装的loopj/android-async-http来进行http访问。但是,后来,Google明确支持使用HttpUrlConnection。此时,经过调研,你们觉得square/okhttp基于HttpUrlConnection,符合你们的要求。

现在,不管是否将HttpClient替换成okhttp,你们都可能面临以下困境:

  1. 需求都做不完,根本没有排期做这个替换。于是,你们面临离google的支持越来越远,离风险越来越近的困境;
  2. 辛辛苦苦耗费人力将HttpClient替换成okhttp。但由于两者变化很大,需要投入很多测试资源,来重新确认这些接口是否正常访问。一旦出现问题,还需要安排研发资源,去一一排查。
  3. 替换的工作量太大。替换一部分之后,发现没有足够人力去继续完成。于是,替换终止。整个工程又变得混乱和臃肿,同时包含了两种http的封装库和调用。

模块化可以有效解决这些问题。通用的做法,是按照业务,功能等将整个项目分成不同的模块,由不同的研发测试小组负责。
每个模块又分为接口和实现两个部分。接口部分提供给模块外部调用,而实现部分则禁止来自外部的调用。

[apt]

那么,如何将模块的接口和实现部分关联起来呢?通过APT工具,可以轻松地将接口部分和实现部分关联起来。

APT,即Annotation Processing Tool,可以理解为“编译时注解处理器工具”。

官方说明:
“The apt tool is a command-line utility for annotation processing. It includes a set of reflective APIs and supporting infrastructure to process program annotations (JSR 175). These reflective APIs provide a build-time, source-based, read-only view of program structure. They are designed to cleanly model the Java programming language’s type system after the addition of generics (JSR 14).”

简单理解如下:
apt工具是javac工具的一部分。在编译时,apt工具首先会扫描工程下Java源码中的编译时注解,再根据预先定义的编译时注解处理工具,生成指定的Java源码文件。紧接着,生成的Java源码文件和之前项目下的Java源码一起,由javac工具来编译成class。

但是,APT有一个局限性,就是只会扫描Java源码,不会扫描jar ,aar 和class 。也就是说,所有模块需要以源码形式存在。而现在通用的做法是,将模块打包成jar或者aar,发布到Maven库,再由其他模块自行引用。

有没有办法将APT的这种功能和特性延伸到jar和aar呢?
于是,ToyBricks应运而生。

ToyBricks简介

ToyBricks简介

ToyBricks是一个Android项目模块化的解决方案,主要包括四个部分,APT注解,APT注解处理器,ToyBricks插件(Gradle Plugin),ToyBricks库。

[ToyBricks]

其中:

  1. APT注解,主要定义了两个注解:Interface(接口,例如:IText),Implementation (实现,例如:TextImpl)
  2. APT注解处理器,在javac编译java源码之前。APT注解处理器会扫描Java源码中带有上面两个注解的接口和类,并且生成一个json文件, ToyBricks.json.
  3. ToyBricks插件(Gradle Plugin),负责ToyBricks.json的打包,合并,生成Java源文件等工作
  4. ToyBricks,提供对外调用方法。通过参数传入接口,返回相应的实现。

ToyBricks原理分析

下面以接口IText和实现TextImpl为例,简单介绍下ToyBricks原理。

主要分为两个部分:

Android Library(最终可能打包成jar,aar,并发布到maven库)

如果工程是Android库模块,则主要流程如下:
1.在javac编译java源码之前,由APT注解处理器扫描Java源码中带有上面两个注解的接口和类,生成ToyBricks.json。

1
2
3
4
5
6
7
8
9
10
{
"interfaceList" : [ "com.github.snowdream.toybricks.app.IText" ],
"globalImplementation" : {
"com.github.snowdream.toybricks.app.IText" : "com.github.snowdream.toybricks.app.impl.NewTextGobalImpl"
},
"defaultImplementation" : {
"com.github.snowdream.toybricks.app.IText" : "com.github.snowdream.toybricks.app.impl.TextImpl"
},
"singletonImplementation" : [ "com.github.snowdream.toybricks.app.impl.NewTextGobalImpl" ]
}

2.在打包jar,aar的时候,由ToyBricks插件(Gradle Plugin)提前处理,保证ToyBricks.json能被拷贝进去jar包或者aar包的根目录下,并随同一起分布到maven仓库。

Android Application

如果工程是Android应用模块,则主要流程如下:

  1. 第一步,和Android Library第一步一致。
  2. Javac编译Java源代码
  3. 扫描所有依赖的库文件,过滤出所有包含ToyBricks.json文件的jar包或者aar包,并且提取出来。提取完毕后,合并所有的ToyBricks.json文件,成为一个ToyBricks.json。
  4. 将最终的ToyBricks.json按照预定规则生成一个Java源码文件.文件名为: InterfaceLoaderImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.github.snowdream.toybricks.annotation.impl;
import com.github.snowdream.toybricks.annotation.InterfaceLoader;
import java.lang.Class;
import java.lang.Override;
import java.util.HashMap;
/**
*
* Created by snowdream
*
* This file is automatically generated by apt(Annotation Processing Tool)
* Do not modify this file -- YOUR CHANGES WILL BE ERASED!
*
* This file should *NOT* be checked into Version Control Systems,
* as it contains information specific to your local configuration.
*/
final class InterfaceLoaderImpl implements InterfaceLoader {
private static HashMap<Class, Object> sSingletonMap = new HashMap<Class, Object>();
private static HashMap<Class, Class> sGlobalMap = new HashMap<Class, Class>();
private static HashMap<Class, Class> sDefaultMap = new HashMap<Class, Class>();
public InterfaceLoaderImpl() {
addGlobalMap();
addDefaultMap();
addSingletonMap();
}
private void addGlobalMap() {
sGlobalMap.put(com.github.snowdream.toybricks.app.IText.class,com.github.snowdream.toybricks.app.impl.NewTextGobalImpl.class);
}
private void addDefaultMap() {
sDefaultMap.put(com.github.snowdream.toybricks.app.IText.class,com.github.snowdream.toybricks.app.impl.TextImpl.class);
}
private void addSingletonMap() {
sSingletonMap.put(com.github.snowdream.toybricks.app.impl.NewTextGobalImpl.class,null);
}
@Override
public <T> T getImplementation(Class<T> clazz) {
T implementation = null;
boolean isSingleton = false;
Class implClass;
implClass = sGlobalMap.get(clazz);
if (implClass == null) {
implClass = sDefaultMap.get(clazz);
}
if (implClass != null) {
isSingleton = sSingletonMap.containsKey(implClass);
if (isSingleton) {
implementation = (T) sSingletonMap.get(implClass);
if (implementation != null) {
return implementation;
}
}
try {
implementation = (T) implClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (isSingleton && implementation != null) {
sSingletonMap.put(implClass, implementation);
}
}
return implementation;
}
}

5.再次使用Javac工具编译InterfaceLoaderImpl.java文件。
6.这个文件就类似字典索引,通过这个文件,就可以通过传入接口,来查找对应的实现类。

总结

与APT工具相比,ToyBricks能够将接口和实现之间的关系进行持久化,存储在jar和aar中,并随之发布到Maven仓库,实现接口和实现的彻底分离。

如果您对ToyBricks有什么问题或者建议,欢迎通过后面的联系方式联系我。

参考资料:

  1. SnowdreamFramework/ToyBricks
  2. Annotation Processing Tool (apt)
  3. ANNOTATION PROCESSING 101
  4. Annotation-Processing-Tool详解
  5. Java注解处理器
  6. 什么是高内聚、低耦合?

联系方式

sn0wdr1am

原文:http://mrhaki.blogspot.com/2017/04/gradle-goodness-enable-build-cache-for.html

Gradle 3.5 引入了构建缓存特性。通过构建缓存,我们可以在不同的电脑之间,不同的builds之间,共享Task输出结果。举个例子,持续集成服务器上构建的输出,可以在开发者的电脑上复用。要启用这项特性,我们只需要添加命令行选项--build-cache。或者,我们还可以我们工程下的gradle.properties文件中,将org.gradle.caching属性设置为true。为了对所有工程都启用构建缓存特性,我们可以在Gradle主目录下的gradle.properties文件( USER_HOME/.gradle/gradle.properties)中设置这个属性。

在下面的例子中,我们在~/.gradle/gradle.properties文件中设置org.gradle.caching属性:

1
2
# File: ~/.gradle/gradle.properties
org.gradle.caching=true

如果我们想禁用这项特性,我们可以添加命令行选项--no-build-cache

基于 Gradle 3.5 编写。

联系方式

sn0wdr1am

原文:http://mrhaki.blogspot.com/2016/12/gradle-goodness-run-task-ignoring-up-to.html

Gradle构建很快,是因为支持增量任务。简单来说,Gradle可以在运行Task前,就知道Task的输入和输出是否改变。如果什么都没有改变,那么这个Task将会被标记为up-to-date,并且不会被执行,否则则会被执行。如果不管一个Task是否是up-to-date,我们都希望能够运行它,此时我们需要增加一个命令行选项--rerun-tasks

在下面的例子中,我们为一个简单的Java工程,运行assemble任务,可以看到所有的task都运行了。当我们再次运行此Task的时候,我们看到所有的task都提示“up-to-date”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ gradle assemble
:compileJava
:processResources
:classes
:jar
:assemble
BUILD SUCCESSFUL
Total time: 1.765 secs
$ gradle assemble
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
BUILD SUCCESSFUL
Total time: 0.715 secs
$

为了忽略up-to-date检查,强制执行所有task,我们需要添加这个选项 –rerun-tasks:

1
2
3
4
5
6
7
8
9
10
11
$ gradle --rerun-tasks assemble
:compileJava
:processResources
:classes
:jar
:assemble
BUILD SUCCESSFUL
Total time: 1.037 secs
$

基于Gradle 3.2.1编写。

联系方式

sn0wdr1am

原文: http://mrhaki.blogspot.com/2017/02/gradle-goodness-check-operating-system.html

有时候,我们想检测下构建脚本在哪个操作系统上执行。举个例子,比如我们有一些Task,需要在Windows操作系统上执行,但是不在其他操作系统上执行。Gradle有一个内部的类org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem,但是我们不应该在构建脚本中使用这个类。Gradle在内部使用这个类,并且可能毫无警告地修改它。如果我们依赖这个类,一旦这个类被修改了,我们的构建脚本可能无法正常运行。但是,我们可以使用一个来自Ant的类(org.apache.tools.ant.taskdefs.condition.Os)。这个类包含各种方法和变量,来检测操作系统名称,版本和架构。 这些取值都基于Java的系统变量,包括os.name, os.version 和 os.arch.

在下面的例子中,为了我们稍后能够直接调用这个类的方法和引用这个类的常量,我们在构建脚本中静态引入这个Os类。 我们添加了几个基于onlyif条件的Task。只有当onlyif条件为true时,Task才会被执行。Task osInfo则简单显示了当前操作系统的一些属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// File: build.gradle
import static org.apache.tools.ant.taskdefs.condition.Os.*
task os {
description 'Run all conditional os tasks'
}
// Create 3 tasks that simply print
// the task name that is executed
// if the build scripts runs on the
// recognized operating system.
[FAMILY_WINDOWS, FAMILY_UNIX, FAMILY_MAC].each { osName ->
// Create task.
tasks.create(osName) {
description "Run when OS is ${osName}"
// Add condition to check operating system.
onlyIf { isFamily(osName) }
doLast {
println "Execute task '${it.name}'"
}
}
// Add task as dependency for the os task.
os.dependsOn osName
}
task osInfo {
description 'Show information about the operating system'
doLast {
println "Family: ${OS_NAME}"
println "Version: ${OS_VERSION}"
println "Architecture: ${OS_ARCH}"
}
}

接下来,让我们在MacOS上运行这些Task: os and osInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gradle os osInfo
mac
Execute task 'mac'
:unix
Execute task 'unix'
:windows SKIPPED
:os
:osInfo
Family: mac os x
Version: 10.12.3
Architecture: x86_64
BUILD SUCCESSFUL
Total time: 0.697 secs

基于Gradle 3.3编写。

联系方式

sn0wdr1am

原文: http://mrhaki.blogspot.com/2017/02/gradle-goodness-skip-task-when-input.html

Gradle 能够很好的支持增量构建。这就是说,Gradle可以根据Task的输入和输出来决定一个Task是否需要被执行。举个例子,假如输入和输出文件没有任何改变,那么这个Task将会被跳过。通过定义Task的输入和输出,可以让我们自定义的Task也支持增量构建。我们还可以定义一个Task,当它的输入文件集合/文件夹不存在或者为空时,跳过不执行。Gradle提供了一个注解@SkipWhenEmpty,我们可以应用于Task的输入。

下面的例子,我们将会定义一个DisplayTask,用于打印一个文件夹下所有文件的内容。当这个文件夹为空时,我们希望跳过这个Task,不执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
task display(type:DisplayTask) {
contentDir = file('src/content')
}
class DisplayTask extends DefaultTask {
@SkipWhenEmpty
@InputDirectory
File contentDir
DisplayTask() {
description = 'Show contents of files'
}
@TaskAction
void printMessages() {
contentDir.eachFile { file ->
println file.text
}
}
}

当输入文件夹下没有任何文件时,我们运行这个Task,将会在终端看到 NO-SOURCE 字样。如果我们不添加这个注解@SkipWhenEmpty,这个构建任务将会失败。

1
2
3
4
5
6
$ gradle display
:display NO-SOURCE
BUILD SUCCESSFUL
Total time: 0.866 secs

接下来,我们在这个文件夹(src/content)下添加一个文件,然后再次运行这个Task:

1
2
3
4
5
6
7
8
$ gradle display
:display
Gradle rocks!
BUILD SUCCESSFUL
Total time: 0.866 secs

使用Gradle 3.4编写。

联系方式

sn0wdr1am

翻译自: http://mrhaki.blogspot.com/2017/03/gradle-goodness-create-shortcut-key-to.html

我们可以在IntelliJ IDEA中打开一个Gradle工程,并且获得IntelliJ对Gradle的原生支持。当我们在Gradle build文件中添加一个新的依赖或者插件时,需要在 IntelliJ IDEA中刷新工程。我们需要刷新这个Gradle工程,来保证IntelliJ IDEA 能够同步这些改变。Gradle工具栏有一个Icon,点击它可以刷新所有Gradle工程。但是这样做,意味着我们需要移动鼠标,而我们现在需要一个快捷键来完成这些操作,这样,我们就不需要把手移开键盘。

刷新所有Gradle项目的操作,实际上是刷新所有外部功能的操作。我们可以通过 Preferences | Keymap 来添加键盘快捷键。我们在搜索栏搜索“refresh all external”来找到这个动作。

idea-refresh-gradle-keymap.png

我们可以右键点击这个Action,然后选择“Keyboard Shortcut”来定义新的键盘快捷键。

现在当我们修改Gradle build文件时,我们可以简单的使用快捷键来刷新Gradle工程。

下面我们介绍如何将这个刷新Gradle工程的Action添加到工具栏。右键点击工具栏,并且选择“option Customize Menus and Toolbars…. ”。现在可以将”Refresh all external projects”添加到工具栏:

idea-refresh-gradle-keymap-assign.png

联系方式

sn0wdr1am

在Gradle下,我们可以通过由Java插件添加的测试任务,来运行单元测试代码。默认情况下,项目下的所有单元测试代码都会执行。但是,如果我们只想运行一个简单的单元测试,我们可以通过Java系统属性test.single来制定这个单元测试的名字。实际上,系统属性的样式是taskName.single. 其中taskName是项目下测试类任务的名称。下面我们将展示在构建中如何实践的:

首先,我们创建一个简单的build.gradle文件,用来运行单元测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// File: build.gradle
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:[4,)'
}
test {
testLogging {
// Show that tests are run in the command-line output
events 'started', 'passed'
}
}

下一步,我们创建两个测试类,每个测试类包含一个简单的单元测试方法。稍后,我们会演示,如何运行其中的一个单元测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
// File: src/test/java/com/mrhaki/gradle/SampleTest.java
package com.mrhaki.gradle;
import static org.junit.Assert.*;
import org.junit.*;
public class SampleTest {
@Test public void sample() {
assertEquals("Gradle is gr8", "Gradle is gr8");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// File: src/test/java/com/mrhaki/gradle/AnotherSampleTest.java
package com.mrhaki.gradle;
import static org.junit.Assert.*;
import org.junit.*;
public class AnotherSampleTest {
@Test public void anotherSample() {
assertEquals("Gradle is great", "Gradle is great");
}
}

为了只运行SampleTest,我们需要在终端执行test任务的时候,添加一个Java系统属性 -Dtest.single=Sample

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ gradle -Dtest.single=Sample test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
com.mrhaki.gradle.SampleTest > sample STARTED
com.mrhaki.gradle.SampleTest > sample PASSED
BUILD SUCCESSFUL
Total time: 11.404 secs

请注意,现在只有一个单元测试执行了。Gradle将会获取Sample的值,并且按照下面的样式 **/<Java system property value=Sample>*.class去查找单元测试类。因此,我们不需要输入单元测试的完整包名和类名。而为了仅仅执行AnotherSampleTest单元测试类,我们可以通过改变这个Java系统属性(test.single)来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ gradle -Dtest.single=AnotherSample test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
com.mrhaki.gradle.AnotherSampleTest > anotherSample STARTED
com.mrhaki.gradle.AnotherSampleTest > anotherSample PASSED
BUILD SUCCESSFUL
Total time: 5.62 secs

我们还可以使用Java系统样式来执行多个单元测试。例如:我们可以使用*Sample来同时运行 SampleTest 和A notherSampleTest。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ gradle -Dtest.single=*Sample test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
com.mrhaki.gradle.AnotherSampleTest > anotherSample STARTED
com.mrhaki.gradle.AnotherSampleTest > anotherSample PASSED
com.mrhaki.gradle.SampleTest > sample STARTED
com.mrhaki.gradle.SampleTest > sample PASSED
BUILD SUCCESSFUL
Total time: 5.605 secs

为了展示对于其他类型的测试任务,这种Java系统属性都有效。我们在build.gradle 文件中添加了一个新的任务。我们命名这个测试任务为sampleTest,包含了我们所有的测试类。我们同样应用了和之前一样的testLogging设置,方便跟踪单元测试输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// File: build.gradle
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:[4,)'
}
task sampleTest(type: Test, dependsOn: testClasses) {
include '**/*Sample*'
}
tasks.withType(Test) {
testLogging {
events 'started', 'passed'
}
}

下面我们值运行SampleTest测试类,但是我们换一种方式使用Java系统属性 -DsampleTest.single=S*:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ gradle -DsampleTest.single=S* sampleTest
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:sampleTest
com.mrhaki.gradle.SampleTest > sample STARTED
com.mrhaki.gradle.SampleTest > sample PASSED
BUILD SUCCESSFUL
Total time: 10.677 secs

实例使用 Gradle 1.6 编写。

联系方式

sn0wdr1am

一提到Docker,你可能想到云服务,运维等等。
今天,我们要谈谈Docker的本地应用,如何基于Docker和Debian打造一款个人专属操作系统。

简介

一个Docker镜像运行起来就相当于一个没有桌面的Linux系统。

现在,我们给一个基于Debian的Docker镜像,加上Mate桌面,就成了一个完整的Linux操作系统了。

为了保证我们可以通过网络来访问这个系统,我们再安装上OpenSSH和X2GO。

这样,一款基本的个人专属操作系统就完成了。当然,你可以在这个基础上,增加常用的软件,打造自己的个人专属操作系统。

构成

主要包含以下几个部分:

  • Debian jessie
  • Mate Desktop
  • Openssh-server
  • X2goserver

下载 && 安装

1. snowdream/desktop

下载Docker镜像:

1
docker pull snowdream/desktop

2. X2Go 客户端

以mac为例,其他参考: http://wiki.x2go.org/doku.php/doc:installation:x2goclient

先下载安装XQuartz-2.7.11.dmg(https://www.xquartz.org)

再下载安装x2goclient (https://code.x2go.org/releases/binary-macosx/x2goclient/releases/4.1.0.0/)

运行

1. 启动snowdream/desktop

通过以下Docker命令,启动镜像。
请留意提示的root和dockerx用户的密码,并记录下来。

1
2
CID=$(docker run -p 2222:22 -t -d snowdream/desktop)
docker logs $CID

2. 通过ssh访问

通过以下终端命令,连接上面的镜像。
密码见前面的提示。

1
ssh root@localhost -p 2222

3. 通过x2go访问桌面

  1. 启动x2go客户端
  2. 配置x2go客户端

点击主界面工具栏第三个按钮,看看全局设置中,XQuartz的路径和版本是否正确。

配置x2go客户端

接着,按照下面提示,创建一个会话。

其中,Host为主机IP,Login为用户名,SSH port为ssh端口,
底部的会话桌面选择Mate。

配置x2go客户端

3.启动会话,连接桌面。

联系方式

sn0wdr1am

在Gradle中运行单元测试很简单。一般情况下,如果有一个单元测试CASE执行失败,也会导致构建失败。但是我们却不能立刻在控制台看到为什么这个单元测试失败。我们需要先打开生成的HTML格式的测试报告。现在我们还有其他办法。

首先,我们创建一个下面的Gradle build文件:

1
2
3
4
5
6
7
8
9
10
// File: build.gradle
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:[4,)'
}

然后,我们使用下面的Junit单元测试。注意,这个单元测试一直会失败,因为这就是我们想要的CASE场景。

1
2
3
4
5
6
7
8
9
10
11
12
// File: src/test/java/com/mrhaki/gradle/SampleTest.java
package com.mrhaki.gradle;
import org.junit.*;
public class SampleTest {
@Test public void sample() {
Assert.assertEquals("Gradle is gr8", "Gradle is great");
}
}

我们通过执行test任务,来运行单元测试。如果我们运行这个任务,我们发现控制台显示哪一行单元测试失败了,但是我们却看不到单元测试失败的具体原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
com.mrhaki.gradle.SampleTest > sample FAILED
org.junit.ComparisonFailure at SampleTest.java:8
1 test completed, 1 failed
:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 4.904 secs

我们可以再次运行test任务,但是现在我们通过添加命令行选项-i 或者 –info来设置Gradle日志级别到info。现在我们可以在终端看到单元测试失败的具体原因了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ gradle test -i
Starting Build
Settings evaluated using empty settings script.
Projects loaded. Root project using build file
...
Successfully started process 'Gradle Worker 1'
Gradle Worker 1 executing tests.
Gradle Worker 1 finished executing tests.
com.mrhaki.gradle.SampleTest > sample FAILED
org.junit.ComparisonFailure: expected:<gradle is gr[8]> but was:<gradle is gr[eat]>
at org.junit.Assert.assertEquals(Assert.java:115)
at org.junit.Assert.assertEquals(Assert.java:144)
at com.mrhaki.gradle.SampleTest.sample(SampleTest.java:8)
Process 'Gradle Worker 1' finished with exit value 0 (state: SUCCEEDED)
1 test completed, 1 failed
Finished generating test XML results (0.025 secs)
Generating HTML test report...
Finished generating test html results (0.027 secs)
:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --debug option to get more log output.
BUILD FAILED
Total time: 5.117 secs

但是这样做仍然会产生很多干扰的日志。最好的办法就是通过配置test任务来自定义单元测试日志级别。我们可以配置不同的日志级别。为了获取单元测试失败的具体原因我们可以仅仅将exceptionFormat设置为full。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// File: build.gradle
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:[4,)'
}
test {
testLogging {
exceptionFormat = 'full'
}
}

我们可以再次运行test任务,并且使用正常的日志级别,但是这次,我们依然能够看到单元测试失败的具体原因,而没有其他的干扰日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
com.mrhaki.gradle.SampleTest > sample FAILED
org.junit.ComparisonFailure: expected:<gradle is gr[8]> but was:<gradle is gr[eat]>
at org.junit.Assert.assertEquals(Assert.java:115)
at org.junit.Assert.assertEquals(Assert.java:144)
at com.mrhaki.gradle.SampleTest.sample(SampleTest.java:8)
1 test completed, 1 failed
:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 5.906 secs

实例使用 Gradle 1.6 编写。

联系方式

sn0wdr1am

Fork me on GitHub