Zihan's BLOG

Design & Code

前言

密码的存储关乎安全问题,明文存储密码会带来极大的安全隐患。因此,需要存储密码哈希值来保证安全性。

MD5

MD5(Message-Digest Algorithm)算法会产生一个 128bit(16byte)的哈希值,用十六进制表示为长度 32 字符。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String passwordToHash = "password";
String generatedPassword = null;
try {
// Create MessageDigest instance for MD5
MessageDigest md = MessageDigest.getInstance("MD5");
//Add password bytes to digest
md.update(passwordToHash.getBytes());
//Get the hash's bytes
byte[] bytes = md.digest();
//This bytes[] has bytes in decimal format;
//Convert it to hexadecimal format
StringBuilder sb = new StringBuilder();
for(int i=0; i< bytes.length ;i++)
{
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
}
//Get complete hashed password in hex format
generatedPassword = sb.toString();
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}

MD5 的缺陷

  • 容易遭受暴力破解(brute-force)和字典攻击(dictionary attack)
  • 通过彩虹表(rainbow table)可以快速得到密码原文
  • 不能抵御哈希碰撞,不同的密码可能产生相同的哈希值

通过加盐(salt)增加 MD5 的安全性

维基百科对盐(salt)的定义如下

盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为“加盐”。其作用是让加盐后的散列结果和没有加盐的结果不相同,在不同的应用情景中,这个处理可以增加额外的安全性。

盐的产生方式

  • 盐可以是特定字符串
  • 也可以是随机字符串
  • 随机产生的盐需要存储起来用作下次验证
    • 随机产生盐的示例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      private static byte[] getSalt() throws NoSuchAlgorithmException{
      //Always use a SecureRandom generator
      SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
      //Create array for salt
      byte[] salt = new byte[16];
      //Get a random salt
      sr.nextBytes(salt);
      //return salt
      return salt;
      }

MD5 加盐示例

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
private static String getSecurePassword(String passwordToHash, byte[] salt)
{
String generatedPassword = null;
try {
// Create MessageDigest instance for MD5
MessageDigest md = MessageDigest.getInstance("MD5");
//Add password bytes to digest
md.update(salt);
//Get the hash's bytes
byte[] bytes = md.digest(passwordToHash.getBytes());
//This bytes[] has bytes in decimal format;
//Convert it to hexadecimal format
StringBuilder sb = new StringBuilder();
for(int i=0; i< bytes.length ;i++)
{
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
}
//Get complete hashed password in hex format
generatedPassword = sb.toString();
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return generatedPassword;
}

SHA

SHA(Secure Hash Algorithm) 是一个能够产生更强安全性的散列函数算法。SHA不能避免碰撞,但相比MD5碰撞出现的概率要低得多。Java实现了 4 种SHA算法:

  • SHA-1 (160bit)
  • SHA-256 (256bit)
  • SHA-384 (384bit)
  • SHA-512 (512bit)

安全性依此增强。

使用方法

1
2
3
4
MessageDigest md = MessageDigest.getInstance("SHA-1");
MessageDigest md = MessageDigest.getInstance("SHA-256");
MessageDigest md = MessageDigest.getInstance("SHA-384");
MessageDigest md = MessageDigest.getInstance("SHA-512");

其余代码同MD5示例一致。

PBKDF2WithHmacSHA1

// TODO

本文翻译自(未全部翻译): https://howtodoinjava.com/security/how-to-generate-secure-password-hash-md5-sha-pbkdf2-bcrypt-examples/

Java API

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 有五个 build() 方法,每一种都允许你从不同的资源中创建一个 SqlSession 实例。

1
2
3
4
5
SqlSessionFactory build(InputStream inputStream) // 最常用
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

mybatis-config.xml创建SqlSessionFactory示例:

1
2
3
4
String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

SqlSessionFactory

创建SqlSession实例需要考虑:

  • 事务处理
  • 连接
  • 执行语句
1
2
3
4
5
6
7
8
SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType,TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)

默认的openSession()方法没有参数,它会创建有如下特性的SqlSession

  • 会开启一个事务(也就是不自动提交)
  • 将从由当前环境配置的 DataSource 实例中获取 Connection 对象
    事务隔离级别将会使用驱动或数据源的默认设置
  • 预处理语句不会被复用,也不会批量处理更新

SqlSession

事务控制方法

1
2
3
4
void commit()
void commit(boolean force)
void rollback()
void rollback(boolean force)

确保 SqlSession 被关闭

1
2
3
4
5
6
7
try (SqlSession session = sqlSessionFactory.openSession()) {
// following 3 lines pseudocode for "doing some work"
session.insert(...);
session.update(...);
session.delete(...);
session.commit();
}

使用映射器

1
<T> T getMapper(Class<T> type)

映射器注解示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Results(id = "userResult", value = {
@Result(property = "id", column = "uid", id = true),
@Result(property = "firstName", column = "first_name"),
@Result(property = "lastName", column = "last_name")
})
@Select("select * from users where id = #{id}")
User getUserById(Integer id);

@Results(id = "companyResults")
@ConstructorArgs({
@Arg(property = "id", column = "cid", id = true),
@Arg(property = "name", column = "name")
})
@Select("select * from company where id = #{id}")
Company getCompanyById(Integer id);

SqlProvider 注解示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);

class UserSqlBuilder {
public static String buildGetUsersByName(final String name) {
return new SQL(){{
SELECT("*");
FROM("users");
if (name != null) {
WHERE("name like #{value} || '%'");
}
ORDER_BY("id");
}}.toString();
}
}
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
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(
@Param("name") String name, @Param("orderByColumn") String orderByColumn);

class UserSqlBuilder {

// If not use @Param, you should be define same arguments with mapper method
public static String buildGetUsersByName(
final String name, final String orderByColumn) {
return new SQL(){{
SELECT("*");
FROM("users");
WHERE("name like #{name} || '%'");
ORDER_BY(orderByColumn);
}}.toString();
}

// If use @Param, you can define only arguments to be used
public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
return new SQL(){{
SELECT("*");
FROM("users");
WHERE("name like #{name} || '%'");
ORDER_BY(orderByColumn);
}}.toString();
}
}

SQL 语句构建器

MyBatis提供的SQL语句构建有两种写法

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
// Anonymous inner class
public String deletePersonSql() {
return new SQL() {{
DELETE_FROM("PERSON");
WHERE("ID = #{id}");
}}.toString();
}

// Builder / Fluent style
public String insertPersonSql() {
String sql = new SQL()
.INSERT_INTO("PERSON")
.VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
.VALUES("LAST_NAME", "#{lastName}")
.toString();
return sql;
}

// With conditionals (note the final parameters, required for the anonymous inner class to access them)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (id != null) {
WHERE("P.ID like #{id}");
}
if (firstName != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (lastName != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}

public String deletePersonSql() {
return new SQL() {{
DELETE_FROM("PERSON");
WHERE("ID = #{id}");
}}.toString();
}

public String insertPersonSql() {
return new SQL() {{
INSERT_INTO("PERSON");
VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
VALUES("LAST_NAME", "#{lastName}");
}}.toString();
}

public String updatePersonSql() {
return new SQL() {{
UPDATE("PERSON");
SET("FIRST_NAME = #{firstName}");
WHERE("ID = #{id}");
}}.toString();
}
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 String selectPersonSql() {
return new SQL()
.SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
.FROM("PERSON P", "ACCOUNT A")
.INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
.WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
.ORDER_BY("P.ID", "P.FULL_NAME")
.toString();
}

public String insertPersonSql() {
return new SQL()
.INSERT_INTO("PERSON")
.INTO_COLUMNS("ID", "FULL_NAME")
.INTO_VALUES("#{id}", "#{fullName}")
.toString();
}

public String updatePersonSql() {
return new SQL()
.UPDATE("PERSON")
.SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
.WHERE("ID = #{id}")
.toString();
}

if

1
2
3
4
5
6
7
8
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
1
2
3
4
5
6
7
8
9
10
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

choose, when, otherwise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>

trim, where, set

  • select
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
1
2
3
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
  • update
1
2
3
4
5
6
7
8
9
10
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
1
2
3
<trim prefix="SET" suffixOverrides=",">
...
</trim>

foreach

1
2
3
4
5
6
7
8
9
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

bind

1
2
3
4
5
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>

多数据库支持

1
2
3
4
5
6
7
8
9
10
11
<insert id="insert">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
</if>
</selectKey>
insert into users values (#{id}, #{name})
</insert>

插件式脚本语言(Pluggable Scripting Languages)

  • TODO

主要对象的 Scope 和 Lifecycle

对象 Scope Lifecycle 备注
SqlSessionFactoryBuilder 方法 读取完配置,创建完 SqlSessionFactory 销毁 N/A
SqlSessionFactory 应用全局 始终存在 单例实现
SqlSession 单次请求或方法 请求完成或方法执行完成关闭 确保使用完后关闭(close)

MyBatis 配置

properties

  • 使用外部properties配置文件
1
2
3
4
5
6
7
8
9
10
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>

settings

  • MyBatis的基本配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods"
value="equals,clone,hashCode,toString"/>
</settings>

typeAliases

  • 类型别名
1
2
3
4
5
6
7
8
9
10
11
12
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

<typeAliases>
<package name="domain.blog"/>
</typeAliases>
1
2
3
4
@Alias("author")
public class Author {
...
}

typeHandlers

  • TODO

Handling Enums

  • EnumTypeHandler
  • EnumOrdinalTypeHandler

objectFactory

  • 自定义对象工厂

plugins

  • 拦截器

environments

  • 多种环境不同数据库(test, stage, production)
  • 一个SqlSessionFactory实例对应一个数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
  • transactionManager

    • JDBC
    • MANAGED
  • dataSource

    • UNPOOLED
    • POOLED (常用)
    • JNDI
  • 注意点

    • 配置poolPing防止数据库单方面切断连接

databaseIdProvider

  • 多数据库支持
1
2
3
4
5
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>

mappers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Using classpath relative resources -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>

Mapper XML 配置文件

select

  • 示例
1
2
3
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
  • 属性
1
2
3
4
5
6
7
8
9
10
11
12
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">

insert, update 和 delete

  • 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<insert id="insertAuthor">
insert into Author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
update Author set
username = #{username},
password = #{password},
email = #{email},
bio = #{bio}
where id = #{id}
</update>

<delete id="deleteAuthor">
delete from Author where id = #{id}
</delete>
  • 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">

<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">

<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
  • 主键的自动生成
1
2
3
4
5
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>

sql

  • 用于创建可重用的sql语句片段
  • 示例
1
2
3
4
5
6
7
8
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<sql id="sometable">
${prefix}Table
</sql>

<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select>

Parameters

Result Maps

  • constructor
1
2
3
4
5
<constructor>
<idArg column="id" javaType="int" name="id" />
<arg column="age" javaType="_int" name="age" />
<arg column="username" javaType="String" name="username" />
</constructor>

association (“has-one” type relationship)

1
2
3
4
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
  • Nested Select for Association
1
2
3
4
5
6
7
8
9
10
11
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
  • Nested Results for Association (外连接)
1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" resultMap="authorResult" />
</resultMap>

<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</association>
</resultMap>

collection

1
2
3
4
5
<collection property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
  • Nested Select for Collection
1
2
3
4
5
6
7
8
9
10
11
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
  • Nested Results for Collection
1
2
3
4
5
6
7
8
9
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
</resultMap>

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</resultMap>

discriminator

1
2
3
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>

cache

  • 开启二级缓存
1
<cache/>
  • 二级缓存属性配置
1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
  • cache-ref
1
<cache-ref namespace="com.someone.application.data.SomeMapper"/>

前言

Let's Encrypt提供免费的SSL证书,官方现已支持泛域名证书的签发。

Let's Encrypt官方提供certbot工具用于证书的签发,本文介绍的是另一个工具acme.sh

安装 acme.sh

1
curl https://get.acme.sh | sh

设置 DNS API

这里以Cloudflare为例,其他DNS服务商与之类似

1
2
export CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
export CF_Email="[email protected]"

签发证书

1
acme.sh --issue --dns_cf -d vermouthx.com -d '*.vermouthx.com'

安装证书

acme.sh签发的证书存放在~/.acme.sh/下,但是我们不能直接使用证书,在使用之前还必须安装证书。

1
2
3
4
acme.sh --install-cert -d vermouthx.com \
--ca-file /path/to/keyfile/in/nginx/ca.pem \
--key-file /path/to/keyfile/in/nginx/key.pem \
--fullchain-file /path/to/fullchain/nginx/cert.pem

配置 Nginx

下面是一份示例配置

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
server {
listen 80;
server_name vermouthx.com;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name vermouthx.com;

access_log /var/log/nginx/blog-access.log;
error_log /var/log/nginx/blog-error.log;

root /var/www/hexo;

location / {
index index.html;
}

# certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
ssl_certificate /etc/nginx/ssl/vermouthx.com/cert.pem;
ssl_certificate_key /etc/nginx/ssl/vermouthx.com/key.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;
# OCSP Stapling ---
# fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;
# verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/nginx/ssl/vermouthx.com/ca.pem;
resolver 8.8.8.8;
}

游戏中的对象

  1. 玩家飞机(一架)
  2. 敌人飞机(多架)
  3. 玩家子弹
  4. 敌人子弹
  5. 宝物(用于改变玩家子弹类型以及玩家飞机外形)

实体类设计

思路:游戏中所有的对象都包括属性:X 轴,Y 轴坐标、长度、宽度以及对应的图片素材,方法:移动(move)、绘图(draw)。因此我们可以设计一个 GameObject 抽象类,让其他的实体来继承GameObject类。部分代码如下

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
public abstract class GameObject {
/**
* x coordinate
*/
private int x;
/**
* y coordinate
*/
private int y;

/**
* width
*/
private int width;

/**
* height
*/
private int height;

/**
* image
*/
private BufferedImage image;

public GameObject() {
}

public GameObject(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
// ...get set
// ...

实体层的类层次结构如下图

碰撞检测

思路:所有对象都有 X、Y、宽度以及长度,因此我们可以借助 Java 原生的Rectangle类的intersects方法来完成碰撞检测。具体方法为给GameObject添加一个getRectangle方法

1
2
3
public Rectangle getRectangle() {
return new Rectangle(x, y, width, height);
}

该方法会生成一个坐标为(x, y),宽度值为width,高度值为heightRectangle类对象。我们可以用以下形式来判定是否发生碰撞

1
instaneOfGameObject1.getRectangle().intersects(instaneOfGameObject2.getRectangle())

若发生碰撞则返回true,反之则false
关于Rectangle类可以查阅文档,获取详细信息。

数据的传输

DTO(Data Transfer Object),即数据传输对象。我们设计一个dto层来实现各层的数据传输,也就是说dto充当了信使的角色,我们在dto中存储游戏是否开始、是否暂停、玩家分数、难度相关系数、玩家飞机、敌人飞机、子弹、宝物等游戏相关的数据。

部分代码如下:

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
public class GameDTO {

private boolean isStart;

private boolean isPause;

private boolean isBoss;

private int score;

private Difficulty difficulty;

private BasePlane playerPlane;

private BossPlane bossPlane;

private final List<BaseBullet> playerBullets;

private final List<BasePlane> enemyPlanes;

private final List<BaseBullet> enemyBullets;

private final List<BaseItem> items;

public GameDTO() {
isStart = false;
isPause = false;
isBoss = false;
score = 0;
playerPlane = new PlayerPlane();
playerBullets = new LinkedList<>();
enemyPlanes = new LinkedList<>();
enemyBullets = new LinkedList<>();
items = new LinkedList<>();
}

由于游戏体量不大,所以我们只设计GameDTO一个类就可以了。
出于性能的考虑,我们给GameDTO加上单例,使整个程序只有一个GameDTO对象,具体实现方式为

1
2
3
4
5
6
7
8
9
10
/**
* single instance dto
*/
private static GameDTO dto;

public static GameDTO getDto() {
if (dto == null)
dto = new GameDTO();
return dto;
}

当有对象需要 DTO 对象时,通过GameDTO.getDto()就可以获取 dto 对象。

游戏中的配置

为了防止出现过多的硬编码,我们需要将硬编码项相关的数据全部写入配置,通过读取配置来获取相关数据。
配置可以有很多形式,比如XMLJSONYAML等。
这里我们为了方便直接将配置以static变量的形式写入到 Java 类中。
部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class GameConfig {

private GameConfig() {

}

/**
* game name
*/
private static final String GAME_NAME = "Aircraft";
/**
* window width
*/
private static final int WINDOW_WIDTH = 520;
/**
* window height
*/
private static final int WINDOW_HEIGHT = 700;

...
...

将构造方法私有化是为了使其无法实例化。当需要某个配置数据时使用GameConfig.getXXX()的方式获取,如此我们便完成了硬编码的去除。

控制器的设计

控制器分为玩家控制器(主要处理玩家的按键操作),游戏控制器和登录控制器(未完成)
游戏控制器主要负责与上层界面的交互,具体为创建界面,处理界面的跳转、随机产生敌人飞机、随机产生宝物、刷新界面、监听用户的操作并将其交给用户控制器处理。

具体实现见代码

界面层的设计

  1. 游戏对象的绘制

设计图如下

其中Frame主要是起到容器的作用,实际内容在Panel中绘制。

LoginFrame为游戏登录界面,GameFrame为游戏主体界面,游戏启动时Panel设为LaunchPanel,选择难度后进入主游戏界面,此时将由GameControllerLaunchPanel变换为GamePanel

Panel中绘制界面的方法为paintComponent(Graphics g),我们通过dto来获取游戏对象及其相关的数据,由于每个游戏实体对象都有draw(g)方法,所以我们只要调用draw(g)方法就可以完成游戏对象的绘制。

  1. 界面层需要自己处理的逻辑

对于一些背景音乐、按钮、地图的移动,交由界面层处理,而不由控制器和实体方法处理。

线程的设计

  1. 实体层实体类中的线程设计

游戏中敌人飞机的移动、子弹的移动(包括玩家和敌机的)、宝物的移动、敌机的自动发射子弹,都需要使用线程。

所以我们在BaseBulletBaseItem都加入一个线程

1
2
3
4
5
private Thread thread;
// get
// ...
// set
// ...

由于敌机需要两个线程(移动,射击),所以在EnemyPlane添加两个线程

1
2
private Thread moveThread;
private Thread shootThread;
  1. 实体类中线程的启动

子弹的线程由飞机在调用射击shoot方法时启动。

敌机的移动线程由游戏控制器在随机生成时启动。

  1. 游戏控制器中的线程

游戏控制器由三条线程,处理多按键事件的线程、随机产生宝物的线程、随机产生敌机的线程。

  1. 线程锁

 在对实体类对象操作时有可能出现冲突,所以我们需要在dto以及其他相关地方中加上关键字synchronized,完成线程锁的设定。

dto中部分代码如下

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
public void addPlayerBullet(BaseBullet bullet) {
synchronized (playerBullets) {
playerBullets.add(bullet);
}
}

public void removePlayerBullet(BaseBullet bullet) {
synchronized (playerBullets) {
playerBullets.remove(bullet);
}
}

public void addEnemyPlane(BasePlane plane) {
synchronized (enemyPlanes) {
enemyPlanes.add(plane);
}
}

public void removeEnemyPlane(BasePlane plane) {
synchronized (enemyPlanes) {
enemyPlanes.remove(plane);
}
}

public void addEnemyBullet(BaseBullet bullet) {
synchronized (enemyBullets) {
enemyBullets.add(bullet);
}
}

public void removeEnemyBullet(BaseBullet bullet) {
synchronized (enemyBullets) {
enemyBullets.remove(bullet);
}
}

public void addItem(BaseItem item) {
synchronized (items) {
items.add(item);
}
}

public void removeItem(BaseItem item) {
synchronized (items) {
items.remove(item);
}
}

碰撞检测的时机

碰撞检测可以有两种方案:

  1. 在控制器中加一条线程,不停的循环检测
  2. 在实体对象的每次调用move方法时检测

这里,我选择第二种方案,较之于第一种方案更高效。

具体实现为,在每个实体类中添加一个collisionDetect方法,下面是玩家子弹的实现,其他类与之类似

1
2
3
4
5
6
7
8
9
10
11
@Override
public void collisionDetect() {
synchronized (GameDTO.getDto().getEnemyPlanes()) {
for (BasePlane enemyPlane : GameDTO.getDto().getEnemyPlanes()) {
if (!enemyPlane.isDead() && getRectangle().intersects(enemyPlane.getRectangle())) {
setHit(true);
enemyPlane.setDead(true);
}
}
}
}

总结

至此,飞机大战游戏的主要设计就阐述完了。其中还有许多尚未完成的部分:大招、血条、BOSS、散弹、相同子弹的加强。散弹的实现是比较复杂的,因为散弹不同与其他子弹,它不是一颗子弹,而是多颗子弹同时出去,而且有斜向移动。具体的实现方法读者可自行斟酌。

附源码:https://github.com/WhiteVermouth/Aircraft

搭建步骤

  1. 在本地完成 hexo 博客的初始化
  2. 在服务端完成 git 仓库的初始化
  3. 使用 git 一键部署

在本地搭建 hexo

  1. 安装 hexo-cli
1
yarn gload add hexo-cli
  1. 初始化 hexo 博客
1
hexo init blog
  1. 安装 git 部署插件
1
yarn add hexo-deployer-git

至此 hexo 的本地初始化已完成,使用hexo server可在本地启动一个服务,用于测试预览博客。

在服务端建立 git 仓库

  1. 安装 git
1
sudo apt-get install git
  1. 建立 git 裸库
1
2
3
sudo mkdir /var/repo
cd /var/repo
sudo git init --bare blog.git
  1. 添加 git hook 用于同步网站根目录
1
2
cd /var/repo/blog.git/hooks
sudo vim post-receive

输入以下内容

1
2
#!/bin/sh
git --work-tree=/var/www/hexo --git-dir=/var/repo/blog.git checkout -f

注意修改 work-tree 的路径

添加权限

1
sudo chmod +x post-receive
  1. 创建 git 账户

此处我使用了Ansible来快速自动化完成创建

关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
- name: add git user
become: yes
user:
name: git
shell: /usr/bin/git-shell
group: git
- name: add ssh key
become: yes
authorized_key:
user: git
key: "{{ item }}"
with_file:
- ~/.ssh/id_rsa.pub

出于安全考虑,建议将 git 账户的shell设置为git-shell

  1. 修改 git 库的权限
1
sudo chown -R git:git blog.git

修改本地_config.yml

在_config.yml 找到 deploy,加入以下代码

1
2
3
4
deploy:
type: git
repo: git@serverip:/var/repo/blog.git
branch: master

部署

在本地执行

1
2
hexo g
hexo d

至此,已完成博客的部署

简介

Shadowsocks-libev: Shadowsocks-libev is a lightweight secured SOCKS5 proxy for embedded devices and low-end boxes.

Simple-obfs: Simple-obfs is a simple obfusacting tool, designed as plugin server of shadowsocks.

Docker: Docker is an open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center VMs, or the cloud.

编写 Dockerfile

首先编写 shadowsocks-libevDockerfile

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
#
# Dockerfile for shadowsocks-libev
#

FROM alpine

RUN set -ex \
# Build environment setup
&& apk add --no-cache --virtual .build-deps \
autoconf \
automake \
build-base \
c-ares-dev \
libev-dev \
libtool \
libsodium-dev \
linux-headers \
mbedtls-dev \
pcre-dev \
git \
# Build and install
&& cd /tmp \
&& git clone https://github.com/shadowsocks/shadowsocks-libev.git \
&& cd shadowsocks-libev && git submodule update --init --recursive \
&& ./autogen.sh \
&& ./configure --prefix=/usr --disable-documentation \
&& make install \
&& apk del .build-deps \
# Runtime dependencies setup
&& apk add --no-cache \
rng-tools \
$(scanelf --needed --nobanner /usr/bin/ss-* \
| awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
| sort -u) \
&& rm -rf /tmp/*

ENTRYPOINT ["ss-server"]

接下来是 simple-obfsDockerfile

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
#
# Dockerfile for shadowsocks-libev-simple-obfs
#

FROM alpine

RUN set -ex \
# Build environment setup
&& apk add --no-cache --virtual .build-deps gcc autoconf make libtool automake zlib-dev openssl asciidoc xmlto libpcre32 libev-dev g++ linux-headers git \
# Build and install
&& cd /tmp \
&& git clone https://github.com/shadowsocks/simple-obfs.git \
&& cd simple-obfs \
&& git submodule update --init --recursive \
&& ./autogen.sh \
&& ./configure --prefix=/usr --disable-documentation \
&& make && make install \
&& apk del .build-deps \
# Runtime dependencies setup
&& apk add --no-cache \
$(scanelf --needed --nobanner /usr/bin/obfs-* \
| awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
| sort -u) \
&& rm -rf /tmp/*

ENTRYPOINT ["obfs-server"]

运行 docker build 和 docker push

shadowsocks-libevsimple-obfsDockerfile 目录下分别运行

1
2
docker build ./Dockerfile -t vermouthx/shadowsocks-libev
docker build ./Dockerfile -t vermouthx/simple-obfs

将 build 好的 image push 到 docker cloud

1
2
docker push vermouthx/shadowsocks-libev
docker push vermouthx/simple-obfs

运用 Ansible 实现 VPS 自动化部署

关于AnsibleAnsible是一个自动化运维工具,具体可看官网

编写 Ansible Playbook的相关tasks,关键代码如下

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
- name: deploy shadowsocks-libev
become: yes
docker_container:
name: ss-server
image: vermouthx/shadowsocks-libev
volumes:
- "/etc/shadowsocks-libev:/etc/shadowsocks-libev"
entrypoint: ss-server
command: "-c /etc/shadowsocks-libev/config.json"
ports:
- "9999:9999/udp"
user: nobody
networks:
- name: shadowsocks
state: "{{ container_state }}"
recreate: yes
tags: ss
- name: deploy simple-obfs
become: yes
docker_container:
name: obfs-server
image: vermouthx/simple-obfs
volumes:
- "/etc/simple-obfs:/etc/simple-obfs"
command: "-c /etc/simple-obfs/config.json"
ports:
- "9999:8139"
user: nobody
networks:
- name: shadowsocks
state: "{{ container_state }}"
recreate: yes
tags: obfs

运行该 playbook 即可完成shadowsocks+simple-obfs的部署

Dockerfile的源码:https://github.com/WhiteVermouth/shadowsocks-docker

When you shutdown or reboot your system, systemd tries to stop all services as fast as it can. That involves bringing down the network and terminating all processes that are still alive – usually in that order. So when systemd kills the forked SSH processes that are handling your SSH sessions, the network connection is already disabled and they have no way of closing the client connection gracefully.

Your first thought might be to just kill all SSH processes as the first step during shutdown, and there are quite a few systemd service files out there that do just that.

But there is of course a neater solution (how it’s “supposed” to be done): systemd-logind.
systemd-logind keeps track of active user sessions (local and SSH ones) and assigns all processes spawned within them to so-called “slices”. That way, when the system is shut down, systemd can just SIGTERM everything inside the user slices (which includes the forked SSH process that’s handing a particular session) and then continue shutting down services and the network.

systemd-logind requires a PAM module to get notified of new user sessions and you’ll need dbus to use loginctl to check its status, so install both of those:

apt-get install libpam-systemd dbus

source

0%