博客

  • libGDX游戏入门第二版

    libGDX游戏入门第二版

    Learning LibGDX Game Development是出版较早的关于Libgdx的书籍,前不久出了第二版。

    书是packt旗下的,定位是初学者级别,即对针对初次接触Libgdx的开发者。
    其中包含大量细致的步骤指导。

    第二版相较于第一版主要的变化在于:

    • Libgdx版本提升到1.x时代
    • 增加了3d相关内容
    • 增加了Gradle构建的相关章节

    当然,只有英文版的,在amazon能够买到,不过还是推荐买packt的kindle版,便宜又方便。

    地址:http://bit.ly/1zYQS1E

    如果上面地址访问不起,可以试试:https://www.packtpub.com/game-development/learning-libgdx-game-development-second-edition

  • 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;
    }
    }
  • Java web容器中load-on-startup

    昨天一同事提到load-on-startup的问题,虽然知道它在标准中有提到,但是具体的细节又有点记不清楚了。

    刚才想到看了一下JSR 315: JavaTM Servlet 3.0 Specification

    load-on-startup的作用

    load-on-startup是servlet tag下的一个值。它告诉容器加载顺序,如果不声明这个值那么容器只会在它认为需要的时候才加载它。

    这样有时会出现一个问题,如果Servlet启动时会初始化数据库连接或者加载其他信息,如果这些耗时较长的处理不在容器启动时完成,那就会在容器认为必要时加载Servlet是完成,也就是说会出现响应问题。

    所以有load-on-startup这个设置来允许你指定servlet需要在部署到容器时就加载。

    load-on-startup的值

    load-on-startup的应该是数字,有时会出现以下几种情况:

    • 如果两个Servlet的load-on-startup的值相同,那么加载顺序是在web.xml中的声明顺序。
    • 如果值为零或者负数,那么在需要时才加载。
    • 如果没有,那么加载由容器决定。
  • 共享常用的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)
  • Spring根据包名搜索类

    有时候我们会遇到这种情况,需要根据动态获取某个包下的一些特定类。

    比如获取”com.xxx.domain”下所有类。

    Spring中ClassPathScanningCandidateComponentProvider可以快速完成这项任务。

    Spring最核心的一个部分就是Ioc,通过@Component来注解需要委托管理的类。

    ClassPathScanningCandidateComponentProvider提供了一个默认的过滤器来处理Component。

    protectedvoidregisterDefaultFilters() {
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
    try {
    this.includeFilters.add(new AnnotationTypeFilter(((Class < ?extends Annotation > ) ClassUtils.forName("javax.annotation.ManagedBean", cl)),false));
    logger.debug("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
    }catch(ClassNotFoundException ex) {
    // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
    }
    try {
    this.includeFilters.add(new AnnotationTypeFilter(((Class < ?extends Annotation > ) ClassUtils.forName("javax.inject.Named", cl)),false));
    logger.debug("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
    }catch(ClassNotFoundException ex) {
    // JSR-330 API not available - simply skip.
    }
    }

    当然,我们不需要这个过滤器,所以在实例化ClassPathScanningCandidateComponentProvider时传入参数false。

    我们可以自定义过滤器来做一些逻辑相关的东西,比如加载以Task结尾的类。

    final ClassPathScanningCandidateComponentProvider provider =new ClassPathScanningCandidateComponentProvider(false);
    provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*Task")));
    final Set < BeanDefinition > classes = provider.findCandidateComponents("cn.acgmo.job");
    for (BeanDefinition bean: classes) {
    jobClasses.add(Class.forName(bean.getBeanClassName()));
    }

    虽然还有一个很方便的Reflection库可以选择,但是大部分情况下Spring就足够了。

  • Spring Boot获取Active Profile的值

    Spring Boot提供了profile机制,可以快速的在不同情况下切换配置。

    默认支持很多配置,从Spring整体的行为到具体的数据库,渲染引擎的行为都有支持。按照文档给出的配置名称和可选值操作就可以了。

    有些时候我们需要基于profile来实现自己的一些配置,这个时候就需要自己完成一些后面的细节了。

    举个比较常见的例子,profile分为了dev和prod两种。而项目本身需要支持上传图片功能,需要在profile中指定文件配置,比如

    upload.path=C:Users315junAppDataLocalTemp

    要获取这个值,我们有三种办法

    • 从Env中读取
    @Autowired
    private Environment env;
    String path=env.getProperty("upload.path");
    • 构造一个Config类,通过注解自动注入
    @ConfigurationProperties(prefix ="upload")
    publicclass UpdatePathConfig {
    private String path;
    
    public StringgetPath() {
    return path;
    }
    
    publicvoidsetPath(String path) {
    this.path = path;
    }
    }
    • 通过Value注解注入
      这种方法最直接,在对应的字段上标注即可。
    @Value("${upload.path}")
    private String path;
  • Spring Boot 中文乱码问题

    Spring Boot默认的编码并不是UTF8,在一些表单中中文会有乱码的情况,需要添加一个Filter来解决。

    Spring Boot大量使用注解,而不是xml配置文件,所以我们的Filter也使用Java-base的配置模式。

    @Beanpublic FiltercharacterEncodingFilter() {
      CharacterEncodingFilter characterEncodingFilter =new CharacterEncodingFilter();
      characterEncodingFilter.setEncoding("UTF-8");
      characterEncodingFilter.setForceEncoding(true);
      return characterEncodingFilter;
    }

    相关文章:

    再谈Spring Boot中的乱码和编码问题

    网站乱码和ISO-8859-1与UTF-8

     

  • 使用JsonNodeFactory快速构建Json响应

    在Spring中可以使用@ResponseBody来让返回对象直接转化为Json格式,这种方式需要提供一个专门的对象来作为内容的承载。

    如果只是需要快速的一段简单的Json响应就没有必要构造一个对象,然后让对象来承载内容了,可以直接使用JsonNodeFactory来构造。

    JsonNodeFactory是jackson中的类,我使用的是Spring Boot,自动包含了这个依赖库。

    使用时通过JsonNodeFactory.instance来获取工厂对象,然后进行创建。

    @RequestMapping(value ="/status")
    @ResponseBody
    public ObjectNodeshowSuccessStatus() {
    JsonNodeFactory factory = JsonNodeFactory.instance;
    ObjectNode objectNode = factory.objectNode();
    objectNode.set("status", factory.textNode("success"));
    return objectNode;
    }

    `

  • 快速更改Spring Boot的Active Profile设定

    Spring Boot支持Active Profile,可以方便在不同的环境中快速切换配置,但是这个配置本身的修改却不够简洁。

    如果有两个profile,分别为dev和prod,那么在application.properties文件中

    spring.profiles.active=dev

    就指定了dev为当前配置。

    因为使用了gradle构建工具,所以希望通过类似./gradlew dev bootRun的方式直接切换。

    虽然Spring Boot本身支持运行时带参数,但是每次使用又有些繁琐,稍微修改一下build.gradle文件如下:

    task dev << {
    changeActiveProfile("dev")
    }
    task prod << {
    changeActiveProfile("prod")
    }
    def changeActiveProfile(profile) {
    def pFile = file("src/main/resources/application.properties")
    Properties p = new Properties()
    pFile.withInputStream {stream ->
    p.load(stream)
    }
    p.setProperty("spring.profiles.active", profile)
    pFile.withOutputStream {stream ->
    p.store(stream,"Change active profile to ${profile}")
    }
    }

    这样就可以每次运行dev或者prod任务时直接修改配置文件。

    使用的时候直接

    gradle prod war
    gradle dev bootRun