线段拟合曲线

QPainter 提供了绘制线段、矩形、椭圆、圆、圆弧、路径等的函数,如果想绘制正弦 (y=sin(x))、余弦 (y=cos(x)) 的曲线,QPainter 没有提供相应的绘制函数,应该怎么办呢?

李小龙的武术哲学: 以无法为有法,以无限为有限。

数学曲线是连续的,计算机的世界却是离散的,离散的世界使用极限的方式就可以模拟出连续的效果。可以把曲线想象成是一条一条线段连起来形成的图形,这些线段越短,连成的图形就越逼近曲线,这种方法就是线段拟合曲线,学过微积分的同学是不是感觉这个方法很熟悉?

下面以绘制正弦 (y=sin(x)) 曲线为例进行介绍:

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
void FittingCurveWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(10, 150);

// 绘制坐标轴
painter.setPen(QPen(Qt::gray, 1, Qt::DashLine));
painter.drawLine(0, 0, 700, 0);
painter.drawLine(0, -200, 0, 200);
painter.setPen(QPen(Qt::black, 1));

// 计算正弦的坐标点,绘制线段
qreal prex = 0, prey = 0;

// [0, 314] 归一为 [0, PI]
for (int i = 0; i <= 628; ++i) {
qreal x = i;
qreal y = qSin(i/314.0*M_PI) * 100;

painter.drawLine(prex, prey, x, y);

prex = x;
prey = y;
}
}

Nginx 验证 Token

为了提高效率,常把 Nginx 作为静态文件服务器,把视频文件,JS,CSS 等放到 Nginx 上。例如我们要开发一个视频网站,免费视频不需要访问权限验证,收费视频就需要对用户的权限进行验证,验证通过了才能够继续访问,Nginx 可以借助 Lua 来实现访问验证,用户信息使用 token 表示

Nginx 简单的验证代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location ~ /private/.+\.mp4$ {
root html;

access_by_lua '
-- 应用的 ID 和 key,和应用服务器上的一致
local appIdKeys = {["app_1"] = "key_1", ["app_2"] = "key_2"};

local args = ngx.req.get_uri_args();
local appId = args["appId"];
local appKey = appIdKeys[appId];

local token1 = args["token"]; -- 参数中 token
local token2 = ngx.md5(appId .. appKey); -- 用应用的 ID 找到对应的 key,然后根据算法计算 token

-- 如果参数中的 token 和计算得到的 token 不相等,则说明访问非法,禁止访问,否则放行访问
if token1 ~= token2 then
ngx.exit(ngx.HTTP_FORBIDDEN);
end
';
}

Nginx 和应用服务器上同时存储 appId 和 appKey,这样就能根据参数中的 appId 查找到对应的 appKey。至于使用 Lua 的变量存储,或者使用数据库,还是文件,根据具体的情况而定(Nginx 中 Lua 能够访问数据、Redis 等)。

上面的验证规则比较简单,如果其他人得到了 token,就可以无限制的访问了,为了增强安全性,可以使用更多的参数生成 token,例如用户 id,限制 URL 期限的时间戳等。

Nginx 默认没有安装 Lua 模块,需要自己安装,可参考 http://qtdebug.com/mac-nginx-lua

Nginx 安装 Lua 支持

Nginx 支持 Lua 需要安装 lua-nginx-module 模块,一般常用有 2 种方法:

  • 编译 Nginx 的时候带上 lua-nginx-module 模块一起编译

  • 使用 OpenResty: Nginx + 一些模块,默认启用了 Lua 支持(推荐使用此方式)

    OpenResty is just an enhanced version of Nginx by means of addon modules anyway. You can take advantage of all the exisitng goodies in the Nginx world.

    OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

    OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

HTML5 使用 MQTT

HTML5 中也能使用 MQTT:

  1. ActiveMQ 启用 MQTT,可参考 http://qtdebug.com/misc-activemq/

  2. 启动 ActiveMQ: activemq start

  3. 使用 MQTT 的 HTML 如下:

    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
    <html>

    <head>
    <title>Test Ws mqtt.js</title>
    </head>

    <body>
    <script src="./browserMqtt.js"></script>
    <script>
    // 虽然使用的是 MQTT,但底层还是使用 WebSocket 实现的,所以这里的端口需要使用 ActiveMQ 里 WS 的端口 61614,而不是 MQTT 的端口 1883
    var client = mqtt.connect('ws://127.0.0.1:61614'); // you add a ws:// url here
    client.subscribe('foo'); // 订阅 Topic

    client.on('message', function(topic, payload) {
    console.log([payload].join('')); // 提取消息需要使用 [].join()
    })

    client.publish('foo', 'Hello World!'); // 发送消息

    // 不停的发送消息进行测试
    setInterval(function() {
    client.publish('foo', 'Time: ' + new Date().getTime());
    }, 1000);
    </script>
    </body>

    </html>
  4. 写一个 Java 的 MQTT 发布和订阅的程序一起测试,可参考 http://qtdebug.com/misc-mqtt/

下载 browserMqtt.js,也可以自己编译(新版本好像有问题,只能发消息,不能订阅消息),详细文档请参考 https://github.com/mqttjs/MQTT.js

简单的 Mock 工具 RestServerMock

前后端分离,如果前端需要等到服务器端接口开发完成后才能继续的话,效率太低,使用 Mock 工具能够更好的使得前后端分离各自开发,RestServerMock 一个是简单的静态 Web 服务器的 Mock 工具:

A Simple REST HTTP server that serves the configured JSON responses rest-server-mock.

Currently it servers JSON responses with status code 200. Future versions will support more options including : status codes, headers, encodings, etc.

Enum 注入

注入 Enum 可以使用字符串直接注入,或者借助 org.springframework.beans.factory.config.FieldRetrievingFactoryBean,下面以注入 FastJson 的 SerializerFeature 和自定义 enum 为例.

1
2
3
4
5
6
7
8
9
10
package com.alibaba.fastjson.serializer;

public enum SerializerFeature {
QuoteFieldNames,
UseSingleQuotes,
WriteMapNullValue,
WriteEnumUsingToString,
WriteEnumUsingName,
...
}
1
2
3
public enum Color {
RED, GREEN, BLUE
}

Spring Boot 热更新

热更新在开发中对于提高效率是非常重要的,SpringBoot 带了一个 org.springframework.boot:spring-boot-devtools,但是在 SpringBoot + Gradle + IDEA 的配合中不怎么好用,下面介绍另一种热更新的方式:

  • 引入 springloaded(不需要配置其他的):

    1
    2
    3
    4
    5
    6
    7
    8
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'org.springframework:springloaded:1.2.8.RELEASE'
    }
    }
  • 终端进入项目目录,执行 gradle -t classes 启动一个监听任务,当发现项目中的 Java 类发生变化时进行自动编译,模版文件变化时自动复制到 build 对应的目录中

  • 终端进入项目目录,执行 gradle bootRun 启动项目

    修改 Java 文件和模版文件等看看效果

Spring Boot Thymeleaf

SpringBoot 默认使用 Thymeleaf 2,为了使用 Thymeleaf 3,需要引入下面的依赖:

1
2
3
4
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.thymeleaf:thymeleaf:3.0.7.RELEASE')
compile('org.thymeleaf:thymeleaf-spring4:3.0.7.RELEASE')
compile('nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.2.2')

application.properties 中配置:

1
2
3
4
5
spring.thymeleaf.mode=HTML5
spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html

Controller:

1
2
3
4
@GetMapping("/hello")
public String hello() {
return "hello"; // View 的名字为 hello.html
}

hello.html:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8"/>
</head>

<body>
Thymeleaf
</body>

</html>

SpringBoot Redis

SpringBoot 中使用 Redis 非常简单:

  1. 引入依赖

    1
    compile('org.springframework.boot:spring-boot-starter-data-redis')
  2. 在 application.properties 中配置 Redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.password=
    spring.redis.database=0
    spring.redis.pool.max-active=8
    spring.redis.pool.max-wait=-1
    spring.redis.pool.max-idle=8
    spring.redis.pool.min-idle=0
    spring.redis.timeout=0
  3. 使用 StringRedisTemplate 访问 Redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class HelloController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/redis")
    public String redis() {
    return redisTemplate.opsForValue().get("user");
    }
    }

SpringBoot MyBatis

SpringBoot 使用 MyBatis 主要为以下 4 步:

  1. 引入依赖

    1
    2
    compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')
    runtime('mysql:mysql-connector-java')
  2. 配置数据源: 配置 application.properties

    1
    2
    3
    4
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  3. 编写 Mapper: 使用注解 @Mapper 自动生成 Mapper 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.xtuer.mapper;

    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;

    import java.util.Map;

    @Mapper
    public interface UserMapper {
    @Select("SELECT * FROM user WHERE username=#{username}")
    public Map findUserByUsername(String username);
    }
  4. 使用 Mapper: 使用 @Autowired 装配 mapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.xtuer.controller;

    import com.xtuer.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.Map;

    @RestController
    public class HelloController {
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/hello")
    public Map hello(@RequestParam String username) {
    return userMapper.findUserByUsername(username);
    }
    }

Hexo 跳过指定文件的渲染

Hexo 博客中所见文章都是经由渲染的静态网页,而静态网页的样式都直接由 Hexo 的主题控制,所以 Hexo 博客大部分都呈现出一种高度的统一化与规范化。不过 Hexo 提供了跳过渲染功能,使得我们可以直接在博客中放入自定义网页: 在 _config.yml 配置中配置 skip_render:

如果要跳过 source 文件夹下的test.html,可以这样配置:

1
skip_render: test.html

注意,千万不要加上个/写成/test.html,这里只能填相对于 source 文件夹的相对路径

如果要忽略 source 下的 test 文件夹下所有文件,可以这样配置:

1
skip_render: test/*

如果要忽略 source 下的 test 文件夹下.html文件,可以这样配置:

1
skip_render: test/*.html

如果要忽略 source 下的 test 文件夹内所有文件包括子文件夹以及子文件夹内的文件,可以这样配置:

1
skip_render: test/**

如果要忽略多个路径的文件或目录,可以这样配置:

1
2
3
skip_render:
- test.html
- test/*

参考:

九宫格绘图

很多时候都会使用图片作为 widget 的背景,如果图片和 widget 一样大的话那就没什么好说的,背景效果和图片的效果看上去一样,可更多的时候我们会面临图片和 widget 不一样大,如果把图片简单的缩放到和 widget 一样大作为背景的话,背景常常会变形、有锯齿等,如下面的背景图大小为 128 x 108,要作为 300 x 200 大小的背景,直接缩放绘制的效果很不好,如若使用接下来将要介绍的九宫格绘图技术来绘制背景的话,效果正是我们期望的:

  • 左边是直接缩放绘制的效果,背景发虚,有锯齿,圆角被放大
  • 右边是九宫格技术绘制的效果,圆角和背景的圆角一样

Spring Security QQ 登陆

Spring Security 中实现 QQ 登陆,可以在 FORM_LOGIN_FILTER 前插入一个 filter 用于拦截 QQ 登陆成功后的回调,进行身份认证。

开发前需要准备一个 QQ 互联账号和修改 hosts,按照下面的说明操作即可。

要点: Spring Security 中身份认证成功的标志很简单,只要用用户信息创建一个 Authentication 对象,保存到 SecurityContextHolder 就可以了。

Spring Security 发现 SecurityContextHolder 中有 Authentication 后,就认为用户已经通过了身份认证,对访问的资源进行权限验证时调用 Authentication.getAuthorities() 获取用户的权限进行验证。

注册 QQ 互联账号

  1. 在开发前,需要在 QQ 互联 注册一个开发者账号: https://connect.qq.com
  2. 然后点击 应用管理: https://connect.qq.com/manage.html
  3. 创建 网站应用,里面有开发需要的 APP IDAPP Key

修改 hosts

例如我们在 QQ 互联中填写的回调 URL 为 http://open.qtdebug.com:8080/oauth/qq/callback,很显然 QQ 服务器是不能访问这个地址的,因为这个地址不存在,为了在 QQ 登陆成功后 QQ 服务器能访问这个地址,需要在系统的 hosts 文件里添加 127.0.0.1 open.qtdebug.com

还有另一种方式是使用如 Ngrok 把本地映射为外网可访问。

SpringBoot Start

Spring Boot 创建入门级 RESTful Web 项目简单到令人发指,下面就来看看怎么用吧(如果想知道 Spring Boot 是啥,搜索即可):

  1. 创建项目的骨架
  2. 添加 RestController
  3. 启动项目: gradle bootRun
  4. 打包项目: gradle build

创建项目的骨架

  1. 访问 http://start.spring.io
  2. 选择 Gradle: Generate a Gradle Project with Java Spring Boot 1.5.6
  3. 填写 Group(项目的包名,例如 com.xtuer) 和 Artifact(可不填)
  4. Search for dependencies 输入 web
  5. 点击 Generate Project,会自动下载项目骨架的 zip 文件
  6. 解压,如果不需要里面的 gradlew,删除即可

Spring Security JWT + Token 认证

Spring Security Session + Token 认证 中介绍了 Token 相关的身份验证,但是怎么验证 token 和使用 token 获取用户信息没有进行介绍,可以把 token 存储到 Redis、数据库等,下面介绍另一种 token 实现方法 JWT(Json Web Token),这种 token 不需要存储到服务器,自身就能进行验证。

JWT 中存储了 token 的签名,用户信息,还可以存储 token 的签发时间用于服务器验证 token 的有效期,并且这些信息如果被篡改了的话就会导致 token 失效,JWT 的理论请参考 http://www.jianshu.com/p/576dbf44b2ae

为了在 Spring Security 中使用 JWT,需要修改下面 3 个类:

  • TokenAuthenticationFilter
  • TokenService
  • JwtUtils

Spring Security 权限继承

如下面层级结构的权限:

1
2
3
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST

A user who is authenticated with ROLE_ADMIN, will behave as if they have all four roles, as well the user will have the authorities ROLE_USER and ROLE_GUEST who is authenticated with ROLE_USER请参考帮助文档

Spring Security 里通过 RoleHierarchyVoter 实现权限继承(只需要配置,不需要写代码)。

注意:

<http> 需要设置 use-expressions 为 false,禁用 SpEL 表达式进行权限判断,因为它为 true 时 Spring Security 使用 WebExpressionConfigAttribute,它的 getAttribute() 总是返回 null,导致 RoleVoter.supports() 总是返回 false,于是权限校验失败,为 false 时使用的是 SecurityConfig,这时就没问题了,可以在 RoleVoter.vote() 中打断点进行验证。

坑爹的是,Spring Security 的帮助文档里没说这个,走了不少弯路。

微信企业号开发

微信企业号主要是为了链接组织和内部员工的,通过提供通讯录、组织新闻公告、活动、投票、调研、论坛 BBS、意见建议墙、招聘、考勤、流程审批、任务管理等一系列功能和服务,提高组织运作效率。微信企业号是微信为企业客户提供的移动服务,旨在提供企业移动应用入口。

微信企业号,有以下一些特点:

  • 关注更安全

    只有企业通讯录的成员才能关注企业号,分级管理员、保密消息等各种特性确保企业内部信息的安全

  • 应用可配置

    企业可自行在企业号中配置多个服务号,可以连接不同的企业应用系统,只有授权的企业成员才能使用相应的服务号

  • 消息无限制

    发送消息无限制,并提供完善的管理接口及微信原生能力,以适应企业复杂、个性化的应用场景

  • 使用更便捷

    企业号在微信中有统一的消息入口,用户可以更方便地管理企业号消息。微信通讯录也可以直接访问企业号中的应用

Spring Security Session + Token 认证

前面通过表单进行登陆,会为用户创建一个 session 保存在服务器端,session id 保存在 cookie 中,每次访问服务器的时候服务器端从 cookie 中读取 session id 然后找到用户的 session,就能知道当前用户的信息。但是对于移动端来说,传递 cookie 不是很方便,一般都会使用 token 来进行验证。

Token 就是一个字符串(可以使用 uuid),验证时使用的 token 可以理解为和 session id 的功能差不多:

  1. 用户申请 token 时,可以把 token 作为 key,用户信息的对象作为 value 保存到 Redis 中,把 token 返回给移动端
  2. 移动端保存 token,有很多种方式,例如保存到文件中,sqlite 里都可以
  3. 每次访问的时候把 token 放到请求的 header 中
  4. 服务器端从 header 中读取 token,然后用 token 作为 key 去 Redis 中去读用户数据
  5. 如果读取到的用户数据有效,则说明用户是合法的,认证通过,继续访问,否则返回错误,终止请求

使用纯 token 验证,不支持 session,这样的应用一般都是用来提供纯数据服务(应用中没有网页,很多微服务就是这样的),以下叫 DSA(Data Service Application),但是数据也是需要后台功能来管理的,大多都会使用 Web 应用,叫 DMA(Data Management Application),Web 应用需要使用 session,也就是说 DSA 和 DMA 是独立的 2 个应用,不能共存,因为 DSA 中不支持 session,而 DMA 中需要 session。这种设计的好处是 DSA 很轻量级,只关心数据服务,能够降低开发的复杂度,还有其它比如每个服务都很简单,只关注于一个业务功能,每个微服务可以由不同的团队独立开发,微服务是松散耦合的等等。但是也有缺点,比如有可能对资源的访问需要重复实现,例如一个电子图书馆程序,读取图书信息的 API /api/books/{bookId} 在 DSA 中需要实现,在 DMA 中也要提供实现,因为 DMA 中也需要读取图书信息进行管理,就算用分布式服务使用 dubbo 负责服务治理,由 DMA 提供访问数据的逻辑,但是 DSA 和 DMA 里都至少也要各自有个 Controller 来处理这个 URL 吧。

本文的目的,是要实现一个 Web 应用即支持 session,同时又能支持使用 token 进行身份验证时不生成 session:

  • 浏览器访问 /api/books/{bookId} 时,从 cookie 中读取 session id 找到对应的 session,获取当前用户,如果没有登陆则跳转到登陆页面进行登陆,登陆成功会创建 session
  • 移动端访问 /api/books/{bookId} 时,从 header 中读取 token 找到对应的用户,如果没有 token 或者 token 过期、用户信息无效则返回错误提示未登陆认证(token 可以事先请求保存起来),整个过程不会产生 session

Qt 程序简单打包

程序在开发工具例如 Qt Creator 中运行没有问题,不少同学开发好后就直接把 xxx.exe 给用户使用,用户双击 xxx.exe 后提示错误,打开程序失败。很是奇怪: 程序在我的电脑里打开好好的,为什么到其他电脑上就不行了呢,是不是他的电脑有问题?不知道此同学有没有在自己电脑上双击过这个程序!

例如双击下面的 Gui.exe,提示找不到 libgcc_s_dw2-1.dll,那是因为 Qt 的程序运行的时候除了需要可执行程序本身外,还需要依赖一些其他的 dll,需要把这些 dll 一起打包给用户才行:

Qt 程序打包一般有 2 种方式,纯手动打包和半自动打包,下面以 Windows 中打包 Qt 程序 Gui.exe 为例进行介绍,环境如下:

  • 安装 MinGW 的 Qt 5.9.1 到 F
  • DLL 目录: F:\Qt\Qt5.9.1\5.9.1\mingw53_32\bin
  • Qt 的插件目录: F:\Qt\Qt5.9.1\5.9.1\mingw53_32\plugins

GitBook 入门

GitBook 是一个使用 Markdown 文件,用来写书、说明文档等的工具,它的官网已经有 5 万多本使用 GitBook 写的书了,现在不少公司都开始用 GitBook 来写项目文档、使用手册等。下面就简要的介绍怎么使用 GitBook,首先需要安装下面这些软件:

  1. 安装 Git
  2. 安装 Nodejs
  3. 安装 GitBook: npm install gitbook -g
  4. 安装 GitBook-Cli: npm install gitbook-cli -g

本地搭建 GitBook

本地搭建 GitBook 的好处是我们可以自己管理文件的存储,例如可以放到公司的 Git 私服上:

  1. 创建目录例如 Pandora 用于存放书的文件,进入目录

  2. 创建文件 README.md 和 SUMMARY.md,它们是 GitBook 最重要的 2 个文件,README.md 对书进行介绍,在 SUMMARY.md 中描述书的目录结构,其内容可参考如下:

    README.md:

    1
    本书用于介绍 Pandora 项目的使用说明

    SUMMARY.md:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # Summary

    * [简介](README.md)
    * [第一章](chapter1/README.md)
    * [第一节](chapter1/section1.md)
    * [第二节](chapter1/section2.md)
    * [第二章](chapter2/README.md)
    * [第一节](chapter2/section1.md)
    * [第二节](chapter2/section2.md)
    * [结束](end/README.md)
  3. 执行 gitbook init:

    会自动创建 SUMMARY.md 中描述的目录结构对应的文件夹和文件,每次执行这个命令都会创建还没有还不存在的文件和文件夹,但是不会影响已经创建的,所以不用担心多次执行 gitbook init 导致数据丢失

  4. 编辑书的 Markdown 文件,例如 section1.md 等

  5. 安装插件 gitbook install

  6. 执行 gitbook serve,在浏览器里访问 http://localhost:4000 就可以看到上面写的书了,以后常用的也是这个命令,当文件变化后可以在网页中及时看到新的变化

    Bug: Windows 中 gitbook serve 运行后,当文件发生变化时不是自动更新网页而是退出,可以使用下面的脚本来运行 gitbook serve 解决这个问题:

    1
    2
    3
    4
    @Echo off
    :Start
    call gitbook serve
    goto Start

    把上面的文件保存为 gs.bat 放到 GitBook 目录中,运行 gs.bat 就可以了。

HTML5 播放器 Video.js

Video.js 是一个简洁、漂亮的 HTML5 播放器,支持字幕,还可支持 Flash(不支持 HTML5 时自动切换到 Flash),使用很简单,也能自定义插件:

Video.js is a JavaScript and CSS library that makes it easier to work with and build on HTML5 video. This is also known as an HTML5 Video Player. Video.js provides a common controls skin built in HTML/CSS, fixes cross-browser inconsistencies, adds additional features like fullscreen and subtitles, manages the fallback to Flash or other playback technologies when HTML5 video isn’t supported, and also provides a consistent JavaScript API for interacting with the video.

事件的坐标

JS 的事件有几个重要的坐标:

  • (offsetX, offsetY): 事件触发点在事件源元素的坐标系统中的坐标(相对于元素的左上角,左上角坐标为 (0, 0))
  • (pageX, pageY): 事件触发点相对于整个页面左上角的坐标,包括了滚动条隐藏的部分
  • (clientX, clientY): 事件触发点相对于页面可视部分(客户区)左上角的坐标,不包括滚动条隐藏的部分
  • (screenX, screenY): 事件触发点相对于屏幕左上角的坐标

限制同一个账号的登陆用户

有时希望限制同一个账号同时只能有 1 个用户登陆,通常为后一次登录将使前一次登录失效。Spring Security 的 session-management为我们提供了这种限制:

  1. 在 web.xml 中定义监听器 HttpSessionEventPublisher

    1
    2
    3
    <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
  2. 通过 concurrency-control 来限制账号登陆数

    1
    2
    3
    4
    5
    6
    <http auto-config="true">
    ...
    <session-management>
    <concurrency-control max-sessions="1"/>
    </session-management>
    </http>

测试 ThreadLocal

每个 Thread 都有一个 ThreadLocalMap 的对象,存储时以 ThreadLocal 变量为 key,set() 的参数作为 value,这样同一个 ThreadLocal 变量在不同的线程中就可以存储不同的数据。

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
public class ThreadLocalTest {
private static ThreadLocal<String> foo = new ThreadLocal<>();

public static void main(String[] args) throws Exception {
new Thread(() -> {
await(300);
System.out.println(Thread.currentThread().getName() + ": " + foo.get()); // [2] 输出: Thread-1: null
foo.set("1"); // [3]

await(1000);
System.out.println(Thread.currentThread().getName() + ": " + foo.get()); // [5] 输出: Thread-1: 1
}, "Thread-1").start();

new Thread(() -> {
foo.set("2"); // [1]

await(600);
System.out.println(Thread.currentThread().getName() + ": " + foo.get()); //[4] 输出: Thread-2: 2
}, "Thread-2").start();
}

public static void await(long timeout) {
try { Thread.sleep(timeout); } catch (InterruptedException e) {}
}
}

输出:

1
2
3
Thread-1: null
Thread-2: 2
Thread-1: 1

ThreadLocal 的变量一般定义为 private static 的。

截取 Canvas 绘制的图形

使用 canvas 绘图时,很多时候在 canvas 上绘制的图像只占 canvas 的一部分,如果把整个 canvas 的图像发送到服务器就比较浪费空间和带宽,所以保存真正绘制的图像部分是有必要的。

下图的上部分为一个的 canvas,绘制的图像大概只占它的四分之一,我们的目的是把 canvas 中多余的部分去掉,得到真正绘制的图像,如下部分显示:

MySQL 命令行客户端 MyCLI

MyCLI 是一个 MySQL 的命令行客户端,可以实现自动补全(auto-completion)和语法高亮,具体特性如下:

  • 智能补全
  • SQL 语法高亮显示
  • 自动完成输入 SQL关键字以及数据库列表
  • SELECT * FROM <tab> 只显示表名
  • SELECT * FROM users WHERE <tab> 只显示列名
  • 支持 tab 自动补全
  • MySQL 的输出会通过 less 命令进行格式化输出
  • 支持 ssl 连接

Qt 调用摄像头

可以使用 OpenCV 来操作摄像头,不过 Qt5 已经自带了调用系统摄像头的功能,在 .pro 文件中增加下面的模块:

1
QT += multimedia multimediawidgets

主要是使用下面 3 个类,使用起来很方便:

  • QCamera
  • QCameraViewfinder
  • QCameraImageCapture

下面代码的效果为

Thymeleaf 语法

Thymeleaf 使用 HTML 元素的属性获取 model 中的数据,属性的前缀是 th:,例如 th:text, th:src

变量访问

1
2
3
4
5
6
7
<!-- 使用属性的方式访问变量 -->
<span th:text="${name}">Thymeleaf 解析后会被覆盖</span>
<span th:text="|Welcome ${name}|">Thymeleaf 解析后会被覆盖</span>
<span th:text="'Welcome ' + ${name}">Thymeleaf 解析后会被覆盖</span>

<!-- 非属性的方式访问变量 -->
<span>[[${name}]]</span>

字符串拼接时 |...| 的方式更简洁,但是里面不能包含表达式,第三种方式功能强大,可以包含表达式。

变量访问也可以使用级联的方式: ${user.name}

null 处理

表达式 ${foo}? 当 foo 为 null 时返回 false;级联调用时变量访问前先用 ? 进行判断可以减少一个一个的条件判断

1
2
<div th:text="${foo}?'Alt'"></div>
<div th:text="${foo?.bar?.fix}"></div>

使用 URL

1
<a th:href="@{/login}" th:if="${session.user == null}">Login</a>

使用 th:href="@{/uri}" 引入 URL,/ 开头时会在 URI 前面加上项目的 context path

Thymeleaf 集成

有了 Freemarker,Velocity 等模版后,为什么要选择 Thymeleaf?

  • Freemarker 的模版还是有一些非 HTML 的标签在里面,对于前端来说需要学习相关语法
  • Velocity 虽然也很好,但是已经很久不更新了,Spring 5 已经官方宣布不支持 Velocity 了
  • Thymeleaf 的语法就是 HTML 的语法,动态内容部分使用 HTML 的属性来实现,属性部分不会影响 HTML 的设计,前后端可以很好的分离

软件开发流程

第一步: 我们要确定一个可行的设计方案: 第二步: 我们要开始把框架搭好 第三步: 我们开始一个模块一个模块的完成
第四步: 可以拿去给测试们看了(白盒黑盒都有) 第五步: 产品经过测试通过可以拿去给安全组检查了 最后: 我们的产品就可以上线了

I make things.

Gradle Deploy

项目打包后一般可以按照以下几个步骤进行部署:

  1. 选择正确的环境打包: 测试环境、线上环境等
  2. 把 war 包上传到服务器(使用 FTP、scp 等)
  3. 停止 tomcat: <tomcat>/bin/shutdown.sh
  4. 删除服务器上的项目文件: rm -rf <project_path>
  5. 解压 war 包到项目路径下: unzip project.war -d <project_path>
  6. 启动 tomcat: <tomcat>/bin/startup.sh
  7. 删除上传的 war 包
  8. 如果有 N 个服务器,就需要重复 2 到 7 共 N 次

每次部署都要重复这么多步骤,效率不高,而且容易疏忽出错,为了解决这些问题,借助 Gradle 的 deploy 插件,一条命令 gradle deploy 就完成上面的这些事了。

实现 Steps 路径样式

如下图使用多个 步骤 表示一个过程:

这样的组件 Qt 没有提供,需要我们自己实现,可以用下面几种方式实现:

  • 使用 QPainter 绘图:计算每一个步骤的图形(可以使用 QPainterPath)和位置,然后在 QPainterPath 上填充背景和文字

  • QPushButton + QSS Border-Image + 绝对坐标定位:因为 QPushButton 之间有重叠,而不是一个紧挨着一个的排列,所以需要计算每个步骤的坐标进行定位,使用 PS 设计步骤在不同状态时的背景图,需要 6 张图片:

    • 当前步骤:第一个位置的图、中间的图、最后一个位置的图

    • 非当前步骤:第一个位置的图、中间的图、最后一个位置的图

    • 每个图是步骤的完整背景图,例如

      优点:直观

      缺点:需要手动计算坐标

  • QPushButton + QSS Border-Image + QHBoxLayout:使用 Layout 把 QPushButton 一个紧挨着一个的排列,使用 PS 设计步骤在不同状态时的背景图,需要 5 张图片:

    • 当前步骤:当前步骤前一个步骤的图、最后一个位置的图

    • 非当前步骤:第一个位置的图、中间的图、最后一个位置的图

    • 每个图都不是步骤的完整背景图,例如

      优点:能够使用 Layout 进行布局,不需要手动计算坐标

      缺点:不够直观,不过,在步骤之间加一点空隙,估计大家都明白怎么做了:每一个步骤的背景都有一部分在它的前一个步骤上:

Nginx + Tomcat 使用 Https

Nginx 作为前端反向代理或者负载均衡,Tomcat 不需要自己处理 https,https 由 Nginx 处理:

  • 用户首先和 Nginx 建立连接,完成 SSL 握手
  • 而后 Nginx 作为代理以 http 协议将请求转发给 Tomcat 处理
  • Nginx 再把 Tomcat 的输出通过 SSL 加密发回给用户

这中间是透明的,Tomcat 只是在处理 http 请求而已(默认监听 8080 端口)。因此,这种情况下不需要配置 Tomcat 的 SSL,只需要配置 Nginx 的 SSL,Tomcat 和 Nginx 需要配置以下几项:

  • Nginx 中启用 https:

    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
    http {
    include mime.types;
    default_type text/html;
    gzip on;
    gzip_types text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/richtext image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;
    sendfile on;

    # Tomcat 服务器集群
    upstream app_server {
    server 127.0.0.1:8080 weight=4;
    server 127.0.0.1:8081 weight=2;
    server 127.0.0.1:8082 weight=1;
    }

    server {
    listen 443; # https 的默认端口是 443
    charset utf-8;
    server_name www.xtuer.com; # host_name of URL

    # 启用 https
    ssl on;
    ssl_certificate /Users/Biao/Desktop/cert/server.crt;
    ssl_certificate_key /Users/Biao/Desktop/cert/server.key;

    location / {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 把 https 的协议告知 Tomcat,否则 Tomcat 可能认为是 http 的请求
    proxy_set_header X-Forwarded-Proto $scheme;
    # 请求转发给 Tomcat 集群处理
    proxy_pass http://app_server;
    }
    }
    }

    关键是以下几项:

    • ssl on
    • ssl_certificate
    • ssl_certificate_key
    • X-Forwarded-Proto
  • Tomcat 的 server.xml 的 Host 中配置 Valve:

    1
    2
    3
    <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="X-Forwarded-Proto" protocolHeaderHttpsValue="https"/>
    </Host>
    • X-Forwarded-Proto 是为了正确地识别实际用户发出的协议是 http 还是 https。
    • X-Forwarded-For 是为了获得实际用户的 IP。

Vue 自定义组件

Vue 提供了自定义组件的功能,可以定义全局组件,也可以定义局部组件:

  • 全局组件: 使用 Vue.component() 来注册
  • 局部组件: 使用 Vue 对象的 components 属性来注册

下面先介绍全局组件的自定义,然后再简要的介绍局部组件的自定义。

QtCreator 中重构 Widget 的名字

QtCreator 中创建的 Qt Designer Form Class 包含三个文件: .h, .cpp, .ui,例如我们创建了一个 Form Class Widget,则包含下面三个文件:Widget.h, Widget.cpp, Widget.ui,其中的类名为 Widget,如果想要把其重命名为 MyWidget,则可以按照下面几步进行:

  • 文件重命名:
    • Widget.h -> MyWidget.h
    • Widget.cpp -> MyWidget.cpp
    • Widget.ui -> MyWidget.ui
  • 修改 MyWidget.ui 中的 objectName 为 MyWidget
  • 重构 MyWidget.h 中的类名:
    • Ui::MyWidget -> Ui::Widget
    • MyWidget -> Widget
  • 修改 MyWidget.h 中 #ifndef 的名字: WIDGET_H -> MYWIDGET_H
  • 修改 MyWidget.cpp 中的 #include "ui_Widget.h" -> #include "ui_MyWidget.h"

签名验证

签名验证

签名验证涉及到客户端(比如一个 Web 应用)和服务器端,每个客户端在服务器上有一个对应的 app_idapp_key,大致步骤如下:

  1. 客户端使用 app_id + app_key + 其他参数生成签名字符串 sign

  2. 把 app_id、其他参数 和 sign 一起发送给服务器(app_key 不发送)

  3. 服务器接收到请求后,根据参数中的 app_id 查找到对应的 app_key,然后根据签名算法生成签名字符串 sign2

    客户端和服务器端使用同样的签名算法生成签名字符串。

  4. 字符串比较参数中的 sign 和服务器生成的 sign2,如果相等则签名没问题,放行访问,否则签名无效,拒绝访问

自定义随机函数

JavaScript 已经自带了随机数生成函数,为什么我们还需要弄一个随机数的生成工具呢?

例如 Web 的考试系统里,加载试卷后,需要把试卷的题目顺序打乱,如果用 JS 的随机数函数的话,每次打乱的顺序都是不一样的,因为每次生成的随机数序列都不一样。问题出现了,同一个学员刷新试卷后,题目的顺序和上一次的竟然不一样,这不符合实际要求,应该是不同学员的题目顺序不一样,但是同一个学员的题目顺序永远是一样的。这样就不能使用直接使用原生的随机函数了,下面定义一个随机数生成函数,随机数种子是一个字符串,这样就可以用学员的编码来作为随机数种子生成随机数打乱题目的顺序了,因为每次刷新时同一个学员的编码都是一样的,所以生成的随机数序列都相同,就保证了同一个学员的试卷题目顺序一直都是一样的。

Velocity 语法

Velocity 比较接近脚本语言,例如 JS

1
2
3
4
5
6
7
8
9
#if ($foo < 10)
...
#elseif ($foo == 10)
...
#elseif ($foo == 12)
...
#else
...
#end

比较一下 Freemarker

1
2
3
4
5
6
7
8
9
<#if foo < 10> 
...
<#elseif foo == 10>
...
<#elseif foo == 12>
...
<#else>
...
</#if>

使用 Velocity 生成静态页面

Velocity 可以作为 SpringMVC 的 View 使用,也可以用来生成邮件,静态页面等。

Velocity 模版中可以直接调用对象的方法,这点比 Freemarker 好用,if else foreach 等语句也更舒服。

Gradle 依赖

1
2
compile 'org.apache.velocity:velocity:1.7'
compile 'org.apache.velocity:velocity-tools:2.0'

集成 Velocity

JSP 和 Velocity 都用于显示层,但是都有自己的优缺点。

Velocity 比 Freemarker 快,而且语法也更舒服。

Velocity 的优点:
  1. 不能编写 Java 代码,可以实现严格的 MVC 分离,可维护性好
  2. 性能不错,比 JSP 快
  3. 对 JSP 标签支持良好
  4. 内置大量常用函数
  5. 宏定义非常简单(类似 JSP 标签)
  6. 使用表达式语言
  7. 美工和技术的工作分离(例如命名为 .htm 的格式,不需要经过 Server 就能在浏览器里看到效果,JSP 这一点不太方便)
Velocity 的缺点:
  1. 不是官方标准
  2. 用户群体和第三方标签库没有 JSP 多

JS 绘制椭圆

Canvas 还没有提供直接绘制椭圆的功能,下面使用 bezierCurveTo() 来绘制椭圆。

圆也是使用 arc 来绘制的,在新版的 JS 中提供了 ellipse 来绘制椭圆,但是很多浏览器都还不支持

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
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<script src="http://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<style media="screen">
canvas {
border: 1px solid grey;
}
</style>
</head>

<body>
<canvas id="canvas" width="500" height="300">Your browser does not support canvas.</canvas>

<script>
var canvas = $('#canvas').get(0);
var ctx = canvas.getContext('2d');

drawEllipse(ctx, 100, 100, 80, 120);
drawEllipse(ctx, 200, 200, 200, 80);

function drawEllipse(context, centerX, centerY, width, height) {
context.beginPath();
context.moveTo(centerX, centerY - height / 2);

context.bezierCurveTo(
centerX + width / 2, centerY - height / 2,
centerX + width / 2, centerY + height / 2,
centerX, centerY + height / 2
);
context.bezierCurveTo(
centerX - width / 2, centerY + height / 2,
centerX - width / 2, centerY - height / 2,
centerX, centerY - height / 2
);
context.closePath();
context.stroke();
}
</script>
</body>

</html>

Canvas 像素数据处理

Canvas 的 context 调用 getImageData() 获取 canvas 中图片的像素数据,处理好后再调用 putImageData() 设置回 canvas。

1
2
3
4
5
6
7
8
var canvas = $('#canvas').get(0);
canvas.width = 500; // canvas 的实际宽度,默认是 300
canvas.height = 300;
var ctx = canvas.getContext('2d');

var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 获取像素数据
grayscale(imageData); // 处理像素数据
ctx.putImageData(imageData, 0, 0); // 设置回 canvas

测试 Java 生成 UUID 是否重复

本文的目的是为了测试 Java 的 UUID.randomUUID() 生成 UUID 是否重复,使用了 2 种方式:

  1. 多线程加 ConcurrentSkipListSet
  2. 多线程加 MySQL
    1. 多线程生成 UUID
    2. 保存 UUID 到文件
    3. 导入文件中的 UUID 到 MySQL
    4. 使用 GROUP BY 和 HAVING 查找重复的 UUID

结果:尝试了多次,生成 1 万个,10 万,100 万个 UUID 都没有发现重复的情况。

MySQL 基于条件判断的数据插入

在编写程序时,我们经常会遇到一些基于条件判断的逻辑,比如:判断该条数据是否已经在数据库中存在,如果不存在,则插入。

技巧一:使用 ignore 关键字

如果是用主键 primary 或者唯一索引 unique 区分了记录的唯一性,避免重复插入记录可以使用 insert ignore into

当插入数据时,如出现错误时,如重复数据,将不返回错误,只以警告形式返回。所以使用 ignore 请确保语句本身没有问题,否则也会被忽略掉。

JDBC 和 MyBatis 性能比较

以下为 JDBC 和 MyBatis 的性能比较参考,MyBatis 的性能比 JDBC 大概慢三分之一,看上去相差挺大的,不过网络才是对效率影响最大的因素,局域网中有 200 多倍的影响,这么比较起来,MyBatis 和 JDBC 本身的效率差距可以忽略不计了。

测试 2 种情况 (每条记录有 7 个字段)

  • 向本机的数据库插入 66720 条记录
  • 向局域网中其他机器上的数据库插入 20000 条记录

在向局域网中其他机器上不停的插入数据时,2 台机器的 CPU 占用都很小,也就百分之几,因为大多数时候都在等待 IO。

Vue 后台管理简单框架(二)- 多页

Vue 后台管理简单框架(一)中介绍了单页 SPA 的实现,但是实际系统中后台管理的功能很可能是需要多页的,例如要开发一个学习系统,学生和老师的管理功能完全不一样,如果非要把它们放在一起使用 SPA 的方式也可以,左边菜单栏根据角色是老师或则学生来动态显示也是可以的,但是这样会导致管理页的代码很多,功能都放在一起,开发的时候可能不够清晰,增加开发难度,如果把它们分开,使用多页的方式来实现,功能模块就很清晰了,不失为一个好办法。还有例如 PC 的网页和移动设备的网页实现不同,如果放在同一个页面就需要做各种判断来确定对应设备显示的内容也会把很简单的逻辑搞的很复杂,使用不同的页面的话就会很清晰了。

下面就来介绍把 vue-cli 创建的工程改造为支持多页:

  • 不同页面的文件放在不同的文件夹下

    每个页面都有自己的 router.js, index.js, index.vue, index.html

  • 修改 4 个配置文件:

    • webpack.base.conf.js: 修改入口文件 entry
    • webpack.dev.conf.js: 修改 HtmlWebpackPlugin
    • webpack.prod.conf.js: 修改 HtmlWebpackPlugin,删除 CommonsChunkPlugin
    • .eslintrc.js