Content Table

任务队列

可以使用 Java 提供的线程池简单地实现一个任务队列:

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
package com.xtuer.util;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 任务队列,同时执行任务的数量由构造函数的参数 concurrentTaskCount 指定。
*/
public class TaskQueue {
private ExecutorService executor;

/**
* 创建任务队列,concurrentTaskCount 指定同时执行任务的数量。
* 有些情况下任务需要排队一个执行完后再执行另一个,此时 concurrentTaskCount 传入 1。
*
* @param concurrentTaskCount 同时执行任务的数量
*/
public TaskQueue(int concurrentTaskCount) {
executor = Executors.newFixedThreadPool(concurrentTaskCount);
}

/**
* 添加任务,根据不同的业务逻辑定义一个任务类,继承自 Runnable,
* 可以在属性中存储任务相关的数据,在 run() 中实现任务逻辑。
* 当然也可以重载 addTask() 函数实现添加不同的任务。
*
* @param task
*/
public void addTask(Runnable task) {
executor.submit(task);
}

/**
* 下面的实现是为了测试使用
*
* @param n 任务内容
* @param delay 任务消耗的时间,单位为秒,为了测试用的
*/
public void addTask(int n, int delay) {
addTask(() -> {
// 模拟任务执行消耗时间
try {
Thread.sleep(delay * 1000);
} catch (InterruptedException e) {
}

System.out.println(n + " started at " + System.currentTimeMillis() + " and elapsed " + delay * 1000);
});
}

/**
* 销毁任务队列,不再接受新的任务。
* Spring bean 的 destroy-method 函数。
*/
public void destroy() {
executor.shutdown();
}

public static void main(String[] args) throws Exception {
TaskQueue taskQueue = new TaskQueue(1);

taskQueue.addTask(1, 1);
taskQueue.addTask(2, 1);
taskQueue.addTask(3, 1);
taskQueue.addTask(4, 1);
taskQueue.addTask(5, 1);

taskQueue.destroy();
}
}

可以如下使用 Spring bean 来生成任务队列的对象

1
2
3
4
5
6
7
8
9
<!--单任务队列-->
<bean id="singleTaskQueue" class="com.xtuer.util.TaskQueue" destroy-method="destroy">
<constructor-arg value="1"/>
</bean>

<!--多任务队列-->
<bean id="multiTaskQueue" class="com.xtuer.util.TaskQueue" destroy-method="destroy">
<constructor-arg value="222"/>
</bean>

然后在 Controller 中如下使用

1
2
3
4
5
6
7
8
9
10
11
@Resource(name="singleTaskQueue")
private TaskQueue singleTaskQueue;

@GetMapping("/tasks/{taskId}")
@ResponseBody
public Result task(@PathVariable int taskId) {
Random rand = new Random();
singleTaskQueue.addTask(taskId, rand.nextInt(4) + 1); // 任务执行时间为 1 到 4 秒

return Result.ok("" + taskId);
}

完全自己实现的话,任务队列继承 Thread,用一个 list 存储任务,在 run() 函数中用循环查看是否有任务可执行,如果没有则调用 wait() 等待,当调用 addTask() 添加新的任务后调用 notify() 让 while 循环中可获取一个任务执行,获取和添加任务时还要锁住队列等,如果同时允许执行多个任务则还要用一个计数器记录正在执行的任务数,需要处理好各种细节。使用 Executors.newFixedThreadPool() 后,这些细节都不需要我们关心了。

Office 文档转为 PDF 和 HTML

下面介绍使用 JodConverter + LibreOffice 把 Windows Office 的 doc,docx,xls 等文档转换为 PDF 和 HTML:

  • HTML:
    • 优点: 用浏览器打开方便,便于实现 doc 等在线预览
    • 缺点: 相对于 PDF 大不少,图片是独立文件,格式也没有 PDF 的漂亮
  • PDF:
    • 优点: 比 HTML 格式小,格式比较接近于原文档
    • 缺点: 相对于 HTML 在线预览不够方便,也可以借助 pdf.js + HTML5 实现在线预览

HttpServletResponse 下载文件

实现点击按钮下载文件以及点击 a 标签下载文件,注意一下几个问题:

  • 浏览器中点击链接下载文件没啥好说的,但是点击按钮怎么实现下载呢?

    1
    调用 window.open(url) 就可以了
  • 服务器端需要设置响应头表明是以流的形式下载文件

    1
    response.setContentType("application/octet-stream");
  • 文件名有中文时需要处理乱码问题

    1
    2
    String filename = new String(paper.getOriginalName().getBytes("UTF-8"), "ISO8859_1"); // 解决乱码问题
    response.setHeader("Content-Disposition", "attachment;filename=" + filename);

数据库常用基础

创建数据库

1
CREATE DATABASE ebag DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

增加新列

1
ALTER TABLE table_name ADD COLUMN column_name varchar(64)

分页查询

MySQL 中使用 LIMIT 进行分页,第一个参数是起始位置 offset,从 0 开始,第二个参数是要取多少条记录

1
SELECT * FROM question WHERE subject_code='XXX' LIMIT 0, 30

插入更新

查看 UNIQUE 索引或 PRIMARY KEY 对应的行是否存在,存在则更新 (执行 ON DUPLICATE KEY UPDATE 后面的语句),不存在则插入新行

1
2
3
# id 是唯一主键
INSERT INTO question (id, type, content) VALUES (#{id}, #{type}, #{content})
ON DUPLICATE KEY UPDATE content=#{content}

插入时先使用条件查询,满足条件时才插入,不满足条件就不进行插入

1
2
3
4
5
6
7
8
# 根据条件查询,满足条件时才插入
INSERT INTO paper_knowledge_point_relation(paper_id, knowledge_point_id, tenant_code)
SELECT #{paperId}, #{knowledgePointId}, #{tenantCode}
FROM dual
WHERE NOT EXISTS(
SELECT 1 FROM paper_knowledge_point_relation
WHERE paper_id=#{paperId} AND knowledge_point_id=#{knowledgePointId} AND tenant_code=#{tenantCode}
)

查询更新

查询的结果作为临时表,更新知识点下的题目数量

1
2
3
UPDATE question_knowledge_point qkp
JOIN (SELECT knowledge_point_id AS id, COUNT(id) AS count FROM question GROUP BY knowledge_point_id) AS t ON qkp.id=t.id
SET qkp.count=t.count

使用了子查询

左连接

查询所有题目及它的选项

1
2
3
SELECT q.id, q.content, qo.id, qo.content
FROM question q
LEFT JOIN question_option qo ON q.id=qo.question_id

内连接

内连接 JOIN 和 WHERE 等价,查询所有有选项的题目

1
2
3
4
5
6
7
SELECT q.id, q.content, qo.id, qo.content
FROM question q
JOIN question_option qo ON q.id=qo.question_id

SELECT q.id, q.content, qo.id, qo.content
FROM question q, question_option qo
WHERE q.id=qo.question_id

可参考 https://www.cnblogs.com/eflylab/archive/2007/06/25/794278.html

分组

统计有选项的题目的选项个数

1
2
3
4
SELECT q.id, count(1), qo.id, qo.content
FROM question q
JOIN question_option qo ON q.id=qo.question_id
GROUP BY q.id

数据量大时 JOIN 比 LEFT JOIN 快很多

类型转换

使用 CAST 转换类型

1
SELECT CAST(id AS CHAR) AS id FROM question

切换 0 和 1

如果是 0 则设置为 1,否则设置为 0,对于切换 true 和 false 很有用,很像三元运算符

1
UPDATE demo SET is_marked=IF(is_marked=0, 1, 0)

字符串连接

使用 CONCAT(p1, p2, p3) 连接字符串

1
SELECT CONCAT(subject_code, '-', original_id) FROM question WHERE is_marked=1

返回布尔值

JDBC 标准中,0 表示 false,1 表示 true,大于 1 和小于 0 的数没有定义,MySQL 的 JDBC Driver 中 <=0 表示 false,>=1 表示 true,为了保险起见,使用 EXISTS 来查询返回布尔值

1
2
3
SELECT EXISTS (
SELECT 1 FROM paper WHERE paper_id=#{paperId}
)

建表语句

建表语句中需要有足够的注释描述每一列的作用,便于维护

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
#-------------------------------------------
# 表名:question
# 作者:公孙二狗
# 日期:2018-04-01
# 版本:1.0
# 描述:保存题目
#------------------------------------------
DROP TABLE IF EXISTS question;

CREATE TABLE question (
id bigint(20) unsigned NOT NULL COMMENT '题目 ID',
type varchar(8) DEFAULT '' COMMENT '题目类型',
content mediumtext COMMENT '题目内容:题干+选项',
analysis mediumtext COMMENT '题目解析',
answer text COMMENT '题目答案',
demand varchar(32) DEFAULT '' COMMENT '教学要求',
score int(11) DEFAULT 0 COMMENT '题目分值',
difficulty int(11) DEFAULT 0 COMMENT '题目难度',
original_id varchar(64) DEFAULT '' COMMENT '题目在乐教乐学数据库中的 ID',
subject_code varchar(64) DEFAULT '' COMMENT '题目的科目编码',
knowledge_point_code varchar(8) DEFAULT '' COMMENT '题目的知识点编码',
knowledge_point_id bigint(20) DEFAULT 0 COMMENT '题目的知识点 ID',
is_marked tinyint(4) DEFAULT 0 COMMENT '是否被标记过,0 为未标记,1 为已标记',
created_time datetime DEFAULT NULL COMMENT '创建时间',
updated_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB COMMENT '存储题目的表';

复合主键: PRIMARY KEY (code, type),因为用 id 作为主键,所以不推荐使用复合主键,可以使用多列建立唯一约束。

唯一约束

UNIQUE 约束唯一标识数据库表中的每条记录。

UNIQUE 和 PRIMARY KEY 约束均为列或列集合提供了唯一性的保证。

PRIMARY KEY 拥有自动定义的 UNIQUE 约束。

每个表可以有多个 UNIQUE 约束,但是每个表只能有一个 PRIMARY KEY 约束。

  • 唯一约束

    1
    ALTER TABLE table_name ADD CONSTRAINT dict_identifier UNIQUE(code, type)
  • 删除约束

    1
    ALTER TABLE table_name DROP CONSTRAINT dict_identifier

添加索引

  • 唯一索引

    1
    ALTER TABLE table_name ADD UNIQUE (`column`)
  • 普通索引

    1
    ALTER TABLE table_name ADD INDEX index_name (`column`)
  • 多列索引

    1
    ALTER TABLE table_name ADD INDEX index_name (`column1`, `column2`, `column3`)
  • 建表时用 KEY 创建索引

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #-------------------------------------------
    # 表名:dict
    # 作者:二狗
    # 日期:2018-03-07
    # 版本:1.0
    # 描述:保存字典数据
    #------------------------------------------
    CREATE TABLE dict (
    id bigint(20) unsigned NOT NULL COMMENT '字典的 ID',
    code varchar(128) NOT NULL COMMENT '字典的编码',
    value varchar(256) NOT NULL COMMENT '字典的值',
    type varchar(128) NOT NULL COMMENT '字典的类型',
    description text COMMENT '字典的描述',
    PRIMARY KEY (id),
    UNIQUE KEY dict_identifier (code, type) COMMENT 'code + type 唯一标记一个字典数据',
    KEY idx_type (type) COMMENT '类型建立索引'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

删除索引

1
ALTER TABLE table_name DROP INDEX index_name

创建视图

1
2
3
4
5
6
7
8
9
10
11
DROP VIEW IF EXISTS view_paper_knowledge_point;

CREATE VIEW view_paper_knowledge_point
AS SELECT
pkpr.paper_id AS paper_id,
kp.knowledge_point_id AS knowledge_point_id,
kp.name AS name,
kp.tenant_code AS tenant_code
FROM paper_knowledge_point_relation pkpr
LEFT JOIN knowledge_point kp ON pkpr.knowledge_point_id = kp.knowledge_point_id
WHERE kp.is_deleted=0;

允许其他机器访问 MySQL

A 机器上的 MySQL 默认只能 A 机器上的软件访问,即 localhost,如果 B 机器上的软件想访问 A 机器上的 MySQL,需要 MySQL 对 B 机器的 IP 进行授权。

如果是 Homebrew 安装的 MySQL,修改文件 /usr/local/etc/my.cnf 中的 bind-address = 0.0.0.0 即可,然后重启 MySQL,不能使用下面的方式进行修改。

方式一

  • 进如数据库 mysql: use mysql

  • 任意主机以用户 root 和密码 root 连接到 MySQL 服务器

    1
    2
    3
    4
    5
    6
    7
    8
    # MySQL 5.7
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;
    FLUSH PRIVILEGES;

    # MySQL 8.0 创建用户和授权不能使用同一个 SQL 完成
    CREATE user root@'%' IDENTIFIED WITH mysql_native_password BY 'root';
    GRANT ALL ON *.* TO root@'%' WITH GRANT OPTION;
    FLUSH PRIVILEGES;
  • 指定 IP 为(如192.168.10.186)的主机以用户 alice 和密码 Passw0rd 连接到 MySQL 服务器

    1
    2
    GRANT ALL PRIVILEGES ON *.* TO 'alice'@'192.168.10.186' IDENTIFIED BY 'Passw0rd' WITH GRANT OPTION; 
    FLUSH PRIVILEGES;

方式二

网上还看到说直接修改 user 表中 User root 的 Host 为 **%**,最好别这么干,不小心会哭的:

1
2
3
4
USE mysql;
SELECT user, host FROM user;
UPDATE user SET host='%' WHERE user='root';
FLUSH PRIVILEGES;

按照上面的修改 host 为 % 后外网可以访问了,但是本地却访问出错:

1
2
3
mysql -uroot -p 
提示
Access denied for user 'root'@'localhost' (using password: YES) when trying

可用按下面的方式补救:

1
2
3
4
5
6
7
8
9
10
1. 启动 mysqld_safe
mysqld_safe --user=mysql --skip-grant-tables --skip-networking &
2. 登陆修改
mysql -u root mysql
use mysql
UPDATE user SET host='localhost' WHERE user='root';
FLUSH PRIVILEGES;
quit

这时可以看到 user 中关于 root 的记录会多一条

前 16 名 Java 实用工具类

从 GitHub 随机选择的 50,000 个开源 Java 项目中统计出最常用的 16 个 Java 实用工具类类及其最常用的方法,类列表和方法列表都按人气排序。

  1. org.apache.commons.io.IOUtils
  • closeQuietly ( )
  • toString ( )
  • copy ( )
  • toByteArray ( )
  • write ( )
  • toInputStream ( )
  • readLines ( )
  • copyLarge ( )
  • lineIterator ( )
  • readFully ( )

拖拽普通 Element 到 zTree

zTree 不支持 jQuery ui 的拖拽操作,因为 drop 事件阻止了 mouse up 事件的冒泡以致 zTree 不能调用 onMouseUp 的回调函数。为了拖拽普通的 element 到 zTree 上,需要自己实现拖拽功能,Drag With Other DOMs 演示了具体的实现,但是代码太多,不易于理解,这里把拖拽相关的核心代码提取出来,就能快速的理解拖拽的实现。

Vue DOM 更新完成后再执行函数

Vue 的数据变化后会更新 DOM,但只能保证在当前 tick 里面的代码全部执行完毕后更新 (事件队列),不能保证数据一变化后就能用 document.querySelector() 立即获取到最新的 DOM。要保证在 DOM 更新以后执行某一块代码,就必须把这块代码放到下一次事件循环里面,比如 setTimeout(fn, 0),这样 DOM 更新后,就会立即执行这块代码。

有些时候 DOM 更新完成后执行某些操作是有必要的,这时就可以使用 Vue.nextTick() 注册一个函数放到 Vue 的事件队列里,使其在下一个 tick 被执行。

例如使用 Vue + Semantic Ui 创建 Popup,新创建的 Popup 需要执行 popup() 后才会生效,此时在 DOM 更新完成后需要执行一下 popup() 函数。

一般有 3 种方式调用 Vue.nextTick():

  • 普通事件处理函数中,下面的 **[[1]]**,当有多个地方修改同一个变量时,每个地方都需要执行一次
  • 监听指定的数据变化时,下面的 **[[2]]**,粒度细,只与数据是否变化有关,和修改数据的地方无关
  • updated() 回调中,下面的 **[[3]]**,只要 DOM 变化了都会调用,无关的数据变化时都会调用,最省事,但是需要小心测试看看是否有副作用