分类: Gradle

  • netflix的gradle插件

    Netflix在开源界的声誉稳步上涨,主要源于其公司产品特性中抽象出的微服务体系概念落地的开源技术。NetflixOSS不管包含主要的抽象组件,也开源了它使用的gradle插件,很多插件非常易用。

    Gradle作为构建工具,有远优于Maven的配置灵活性和Groovy对于Java世界极高的亲和力。

    一个普通的模板项目一般只是用了java和对应IDE插件,比如

    apply plugin: java
    apply plugin: idea
    apply plugin: eclipse

    但是除此之外还有一些基本的,比如发布,版本号升级,项目联系人,协议检查等等等等基本配置。

    Netflix的gradle-netflixoss-project-plugin可以快速实现这些。

    buildscript {
      repositories { jcenter() }
      dependencies { classpath 'com.netflix.nebula:gradle-netflixoss-project-plugin:3.2.2' }
    }
    
    allprojects {
        apply plugin: 'nebula.netflixoss'
    }

    这个插件其实也是调用其他插件,除了协议检查使用了hierynomus的以外,其他都是 Netflix自己的。默认的协议header是Netflix的,需要自己重新配置,格式如下:

    Copyright ${year} Netflix, Inc.
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

    参考链接:

    Github上的仓库:https://github.com/nebula-plugins/gradle-netflixoss-project-plugin

    HEADER格式:https://github.com/nebula-plugins/gradle-netflixoss-project-plugin/blob/master/src/main/resources/netflixoss/HEADER

  • 使用Gradle注册SpringXD的module

    使用Gradle注册SpringXD的module

    SpringXD是Pivotal的大数据产品,提供了一个抽象的数据处理平台。

    SpringXD将数据解决方案抽象为数据吸纳,分析,流调度和输出四大块。

    作为源头的数据吸纳可以从各种数据源中获取需要的数据,基于Spring的另外一个项目spring-integration,这一部分的大部分实现都可以使用简单的dsl实现。

    在SpringXD中每一个部分的组件都可以自己编写并注册到服务中,方便之后的使用。

    但是在编写阶段每次都需要打包,上传服务器,然后注册,整个过程还是有点繁琐的。

    Gradle作为一个构建工具,自然可以通过自定义任务完成这个任务。

    微博数据吸纳

    这里以微博的数据源为例。

    微博的数据源因为新浪微博提供了sdk,所以自己编写稍微方便一些。

    SpringXD项目默认也提供了twitter的两个source方便测试。

    先看一下build.gradle的配置

    ext {
    xdVersion ='1.1.1.RELEASE'
    springVersion ='4.1.3.RELEASE'
    moduleType ='source'
    moduleName ='weibo'
    xdServer ="http://192.168.0.12:9393"
    version ='0.0.1-SNAPSHOT'
    }

    最关键的配置变量是moduleType,moduleName和xdServer。

    为了简单明了,WeiboSource并不具有配置参数(一般情况下都需要提供一些配置参数的,比如apiKey),主配置文件如下

    <?xml version="1.0" encoding="UTF-8"?>
    <beans:beans xmlns="http://www.springframework.org/schema/integration"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <channel id="output"/>
    
    <beans:bean class="com.huangyunkun.xd.WeiboSource">
    <beans:property name="autoStartup" value="false"/>
    <beans:property name="outputChannel" ref="output"/>
    </beans:bean>
    
    </beans:beans>

    因为打包后配置需要也在jar中,在jar配置中增加

    jar {
    from("modules/${moduleType}/${moduleName}/config") {
    into"config"
    }
    }

    注册到SpringXD

    SpringXD提供了一套Restful的api用于常用的操作,比如stream的管理,容器状态等。

    xd shell也是调用了这个接口,也有java的实现。

    在build.gradle中加入

    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath('org.springframework.xd:spring-xd-rest-client:+')
    }
    }

    新定义一个任务reg

    task reg(dependsOn: jar) << {
    SpringXDTemplatetemplate = newSpringXDTemplate(newURI(xdServer));
    ModuleOperations moduleOperations =template.moduleOperations();
    moduleOperations.uploadModule(moduleName,RESTModuleType.valueOf(moduleType), new
    FileSystemResource(jar.archivePath.path),true);
    }

    然后执行gradlew reg即可注册成功。

    注册成功

    注册成功后就可以正常使用了

    创建新的stream
    stream输出

    当然管理界面也可以看到相关信息

    管理界面

    其他问题

    • 这种注册方式是强制的,也就是说如果有同名source就会被覆盖。

    • 因为Module的类型是通过RESTModuleType.valueOf(moduleType)获取的,所以也可以这样注册sink等其他类型的模块。

    • 如果有对应的module正在被使用,那么是没法重新注册的。

  • 在Gradle中限制对jar包签名时机

    Maven仓库是一个包含大量依赖库的地方,有时候我们需要发布自己的库到仓库。

    仓库虽然对于发布的库的具体功能和作用没有太多要求,但是有一些强制要求是必须的。

    发布的内容物可以是jar,aar等,但是都必须满足一下条件:

    • Metadata(pom.xml)
    • 签名
    • source jar
    • javadoc jar

    Gradle自身包含了mvn插件和signing插件,所以整个工作还是比较简单的,大体配置如下:

    signing {
    required {
    isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives")
    }
    signconfigurations.archives
    }
    task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier ='javadoc'from'build/docs/javadoc'
    }
    task sourcesJar(type: Jar) {
    classifier ='sources'fromsourceSets.main.allSource
    }

    如果是快照版本就会发布到snapshot仓库,否则就是staging库。

    对于快照版本是不需要签名的,所以在操作中尽量快照版本不签名,一来是节约时间,二来是快照版本如果通过CI发布的,就可以省去很多事。

    signing插件的配置中通过指定required来决定是否跳过签名。

    可以通过以下代码来判断

    ext.isReleaseVersion = !version.endsWith("SNAPSHOT") signing {
    required {
    isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives")
    }
    sign configurations.archives
    }

    当然如果你的CI中有特殊的环境变量,也可以加入判断中。

    比如SnapCI中的环境变量有

    $ exportSNAP_CI=true
    $ exportCI=true
    $ exportSNAP_CACHE_DIR=/var/go
    $ exportLANG=en_US.UTF-8
    $ exportLC_ALL=en_US.UTF-8
    $ exportSNAP_WORKING_DIR=/var/snap-ci/repo
    $ exportSNAP_PIPELINE_COUNTER=12
    $ exportSNAP_STAGE_NAME=upload
    $ exportSNAP_BRANCH=master
    $ exportSNAP_COMMIT=768980f107b8757812f23a934979549e40977c3d
    $ exportSNAP_COMMIT_SHORT=768980f
    $ exportSNAP_TRACKING_PIPELINE=true
    $ exportSNAP_INTEGRATION_PIPELINE=false
  • Gradle中获取可用端口

    在项目需要用到集成测试,即打包整个项目,通过容器启动,然后直接测试。

    测试的环境不一定是稳定的,所以容器对外的端口最好是保证可用的。

    以一个Spring Boot的项目为例子,使用gretty插件来启动应用。

    gretty {
    springBoot =false
    port =7777
    integrationTestTask = 'e2eTest'
    }

    如果端口7777被占用,那么测试就会失败。

    稍微修改一下,使用AvailablePortFinder来寻找可用端口。

    AvailablePortFinderavailablePortFinder = AvailablePortFinder.createPrivate()
    IntegerhttpPort = availablePortFinder.nextAvailable
    
    gretty {
    springBoot =false
    port = httpPort
    integrationTestTask = 'e2eTest'
    }

    这样项目启动时的端口就可以保证可用性了。

    在测试中的使用也很简单,因为gretty对于集成测试的支持很好,相关属性都会写入系统属性以供测试使用。

    在测试中可以这样使用

    privatefinalstatic String baseUrl ="http://localhost:8080/budget";
    
    static {
    RestAssured.defaultParser = Parser.JSON;
    String property = System.getProperty("gretty.httpBaseURI");
    if (property !=null) {
    RestAssured.baseURI = property;
    }else {
    RestAssured.baseURI = baseUrl;
    }
    }
  • 共享常用的Gradle配置片段

    Gradle灵活而强大,而且自定义简单,会Groovy用Groovy,不会或者不想学的直接写Java也可以。

    对于build.gradle而言,简单的项目还好,复杂项目这个配置就有点长了。

    虽然可以通过将Gradle Script抽成单个文件的方式来让配置更清晰,但是这样并没有做到常用配置的共享。

    因为很多配置其实是多个项目可以共用的,比如idea相关配置。

    分割配置

    当build.gradle文件越发繁琐的时候,最简单直接的方法就是将其拆分,比如idea相关配置可以拆分到 gradle目录中的idea.gradle文件中。

    apply plugin:'idea'
    
    idea {
    module {
    inheritOutputDirs =false
    outputDir =file("$buildDir/classes/main/")
    }
    
    project {
    ipr {
    withXml { provider ->
    def node = provider.asNode()
    node.component.find { it.'@name' =='VcsDirectoryMappings' }?.mapping[0].'@vcs' ='Git'
    }
    }
    }
    }

    然后在build.gradle文件中使用apply from引入:

    applyfrom:'gradle/idea.gradle'

    这样可以虽然让build.gradle简洁了,但是并没有达到共享gradle脚本片段的目的。

    网络直接地址共享

    因为gradle是支持从网络加载配置的,所以可以将公共的gradle配置片段共享出来,使用者直接从网络加载就可以了。

    applyfrom:'http://server-url/idea.gradle'

    当然这样就要求你需要一个服务器来提供这个服务,至少也是一个文件服务器。

    如果有现有的repo,如Nexus,那么还可以这样共享

    apply from: 'http://server-url/nexus/service/local/artifact/maven/redirect?r=repository-name&g=group-name&a=build-common&e=gradle&v=LATEST'

    这样就可以还可以达到版本管理的功能。

    从依赖中解压

    如果gradle配置片段已经存在于repo中了,还有一种选择是将其下载并解压,然后再引入。

    buildscript {
    repositories {
    //my own repo
    }
    dependencies {
    classpath'com.huangyunkun:common-build:1.0.0-SNAPSHOT'
    }
    dependencies {
    ant.unjar src:configurations.classpath.singleFile, dest:'gradle'
    }
    }
    
    applyfrom:'gradle/common.gradle'

    直接从依赖中获取配置

    上面的办法需要解压,其实不解压也是可以的,直接从依赖中加载即可。

    buildscript {
    repositories {
    //my own repo
    }
    dependencies {
    classpath'com.huangyunkun:common-build:1.0.0-SNAPSHOT'
    }
    }
    
    afterEvaluate {project ->
    applyfrom:project.buildscript.classLoader.getResource('path/to/your/resource/in/the/jar').toURI()
    }

    直接使用插件

    当然,还有一种办法是直接打包一个gradle plugin。

    buildscript {
    repositories {
    //my own repo
    }
    dependencies {
    classpath'com.huangyunkun:common-build:1.0.0-SNAPSHOT'
    }
    }
    
    apply plugin:"common-build"

    我始终觉得为了共享配置片段而打包一个plugin有点不值,不过这种方法的代码确实最少最容易懂得。

  • 使用Gradle下载phantomjs

    对于Web应用开发,测试中有一个很重要的测试是Functional Test(或者叫Integration Test)。

    Functional Test除了需要相关的库以外还需要一个Driver,可以是Chrome,Firefox等等。

    因为并不是每一个开发机器或者CI服务器都有浏览器的,所以保证测试的可用性是相当重要的。

    常用的方法是将浏览器提前下载,放置在项目目录中,纳入版本管理。这样测试的运行就可以直接使用。

    但是这种方法有两个问题:

    • 将大量文件纳入了版本管理中
    • 对于不同平台还需要下载不同平台的Driver

    其实还有一个办法就是让构建工具自动下载,下载的时候可以根据平台来下载指定版本。这样即提升了易用性,也降低了切换难度。

    这里以phantomjs为例,来看看怎么让Gradle去下载平台相关的phantomjs。

    首先明确phantomjs的下载地址,在bitbucket上,https://bitbucket.org/ariya/phantomjs/downloads

    文件名是phantomjs+版本号+平台。

    首先对于平台的判断使用commons-io库来操作。

    import org.apache.tools.ant.taskdefs.condition.Os
    
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath"commons-io:commons-io:2.+"
    }
    }

    判断代码如下:

    def osFilenamePart
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
    osFilenamePart ="windows.zip"
    }elseif (Os.isFamily(Os.FAMILY_MAC)) {
    osFilenamePart ="macosx.zip"
    }elseif (Os.isFamily(Os.FAMILY_UNIX)) {
    osFilenamePart = Os.isArch("amd64") ?"linux-x86_64.tar.bz2":"linux-i686.tar.bz2"
    }

    下载文件也是common-io库中的FileUtils类。

    import org.apache.commons.io.FileUtils
    
    def filename ="phantomjs-$phantomJsVersion-$osFilenamePart"
    def outputFile =file("$buildDir/webdriver/$filename")
    inputs.property("phantomJsVersion", phantomJsVersion)
    outputs.file(outputFile)
    
    doLast {
    FileUtils.copyURLToFile(new URL("https://bitbucket.org/ariya/phantomjs/downloads/$filename"), outputFile)
    }

    当然下载之后还需要一个解压过程

    task unzipPhantomJs(type: Copy) {
    defoutputDir =file("$buildDir/webdriver/phantomjs")dependsOndownloadPhantomJsoutputs.dir(outputDir)
    
    defarchive =downloadPhantomJs.outputs.files.singleFile
    
    from(Os.isFamily(Os.FAMILY_MAC) ||Os.isFamily(Os.FAMILY_WINDOWS) ?zipTree(archive) :tarTree(archive))into(outputDir)eachFile {
    FileCopyDetailsfcp - >fcp.relativePath =newRelativePath(!fcp.directory, *fcp.relativePath.segments[1.. - 1])
    }
    }

    同样的思路,对于其他情况,比如Chrome也是试用的,只不过下载地址稍有不同。

    def driverOsFilenamePart
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
    driverOsFilenamePart ="win32"
    }elseif (Os.isFamily(Os.FAMILY_MAC)) {
    driverOsFilenamePart ="mac32"
    }elseif (Os.isFamily(Os.FAMILY_UNIX)) {
    driverOsFilenamePart = Os.isArch("amd64") ?"linux64":"linux32"
    }
    FileUtils.copyURLToFile(new URL("http://chromedriver.storage.googleapis.com/${chromeDriverVersion}/chromedriver_${driverOsFilenamePart}.zip"), outputFile)
  • 修改Gradle中Wrapper版本

    一直在用Gradle,它提供的wrapper是一个很实用的功能。

    可以快速切换版本,保证构建的一致性,还可以方便没有安装Gradle的用户,也能保持构建工具版本的一致性。

    启用Wrapper以后会创建一个gradle目录,其中包含一个gradle-wrapper.properties文件,内容一般如下:

    distributionBase=GRADLE_USER_HOME
    distributionPath=wrapper/dists
    zipStoreBase=GRADLE_USER_HOME
    zipStorePath=wrapper/dists
    distributionUrl=https://services.gradle.org/distributions/gradle-2.0-bin.zip

    偶尔需要修改Gradle Wrapper版本的时候我会直接修改最后一行的distributionUrl参数。

    今天看文档才发现了一个Wrapper任务,每次的修改只需要修改这个任务,然后运行就会自动创建或者修改上面那个文件了。

    task wrapper(type: Wrapper) {
    gradleVersion = '2.0'
    }

    之后运行./gradlew wrapper即可。

  • Libgdx使用Gradle构建速度慢的问题

    Libgdx正式推出1.0版本,其中最重要的一个变化就是正式启用Gradle模板支持。

    填写必要信息后会自动生成Gradle配置文件,通过Gradle可以创建Idea和Eclipse的项目文件,更可以快速升级和添加依赖。

    但是很多人再使用的时候,特别是第一次接触Gradle,会遇到很多问题,最关键的一个就是速度慢。本文会介绍这个问题的原因和解决方法。

    Gradle Wrapper

    Gradle的Wrapper很多文章翻译为包装器。包装器的出现是基于这样的需求,即让没有装Gradle的机器上也能正常的构建你的项目。

    它和Groovy里的Grape类似。如果目标机器上没有Gradle,包装器将先下载安装合适版本的Gradle,然后再运行相应的任务。如果有,但是版本不同,它也可以保证构建工具本身的版本相同。

    出发点是好的,但是现实确实复杂的。因为默认下载的地址http://services.gradle.org/在中国访问速度很慢很慢,而且需要下载的文件大小还很大,一般有40+M,基本上是不能下载成功的。

    对于这个问题有两种解决方法:

    • 使用本地Gradle
    • 修改下载地址

    使用本地Gradle

    这种方法实质上是不使用包装器,而是通过各种方法安装Gradle,然后直接执行。注意,执行的时候不要调用gradlew [command]而是使用gradle [command]

    这种方法简单,但是不能享受到包装器的优势。

    修改下载地址

    访问Gradle Distributions的速度慢,我们可以使用一个快的。

    Wrapper任务有一个名为distributionUrl的属性,直接修改它指定新的下载地址就行了。

    可以在build.gradle中修改,比如

    task prepareWrapper(type: Wrapper) {
    gradleVersion ='1.12'
    distributionUrl ='alternative.location'
    }

    或者创建gradle-wrapper.properties文件

    distributionUrl=http://some.location.net/gradle-distributions/gradle-1.12-bin.zip

    当然具体的替代网址就随意了,也可以使用自建服务器。

    Gradle Repositories

    Gradle中的依赖会根据配置自动解析,而Libgdx Gradle模板使用了mavenCentral(),这是maven的中央仓库,速度实在一般,使用镜像替换之。

    Oschina提供了镜像,地址为http://maven.oschina.net/content/groups/public/

    用以下语句替换mavenCentral()就可以了。

    maven {
    url'http://maven.oschina.net/content/groups/public/'
    }

    可以使用gradlew tasks来检测依赖是否下载成功。

    参考资料

    Wrapper
    开源中国 Maven 库使用帮助

  • 使用Gradle自动发布Java Web到SAE

    使用Gradle自动发布Java Web到SAE

    现在像SAE这类的应用引擎已经比较多了,百度和腾讯都提供了相似的平台。

    我很早的时候就开始用SAE,当时还为了迁就SAE学习了PHP(当时只支持PHP和另外一个什么语言)。后来SAE支持Java了,版本是6,容器是Jetty 7.4,而常用的框架也基本能跑。

    代码的部署使用svn,稍微有点麻烦。最近在做一个Java Web的练习,代码放在github上,每次上传到SAE很烦。项目使用Gradle管理,所以琢磨着怎么把自动发布到SAE这个事情交给Gradle来做。

    流程分析

    先来看一下SAE这边。

    建立默认版本,版本号为1。然后通过代码管理页面上传一个war包。

    进入svn仓库看一看

    其实就是把对应的war包放在版本号目录的根目录就可以了。

    而调用

    gradle war

    就可以生成war包,而文件地址通过以下方法获得

    war.archivePath

    这样就只需要checkout原有代码,然后将新的war拷贝过去,然后commit就行了。

    直接调用SVN命令

    Gradle有一种任务类型是Exec,可以直接在命令行调用命令。

    比如我们的checkout操作就可以这样

    task checkoutRepo(type:Exec) {
    workingDir'build/'
    commandLine'svn','checkout',repo_path,"svn"
    }

    这样代码仓库就被签出到build/svn目录下了。

    在使用copy指令

    copy{
    from war.archivePath
    into"build/svn/"+numberVersion
    }

    当然,gradle生成的war包的名字组成是

    ${baseName}-${appendix}-${version}-${classifier}.${extension}

    如果要重命名的话,可以使用rename,比如利用重命名去掉version信息

    copy {
    from war.archivePath
    into svnPath +"/" + numberVersion +"/"
    rename { String fileName ->
    fileName.replace("-" +version,'')
    }
    }

    然后再次提交

    workingDir'build/svn'
    
    commandLine'svn','commit','-m',"Built By Gradle in"+newDate()

    为了方便调试还可以导出命令运行的输出,提交部分的任务如下

    task commitWarToSae(type:Exec) {
    workingDir'build/svn'
    commandLine'svn','commit','-m',"Built By Gradle in"+newDate()
    
    standardOutput=new ByteArrayOutputStream()
    
    ext.output= {
    return standardOutput.toString()
    }
    }

    这样勉强可以完成任务,但是问题是我的运行环境不一定包含了svn。我平时用的也比较少,不想安装它。这样只有使用其他方法了。

    在Gradle中调用SVNKit

    SVNKit 是一个纯 Java 的 SVN 客户端库,使用 SVNKit 无需安装任何 SVN 的客户端,支持各种操作系统。使用它一方面不需要考虑安装问题,另一方面SVNKit在maven仓库中有,不需要手动管理,最新版本为1.7.8

    首先在buildscript中配置SVNKit依赖

    buildscript {
    repositories {
    mavenLocal()
    mavenCentral()
    }
    dependencies {
    classpath(
    'org.tmatesoft.svnkit:svnkit:1.7.8',
    'org.tmatesoft.svnkit:svnkit-cli:1.7.8'
    )
    }
    }

    然后引用cli中的SVN,这样就可以直接调用程序的方法。

    import org.tmatesoft.svn.cli.SVN;

    调用时使用

    SVN.main(String[] args)

    这是可以直接将上文的调用svn的地方全部改为调用SVN.main,但是这之前还有一个问题。

    SVN.main实质上只有一句话,调用了svn目录下的SVN类,而这个类继承了AbstractSVNLauncher。而AbstractSVNLauncher的代码中包含了错误处理

    publicvoidfailure() {
    setCompleted();
    try {
    System.exit(1);
    }catch (SecurityException se) {
    
    }
    }
    
    publicvoidsuccess() {
    setCompleted();
    try {
    System.exit(0);
    }catch (SecurityException se) {
    
    }
    }

    对于System.exit()的调用明显是我们不希望的,即使SVNKit的运行出现了错误,我们依然期望其他任务继续运行。

    通过Java的SecurityManager来达到目的

    def _disableSystemExitCall = {
    System.setSecurityManager(
    new SecurityManager() {
    @Override
    publicvoidcheckPermission(java.security.Permission perm) {}
    
    @Override
    publicvoidcheckExit(int status) {thrownew SecurityException(); }
    }
    );
    };
    
    def _enableSystemExitCall = { System.setSecurityManager(null); };
    
    def doSvn = { String... aSvnArgs ->
    _disableSystemExitCall();
    try {
    SVN.main(aSvnArgs as String[]);
    }finally {
    _enableSystemExitCall();
    }
    };

    配置完成了,先来测试一下帮助信息的显示

    task('showSvnHelp') << {
    doSvn("help")
    }

    运行

    gradle showSvnHelp

    可以看到效果。

    剩下的事情就简单了,把所有事情放到一个新的任务中去,命名为uploadWarToSae

    task('uploadWarToSae') << {
    ext {
    repo = YOUR_REPO_PATH
    username = YOUR_USER_NAME
    password = YOUR_PASSWORD
    numberVersion = YOUR_NUMBER_VERSION
    svnPath ='build/svn'
    }
    
    delete(svnPath)
    
    doSvn("checkout", repo, svnPath,"--username", username,"--password", password)
    
    copy {
    from war.archivePath
    into svnPath +"/" + numberVersion +"/"
    rename { String fileName ->
    fileName.replace("-" +version,'')
    }
    }
    
    doSvn("commit","-m","Built By Gradle in" +new Date(),"--username", username,"--password", password, svnPath)
    }

    试着运行看看,你可以会遇到一下问题

    • Server certificate verification failed
    • Fingerprint: * (R)eject, accept (t)emporarily or accept (p)ermanently?
    • failed: SSL error: certificate verify failed

    从svn的帮助中可以找到这几个指令:—no-auth-cache —non-interactive —trust-server-cert

    将它们加入doSvn中(其实也可以在调用的时候添加)

    def doSvn= {String... aSvnArgs->
    _disableSystemExitCall();
    List<string> cmds= aSvnArgs as LinkedList<string>;
    cmds.add("--no-auth-cache");
    cmds.add("--non-interactive");
    cmds.add("--trust-server-cert")
    try {
    SVN.main(cmds asString[]);
    } finally {
    _enableSystemExitCall();
    }
    };

    这样就没有问题了。

    如果你觉得用户名和密码直接写入build.gradle不安全,你也可以考虑将私密信息写入gradle.properties中去。

    集成Travis

    Travis是一个和Github集成的非常好的CI。使用它你需要在项目中配置.travis.yml文件。

    我希望让travis自动将每次的更改打包后上传到SAE中,当然直接调用

    gradle uploadWarToSae

    就行了。但是私密信息写在哪里?

    Travis提供了利用RSA密匙加密的方法。具体细节参考文末链接。

    travis encrypt-a env.global USERNAME=username PASSWORD=password

    这样就可以在命令行中调用了,比如

    gradle uploadWarToSae -Pusername=$USERNAME -Ppassword=$PASSWORD

    然后改造uploadWarToSae方法,让其可以接受参数。

    task('uploadWarToSae') << {
    if (project.hasProperty('username') &&project.hasProperty("password")) {
    ext {
    repo ='https://svn.sinaapp.com/blackjack/'
    numberVersion =1
    svnPath ='build/svn'
    }
    delete(svnPath)
    doSvn("checkout", repo, svnPath,"--username", username,"--password", password)
    
    copy {
    from war.archivePath
    into svnPath +"/" + numberVersion +"/"
    rename { String fileName ->
    fileName.replace("-" + version,'')
    }
    }
    
    doSvn("commit","-m","Built in" +new Date(),"--username", username,"--password", password, svnPath)
    }else {
    thrownew InvalidUserDataException("没有提供sae的安全邮箱和密码,部署失败")
    }
    }

    运行效果如下:

    结语

    如果使用的是BAE的话,官方提供了maven的部署插件的…而SAE不知道有没有提供,反正从官方文档中没有看到。

    附上几个参考地址:

    AbstractSVNLauncher.java

    Preventing System.exit() from API

    disabling System.exit()

    encryption-keys

  • Gradle中ProGuard的配置

    好久没有写博客了…元旦前赶紧写一篇吧…

    这些日子琢磨了一下gradle。对比起maven确实在配置上灵活很多,对groovy的支持可以更容易的自定义任务。

    由于最近的几个项目中都使用到了moco这个开源项目,它使用gradle管理,使用命令gradle uberjar可以生成一个独立运行包,这个包有8M大。我使用的环境比较特殊…8M有点大了,就琢磨这使用ProGuard给它瘦个身,效果不错,简单配置以后大小变成了4.6M,只有原来的57%了。

    在配置的过程中遇到了很多难题,google之后都没有什么中文参考,故记录下了这次尝试,分享给大家。

    ProGuard简介

    ProGuard是一个压缩、优化和混淆Java 字节码文件的软件。它可以删除无用的类、字段、方法和属性。还可以删除没用的注释,优化 字节码文件。它还可以使用简短的无意义的名称来重命名已经存在的类、字段、方法和属性。

    我最开始接触ProGuard是在Android的开发中,这个工具已经成为标配了。其实ProGuard一样可以用于一般的Java项目。它的主要功能有三块:压缩,优化,混淆。其中压缩是删除没有用到的类等,当你的项目使用到了大量开源库的时候这个功能尤其明显。这也是本文主要用到的功能。优化可以去除一些无用参数,还可以进行一些代码内联。混淆功能是将包名、类名、方法名等等用无意义的字符替换掉,可以增加反编译的难度,保护你的代码。混淆也可以减少包的大小,但是混淆后不利于调试。

    ProGuard的配置

    大部分的工具都有配置,如果你没有太大的需求,一般可以直接使用。而ProGuard的配置是不可缺少的,特别是刚刚接触这个工具的时候,我觉得这个配置完全是自己写不出来的,我是针对每一个用到的包去搜索复制相关配置。后面慢慢发现其实还好,了解了原理以后就可以自己配置了。

    ProGuard会从你给出的入口点切入开始分析,一般是程序的main方法,如果某个类或者方法没有时候就会被去除。然而有时候程序中用到了某些类是通过配置文件或者反射等方法得到的,这种情况ProGuard可能就会误删。虽然目前ProGuard可以分析一些简单的情况,但是更多的时候还是需要自己配置。

    在Gradle中使用ProGuard

    ProGuard提供了Gradle插件,这样我们可以更方便的将ProGuard集成进来了。当前最新版是4.10。

    这个插件的介绍页面我死活打不开了…主要的参考也是其他项目的配置。

    先来看看如果在Gradle中生成一个独立包。这个包需要包含自身的源码、资源还有所有的第三方依赖。如果你的项目是一个多项目,那么你的独立包还应该其他需要的子项目,一般情况下配置如下:

    task uberjar(type: Jar, dependsOn: jar) {
    classifier ='standalone'
    from files(sourceSets.main.output.classesDir)
    from files(sourceSets.main.output.resourcesDir)
    fromconfigurations.runtime.asFileTree.files.collect { zipTree(it) }
    
    manifest {
    attributes'Main-Class': YOUR MAINCLASS,
    'Implementation-Title':"${project.name}",
    'Implementation-Version':"${version}",
    'Implementation-Vendor': YOUR NAME,
    'Built-Date':new Date().getDateTimeString(),
    'Built-With':"gradle-${project.getGradle().getGradleVersion()},groovy-${GroovySystem.getVersion()}",
    'Created-By':'Java ' + System.getProperty('java.version') +' (' + System.getProperty('java.vendor') +')'
    }
    }

    更多的细节可以查看Gradle文档中关于Jar任务的描述。

    对于一个任务,首先声明这个项目的类型,它依赖于哪个项目。其次就是生成的jar的名称,名称规则为

    ${baseName}-${appendix}-${version}-${classifier}.${extension}

    第一句配置就是将classifier指定为standalone。

    第二三句指明该Jar文件包含项目源代码和资源文件。

    第四句将所有运行时所用到的第三方库等等打包进来了。

    后面的是manifest的配置。

    如果你是多项目的,那么就需要添加依赖子项目的classesDir和resourcesDir。

    以上的配置基本可用,但如果你仔细看看META-INF文件夹就有点小问题了,如果你使用了库比较多,那么你将会有很多重复的NOTICE、LICENSE文件在这个包里面,这样会对ProGurad的使用造成问题,比如错误【proguard can’t write resource duplicate zip entry】。

    一般我推荐加上以下两句:

    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    exclude('META-INF/maven/**')

    第一句是忽略重复文件,第二句是排除maven目录。

    生成独立包以后就可以使用ProGuard来处理了,任务类型是:proguard.gradle.ProGuardTask,依赖刚才的uberjar任务。

    首先在Gradle的配置中添加buildscript,在其中指明插件依赖:

    buildscript{
    repositories{
    mavenCentral()
    }
    dependencies{
    classpath'net.sf.proguard:proguard-gradle:4.10'
    }
    }

    ProGuardTask最主要的参数是injars和outjars,分别指定输入文件和输出文件,其他的细节配置可以在proguard.pro中定义。

    task proguard(type: proguard.gradle.ProGuardTask,dependsOn:uberjar) {
    ext{
    jar.classifier="standalone"
    injar= jar.archivePath
    jar.classifier="proguard"
    outJar= jar.archivePath
    }
    injars injar
    outjars outJar
    configuration 'proguard.pro'
    }

    ext中的配置主要是获取输入文件和输出文件的路径,以作为injars和outjars的参数。

    在proguard.pro文件中我们一般需要指定rt.jar所在,如果出现【can’t find referenced class javax.crypto】错误,那么你还需要指定jce所在。一般配置如下:

    -libraryjars <java.home>/lib/rt.jar
    -libraryjars <java.home>/lib/jce.jar
    -printusage shrinking.outpu
    
    -dontobfuscate
    -dontoptimize
    
    -keepattributes *Annotation*,EnclosingMethod
    
    -keep public class com.github.dreamhead.moco.bootstrap.Main {
    public static void main(java.lang.String[]);
    }

    第三句是输出ProGuard去除了哪些类和方法,以方便排错。后面两句是制定不混淆、不优化,这样ProGuard只会为我们压缩包。接下来的配置就比较好说了,一般有两种选择:

    1.直接运行,针对具体的报错进行处理

    2.依次搜索你用到的第三方库,看看有没有配置好的。

    比如Google的Guava库,官方就有说明,需要一个jsr305包和一个javax.inject包,你可以使用参数libraryjars提供这两个包,也可以直接简单的使用-dontwarn com.google.**忽略掉这些。

    如果你使用到了注解,那么推荐你添加

    -keepattributes*Annotation*,EnclosingMethod

    如果你使用Json的相关库,那么最好保护一下你的model类和相关的get和setter方法,添加

    -keep class modal.**{void set*(***);*** get*();}
    -keepclassmembers class model.** {public<fields>;}

    如果使用到了枚举的话可以考虑

    -keepclassmembersenum * {
    publicstatic **[]values();
    publicstatic **valueOf(java.lang.String);
    }

    如果使用了native相关的,那么必须添加

    -keepclasseswithmembernames class * {
    native <methods>;
    }

    如果使用了日志工具,比如apache common logging、logback等,那么你需要保护好你配置文件中指定的记录器,比如

    -keeppublicclass org.apache.commons.logging.impl.**{*;}
    -keeppublicclass org.slf4j.** {*;}
    -keeppublicclass ch.** {*;}

    其他的问题就根据情况配置了,一般情况下用不要轻易使用dontwarn这个配置,因为它只是屏蔽了错误,并没有解决问题。当然,如果你觉得你的思路足够清晰的话那么放心的使用。

    Gradle中测试ProGuard的结果

    ProGuard输出结果是一个jar包,有时候很难说输出的结果是完全正确的,这个时候我们需要对压缩过的包进行测试。

    直接使用项目本身的单元测试是最好的选择,Gradle的Test任务支持Junit和TestNG。而我们要做的就是自定义一个Test任务,保留测试的内容,将源代码和第三方库移除,然后将我们压缩过的包链接过去,这样我们的测试就是测试生成好的Jar包,而不是源代码了。

    相关配置如下:

    task proguardCheck(type:Test,dependsOn:proguard){
    ext{
    jar.classifier="proguard"
    outJar= jar.archivePath
    }
    testLogging { exceptionFormat"full" }
    classpath =classpath-files(sourceSets.main.output.classesDir)-files(configurations.runtime)+files(outJar)
    }

    配置中的ext块获取到了压缩好的Jar包的位置。classpath是我们测试时用到的东西,在这里通过

    -files(sourceSets.main.output.classesDir)

    移除项目本身的源代码。再通过

    -files(configurations.runtime)

    移除依赖的第三方库。

    最后通过

    +files(outJar)

    将我们压缩好的Jar包链接过去。

    这样的话我们就可以在修改proguard.pro配置以后运行这个测试来检查压缩后的包时候正常工作。

    Mac OS下的兼容问题

    因为proguard的配置中我们配置了rt.jar,jce.jar,当然如果有需要你还可以添加jsse.jar等等,嫌麻烦的话可以只提供rt.jar。

    但是Mac Os中的java可能不是包含的rt.jar,而有可能是classes.jar。这个时候就需要判断一下环境了。

    在Gradle可以通过

    if(System.properties["os.name"].toLowerCase().contains("mac"))
    {
    if(!newFile(javaBase+javaRt).exists())
    {
    ...do something
    }
    }

    来处理一下针对mac os的情况,比如

    javaBase=System.properties["java.home"]
    javaRt="/lib/rt.jar"
    if(System.properties["os.name"].toLowerCase().contains("mac"))
    {
    javaRt="/../Classes/classes.jar"
    }
    libraryjars javaBase+javaRt

    首先获取到java home路径,然后指定runtime.jar的相对位置,如果是mac平台的话就指定其他路径,最后拼接就可以了。

    这个方法可以解决一部分问题…

    我测试的时候找了三台不同的mac机器,发现有两台用不起。它们的文件系统没有classes.jar,却有rt.jar。

    仔细查看才发现,这两台机器装的Oracle的Java7…有rt.jar而且路径和其他平台一样的。

    修改为

    javaBase=System.properties["java.home"]
    javaRt="/lib/rt.jar"
    if(System.properties["os.name"].toLowerCase().contains("mac"))
    {
    if(!new File(javaBase+javaRt).exist()){
    javaRt="/../Classes/classes.jar"
    }
    }
    libraryjars javaBase+javaRt

    这样应该可以解决这个问题了。

    参考

    最后附上几个参考链接:

    http://proguard.sourceforge.net/index.html

    https://github.com/dreamhead/moco/blob/master/build.gradle

    https://github.com/MinecraftForge/Installer/blob/master/build.gradle

    https://github.com/deengames/Freedom/blob/master/proguard.cfg