Drools 语法规则

What is drools?

Drools is a business rule management system with a forward and backward chaining inference based rules engine, more correctly known as a production rule system, using an enhanced implementation of the Rete algorithm.1

基础 API

在 Drools 当中,规则的编译与运行要通过 Drools 提供的各种 API 来实现,这些 API 总体来讲可以分为三类:规则编译、规则收集和规则的执行。

在 drools 6.x 以后这些 API 都整合到 kie API 中了

KIE 定义的接口可以在 GitHub droolsjbpm-knowledge 这个项目中查看。

KnowledgeBuilder

KnowledgeBuilder 在业务代码当中整理已经编写好的规则,对这些规则文件进行编译,最终产生编译好的规则包(KnowledgePackage)给其它的应用程序使用。

KnowledgeBase

KnowledgeBase 是 Drools 提供的用来收集应用当中知识(knowledge)定义的知识库对象,在一个 KnowledgeBase 当中可以包含普通的规则(rule)、规则流 (rule flow)、函数定义 (function)、用户自定义对象(type model)等。KnowledgeBase 本身不包含任何业务数据对象,业务对象都是插入到由 KnowledgeBase 产生的两种类型的 session 对象当中,通过 session 对象可以触发规则执行或开始一个规则流执行。

StatefulKnowledgeSessions

StatefulKnowledgeSession 对象是一种最常用的与[[规则引擎]]进行交互的方式,它可以与规则引擎建立一个持续的交互通道,在推理计算的过程当中可能会多次触发同一数据集。在用户的代码当中,最后使用完 StatefulKnowledgeSession 对象之后,一定要调用其 dispose() 方法以释放相关内存资源。

public interface StatefulKnowledgeSession
	extends
	KieSession, KieRuntime {

	KieBase getKieBase();
}

StateLessKnowledgeSession

StatelessKnowledgeSession 的作用与 StatefulKnowledgeSession 相仿,它们都是用来接收业务数据、执行规则的。事实上,StatelessKnowledgeSession 对 StatefulKnowledgeSession 做了包装,使得在使用 StatelessKnowledgeSession 对象时不需要再调用 dispose() 方法释放内存资源了

调用 execute(...) 方法会在内部实例化 StatefulKnowledgeSession 对象,添加用户数据,执行命令,调用 fireAllRules,最后自动调用 dispose().

public interface StatelessKnowledgeSession
	extends
		StatelessKieSession {

}

FACT 对象

Fact 是指在 Drools 规则应用当中,将一个普通的 JavaBean 插入到规则的 WorkingMemory 当中后的对象。

Drools 规则可以对 Fact 对象进行任意的读写操作,当一个 JavaBean 插入到 WorkingMemory 当中变成 Fact 之后,Fact 对象不是原来的 JavaBean 对象的 Clone,而是原来 JavaBean 对象的引用。

规则文件

一个标准的 Drools 规则文件就是一个以“.drl”结尾的文本文件。

Drools 规则文件包含一个或多个 rule 声明,每一个 rule 由一个或多个条件以及要执行的动作(Action)组成。一个规则文件还可以有 0 个或多个 import 声明,global 声明和 function 声明。

Drools 规则文件大致可以包含这些部分:

package package-name
imports
globals
functions
queries
rules

package 是必须的,除 package 之外,其它对象在规则文件中的顺序是任意的,也就是说在规则文件当中必须要有一个 package 声明,同时 package 声明必须要放在规则文件的第一行。

package

package 是一系列 rule 的一个命名空间,这个空间中所有的rule 名字都是唯一的。package-name 必须遵守 Java 命名规范。

import

Drools 文件中的 import 语句和 Java 的 import 语句类似,引入指定对象的路径及全称。

global

global 用于定义全局变量。

  • 全局变量不会插入到 Working Memory
  • 全局变量的改变不会通知规则引擎,规则引擎不会追踪全局变量的变化
  • 多个包中同时定义相同标识符的全局变量,全局变量必须是相同类型,并引用一个相同的全局值

function

function 提供了一种在规则源文件中插入语义代码的方式。在规则中使用函数的优点是可以把逻辑放在一个地方。

function String hello(String name) {
  return "hello " + name + "!";
}

注意这里的,function 并不是 java 语法的一部分。Drools 支持函数的导入:

import function my.package.Foo.hello

Type declaration

规则引擎中,可以:

  • 允许新的类型声明
  • 允许元数据类型的声明

类型声明

declare Address
   number : int
   streetName : String
   city : String
end

定义一个新的类型 Address, 有三个属性,每个属性的类型都是 Java 中有效的数据类型。

定义 Person

import java.util.Date

declare Person
	name : String
	dateOfBirth : Date
	address : Address
end

定义该新类型后,Drools 会在编译期间生成对应的 Java 类字节码。

声明枚举类型

declare enum DaysOfWeek
   SUN("Sunday"),MON("Monday"),TUE("Tuesday"),WED("Wednesday"),THU("Thursday"),FRI("Friday"),SAT("Saturday");

   fullName : String
end

声明后可以直接应用于规则中:

rule "Test Enum Rule"
when
  $p: Employee( dayOff == DaysOfWeek.MONDAY )
then
  ...
end

声明云数据 (metadata)

@metadata_key( metadata_value )

Rule

一条规则的大致框架包括如下几部分:

rule "name"
    attributes
    when
        LHS
    then
        RHS
end

一个规则通常包括三个部分:

  • 属性部分(attribute),非必须,最好写在一行,关于规则属性部分,后文有更详细的介绍
  • 条件部分(LHS)
  • 结果部分(RHS)

对于一个完整的规则来说,这三个部分都是可选的(可以为空),也就是说如下所示的规则是合法的:

rule "name"
when
then
end

注释

drl 文件中对规则进行注释,和 Java 一样可以使用

  • 单行注释 //
  • 多行注释 /* xxx */

Drools 5 中定义了 hard 和 soft 关键字,Hard 关键字是保留字,不能够在规则中自定义随意使用

true
false
accumulate
collect
from
null
over
then
when

规则举例

rule "validate holiday by eval"
dialect "mvel"
when
    h1 : Holiday( )
    eval( h1.when == "july" )
then
    System.out.println(h1.name + ":" + h1.when);
end

或者

rule "validate holiday"
dialect "mvel"
when
    h1 : Holiday( `when` == "july" )
then
    System.out.println(h1.name + ":" + h1.when);
end

条件部分

条件部分又被称之为 Left Hand Side,简称为 LHS。 在 LHS 当中,可以包含 0~n 个条件,如果 LHS 部分没空的话,那么引擎会自动添加一个 eval(true) 的条件,由于该条件总是返回 true,所以 LHS 为空的规则总是返回 true。LHS 部分是由一个或多个条件组成,条件又称之为 pattern(匹配模式),多个 pattern 之间用可以使用 and 或 or 来进行连接,同时还可以使用小括号来确定 pattern 的优先级。

绑定对象语法

[ 绑定变量名 ]: Object([field 约束 ])

绑定变量是可选的,如果当前规则 LHS 部分的其他规则需要使用到这个对象,可以通过为该对象设定一个绑定变量名来实现对其引用,对于绑定变量,通常在其变量名前增加 $ 符号来和 Fact 区别。field 约束表示的是对对象中 field 的约束。

比如对于该规则

rule "rule1"
when
    $customer:Customer(age>20, gender=="male")
    Order(customer==$customer, price>1000)
then
<action>
End

规则含义:包含两个 pattern,第一个 pattern 有三个约束,分别是:

  • 对象类型必须是 Customer;
  • 同时 Customer 的 age 要大于 20
  • 且 gender 要是 male;

第二个 pattern 也有三个约束,分别是:

  • 对象类型必须是 Order,
  • 同时 Order 对应的 Customer 必须是前面的那个 Customer
  • 且当前这个 Order 的 price 要大于 1000。

在这两个 pattern 没有符号连接,在 Drools 当中在 pattern 中没有连接符号,那么就用 and 来作为默认连接,所以在该规则的 LHS 部分中两个 pattern 只有都满足了才会返回 true。默认情况下,每行可以用“;”来作为结束符(和 Java 的结束一样),当然行尾也可以不加“;”结尾。

操作符

Drools 中的操作符有很多种类:

  • Arithmetic operators (+, -, *, /, %) 算数操作符
  • Relational operators (>, >=, ==, !=) 关系操作符
  • Logical operators 逻辑操作符

    • conjunction (and, &&, ",") 与
    • disjunction (or, ||) 或
    • negation (!, do not confuse with not) 取反 (!, 不要和 not 混淆)
  • Drools operators (in, matches, etc…) Drools 操作符 (in, matches, 等等…)

一些操作符都非常通俗易懂,这里有几个需要特别注意

约束连接

对象内部多个约束连接,可以使用 &&, || 或者 ,(and) 。优先级 && > ||

,&& || 不能混用,在 &&|| 出现的语句中不能出现 ,

比较操作符

Drools 中一共提供了 12 种类型的比较操作符,>, >=, <, <=, ==, != ,contains, not contains, memberof, not memberof, matches, not matches 。前六个比较常用,不介绍了,现在结束一下后几个。

contains 举例:

when
    $order:Order();
    $customer:Customer(age >20, orders contains $order);
then
    System.out.println($customer.getName());
End

in 操作符

  • in 操作符是表示值在一个集合内部,集合中的数据需要单独列出

    when e : Emp (deptno in (10,20))

等效于

   e : Emp(deptno == 10 || deptno == 20)
   e : (Emp(deptno == 10) or Emp(deptno == 20))

matches 操作符

matches 是某个字段和 Java 正则匹配

when
    $customer:Customer(name matches "吴.*");
then
    System.out.println($customer.getName());
end

matches 操作符匹配是否匹配 Java 正则表达式。

.   匹配单一字符
.*  匹配任何字符,包括空字符串

不匹配需要这么写

when
    e: Emp(name not matches "B.*")

下面的写法是错误的!!!

when
    e: Emp(name ! matches "B.*")

    e: ! Emp(name matches "B.*")

操作符优先级

(nested) property access    .
List/Map access            [ ]
constraint binding   :
multiplicative       * / %
additive             + -
shift                << >> >>>
relational           < > <= >= instanceof
equality             == !=
bit-wise non-short circuiting AND               &
bit-wise non-short circuiting exclusive OR	^
bit-wise non-short circuiting inclusive OR	|
logical AND	&&
logical OR	||
ternary	? :
Comma separated AND	,

Drools 还支持一些高级语法规则,更多可以参考这里

结果部分

Right Hand Side,又被称为结果部分,RHS,规则中 then 后面部分就是 RHS,只有在 LHS 所有条件都满足时 RHS 部分才会执行。

RHS 部分是规则真正要做的事情,将条件满足而触发的动作写在该部分中,RHS 中可以使用 LHS 中定义的绑定变量名、设置的全局变量,或者直接编写 Java 代码(需要 import 相应的类)

RHS 中,提供了对当前 Working Memory 实现快速操作的宏函数和宏定义,比如 insert/insetLogical, update 和 retract,实现对当前 Working Memory 中 Fact 对象的新增、删除或者修改。

insert

insert(new Object());

一旦调用 insert 函数, Drools 会重新与所有规则再重新匹配一次,对于没有设置 no-loop 属性为 true 的规则,如果条件满足,不管之前是否执行过都会再执行一次,这个特性不仅存在于 insert 函数,update,retract 宏函数都有该特性,所以某些情况下考虑不周可能造成死循环。

update

对 Fact 进行更新,比如更新 Fact 中的某个字段,对应的相关的 Fact 都会更新,然后会通知 Drools 引擎该修改。

retract

用来将 Working Memory 中某个 Fact 对象删除。

modify

对 Fact 对象多个属性修改,修改完成后自动更新到当前 Working Memory 中。

modify ( <fact-expression> ) {
    <expression>,
    <expression>,
    ...
}

属性部分

规则属性是用来控制规则执行的重要工具,显示地声明了对规则行为的影响。

规则的属性有 13 个

  • activation-group
  • agenda-group
  • auto-focus
  • date-effective
  • date-expires
  • dialect
  • duration
  • enabled
  • lock-on-active
  • no-loop
  • ruleflow-group
  • salience
  • when

salience

salience 用来设置规则执行的优先级,salience 属性值是一个数字,数字越大优先级越高,可以是负值。salience 表示规则的优先级,值越大在激活队列中优先级越高。

  • 默认情况下,规则的 salience 是 0
  • type: Integer

所以不手动设置规则的 salience 属性情况下,执行的顺序是随机的。

rule "rule1"
salience 1
when
    eval(true)
then
    System.out.println("rule1");
End

no-loop

no-loop 属性的作用是用来控制已经执行过的规则在条件再次满足时是否再次执行。默认情况下规则的 no-loop 属性的值为 false,如果 no-loop 属性值为 true,那么就表示该规则只会被引擎检查一次。

当规则的 RHS 改变了 LHS 条件会导致该规则重新匹配执行,可以合理地使用来避免 Drools 规则进入死循环。

  • 默认值:false
  • type: Boolean

在上面提到的 insert 后,如果没有设置 no-loop 的规则会再检查一次。

date-effective

控制规则只有在到达指定时间后才会触发。只有当系统时间 >=date-effective 设置的时间值时,规则才会触发执行,否则执行将不执行。在没有设置该属性的情况下,规则随时可以触发,没有这种限制。

date-effective 可接受的日期格式为 “dd-MMM-yyyy”

rule "rule1"
date-effective "25-Sep-2019"
when
    eval(true);
then
    System.out.println("rule1 is execution!");
End

date-expires

该属性的作用与 date-effective 属性恰恰相反, date-expires 的作用是用来设置规则的有效期。如果 date-expires 的值大于系统时间,那么规则就执行,否则就不执行。

enabled

设置是否可用

dialect

该属性用来定义规则当中要使用的语言类型,目前 Drools 版本当中支持两种类型的语言:mveljava,默认情况下,如果没有手工设置规则的 dialect,那么使用的 java 语言。

  • type: String

想要了解 mveljava 这两个方言的区别可以参考:[[Drools 规则中 mvel 和 java 的差别]],一句话总结一下就是 MVEL 是 Java 实现的一套表达式解析语言。

duration

如果设置了该属性,那么规则将在该属性指定的值之后在另外一个线程里触发。该属性对应的值为一个长整型,单位是毫秒。

rule "rule1"
duration 3000
when
  eval(true)
then
  System.out.println("rule thread
  id:"+Thread.currentThread().getId());
end

lock-on-active

确认规则只执行一次。 将 lock-on-action 属性的值设置为 true,可能避免因某些 Fact 对象被修改而使已经执行过的规则再次被激活执行。lock-on-active 是 no-loop 的增强版属性。

一个组里面的多条规则都可以设置这个标志,当使用了这个标志的规则中的一条被成功触发后,会阻止其他规则的触发。

  • lock-on-active 属性默认值为 false
  • type: Boolean

不管何时 ruleflow-groupagenda-group被激活,只要其中的所有规则将 lock-on-active 设置为 true,那么这些规则都不会再被激活。

宏函数 insert, update, retract 都可以对 fact 进行操作,这些动作都可以导致 rule 重新匹配。

activation-group

该属性的作用是将若干个规则划分成一个组,用一个字符串来给这个组命名,这样在执行的时候,具有相同 activation-group 属性的规则中只要有一个会被执行,其它的规则都将不再执行。

  • type: String

在一组具有相同 activation-group 属性的规则当中,只有一个规则会被执行,其它规则都将不会被执行。当然对于具有相同 activation-group 属性的规则当中究竟哪一个会先执行,则可以用类似 salience 之类属性来实现。

rule "rule1"
activation-group "test"
when
    eval(true)
then
    System.out.println("rule1 execute");
end

rule "rule 2"
activation-group "test"
when
    eval(true)
then
    System.out.println("rule2 execute");
End

rule1 和 rule2 这两个规则因为具体相同名称的 activation-group 属性,所以它们只有一个会被执行。

agenda-group

Agenda Group 是用来在 Agenda 的基础之上,对现在的规则进行再次分组,具体的分组方法可以采用为规则添加 agenda-group 属性来实现。

  • 默认值: MAIN
  • type: String

agenda-group 属性的值也是一个字符串,通过这个字符串,可以将规则分为若干个 Agenda Group,默认情况下,引擎在调用这些设置了 agenda-group 属性的规则的时候需要显示的指定某个 Agenda Group 得到 Focus(焦点),这样位于该 Agenda Group 当中的规则才会触发执行,否则将不执行。

rule "rule1"
agenda-group "001"
when
eval(true)
then
System.out.println("rule1 execute");
end

rule "rule 2"
agenda-group "002"
when
eval(true)
then
System.out.println("rule2 execute");
End

java 代码

//getSession 获取 KieSession 的方法自己写的。
KieSession ks = getSession();
// 设置 agenda-group 的 auto-focus 使其执行
ks.getAgenda().getAgendaGroup("group1").setFocus();

当这个 group 被 setFocus 的时候,会将整个组压入栈中,执行的时候再取出来。

在 Drool 的规则 RHS 中还可以

kcontext.getKieRuntime().getAgenda().getAgendaGroup("Route-AgeRange").setFocus();

auto-focus

在已设置了 agenda-group 的规则上设置该规则是否可以自动独取 Focus,如果该属性设置为 true,那么在引擎执行时,就不需要显示的为某个 Agenda Group 设置 Focus,否则需要。

  • 默认:false
  • type: Boolean

对于规则的执行的控制,还可以使用 Agenda Filter 来实现。在 Drools 当中,提供了一个名为 org.drools.runtime.rule.AgendaFilter 的 Agenda Filter 接口,用户可以实现该接口,通过规则当中的某些属性来控制规则要不要执行。org.drools.runtime.rule.AgendaFilter 接口只有一个方法需要实现,方法体如下:

public boolean accept(Activation activation);

在该方法当中提供了一个 Activation 参数,通过该参数我们可以得到当前正在执行的规则对象或其它一些属性,该方法要返回一个布尔值,该布尔值就决定了要不要执行当前这个规则,返回 true 就执行规则,否则就不执行。

在引擎执行规则的时候,我们希望使用规则名来对要执行的规则做一个过滤,此时就可以通过 AgendaFilter 来实现,示例代码既为我们实现的一个 AgendaFilter 类源码。

import org.drools.runtime.rule.Activation;
import org.drools.runtime.rule.AgendaFilter;
public class TestAgendaFilter implements AgendaFilter {
    private String startName;
    public TestAgendaFilter(String startName){
        this.startName=startName;
    }
public boolean accept(Activation activation) {
        String ruleName=activation.getRule().getName();
        if(ruleName.startsWith(this.startName)){
            return true;
        }else{
            return false;
        }
    }
}

过滤方法是规则名的前缀,通过 Activation 得到当前的 Rule 对象,然后得到当前规则的 name,再用这个 name 与给定的 name 前缀进行比较,如果相同就返回 true,否则就返回 false。

java:

TestAgendaFilter filter = new TestAgendaFilter("activation")
int count = ks.fireAllRules(filter)

ruleflow-group

在使用规则流的时候要用到 ruleflow-group 属性,该属性的值为一个字符串,作用是用来将规则划分为一个个的组,然后在规则流当中通过使用 ruleflow-group 属性的值,从而使用对应的规则。

  • type: String

简单的来说,只有当被 ruleflow-group 圈定的组被激活时,ruleflow-group 中的规则才能被命中。

函数

代码块,封装多个规则中可能共享的相同规则代码

function void/Object functionName(Type arg ...) {
}

使用定义的 function,则需要 import function,通过 import 语句,实现将 Java 类中静态方法引入到一个规则文件中,使得该文件中规则可以像普通 Drools 函数一样来使用 Java 类中的静态方法

import function test.RuleTools.printInfo;

调用

RuleTools.printInfo(...)

reference


2019-03-28 drools , kie , rule-engine

JSON 反序列化重命名

Java 中有很多 JSON 相关的类库,项目中也频繁的使用 Jackson, fastjson, gson 等等类库。不过这些类库在反序列化 JSON 字符串到 Object 并且进行重命名字段的方法都不太一致,这里就列一下做个参考。

假设有原始字符串

String originStr = "{\"familyName\":\"Ein\",\"age\":20,\"salary\":1000.0}";

反序列化到类 Employee 上。

GSON

类定义

@Data
public class EmployeeGson {

    @SerializedName(value = "fullname", alternate = {"Name", "familyName"})
    private String name;
    private int age;
    @SerializedName("salary")
    private float wage;
}

测试方法

@Test
public void testRenameFieldGson() {
	String originStr = "{\"familyName\":\"Ein\",\"age\":20,\"salary\":1000.0}";
	EmployeeGson employee = new Gson().fromJson(originStr, EmployeeGson.class);
	System.out.println(employee);
}

Fastjson

@Data
public class EmployeeFastjson {
    @JSONField(name = "familyName")
    private String name;
    private int age;
    @JSONField(name = "salary")
    private float wage;
}

测试方法

@Test
public void testRenameFieldFastjson() {
	String originStr = "{\"familyName\":\"Ein\",\"age\":20,\"salary\":1000.0}";
	EmployeeFastjson employee = JSON.parseObject(originStr, EmployeeFastjson.class);
	System.out.println(employee);
}

Jackson

@Data
public class EmployeeJackson {

    @JsonProperty("familyName")
    private String name;
    private int age;
    @JsonProperty("salary")
    private float wage;

}

测试方法

@Test
public void testRenameFieldJackson() throws IOException {
	String originStr = "{\"familyName\":\"Ein\",\"age\":20,\"salary\":1000.0}";
	EmployeeJackson employeeJackson = new ObjectMapper()
			.readValue(originStr, EmployeeJackson.class);
	System.out.println(employeeJackson);
}

2019-03-27 json , gson , fastjson

Function 计算

函数计算,阿里云叫做 Function Compute,Aws 叫做 lambda 函数,GCP 叫做 Cloud Functions,各家都有各家的产品。就如同 AWS 页面介绍的那样,函数计算是一个无服务计算,可以用代码来响应事件并自动管理底层计算资源,比如通过 Amazon Gate API 发送 HTTP 请求,在 S3 桶中修改对象等等。

Serverless

抽象的 Serverless 很难概括,不过 Serverless 也经常被人叫做 Function as a Server(FaaS),这就比较好理解了,比如最常见的存储服务,原来的方式是用户租用云服务器,这种方式需要用户自行部署存储服务,磁盘上的数据也不能共享,于是后来发展出来对象存储,文件存储,消息服务等等,这些服务不再有机器的概念,用户可以轻松的扩容和负载均衡,通过平台提供的 API 进行数据的读写,共享。按照实际存储的数量和访问次数付费,这种就是所谓的 Serverless。

FaaS 的特征就是时间驱动,细粒度,弹性收缩,无需管理服务器等底层资源。

拆分微服务有三个考量,组织结构(参考康威定律),运维发布频率(比如将每周发布两次的服务与每两个月发布一次的服务进行拆分)和逻辑调用频度(将高频调用逻辑和低频调用逻辑分开,在 Serverless 架构下能够进一步降低成本)。

Serverless 适用的两大场景

  • 应用负载有显著的波峰波谷
  • 典型用例 - 基于事件的数据处理

函数计算的优势

  • 不需要管理服务器等基础设施,开发者只需要关注于逻辑开发
  • 事件驱动
  • 可以快速扩容
  • 按需付费
  • 将监控,日志,报警等等繁琐的事务隔离开

reference


2019-03-26 function-compute , serverless , gcp , aws

使用 Git worktree 将同一个项目分裂成多个本地目录

在偶然逛 StackOverflow 的时候看到一个提问,能不能在同一个 repo 中同时有两份代码,并且可以保持两份相似但不是完全相同的代码并行开发?虽然对其需求有些好奇和疑惑 ,但也关注了一下下方的回答。

这个时候我知道了 git 原来还有一个命令叫做 git worktree 这是 Git 2.15 版本引入的新概念。我们都知道一个正常的 git workflow 可能就是从 master 拉出新分支 feature 进行功能开发,如果遇到有紧急 bug,那么从 master 拉出 hotfix 分支紧急修复在合并。这是一个比较常规的工作流,那么 git worktree 为何要被引用进来。从官方的文档 1 上能看到 git worktree 的作用是将多个 working trees 附加到同一个 repository 中,允许用户一次 check out 多个分支。但是问题是为了解决相同的问题,为何要引入一个更加复杂的 git worktree ?

疑惑

于是我又去找了一些材料 2,这个回答解决了我部分疑惑,他说到在大型软件开发过程中可能经常需要维护一个古老的分支,比如三年前的分支,当然 git 允许你每个分支维护一个版本,但是切换 branch 的成本太高,尤其是当代码变动很大的时候,有可能改变了项目结构,甚至可能变更了 build system,如果切换 branch,IDE 可能需要花费大量的时间来重新索引和设置。

但是通过 worktree, 可以避免频繁的切换分支,将老的分支 checkout 到单独的文件夹中作为 worktree,每一个分支都可以有一个独立的 IDE 工程。当然像过去一样你也可以在磁盘上 clone 这个 repo 很多次,但这意味着很多硬盘空间的浪费,甚至需要在不同的仓库中拉取相同的变更很多次。

回到原来的问题,使用 git worktree 确实能够解决最上面提及的问题。

使用

git worktree 的命令只有几行非常容易记住

git worktree add ../new-dir some-existing-branch
git worktree add [path] [branch]

这行命令将在 new-dir 目录中将 some-existing-branch 中的内容 check out 出来,就像在该目录中 clone 了一份新代码一样。新的文件地址可以在文件系统中的任何位置,但是注意千万不要将目录放到主仓库中。在此之后新目录中的内容就可以和主仓库中的内容一样,新建分支,push 到远端。

当工作结束后可以直接删除该目录,然后运行 git worktree prune.

总结

git worktree 非常适合大型项目又需要维护多个分支,想要避免来回切换的情况,这里总结一些优点:

  • git worktree 可以快速进行并行开发,同一个项目多个分支同时并行演进
  • git worktree 的提交可以在同一个项目中共享
  • git worktree 和单独 clone 项目相比,节省了硬盘空间,又因为 git worktree 使用 hard link 实现,要远远快于 clone
  1. https://git-scm.com/docs/git-worktree 

  2. https://stackoverflow.com/a/31951225/1820217 


2019-03-21 git , git-worktree , scm , version-control

CPU 负载

之前在 Openwrt 负载 中也曾经谈到过 CPU 的负载,通过 top, uptime 等等命令都可以非常快速的查询当前 CPU 的负载。

CPU 的 load average(平均负载)指的是一段时间内正在使用和等待使用 CPU 的平均任务数

还有一个判断 CPU 的指标是 CPU 的利用率。同样使用 top 命令也能够查到。但是并不意味着负载高就一定 CPU 利用率高。

用电话亭来表示 CPU ,把等待打电话的人比作 CPU 任务的话,假设一个队列的人排队打电话,每个人只能打 1 分钟电话,时间到了必须重新排队,那么随着时间变化排队的人数会发生变化,那么 CPU 的平均负载就是每隔 1 分钟,5 分钟,15 分钟采样一次的数值。

而 CPU 的利用率就是电话在拨打的时间长度,但是负载高并不意味着利用率高,可能有人排队等到能打电话时拿着话筒等待了几十秒才拨打电话,那么这浪费的几十秒就不能算是 CPU 的利用率。

问题分析

负载高 CPU 利用率低

说明等待运行的任务很多,很有可能有任务僵死,通过 ps –axjf 查看有没有任务处于 D 状态,该状态为不可中断的睡眠状态,处于 D 状态的进程通常是在等待 IO,通常是 IO 密集型任务,如果大量请求都集中于相同的 IO 设备,超出设备的响应能力,会造成任务在运行队列里堆积等待,也就是 D 状态的进程堆积,那么此时 Load Average 就会飙高。

负载低 CPU 利用率高

说明任务少,但是任务执行时间长,有可能是程序本身有问题,如果没有问题那么计算完成后则利用率会下降。这种场景,通常是计算密集型任务,即大量生成耗时短的计算任务。

CPU 使用率低,IO 繁忙,负载低

这种场景,通常是低频大文件读写,由于请求数量不大,所以任务都处于 R 状态(表示正在运行,或者处于运行队列,可以被调度运行),负载数值反映了当前运行的任务数,不会飙升,IO 设备处于满负荷工作状态,导致系统响应能力降低。


2019-03-20 cpu , load , linux , java

Jenkins 使用

这篇文章主要记录一下 Jenkins Pipeline Syntax 的使用。

Pipeline

Jenkins Pipeline 是什么,简单的来说就是一组定义好的任务,相互连接在一起串行或者并行的来执行,比如非常常见的 build,test,deploy 这样需要重复频繁进行的工作。

更加具体地来说就是 Jenkins 定义了一组非常强大的扩展插件用来支持 CI/CD ,用户可以扩展这些内容来实现自己的内容。这么定义呢?那就是本文的重点,Jenkins 允许用户用一种近似伪代码的形式来编写自己的自定义任务,这个特殊的语法叫做 Pipeline DSL(Domain-Specific Language 特定领域语言)。这一套语法借鉴了 Groovy 的语法特点,有一些些略微的差别。

Jenkins Pipeline 的定义会以文本形式写到 Jenkinsfile 文件中。

举例说明:

pipeline {
  agent any ①
  stages {
      stage('Build') { ②
          steps { ③
              sh 'make' ④
          }
      }
      stage('Test'){
          steps {
              sh 'make check'
              junit 'reports/**/*.xml' ⑤
          }
      }
      stage('Deploy') {
          steps {
              sh 'make publish'
          }
      }
  }
}

说明:

  1. agent 表示 Jenkins 需要分配一个 executor 和 workspace 给该 pipeline
  2. stage 表示 Pipeline 的 stage
  3. steps 表示 stage 中需要进行的步骤 单一任务,定义具体让 Jenkins 实现的内容。比如执行一段 shell 脚本
  4. sh 执行给定的 shell 命令
  5. junit 是由 plugin:junit[JUnit plugin] 提供的聚合测试

Pipeline 定义的脚本使用 Groovy 书写,基本的 Pipeline 可以通过如下方式创建:

  • 在 Jenkins web UI 中直接填写脚本
  • 项目根目录创建 Jenkinsfile 文件,并提交到项目版本控制

Jenkinsfile 的使用有如下优势:

  • 允许用户通过一个文件来定义所有分支,所有 pull requests 的自动化任务
  • 可以 review Pipeline 的代码并进行审计
  • 通过文件进行管理可以便捷的进行多人协作

Pipeline 语法

Jenkins Pipeline 其实有两种语法

  • Declarative
  • Scripted

Declarative Pipeline, 提供了一种比较易读的方式,这种语法包含了预先定义好的层级结构,用户可以在此基础上进行扩展。但是这种模式也有一定的限制,比如所有声明式管道都必须包含在 pipeline 块中。

Scripted Pipeline 会在 Jenkins master 节点中借助一个轻量的执行器来运行。它使用极少的资源来将定义好的 Pipeline 转换成原子的命令。

Declarative 和 Scripted 方式都很大的差别,需要注意。

post 语法块

post section 定义了 Pipeline 执行结束后要进行的操作。支持在里面定义很多 Conditions 块:always, changed, failure, success 和 unstable。这些条件块会根据不同的返回结果来执行不同的逻辑。比如常用的 failure 之后进行通知。

  • always:不管返回什么状态都会执行,可以在其中定义一些清理环境等等操作
  • changed:如果当前管道返回值和上一次已经完成的管道返回值不同时候执行,比如说从失败恢复成功状态
  • failure:当前管道返回状态值为”failed”时候执行,在 Web UI 界面上面是红色的标志
  • success:当前管道返回状态值为”success”时候执行,在 Web UI 界面上面是绿色的标志
  • unstable:当前管道返回状态值为”unstable”时候执行,通常因为测试失败,代码不合法引起的。在 Web UI 界面上面是黄色的标志
  • aborted: 当 Pipeline 中止时运行,通常是被手动中止

post 指令可以和 agent 同级,也可以和放在 stage 中。

// Declarative //
pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
    post {
        always {
            echo 'I will always say Hello again!'
        }
    }
}

Node 块

Jenkins 执行的机器被称作 node,主节点是 master,其他节点 slave。在 Pipeline 文件中可以指定当前任务运行在哪一个节点中。

stages 块

由一个或者多个 stage 指令组成,stages 块是核心逻辑。对主要部分 Build,Test,Deploy 单独定义 stage 指令。

一个 stage 下至少需要一个 steps,一般也就定义一个就足够了。

step 块

在 steps 中定义 step。

Jenkins 中其他指令

agent

指定整个 pipeline 或某个特定的 stage 的执行环境

  • any - 任意一个可用的 agent,那么定义的任务会跑在任意一个可用的 agent 上
  • none - 如果放在 pipeline 顶层,那么每一个 stage 都需要定义自己的 agent 指令
  • label - 在 jenkins 环境中指定标签的 agent 上面执行,比如 agent { label ‘my-defined-label’ }
  • node - agent { node { label ‘labelName’ } } 和 label 一样,但是可用定义更多可选项
  • docker - 指定在 docker 容器中运行
  • dockerfile - 使用源码根目录下面的 Dockerfile 构建容器来运行

parameters

参数指令,触发这个管道需要用户指定的参数,然后在 step 中通过 params 对象访问这些参数。

pipeline {
    agent any
    parameters {
        string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
    }
    stages {
        stage('Example') {
            steps {
                echo "Hello ${params.PERSON}"
            }
        }
    }
}

triggers

触发器指令定义了这个管道何时该执行,一般我们会将管道和 GitHub、GitLab、BitBucket 关联, 然后使用它们的 webhooks 来触发,就不需要这个指令了。如果不适用 webhooks,就可以定义两种 cron 和 pollSCM

  • cron - linux 的 cron 格式 triggers { cron('H 4/* 0 0 1-5') }
  • pollSCM - jenkins 的 poll scm 语法,比如 triggers { pollSCM('H 4/* 0 0 1-5') }

    pipeline { agent any triggers { cron(‘H 4/* 0 0 1-5’) } stages { stage(‘Example’) { steps { echo ‘Hello World’ } } } }

stage

stage 指令定义在 stages 块中,里面必须至少包含一个 steps 指令,一个可选的 agent 指令,以及其他 stage 相关指令。

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

tools

定义自动安装并自动放入 PATH 里面的工具集合,工具名称必须预先在 Jenkins 中配置好了 → Global Tool Configuration.

pipeline {
    agent any
    tools {
        maven 'apache-maven-3.0.1' ①
    }
    stages {
        stage('Example') {
            steps {
                sh 'mvn --version'
            }
        }
    }
}

内置条件

  • branch - 分支匹配才执行 when { branch 'master' }
  • environment - 环境变量匹配才执行 when { environment name: ‘DEPLOY_TO’, value: ‘production’ }
  • expression - groovy 表达式为真才执行 expression { return params.DEBUG_BUILD } }

Pipeline global variables

地址:

  • http://jenkins.url/pipeline-syntax/globals
  • http://jenkins.url/env-vars.html

reference


2019-03-13 jenkins , ci-cd , program

SD 卡种类和标示

如果注意观察 SD 卡面上的内容就会发现上面有很多标签,除开 SD 的品牌,可能还会看见,micro,I, U,等等标识,这些标识都不是厂家随意标注的,每一个都有其特殊的含义。了解这些特殊的标示之后对 SD 卡的选购也有一定的便捷。

microSD vs SD 卡

microSD 卡和 SD 卡的区别其实不用太多交代,基本上从大小就能看出区别。因为体积的区别,所以 microSD 卡经常用于便携,小型设备,比如手机,行车记录仪,运动相机等设备中,而大的 SD 卡则会用于单反等设备。

SD vs SDHC vs SDXC

  • SD 卡,Secure Digital Memory Card,安全数字存储卡,SD 1.0 标准,由日本 Panasonic、TOSHIBA 及美国 SanDisk 公司于 1999 年 8 月共同开发研制
  • SDHC,Secure Digital High Capacity,高容量 SD 存储卡,SD 2.0 标准,2006 年 5 月 SD 协会发布了 SD 2.0 的系统规范,并在其中规定 SDHC 是符合该规范,SDHC 存储卡容量为:4GB–32GB
  • SDXC,SD eXtended Capacity,容量扩大化的安全存储卡,新一代 SD 存储卡标准,SD 3.0 标准,旨在大幅提高内存卡界面速度及存储容量,SDXC 存储卡的目前最大容量可达 512GB,理论上最高容量能达到 2TB
SD 卡标示 SD SDHC SDXC
容量 上限 2G 2GB - 32 GB 32G - 2T
格式 FAT 12, 16 FAT 32 exFAT

可以看到其实我们平时所说的 SD 卡一直在发展,不管是容量还是速度。

速度等级

在一些稍微老一些的卡上还可能看到 class 2, class 4, class 6, class 10 这样的标识,新一代的 SD 卡会以一个圆圈中间写一个 10 数字来标识 class 10,这个标识表示的是 SD 卡的速度等级。在 2006 年制定 SD 2.0 标准的时候引入了 Class2、Class4、Class6、Class10 级别。

这个 Class 等级,基本上可以理解为 class 10 是 10M/s 的写入速度。

但其实这个级别现在来看又已经过时了,所以现在在 2019 年又会看到 UHS-I,UHS-II,这样的标识,在卡面上会简写成 I,或者 II 这样,这就是 UHS 速度等级。也经常会在旁边看到英文字幕 U 中间写 1 或者 3 这样的标识,这代表着两种不同的速度等级,U1 = 10M/s, U3 = 30M/s.

UHS 速度等级 1 和 3 则是被设计用于 UHS 总线界面,“U1”和“U3”代表的是 UHS 接口规范下的写入速度标准,U1 表示 UHS Class 1,最低写入速度 10MB/s,U3 表示 UHS Class 3,最低写入速度 30MB/s。为了区分 Speed Class 的 Class 2,UHS Class 并没有设置 U2 等级,目前仅有 U1 和 U3,下一个等级也将是 U5。

另外还有一个视频速度等级,会标识成 V6,V10,V30,V60,V90 这样。V 后面的数字和简单的理解成 SD 卡的写入速度,比如 V90 ,就可以理解成写入速度 90M/s .

最低写入速度 Class 写入速度等級标示 UHS Speed Class 速度 Video Speed Class 建议使用环境
2 MB/s C2     720p
4 MB/s C4     高画质拍摄
6 MB/s C6   V6 高画质拍摄
10 MB/s C10 U1 V10 1080p Full HD
30 MB/s   U3 V30 4K Video 60/120 fps
60 MB/s     V60 8K Video 60/120 fps
90 MB/s     V90 8K Video 60/120 fps

sdcard speed


2019-03-10 sdcard , sd , tf

jks pem cer pfx 不同种类的证书

通常在安全级别较高的场景经常需要对通信信息进行加密传输,有一种情况就是非对称加密,将信息使用对方提供的公钥加密传输,然后对方接收到之后使用私钥解密。今天在对接时对方发送了一个压缩包,其中包含了 SSL 不同类型的证书,包括了 jks, pem, cer, pfx 等等文件,现在就来了解一下。

jks

jks 全称 Java KeyStore ,是 Java 的 keytools 证书工具支持的证书私钥格式。jks 包含了公钥和私钥,可以通过 keytool 工具来将公钥和私钥导出。因为包含了私钥,所以 jks 文件通常通过一个密码来加以保护。一般用于 Java 或者 Tomcat 服务器。

keytool -exportcert -rfc -alias mycert -file mycert.cer -keystore mykeys.jks -storepass passw0rd

pfx

pfx 全称是 Predecessor of PKCS#12, 是微软支持的私钥格式,二进制格式,同时包含证书和私钥,一般有密码保护。一般用于 Windows IIS 服务器。

openssl pkcs12 -in xxx.pfx

转为 pem

openssl pkcs12 -in for-iis.pfx -out for-iis.pem -nodes

cer

cer 是证书的公钥,一般都是二进制文件,不保存私钥。

der

二进制格式,Java 和 Windows 服务器偏向使用

openssl x509 -in certificate.der -inform der -text -noout

pem

pem 全称是 Privacy Enhanced Mail,格式一般为文本格式,以 -----BEGIN 开头,以 -----END 结尾,中间内容是 BASE64 编码,可保存公钥,也可以保存私钥。有时候会将 pem 格式的私钥改后缀为 .key 以示区别。

这种格式的证书常用于 Apache 和 Nginx 服务器,所以我们在配置 Nginx SSL 的时候就会发现这种格式的证书文件。


2019-03-02 ssl , jks , pem , cer , pfx , certificate

Spring 中的 @Transactional 注解

Spring 中有两种不同方式实现事务 —- annotations 和 AOP。

配置事务

在 Spring 3.1 及以后可以使用 @EnableTransactionManagement 注解 1

3.1 之前可以使用 XML 配置,注意几个 tx 的命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
                    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="false"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

之后在类实现方法中添加 @Transactional 注解即可。

@Transactional(propagation=Propagation.NOT_SUPPORTED)

@Transactional 有如下的属性

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
  String value() default "";
  Propagation propagation() default Propagation.REQUIRED;
  Isolation isolation() default Isolation.DEFAULT;
  int timeout() default -1;
  boolean readOnly() default false;
  Class<? extends Throwable>[] rollbackFor() default {};
  String[] rollbackForClassName() default {};
  Class<? extends Throwable>[] noRollbackFor() default {};
  String[] noRollbackForClassName() default {};
}

传播性

Propagation 支持 7 种不同的传播机制:

  • REQUIRED:如果存在事务,则加入当前事务;如果没有事务则开启一个新的事务。
  • SUPPORTS: 如果存在一个事务,加入当前事务;如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS 与不使用事务有少许不同。
  • NOT_SUPPORTED:总是非事务地执行,并挂起任何存在的事务。
  • REQUIRES_NEW:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
  • MANDATORY:如果已经存在一个事务,支持当前事务;如果没有一个活动的事务,则抛出异常。
  • NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常
  • NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按 REQUIRED 属性执行。

隔离性

Isolation

  • DEFAULT 默认
  • READ_UNCOMMITTED: 未授权读取级别 以操作同一行数据为前提,读事务允许其他读事务和写事务,未提交的写事务禁止其他写事务(但允许其他读事务)。此隔离级别可以防止更新丢失,但不能防止脏读 y(一个事务读取到另一个事务未提交的数据)、不可重复读(同一事务中多次读取数据不同)、幻读(一个事务读取到另一个事务已提交的 insert 数据)。此隔离级别可以通过“排他写锁”实现
  • READ_COMMITTED: 授权读取级别 以操作同一行数据为前提,读事务允许其他读事务和写事务,未提交的写事务禁止其他读事务和写事务。此隔离级别可以防止更新丢失、脏读,但不能防止不可重复读、幻读。此隔离级别可以通过“瞬间共享读锁”和“排他写锁”实现
  • REPEATABLE_READ: 可重复读取级别 以操作同一行数据为前提,读事务禁止其他写事务(但允许其他读事务),未提交的写事务禁止其他读事务和写事务。此隔离级别可以防止更新丢失、脏读、不可重复读,但不能防止幻读。此隔离级别可以通过“共享读锁”和“排他写锁”实现
  • SERIALIZABLE: 序列化级别 提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。此隔离级别可以防止更新丢失、脏读、不可重复读、幻读。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到

超时

默认是 30 秒

@Transactional(timeout=30)

只读性

readOnly true of false

多次查询保证结果一致性

回滚异常类

rollbackFor

一组异常类,遇到时 确保 进行回滚。默认情况下 checked exceptions 不进行回滚,仅 unchecked exceptions(即 RuntimeException 的子类)才进行事务回滚

回滚异常类名

rollbackForClassname 一组异常类名,遇到时 确保 进行回滚

不回滚异常类

noRollbackFor 一组异常类,遇到时确保 不 回滚。

不回滚异常类名

noRollbackForClassname 一组异常类,遇到时确保不回滚

实现

默认情况下,数据库按照单独一条语句单独一个事务方式,自动提交模式,每条语句执行完毕,如果执行成功则隐式提交事务,如果失败则隐式回滚。

开启事务管理之后,Spring 会在 org/springframework/jdbc/datasource/DataSourceTransactionManager.java 中将底层自动提交特性关闭。

Spring 事务管理回滚的推荐做法是在当前事务的上下文抛出异常,Spring 事务管理会捕捉任何未处理的异常,然后根据规则决定是否回滚事务。

默认配置下,Spring 只有在抛出异常为运行时 unchecked 异常时才回滚事务,也就是抛出的异常为 RuntimeException 子类(Error 也会)导致回滚,而 checked 异常则不会导致事务回滚。

Spring 在注解了 @Transactional 的类或者方法上创建了一层代理,这一层代理在运行时是不可见的, 这层代理使得 Spring 能够在方法执行之前或者之后增加额外的行为。调用事务时,首先调用的是 AOP 代理对象,而不是目标对象,事务切面通过 TransactionInterceptor 增强事务,在进入目标方法前打开事务,退出目标方法时提交 / 回滚事务。

使用注意

注意事项:

  • @Transactional 注解可以被用于接口定义、接口方法、类定义和类的 public 方法上
  • @Transactional 注解只能应用到 public 可见度的方法上
  • 建议在具体类实现(或方法中)使用 @Transactional 注解,不要在类所实现的接口中使用
  • 单纯 @Transactional 注解不能开启事务行为,必须在配置文件中使用配置元素,才真正开启事务行为
  • @Transactional 的事务开启,或者是基于接口的,或者是基于类的代理被创建

其他注意事项,比如通过 this 调用事务方法时将不会有事务效果,比如

@Service
public class TargetServiceImpl implements TargetService
{
    public void a()
    {
        this.b();
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void b()
    {
    // 执行数据库操作
    }
}

比如此时调用 this.b() 将不会执行 b 的事务切面。

reference

  1. https://www.baeldung.com/transaction-configuration-with-jpa-and-spring 


2019-03-01 spring , spring-mvc , spring-boot

Maven 插件学习之: shade 插件

maven shade plugin 插件允许把工程使用到的依赖打包到一个 uber-jar(单一 jar 包) 中并隐藏(重命名)起来。

Shade Plugin 绑定到 package 生命周期。

使用

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <configuration>
          <!-- put your configurations here -->
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  ...
</project>

实例

该插件允许我们选择最终打的包中包含或者去除那些包,具体可以参考官网

该插件也允许我们将一些类重定位到其他地方(Relocating Classes),如果 uber JAR 被其他项目所以来,直接使用 uber JAR artifact 依赖中的类可能导致和其他相同类的冲突。解决这种问题的方法之一,就是将类重新移动到新位置。官网

默认情况下,到执行 installed/deployed 时默认会生成两个 jar 包,一个以 -shaded 结尾,这个名字是可以配置的。

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <shadedArtifactAttached>true</shadedArtifactAttached>
              <shadedClassifierName>customName</shadedClassifierName> <!-- Any name that makes sense -->
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

创建可执行 jar 包,可以将入口添加进来。 官网

reference


2019-02-27 maven , maven-plugin , build , java

电子书

最近文章

  • 读书是否是唯一重要的事? 不久之前和朋友约去了趟植物园,聊起读书是否是第一要务的时候产生了一些分歧,关于是否要去学习如何学习这一件事情产生了一些分歧。我站在的立场是读书是必须的,而我朋友则认为在有限的时间里面实践要优先于读书。而关于要不要学习如何学习这一件事情,他仍然坚持自己的实践而非去了解如何学习。
  • Android 上的 RIME 输入法 trime 同文输入法使用 早之前就已经在 Linux 和 macOS 上配置了 RIME 并且一直使用到现在,但是在主力的 Android 上从最早的触宝输入法,后来切换成 Gboard,日常使用倒是没什么大问题,就是有一些词总是需要翻页才能找到,这让我非常不爽,就想手机上能不能用 RIME,于是就有了这篇水文。
  • Obsidian 使用篇一:使用 markdown-clipper 全文保存网页 之前使用整理 Evernote 代替品 的时候就提出了我自己的一个需求,就是有一个完善的 Web Clip 系统,Evernote 和 WizNote 都做的比较不错。但 Obsidian 并没有提供类似的工具,不过幸好 Obsidian 使用 Markdown 来管理文档,这样的开放程度使得我可以寻找一个将网页变为 Markdown 的浏览器扩展就能做到。
  • 使用了半年 macOS 之后 我又回到了 Linux 的怀抱 我在使用了半年 macOS 之后,又回到了 Linux 的怀抱,虽然 macOS 有其自身的优势,我也不否认 macOS 系统上软件生态的友好,但我发现即使我将日常开发主力机器装回到 Linux,也没有丧失操作系统的便捷性和易用性。这或许和我下意识的只使用跨平台的软件有关,并且最长使用的软件几乎都是一套快捷键。
  • 重置 macOS S.M.C 和 NVRAM 今天用得好好的电脑突然三次黑屏,两次发生在早上刚刚使用的时候,一次发生在晚上回家之后。所以一怒之下就直接上官网联系了 Apple Support,但是也不知道是不是我直接登录的 .com 网站,在我提交了 Support 之后一分钟一个外国小哥打了电话过来,我一下子没反应过来,只能用着不那么熟练的英语开始了 macOS 修复之路。