☻Blog("Laziji")

System.out.print("辣子鸡的博客");

0%

DataX是阿里巴巴开源的数据同步工具

github地址https://github.com/alibaba/DataX

最近使用的时候发现一个严重的BUG, 在Window下用DataX写入HDFS时会直接删除目标目录

原因是DataX在数据同步结束时会删除临时文件, 但是HDFS没有正确解析Window下的分隔符, 而DataX又在代码中插入的是系统分隔符而不是用统一的Linux分隔符

解决方法是在HdfsWriter.java中的buildFilePathbuildTmpFilePath方法中统一使用Linux分隔符即可, 配置路径的时候也统一用'/'

这里介绍一种使用service来实现全局变量的方法

创建一个SessionService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class SessionService {

private globalContext = {}

constructor() {}

getGlobal(key: string, defaultData?: {}) {
if (!this.globalContext[key]) {
this.globalContext[key] = defaultData || {};
}
return this.globalContext[key];
}
}

session中有一个全局上下文, 在组件中通过key获取全局域

组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@Component({
templateUrl: "test.component.html"
})
export class TestComponent {

private global = {
user: {},
};

constructor(private session: SessionService) {
this.data.global = this.session.getGlobal("TEST", this.data.global);
}
}

只需要在初始化的使用session.getGlobal(), private global里的内容就是全局的了, 多次载入组件内容保持一致

HTML

1
2
<div id="main"></div>
<script src="/echarts-all.js"></script>

这里需要引人echarts-all.js, 这个文件在2.0版本的官网中下载

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var heatData = [];
for (var i = 0; i < 100; ++i) {
heatData.push([
100 + Math.random() * 600,
150 + Math.random() * 50,
Math.random()
]);
}
echarts.init(document.getElementById('main')).setOption({
series: [{
type: 'heatmap',
data: heatData,
opacity: 0.6
}]
});

这是最简配置, 其中opacity: 0.6决定热力图的透明度, 设置为半透明后再为Div #main设置背景图片即可

CSS

1
2
3
4
5
6
7
#main {
width: 600px;
height: 400px;
background-image: url('/map.jpg');
background-repeat: no-repeat;
background-position: right top;
}

有时候代码中会允许用户提交一小段js代码并返回运行结果, 例如在模版配置中允许用户选择几个内置参数配置一个字符串
就像main/java/{packagePath}/database/service/{customClassName}Service.java 允许用户配置自定义文件路径, 其中packagePathcustomClassName… 是内置对象

要实现这个功能

  • 其中一种方法就是直接字符串替换, 但是有点不严谨, 例如用户正好需要输入字符串"{customClassName}" 那就出现问题了, 就需要再考虑转义的问题
  • 另一种方法就是使用eval(), 但是直接执行用户提交的脚本是有风险的, 这里就可以用到虚拟环境, Node.js自带的vm
1
2
3
4
5
6
7
8
9
10
11
12
const vm = require('vm');

let data = {
customClassName:"TestClass",
packagePath:"com/a/b/c",
className:"MainClass",
//...
};
let format = "main/java/${packagePath}/database/service/${customClassName}Service.java";
vm.createContext(data);
let value = vm.runInContext("`" + format + "`", data);
console.log(value);

使用执行脚本的方式避免了前面转义的问题, 而且极大扩充了自定义性, 用户可以在${customClassName}中调用js原生的字符串操作函数

这里提供了一种虚拟环境的使用场景, 但是即使在虚拟环境下也不建议运行不信任的脚本

在js中耗时方法一般都是异步的, 使用回调方法来返回结果, 对SQLite的操作也是如此

这里使用sqlite3

1
npm install --save sqlite3

安装完后引人

1
const sqlite3 = require('sqlite3');

正常是像这样使用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.db = new sqlite3.Database("app.db", (e) => {
if (e) {
//...
}
});

this.db.get(sql, params, (e, result) => {
if (e) {

} else {
//result
}
})

由于查询结果在回调方法中, 逻辑复杂的时候不得不一层套一层

所以再引人一个包bluebird

1
npm install --save bluebird
1
const Promise = require("bluebird");

使用这个包可以在nodejs中像在js中一样使用Promise

这时候我们就可以对操作数据库的方法进行封装

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
class Database {
async connect() {
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database("app.db", (e) => {
if (e) {
reject(e);
} else {
resolve();
}
});
});
}

async get(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (e, result) => {
if (e) {
reject(e);
} else {
resolve(result);
}
})
});
}

async run(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.run(sql, params, (e) => {
if (e) {
reject(e);
} else {
resolve();
}
})
});
}
}

使用的时候就可以像在Java代码中一样

1
2
3
4
5
6
7
8
class Test{
async test(){
let db = new Database();
await db.connect();
let result = await db.get("select * from user");
console.log(result);
}
}

在用Java写项目的过程中存在很多重复性的工作, 比如数据库层的编写, XMLDaoService 大多是重复的

不止这些, 还有些前端页面也是, 这里分享一个Java编写的代码生成器mybatis-generator 虽然名字叫mybatis 但是不限应用于mybatis中, 任何与数据库表对应的代码都可以

项目地址

https://github.com/GitHub-Laziji/mybatis-generator

欢迎贡献各种模版

使用

目前项目中包含两个模版在resources下, 如果模版不合适可以自己模仿其中的模版进行修改

  • mybatis2 是根据 commons-mybatis 通用Mapper编写的, 依赖commons-mybatis 2.0
  • mybatis-default 这个生成的是简单的mybatis实体类、Dao接口以及XML, 不依赖其他包

配置文件

resources下创建application-${name}.yml文件, ${name}随意, 例如: application-example.yml, 可创建多个

配置文件属性:

  • spring.datasource 填入自己的项目数据库相关配置
  • generator.package 项目包名
  • generator.template.mapping 用于自定义生成文件的包格式以及文件名
  • generator.template.path 表示模版文件的路径目前可以选mybatismybatis-default

generator.template.mapping中可选的动态属性包含:

  • {packagePath} 包文件路径 例如: com/xxx/xxx
  • {className} 类名 由表名使用驼峰命名法得来
  • {customClassName} 自定义类名 (若未指定自定义类名, 则就是类名)
  • {suffix} 类名后缀 DO或VO (根据是否为视图)

一般按以下配置即可, 也可以自行扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
datasource:
url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxxx?characterEncoding=utf-8
username: xxxxxx
password: xxxxxx

generator:
package: com.xxx.xxx
template:
path: mybatis2
mapping: |
Model.java.vm: main/java/{packagePath}/database/model/{customClassName}.java
Query.java.vm: main/java/{packagePath}/database/query/{customClassName}Query.java
Dao.java.vm: main/java/{packagePath}/database/dao/{customClassName}Dao.java
Service.java.vm: main/java/{packagePath}/database/service/{customClassName}Service.java

生成代码

在test文件下创建测试类

  • @ActiveProfiles("example")中填入刚才配置文件名的name
  • tableNames需要生成的表, 可以多个
  • zipPath 代码导出路径

调用generatorService.generateZip传入参数可以是表名数组String[]或者TableItem[]

运行测试方法

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
package pg.laziji.generator;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import pg.laziji.generator.mybatis.GeneratorService;

import javax.annotation.Resource;
import java.io.IOException;

@ActiveProfiles("example")
@RunWith(SpringRunner.class)
@SpringBootTest
public class ExampleTest {

@Resource
private GeneratorService generatorService;

@Test
public void test(){
String zipPath = "/home/code.zip";

// String[] tableNames = new String[]{"table1","table2"};
// generatorService.generateZip(tableNames,zipPath);

TableItem[] tableItems = new TableItem[]{
new TableItem("table1", "TableA"),
new TableItem("table2", "TableB")
};
generatorService.generateZip(tableItems,zipPath);
}
}

xxl-job提供的JobHandler例子中, 执行器都是与web应用整合在一起, 其实执行器完全可以分离出来单独启动, 更容易了解执行器的原理

执行器的核心代码

首先进行JobHandler的注册

1
XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());

随后即可启动执行器, 这里为了简单, 配置写死了, 后面可以引入配置文件更加灵活

1
2
3
4
5
6
7
8
9
XxlJobExecutor xxlJobExecutor = new XxlJobExecutor();
xxlJobExecutor.setAdminAddresses("http://127.0.0.1:8080/xxl-job-admin");
xxlJobExecutor.setAppName("test");
xxlJobExecutor.setIp("");
xxlJobExecutor.setPort(9997);
xxlJobExecutor.setAccessToken("t");
xxlJobExecutor.setLogPath("/data/applogs/xxl-job/jobhandler");
xxlJobExecutor.setLogRetentionDays(-1);
xxlJobExecutor.start();

由于xxlJobExecutor.start()是新开一个线程来开启执行器, 所以若是普通Java应用启动则需加上这个, 防止主进程结束, 导致程序关闭, Web应用则不需要了

1
2
3
while(true){
Thread.sleep(3000);
}

Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.0.1</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>

再配置logback.xml即可

首先启动调度中心也就是xxl-job-admin项目, 再启动项目, 就可以在http://127.0.0.1:8080/xxl-job-admin中看到执行器了

JobHandler

JobHandler只需继承IJobHandlerexecute中实现自己的逻辑即可,@JobHandler(value="demoJobHandler")注解只是为了方便通过扫描自动注册, 如果是像上面那样简单的实现, 其实不需要这个

1
2
3
4
5
6
7
8
public class DemoJobHandler extends IJobHandler {

@Override
public ReturnT<String> execute(String param) throws Exception {
XxlJobLogger.log("Hello World");
return SUCCESS;
}
}

至此, 完整的xxl-job执行器就完成了

监控Topic的情况时需要查看Topic队列的收尾的偏移情况, API中没有提供直接的方法, 不过可以按以下方法间接获取

思路

在Java中查看consumer在Topic的偏移情况可以用consumer.position(topicPartition), 所以可以创建一个consumer, 通过seekToBeginning()seekToEnd()把偏移定位到首尾后再获取当前偏移

例子

1
2
3
4
5
6
7
private void printOffset(KafkaConsumer consumer, TopicPartition topicPartition){
List<TopicPartition> topicPartitions = Collections.singletonList(topicPartition);
consumer.seekToBeginning(topicPartitions);
System.out.println("BeginningOffset: " + consumer.position(topicPartition));
consumer.seekToEnd(topicPartitions);
System.out.println("EndOffset: " + consumer.position(topicPartition));
}

重定向

重定向的分为两种301302, 这两种使用起来感觉差不多, 但是对搜索引擎来说是有区别的

  • 301 表示永久重定向, 搜索引擎收到301响应时会把旧的地址的权值传给新的, 所以当网站更改域名或者网站内部资源url改变时应该使用301重定向
  • 302 表示临时重定向, 顾名思义就是告诉搜索引擎, 这个新地址只是临时用一用

权值分散

当一个网站有多个域名的时候, 据个人经验, 应当把所以域名都重定向到一个主域名上, 例如abc.comwww.abc.comhome.abc.com, 这三个都指向一个网站, 会造成权值的分散

更坏的情况是别人把他的域名定到你的网站上, 让你的网站来养着他的域名, 等时机成熟, 别人把域名一收, 你的网站就突然流失了一大部分流量

所以最好就是在代码中(或者在容器中)对域名进行重定向, 检测请求中的域名, 若不是主域名如www.abc.com就全部301重定向过来

springboot中的实现

完整代码
HostInterceptor.java

编写一个通用的拦截器, 首先检测请求方式, 只对get请求进行重定向, 因为get请求来自浏览器, 会自动对301重定向进行页面跟随。 之后检查域名若不在白名单中则进行重定向

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
public class HostInterceptor implements HandlerInterceptor {

private String redirectHost;
private Integer redirectPort;
private Set<String> hostWhitelistSet = new TreeSet<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) {

if (redirectHost == null) {
return true;
}

if (!"get".equals(request.getMethod().toLowerCase())) {
return true;
}

String host = request.getHeader("host");
if (hostWhitelistSet.contains(host)) {
return true;
}

response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
StringBuilder url = new StringBuilder();
if (request.isSecure()) {
url.append("https://");
} else {
url.append("http://");
}
url.append(redirectHost);
if (redirectPort != null && redirectPort != 80) {
url.append(':').append(redirectPort);
}
url.append(request.getRequestURI());
String queryString = request.getQueryString();
if (queryString != null) {
url.append('?').append(queryString);
}
response.setHeader("location", url.toString());
return false;
}

public void addHostWhitelist(String host) {
if (host == null) {
return;
}
hostWhitelistSet.add(host);
}

public String getRedirectHost() {
return redirectHost;
}

public void setRedirectHost(String redirectHost) {
if (redirectHost == null) {
return;
}
this.redirectHost = redirectHost;
hostWhitelistSet.add(redirectHost);
}

public Integer getRedirectPort() {
return redirectPort;
}

public void setRedirectPort(Integer redirectPort) {
if (redirectPort == null || redirectPort < 1 || redirectPort > 65535) {
return;
}
this.redirectPort = redirectPort;
}
}

使用如下, 在启动类中配置拦截器, 我们可以在白名单中配置主域名(主域名自动加入白名单)和localhost(用于本地开发)

1
2
3
4
HostInterceptor hostInterceptor = new HostInterceptor();
hostInterceptor.setRedirectHost("www.abc.com");
hostInterceptor.addHostWhitelist("localhost");
registry.addInterceptor(hostInterceptor).addPathPatterns("/**");

AlphaZero的设计十分精妙, 模拟人的思维方式, 并且相比上一代的AlphaGo去除了人类棋谱的训练, 不仅更加精简, 而且棋力更上了一个层次
设计主要分为两部分

神经网络(走子价值网络)

神经网络在其中的作用相当于人的棋感, 根据当前局面, 不进行推演, 直接判断哪里是好棋哪里是坏棋

输入

谷歌的AlphaGo Zero采用的19 * 19 * 17的输入
即一个19 * 19代表当前局面的黑棋或白棋的位置, 0代表没有, 1代表有
所以一个完整的局面需要19 * 19 * 2来表示, 输入包含8个历史输入, 因为围棋中存在打劫, 当前可选位置与历史有关, 所以历史局面是必须的
除此之外还有一个参数就是当前局面是哪一方走子, 用0或1表示, 为了方便, 把0或1扩展到19 * 19的平面, 即全为0或1的平面
一共2*8+1, 17个平面作为神经网络的输入

这里以无禁手五子棋为例, 在五子棋中输入可以进行简化, 因为五子棋的下子可以认为只与当前局面有关, 与历史无关
可以采用15 * 15 * 3的输入

输出

大小为361pi, 代表每个位置下子的概率
以及z当前局面的价值, 在[-1,1]之间

中间层

中间层包括一个卷积块, 之后连接一个残差网络, 然后残差网络的输出作为策略头(走子概率), 和价值头(胜率)的输入, 两个头都是一个卷积层跟上一个全连接层
AlphaZero中使用了多层残差网络来获得更强的学习能力, 但是在普通PC上自我对弈时过于缓慢, 可以尝试减少层数, 或者去掉残差网络
可以先实现, 之后再优化的时候适当加入

MCTS(蒙特卡洛树搜索)

MCTS相当于人在下棋过程中的推演过程, 利用MCTS改善每个局面的走子概率, 就像人可以通过推演发现一些在当前局面看起来不是很好的棋
MCTS分为这几个过程

A 选择

从当前局面节点(通过N,P,V计算所有节点的价值)选择价值最大的节点, 最为下一个节点

B 扩展

若当前局面没有搜索过, 使用神经网络预测当前局面的走子概率

C 价值回传

不断进行AB, 直到达到叶子节点, 棋局结束, 把胜负结果回传, 并将节点加入神经网络进行训练