博客

  • Cloud Foundry的buildpack结构

    Cloud Foundry的buildpack结构

    Buildpack是Cloud Foundry中一个很基本的组件。它意在为应用提供框架和运行环境的支持。也就是说当你上传内容物到Cloud Foundry上时,会根据Buildpack的内容为你准备必要的基本内容物来支持你的应用。

    一个Buildpack通常要包括以下三个关键步骤:

    1. 检测
    2. 编译
    3. 发布

    其中的检测步骤主要是判断当前上传的内容物是否可以使用当前的buildpack处理,一般情况下检测可以很简单,比如一个gradle-java-buildpack就可以检测当前目录是否包含build.gradle文件。

    #!/usr/bin/env bash
    # bin/detect <build-dir>
    
    elif [ -f $1/build.gradle ]; then
      echo "Gradle" && exit 0
    
    else
      echo "no" && exit 1
    fi

    编译步骤主要是准备必要的依赖和环境,这里就要分两种情况了,就是编译是否真正发生。如果上传的内容物是一个jar,那就只需要提供jre就行了,如果上传的内容物是源代码,那就是需要jdk环境,并且执行必要的编译打包步骤,最后在提取运行包。

    具体使用哪种情况主要看对于Cloud Foundry的使用偏好,我是比较喜欢在Cloud Foundry直接编译的。

    一般情况下这一步会比较耗时,以java项目为例,下载一个jdk或者jre是必要的,为了节约时间,Cloud Foundry会在这一步提供一个缓存目录以供使用。

    最后一步是发布,这一步必须生成一个YAML文件,里面是必要的运行命令。

    类似于:

    default_process_types:
      web: bundle exec rackup config.ru -p $PORT

    如果需要清理一些内容,可以考虑一开始就放在tmp目录中,或者在第二步手动删除。

  • 快速生成.gitignore文件

    快速生成.gitignore文件

    当新启动一个项目的时候,特别是项目会托管到GIT仓库的时候我们一般会选择添加.gitignore文件。

    gitignore可以手动添加,但是随着语言,IDE工具,框架的变动,创建.gitignore也越发复杂了,经常会漏掉一些东西。

    这里有一个网站可以帮助生成gitignore文件: https://www.gitignore.io/

     

  • sited(多多猫)插件开发

    最近看漫画有点多,因为各种各样的原因各个网站的漫画都不全,好不容易找到一个好用的,估计服务器不在大陆,速度特别慢。但是又只有网页版本,自然没有各种缓存,离线 下载功能了。

    本来寻思自己写一个,但是在知乎看到一个推荐多多猫的,就简单尝试了一下,果然不要自己造轮子呀。

    多多猫这个app本身是没有任何内容的,它只是一个插件容器,内容来源是需要插件实现的。而插件的标准(?)名为sited,是xml格式,具体代码实现是javascript。简单来说文档是有的,而且只找到了一份官方的,可以看出有些内容并没有及时更新,但是照着做一个还是没有问题的。

    简单来说一下这个漫画网站的结构,主页就是一个列表,每个漫画有分页,一页10张图,一般一个漫画200张左右,就是一个没有目录结构的顺序图集。

    先来看看插件大体格式

    <?xml version="1.0" encoding="utf-8"?>
    <sited ver="1" debug="0" engine="32" schema="1">
        <meta>
            <title>XXX漫画</title>
            <encode>utf-8</encode>
            <ua>Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87
                Safari/537.36
            </ua>
        </meta>
        <main dtype="4" durl="xxx">
            <home>
                <hots cache="10m" title="首页" method="get" parse="hots_parse" url="xxx" showWeb="0"
                      showImg="2"/>
            </home>
        </main>
        <script>
            <require>
                <item url="http://sited.noear.org/addin/js/cheerio.js" lib="cheerio"/>
            </require>
            <code>
            </code>
        </script>
    </sited>

    主要结构是meta,main和script三大块。其中meta主要是插件的一些信息,main定义的逻辑节点,比如主页,tag,详细页面等等,script中是具体的处理逻辑。

    每一块的所有属性都在文档中有这里就不细说了。先来看看home块,最关键的是parse属性,这里定义的是处理方法,而根据文档需要返回一个对象,包含了name,logo,url三个属性。

    function hots_parse(url, html) {
        var $ = cheerio.load(html);
        var list = [];
        $('.post_box').each(function () {
            var slf = $(this);
            var bm = {};
            bm.name = slf.children("div.c-top").find('a').text();
            bm.url = urla(slf.children("div.c-top").find('a').attr('href'));
            bm.logo = urla(slf.children("div.c-con").find('img').attr('src'));
            list.push(bm);
        });
        return JSON.stringify(list);
    }

    这里依赖了cheerio作为网页解析,而返回的是一个json格式的list,当然具体的解析逻辑要根据需求来。

    具体的详细页面的解析式定义在book块中,根据文档这里有两个函数,一个是根据网址返回一个图片地址列表,一个是根据网址返回要处理的网址。

    根据网站的结构,在列表页获取的只是漫画的前十张图,还需要获取所有的分页地址,然后再处理,所以这里两个函数都需要。

    <book cache="1d" method="get" parse="book_parse" parseUrl="book_parse_url" showWeb="0" showImg="2"/

    先来看看book_parse_url函数

    function book_parse_url(url, html) {
        var $ = cheerio.load(html);
        var list = [];
        var max = $('.single-navi').last().text();
        for (var i = 1; i <= max; i++) {
            list.push(url + '/' + i);
        }
        return list.join(';');
    }

    这里返回的是一个字符串,而具体的处理函数返回的是一个列表

    function book_parse(url, html) {
        var $ = cheerio.load(html);
        var list = [];
        $('.entry-content').children("p").each(function () {
            var slf = $(this);
            list.push(slf.children("img").attr('src'));
        });
        return JSON.stringify(list)
    }

    这样就实现了整个插件的核心逻辑,可以自由浏览漫画,同时因为多多猫作为插件容器实现了通用功能,这样也就拥有了历史记录,离线下载,收藏等功能了。

    总的来说多多猫和sited插件体系是一个很有意思的东西,对于不同资源的定义和插件标准的设计能否覆盖大部分需求,唯一的问题就是开发时如果有任何问题要么显示插件格式解析失败,要么就是白页面,所以调试基本靠脑补。不过好在代码量不大,出错几率也不是很高。

  • BT种子文件转二维码

    自从入手小米路由器以来,下载电影就只选择最大的了,因为路由器下载可以在白天上班时间下载,而且夜间也可以,下载完成还可以用手机或者电视播放。

    小米路由器APP端支持扫码下载和手动添加任务,当然手机上添加BT种子挺麻烦的,所以我一般用扫描二维码,好在很多下载站都提供了二维码。二维码中一般是种子的下载地址或者磁力链。但是偶尔会遇到只有种子文件的情况,所以就需要想个办法来转化成为二维码。

    最终效果在这里:https://to-qr.herokuapp.com/

    简单的地方就跳过了,比如如何上传文件,显示二维码这些,这里只关注两个问题。

    1. BT文件转磁力链接
    2. 磁力链接转二维码

    BT文件转磁力链接

    磁力链接中最关键的内容就是info hash,这个值可以轻松从BT文件中解析。

    具体原理可以参考BT种子文件结构。这里使用ttorrent-core库。

    package com.huangyunkun.service;
    
    import com.turn.ttorrent.common.Torrent;
    import org.apache.commons.io.FileUtils;
    import org.springframework.stereotype.Service;
    
    import java.io.File;
    import java.io.IOException;
    import java.security.NoSuchAlgorithmException;
    
    @Service
    public class TorrentService {
        public String getLink(File file) throws IOException, NoSuchAlgorithmException {
            Torrent torrent = new Torrent(FileUtils.readFileToByteArray(file), false);
            return "magnet:?xt=urn:btih:" + torrent.getHexInfoHash();
        }
    }
    

    磁力链接转二维码

    这里的转化其实就是一个简单的文本转二维码。二维码的生成库比较多。这里使用的是QRGen。

    compile('com.github.kenglxn.QRGen:javase:2.2.0')
    File QR = QRCode.from(link).withSize(512, 512).file();
    
  • 数据库迁移工具Flyway对比Liquibase

    很多应用的运行是需要数据库支持的,而随着快速迭代,产品更替的节奏加快,除了产品本身需要不断更新以外,数据库也需要做出合适的管理了。

    为什么需要数据库迁移管理

    比如第一个版本的产品只包含了最基本的功能,而第二版本就需要增加评论功能,这就涉及到数据结构的修改(包括创建新表,修改旧表的列,增加已有表的列等等)。直接进入产品数据库修改数据库并不适合快速的开发节奏,不仅仅不安全,更多的情况下数据库可能并不对外或者并不适合对外直接暴露连接,比如PAAS平台的数据库以服务的形式直接提供。

    对比代码管理的一些实践,很明显在数据库方面做的还欠缺很多。比如代码管理中我们有

    • 版本管理(svn,git等等)
    • 持续集成技术
    • 良好的发布工具和流程

    而在数据库方面会遇到很多问题

    • 某台数据库现在是什么状态
    • 修改变更的脚本是否已经应用
    • 对于生产环境的紧急修复有没有被应用在测试环境
    • 如何创建一个新的数据库实例

    数据库迁移工具可以很好的管理这些问题,并提供了以下特性

    • 从迁移脚本中创建新的数据库
    • 检查数据库状态
    • 从一个版本快速到达另外一个版本

     Flyway和Liquibase

    数据库迁移工具很多,这里我们选择Flyway和Liquibase来说主要是两个原因,一是它们都是Java生态圈的,其次就是Spring Boot提供了这两者的内建支持,可以很快应用到产品中。

    Flyway相对简单,直接将你需要执行的SQL语句保存为文件,放入应用中执行即可。比如

    V1__init-database.sql
    V2__add-comment.sql

    Flyway的好处在于简单,而且直接书写SQL并不需要额外的学习。

    Liquibase相对就复杂了很多,它支持四种格式

    • xml
    • json
    • yaml
    • sql

    如果使用过Flyway就会有一定的体会,Flyway的简单是有代价的,举个简单的例子,如果我们开发环境是h2数据库,而测试环境和产品环境是MySQL,这里就有一个问题,SQL语句并不是一个广泛兼容的语言,有些关键字是独有的,而我们并不希望放弃这部分功能。这种情况下你就需要书写两套SQL迁移文件。Spring Boot是内建这种支持的,可以从目录上做区分。

    而Liquibase可以根据数据库的情况为你生成最后的迁移语句,同时因为数据库变动首先是被Liquibase解析,所以也可以简单支持回滚。

    来看一个Liquibase的例子,以XML为例,我个人觉得yaml更简洁,但是经常有对齐的问题。

    <?xml version="1.0" encoding="UTF-8"?>
    
    <databaseChangeLog
            xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
            xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
            http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
        <changeSet id="1" author="nvoxland">
            <createTable tableName="person">
                <column name="id" type="int" autoIncrement="true">
                    <constraints primaryKey="true" nullable="false"/>
                </column>
                <column name="firstname" type="varchar(50)"/>
                <column name="lastname" type="varchar(50)">
                    <constraints nullable="false"/>
                </column>
                <column name="state" type="char(2)"/>
            </createTable>
        </changeSet>
        <changeSet id="2" author="nvoxland">
            <addColumn tableName="person">
                <column name="username" type="varchar(8)"/>
            </addColumn>
            <addLookupTable
                existingTableName="person" existingColumnName="state"
                newTableName="state" newColumnName="id" newColumnDataType="char(2)"/>
        </changeSet>
    </databaseChangeLog>

    Liquibase支持大部分常见的数据库变动操作,比如建表,删表,变动字段等等。

    Liquibase可以在不使用SQL的情况下造成数据库变动,其可读性更高一些,特别是团队并不直接使用SQL而整体相关知识储备不完善的情况下优势更明显。

    结论

    两款数据库迁移工具其实定位上是差别的,一般我的倾向是小项目,整体变动不大的用Flyway,而大应用和企业应用用Liquibase更合适。

  • 使用onelogin快速集成Okta SSO

    使用onelogin快速集成Okta SSO

    Okta是专业的身份管理平台,它是以整合的思路管理企业应用和资源。

    如果企业有一个内部应用需要认证后使用,那么Okta提供的单点登录功能就是一个很好的集成点。集成Okta可以将用户认证部分从应用剥离,使用者还是可以使用Okta的信息登录,并且应用也能获得对应的信息。

    本文选用的是通过SAML协议集成Okta。SAML即安全断言标记语言,英文全称是Security Assertion Markup Language。 它是一个基于XML的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。

    来个示意图:

    从图中可以看到集成部分从收到Response之后应用就获得用户的信息(比如Name ID),之后的事情就是应用自己的了。

    Saml毕竟是一个标准协议,直接使用公开的库就行了。OpenSaml本来是最合适的,但是使用有些麻烦,最后选择的是onelogin的java-saml库。

    必要的配置只有五个

    onelogin.saml2.sp.entityid
    onelogin.saml2.sp.assertion_consumer_service.url
    onelogin.saml2.idp.entityid
    onelogin.saml2.idp.single_sign_on_service.url
    onelogin.saml2.idp.x509cert

    这个在Okta的配置页面就有。

    一般应用程序需要提供两个功能点,一个是GET方法检测是否需要进入SSO流程,一个是POST方法用于接收Saml的返回断言。

    @GET
        public Response showLoginPage(@Context HttpServletResponse response,
                                      @Context HttpServletRequest request) throws Exception {
            Auth auth = new Auth(request, response);
            auth.login();
            return Response.ok().build();
        }
    @POST
        public Response retrieveInformation(@Context HttpServletResponse response,
                                      @Context HttpServletRequest request,
                                            @Context UserRepository userRepository,
                                            @Context AuthenticationService authenticationService) throws Exception {
            Auth auth = new Auth(request, response);
            auth.processResponse();
            if(auth.isAuthenticated() && auth.getErrors().isEmpty()) {
                String userId = auth.getNameId();
                // 自己的逻辑处理
                return Response.temporaryRedirect(uri).build();
            }else{
                return Response.serverError().build();
            }
        }

     

  • JDBC的MySQL连接字符串

    JDBC的MySQL连接字符串

    今天迁移一个应用,放在heroku死活没法运行。仔细对比以后发现heroku会暴露名为JDBC_DATABASE_URL的环境变量,而这个环境变量只有一个默认参数reconnection=true。

    而原应用运行环境中还多了两个参数,一个是allowMultiQueries 和zeroDateTimeBehavior 。最开始没有太在意,想着应用很简单,无非就是影响一下性能而已,结果直接无法运行,被迫加上。

    来看看这两个参数,第一个allowMultiQueries 是运行一次运行多行。多行是指命令层级的含义,比如select ‘hello’;select ‘world’ 。

    第二个zeroDateTimeBehavior 是对于时间的处理,比如有一个条插入语句,没有提供需要的时间时,默认情况会抛出

    Cannot convert value '0000-00-00 00:00:00' from column 7 to TIMESTAMP

    这时候将zeroDateTimeBehavior 指定为convertToNull可以避免写入错误的值。

  • Spring Boot 1.5发布

    Spring Boot 1.5今天发布了。

    新版本添加了基于unboundid对于LDAP的支持和Apache Kafka的支持。还有一个有趣的改动是Flyway添加了对于vendor的支持,默认的数据库版本管理地址从db/migration/ 变成了flyway.locations=db/migration/{vendor} ,这里的vendor指的是不同的数据库类型,比如db/migration/mysql 。

    新版本还移除了Gradle 1.x的支持(Gradle最新版是3.x)。比较遗憾的是因为CRaSH project已经处于不活跃状态,所以Spring Boot 1.5移除了所有关于CRaSH的支持,也就是remote功能被废弃了。

  • Splunk使用的一些小窍门

    Splunk可以用作日志分析工具,之前只是简单的把它当作日志搜集工具并没有其他用。

    突然有一个很冷门的小应用,在线上运行了好几年,它使用的AWS SNS推送服务,推送服务是针对邮编的,现在想知道每个邮编每月的发送量。

    首先看关键字,之前代码每发送一个信息会打一条日志Published message: xxxx to customer-alert-61000,最后那个是邮编。

    首先提取自定义字段

    rex field=_raw "customer-alert-(?<postcode>.*)"

    作用就是匹配正则,并把匹配的内容作为新的field,名为postcode。

    然后规定时间长度为

    timechart span=1mon count

    最后以postcode为分组依据

    timechart span=1mon count by postcode

    Splunk对于分组会默认有一个限制,超过限制的部分会命名为Others,显然我们不需要Others,而是需要所有分组,邮编有超过两千个,直接粗暴一些,禁用others,然后最大长度限制为3000

    useother=f limit=3000

     

  • libGDX快速集成lua脚本

    libGDX快速集成lua脚本

    Lua确实是一个非常常用的脚本,很多时候处于快速更新应用或者游戏AI自身所需,我们必须在游戏中集成脚本支持。而Lua是一个不错的选择。

    Lua语言规范精简、运行时库小,非常容易创建受控环境,安全性也不错。虽然是一个小巧的脚本语言,但是支持协程,能够在关键时刻发挥很大作用。

    要在libGDX中集成Lua非常简单,只需要选择一款java实现的Lua运行环境即可。本文以luaj为例。

    首先在build.gradle中添加依赖

    compile "org.luaj:luaj-jse:3.0.1"

    然后在选择需要调用的地方,这里来个简单的例子,我们需要在stage初始化时添加一个Image对象,而Image对象的资源来自TextureAtlas类,为了演示的完整性,我们还会把Lua脚本执行结果赋值给Java上下文。

    这是原有的Java代码

    atlas = DartsGame.getManager().get("pack/sha/default.pack", TextureAtlas.class); // 获取图册
    man = new Image(atlas.findRegion(PLAYER)); // 获取图册中的Player.png并创建image对象
    man.setName("player");
    man.setX(0);
    man.setY(160 - man.getHeight() / 2); // 设置Y值,以让图片在中间显示
    stage.addActor(man); // 将主角添加到舞台
    

    新创建一个Lua脚本

    function man(stage, atlas)
        man = luajava.newInstance("com.badlogic.gdx.scenes.scene2d.ui.Image", atlas:findRegion("Player"))
        man:setName("player")
        man:setX(0)
        man:setY(160 - man:getHeight() / 2)
        stage:addActor(man)
        return man
    end

    这里我们return man是为了演示Lua脚本和Java宿主的交互,并没有太多实际的意义。

    然后返回Java代码,一般我们会在一些常见的地方添加脚本调用点,但是对应的调用可以为空,一般我会选择Stage初始化的时候,Stage update的时候。然后会传入一些常见的Java对象给予Lua脚本,常见的包括stage和assetManager。

    Globals globals = JsePlatform.standardGlobals();
    LuaValue load = globals.load(Gdx.files.local("man.lua").readString());
    load.call();
    Varargs varargs = globals.get("man").invoke(new LuaValue[] {
        CoerceJavaToLua.coerce(stage),
        CoerceJavaToLua.coerce(atlas)
    });
    man = (Image) CoerceLuaToJava.coerce(varargs.arg1(), Image.class);

    这里需要注意的是CoerceJavaToLua和CoerceLuaToJava两个类,它们分别提供了Java对象转Lua对象和Lua对象转Java对象的功能。因为Lua默认支持的类型非常少,所以大部分时候LuaValue.of是不够用的。这两个类其实使用了userdatas作为传递对象,直接将Java的对象放入userdatas中,并保持class的信息。

    public Object coerce(LuaValue value) {
        switch (value.type()) {
        case LuaValue.TNUMBER:
            return value.isint() ? (Object) new Integer(value.toint()) : (Object) new Double(value.todouble());
        case LuaValue.TBOOLEAN:
            return value.toboolean() ? Boolean.TRUE: Boolean.FALSE;
        case LuaValue.TSTRING:
            return value.tojstring();
        case LuaValue.TUSERDATA:
            return value.optuserdata(targetType, null);
        case LuaValue.TNIL:
            return null;
        default:
            return value;
        }
    }

    因为coerce也支持普通Lua类型,所以可以放心使用。

    另外Intellij有Lua脚本插件,所以写起来还好