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)。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 程序简单打包

程序在开发工具例如 QtCreator 中运行没有问题,不少同学开发好后就直接把 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 -g gitbook-cli
  5. 下载 GitBook Editor, GitBook 官方提供的编辑器: https://www.gitbook.com/editor

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 已经自带了调用系统摄像头的功能,使用起来很方便,主要是使用下面 3 个类:

  • QCamera
  • QCameraViewfinder
  • QCameraImageCapture

下面代码的效果为

Thymeleaf 语法

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

变量访问

1
2
3
<span th:text="${name}">Thymeleaf 解析后会被覆盖</span>
<span th:text="|Welcome ${name}|">Thymeleaf 解析后会被覆盖</span>
<span th:text="'Welcome ' + ${name}">Thymeleaf 解析后会被覆盖</span>

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

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

使用 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 就完成上面的这些事了。

CentOS 7 简单使用

下载 CentOS

访问 https://www.centos.org/download/ 下载 Minimal ISO 即可,其他的虽然功能齐全,但是太大了。

必要工具

  • yum install zip unzip:

    1
    2
    3
    4
    5
    # 把文件夹 H5 和文件 x.html 压缩成 result.zip
    zip -r result.zip H5 x.html
    # 解压 filename.zip, 如无 -d 则解压到当前目录,有则解压到目录 dest-directory
    unzip filename.zip [-d dest-directory]
  • yum install net-tools (安装后才能使用 ifconfig 等)

  • tar 解压 tar.gz: tar xf filename.tar.gz

实现 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,则可以按照下面几步进行:

  • 文件重命名为 MyWidget.h, MyWidget.cpp, MyWidget.ui
  • 修改 MyWidget.ui 中的 objectName
  • 重构 MyWidget.h 中的类名 Ui::Widget 和 Widget,同时也可修改 #ifndef 的名字
  • 修改 MyWidget.cpp 中的 #include

签名验证

签名验证

签名验证涉及到客户端(比如一个 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 本身的效率差距可以忽略不计了。

测试时,向本机的数据库插入 66720 条记录,向局域网中其他机器上的数据库插入 20000 条记录,每条记录有 7 个字段

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

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

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

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

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

    每个页面都有自己的 router, store

  • 修改 3 个配置文件:

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

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

vue-cli 简单搭建项目框架 后我们知道了怎么使用 vue-cli 创建一个项目,但是页面比较简单,一般后台管理功能的界面会像下面这样子:

下面介绍怎么实现一个这样的界面,UI 的框架使用 Element,命令行进入前面创建的项目目录,安装 Element:

1
npm install element-ui --save

然后在 main.js 中引入 Element 就能使用 Element 了,具体参考 main.js:

1
2
3
4
import Element from 'element-ui';
import 'element-ui/lib/theme-default/index.css';
Vue.use(Element);

任务队列

可以使用 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() 后,这些细节都不需要我们关心了。

Doc docx xls 等转为 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);

允许其他机器访问 MySQL

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

方式一

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

    1
    2
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '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 的记录会多一条