<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Java on 分享技术带来的喜悦</title><link>https://bridgeli.cn/categories/java/</link><description>Recent content in Java on 分享技术带来的喜悦</description><generator>Hugo -- 0.156.0</generator><language>zh-cn</language><lastBuildDate>Fri, 13 Mar 2026 22:26:48 +0800</lastBuildDate><atom:link href="https://bridgeli.cn/categories/java/index.xml" rel="self" type="application/rss+xml"/><item><title>mvn install 和 mvn install:install-file 在传递依赖方面的区别</title><link>https://bridgeli.cn/posts/2026-03-13-mvn/</link><pubDate>Fri, 13 Mar 2026 22:26:48 +0800</pubDate><guid>https://bridgeli.cn/posts/2026-03-13-mvn/</guid><description>&lt;p&gt;一句话总结：Maven 的传递依赖不是靠 JAR 文件本身实现的，而是靠 .pom 文件中的元数据。如果你用 mvn install:install-file 安装一个 JAR 却没提供 POM，那它就是一座“断桥”——别人能引用你，但走不到你依赖的库。&lt;/p&gt;
&lt;p&gt;在日常开发中，我们经常遇到这样的场景：第三方供应商给了你一个闭源 SDK，只有 .jar 文件，我们都会使用：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mvn install:install-file -Dfile=your-jar.jar -DgroupId=com.yourcompany -DartifactId=your-jar -Dversion=1.0.0 -Dpackaging=jar
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看起来一切正常，项目也能编译通过。但一运行就报错：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;java.lang.NoClassDefFoundError: com/yourcompany/yourjar/YourClass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;奇怪了，你的 JAR 确实依赖它，为什么 Maven 没自动下载？答案就藏在 传递依赖（Transitive Dependencies） 的工作机制里。&lt;/p&gt;
&lt;p&gt;什么是传递依赖？假设你的项目 A 依赖库 B，而库 B 又依赖库 C 和 D。在 Maven 中，你不需要显式声明 C 和 D，Maven 会自动把它们加入 classpath —— 这就是传递依赖。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.google.guava&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;guava&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;32.1.3-jre&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Guava 的 POM 文件中声明了对 jsr305、checker-qual 等库的依赖。当你引入 Guava 时，这些依赖会自动传递进来。但有个前提：传递依赖的信息必须存在于该构件对应的 .pom 文件中。&lt;/p&gt;
&lt;p&gt;mvn install：自带“导航地图”&lt;/p&gt;
&lt;p&gt;当你在一个标准 Maven 项目中运行：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mvn install
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Maven 会：编译代码、运行测试、打包生成 target/my-app-1.0.jar；同时将项目的 pom.xml 处理后安装为 my-app-1.0.pom 到本地仓库（如 ~/.m2/repository/com/example/my-app/1.0/）；这个 .pom 文件完整保留了 &lt;dependencies&gt; 块。因此，当其他项目引用 com.example:my-app:1.0 时，Maven 读取 .pom，自动解析并下载所有传递依赖。&lt;/p&gt;</description></item><item><title>本地开发不同项目使用不同版本 JDK 的解决方案</title><link>https://bridgeli.cn/posts/2025-12-25-%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E4%B8%8D%E5%90%8C%E9%A1%B9%E7%9B%AE%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%90%8C%E7%89%88%E6%9C%AC-jdk-%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</link><pubDate>Thu, 25 Dec 2025 02:24:23 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-12-25-%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E4%B8%8D%E5%90%8C%E9%A1%B9%E7%9B%AE%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%90%8C%E7%89%88%E6%9C%AC-jdk-%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</guid><description>&lt;p&gt;一般我们在公司工作中，很少有人就负责一个项目，而负责不同的项目，由于各种原因使用的 JDK 版本可能并不相同，如果存在不兼容的情况，那么本地 mvn clean compile 的时候就会报错，这个时候就需要我们去修改我们设置的环境变量，如果多个项目同时开发，那么就需要时不时切来切去，特别烦人，其实这个问题 maven 早就替大家考虑过了，我们只需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改 maven 的 toolchains.xml 文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;!&amp;amp;#8211;
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
&amp;#34;License&amp;#34;); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
&amp;#34;AS IS&amp;#34; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
&amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211;
| This is the toolchains file for Maven. It can be specified at two levels:
|
| 1. User Level. This toolchains.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/toolchains.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -t /path/to/user/toolchains.xml
|
| 2. Global Level. This toolchains.xml file provides configuration for all Maven
| users on a machine (assuming they&amp;amp;#8217;re all using the same Maven
| installation). It&amp;amp;#8217;s normally provided in
| ${maven.conf}/toolchains.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gt /path/to/global/toolchains.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation.
|&amp;amp;#8211;&amp;gt;
&amp;lt;toolchains xmlns=&amp;#34;http://maven.apache.org/TOOLCHAINS/1.1.0&amp;#34; xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xsi:schemaLocation=&amp;#34;http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211;
| With toolchains you can refer to installations on your system. This
| way you don&amp;amp;#8217;t have to hardcode paths in your pom.xml.
|
| Every toolchain consist of 3 elements:
| * type: the type of tool. An often used value is &amp;amp;#8216;jdk&amp;amp;#8217;. Toolchains-aware
| plugins should document which type you must use.
|
| * provides: A list of key/value-pairs.
| Based on the toolchain-configuration in the pom.xml Maven will search for
| matching &amp;lt;provides/&amp;gt; configuration. You can decide for yourself which key-value
| pairs to use. Often used keys are &amp;amp;#8216;version&amp;amp;#8217;, &amp;amp;#8216;vendor&amp;amp;#8217; and &amp;amp;#8216;arch&amp;amp;#8217;. By default
| the version has a special meaning. If you configured in the pom.xml &amp;amp;#8216;1.5&amp;amp;#8217;
| Maven will search for 1.5 and above.
|
| * configuration: Additional configuration for this tool.
| Look for documentation of the toolchains-aware plugin which configuration elements
| can be used.
|
| See also https://maven.apache.org/guides/mini/guide-using-toolchains.html
|
| General example
&amp;lt;toolchain&amp;gt;
&amp;lt;type/&amp;gt;
&amp;lt;provides&amp;gt;
&amp;lt;version&amp;gt;1.0&amp;lt;/version&amp;gt;
&amp;lt;/provides&amp;gt;
&amp;lt;configuration/&amp;gt;
&amp;lt;/toolchain&amp;gt;
| JDK examples
&amp;lt;toolchain&amp;gt;
&amp;lt;type&amp;gt;jdk&amp;lt;/type&amp;gt;
&amp;lt;provides&amp;gt;
&amp;lt;version&amp;gt;1.5&amp;lt;/version&amp;gt;
&amp;lt;vendor&amp;gt;sun&amp;lt;/vendor&amp;gt;
&amp;lt;/provides&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;jdkHome&amp;gt;/path/to/jdk/1.5&amp;lt;/jdkHome&amp;gt;
&amp;lt;/configuration&amp;gt;
&amp;lt;/toolchain&amp;gt;
&amp;lt;toolchain&amp;gt;
&amp;lt;type&amp;gt;jdk&amp;lt;/type&amp;gt;
&amp;lt;provides&amp;gt;
&amp;lt;version&amp;gt;1.6&amp;lt;/version&amp;gt;
&amp;lt;vendor&amp;gt;sun&amp;lt;/vendor&amp;gt;
&amp;lt;/provides&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;jdkHome&amp;gt;/path/to/jdk/1.6&amp;lt;/jdkHome&amp;gt;
&amp;lt;/configuration&amp;gt;
&amp;lt;/toolchain&amp;gt;
&amp;amp;#8211;&amp;gt;
&amp;lt;toolchain&amp;gt;
&amp;lt;type&amp;gt;jdk&amp;lt;/type&amp;gt;
&amp;lt;provides&amp;gt;
&amp;lt;version&amp;gt;17&amp;lt;/version&amp;gt;
&amp;lt;/provides&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;jdkHome&amp;gt;D:\J2EE\Java\jdk-17&amp;lt;/jdkHome&amp;gt;
&amp;lt;/configuration&amp;gt;
&amp;lt;/toolchain&amp;gt;
&amp;lt;toolchain&amp;gt;
&amp;lt;type&amp;gt;jdk&amp;lt;/type&amp;gt;
&amp;lt;provides&amp;gt;
&amp;lt;version&amp;gt;8&amp;lt;/version&amp;gt;
&amp;lt;/provides&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;jdkHome&amp;gt;D:\J2EE\Java\jdk1.8.0_311&amp;lt;/jdkHome&amp;gt;
&amp;lt;/configuration&amp;gt;
&amp;lt;/toolchain&amp;gt;
&amp;lt;/toolchains&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;修改项目的 pom.xml 文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;profiles&amp;gt;
&amp;lt;profile&amp;gt;
&amp;lt;id&amp;gt;dev&amp;lt;/id&amp;gt;
&amp;lt;activation&amp;gt;
&amp;lt;activeByDefault&amp;gt;false&amp;lt;/activeByDefault&amp;gt;
&amp;lt;/activation&amp;gt;
&amp;lt;build&amp;gt;
&amp;lt;plugins&amp;gt;
&amp;lt;plugin&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;maven-toolchains-plugin&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.1.0&amp;lt;/version&amp;gt;
&amp;lt;executions&amp;gt;
&amp;lt;execution&amp;gt;
&amp;lt;goals&amp;gt;
&amp;lt;goal&amp;gt;toolchain&amp;lt;/goal&amp;gt;
&amp;lt;/goals&amp;gt;
&amp;lt;/execution&amp;gt;
&amp;lt;/executions&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;toolchains&amp;gt;
&amp;lt;jdk&amp;gt;
&amp;lt;version&amp;gt;8&amp;lt;/version&amp;gt;
&amp;lt;/jdk&amp;gt;
&amp;lt;/toolchains&amp;gt;
&amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;
&amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&amp;lt;/profile&amp;gt;
&amp;lt;/profiles&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;修改完我们在编译的时候只需要运行：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
mvn clean compile -Pdev
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;即可，这样编译就会自动用 jdk8 也不是系统环境变量配置的，当然还有更简单的，我们可以修改 pom.xml 中的配置，只需要把：&lt;/p&gt;</description></item><item><title>使用 docker 一键部署 ELK(包含中文分词) 服务脚本</title><link>https://bridgeli.cn/posts/2025-08-28-%E4%BD%BF%E7%94%A8-docker-%E4%B8%80%E9%94%AE%E9%83%A8%E7%BD%B2-elk-%E6%9C%8D%E5%8A%A1%E8%84%9A%E6%9C%AC/</link><pubDate>Thu, 28 Aug 2025 09:26:14 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-08-28-%E4%BD%BF%E7%94%A8-docker-%E4%B8%80%E9%94%AE%E9%83%A8%E7%BD%B2-elk-%E6%9C%8D%E5%8A%A1%E8%84%9A%E6%9C%AC/</guid><description>&lt;p&gt;前几天公司有个需求要做全文索引，于是写了一个脚本使用 docker 一键部署 ELK 服务，内容如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
#!/bin/bash
set -e
echo &amp;#34;==================================================&amp;#34;
echo &amp;#34;🚀 开始部署 ELK + MySQL 同步（中文分词 + 固定索引）&amp;#34;
echo &amp;#34;==================================================&amp;#34;
\# ==================== 配置区（请修改）====================
MYSQL_HOST=&amp;#34;192.168.124.6&amp;#34; # ✏️ 修改为你的 MySQL IP
MYSQL_USER=&amp;#34;root&amp;#34; # 读取权限用户
MYSQL_PASSWORD=&amp;#34;123456&amp;#34; # 用户密码
MYSQL_DB=&amp;#34;ams&amp;#34; # 数据库名
ELASTIC_PASSWORD=&amp;#34;i*B4j6eD+g0e&amp;#34; # ES 密码（至少 8 位，含大小写+数字）
ES_VERSION=&amp;#34;8.11.3&amp;#34; # 必须与 IK 插件版本一致
LOGSTASH_VERSION=&amp;#34;8.11.3&amp;#34;
\# ========================================================
\# 项目路径
ROOT_DIR=&amp;#34;/project/elastic-sync&amp;#34;
mkdir -p &amp;#34;$ROOT_DIR&amp;#34;
cd &amp;#34;$ROOT_DIR&amp;#34;
echo &amp;#34;📁 创建项目目录结构&amp;#34;
mkdir -p config/mysql config data/es data/kibana logs/logstash plugins/ik
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 下载 IK 分词插件 &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;
IK_URL=&amp;#34;https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-ik-${ES_VERSION}.zip&amp;#34;
IK_DIR=&amp;#34;$ROOT_DIR/plugins/ik&amp;#34;
if [ ! -f &amp;#34;$IK_DIR/plugin-descriptor.properties&amp;#34; ]; then
echo &amp;#34;📥 正在下载 IK 分词插件 v${ES_VERSION}&amp;amp;#8230;&amp;#34;
wget -q &amp;#34;$IK_URL&amp;#34; -O /tmp/ik.zip
unzip -q /tmp/ik.zip -d &amp;#34;$IK_DIR&amp;#34;
rm /tmp/ik.zip
chown -R 1000:1000 &amp;#34;$IK_DIR&amp;#34;
echo &amp;#34;✅ IK 插件安装完成&amp;#34;
else
echo &amp;#34;ℹ️ IK 插件已存在，跳过安装&amp;#34;
fi
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 生成 elasticsearch.yml &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
cat &amp;gt; config/elasticsearch.yml &amp;lt;&amp;lt; EOF
cluster.name: production-cluster
node.name: node-1
node.roles: [ data, master, ingest ]
path:
data: /usr/share/elasticsearch/data
logs: /usr/share/elasticsearch/logs
network.host: 0.0.0.0
http.port: 9200
http.cors.enabled: true
http.cors.allow-origin: &amp;#34;*&amp;#34;
discovery.type: single-node
xpack.security.enabled: true
xpack.security.http.ssl.enabled: false
xpack.monitoring.collection.enabled: true
EOF
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 生成 logstash.yml &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
cat &amp;gt; config/logstash.yml &amp;lt;&amp;lt; EOF
http.host: &amp;#34;0.0.0.0&amp;#34;
xpack.monitoring.enabled: false
config.reload.automatic: false
EOF
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 生成 logstash.conf &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
cat &amp;gt; config/logstash.conf &amp;lt;&amp;lt; EOF
input {
jdbc {
jdbc_connection_string =&amp;gt; &amp;#34;jdbc:mysql://$MYSQL_HOST:3306/$MYSQL_DB?useUnicode=true&amp;amp;characterEncoding=UTF-8&amp;amp;useSSL=false&amp;amp;serverTimezone=Asia/Shanghai&amp;#34;
jdbc_user =&amp;gt; &amp;#34;$MYSQL_USER&amp;#34;
jdbc_password =&amp;gt; &amp;#34;$MYSQL_PASSWORD&amp;#34;
jdbc_driver_library =&amp;gt; &amp;#34;/usr/share/logstash/mysql/mysql-connector-java-8.0.30.jar&amp;#34;
jdbc_driver_class =&amp;gt; &amp;#34;com.mysql.cj.jdbc.Driver&amp;#34;
jdbc_default_timezone =&amp;gt; &amp;#34;Asia/Shanghai&amp;#34;
statement =&amp;gt; &amp;#34;
SELECT * FROM article
WHERE updated_at &amp;gt;= :sql_last_value
ORDER BY updated_at ASC
&amp;#34;
use_column_value =&amp;gt; true
tracking_column =&amp;gt; &amp;#34;updated_at&amp;#34;
tracking_column_type =&amp;gt; &amp;#34;timestamp&amp;#34;
last_run_metadata_path =&amp;gt; &amp;#34;/usr/share/logstash/.logstash_jdbc_last_run&amp;#34;
schedule =&amp;gt; &amp;#34;\*/2 \* \* \* *&amp;#34;
}
}
filter {
\# 清洗 content 字段中的 HTML 标签
if [content] {
mutate {
gsub =&amp;gt; [
&amp;#34;content&amp;#34;, &amp;#34;&amp;lt;[^&amp;gt;]*&amp;gt;&amp;#34;, &amp;#34;&amp;#34; # 删除所有 HTML 标签：&amp;lt;p&amp;gt;、&amp;lt;div&amp;gt;、&amp;lt;span&amp;gt; 等
]
}
\# 可选：进一步清理多余的空白字符
mutate {
gsub =&amp;gt; [
&amp;#34;content&amp;#34;, &amp;#34;\s+&amp;#34;, &amp;#34; &amp;#34; # 多个空白字符（空格、换行、制表符）合并为一个空格
]
}
mutate {
strip =&amp;gt; [&amp;#34;content&amp;#34;] # 去除首尾空格
}
}
\# 如果 del_flag 是 1，标记该记录用于删除
if [del_flag] == 1 {
mutate {
add_tag =&amp;gt; [&amp;#34;delete_document&amp;#34;]
}
}
}
output {
if &amp;#34;delete_document&amp;#34; in [tags] {
elasticsearch {
hosts =&amp;gt; [&amp;#34;http://elasticsearch:9200&amp;#34;]
user =&amp;gt; &amp;#34;elastic&amp;#34;
password =&amp;gt; &amp;#34;$ELASTIC_PASSWOR&amp;#34;
action =&amp;gt; &amp;#34;delete&amp;#34;
document_id =&amp;gt; &amp;#34;%{id}&amp;#34;
index =&amp;gt; &amp;#34;articles&amp;#34;
}
} else {
elasticsearch {
hosts =&amp;gt; [&amp;#34;http://elasticsearch:9200&amp;#34;]
user =&amp;gt; &amp;#34;elastic&amp;#34;
password =&amp;gt; &amp;#34;$ELASTIC_PASSWOR&amp;#34;
index =&amp;gt; &amp;#34;articles&amp;#34; # ✅ 固定索引
document_id =&amp;gt; &amp;#34;%{id}&amp;#34; # 支持 varchar id
doc_as_upsert =&amp;gt; true # 更新覆盖
}
}
stdout { codec =&amp;gt; rubydebug }
}
EOF
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 生成 docker-compose.yml &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
cat &amp;gt; docker-compose.yml &amp;lt;&amp;lt; EOF
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:$ES_VERSION
container_name: elasticsearch
environment:
&amp;amp;#8211; discovery.type=single-node
&amp;amp;#8211; ES_JAVA_OPTS=-Xms2g -Xmx2g
&amp;amp;#8211; xpack.security.enabled=true
&amp;amp;#8211; xpack.security.http.ssl.enabled=false
&amp;amp;#8211; ELASTIC_PASSWORD=$ELASTIC_PASSWORD
ports:
&amp;amp;#8211; &amp;#34;9200:9200&amp;#34;
volumes:
&amp;amp;#8211; ./data/es:/usr/share/elasticsearch/data
&amp;amp;#8211; ./config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
&amp;amp;#8211; ./plugins/ik:/usr/share/elasticsearch/plugins/ik
networks:
&amp;amp;#8211; elastic
restart: unless-stopped
healthcheck:
test: [&amp;#34;CMD-SHELL&amp;#34;, &amp;#34;curl -f http://localhost:9200 || exit 1&amp;#34;]
interval: 30s
timeout: 10s
retries: 3
kibana:
image: docker.elastic.co/kibana/kibana:$LOGSTASH_VERSION
container_name: kibana
depends_on:
elasticsearch:
condition: service_healthy
environment:
&amp;amp;#8211; ELASTICSEARCH_HOSTS=[&amp;#34;http://elasticsearch:9200&amp;#34;]
&amp;amp;#8211; ELASTICSEARCH_USERNAME=elastic
&amp;amp;#8211; ELASTICSEARCH_PASSWORD=$ELASTIC_PASSWORD
&amp;amp;#8211; SERVER_NAME=kibana.example.com
&amp;amp;#8211; I18N_LOCALE=zh-CN
ports:
&amp;amp;#8211; &amp;#34;5601:5601&amp;#34;
volumes:
&amp;amp;#8211; ./data/kibana:/usr/share/kibana/data
networks:
&amp;amp;#8211; elastic
restart: unless-stopped
logstash:
image: docker.elastic.co/logstash/logstash:$LOGSTASH_VERSION
container_name: logstash
depends_on:
&amp;amp;#8211; elasticsearch
volumes:
&amp;amp;#8211; ./config/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
&amp;amp;#8211; ./config/logstash.yml:/usr/share/logstash/config/logstash.yml
&amp;amp;#8211; ./logs/logstash:/var/log/logstash
&amp;amp;#8211; ./config/mysql:/usr/share/logstash/mysql
networks:
&amp;amp;#8211; elastic
restart: unless-stopped
networks:
elastic:
driver: bridge
EOF
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 下载 MySQL JDBC 驱动 &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
JDBC_JAR=&amp;#34;config/mysql/mysql-connector-java-8.0.30.jar&amp;#34;
if [ ! -f &amp;#34;$JDBC_JAR&amp;#34; ]; then
echo &amp;#34;📥 下载 MySQL JDBC 驱动&amp;amp;#8230;&amp;#34;
mkdir -p config/mysql
wget -q https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.30/mysql-connector-java-8.0.30.jar -O &amp;#34;$JDBC_JAR&amp;#34;
echo &amp;#34;✅ JDBC 驱动下载完成&amp;#34;
fi
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 设置权限 &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
echo &amp;#34;🔐 设置目录权限&amp;#34;
chown -R 1000:1000 data/es plugins/ik
chmod -R 755 config logs
chmod -R 777 data/kibana
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 生成创建索引脚本 &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
cat &amp;gt; create-index.sh &amp;lt;&amp;lt; &amp;amp;#8216;EOF&amp;amp;#8217;
#!/bin/bash
echo &amp;#34;🔄 正在创建索引 &amp;amp;#8216;articles&amp;amp;#8217; 并配置 IK 分词器&amp;amp;#8230;&amp;#34;
curl -X PUT &amp;#34;http://localhost:9200/articles&amp;#34; \
-u elastic:$ELASTIC_PASSWORD \
-H &amp;#34;Content-Type: application/json&amp;#34; \
-d &amp;amp;#8216;
{
&amp;#34;settings&amp;#34;: {
&amp;#34;index&amp;#34;: {
&amp;#34;number_of_shards&amp;#34;: 1,
&amp;#34;number_of_replicas&amp;#34;: 1
},
&amp;#34;analysis&amp;#34;: {
&amp;#34;analyzer&amp;#34;: {
&amp;#34;ik_analyzer&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;custom&amp;#34;,
&amp;#34;tokenizer&amp;#34;: &amp;#34;ik_max_word&amp;#34;,
&amp;#34;filter&amp;#34;: [&amp;#34;lowercase&amp;#34;]
}
}
}
},
&amp;#34;mappings&amp;#34;: {
&amp;#34;properties&amp;#34;: {
&amp;#34;id&amp;#34;: { &amp;#34;type&amp;#34;: &amp;#34;keyword&amp;#34; },
&amp;#34;title&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;,
&amp;#34;analyzer&amp;#34;: &amp;#34;ik_max_word&amp;#34;,
&amp;#34;search_analyzer&amp;#34;: &amp;#34;ik_smart&amp;#34;
},
&amp;#34;content&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;,
&amp;#34;analyzer&amp;#34;: &amp;#34;ik_max_word&amp;#34;,
&amp;#34;search_analyzer&amp;#34;: &amp;#34;ik_smart&amp;#34;
},
&amp;#34;author&amp;#34;: { &amp;#34;type&amp;#34;: &amp;#34;keyword&amp;#34; },
&amp;#34;created_at&amp;#34;: { &amp;#34;type&amp;#34;: &amp;#34;date&amp;#34; },
&amp;#34;updated_at&amp;#34;: { &amp;#34;type&amp;#34;: &amp;#34;date&amp;#34; }
}
}
}&amp;amp;#8217; &amp;amp;&amp;amp; echo &amp;#34;✅ 索引 &amp;amp;#8216;articles&amp;amp;#8217; 创建成功！&amp;#34;
EOF
chmod +x create-index.sh
\# &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;- 完成 &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-
echo &amp;#34;==================================================&amp;#34;
echo &amp;#34;🎉 部署准备完成！&amp;#34;
echo &amp;#34;==================================================&amp;#34;
echo &amp;#34;&amp;#34;
echo &amp;#34;📌 下一步操作：&amp;#34;
echo &amp;#34;1. 检查配置：nano setup-elastic-sync.sh （修改 MySQL 地址、用户、密码）&amp;#34;
echo &amp;#34;2. 增加权限：chmod +x setup-elastic-sync.sh&amp;#34;
echo &amp;#34;3. 执行脚本：sudo ./setup-elastic-sync.sh&amp;#34;
echo &amp;#34;4. 启动服务：sudo docker compose up -d&amp;#34;
echo &amp;#34;5. 创建索引：bash ./create-index.sh&amp;#34;
echo &amp;#34;6. 访问 Kibana：http://你的服务器IP:5601&amp;#34;
echo &amp;#34; &amp;amp;#8211; 用户：elastic&amp;#34;
echo &amp;#34; &amp;amp;#8211; 密码：$ELASTIC_PASSWORD&amp;#34;
echo &amp;#34;&amp;#34;
echo &amp;#34;💡 首次运行会全量同步 articles 表，之后每 2 分钟增量同步&amp;#34;
echo &amp;#34;🔍 在 Kibana 中搜索中文（如“阿里巴巴”），应能命中结果&amp;#34;
echo &amp;#34;&amp;#34;
echo &amp;#34;⚠️ 注意：必须先运行 create-index.sh 再让 Logstash 写入，否则分词无效！&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果 kibana 用 elastic 用户连不上，通过 Elasticsearch 的 Security API 为内置用户 kibana_system 修改密码：&lt;/p&gt;</description></item><item><title>关于 Redis incr 的一个问题</title><link>https://bridgeli.cn/posts/2025-07-30-%E5%85%B3%E4%BA%8E-redis-incr-%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/</link><pubDate>Wed, 30 Jul 2025 10:23:46 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-07-30-%E5%85%B3%E4%BA%8E-redis-incr-%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/</guid><description>&lt;p&gt;前一段时间有一个需求，需要计数，理所当地的使用了 redis 的 incr 方法。代码大概如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Scheduled(cron = &amp;#34;0 0/10 \* \* * ?&amp;#34;)
public void test() {
long yellowInterval = 5L;
boolean isReachable = false;
// TODO
long delta = isReachable ? -1L : 1L;
ValueOperations&amp;lt;String, Long&amp;gt; valueOperations = redisTemplate.opsForValue();
String key = RFID_NETWORK_STATUS_PREFIX + rfDevice.getId();
Long increment = valueOperations.increment(key, delta);
if (increment == null || increment &amp;lt;= 0L) {
}
} else if (increment &amp;gt;= yellowInterval) {
if (Constants.RFID_NETWORK_STATUS_GREEN.equals(rfDevice.getNetworkStatus())) {
}
if (increment &amp;gt;= yellowInterval * 3) {
valueOperations.set(key, yellowInterval * 3);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;大概就是某个操作之后记一下数，加一或者减一，如果加到了某个值，就把它设置为某个值。我们这里先不考虑并发问题。结果在运行的时候遇到了一个问题，报错信息如下：&lt;/p&gt;</description></item><item><title>Gradle 项目打包构建中的两个小问题</title><link>https://bridgeli.cn/posts/2025-05-17-gradle-%E9%A1%B9%E7%9B%AE%E6%89%93%E5%8C%85%E6%9E%84%E5%BB%BA%E4%B8%AD%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%B0%8F%E9%97%AE%E9%A2%98/</link><pubDate>Sat, 17 May 2025 07:42:46 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-05-17-gradle-%E9%A1%B9%E7%9B%AE%E6%89%93%E5%8C%85%E6%9E%84%E5%BB%BA%E4%B8%AD%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%B0%8F%E9%97%AE%E9%A2%98/</guid><description>&lt;ol&gt;
&lt;li&gt;打包的时候报错，提示 jar 重复，具体详情：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
* What went wrong:
Execution failed for task &amp;amp;#8216;:web-admin:bootJar&amp;amp;#8217;.
&amp;gt; Entry BOOT-INF/lib/jaxb-core-4.0.3.jar is a duplicate but no duplicate handling strategy has been set. Please refer to https://docs.gradle.org/7.6.3/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:duplicatesStrategy for details.
* Try:
&amp;gt; Run with &amp;amp;#8211;stacktrace option to get the stack trace.
&amp;gt; Run with &amp;amp;#8211;info or &amp;amp;#8211;debug option to get more log output.
&amp;gt; Run with &amp;amp;#8211;scan to get full insights.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在打包Spring Boot应用时，BOOT-INF/lib/jaxb-core-4.0.3.jar 文件出现了重复项，而构建脚本中没有设置处理重复文件的策略。Gradle不允许默认情况下存在重复文件，因此构建失败。要解决这个问题，只修改构建配置：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
bootJar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这段代码告诉 Gradle 在发现重复文件时排除它们。根据你的需求，你也可以选择其他策略如 DuplicatesStrategy.INCLUDE 或者 DuplicatesStrategy.WARN。然后清理和重新构建项目即可。&lt;/p&gt;</description></item><item><title>nginx 代理 sse 接口，报：(failed) net::ERR HTTP2 PROTOCOL ERROR</title><link>https://bridgeli.cn/posts/2025-04-03-nginx-%E4%BB%A3%E7%90%86-sse-%E6%8E%A5%E5%8F%A3%E6%8A%A5failed-neterr-http2-protocol-error/</link><pubDate>Thu, 03 Apr 2025 07:49:30 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-04-03-nginx-%E4%BB%A3%E7%90%86-sse-%E6%8E%A5%E5%8F%A3%E6%8A%A5failed-neterr-http2-protocol-error/</guid><description>&lt;p&gt;前一段时间曾写了一篇关于&lt;a href="http://www.bridgeli.cn/archives/773"&gt;Spring MVC 通过 SSE 实现消息推送&lt;/a&gt;的小文章，后来系统上线的时候，遇到了一个小问题，打开浏览器的 network，看到接口报：(failed) net::ERR HTTP2 PROTOCOL ERROR，通常是因为 HTTP/2 协议与 SSE 的某些特性不兼容所导致的。SSE 是基于 HTTP 协议的服务器推送技术，它要求连接保持打开状态以便服务器可以持续发送更新给客户端。我们使用的 nginx version 是：nginx/1.26.1&lt;/p&gt;
&lt;p&gt;只需要按如下配置即可解决：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
server {
listen 80;
server_name bridgeli.com;
access_log /var/log/nginx/bridgeli_access.log;
error_log /var/log/nginx/bridgeli_error.log warn;
location ^~ /admin-api/ {
proxy_pass http://192.168.124.34:8080/;
\# 确保使用HTTP/1.1来支持SSE
proxy_http_version 1.1;
\# 关闭代理连接的“Connection”头，以避免潜在的问题
proxy_set_header Connection &amp;amp;#8221;;
\# 增加超时设置，确保长时间连接不会被关闭
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
\# 如果需要禁用HTTP/2（可选）
\# 注意：这个设置是在server块中，而不是location块中
\# listen 80 http2 off; 对于HTTP/2协议错误特别有用
}
location / {
root /project/www/bridgeli/admin/;
try_files $uri $uri/ /index.html;
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>关于 druid 监控的两个小问题</title><link>https://bridgeli.cn/posts/2025-03-12-%E5%85%B3%E4%BA%8E-druid-%E7%9B%91%E6%8E%A7%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%B0%8F%E9%97%AE%E9%A2%98/</link><pubDate>Wed, 12 Mar 2025 09:44:14 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-03-12-%E5%85%B3%E4%BA%8E-druid-%E7%9B%91%E6%8E%A7%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%B0%8F%E9%97%AE%E9%A2%98/</guid><description>&lt;p&gt;前一段时间做一个小需求，需要展示 druid 的监控页面，我们都知道 druid 监控地址是：http://ip:port/druid/index.html，但是当时一直报 404，后来查了资料，引入的 jar 包：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.2.24&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;应该是因为不是通过 starter 包引入的，所以需要手动配置：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import com.alibaba.druid.support.jakarta.StatViewServlet;
import com.alibaba.druid.support.jakarta.WebStatFilter;
import com.alibaba.druid.util.Utils;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class DruidConfig {
@Bean
public ServletRegistrationBean&amp;lt;StatViewServlet&amp;gt; statViewServlet() {
ServletRegistrationBean&amp;lt;StatViewServlet&amp;gt; servletRegistrationBean = new ServletRegistrationBean&amp;lt;&amp;gt;(new StatViewServlet(), &amp;#34;/druid/*&amp;#34;);
// 设置登录用户名和密码
servletRegistrationBean.addInitParameter(&amp;#34;loginUsername&amp;#34;, &amp;#34;BridgeLi&amp;#34;);
servletRegistrationBean.addInitParameter(&amp;#34;loginPassword&amp;#34;, &amp;#34;BridgeLi&amp;#34;);
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean&amp;lt;WebStatFilter&amp;gt; webStatFilter() {
FilterRegistrationBean&amp;lt;WebStatFilter&amp;gt; filterRegistrationBean = new FilterRegistrationBean&amp;lt;&amp;gt;(new WebStatFilter());
filterRegistrationBean.addUrlPatterns(&amp;#34;/*&amp;#34;);
filterRegistrationBean.addInitParameter(&amp;#34;exclusions&amp;#34;, &amp;#34;\*.js,\*.gif,\*.jpg,\*.png,\*.css,\*.ico,/druid/*&amp;#34;);
return filterRegistrationBean;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;另外一个小问题就是，这么引入之后，打开监控页最下面会有 alibaba 的广告，一般情况下，我们肯定是想去掉的，广告代码所在的位置在 support/http/resources/js/common.js 这个 js 里面，网上有人说直接解压删掉，重新打包就行了，但是这个有问题就是必须用重新打包的这个 jar，搜了一下资料，其实去掉也很简单，在上面的那个类里面增加一个 配置过滤掉即可：&lt;/p&gt;</description></item><item><title>Spring MVC 通过 SSE 实现消息推送</title><link>https://bridgeli.cn/posts/2025-02-27-spring-mvc-%E9%80%9A%E8%BF%87-sse-%E5%AE%9E%E7%8E%B0%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81/</link><pubDate>Thu, 27 Feb 2025 01:26:08 +0000</pubDate><guid>https://bridgeli.cn/posts/2025-02-27-spring-mvc-%E9%80%9A%E8%BF%87-sse-%E5%AE%9E%E7%8E%B0%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81/</guid><description>&lt;p&gt;又好久没有写文章了，自从有了大模型之后写文章的态度越来越提不起兴趣了，有问题，直接问大模型即可。前几天公司有个需求，想用 SSE 实现，之前从没写过，所以让大模型直接写，然后实现超级简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编写 SSE 服务，来进行创建链接和发送消息&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Getter
@Service
public class SseService {
private final Map&amp;lt;String, SseEmitter&amp;gt; emitters = new ConcurrentHashMap&amp;lt;&amp;gt;();
public SseEmitter stream(String usrId) {
SseEmitter emitter = emitters.computeIfAbsent(usrId, k -&amp;gt; new SseEmitter(Long.MAX_VALUE));
emitter.onCompletion(() -&amp;gt; {
log.info(&amp;#34;SSE emitter completed&amp;#34;);
emitters.remove(usrId);
});
emitter.onError((throwable) -&amp;gt; {
log.error(&amp;#34;Error occurred in SSE emitter&amp;#34;, throwable);
emitter.complete();
emitters.remove(usrId);
});
emitter.onTimeout(() -&amp;gt; {
log.warn(&amp;#34;SSE emitter timed out&amp;#34;);
emitter.complete();
emitters.remove(usrId);
});
// 可选：连接成功时向客户端发送一个初始事件
try {
emitter.send(SseEmitter.event().name(&amp;#34;connect&amp;#34;).data(&amp;#34;连接成功&amp;#34;));
} catch (IOException e) {
log.error(&amp;#34;Error occurred while sending initial event&amp;#34;, e);
emitter.completeWithError(e);
}
return emitter;
}
public void send(List&amp;lt;String&amp;gt; userIds, String name, Object object) {
if (!emitters.isEmpty()) {
// 遍历所有用户的 SseEmitter，推送数据
if (CollectionUtils.isEmpty(userIds)) {
emitters.forEach((userId, emitter) -&amp;gt; {
try {
emitter.send(SseEmitter.event().name(name).data(object));
} catch (IOException e) {
// 如果发送失败，则移除该用户的 emitter
log.error(&amp;#34;Error occurred while sending event to user {}&amp;#34;, userId, e);
emitter.completeWithError(e);
emitters.remove(userId);
}
});
} else {
userIds.forEach(userId -&amp;gt; {
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event().name(name).data(object));
} catch (IOException e) {
// 如果发送失败，则移除该用户的 emitter
log.error(&amp;#34;Error occurred while sending event to user {}&amp;#34;, userId, e);
emitter.completeWithError(e);
emitters.remove(userId);
}
}
});
}
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;编写对应的 Controller 给前端提供接口：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import cn.bridgeli.BaseAuthController;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@Slf4j
@RestController
@Tag(name = &amp;#34;SSE 推送服务&amp;#34;)
@RequestMapping(&amp;#34;/auth/common/sse&amp;#34;)
public class SseController extends BaseAuthController {
@Resource
private SseService sseService;
@GetMapping(value = &amp;#34;/stream&amp;#34;, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
return sseService.stream(getLoginUsr().getUsrId());
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;消息推送具体实现：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import cn.bridgeli.common.SseService;
import cn.bridgeli.monitor.MonitorService;
import cn.bridgeli.vo.CpuInfoVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
@Component
@Slf4j
public class ScheduledTask {
@Resource
private MonitorService monitorService;
@Resource
private SseService sseService;
/**
* 每分钟执行一次
*/
@Scheduled(cron = &amp;#34;0 0/1 \* \* * ?&amp;#34;)
public void updateOrderStatus() {
log.info(&amp;#34;=============定时任务=============&amp;#34;);
Map&amp;lt;String, SseEmitter&amp;gt; emitters = sseService.getEmitters();
if (null == emitters || emitters.isEmpty()) {
log.info(&amp;#34;sse emitters is empty&amp;#34;);
return;
}
CpuInfoVo cpuData = monitorService.getCpuData();
sseService.send(null, &amp;#34;cpu&amp;#34;, cpuData);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其实就是前端连接之后创建一个连接，保存连接，然后别的地方产生消息，推送消息，我的实例是通过 oshi 获取 CPU 使用率，实现对 CPU 的实时监控。&lt;/p&gt;</description></item><item><title>全国中小企业融资综合信用服务平台-省级节点数据接口规范-河南省营商环境和社会信用建设中心</title><link>https://bridgeli.cn/posts/2024-09-24-%E5%85%A8%E5%9B%BD%E4%B8%AD%E5%B0%8F%E4%BC%81%E4%B8%9A%E8%9E%8D%E8%B5%84%E7%BB%BC%E5%90%88%E4%BF%A1%E7%94%A8%E6%9C%8D%E5%8A%A1%E5%B9%B3%E5%8F%B0-%E7%9C%81%E7%BA%A7%E8%8A%82%E7%82%B9%E6%95%B0%E6%8D%AE/</link><pubDate>Tue, 24 Sep 2024 06:37:18 +0000</pubDate><guid>https://bridgeli.cn/posts/2024-09-24-%E5%85%A8%E5%9B%BD%E4%B8%AD%E5%B0%8F%E4%BC%81%E4%B8%9A%E8%9E%8D%E8%B5%84%E7%BB%BC%E5%90%88%E4%BF%A1%E7%94%A8%E6%9C%8D%E5%8A%A1%E5%B9%B3%E5%8F%B0-%E7%9C%81%E7%BA%A7%E8%8A%82%E7%82%B9%E6%95%B0%E6%8D%AE/</guid><description>&lt;p&gt;开始之前先说一点题外话，几年前曾经看过一个视频，其中一个观点大概就是程序员是一个反传统的群体，其他群体掌握了某个技术，一般都是当做内部商业机密，而程序员则不一样，喜欢开源，尤其 GPL 协议的开源，不仅自己毫无保留的开源，还要求使用他的软件也得开源，也正是这种开源造就了互联网的蓬勃发展。我目前所在公司因为是做金融相关的公司，国家出于某些原因，要求要上报相关的数据到省平台，而省平台的技术采用的是 webservice，和我们目前习惯的 http 接口不太一样，所以前一段时间在写这个的时候走了不少弯路，而网上也没有参考资料，所以决定把相关的核心代码公布出来，供需要的同学参考。需要说明的是：这是我们河南省的系统相关接口，不知道外省是否一致，省平台给的接口文档名是：全国中小企业融资综合信用服务平台省级节点数据接口规范V5.3.pdf，首页写的是：全国中小企业融资综合信用服务平台省级节点数据接口规范，国家公共信用信息中心，河南省营商环境和社会信用建设中心，2024年7月&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;jar 包引入&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;cn.hutool&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;hutool-all&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;5.8.10&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.bouncycastle&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;bcprov-jdk15on&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.70&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.bouncycastle&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;bcpkix-jdk15on&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.70&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.cxf&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;cxf-rt-frontend-jaxws&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.0.5&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.cxf&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;cxf-rt-transports-http&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.0.5&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;上报工具类，其中：queryData 方法是用来查询，sendData 方法用来上报数据&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import javax.xml.namespace.QName;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.PublicKey;
@Slf4j
public class XyhnUtil {
private static String pubKey;
private static String queryPubKey;
private static PublicKey reportPublicKey;
// private static XyhnConfig xyhnConfig = SpringUtils.getBean(XyhnConfig.class);
private static XyhnConfig xyhnConfig = null;
/**
* 授权查询省平台接口
*
* @param method 要调取的方法名
* @param object 查询方法的参数（不包含：publicKey、appKey）
* @return 省平台解密后的数据
*/
public static String queryData(String method, JSONObject object) {
//1、组装报文
if (StringUtils.isEmpty(queryPubKey)) {
queryPubKey = convertFileToBase64(xyhnConfig.getQueryPubKeyPath());
}
object.put(&amp;#34;publicKey&amp;#34;, queryPubKey);
object.put(&amp;#34;appKey&amp;#34;, xyhnConfig.getQueryAppKey());
//2、发送报文
String jsonRes = callInterface(&amp;#34;query&amp;#34;, method, object.toJSONString());
//3、解密返回数据
JSONObject json = JSONObject.parseObject(jsonRes);
Boolean success = json.getBoolean(&amp;#34;success&amp;#34;);
if (&amp;#34;uploadLicense&amp;#34;.equals(method)) {
if (null == success || !success) {
return jsonRes;
}
} else if (&amp;#34;cancelLicense&amp;#34;.equals(method)) {
if (null != success &amp;amp;&amp;amp; success) {
return jsonRes;
}
}
BigInteger d = new BigInteger(xyhnConfig.getQueryPriKey(), 16);
BCECPrivateKey bcecPrivateKey = GMUtil.getPrivatekeyFromD(d);
String key0 = json.getString(&amp;#34;key&amp;#34;);
String data = json.getString(&amp;#34;data&amp;#34;);
String signatureData = json.getString(&amp;#34;signatureData&amp;#34;);
byte[] decode = Hex.decode(key0);
// sm2解密
byte[] bytes1 = GMUtil.sm2Decrypt(decode, bcecPrivateKey);
// sm4解密
String content = GMUtil.sm4Decrypt(new String(bytes1), data);
log.info(&amp;#34;content: &amp;#34; + content);
// 4、验签
File file = base64ToFileEx(json.getString(&amp;#34;pubKey&amp;#34;));
PublicKey publicKey = GMUtil.getPublickeyFromX509File(file);
byte[] signatureData1 = Hex.decode(signatureData);
boolean verifyRes = GMUtil.verifySm3WithSm2(content.getBytes(), xyhnConfig.getUserId().getBytes(), signatureData1, publicKey);
log.info(&amp;#34;method 验签结果：&amp;#34; + verifyRes);
if (!verifyRes) {
return null;
}
return content;
}
private static PublicKey getPublicKey() {
//1、组装报文
if (StringUtils.isEmpty(pubKey)) {
pubKey = convertFileToBase64(xyhnConfig.getPubKeyPath());
}
JSONObject jsonObject = new JSONObject();
jsonObject.put(&amp;#34;key&amp;#34;, xyhnConfig.getAppKey());
JSONObject object = new JSONObject();
object.put(&amp;#34;requestData&amp;#34;, jsonObject);
object.put(&amp;#34;publicKey&amp;#34;, pubKey);
//2、发送报文
String jsonRes = callInterface(&amp;#34;report&amp;#34;, &amp;#34;getPublicKey&amp;#34;, object.toJSONString());
//3、解密返回数据
JSONObject json = JSONObject.parseObject(jsonRes);
BigInteger d = new BigInteger(xyhnConfig.getPriKey(), 16);
BCECPrivateKey bcecPrivateKey = GMUtil.getPrivatekeyFromD(d);
String key0 = json.getString(&amp;#34;key&amp;#34;);
String data = json.getString(&amp;#34;data&amp;#34;);
String signatureData = json.getString(&amp;#34;signatureData&amp;#34;);
byte[] decode = Hex.decode(key0);
// sm2解密
byte[] bytes1 = GMUtil.sm2Decrypt(decode, bcecPrivateKey);
// sm4解密
String returnData = GMUtil.sm4Decrypt(new String(bytes1), data);
log.info(&amp;#34;returnData:&amp;#34; + returnData);
// 4、验签
File file = base64ToFileEx(returnData);
PublicKey publicKey = GMUtil.getPublickeyFromX509File(file);
byte[] signatureData1 = Hex.decode(signatureData);
boolean verifyRes = GMUtil.verifySm3WithSm2(returnData.getBytes(), xyhnConfig.getUserId().getBytes(), signatureData1, publicKey);
log.info(&amp;#34;getPublicKey 接口验签结果：&amp;#34; + verifyRes);
if (!verifyRes) {
return null;
}
return publicKey;
}
public static String sendData(String jsonStr, String method) {
log.info(&amp;#34;调用省平台方法名：&amp;#34; + method + &amp;#34;，参数：&amp;#34; + jsonStr);
try {
if (StringUtils.isEmpty(pubKey)) { // 解析公钥
pubKey = convertFileToBase64(xyhnConfig.getPubKeyPath());
}
if (reportPublicKey == null) { //请求接口，获取公钥
reportPublicKey = getPublicKey();
}
// 1、摘要签名 sm2withsm3
byte[] msg = jsonStr.getBytes(CharEncoding.ISO_8859_1);
byte[] userIdBytes = xyhnConfig.getUserId().getBytes();
BigInteger d = new BigInteger(xyhnConfig.getPriKey(), 16);
BCECPrivateKey bcecPrivateKey = GMUtil.getPrivatekeyFromD(d);
byte[] sig = GMUtil.signSm3WithSm2(msg, userIdBytes, bcecPrivateKey);
String signature = Hex.toHexString(sig);
// 2、sm4加密数据报文
String key1 = RandomUtil.randomString(16);
String jsonobj = GMUtil.sm4Encrypt(key1, jsonStr);
//3、sm2加密16位随机码key
byte[] datamsg = GMUtil.sm2Encrypt(key1.getBytes(), reportPublicKey);
String s = Hex.toHexString(datamsg);
//4、组装并发送报文
JSONObject jsonObject2 = new JSONObject();
jsonObject2.put(&amp;#34;requestData&amp;#34;, jsonobj); // sm4加密的数据集
jsonObject2.put(&amp;#34;key&amp;#34;, s); // sm2加密的16位随机码
jsonObject2.put(&amp;#34;signatureData&amp;#34;, signature); // 签名
jsonObject2.put(&amp;#34;publicKey&amp;#34;, pubKey); // 我的公钥
jsonObject2.put(&amp;#34;appKey&amp;#34;, xyhnConfig.getAppKey()); // 数据授权的key
String setFPRes = callInterface(&amp;#34;report&amp;#34;, method, jsonObject2.toJSONString());
return setFPRes;
} catch (Exception ex) {
log.error(&amp;#34;请求省平台接口异常&amp;#34;, ex);
return null;
}
}
/**
* 请求接口
*
* @param type，report 回传数据，query 查询
* @param method
* @param json
* @return
*/
private static String callInterface(String type, String method, String json) {
log.info(&amp;#34;调用省平台类型:&amp;#34; + type + &amp;#34;，接口：&amp;#34; + method + &amp;#34;，参数：&amp;#34; + json);
Client client = null;
QName name = null;
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
String result = null;
try {
if (&amp;#34;report&amp;#34;.equals(type)) {
client = dcf.createClient(xyhnConfig.getWsAddr());
name = new QName(xyhnConfig.getNamespaceURI(), method);
} else if (&amp;#34;query&amp;#34;.equals(type)) {
client = dcf.createClient(xyhnConfig.getQueryWsAddr());
name = new QName(xyhnConfig.getQueryNamespaceURI(), method);
}
Object[] objects = client.invoke(name, json);
result = objects[0].toString();
} catch (Exception e) {
log.error(&amp;#34;调用省平台接口报错&amp;#34;, e);
} finally {
if (client != null) {
try {
client.close();
} catch (Exception e) {
log.error(&amp;#34;关闭Client资源时发生异常&amp;#34;, e);
}
}
}
log.info(&amp;#34;调用省平台类型:&amp;#34; + type + &amp;#34;，接口：&amp;#34; + method + &amp;#34;，返回值：&amp;#34; + result);
return result;
}
private static String convertFileToBase64(String imgPath) {
byte[] data = null;
// 读取图片字节数组
try (InputStream in = new FileInputStream(imgPath);) {
data = new byte[in.available()];
in.read(data);
} catch (IOException e) {
log.error(&amp;#34;IOException&amp;#34;, e);
}
// 对字节数组进行Base64编码，得到Base64编码的字符串
String base64Str = java.util.Base64.getEncoder().encodeToString(data);
return base64Str;
}
private static File base64ToFileEx(String base64) {
if (StringUtils.isBlank(base64)) {
return null;
}
byte[] buff = Base64.decode(base64);
File file = null;
FileOutputStream fout = null;
try {
file = File.createTempFile(&amp;#34;tmp&amp;#34;, null);
fout = new FileOutputStream(file);
fout.write(buff);
file.deleteOnExit();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fout != null) {
try {
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return file;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;用到的工具类&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jcajce.spec.SM2ParameterSpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
@Slf4j
public class GMUtil {
private static X9ECParameters x9ECParameters = GMNamedCurves.getByName(&amp;#34;sm2p256v1&amp;#34;);
private static ECDomainParameters ecDomainParameters;
private static ECParameterSpec ecParameterSpec;
public static byte[] signSm3WithSm2(byte[] msg, byte[] userId, PrivateKey privateKey) {
return rsAsn1ToPlainByteArray(signSm3WithSm2Asn1Rs(msg, userId, privateKey));
}
public static byte[] signSm3WithSm2Asn1Rs(byte[] msg, byte[] userId, PrivateKey privateKey) {
try {
SM2ParameterSpec parameterSpec = new SM2ParameterSpec(userId);
Signature signer = Signature.getInstance(&amp;#34;SM3withSM2&amp;#34;, &amp;#34;BC&amp;#34;);
signer.setParameter(parameterSpec);
signer.initSign(privateKey, new SecureRandom());
signer.update(msg, 0, msg.length);
byte[] sig = signer.sign();
return sig;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static boolean verifySm3WithSm2(byte[] msg, byte[] userId, byte[] rs, PublicKey publicKey) {
return verifySm3WithSm2Asn1Rs(msg, userId, rsPlainByteArrayToAsn1(rs), publicKey);
}
public static boolean verifySm3WithSm2Asn1Rs(byte[] msg, byte[] userId, byte[] rs, PublicKey publicKey) {
try {
SM2ParameterSpec parameterSpec = new SM2ParameterSpec(userId);
Signature verifier = Signature.getInstance(&amp;#34;SM3withSM2&amp;#34;, &amp;#34;BC&amp;#34;);
verifier.setParameter(parameterSpec);
verifier.initVerify(publicKey);
verifier.update(msg, 0, msg.length);
return verifier.verify(rs);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3) {
int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
byte[] result = new byte[c1c2c3.length];
System.arraycopy(c1c2c3, 0, result, 0, c1Len);
System.arraycopy(c1c2c3, c1c2c3.length &amp;amp;#8211; 32, result, c1Len, 32);
System.arraycopy(c1c2c3, c1Len, result, c1Len + 32, c1c2c3.length &amp;amp;#8211; c1Len &amp;amp;#8211; 32);
return result;
}
public static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2) {
int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
byte[] result = new byte[c1c3c2.length];
System.arraycopy(c1c3c2, 0, result, 0, c1Len);
System.arraycopy(c1c3c2, c1Len + 32, result, c1Len, c1c3c2.length &amp;amp;#8211; c1Len &amp;amp;#8211; 32);
System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length &amp;amp;#8211; 32, 32);
return result;
}
public static byte[] sm2Decrypt(byte[] data, PrivateKey key) {
return sm2DecryptOld(changeC1C3C2ToC1C2C3(data), key);
}
public static byte[] sm2Encrypt(byte[] data, PublicKey key) {
return changeC1C2C3ToC1C3C2(sm2EncryptOld(data, key));
}
public static byte[] sm2EncryptOld(byte[] data, PublicKey key) {
BCECPublicKey localECPublicKey = (BCECPublicKey) key;
ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(localECPublicKey.getQ(), ecDomainParameters);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
try {
return sm2Engine.processBlock(data, 0, data.length);
} catch (InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
public static byte[] sm2DecryptOld(byte[] data, PrivateKey key) {
BCECPrivateKey localECPrivateKey = (BCECPrivateKey) key;
ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(localECPrivateKey.getD(), ecDomainParameters);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(false, ecPrivateKeyParameters);
try {
return sm2Engine.processBlock(data, 0, data.length);
} catch (InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
public static byte[] sm4Encrypt(byte[] keyBytes, byte[] plain) {
byte[] keyBytes0;
if (keyBytes.length != 16) {
keyBytes0 = new byte[16];
for (int i = 0; i &amp;lt; keyBytes0.length; ++i) {
if (keyBytes.length &amp;gt; i) {
keyBytes0[i] = keyBytes[i];
}
}
keyBytes = keyBytes0;
}
if (plain.length % 16 != 0) {
keyBytes0 = new byte[16 * (plain.length / 16 + 1)];
System.arraycopy(plain, 0, keyBytes0, 0, plain.length);
plain = keyBytes0;
}
try {
Key key = new SecretKeySpec(keyBytes, &amp;#34;SM4&amp;#34;);
Cipher out = Cipher.getInstance(&amp;#34;SM4/ECB/NoPadding&amp;#34;, &amp;#34;BC&amp;#34;);
out.init(1, key);
return out.doFinal(plain);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static byte[] sm4Decrypt(byte[] keyBytes, byte[] cipher) {
byte[] keyBytes0;
if (keyBytes.length != 16) {
keyBytes0 = new byte[16];
for (int i = 0; i &amp;lt; keyBytes0.length; ++i) {
if (keyBytes.length &amp;gt; i) {
keyBytes0[i] = keyBytes[i];
}
}
keyBytes = keyBytes0;
}
if (cipher.length % 16 != 0) {
keyBytes0 = new byte[16 * (cipher.length / 16 + 1)];
System.arraycopy(cipher, 0, keyBytes0, 0, cipher.length);
cipher = keyBytes0;
}
try {
Key key = new SecretKeySpec(keyBytes, &amp;#34;SM4&amp;#34;);
Cipher in = Cipher.getInstance(&amp;#34;SM4/ECB/NoPadding&amp;#34;, &amp;#34;BC&amp;#34;);
in.init(2, key);
byte[] bytes = in.doFinal(cipher);
for (int i = bytes.length &amp;amp;#8211; 1; i &amp;gt;= 0; &amp;amp;#8211;i) {
if (bytes[i] != 0) {
byte[] bytes1 = new byte[i + 1];
System.arraycopy(bytes, 0, bytes1, 0, i + 1);
bytes = bytes1;
i = -1;
}
}
return bytes;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String sm4Encrypt(String key, String plan) {
byte[] keyBytes = new byte[16];
byte[] keyBytes0 = key.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i &amp;lt; keyBytes.length; ++i) {
if (keyBytes0.length &amp;gt; i) {
keyBytes[i] = keyBytes0[i];
}
}
byte[] cipher = plan.getBytes(StandardCharsets.UTF_8);
byte[] bytes = sm4Encrypt(keyBytes, cipher);
return Hex.toHexString(bytes).toUpperCase();
}
public static String sm4Decrypt(String key, String cipher) {
byte[] keyBytes = new byte[16];
byte[] keyBytes0 = key.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i &amp;lt; keyBytes.length; ++i) {
if (keyBytes0.length &amp;gt; i) {
keyBytes[i] = keyBytes0[i];
}
}
byte[] cipherbytes = Hex.decode(cipher);
byte[] bytes = sm4Decrypt(keyBytes, cipherbytes);
return new String(bytes, StandardCharsets.UTF_8);
}
private static byte[] bigIntToFixexLengthBytes(BigInteger rOrS) {
byte[] rs = rOrS.toByteArray();
if (rs.length == 32) {
return rs;
} else if (rs.length == 33 &amp;amp;&amp;amp; rs[0] == 0) {
return Arrays.copyOfRange(rs, 1, 33);
} else if (rs.length &amp;lt; 32) {
byte[] result = new byte[32];
Arrays.fill(result, (byte) 0);
System.arraycopy(rs, 0, result, 32 &amp;amp;#8211; rs.length, rs.length);
return result;
} else {
throw new RuntimeException(&amp;#34;err rs: &amp;#34; + Hex.toHexString(rs));
}
}
private static byte[] rsAsn1ToPlainByteArray(byte[] rsDer) {
ASN1Sequence seq = ASN1Sequence.getInstance(rsDer);
byte[] r = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue());
byte[] s = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue());
byte[] result = new byte[64];
System.arraycopy(r, 0, result, 0, r.length);
System.arraycopy(s, 0, result, 32, s.length);
return result;
}
private static byte[] rsPlainByteArrayToAsn1(byte[] sign) {
if (sign.length != 64) {
throw new RuntimeException(&amp;#34;err rs. &amp;#34;);
} else {
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, 32, 64));
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(new ASN1Integer(r));
v.add(new ASN1Integer(s));
try {
return (new DERSequence(v)).getEncoded(&amp;#34;DER&amp;#34;);
} catch (IOException var5) {
throw new RuntimeException(var5);
}
}
}
public static BCECPrivateKey getPrivatekeyFromD(BigInteger d) {
ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecParameterSpec);
return new BCECPrivateKey(&amp;#34;EC&amp;#34;, ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION);
}
public static PublicKey getPublickeyFromX509File(File file) {
try {
CertificateFactory cf = CertificateFactory.getInstance(&amp;#34;X.509&amp;#34;, &amp;#34;BC&amp;#34;);
FileInputStream in = new FileInputStream(file);
X509Certificate x509 = (X509Certificate) cf.generateCertificate(in);
return x509.getPublicKey();
} catch (Exception var4) {
throw new RuntimeException(var4);
}
}
static {
ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
if (Security.getProvider(&amp;#34;BC&amp;#34;) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;相关配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = &amp;#34;xyhn&amp;#34;)
//@RefreshScope
public class XyhnConfig {
private String userId;
private String platformId;
private String appKey;
private String priKey;
private String wsAddr;
private String namespaceURI;
private String pubKeyPath;
private String queryAppKey;
private String queryPriKey;
private String queryWsAddr;
private String queryNamespaceURI;
private String queryPubKeyPath;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;以上是上报和查询数据的核心方法，下面是查询具体数据的封装&lt;/p&gt;</description></item><item><title>使用 knife4j 实现 Swagger 文档增强</title><link>https://bridgeli.cn/posts/2024-06-10-%E4%BD%BF%E7%94%A8-knife4j-%E5%AE%9E%E7%8E%B0-swagger-%E6%96%87%E6%A1%A3%E5%A2%9E%E5%BC%BA/</link><pubDate>Mon, 10 Jun 2024 05:56:21 +0000</pubDate><guid>https://bridgeli.cn/posts/2024-06-10-%E4%BD%BF%E7%94%A8-knife4j-%E5%AE%9E%E7%8E%B0-swagger-%E6%96%87%E6%A1%A3%E5%A2%9E%E5%BC%BA/</guid><description>&lt;p&gt;相信使用 Java 开发的人，对 Swagger 一定不会感到陌生，不过个人对 Swagger 一直没有太多好感，因为他的 UI 实在太难看了，用起来也颇为不顺手，所以国内有人开发了 knife4j 对 Swagger 进行增强，随着时间的推移，现在很多项目都在从 Java8 到 Java17，SpringBoot2 到 SpringBoot3 的迁移，发现 knife4j 现在也开始做了支持，而且用起来更方便。下面简单说一说如何使用。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.github.xiaoymin&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;knife4j-openapi3-jakarta-spring-boot-starter&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.5.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从中我们可以看到 artifactId 做了全新的修改，这个需要注意。另外 Spring Boot 3 只支持 OpenAPI3 规范。Knife4j提供的 starter 已经引用 springdoc-openapi 的 jar，大家需注意避免 jar 包冲突，引入之后，其余的配置，开发者即可完全参考 springdoc-openapi 的项目说明，Knife4j 只提供了增强部分，如果要启用 Knife4j 的增强功能，可以在配置文件中进行开启，其实个人测试就算完全不配置，此时也已经可以通过 http://ip:port/doc.html 查看文档：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
knife4j:
enable: true
basic:
enable: true
username: BridgeLi
password: BridgeLi
springdoc:
default-flat-param-object: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最后，使用 OpenAPI3 的规范注解，注释各个 Spring 的 Rest 接口。&lt;/p&gt;</description></item><item><title>如何构建一个可重复读流 InputStream 的 HttpServletRequest？</title><link>https://bridgeli.cn/posts/2024-05-03-%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E6%B5%81-inputstream-%E7%9A%84-httpservletrequest/</link><pubDate>Fri, 03 May 2024 11:48:02 +0000</pubDate><guid>https://bridgeli.cn/posts/2024-05-03-%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E6%B5%81-inputstream-%E7%9A%84-httpservletrequest/</guid><description>&lt;p&gt;之前在某公司工作的时候，领导要求所有前端向后端传递的参数都要经过前端加密，后端解密。说一句题外话：个人认为这种操作纯属脱裤子放屁，没啥用。因为前端代码都是公开的，无论你采用对称加密、非对称加密，或者摘要算法验签等等，对于稍懂技术的人来说，稍稍分析一下就能找到前端加密的方法，然后直接用相同的方式加密就行，所以这就是障眼法，只能骗骗不懂技术的人。不过领导的要求吗，既然定下来了，那么我们总要服从。因为每个方法都需要有这个解密或者验签的过程，我们自然而然想要到了通过 Filter、Interceptor 或者 AOP 等技术统一来做，不可能在各个方法中做这件事，在但是我们都知道，对于 post、put 等请求，参数都是放在请求体中的，需要通过流读出来，而流是不可以重复读的，所以我们应该怎么来解决这个问题，来构造一个可以重复读流 InputStream 的 HttpServletRequest。&lt;/p&gt;
&lt;p&gt;解决方法：使用自定义类来缓存 stream 即可 RequestWrapper 类：缓存字节数据&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.filter;
import cn.bridgeli.utils.http.HttpHelper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 构建可重复读取inputStream的request
*
* @author BridgeLi
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request) {
super(request);
body = HttpHelper.getBodyString(request).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public int available() throws IOException {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
package cn.bridgeli.utils.http;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 通用http工具封装
*
* @author BridgeLi
*/
public class HttpHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);
public static String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
try (InputStream inputStream = request.getInputStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = &amp;#34;&amp;#34;;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
LOGGER.error(&amp;#34;getBodyString出现问题！&amp;#34;, e);
}
return sb.toString();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后，可以在 Servlet 或 Filter 中使用 RepeatableFilter 替换原始的 HttpServletRequest。&lt;/p&gt;</description></item><item><title>javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target</title><link>https://bridgeli.cn/posts/2024-04-21-javax-net-ssl-sslhandshakeexception-sun-security-validator-validatorexception-pkix-path-building-failed-sun-security-provider-certpath-suncertpathbuilderexception-unable-to-find-valid-certificatio/</link><pubDate>Sun, 21 Apr 2024 05:46:06 +0000</pubDate><guid>https://bridgeli.cn/posts/2024-04-21-javax-net-ssl-sslhandshakeexception-sun-security-validator-validatorexception-pkix-path-building-failed-sun-security-provider-certpath-suncertpathbuilderexception-unable-to-find-valid-certificatio/</guid><description>&lt;p&gt;自从 ChatGPT 横空出世之后，突然没了写博客的兴趣，因为无论有什么问题，随便往 ChatGPT 一帖，结果就哗哗的出来了，今天这个问题就是。前一段时间和某公司联调接口，对方的接口测试环境好好的，一上线调不通，报错信息就是标题，只见贴到 ChatGPT，ChatGPT 回答如下：&lt;/p&gt;
&lt;p&gt;这个错误通常是由于缺少信任证书或证书链不完整导致的。解决方法通常有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**添加证书到信任库**：获取正确的证书，并将其添加到 Java 的信任库中。可以使用 `keytool` 工具来完成此操作。具体步骤如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;– 首先，使用以下命令导出证书：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;keytool -printcert -rfc -sslserver &amp;lt;hostname&amp;gt; -port &amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中 &lt;code&gt;&amp;lt;hostname&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;port&amp;gt;&lt;/code&gt; 是你要访问的 HTTPS 接口的主机名和端口号。&lt;/p&gt;
&lt;p&gt;– 然后将证书导出到一个文件中。&lt;/p&gt;
&lt;p&gt;– 最后，使用以下命令将证书添加到 Java 的信任库中：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;keytool -importcert -file &amp;lt;certificate-file&amp;gt; -keystore &amp;lt;path-to-truststore&amp;gt; -alias &amp;lt;alias&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中 &lt;code&gt;&amp;lt;certificate-file&amp;gt;&lt;/code&gt; 是你导出的证书文件，&lt;code&gt;&amp;lt;path-to-truststore&amp;gt;&lt;/code&gt; 是 Java 信任库的路径，&lt;code&gt;&amp;lt;alias&amp;gt;&lt;/code&gt; 是证书的别名。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;**忽略证书验证**：如果你确定你要连接的服务器是安全的，并且不需要严格的证书验证，你可以在 Java 代码中忽略证书验证。但这不是推荐的做法，因为可能会导致安全风险。你可以使用 `TrustManager` 接口实现自定义的信任管理器，然后将其应用于 SSL 连接。下面是一个简单的示例代码：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import javax.net.ssl.*;
import java.security.cert.X509Certificate;
public class SSLCertificateIgnore {
public static void main(String[] args) throws Exception {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
// Install the all-trusting trust manager
SSLContext sc = SSLContext.getInstance(&amp;#34;SSL&amp;#34;);
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// Now you can access the HTTPS endpoint without certificate validation
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;请注意，这种方法会降低安全性，因为它会使你的应用程序容易受到中间人攻击。因此，最好的做法是第一种方法，即添加正确的证书到信任库中。&lt;/p&gt;</description></item><item><title>AntPathMatcher 路径匹配</title><link>https://bridgeli.cn/posts/2022-03-27-antpathmatcher-%E8%B7%AF%E5%BE%84%E5%8C%B9%E9%85%8D/</link><pubDate>Sun, 27 Mar 2022 03:09:39 +0000</pubDate><guid>https://bridgeli.cn/posts/2022-03-27-antpathmatcher-%E8%B7%AF%E5%BE%84%E5%8C%B9%E9%85%8D/</guid><description>&lt;p&gt;公司项目使用 AntPathMatcher 路径匹配是否登陆，之前没有接触过，刚好趁这次机会学习了一番。&lt;/p&gt;
&lt;p&gt;一、基本规则&lt;/p&gt;
&lt;p&gt;1、? 匹配一个字符（除过操作系统默认的文件分隔符）&lt;br&gt;
2、* 匹配0个或多个字符&lt;br&gt;
3、** 匹配0个或多个目录&lt;br&gt;
4、{spring:[a-z]+} 将正则表达式 [a-z]+ 匹配到的值，赋值给名为 spring 的路径变量&lt;/p&gt;
&lt;p&gt;PS：必须是完全匹配才行，在 SpringMVC 中只有完全匹配才会进入 controller 层的方法&lt;/p&gt;
&lt;p&gt;二、注意事项：&lt;/p&gt;
&lt;p&gt;1、匹配文件路径，需要匹配某目录下及其各级子目录下所有的文件，使用 /&lt;em&gt;*/* 而非 *.*，因为有的文件不一定含有文件后缀&lt;br&gt;
2、匹配文件路径，使用 AntPathMatcher 创建一个对象时，需要注意 AntPathMatcher 也有有参构造，传递路径分隔符参数 pathSeparator，对于文件路径的匹配来说，可以根据不同的操作系统来传递各自的文件分隔符，以此防止匹配文件路径错误&lt;br&gt;
3、最长匹配规则（has more characters），即越精确的模式越会被优先匹配到。例如，URL请求 /app/dir/file.jsp，现在存在两个路径匹配模式 /&lt;/em&gt;*/*.jsp 和 /app/dir/*.jsp，那么会根据模式 /app/dir/*.jsp 来匹配&lt;/p&gt;
&lt;p&gt;三、实例&lt;/p&gt;
&lt;p&gt;可以参考若依框架：com.ruoyi.gateway.filter.AuthFilter 和 com.ruoyi.gateway.filter.XssFilter&lt;/p&gt;</description></item><item><title>身份证校验方法</title><link>https://bridgeli.cn/posts/2022-01-23-%E8%BA%AB%E4%BB%BD%E8%AF%81%E6%A0%A1%E9%AA%8C%E6%96%B9%E6%B3%95/</link><pubDate>Sun, 23 Jan 2022 08:49:35 +0000</pubDate><guid>https://bridgeli.cn/posts/2022-01-23-%E8%BA%AB%E4%BB%BD%E8%AF%81%E6%A0%A1%E9%AA%8C%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;我国的身份证编制是有标准的，每一位都不是随便瞎写的，就像我国的地图坐标经纬度一样，并不是真是的经纬度，而是人为加入了偏转，被称为：火星坐标系，但是工作中发现很多人并不了解，在工作中，用户输入的身份证号是否正确，我们根据这个规则是可以做初步校验的，当然真是的校验肯定是要通过公安部授权的接口，这是收费的。但是初步校验是真简单的，我个人发现有些系统并没有加入，所以今天写一篇小文章，做一个常用的工具类来校验身份证号，至于具体的规则，大家可以搜一下这个国标：GB11643-1999，代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.junit.Test;
/**
* @author BridgeLi
* @date 2022/1/23 15:01
*/
public class IdNoUtil {
@Test
public void testId() {
String IdNo = &amp;#34;&amp;#34;;
boolean b = validateIdNo(IdNo);
Assert.assertTrue(b);
}
public static boolean validateIdNo(String IdNo) {
if (StringUtils.isBlank(IdNo) || IdNo.length() != 18) {
return false;
}
char[] charArray = IdNo.toCharArray();
//前十七位加权因子
int[] idCardWi = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
//这是除以11后，可能产生的11位余数对应的验证码
String[] idCardY = {&amp;#34;1&amp;#34;, &amp;#34;0&amp;#34;, &amp;#34;X&amp;#34;, &amp;#34;9&amp;#34;, &amp;#34;8&amp;#34;, &amp;#34;7&amp;#34;, &amp;#34;6&amp;#34;, &amp;#34;5&amp;#34;, &amp;#34;4&amp;#34;, &amp;#34;3&amp;#34;, &amp;#34;2&amp;#34;};
int sum = 0;
for (int i = 0; i &amp;lt; 17; i++) {
int current = Integer.parseInt(String.valueOf(charArray[i]));
int count = current * idCardWi[i];
sum += count;
}
char idCardLast = charArray[17];
int idCardMod = sum % 11;
if (idCardY[idCardMod].equalsIgnoreCase(String.valueOf(idCardLast))) {
return true;
} else {
return false;
}
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>JWT 实际应用例子</title><link>https://bridgeli.cn/posts/2021-12-26-jwt-%E5%AE%9E%E9%99%85%E5%BA%94%E7%94%A8%E4%BE%8B%E5%AD%90/</link><pubDate>Sun, 26 Dec 2021 08:24:00 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-12-26-jwt-%E5%AE%9E%E9%99%85%E5%BA%94%E7%94%A8%E4%BE%8B%E5%AD%90/</guid><description>&lt;p&gt;JWT 是什么，很多网站都有例子，但是如何使用，却不是很多，今天就介绍一个很具体的、能在项目中实际应用的例子。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;pom&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;0.9.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.auth0&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;java-jwt&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.4.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;Java 代码&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JwtUtil {
static String key = &amp;#34;key&amp;#34;;
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法 私匙使用用户密码
*
* @param ttlMillis jwt过期时间
* @param user 登录成功的user对象
* @return
*/
public static String createJWT(long ttlMillis, User user) {
//指定签名的时候使用的签名算法，也就是header那部分，jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明（根据特定的业务需要添加，如果要拿这个做验证，一般是需要和jwt的接收方提前沟通好验证方式的）
Map&amp;lt;String, Object&amp;gt; claims = new HashMap&amp;lt;&amp;gt;();
// claims.put(&amp;#34;id&amp;#34;, user.getId());
claims.put(&amp;#34;username&amp;#34;, user.getUsername());
// claims.put(&amp;#34;password&amp;#34;, user.getPassword());
//生成签发人
String subject = user.getUsername();
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder，设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明，一定要先设置这个自己创建的私有的声明，这个是给builder的claim赋值，一旦写在标准的声明赋值之后，就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID)：是JWT的唯一标识，根据业务需要，这个可以设置为一个不重复的值，主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
//iat: jwt的签发时间
.setIssuedAt(now)
//代表这个JWT的主体，即它的所有人，这个是一个json格式的字符串，可以存放什么userid，roldid之类的，作为什么用户的唯一标志。
.setSubject(subject)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, key);
if (ttlMillis &amp;gt;= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* Token的解密
*
* @param token 加密后的token
* @param user 用户的对象
* @return
*/
public static Claims parseJWT(String token, User user) {
//签名秘钥，和生成的签名的秘钥一模一样
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校验token
* 在这里可以使用官方的校验，我这里校验的是token中携带的密码于数据库一致的话就校验通过
*
* @param token
* @param user
* @return
*/
public static Boolean isVerify(String token, User user) {
//签名秘钥，和生成的签名的秘钥一模一样
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
if (claims.get(&amp;#34;username&amp;#34;).equals(user.getUsername())) {
return true;
}
return false;
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>关于 CPU 的缓存的证明和应用</title><link>https://bridgeli.cn/posts/2021-11-29-%E5%85%B3%E4%BA%8E-cpu-%E7%9A%84%E7%BC%93%E5%AD%98%E7%9A%84%E8%AF%81%E6%98%8E%E5%92%8C%E5%BA%94%E7%94%A8/</link><pubDate>Mon, 29 Nov 2021 14:32:58 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-11-29-%E5%85%B3%E4%BA%8E-cpu-%E7%9A%84%E7%BC%93%E5%AD%98%E7%9A%84%E8%AF%81%E6%98%8E%E5%92%8C%E5%BA%94%E7%94%A8/</guid><description>&lt;ol&gt;
&lt;li&gt;证明：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先，我们都知道现在的 CPU 多核技术，同时会有三级缓存（L1，L2，L3 ），如图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://www.bridgeli.cn/wp-content/uploads/2021/11/cache.architecture-300x153.png" alt="" width="300" height="153" class="alignnone size-medium wp-image-730" /&gt;
&lt;p&gt;缓存基本上来说就是把后面的数据加载到离自己近的地方，对于 CPU 来说，是一个字节一个字节的加载数据的吗？其实不是的，一般来说都是要一块一块的加载的，对于这样的一块一块的数据单位，我们叫做“Cache Line”，中文翻译：缓存行，一般来说，一个主流的 CPU 的 Cache Line 是 64 Bytes，也就是 8 个 64 位的整型，这就是 CPU 从内存中捞数据上来的最小数据单位。那么这个如何证明呢？&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.util.concurrent.CountDownLatch;
/**
* @author BridgeLi
* @date 2021/11/29 20:41
*/
public class CacheLineTest {
private static long loop = 1_0000_0000L;
private static class T {
// private volatile long x1, x2, x3, x4, x5, x6, x7;
private volatile long x = 0L;
// private volatile long x8, x9, x10, x11, x12, x13, x14;
}
private static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t1 = new Thread(() -&amp;gt; {
for (long i = 0; i &amp;lt; loop; i++) {
arr[0].x = i;
}
countDownLatch.countDown();
}, &amp;#34;t1&amp;#34;);
Thread t2 = new Thread(() -&amp;gt; {
for (long i = 0; i &amp;lt; loop; i++) {
arr[1].x = i;
}
countDownLatch.countDown();
}, &amp;#34;t2&amp;#34;);
long currentTimeMillis = System.currentTimeMillis();
t1.start();
t2.start();
countDownLatch.await();
System.out.println(System.currentTimeMillis() &amp;amp;#8211; currentTimeMillis);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们定义了一个长度为 2 的数组，数组中的元素是 T 类型，T 有一个属性 x，我们同时启动两个线程分别给第一个元素和第二个元素中的 x 复制从 0 到一亿减 1，这个时候我们测试他耗时多少，不同的电脑配置肯定是不同的，我的电脑大概是四千多毫秒，然后我们把 T 对象中属性 x 前后各被注释调的一行打开再跑一次看看，变成了大概 700 毫秒，相差整整 6 倍！这是为何？&lt;/p&gt;</description></item><item><title>神奇的 (a == (Integer) 1 &amp;&amp; a == (Integer) 2 &amp;&amp; a == (Integer) 3) = true</title><link>https://bridgeli.cn/posts/2021-10-31-%E7%A5%9E%E5%A5%87%E7%9A%84-a-integer-1-a-integer-2-a-integer-3-true/</link><pubDate>Sun, 31 Oct 2021 03:24:36 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-10-31-%E7%A5%9E%E5%A5%87%E7%9A%84-a-integer-1-a-integer-2-a-integer-3-true/</guid><description>&lt;p&gt;前一段时间看了一篇文章 (a == (Integer) 1 &amp;amp;&amp;amp; a == (Integer) 2 &amp;amp;&amp;amp; a == (Integer) 3) 是否可以为 true，当时第一反应怎么可能，谁知道再往下看，作者竟然给出来如下代码，一运行神奇的事出现了，真的为 true，代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.lang.reflect.Field;
public class Magic {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class cache = Integer.class.getDeclaredClasses()[0];
Field c = cache.getDeclaredField(&amp;#34;cache&amp;#34;);
c.setAccessible(true);
Integer[] array = (Integer[]) c.get(cache);
// array[129] is 1
array[130] = array[129];
// Set 2 to be 1
array[131] = array[129];
// Set 3 to be 1
Integer a = 1;
if (a == (Integer) 1 &amp;amp;&amp;amp; a == (Integer) 2 &amp;amp;&amp;amp; a == (Integer) 3) {
System.out.println(true);
} else {
System.out.println(false);
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为作者没有给出解释，所以就研究了一番，发现需要基础非常扎实才能写出这段代码，这段代码之所以为 true，要理解如下几个问题：&lt;/p&gt;</description></item><item><title>GeoHash 算法的 Java 版实现</title><link>https://bridgeli.cn/posts/2021-09-25-geohash-%E7%AE%97%E6%B3%95%E7%9A%84-java-%E7%89%88%E5%AE%9E%E7%8E%B0/</link><pubDate>Sat, 25 Sep 2021 06:02:54 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-09-25-geohash-%E7%AE%97%E6%B3%95%E7%9A%84-java-%E7%89%88%E5%AE%9E%E7%8E%B0/</guid><description>&lt;p&gt;之前曾经做过一个类 LBS 的小需求，当时是用 redis 做的，就是&lt;a href="http://www.bridgeli.cn/archives/618"&gt;这篇文章&lt;/a&gt;，其实 GeoHash 算法，我们也可以自己实现，具体如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
public class GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;
private static int numbits = 5 * 5; //经纬度单独编码长度
private static double minLat;
private static double minLng;
private final static char[] digits = {&amp;amp;#8216;0&amp;amp;#8217;, &amp;amp;#8216;1&amp;amp;#8217;, &amp;amp;#8216;2&amp;amp;#8217;, &amp;amp;#8216;3&amp;amp;#8217;, &amp;amp;#8216;4&amp;amp;#8217;, &amp;amp;#8216;5&amp;amp;#8217;, &amp;amp;#8216;6&amp;amp;#8217;, &amp;amp;#8216;7&amp;amp;#8217;, &amp;amp;#8216;8&amp;amp;#8217;,
&amp;amp;#8216;9&amp;amp;#8217;, &amp;amp;#8216;b&amp;amp;#8217;, &amp;amp;#8216;c&amp;amp;#8217;, &amp;amp;#8216;d&amp;amp;#8217;, &amp;amp;#8216;e&amp;amp;#8217;, &amp;amp;#8216;f&amp;amp;#8217;, &amp;amp;#8216;g&amp;amp;#8217;, &amp;amp;#8216;h&amp;amp;#8217;, &amp;amp;#8216;j&amp;amp;#8217;, &amp;amp;#8216;k&amp;amp;#8217;, &amp;amp;#8216;m&amp;amp;#8217;, &amp;amp;#8216;n&amp;amp;#8217;, &amp;amp;#8216;p&amp;amp;#8217;,
&amp;amp;#8216;q&amp;amp;#8217;, &amp;amp;#8216;r&amp;amp;#8217;, &amp;amp;#8216;s&amp;amp;#8217;, &amp;amp;#8216;t&amp;amp;#8217;, &amp;amp;#8216;u&amp;amp;#8217;, &amp;amp;#8216;v&amp;amp;#8217;, &amp;amp;#8216;w&amp;amp;#8217;, &amp;amp;#8216;x&amp;amp;#8217;, &amp;amp;#8216;y&amp;amp;#8217;, &amp;amp;#8216;z&amp;amp;#8217;};
//定义编码映射关系
final static HashMap&amp;lt;Character, Integer&amp;gt; lookup = new HashMap&amp;lt;Character, Integer&amp;gt;();
//初始化编码映射内容
static {
int i = 0;
for (char c : digits) {
lookup.put(c, i++);
}
}
public GeoHash() {
setMinLatLng();
}
public String encode(double lat, double lon) {
BitSet latbits = getBits(lat, -90, 90);
BitSet lonbits = getBits(lon, -180, 180);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i &amp;lt; numbits; i++) {
buffer.append((lonbits.get(i)) ? &amp;amp;#8216;1&amp;amp;#8217; : &amp;amp;#8216;0&amp;amp;#8217;);
buffer.append((latbits.get(i)) ? &amp;amp;#8216;1&amp;amp;#8217; : &amp;amp;#8216;0&amp;amp;#8217;);
}
String code = base32(Long.parseLong(buffer.toString(), 2));
//Log.i(&amp;#34;okunu&amp;#34;, &amp;#34;encode lat = &amp;#34; + lat + &amp;#34; lng = &amp;#34; + lon + &amp;#34; code = &amp;#34; + code);
return code;
}
public ArrayList&amp;lt;String&amp;gt; getAroundGeoHash(double lat, double lon) {
//Log.i(&amp;#34;okunu&amp;#34;, &amp;#34;getArroundGeoHash lat = &amp;#34; + lat + &amp;#34; lng = &amp;#34; + lon);
ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
double uplat = lat + minLat;
double downLat = lat &amp;amp;#8211; minLat;
double leftlng = lon &amp;amp;#8211; minLng;
double rightLng = lon + minLng;
String leftUp = encode(uplat, leftlng);
list.add(leftUp);
String leftMid = encode(lat, leftlng);
list.add(leftMid);
String leftDown = encode(downLat, leftlng);
list.add(leftDown);
String midUp = encode(uplat, lon);
list.add(midUp);
String midMid = encode(lat, lon);
list.add(midMid);
String midDown = encode(downLat, lon);
list.add(midDown);
String rightUp = encode(uplat, rightLng);
list.add(rightUp);
String rightMid = encode(lat, rightLng);
list.add(rightMid);
String rightDown = encode(downLat, rightLng);
list.add(rightDown);
//Log.i(&amp;#34;okunu&amp;#34;, &amp;#34;getArroundGeoHash list = &amp;#34; + list.toString());
return list;
}
//根据经纬度和范围，获取对应的二进制
private BitSet getBits(double lat, double floor, double ceiling) {
BitSet buffer = new BitSet(numbits);
for (int i = 0; i &amp;lt; numbits; i++) {
double mid = (floor + ceiling) / 2;
if (lat &amp;gt;= mid) {
buffer.set(i);
floor = mid;
} else {
ceiling = mid;
}
}
return buffer;
}
//将经纬度合并后的二进制进行指定的32位编码
private String base32(long i) {
char[] buf = new char[65];
int charPos = 64;
boolean negative = (i &amp;lt; 0);
if (!negative) {
i = -i;
}
while (i &amp;lt;= -32) {
buf[charPos&amp;amp;#8211;] = digits[(int) (-(i % 32))];
i /= 32;
}
buf[charPos] = digits[(int) (-i)];
if (negative) {
buf[&amp;amp;#8211;charPos] = &amp;amp;#8216;-&amp;amp;#8216;;
}
return new String(buf, charPos, (65 &amp;amp;#8211; charPos));
}
private void setMinLatLng() {
minLat = MAXLAT &amp;amp;#8211; MINLAT;
for (int i = 0; i &amp;lt; numbits; i++) {
minLat /= 2.0;
}
minLng = MAXLNG &amp;amp;#8211; MINLNG;
for (int i = 0; i &amp;lt; numbits; i++) {
minLng /= 2.0;
}
}
//根据二进制和范围解码
private double decode(BitSet bs, double floor, double ceiling) {
double mid = 0;
for (int i = 0; i &amp;lt; bs.length(); i++) {
mid = (floor + ceiling) / 2;
if (bs.get(i)) {
floor = mid;
} else {
ceiling = mid;
}
}
return mid;
}
//对编码后的字符串解码
public double[] decode(String geohash) {
StringBuilder buffer = new StringBuilder();
for (char c : geohash.toCharArray()) {
int i = lookup.get(c) + 32;
buffer.append(Integer.toString(i, 2).substring(1));
}
BitSet lonset = new BitSet();
BitSet latset = new BitSet();
//偶数位，经度
int j = 0;
for (int i = 0; i &amp;lt; numbits * 2; i += 2) {
boolean isSet = false;
if (i &amp;lt; buffer.length()) {
isSet = buffer.charAt(i) == &amp;amp;#8216;1&amp;amp;#8217;;
}
lonset.set(j++, isSet);
}
//奇数位，纬度
j = 0;
for (int i = 1; i &amp;lt; numbits * 2; i += 2) {
boolean isSet = false;
if (i &amp;lt; buffer.length()) {
isSet = buffer.charAt(i) == &amp;amp;#8216;1&amp;amp;#8217;;
}
latset.set(j++, isSet);
}
double lon = decode(lonset, -180, 180);
double lat = decode(latset, -90, 90);
return new double[]{lat, lon};
}
public static void main(String[] args) {
GeoHash geohash = new GeoHash();
String s = geohash.encode(39.923201, 116.390705);
System.out.println(&amp;#34;geohash：&amp;#34; + s);
ArrayList&amp;lt;String&amp;gt; aroundGeoHash = geohash.getAroundGeoHash(39.923201, 116.390705);
for (String s1 : aroundGeoHash) {
System.out.println(&amp;#34;aroundGeoHash：&amp;#34; + s1);
}
double[] geo = geohash.decode(s);
System.out.println(geo[0] + &amp;#34; &amp;#34; + geo[1]);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;参考：https://www.jianshu.com/p/2fd0cf12e5ba&lt;/p&gt;</description></item><item><title>Spring boot 自动装配实现的原理 – 文字简述版</title><link>https://bridgeli.cn/posts/2021-08-29-spring-boot-%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%8E%9F%E7%90%86-%E6%96%87%E5%AD%97%E7%AE%80%E8%BF%B0%E7%89%88/</link><pubDate>Sun, 29 Aug 2021 03:34:29 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-08-29-spring-boot-%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%8E%9F%E7%90%86-%E6%96%87%E5%AD%97%E7%AE%80%E8%BF%B0%E7%89%88/</guid><description>&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当启动 Spring boot 应用程序的时候，会先创建 SpringApplication 的对象，在对象的构造方法中会进行某些参数的初始化工作，最主要的是判断当前应用程序的类型以及初始化器和监听器，在这个过程中会加载整个应用程序的 spring.factories 文件，将文件的内容放到缓存对象中，方便后续获取。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SpringApplication 对象创建完成之后，开始执行 run 方法，来完成整个启动，启动过程中最主要的有两个方法，第一个叫做 prepareContext，第二个叫做 refreshContext，在这两个关键步骤中完成了自动装配的核心功能，前面的处理逻辑包含了上下文对象的创建，banner 的打印，异常报告器的准备等各个准备工作，方便后续来进行调用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 prepareContext 方法中主要完成的是对上下文对象的初始化操作，包含了属性值的设置，比如环境对象，在整个过程中有一个非常重要的方法，叫做 load，load 主要完成一件事，将当前启动类作为一个 beanDefinition 注册到 registry 中，方便后续在进行 BeanFactoryPostProcessor 调用执行的时候，找到对应的主类，来完成 @SpringBootApplication、@EnableAutoConfiguration 等注解的解析工作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 refreshContext 方法中会进行整个容器的刷新过程，会调用 Spring 中的 refresh 方法，refresh 中有 13 个非常关键的方法，来完成整个 Spring 应用程序的启动，在自动装配过程中，会调用 invokeBeanFactoryPostProcessor 方法，在此方法中主要对 ConfigurationClassPostProcessor 类的处理，他是 BeanFactoryPostProcessor 的子类也是，BeanDefinitionRegistryPostProcessor 的子类，在调用的时候会先调用 BeanDefinitionRegistryPostProcessor 中的 postProcessBeanDefinitionRegistry 方法，然后调用 BeanFactoryPostProcessor 中的 postProcessBeanFactory 方法，在执行 postProcessBeanDefinitionRegistry 方法的时候会解析处理各种注解，包含 @PropertySource、@ComponentScan、@ComponentScans、@Bean、@Import 等注解，最主要的是 @Import 注解的解析。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在解析 @Import 注解的时候，会有一个 getImport 的方法，从主类开始递归解析注解，把所有包含 @Import 的注解都解析道，然后在 processImport 方法中对 Import 的类进行分类，此处最主要的是识别 AutoConfigurationImportSelect 归属于 ImportSelect 的子类，在后续过程中会调用 deferredImportSelectorHandler 中的 process 方法，来完善 EnableAutoConfiguration 的加载。&lt;/p&gt;</description></item><item><title>好用的 IDEA 插件</title><link>https://bridgeli.cn/posts/2021-07-17-%E5%A5%BD%E7%94%A8%E7%9A%84-idea-%E6%8F%92%E4%BB%B6/</link><pubDate>Sat, 17 Jul 2021 12:47:57 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-07-17-%E5%A5%BD%E7%94%A8%E7%9A%84-idea-%E6%8F%92%E4%BB%B6/</guid><description>&lt;p&gt;好久没有好好的写过博客了，不过这一篇也没打算好好写。前一段时间换工作，所谓工欲善其事，必先利其器，所以常用的软件都需要重新配置，而作为一名 Java 程序员，最重要的就是 IDEA 了，所以这次就写一下，我个人认为比较好用的 IDEA 插件，以及他们的作用，后面如果发现更好用的插件了，也会在这篇文章里面更新。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;IDE Eval Reset，开发者：zhile.io&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;大家众所周知的，IDEA 很好用，但是付费软件，还不便宜，但是在国内很多人都是找各种方法破解，人家也在做反破解，一直搞攻防战，但是 IDEA 有一个很人性的一点，可以试用 30 天，所以这个插件就是让大家无限试用。&lt;/p&gt;
&lt;p&gt;安装完成之后，在 Help 菜单下面会多一个：Eval Rest 的子菜单，就可以重制 30 天的有效期，当然也可以选上右下角的：Auto reset before per restart，啥功能不用说了吧。&lt;/p&gt;
&lt;p&gt;最后多说一句：请大家最好不要滥用此功能，最好还是支持正版。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Maven Helper，开发者：Vojtech Krasa&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在我刚工作的时候的那个年代，我们 build 还用 Ant，现在可能很多人都没听说过了，后来才开始用 maven，但无论无论如何都还有一个巨大的问题，jar 包冲突，有时会出莫名其妙的问题，所以这个插件就是用来分析 maven 项目的 jar 冲突的。&lt;/p&gt;
&lt;p&gt;装上这个插件之后，在 pom 文件左下方会多一个：Dependency Analyzer 的子菜单，点一下，就可以看到那些 jar 冲突了，然后选中，在右侧排除掉即可。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;GsonFormatPlus，开发者：mars-men&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在我们的工作中，使用第三方的接口，现在数据一般都是用 json，所以不可避免的要用对象和 json 的互转，而我们根据 json 写对象的时候，自己一行一行的写，不仅容易错，而且还没有效率，唯一的好处就是锻炼大家打字的说平，所以这个插件应运而生了，他可以很轻松的根据 json 数据，生成实体类。&lt;/p&gt;
&lt;p&gt;安装完成后，我们只需要新建一个实体类，然后在该类中，摁下 option + s 键，然后把 json 数据 copy 到那个框里面，就可以直接生成相应的实体类了，巨方便快捷。&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;any-rule，开发者：any-rule&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在我们工作中，自从有了正则表达式，我们的工作量可能减轻了很多，但很多时候写正则表达式也挺头疼的，最起码我是这样的，老是写不对。所以这个插件就是帮我们生成一些常见的正则表达式。&lt;/p&gt;
&lt;p&gt;安装完成后，你只需要摁下 option + a，然后就可以看到一些常见的正则表达式，选择合适的即可。&lt;/p&gt;</description></item><item><title>再谈 ThreadLocal</title><link>https://bridgeli.cn/posts/2021-04-22-%E5%86%8D%E8%B0%88-threadlocal/</link><pubDate>Thu, 22 Apr 2021 09:49:30 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-04-22-%E5%86%8D%E8%B0%88-threadlocal/</guid><description>&lt;p&gt;几年前我曾经写过两篇关于 ThreadLocal 的文章，分别是&lt;a href="http://www.bridgeli.cn/archives/366"&gt;ThreadLocal类之简单理解&lt;/a&gt;和&lt;a href="http://www.bridgeli.cn/archives/367"&gt;ThreadLocal类之简单应用示例&lt;/a&gt;，不过限于当时的水平，有些问题并没有说的很明白，所以今天再写一篇文章，重新说说这个类。&lt;/p&gt;
&lt;p&gt;我们首先看一个例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
/**
* @author BridgeLi
* @date 2021/4/21 11:02
*/
public class User {
String name = &amp;#34;Denny&amp;#34;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后我们有一个操作：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import org.junit.Test;
/**
* @author BridgeLi
* @date 2021/4/21 10:28
*/
public class ThreadTest {
private User user = new User();
@Test
public void testThreadLocal() {
new Thread(() -&amp;gt; {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(user.name);
}).start();
new Thread(() -&amp;gt; user.name = &amp;#34;BridgeLi&amp;#34;).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个时候我们就知道一定会有线程安全问题，所以我们怎么解决这个问题呢？就是 ThreadLocal，请看下面：&lt;/p&gt;</description></item><item><title>以 Java 为例简单说明常见 IO 模型</title><link>https://bridgeli.cn/posts/2021-03-30-%E4%BB%A5-java-%E4%B8%BA%E4%BE%8B%E7%AE%80%E5%8D%95%E8%AF%B4%E6%98%8E%E5%B8%B8%E8%A7%81-io-%E6%A8%A1%E5%9E%8B/</link><pubDate>Tue, 30 Mar 2021 13:35:01 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-03-30-%E4%BB%A5-java-%E4%B8%BA%E4%BE%8B%E7%AE%80%E5%8D%95%E8%AF%B4%E6%98%8E%E5%B8%B8%E8%A7%81-io-%E6%A8%A1%E5%9E%8B/</guid><description>&lt;ol&gt;
&lt;li&gt;BIO&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们先看一个 Java 例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author bridgeli
*/
public class SocketBIO {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9090, 20);
System.out.println(&amp;#34;step1: new ServerSocket(9090) &amp;#34;);
while (true) {
Socket client = server.accept();
System.out.println(&amp;#34;step2:client: &amp;#34; + client.getPort());
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = client.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream));
while (true) {
String dataLine = reader.readLine(); //阻塞2
if (null != dataLine) {
System.out.println(dataLine);
} else {
client.close();
break;
}
}
System.out.println(&amp;#34;客户端断开&amp;#34;);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null!= inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;BIO 是最初始的 IO 模型，该模型有两个大问题：1. accept 是阻塞的；2. read 也是阻塞的，也就是说我们的服务器起来之后，首先会在 accept 处阻塞，等待客户端连接，但有一个客户端连接的时候，我们可以从客户端处读取数据，这个时候也是阻塞的，所以我们的系统只能是单连接的，当有多个客户端连接的时候，只能一个一个的排着队连接，然后从客户端中读取数据，为了实现多连接，这就要求我们必须启用线程来解决，最开始等待客户端连接，然后有一个客户端连上了之后，启动一个线程读取客户端的数据，然后主线程继续等待客户端连接。&lt;/p&gt;</description></item><item><title>Java 的引用类型和使用场景</title><link>https://bridgeli.cn/posts/2021-02-28-java-%E7%9A%84%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B%E5%92%8C%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF/</link><pubDate>Sun, 28 Feb 2021 08:26:07 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-02-28-java-%E7%9A%84%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B%E5%92%8C%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF/</guid><description>&lt;p&gt;每种编程语言都有自己操作内存中元素的方式，例如在 C 和 C++ 里是通过指针，而在 Java 中则是通过“引用”。在 JDK.1.2 之后，Java 对引用的概念进行了扩充，将引用分为了：强引用（Strong Reference）、软引用（Soft Reference）、弱引用（Weak Reference）、虚引用（Phantom Reference）4 种，这 4 种引用的强度依次减弱，今天这篇文章就简单介绍一下这四种类型，并简单说一下他们的使用场景。&lt;/p&gt;
&lt;p&gt;1， 强引用（Strong Reference）&lt;/p&gt;
&lt;p&gt;强引用类型，是我们最常讲的一个类型，我们先看一个例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.reference;
/**
* @author BridgeLi
* @date 2021/2/26 10:02
*/
public class User {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(&amp;#34;finalize&amp;#34;);
}
}
package cn.bridgeli.demo.reference;
import org.junit.Test;
/**
* @author BridgeLi
* @date 2021/2/26 10:03
*/
public class StrongReferenceTest {
@Test
public void testStrongReference() {
User user = new User();
user = null;
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们都知道当一个实例对象具有强引用时，垃圾回收器不会回收该对象，当内存不足时，宁愿 OOM，也就是抛出 OutOfMemeryError 异常也不会回收强引用的对象，因为 JVM 认为强引用的对象是用户正在使用的对象，它无法分辨出到底该回收哪个，强行回收有可能导致系统严重错误。但是当对象被赋值为 null 之后，会被回收，并且会执行对象的 finalize 函数，此时我们可以通过该函数拯救自己，但是有两点需要注意一个是只能拯救一次，当再次被垃圾回收的时候就不能拯救了，另一个就是有事没事千万不要重写次函数，本例只是为了说明问题重写了此函数，如果在工作中误重写了此函数，可能会导致垃圾不能回收，最终 OOM，另外有熟悉 GC 的同学没？猜一下我为什么要 sleep 一下？&lt;/p&gt;</description></item><item><title>用两个线程交替打印数字和字母</title><link>https://bridgeli.cn/posts/2021-02-07-%E7%94%A8%E4%B8%A4%E4%B8%AA%E7%BA%BF%E7%A8%8B%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0%E6%95%B0%E5%AD%97%E5%92%8C%E5%AD%97%E6%AF%8D/</link><pubDate>Sun, 07 Feb 2021 07:06:13 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-02-07-%E7%94%A8%E4%B8%A4%E4%B8%AA%E7%BA%BF%E7%A8%8B%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0%E6%95%B0%E5%AD%97%E5%92%8C%E5%AD%97%E6%AF%8D/</guid><description>&lt;p&gt;前一段时间听马士兵老师讲课，讲到某公司的一个面试，两个线程，其中一个线程输出ABC，另一个线程输出123，如何控制两个线程交叉输出1A2B3C，由于本人多线程掌握的一直不是很好，所以听完这道题，个人感觉收获良多，这是一个学习笔记。这道题有多种解法，不过有些属于纯炫技，所以只记录常见的三种解法。首先看第一种&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;park 和 unpark&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
/**
* @author BridgeLi
* @date 2021/2/6 16:14
*/
public class Thread_Communication_Park_Unpark {
static Thread t1 = null;
static Thread t2 = null;
public static void main(String[] args) {
final List&amp;lt;Integer&amp;gt; integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
final List&amp;lt;String&amp;gt; strings = Lists.newArrayList(&amp;#34;A&amp;#34;, &amp;#34;B&amp;#34;, &amp;#34;C&amp;#34;, &amp;#34;D&amp;#34;, &amp;#34;E&amp;#34;, &amp;#34;F&amp;#34;, &amp;#34;G&amp;#34;);
t1 = new Thread(() -&amp;gt; integers.forEach(item -&amp;gt; {
System.out.print(item);
LockSupport.unpark(t2);
LockSupport.park();
}), &amp;#34;t1&amp;#34;);
t2 = new Thread(() -&amp;gt; strings.forEach(item -&amp;gt; {
LockSupport.park();
System.out.print(item);
LockSupport.unpark(t1);
}), &amp;#34;t2&amp;#34;);
t1.start();
t2.start();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个是最简单的实现方法，LockSupport.park() 使当前线程阻塞，而 LockSupport.unpark() 则表示唤醒一个线程，所以他需要一个参数，表示你要唤醒哪个线程，很好理解，也比较简单。&lt;/p&gt;</description></item><item><title>利用 DeferredResult 实现 http 轮询实时返回数据接口</title><link>https://bridgeli.cn/posts/2021-01-09-%E5%88%A9%E7%94%A8-deferredresult-%E5%AE%9E%E7%8E%B0-http-%E8%BD%AE%E8%AF%A2%E5%AE%9E%E6%97%B6%E8%BF%94%E5%9B%9E%E6%95%B0%E6%8D%AE%E6%8E%A5%E5%8F%A3/</link><pubDate>Sat, 09 Jan 2021 07:48:48 +0000</pubDate><guid>https://bridgeli.cn/posts/2021-01-09-%E5%88%A9%E7%94%A8-deferredresult-%E5%AE%9E%E7%8E%B0-http-%E8%BD%AE%E8%AF%A2%E5%AE%9E%E6%97%B6%E8%BF%94%E5%9B%9E%E6%95%B0%E6%8D%AE%E6%8E%A5%E5%8F%A3/</guid><description>&lt;p&gt;博客有半年没更新了，不是我偷懒，而是之前服务器到期了，开博客这么多年，钱其实花了不少，但是没有一点收益，所以上了谷歌广告，如果文章对你稍稍有一点帮助，希望能花一秒钟帮忙点一下广告，谢谢。&lt;/p&gt;
&lt;p&gt;今天这篇文章呢，不难，其实是解答我一直以来心里的一个疑问。是这样的，之前看五八技术委员会主席沈剑老师的公众号架构师之路的一篇文章：http 如何像 tcp 一样实时的收消息，里面其中的一个方案是用 http 短连接轮询的方式实现“伪长连接”。但是对于轮询，我们的第一反应肯定是有延时，但是标题不是说的是实时吗？当然我们可以把轮询的时长缩短一些，先不说这样大部分时间的轮询调用，可能都没消息返回，造成服务器资源浪费，轮询时间再短也是有延时啊，所以难道是伪实时？反正一般消息延时个三五秒，甚至十秒八秒一分钟，大家也不会在意，只会认为对方返回慢，对不起，这是我们程序员的锅，但是 http 真的不能实现实时吗？沈剑老师提出了一种方法：首选 webim 和 webserver 之间建立一条 http 连接，专门用作消息通道，这条连接叫 http 消息连接。然后会有如下处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有消息到达的时候，这个 http 消息连接将被夯住，不返回，由于 http 是短连接，这个 http 消息连接最多被夯住 90 秒，就会被断开（这是浏览器或者 webserver 的行为）；&lt;/li&gt;
&lt;li&gt;在 1 的情况下，如果 http 消息连接被断开，立马再发起一个 http 消息连接；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时在在 1 和 2 的配合下，浏览器与 webserver 之间将永远有一条消息连接在，然后还有一种情况&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;每次收到消息时，这个消息连接就能及时将消息带回浏览器页面，并且在返回后，会立马再发起一个 http 消息连接&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样就能做到使用 http 端连接轮询的方式实现了实时收消息。不过需要说明的是，其实还有一种情况：消息到达时，上一个 http 消息连接正在返回，也就是第二种情况的时候突然来了一个消息，此时没有 http 消息连接可用。虽然理论上 http 消息连接的返回是瞬时的，没有消息连接可用出现的概率极小，但是根据墨菲定律我们知道，这种情况肯定会出现，所以这种情况下我们可以将消息暂存入消息池中，下一个消息连接到达后，无需等待，直接去消息池中取消息，将将消息带回，然后立刻返回生成新的消息连接即可。&lt;/p&gt;
&lt;p&gt;以上过程，可以参考沈剑老师的公众号，链接：https://mp.weixin.qq.com/s/6BCucq6QsH8lfDGLtQCl2A&lt;/p&gt;
&lt;p&gt;不过以上都不是今天这篇文章的重点，和今天这篇文章的标题也没有任何关系。重点是当时看了沈剑老师的这篇文章后我一直有一个疑问：第一步的时候如何夯住？总不能 sleep 吧，这多不优雅啊，由于一直以为没有遇到过类似的需求，所以这么几年来我也没深究这个问题，但是心里确实一直记着，直到前一段时间，听马士兵教育的公开课，当时再讲类似的问题的时候提到了夯住 http 的连接（具体是哪个问题，还真不记得了），虽然当时上课的老师没提怎么实现，但是评论区我问了一下，如何夯住不返回？然后有一个同学回复说，用 DeferredResult，然后下课后搜了一下资料，果然可以，如下是实现的笔记，所以这才是重点，希望对有这个疑问的同学也有一点帮助。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;消息返回实体类，大家可以根据实际情况，自己定义即可：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.deferredresulttest.entity;
import lombok.Data;
import lombok.Getter;
/**
* @author bridgeli
*/
@Data
public class DeferredResultResponse {
private Integer code;
private String msg;
public enum Msg {
TIMEOUT(&amp;#34;超时&amp;#34;),
FAILED(&amp;#34;失败&amp;#34;),
SUCCESS(&amp;#34;成功&amp;#34;);
@Getter
private String desc;
Msg(String desc) {
this.desc = desc;
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;controller 接口：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.deferredresulttest.controller;
import cn.bridgeli.deferredresulttest.entity.DeferredResultResponse;
import cn.bridgeli.deferredresulttest.service.DeferredResultService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import javax.annotation.Resource;
/**
* @author bridgeli
*/
@RestController
@RequestMapping(value = &amp;#34;/deferred-result&amp;#34;)
public class DeferredResultController {
@Resource
private DeferredResultService deferredResultService;
/**
* 为了方便测试，简单模拟一个
* 多个请求用同一个requestId会出问题
*/
private final String requestId = &amp;#34;test&amp;#34;;
@GetMapping(value = &amp;#34;/get&amp;#34;)
public DeferredResult&amp;lt;DeferredResultResponse&amp;gt; get(@RequestParam(value = &amp;#34;timeout&amp;#34;, required = false, defaultValue = &amp;#34;10000&amp;#34;) Long timeout) {
DeferredResult&amp;lt;DeferredResultResponse&amp;gt; deferredResult = new DeferredResult&amp;lt;&amp;gt;(timeout);
deferredResultService.process(requestId, deferredResult);
return deferredResult;
}
/**
* 设置DeferredResult对象的result属性，模拟异步操作
*
* @param desired
* @return
*/
@GetMapping(value = &amp;#34;/result&amp;#34;)
public String settingResult(@RequestParam(value = &amp;#34;desired&amp;#34;, required = false, defaultValue = &amp;#34;成功&amp;#34;) String desired) {
DeferredResultResponse deferredResultResponse = new DeferredResultResponse();
if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)) {
deferredResultResponse.setCode(HttpStatus.OK.value());
deferredResultResponse.setMsg(desired);
} else {
deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc());
}
deferredResultService.settingResult(requestId, deferredResultResponse);
return &amp;#34;Done&amp;#34;;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中：/get 接口模拟沈剑老师说的消息连接，/result 接口模拟有一条新消息来了，然后 /get 接口会立即返回。主要注意的是 requestId，在实际项目中不能使用同一个，否则会出现问题，这个测一下就知道了，也很容易想到原因。&lt;/p&gt;</description></item><item><title>规则引擎入门</title><link>https://bridgeli.cn/posts/2020-07-12-%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E%E5%85%A5%E9%97%A8/</link><pubDate>Sun, 12 Jul 2020 07:14:11 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-07-12-%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;关于规则引擎，我们在工作中应该会经常遇到，例如我们对不同的用户给不同的折扣。前一段时间在网上闲逛，发现一个很简单的规则引擎，一下是学习笔记。&lt;/p&gt;
&lt;p&gt;在使用之前，我们要先导入 jar 包：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.jeasy&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;easy-rules-core&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.jeasy&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;easy-rules-mvel&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;一. 使用零配置的方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;规则引擎入口：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.rule;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.api.RulesEngine;
import org.jeasy.rules.core.DefaultRulesEngine;
import org.jeasy.rules.core.RulesEngineParameters;
import org.junit.Test;
/**
* @author bridgeli
*/
public class ThreeEightRuleTest {
@Test
public void testRule() {
/**
* 创建规则执行引擎
* 注意: skipOnFirstAppliedRule意思是，只要匹配到第一条规则就跳过后面规则匹配
*/
RulesEngineParameters parameters = new RulesEngineParameters().skipOnFirstAppliedRule(true);
RulesEngine rulesEngine = new DefaultRulesEngine(parameters);
//创建规则
Rules rules = new Rules();
rules.register(new EightRule());
rules.register(new ThreeRule());
rules.register(new ThreeEightRuleUnitGroup(new EightRule(), new ThreeRule()));
rules.register(new OtherRule());
Facts facts = new Facts();
for (int i = 1; i &amp;lt;= 50; i++) {
//规则因素，对应的name，要和规则里面的@Fact 一致
facts.put(&amp;#34;number&amp;#34;, i);
//执行规则
rulesEngine.fire(rules, facts);
System.out.println();
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个是判断 1- 50 里面，哪些是 3 的倍数、哪些是 8 的倍数、哪些是 3 和 8 的倍数。&lt;/p&gt;</description></item><item><title>关于 CPU 乱序执行的证明</title><link>https://bridgeli.cn/posts/2020-07-05-%E5%85%B3%E4%BA%8E-cpu-%E4%B9%B1%E5%BA%8F%E6%89%A7%E8%A1%8C%E7%9A%84%E8%AF%81%E6%98%8E/</link><pubDate>Sun, 05 Jul 2020 02:30:15 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-07-05-%E5%85%B3%E4%BA%8E-cpu-%E4%B9%B1%E5%BA%8F%E6%89%A7%E8%A1%8C%E7%9A%84%E8%AF%81%E6%98%8E/</guid><description>&lt;p&gt;在学习 volatile 关键字的时候，我们都知道他有两个作用：1. 内存可见性；2. 禁止指令重排序。但是我们一般都是说，那么怎么证明呢？请看下面这段代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
/**
* @author BridgeLi
* @date 2020/7/4 10:27
*/
public class Disorder {
private static int x = 0;
private static int y = 0;
private static volatile int a = 0;
private static volatile int b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
}, &amp;#34;one&amp;#34;);
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
}, &amp;#34;two&amp;#34;);
one.start();
two.start();
one.join();
two.join();
if (0 == x &amp;amp;&amp;amp; 0 == y) {
System.out.println(&amp;#34;第 &amp;#34; + i + &amp;#34; 次（&amp;#34; + x + &amp;#34;, &amp;#34; + y + &amp;#34;)&amp;#34;);
break;
}
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果仔细分析这段代码，我们就会发现，如果 CPU 没有乱序执行，那么无论任何时候 x 和 y 都不可能同时为零，但是事实上，这段代码是有可能出现 x 和 y 同时为零的，具体大家可以自己测试，需要说明的时候，什么时候指令重排了，要看运气，可能很快出现，也可能要等一会。&lt;/p&gt;</description></item><item><title>Redis 实现布隆过滤器</title><link>https://bridgeli.cn/posts/2020-06-06-redis-%E5%AE%9E%E7%8E%B0%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/</link><pubDate>Sat, 06 Jun 2020 10:36:09 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-06-06-redis-%E5%AE%9E%E7%8E%B0%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/</guid><description>&lt;p&gt;昨天听马士兵教育张福刚讲公开课，里面讲解了布隆过滤器，今天无聊没事干，整理了一下笔记。关于布隆过滤器是什么东西，有什么应用场景就不做讨论了，网上有很多，大家可以自行了解，只记录实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;pom 依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.google.guava&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;guava&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;18.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;具体实现&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import java.nio.charset.StandardCharsets;
/**
* @author BridgeLi
* @date 2020/6/6 16:38
*/
public class BloomFilter {
private Jedis jedis = null;
/**
* 预估的数据量
*/
private static long n = 10000;
/**
* 容忍的错误率
*/
private static double fpp = 0.01;
private static long numBits = optimalNumOfBits(n, fpp);
private static int numHashFunctions = optimalNumOfHashFunctions(n, numBits);
/**
* 根据预估数据量 n 和允许的错误率 fpp 计算需要的 bit 数组的长度
*
* @param n
* @param fpp
* @return
*/
private static long optimalNumOfBits(long n, double fpp) {
if (0 == fpp) {
fpp = Double.MIN_VALUE;
}
return (long) (-n \* Math.log(fpp) / (Math.log(2) \* Math.log(2)));
}
/**
* 根据预估的数据量和计算出来的需要的 bit 数组的长度，计算所需要的 hash 函数的个数
*
* @param n
* @param numBits
* @return
*/
private static int optimalNumOfHashFunctions(long n, long numBits) {
return Math.max(1, (int) Math.round((double) numBits / n * Math.log(2)));
}
/**
* 预热数据
*/
@Before
public void testBloomFilterBefore() {
BloomFilter bloomFilter = new BloomFilter();
bloomFilter.init();
for (int i = 0; i &amp;lt; n; i++) {
bloomFilter.put(&amp;#34;bf&amp;#34;, String.valueOf(i + 100));
}
}
/**
* 过滤数据
*/
@Test
public void testBloomFilter() {
BloomFilter bloomFilter = new BloomFilter();
bloomFilter.init();
int ex_count = 0;
int ne_count = 0;
for (int i = 0; i &amp;lt; 2 * n; i++) {
boolean exist = bloomFilter.isExist(&amp;#34;bf&amp;#34;, String.valueOf(i + 100));
if (exist) {
ex_count++;
} else {
ne_count++;
}
}
System.out.println(&amp;#34;ex_count: &amp;#34; + ex_count + &amp;#34;, ne_count: &amp;#34; + ne_count);
}
private void init() {
JedisPool jedisPool = new JedisPool(&amp;#34;127.0.0.1&amp;#34;, 6379);
jedis = jedisPool.getResource();
}
public boolean isExist(String where, String key) {
long[] indexs = getIndexs(key);
boolean result = false;
try (Pipeline pipeline = jedis.pipelined()) {
for (long index : indexs) {
pipeline.getbit(where, index);
}
// 只要有一个位置为 false，即代表该数据不存在
result = !pipeline.syncAndReturnAll().contains(false);
} catch (Exception e) {
}
return result;
}
public void put(String where, String key) {
long[] indexs = getIndexs(key);
try (Pipeline pipeline = jedis.pipelined()) {
for (long index : indexs) {
pipeline.setbit(where, index, true);
}
pipeline.sync();
} catch (Exception e) {
}
}
private long[] getIndexs(String key) {
long hash1 = Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(StandardCharsets.UTF_8)).asLong();
long hash2 = hash1 &amp;gt;&amp;gt;&amp;gt; 16;
long[] result = new long[numHashFunctions];
for (int i = 0; i &amp;lt; numHashFunctions; i++) {
long combinedHash = hash1 + i * hash2;
if (combinedHash &amp;lt; 0) {
combinedHash = ~combinedHash;
}
result[i] = combinedHash % numBits;
}
return result;
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>关于 JPA 连表查询和 redis 序列化遇到的小问题</title><link>https://bridgeli.cn/posts/2020-04-11-%E5%85%B3%E4%BA%8E-jpa-%E8%BF%9E%E8%A1%A8%E6%9F%A5%E8%AF%A2%E5%92%8C-redis-%E5%BA%8F%E5%88%97%E5%8C%96%E9%81%87%E5%88%B0%E7%9A%84%E5%B0%8F%E9%97%AE%E9%A2%98/</link><pubDate>Sat, 11 Apr 2020 12:42:56 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-04-11-%E5%85%B3%E4%BA%8E-jpa-%E8%BF%9E%E8%A1%A8%E6%9F%A5%E8%AF%A2%E5%92%8C-redis-%E5%BA%8F%E5%88%97%E5%8C%96%E9%81%87%E5%88%B0%E7%9A%84%E5%B0%8F%E9%97%AE%E9%A2%98/</guid><description>&lt;p&gt;一、JPA&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;连表查询时数据长度正常，内容都是重复的，MySQL 数据库运行查询语句结果正常&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先看写法：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.repository;
import cn.bridgeli.demo.entity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
/**
* @author BridgeLi
*/
public interface E1Repository extends JpaRepository&amp;lt;E1, Integer&amp;gt; {
@Query(value = &amp;#34;SELECT t1.id, t1.name, t2.score FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id LIMIT ?1, ?2&amp;#34;, nativeQuery = true)
List&amp;lt;E1&amp;gt; queryE1s(Integer pageNum, Integer pageSize);
}
package cn.bridgeli.demo.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Transient;
/**
* @author BridgeLi
*/
@Data
@Entity
public class E1 {
@Id
private Integer id;
private String name;
@Transient
private String course;
private Integer score;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整体大概就是有两张表 t1 和 t2，一对多的关系，t1 的主键是 t2 的外键，执行的截图我就不做了，问题呢，大概就是上面描述的那样，有一个连表查询的需求，JPA 做的，返回给前端的数据，返回长度是对的，但是内容都是重复的，当时第一次看到这个问题的时候，怀疑是 SQL 的问题，然后就把&lt;/p&gt;</description></item><item><title>Dubbo 自定义拦截器</title><link>https://bridgeli.cn/posts/2020-03-22-dubbo-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8B%A6%E6%88%AA%E5%99%A8/</link><pubDate>Sun, 22 Mar 2020 08:57:44 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-03-22-dubbo-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8B%A6%E6%88%AA%E5%99%A8/</guid><description>&lt;p&gt;写了 Spring AOP 实现自定义注解，打印日志之后，感觉在调用第三方 dubbo 接口的时候，依然会有同样的问题，然后看了一下 dubbo 的官方文档，决定下一个 filter，实现 dubbo 接口的日志拦截，以下是自己完的一个小例子，同样也是供需要的同学参考。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;filter 具体实现如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.filter;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.service.GenericService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author bridgeli
*/
public class DubboServiceFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class);
@Override
public Result invoke(Invoker&amp;lt;?&amp;gt; invoker, Invocation invocation) throws RpcException {
// 打印入参日志
String className = invocation.getInvoker().getInterface().getName();
String methodName = invocation.getMethodName();
String arguments = StringUtils.join(invocation.getArguments(), &amp;#34;;&amp;#34;);
LOGGER.info(&amp;#34;调用 dubbo 服务接口: &amp;#34; + className + &amp;#34;#&amp;#34; + methodName + &amp;#34;，参数：&amp;#34; + arguments);
//开始时间
long startTime = System.currentTimeMillis();
//执行接口调用逻辑
Result result = invoker.invoke(invocation);
//调用耗时
long elapsed = System.currentTimeMillis() &amp;amp;#8211; startTime;
//如果发生异常 则打印异常日志
if (result.hasException() &amp;amp;&amp;amp; invoker.getInterface() != GenericService.class) {
LOGGER.error(&amp;#34;dubbo执行异常，接口：&amp;#34; + className + &amp;#34;#&amp;#34; + methodName + &amp;#34;，参数：&amp;#34; + arguments, result.getException());
} else {
//打印响应日志
LOGGER.info(&amp;#34;dubbo服务响应成功：&amp;#34; + className + &amp;#34;#&amp;#34; + methodName + &amp;#34;，参数：&amp;#34; + arguments + &amp;#34;，返回值：&amp;#34; + result.getValue() + &amp;#34;，用时：&amp;#34; + elapsed);
}
//返回结果响应结果
return result;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;在/src/main/resources/META-INF/dubbo目录下新增纯文本文件 com.alibaba.dubbo.rpc.Filter 内容为：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
dubboServiceFilter=cn.bridgeli.demo.filter.DubboServiceFilter
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;最后在服务提供者配置文件中添加配置使拦截器生效：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dubbo:provider filter=&amp;#34;dubboServiceFilter&amp;#34;/&amp;gt;
或者
&amp;lt;dubbo:service filter=&amp;#34;dubboServiceFilter&amp;#34;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样即可实现。不过需要说明的是，因为我们项目用的 dubbo 版本是：2.5.3，所以包名和配置名还都是：com.alibaba.dubbo，而最新的版本阿里已经捐献给 apache，所以都变成了：org.apache.dubbo。最后的最后想说的是，具体大家可以参考 dubbo 的官方文档，个人认为 dubbo 的官方文档写的是极好的，各种通俗易懂。&lt;/p&gt;</description></item><item><title>Spring AOP 实现自定义注解</title><link>https://bridgeli.cn/posts/2020-03-15-spring-aop-%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3/</link><pubDate>Sun, 15 Mar 2020 07:32:50 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-03-15-spring-aop-%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3/</guid><description>&lt;p&gt;自工作后，除了一些小项目配置事务使用过 AOP，真正自己写 AOP 机会很少，另一方面在工作后还没有写过自定义注解，一直很好奇注解是怎么实现他想要的功能的，刚好做项目的时候，经常有人日志打得不够全，经常出现问题了，查日志的才发现忘记打了，所以趁此机会，搜了一些资料，用 AOP + 自定义注解，实现请求拦截，自定义打日志，玩一下这两个东西，以下是自己完的一个小例子，也供需要的同学参考。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;注解如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author bridgeli
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
/**
* 方法描述
*
* @return
*/
String desc() default &amp;#34;&amp;#34;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;切面&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.annotation;
import cn.bridgeli.utils.AuthorizeUtil;
import cn.bridgeli.entity.Principal;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @author bridgeli
* 1. 这是一个切面类
*/
@Aspect
@Component
@Slf4j
public class MyLogAspect {
/**
* 2. PointCut表示这是一个切点，@annotation表示这个切点切到一个注解上，后面带该注解的全类名
* 切面最主要的就是切点，所有的故事都围绕切点发生
* logPointCut()代表切点名称
*/
@Pointcut(&amp;#34;@annotation(cn.bridgeli.demo.annotation.MyLog)&amp;#34;)
public void logPointCut() {
}
/**
* 3. 环绕通知
*
* @param joinPoint
* @param myLog
* @return
*/
@Around(value = &amp;#34;logPointCut() &amp;amp;&amp;amp; @annotation(myLog)&amp;#34;, argNames = &amp;#34;joinPoint,myLog&amp;#34;)
public Object logAround(ProceedingJoinPoint joinPoint, MyLog myLog) {
// 获取方法名
String methodFullPathName = joinPoint.getTarget().getClass().getName() + &amp;#34;#&amp;#34; + joinPoint.getSignature().getName();
// 获取参数
String params = StringUtils.join(joinPoint.getArgs(), &amp;#34;;&amp;#34;);
Principal currentUser = AuthorizeUtil.getCurrentUser();
log.info(&amp;#34;当前登陆用户：&amp;#34; + (null == currentUser ? &amp;#34;&amp;#34; : currentUser.toString()) + &amp;#34;，进入 [ &amp;#34; + methodFullPathName + &amp;#34; ] 方法, 方法的描述：&amp;#34; + myLog.desc() + &amp;#34;，参数为:&amp;#34; + params);
// 继续执行方法
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error(&amp;#34;切面执行报错，参数：&amp;#34; + params, e);
}
long elapsed = System.currentTimeMillis() &amp;amp;#8211; startTime;
log.info(&amp;#34;[ &amp;#34; + methodFullPathName + &amp;#34; ] 方法执行结束，返回值为：&amp;#34; + (null == result ? &amp;#34;&amp;#34; : result.toString()) + &amp;#34;，用时：&amp;#34; + elapsed);
return result;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后只需要在想使用的地方 @MyLog 就可以了，当然也可以加上 @MyLog(desc = “这是方法描述”)，这样打出来的日志还会有方法是做什么的，别人看日志的时候能够一目了然。&lt;/p&gt;</description></item><item><title>Java 使用 FFmpeg 处理视频文件示例</title><link>https://bridgeli.cn/posts/2020-02-29-java-%E4%BD%BF%E7%94%A8-ffmpeg-%E5%A4%84%E7%90%86%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6%E7%A4%BA%E4%BE%8B/</link><pubDate>Sat, 29 Feb 2020 09:01:09 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-02-29-java-%E4%BD%BF%E7%94%A8-ffmpeg-%E5%A4%84%E7%90%86%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6%E7%A4%BA%E4%BE%8B/</guid><description>&lt;p&gt;Java 使用 FFmpeg 处理视频文件示例&lt;/p&gt;
&lt;p&gt;目前在公司做一个小东西，里面用到了 FFmpeg 简单处理音视频，感觉功能特别强大，在做之前我写了一个小例子，现在记录一下。&lt;/p&gt;
&lt;p&gt;首先说明，我是在 &lt;a href="https://ffmpeg.zeranoe.com/builds/"&gt;https://ffmpeg.zeranoe.com/builds/&lt;/a&gt; 这个地方下载的软件，Windows 和 Mac 解压之后即可使用。具体代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* @author BridgeLi
* @date 2020/2/29 15:40
*/
public class FfmpegTest {
private static final String OS = System.getProperty(&amp;#34;os.name&amp;#34;).toLowerCase();
private static final String FFMPEG_PATH = &amp;#34;/Users/bridgeli/ffmpeg-20200216-8578433-macos64-static/bin/ffmpeg&amp;#34;;
@Test
public void testFfmpeg() {
String inputWavFile = &amp;#34;/Users/bridgeli/inputWavFile.wav&amp;#34;;
String inputMp3File = &amp;#34;/Users/bridgeli/inputMp3File.mp3&amp;#34;;
String inputMp4File = &amp;#34;/Users/bridgeli/inputMp4File.mp4&amp;#34;;
String outMergeMp3File = &amp;#34;/Users/bridgeli/outMergeMp3File.mp3&amp;#34;;
String outMergeMp3AndMp4File = &amp;#34;/Users/bridgeli/outMergeMp3AndMp4File.mp4&amp;#34;;
String outConcatMp3File = &amp;#34;/Users/bridgeli/outConcatMp3File.mp3&amp;#34;;
// 拼接
String command = null;
if (OS.contains(&amp;#34;mac&amp;#34;) || OS.contains(&amp;#34;linux&amp;#34;)) {
command = FFMPEG_PATH + &amp;#34; -i &amp;#34; + inputMp3File + &amp;#34; -i &amp;#34; + inputWavFile + &amp;#34; -filter_complex \[0:0\]\[1:0\]concat=n=2:v=0:a=1[a] -map [a] &amp;#34; + outConcatMp3File;
} else if (OS.contains(&amp;#34;windows&amp;#34;)) {
command = FFMPEG_PATH + &amp;#34; -i &amp;#34; + inputMp3File + &amp;#34; -i &amp;#34; + inputWavFile + &amp;#34; -filter_complex \&amp;#34;\[0:0\]\[1:0\]concat=n=2:v=0:a=1[a]\&amp;#34; -map \&amp;#34;[a]\&amp;#34; &amp;#34; + outConcatMp3File;
}
// 合并（视频和音频）
// String command = FFMPEG_PATH + &amp;#34; -i &amp;#34; + inputMp4File + &amp;#34; -i &amp;#34; + outConcatMp3File + &amp;#34; -c:v copy -c:a aac -strict experimental &amp;#34; + outMergeMp3AndMp4File;
// 合并
// String command = FFMPEG_PATH + &amp;#34; -i &amp;#34; + inputMp3File + &amp;#34; -i &amp;#34; + inputWavFile + &amp;#34; -filter_complex amerge -ac 2 -c:a libmp3lame -q:a 4 &amp;#34; + outMergeMp3File;
System.out.println(command);
Process process = null;
try {
process = Runtime.getRuntime().exec(command);
} catch (IOException e) {
e.printStackTrace();
}
if (null == process) {
return;
}
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
try (InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader)) {
String line = null;
StringBuffer context = new StringBuffer();
while ((line = br.readLine()) != null) {
context.append(line);
}
System.out.println(&amp;#34;error message: &amp;#34; + context);
} catch (IOException e) {
e.printStackTrace();
}
process.destroy();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在我的认知中，完成任务是第一位的，所以按照这个简单处理一下音视频是没有问题的，具体更强大的语法，大家可以自己查询相关文档，也可以参考 &lt;a href="https://blog.csdn.net/shshjj/article/details/98185454"&gt;https://blog.csdn.net/shshjj/article/details/98185454&lt;/a&gt; 这篇文中，其中我个人也在学习中。下面说两个在使用的过程中遇到的问题。&lt;/p&gt;</description></item><item><title>日期中用 YYYY 一定会报错吗？</title><link>https://bridgeli.cn/posts/2020-02-03-%E6%97%A5%E6%9C%9F%E4%B8%AD%E7%94%A8-yyyy-%E4%B8%80%E5%AE%9A%E4%BC%9A%E6%8A%A5%E9%94%99%E5%90%97/</link><pubDate>Mon, 03 Feb 2020 08:43:20 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-02-03-%E6%97%A5%E6%9C%9F%E4%B8%AD%E7%94%A8-yyyy-%E4%B8%80%E5%AE%9A%E4%BC%9A%E6%8A%A5%E9%94%99%E5%90%97/</guid><description>&lt;p&gt;今年春节真是打破了 N 多传统，很多人都是在家连门都没出过，从今天开始也要开始在家远程办公了，因为和小伙伴合作开发一个功能，但是接口目前还没给到，然后记得今年元旦前后，关于 YYYY 报错的问题，突然火了，据说有 N 多程序员被火速召回公司改 bug，所以决定写篇小文章说说这个问题：YYYY 一定会报错吗？&lt;/p&gt;
&lt;p&gt;首先需要说明的是：我用的 JDK 版本为：jdk-8u131-macosx-x64，所以具体表现为应该显示：2019-12-31，结果确是：2020-12-31，另外经过我的测试其实不仅 format 的时候报错，parse 的时候同样也会报错，我写了一段示例代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import org.joda.time.DateTime;
import org.junit.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Created by bridgeli on 2019/02/03.
*/
public class DateTest {
@Test
public void testSimpleDateFormat() throws ParseException {
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;);
DateTime dateTime1 = new DateTime(2019, 12, 31, 23, 59, 59);
String date1 = simpleDateFormat1.format(dateTime1.toDate());
System.out.println(&amp;#34;date1: &amp;#34; + date1);
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat(&amp;#34;yyyy-MM-dd&amp;#34;);
DateTime dateTime2 = new DateTime(2020, 01, 01, 23, 59, 59);
String date2 = simpleDateFormat2.format(dateTime2.toDate());
System.out.println(&amp;#34;date2: &amp;#34; + date2);
SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat(&amp;#34;YYYY-MM-dd HH:mm:ss&amp;#34;);
DateTime dateTime3 = new DateTime(2019, 12, 31, 23, 59, 59);
String date3 = simpleDateFormat3.format(dateTime3.toDate());
System.out.println(&amp;#34;date3: &amp;#34; + date3);
SimpleDateFormat simpleDateFormat4 = new SimpleDateFormat(&amp;#34;YYYY-MM-dd&amp;#34;);
DateTime dateTime4 = new DateTime(2020, 01, 01, 23, 59, 59);
String date4 = simpleDateFormat4.format(dateTime4.toDate());
System.out.println(&amp;#34;date4: &amp;#34; + date4);
SimpleDateFormat simpleDateFormat5 = new SimpleDateFormat(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;);
Date date5 = simpleDateFormat5.parse(&amp;#34;2019-12-31 23:59:59&amp;#34;);
System.out.println(&amp;#34;date5: &amp;#34; + date5);
SimpleDateFormat simpleDateFormat6 = new SimpleDateFormat(&amp;#34;yyyy-MM-dd&amp;#34;);
Date date6 = simpleDateFormat6.parse(&amp;#34;2020-01-01&amp;#34;);
System.out.println(&amp;#34;date6: &amp;#34; + date6);
SimpleDateFormat simpleDateFormat7 = new SimpleDateFormat(&amp;#34;YYYY-MM-dd HH:mm:ss&amp;#34;);
Date date7 = simpleDateFormat7.parse(&amp;#34;2019-12-31 23:59:59&amp;#34;);
System.out.println(&amp;#34;date7: &amp;#34; + date7);
SimpleDateFormat simpleDateFormat8 = new SimpleDateFormat(&amp;#34;YYYY-MM-dd&amp;#34;);
Date date8 = simpleDateFormat8.parse(&amp;#34;2020-01-01&amp;#34;);
System.out.println(&amp;#34;date8: &amp;#34; + date8);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面这段代码的输出结果为：&lt;/p&gt;</description></item><item><title>纠错：Java 内存模型（JMM）</title><link>https://bridgeli.cn/posts/2020-01-01-%E7%BA%A0%E9%94%99java-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8Bjmm/</link><pubDate>Wed, 01 Jan 2020 03:03:52 +0000</pubDate><guid>https://bridgeli.cn/posts/2020-01-01-%E7%BA%A0%E9%94%99java-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8Bjmm/</guid><description>&lt;p&gt;当一个 Java 程序员工作一段时间之后，不可避免的要去了解 JVM，而了解 JVM 的时候，自然就会看到 Java 的内存模型，但是个人看过有太多的人概念不清不楚，有太多的人把 Java 内存结构，记得曾经看过一篇文章，一个同学去面试，面试官问他：简单聊聊 Java 的内存模型，他说完之后，面试官说：你说的不对啊，内存模型应该是堆、栈、常量池、方法区、程序计数器等等，然后他瞬间就不想去这家公司了，这不是让一个半吊子去当面试官吗，自己概念都没搞清楚就去说别人，个人也看过太多文章把这个搞错，所以今天就写了一篇小文章，说说自己对 JMM 的理解，当然我说的可能也不对，也不可能说透。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主内存和工作内存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Java 内存模型规定了所有的变量都存储在主内存，每个线程还有自己的工作内存，线程的工作内存中存储了被该线程使用到的变量的主内存存储拷贝，程对变量的所有操作（读取、赋值等）都必须在工作内存中进行，不能直接读写主内存中的变量，不同的线程之间也无法直接访问对方工作内存的变量，线程间变量值的传递均需要通过主内存来完成，主内存和工作内存之间的交互有 8 个人原子性的操作来实现，具体详细的可以再查资料。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;重排序&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重排序是编译器和处理器为了优化程序性能，而对指令顺序进行重新排序的一种手段。但 as-if-serial 语义的意思是：不管怎么重排序，单线程程序的执行结果都不能被改变，所以编译器和处理器都不会对存在数据依赖关系的操作做重排序。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;happens-before&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果操作 1 happens-before 操作 2，那么第操作1的执行结果将对操作 2 可见，而且操作1的执行顺序排在第操作 2 之前。两个操作之间存在 happens-before 关系，并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致，那么这种重排序并不非法。另外 happens-before 具有传递性，由于这些规则的存在，Java 内存模型会保证重排序后的执行，在线程内看起来和串行的效果是一样的，这就是程序的顺序一致性。&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;对于 volatile 型变量的特殊规则&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当一个变量被定义为 volatile 之后，他将具备两种特性，1. 保证此变量对所有线程的可见行，这里的可见行是指当一个线程修改了这个变量的值，新值对于其他线程来说是可以立即得知的，而普通变量做不到这一点。但是这个要多说一点，有些同学看到这可能认为：volatile 变量在各个线程中是一致的，所以基于 volatile 变量的运算在并发下是安全的，其实这句话的论据对，但结论是错的，因为 Java 中的运算并非原子的，volatile 的应用场景在于读多写少的地方：例如修饰一个 boolean 变量作为一个开关。2. 禁止指令重拍优化，这个在写单例的时候，我想大家都已经知道了，不再赘述。&lt;/p&gt;
&lt;ol start="5"&gt;
&lt;li&gt;final&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;被 final 修饰的字段，一旦完成了初始化，其他线程就能看到它，并且它也不会再变了。即只要不可变对象被正确的构建出来（没有发生 this 引用溢出），它就是线程安全的。&lt;/p&gt;</description></item><item><title>Apache Commons Codec — 加密与编码</title><link>https://bridgeli.cn/posts/2019-09-30-apache-commons-codec-%E5%8A%A0%E5%AF%86%E4%B8%8E%E7%BC%96%E7%A0%81/</link><pubDate>Mon, 30 Sep 2019 06:59:19 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-09-30-apache-commons-codec-%E5%8A%A0%E5%AF%86%E4%B8%8E%E7%BC%96%E7%A0%81/</guid><description>&lt;p&gt;明天就是十一假期了，公司也没多大事，刷知乎，看到有人吐槽曾经的一个合作伙伴连 md5 都写不对，告诉对方写错了，对方顾头不顾腚的修，还是没修对，然后测了一下自己写的想亏写对了，不然又遗留 bug 了，不过看下面评论，有人提到 Apache Commons Codec 里面都已经写好了，看了一下确实，反正也无聊，也写不了大文章，写着玩玩。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先看自己手写的 md5 // 今后大家不要这么写了，太傻了&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Created by bridgeli on 2019/7/12.
*/
public class EncryptUtils {
private static Logger logger = LoggerFactory.getLogger(EncryptUtils.class);
private EncryptUtils() {
}
public static String getMD5(String content) {
if (null == content) {
return &amp;#34;&amp;#34;;
}
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance(&amp;#34;md5&amp;#34;);
} catch (NoSuchAlgorithmException e) {
logger.error(&amp;#34;md5 error&amp;#34;, e);
}
if (null == messageDigest) {
return &amp;#34;&amp;#34;;
}
messageDigest.update(content.getBytes());
byte[] bytes = messageDigest.digest();
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
String str = Integer.toHexString(b &amp;amp; 0xFF);
if (str.length() == 1) {
stringBuilder.append(&amp;#34;0&amp;#34;);
}
stringBuilder.append(str);
}
String result = stringBuilder.toString();
return result;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;借助别人写好的工具类实现&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先引入依赖&lt;/p&gt;</description></item><item><title>关于 alibaba fastjson 的两个小知识点</title><link>https://bridgeli.cn/posts/2019-08-25-%E5%85%B3%E4%BA%8E-alibaba-fastjson-%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/</link><pubDate>Sun, 25 Aug 2019 08:22:47 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-08-25-%E5%85%B3%E4%BA%8E-alibaba-fastjson-%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/</guid><description>&lt;ol&gt;
&lt;li&gt;json 转 JavaBean 大小写不敏感&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在工作中，我个人经常使用的 json 的工具类是 Google 的 gson，前几天做一个需求的也自然而然的使用这个，但是在和其他部门联调的时候，发现他的属性全是小写，而不是刚开始约定的小驼峰，所以导致他传过来的字符串，我这边转不成 Java bean，在和他讨论的时候，他说他一直就这么写，而且别人使用的时候是没有问题的，然后就找到其他的使用的同学，他竟然说他没在意过这个问题，线上测试了一下，确实没有问题，然后就看一下怎么实现的，然后突然发现，他使用的是 alibaba 的 fastjson 转 Java bean，然后测试了一下，发现 alibaba fastjson 转 Java bean 竟然是大小写不敏感的，特此记录一下，测试代码入校:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;fastjson&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.2.37&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
package cn.bridgeli.demo;
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class FastJsonTest {
@Test
public void testFastJsonParseObject() {
String jsonStr = &amp;#34;{\&amp;#34;USERNAME\&amp;#34;:\&amp;#34;BridgeLi\&amp;#34;}&amp;#34;;
User user = JSON.parseObject(jsonStr, User.class);
System.out.println(user.getUsername());
}
}
class User implements Serializable {
private static final long serialVersionUID = 4202834388700617773L;
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们看到测试的例子中 Java bean 的属性是小写的，json 字符串中的属性是全大写的，但转换一点问题都没有。&lt;/p&gt;</description></item><item><title>关于 MySQL 和 MyBatis 易错的几个点</title><link>https://bridgeli.cn/posts/2019-07-08-%E5%85%B3%E4%BA%8E-mysql-%E5%92%8C-mybatis-%E6%98%93%E9%94%99%E7%9A%84%E5%87%A0%E4%B8%AA%E7%82%B9/</link><pubDate>Mon, 08 Jul 2019 03:30:12 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-07-08-%E5%85%B3%E4%BA%8E-mysql-%E5%92%8C-mybatis-%E6%98%93%E9%94%99%E7%9A%84%E5%87%A0%E4%B8%AA%E7%82%B9/</guid><description>&lt;p&gt;由于某些不可抗拒力原因，自从开博以来断更了一个月，昨天晚上突然发现竟然解封了，今天立即写一篇小文章感谢党感谢政府感谢人民。话说，这一周有一个实习的同学，在写一个小东西的时候，发现一个问题，排序没有生效，刚好之前我也看过另外一个问题，现在算是总结一下。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ORDER BY 不生效&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码大概就是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
SELECT * FROM t ORDER BY &amp;#34;id DESC&amp;#34;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们其实可以很明显的看出来这个 SQL 有问题，ORDER BY 的后面多了引号，关键是这个 SQL 报错吗？如果 id 是随便写的一个不存在的列报错吗？答案是都不报错，排序也不生效，大家可以测试一些，这有时候就比较坑了，具体为什么会出现这个错误，后面再说，先说第二个。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;分页问题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
/**
* 开始页码
*/
private String pageSize;
/**
* 分页量
*/
private String offset;
&amp;lt;if test=&amp;#34;pageSize != null&amp;#34;&amp;gt;
&amp;lt;if test=&amp;#34;offset != null&amp;#34;&amp;gt;
limit #{offset}, #{pageSize}
&amp;lt;/if&amp;gt;
&amp;lt;if test=&amp;#34;offset == null&amp;#34;&amp;gt;
limit #{pageSize}
&amp;lt;/if&amp;gt;
&amp;lt;/if&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个错误就比较明显了，一般人也不会犯，为什么要单独说一下呢？因为某同事说，分页用 # 会出错，但是我记得我一直用 # 没问题啊，刚好在遇到上面这个问题的时候，顺便测试了一些，还真有错，奇怪啊，其实原因很简单，就是分页的对象的属性有误，我们知道分页的属性，一般都是用 Integer，这个地方却写了 String，真是没事找事啊，一般谁会这么写？个人认为除非脑子有病。下面看第三个问题，也是一个比较诡异的问题。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;UPDATE&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
UPDATE t SET username = &amp;#34;BridgeLi&amp;#34; AND passwd = &amp;#34;BridgeLi&amp;#34; WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个 SQL 有问题吗？会报错吗？这个问题有时候真不太看得出来，其实也很明显，SET 后面的各个列应该是 “,” 相连，而这个用的是 AND，有时候脑子一抽，还真有可能写错。但是会报错吗？这个问题还真不好答复，因为这个 SQL 这么写，相当于：&lt;/p&gt;</description></item><item><title>Redis GeoHash 的一个小示例</title><link>https://bridgeli.cn/posts/2019-05-19-redis-geohash-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E7%A4%BA%E4%BE%8B/</link><pubDate>Sun, 19 May 2019 09:38:46 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-05-19-redis-geohash-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E7%A4%BA%E4%BE%8B/</guid><description>&lt;p&gt;上周产品经理提了一个类似于 LBS 的应用，第一时间想到了忘记了之前什么时候看 Redis 的 API，发现 Redis 自 3.2 版本之后，新增了一类关于地理位置相关的 API，于是拿来测试一下，发现特别好用，写一个小例子作为笔记。&lt;/p&gt;
&lt;p&gt;首先需要说明的是，由于我们公司的 JDK 的版本是 1.7，所以我采用的 spring-data-redis 的版本是：1.8.20.RELEASE，最新二点几的版本已经不支持 JDK 1.7，而一点几和二点几的版本的 API 有略微的差异（下面会说明，还有一点点我的小感悟），废话不多说，直接看例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Override
public Long geoAdd(String key, List&amp;lt;Entity&amp;gt; entities) {
redisTemplate.delete(key);
GeoOperations geoOperations = redisTemplate.opsForGeo();
Map&amp;lt;String, Point&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
Point point = null;
for (Entity entity : entities) {
point = new Point(entity.getLongitude(), entity.getLatitude());
map.put(gson.toJson(entity), point);
}
Long add = geoOperations.geoAdd(key, map);
return add;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;参数就两个很简单，一个是 key，一个是数据集，我们将在这个集合中找出符合条件的数据，需要说明的是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一行我先调用了一个 delete 方法，是将上次放进去的数据删除，因为这个命令是 add，也就是新增，但是 redis 并没有提供直接删除这个 key 的命令（有一个 remove 的方法，但是需要传入删除哪些数据，也就是不能只给一个 key，把这个 key 对应的数据都删除，个人感觉不太好用），当然你也可以在计算后取得相应的数据之后删除，个人感觉都一样，不要忘记清理数据就行，另一个方法就是设置过期时间，也都行；&lt;/li&gt;
&lt;li&gt;为什么要清理数据？因为这个 add，不同的情况，放进去的数据应该不同的，如果 entities 已经发生变更，而一直 add，那么数据将会乱掉，所以先把之前的数据删掉再说；&lt;/li&gt;
&lt;li&gt;我个人采用的是把数据放到了 Map 中，其中 key 是对象序列化之后的 json 串，目的是为了下面找到对应的数据之后，直接反序列化成对象进行返回，当然也可以采用其他的方案，还有就是 add 还有一些其他的 API，这个大家可以自己看文档，选择合适的就行;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;数据放进去之后就是计算了，我们的需求就是算一个人旁边几公里内有多少符合条件的数据，代码如下：&lt;/p&gt;</description></item><item><title>分享 Guava 的一些常见方法</title><link>https://bridgeli.cn/posts/2019-04-30-%E5%88%86%E4%BA%AB-guava-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B8%B8%E8%A7%81%E6%96%B9%E6%B3%95/</link><pubDate>Tue, 30 Apr 2019 07:06:25 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-04-30-%E5%88%86%E4%BA%AB-guava-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B8%B8%E8%A7%81%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;前几天同事分享了一些关于 Guava 的一起基础用法，我之前没用过，感觉挺好的，所以记一些常见的方法。&lt;/p&gt;
&lt;p&gt;一. 基础工具类，字符串相关的&lt;/p&gt;
&lt;p&gt;其实这些在 apache commons-lang3，算是重复造轮子吧，简单说一下。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;判断字符串是否为空，之前看到很多人自己定义，这些可能是一些老程序员吧，apache commons-lang3，Guava 的如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
boolean nullOrEmpty = Strings.isNullOrEmpty(&amp;#34;&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;补全字符串(在前面补全和后面补全)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
String padStart0 = Strings.padStart(&amp;#34;3&amp;#34;, 2, &amp;amp;#8216;a&amp;amp;#8217;);
System.out.println(&amp;#34;padStart0 = &amp;#34; + padStart0);
String padStart1 = Strings.padStart(&amp;#34;333&amp;#34;, 2, &amp;amp;#8216;a&amp;amp;#8217;);
System.out.println(&amp;#34;padStart1 = &amp;#34; + padStart1);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;拆分和合并字符串&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
List&amp;lt;String&amp;gt; list = Splitter.on(&amp;#34;,&amp;#34;).splitToList(&amp;#34;Denny,BridgeLi,CCC&amp;#34;);
System.out.println(&amp;#34;list = &amp;#34; + list);
String join = Joiner.on(&amp;#34;,&amp;#34;).join(list);
System.out.println(&amp;#34;join = &amp;#34; + join);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;对象相等判断和 ToStringHelper 类&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
boolean equal = Objects.equal(&amp;#34;&amp;#34;, &amp;#34;&amp;#34;);
MoreObjects.toStringHelper();
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;二. 集合类相关的，这些个人感觉还是非常常用的&lt;/p&gt;</description></item><item><title>关于 CAP 理论 和 BASE 理论</title><link>https://bridgeli.cn/posts/2019-03-31-%E5%85%B3%E4%BA%8E-cap-%E7%90%86%E8%AE%BA-%E5%92%8C-base-%E7%90%86%E8%AE%BA/</link><pubDate>Sun, 31 Mar 2019 15:12:55 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-03-31-%E5%85%B3%E4%BA%8E-cap-%E7%90%86%E8%AE%BA-%E5%92%8C-base-%E7%90%86%E8%AE%BA/</guid><description>&lt;p&gt;一、CAP 理论&lt;/p&gt;
&lt;p&gt;CAP 理论是分布式计算领域公认的一个定理，是分布式架构师必须掌握的理论，目前网上关于这块的资料也很多，各种说法，其实 CAP 理论自身也是一个不断发展的过程，相比之下比较准确的说法应该是：在一个分布式系统（指互相连接并共享数据的节点的集合）中，当涉及读写操作时，只能保证一致性（Consistence）、可用性（Availability）、分区容错性（Partition Tolerance）三者中的两个，另外一个必须被牺牲。也就是说必须是相关连接并且共享数据的分布式系统才是我们讨论 CAP 的基础，另外就是 CAP 理论关注的是对数据的读写操作，而不是分布式系统的所有功能。明白了这些之后，下面对 CAP 逐一讲解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一致性（Consistency）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;很多资料都说，对一致性的讲解时所有节点在同一时刻都能看到相同的数据，其实更准确的应该是：对某个客户端来说，读操作保证能够返回最新的写操作结果，是否数据一定一直呢？还真不一定，对于系统执行事务来说，在事务执行过程中，系统其实处于一个不一致的状态，不同的节点的数据并不完全一致，因为事务在执行过程中，client 是无法读取到未提交的数据的，只有等到事务提交后，client 才能读取到事务写入的数据，而如果事务失败则会进行回滚，client 也不会读取到事务中间写入的数据。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;可用性（Availability）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般的解释是，每个请求都能得到成功或者失败的响应，更准确的是，非故障的节点在合理的时间内返回合理的响应（不是错误和超时的相应），其实什么是成功和失败？超时报错算不算失败的响应？所以应该是合理的响应，另外故障节点肯定是不能返回结果了。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;分区容错性（Partition Tolerance）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般说法是，出现消息丢失或者分区错误时系统能够继续运行，其实更准确的是，当出现网络分区后，系统能够继续“履行职责”。因为关于运行，只要不宕机就可以说在运行。&lt;br&gt;
综上，其实 CAP 理论也是一个自身不断发展的历程，虽然以前的理论也能说明问题，但是仔细深究起来发现有些问题，下面会有说明。&lt;/p&gt;
&lt;p&gt;CAP 应用&lt;/p&gt;
&lt;p&gt;CAP 理论开明宗义就说我们必须放弃一个，但是其实仔细想想我们可以放弃 P 吗？因为网络本身无法做到 100% 可靠，有可能出故障，所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P，那么当发生分区现象时，为了保证 C，系统需要禁止写入，当有写入请求时，系统返回 error（例如，当前系统不允许写入），这又和 A 冲突了，因此，分布式系统理论上不可能选择 CA 架构，只能选择 CP 或者 AP 结果，但是需要说明的是，按照通常解释，是可以 CA 的，因为通常的解释 A 是返回失败或者成功的结果，而不允许写入，其实就是失败的结果，满足 A 的定义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CP&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了保证一致性，当发生分区现象后，N1 节点上的数据已经更新到 y，但由于 N1 和 N2 之间的复制通道中断，数据 y 无法同步到 N2，N2 节点上的数据还是 x。这时客户端 C 访问 N2 时，N2 需要返回 Error，提示客户端 C“系统现在发生了错误”，这种处理方式违背了 A 的要求，因此 CAP 三者只能满足 CP&lt;/p&gt;</description></item><item><title>Maven 打包 Excel 文件损坏</title><link>https://bridgeli.cn/posts/2019-01-13-maven-%E6%89%93%E5%8C%85-excel-%E6%96%87%E4%BB%B6%E6%8D%9F%E5%9D%8F/</link><pubDate>Sun, 13 Jan 2019 09:10:03 +0000</pubDate><guid>https://bridgeli.cn/posts/2019-01-13-maven-%E6%89%93%E5%8C%85-excel-%E6%96%87%E4%BB%B6%E6%8D%9F%E5%9D%8F/</guid><description>&lt;p&gt;前几天在项目中遇到一个小问题，有一个 Excel 文件放在 classpath 下，通过流下载下来，本地测试的时候一点问题都没，但是部署到测试环境却不行了，说文件已损坏，然后打不开，简单代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@RequestMapping(value = &amp;#34;/export&amp;#34;, method = RequestMethod.GET)
public void export(HttpServletResponse response) {
ServletOutputStream servletOutputStream = null;
String filename = &amp;#34;template.xlsx&amp;#34;;
InputStream inputStream = ExcelHandleController.class.getClassLoader().getResourceAsStream(filename);
try {
byte[] b = new byte[inputStream.available()];
inputStream.read(b);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader(&amp;#34;Content-Disposition&amp;#34;, &amp;#34;attachment;filename=&amp;#34; + filename);
response.setContentType(&amp;#34;application/octet-stream;charset=UTF-8&amp;#34;);
//获取响应报文输出流对象
servletOutputStream = response.getOutputStream();
//输出
servletOutputStream.write(b);
} catch (IOException e) {
logger.error(&amp;#34;文件下载出错&amp;#34;, e);
} finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
logger.error(&amp;#34;文件下载出错&amp;#34;, e);
}
}
if (null != servletOutputStream) {
try {
servletOutputStream.flush();
servletOutputStream.close();
} catch (IOException e) {
logger.error(&amp;#34;文件下载出错&amp;#34;, e);
}
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当时就感觉这代码很简单啊，没什么问题的，但是下载下来就是不对，想到是不是测试环境有什么问题，查了一下没什么特殊之处，最后发现 Git 上的代码中的文件并没有什么问题，但是打成 war 包解压之后这文件本身就已经损坏了，而且比对了一下两个文件，发现 war 包之中的文件变大了，搜了一下资料，才知道原来 maven 打包会对一些文件转码，这个过程中会损坏一些文件，所以打不开，网上的资料也挺多，最简单的就是增加一个 plugin，不让该文件转码，代码如下：&lt;/p&gt;</description></item><item><title>Java 学习之路</title><link>https://bridgeli.cn/posts/2018-12-31-java-%E5%AD%A6%E4%B9%A0%E4%B9%8B%E8%B7%AF/</link><pubDate>Mon, 31 Dec 2018 08:03:17 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-12-31-java-%E5%AD%A6%E4%B9%A0%E4%B9%8B%E8%B7%AF/</guid><description>&lt;p&gt;前几天刷微博，看到博主 @Java大本营 发了一个图片，总结 Java 一些常见的知识点，感觉挺好，整理成文字版，发在我的个人博客，作为一个大家学习复习的文档，也欢迎有人在评论中留下各种参考资料，一下是正文。&lt;/p&gt;
&lt;p&gt;一、基础篇&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JVM&lt;br&gt;
①. JVM 内存结构&lt;br&gt;
堆、栈、方法区、直接内存、堆和栈的区别&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;②. Java 内存模型&lt;br&gt;
内存可见性、重排序、顺序一致性、volatile、锁、final&lt;/p&gt;
&lt;p&gt;③. 垃圾回收&lt;br&gt;
内存分配策略、垃圾收集器(G1)、GC 算法、GC 参数、对象存活的判定&lt;/p&gt;
&lt;p&gt;④. JVM 参数及调优&lt;/p&gt;
&lt;p&gt;⑤. Java 对象模型&lt;br&gt;
oop-klass、对象头&lt;/p&gt;
&lt;p&gt;⑥. HotSpot&lt;br&gt;
即时编译器、编译优化&lt;/p&gt;
&lt;p&gt;⑦. 类加载机制&lt;br&gt;
ClassLoader、类加载过程、双亲委派(破坏双亲委派)、模块化(jboss、modules、osgl、jigsaw)&lt;/p&gt;
&lt;p&gt;⑧. 虚拟机性能监控与故障处理工具&lt;br&gt;
jps、jstack、jmap、jstat、jconsole、jinfo、jhat、javap、btrace、tprofiler&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;编译与反编译&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;javac、javap、jad、CRF&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Java 基础知识&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;①. 阅读源代码&lt;br&gt;
String、Integer、Long、Enum、BigDecimal、ThreadLocal、ClassLoader &amp;amp; URLClassLoader、ArrayList &amp;amp; LinkedList、HashMap &amp;amp; LinkedHashMap &amp;amp; TreeMap &amp;amp; ConcurrentHashMap、HashSet &amp;amp; LinkedHashSet &amp;amp; TreeSet&lt;/p&gt;
&lt;p&gt;②. Java 中各种变量的类型&lt;/p&gt;
&lt;p&gt;③. 熟悉 Java String 的使用，熟悉 String 的各种函数&lt;br&gt;
JDK 6 和 JDK 7 中 substring 的原理及区别、replaceFirst、replaceAll、replace 的区别、String 对 “+” 的重载、String.valueOf 和 Integer.toString 的区别、字符串的不可变性&lt;/p&gt;</description></item><item><title>使用 Spring AOP 注意事项</title><link>https://bridgeli.cn/posts/2018-11-25-%E4%BD%BF%E7%94%A8-spring-aop-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/</link><pubDate>Sun, 25 Nov 2018 11:48:23 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-11-25-%E4%BD%BF%E7%94%A8-spring-aop-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/</guid><description>&lt;p&gt;说实话，由于我个人某些基础不是很牢固，所以前一段时间关于 Spring Aop 踩了一个坑，其实很简单，今天就记录一下，先说结论：&lt;/p&gt;
&lt;p&gt;不能被 Spring AOP 增强的方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基于接口的动态代理：除 public 外的其它所有的方法，此外 public static 也不能被增强&lt;/li&gt;
&lt;li&gt;基于 CGLib 的动态代理：private、static、final 的方法，也就是只有 public 和 protected 可以，但是要注意切入点语法的配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;测试用例如下，pom 文件：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;slf4j-log4j12&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.7.7&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.3.11.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.aspectj&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;aspectjweaver&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.8.10&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.3.11.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;配置文件就比较简单了：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34;
xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xmlns:context=&amp;#34;http://www.springframework.org/schema/context&amp;#34;
xmlns:aop=&amp;#34;http://www.springframework.org/schema/aop&amp;#34;
xsi:schemaLocation=&amp;#34;http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd&amp;#34;&amp;gt;
&amp;lt;context:annotation-config/&amp;gt;
&amp;lt;context:component-scan base-package=&amp;#34;cn.bridgeli&amp;#34;/&amp;gt;
&amp;lt;aop:aspectj-autoproxy/&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;切面类：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogInterceptor {
// @Pointcut(&amp;#34;execution(\* \* com.bjsxt.service..*.add(..))&amp;#34;)
@Pointcut(&amp;#34;execution(\* cn.bridgeli.demo.service..\*.*(..))&amp;#34;)
public void myMethod() {
}
@Before(&amp;#34;myMethod()&amp;#34;)
public void before() {
System.out.println(&amp;#34;method before&amp;#34;);
}
@Around(&amp;#34;myMethod()&amp;#34;)
public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println(&amp;#34;method around start&amp;#34;);
pjp.proceed();
System.out.println(&amp;#34;method around end&amp;#34;);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试类：&lt;/p&gt;</description></item><item><title>【转载】Redis 分布式锁进化史</title><link>https://bridgeli.cn/posts/2018-10-14-%E8%BD%AC%E8%BD%BDredis-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E8%BF%9B%E5%8C%96%E5%8F%B2/</link><pubDate>Sun, 14 Oct 2018 09:32:11 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-10-14-%E8%BD%AC%E8%BD%BDredis-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E8%BF%9B%E5%8C%96%E5%8F%B2/</guid><description>&lt;p&gt;按：系统架构经过多年演进，现在越来越多的系统采用微服务架构，而说到微服务架构必然牵涉到分布式，以前单体应用加锁是很简单的，但现在分布式系统下加锁就比较难了，我之前曾简单写过&lt;a href="https://www.bridgeli.cn/archives/326"&gt;一篇文章&lt;/a&gt;，关于分布式锁的实现，但有一次发现实现的分布式锁是有问题的，因为出问题的概率很低，所以当时也没在意，前几天和朋友聊这个问题，想起来看过一篇文章，写的不错，今天特转载过来，希望能让更多的人看到，同时也加深一下记忆。原文链接是：http://tech.dianwoda.com/2018/04/11/redisfen-bu-shi-suo-jin-hua-shi/&lt;/p&gt;
&lt;p&gt;以下为原文：&lt;/p&gt;
&lt;p&gt;近两年来微服务变得越来越热门，越来越多的应用部署在分布式环境中，在分布式环境中，数据一致性是一直以来需要关注并且去解决的问题，分布式锁也就成为了一种广泛使用的技术，常用的分布式实现方式为Redis，Zookeeper，其中基于Redis的分布式锁的使用更加广泛。&lt;/p&gt;
&lt;p&gt;但是在工作和网络上看到过各个版本的Redis分布式锁实现，每种实现都有一些不严谨的地方，甚至有可能是错误的实现，包括在代码中，如果不能正确的使用分布式锁，可能造成严重的生产环境故障，本文主要对目前遇到的各种分布式锁以及其缺陷做了一个整理，并对如何选择合适的Redis分布式锁给出建议。&lt;/p&gt;
&lt;p&gt;一. 各个版本的Redis分布式锁&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;V1.0&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
tryLock() {
SETNX Key 1
EXPIRE Key Seconds
}
release() {
DELETE Key
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个版本应该是最简单的版本，也是出现频率很高的一个版本，首先给锁加一个过期时间操作是为了避免应用在服务重启或者异常导致锁无法释放后，不会出现锁一直无法被释放的情况。&lt;/p&gt;
&lt;p&gt;这个方案的一个问题在于每次提交一个Redis请求，如果执行完第一条命令后应用异常或者重启，锁将无法过期，一种改善方案就是使用Lua脚本（包含SETNX和EXPIRE两条命令），但是如果Redis仅执行了一条命令后crash或者发生主从切换，依然会出现锁没有过期时间，最终导致无法释放。&lt;/p&gt;
&lt;p&gt;另外一个问题在于，很多同学在释放分布式锁的过程中，无论锁是否获取成功，都在finally中释放锁，这样是一个锁的错误使用，这个问题将在后续的V3.0版本中解决。&lt;/p&gt;
&lt;p&gt;针对锁无法释放问题的一个解决方案基于GETSET命令来实现&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;V1.1 基于GETSET&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
tryLock() {
NewExpireTime = CurrentTimestamp + ExpireSeconds
if (SETNX Key NewExpireTime Seconds) {
oldExpireTime = GET(Key)
if (oldExpireTime &amp;lt; CurrentTimestamp) {
NewExpireTime = CurrentTimestamp+ExpireSeconds
CurrentExpireTime = GETSET(Key,NewExpireTime)
if (CurrentExpireTime == oldExpireTime) {
return 1;
} else {
return 0;
}
}
}
}
release() {
DELETE key
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;思路：&lt;/p&gt;</description></item><item><title>【转载】设计 RPC 接口时，你有考虑过这些吗？</title><link>https://bridgeli.cn/posts/2018-09-02-%E8%BD%AC%E8%BD%BD%E8%AE%BE%E8%AE%A1-rpc-%E6%8E%A5%E5%8F%A3%E6%97%B6%E4%BD%A0%E6%9C%89%E8%80%83%E8%99%91%E8%BF%87%E8%BF%99%E4%BA%9B%E5%90%97/</link><pubDate>Sun, 02 Sep 2018 08:05:17 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-09-02-%E8%BD%AC%E8%BD%BD%E8%AE%BE%E8%AE%A1-rpc-%E6%8E%A5%E5%8F%A3%E6%97%B6%E4%BD%A0%E6%9C%89%E8%80%83%E8%99%91%E8%BF%87%E8%BF%99%E4%BA%9B%E5%90%97/</guid><description>&lt;p&gt;按：系统架构经过多年演进，现在越来越多的系统采用微服务架构，而微服务架构最重要的就是面向接口编程，所以接口的设计就尤为重要了，我一直认为一个好的接口自己会说话，也就是看到接口，我就知道这个接口是干啥的、参数是啥、返回值是啥以及可能会遇到哪些问题，但目前对 RPC 接口设计可以说有两派，前一段时间看了一篇文章是我的想法完全一样，特转载到本人博客，希望更多的能够看到、有更多的人参与讨论两种的优劣。&lt;/p&gt;
&lt;p&gt;在开始之前呢，先吐槽一个本文没有提到的问题，我不知道有些人是怎么想的，对外暴露的接口，也就是最终被打成 jar 包供外部服务依赖模块，有些人喜欢在里面引入各种无关的第三方依赖，我遇到过把 spring mvc、spring、mybatis、dubbo 等等还有其他的像 POI 什么乱七八糟的第三方 jar 都依赖进来的，说实话，我只想问：可以说脏话吗？不可以啊，那好吧，我无话可说了，我们开始看原文吧。&lt;/p&gt;
&lt;p&gt;RPC 框架的讨论一直是各个技术交流群中的热点话题，阿里的 dubbo，新浪微博的 motan，谷歌的 grpc，以及不久前蚂蚁金服开源的 sofa，都是比较出名的 RPC 框架。RPC 框架，或者一部分人习惯称之为服务治理框架，更多的讨论是存在于其技术架构，比如 RPC 的实现原理，RPC 各个分层的意义，具体 RPC 框架的源码分析…但却并没有太多话题和“如何设计 RPC 接口”这样的业务架构相关。&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/09/jokes-1024x482.png" alt="" width="1024" height="482" class="alignnone size-medium wp-image-572" /&gt;
&lt;p&gt;可能很多小公司程序员还是比较关心这个问题的，这篇文章主要分享下一些个人眼中 RPC 接口设计的最佳实践。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初识 RPC 接口设计&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于 RPC 中的术语每个程序员的理解可能不同，所以文章开始，先统一下 RPC 术语，方便后续阐述。&lt;/p&gt;
&lt;p&gt;大家都知道共享接口是 RPC 最典型的一个特点，每个服务对外暴露自己的接口，该模块一般称之为 api；外部模块想要实现对该模块的远程调用，则需要依赖其 api；每个服务都需要有一个应用来负责实现自己的 api，一般体现为一个独立的进程，该模块一般称之为 app。&lt;/p&gt;
&lt;p&gt;api 和 app 是构建微服务项目的最简单组成部分，如果使用 maven 的多 module 组织代码，则体现为如下的形式。&lt;/p&gt;
&lt;p&gt;serviceA 服务&lt;/p&gt;
&lt;p&gt;serviceA/pom.xml 定义父 pom 文件&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;modules&amp;gt;
&amp;lt;module&amp;gt;serviceA-api&amp;lt;/module&amp;gt;
&amp;lt;module&amp;gt;serviceA-app&amp;lt;/module&amp;gt;
&amp;lt;/modules&amp;gt;
&amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
&amp;lt;groupId&amp;gt;moe.cnkirito&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;serviceA&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;serviceA/serviceA-api/pom.xml 定义对外暴露的接口，最终会被打成 jar 包供外部服务依赖&lt;/p&gt;</description></item><item><title>关于 tomcat 排查错误的一个小小感悟</title><link>https://bridgeli.cn/posts/2018-08-17-%E5%85%B3%E4%BA%8E-tomcat-%E6%8E%92%E6%9F%A5%E9%94%99%E8%AF%AF%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E5%B0%8F%E6%84%9F%E6%82%9F/</link><pubDate>Fri, 17 Aug 2018 15:25:38 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-08-17-%E5%85%B3%E4%BA%8E-tomcat-%E6%8E%92%E6%9F%A5%E9%94%99%E8%AF%AF%E7%9A%84%E4%B8%80%E4%B8%AA%E5%B0%8F%E5%B0%8F%E6%84%9F%E6%82%9F/</guid><description>&lt;p&gt;前几天响应公司的要求，系统日志接入公司的 ELK，按照中间件的同学要求之后，果然不出意外的遇到了问题，项目跑不起来了，控制台 catalina.out 打印日志如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
Aug 16, 2018 10:02:21 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler [&amp;#34;http-apr-8144&amp;#34;]
Aug 16, 2018 10:02:21 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler [&amp;#34;ajp-apr-58144&amp;#34;]
Aug 16, 2018 10:02:21 AM org.apache.catalina.startup.Catalina start
INFO: Server startup in 28819 ms
Aug 16, 2018 10:02:23 AM org.apache.catalina.loader.WebappClassLoaderBase loadClass
INFO: Illegal access: this web application instance has been stopped already. Could not load com.alibaba.rocketmq.shade.io.netty.util.concurren
t.DefaultPromise$2. The eventual following stack trace is caused by an error thrown for debugging purposes as well as to attempt to terminate t
he thread which caused the illegal access, and has no functional impact.
java.lang.IllegalStateException
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1743)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1701)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
Exception in thread &amp;#34;NettyClientWorkerThread_3&amp;#34; java.lang.NoClassDefFoundError: com/alibaba/rocketmq/shade/io/netty/util/concurrent/DefaultPromi
se$2
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.ClassNotFoundException: com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise$2
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1858)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1701)
&amp;amp;#8230; 4 more
Aug 16, 2018 10:02:23 AM org.apache.catalina.loader.WebappClassLoaderBase loadClass
INFO: Illegal access: this web application instance has been stopped already. Could not load com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise$2. The eventual following stack trace is caused by an error thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access, and has no functional impact.
java.lang.IllegalStateException
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1743)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1701)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
Exception in thread &amp;#34;NettyClientWorkerThread_4&amp;#34; java.lang.NoClassDefFoundError: com/alibaba/rocketmq/shade/io/netty/util/concurrent/DefaultPromise$2
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
Exception in thread &amp;#34;NettyClientWorkerThread_2&amp;#34; Exception in thread &amp;#34;NettyClientWorkerThread_1&amp;#34; java.lang.NoClassDefFoundError: com/alibaba/rocketmq/shade/io/netty/util/concurrent/DefaultPromise$2
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
java.lang.NoClassDefFoundError: com/alibaba/rocketmq/shade/io/netty/util/concurrent/DefaultPromise$2
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
Exception in thread &amp;#34;NettyClientSelector_1&amp;#34; java.lang.NoClassDefFoundError: com/alibaba/rocketmq/shade/io/netty/util/concurrent/DefaultPromise$2
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:590)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.DefaultPromise.setSuccess(DefaultPromise.java:398)
at com.alibaba.rocketmq.shade.io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:151)
at java.lang.Thread.run(Thread.java:745)
Aug 16, 2018 10:02:28 AM org.apache.catalina.loader.WebappClassLoaderBase loadClass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;很常见的一个错，java.lang.NoClassDefFoundError，找不到 com/alibaba/rocketmq/shade/io/netty/util/concurrent/DefaultPromise，但是不应该啊，因为什么都没有修改，之前都是跑的好好地，怎么会突然找不到这个类，莫名其妙，有同事说，是不是修改 jar 的版本之类的，不过我确实没有修改，只是引入了公司 ELK 相关的 jar 而已，而且看了一下这个类在 classpath 下是存在的，后来经同事提醒，这个地方可能不是真正的报错的地方，看一下 tomcat 的 localhost 日志，果然在发现了如下的报错：&lt;/p&gt;</description></item><item><title>JVM 群关于 Autowired 的讨论</title><link>https://bridgeli.cn/posts/2018-07-29-jvm-%E7%BE%A4%E5%85%B3%E4%BA%8E-autowired-%E7%9A%84%E8%AE%A8%E8%AE%BA/</link><pubDate>Sun, 29 Jul 2018 06:44:53 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-07-29-jvm-%E7%BE%A4%E5%85%B3%E4%BA%8E-autowired-%E7%9A%84%E8%AE%A8%E8%AE%BA/</guid><description>&lt;p&gt;前一段时间 JVM 群有人遇到了一个 stackoverflow 的问题，引发了一个关于 Autowired 的讨论，由于我做的项目可能比较小，并没有遇到过，但感觉这也许就是一个坑，记录下来&lt;br&gt;
，如果谁有遇到这个问题，说不定就有帮助。&lt;/p&gt;
&lt;p&gt;下面我会贴出来群里面的讨论，如果不想看，直接看我的得出的结论，所以 TL;DR 版：&lt;/p&gt;
&lt;p&gt;spring 中依赖注入有两个注解：Autowired 和 Resource。Resource 的注入的时候是 byName，而 Autowired 注入的时候是 byType，所以平时并没有很大的区别，但 Autowired 和 getBean(Object.class) 会导致栈很深，有可能会导致 stackoverflow，所以如果遇到这个栈很深，不使用 Autowired 会缓解一下，也就是根据我的理解，在项目中能用 Resource 还是用 Resource 比较好。&lt;/p&gt;
&lt;p&gt;原版群里的详细讨论如下图（至于深层次的原因，大家可以根据源码自己跟进看一下）：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource1-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-551" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource2-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-552" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource3-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-553" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource4-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-554" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource5-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-555" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource6-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-556" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource7-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-557" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource8-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-558" /&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/Autowired_Resource9-768x1451.png" alt="" width="768" height="1451" class="alignnone size-medium wp-image-559" /&gt;
&lt;p&gt;里面 深圳-随缘-颉 贴出的一张图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/07/spring_papulateBean-768x445.jpeg" alt="" width="768" height="445" class="alignnone size-medium wp-image-561" /&gt;
&lt;p&gt;感谢这一群小伙伴，如果有信息泄漏，很抱歉。&lt;/p&gt;
&lt;p&gt;另外曾经有朋友因为 JVM 的存在，就认为 Java 没有内存泄漏的问题，这是对 Java 有多大的误解啊。&lt;/p&gt;</description></item><item><title>Markdown 基本语法介绍</title><link>https://bridgeli.cn/posts/2018-07-15-markdown-%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BB%8B%E7%BB%8D/</link><pubDate>Sun, 15 Jul 2018 08:12:12 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-07-15-markdown-%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BB%8B%E7%BB%8D/</guid><description>&lt;p&gt;Markdown 是我很喜欢的一个轻量级标记语言，但也因为不常写，所以有些语法记得不是很清楚，经常写的时候需要查一些资料，所以这次就把一些简单的常用的语法做个笔记。&lt;/p&gt;
&lt;p&gt;在介绍 markdown 语法之前，先写一点废话。&lt;/p&gt;
&lt;p&gt;一. markdown是什么？&lt;/p&gt;
&lt;p&gt;简单的一句话就是，Markdown 可谓是程序员必备的一种写作格式！你还在用 word 写文档么？简直 low 爆了，赶紧抛弃，从现在开始，立刻，马上，学习下 markdown。&lt;/p&gt;
&lt;p&gt;二. markdown有什么好处？&lt;/p&gt;
&lt;p&gt;简单说，语法简单、再也不用专注排版、兼容 html、还有其他的的很多很强大的功能（这么厉害，你的博客为啥不用？因为 WordPress 原生编辑器不支持 markdown，也一直没有安装第三方的插件，所以就没有）&lt;/p&gt;
&lt;p&gt;三. markdown 编辑器有哪些？&lt;/p&gt;
&lt;p&gt;个人最喜欢的开发工具是，GitHub 出品的 Atom，自带提示；之前也在 Windows 下用过 markdownpad，也还行，mac 下有很多人推荐 mou，另外据说 Sublime 也不错，还有一些在线工具据说也不错。&lt;/p&gt;
&lt;p&gt;四. markdown 语法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;标题 1 到 6：#1 到 #6&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码示例：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
\# 标题一
\### 标题三
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;区块：&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码示例：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;gt; 这是区块
&amp;gt;
&amp;gt; 第二行区块
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;斜体：*斜体*&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码示例：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
\*斜体\*
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;加粗：**加粗**&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码示例：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
\*\*该部分加粗\*\*
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="5"&gt;
&lt;li&gt;删除线：&lt;del&gt;删除线&lt;/del&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
~~这是加删除线的文字~~
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="6"&gt;
&lt;li&gt;换行和分段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;换行：只需在行末加两个空格键和一个回车键即可换行。快捷键：control + 回车键&lt;br&gt;
分段：段落之间空一行即可&lt;/p&gt;</description></item><item><title>Git 配置多个用户身份和强制检查各个项目用户名邮箱设置</title><link>https://bridgeli.cn/posts/2018-07-01-git-%E9%85%8D%E7%BD%AE%E5%A4%9A%E4%B8%AA%E7%94%A8%E6%88%B7%E8%BA%AB%E4%BB%BD%E5%92%8C%E5%BC%BA%E5%88%B6%E6%A3%80%E6%9F%A5%E5%90%84%E4%B8%AA%E9%A1%B9%E7%9B%AE%E7%94%A8%E6%88%B7%E5%90%8D%E9%82%AE/</link><pubDate>Sun, 01 Jul 2018 09:31:02 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-07-01-git-%E9%85%8D%E7%BD%AE%E5%A4%9A%E4%B8%AA%E7%94%A8%E6%88%B7%E8%BA%AB%E4%BB%BD%E5%92%8C%E5%BC%BA%E5%88%B6%E6%A3%80%E6%9F%A5%E5%90%84%E4%B8%AA%E9%A1%B9%E7%9B%AE%E7%94%A8%E6%88%B7%E5%90%8D%E9%82%AE/</guid><description>&lt;p&gt;今天的文章比较简单，1. 就是为 Git 单个项目做身份配置，就是配置单独的邮箱和用户名。因为我们平时可能会在不同的几个项目中工作，各个项目的用户名可能不同，最基本的就是公司的项目和我们自己在 GitHub 上玩，所以为了保证日志的准确性和提交时无误，最好对各个项目设置。以前没有研究过，所以就一只默认用公司的用户名玩，但一直感觉不太好，2. 在提交时，user.name, user.email会进入日志。这些信息，是追踪代码变更的关键，所以必须配置，偶然看见秋大有篇文章写这个，试了一些不错，记录一下。&lt;/p&gt;
&lt;p&gt;全局配置和项目配置&lt;/p&gt;
&lt;p&gt;全局配置信息在: ~/.gitconfig&lt;br&gt;
项目配置在项目目录下的： ./.git/config&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;配置多个用户身份&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;git config –global 操作全局配置, 不带 –global 选项的话，会尝试相对于当前目录下找：./git/config, 找不到的话，报错，所以如此一来，我们就可以：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
\# for global setting
git config &amp;amp;#8211;global user.name xxx
git config &amp;amp;#8211;global user.email xxx@xxx.com
\# for repository
git config user.name xxxx
git config user.email xxxx@xxx.com
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;另，我们都知道 Linux 下，我们可以通过 alias 命令设置别名（之前一直就不住这个命令，每次都是查一下，后来一朋友说多好记啊 ali as，阿里 as，就再也忘不了了），我们可以通过一个短命令把 git 日志重新格式化一下，至于其他的像：git status，git commit 是否重命名就看习惯了（看过有人把常错的单词都重命名为正确的，省时间，机智）&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
git config &amp;amp;#8211;global alias.lg &amp;#34;log &amp;amp;#8211;color &amp;amp;#8211;graph &amp;amp;#8211;pretty=format:&amp;amp;#8217;%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&amp;lt;%an&amp;gt;%Creset&amp;amp;#8217; &amp;amp;#8211;abbrev-commit &amp;amp;#8211;&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;强制检查各个项目用户名邮箱设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用 pre-commit 钩子进行检查，所以在 .git/hooks 中添加一个 pre-commit 文件（注意权限），内容如下：&lt;/p&gt;</description></item><item><title>org.apache.ibatis.builder.IncompleteElementException: Could not find parameter map</title><link>https://bridgeli.cn/posts/2018-06-02-org-apache-ibatis-builder-incompleteelementexception-could-not-find-parameter-map/</link><pubDate>Sat, 02 Jun 2018 09:17:30 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-06-02-org-apache-ibatis-builder-incompleteelementexception-could-not-find-parameter-map/</guid><description>&lt;p&gt;上周和同事一块开发一个功能模块，在开发中拉下来同事代码，在测试的时候，突然跑不通了，报错信息如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
org.apache.ibatis.builder.IncompleteElementException: Could not find parameter map java.util.Map
at org.apache.ibatis.builder.MapperBuilderAssistant.getStatementParameterMap(MapperBuilderAssistant.java:320)
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:296)
at org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode(XMLStatementBuilder.java:109)
at org.apache.ibatis.session.Configuration.buildAllStatements(Configuration.java:753)
at org.apache.ibatis.session.Configuration.hasStatement(Configuration.java:723)
at org.apache.ibatis.session.Configuration.hasStatement(Configuration.java:718)
at org.apache.ibatis.binding.MapperMethod$SqlCommand.&amp;lt;init&amp;gt;(MapperMethod.java:201)
at org.apache.ibatis.binding.MapperMethod.&amp;lt;init&amp;gt;(MapperMethod.java:48)
at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:59)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)
at com.sun.proxy.$Proxy40.insertSelective(Unknown Source)
at com.weidai.urge.service.disposition.BidDispositionOrderServiceTest.testInsertSelective(BidDispositionOrderServiceTest.java:48)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.IllegalArgumentException: Parameter Maps collection does not contain value for java.util.Map
at org.apache.ibatis.session.Configuration$StrictMap.get(Configuration.java:853)
at org.apache.ibatis.session.Configuration.getParameterMap(Configuration.java:625)
at org.apache.ibatis.builder.MapperBuilderAssistant.getStatementParameterMap(MapperBuilderAssistant.java:318)
&amp;amp;#8230; 39 more
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看报错信息，也就是自己跑一个 junit test，测试批量插入，突然他就不行了，再看看自己写的批量插入没问题啊，我也没用 map，感觉好奇怪，就网上搜了一下这个问题，原来确实有问题，也确实在这个配置文件中，提示的错误也对，就是提示的位置不对。mybatis 中只要有任何一个地方报错，都无法通过。最后搜了一下发现是刚刚来下来的代码，同事新增了一个方法上将 parameterType 写成了 parameterMap 了&lt;/p&gt;</description></item><item><title>上传 Java 库到 Maven central repository</title><link>https://bridgeli.cn/posts/2018-05-20-%E4%B8%8A%E4%BC%A0-java-%E5%BA%93%E5%88%B0-maven-central-repository/</link><pubDate>Sun, 20 May 2018 08:17:54 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-05-20-%E4%B8%8A%E4%BC%A0-java-%E5%BA%93%E5%88%B0-maven-central-repository/</guid><description>&lt;p&gt;之前看过 Trinea 写过一篇文章，如果上传 Java 库到 Maven central repository，前一段时间感觉公司封装的 mybatis-generator 不好用，完全没有解决原生的 mybatis-generator 的问题，所以就重新做了一次封装，主要是加了查询分页，然后就想到是不是可以上传到 Maven central repository 玩玩，看了一下 Trinea 的这篇文章感觉挺简单的（原文见后面参考资料），但实际上还是有一些坑，具体的可以看 Trinea 的这篇文章，我主要写一下遇到的一些坑。&lt;/p&gt;
&lt;p&gt;先说明一下，pom 文件请参考我的配置：https://github.com/bridgeli/mybatis-generator-plugin/blob/master/pom.xml，这里面所有的配置都是必须的，也是最少的了吧，自己写的时候修改一些内容，适配自己的就好了，Trinea 写的那个 pom 还是比较适合 Android。&lt;/p&gt;
&lt;p&gt;发布 release 版本的时候，使用命令&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
mvn release:clean release:prepare release:perform
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行这个命令的时候，可能会遇到三个问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Git 报错，使用 Git 几年了，进入会遇到这样的错误。错误信息：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
Unable to commit files
[ERROR] Provider message:
[ERROR] The git-add command failed.
[ERROR] Command output:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实在不知道咋回事，可能是我有些文件没提交，反正提交之后就好了，看到有人说他报错是因为他不是 Git 管理的项目，我作为 Git 脑残粉，肯定是的，可能就是因为没有提交吧，反正最后就这么莫名其妙的解决了。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;提示没有 snapshot 版本，所以执行这个命令的时候，版本号和发布 snapshot 一样就好，而不是自己手动改成 release 版。&lt;/li&gt;
&lt;li&gt;因为此时会发布一个 release 版本，会有 tag 推送到 GitHub 有可能会遇到 401，看到这个 error code，应该都清楚咋回事，没权限。不知道，病急乱投医，我是在 GitHub 上添加了一个 GPG key 并且在 pom 文件中新增一个 plugin。这个问题有人说是账号和密码错误，但是我很肯定我的账号和密码一点也没有错。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;plugin&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;maven-gpg-plugin&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.5&amp;lt;/version&amp;gt;
&amp;lt;executions&amp;gt;
&amp;lt;execution&amp;gt;
&amp;lt;id&amp;gt;sign-artifacts&amp;lt;/id&amp;gt;
&amp;lt;phase&amp;gt;verify&amp;lt;/phase&amp;gt;
&amp;lt;goals&amp;gt;
&amp;lt;goal&amp;gt;sign&amp;lt;/goal&amp;gt;
&amp;lt;/goals&amp;gt;
&amp;lt;/execution&amp;gt;
&amp;lt;/executions&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最终解决了这个问题，关于这个问题，可能是我操作的问题没有看到过别人说过，另外在 GitHub 上他的颜色一直还是：灰的，没有显示使用过，所以可能是我操作的有问题，不需要在 GitHub 上添加这个 GPG key，具体大家可以测一下，我后期也会测试。&lt;br&gt;
另，需要说明的是，生成 GPG key 的时候用到的邮箱和我在 GitHub 上的邮箱，我用的不是同一个邮箱，建议还是用同一个吧，省得出莫名其妙的问题。&lt;/p&gt;</description></item><item><title>Java Thread 同步</title><link>https://bridgeli.cn/posts/2018-05-12-java-thread-%E5%90%8C%E6%AD%A5/</link><pubDate>Sat, 12 May 2018 13:33:53 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-05-12-java-thread-%E5%90%8C%E6%AD%A5/</guid><description>&lt;p&gt;之前遇到一个问题，就是如何让线程同步，由于自己多线程的东西实在不懂，所以不知道怎么办，但感觉应该是一个很简单的东西，所以就从网上搜一下资料，原来如此简单，直接调用 join 方法就好了。写篇博客记录一下 join 的使用方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Thread类中的join方法的主要作用就是同步，它可以使得线程之间的并行执行变为串行执行。具体看代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest(&amp;#34;bridgeli&amp;#34;);
ThreadJoinTest t2 = new ThreadJoinTest(&amp;#34;liqiao&amp;#34;);
t1.start();
/**
* join的意思是使得放弃当前线程的执行，并返回对应的线程，例如下面代码的意思就是：
* 程序在main线程中调用t1线程的join方法，则main线程放弃cpu控制权，并返回t1线程继续执行直到线程t1执行完毕
* 所以结果是t1线程执行完后，才到主线程执行，相当于在main线程中同步t1线程，t1执行完了，main线程才有执行的机会
*
* join方法可以传递参数，join(10000)表示main线程会等待t1线程10毫秒，10毫秒过去后，
* main线程和t1线程之间执行顺序由串行执行变为普通的并行执行
*/
t1.join(10000);
t2.start();
}
}
package cn.bridgeli.demo;
public class ThreadJoinTest extends Thread {
public ThreadJoinTest(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i &amp;lt; 100; i++) {
System.out.println(this.getName() + &amp;#34;:&amp;#34; + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;上面注释也大概说明了 join 方法的作用：在 主线程 中调用了 t1 线程的 join() 方法时，表示只有当 t1 线程执行完毕时，主线程 才能继续执行，也就是开始执行 t2。注意，join 方法其实也可以传递一个参数给它的，表示：如果 主线程 在 t1 执行 1000 毫秒之后，继续执行，也就是开启 t2 线程。&lt;/p&gt;</description></item><item><title>介绍一个 Mybatis 插件：mybatis-generator-plugin</title><link>https://bridgeli.cn/posts/2018-04-29-%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%AA-mybatis-%E6%8F%92%E4%BB%B6mybatis-generator-plugin/</link><pubDate>Sun, 29 Apr 2018 09:37:55 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-04-29-%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%AA-mybatis-%E6%8F%92%E4%BB%B6mybatis-generator-plugin/</guid><description>&lt;p&gt;在实际开发中，我们都是先建表，然后根据表生成对应的 Java 类，现在很流行的 ORMapping 框架是: Mybatis，所以我们需要生成 entity、mapper 和 xml，我们都知道有一个插件是：mybatis-generator，使用它就可以很方便的生成这些结构化的重复性基础性的代码，但是他有一个问题，生成的查询没有分页，所以很烦。然后我搜索了一些资料，重新封装了一下，重新命名为：mybatis-generator-plugin，具体源码放在了 GitHub 上：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
https://github.com/bridgeli/mybatis-generator-plugin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;大家可以 clone 下来，deploy 到自己公司的私服，或者 install 到本地使用。具体就是在 pom 文件中。具体用法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 pom 文件中添加如下内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;properties&amp;gt;
&amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
&amp;lt;/properties&amp;gt;
&amp;lt;build&amp;gt;
&amp;lt;plugins&amp;gt;
&amp;lt;plugin&amp;gt;
&amp;lt;groupId&amp;gt;org.mybatis.generator&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;mybatis-generator-maven-plugin&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.3.5&amp;lt;/version&amp;gt;
&amp;lt;dependencies&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;cn.bridgeli&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;mybatis-generator-plugin&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;verbose&amp;gt;true&amp;lt;/verbose&amp;gt;
&amp;lt;overwrite&amp;gt;true&amp;lt;/overwrite&amp;gt;
&amp;lt;configurationFile&amp;gt;src/main/resources/generatorConfig.xml&amp;lt;/configurationFile&amp;gt;
&amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;
&amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中：configurationFile 配置 generatorConfig.xml 文件的位置，我们生成的对应的文件，都是根据这个文件来的。所以第二步就是：&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;在对应的文件下添加该文件，它的模板如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;!DOCTYPE generatorConfiguration
PUBLIC &amp;#34;-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN&amp;#34;
&amp;#34;http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd&amp;#34;&amp;gt;
&amp;lt;generatorConfiguration&amp;gt;
&amp;lt;!&amp;amp;#8211; 请替换本地jar路径 此文件不要上传 &amp;amp;#8211;&amp;gt;
&amp;lt;classPathEntry
location=&amp;#34;/Users/bridgeli/.m2/repository/mysql/mysql-connector-java/5.1.33/mysql-connector-java-5.1.33.jar&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; mvn mybatis-generator:generate &amp;amp;#8211;&amp;gt;
&amp;lt;context id=&amp;#34;oneHour&amp;#34;
targetRuntime=&amp;#34;cn.bridgeli.plugin.IntrospectedTableMyBatis3ImplExt&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;suppressAllComments&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;useActualColumnNames&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 格式化java代码 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;javaFormatter&amp;#34;
value=&amp;#34;org.mybatis.generator.api.dom.DefaultJavaFormatter&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 格式化XML代码 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;xmlFormatter&amp;#34;
value=&amp;#34;org.mybatis.generator.api.dom.DefaultXmlFormatter&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置插件 &amp;amp;#8211;&amp;gt;
&amp;lt;plugin type=&amp;#34;cn.bridgeli.mybatis.MySQLLimitPlugin&amp;#34;&amp;gt;&amp;lt;/plugin&amp;gt;
&amp;lt;commentGenerator&amp;gt;
&amp;lt;!&amp;amp;#8211; 是否去除自动生成的注释 true：是 ： false:否 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;suppressAllComments&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/commentGenerator&amp;gt;
&amp;lt;!&amp;amp;#8211;数据库连接的信息：驱动类、连接地址、用户名、密码 &amp;amp;#8211;&amp;gt;
&amp;lt;jdbcConnection driverClass=&amp;#34;com.mysql.jdbc.Driver&amp;#34;
connectionURL=&amp;#34;jdbc:mysql://ip:port/databasename&amp;#34; userId=&amp;#34;username&amp;#34;
password=&amp;#34;password&amp;#34;&amp;gt;
&amp;lt;/jdbcConnection&amp;gt;
&amp;lt;javaTypeResolver&amp;gt;
&amp;lt;property name=&amp;#34;forceBigDecimals&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;/javaTypeResolver&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置model生成位置 &amp;amp;#8211;&amp;gt;
&amp;lt;javaModelGenerator targetPackage=&amp;#34;cn.bridgeli.entity&amp;#34;
targetProject=&amp;#34;src/main/java&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;enableSubPackages&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;trimStrings&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/javaModelGenerator&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置sqlmap生成位置 &amp;amp;#8211;&amp;gt;
&amp;lt;sqlMapGenerator targetPackage=&amp;#34;cn/bridgeli/mapper&amp;#34;
targetProject=&amp;#34;src/main/resources&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;enableSubPackages&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/sqlMapGenerator&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置mapper接口生成位置 &amp;amp;#8211;&amp;gt;
&amp;lt;javaClientGenerator type=&amp;#34;XMLMAPPER&amp;#34;
targetPackage=&amp;#34;cn.bridgeli.mapper&amp;#34; targetProject=&amp;#34;src/main/java&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;enableSubPackages&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/javaClientGenerator&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置表信息 &amp;amp;#8211;&amp;gt;
&amp;lt;table tableName=&amp;#34;table_name&amp;#34; enableCountByExample=&amp;#34;true&amp;#34;
domainObjectName=&amp;#34;DomainObjectName&amp;#34; enableDeleteByExample=&amp;#34;false&amp;#34;
enableSelectByExample=&amp;#34;true&amp;#34; enableUpdateByExample=&amp;#34;true&amp;#34;&amp;gt;
&amp;lt;generatedKey column=&amp;#34;ID&amp;#34; sqlStatement=&amp;#34;MySQL&amp;#34;
identity=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;/context&amp;gt;
&amp;lt;/generatorConfiguration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里面需要说明就是：&lt;/p&gt;</description></item><item><title>介绍一个强大易用的日期和时间库：Joda-Time</title><link>https://bridgeli.cn/posts/2018-03-31-%E7%BB%8D%E4%B8%80%E4%B8%AA%E5%BC%BA%E5%A4%A7%E6%98%93%E7%94%A8%E7%9A%84%E6%97%A5%E6%9C%9F%E5%92%8C%E6%97%B6%E9%97%B4%E5%BA%93joda-time/</link><pubDate>Sat, 31 Mar 2018 11:28:34 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-03-31-%E7%BB%8D%E4%B8%80%E4%B8%AA%E5%BC%BA%E5%A4%A7%E6%98%93%E7%94%A8%E7%9A%84%E6%97%A5%E6%9C%9F%E5%92%8C%E6%97%B6%E9%97%B4%E5%BA%93joda-time/</guid><description>&lt;p&gt;在 Java 中处理日期和时间是很常见的需求，基础的工具类就是我们熟悉的 Date 和 Calendar,之前我也曾经&lt;a href="https://www.bridgeli.cn/archives/181"&gt;写过一篇文章利用这两个类&lt;/a&gt;，怎么处理时间，然而这些工具类的 api 使用并不是很方便和强大，于是就诞生了Joda-Time 这个专门处理日期时间的库。而且 Joda-Time 很优秀，用了他之后再也停不下来，其在 Java 8 出现前的很长时间内成为 Java 中日期时间处理的事实标准，用来弥补 JDK 的不足。项目中要想使用 Joda-Time 很简单，只需要引入依赖：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;joda-time&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;joda-time&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;2.9.9&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;下面说一些常用的例子，供平时参考&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建任意时间对象&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dateTime=new DateTime(2018, 03, 31, 16, 57,55);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;创建当前时间对象&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dateTime=new DateTime();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;格式化时间输出&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dateTime = new DateTime();
System.out.println(dateTime.toString(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;));
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;解析文本格式时间&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTimeFormatter format = DateTimeFormat.forPattern(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;);
DateTime dateTime = DateTime.parse(&amp;#34;2018-03-31 17:01:45&amp;#34;, format);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="5"&gt;
&lt;li&gt;在当前日期上加上 90 天并输出结果&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dateTime = new DateTime();
System.out.println(dateTime.plusDays(90).toString(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="6"&gt;
&lt;li&gt;获取今天的开始时间&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime nowTime = new DateTime();
DateTime startOfDay = nowTime.withTimeAtStartOfDay();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="7"&gt;
&lt;li&gt;获取今天的结束时间&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime nowTime = new DateTime();
DateTime endOfDay = nowTime.millisOfDay().withMaximumValue();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="8"&gt;
&lt;li&gt;计算两个日期的相隔天数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime nowTime = new DateTime();
DateTime futureTime = new DateTime(2019, 10, 1, 0, 0, 0);
Int days = Days.daysBetween(nowTime, futureTime).getDays();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="9"&gt;
&lt;li&gt;本月的第一天和最后一天&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dateTime = new DateTime();
DateTime firstDateTimeOfMonth = dateTime.dayOfMonth().withMinimumValue().withTimeAtStartOfDay();
DateTime lastDateTimeOfMonth = dateTime.dayOfMonth().withMaximumValue().withTimeAtStartOfDay();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="10"&gt;
&lt;li&gt;上个月的第一天和最后一天&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dateTime = new DateTime();
DateTime startDate = dateTime.plusMonths(-1).dayOfMonth().withMinimumValue().withTimeAtStartOfDay();
DateTime endDate = dateTime.plusMonths(-1).dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="11"&gt;
&lt;li&gt;在每天的6:30处理一些东西&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dt=new DateTime().withHourOfDay(6).withMinuteOfHour(30).withSecondOfMinute(0);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="12"&gt;
&lt;li&gt;在每月的7号的6:30处理一些东西&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dt=new DateTime().withDayOfMonth(7).withHourOfDay(6).withMinuteOfHour(30).withSecondOfMinute(0);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="13"&gt;
&lt;li&gt;在每年的8月的7号的6:30处理一些东西&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dt=new DateTime().withMonthOfYear(8).withDayOfMonth(7).withHourOfDay(6).withMinuteOfHour(30).withSecondOfMinute(0);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="14"&gt;
&lt;li&gt;获取年终 月、日、时、分、秒、毫秒&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTime dt = new DateTime();
//获取当前时间的年
int year = dt.getYear();
//获取当前时间的月
int month = dt.getMonthOfYear();
//获取当前时间是一年中的第几天
int dayOfYear = dt.getDayOfYear();
//获取一个月中的天
int day = dt.getDayOfMonth();
//获取一周中的周几
int week = dt.getDayOfWeek();
//一天中的第几小时(取整)
int hour = dt.getHourOfDay();
//获取星期年
int weekOfyear = dt.getWeekyear();
//当前时间的秒中的毫秒
int ms = dt.getMillisOfSecond();
//获取当前时间的秒
int second = dt.getSecondOfDay();
//获取当前时间的毫秒
long millis = dt.getMillis();
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="15"&gt;
&lt;li&gt;判断时间跨度是否包含当前时间,某个时间&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
Interval interval = new Interval(new DateTime(2018, 1, 1, 0, 0, 0), new DateTime(2018, 3, 30, 0, 0, 0));
System.out.println(interval.containsNow());
boolean contained = interval.contains(new DateTime(&amp;#34;2012-03-01&amp;#34;));
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="16"&gt;
&lt;li&gt;与JDK互转换&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
//通过jdk时间对象构造
Date date = new Date();
DateTime dateTime = new DateTime(date);
Calendar calendar = Calendar.getInstance();
dateTime = new DateTime(calendar);
// Joda-time 各种操作&amp;amp;#8230;..
dateTime = dateTime.plusDays(1) // 增加天
.plusYears(1)// 增加年
.plusMonths(1)// 增加月
.plusWeeks(1)// 增加星期
.minusMillis(1)// 减分钟
.minusHours(1)// 减小时
.minusSeconds(1);// 减秒数
// 计算完转换成jdk 对象
Date date2 = dateTime.toDate();
Calendar calendar2 = dateTime.toCalendar(Locale.CHINA);
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="17"&gt;
&lt;li&gt;解析 CST 格式的时间字符串&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
DateTimeFormatter format = DateTimeFormat.forPattern(&amp;#34;EEE MMM dd HH:mm:ss zzz yyyy&amp;#34;);
DateTime dateTime = DateTime.parse(&amp;#34;Fri Mar 30 00:00:00 CST 2018&amp;#34;, format);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当然作为一个强大的 时间日期 处理工具类，他还有更多更强大的 API，可以参看：&lt;/p&gt;</description></item><item><title>记一次使用 lombok 小小的成长感悟</title><link>https://bridgeli.cn/posts/2018-02-25-%E8%AE%B0%E4%B8%80%E6%AC%A1%E4%BD%BF%E7%94%A8-lombok-%E5%B0%8F%E5%B0%8F%E7%9A%84%E6%88%90%E9%95%BF%E6%84%9F%E6%82%9F/</link><pubDate>Sun, 25 Feb 2018 11:34:01 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-02-25-%E8%AE%B0%E4%B8%80%E6%AC%A1%E4%BD%BF%E7%94%A8-lombok-%E5%B0%8F%E5%B0%8F%E7%9A%84%E6%88%90%E9%95%BF%E6%84%9F%E6%82%9F/</guid><description>&lt;p&gt;公司项目里面用了 lombok，感觉这个东西真是个好东西，然后公司也用的简单，所以也没仔细看文档就开始想当然的用了，然后就悲剧了，今天就记录一下这件事，写一下经验教训，具体怎么用，大家可以看最后的参考。&lt;br&gt;
lombok 有一个很好用的注解：@Data，当时以为这个注解就是相当于：@Getter和@Setter，所以有一次要重写 equals 和 hashcode 方法，然后就让 IDE 自动生成了，当时也没仔细看生成的是什么样子，然后就发现了 bug，仔细一看生成的 equals 方法原来是这样的：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import lombok.Data;
import java.util.Objects;
/**
* Created by bridgeli on 2018/2/25.
*/
@Data
public class LomBokTest {
private Integer id;
private String username;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
LomBokTest that = (LomBokTest) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), id);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当时还以为是 idea 的 bug，因为是 idea 自动生成的，之前用 eclipse 自动生成从没问题，eclipse 自动生成的是这样的：&lt;/p&gt;</description></item><item><title>NullPointerException in Java with no StackTrace</title><link>https://bridgeli.cn/posts/2018-01-07-nullpointerexception-in-java-with-no-stacktrace/</link><pubDate>Sun, 07 Jan 2018 10:39:30 +0000</pubDate><guid>https://bridgeli.cn/posts/2018-01-07-nullpointerexception-in-java-with-no-stacktrace/</guid><description>&lt;p&gt;这周一个项目遇到一个问题，同事查看日志发现抛出：NullPointerException，却没有堆栈信息，然后同事感觉很奇怪，因为打日志的方法，打印的确实是：e，而不是很多人不明所以的打印的：e.getMessage()。然后我看了一下想起来我看过某本书上说过的，JIT 优化。当某个异常抛出很多次之后，由于 Java 虚拟机 JIT 优化，会省略堆栈信息。往上面翻日志肯定可以会找到报错的地方，当然会出现报错的信息太多，比较难翻。写这篇文章的本来想找找那本书，参考一下的，结果忘了是那本书了，一时没找到，不过这个问题虽然不是非常常见，但是网上还是有很多说明的，所以就简单说说 JVM 有一个参数：OmitStackTraceInFastThrow 来控制是否开启此优化。关于此参数的简单说明：&lt;/p&gt;
&lt;p&gt;JVM参数-XX:-OmitStackTraceInFastThrow参数可以关掉JVM对堆栈信息的优化。如果设置了这个参数，那么异常堆栈就能完整输出了。&lt;/p&gt;
&lt;p&gt;“在服务器中的VM编译器现在提供准确的所有的“冷”内置异常堆栈回溯功能。为了性能考虑，当这些异常被抛出很多次时，这个方法会被重新编译，此后编译器将使用一种更快的抛出异常的方式，即抛出预先分配好的不带堆栈信息的异常。要完全关闭掉这种预分配的异常，就需要使用-XX:-OmitStackTraceInFastThrow参数。”&lt;/p&gt;
&lt;p&gt;相关stackoverflow讨论：https://stackoverflow.com/questions/2411487/nullpointerexception-in-java-with-no-stacktrace&lt;/p&gt;
&lt;p&gt;笨神小程序 JVMPocket 对此参数的解释截图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2018/01/OmitStackTraceInFastThrow-305x1024.png" alt="" width="305" height="1024" class="alignnone size-medium wp-image-510" /&gt;
&lt;p&gt;另，看到网上有部分人建议关闭此参数，个人是建议的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于Java 在虚拟机中运行，天生是比 c 要慢的，为了优化此问题，那些 JVM 大神们搞出了 JIT 优化，对性能有了大幅提升。&lt;/li&gt;
&lt;li&gt;异常信息使我们应该特别关注了，当出现了异常应该尽快发现问题解决问题，而不是这个异常发生很多次了但是一直不知道。&lt;/li&gt;
&lt;li&gt;打详细堆栈信息是很影响性能的，如果这个异常出现很多次了，没必要每次都打出来异常，看一个地方就知道了，而不是让他一直打，影响性能。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那有人说了当遇到此问题时，堆栈信息看不到，日志又比较难翻怎么办？我们好像也不能动态的添加 JVM 的启动参数，所以保险起见还是关闭此参数。个人认为：这个异常这么常出现，而且是 JIT 的优化导致看不到堆栈信息了，可以找一台机器重启一下就好了，没必要因小失大，我们从笨神的小程序也可以看到 JVM 默认是开启此参数的，开启肯定是有其道理的。&lt;/p&gt;</description></item><item><title>是的，我也开启了全站HTTPS</title><link>https://bridgeli.cn/posts/2017-09-03-%E6%98%AF%E7%9A%84%E6%88%91%E4%B9%9F%E5%BC%80%E5%90%AF%E4%BA%86%E5%85%A8%E7%AB%99https/</link><pubDate>Sun, 03 Sep 2017 11:25:18 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-09-03-%E6%98%AF%E7%9A%84%E6%88%91%E4%B9%9F%E5%BC%80%E5%90%AF%E4%BA%86%E5%85%A8%E7%AB%99https/</guid><description>&lt;p&gt;现在的趋势都在全站HTTPS，据说在Google内部有一个时间表，会把所有未开启HTTPS的网站标注为不安全（目前仅仅会把带密码框的输入页标注为不安全），所以一直想玩玩，去年的时候就看到新浪timyang的博客开启了全站HTTPS，并写了一篇文章如何开启，当时就想玩玩，但感觉还是稍有麻烦，而且当时的博客服务器用的Apache，对Apache配置不熟，想着是自己的小博客就没动，前几天突然看到coolshell网也开启了全站HTTPS，发现现在配置变得很简单了，而且我的博客服务器也由Apache换成了Nginx，所以就玩了玩，确实很方便。&lt;br&gt;
首先声明，无论是timyang还是左耳朵耗子，使用的都是Let’s Encrypt，他是一个公益组织，表示感谢，网址：https://letsencrypt.org/&lt;br&gt;
下面写一下开启的方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先，打开 &lt;a href="https://certbot.eff.org"&gt;https://certbot.eff.org&lt;/a&gt; 网页。&lt;/li&gt;
&lt;li&gt;在那个机器上图标下面，你需要选择一下你用的 Web 接入软件 和你的 操作系统。比如，我选的，nginx 和 CentOS 6。&lt;/li&gt;
&lt;li&gt;然后就会跳转到一个安装教程网页。你就照着做一遍就好了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如我选的这个调到安装教程页，出现的命令是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
wget https://dl.eff.org/certbot-auto
chmod a+x certbot-auto
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后，运行如下命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
./path/to/certbot-auto &amp;amp;#8211;nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;certbot-auto 会自动检查到你的 nginx.conf 下的配置，把你所有的虚拟站点都列出来，然后让你选择需要开启 https 的站点。你就简单的输入列表编号（用空格分开），因为我的就一个所以直接回车就好了（里面有一些需要你填写的东西，连我这英文巨差的人都可以看懂，相信大家都能看的懂），然后，certbot-auto 就帮你下载证书并更新 nginx.conf 了。&lt;/p&gt;
&lt;p&gt;但在这个过程中需要注意的一点：记得开启你的 443 端口，我博客用的阿里云，当时没开启直接报错了，开启之后重新执行一下这个命令就好了。&lt;/p&gt;
&lt;p&gt;你打开你的 nginx.conf 文件 ，你可以发现你的文件中的 server 配置中可能被做了如下的修改：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/www.bridgeli.cn/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/www.bridgeli.cn/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;和&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
if ($scheme != &amp;#34;https&amp;#34;) {
return 301 https://$host$request_uri;
} # managed by Certbot
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;另外开启HTTPS之后都建议开启HTTP/2性能更好，但这要求你的Nginx版本要大于 1.9.5 ，开启的方法也无比简单，只需要在 nginx.conf 的 listen 443 ssl; 后面加上 http2 就好了。如下所示：&lt;/p&gt;</description></item><item><title>秒杀系统架构优化思路[转载]</title><link>https://bridgeli.cn/posts/2017-08-08-%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E4%BC%98%E5%8C%96%E6%80%9D%E8%B7%AF%E8%BD%AC%E8%BD%BD/</link><pubDate>Tue, 08 Aug 2017 13:15:19 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-08-08-%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E4%BC%98%E5%8C%96%E6%80%9D%E8%B7%AF%E8%BD%AC%E8%BD%BD/</guid><description>&lt;p&gt;看过很多写秒杀的文章，感觉还是58沈剑老师的这篇写的最好最接地气，博客第一百篇文章本想自己写一篇的，最后想想还是转载沈剑老师的这篇好了，因为看完这篇真的很受启发。&lt;/p&gt;
&lt;p&gt;原文出处微信公众号：架构师之路，微信号：road5858，链接地址：http://mp.weixin.qq.com/s/5aMN9SqaWa57rYGgtdAF_A&lt;/p&gt;
&lt;p&gt;以下是原文：&lt;/p&gt;
&lt;p&gt;本文曾在“架构师之路”上发布过，近期支援Qcon-AS大会，在微信群里分享了该话题，故对原文进行重新整理与发布。&lt;/p&gt;
&lt;p&gt;一、秒杀业务为什么难做&lt;br&gt;
1）im系统，例如qq或者微博，每个人都读自己的数据（好友列表、群列表、个人信息）；&lt;br&gt;
2）微博系统，每个人读你关注的人的数据，一个人读多个人的数据；&lt;br&gt;
3）秒杀系统，库存只有一份，所有人会在集中的时间读和写这些数据，多个人读一个数据。&lt;/p&gt;
&lt;p&gt;例如：小米手机每周二的秒杀，可能手机只有1万部，但瞬时进入的流量可能是几百几千万。&lt;br&gt;
又例如：12306抢票，票是有限的，库存一份，瞬时流量非常多，都读相同的库存。读写冲突，锁非常严重，这是秒杀业务难的地方。那我们怎么优化秒杀业务的架构呢？&lt;/p&gt;
&lt;p&gt;二、优化方向&lt;br&gt;
优化方向有两个（今天就讲这两个点）：&lt;br&gt;
（1）将请求尽量拦截在系统上游（不要让锁冲突落到数据库上去）。传统秒杀系统之所以挂，请求都压倒了后端数据层，数据读写锁冲突严重，并发高响应慢，几乎所有请求都超时，流量虽大，下单成功的有效流量甚小。以12306为例，一趟火车其实只有2000张票，200w个人来买，基本没有人能买成功，请求有效率为0。&lt;br&gt;
（2）充分利用缓存，秒杀买票，这是一个典型的读多些少的应用场景，大部分请求是车次查询，票查询，下单和支付才是写请求。一趟火车其实只有2000张票，200w个人来买，最多2000个人下单成功，其他人都是查询库存，写比例只有0.1%，读比例占99.9%，非常适合使用缓存来优化。好，后续讲讲怎么个“将请求尽量拦截在系统上游”法，以及怎么个“缓存”法，讲讲细节。&lt;/p&gt;
&lt;p&gt;三、常见秒杀架构&lt;br&gt;
常见的站点架构基本是这样的（绝对不画忽悠类的架构图）&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/08/design.png" alt="" width="118" height="176" class="alignnone size-full wp-image-386" /&gt;
&lt;p&gt;（1）浏览器端，最上层，会执行到一些JS代码&lt;br&gt;
（2）站点层，这一层会访问后端数据，拼html页面返回给浏览器&lt;br&gt;
（3）服务层，向上游屏蔽底层数据细节，提供数据访问&lt;br&gt;
（4）数据层，最终的库存是存在这里的，mysql是一个典型（当然还有会缓存）&lt;br&gt;
这个图虽然简单，但能形象的说明大流量高并发的秒杀业务架构，大家要记得这一张图。&lt;br&gt;
后面细细解析各个层级怎么优化。&lt;/p&gt;
&lt;p&gt;四、各层次优化细节&lt;br&gt;
第一层，客户端怎么优化（浏览器层，APP层）&lt;br&gt;
问大家一个问题，大家都玩过微信的摇一摇抢红包对吧，每次摇一摇，就会往后端发送请求么？回顾我们下单抢票的场景，点击了“查询”按钮之后，系统那个卡呀，进度条涨的慢呀，作为用户，我会不自觉的再去点击“查询”，对么？继续点，继续点，点点点。。。有用么？平白无故的增加了系统负载，一个用户点5次，80%的请求是这么多出来的，怎么整？&lt;br&gt;
（a）产品层面，用户点击“查询”或者“购票”后，按钮置灰，禁止用户重复提交请求；&lt;br&gt;
（b）JS层面，限制用户在x秒之内只能提交一次请求；&lt;br&gt;
APP层面，可以做类似的事情，虽然你疯狂的在摇微信，其实x秒才向后端发起一次请求。这就是所谓的“将请求尽量拦截在系统上游”，越上游越好，浏览器层，APP层就给拦住，这样就能挡住80%+的请求，这种办法只能拦住普通用户（但99%的用户是普通用户）对于群内的高端程序员是拦不住的。firebug一抓包，http长啥样都知道，js是万万拦不住程序员写for循环，调用http接口的，这部分请求怎么处理？&lt;/p&gt;
&lt;p&gt;第二层，站点层面的请求拦截&lt;br&gt;
怎么拦截？怎么防止程序员写for循环调用，有去重依据么？ip？cookie-id？…想复杂了，这类业务都需要登录，用uid即可。在站点层面，对uid进行请求计数和去重，甚至不需要统一存储计数，直接站点层内存存储（这样计数会不准，但最简单）。一个uid，5秒只准透过1个请求，这样又能拦住99%的for循环请求。&lt;br&gt;
5s只透过一个请求，其余的请求怎么办？缓存，页面缓存，同一个uid，限制访问频度，做页面缓存，x秒内到达站点层的请求，均返回同一页面。同一个item的查询，例如车次，做页面缓存，x秒内到达站点层的请求，均返回同一页面。如此限流，既能保证用户有良好的用户体验（没有返回404）又能保证系统的健壮性（利用页面缓存，把请求拦截在站点层了）。&lt;br&gt;
页面缓存不一定要保证所有站点返回一致的页面，直接放在每个站点的内存也是可以的。优点是简单，坏处是http请求落到不同的站点，返回的车票数据可能不一样，这是站点层的请求拦截与缓存优化。&lt;/p&gt;
&lt;p&gt;好，这个方式拦住了写for循环发http请求的程序员，有些高端程序员（黑客）控制了10w个肉鸡，手里有10w个uid，同时发请求（先不考虑实名制的问题，小米抢手机不需要实名制），这下怎么办，站点层按照uid限流拦不住了。&lt;/p&gt;
&lt;p&gt;第三层 服务层来拦截（反正就是不要让请求落到数据库上去）&lt;br&gt;
服务层怎么拦截？大哥，我是服务层，我清楚的知道小米只有1万部手机，我清楚的知道一列火车只有2000张车票，我透10w个请求去数据库有什么意义呢？没错，请求队列！&lt;br&gt;
对于写请求，做请求队列，每次只透有限的写请求去数据层（下订单，支付这样的写业务）&lt;br&gt;
1w部手机，只透1w个下单请求去db&lt;br&gt;
3k张火车票，只透3k个下单请求去db&lt;br&gt;
如果均成功再放下一批，如果库存不够则队列里的写请求全部返回“已售完”。&lt;/p&gt;
&lt;p&gt;对于读请求，怎么优化？cache抗，不管是memcached还是redis，单机抗个每秒10w应该都是没什么问题的。如此限流，只有非常少的写请求，和非常少的读缓存mis的请求会透到数据层去，又有99.9%的请求被拦住了。&lt;/p&gt;
&lt;p&gt;当然，还有业务规则上的一些优化。回想12306所做的，分时分段售票，原来统一10点卖票，现在8点，8点半，9点，…每隔半个小时放出一批：将流量摊匀。&lt;br&gt;
其次，数据粒度的优化：你去购票，对于余票查询这个业务，票剩了58张，还是26张，你真的关注么，其实我们只关心有票和无票？流量大的时候，做一个粗粒度的“有票”“无票”缓存即可。&lt;br&gt;
第三，一些业务逻辑的异步：例如下单业务与 支付业务的分离。这些优化都是结合 业务 来的，我之前分享过一个观点“一切脱离业务的架构设计都是耍流氓”架构的优化也要针对业务。&lt;/p&gt;
&lt;p&gt;好了，最后是数据库层&lt;br&gt;
浏览器拦截了80%，站点层拦截了99.9%并做了页面缓存，服务层又做了写请求队列与数据缓存，每次透到数据库层的请求都是可控的。db基本就没什么压力了，闲庭信步，单机也能扛得住，还是那句话，库存是有限的，小米的产能有限，透这么多请求来数据库没有意义。&lt;br&gt;
全部透到数据库，100w个下单，0个成功，请求有效率0%。透3k个到数据，全部成功，请求有效率100%。&lt;/p&gt;
&lt;p&gt;五、总结&lt;br&gt;
上文应该描述的非常清楚了，没什么总结了，对于秒杀系统，再次重复下我个人经验的两个架构优化思路：&lt;br&gt;
（1）尽量将请求拦截在系统上游（越上游越好）；&lt;br&gt;
（2）读多写少的常用多使用缓存（缓存抗读压力）；&lt;br&gt;
浏览器和APP：做限速&lt;br&gt;
站点层：按照uid做限速，做页面缓存&lt;br&gt;
服务层：按照业务做写请求队列控制流量，做数据缓存&lt;br&gt;
数据层：闲庭信步&lt;br&gt;
并且：结合业务做优化&lt;/p&gt;
&lt;p&gt;六、Q&amp;amp;A&lt;br&gt;
问题1、按你的架构，其实压力最大的反而是站点层，假设真实有效的请求数有1000万，不太可能限制请求连接数吧，那么这部分的压力怎么处理？&lt;br&gt;
答：每秒钟的并发可能没有1kw，假设有1kw，解决方案2个：&lt;br&gt;
（1）站点层是可以通过加机器扩容的，最不济1k台机器来呗。&lt;br&gt;
（2）如果机器不够，抛弃请求，抛弃50%（50%直接返回稍后再试），原则是要保护系统，不能让所有用户都失败。&lt;/p&gt;
&lt;p&gt;问题2、“控制了10w个肉鸡，手里有10w个uid，同时发请求” 这个问题怎么解决哈？&lt;br&gt;
答：上面说了，服务层写请求队列控制&lt;/p&gt;</description></item><item><title>巧用CAS解决数据一致性问题[转载]</title><link>https://bridgeli.cn/posts/2017-07-22-%E5%B7%A7%E7%94%A8cas%E8%A7%A3%E5%86%B3%E6%95%B0%E6%8D%AE%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98%E8%BD%AC%E8%BD%BD/</link><pubDate>Sat, 22 Jul 2017 05:07:07 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-07-22-%E5%B7%A7%E7%94%A8cas%E8%A7%A3%E5%86%B3%E6%95%B0%E6%8D%AE%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98%E8%BD%AC%E8%BD%BD/</guid><description>&lt;p&gt;这周不太忙的时候看了58到家沈剑老师的一系列的文章，感觉沈剑老师的文章做到了深入浅出，浅显易懂，看完收获很大，有些文章完美的解决了我一直一来的疑惑，所以转载到自己博客，希望对大家也有所帮助。&lt;/p&gt;
&lt;p&gt;原文出处微信公众号：架构师之路，微信号：road5858，链接地址：http://mp.weixin.qq.com/s/_XlzbmBSj_i-S2PkE5tI_w&lt;/p&gt;
&lt;p&gt;以下是原文：&lt;/p&gt;
&lt;p&gt;缘起：在高并发的分布式环境下，对于数据的查询与修改容易引发一致性问题，本文将分享一种非常简单但有效的优化方法。&lt;/p&gt;
&lt;p&gt;一、业务场景&lt;/p&gt;
&lt;p&gt;业务场景为，购买商品的过程要对余额进行查询与修改，大致的业务流程如下：&lt;br&gt;
（1）从数据库查询用户现有余额 SELECT money FROM t_yue WHERE uid=$uid，不妨设查询出来的$old_money=100元&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/07/step1.png" alt="step1" width="229" height="149" class="alignnone size-full wp-image-373" /&gt;
&lt;p&gt;（2）业务层实施业务逻辑，比如购买一个80元的商品，并且打九折&lt;br&gt;
if($old_money&amp;gt; 80*0.9) $new_money=$old_money-80*0.9=28&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/07/step2.png" alt="step2" width="243" height="69" class="alignnone size-full wp-image-374" /&gt;
&lt;p&gt;（3）将数据库中的余额进行修改 UPDAtE t_yue SET money=$new_money WHERE uid=$uid&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/07/step3.png" alt="step3" width="243" height="143" class="alignnone size-full wp-image-375" /&gt;
&lt;p&gt;在并发量低的情况下，这个流程没有任何问题，原有金额100元，购买了80元的九折商品（72元），剩余28元。&lt;/p&gt;
&lt;p&gt;二、潜在的问题&lt;/p&gt;
&lt;p&gt;在分布式环境中，如果并发量很大，这种“查询+修改”的业务很容易出现数据不一致。极限情况下，可能出现这样的异常流程：&lt;br&gt;
（1）业务1和业务2同时查询余额，是100元&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/07/step4.png" alt="step4" width="249" height="181" class="alignnone size-full wp-image-376" /&gt;
&lt;p&gt;（2）业务1和业务2进行逻辑计算，算出各自业务的余额，假设业务1算出的余额是28元，业务2算出的余额是38元&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/07/step5.png" alt="step5" width="243" height="89" class="alignnone size-full wp-image-377" /&gt;
&lt;p&gt;（3）业务1对数据库中的余额先进行修改，设置成28元。&lt;br&gt;
业务2对数据库中的余额后进行修改，设置成38元。&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/07/step6-300x120.png" alt="step6" width="300" height="120" class="alignnone size-medium wp-image-378" /&gt;
&lt;p&gt;此时异常出现了，原有金额100元，业务1扣除了72元，业务2扣除了62元，最后剩余38元。&lt;/p&gt;
&lt;p&gt;三、问题原因&lt;/p&gt;
&lt;p&gt;高并发环境下，对同一个数据的并发读（两边都读出余额是100）与并发写（一个写回28，一个写回38）导致的数据一致性问题。&lt;/p&gt;
&lt;p&gt;四、原因分析&lt;/p&gt;
&lt;p&gt;业务1的写回：原有金额100，这是一个初始状态，写回金额28，理论上只有在原有金额为100的时候才允许写回成功，这一步没问题。&lt;br&gt;
业务2的写回：的原有金额100，这是一个初始状态，写回金额38，理论上只有在原有金额为100的时候才允许写回成功，可实际上，这个时候数据库中的金额已经变为28了，这一步的写操作不应该成功。&lt;/p&gt;
&lt;p&gt;五、简易解决方案&lt;/p&gt;
&lt;p&gt;在set写回的时候，加上初始状态的条件compare，只有初始状态不变时，才允许set写回成功，这正是大家常说的“Compare And Set”（CAS），是一种常见的降低读写锁冲突，保证数据一致性的方法。&lt;/p&gt;
&lt;p&gt;六、业务的升级&lt;/p&gt;
&lt;p&gt;业务线使用CAS解决高并发时数据一致性问题，只需要在进行set操作时，compare一下初始值，如果初始值变换，不允许set成功。&lt;br&gt;
对于上文中的业务场景，只需要将“UPDAtEt_yue SET money=$new_money WHERE uid=$uid”升级为&lt;br&gt;
“UPDAtE t_yue SETmoney=$new_money WHERE uid=$uid AND money=$old_money”即可。&lt;br&gt;
并发操作发生时：&lt;br&gt;
业务1执行 =&amp;gt; UPDAtE t_yue SET money=28 WHERE uid=$uid AND money=100&lt;br&gt;
业务2执行 =&amp;gt; UPDAtE t_yue SET money=38 WHERE uid=$uid AND money=100&lt;br&gt;
【这两个操作同时进行时，只能有一个执行成功】。&lt;/p&gt;</description></item><item><title>程序猿的自我修养之开发规范</title><link>https://bridgeli.cn/posts/2017-07-09-%E7%A8%8B%E5%BA%8F%E7%8C%BF%E7%9A%84%E8%87%AA%E6%88%91%E4%BF%AE%E5%85%BB%E4%B9%8B%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/</link><pubDate>Sun, 09 Jul 2017 12:14:44 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-07-09-%E7%A8%8B%E5%BA%8F%E7%8C%BF%E7%9A%84%E8%87%AA%E6%88%91%E4%BF%AE%E5%85%BB%E4%B9%8B%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/</guid><description>&lt;p&gt;有感于公司代码比较乱，完全没有规范，而我则受益于实习的时候的老大zeak的严格要求，看到这种情况表示有点难以接受，所以和老大讨论后，基于阿里的规范经过删减写了这么一个标准，今天发出来，不仅供自己时时对照，也供大家参考，最后感谢一下阿里出这份标准。&lt;/p&gt;
&lt;p&gt;一、 编程规约&lt;/p&gt;
&lt;p&gt;（一）命令风格&lt;/p&gt;
&lt;p&gt;整体要求，见名知义，英文为主，类名一般是名称或者形容词，方法名为动词&lt;/p&gt;
&lt;p&gt;具体要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码中的命名均不能以下划线或美元符号开始，也不能以下划线或美元符号结束。除非特殊情况最好不要用美元符号（大家猜猜为啥？）&lt;/li&gt;
&lt;li&gt;类名使用 UpperCamelCase 风格，必须遵从驼峰形式，但以下情形例外:BO / DTO / VO&lt;/li&gt;
&lt;li&gt;方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格，必须遵从 驼峰形式&lt;/li&gt;
&lt;li&gt;常量命名全部大写，单词间用下划线隔开，力求语义表达完整清楚，不要嫌名字长&lt;/li&gt;
&lt;li&gt;抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类 命名以它要测试的类的名称开始，以 Test 结尾，枚举类名建议带上 Enum 后缀，枚举成员名称需要全大写，单词间用下划线隔开&lt;/li&gt;
&lt;li&gt;中括号是数组类型的一部分，数组定义如下:String[] args&lt;/li&gt;
&lt;li&gt;POJO 类中布尔类型的变量，都不要加 is&lt;/li&gt;
&lt;li&gt;包名统一使用小写，点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数形式，但是类名如果有复数含义，类名可以使用复数形式&lt;/li&gt;
&lt;li&gt;杜绝完全不规范的缩写，避免望文不知义&lt;/li&gt;
&lt;li&gt;如果使用到了设计模式，建议在类名中体现出具体模式&lt;/li&gt;
&lt;li&gt;接口类中的方法和属性不要加任何修饰符号(public 也不要加)，并加上有效的 Javadoc 注释。尽量不要在接口里定义变量，如果一定要定义变量，肯定是 与接口方法相关，并且是整个应用的基础常量&lt;/li&gt;
&lt;li&gt;各层命名规约:&lt;/li&gt;
&lt;/ol&gt;
&lt;ol&gt;
&lt;li&gt;获取单个对象的方法用get或者find做前缀&lt;/li&gt;
&lt;li&gt;获取多个对象的方法用list或者query做前缀。&lt;/li&gt;
&lt;li&gt;获取统计值的方法用count做前缀。&lt;/li&gt;
&lt;li&gt;插入的方法用save(推荐)或insert做前缀。&lt;/li&gt;
&lt;li&gt;删除的方法用remove(推荐)或delete做前缀。&lt;/li&gt;
&lt;li&gt;修改的方法用update做前缀。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(二) 常量定义&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不允许任何魔法值直接出现在代码中&lt;/li&gt;
&lt;li&gt;long 或者 Long 初始赋值时，必须使用大写的 L，不能是小写的 l&lt;/li&gt;
&lt;li&gt;如果变量值仅在一个范围内变化，且带有名称之外的延伸属性，定义为枚举类&lt;/li&gt;
&lt;li&gt;常量类大写，各个单词之前下划线分开&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(三) 代码格式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;大括号的使用约定:&lt;/li&gt;
&lt;/ol&gt;
&lt;ol&gt;
&lt;li&gt;左大括号前不换行。&lt;/li&gt;
&lt;li&gt;左大括号后换行。&lt;/li&gt;
&lt;li&gt;右大括号前换行。&lt;/li&gt;
&lt;li&gt;右大括号后还有else等代码则不换行;表示终止的右大括号后必须换行。&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="2"&gt;
&lt;li&gt;左小括号和字符之间不出现空格;同样，右小括号和字符之间也不出现空格&lt;/li&gt;
&lt;li&gt;if/for/while/switch/do 等保留字与括号之间都必须加空格&lt;/li&gt;
&lt;li&gt;任何二目、三目运算符的左右两边都需要加一个空格&lt;/li&gt;
&lt;li&gt;缩进采用 4 个空格，禁止使用 tab 字符（根据今年stackoverflow公布数据，空格党的平均工资略高于tab党，大家猜猜为啥？）&lt;/li&gt;
&lt;li&gt;单行字符数限制不超过 120 个，超出需要换行，换行时遵循如下原则:&lt;/li&gt;
&lt;/ol&gt;
&lt;ol&gt;
&lt;li&gt;第二行相对第一行缩进 4 个空格，从第三行开始，不再继续缩进。&lt;/li&gt;
&lt;li&gt;运算符与下文一起换行。&lt;/li&gt;
&lt;li&gt;方法调用的点符号与下文一起换行。&lt;/li&gt;
&lt;li&gt;在多个参数超长，在逗号后换行。&lt;/li&gt;
&lt;li&gt;在括号前不要换行。&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="7"&gt;
&lt;li&gt;方法参数在定义和传入时，多个参数逗号后边必须加空格&lt;/li&gt;
&lt;li&gt;没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐&lt;/li&gt;
&lt;li&gt;方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行，没有必要插入多个空行进行隔开&lt;/li&gt;
&lt;li&gt;IDE的text file encoding设置为UTF-8; IDE中文件的换行符使用Unix格式， 不要使用 windows 格式。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于代码格式的设置，eclipse用户可以参考我的GitHub：&lt;a href="https://github.com/bridgeli/practical-doc/tree/master/eclipse_setting" title="eclipse设置" target="_blank"&gt;eclipse设置&lt;/a&gt;，按照这个设置，当你保存的时候eclipse会自动帮你formatter你改动过的代码。&lt;/p&gt;</description></item><item><title>ThreadLocal类之简单应用示例</title><link>https://bridgeli.cn/posts/2017-06-18-threadlocal%E7%B1%BB%E4%B9%8B%E7%AE%80%E5%8D%95%E5%BA%94%E7%94%A8%E7%A4%BA%E4%BE%8B/</link><pubDate>Sun, 18 Jun 2017 12:50:29 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-06-18-threadlocal%E7%B1%BB%E4%B9%8B%E7%AE%80%E5%8D%95%E5%BA%94%E7%94%A8%E7%A4%BA%E4%BE%8B/</guid><description>&lt;p&gt;在日常开发的系统中，日期处理是非常非常用的一个功能，处理的日期的时候就需要用到SimpleDateFormat对象，但是我们都知道SimpleDateFormat本身不是线程安全的（如果不知道的请看源码），所以就需要频繁创建SimpleDateFormat这个对象。但是我们知道创建这个对象本身不仅是很费时的，而且创建的这些对象存活期很短，导致内存中大量这样的对象需要被GC，所以我们自然而然的想到使用ThreadLocal来给每个线程缓存一个SimpleDateFormat实例，提高性能。下面是一个具体的实现的小例子，其实不仅针对SimpleDateFormat对象，对于数据库连接等等都可以这么使用。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
public class DateFormatFactory {
private static final Map&amp;lt;DatePatternEnum, ThreadLocal&amp;lt;DateFormat&amp;gt;&amp;gt; pattern2ThreadLocal;
static {
DatePatternEnum[] patterns = DatePatternEnum.values();
int len = patterns.length;
pattern2ThreadLocal = new HashMap&amp;lt;DatePatternEnum, ThreadLocal&amp;lt;DateFormat&amp;gt;&amp;gt;(len);
for (int i = 0; i &amp;lt; len; i++) {
DatePatternEnum datePatternEnum = patterns[i];
final String pattern = datePatternEnum.pattern;
pattern2ThreadLocal.put(datePatternEnum, new ThreadLocal&amp;lt;DateFormat&amp;gt;() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(pattern);
}
});
}
}
// 获取DateFormat
public static DateFormat getDateFormat(DatePatternEnum patternEnum) {
ThreadLocal&amp;lt;DateFormat&amp;gt; threadDateFormat = pattern2ThreadLocal.get(patternEnum);
// 不需要判断threadDateFormat是否为空
return threadDateFormat.get();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对应的时间枚举类（如果还有其他格式的时间需要处理，可以直接在这个类里面添加即可）：&lt;/p&gt;</description></item><item><title>ThreadLocal类之简单理解</title><link>https://bridgeli.cn/posts/2017-06-11-threadlocal%E7%B1%BB%E4%B9%8B%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3/</link><pubDate>Sun, 11 Jun 2017 12:21:40 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-06-11-threadlocal%E7%B1%BB%E4%B9%8B%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3/</guid><description>&lt;p&gt;当年实习的时候，当时公司一个相当有经验的工程师zeak带我们，从他那第一次听说了ThreadLocal类，但由于自己基础薄弱，没有理解到底怎么回事，工作中也没有用过，就一直没有太放在心上，刚好这一段时间不太忙，仔细玩了一下，欢迎高手批评。&lt;br&gt;
ThreadLocal，线程本地变量。他为变量在每个线程中都创建了一个副本，那么每个线程可以访问自己内部的副本变量。简单理解就是，对于非线程安全的变量在线程内部共享不用每次都new，是一种空间换时间的做法。ThreadLocal类提供的几个方法：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看名字就知道这些方法是干嘛的了，下面我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。首先是get方法的实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;先取得当前线程，然后通过getMap(t)方法获取到一个map，map的类型为ThreadLocalMap。然后接着下面获取到&amp;lt;key,value&amp;gt;键值对，注意这里获取键值对传进去的是 this，而不是当前线程t。如果获取成功，则返回value值。如果map为空，则调用setInitialValue方法返回value。那么getMap方法中又做了什么呢？&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;原来是返回当前线程t中的一个成员变量threadLocals，而threadLocals则是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ThreadLocal.ThreadLocalMap threadLocals = null;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;那就看看ThreadLocalMap的实现了：&lt;/p&gt;</description></item><item><title>Java集合类ArrayList删除特定元素</title><link>https://bridgeli.cn/posts/2017-05-28-java%E9%9B%86%E5%90%88%E7%B1%BBarraylist%E5%88%A0%E9%99%A4%E7%89%B9%E5%AE%9A%E5%85%83%E7%B4%A0/</link><pubDate>Sun, 28 May 2017 04:25:52 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-05-28-java%E9%9B%86%E5%90%88%E7%B1%BBarraylist%E5%88%A0%E9%99%A4%E7%89%B9%E5%AE%9A%E5%85%83%E7%B4%A0/</guid><description>&lt;p&gt;前一段时间入职新公司，熟悉公司系统原有代码的时候，发现公司代码那个烂啊，系统能正常跑，都不能用侥幸来形容，就是创造了一个奇迹。因为里面不仅没有coding style，而且竟然有很明显的常识性错误。其中当我一眼指出最明显的早就应该出过问题的一个地方，项目组几乎所有成员，是的，几乎全部成员，都说这个还真不知道，涨知识了，那就是：Java集合类ArrayList删除特定元素。发现原来不是所有人都知道怎么做，这难道不是最基础的吗？唉，真不知道这些系统是怎么跑起来的。我们首先看两种错误的写法，第一种：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Test
public void testRemove1() {
List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;() {
private static final long serialVersionUID = 1L;
{
add(&amp;#34;cn&amp;#34;);
add(&amp;#34;bridgeli&amp;#34;);
add(&amp;#34;blog&amp;#34;);
}
};
for (int i = 0, len = list.size(); i &amp;lt; len; i++) {
String str = list.get(i);
if (&amp;#34;cn&amp;#34;.equals(str)) {
list.remove(str);
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个写法如果你不知道错在哪，那你得真的好好补基础了。由于这个错误比较明显，所以有人搞了下面这种写法，也是我们公司的同事犯的一个错误：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Test
public void testRemove2() {
List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;() {
private static final long serialVersionUID = 1L;
{
add(&amp;#34;cn&amp;#34;);
add(&amp;#34;bridgeli&amp;#34;);
add(&amp;#34;blog&amp;#34;);
}
};
for (String str : list) {
if (&amp;#34;cn&amp;#34;.equals(str)) {
list.remove(str);
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;跑一下这个例子看看，把cn换成bridgeli试试，出乎不出乎你的意料？下面我们就来简单探究一下原因foreach的原理，其实特别简单：&lt;/p&gt;</description></item><item><title>关于synchronized用法的简单理解</title><link>https://bridgeli.cn/posts/2017-05-14-%E5%85%B3%E4%BA%8Esynchronized%E7%94%A8%E6%B3%95%E7%9A%84%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3/</link><pubDate>Sun, 14 May 2017 09:20:14 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-05-14-%E5%85%B3%E4%BA%8Esynchronized%E7%94%A8%E6%B3%95%E7%9A%84%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3/</guid><description>&lt;p&gt;synchronized 关键字既可以用于声明方法，也可以用于声明代码块，他们之间有什么区别呢？下面让我们逐一测试一下。&lt;br&gt;
先看以第一个例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package demo;
public class SynchronizedDemo1 {
public synchronized static void foo1() {
}
public synchronized static void foo2() {
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在这个例子中，foo1 和 foo2 是类的两个静态方法。在不同的线程中，这两个方法的调用时互斥的，不仅是他们之间，任何两个不同的线程的调用也互斥。下面看第二个例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package demo;
public class SynchronizedDemo2 {
public synchronized void foo3() {
}
public synchronized void foo4() {
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在这个例子中，foo3 和 foo4 是类的两个成员方法，在多线程环境中，调用同一个对象的 foo3 或者 foo4 是互斥的，与上一个例子的差别在于，这是针对同一个对象的多线程方法调用互斥。下面再看最后一个例子：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package demo;
public class SynchronizedDemo3 {
public void foo5() {
synchronized (this) {
}
}
public void foo6() {
synchronized (SynchronizedDemo3.class) {
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在这个例子中，synchronized 用来修饰代码块，需要注意的是：synchronized 后面会有一个参数，其实这个就是用于同步的锁所属的对象。在这个例子中 synchronized (this) 与 SynchronizedDemo2 中加 synchronized 的成员方法是互斥的，而 synchronized (SynchronizedDemo3.class) 与 SynchronizedDemo1 中加 synchronized 的静态方法是互斥的。synchronized 用于修饰代码块会更加灵活，因为除了前面的这个例子外，synchronized 后面的参数可以是任意对象。&lt;/p&gt;</description></item><item><title>Java GC之常见监控可视化工具总结（下）</title><link>https://bridgeli.cn/posts/2017-04-04-355/</link><pubDate>Tue, 04 Apr 2017 11:58:54 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-04-04-355/</guid><description>&lt;p&gt;&lt;a href="https://www.bridgeli.cn/archives/349" title="Java GC之常见监控与分析命令总结（上）"&gt;上一篇文章&lt;/a&gt;总结一下监控和分析的常见命令，那些是基础，但是有些同学看到命令行就害怕，所以这篇文件总计一下两个常用的可视化工具。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JConsole&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;JConsole工具在JDK/bin目录下，启动JConsole后，将自动搜索本机运行的jvm进程，不需要jps命令来查询指定。双击其中一个jvm进程即可开始监控，也可使用“远程进程”来连接远程服务器&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/JConsole_start.jpg" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;进入JConsole主界面，有“概述”、“内存”、“线程”、“类”、“VM摘要”和”Mbean”六个页签：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/JConsole_process.jpg" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;内存页签相当于jstat命令，用于监视收集器管理的虚拟机内存(Java堆和永久代)变化趋势，还可在详细信息栏观察全部GC执行的时间及次数&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/JConsole_mem.jpg" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;线程页签：线程长时间停顿的主要原因有：等待外部资源（数据库连接、网络资源、设备资源等）、死循环、锁等待（活锁和死锁）&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/JConsole_thread.jpg" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;最后一个常用页签，VM页签，可清楚的了解显示指定的JVM参数及堆信息&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/JConsole_jntro.jpg" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;ol start="2"&gt;
&lt;li&gt;VisualVM：多合一故障处理工具&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;VisualVM是一个集成多个JDK命令行工具的可视化工具。VisualVM基于NetBeans平台开发，它具备了插件扩展功能的特性，通过插件的扩展，可用于显示虚拟机进程及进程的配置和环境信息(jps，jinfo)，监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等。VisualVM在JDK/bin目录下&lt;/p&gt;
&lt;p&gt;①. 安装插件： 工具- 插件&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/Visual_GC.png" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;②. 监控垃圾回收&lt;/p&gt;
&lt;p&gt;在左侧的“Application”测看下，有个“Local”节点，所有本地正在运行的Java应用都将罗列在这里。Java VisualVM是一个Java应用。所以，它将自己也列在这里。为了方便学习，我们将监控Java VisualVM自身的垃圾回收过程。双击“Local”节点下的VisualVM图标，现在，应用监视窗口在右侧打开。我们关注的是“Visual GC”，点击它&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/GC.png" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;再配合其他的标签页，例如“Threads”以及线程转储你，我们就可以深入详细地了解这方面的内容。在“监视”标签页，我们可以监控整个堆内存的使用情况，这些都不贴图了，大家可以随便玩。&lt;/p&gt;
&lt;p&gt;③. 在VisualVM中生成dump文件&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="http://om2v5fbz2.bkt.clouddn.com/dump.png" alt="" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;p&gt;参考资料：周志明《深入理解Java虚拟机》第二版第四章&lt;/p&gt;</description></item><item><title>Java GC之常见监控与分析命令总结（上）</title><link>https://bridgeli.cn/posts/2017-03-19-java-gc%E4%B9%8B%E5%B8%B8%E8%A7%81%E7%9B%91%E6%8E%A7%E4%B8%8E%E5%88%86%E6%9E%90%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93%E4%B8%8A/</link><pubDate>Sun, 19 Mar 2017 13:36:26 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-03-19-java-gc%E4%B9%8B%E5%B8%B8%E8%A7%81%E7%9B%91%E6%8E%A7%E4%B8%8E%E5%88%86%E6%9E%90%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93%E4%B8%8A/</guid><description>&lt;p&gt;&lt;a href="https://www.bridgeli.cn/archives/347" title="Java GC之常见垃圾收集器参数总结"&gt;上一篇文章&lt;/a&gt;简单写了几种常见的垃圾收集器的参数设置，设置参数的时候离不开对对系统进行监控和分析，所以总结一下监控和分析的常见命令。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;jps：JVM Process Status Tool，显示指定系统内所有的HotSpot虚拟机进程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;命令格式：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jps \[options\] \[hostid\]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;hostid为RMI注册表中注册的主机名，其他常用参数如下：&lt;br&gt;
-q：只输出LVMID，省略主类的名称&lt;br&gt;
-m：输出虚拟机进程启动的时候传递给主类main()方法的参数&lt;br&gt;
-l：输出主类的全名，如果进程执行的是jar包，输出jar路径&lt;br&gt;
-v：输出虚拟机进程启动时JVM参数&lt;/p&gt;
&lt;p&gt;命令执行样例：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jps -l
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;jstat：JVM Statistics Monitoring Tool，用于收集Hotspot虚拟机各方面的运行数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;命令格式：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jstat \[option vmid [interval[s|ms\] \[count\]]]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于命令格式中的VMID和LVMID，如过是本地虚拟机进程，VMID和LVMID是一致的，如果是远程虚拟机，那VMID的格式应当是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
\[protocol:\] \[//\] lvmid[@hostname[:port]/servername]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;interval和count代表查询的间隔和次数，如果省略这两个参数，说明只查一次，其他常用参数：&lt;/p&gt;
&lt;p&gt;-class：监视装载类、卸载类、总空间以及类装载所耗费的时间&lt;br&gt;
-gc：监视java堆状况，包括eden区、两个survivor区、老年代、永久代等的容量、已用空间、GC时间合计信息&lt;br&gt;
-gccapacity：监视内容与-gc基本相同，但输出主要关注java堆各个区域使用到最大、最小空间&lt;br&gt;
-gcutil：监视内容与-gc基本相同，但输出主要关注已使用控件占总空间的百分比&lt;br&gt;
-gccause：与-gcutil功能一样，但是会额外输出导致上一次gc产生的原因&lt;br&gt;
-gcnew：监视新生代GC情况&lt;br&gt;
-gcnewcapacity：监视内容与-gcnew基本相同，输出主要关注使用到的最大、最小空间&lt;br&gt;
-gcold：监视老年代GC情况&lt;br&gt;
-gcoldcapacity：监视内容与-gcold基本相同，输出主要关注使用到的最大、最小空间&lt;br&gt;
-gcpermcapacity：输出永久代使用到的最大、最小空间&lt;br&gt;
-compiler：输出JIT编译过的方法、耗时等信息&lt;br&gt;
-printcompilation：输出已经被JIT编译过的方法&lt;/p&gt;
&lt;p&gt;命令执行样例：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jstat -gcutil 2764 1000
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;jinfo：Configuration Info for Java，显示虚拟机的配置信息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表，但如果想知道未被显式指定的参数的系统默认值，除了去找资料以外，就得使用jinfo的-flag选项，命令格式：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jinfo [option] pid
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行样例：查询CMSInitiatingOccupancyFraction参数值&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jinfo -flag CMSInitiatingOccupancyFraction 2764
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;jmap：Memory Map for Java，生成虚拟机的内存转储快照（heapdump文件）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;命令格式：&lt;/p&gt;</description></item><item><title>Java GC之常见垃圾收集器参数总结</title><link>https://bridgeli.cn/posts/2017-03-05-java-gc%E4%B9%8B%E5%B8%B8%E8%A7%81%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E5%8F%82%E6%95%B0%E6%80%BB%E7%BB%93/</link><pubDate>Sun, 05 Mar 2017 15:32:07 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-03-05-java-gc%E4%B9%8B%E5%B8%B8%E8%A7%81%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E5%8F%82%E6%95%B0%E6%80%BB%E7%BB%93/</guid><description>&lt;p&gt;&lt;a href="https://www.bridgeli.cn/archives/347" title="Java GC之常见垃圾收集器参数总结"&gt;上一篇文章&lt;/a&gt;简单写了几种常见的垃圾收集器，俗话说，好记性不如烂笔头，今天总结一下这些垃圾收集器的参数总结，供自己和需要的读者将来查阅&lt;/p&gt;
&lt;p&gt;-XX:+UseSerialGC : Jvm运行在Client模式下的默认值，打开此开关后，使用Serial + Serial Old的收集器组合进行内存回收&lt;/p&gt;
&lt;p&gt;-XX:+UseParNewGC : 打开此开关后，使用ParNew + Serial Old的收集器进行垃圾回收&lt;/p&gt;
&lt;p&gt;-XX:+UseConcMarkSweepGC : 使用ParNew + CMS +  Serial Old的收集器组合进行内存回收，Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用&lt;/p&gt;
&lt;p&gt;-XX:+UseParallelGC : Jvm运行在Server模式下的默认值，打开此开关后，使用Parallel Scavenge +  Serial Old的收集器组合进行回收&lt;/p&gt;
&lt;p&gt;-XX:+UseParallelOldGC : 使用Parallel Scavenge +  Parallel Old的收集器组合进行回收&lt;/p&gt;
&lt;p&gt;-XX:SurvivorRatio : 新生代中Eden区域与Survivor区域的容量比值，默认为8，代表Eden:Subrvivor = 8:1&lt;/p&gt;
&lt;p&gt;-XX:PretenureSizeThreshold : 直接晋升到老年代对象的大小，设置这个参数后，大于这个参数的对象将直接在老年代分配&lt;/p&gt;
&lt;p&gt;-XX:MaxTenuringThreshold : 晋升到老年代的对象年龄，每次Minor GC之后，年龄就加1，当超过这个参数的值时进入老年代&lt;/p&gt;
&lt;p&gt;-XX:UseAdaptiveSizePolicy : 动态调整java堆中各个区域的大小以及进入老年代的年龄&lt;/p&gt;
&lt;p&gt;-XX:+HandlePromotionFailure : 是否允许新生代收集担保，进行一次minor gc后, 另一块Survivor空间不足时，将直接会在老年代中保留&lt;/p&gt;
&lt;p&gt;-XX:ParallelGCThreads : 设置并行GC进行内存回收的线程数&lt;/p&gt;
&lt;p&gt;-XX:GCTimeRatio : GC时间占总时间的比列，默认值为99，即允许1%的GC时间，仅在使用Parallel Scavenge 收集器时有效&lt;/p&gt;
&lt;p&gt;-XX:MaxGCPauseMillis : 设置GC的最大停顿时间，在Parallel Scavenge 收集器下有效&lt;/p&gt;</description></item><item><title>Java GC之常见垃圾收集器</title><link>https://bridgeli.cn/posts/2017-02-26-java-gc%E4%B9%8B%E5%B8%B8%E8%A7%81%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/</link><pubDate>Sun, 26 Feb 2017 14:39:40 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-02-26-java-gc%E4%B9%8B%E5%B8%B8%E8%A7%81%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/</guid><description>&lt;p&gt;&lt;a href="https://www.bridgeli.cn/archives/335" title="Java GC之垃圾回收算法"&gt;上一篇文章&lt;/a&gt;简单写了JVM的常见垃圾回收算法，今天就让我们看看根据这些算法有哪些常见的垃圾收集器，他们有什么特点，然后根据自己的应用特点和要求组合出各个年代所使用的收集器。&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/gccollect-300x205.jpg" alt="gccollect" width="300" height="205" class="alignnone size-medium wp-image-337" /&gt;
&lt;p&gt;上图展示了JDK1.7Update14之后的HotSpot虚拟机的7种作用于不同分代的收集器，如果两个收集器之间存在连线，就说明它们可以搭配使用。虚拟机所处的区域，则表示它是属于新生代收集器还是老年代收集器&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Serial收集器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Serial收集器是最基本、发展历史最悠久的收集器，在JDK 1.3.1之前是虚拟机新生代收集的唯一选择。见名知意它是一个单线程的收集器，“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作，更重要的是在它进行垃圾收集时，必须暂停其他所有的工作线程，直到它收集结束，也就是传说中的Stop The World，简称STW。那么它是不是已经被淘汰的一个垃圾收集器呢，事实上并不是，由于与其他收集器的单线程比简单而高效，对于限定单个CPU的环境来说，Serial收集器由于没有线程交互的开销，专心做垃圾收集自然可以获得最高的单线程收集效率。所以Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。它的运行示意图如下：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/serial-serialold-300x73.jpg" alt="serial-serialold" width="300" height="73" class="alignnone size-medium wp-image-340" /&gt;
&lt;ol start="2"&gt;
&lt;li&gt;ParNew收集器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ParNew收集器其实就是Serial收集器的多线程版本，是一个并行垃圾收集器，并行的含义是：多条垃圾收集线程并行工作，但此时用户线程仍然处于等待状态。除了使用多条线程进行垃圾收集之外，其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样，在实现上，这两种收集器也共用了相当多的代码。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器，还有一个和性能无关的但是很重要的原因：除了Serial收集器外，目前只有它能与CMS收集器配合工作。和Serial收集器相比，ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果，甚至由于存在线程交互的开销，该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而，随着可以使用的CPU的数量的增加，它对于GC时系统资源的有效利用还是很有好处的。其运行示意图如下：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/parnew-serialord-300x76.jpg" alt="parnew-serialord" width="300" height="76" class="alignnone size-medium wp-image-339" /&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Parallel Scavenge收集器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Parallel Scavenge收集器是一个新生代收集器，它也是使用复制算法的收集器，又是并行的多线程收集器。那么他有什么应用场景呢？事实上Parallel Scavenge收集器的特点是它的关注点与其他收集器不同，其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间，而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量（Throughput），所以由于与吞吐量关系密切，Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。所以它的应用场景体现在：停顿时间越短就越适合需要与用户交互的程序，良好的响应速度能提升用户体验，而高吞吐量则可以高效率地利用CPU时间，尽快完成程序的运算任务，主要适合在后台运算而不需要太多交互的任务。除此之外和ParNew收集器相比，它具有自适应调节策略。Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后，就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了，虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量，这种调节方式称为GC自适应的调节策略（GC Ergonomics），其运行示意图如下：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/parallelscavenge-parallelold-300x76.jpg" alt="parallelscavenge-parallelold" width="300" height="76" class="alignnone size-medium wp-image-338" /&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Serial Old收集器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Serial Old是Serial收集器的老年代版本，它同样是一个单线程收集器，使用标记－整理算法。它的主要应用场景：1. 给Client模式下的虚拟机使用（还记得Client模式下新生代的默认垃圾收集器是啥吗？）；2. 在Server模式下，那么它主要还有两大用途：一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用，另一种用途就是作为CMS收集器的后备预案，在并发收集发生Concurrent Mode Failure时使用。运行示意图见Serial垃圾收集器&lt;/p&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Parallel Old收集器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Parallel Old是Parallel Scavenge收集器的老年代版本，使用多线程和“标记－整理”算法。在注重吞吐量以及CPU资源敏感的场合，都可以优先考虑Parallel Scavenge加Parallel Old收集器。这个收集器是在JDK 1.6中才开始提供的，在此之前，新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是，如果新生代选择了Parallel Scavenge收集器，老年代除了Serial Old收集器外别无选择（Parallel Scavenge收集器无法与CMS收集器配合工作）。由于老年代Serial Old收集器在服务端应用性能上的“拖累”，使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果，由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力，在老年代很大而且硬件比较高级的环境中，这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后，“吞吐量优先”收集器终于有了比较名副其实的应用组合。运行示意图见Parallel Scavenge垃圾收集器&lt;/p&gt;
&lt;ol start="6"&gt;
&lt;li&gt;CMS收集器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在JDK 1.5时期，HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器，这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器，它第一次实现了让垃圾收集线程与用户线程同时工作。并发的含义就是：指用户线程与垃圾收集线程同时执行（但不一定是并行的，可能会交替执行），用户程序在继续运行，而垃圾收集程序运行于另一个CPU上，他可不用同于并行，并行会有STW，并发几乎没有STW。CMS（Concurrent Mark Sweep）收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上，这类应用尤其重视服务的响应速度，希望系统停顿时间最短，以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。它的运行示意图如下：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/cms-300x79.jpg" alt="cms" width="300" height="79" class="alignnone size-medium wp-image-336" /&gt;
&lt;p&gt;从图上可以看出，CMS收集器是基于“标记—清除”算法实现的，它的运作过程相对于前面几种收集器来说更复杂一些，整个过程分为4个步骤：&lt;/p&gt;
&lt;p&gt;①. 初始标记（CMS initial mark）&lt;br&gt;
初始标记仅仅只是标记一下GC Roots能直接关联到的对象，速度很快，需要“Stop The World”。&lt;/p&gt;</description></item><item><title>Java GC之垃圾回收算法</title><link>https://bridgeli.cn/posts/2017-02-19-java-gc%E4%B9%8B%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/</link><pubDate>Sun, 19 Feb 2017 14:07:54 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-02-19-java-gc%E4%B9%8B%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/</guid><description>&lt;p&gt;&lt;a href="https://www.bridgeli.cn/archives/330" title="Java GC之对象已死吗"&gt;上一篇文章&lt;/a&gt;简单写了一下JVM如何判断一个对象是否已经死了，当判断出一个对象已经死了之后，接下来就要进行垃圾回收了，所以在进行垃圾回收之前，先让我们看看垃圾回收的算法有哪些。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;标记-清除算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;标记清除见名知意该算法分为“标记”和“清除”两个阶段：首先标记出所有需要回收的对象，在标记完成之后统一回收所有被标记的对象，至于如何标记就是上一篇文章中所讲的方法了。这种算法比较简单，容易理解，同时也是一个最基础的垃圾回收算法，后面所讲的算法都是对他的改进，至于为什么需要改进，因为他主要存在两个不足：&lt;/p&gt;
&lt;p&gt;①. 效率问题，标记和清除两个阶段的效率都不高；&lt;br&gt;
②. 空间问题，标记清除之后会留下大量的不连续的内存碎片，内存碎片会导致当后面在程序的运行过程中可能需要给较大的对象分配空间时，无法找到足够的内存而不得不提前触发另一次垃圾回收。这种算法的执行过程如下图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/mark-sweep-300x198.jpg" alt="mark-sweep" width="300" height="198" class="alignnone size-medium wp-image-334" /&gt;
&lt;ol start="2"&gt;
&lt;li&gt;复制算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;复制算法为了解决标记清除算法的效率问题：它将内存分为容量大小相同的两块，每次只使用其中一块，当这一块的内存空间用完了，他就讲里面存活着的对象复制到另一块上面，然后再把已使用过的这一块内存空间一次性清理掉，这样使得每次都是对半个内存区域进行会回收，内存分配时也不用考虑空间碎片的问题，只需要移动指针，按顺序分配即可，实现简单，运行高效。但是他也有缺点：每次使用的内存空间只有整个空间的一半，这“浪费”有点高啊。复制算法的执行过程如下图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/copying-300x176.jpg" alt="copying" width="300" height="176" class="alignnone size-medium wp-image-332" /&gt;
&lt;p&gt;不过现代的商业JVM都采用了这种算法来回收新生代，究其原因不仅仅他解决了效率问题，更是经研究表明：新生代中的对象高达98%都是“朝生夕死”的，所以这样一来就不需要1:1来划分空间了。直接将内存空间分为一块较大的Eden区和两块较小的Survivor区，每次使用Eden和其中一块Survivor区，当需要回收时，直接将Eden和Survivor中还存活的对象一次性的复制到另一块Survivor区，最后在清理Eden和刚使用过的Survivor。HotSpot虚拟机Eden和Survivor的大小比例默认为8:1，也就是每次使用的空间大小是90%，只“浪费”了10%。但是98%的对象回收也不能保证，每次存活的对象所使用的空间小于10%，所以当Survivor空间不够时，就需要其他空间（一般是老年代）进行分配担保，如果另一块Survivor空间没有足够的空间存放上一次新生代垃圾回收存活的对象时，这些对象将直接通过分配担保机制进入老年代。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;标记-整理算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;复制算法适用于新生代的对象“朝生夕死”，如果一个区域内的对象老是不死，不仅内次都需要复制大量的对象，效率很低，而且还需要额外的空间进行担保（分配担保是一个很复杂的东西，今后有机会会说到），所以对于老年代的对象，这种算法是不适合的，于是就提出了标记-整理算法。&lt;br&gt;
标记-整理算法和标记-清除算法一样，也是分两个阶段，而且第一个阶段也一样，都是标记，所不同的是第二个阶段，不是对可回收的对象进行直接清理，而是让还存活着的对象向一端移动，然后直接清理掉端边界以为的内存空间，该算法的执行过程如下图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2017/02/mark-compact-300x197.jpg" alt="mark-compact" width="300" height="197" class="alignnone size-medium wp-image-333" /&gt;
&lt;ol start="4"&gt;
&lt;li&gt;分代收集算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当前的商业JVM都是采用的这种算法，其实这种算法并没有什么新思想，而是根据对象存活周期的不同将内存划分几块，一般是把Java堆分为两块：新生代和老年代，这样就可以根据各个代的不同特点采取最适当的垃圾收集算法。新生代的对象大多都是“朝生夕死”的，而且还可以有老年代进行担保，那就采用复制算法，只需要付出少量的存活对象的复制成本就可以完成收集，而且一般也不需要启用担保策略，而老年代的对象存活率一般比较高、没有空间进行担保，就只有采用“标记-清理”或者“标记-整理”算法来进行回收了。&lt;/p&gt;
&lt;p&gt;参考资料：周志明《深入理解Java虚拟机》第二版第三章&lt;/p&gt;</description></item><item><title>Java GC之对象已死吗</title><link>https://bridgeli.cn/posts/2017-02-11-java-gc%E4%B9%8B%E5%AF%B9%E8%B1%A1%E5%B7%B2%E6%AD%BB%E5%90%97/</link><pubDate>Sat, 11 Feb 2017 13:58:44 +0000</pubDate><guid>https://bridgeli.cn/posts/2017-02-11-java-gc%E4%B9%8B%E5%AF%B9%E8%B1%A1%E5%B7%B2%E6%AD%BB%E5%90%97/</guid><description>&lt;p&gt;差不多两年以前曾经写过一篇文章：&lt;a href="https://www.bridgeli.cn/archives/156" title="JAVA 性能调优"&gt;JAVA 性能调优&lt;/a&gt;，其实在那篇文章中只是简单的说了，对象的分布。这篇文章继续对分布于堆中的对象的生命周期进行说明，也就是确定堆中的这些对象哪些还是“活着”的，哪些是已经“死去”（即不可能再被任何途径使用的对象）的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引用计数算法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有很多人认为判断对象是否活着的算法是这样的：给对象添加一个引用计数器，每当有一个地方引用他的时候，计数器就加1，引用失效的时候，计数器减1，当计数器的数值为0时就是不可能在被引用的对象，此时就就可以认为是已死的对象。引用计数器算法实现简单，效率也很高，是一个不错的算法，但是主流的Java虚拟机并没有采用这种算法来管理内存，其中最主要的原因就是：它很难解决对象之间循环引用的问题。&lt;br&gt;
举一个简单的例子：对象objA和objB都有字段instance，赋值令，除此之外，这两个对象再无任何引用，实际上他们已经不可能在被访问到，但是他们因为相互引用对方，计数器都不可能为0，计数器算法是无法通知GC收集器收集他们的。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package demo;
/**
* testGC()方法执行后，objA和objB会不会被GC呢？
*
* @author BridgeLi
*
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
// 这个成员的唯一意义就是占用内存，以便能在GC日志中看清楚是否被回收过
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设发生了GC，看objA和objB是否能被回收
System.gc();
}
public static void main(String[] args) {
ReferenceCountingGC.testGC();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从这个例子的运行结果来看，虚拟机并没有这两个对象存在相互引用就不收集他们，从而证明了Java虚拟机不是通过引用计数算法来判断对象是否已死的。&lt;/p&gt;</description></item><item><title>Spring aop应用之实现数据库读写分离</title><link>https://bridgeli.cn/posts/2016-12-31-spring-aop%E5%BA%94%E7%94%A8%E4%B9%8B%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/</link><pubDate>Sat, 31 Dec 2016 12:12:51 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-12-31-spring-aop%E5%BA%94%E7%94%A8%E4%B9%8B%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/</guid><description>&lt;p&gt;去年五月份的时候曾经写过一篇：&lt;a href="https://www.bridgeli.cn/archives/166" title="Spring加Mybatis实现MySQL数据库主从读写分离"&gt;Spring加Mybatis实现MySQL数据库主从读写分离&lt;/a&gt;，实现的原理是配置了多套数据源，相应的sqlsessionfactory，transactionmanager和事务代理各配置了一套，如果从库或数据库有多个的时候，需要配置的信息会越来越多，远远不够优雅，在我们编程界有一个规范：约定优于配置。所以就用Sping的aop实现了一个简单的数据库分离方案，具体实现代码放在了Github上，地址如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
https://github.com/bridgeli/practical-util/tree/master/src/main/java/cn/bridgeli/datasource
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读者如果想使用再简单的方法就是把这个代码download下来，放到自己的项目里面，当然更优雅的方式是：打成jar包，放到项目里面了，具体打jar的方法，老夫就不在这里多说了，相信看这篇文章的读者肯定都会了。当然仅仅有这份代码，他们是不会自动生效的，既然是使用Spring的Aop实现数据库读写分离，所以肯定会有牵涉到Aop的配置了，所以在spring-mybatis.xml中有如下配置：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34;
xmlns:aop=&amp;#34;http://www.springframework.org/schema/aop&amp;#34; xmlns:context=&amp;#34;http://www.springframework.org/schema/context&amp;#34;
xmlns:p=&amp;#34;http://www.springframework.org/schema/p&amp;#34; xmlns:tx=&amp;#34;http://www.springframework.org/schema/tx&amp;#34;
xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xsi:schemaLocation=&amp;#34;
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置写数据源 &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;masterDataSource&amp;#34; class=&amp;#34;com.alibaba.druid.pool.DruidDataSource&amp;#34; destroy-method=&amp;#34;close&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;driverClassName&amp;#34; value=&amp;#34;${bridgeli.jdbc.driver}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;url&amp;#34; value=&amp;#34;${bridgeli.jdbc.url}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;username&amp;#34; value=&amp;#34;${bridgeli.jdbc.username}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;password&amp;#34; value=&amp;#34;${bridgeli.jdbc.password}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;validationQuery&amp;#34; value=&amp;#34;${bridgeli.jdbc.validationQuery}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;initialSize&amp;#34; value=&amp;#34;1&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;maxActive&amp;#34; value=&amp;#34;20&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;minIdle&amp;#34; value=&amp;#34;0&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;maxWait&amp;#34; value=&amp;#34;60000&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;testOnBorrow&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;testOnReturn&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;testWhileIdle&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;timeBetweenEvictionRunsMillis&amp;#34; value=&amp;#34;60000&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;minEvictableIdleTimeMillis&amp;#34; value=&amp;#34;25200000&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;removeAbandoned&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;removeAbandonedTimeout&amp;#34; value=&amp;#34;1800&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;logAbandoned&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;filters&amp;#34; value=&amp;#34;stat&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; 配置读数据源 &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;parentSlaveDataSource&amp;#34; class=&amp;#34;com.alibaba.druid.pool.DruidDataSource&amp;#34; destroy-method=&amp;#34;close&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;driverClassName&amp;#34; value=&amp;#34;${bridgeli.jdbc.driver}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;validationQuery&amp;#34; value=&amp;#34;${bridgeli.jdbc.validationQuery}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;initialSize&amp;#34; value=&amp;#34;1&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;maxActive&amp;#34; value=&amp;#34;20&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;minIdle&amp;#34; value=&amp;#34;0&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;maxWait&amp;#34; value=&amp;#34;60000&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;testOnBorrow&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;testOnReturn&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;testWhileIdle&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;timeBetweenEvictionRunsMillis&amp;#34; value=&amp;#34;60000&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;minEvictableIdleTimeMillis&amp;#34; value=&amp;#34;25200000&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;removeAbandoned&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;removeAbandonedTimeout&amp;#34; value=&amp;#34;1800&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;logAbandoned&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;filters&amp;#34; value=&amp;#34;stat&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;slaveDataSource1&amp;#34; class=&amp;#34;com.alibaba.druid.pool.DruidDataSource&amp;#34; destroy-method=&amp;#34;close&amp;#34; parent=&amp;#34;parentSlaveDataSource&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;url&amp;#34; value=&amp;#34;${bridgeli_slave1.jdbc.url}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;username&amp;#34; value=&amp;#34;${bridgeli_slave1.jdbc.username}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;password&amp;#34; value=&amp;#34;${bridgeli_slave1.jdbc.password}&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;dataSource&amp;#34; class=&amp;#34;cn.bridgeli.datasource.MasterSlaveDataSource&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;targetDataSources&amp;#34;&amp;gt;
&amp;lt;map&amp;gt;
&amp;lt;entry key-ref=&amp;#34;masterDataSource&amp;#34; value-ref=&amp;#34;masterDataSource&amp;#34;/&amp;gt;
&amp;lt;entry key-ref=&amp;#34;slaveDataSource1&amp;#34; value-ref=&amp;#34;slaveDataSource1&amp;#34;/&amp;gt;
&amp;lt;/map&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property name=&amp;#34;defaultTargetDataSource&amp;#34; ref=&amp;#34;masterDataSource&amp;#34;/&amp;gt;
&amp;lt;property name=&amp;#34;masterSlaveSelector&amp;#34; ref=&amp;#34;dataSelector&amp;#34;/&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;dataSelector&amp;#34; class=&amp;#34;cn.bridgeli.datasource.MasterSlaveSelectorByPoll&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;masters&amp;#34;&amp;gt;
&amp;lt;list&amp;gt;
&amp;lt;ref bean=&amp;#34;masterDataSource&amp;#34;/&amp;gt;
&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property name=&amp;#34;slaves&amp;#34;&amp;gt;
&amp;lt;list&amp;gt;
&amp;lt;ref bean=&amp;#34;slaveDataSource1&amp;#34;/&amp;gt;
&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property name=&amp;#34;defaultDataSource&amp;#34; ref=&amp;#34;masterDataSource&amp;#34;/&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;aop:aspectj-autoproxy/&amp;gt;
&amp;lt;!&amp;amp;#8211; mybaits 数据工厂 &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;sqlSessionFactory&amp;#34; class=&amp;#34;org.mybatis.spring.SqlSessionFactoryBean&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dataSource&amp;#34; ref=&amp;#34;dataSource&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; 自动扫描所有注解的路径 &amp;amp;#8211;&amp;gt;
&amp;lt;bean class=&amp;#34;org.mybatis.spring.mapper.MapperScannerConfigurer&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;basePackage&amp;#34; value=&amp;#34;cn.bridgeli.mapper&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; &amp;lt;property name=&amp;#34;sqlSessionFactory&amp;#34; ref=&amp;#34;sqlSessionFactory&amp;#34; /&amp;gt; &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;sqlSessionFactoryBeanName&amp;#34; value=&amp;#34;sqlSessionFactory&amp;#34;&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; 数据库切面 &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;masterSlaveAspect&amp;#34; class=&amp;#34;cn.bridgeli.datasource.MasterSlaveAspect&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;prefixMasters&amp;#34;&amp;gt;
&amp;lt;list&amp;gt;
&amp;lt;value&amp;gt;save&amp;lt;/value&amp;gt;
&amp;lt;value&amp;gt;update&amp;lt;/value&amp;gt;
&amp;lt;value&amp;gt;delete&amp;lt;/value&amp;gt;
&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;aop:config&amp;gt;
&amp;lt;aop:aspect id=&amp;#34;c&amp;#34; ref=&amp;#34;masterSlaveAspect&amp;#34;&amp;gt;
&amp;lt;aop:pointcut id=&amp;#34;tx&amp;#34; expression=&amp;#34;execution(\* cn.bridgeli.service..\*.*(..))&amp;#34;/&amp;gt;
&amp;lt;aop:before pointcut-ref=&amp;#34;tx&amp;#34; method=&amp;#34;before&amp;#34;/&amp;gt;
&amp;lt;/aop:aspect&amp;gt;
&amp;lt;/aop:config&amp;gt;
&amp;lt;context:annotation-config /&amp;gt;
&amp;lt;context:component-scan base-package=&amp;#34;cn.bridgeli&amp;#34; /&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样我们就很优雅的利用Spring的Aop实现了对数据库的读写分离，读的时候走slaveDataSource1这个数据源，写的时候走masterDataSource这个数据源。哎，等等，这里哪里体现了约定优于配置这一规范，他们怎么知道哪些方法走读库哪些走写库？同学你别急，仔细读读这个配置文件，你就会发现在第98行，配置了一个MasterSlaveAspect，也就是说代码里面service层（为什么是service层？）的方法以这里面配置的这些关键字打头，都将会走写库，所以当我们想让一个方法走主库的时候，必须在这个地方添加该方法的前缀或者用这里面已有的前缀，这就要求我们必须约定好走主库的方法的打头，即约定优于配置。&lt;/p&gt;</description></item><item><title>Blowfish加密算法Java版简单实现</title><link>https://bridgeli.cn/posts/2016-12-18-blowfish%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95java%E7%89%88%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/</link><pubDate>Sun, 18 Dec 2016 12:04:43 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-12-18-blowfish%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95java%E7%89%88%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/</guid><description>&lt;p&gt;前几天网上突然出现流言：某东发生数据泄露12G，最终某东在一篇声明中没有否认，还算是勉强承认了吧，这件事对于一般人有什么影响、应该怎么做已经有一堆人说了，所以就不凑热闹了，咱来点对程序猿来说实际点的，说一个个人认为目前比较安全的加密算法：Blowfish。&lt;br&gt;
上代码之前，先说几点Blowfish加密算法的特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对称加密，即加密的密钥和解密的密钥是相同的；&lt;/li&gt;
&lt;li&gt;每次加密之后的结果是不同的（这也是老夫比较欣赏的一点）；&lt;/li&gt;
&lt;li&gt;可逆的，和老夫之前的文章介绍的md5等摘要算法不一样，他是可逆的；&lt;/li&gt;
&lt;li&gt;速度快，加密和解密的过程基本上由ADD和XOR指令运算组成；&lt;/li&gt;
&lt;li&gt;免费，任何人都可以免费使用不需要缴纳版权费；&lt;/li&gt;
&lt;li&gt;BlowFish 每次只能加密和解密8字节数据；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来就是最重要的部分，Blowfish加密算法的实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.encrypt;
public enum BlowfishManager {
BRIDGELI_CN(&amp;#34;bridgeli_cn!@#$abc123_&amp;#34;);
private BlowfishManager(String secret) {
this.blowfish = new Blowfish(secret);
}
private Blowfish blowfish;
public Blowfish getBlowfish() {
return blowfish;
}
/**
* 解密
* @param sCipherText
* @return
*/
public String decryptString(String sCipherText){
return this.getBlowfish().decryptString(sCipherText);
}
/**
* 加密
* @param sPlainText
* @return
*/
public String encryptString(String sPlainText){
return this.getBlowfish().encryptString(sPlainText);
}
public static void main(String[] args) {
String encryptString = BlowfishManager.BRIDGELI_CN.encryptString(10 + &amp;#34;&amp;#34;);
System.out.println(encryptString);
String decryptString = BlowfishManager.BRIDGELI_CN.decryptString(encryptString);
System.out.println(decryptString);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这是对外的接口，使用起来非常简单，对用户很友好，下面是算法的具体实现：&lt;/p&gt;</description></item><item><title>Dubbo服务telnet调试法</title><link>https://bridgeli.cn/posts/2016-11-27-dubbo%E6%9C%8D%E5%8A%A1telnet%E8%B0%83%E8%AF%95%E6%B3%95/</link><pubDate>Sun, 27 Nov 2016 13:12:26 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-11-27-dubbo%E6%9C%8D%E5%8A%A1telnet%E8%B0%83%E8%AF%95%E6%B3%95/</guid><description>&lt;p&gt;公司的RPC的服务使用的是阿里巴巴的dubbo，老夫之前曾经写过一篇如何在测试环境&lt;a href="https://www.bridgeli.cn/archives/306" title="Dubbo远程debug方法"&gt;远程调试dubbo服务&lt;/a&gt;，详情请参考这篇，但一直对如何调试线上dubbo服务不得法，不得已每次都需要写一个web服务调一下看数据，前一段时间经新来的一个同事提示可以使用Telnet调试，网上搜了一下资料，发现真的很爽，以下是学习笔记。&lt;br&gt;
需要说明的是：Dubbo2.0.5以上版本服务提供端口支持telnet命令，不过应该没有公司使用2.0.5以下版本吧。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入调试模式&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
telnet localhost 20880
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;即：telnet + ip + 端口，这个不用解释，使用dubbo的肯定都知道&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;ls&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用上一个命令之后，敲一下回车，就进入dubbo的telnet调试服务了，然后就可以使用ls命令了，这个命令有几个用法：&lt;/p&gt;
&lt;p&gt;①. 显示服务列表&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ls
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;②. 显示服务详细信息列表&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ls -l
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;③. 显示服务的方法列表&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ls XxxService
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;④. 显示服务的方法详细信息列表&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ls -l XxxService
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;ps&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个命令主要是看连接信息，也有如下几个用法：&lt;/p&gt;
&lt;p&gt;①. 显示服务端口列表&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ps
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;②. 显示服务地址列表&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ps -l
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;③. 显示端口上的连接信息&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ps 20880
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;④, 显示端口上的连接详细信息&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ps -l 20880
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;cd&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个缺省服务，主要有以下两个用法：&lt;/p&gt;
&lt;p&gt;①. 改变缺省服务，当设置了缺省服务，凡是需要输入服务名作为参数的命令，都可以省略服务参数&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
cd XxxService
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;②. 取消缺省服务&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
cd /
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="5"&gt;
&lt;li&gt;pwd&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;显示当前缺省服务&lt;/p&gt;
&lt;ol start="6"&gt;
&lt;li&gt;trace&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个命令顾名思义：跟踪，具体有以下用法：&lt;/p&gt;
&lt;p&gt;①. 跟踪1次服务任意方法的调用情况&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
trace XxxService
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;②. 跟踪10次服务任意方法的调用情况&lt;/p&gt;</description></item><item><title>VIM常用命令</title><link>https://bridgeli.cn/posts/2016-11-06-vim%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</link><pubDate>Sun, 06 Nov 2016 13:05:15 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-11-06-vim%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</guid><description>&lt;p&gt;上个周苹果公司悍然发布了新版mac，消灭了功能键，包括ESC，终于使下面这个段子成为了事实：问，如何生成一段随机数？答：让一个非开发人员退出vim。哈哈，现在开发人员是不是也可以产生随机字符串了？发现自己作为一个vim党，竟然对很多vim常用的命令都不知道，今天就记一下笔记，让自己这个vim党称呼实至名归。&lt;br&gt;
首先要说明的是，基本的vim命令像A、I、O进入编辑模式，ESC进入命令模式，“:”进入末行模式，以及常用的什么dd，yy，p等都认为大家已经熟练掌握，就不说了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;替换字符&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
:%s/oldchar/newchar/g
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个命令同样可以解决：&lt;/p&gt;
&lt;p&gt;注1. 在windows记事本下的文件放到Linux下时，行末多出来一个^M，这个问题，直接把oldchar换成\r，newchar传承空就可以了。&lt;br&gt;
注2. 在windows记事本下的文件放到Linux下时，行末多出来一个^@，这个问题，直接把oldchar换成先摁ctrl+v，然后摁ctrl+2，newchar传承空就可以了。&lt;br&gt;
注3. 在windows记事本下的文件放到Linux下时，行末多出来一个^A，这个问题，直接把oldchar换成先摁ctrl+v，然后摁ctrl+A，newchar传承空就可以了。&lt;br&gt;
注4. oldchar也可以用正则表达式，之前一直不知道怎么在每一行的行末加东西，其实如此简单而已。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;加密文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;进入末行模式，然后输入大写的X，然后输入密码，保存退出即可，这样的话今后每次打开都需要输入密码才行，否则就是一堆乱码。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;undo和redo&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个比较简单，undo直接摁u，redo是ctrl+r&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;简单的移动光标&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
0 数字零，到行头
^ 到本行第一个不是blank字符的位置
$ 到本行行尾
g_ 到本行最后一个不是blank字符的位置
/pattern 搜索 pattern 的字符串,如果搜索出多个匹配，可按n键到下一个
. (小数点) 可以重复上一次的命令
N&amp;lt;command&amp;gt; 重复某个命令N次
:N 到第N行
gg 到第一行。（陈皓注：相当于1G，或 :1）
G 到最后一行
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="5"&gt;
&lt;li&gt;块操作: ctrl-v&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;块操作，典型的操作： 0、ctrl-v、ctrl-d、I、ESC&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
^ 到行头
ctrl-v 开始块操作
ctrl-d 向下移动 (你也可以使用hjkl来移动光标，或是使用%，或是别的)
I 插入，然后输入
ESC 来为每一行生效。
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="6"&gt;
&lt;li&gt;自动提示&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在输入模式下，你可以输入一个词的开头，然后按 ctrl-p或是ctrl-n，自动补齐功能就出现了&lt;/p&gt;
&lt;ol start="7"&gt;
&lt;li&gt;可视化选择： v,V,ctrl-v&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
ctrl-v，我们可以使用 v 和 V。一但被选好了，你可以做下面的事：
J 把所有的行连接起来（变成一行）
&amp;lt; 或 &amp;gt; 左右缩进
= 自动给缩进
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="8"&gt;
&lt;li&gt;窗口分屏浏览&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
:He 在下边分屏浏览目录
:He! 在上分屏浏览目录
:Ve 在左边分屏间浏览目录
:Ve! 要在右边则是
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="9"&gt;
&lt;li&gt;多页签（tab page）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在末行模式下，输入：&lt;/p&gt;</description></item><item><title>我看拉勾一拍之系统架构</title><link>https://bridgeli.cn/posts/2016-10-23-%E6%88%91%E7%9C%8B%E6%8B%89%E5%8B%BE%E4%B8%80%E6%8B%8D%E4%B9%8B%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/</link><pubDate>Sun, 23 Oct 2016 13:57:34 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-10-23-%E6%88%91%E7%9C%8B%E6%8B%89%E5%8B%BE%E4%B8%80%E6%8B%8D%E4%B9%8B%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/</guid><description>&lt;p&gt;今年年中的时候由公司平台部转组到Alpha项目中心负责公司一拍项目组的技术研发工作，到现在已经快有将近半年的时间了，随着对系统的越来越熟悉，对原有系统的架构也越来越感到有些不合理的地方，随着自己水平的提升感觉对架构也有了一点自己的理解，所以今天就借这个机会说说自己的不成熟的建议。&lt;/p&gt;
&lt;p&gt;一. 原有的架构&lt;/p&gt;
&lt;p&gt;俗话说，一图胜千言，直接上图：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2016/10/old_architecture-225x300.jpg" alt="old_architecture-225x300" width="300" height="225" class="alignnone size-full wp-image-307" /&gt;
&lt;p&gt;解释一下这几个系统分别的作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;后台管理系统不用说了，管理C和B可见的内容；&lt;/li&gt;
&lt;li&gt;C端用户系统，是对C可见的一个系统，一拍是一个招聘系统，所以就是对候选人操作的后台；&lt;/li&gt;
&lt;li&gt;B端用户系统，是对B可见的一个系统，通俗点讲就是HR操作的后台；&lt;/li&gt;
&lt;li&gt;Dubbo系统，是对兄弟部门和B提供服务的一个系统；&lt;/li&gt;
&lt;li&gt;Recommend系统，其实也是一个Dubbo系统，区别在于Dubbo系统是对外提供各种服务的，而Recommend系统是发现全站系统用户的行为，然后对用户的行为进行分析，甄选出一部分C端用户作为一拍的现在用户；&lt;/li&gt;
&lt;li&gt;msgpush系统，是用netty做的一个实时消息推送的IM服务，目前主要是给后台管理用户和C端候选人聊天的一个系统；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中：1. 在我们接手之前B和C是同一个系统，也就是说B是我们这半年新加的一个系统；2. Recommend和msgpush系统也同样是我们这半年新增的一个系统&lt;/p&gt;
&lt;p&gt;二. 系统架构存在的问题&lt;/p&gt;
&lt;p&gt;目前后台管理系统、C端用户系统、Dubbo系统各自独立，这样存在的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最主要的是各自分别操作数据库，这样只要底层数据库发送变动，那么三个系统操作数据库的地方都要同步修改三次；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作数据库的地方代码冗余，很多地方都一样，这样一个地方出bug，三个地方要同步修改，然后都要上线；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后台管理系统采用分层的模式分模块而不是根据业务分模块，这样每次上线service和dao都要先deploy jar到maven私服；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当时为了快速迭代，Recommend系统也是单独操作操作数据库，不过还好用了后台管理系统的dao这个jar包，但是首先根本不需要这么重的一个jar包，其次jar出bug了，有时候他也需要重新上线啊，不然这个jar包就会一直很旧，当然只要不涉及到他操作数据库的地方出bug，你不改也是可以的；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;目前C和B虽然已经分开，但如果用户激增，横向扩展依然不合理，只能整体加机器，而不能针对性对某些模块单独加机器；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码中存在的问题：不知道什么原因大量的逻辑被写在了controller层，导致代码可复用性差；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于之前后台管理系统和其他系统不是同一个团队开发的，命名各有各的风格，代码不仅冗余还同样一个类名字不一样；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;很多系统日志配置的也有问题，错误日志和最基本的业务日志没有区分开，目前在将就用；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统中的jar依赖不仅存在循环依赖，而且加入了大量的自己不需要的依赖，导致各种jar冲突出问题；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综合以上问题，我个人认为这是一个：可维护性、可扩展性不高的系统。&lt;/p&gt;
&lt;p&gt;三. 我的个人思考&lt;/p&gt;
&lt;p&gt;同样先来一张图，来总体说明一下我的想法：&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2016/10/new_architecture-225x300.jpg" alt="new_architecture-225x300" width="300" height="225" class="alignnone size-full wp-image-307" /&gt;
&lt;p&gt;整体来说只有相对独立的实时消息推送系统不懂，然后把其余的各个业务层抽象成微服务，采用公司目前使用比较成熟的dubbo作为rpc框架，controller层只负责业务转发不负责逻辑的一个简单系统，这样带来的好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dubbo系统作为核心的业务系统，分别对兄弟部门、后台管理系统、C端用户系统、B端用户系统、Recommend系统提供服务，如果用户量增加，不仅可以整体增加Dubbo系统的机器，也可以把调用量大的接口单独拆出来，部署到另外的机器上，实现隔离，不会因某个接口调用量大，导致整个系统不可用，而且把数据库的底层操作也放到了这个Dubbo系统中，这样就可以避免数据库修改，要修改多处的问题；&lt;/li&gt;
&lt;li&gt;抽象出来的这个dubbo系统，不仅可以解决后台系统和其他系统命名不一样的问题，而且和可以解决不同团队造成的coding style不一的问题，一举多得；&lt;/li&gt;
&lt;li&gt;由于后台管理系统、C端用户系统、B端用户系统、Recommend系统都调用Dubbo系统，所以他们的controller层讲极其简单，很多业务逻辑类似的东西全部放到了Dubbo系统里面，代码的可复用性提高了不少；&lt;/li&gt;
&lt;li&gt;抽象出来的逻辑都统一放到了dubbo中，这样系统如果有bug，这样就做到了一个地方修改，这样多个地方就可以同时生效；&lt;/li&gt;
&lt;li&gt;由于业务都在Dubbo系统里面这样同时也避免了曾经出现了，后台管理系统和BC系统使用的缓存不一致，导致缓存出问题的这种低级bug；&lt;/li&gt;
&lt;li&gt;由于controller层简单没有逻辑，这样就可以避免目前由于后台管理系统单机，修改一个业务逻辑bug重启系统，导致后台不可用的问题，因为只需要重启dubbo就行了；&lt;/li&gt;
&lt;li&gt;这样controller层变得很轻，只需要一个简单的servlet容器，对机器的要求会降低不少；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;四. 备注&lt;/p&gt;
&lt;p&gt;由于我工作时间不长，见过的系统更有限，所以对系统架构几乎没有什么经验，这些只是我个人的一点很粗浅的理解，例如把dubbo做的那么重，虽然可扩展性提高了不少，但其实也不知道算不算合理，因为调用rpc服务，肯定会增加网络IO延时，所以这些算是我个人的抛砖引玉吧，一方面希望对同样和我一样没有经验的小伙伴能有所帮助，另一方面希望有经验的小伙伴能留言交流&lt;/p&gt;
&lt;p&gt;五. 总结&lt;/p&gt;
&lt;p&gt;以上便是我个人对拉勾一拍所有的核心系统进行了审视后的一番分析，如果这些核心系统架构的重构真的达到自己的理想状况这将是一番浩大的工程，对于高速发展的互联网公司来说，这就是一边驾驶者一辆高速前进的汽车，一边对这辆汽车进行换轮胎换发动机，先不说工作量的问题，难度程度也可见一斑。&lt;/p&gt;</description></item><item><title>Dubbo远程debug方法</title><link>https://bridgeli.cn/posts/2016-08-14-dubbo%E8%BF%9C%E7%A8%8Bdebug%E6%96%B9%E6%B3%95/</link><pubDate>Sun, 14 Aug 2016 21:28:15 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-08-14-dubbo%E8%BF%9C%E7%A8%8Bdebug%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;公司项目的rpc服务基于阿里巴巴的dubbo架构，开发dubbo项目的时候测试只能跑junit test，但实际工作中由于很多时候junit test写的不全，出了问题只能再加日志分析原因（典型的没事找事型），这次和公司移动端的推送联调IM服务，发现他们已经把老夫之前听说的远程debug用在了实际工作中，刚好趁此机会实验了一把，以下是笔记，以待自己和需要的朋友参考。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dubbo服务的设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们自己观察dubbo的start.sh和start.bat这两个脚本会发现有如下两端代码&lt;/p&gt;
&lt;p&gt;①. start.sh&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
JAVA_DEBUG_OPTS=&amp;#34;&amp;#34;
if [ &amp;#34;$1&amp;#34; = &amp;#34;debug&amp;#34; ]; then
JAVA_DEBUG_OPTS=&amp;#34; -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n &amp;#34;
fi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;②. start.bat&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
if &amp;#34;&amp;#34;%1&amp;#34;&amp;#34; == &amp;#34;&amp;#34;debug&amp;#34;&amp;#34; goto debug
if &amp;#34;&amp;#34;%1&amp;#34;&amp;#34; == &amp;#34;&amp;#34;jmx&amp;#34;&amp;#34; goto jmx
java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -classpath ..\conf;%LIB_JARS% com.alibaba.dubbo.container.Main
goto end
:debug
java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n -classpath ..\conf;%LIB_JARS% com.alibaba.dubbo.container.Main
goto end
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;也就是说，脚本已经支持远程debug，只需要的在启动的时候传入一个参数 debug 即可，其余的几乎不用做任何修改&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;eclipse的设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们把远程的服务以支持debug的模式启动之后，就需要把本地的项目也起来了，否则怎么debug呢，本地的设置其实非常简单，一张图搞定&lt;/p&gt;
&lt;img loading="lazy" decoding="async" src="https://www.bridgeli.cn/wp-content/uploads/2016/08/remote_debug-300x168.png" alt="remote_debug-300x168" width="300" height="168" class="alignnone size-full wp-image-307" /&gt;
&lt;p&gt;看了这张图，我相信不用我多说了，远程远程debug如此简单&lt;/p&gt;</description></item><item><title>多线程应用之批量数据处理</title><link>https://bridgeli.cn/posts/2016-07-24-%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%BA%94%E7%94%A8%E4%B9%8B%E6%89%B9%E9%87%8F%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/</link><pubDate>Sun, 24 Jul 2016 14:15:37 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-07-24-%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%BA%94%E7%94%A8%E4%B9%8B%E6%89%B9%E9%87%8F%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/</guid><description>&lt;p&gt;我们都知道多线程是为了加快数据处理的，但至于怎么用，因为在工作中，我一直很少用，所以对多线程不是很了解。之前处理一个功能时，由于没有经验，导致速度很慢，前一段时间经老大提示，可以用多线程解决，突然发现原来多线程可以这么用可以来处理这一类问题，今天记录一下，作为笔记也作为一个给读者的参考，好了先说一下问题：公司的业务的业务不仅分模块而且是分库分表的，这样就导致一个问题，当我们要查询一个数据时，不能连表查询，不能只通过一个接口获得数据，最容易想到的常规做法就是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
public List&amp;lt;Data&amp;gt; queryDatas() {
List&amp;lt;Data&amp;gt; datas = queryDataFromDB();
if(null != datas &amp;amp;&amp;amp; datas.size() &amp;gt; 0) {
for(Data data : datas) {
Object object = getObjectFromDb(data.getId());
data.setAttr1(object.getAttr);
}
}
return datas;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这么做，虽然可以满足业务需求，但效率实在是太低了，尤其是列表数据越大时，如果不只一个属性要这么做时，速度是会慢到要死人的。所以经老大提示参考同事的实现就采用了如下方法：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
private static final ExecutorService executor = Executors.newFixedThreadPool(20);
public List&amp;lt;Data&amp;gt; queryDatas() {
List&amp;lt;Data&amp;gt; datas = queryDataFromDB();
if(null != datas &amp;amp;&amp;amp; datas.size() &amp;gt; 0) {
batchSetAttr(datas);
}
return datas;
}
private boolean batchSetAttr(final List&amp;lt;Data&amp;gt; datas) {
final CompletionService&amp;lt;Data&amp;gt; completionService = new ExecutorCompletionService&amp;lt;&amp;gt;(executor);
for (final Data data : datas) {
completionService.submit(new Callable&amp;lt;Data&amp;gt;() {
@Override
public Data call() throws Exception {
Object object = getObjectFromDb(data.getId());
data.setAttr1(object.getAttr);
return data;
}
});
}
try {
for (int i = 0, size = datas.size(); i &amp;lt; size; i++) {
Future&amp;lt;Data&amp;gt; future = completionService.take();
Data d = future.get();
}
} catch (InterruptedException e) {
logger.error(&amp;#34;InterruptedException&amp;#34;, e);
return false;
} catch (ExecutionException e) {
logger.error(&amp;#34;ExecutionException&amp;#34;, e);
return false;
}
return true;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;利用多线程批量查询，返回时一一设置值，最终达到提高速度的目的。&lt;br&gt;
最后需要说明一点：线程池的大小，大家可以根据自己的实际情况来设置，并不是越大越好；&lt;/p&gt;</description></item><item><title>DFA算法应用之敏感词过滤</title><link>https://bridgeli.cn/posts/2016-05-02-dfa%E7%AE%97%E6%B3%95%E5%BA%94%E7%94%A8%E4%B9%8B%E6%95%8F%E6%84%9F%E8%AF%8D%E8%BF%87%E6%BB%A4/</link><pubDate>Mon, 02 May 2016 12:38:56 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-05-02-dfa%E7%AE%97%E6%B3%95%E5%BA%94%E7%94%A8%E4%B9%8B%E6%95%8F%E6%84%9F%E8%AF%8D%E8%BF%87%E6%BB%A4/</guid><description>&lt;p&gt;公司在做一个社区应用，由于我朝特色，众所周知社区应用有一个很重要的就是要进行敏感词的过滤，这块由一个同事负责，听他说，有一个算法叫DFA，可以做这个，个人比较感兴趣，就到网上查了一些资料，有一篇文章写的特别好，老夫的这篇文章就是把其核心的部分（就是怎么应用，老夫一直有一个观点，理论弱于实践，理论懂得再多不会用一点用没有，所以老夫认为应用是核心）摘出来，留作笔记，如果有想了解其原理的，请点击下方的参考资料，好了，既然是应用那么就直接上代码了：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.dfa;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class SensitivewordFilter {
@SuppressWarnings(&amp;#34;rawtypes&amp;#34;)
private Map sensitiveWordMap = null;
public static int minMatchTYpe = 1; // 最小匹配规则
public static int maxMatchType = 2; // 最大匹配规则
/**
* 初始化敏感词库
*/
public SensitivewordFilter() {
sensitiveWordMap = new SensitiveWordInit().initKeyWord();
}
/**
* 判断文字是否包含敏感字符
*
* @param txt
* 文字
* @param matchType
* 匹配规则 1：最小匹配规则，2：最大匹配规则
* @return 若包含返回true，否则返回false
*/
public boolean isContaintSensitiveWord(String txt, int matchType) {
boolean flag = false;
for (int i = 0; i &amp;lt; txt.length(); i++) {
int matchFlag = this.CheckSensitiveWord(txt, i, matchType); // 判断是否包含敏感字符
if (matchFlag &amp;gt; 0) {
flag = true;
}
}
return flag;
}
/**
* 获取文字中的敏感词
*
* @param txt
* 文字
* @param matchType
* 匹配规则&amp;amp;nbsp;1：最小匹配规则，2：最大匹配规则
* @return
*/
public Set&amp;lt;String&amp;gt; getSensitiveWord(String txt, int matchType) {
Set&amp;lt;String&amp;gt; sensitiveWordList = new HashSet&amp;lt;String&amp;gt;();
for (int i = 0; i &amp;lt; txt.length(); i++) {
int length = CheckSensitiveWord(txt, i, matchType);
if (length &amp;gt; 0) {
sensitiveWordList.add(txt.substring(i, i + length));
i = i + length &amp;amp;#8211; 1; // 减1的原因，是因为for会自增
}
}
return sensitiveWordList;
}
/**
* 替换敏感字字符
*
* @param txt
* @param matchType
* @param replaceChar
\* 替换字符，默认\*
*/
public String replaceSensitiveWord(String txt, int matchType, String replaceChar) {
String resultTxt = txt;
Set&amp;lt;String&amp;gt; set = getSensitiveWord(txt, matchType); // 获取所有的敏感词
Iterator&amp;lt;String&amp;gt; iterator = set.iterator();
String word = null;
String replaceString = null;
while (iterator.hasNext()) {
word = iterator.next();
replaceString = getReplaceChars(replaceChar, word.length());
resultTxt = resultTxt.replaceAll(word, replaceString);
}
return resultTxt;
}
/**
* 获取替换字符串
*
* @param replaceChar
* @param length
* @return
*/
private String getReplaceChars(String replaceChar, int length) {
String resultReplace = replaceChar;
for (int i = 1; i &amp;lt; length; i++) {
resultReplace += replaceChar;
}
return resultReplace;
}
/**
* 检查文字中是否包含敏感字符，检查规则如下：&amp;lt;br&amp;gt;
*
* @param txt
* @param beginIndex
* @param matchType
* @return，如果存在，则返回敏感词字符的长度，不存在返回0
*/
@SuppressWarnings({ &amp;#34;rawtypes&amp;#34; })
public int CheckSensitiveWord(String txt, int beginIndex, int matchType) {
boolean flag = false; // 敏感词结束标识位：用于敏感词只有1位的情况
int matchFlag = 0; // 匹配标识数默认为0
char word = 0;
Map nowMap = sensitiveWordMap;
for (int i = beginIndex; i &amp;lt; txt.length(); i++) {
word = txt.charAt(i);
nowMap = (Map) nowMap.get(word); // 获取指定key
if (nowMap != null) { // 存在，则判断是否为最后一个
matchFlag++; // 找到相应key，匹配标识+1
if (&amp;#34;1&amp;#34;.equals(nowMap.get(&amp;#34;isEnd&amp;#34;))) { // 如果为最后一个匹配规则,结束循环，返回匹配标识数
flag = true; // 结束标志位为true
if (SensitivewordFilter.minMatchTYpe == matchType) { // 最小规则，直接返回,最大规则还需继续查找
break;
}
}
} else { // 不存在，直接返回
break;
}
}
if (matchFlag &amp;lt; 2 || !flag) { // 长度必须大于等于1，为词
matchFlag = 0;
}
return matchFlag;
}
public static void main(String[] args) {
SensitivewordFilter filter = new SensitivewordFilter();
System.out.println(&amp;#34;敏感词的数量：&amp;#34; + filter.sensitiveWordMap.size());
String string = &amp;#34;太多的伤感情怀也许只局限于饲养基地 荧幕中的情节，主人公尝试着去用某种方式渐渐的很潇洒地释自杀指南怀那些自己经历的伤感。&amp;#34;
+ &amp;#34;然后法轮功 我们的扮演的角色就是跟随着主人公的喜红客联盟 怒哀乐而过于牵强的把自己的情感也附加于银幕情节中，然后感动就流泪，&amp;#34;
+ &amp;#34;难过就躺在某一个人的怀里尽情的阐述心扉或者手机卡复制器一个人一杯红酒一部电影在夜三级片 深人静的晚上，关上电话静静的发呆着。&amp;#34;;
Set&amp;lt;String&amp;gt; set = filter.getSensitiveWord(string, 1);
System.out.println(&amp;#34;语句中包含敏感词的个数为：&amp;#34; + set.size() + &amp;#34;。包含：&amp;#34; + set);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个主要是应用，DFA的核心是下面：&lt;/p&gt;</description></item><item><title>记一次线上操作bug</title><link>https://bridgeli.cn/posts/2016-04-24-%E8%AE%B0%E4%B8%80%E6%AC%A1%E7%BA%BF%E4%B8%8A%E6%93%8D%E4%BD%9Cbug/</link><pubDate>Sun, 24 Apr 2016 14:44:25 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-04-24-%E8%AE%B0%E4%B8%80%E6%AC%A1%E7%BA%BF%E4%B8%8A%E6%93%8D%E4%BD%9Cbug/</guid><description>&lt;p&gt;身为程序猿，可以说天天都会遇到bug，今天没为什么记下这次bug呢？说来惭愧，因为这次bug是由于自己不仔细没有仔细检查没有测试就对线上数据下手造成的，一方面是记下这个bug的由来，修复方法和犯下的失误的地方，另一方面也是留下记录警示自己操作线上数据一定要小心再小心，还有就是不要对自己过于自信，测试很重要。&lt;br&gt;
先说一下bug的缘由，19号晚上我们上线了一个新功能，有一个功能模块是另外一个同事负责的，所以对其实现不是很了解，但数据导入有老夫负责，所以数据导入的时候，有一个结束时间没有考虑清楚，只有日期没有时间（产品经理和另一位同事当时也没有给我说），所以数据库里面结束时间变成了默认的“00:00:00”，本来修起来应该很简单，读出来update一下时间就好了，但由于是部门间的协作，比较麻烦，就考虑用SQL解决，所以就写出了如下的SQL：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
CREATE TABLE t_goods_bak AS SELECT REPLACE(a.endtime,&amp;amp;#8217;00:00:00&amp;amp;#8242;,&amp;amp;#8217;23:59:59&amp;amp;#8242;) end_time,a.* FROM t_goods a;
ALTER TABLE \`commercialization\`.\`t_goods_bak\`
CHANGE \`id\` \`id\` INT(11) DEFAULT 0 NOT NULL FIRST,
CHANGE \`end_time\` \`end_time\` DATETIME CHARSET utf8 COLLATE utf8_general_ci NOT NULL AFTER \`endtime\`,
CHANGE \`price\` \`price\` DECIMAL(10,2) NOT NULL COMMENT &amp;amp;#8216;商品单价&amp;amp;#8217; AFTER \`end_time\`;
ALTER TABLE \`commercialization\`.\`t_goods_bak\`
DROP COLUMN \`endtime\`;
ALTER TABLE \`commercialization\`.\`t_goods_bak\`
CHANGE \`end_time\` \`endtime\` VARCHAR(19) CHARSET utf8 COLLATE utf8_general_ci DEFAULT &amp;amp;#8221; NOT NULL COMMENT &amp;amp;#8216;商品失效时间&amp;amp;#8217;;
DROP TABLE \`t_goods_bak\`;
RENAME TABLE \`commercialization\`.\`t_goods_bak\` TO \`commercialization\`.\`t_goods\`;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整体思想就是新建一张表，在新建这张表的时候，把数据修对，修对的数据放在了新添加的end_time字段，然后把这张新表t_goods_bak修改成和原来的表一致，最后把原表删除，再把这张表改一下名字，就达到了替换以前表的目的，所以就OK，看到这里也许有同学已经发现问题了：先别OK，你这新表没主键啊！！！&lt;br&gt;
对，老夫当时就没有多想，以为就此OK了，所以就出现bug了，因为我没有仔细看SQL语句（这些SQL除了，第一句之外都是自动生成的），新表根本没主键，这还不是问题的关键，仔细看第二句SQL，id字段默认是0，所以所有插入的数据，默认值都是0，因为没有自增，这就是最为关键的两个问题。所以综上所述，关于修这个bug，老夫至少忘了如下几件事：&lt;/p&gt;</description></item><item><title>Spring和websocket整合应用示例（下）</title><link>https://bridgeli.cn/posts/2016-04-04-spring%E5%92%8Cwebsocket%E6%95%B4%E5%90%88%E5%BA%94%E7%94%A8%E7%A4%BA%E4%BE%8B%E4%B8%8B/</link><pubDate>Mon, 04 Apr 2016 14:14:24 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-04-04-spring%E5%92%8Cwebsocket%E6%95%B4%E5%90%88%E5%BA%94%E7%94%A8%E7%A4%BA%E4%BE%8B%E4%B8%8B/</guid><description>&lt;p&gt;在&lt;a href="https://www.bridgeli.cn/archives/262" title="Spring和websocket整合应用示例（上）"&gt;上篇&lt;/a&gt;中，我们已经实现了websocket，但还有一个核心的业务实现类没有实现，这里我们就实现这个业务核心类，因为老夫参与的这个系统使用websocket发送消息，所以其实现就是如何发送消息了。&lt;/p&gt;
&lt;ol start="7"&gt;
&lt;li&gt;NewsListenerImpl的实现&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.websocket;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import cn.bridgeli.DateUtil;
import cn.bridgeli.enumeration.PlatNewsCategoryType;
import cn.bridgeli.model.PlatNewsVo;
import cn.bridgeli.model.SearchCondition;
import cn.bridgeli.quartz.impl.TimingJob;
import cn.bridgeli.service.PlatNewsService;
import org.apache.commons.lang.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Description : 站内消息监听器实现
* @Date : 16-3-7
*/
@Component
public class NewsListenerImpl implements NewsListener{
private static final Logger logger = LoggerFactory.getLogger(NewsListenerImpl.class);
Gson gson = new GsonBuilder().setDateFormat(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;).create();
//线程池
private ExecutorService executorService = Executors.newCachedThreadPool();
//任务调度
private SchedulerFactory sf = new StdSchedulerFactory();
@Autowired
private PlatNewsService platNewsService;
@Override
public void afterPersist(PlatNewsVo platNewsVo) {
logger.info(&amp;#34;监听到有新消息添加。。。&amp;#34;);
logger.info(&amp;#34;新消息为:&amp;#34;+gson.toJson(platNewsVo));
//启动线程
if(null != platNewsVo &amp;amp;&amp;amp; !StringUtils.isBlank(platNewsVo.getCurrentoperatoremail())){
//如果是定时消息
if(platNewsVo.getNewsType() == PlatNewsCategoryType.TIMING_TIME.getCategoryId()){
startTimingTask(platNewsVo); //定时推送
}else{
//立即推送
executorService.execute(new AfterConnectionEstablishedTask(platNewsVo.getCurrentoperatoremail()));
}
}
}
@Override
public void afterConnectionEstablished(String email) {
logger.info(&amp;#34;建立websocket连接后推送新消息。。。&amp;#34;);
if(!StringUtils.isBlank(email)){
executorService.execute(new AfterConnectionEstablishedTask(email));
}
}
/**
* @Description ： 如果新添加了定时消息，启动定时消息任务
* @param platNewsVo
*/
private void startTimingTask(PlatNewsVo platNewsVo){
logger.info(&amp;#34;开始定时推送消息任务。。。&amp;#34;);
Date timingTime = platNewsVo.getTimingTime();
if(null == timingTime){
logger.info(&amp;#34;定时消息时间为null。&amp;#34;);
return;
}
logger.info(&amp;#34;定时推送任务时间为：&amp;#34;+DateUtil.date2String(timingTime));
JobDetail jobDetail= JobBuilder.newJob(TimingJob.class)
.withIdentity(platNewsVo.getCurrentoperatoremail()+&amp;#34;定时消息&amp;#34;+platNewsVo.getId(), &amp;#34;站内消息&amp;#34;)
.build();
//传递参数
jobDetail.getJobDataMap().put(&amp;#34;platNewsService&amp;#34;,platNewsService);
jobDetail.getJobDataMap().put(&amp;#34;userEmail&amp;#34;,platNewsVo.getCurrentoperatoremail());
Trigger trigger= TriggerBuilder
.newTrigger()
.withIdentity(&amp;#34;定时消息触发&amp;#34;+platNewsVo.getId(), &amp;#34;站内消息&amp;#34;)
.startAt(timingTime)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(0) //时间间隔
.withRepeatCount(0) //重复次数
)
.build();
//启动定时任务
try {
Scheduler sched = sf.getScheduler();
sched.scheduleJob(jobDetail,trigger);
if(!sched.isShutdown()){
sched.start();
}
} catch (SchedulerException e) {
logger.info(e.toString());
}
logger.info(&amp;#34;完成开启定时推送消息任务。。。&amp;#34;);
}
/**
* @Description : 建立websocket链接后的推送线程
*/
class AfterConnectionEstablishedTask implements Runnable{
String email ;
public AfterConnectionEstablishedTask(String email){
this.email = email;
}
@Override
public void run() {
logger.info(&amp;#34;开始推送消息给用户:&amp;#34;+email+&amp;#34;。。。&amp;#34;);
if(!StringUtils.isBlank(email)){
SearchCondition searchCondition = new SearchCondition();
searchCondition.setOperatorEmail(email);
JSONArray jsonArray = new JSONArray();
for(PlatNewsCategoryType type : PlatNewsCategoryType.values()){
searchCondition.setTypeId(type.getCategoryId());
int count = platNewsService.countPlatNewsByExample(searchCondition);
JSONObject object = new JSONObject();
object.put(&amp;#34;name&amp;#34;,type.name());
object.put(&amp;#34;description&amp;#34;,type.getDescription());
object.put(&amp;#34;count&amp;#34;,count);
jsonArray.add(object);
}
if(null != jsonArray &amp;amp;&amp;amp; jsonArray.size()&amp;gt;0){
UserSocketVo userSocketVo = WSSessionLocalCache.get(email);
TextMessage reMessage = new TextMessage(gson.toJson(jsonArray));
try {
if(null != userSocketVo){
//推送消息
userSocketVo.getWebSocketSession().sendMessage(reMessage);
//更新推送时间
userSocketVo.setLastSendTime(DateUtil.getNowDate());
logger.info(&amp;#34;完成推送新消息给用户:&amp;#34;+userSocketVo.getUserEmail()+&amp;#34;。。。&amp;#34;);
}
} catch (IOException e) {
logger.error(e.toString());
logger.info(&amp;#34;站内消息推送失败。。。&amp;#34;+e.toString());
}
}
}
logger.info(&amp;#34;结束推送消息给&amp;#34;+email+&amp;#34;。。。&amp;#34;);
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个类就是websocket的核心业务的实现，其具体肯定和业务相关，由于业务的不同，实现肯定不同，因为老夫参与的系统是发送消息，所以里面最核心的一句就是：&lt;/p&gt;</description></item><item><title>Spring和websocket整合应用示例（上）</title><link>https://bridgeli.cn/posts/2016-04-04-spring%E5%92%8Cwebsocket%E6%95%B4%E5%90%88%E5%BA%94%E7%94%A8%E7%A4%BA%E4%BE%8B%E4%B8%8A/</link><pubDate>Mon, 04 Apr 2016 14:05:19 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-04-04-spring%E5%92%8Cwebsocket%E6%95%B4%E5%90%88%E5%BA%94%E7%94%A8%E7%A4%BA%E4%BE%8B%E4%B8%8A/</guid><description>&lt;p&gt;嗯，这次真的仅仅是一个入门教程，因为老夫表示自己也不会。近期老夫参与开发公司的一个CRM系统，系统中有很多消息的推送，由一个同事负责，其用到了websocket技术，老夫比较感兴趣，删繁就简，整理了一个教程，留作自己笔记，因很多原理老夫也是不甚了了，以备将来用到了有资料可查。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;maven依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;javax.servlet&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;javax.servlet-api&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.1.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;jackson-core&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;2.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;jackson-databind&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;2.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-websocket&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.0.1.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-messaging&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.0.1.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;spring-servlet的配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34;
xmlns:context=&amp;#34;http://www.springframework.org/schema/context&amp;#34;
xmlns:mvc=&amp;#34;http://www.springframework.org/schema/mvc&amp;#34;
xmlns:tx=&amp;#34;http://www.springframework.org/schema/tx&amp;#34; xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xmlns:websocket=&amp;#34;http://www.springframework.org/schema/websocket&amp;#34;
xsi:schemaLocation=&amp;#34;
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd&amp;#34;&amp;gt;
&amp;amp;#8230;&amp;amp;#8230;
&amp;lt;!&amp;amp;#8211; websocket &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;websocket&amp;#34; class=&amp;#34;cn.bridgeli.websocket.WebsocketEndPoint&amp;#34;/&amp;gt;
&amp;lt;websocket:handlers&amp;gt;
&amp;lt;websocket:mapping path=&amp;#34;/websocket&amp;#34; handler=&amp;#34;websocket&amp;#34;/&amp;gt;
&amp;lt;websocket:handshake-interceptors&amp;gt;
&amp;lt;bean class=&amp;#34;cn.bridgeli.websocket.HandshakeInterceptor&amp;#34;/&amp;gt;
&amp;lt;/websocket:handshake-interceptors&amp;gt;
&amp;lt;/websocket:handlers&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，path对应的路径就是前段通过ws协议调的接口路径&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;HandshakeInterceptor的实现&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.websocket;
import cn.bridgeli.utils.UserManager;
import cn.bridgeli.util.DateUtil;
import cn.bridgeli.sharesession.UserInfo;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import java.util.Date;
import java.util.Map;
/**
* @Description :创建握手（handshake）接口
* @Date : 16-3-3
*/
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor{
private static final Logger logger = LoggerFactory.getLogger(HandshakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map&amp;lt;String, Object&amp;gt; attributes) throws Exception {
logger.info(&amp;#34;建立握手前&amp;amp;#8230;&amp;#34;);
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
UserInfo currUser = UserManager.getSessionUser(attrs.getRequest());
UserSocketVo userSocketVo = new UserSocketVo();
String email= &amp;#34;&amp;#34;;
if(null != currUser){
email = currUser.getEmail();
}
if(StringUtils.isBlank(email)){
email = DateUtil.date2String(new Date());
}
userSocketVo.setUserEmail(email);
attributes.put(&amp;#34;SESSION_USER&amp;#34;, userSocketVo);
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
logger.info(&amp;#34;建立握手后&amp;amp;#8230;&amp;#34;);
super.afterHandshake(request, response, wsHandler, ex);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为老夫不是很懂，所以最大限度的保留原代码，这其实就是从单点登录中取出当前登录用户，转成UserSocketVo对象，放到Map中。所以接下来我们看看UserSocketVo对象的定义&lt;/p&gt;</description></item><item><title>MalformedByteSequenceException: 3 字节的 UTF-8 序列的字节 2 无效</title><link>https://bridgeli.cn/posts/2016-03-20-malformedbytesequenceexception-3-%E5%AD%97%E8%8A%82%E7%9A%84-utf-8-%E5%BA%8F%E5%88%97%E7%9A%84%E5%AD%97%E8%8A%82-2-%E6%97%A0%E6%95%88/</link><pubDate>Sun, 20 Mar 2016 14:23:00 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-03-20-malformedbytesequenceexception-3-%E5%AD%97%E8%8A%82%E7%9A%84-utf-8-%E5%BA%8F%E5%88%97%E7%9A%84%E5%AD%97%E8%8A%82-2-%E6%97%A0%E6%95%88/</guid><description>&lt;p&gt;今天这篇文章比较简单，写一个老夫近期工作中遇到的一个问题，这个问题困扰了老夫几个月了，虽然借助强大的Google百度了好久，但一直没有彻底解决，一直感觉挺简单一问题，也挺常见的一问题（网上问这个问题的还挺多），怎么就没有一个靠谱点的解决方案呢，刚好上个周呢时间稍有空闲，于是仔细研究了一下，终于找到了问题的根源，然后同事一言点醒梦中人，豁然开朗，一举解决，所以记录一下，先说一下异常：MalformedByteSequenceException: 3 字节的 UTF-8 序列的字节 2 无效，详细的堆栈信息如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
严重: StandardWrapper.Throwable
org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from file [D:J2EEapache-tomcat-7.0.62webappscrm-v1.0WEB-INFclassesspring-kafka-consumer.xml]; nested exception is com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: 3 字节的 UTF-8 序列的字节 2 无效。
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:409)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:335)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:303)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:216)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:187)
at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:125)
at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:94)
at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:540)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:454)
at org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(FrameworkServlet.java:658)
at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:624)
at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:672)
at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:543)
at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:484)
at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:136)
at javax.servlet.GenericServlet.init(GenericServlet.java:158)
at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1284)
at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1197)
at org.apache.catalina.core.StandardWrapper.allocate(StandardWrapper.java:864)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:134)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: 3 字节的 UTF-8 序列的字节 2 无效。
at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.invalidByte(UTF8Reader.java:687)
at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.read(UTF8Reader.java:408)
at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(XMLEntityScanner.java:1735)
at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.scanData(XMLEntityScanner.java:1234)
at com.sun.org.apache.xerces.internal.impl.XMLScanner.scanComment(XMLScanner.java:778)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanComment(XMLDocumentFragmentScannerImpl.java:1038)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2988)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:606)
at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:117)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:848)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:777)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:243)
at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:347)
at org.springframework.beans.factory.xml.DefaultDocumentLoader.loadDocument(DefaultDocumentLoader.java:76)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadDocument(XmlBeanDefinitionReader.java:428)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
&amp;amp;#8230; 35 more
三月 16, 2016 5:41:59 下午 org.apache.catalina.core.StandardWrapperValve invoke
严重: Allocate exception for servlet springServlet
com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: 3 字节的 UTF-8 序列的字节 2 无效。
at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.invalidByte(UTF8Reader.java:687)
at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.read(UTF8Reader.java:408)
at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(XMLEntityScanner.java:1735)
at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.scanData(XMLEntityScanner.java:1234)
at com.sun.org.apache.xerces.internal.impl.XMLScanner.scanComment(XMLScanner.java:778)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanComment(XMLDocumentFragmentScannerImpl.java:1038)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2988)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:606)
at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:117)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:848)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:777)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:243)
at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:347)
at org.springframework.beans.factory.xml.DefaultDocumentLoader.loadDocument(DefaultDocumentLoader.java:76)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadDocument(XmlBeanDefinitionReader.java:428)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:335)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:303)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:216)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:187)
at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:125)
at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:94)
at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:540)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:454)
at org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(FrameworkServlet.java:658)
at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:624)
at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:672)
at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:543)
at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:484)
at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:136)
at javax.servlet.GenericServlet.init(GenericServlet.java:158)
at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1284)
at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1197)
at org.apache.catalina.core.StandardWrapper.allocate(StandardWrapper.java:864)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:134)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看完堆栈信息，我相信遇到这个问题的同学，已经知道咋回事了。说起来也很简单，就是tomcat跑一个web项目时，报这个异常，导致项目起不来。在仔细看之后，应该是编码问题导致的，对，就是编码的问题了，看报错的那个XML文件编译之后的情况如下：&lt;/p&gt;</description></item><item><title>POI解析Excel示例</title><link>https://bridgeli.cn/posts/2016-03-13-poi%E8%A7%A3%E6%9E%90excel%E7%A4%BA%E4%BE%8B/</link><pubDate>Sun, 13 Mar 2016 14:13:56 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-03-13-poi%E8%A7%A3%E6%9E%90excel%E7%A4%BA%E4%BE%8B/</guid><description>&lt;p&gt;在Java的世界里，对于解析Excel，目前市场上有两个不错的框架，一个是jxl另一个是poi，之前老夫曾对jxl可以说是倍加赞赏（当时老夫还为了它而写了一篇文章，详见&lt;a href="https://www.bridgeli.cn/archives/50" title="JXL解析Excel常用方法"&gt;这里&lt;/a&gt;），因为一直认为它虽然有bug，虽然兼容性不好，但是它简单易用啊，只要自己够仔细认真就能避开这些坑，但是从这周起，老夫决定jxl一生黑，因为随着时间的推移，现在Excel的版本越来越新，而jxl只支持2003之前的版本，可以说解析起来异常麻烦，而poi有Apache做保证，表现越来越好，使用起来其实也不是很复杂，所以老夫决定之后再次遇到解析Excel的只用poi。&lt;br&gt;
下面是老夫写的一个解析Excel的一个工具类，希望对大家有所帮助。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解析Excel所需的类库的maven依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.poi&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;poi&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.14&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.poi&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;poi-ooxml&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;3.14&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;commons-io&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.3.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;解析的具体方法&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.io.FileInputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.Test;
public class ExcelReader {
protected static final String dateTimeFmtPattern = &amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;;
protected static final String dateFmtPattern = &amp;#34;yyyy-MM-dd&amp;#34;;
protected static final DataFormatter formatter = new DataFormatter();
@Test
public void testReader() throws Exception {
List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; list = readExcel(&amp;#34;E:/test1.xls&amp;#34;);
List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; list2 = readExcel(&amp;#34;E:/test1.xlsx&amp;#34;);
}
/**
* 读取excel文件（同时支持2003和2007格式）
*
* @param fileName
* 文件名，绝对路径
* @return list中的map的key是列的序号
* @throws Exception
* io异常等
*/
public static List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; readExcel(String fileName) throws Exception {
FileInputStream fis = null;
Workbook wb = null;
List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; list = null;
try {
String extension = FilenameUtils.getExtension(fileName);
fis = new FileInputStream(fileName);
list = read(fis, extension);
return list;
} finally {
if (null != wb) {
wb.close();
}
if (null != fis) {
fis.close();
}
}
}
/**
* 读取excel文件（同时支持2003和2007格式）
*
* @param fis
* 文件输入流
* @param extension
* 文件名扩展名: xls 或 xlsx 不区分大小写
* @return list中的map的key是列的序号
* @throws Exception
* io异常等
*/
public static List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; read(FileInputStream fis, String extension) throws Exception {
Workbook wb = null;
List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; list = null;
try {
if (&amp;#34;xls&amp;#34;.equalsIgnoreCase(extension)) {
wb = new HSSFWorkbook(fis);
} else if (&amp;#34;xlsx&amp;#34;.equalsIgnoreCase(extension)) {
wb = new XSSFWorkbook(fis);
} else {
throw new Exception(&amp;#34;file is not office excel&amp;#34;);
}
list = readWorkbook(wb);
return list;
} finally {
if (null != wb) {
wb.close();
}
}
}
protected static List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; readWorkbook(Workbook wb) throws Exception {
List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; list = new LinkedList&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt;();
for (int k = 0; k &amp;lt; wb.getNumberOfSheets(); k++) {
Sheet sheet = wb.getSheetAt(k);
int rows = sheet.getPhysicalNumberOfRows();
for (int r = 0; r &amp;lt; rows; r++) {
Row row = sheet.getRow(r);
if (row == null) {
continue;
}
Map&amp;lt;String, String&amp;gt; map = new HashMap&amp;lt;String, String&amp;gt;();
int cells = row.getPhysicalNumberOfCells();
for (int c = 0; c &amp;lt; cells; c++) {
Cell cell = row.getCell(c);
if (cell == null) {
continue;
}
String value = getCellValue(cell);
map.put(String.valueOf(cell.getColumnIndex() + 1), value);
}
list.add(map);
}
}
return list;
}
protected static String getCellValue(Cell cell) {
String value = null;
switch (cell.getCellType()) {
case Cell.CELL_TYPE_FORMULA: // 公式
case Cell.CELL_TYPE_NUMERIC: // 数字
double doubleVal = cell.getNumericCellValue();
short format = cell.getCellStyle().getDataFormat();
String formatString = cell.getCellStyle().getDataFormatString();
if (format == 14 || format == 31 || format == 57 || format == 58 || (format &amp;gt;= 176 &amp;amp;&amp;amp; format &amp;lt;= 183)) {
// 日期
Date date = DateUtil.getJavaDate(doubleVal);
value = formatDate(date, dateFmtPattern);
} else if (format == 20 || format == 32 || (format &amp;gt;= 184 &amp;amp;&amp;amp; format &amp;lt;= 187)) {
// 时间
Date date = DateUtil.getJavaDate(doubleVal);
value = formatDate(date, &amp;#34;HH:mm&amp;#34;);
} else {
value = String.valueOf(doubleVal);
}
break;
case Cell.CELL_TYPE_STRING: // 字符串
value = cell.getStringCellValue();
break;
case Cell.CELL_TYPE_BLANK: // 空白
value = &amp;#34;&amp;#34;;
break;
case Cell.CELL_TYPE_BOOLEAN: // Boolean
value = String.valueOf(cell.getBooleanCellValue());
break;
case Cell.CELL_TYPE_ERROR: // Error，返回错误码
value = String.valueOf(cell.getErrorCellValue());
break;
default:
value = &amp;#34;&amp;#34;;
break;
}
return value;
}
@SuppressWarnings(&amp;#34;deprecation&amp;#34;)
private static String formatDate(Date d, String sdf) {
String value = null;
if (d.getSeconds() == 0 &amp;amp;&amp;amp; d.getMinutes() == 0 &amp;amp;&amp;amp; d.getHours() == 0) {
// value = DateTimeUtil.getFormatedDate(d, dateFmtPattern);
} else {
// value = DateTimeUtil.getFormatedDate(d, sdf);
}
return value;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于这些第三方工具类的框架来说，老夫一直认为我们没有必要每次都自己去一步一步的写，只要写一次就够了，所以本文就是老夫的一个笔记而已，希望做到无论是老夫还是渎职今后只要需要解析Excel的时候，找到这里，把这里的方法copy出去，改吧改吧就能用了，另外本文也只牵涉到对Excel的解析而已，并没有生成的部分，一方面我在工作中解析多余生成，另一方面我相信大家只要会解析生成也一定不是大问题，网上资料这么多，所以就留给读者自己去探索了&lt;/p&gt;</description></item><item><title>Junit Test之Easy Mock Test入门</title><link>https://bridgeli.cn/posts/2016-02-29-junit-test%E4%B9%8Beasy-mock-test%E5%85%A5%E9%97%A8/</link><pubDate>Mon, 29 Feb 2016 14:37:48 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-02-29-junit-test%E4%B9%8Beasy-mock-test%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;这一段时间公司的项目进行分模块分层进行专人维护开发，所以就会有不同的service和dao有不同的人来开发，这里我们假设service和dao不同的人开发，service是依赖dao的，如果我们的dao开发人员比较忙并没有把dao模块开发好，service如果要对自己的模块进行测试该怎么做呢？这个时候我们的Easy Mock Test就可以派上用场了。&lt;br&gt;
首先开发service的和dao的会讨论商量出来一套接口，假设dao的接口如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.dao;
import cn.bridgeli.model.User;
public interface UserDao {
public User getUserById(int id);
public User getUserByUsername(String username);
//&amp;amp;#8230;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;dao模块的小朋友把它打成一个jar包，扔给service开发人员，然后我们亲爱的service开发人员就自己玩去了，最后我们的service开发人员完成了自己的任务，写下了如下的代码，当然这只是一个demo而已：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.service.impl;
import cn.bridgeli.dao.UserDao;
import cn.bridgeli.model.User;
import cn.bridgeli.service.UserService;
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public boolean login(String username, String password) {
User user = userDao.getUserByUsername(username);
System.out.println(&amp;#34;id==&amp;#34; + user.getId());
if (user != null) {
String passwordInDao = user.getPassword();
if (passwordInDao != null &amp;amp;&amp;amp; passwordInDao.equalsIgnoreCase(password)) {
return true;
}
}
return false;
}
@Override
public User getUserById(int userId) {
return userDao.getUserById(userId);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是我们的service怎么知道自己写的有没有问题呢？现在我们的service想对自己的模块进行测试，但dao开发人员还没开始，这肯定是没办法开始的，那么怎么办呢？很简单，只需要这么做就可以了：&lt;/p&gt;</description></item><item><title>集群Quartz的配置方法</title><link>https://bridgeli.cn/posts/2016-01-03-%E9%9B%86%E7%BE%A4quartz%E7%9A%84%E9%85%8D%E5%88%B6%E6%96%B9%E6%B3%95/</link><pubDate>Sun, 03 Jan 2016 13:23:26 +0000</pubDate><guid>https://bridgeli.cn/posts/2016-01-03-%E9%9B%86%E7%BE%A4quartz%E7%9A%84%E9%85%8D%E5%88%B6%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;一般系统随着用户量的增长，慢慢的都会由单机走向集群，而很多时候我们又需要跑一些定时任务，Quartz就是为此而生，那么单机好办，集群中的Quartz又该如何配置呢？集群中的Quartz各节点之间是通过同一个数据库实例(准确的说是同一个数据库实例的同一套表)来感知彼此的，既然是通过数据库，那么就先看看数SQL文件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SQL文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
#
\# Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar
#
\# In your Quartz properties file, you&amp;amp;#8217;ll need to set
\# org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#
DROP TABLE IF EXISTS QRTZ_JOB_LISTENERS;
DROP TABLE IF EXISTS QRTZ_TRIGGER_LISTENERS;
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
CREATE TABLE QRTZ_JOB_DETAILS
(
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_VOLATILE VARCHAR(1) NOT NULL,
IS_STATEFUL VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_JOB_LISTENERS
(
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
JOB_LISTENER VARCHAR(200) NOT NULL,
PRIMARY KEY (JOB_NAME,JOB_GROUP,JOB_LISTENER),
FOREIGN KEY (JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_TRIGGERS
(
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
IS_VOLATILE VARCHAR(1) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CRON_TRIGGERS
(
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(200) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_TRIGGER_LISTENERS
(
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
TRIGGER_LISTENER VARCHAR(200) NOT NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_LISTENER),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CALENDARS
(
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (CALENDAR_NAME)
);
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (TRIGGER_GROUP)
);
CREATE TABLE QRTZ_FIRED_TRIGGERS
(
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
IS_VOLATILE VARCHAR(1) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_STATEFUL VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (ENTRY_ID)
);
CREATE TABLE QRTZ_SCHEDULER_STATE
(
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (INSTANCE_NAME)
);
CREATE TABLE QRTZ_LOCKS
(
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (LOCK_NAME)
);
INSERT INTO QRTZ_LOCKS values(&amp;amp;#8216;TRIGGER_ACCESS&amp;amp;#8217;);
INSERT INTO QRTZ_LOCKS values(&amp;amp;#8216;JOB_ACCESS&amp;amp;#8217;);
INSERT INTO QRTZ_LOCKS values(&amp;amp;#8216;CALENDAR_ACCESS&amp;amp;#8217;);
INSERT INTO QRTZ_LOCKS values(&amp;amp;#8216;STATE_ACCESS&amp;amp;#8217;);
INSERT INTO QRTZ_LOCKS values(&amp;amp;#8216;MISFIRE_ACCESS&amp;amp;#8217;);
commit;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其实这个文件在Quartz的文档中是可以找到的，这里贴出来只是为了大家方便，建好数据库之后，接下来肯定就是看看数据源的问题了&lt;/p&gt;</description></item><item><title>Bug之我见</title><link>https://bridgeli.cn/posts/2015-09-27-bug%E4%B9%8B%E6%88%91%E8%A7%81/</link><pubDate>Sun, 27 Sep 2015 15:02:54 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-09-27-bug%E4%B9%8B%E6%88%91%E8%A7%81/</guid><description>&lt;p&gt;作为一个工作两年多的程序猿，可以说每天都在和bug打交道，一方面我们在源源不断的创造bug，另一方面我们又致力于消灭一个又一个被我们创造出来的bug。有人说，话不可绝对，否则就是错了，那么说句话的人有没有意识到他这句话就是绝对的呢？是不是也是错的呢？借用他这句话的意思，那么他说的这句话肯定也是错的，事实上也确实就是错的，因为在软件编程界有一句话：世界上没有bug的系统是不存在的，这句话可以说是绝对的正确的。那么世界上没有bug的系统既然是不存在的，那么这么多系统是怎么被上线又运行良好的呢？这就要从什么是bug，bug的严重程度，优先级说起。&lt;br&gt;
首先要说明的是，老夫是一个程序猿，作为一个软件开发者，对bug的理解肯定没有相关的QA人员理解深刻，所以难免有错误，既然如此老夫为什么又要越俎代庖写下这篇文章呢？因为老夫工作的这两年里，深深地感觉有相当一部分QA，对bug的理解并没有那么的深刻，所以老夫希望能够根据自己工作的经验、自己对bug的理解写出来，以期能够抛砖引玉，希望有识之士给予指点，好了，下面我们就开始从什么是bug说起。&lt;/p&gt;
&lt;p&gt;一. 什么是bug&lt;/p&gt;
&lt;p&gt;bug，在IT公司里，每天都会听到这个词，那么什么是bug呢？或者说什么不是bug呢？在IT公司里面，有一部分人员是专职的QA，那么是QA说这是bug就是bug的吗？因为他们最喜欢的提bug了，他们的工作也是提出软件中的各种bug，还是程序猿说这不是bug就不是bug了呢？因为程序猿最不喜欢听到的一句话也许就是：某某系统或者功能又出bug了吧。我想肯定都不是，因为如果QA说是bug就是bug的话，因为QA也可能对需求理解的并不充分或者说理解错误；但如果程序猿说不是bug就不是bug的话，那么世界上的所有系统将不会有bug，又何谈世界上没有bug的系统是不存在的呢？下面老夫给出自己的理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;系统说明书上有而系统上却没有这个功能；&lt;/li&gt;
&lt;li&gt;系统有这个功能，但是所表现出来的和软件需求说明书上的不一样；&lt;/li&gt;
&lt;li&gt;系统说明书上没有，而系统上却有这个功能；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我想①和②大家都能理解的，就不举例说明了，但③可能有些人不能理解，需求说明书上没有，程序猿会做？当然不是不可能的，例如登陆的时候，需求说明上并没有验证码这一项，而程序猿根据自己的经验，很多系统都有，自作主张加上了，那么是不是一个bug呢？肯定是的，理解了什么是bug之后，我们看看bug的严重程度。&lt;/p&gt;
&lt;p&gt;二. bug的严重程度&lt;/p&gt;
&lt;p&gt;严重程度的英文是：Severity，一般来说不同公司对bug严重程度的是分级是不同的，但一般是根据系统是否符合产品说明书以及缺陷对于现阶段测试的影响而定义的，这里只给一个一般性的说明，一般来说bug可分一下五级：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;紧急（Crash）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般是：不能执行正常基本流程，或者重要功能没有实现，使系统崩溃或资源严重不足。无法正常更正的问题。&lt;br&gt;
例如：由于程序所引起的死机、非法退出、程序中断、死循发生死锁、数据通讯错误、功能与需求严重不符、异常退出、重要功能未实现等等&lt;br&gt;
需要说明的是：一般紧急问题需要版本立即处理，处理后立即发版本。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;非常高（Major）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;高一般是：影响其他模块功能不能正常操作，导致一些流程无法进行。&lt;br&gt;
如：小功能没有实现、系统所提供的功能或服务受明显的影响、非法操作数据溢出、一般的数值计算错误&lt;br&gt;
非常高的问题可视情况延迟处理，处理后需发版本，方便后期测试。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;高（Minor）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般是：用户常操作的基本功能没有实现，存在合理的更正办法，用户可进行基本操作。&lt;br&gt;
如：界面错误、格式错误、简单的输入限制未放在前台进行控制、小流程、功能错误&lt;br&gt;
紧急、非常高、高等级的Bug的修改直接影响到产品能否合格的交付。&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;中（Trivial）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;中一般是：操作者使用不方便，但是不影响功能的实现。可改善的功能。数据边界不合理。&lt;br&gt;
如：辅助说明描述不清楚、显示格式不规范、系统处理未优化（性能）、长时间操作未给用户进度提示、提示或页面文字规范、操作时未给用户提示、个别不影响产品理解的错别字、文字排列不整齐等一些小问题、使操作者不方便，操作失误可能导致严重问题出现、浏览器、系统、数据兼容性&lt;/p&gt;
&lt;ol start="5"&gt;
&lt;li&gt;低（Nice to Have）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;低一般是：易用性和建议性的功能&lt;br&gt;
如：不影响使用的瑕疵、更好的实现方式。&lt;br&gt;
需要说明的是，bug的严重程度一般应由测试人员定级&lt;/p&gt;
&lt;p&gt;三. bug的优先级&lt;/p&gt;
&lt;p&gt;bug的优先级的英文是：Priority，bug优先级应根据发版需求、公司要求、bug发生率和bug严重程度来划分。主要用于提示研发同事即时修复bug，使版本能按照预期发布，和严重程度一样，也分一下五级：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;紧急（Urgent）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;立即修复的bug，他阻止相关开发人员的进一步开发活动，立即进行修复工作；阻止与此密切相关功能的进一步测试&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;非常高（Very High）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本版本必须修复的bug&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;高（High）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;必须修改，不一定马上修改，但需确定在某个特定里程碑结束前须修正&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;中（Medium）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时间允许应该修复的bug或可以暂时存在的bug&lt;/p&gt;
&lt;ol start="5"&gt;
&lt;li&gt;低（Low）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可不修复的建议，可以一直存在的问题&lt;/p&gt;
&lt;p&gt;四. bug的严重程度和优先级的关系&lt;/p&gt;
&lt;p&gt;看了bug的严重程度和优先级之后，我们再说说他们之间的关系？当时我和同事再说这个问题的时候，他们都认为这还用说，bug严重高的优先级也一定高啊！事实是这样的，事实上他们之间并没有关系，严重级别的bug不一定优先级别高；优先级别高的bug也不一定严重级别高。因为优先级别和暂时发版的模块密切相关，但是bug严重程度与暂时发版无关，另外Bug的优先级别和公司要求也有关，如电子商务网站，那么界面优化用户体验对于公司就很重要；虽然界面上的问题并不影响使用，所有严重级别不高，但优先级却非常高；而有些系统只是内部使用，界面优化就不那么重要了；还有一些网站只是数据正确性要求比较高，界面和操作要求不高等等。这些都是严重程度很低，优先级却不一定的例子，最后再给大家说一个windows的记事本的bug，大家可以新建一个记事本在里面输入：“联通”俩字保存退出，再打开看看，这个严重程度够大了吧？但微软却一直并没有修，说明了什么问题？类似的例子还有很多，大家可以自己想想：什么的bug严重级别很高，优先级也很高，什么的bug严重级别很高，优先级却很低，什么样的bug严重程度很低，优先级却很高，什么样的bug严重程度很低，优先级也很低。&lt;/p&gt;</description></item><item><title>Blog迁移记</title><link>https://bridgeli.cn/posts/2015-09-05-blog%E8%BF%81%E7%A7%BB%E8%AE%B0/</link><pubDate>Sat, 05 Sep 2015 14:54:58 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-09-05-blog%E8%BF%81%E7%A7%BB%E8%AE%B0/</guid><description>&lt;p&gt;以前老夫的Blog是在西部数码买的空间，转眼一眼就到期了，幸好一朋友免费赞助的一空间，然后就把自己的Blog迁移过去了，下面就总结一下自己的迁移经验，以供需要的朋友或者自己将来再次迁移只用，先说明，老夫用的是典型的LAMP环境，另外Linux环境的Ubuntu。&lt;/p&gt;
&lt;p&gt;LAMP环境，Linux最好弄了，你买空间的时候，选择Linux，并选择发行版本就完了，下面开始从A说起&lt;/p&gt;
&lt;p&gt;一. A（Apache）&lt;/p&gt;
&lt;p&gt;1.安装apache2&lt;/p&gt;
&lt;p&gt;安装命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
sudo apt-get install apache2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;启动/停止/重启apache2:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
service apache2 start/stop/restart
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;卸载apache2&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之前卸载重新安装后找不到apache2.conf配置文件，测试使用以下方式卸载后可用。&lt;/p&gt;
&lt;p&gt;(1)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
sudo apt-get &amp;amp;#8211;purge remove apache2
sudo apt-get &amp;amp;#8211;purge remove apache2.2-common
sudo apt-get autoremove
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(2) （关键一步）找到没有删除掉的配置文件，一并删除&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
sudo find /etc -name &amp;#34;\*apache\*&amp;#34; -exec rm -rf {} ;
sudo rm -rf /var/www
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;二、M（MySQL）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装MySQL&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
sudo apt-get install mysql-server
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;修改MySQL的默认字符集&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;登陆进去之后查看字符集：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
show variables like &amp;amp;#8216;%character%&amp;amp;#8217;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;默认字符集不是UTF-8，不符合我们的要求。则需要修改。修改过程如下：&lt;br&gt;
使用如下命令，打开MySQL的配置文件：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
vim /etc/mysql/my.cnf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在[client]下添加如下一行代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
[client]
default-character-set=utf8
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在[mysqld]下添加如下两行代码：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
[mysqld]
character-set-server = utf8
init_connect=&amp;amp;#8217;SET NAMES utf8&amp;amp;#8242;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;保存后退出，然后重启MySQL：&lt;/p&gt;</description></item><item><title>Dubbo和zookeeper入门实例</title><link>https://bridgeli.cn/posts/2015-07-26-dubbo%E5%92%8Czookeeper%E5%85%A5%E9%97%A8%E5%AE%9E%E4%BE%8B/</link><pubDate>Sun, 26 Jul 2015 14:05:52 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-07-26-dubbo%E5%92%8Czookeeper%E5%85%A5%E9%97%A8%E5%AE%9E%E4%BE%8B/</guid><description>&lt;p&gt;公司项目里用到了dubbo，感觉挺好玩，以前没有玩过，自己抽时间就小小研究了一下，今天记录一下自己的学习成果。&lt;br&gt;
关于Dubbo和zookeeper是干嘛的，网上一搜一大堆所以就不多做介绍了，想了解的可以自己搜搜看，今天就只记录怎么跑一个最基本的Dubbo和zookeeper小示例程序是怎么跑起来的，当然虽然是一个demo，但和真实环境也是无差的哦。&lt;/p&gt;
&lt;p&gt;一. 安装zookeeper&lt;/p&gt;
&lt;p&gt;要想使用Dubbo，必须给Dubbo一个注册中心，当然这个注册中心不一定必须是zookeeper，也可以是redis等，但用zookeeper是一个相对比较好的方式，咱们暂且就这么办。&lt;br&gt;
关于zookeeper的安装，老夫窃以为这篇文章写得非常棒，就不多赘述了，大家可以直接参考：http://blog.csdn.net/wxwzy738/article/details/16330253，看了这篇文章，大家立马就会就明白怎么一回事了，看过自知。&lt;/p&gt;
&lt;p&gt;二. Dubbo实现&lt;/p&gt;
&lt;p&gt;Dubbo的实现肯定是要靠自己写代码啦，我的代码使用maven编译的，引入的jar文件如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;依赖的jar文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;dubbo&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;2.5.3&amp;lt;/version&amp;gt;
&amp;lt;exclusions&amp;gt;
&amp;lt;exclusion&amp;gt;
&amp;lt;artifactId&amp;gt;spring&amp;lt;/artifactId&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;/exclusion&amp;gt;
&amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;slf4j-log4j12&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.7.7&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.101tec&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;zkclient&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;0.5&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-test&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.1.7.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.1.7.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中junit和spring-test大家肯定看出来是干嘛的了，为了跑测试，Dubbo分为生产者和消费者，接下来我们先看看生产者是怎么实现的&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;生产者的实现&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;①. 配置文件applicationProvider.xml&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34;
xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xmlns:dubbo=&amp;#34;http://code.alibabatech.com/schema/dubbo&amp;#34;
xmlns:context=&amp;#34;http://www.springframework.org/schema/context&amp;#34;
xsi:schemaLocation=&amp;#34;
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; Auto Scan &amp;amp;#8211;&amp;gt;
&amp;lt;context:component-scan base-package=&amp;#34;cn.bridgeli.provider&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; Application name &amp;amp;#8211;&amp;gt;
&amp;lt;dubbo:application name=&amp;#34;hello-world-app&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; registry address, used for service to register itself &amp;amp;#8211;&amp;gt;
&amp;lt;dubbo:registry address=&amp;#34;zookeeper://127.0.0.1:2181&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; expose this service through dubbo protocol, through port 20880 &amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211;
&amp;lt;dubbo:protocol name=&amp;#34;dubbo&amp;#34; port=&amp;#34;20880&amp;#34; /&amp;gt;
&amp;lt;dubbo:protocol name=&amp;#34;dubbo&amp;#34; port=&amp;#34;9090&amp;#34; server=&amp;#34;netty&amp;#34;
client=&amp;#34;netty&amp;#34; codec=&amp;#34;dubbo&amp;#34; serialization=&amp;#34;hessian2&amp;#34; charset=&amp;#34;UTF-8&amp;#34;
threadpool=&amp;#34;fixed&amp;#34; threads=&amp;#34;100&amp;#34; queues=&amp;#34;0&amp;#34; iothreads=&amp;#34;9&amp;#34; buffer=&amp;#34;8192&amp;#34;
accepts=&amp;#34;1000&amp;#34; payload=&amp;#34;8388608&amp;#34; /&amp;gt;
&amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211; Service interface Concurrent Control &amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211; &amp;lt;dubbo:service interface=&amp;#34;cn.bridgeli.provider.service.ProviderService&amp;#34; ref=&amp;#34;providerService&amp;#34;/&amp;gt; &amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211; 扫描注解包路径，多个包用逗号分隔，不填pacakge表示扫描当前ApplicationContext中所有的类 &amp;amp;#8211;&amp;gt;
&amp;lt;dubbo:annotation package=&amp;#34;cn.bridgeli.provider.service&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; Default Protocol &amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211;
&amp;lt;dubbo:protocol server=&amp;#34;netty&amp;#34; /&amp;gt;
&amp;amp;#8211;&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注释很详细，不解释，就是dubbo和spring的集成&lt;/p&gt;</description></item><item><title>Java中常见的日期处理方法</title><link>https://bridgeli.cn/posts/2015-05-31-java%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E6%97%A5%E6%9C%9F%E5%A4%84%E7%90%86/</link><pubDate>Sun, 31 May 2015 15:26:20 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-05-31-java%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E6%97%A5%E6%9C%9F%E5%A4%84%E7%90%86/</guid><description>&lt;p&gt;在实际工作中日期是我们非常用的一种类型，其原因相信不用我说了大家都明白，今天就写一篇文章记录一下自己在工作中常用的日期处理&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;日期格式化&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;日期格式化，在工作中，很多时候我们拿到的日期的格式不是我们想要的，那么就需要我们把它格式化成我们想要的，那么怎么做呢？&lt;br&gt;
举个例子：我们想格式化当日时间为：2015-05-31 22:05:30，就可以这么做：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.date;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTest {
public void testSimpleDateFormat() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;);
Date date = new Date();
String now = simpleDateFormat.format(date);
System.out.println(now);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;那么大家就可以根据自己的需要，去格式化大家想要的格式了，需要注意的一点是：我这里的“HH”是大写的表明是24时计时法，如果写成小写的“hh”，就变成12时计时法了。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;获取当前的月份&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我想有些人可能已经想到了，把上面的改成&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&amp;#34;MM&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个是能达到目的，但不够优雅，也不是官方所推荐的，也比较难以维护，下面我们看看标准的实现&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo.date;
import java.util.Calendar;
import java.util.Date;
public class DateTest {
public void testSimpleDateFormat() {
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.get(Calendar.MONTH));
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其实在JDK的早期版本中，Date类型是直接有genMonth()方法的，但由于Date类型比较简单，功能有限，所以后来JDK就把这个方法给废弃了（但为了兼容，目前还是可以用的），然后创造了Calendar，所以我们可以利用Calendar获取年份、月份、日期等等，具体可以查询JDK的API&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;获得当前周几&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个其实和上一个很类似，具体不多说了，大家查JDK的API中的Calendar就可以看到了&lt;/p&gt;</description></item><item><title>Nginx配置使用入门</title><link>https://bridgeli.cn/posts/2015-05-17-nginx%E9%85%8D%E7%BD%AE%E4%BD%BF%E7%94%A8%E5%85%A5%E9%97%A8/</link><pubDate>Sun, 17 May 2015 14:07:24 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-05-17-nginx%E9%85%8D%E7%BD%AE%E4%BD%BF%E7%94%A8%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;Nginx作为当今数一数二的负载均衡服务器，应用十分广泛，今天记录一下，大名鼎鼎的Nginx的配置信息&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个简单的负载均衡的示例，把www.domain.com均衡到本机不同的端口，也可以改为均衡到不同的地址上。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
http {
upstream myproject {
server 127.0.0.1:8000 weight=3;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
server {
listen 80;
server_name www.domain.com;
location / {
proxy_pass http://myproject;
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;weight就是权重，数值越大，分配到这个服务器的概率就越大，以本例来说，分配8000上的概率为二分之一，其他的三个均为为六分之一，另外需要注意的是：proxy_pass和upstream一定要一样&lt;/p&gt;
&lt;p&gt;注：这么配置仅仅是解决了负载均衡的问题，但是没有解决session共享的问题，为了解决session共享我们有两个方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;配置粘连，也就是说，只要某一个请求被分配到该服务器，那么今后的该客端的所有请求全都分配到该服务器上；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在代码中解决session共享问题&lt;br&gt;
目前针对这两种方案，我都不是很熟悉，我将来会写一篇文章来解决这个问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态资源处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
location ~ .(jpg|png|jpeg|bmp|gif|swf|css|js)$ {
expires 30d;
root /nginx-1.8.0;#root
break;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;就是说上面的这些静态资源文件都是相对于：/nginx-1.8.0的，所以如果我们代码中写的路径是img/bridgeli.png，那么我们就需要在nginx-1.8.0建一个img目录，把我们bridgeli.png放在里面。&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;完整示例&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
#!nginx
\# 使用的用户和组
user www www;
\# 指定工作衍生进程数
worker_processes 2;
\# 指定 pid 存放的路径
pid /var/run/nginx.pid;
\# [ debug | info | notice | warn | error | crit ]
\# 可以在下方直接使用 [ debug | info | notice | warn | error | crit ] 参数
error_log /var/log/nginx.error_log info;
events {
\# 允许的连接数
connections 2000;
\# use [ kqueue | rtsig | epoll | /dev/poll | select | poll ] ;
\# 具体内容查看 http://wiki.codemongers.com/事件模型
use kqueue;
}
http {
include conf/mime.types;
default_type application/octet-stream;
log_format main &amp;amp;#8216;$remote_addr &amp;amp;#8211; $remote_user [$time_local] &amp;amp;#8216;
&amp;amp;#8216;&amp;#34;$request&amp;#34; $status $bytes_sent &amp;amp;#8216;
&amp;amp;#8216;&amp;#34;$http_referer&amp;#34; &amp;#34;$http_user_agent&amp;#34; &amp;amp;#8216;
&amp;amp;#8216;&amp;#34;$gzip_ratio&amp;#34;&amp;amp;#8217;;
log_format download &amp;amp;#8216;$remote_addr &amp;amp;#8211; $remote_user [$time_local] &amp;amp;#8216;
&amp;amp;#8216;&amp;#34;$request&amp;#34; $status $bytes_sent &amp;amp;#8216;
&amp;amp;#8216;&amp;#34;$http_referer&amp;#34; &amp;#34;$http_user_agent&amp;#34; &amp;amp;#8216;
&amp;amp;#8216;&amp;#34;$http_range&amp;#34; &amp;#34;$sent_http_content_range&amp;#34;&amp;amp;#8217;;
client_header_timeout 3m;
client_body_timeout 3m;
send_timeout 3m;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
gzip on;
gzip_min_length 1100;
gzip_buffers 4 8k;
gzip_types text/plain;
output_buffers 1 32k;
postpone_output 1460;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
send_lowat 12000;
keepalive_timeout 75 20;
#lingering_time 30;
#lingering_timeout 10;
#reset_timedout_connection on;
server {
listen one.example.com;
server_name one.example.com www.one.example.com;
access_log /var/log/nginx.access_log main;
location / {
proxy_pass http://127.0.0.1/;
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;
client_max_body_size 10m;
client_body_buffer_size 128k;
client_body_temp_path /var/nginx/client_body_temp;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_send_lowat 12000;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
proxy_temp_path /var/nginx/proxy_temp;
charset UTF-8;
}
error_page 404 /404.html;
location /404.html {
root /spool/www;
charset on;
source_charset UTF-8;
}
location /old_stuff/ {
rewrite ^/old_stuff/(.*)$ /new_stuff/$1 permanent;
}
location /download/ {
valid_referers none blocked server_names *.example.com;
if ($invalid_referer) {
#rewrite ^/ http://www.example.com/;
return 403;
}
#rewrite_log on;
\# rewrite /download/\*/mp3/\*.any_ext to /download/\*/mp3/\*.mp3
rewrite ^/(download/.\*)/mp3/(.\*)..*$
/$1/mp3/$2.mp3 break;
root /spool/www;
#autoindex on;
access_log /var/log/nginx-download.access_log download;
}
location ~* ^.+.(jpg|jpeg|gif)$ {
root /spool/www;
access_log off;
expires 30d;
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这是Nginx官方网站的一个例子，说实话里面的很多配置我也不是很清楚，但很多并不是非常难以理解&lt;/p&gt;</description></item><item><title>Spring加Mybatis实现MySQL数据库主从读写分离</title><link>https://bridgeli.cn/posts/2015-05-03-spring%E5%8A%A0mybatis%E5%AE%9E%E7%8E%B0mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%BB%E4%BB%8E%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/</link><pubDate>Sun, 03 May 2015 15:19:38 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-05-03-spring%E5%8A%A0mybatis%E5%AE%9E%E7%8E%B0mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%BB%E4%BB%8E%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/</guid><description>&lt;p&gt;上周在一个同事的指点下，实现了Spring加Mybatis实现了MySQL的主从读写分离，今天记一下笔记，以供自己今后参考，下面是配置文件的写法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据源也就是jdbc.properties，因为是主从读写分离，那么肯定有两个数据源了&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
jdbc.driver=org.mariadb.jdbc.Driver
\# 从库，只读
slave.jdbc.url=jdbc:mariadb://xxx.xxx.xxx.xxx:3306/xxx?characterEncoding=UTF-8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;noAccessToProcedureBodies=true&amp;amp;autoReconnect=true
slave.jdbc.username=xxx
slave.jdbc.password=xxx
\# 主库，需要写
master.jdbc.url=jdbc:mariadb://xxx.xxx.xxx.xxx:3306/xxx?characterEncoding=UTF-8&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;noAccessToProcedureBodies=true&amp;amp;autoReconnect=true
master.jdbc.username=xxx
master.jdbc.password=xxx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个非常简单和普通的区别不是很大，另外数据库的驱动是：mariadb，动下面就是第二个配置文件spring.xml&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;spring.xml&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34;
xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34; xmlns:context=&amp;#34;http://www.springframework.org/schema/context&amp;#34;
xsi:schemaLocation=&amp;#34;
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; Import properties file &amp;amp;#8211;&amp;gt;
&amp;lt;context:property-placeholder location=&amp;#34;classpath:jdbc.properties&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; Auto Scan &amp;amp;#8211;&amp;gt;
&amp;lt;context:component-scan base-package=&amp;#34;cn.bridgeli.demo&amp;#34; /&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个文件很简单，有两个作用，①. 引入数据源配置文件；②. spring扫描的文件的路径&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;spring-mybatis.xml配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34; xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34; xmlns:tx=&amp;#34;http://www.springframework.org/schema/tx&amp;#34; xmlns:aop=&amp;#34;http://www.springframework.org/schema/aop&amp;#34; xsi:schemaLocation=&amp;#34;
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
&amp;#34;&amp;gt;
&amp;lt;bean id=&amp;#34;slaveDataSourceImpl&amp;#34; class=&amp;#34;com.jolbox.bonecp.BoneCPDataSource&amp;#34; destroy-method=&amp;#34;close&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;driverClass&amp;#34; value=&amp;#34;${jdbc.driver}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;jdbcUrl&amp;#34; value=&amp;#34;${slave.jdbc.url}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;username&amp;#34; value=&amp;#34;${slave.jdbc.username}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;password&amp;#34; value=&amp;#34;${slave.jdbc.password}&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 检查数据库连接池中空闲连接的间隔时间，单位是分，默认值：240，如果要取消则设置为0 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;idleConnectionTestPeriodInMinutes&amp;#34; value=&amp;#34;10&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 连接池中未使用的链接最大存活时间，单位是分，默认值：60，如果要永远存活设置为0 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;idleMaxAgeInMinutes&amp;#34; value=&amp;#34;10&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 每个分区最大的连接数 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;maxConnectionsPerPartition&amp;#34; value=&amp;#34;20&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 每个分区最小的连接数 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;minConnectionsPerPartition&amp;#34; value=&amp;#34;10&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 分区数 ，默认值2，最小1，推荐3-4，视应用而定 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;partitionCount&amp;#34; value=&amp;#34;3&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 每次去拿数据库连接的时候一次性要拿几个,默认值：2 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;acquireIncrement&amp;#34; value=&amp;#34;3&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 缓存prepared statements的大小，默认值：0 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;statementsCacheSize&amp;#34; value=&amp;#34;50&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 在做keep-alive的时候的SQL语句 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;connectionTestStatement&amp;#34; value=&amp;#34;select 1 from dual&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 在每次到数据库取连接的时候执行的SQL语句，只执行一次 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;initSQL&amp;#34; value=&amp;#34;select 1 from dual&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;closeConnectionWatch&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;logStatementsEnabled&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;transactionRecoveryEnabled&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;masterDataSourceImpl&amp;#34; class=&amp;#34;com.jolbox.bonecp.BoneCPDataSource&amp;#34; destroy-method=&amp;#34;close&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;driverClass&amp;#34; value=&amp;#34;${jdbc.driver}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;jdbcUrl&amp;#34; value=&amp;#34;${master.jdbc.url}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;username&amp;#34; value=&amp;#34;${master.jdbc.username}&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;password&amp;#34; value=&amp;#34;${master.jdbc.password}&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 检查数据库连接池中空闲连接的间隔时间，单位是分，默认值：240，如果要取消则设置为0 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;idleConnectionTestPeriodInMinutes&amp;#34; value=&amp;#34;10&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 连接池中未使用的链接最大存活时间，单位是分，默认值：60，如果要永远存活设置为0 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;idleMaxAgeInMinutes&amp;#34; value=&amp;#34;10&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 每个分区最大的连接数 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;maxConnectionsPerPartition&amp;#34; value=&amp;#34;20&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 每个分区最小的连接数 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;minConnectionsPerPartition&amp;#34; value=&amp;#34;10&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 分区数 ，默认值2，最小1，推荐3-4，视应用而定 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;partitionCount&amp;#34; value=&amp;#34;3&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 每次去拿数据库连接的时候一次性要拿几个,默认值：2 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;acquireIncrement&amp;#34; value=&amp;#34;3&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 缓存prepared statements的大小，默认值：0 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;statementsCacheSize&amp;#34; value=&amp;#34;50&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 在做keep-alive的时候的SQL语句 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;connectionTestStatement&amp;#34; value=&amp;#34;select 1 from dual&amp;#34; /&amp;gt;
&amp;lt;!&amp;amp;#8211; 在每次到数据库取连接的时候执行的SQL语句，只执行一次 &amp;amp;#8211;&amp;gt;
&amp;lt;property name=&amp;#34;initSQL&amp;#34; value=&amp;#34;select 1 from dual&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;closeConnectionWatch&amp;#34; value=&amp;#34;false&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;logStatementsEnabled&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;transactionRecoveryEnabled&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; DataSource/Master &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;masterDataSource&amp;#34;
class=&amp;#34;org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;targetDataSource&amp;#34; ref=&amp;#34;masterDataSourceImpl&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;masterTransactionManager&amp;#34;
class=&amp;#34;org.springframework.jdbc.datasource.DataSourceTransactionManager&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dataSource&amp;#34; ref=&amp;#34;masterDataSource&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;masterTransactionTemplate&amp;#34;
class=&amp;#34;org.springframework.transaction.support.TransactionTemplate&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;transactionManager&amp;#34; ref=&amp;#34;masterTransactionManager&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; DataSource/Slave &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;slaveDataSource&amp;#34;
class=&amp;#34;org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;targetDataSource&amp;#34; ref=&amp;#34;slaveDataSourceImpl&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;slaveTransactionManager&amp;#34;
class=&amp;#34;org.springframework.jdbc.datasource.DataSourceTransactionManager&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dataSource&amp;#34; ref=&amp;#34;slaveDataSource&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean id=&amp;#34;slaveTransactionTemplate&amp;#34;
class=&amp;#34;org.springframework.transaction.support.TransactionTemplate&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;transactionManager&amp;#34; ref=&amp;#34;slaveTransactionManager&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; Mybatis/Master &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;masterSqlSessionFactory&amp;#34; class=&amp;#34;org.mybatis.spring.SqlSessionFactoryBean&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dataSource&amp;#34; ref=&amp;#34;masterDataSource&amp;#34;&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property name=&amp;#34;configLocation&amp;#34; value=&amp;#34;classpath:mybatis.xml&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;typeAliasesPackage&amp;#34; value=&amp;#34;cn.bridgeli.demo.entity&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;mapperLocations&amp;#34;&amp;gt;
&amp;lt;list&amp;gt;
&amp;lt;value&amp;gt;classpath:cn/bridgeli/demo/mapper/master/*.xml&amp;lt;/value&amp;gt;
&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean class=&amp;#34;org.mybatis.spring.mapper.MapperScannerConfigurer&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;basePackage&amp;#34; value=&amp;#34;cn.bridgeli.demo.mapper.master&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;sqlSessionFactoryBeanName&amp;#34; value=&amp;#34;masterSqlSessionFactory&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; Mybatis/Slave &amp;amp;#8211;&amp;gt;
&amp;lt;bean id=&amp;#34;slaveSqlSessionFactory&amp;#34; class=&amp;#34;org.mybatis.spring.SqlSessionFactoryBean&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dataSource&amp;#34; ref=&amp;#34;slaveDataSource&amp;#34;&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property name=&amp;#34;configLocation&amp;#34; value=&amp;#34;classpath:mybatis.xml&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;typeAliasesPackage&amp;#34; value=&amp;#34;cn.bridgeli.demo.entity&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;mapperLocations&amp;#34;&amp;gt;
&amp;lt;list&amp;gt;
&amp;lt;value&amp;gt;classpath:cn/bridgeli/demo/mapper/slave/*.xml&amp;lt;/value&amp;gt;
&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;bean class=&amp;#34;org.mybatis.spring.mapper.MapperScannerConfigurer&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;basePackage&amp;#34; value=&amp;#34;cn.bridgeli.demo.mapper.slave&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;sqlSessionFactoryBeanName&amp;#34; value=&amp;#34;slaveSqlSessionFactory&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;!&amp;amp;#8211; Configuration transaction advice &amp;amp;#8211;&amp;gt;
&amp;lt;tx:advice id=&amp;#34;txAdvice&amp;#34; transaction-manager=&amp;#34;masterTransactionManager&amp;#34;&amp;gt;
&amp;lt;tx:attributes&amp;gt;
&amp;lt;tx:method name=&amp;#34;add*&amp;#34; propagation=&amp;#34;REQUIRED&amp;#34; /&amp;gt;
&amp;lt;tx:method name=&amp;#34;update*&amp;#34; propagation=&amp;#34;REQUIRED&amp;#34; /&amp;gt;
&amp;lt;tx:method name=&amp;#34;delete*&amp;#34; propagation=&amp;#34;REQUIRED&amp;#34; /&amp;gt;
&amp;lt;tx:method name=&amp;#34;get*&amp;#34; read-only=&amp;#34;true&amp;#34; propagation=&amp;#34;SUPPORTS&amp;#34; /&amp;gt;
&amp;lt;tx:method name=&amp;#34;list*&amp;#34; read-only=&amp;#34;true&amp;#34; propagation=&amp;#34;SUPPORTS&amp;#34; /&amp;gt;
&amp;lt;/tx:attributes&amp;gt;
&amp;lt;/tx:advice&amp;gt;
&amp;lt;!&amp;amp;#8211; Configuration transaction aspect &amp;amp;#8211;&amp;gt;
&amp;lt;aop:config&amp;gt;
&amp;lt;aop:pointcut id=&amp;#34;systemServicePointcut&amp;#34;
expression=&amp;#34;execution(\* cn.bridgeli.demo.service.\*.*(..))&amp;#34; /&amp;gt;
&amp;lt;aop:advisor advice-ref=&amp;#34;txAdvice&amp;#34; pointcut-ref=&amp;#34;systemServicePointcut&amp;#34; /&amp;gt;
&amp;lt;/aop:config&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个配置文件老夫是完整的copy了下来，看起来也比较易懂，就不做解释了，需要说明的mybatis下那些dao的接口，分别对应cn.bridgeli.demo.mapper.master、cn.bridgeli.demo.mapper.slave，cn.bridgeli.demo.mapper.master下的这些dao接口是要写的，另一个是读的，这些接口对应的配置文件肯定就是他们对应的文件夹下面的xml文件了，在将来的项目中几乎可以照抄&lt;/p&gt;</description></item><item><title>MyBatis下最好的分页实现：mybatis-paginator使用入门</title><link>https://bridgeli.cn/posts/2015-04-26-mybatis%E4%B8%8B%E6%9C%80%E5%A5%BD%E7%9A%84%E5%88%86%E9%A1%B5%E5%AE%9E%E7%8E%B0mybatis-paginator%E4%BD%BF%E7%94%A8%E5%85%A5%E9%97%A8/</link><pubDate>Sun, 26 Apr 2015 15:10:02 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-04-26-mybatis%E4%B8%8B%E6%9C%80%E5%A5%BD%E7%9A%84%E5%88%86%E9%A1%B5%E5%AE%9E%E7%8E%B0mybatis-paginator%E4%BD%BF%E7%94%A8%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;前两天写一个项目，发现在MyBatis下一个最好的分页实现类库mybatis-paginator，今天就写一篇其入门教程供大家参考。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先引入maven依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;com.github.miemiedev&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;mybatis-paginator&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.2.15&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从这个依赖中，我们可以看到 1 他是mybatis的一个插件，2 其开源在GitHub上，感兴趣的可以去GitHub上搜源码看看，既然是mybatis的插件，下一步肯定就是和mybatis的集成了.&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;和mybatis集成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;①. 新建一个mybatis.xml的配置文件，其内容如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34; ?&amp;gt;
&amp;lt;!DOCTYPE configuration PUBLIC &amp;#34;-//mybatis.org//DTD Config 3.0//EN&amp;#34; &amp;#34;http://mybatis.org/dtd/mybatis-3-config.dtd&amp;#34;&amp;gt;
&amp;lt;configuration&amp;gt;
&amp;lt;settings&amp;gt;
&amp;lt;setting name=&amp;#34;cacheEnabled&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;setting name=&amp;#34;lazyLoadingEnabled&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/settings&amp;gt;
&amp;lt;plugins&amp;gt;
&amp;lt;plugin
interceptor=&amp;#34;com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dialectClass&amp;#34;
value=&amp;#34;com.github.miemiedev.mybatis.paginator.dialect.MySQLDialect&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;asyncTotalCount&amp;#34; value=&amp;#34;true&amp;#34; /&amp;gt;
&amp;lt;/plugin&amp;gt;
&amp;lt;/plugins&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;②. 和spring集成，即在spring-mybatis.xml中引入该文件&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;amp;#8230;&amp;amp;#8230;
&amp;lt;bean id=&amp;#34;sqlSessionFactory&amp;#34; class=&amp;#34;org.mybatis.spring.SqlSessionFactoryBean&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;dataSource&amp;#34; ref=&amp;#34;dataSource&amp;#34;&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property name=&amp;#34;configLocation&amp;#34; value=&amp;#34;classpath:mybatis.xml&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;typeAliasesPackage&amp;#34; value=&amp;#34;cn.bridgeli.demo.entity&amp;#34; /&amp;gt;
&amp;lt;property name=&amp;#34;mapperLocations&amp;#34; value=&amp;#34;classpath:cn/bridgeli/demo/mapper/*.xml&amp;#34; /&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;amp;#8230;&amp;amp;#8230;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为这个文件太长，就不贴全文了，仅把位置这一块贴出来，当我们把这些配置工作做好之后，下一步就是如何使用了。&lt;/p&gt;</description></item><item><title>正则表达式入门</title><link>https://bridgeli.cn/posts/2015-04-06-%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%85%A5%E9%97%A8/</link><pubDate>Mon, 06 Apr 2015 14:16:10 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-04-06-%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;今天讲讲正则表达式，正则表达式在编程中是非常常用的一项技术，也是非常行之有效的技术，有了他，很多复杂的问题就变得的非常简单了，常见的用途有：字符串匹配（或者叫字符匹配）、字符串查找、字符串替换，典型应用有：用户注册时用户名和密码的验证、检测IP地址是否正确，从网页中揪出链接等等，从常见用途中我们看到，一言以蔽之，正则表达式就是对字符串的处理，所以正则表达式牵涉到的类有三个：java.lang.String、java.util.regex.Pattern、java.util.regex.Matcher，其实正则的用途和功能非常强大，今天老夫就写一些最基本的用法，其实那些高级用法也是从这些基本用法来的，今后看看有没有机会写一下高级用法（主要是老夫现在也不会，，，），下面我们来看看这些最基本的语法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;字符类&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
[abc] a、b 或 c（简单类）
[^abc] 任何字符，除了 a、b 或 c（否定）
[a-zA-Z] a 到 z 或 A 到 Z，两头的字母包括在内（范围）
[a-d[m-p]] a 到 d 或 m 到 p：[a-dm-p]（并集）
[a-z&amp;amp;&amp;amp;[def]] d、e 或 f（交集）
[a-z&amp;amp;&amp;amp;[^bc]] a 到 z，除了 b 和 c：[ad-z]（减去）
[a-z&amp;amp;&amp;amp;[^m-p]] a 到 z，而非 m 到 p：[a-lq-z]（减去）
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;预定义字符类&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
. 任何字符（与行结束符可能匹配也可能不匹配）
d 数字：[0-9]
D 非数字： [^0-9]
s 空白字符：[ tnx0Bfr]
S 非空白字符：[^s]
w 单词字符：[a-zA-Z_0-9]
W 非单词字符：[^w]
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;边界匹配器&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
^ 行的开头
$ 行的结尾
b 单词边界
B 非单词边界
A 输入的开头
G 上一个匹配的结尾
Z 输入的结尾，仅用于最后的结束符（如果有的话）
z 输入的结尾
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;Greedy 数量词&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
X? X，一次或一次也没有
X* X，零次或多次
X+ X，一次或多次
X{n} X，恰好 n 次
X{n,} X，至少 n 次
X{n,m} X，至少 n 次，但是不超过 m 次
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="5"&gt;
&lt;li&gt;反斜线、转义和引用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
反斜线字符 (&amp;amp;#8221;) 用于引用转义构造，如上表所定义的，同时还用于引用其他将被解释为非转义构造的字符。因此，表达式 \ 与单个反斜线匹配，而 { 与左括号匹配。
在不表示转义构造的任何字母字符前使用反斜线都是错误的；它们是为将来扩展正则表达式语言保留的。可以在非字母字符前使用反斜线，不管该字符是否非转义构造的一部分。
根据 Java Language Specification 的要求，Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。因此必须在字符串字面值中使用两个反斜线，表示正则表达式受到保护，不被 Java 字节码编译器解释。例如，当解释为正则表达式时，字符串字面值 &amp;#34;b&amp;#34; 与单个退格字符匹配，而 &amp;#34;\b&amp;#34; 与单词边界匹配。字符串字面值 &amp;#34;(hello)&amp;#34; 是非法的，将导致编译时错误；要与字符串 (hello) 匹配，必须使用字符串字面值 &amp;#34;\(hello\)&amp;#34;。
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其实正则表达式的用法说说难不难，但说简单一点也不简单，下面是一个例子对以上的这些语法进行测试，当我们想不起来的时候，可以把这些例子拷出来跑一下，看一下效果就知道了&lt;/p&gt;</description></item><item><title>JAVA 性能调优</title><link>https://bridgeli.cn/posts/2015-03-29-java-%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98/</link><pubDate>Sun, 29 Mar 2015 15:54:19 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-03-29-java-%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98/</guid><description>&lt;p&gt;学习Java性能调优之前，我们必须得先了解Java中的内存分配：堆、栈、非堆&lt;br&gt;
为了更好的说明这个问题，我们先看一个程序：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;package cn.bridgeli.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class Test {
public void test() {
List&amp;lt;Email&amp;gt; emails = new ArrayList&amp;lt;Email&amp;gt;();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
Class.forName(&amp;#34;&amp;#34;);
String url = &amp;#34;&amp;#34;;
conn = DriverManager.getConnection(url, &amp;#34;&amp;#34;, &amp;#34;&amp;#34;);
pstmt = conn.prepareStatement(&amp;#34;&amp;#34;);
rs = pstmt.executeQuery();
Email email = null;
while (rs.next()) {
email = new Email();
email.setSubject(&amp;#34;&amp;#34;);
emails.add(email);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// close conn
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在这段代码中，那些哪些数据放在堆上，哪些数据放在栈上，又有哪些数据放在方法去呢？一言以蔽之：在方法中，“=”左边的值，全部放在栈上，占4个字节，而“=”右边的肯定就是放在堆上了，所以该段代码中像：emails、conn的变量都是放在栈上的，而我们 new 出来的全部放在了堆上，而方法区是没有放东西的，既然new 出来的Email是放在堆上的，那么Email中的这些变量又是放在哪呢？&lt;/p&gt;</description></item><item><title>SpringMVC中Interceptor和自定义filter的典型应用</title><link>https://bridgeli.cn/posts/2015-03-08-springmvc%E4%B8%ADinterceptor%E5%92%8C%E8%87%AA%E5%AE%9A%E4%B9%89filter%E7%9A%84%E5%85%B8%E5%9E%8B%E5%BA%94%E7%94%A8/</link><pubDate>Sun, 08 Mar 2015 15:03:43 +0000</pubDate><guid>https://bridgeli.cn/posts/2015-03-08-springmvc%E4%B8%ADinterceptor%E5%92%8C%E8%87%AA%E5%AE%9A%E4%B9%89filter%E7%9A%84%E5%85%B8%E5%9E%8B%E5%BA%94%E7%94%A8/</guid><description>&lt;p&gt;今天写写老夫最擅长的Java web，在Java web中Interceptor和filter应用十分广泛，今天就写一个在我们的项目中的一个最基本的应用，过滤或者拦截未登录用户访问某些资源。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SpringMVC中Interceptor&lt;br&gt;
SpringMVC 中的Interceptor 拦截器是相当重要和相当有用的，它的主要作用是拦截用户的请求并进行相应的处理。比如通过它来进行权限验证，或者是来判断用户是否登陆等等。今天就写一个Interceptor在开发中的典型应用：某一系统某些方法肯定是需要用户登陆才能访问的，而另外一些肯定不需要用户登陆就能访问（这样的例子很多，老夫就不举例说明了），那么我们怎么做，才能做到呢？这个时候Interceptor就派上用场了，下面是一个小例子，供参考：&lt;br&gt;
spring-servlet.xml核心代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;beans xmlns=&amp;#34;http://www.springframework.org/schema/beans&amp;#34;
xmlns:mvc=&amp;#34;http://www.springframework.org/schema/mvc&amp;#34; xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xmlns:p=&amp;#34;http://www.springframework.org/schema/p&amp;#34; xmlns:context=&amp;#34;http://www.springframework.org/schema/context&amp;#34;
xsi:schemaLocation=&amp;#34;http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd&amp;#34;&amp;gt;
&amp;lt;mvc:interceptors&amp;gt;
&amp;lt;bean id=&amp;#34;permissionInterceptor&amp;#34; class=&amp;#34;cn.bridgeli.demo.interceptor.PermissionInterceptor&amp;#34;&amp;gt;&amp;lt;/bean&amp;gt;
&amp;lt;/mvc:interceptors&amp;gt;
&amp;amp;#8230;&amp;amp;#8230;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对应的Interceptor的实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;package cn.bridgeli.demo.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.ModelAndViewDefiningException;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.util.UrlPathHelper;
import cn.bridgeli.demo.entity.User;
public class PermissionInterceptor extends HandlerInterceptorAdapter {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
User user = (User) request.getSession().getAttribute(&amp;#34;USER&amp;#34;);
String url = urlPathHelper.getLookupPathForRequest(request);
int flag = url.indexOf(&amp;#34;/admin/&amp;#34;);
if (user == null &amp;amp;&amp;amp; flag != -1) {
ModelAndView mav = new ModelAndView(&amp;#34;error/permissionerror&amp;#34;);
mav.addObject(&amp;#34;ERRORMSG&amp;#34;, &amp;#34;对不起，您没有登录，无法使用该功能！&amp;#34;);
throw new ModelAndViewDefiningException(mav);
}
return true;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;关于InterceptorAdapter的更多用法，大家可以参考http://haohaoxuexi.iteye.com/blog/1750680，老夫以为这篇文章说的相对比较详细易懂，除此之外，我们还可以通过自定义filter来实现；&lt;/p&gt;</description></item><item><title>软件属性小结</title><link>https://bridgeli.cn/posts/2014-12-28-%E8%BD%AF%E4%BB%B6%E5%B1%9E%E6%80%A7%E5%B0%8F%E7%BB%93/</link><pubDate>Sun, 28 Dec 2014 15:02:07 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-12-28-%E8%BD%AF%E4%BB%B6%E5%B1%9E%E6%80%A7%E5%B0%8F%E7%BB%93/</guid><description>&lt;p&gt;一. 功能属性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use Story和Use Case&lt;/li&gt;
&lt;li&gt;功能的三个要素&lt;br&gt;
①. 参与者&lt;br&gt;
用户、角色、用户和角色的关系&lt;br&gt;
②. 流程&lt;br&gt;
整体流程、页面操作流程&lt;br&gt;
③. 表单（UI）&lt;/li&gt;
&lt;li&gt;正确性（精确性）&lt;br&gt;
二. 决定与架构&lt;/li&gt;
&lt;li&gt;权衡软件质量属性&lt;/li&gt;
&lt;li&gt;架构元素&lt;br&gt;
①. 架构（决定）过程&lt;br&gt;
②. 架构（决定）产物&lt;/li&gt;
&lt;li&gt;架构（架构元素集合）&lt;br&gt;
三. 质量属性（非功能性属性）&lt;/li&gt;
&lt;li&gt;开发期质量属性&lt;br&gt;
①. 易理解性和可读性&lt;br&gt;
所有烦人工作成果（如需求文档、设计文档、code等）易读、易理解，可以提高团队开发性率，降低维护成本&lt;br&gt;
考虑的因素：拒绝啰里啰嗦、复杂问题简单化、拒绝学术化、严格遵守codestyle(一旦定下来，团队所有成员必须无条件遵守！！！)&lt;br&gt;
②. 可扩展性&lt;br&gt;
可扩展性是软件适应变化的能力，在软件开发过程中变化时司空见惯的，如需求、设计、算法的改进，程序的变化的等等。&lt;br&gt;
考虑的因素：增量开发、小型大型软件、是否有下一个版本&lt;br&gt;
③. 可重用性&lt;br&gt;
重复利用软件的中某一个组件（如文档模板、架构框架、代码）的能力&lt;br&gt;
考虑的因素：架构（框架）重用、模块重用、重用与分层&lt;br&gt;
④. 可测试性&lt;br&gt;
软件测试的难易程度，软件的可测试性是指软件发生故障并隔离、定位其故障的能力特性，以及在一定的时间和成本的前提下，进行测试设计、测试执行的能力，例如Controller层不要和request、response等耦合、表单等要有Id（为了自动化测试）等&lt;br&gt;
⑤. 可维护性&lt;br&gt;
可维护性是指理解、改正、改动、改进软件的难易程度，影响可维护性的因素有：可理解性、可测试性、可扩展性&lt;br&gt;
改正性维护：软件在使用中发现了隐藏的错误后，为了诊断和改正这个隐藏错误而修改软件的活动&lt;br&gt;
适应性维护：为了适应变化了的环境而修改软件的活动，如：数据库、操作系统、服务器网络带宽等&lt;br&gt;
完善性维护：为了扩充或完善原有软件功能或性能而改动软件的活动&lt;br&gt;
预防性维护：为了提高软件的可维护性和可靠性，为未来的进一步改进打下基础而修改的活动&lt;br&gt;
⑥. 可移植性&lt;br&gt;
是指软件不经修改或稍加修改就可以运行于不同软硬件环境（CPU、OS）的能力，主要是代码的可移植性&lt;br&gt;
考虑的因素：硬件之间、数据库之间&lt;br&gt;
⑦. 兼容性&lt;br&gt;
不同软件或新老版本之间交换信息的能力&lt;br&gt;
考虑的因素：版本之间的兼容、软件之间的兼容，例如：老的客户端能否调用新的服务器版本的API，office和wps兼容性问题（这个存在强势问题，例如微软比较强大、用户比较多，我们就可以不兼容你们金山）&lt;/li&gt;
&lt;li&gt;运行期质量属性&lt;br&gt;
①. 性能&lt;br&gt;
性能通常是指软件的“时间-空间”效率，而不是软件的运行速度。人们总希望软件的运行速度高些，并且占用资源少些，一言以蔽之：既要马儿跑得快，又要马儿吃得少，即性价比最高。&lt;br&gt;
性能优化的关键是：找出限制性能的瓶颈，原则是：管理好自己，控制好别人，那么自己是谁？别人有是谁呢？自己一般是进程（拥有）、内存（分配给自己的）、程序；别人一般是：数据库、文件、其他系统、网络。&lt;br&gt;
控制别人，对外资源（数据库连接、文件流、socket等）一定要关闭，而且在finally里面关闭，当然一些连接可以使用池的概念。&lt;br&gt;
管理好自己，数据库调优、JVM调优、堆栈内存大小的设置等这是一个很大的概念，希望将来能有一篇专门的文章来写这个，其实关于这个网上的资料也挺多的，大家可以自己搜一些来自学一下&lt;br&gt;
②. 安全性&lt;br&gt;
这里的安全性是指信息安全，英文原文是指：security而不是safety，安全性是指防止系统被非法入侵的能力，即属于技术问题又属于安全问题。那么什么样的系统是安全的呢？一般的，如果黑客为非法入侵花费的代价（时间、费用、风险等等）高于得到的好处，那么这样的系统可以认为是安全的&lt;br&gt;
考虑的因素：同源策略、SQL输入、跨站脚本攻击、跨站请求伪造、加密解密技术、API安全性，这些每一个都是一门学问，大家可以自己都一些资料，自己去学习一下&lt;br&gt;
③. 易用性&lt;br&gt;
用户（是指最终用户）使用软件的容易程度，最终用户并不关心软件是怎么实现的，他们只关心UI、操作的方便性、流程的简易程度&lt;br&gt;
④. 可用性&lt;br&gt;
这个比较难描述，只要出现问题就是不可用的，例如：银行系统，A用户向B用户转账，A的钱扣了，B却没收到等等&lt;br&gt;
⑤. 可伸缩性&lt;br&gt;
代表一种弹性，在系统扩展成长的过程中，软件能够保证旺盛的生命力，通过很少的改动甚至只是硬件设备的添置，就能实现整个系统处理能力的线性增长，实现高吞吐量和低延迟的高性能。&lt;br&gt;
考虑的因素：数据库是否支持集群、web服务器的集群（session本地问题、文件本地化问题）&lt;br&gt;
⑥. 互操作性&lt;br&gt;
一般是指对外提供API的调用难易，例如一些开放平台&lt;br&gt;
⑦. 可靠性&lt;br&gt;
在给定的条件下，在给定的时间内，系统不发生故障的概率，可靠性问题一出现一般是很难发现的，他出现无规律，时隐时现&lt;br&gt;
⑧. 健壮性&lt;br&gt;
是指在异常情况下，软件能够正常工作的能力&lt;br&gt;
异常：与预期产生不同结果都叫异常，预期：和用户的预期、QA的预期、需求文档的预期（主要是这个，即运行结果和需求文档描述不一致）&lt;br&gt;
例如：用户注册输入一个已存在的用户名，怎么做？（应该提示用户，不能抱一个505错误）、数据库宕机、网络慢等情形下该怎么做？&lt;br&gt;
容错能力：异常发生后，软件能否正常运行，例如：数据库主从备份、多服务器集群，用户输入错误的数据，能提示用户&lt;br&gt;
恢复能力：例如数据备份，当一个数据库宕机之后，另一个备份数据库能自己立马启动，接下宕机服务器的任务继续工作&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>数据加密算法之MD5和SHA1</title><link>https://bridgeli.cn/posts/2014-12-21-%E6%95%B0%E6%8D%AE%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95%E4%B9%8Bmd5%E5%92%8Csha1/</link><pubDate>Sun, 21 Dec 2014 14:53:22 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-12-21-%E6%95%B0%E6%8D%AE%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95%E4%B9%8Bmd5%E5%92%8Csha1/</guid><description>&lt;p&gt;这个星期记录一下数据加密算法，记得刚开始学编程的时候就有一个疑问：我们的密码就这么放在数据库里面，多不安全啊，数据库管理员不是拿着数据想干嘛就干嘛吗？但是由于认知有限，一直没有解决这个问题，直到去年实习时，当时的项目经理Zack说，用户密码不能明文存放到数据，必须经过MD5加密，终于解决了这个问题。因为MD5的不可逆性，所以就算知道MD5码，只要你不是一些弱密码，一般情况下发生泄密的可能性是非常非常小的，几乎可以认为是绝对安全的，但MD5实现的实现却很简单，今天就记录一下实习时用到的一个MD5加密算法的一个实现：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.demo;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Encryption {
public static String MD5(String input) {
StringBuffer hexString = null;
try {
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance(&amp;#34;MD5&amp;#34;);
// 使用指定的字节更新摘要
mdInst.update(input.getBytes());
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i &amp;lt; md.length; i++) {
String shaHex = Integer.toHexString(md[i] &amp;amp; 0xFF);
if (shaHex.length() &amp;lt; 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hexString.toString();
}
public static String SHA1(String decript) {
StringBuffer hexString = null;
try {
MessageDigest digest = java.security.MessageDigest.getInstance(&amp;#34;SHA-1&amp;#34;);
digest.update(decript.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i &amp;lt; messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] &amp;amp; 0xFF);
if (shaHex.length() &amp;lt; 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hexString.toString();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里面还有一个SHA1机密算法的实现，主要是在自学微信开发时，微信在接入验证的数据经过字典排序后SHA1加密，所以就顺便记录了一下，算是两个比较常用的加密算法吧，供大家参考。&lt;/p&gt;</description></item><item><title>全文检索工具-Lucene（solr）入门</title><link>https://bridgeli.cn/posts/2014-11-02-%E5%85%A8%E6%96%87%E6%A3%80%E7%B4%A2%E5%B7%A5%E5%85%B7-lucenesolr%E5%85%A5%E9%97%A8/</link><pubDate>Sun, 02 Nov 2014 14:43:20 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-11-02-%E5%85%A8%E6%96%87%E6%A3%80%E7%B4%A2%E5%B7%A5%E5%85%B7-lucenesolr%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;最近闲着没事在写微信公众号，其中一个是聊天机器人，和网上的众多机器人原理一样，但是功能没那么强大（主要是只是库不够强大），但是怎么解决“如何根据用户的问题从回答库中找出最匹配的答案呢？”，大家最先想到的也许是数据库的 LIKE 就好了嘛，但是　LIKE 存在如下问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在问答库非常庞大的时候，LIKE 的效率会非常非常的慢；&lt;/li&gt;
&lt;li&gt;LIKE只适用于关键字匹配，并不适合自然语言匹配。举个例子：用户的问题“河南的省会是哪个城市？”，而数据库的的记录是“河南的省会是哪”，虽然无论是从字面上还是意义上都一样，都 LIKE 却无能为力；&lt;/li&gt;
&lt;li&gt;LIKE 无法计算相似度。也就是说 LIKE 返回多条记录时，无法确定那个是最佳答案所以此时全文检索引擎的优越性就体现出来了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;全文检索引擎的原理：扫描知识库的每一条记录并分词建立索引，索引记录了词在每一条记录中出现的位置和次数，当收到用户的问题时，也进行分词，然后从索引中找出包含这些词的所有记录，再分别计算相似度，然后可以找出相似度最高的一条记录返回给用户，下面老夫给出一个自己用Lucene（solr）写的例子，这个例子经验证是可以直接跑起来的，至于其众多API，大家可以自己去查官网文档，其实很容易理解。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.wltea.analyzer.lucene.IKAnalyzer;
import cn.bridgeli.livingsmallhelper.entity.Knowledge;
import cn.bridgeli.livingsmallhelper.mapper.KnowledgeMapper;
import cn.bridgeli.livingsmallhelper.service.ChatService;
public class SolrTest {
private static final Logger LOG = LoggerFactory.getLogger(DataInit.class);
private KnowledgeMapper knowledgeMapper;
@Resource
private ChatService chatService;
public void createIndex() {
List&amp;lt;Knowledge&amp;gt; knowledges = knowledgeMapper.query();
Directory directory = null;
IndexWriter indexWriter = null;
File indexFile = new File(chatService.getIndexDir());
if (!indexFile.exists()) {
try {
directory = FSDirectory.open(indexFile);
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LUCENE_46, new IKAnalyzer(true));
indexWriter = new IndexWriter(directory, indexWriterConfig);
for (Knowledge k : knowledges) {
Document document = new Document();
document.add(new TextField(&amp;#34;question&amp;#34;, k.getQuestion(), Field.Store.YES));
document.add(new IntField(&amp;#34;id&amp;#34;, k.getId(), Field.Store.YES));
document.add(new TextField(&amp;#34;answer&amp;#34;, k.getAnswer() == null ? &amp;#34;&amp;#34; : k.getAnswer(), Field.Store.YES));
document.add(new IntField(&amp;#34;category&amp;#34;, k.getCategory(), Field.Store.YES));
indexWriter.addDocument(document);
}
indexWriter.commit();
} catch (IOException e) {
LOG.error(&amp;#34;IOException&amp;#34;, e);
} finally {
try {
if (null != indexWriter) {
indexWriter.close();
}
if (null != directory) {
directory.close();
}
} catch (IOException e) {
LOG.error(&amp;#34;IOException&amp;#34;, e);
}
}
}
}
@SuppressWarnings(&amp;#34;deprecation&amp;#34;)
private Knowledge searchIndex(String content) {
Knowledge knowledge = null;
Directory directory = null;
IndexReader reader = null;
try {
directory = FSDirectory.open(new File(getIndexDir()));
reader = IndexReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
//这个question 就是以问题为索引去查找，和CreateIndex()中相对应
QueryParser queryParser = new QueryParser(Version.LUCENE_46, &amp;#34;question&amp;#34;, new IKAnalyzer(true));
Query query = queryParser.parse(QueryParser.escape(content));
//这个1 的含义就是找出最相似度最高的一条
TopDocs topDocs = searcher.search(query, 1);
if (topDocs.totalHits &amp;gt; 0) {
knowledge = new Knowledge();
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
Document doc = searcher.doc(scoreDoc.doc);
knowledge.setId(doc.getField(&amp;#34;id&amp;#34;).numericValue().intValue());
knowledge.setQuestion(doc.get(&amp;#34;questory&amp;#34;));
knowledge.setAnswer(doc.get(&amp;#34;answer&amp;#34;));
knowledge.setCategory(doc.getField(&amp;#34;category&amp;#34;).numericValue().intValue());
}
}
} catch (IOException e) {
LOG.error(&amp;#34;IOException&amp;#34;, e);
} catch (ParseException e) {
LOG.error(&amp;#34;ParseException&amp;#34;, e);
} finally {
try {
reader.close();
directory.close();
} catch (IOException e) {
LOG.error(&amp;#34;IOException&amp;#34;, e);
}
}
return knowledge;
}
@Override
public String getIndexDir() {
String classpath = SolrTest.class.getResource(&amp;#34;/&amp;#34;).getPath();
classpath = classpath.replaceAll(&amp;#34;%20&amp;#34;, &amp;#34; &amp;#34;);
LOG.warn(&amp;#34;==================&amp;#34; + classpath);
return classpath + &amp;#34;index/&amp;#34;;
}
@Test
public void testSolr() throws IOException, ParseException {
SolrTest solrTest = new SolrTest();
File indexDir = new File(getIndexDir());
if (!indexDir.exists()) {
solrTest.createIndex(indexDir);
}
solrTest.searchIndex(&amp;#34;你好啊&amp;#34;);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为我是用maven写的，所以对应的pom文件，如下：&lt;/p&gt;</description></item><item><title>Spring mvc中的forward和redirect以及参数传递</title><link>https://bridgeli.cn/posts/2014-10-24-spring-mvc%E4%B8%AD%E7%9A%84forward%E5%92%8Credirect%E4%BB%A5%E5%8F%8A%E5%8F%82%E6%95%B0%E4%BC%A0%E9%80%92/</link><pubDate>Fri, 24 Oct 2014 09:49:46 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-10-24-spring-mvc%E4%B8%AD%E7%9A%84forward%E5%92%8Credirect%E4%BB%A5%E5%8F%8A%E5%8F%82%E6%95%B0%E4%BC%A0%E9%80%92/</guid><description>&lt;ol&gt;
&lt;li&gt;forward和redirect&lt;br&gt;
大家都知道servlet在处理完业务逻辑返回时有两种方法forward和redirect，他们的差异相信不用我再多做解释（如果不知道的请自行谷歌，哪怕是百度也可以），而Spring mvc是对servlet的一种封装，那spring mvc默认采用的是哪一种呢？我们是否可以自己选择采用哪一种方式返回呢？还有我之前在用spring mvc 都是返回到某一个view，它是否可以访问另一个controller呢？针对第一个问题，我们可以看一下spring mvc 的配置文件便知分晓：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;property name=&amp;#34;viewResolvers&amp;#34;&amp;gt;
&amp;lt;list&amp;gt;
&amp;lt;bean class=&amp;#34;org.springframework.web.servlet.view.InternalResourceViewResolver&amp;#34;&amp;gt;
&amp;lt;property name=&amp;#34;prefix&amp;#34; value=&amp;#34;/WEB-INF/pages/&amp;#34;/&amp;gt;
&amp;lt;property name=&amp;#34;suffix&amp;#34; value=&amp;#34;.jsp&amp;#34;/&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;/list&amp;gt;
&amp;lt;/property&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为WEB-INF是一个受保护的目录，客户端是访问不到的，只能通过服务器端访问，根据forward和redirect的区别，我们很容易看到是forward的方式返回的，针对第二个和第三个问题，其实也很简单，我们只需要让该controller返回String即可，然后在方法的最后 return “redirect:/game”;或者return “forward:/game”;即 redirect或者forward + “：” + “/” + controller的路径或者view的名字即可。&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;参数传递&lt;br&gt;
因为公司的项目是用Spring mvc开发的，发现参数传递除了通过model，还可以通过 RedirectAttributes，据说参数的传递和跳转的URL后面带的值会有一定的关系，这个我没具体测试，感兴趣的可以自己测测。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结：这篇文章是近期学到的知识点，以前感觉自己会用Spring mvc了，今天才发现还有好多Spring mvc的特性不知道，值得自己去探索啊！&lt;/p&gt;</description></item><item><title>Java中的split() replace() replaceFirst() replaceAll()四个函数分析</title><link>https://bridgeli.cn/posts/2014-10-20-java%E4%B8%AD%E7%9A%84split-replace-replacefirst-replaceall%E4%B8%89%E4%B8%AA%E5%87%BD%E6%95%B0%E5%88%86%E6%9E%90/</link><pubDate>Mon, 20 Oct 2014 02:34:49 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-10-20-java%E4%B8%AD%E7%9A%84split-replace-replacefirst-replaceall%E4%B8%89%E4%B8%AA%E5%87%BD%E6%95%B0%E5%88%86%E6%9E%90/</guid><description>&lt;p&gt;前几天在公司分割一个很简单字符串，结果却怎么测都不对，最后查了一下资料，终于发现了端倪：&lt;br&gt;
split(regex);&lt;/p&gt;
&lt;p&gt;replace(target, replacement);&lt;br&gt;
replace(oldChar, newChar);&lt;/p&gt;
&lt;p&gt;replaceFirst(regex, replacement);&lt;/p&gt;
&lt;p&gt;replaceAll(regex, replacement)&lt;/p&gt;
&lt;p&gt;仔细看一下，你会发现split()、replaceFirst()、replaceAll()的参数都是Regular Expression，也就是正则表达式，只有replace()的参数是字符或者字符串，由于这些参数类型的差异，很有将得不到预期的结果，下面是一些测试代码的例子，大家可以自己测一下&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.stringtest;
import org.junit.Test;
public class StringTest {
@Test
public void testSplit1() {
String str = &amp;#34;111|222|333|444&amp;#34;;
String[] result = str.split(&amp;#34;|&amp;#34;);
for (String string : result) {
System.out.println(string);
}
// String str = &amp;#34;111|222|333|444&amp;#34;;
// String[] result = str.split(&amp;#34;\|&amp;#34;);
// for (String string : result) {
// System.out.println(string);
// }
}
@Test
public void testSplit2() {
String str = &amp;#34;111,222,333,444&amp;#34;;
String[] result = str.split(&amp;#34;\d&amp;#34;);
for (String string : result) {
System.out.println(string);
}
// String str = &amp;#34;111\222\333\444&amp;#34;;
// String[] result = str.split(&amp;#34;\\&amp;#34;);
// for (String string : result) {
// System.out.println(string);
// }
}
@Test
public void testSplit3() {
String str = &amp;#34;111222333444&amp;#34;;
String[] result = str.split(&amp;#34;\&amp;#34;);
for (String string : result) {
System.out.println(string);
}
// String str = &amp;#34;111\222\333\444&amp;#34;;
// String[] result = str.split(&amp;#34;\\&amp;#34;);
// for (String string : result) {
// System.out.println(string);
// }
}
@Test
public void testReplaceAll() {
String str = &amp;#34;111,222,333,444&amp;#34;;
String result = str.replaceAll(&amp;#34;,&amp;#34;, &amp;#34;$&amp;#34;);
System.out.println(result);
// String str = &amp;#34;111,222,333,444&amp;#34;;
// String result = str.replaceAll(&amp;#34;,&amp;#34;, &amp;#34;\$&amp;#34;);
// System.out.println(result);
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>长链接（URL）转短链接（URL）</title><link>https://bridgeli.cn/posts/2014-10-09-%E9%95%BF%E9%93%BE%E6%8E%A5url%E8%BD%AC%E7%9F%AD%E9%93%BE%E6%8E%A5url/</link><pubDate>Thu, 09 Oct 2014 03:30:42 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-10-09-%E9%95%BF%E9%93%BE%E6%8E%A5url%E8%BD%AC%E7%9F%AD%E9%93%BE%E6%8E%A5url/</guid><description>&lt;p&gt;现在微博越来越流行，大家有事没事都喜欢在微博上说两句，但由于140字的限制，给我们在分析一些长链接的时候，带来了诸多不便，好在微博有自动缩短URL的功能，那我们是否可以自己缩短一个URL呢？答案是肯定的，下面就给出利用百度的API缩短URL的简单例子&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.longurl2short;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class LongURL2Short {
public static String httpRequest(String outputStr) {
StringBuffer buffer = null;
try {
// 建立连接
URL url = new URL(&amp;#34;http://dwz.cn/create.php&amp;#34;);
HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection();
httpUrlConn.setDoInput(true);
httpUrlConn.setDoOutput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(&amp;#34;POST&amp;#34;);
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
// 注意编码格式，防止中文乱码
outputStr = &amp;#34;url=&amp;#34; + outputStr;
outputStream.write(outputStr.getBytes(&amp;#34;UTF-8&amp;#34;));
outputStream.close();
}
// 获取输入流
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, &amp;#34;utf-8&amp;#34;);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 读取返回结果
buffer = new StringBuffer();
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
httpUrlConn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return buffer.toString();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.longurl2short;
import org.junit.Test;
import com.google.gson.Gson;
public class LongUrl2ShortTest {
@Test
public void testHttpRequest() {
String str = LongURL2Short.httpRequest(&amp;#34;http://mp.weixin.qq.com/wiki/index.php?title=%E9%95%BF%E9%93%BE%E6%8E%A5%E8%BD%AC%E7%9F%AD%E9%93%BE%E6%8E%A5%E6%8E%A5%E5%8F%A3&amp;#34;);
Gson gson = new Gson();
ShortUrl shortUrl = gson.fromJson(str, ShortUrl.class);
System.out.println(shortUrl.getTinyurl());
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.longurl2short;
public class ShortUrl {
private String tinyurl;
public String getTinyurl() {
return tinyurl;
}
public void setTinyurl(String tinyurl) {
this.tinyurl = tinyurl;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注：httpRequest方法和前一篇文章 &lt;a href="https://www.bridgeli.cn/archives/78" title="如何用https协议发起一个post请求"&gt;如何用https协议发起一个post请求&lt;/a&gt; 极其类似，本质是一样的，另外该方法经本人测试会有bug：当长URL是.cn时，转换不成功，目前原因还没找到，猜测是由于转化后的URL的后缀也是.cn的原因，具体在测试。&lt;/p&gt;</description></item><item><title>Jsoup在简单防御XSS攻击和网络爬虫的简单应用</title><link>https://bridgeli.cn/posts/2014-09-30-jsoup%E5%9C%A8%E7%AE%80%E5%8D%95%E9%98%B2%E5%BE%A1xss%E6%94%BB%E5%87%BB%E5%92%8C%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB%E7%9A%84%E7%AE%80%E5%8D%95%E5%BA%94%E7%94%A8/</link><pubDate>Tue, 30 Sep 2014 02:00:32 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-30-jsoup%E5%9C%A8%E7%AE%80%E5%8D%95%E9%98%B2%E5%BE%A1xss%E6%94%BB%E5%87%BB%E5%92%8C%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB%E7%9A%84%E7%AE%80%E5%8D%95%E5%BA%94%E7%94%A8/</guid><description>&lt;p&gt;跨站攻击一直是web安全的一大问题，稍有不慎就会中招，各种防不胜防，今天在网上闲逛，发现一个第三方JAR不仅可以简单防御还可以爬取网页，所以写一篇小文以记之，也供有需要的人参考。&lt;br&gt;
预防跨站攻击代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Test
public void testJsoup() {
String unsafe = &amp;#34;&amp;lt;p&amp;gt;&amp;lt;a href=&amp;amp;#8217;http://example.com/&amp;amp;#8217; onclick=&amp;amp;#8217;stealCookies()&amp;amp;#8217;&amp;gt;Link,&amp;lt;alert&amp;gt;0&amp;lt;/alert&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&amp;#34;;
String safe = Jsoup.clean(unsafe, Whitelist.basic());
System.out.println(safe);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中 unsafe 就是用户提交的数据，其中包含 onclick 和 alert 可能会有害，需要过滤，而 Whitelist.basic() 则是过滤规则，safe 就是过滤后的安全数据。&lt;/p&gt;
&lt;p&gt;在网络爬虫方面，曾听北京尚学堂马士兵老师讲过一次正则表达式的简单应用：抓取网页上的邮箱地址，有很多人记不住，但有了这个第三方工具，则轻松很多，他可以根据一个URL直接去抓取，省了不少事，猜测内部也是用正则实现的。&lt;br&gt;
网络爬虫代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Test
public void testSpider() {
String url = &amp;#34;http://www.rijiben.com/&amp;#34;;
Document doc = null;
try {
doc = Jsoup.connect(url).get();
} catch (IOException e) {
e.printStackTrace();
}
if (null != doc) {
Elements listrens = doc.getElementsByAttributeValue(&amp;#34;class&amp;#34;, &amp;#34;listren&amp;#34;);
for (Element listren : listrens) {
String text = listren.select(&amp;#34;li&amp;#34;).select(&amp;#34;a&amp;#34;).html();
System.out.println(text);
}
} else {
System.err.println(&amp;#34;网络出异常！&amp;#34;);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这是抓取日记本上面，历史上的今日的例子，老夫打算将来用到自己的微信公众号上面，怎么样，是不是很简单？&lt;/p&gt;</description></item><item><title>如何用https协议发起一个post请求</title><link>https://bridgeli.cn/posts/2014-09-29-%E5%A6%82%E4%BD%95%E7%94%A8https%E5%8D%8F%E8%AE%AE%E5%8F%91%E8%B5%B7%E4%B8%80%E4%B8%AApost%E8%AF%B7%E6%B1%82/</link><pubDate>Mon, 29 Sep 2014 12:57:06 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-29-%E5%A6%82%E4%BD%95%E7%94%A8https%E5%8D%8F%E8%AE%AE%E5%8F%91%E8%B5%B7%E4%B8%80%E4%B8%AApost%E8%AF%B7%E6%B1%82/</guid><description>&lt;p&gt;这两天研究了微信公众号的开发，发现微信做的太好了，前景太可怕了，如果按照这个趋势，那么将来手机上也许只装一个微信客户端也许就可以做任何事了，其中微信公众号开发自定义菜单时，微信要求用https协议post到微信服务器一个JSON字符串，这里面有两个难点：1. https协议，2. 如何post数据到微信服务器。刚好csdn博主柳峰，有一篇文章是讲解这个的，所以就拿来参考一下，具体代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个是说，相信所有的安全证书，无论是不是安全机构颁发&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
try {
// 创建SSLContext对象，并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance(&amp;#34;SSL&amp;#34;, &amp;#34;SunJSSE&amp;#34;);
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
// 设置请求方式（GET/POST）
httpUrlConn.setRequestMethod(requestMethod);
if (&amp;#34;GET&amp;#34;.equalsIgnoreCase(requestMethod)) {
httpUrlConn.connect();
}
// 当有数据需要提交时
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
// 注意编码格式，防止中文乱码
outputStream.write(outputStr.getBytes(&amp;#34;UTF-8&amp;#34;));
outputStream.close();
}
// 将返回的输入流转换成字符串
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, &amp;#34;utf-8&amp;#34;);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
// 释放资源
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
log.error(&amp;#34;Weixin server connection timed out.&amp;#34;);
} catch (Exception e) {
log.error(&amp;#34;https request error:{}&amp;#34;, e);
}
return jsonObject;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个就是通过https协议向某一个URL通过post或者get提交一个JSON字符串。另外在编程中还有一个常见的向某一个URL请求数据，代码如下：&lt;/p&gt;</description></item><item><title>如何配置一个一键启动的绿色Java web项目</title><link>https://bridgeli.cn/posts/2014-09-20-%E5%A6%82%E4%BD%95%E9%85%8D%E7%BD%AE%E4%B8%80%E4%B8%AA%E4%B8%80%E9%94%AE%E5%90%AF%E5%8A%A8%E7%9A%84%E7%BB%BF%E8%89%B2java-web%E9%A1%B9%E7%9B%AE/</link><pubDate>Sat, 20 Sep 2014 11:01:16 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-20-%E5%A6%82%E4%BD%95%E9%85%8D%E7%BD%AE%E4%B8%80%E4%B8%AA%E4%B8%80%E9%94%AE%E5%90%AF%E5%8A%A8%E7%9A%84%E7%BB%BF%E8%89%B2java-web%E9%A1%B9%E7%9B%AE/</guid><description>&lt;p&gt;我们知道部署J2EE项目，要首先安装JDK，配环境变量，在安装tomcat，然后MySQL数据库（当然也可以是其他任何你喜欢的数据库），把项目打一个war包放到tomcat的webapps包下面，启动tomcat就可以了，但在某些情况下，例如测试美工等，尤其是美工他们的电脑很多时候没必要安装这些乱七八糟的东西，那么我们是否可以不安装这些东西，而让让美工们的电脑跑项目呢？答案是可以的，我们只需要拷贝一个JDK、MySQL、tomcat到美工的电脑，再把项目拷到tomcat的webapps下，写一个bat的文件，让bat文件设置JDK目录、安装MySQL的服务，调用tomcat启动的命令就可以了，srartup.bat代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
set HOME=C:Layout
set JAVA_HOME=%HOME%jdk
set CLASSPATH=%JAVA_HOME%lib
set CATALINA_HOME=%HOME%tomcat
set CATALINA_BASE=%HOME%tomcat
set MYSQL_HOME=%HOME%mysql
set PATH=%PATH%;%JAVA_HOME%bin;%MYSQL_HOME%bin;
taskkill /f /im explorer.exe
start explorer.exe
RunDll32.exe USER32.DLL,UpdatePerUserSystemParameters
echo start lms_mysql on localhost
mysqld &amp;amp;#8211;install lms_mysql &amp;amp;#8211;defaults-file=%MYSQL_HOME%my.ini
net start lms_mysql
echo start tomcat on localhost
@Rem Run Tomcat&amp;amp;#8230;
Call %CATALINA_HOME%binstartup.bat
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;shutdown.bat&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@echo off
echo stop lms_mysql
net stop lms_mysql
mysqld &amp;amp;#8211;remove lms_mysql
echo on
Call %CATALINA_HOME%binshutdown.bat
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>使用JDK自带的工具解析XML文档</title><link>https://bridgeli.cn/posts/2014-09-14-%E4%BD%BF%E7%94%A8jdk%E8%87%AA%E5%B8%A6%E7%9A%84%E5%B7%A5%E5%85%B7%E8%A7%A3%E6%9E%90xml%E6%96%87%E6%A1%A3/</link><pubDate>Sun, 14 Sep 2014 02:39:17 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-14-%E4%BD%BF%E7%94%A8jdk%E8%87%AA%E5%B8%A6%E7%9A%84%E5%B7%A5%E5%85%B7%E8%A7%A3%E6%9E%90xml%E6%96%87%E6%A1%A3/</guid><description>&lt;p&gt;XML和JSON字符串的解析，是Java程序猿的必备技能，关于XML和JSON如何解析，网上的例子可以说是一拉一大把，解析JSON的有什么GSON、json-lib等一大批做得非常好的第三方工具，解析XML的也有什么DOM4J、JDOM、SAX、DOM等等，今天大桥就给大家展示一下如何有JDK自己解析XML，废话不多说，代码如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.parsexmldemo.parsexml;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class ParseXml {
public void parserXml(String fileName) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(fileName);
NodeList rootNode = document.getChildNodes();
parseNode(rootNode.item(0));
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
} catch (ParserConfigurationException e) {
System.out.println(e.getMessage());
} catch (SAXException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public void parseNode(Node node) {
if (node == null) {
return;
}
// if not have child
if (!node.hasChildNodes()) {
NamedNodeMap attrs = node.getAttributes();
if (attrs != null) {
for (int i = 0; i &amp;lt; attrs.getLength(); i++) {
Attr attribute = (Attr) attrs.item(i);
System.out.print(&amp;#34;AttributeName===&amp;#34; + attribute.getName() + &amp;#34; AttributeValue===&amp;#34; + attribute.getValue());
}
}
String str = node.getTextContent().toString().replaceAll(&amp;#34;n&amp;#34;, &amp;#34;&amp;#34;).replaceAll(&amp;#34; &amp;#34;, &amp;#34;&amp;#34;);
if (!str.equals(&amp;#34;&amp;#34;)) {
System.out.println(&amp;#34;TextContent ===&amp;#34; + str);
}
} else {
// get attributes
NamedNodeMap attrs = node.getAttributes();
if (attrs != null) {
for (int i = 0; i &amp;lt; attrs.getLength(); i++) {
Attr attribute = (Attr) attrs.item(i);
System.out.println(&amp;#34;AttributeName===&amp;#34; + attribute.getName() + &amp;#34; AttributeValue===&amp;#34; + attribute.getValue());
}
}
// get child node
NodeList nodes = node.getChildNodes();
for (int j = 0; j &amp;lt; nodes.getLength(); j++) {
parseNode(nodes.item(j));
}
}
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;具体可以参考：&lt;/p&gt;</description></item><item><title>Java在线支付（利用易宝支付的接口）</title><link>https://bridgeli.cn/posts/2014-09-13-java%E5%9C%A8%E7%BA%BF%E6%94%AF%E4%BB%98%E5%88%A9%E7%94%A8%E6%98%93%E5%AE%9D%E6%94%AF%E4%BB%98%E7%9A%84%E6%8E%A5%E5%8F%A3/</link><pubDate>Sat, 13 Sep 2014 03:26:10 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-13-java%E5%9C%A8%E7%BA%BF%E6%94%AF%E4%BB%98%E5%88%A9%E7%94%A8%E6%98%93%E5%AE%9D%E6%94%AF%E4%BB%98%E7%9A%84%E6%8E%A5%E5%8F%A3/</guid><description>&lt;p&gt;随着现在电商等平台如雨后春笋般的发展，在线支付越来越火，各种移动端的支付也是层出不穷，什么微信支付、微博支付等等，其实万变不离其宗，今天大桥就给大家讲解一个Java利用易宝支付在线支付的例子，当然首先要感谢一些传智播客的黎活明老师。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在线支付的第一步，也就是用户在线支付看到的第一个页面，这个页面里主要包含三项：订单号、金额、所选银行，这三个缺一不可。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;%@ page language=&amp;#34;java&amp;#34; import=&amp;#34;java.util.*&amp;#34; pageEncoding=&amp;#34;GBK&amp;#34;%&amp;gt;
&amp;lt;!DOCTYPE HTML PUBLIC &amp;#34;-//W3C//DTD HTML 4.01 Transitional//EN&amp;#34;&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;amp;nbsp;
&amp;lt;title&amp;gt;支付第一步,选择支付银行&amp;lt;/title&amp;gt;
&amp;lt;meta http-equiv=&amp;#34;pragma&amp;#34; content=&amp;#34;no-cache&amp;#34;&amp;gt;
&amp;lt;meta http-equiv=&amp;#34;cache-control&amp;#34; content=&amp;#34;no-cache&amp;#34;&amp;gt;
&amp;lt;meta http-equiv=&amp;#34;expires&amp;#34; content=&amp;#34;0&amp;#34;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;amp;nbsp;
&amp;lt;body&amp;gt;
&amp;lt;table width=&amp;#34;960&amp;#34; border=&amp;#34;0&amp;#34; align=&amp;#34;center&amp;#34;&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td width=&amp;#34;536&amp;#34; valign=&amp;#34;top&amp;#34;&amp;gt;
&amp;lt;form action=&amp;#34;${pageContext.request.contextPath}/servlet/yeepay/PaymentRequestServlet&amp;#34; method=&amp;#34;post&amp;#34;
name=&amp;#34;paymentform&amp;#34;&amp;gt;
&amp;lt;table width=&amp;#34;100%&amp;#34; border=&amp;#34;0&amp;#34;&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td height=&amp;#34;30&amp;#34; colspan=&amp;#34;4&amp;#34;&amp;gt;
&amp;lt;table width=&amp;#34;100%&amp;#34; height=&amp;#34;50&amp;#34; border=&amp;#34;0&amp;#34; cellpadding=&amp;#34;0&amp;#34; cellspacing=&amp;#34;1&amp;#34;
bgcolor=&amp;#34;#A2E0FF&amp;#34;&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td align=&amp;#34;center&amp;#34; bgcolor=&amp;#34;#F7FEFF&amp;#34;&amp;gt;
&amp;lt;h3&amp;gt;订单号：
&amp;lt;INPUT TYPE=&amp;#34;text&amp;#34; NAME=&amp;#34;orderid&amp;#34;&amp;gt;
应付金额：￥&amp;lt;INPUT TYPE=&amp;#34;text&amp;#34; NAME=&amp;#34;amount&amp;#34; size=&amp;#34;6&amp;#34;&amp;gt;元
&amp;lt;/h3&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td colspan=&amp;#34;4&amp;#34;&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td height=&amp;#34;30&amp;#34; colspan=&amp;#34;4&amp;#34; bgcolor=&amp;#34;#F4F8FF&amp;#34;&amp;gt;
&amp;lt;span class=&amp;#34;STYLE3&amp;#34;&amp;gt;请您选择在线支付银行&amp;lt;/span&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td width=&amp;#34;26%&amp;#34; height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;CMBCHINA-NET&amp;#34;&amp;gt;招商银行
&amp;lt;/td&amp;gt;
&amp;lt;td width=&amp;#34;25%&amp;#34; height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;ICBC-NET&amp;#34;&amp;gt;工商银行
&amp;lt;/td&amp;gt;
&amp;lt;td width=&amp;#34;25%&amp;#34; height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;ABC-NET&amp;#34;&amp;gt;农业银行
&amp;lt;/td&amp;gt;
&amp;lt;td width=&amp;#34;24%&amp;#34; height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;CCB-NET&amp;#34;&amp;gt;建设银行
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;CMBC-NET&amp;#34;&amp;gt;中国民生银行总行
&amp;lt;/td&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;CEB-NET&amp;#34;&amp;gt;光大银行
&amp;lt;/td&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;BOCO-NET&amp;#34;&amp;gt;交通银行
&amp;lt;/td&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;SDB-NET&amp;#34;&amp;gt;深圳发展银行
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;BCCB-NET&amp;#34;&amp;gt;北京银行
&amp;lt;/td&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;CIB-NET&amp;#34;&amp;gt;兴业银行
&amp;lt;/td&amp;gt;
&amp;lt;td height=&amp;#34;25&amp;#34;&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;SPDB-NET&amp;#34;&amp;gt;上海浦东发展银行
&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;&amp;lt;INPUT TYPE=&amp;#34;radio&amp;#34; NAME=&amp;#34;pd_FrpId&amp;#34; value=&amp;#34;ECITIC-NET&amp;#34;&amp;gt;中信银行
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td colspan=&amp;#34;4&amp;#34;&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td colspan=&amp;#34;4&amp;#34; align=&amp;#34;center&amp;#34;&amp;gt;
&amp;lt;input type=&amp;#34;submit&amp;#34; value=&amp;#34; 确认支付 &amp;#34;/&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;td colspan=&amp;#34;2&amp;#34; valign=&amp;#34;top&amp;#34;&amp;gt;
&amp;lt;div class=&amp;#34;divts&amp;#34;&amp;gt;
&amp;lt;table width=&amp;#34;400&amp;#34; border=&amp;#34;0&amp;#34; align=&amp;#34;center&amp;#34; cellpadding=&amp;#34;5&amp;#34; cellspacing=&amp;#34;0&amp;#34;&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div id=&amp;#34;blankmessage&amp;#34;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;td width=&amp;#34;290&amp;#34;&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;td width=&amp;#34;120&amp;#34;&amp;gt;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;
取得前台提交的三个参数，并封装易宝支付所需的其他参数，并把这些参数放到Request对象中。
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import cn.bridgeli.utils.ConfigInfo;
import cn.bridgeli.utils.PanymentUtil;
/**
* 发起支付请求
*/
public class PaymentRequestServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding(&amp;#34;GBK&amp;#34;);
String orderid = request.getParameter(&amp;#34;orderid&amp;#34;);//订单号
String amount = request.getParameter(&amp;#34;amount&amp;#34;);//支付金额
String pd_FrpId = request.getParameter(&amp;#34;pd_FrpId&amp;#34;);//选择的支付银行
String p1_MerId = ConfigInfo.getValue(&amp;#34;p1_MerId&amp;#34;);
String keyValue = ConfigInfo.getValue(&amp;#34;keyValue&amp;#34;);
String merchantCallbackURL = ConfigInfo.getValue(&amp;#34;merchantCallbackURL&amp;#34;);
String messageType = &amp;#34;Buy&amp;#34;; // 请求命令,在线支付固定为Buy
String currency = &amp;#34;CNY&amp;#34;; // 货币单位
String productDesc = &amp;#34;&amp;#34;; // 商品描述
String productCat = &amp;#34;&amp;#34;; // 商品种类
String productId = &amp;#34;&amp;#34;; // 商品ID
String addressFlag = &amp;#34;0&amp;#34;; // 需要填写送货信息 0：不需要 1:需要
String sMctProperties = &amp;#34;&amp;#34;; // 商家扩展信息
String pr_NeedResponse = &amp;#34;0&amp;#34;; // 应答机制
String md5hmac = PanymentUtil.buildHmac(messageType, p1_MerId, orderid, amount, currency,
productId, productCat, productDesc, merchantCallbackURL, addressFlag, sMctProperties,
pd_FrpId, pr_NeedResponse, keyValue);
request.setAttribute(&amp;#34;messageType&amp;#34;, messageType);
request.setAttribute(&amp;#34;merchantID&amp;#34;, p1_MerId);
request.setAttribute(&amp;#34;orderId&amp;#34;, orderid);
request.setAttribute(&amp;#34;amount&amp;#34;, amount);
request.setAttribute(&amp;#34;currency&amp;#34;, currency);
request.setAttribute(&amp;#34;productId&amp;#34;, productId);
request.setAttribute(&amp;#34;productCat&amp;#34;, productCat);
request.setAttribute(&amp;#34;productDesc&amp;#34;, productDesc);
request.setAttribute(&amp;#34;merchantCallbackURL&amp;#34;, merchantCallbackURL);
request.setAttribute(&amp;#34;addressFlag&amp;#34;, addressFlag);
request.setAttribute(&amp;#34;sMctProperties&amp;#34;, sMctProperties);
request.setAttribute(&amp;#34;frpId&amp;#34;, pd_FrpId);
request.setAttribute(&amp;#34;pr_NeedResponse&amp;#34;, pr_NeedResponse);
request.setAttribute(&amp;#34;hmac&amp;#34;, md5hmac);
request.getRequestDispatcher(&amp;#34;/WEB-INF/page/connection.jsp&amp;#34;).forward(request, response);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;
因为易宝支付所需的数据都是通过表单提交的，所以取得上一步封装的各个参数，通过一个表单提交到易宝支付提供的接口。所以表单的action应该是易宝支付的接口。
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;%@ page language=&amp;#34;java&amp;#34; pageEncoding=&amp;#34;GBK&amp;#34;%&amp;gt;
&amp;lt;!DOCTYPE HTML PUBLIC &amp;#34;-//W3C//DTD HTML 4.01 Transitional//EN&amp;#34;&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;发起支付请求&amp;lt;/title&amp;gt;
&amp;amp;nbsp;
&amp;lt;meta http-equiv=&amp;#34;pragma&amp;#34; content=&amp;#34;no-cache&amp;#34;&amp;gt;
&amp;lt;meta http-equiv=&amp;#34;cache-control&amp;#34; content=&amp;#34;no-cache&amp;#34;&amp;gt;
&amp;lt;meta http-equiv=&amp;#34;expires&amp;#34; content=&amp;#34;0&amp;#34;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;amp;nbsp;
&amp;lt;body onload=&amp;#34;javascript:document.forms[0].submit()&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; http://tech.yeepay.com:8080/robot/debug.action &amp;amp;#8211;&amp;gt;
&amp;lt;form name=&amp;#34;yeepay&amp;#34; action=&amp;#34;https://www.yeepay.com/app-merchant-proxy/node&amp;#34; method=&amp;amp;#8217;post&amp;amp;#8217;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p0_Cmd&amp;amp;#8217; value=&amp;#34;${messageType}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 请求命令,在线支付固定为Buy &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p1_MerId&amp;amp;#8217; value=&amp;#34;${merchantID}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 商家ID &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;#34;hidden&amp;#34; name=&amp;#34;p2_Order&amp;#34; value=&amp;#34;${orderId}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 商家的交易定单号 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p3_Amt&amp;amp;#8217; value=&amp;#34;${amount}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 订单金额 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p4_Cur&amp;amp;#8217; value=&amp;#34;${currency}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 货币单位 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p5_Pid&amp;amp;#8217; value=&amp;#34;${productId}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 商品ID &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p6_Pcat&amp;amp;#8217; value=&amp;#34;${productCat}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 商品种类 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p7_Pdesc&amp;amp;#8217; value=&amp;#34;${productDesc}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 商品描述 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p8_Url&amp;amp;#8217; value=&amp;#34;${merchantCallbackURL}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 交易结果通知地址 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;p9_SAF&amp;amp;#8217; value=&amp;#34;${addressFlag}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 需要填写送货信息 0：不需要 1:需要 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;pa_MP&amp;amp;#8217; value=&amp;#34;${sMctProperties}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 商家扩展信息 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;pd_FrpId&amp;amp;#8217; value=&amp;#34;${frpId}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; 银行ID &amp;amp;#8211;&amp;gt;
&amp;lt;!&amp;amp;#8211; 应答机制 为“1”: 需要应答机制;为“0”: 不需要应答机制 &amp;amp;#8211;&amp;gt;
&amp;lt;input type=&amp;#34;hidden&amp;#34; name=&amp;#34;pr_NeedResponse&amp;#34; value=&amp;#34;0&amp;#34;&amp;gt;
&amp;lt;input type=&amp;amp;#8217;hidden&amp;amp;#8217; name=&amp;amp;#8217;hmac&amp;amp;#8217; value=&amp;#34;${hmac}&amp;#34;&amp;gt;
&amp;lt;!&amp;amp;#8211; MD5-hmac验证码 &amp;amp;#8211;&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;
编写易宝支付处理上一步处理数据完成后所返回的后台处理类（这个处理类必须在公网上，即使是测试程序，即可以访问的），并根据易宝支付所返回的数据，并完成一下几件事（也就是接收易宝支付处理结果的类）：
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(1). 判断支付的结果&lt;/p&gt;</description></item><item><title>动态代理模拟Spring的AOP</title><link>https://bridgeli.cn/posts/2014-09-12-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E6%A8%A1%E6%8B%9Fspring%E7%9A%84aop/</link><pubDate>Fri, 12 Sep 2014 13:21:07 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-12-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E6%A8%A1%E6%8B%9Fspring%E7%9A%84aop/</guid><description>&lt;p&gt;这两天研究了一下Java的动态代理，自己闲着无聊，用动态代理模拟了一下Spring的AOP，代码如下，当然真正的Spring是直接操作二进制文件，很复杂，有兴趣的可以自己研究下。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.aop;
public interface UserService {
void addUser();
}
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.aop;
public class UserServiceImpl implements UserService {
public void addUser() {
System.out.println(&amp;#34;User add&amp;amp;#8230;&amp;#34;);
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserServiceProxy implements InvocationHandler {
private UserService userService;
public UserServiceProxy(UserService userService) {
this.userService = userService;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(&amp;#34;User add start&amp;amp;#8230;&amp;#34;);
Object object = method.invoke(userService, args);
System.out.println(&amp;#34;User add end&amp;amp;#8230;&amp;#34;);
return object;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import org.junit.Test;
public class UserServiceTest {
@Test
public void testAddUser() {
UserService userService = new UserServiceImpl();
InvocationHandler invocationHandler = new UserServiceProxy(userService);
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), invocationHandler);
userServiceProxy.addUser();
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>JXL解析Excel常用方法</title><link>https://bridgeli.cn/posts/2014-09-06-jxl%E8%A7%A3%E6%9E%90excel%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95/</link><pubDate>Sat, 06 Sep 2014 12:06:59 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-09-06-jxl%E8%A7%A3%E6%9E%90excel%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;目前在市场上有两个最出名的第三方的JAR包：JXL和POI，他们在处理Excel上都有着不俗的表现，但他们有着细微的差别，主要差别如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JXL在处理数据方面速度比较快，而POI相对较慢，当数据较少时，其实并不明显；&lt;/li&gt;
&lt;li&gt;JXL对图片的支持更好，而POI对图片的支持稍弱，但对图片也是支持的；&lt;/li&gt;
&lt;li&gt;JXL对公示的支持能力稍弱，对于复杂的公式显得无能为力，而POI则做得很好，所以如果做财务软件的话，请慎重选择，建议POI，否则很有可能会引起一些意想不到的问题；&lt;/li&gt;
&lt;li&gt;JXL的代码简单，也易于理解，下面是JXL处理Excel的常用方法（将来有可能的话，我会把POI的也贴出来，供大家参考）：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import jxl.CellView;
import jxl.Sheet;
import jxl.Workbook;
import jxl.format.Alignment;
import jxl.format.Border;
import jxl.format.BorderLineStyle;
import jxl.format.CellFormat;
import jxl.format.Colour;
import jxl.format.VerticalAlignment;
import jxl.write.Blank;
import jxl.write.Boolean;
import jxl.write.DateFormat;
import jxl.write.DateTime;
import jxl.write.Formula;
import jxl.write.Label;
import jxl.write.Number;
import jxl.write.NumberFormat;
import jxl.write.WritableCell;
import jxl.write.WritableCellFormat;
import jxl.write.WritableFont;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import jxl.write.biff.RowsExceededException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JxlHelper {
private static final Logger LOG = LoggerFactory.getLogger(JxlHelper.class);
private WritableSheet sheet;
public JxlHelper(WritableSheet sheet) {
this.sheet = sheet;
}
public JxlHelper() {
}
public static WritableWorkbook createWritableWorkbook(String filePath) {
WritableWorkbook wwb = null;
try {
wwb = Workbook.createWorkbook(new File(filePath));
} catch (IOException e) {
LOG.error(&amp;#34;Create Writable Workbook Failed: IOException&amp;#34;, e);
}
return wwb;
}
public static WritableWorkbook createWritableWorkbook(String filePath, Workbook wb) {
WritableWorkbook wwb = null;
try {
wwb = Workbook.createWorkbook(new File(filePath), wb);
} catch (IOException e) {
LOG.error(&amp;#34;Create Writable Workbook Failed: IOException&amp;#34;, e);
}
return wwb;
}
public static Workbook getWorkbook(String filePath) throws Exception {
Workbook wb = Workbook.getWorkbook(new File(filePath));
return wb;
}
public void createSheet(WritableWorkbook wwb, String sheetName, int index) {
try {
wwb.createSheet(sheetName, index);
} catch (Exception e) {
LOG.error(&amp;#34;Create Sheet Failed&amp;#34;, e);
}
}
public void copySheet(WritableWorkbook wwb, int copySheetIndex, String pasteSheetName, int pasteSheetIndex) {
try {
wwb.copySheet(copySheetIndex, pasteSheetName, pasteSheetIndex);
} catch (Exception e) {
LOG.error(&amp;#34;Copy Sheet Failed&amp;#34;, e);
}
}
public void copySheet(WritableWorkbook wwb, String copySheetName, String pasteSheetName, int pasteSheetIndex) {
try {
wwb.copySheet(copySheetName, pasteSheetName, pasteSheetIndex);
} catch (Exception e) {
LOG.error(&amp;#34;Copy Sheet Failed&amp;#34;, e);
}
}
public void removeSheet(WritableWorkbook wwb, int index) {
try {
wwb.removeSheet(index);
} catch (Exception e) {
LOG.error(&amp;#34;Remove Sheet Failed&amp;#34;, e);
}
}
public Sheet[] getSheets(WritableWorkbook wwb) {
return wwb.getSheets();
}
public void setString(int column, int row, String value, int fontSize) throws WriteException {
if (value != null) {
sheet.addCell(new Label(column, row, value));
} else {
sheet.addCell(new Blank(column, row));
}
setCellStyle(column, row, fontSize);
}
public void setString(int column, int row, String value, CellFormat st) throws WriteException {
if (value != null) {
sheet.addCell(new Label(column, row, value, st));
} else {
sheet.addCell(new Blank(column, row, st));
}
}
public void setNumber(int column, int row, double value, CellFormat st) throws WriteException {
sheet.addCell(new Number(column, row, value, st));
}
public void setFormatNumber(int column, int row, double value, int fontSize, LocationEnum location, String format)
throws WriteException {
Number numCell = new Number(column, row, value);
sheet.addCell(numCell);
WritableFont font = new WritableFont(WritableFont.createFont(SystemConstant.INVOICE_FONT), fontSize,
WritableFont.NO_BOLD);
NumberFormat numberFormat = null;
WritableCellFormat cellFormat = null;
if (!StringUtil.isNullOrEmpty(format)) {
numberFormat = new NumberFormat(format);
cellFormat = new WritableCellFormat(font, numberFormat);
} else {
cellFormat = new WritableCellFormat(font);
}
try {
cellFormat.setBorder(Border.ALL, BorderLineStyle.THIN);
if (location.equals(LocationEnum.LEFT)) {
cellFormat.setBorder(Border.LEFT, BorderLineStyle.THICK);
} else if (location.equals(LocationEnum.RIGHT)) {
cellFormat.setBorder(Border.RIGHT, BorderLineStyle.THICK);
}
cellFormat.setAlignment(Alignment.RIGHT);
cellFormat.setVerticalAlignment(VerticalAlignment.CENTRE);
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
} catch (WriteException e) {
LOG.error(&amp;#34;Set Format Number Failed: WriteException&amp;#34;, e);
}
}
public void setBoolean(int column, int row, boolean value) throws WriteException {
Boolean boolCell = new Boolean(column, row, value);
sheet.addCell(boolCell);
}
public void setFormatDateTime(int column, int row, Date value, String format) throws WriteException {
if (value != null) {
WritableCell writableCell = sheet.getWritableCell(column, row);
CellFormat cf = writableCell.getCellFormat();
DateTime cell = new DateTime(column, row, value);
DateFormat dateFormat = new DateFormat(format);
WritableCellFormat wcf = new WritableCellFormat(dateFormat);
wcf.setFont(new WritableFont(WritableFont.ARIAL));
wcf.setBorder(Border.LEFT, cf.getBorderLine(Border.LEFT));
wcf.setBorder(Border.RIGHT, cf.getBorderLine(Border.RIGHT));
wcf.setAlignment(Alignment.CENTRE);
cell.setCellFormat(wcf);
sheet.addCell(cell);
}
}
public void setCellStype2(int column, int row) throws WriteException {
WritableFont font = new WritableFont(WritableFont.ARIAL, 10, WritableFont.BOLD);
WritableCellFormat cellFormat = new WritableCellFormat(font);
cellFormat.setBorder(Border.ALL, BorderLineStyle.THIN);
cellFormat.setAlignment(Alignment.CENTRE);
cellFormat.setVerticalAlignment(VerticalAlignment.CENTRE);
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
}
public void setFormatDateTime(int column, int row, Date value, CellFormat st) throws WriteException {
if (value != null) {
sheet.addCell(new DateTime(column, row, value, st));
} else {
sheet.addCell(new Blank(column, row, st));
}
}
public void setFormula(int column, int row, String formula) {
try {
Formula f = new Formula(column, row, formula);
sheet.addCell(f);
WritableCellFormat cellFormat = new WritableCellFormat();
cellFormat.setAlignment(Alignment.LEFT);
cellFormat.setVerticalAlignment(VerticalAlignment.CENTRE);
cellFormat.setBorder(Border.ALL, BorderLineStyle.THIN);
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
} catch (Exception e) {
LOG.error(&amp;#34;Set Formula Failed&amp;#34;, e);
}
}
public void setFormula(int column, int row, String formula, CellFormat st) {
try {
sheet.addCell(new Formula(column, row, formula, st));
} catch (Exception e) {
LOG.error(&amp;#34;Set Formula Failed&amp;#34;, e);
}
}
public void setCellStyle(int column, int row, int fontSize) {
WritableFont font = new WritableFont(WritableFont.ARIAL, fontSize, WritableFont.BOLD);
WritableCellFormat cellFormat = new WritableCellFormat(font);
try {
cellFormat.setAlignment(Alignment.CENTRE);
cellFormat.setVerticalAlignment(VerticalAlignment.CENTRE);
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
} catch (WriteException e) {
LOG.error(&amp;#34;Set Cell Style Failed: WriteException&amp;#34;, e);
}
}
public void setSumCellBorder(int column, int row, LocationEnum location, String format) {
NumberFormat nf = null;
if (StringUtil.isNotNull(format)) {
nf = new NumberFormat(format);
}
WritableCellFormat cellFormat = new WritableCellFormat(nf);
try {
cellFormat.setBorder(Border.ALL, jxl.format.BorderLineStyle.THIN);
cellFormat.setBorder(Border.TOP, jxl.format.BorderLineStyle.DOUBLE);
if (location.equals(LocationEnum.RIGHT)) {
cellFormat.setBorder(Border.RIGHT, jxl.format.BorderLineStyle.THICK);
}
cellFormat.setBorder(Border.BOTTOM, jxl.format.BorderLineStyle.THICK);
cellFormat.setAlignment(Alignment.RIGHT);
cellFormat.setVerticalAlignment(jxl.format.VerticalAlignment.CENTRE);
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
} catch (WriteException e) {
LOG.error(&amp;#34;Set Sum Cell Border Failed: WriteException&amp;#34;, e);
}
}
public void setCellStyle(int column, int row) {
WritableFont font = new WritableFont(WritableFont.ARIAL);
WritableCellFormat cellFormat = new WritableCellFormat(font);
try {
cellFormat.setAlignment(Alignment.LEFT);
cellFormat.setVerticalAlignment(VerticalAlignment.TOP);
} catch (WriteException e) {
LOG.error(&amp;#34;Set Cell Style Failed: WriteException&amp;#34;, e);
}
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
}
public void hiddenRow(int row) throws RowsExceededException {
// sheet.setRowView(row, true);
CellView cv = new CellView();
cv.setHidden(true);
sheet.setRowView(row, cv);
}
public void removeRow(int row) {
sheet.removeRow(row);
}
public void insertRow(int row, int height) {
sheet.insertRow(row);
try {
sheet.setRowView(row, height);
} catch (RowsExceededException e) {
LOG.error(&amp;#34;Insert Row Failed: RowsExceededException&amp;#34;, e);
}
}
public void mergeCells(int columnStart, int rowsStart, int columnEnd, int rowsEnd) {
try {
sheet.mergeCells(columnStart, rowsStart, columnEnd, rowsEnd);
} catch (RowsExceededException e) {
LOG.error(&amp;#34;Merge Cells Failed: RowsExceededException&amp;#34;, e);
} catch (WriteException e) {
LOG.error(&amp;#34;Merge Cells Failed: WriteException&amp;#34;, e);
}
}
public static void close(WritableWorkbook wwb, Workbook wb) {
try {
if (null != wwb) {
wwb.close();
}
if (null != wb) {
wb.close();
}
} catch (WriteException e) {
LOG.error(&amp;#34;Close WritableWorkbook or Workbook Failed: WriteException&amp;#34;, e);
} catch (IOException e) {
LOG.error(&amp;#34;Close WritableWorkbook or Workbook Failed: IOException&amp;#34;, e);
}
}
public void setBackgroundColour(int column, int row) {
try {
WritableCellFormat cellFormat = new WritableCellFormat();
cellFormat.setBackground(Colour.PALE_BLUE);
cellFormat.setAlignment(Alignment.LEFT);
WritableCell writableCell = sheet.getWritableCell(column, row);
writableCell.setCellFormat(cellFormat);
} catch (WriteException e) {
LOG.error(&amp;#34;Set Background Colour Failed: WriteException&amp;#34;, e);
}
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>反射机制入门</title><link>https://bridgeli.cn/posts/2014-08-31-%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E5%85%A5%E9%97%A8/</link><pubDate>Sun, 31 Aug 2014 14:12:35 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-31-%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E5%85%A5%E9%97%A8/</guid><description>&lt;p&gt;Java程序允许在执行期间获取一个已知名称的类的详细内部构造，这种机制被称为“反射”，反射在struts2、Spring、hibernate等Java常见框架中有着许多经典运用，下面是Java反射机制的入门代码，看了这些代码，相信读者应该对Java的反射机制会有一个入门级的了解，再看那些框架源码时会省力不少。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.TypeVariable;
public class TestReflection {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
String str = &amp;#34;T&amp;#34;;
Class c = Class.forName(str);
Object o = c.newInstance();
T t = (T) c.newInstance();
t.m();
Method[] methods = c.getMethods();
for (Method method : methods) {
if (&amp;#34;m&amp;#34;.equals(method.getName())) {
method.invoke(o);
}
if (&amp;#34;getS&amp;#34;.equals(method.getName())) {
TypeVariable&amp;lt;Method&amp;gt;[] typeParameters = method.getTypeParameters();
Class returnType = method.getReturnType();
}
}
}
}
class T {
int i;
String s;
static {
System.out.println(&amp;#34;T loaded&amp;amp;#8230;&amp;#34;);
}
public T() {
System.out.println(&amp;#34;T Constructed&amp;amp;#8230;&amp;#34;);
}
public String getS() {
return s;
}
public void setI(int i) {
this.i = i;
}
public void m() {
System.out.println(&amp;#34;m invoked&amp;amp;#8230;&amp;#34;);
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>JSP自定义标签</title><link>https://bridgeli.cn/posts/2014-08-30-jsp%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%87%E7%AD%BE/</link><pubDate>Sat, 30 Aug 2014 06:10:16 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-30-jsp%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%87%E7%AD%BE/</guid><description>&lt;p&gt;虽然html很好，尤其是html5越来越火，但仍有很多网站是用JSP做的，JSP里面虽然有很多标签，但我们是否可以自己定义自己的呢？当然可以，参考代码如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写自己的taglib类，并重写里面的方法&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
public class DropDownBoxTaglib extends TagSupport {
private static final long serialVersionUID = 1L;
@Override
public int doStartTag() throws JspTagException {
return SKIP_BODY;
}
@Override
public int doEndTag() throws JspTagException {
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(&amp;#34;applicationContext.xml&amp;#34;);
RequestTypeService requestTypeService = (RequestTypeService) applicationContext.getBean(&amp;#34;requestTypeService&amp;#34;);
List&amp;lt;RequestType&amp;gt; requestTypes = requestTypeService.query();
request.setAttribute(&amp;#34;REQUESTTYPES&amp;#34;, requestTypes);
try {
pageContext.include(&amp;#34;/component/request_type_select.jsp&amp;#34;);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return EVAL_PAGE;
}
@Override
public void release() {
super.release();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;对应的tld文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;taglib xmlns=&amp;#34;http://java.sun.com/xml/ns/j2ee&amp;#34; xmlns:xsi=&amp;#34;http://www.w3.org/2001/XMLSchema-instance&amp;#34;
xsi:schemaLocation=&amp;#34;http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd&amp;#34;
version=&amp;#34;2.0&amp;#34;&amp;gt;
&amp;lt;tlib-version&amp;gt;1.0&amp;lt;/tlib-version&amp;gt;
&amp;lt;jsp-version&amp;gt;1.1&amp;lt;/jsp-version&amp;gt;
&amp;lt;short-name&amp;gt;aug&amp;lt;/short-name&amp;gt;
&amp;lt;tag&amp;gt;
&amp;lt;name&amp;gt;select&amp;lt;/name&amp;gt;
&amp;lt;tag-class&amp;gt;cn.bridgeli.DropDownBoxTaglib&amp;lt;/tag-class&amp;gt;
&amp;lt;body-content&amp;gt;empty&amp;lt;/body-content&amp;gt;
&amp;lt;!&amp;amp;#8211;
&amp;lt;attribute&amp;gt;
&amp;lt;name&amp;gt;formatKey&amp;lt;/name&amp;gt;
&amp;lt;required&amp;gt;false&amp;lt;/required&amp;gt;
&amp;lt;rtexprvalue&amp;gt;true&amp;lt;/rtexprvalue&amp;gt;
&amp;lt;/attribute&amp;gt;
&amp;amp;#8211;&amp;gt;
&amp;lt;/tag&amp;gt;
&amp;lt;/taglib&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;具体哪个文件使用该taglib&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;%@ page language=&amp;#34;java&amp;#34; contentType=&amp;#34;text/html; charset=UTF-8&amp;#34;
pageEncoding=&amp;#34;UTF-8&amp;#34;%&amp;gt;
&amp;lt;%@ taglib uri=&amp;#34;http://java.sun.com/jsp/jstl/core&amp;#34; prefix=&amp;#34;c&amp;#34; %&amp;gt;
&amp;lt;%
List&amp;lt;RequestType&amp;gt;requestTypes = (List
&amp;lt;RequestType&amp;gt;)request.getAttribute(&amp;#34;REQUESTTYPES&amp;#34;);
%&amp;gt;
&amp;lt;select name=&amp;#34;requestTypeId&amp;#34; id=&amp;#34;requestTypeId&amp;#34; style=&amp;#34;width: 608px;&amp;#34;&amp;gt;
&amp;lt;option&amp;gt;&amp;amp;#8211;Select a request type Please&amp;amp;#8211;&amp;lt;/option&amp;gt;
&amp;lt;%
for(int i = 0; i&amp;lt; requestTypes.size(); i++) {
RequestType requestType = requestTypes.get(i);
%&amp;gt;
&amp;lt;option value=
&amp;lt;%= requestType.getId() %&amp;gt;&amp;gt;&amp;lt;%= requestType.getName() %&amp;gt;
&amp;lt;/option&amp;gt;
&amp;lt;%
}
%&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>软件分层的一般方法</title><link>https://bridgeli.cn/posts/2014-08-30-%E8%BD%AF%E4%BB%B6%E5%88%86%E5%B1%82%E7%9A%84%E4%B8%80%E8%88%AC%E6%96%B9%E6%B3%95/</link><pubDate>Sat, 30 Aug 2014 03:07:17 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-30-%E8%BD%AF%E4%BB%B6%E5%88%86%E5%B1%82%E7%9A%84%E4%B8%80%E8%88%AC%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;1. 软件设计的目的：高内聚、低耦合，为了达到这一目的：（1）. 模块化； （2）. 分层&lt;/p&gt;
&lt;p&gt;软件分层依据：（1）. 逻辑分层；（2）. 物理分层&lt;/p&gt;
&lt;p&gt;命名空间：（1）. 类：属性和方法；（2）. 包：其实就是一个文件夹&lt;/p&gt;
&lt;p&gt;包名命名规范：域名倒写+项目名+逻辑或模块，例如：cn.bridgeli.weixin.service&lt;/p&gt;
&lt;p&gt;DB Web的死四层结构：view、servlet、service、dao&lt;/p&gt;
&lt;p&gt;servlet向service传递DTO或者VO，service向DAO传递model，dao直接保存数据到数据库&lt;/p&gt;
&lt;p&gt;类的命名规范：实体名+包的最后一层，但model除外&lt;/p&gt;
&lt;p&gt;例如：UserService、User&lt;/p&gt;
&lt;p&gt;2. 哪些代码写到哪一层&lt;/p&gt;
&lt;p&gt;（1）. 一个表对应一个model类&lt;/p&gt;
&lt;p&gt;（2）. 一个表对应一个dao（四个方法）&lt;/p&gt;
&lt;p&gt;Create、update、getById、delete（可能没有，markfordelete，但依然会这么命名），其他方法一般都以：create、update、delete、get（返回一个对象）、save、find或者query（返回一个list，统一用一个就行）等关键字打头&lt;/p&gt;
&lt;p&gt;（3）. Service由界面操作的都有service，但log没有service层&lt;/p&gt;
&lt;p&gt;（4）servlet接受用户请求&lt;/p&gt;
&lt;p&gt;在开发中，不能跨层调用，不能调上层，只能调下层或者本层的方法&lt;/p&gt;
&lt;p&gt;（5）. Util工具包，所有方法全部是static的，只放一些常用工具，例如StringUtil，检验字符串是否为空、一个字符串是否包含另一个字符串；jdbc中的DBUtil，getConn、getPstmt、getRs以及close等方法&lt;/p&gt;</description></item><item><title>日志的配置</title><link>https://bridgeli.cn/posts/2014-08-29-%E6%97%A5%E5%BF%97%E7%9A%84%E9%85%8D%E7%BD%AE/</link><pubDate>Fri, 29 Aug 2014 13:57:41 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-29-%E6%97%A5%E5%BF%97%E7%9A%84%E9%85%8D%E7%BD%AE/</guid><description>&lt;p&gt;在系统开发中，尤其是上线后，没有日志那绝对是一件不可想象的事，但日志的配置却很简单，一般配置后只要做少量的修改，几乎可以永远到处都可以用了，下面给出日志配置的一般方法。&lt;/p&gt;
&lt;p&gt;注：该配置的Jar包为：log4j、slf4j-api、slf-log4j，即使用slf日志接口，log4j的实现（当然你也可以使用其他的实现，例如hibernate自带的slf的实现）这一目前为止的最佳实践。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
log4j.rootCategory=INFO, stdout, logfile, errorLog
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2}:%L &amp;amp;#8211; %m%n
#log4j.category.org.springframework.beans.factory=info
log4j.appender.consoleAppender.layout.ConversionPattern =ProcessDefinitionId=%X{mdcProcessDefinitionID}
executionId=%X{mdcExecutionId} mdcProcessInstanceID=%X{mdcProcessInstanceID} mdcBusinessKey=%X{mdcBusinessKey} %m%n&amp;#34;
log4j.appender.logfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.logfile.file=C:/invoice/log/log.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.DatePattern=&amp;amp;#8217;.&amp;amp;#8217;yyyy-MM-dd
#log4j.appender.logfile.layout.ConversionPattern=[%d %6p at %C.%M(%F:%L)] %m%n
log4j.appender.logfile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l  %m%n
log4j.appender.logfile.Threshold=INFO
log4j.appender.errorLog=org.apache.log4j.DailyRollingFileAppender
log4j.appender.errorLog.file=C:/invoice/log/error.log
log4j.appender.errorLog.layout=org.apache.log4j.PatternLayout
log4j.appender.errorLog.DatePattern=&amp;amp;#8217;.&amp;amp;#8217;yyyy-MM-dd
log4j.appender.errorLog.layout.ConversionPattern=[%d %6p at %C.%M(%F:%L)] %m%n
log4j.appender.errorLog.Threshold=ERROR
# SQL:
#log4j.logger.com.ibatis=DEBUG
#log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.cache.CacheModel=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientImpl=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.builder.xml.SqlMapParser=DEBUG
#log4j.logger.com.ibatis.common.util.StopWatch=DEBUG
#log4j.logger.java.sql.Connection=DEBUG
#log4j.logger.java.sql.Statement=DEBUG
#log4j.logger.java.sql.PreparedStatement=DEBUG
#log4j.logger.java.sql.ResultSet=DEBUG
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注：这是一最简单的一种配置，还有很多其他的配置项可以灵活配置，例如打印hibernate的SQL语句，而且还可以配置当产生error级别的日志时，自动发送邮件到指定邮箱，具体请参考：http://futeng.iteye.com/blog/2109231&lt;/p&gt;</description></item><item><title>简单邮件的解析</title><link>https://bridgeli.cn/posts/2014-08-29-%E7%AE%80%E5%8D%95%E9%82%AE%E4%BB%B6%E7%9A%84%E8%A7%A3%E6%9E%90/</link><pubDate>Fri, 29 Aug 2014 03:21:37 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-29-%E7%AE%80%E5%8D%95%E9%82%AE%E4%BB%B6%E7%9A%84%E8%A7%A3%E6%9E%90/</guid><description>&lt;p&gt;昨天讲了邮件的发送（这是一个个人笔记，如果有人要参考的话，估计需要做大量的修改才行，但整体逻辑是不会错），既然有发送，肯定会有邮件的解析，那么今天大桥就再写一个例子程序，关于邮件怎么解析。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Service(&amp;#34;parseMailService&amp;#34;)
public class ParseMailServiceImpl {
private static final Logger LOG = LoggerFactory.getLogger(ParseMailServiceImpl.class);
private static final String[] flagOfMailEnds = {&amp;#34;&amp;lt;DIV&amp;gt;&amp;lt;BR&amp;gt;&amp;lt;/DIV&amp;gt;&amp;#34;, &amp;#34;From:&amp;lt;&amp;#34;, &amp;#34;From: &amp;lt;&amp;#34;, &amp;#34;Sent: &amp;#34;, &amp;#34;Kind regards,&amp;#34;,
&amp;#34;Kind Regards,&amp;#34;, &amp;#34;Best regards,&amp;#34;, &amp;#34;Best Regards,&amp;#34;, &amp;#34;Kind regards,&amp;#34;, &amp;#34;Regards,&amp;#34;, &amp;#34;Thanks&amp;#34;,
&amp;#34;&amp;amp;#8212;&amp;amp;#8211; Original Message &amp;amp;#8212;&amp;amp;#8211;&amp;#34;, &amp;#34;&amp;gt; &amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;-&amp;#34;, &amp;#34;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;amp;#8212;&amp;#34;};
private static final String[] flagOfMailStarts = {&amp;#34;Hi,&amp;#34;, &amp;#34;&amp;lt;/HEAD&amp;gt;&amp;#34;};
private static final String[] regExHtmls = {&amp;#34;&amp;lt;[^&amp;gt;]+&amp;gt;&amp;#34;, &amp;#34;&amp;lt;[^&amp;gt;]+&amp;#34;};
@Autowired
private MailServerService mailServerService;
public void parseMail() throws Exception {
LOG.info(&amp;#34;==Start parse Mail==&amp;#34;);
Session session = mailServerService.getSession();
Store store = session.getStore(“pop3”);
store.connect(MAIL_SERVER_HOST, MAIL_ADDRESS, MAIL_SERVER_PASSWORD);
Folder folder = store.getFolder(&amp;#34;INBOX&amp;#34;);
folder.open(Folder.READ_WRITE);
Message[] messages = folder.getMessages();
int mailNo = Integer.parseInt((String) execution.getVariable(SystemConstant.MAIL_NO));
Message message = messages[mailNo];
Map&amp;lt;String, String&amp;gt; contents = parseMessage(message);
message.setFlag(Flags.Flag.DELETED, true);
// message.saveChanges();
mailServerService.closeConn(folder, store);
LOG.info(&amp;#34;==Mail parse success, this mail from: &amp;#34; + contents.get(&amp;#34;user&amp;#34;) + &amp;#34;==&amp;#34;);
}
private String splitContentBySpecialCharacter(String context) {
// Remove mail bottom
for (String flagOfMailEnd : flagOfMailEnds) {
int endIndex = context.indexOf(flagOfMailEnd);
if (endIndex &amp;gt; -1) {
context = context.substring(0, endIndex);
}
}
// Remove mail top
for (String flagOfMailStart : flagOfMailStarts) {
int startIndex = context.indexOf(flagOfMailStart);
if (startIndex &amp;gt; -1) {
context = context.substring(startIndex + flagOfMailStart.length());
}
}
// Filter the HTML tags
for (String regExHtml : regExHtmls) {
Pattern p_html = Pattern.compile(regExHtml, Pattern.CASE_INSENSITIVE);
Matcher m_html = p_html.matcher(context);
context = m_html.replaceAll(&amp;#34;&amp;#34;);
}
context.replaceAll(&amp;#34; &amp;#34;, &amp;#34;&amp;#34;);
// Filter &amp;#34;rn&amp;#34;
Pattern CRLF1 = Pattern.compile(&amp;#34;(rn|r|n|nr){3,}&amp;#34;);
Matcher m1 = CRLF1.matcher(context);
context = m1.replaceAll(&amp;#34;&amp;#34;);
Pattern CRLF = Pattern.compile(&amp;#34;(rn|r|n|nr)&amp;#34;);
Matcher m = CRLF.matcher(context);
context = m.replaceAll(&amp;#34; &amp;#34;);
return context;
}
public Map&amp;lt;String, String&amp;gt; parseMessage(Message message) throws MessagingException, IOException {
Map&amp;lt;String, String&amp;gt; contents = new HashMap&amp;lt;String, String&amp;gt;();
StringBuffer content = new StringBuffer(300);
content = getMailTextContent(message, content);
contents.put(&amp;#34;comment&amp;#34;, splitContentBySpecialCharacter(content.toString()));
return contents;
}
public StringBuffer getMailTextContent(Part part, StringBuffer content) throws MessagingException, IOException {
boolean isContainTextAttach = part.getContentType().indexOf(&amp;#34;name&amp;#34;) &amp;gt; 0;
if (part.isMimeType(&amp;#34;text/*&amp;#34;) &amp;amp;&amp;amp; !isContainTextAttach) {
if (content.length() &amp;gt; 0) {
content.setLength(0);
}
content.append(part.getContent().toString());
} else if (part.isMimeType(&amp;#34;message/rfc822&amp;#34;)) {
getMailTextContent((Part) part.getContent(), content);
} else if (part.isMimeType(&amp;#34;multipart/*&amp;#34;)) {
Multipart multipart = (Multipart) part.getContent();
int partCount = multipart.getCount();
for (int i = 0; i &amp;lt; partCount; i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
getMailTextContent(bodyPart, content);
}
}
return content;
}
}
@Service(&amp;#34;mailServerService&amp;#34;)
public class MailServerServiceImpl implements MailServerService {
private static final Logger LOG = LoggerFactory.getLogger(MailServerServiceImpl.class);
@Override
public Session getSession() {
Properties props = System.getProperties();
props.put(&amp;#34;mail.smtp.port&amp;#34;, 25);
Session session = Session.getDefaultInstance(props);
return session;
}
@Override
public void closeConn(Folder folder, Store store) {
try {
if (null != folder) {
folder.close(true);
}
if (null != store) {
store.close();
}
} catch (MessagingException e) {
LOG.error(&amp;#34;Close connection fail with mail server&amp;#34;, e);
}
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>如何利用模板发送邮件</title><link>https://bridgeli.cn/posts/2014-08-28-%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%E6%A8%A1%E6%9D%BF%E5%8F%91%E9%80%81%E9%82%AE%E4%BB%B6/</link><pubDate>Thu, 28 Aug 2014 11:39:26 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-28-%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%E6%A8%A1%E6%9D%BF%E5%8F%91%E9%80%81%E9%82%AE%E4%BB%B6/</guid><description>&lt;p&gt;目前web应用很多时候都需要像用户发送邮件，尤其是在我们注册的时候，其实这些邮件很多时候内容都是类似的，仅仅是一些有户名等不同，那么我们是否可以利用一个模板呢，模板里面有一些占位符，当我们要发送邮件的时候，仅仅把这些占位符做一替换就行了呢？答案肯定是可以的，请看下面的例子程序：&lt;/p&gt;
&lt;p&gt;1. 邮件模板mailTemplate.xml&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;
&amp;lt;email&amp;gt;
&amp;lt;ProjectMail&amp;gt;
&amp;lt;subject id=&amp;#34;ProjectSubject&amp;#34;&amp;gt; &amp;lt;![CDATA[Hello%toUsername%]]&amp;gt;&amp;lt;/subject&amp;gt;
&amp;lt;content id=&amp;#34;ProjectMailBody&amp;#34;&amp;gt;
&amp;lt;![CDATA[&amp;lt;!DOCTYPE HTML PUBLIC &amp;#34;-//W3C//DTD HTML 4.0 Transitional//EN&amp;#34;&amp;gt;
&amp;lt;HTML&amp;gt;
&amp;lt;HEAD&amp;gt;
&amp;lt;META http-equiv=Content-Type content=&amp;#34;text/html; charset=UTF-8&amp;#34;&amp;gt;
&amp;lt;META content=&amp;#34;MSHTML 6.00.6000.17063&amp;#34; name=GENERATOR&amp;gt;
&amp;lt;STYLE&amp;gt;
TABLE {
MARGIN-LEFT: 20px;
}
TD {
border: 1px solid #CBCBCB;
}
.dataString {
text-align:left;
padding-left:3px;
}
.tableTittle{
background-color: #FF9900;
}
&amp;lt;/STYLE&amp;gt;
&amp;lt;/HEAD&amp;gt;
&amp;lt;BODY bgColor=#ffffff&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;
Hi %name%,
&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;
欢迎注册
&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;
该邮件是系统自动发出，请不要直接回复，谢谢
&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;table width=&amp;#34;800&amp;#34; border=&amp;#34;1&amp;#34; cellspacing=&amp;#34;0&amp;#34; cellpadding=&amp;#34;0&amp;#34;&amp;gt;
&amp;lt;tr class=&amp;#34;tableTittle&amp;#34;&amp;gt;
&amp;lt;th width=&amp;#34;25%&amp;#34;&amp;gt;
&amp;lt;font size=&amp;#34;2&amp;#34; face=&amp;#34;Arial&amp;#34; class=&amp;#34;dataString&amp;#34; &amp;gt;
&amp;lt;p align=&amp;#34;center&amp;#34;&amp;gt;
Name
&amp;lt;/p&amp;gt;
&amp;lt;/font&amp;gt;
&amp;lt;/th&amp;gt;
&amp;lt;th width=&amp;#34;25%&amp;#34;&amp;gt;
&amp;lt;font size=&amp;#34;2&amp;#34; face=&amp;#34;Arial&amp;#34; class=&amp;#34;dataString&amp;#34; &amp;gt;
&amp;lt;p align=&amp;#34;center&amp;#34;&amp;gt;
Date and time
&amp;lt;/p&amp;gt;
&amp;lt;/font&amp;gt;
&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;#listItems&amp;gt;&amp;lt;/#listItems&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;
Kind regards,
&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
&amp;lt;DIV&amp;gt;
&amp;lt;FONT face=Arial size=2&amp;gt;
Bridge
&amp;lt;/FONT&amp;gt;
&amp;lt;/DIV&amp;gt;
]]&amp;gt;
&amp;lt;/content&amp;gt;
&amp;lt;/ProjectMail&amp;gt;
&amp;lt;/email&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;Java代码：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
@Service(&amp;#34;mailService&amp;#34;)
public class MailServiceImpl implements MailService {
private static final Logger LOG = LoggerFactory.getLogger(MailServiceImpl.class);
private static final String DO_INVOICE_ITMES =&amp;#34;&amp;lt;#listItems&amp;gt;&amp;lt;/#listItems&amp;gt;&amp;#34;;
private static final String NAME = &amp;#34;%name%&amp;#34;;
@Override
public void sendMail() {
List&amp;lt;String&amp;gt; mailCCList = new ArrayList&amp;lt;String&amp;gt;();
String attachmentPath = null；
Map&amp;lt;String, String&amp;gt; mail = buildMail(receiverName);
String emailContent = mail.get(&amp;#34;mailContent&amp;#34;);
String subject = mail.get(&amp;#34;emailSubject&amp;#34;);
toSendMail(receiverMailAddress, mailCCList, emailContent, subject, attachmentPath, attachmentName);
}
private void toSendMail(String toAddress, List&amp;lt;String&amp;gt; mailCCList, String emailContent, String subject, String attachmentPath, String attachmentName) {
Properties props = new Properties();
// Set the attribute of mail server
props.put(&amp;#34;mail.smtp.host&amp;#34;, MAIL_SERVER_HOST));
// Need to be authorized, such ability through validation (must have this one)
props.put(&amp;#34;mail.smtp.auth&amp;#34;, &amp;#34;true&amp;#34;);
// Build a session with props objects
Session session = Session.getDefaultInstance(props);
// Using the session as parameters define the message object
MimeMessage message = new MimeMessage(session);
Transport transport = null;
try {
// Load the sender&amp;amp;#8217;s address
message.setFrom(new InternetAddress(MAIL_ADDRESS);
// Load the recipient address
message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
if (StringUtil.isNotEmpty(mailCCList)) {
InternetAddress[] to_mail = new InternetAddress[mailCCList.size()];
for (int i = 0; i &amp;lt; mailCCList.size(); i++) {
to_mail[i] = new InternetAddress(mailCCList.get(i));
}
message.addRecipients(Message.RecipientType.CC, to_mail);
}
// Load the subject
message.setSubject(subject);
// Add mail each part to the multipart, including the text content
// and accessories
MimeMultipart multipart = new MimeMultipart();
// Set the HTML content of the message
BodyPart contentPart = new MimeBodyPart();
// To set the content and format/encoding of BodyPart
contentPart.setContent(emailContent, &amp;#34;text/html;charset=UTF-8&amp;#34;);
multipart.setSubType(&amp;#34;related&amp;#34;);
multipart.addBodyPart(contentPart);
// Add attachment
if (!StringUtil.isNullOrEmpty(attachmentName)) {
contentPart = new MimeBodyPart();
DataSource source = new FileDataSource(attachmentPath);
// Add attachment&amp;amp;#8217;s content
contentPart.setDataHandler(new DataHandler(source));
// Add attachment&amp;amp;#8217;s title
contentPart.setFileName(attachmentName);
multipart.addBodyPart(contentPart);
}
// Add multipart object to the message
message.setContent(multipart);
// Save the mail
message.saveChanges();
transport = session.getTransport(&amp;#34;smtp&amp;#34;);
// Connect the mail server
transport.connect(MAIL_SERVER_HOST, MAIL_SERVER_USERNAME, MAIL_SERVER_PASSWORD);
// Send mail
transport.sendMessage(message, message.getAllRecipients());
LOG.info(&amp;#34;Send mail to: &amp;#34; + toAddress + &amp;#34;,success&amp;#34;);
} catch (AddressException e) {
LOG.error(e);
} catch (NoSuchProviderException e) {
LOG.error(e);
} catch (MessagingException e) {
LOG.error(e);
} finally {
try {
if (null != transport) {
transport.close();
}
} catch (MessagingException e) {
LOG.error(e);
}
}
}
private Map&amp;lt;String, String&amp;gt; buildMail(String receiverName) {
StringBuilder itemsBuilder = new StringBuilder();
Element emailTemplateRoot = getXMLRootElement(&amp;#34;mailTemplate.xml&amp;#34;);
String emailSubject = emailTemplateRoot.selectSingleNode(&amp;#34;//*[@id=&amp;amp;#8217;ProjectSubject&amp;amp;#8217;]&amp;#34;).getText().replace(TOUSERNAME, receiverName.toUpperCase());
StringBuilder content = new StringBuilder();
String mailTemplateContent = emailTemplateRoot.selectSingleNode(&amp;#34;//*[@id=&amp;amp;#8217;ProjectMailBody&amp;amp;#8217;]&amp;#34;).getText();
mailTemplateContent = mailTemplateContent.replace(NAME, receiverName).replace(DO_INVOICE_ITMES, itemsBuilder.toString());
content.append(mailTemplateContent);
Map&amp;lt;String, String&amp;gt; mail = new HashMap&amp;lt;String, String&amp;gt;();
mail.put(&amp;#34;emailSubject&amp;#34;, emailSubject);
mail.put(&amp;#34;mailContent&amp;#34;, content.toString());
return mail;
}
private String getColumnDataString(String data) {
String columnData = null;
columnData = &amp;#34;&amp;lt;td class=&amp;amp;#8217;dataString&amp;amp;#8217;&amp;gt;&amp;lt;font size=&amp;amp;#8217;2&amp;amp;#8242; face=&amp;amp;#8217;Arial&amp;amp;#8217;&amp;gt;&amp;lt;p align=&amp;amp;#8217;center&amp;amp;#8217;&amp;gt;&amp;#34; + data
+ &amp;#34;&amp;lt;/p&amp;gt;&amp;lt;/font&amp;gt;&amp;lt;/td&amp;gt;&amp;#34; + &amp;#34;n&amp;#34;;
return columnData;
}
private Element getXMLRootElement(String emailFile) {
SAXReader reader = new SAXReader();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(emailFile);
Document document = null;
try {
document = reader.read(inputStream);
} catch (DocumentException e) {
LOG.error(&amp;#34;Get XML Root Element fail, DocumentException&amp;#34;, e);
}
return document.getRootElement();
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;2015.05.25补记：&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>异常处理的一般方法</title><link>https://bridgeli.cn/posts/2014-08-26-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E7%9A%84%E4%B8%80%E8%88%AC%E6%96%B9%E6%B3%95/</link><pubDate>Tue, 26 Aug 2014 08:43:06 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-26-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E7%9A%84%E4%B8%80%E8%88%AC%E6%96%B9%E6%B3%95/</guid><description>&lt;p&gt;学习Java的应该都知道，异常处理是其一大特色，使用异常处理的最大优势就是：将问题处理代码和正常执行流程做了区分，使得程序的执行逻辑更加清晰，不再使错误处理代码和正常业务逻辑代码混在一起。Java不仅自己定义了各种各样的异常类来描述一些基本的错误现象，而且还提供了扩展机制，允许程序猿自定义异常类，应用于实际问题处理，Java的异常处理机制可以捕获对应的异常，并进行主动处理，那么既然Java的异常处理机制这么好用，有没有一般性的方法呢？&lt;/p&gt;
&lt;p&gt;首先说明，（1）. Java的异常分为：Error和Exception，Error类及其子类表示不是不可能恢复但很难处理的情况下的一种严重问题，例如内存溢出（大家可以想想Java是否存在内存溢出现象？），我们不可能指望程序能处理，所以本文所有的方法都是针对Exception。&lt;/p&gt;
&lt;p&gt;（2）. 异常处理就五个关键字：try、catch、finally、throw、throws，他们的用法也比较简单，本文不再赘述。&lt;/p&gt;
&lt;p&gt;好了，下面开讲：&lt;/p&gt;
&lt;p&gt;1. Exception分为两类：Exception和RuntimeException，其中Exception必须在系统中处理，而RuntimeException继承与Exception，表示一种设计或实现的问题，如果系统正常运行，此类异常不会出现，例如最常见的NullPointerException，所以一般不强制要求处理。&lt;/p&gt;
&lt;p&gt;2. 像ClassNotFoundException这类异常，一般是jar包没有添加，在系统正常运行中肯定不会出现，所以无需抛出，可以直接try catch掉，但别忘了打log。&lt;/p&gt;
&lt;p&gt;3. 像SQLException之类的，这类异常偶然也会出现，如：数据库服务没打开、没有网络、SQL语法有误等，但这类异常即使抛到Service层依然无法处理，抛到客户端客户也看不懂，还影响用户体验，所以也不要抛出，一般都是转化为RuntimeException，在转化之前打log。&lt;/p&gt;
&lt;p&gt;4. 所以需要我们处理的异常只有ValidationException和BusinessException，这两类异常都是我们自定义的异常，其中ValidationException处理的是验证异常，而BusinessException处理的是业务逻辑异常，这两类异常都需要我们在系统中做出处理，下面会给出ValidationException、BusinessException和自定义转化为RuntimeException的参考方法。&lt;/p&gt;
&lt;p&gt;5. Service层是负责业务逻辑处理的一层，所以所有的验证都要放在Service层，而Service层在进行验证和业务逻辑处理的时候，应该抛出这些所遇到的异常，并加入到ValidationException（如用户登录时用户名或密码为空等）或者BusinessException（如用户名或密码不正确等）异常中，然后在上层（servlet或者Action或者、controller）中进行捕获，捕获到之后放到Request中，带到前台进行处理。&lt;/p&gt;
&lt;p&gt;6. 千万要记得出现异常一定要打log！！！尤其是当你try catch掉一个异常时。其中ValidationException、BusinessException日志级别为warning或info，其他的据具体情况而定，但MyRuntimeException 可以不打，大家可以想想为什么？（前面有答案）&lt;/p&gt;
&lt;p&gt;7. 顺便说一下，关闭资源一定要放在finally里，因为finally的代码无论是否有异常都会被执行，确保打开的资源一定都会被关闭。&lt;/p&gt;
&lt;p&gt;在这里顺便回答一些同学的问题：&lt;/p&gt;
&lt;p&gt;（1）、我在前台已经做了验证，那么是否还需要在后台再做验证？&lt;/p&gt;
&lt;p&gt;答：肯定要，原因①、前台验证一般都是用js来做，而js的解析是在客户端，如果有人禁用了浏览器的js，是不是你的验证就不起作用了。（关于如何禁用js，请自己用Google百度一下）；&lt;/p&gt;
&lt;p&gt;②、不排除有些黑客要黑你，所以他们可以通过一些工具，直接绕过前台，那么你懂得。&lt;/p&gt;
&lt;p&gt;（2）、为什么要抽象Service层，为什么不把Service层的功能全放到servlet中来做，简单来说为什么要把验证放到Service层？&lt;/p&gt;
&lt;p&gt;答：首先为什么抽象Service层，肯定是为了复用，这个是关于分成模型中要讲的，有机会我会在写一篇博文，专讲分层的一般方法，这里不多说，大家可以试想一下，目前移动端越来越火，而我们的servlet是和web相关，那么我们的Servlet能被App复用吗？如果我们把验证放到servlet中，那么如果在开发一套App，我们是不是所有的验证都要重写一篇？而放在Service层则没有这个问题。&lt;/p&gt;
&lt;p&gt;（3）、Java是否存在内存溢出问题？&lt;/p&gt;
&lt;p&gt;答：一般是不存在的，因为jvm有GC机制，会很好的管理内存，但有时候当我们短时间内，打开很多资源而没有关闭的时候，是可能会存在内存溢出问题的。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
package cn.bridgeli.exceptionhandle.exception;
public class BusinessException extends Exception {
private static final long serialVersionUID = 1L;
private int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int
getCode() {
return code;
}
}
package cn.bridgeli.exceptionhandle.exception;
public class MyRuntimeException extends RuntimeException {
private static final long serialVersionUID = 1L;
private int code;
public MyRuntimeException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
package cn.bridgeli.exceptionhandle.exception;
import java.util.HashMap;
import java.util.Map;
public class ValidationException extends Exception {
private static final long serialVersionUID = 1L;
private Map&amp;lt;String, String&amp;gt; errorFields = new HashMap&amp;lt;String, String&amp;gt;();
public ValidationException() {
}
public void addErrorFiel(String name, String message) {
errorFields.put(name, message);
}
public String getErrorField(String name) {
if (errorFields.containsKey(errorFields)) {
return errorFields.get(name);
}
return &amp;#34;&amp;#34;;
}
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>如何在网络上搭建个人博客</title><link>https://bridgeli.cn/posts/2014-08-25-%E5%A6%82%E4%BD%95%E5%9C%A8%E7%BD%91%E7%BB%9C%E4%B8%8A%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/</link><pubDate>Mon, 25 Aug 2014 09:22:47 +0000</pubDate><guid>https://bridgeli.cn/posts/2014-08-25-%E5%A6%82%E4%BD%95%E5%9C%A8%E7%BD%91%E7%BB%9C%E4%B8%8A%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/</guid><description>&lt;p&gt;相信有很多程序猿应该都有过自己搭建博客的冲动，但很多时候有一种无从下手的感觉，第一篇博客就写一下，怎么搭建自己的个人博客吧，希望对大家有点帮助。&lt;/p&gt;
&lt;p&gt;搭建个人博客主要分以下几步：&lt;/p&gt;
&lt;p&gt;1. 购买域名&lt;/p&gt;
&lt;p&gt;这个其实很简单，我们只需要在常见的万网、西部数码注册后，只要该域名还未被注册，填一下基本信息，一般都可以了，另外需要说明的是：（1.）、域名是有限资源，如果一个域名被注册了，那么别人就不能注册了，所以想好了域名，大家可以立马去注册，以防被别人抢注，万一被别人抢注，想买回来就很困难了，大家可以搜一下史上最贵的是个域名；（2）、域名注册是有有效期的，如果有效期到了没有续费成功的话，那么这个域名就会被回收，就有了被别人重新注册的风险，所以大家可以试着设置一个自动续费之类的；（3）、经过我的分析，西部数码的域名注册费用会比万网稍稍便宜一些，当然是仅供参考。&lt;/p&gt;
&lt;p&gt;2. 购买空间&lt;/p&gt;
&lt;p&gt;这个大的分两种：（1）、独立的IP，这个一般会比较贵；（2）、购买空间，没有独立IP，这个相对便宜。一般如果仅仅是搭建一个个人博客的话，购买空间就够了，一般空间提供商会提供一个二级域名供大家调试。&lt;/p&gt;
&lt;p&gt;另外购买空间这个分大陆的和非大陆的，大陆的一般比较贵，但访问速度快，而且需要备案，备案这个一般购买域名的地方都会有提供，另外像360也提供免费备案；非大陆的空间一般都比较便宜，也比较稳定，但访问速度会稍慢一些，而且不需要备案，所以也哟哟很多人选择这个，BridgeLi就是选择的这个方案。好了有了域名和空间我们就可以进行接下来第三步了。&lt;/p&gt;
&lt;p&gt;3. 准备博客系统&lt;/p&gt;
&lt;p&gt;推荐大家使用WordPress，WordPress是目前世界上最流行的博客系统，据说世界上百分之八十的博客，都是WordPress搭建的，可见其影响力，WordPress这么流行的一个原因就是他是开源免费，大家都可以对其进行个性化的开发，而且为了使他更好用，很多牛人对他开发了很对各种各样的插件，大家可以根据自己的具体情况，选用其中部分插件，由此也可见WordPress的可扩展行做的是多么牛，相信这也是其流行的一个原因。&lt;/p&gt;
&lt;p&gt;关于WordPress怎么用，BridgeLi在这里就不多说了，因为巨简单，大家可以自己到WordPress官网上下一个看看。&lt;/p&gt;
&lt;p&gt;不过需要说明的WordPress是PHP语言写得，所以在搭建博客时，需要我们的系统支持PHP，BridgeLi的空间实在西部数码买的，在预装软件一栏里，选择WordPress就可以了，一键生成，属于傻瓜式，当然大家也可以自己一步一步的去搭建，推荐LAMP。&lt;/p&gt;
&lt;p&gt;4. 域名绑定&lt;/p&gt;
&lt;p&gt;一切已经就绪，当然下面就是绑定域名啦，如果域名绑定成功的话，我们应该就可以通过我们的域名来访问我们的博客了。因为BridgeLi是在西部数码买的空间，使用的预装的WordPress系统，所以下面先以这个为例进行讲解，选择个人中心–&amp;gt;主机管理，会有一个域名绑定，我们只需要按照步骤一步一步就可以了，需要说明的是，如果你的域名是在西部数码买的，那么应该在添加域名绑定之后应该局可以访问了，但如果您不是在西部数码买的，像BridgeLi就是在万网买的域名，就需要到万网那边，把西部数码提供的二级域名添加到域名解析里面就行了，关于DNS服务器我们可以忽略，&lt;strong&gt;关于域名解析&lt;/strong&gt;，大家眼看自己空间的类型，如果是独立IP，那么肯定就是A，具体大家可以看万网和西部数码的文档，BridgeLi就不在这里一一赘述了。&lt;/p&gt;
&lt;p&gt;另外还有一种情况是自己搭建的系统，也就是我们只买了一个域名、一个空间，博客系统是自己传上去的，那么如何进行域名绑定呢？当然域名解析肯定还是要到注册地方的域名解析中去添加，添加完成之后，我们就可以通过：域名+ / + 项目名称就行访问了，我们当然不希望是这样的，直接通过域名访问多好啊，这就需要我们对服务器进行一番设置，因为BridgeLi是对Java开发的，对tomcat比较熟，下面就以tomcat为例进行说明，主要有两步：&lt;/p&gt;
&lt;p&gt;（1）、打开server.xml文件，在&lt;host&gt;和&lt;/host&gt;之间插入如下语句。&lt;/p&gt;
&lt;p&gt;&amp;lt;Context path=”” docBase=”” debug=”0″ privileged=”true”&amp;gt;&lt;/Context&gt;&lt;/p&gt;
&lt;p&gt;其中path为空，就是为了去掉访问时的/项目名称，docBase是项目所在的目录，如果是war包，一定要项目名称，如果是源码包（这个应该很少），则一定要详细到webRoot（MyEclipse项目）或者webContent（Eclipse项目）&lt;/p&gt;
&lt;p&gt;（2）、删除webapps文件夹下的ROOT文件夹就可以了&lt;/p&gt;
&lt;p&gt;经过以上四步，我们就在网络上拥有自己的博客了，怎么样，很简单吧？想搭建博客的你，还犹豫什么，马上行动吧！&lt;/p&gt;</description></item></channel></rss>