在 Docker Swarm 环境中, 因服务更新, 迁移, 重启等操作, 我们会产生大量无用镜像与容器
如果不及时清理的话, 镜像会快速增长, 导致占满磁盘空间
理论上我们可以在每个节点配置一个清理的定时任务, 但是新增节点及更新定时任务配置的时候会不太方便
此时我们可以使用 Docker Swarm 的 global mode 为所有的 Docker 节点启动一个清理服务副本, 实现新节点加入时自动启动与配置更新时自动同步到所有节点
创建 docker-compose.yml 文件, 内容如下
1 | # docker-compose 文件版本, 需与 docker engine 兼容, 否则启动失败 |
其中会用到两个脚本, 需与 docker-compose.yml 在同一文件夹中
1 |
|
1 |
|
1 | docker stack deploy --compose-file docker-compose.yml docker-prune |
1 | docker stack rm docker-prune |
1 | 先停止, 再启动, 因为使用到了 docker config, 无法直接更新 |
在企业内部进行代码提交时, commit 中会存在提交者的 username 与 email
但该 username 与 email 是提交者在 Git 客户端自己设置的
如果提交者忘记设置或者设置错误, 并将 commit push 到远程服务后
当协作者需要寻找该 commit 提交者时, 错误的 username 与 email 会对协作者造成障碍
为解决这个问题, 需要在 GitLab 使用 Server Hooks 对 commit 进行校验, 只有 username 与 email 与 GitLab 中的一致才允许 push, 否则拒绝 push 并提示修改
如需对 commit 的 username 与 email 进行校验, 那么需要在校验脚本中获取 push 者的 username 与 email
通过 GitLab Server Hooks 文档可知存在 GL_USERNAME
环境变量, 该变量的值为 push 者的 GitLab 的 username, 但是缺乏 email 相关环境变量
为获取 push 者的 email, 需使用 GitLab 提供的 Users API 进行获取
通过 API 文档可知只有 admin 用户才返回用户 email, 所以需要先使用 admin 账号生成一个 TOKEN
这个 TOKEN 只是用来获取获取用户 email, 故创建时选择 read_user 的范围即可
GitHub 的 platform-samples 项目提供了一个 commit-current-user-check.sh 的 hook, 我们可以将该脚本下载下来, 进行修改即可
以下是修改后的 commit-current-user-check.sh
文件
1 |
|
由该 hook 功能可知其类型应为 pre-receive
我们按照 GitLab 全局配置 Server Hook 文档 将 commit-current-user-check.sh
放在 server hook 的 pre-receive.d
目录下
并添加可执行权限与配置所属者为 git 用户即可
1 | chmod u+x commit-current-user-check.sh |
在 Jenkins 中安装 Generic Webhook Trigger 插件
在 GitLab 中创建一个用于集成的演示项目
并在该项目根路径创建一个 Jenkinsfile 文件, 文件内容如下
1 | pipeline { |
在 Jenkins 创建一个 Multibranch Pipeline 项目
配置分支源为 Git, 项目仓库为 GitLab 中的项目地址
同时配置不通过SCM自动化触发, 该配置是指保存后不要直接运行 Job
然后进行保存
进入项目 Webhook 配置页面
URL 地址格式为 {jenkins 地址}/generic-webhook-trigger/invoke?token={token}
如果 jenkins 地址为内网, 需要在 GitLab 管理配置页面的 Outbound requests 下勾选 Allow requests to the local network from web hooks and services 属性, 否则会保存失败
Generic Webhook Trigger 插件可以通过配置 IP 白名单的方式仅接受允许的 IP
参考链接: https://github.com/jenkinsci/generic-webhook-trigger-plugin#whitelist-hosts
然后点击最下方的保存按钮即可
保存后在 webhooks 页面最下面可以看到该记录, 点击 Test 按钮, 然后选择 Push events, 此时会手动触发一次 webhook 请求, 如果响应成功, 会显示一条 Hook executed successfully: HTTP 200 的消息提示
同时在 Jenkins 的 Blue 页面查看项目活动记录, 可以看到项目运行成功的记录, 其中的消息部分为即为 causeString 属性
除了通过 Webhook 运行的方式外, 我们可能存在手动运行的需求
如果我们在 Jenkinsfile 中没有依赖 genericVariables 配置的环境变量的话, 我们可以直接手动运行, 否则的话使用该变量会出现问题
这个时候需要定义一个与 genericVariables 中 key 名称相同的 parameter
配置方式如下
1 | pipeline { |
如果是通过 webhook 运行, Generic Webhook Trigger 会使用 genericVariables 中的 WEBHOOK_REF 填充 parameter 中的 WEBHOOK_REF
如果是手动运行, Generic Webhook Trigger 会使用 parameter 中的 WEBHOOK_REF 填充 genericVariables 中的 WEBHOOK_REF
GenericTrigger 的属性说明可以查看 插件官方文档
同时可以在项目配置页面点击流水线语法, 然后使用可视化语法生成器生成 trigger 配置
填写配置属性后点击 Generate Declarative Directive 按钮即可生成
]]>String
类型参数时,前后可能存在一些空格,如果未曾去除就直接保存的话,可能会对一些特殊的业务场景造成致命影响。为了杜绝这种情况,需要在接收参数时进行前后空格清除处理而接收 String
参数主要存在俩种情况
url
或 form
表单中的参数对于这种情况,Spring MVC
提供了一个 org.springframework.beans.propertyeditors.StringTrimmerEditor
类,我们只需要在参数绑定中进行注册就行,方式如下
1 |
|
Request Body
中JSON
或XML
对象参数在这里,Spring MVC
是使用 Jackson
对参数进行反序列化,所以对于 String
的处理是在 Jackson
中配置
1 |
|
1 |
|
1 | public class User { |
1 | .class) (SpringRunner |
对与这种所有项目都需要的通用配置,我们应该抽取一个公共模块,然后通过引入依赖来实现自动配置
创建 commons
模块
创建自动配置类 WebMvcStringTrimAutoConfiguration
1 |
|
配置引入依赖后存在 SpringBootApplication
,EnableAutoConfiguration
注解时自动配置
在 resurces
创建 META-INF/spring.factories
文件
1 | # 自动配置 |
spring-boot-actuator
模块虽然为 Spring-Boot 项目提供了监控及管理的 API
,但是并没有提供对应的 UI 管理系统,此时我们可以使用开源的 Spring-Boot-Amin 来为我们的 Spring-Boot 项目提供一个可视化的管理页面首先在创建一个简单的 Spring-Boot 项目,可以选择在 start.spring.io 中创建,然后导入
在项目顶级 pom.xml
定义 spring-boot-admin
的依赖版本
1 | <properties> |
在项目中创建一个用于与 spring-boot-admin
集成的演示模块 client
引入以下依赖
1 | <dependencies> |
创建启动类
1 |
|
此时可以通过启动 main
方法查看启动是否正常
在项目中创建 spring-boot-admin
服务模块 server
引入如下依赖
1 | <dependencies> |
创建启动类
1 |
|
创建配置文件 application.yml
1 | server: |
访问 localhost:8090 即可进入 spring-boot-admin
管理页面,但是此时还没有项目与它进行集成,所以应用数为 0
在演示模块 client
中添加配置文件 application.yml
1 | spring: |
再重启 client
项目,此时 client
项目会向 http://localhost:8090
注册自己的服务信息
刷新 localhost:8090 页面,可发现 client
已经注册成功,此时可以查看 client
相关信息
在默认配置中使用 InetAddress.getLocalHost().getCanonicalHostName()
获取服务地址,本地环境一般没有问题,但是在生产环境中可能获取的是本机内网IP
,导致spring-boot-admin
无法访问注册的服务,所以需要更改注册时的服务地址,配置方式如下
1 | spring: |
使用spring-security
实现登录验证
首先引入依赖
1 | <dependency> |
创建spring-security
配置类
1 |
|
在application.yml
配置默认帐号,密码
1 | spring: |
如果帐号密码是保存在数据库中,配置方式请查看该博客 使用数据库进行身份认证
重启 server
模块,此次刷新 localhost:8090 ,会重定向到登录页面,输入配置的默认帐号,密码即可登录
但是因为 server
端配置了登录验证,所以导致客户端无法注册,此时需要在 client
配置登录 server
所需的帐号与密码
配置方式如下
1 | spring: |
client
端主要是保护 management
中开放的 web
端点
首先引入依赖
1 | <dependency> |
创建spring-security
配置类
1 |
|
在application.yml
配置默认帐号,密码
1 | spring: |
同时为了允许 server
访问 client
的 web
端点,我们在注册时需要提供 client
的帐号与密码
1 | spring: |
在演示项目中,我们选择了开放所有端口,为了安全起见,我们应该只开放需要的端口
其中各个端点的作用请看官网文档 Spring-Boot 端点
1 | management: |
如果想在 server
查看 client
当前写入的日志文件内容,只需在 client
的配置文件中配置 logging.path
或logging.file
属性即可,此时 server
会存在一个 Logfile
的标签,点击该标签即可查看
首先在server
引入依赖
1 | <dependency> |
邮箱发送相关配置
1 | spring: |
server
发送邮件配置
1 | spring: |
此时重启 server
端,然后停止 client
端,检查收件箱,可以发现已经接收到下线通知(接受邮件可能存在延迟)
在上面的示例中,已经演示了一些基本的配置,而在实际开发中,一般情况下是一个 server
对应多个 client
,多个 client
之间注册 server
的配置应该是一样的,此时我们可以选择将一些通用的配置抽取出来,以达到复用的效果
创建 commons
模块
在 resources
下创建 config
文件夹,用于存放通用的配置文件
创建通用邮箱配置文件 application-commons-mail.yml
1 | spring: |
创建 client
端默认用户配置文件 application-commons-spring-boot-admin-client-user.yml
1 | spring: |
创建 server
端用户及 url
配置文件 application-commons-spring-boot-admin-server.yml
1 | spring-boot-admin: |
创建 client
集成 spring-boot-admin
的通用配置文件 application-commons-spring-boot-admin-client.yml
1 | spring: |
将 client
端的 spring-security
配置类 SpringSecurityConfig
迁移到 commons
模块
在 client
端的启动类 ClientApplication
上添加一个注解配置 @ImportAutoConfiguration(SpringSecurityConfig.class)
表示应用这个配置类
修改 client
的配置文件 application.yml
为
1 | spring: |
修改 server
的配置文件 application.yml
为
1 | server: |
但是通过 Spring Boot 文档可知, 可以通过监听 ApplicationFailedEvent
事件进行启动异常处理
6 . An
ApplicationFailedEvent
is sent if there is an exception on startup.
现在我们希望在出现这些异常时, 发送一封邮件到我预定义的邮箱中
发送邮件我们使用 spring-boot-starter-mail
提供的 JavaMailSender
接口, 通过配置 spring.mail
系列参数 Spring Boot 会自动创建 JavaMailSender
但是可能存在发生异常时 JavaMailSender
还没有创建,所以最好是由我们手动创建 JavaMailSender
对象
以下是发送邮件工具类 EmailUtils
1 | package com.github.ghthou.startexceptionnotifications.samples.util; |
然后创建一个 Spring 监听器 StartExceptionNotificationsListener
,监听启动异常, 然后判断当前环境,如果不是 dev
环境,进行异常通知
1 | package com.github.ghthou.startexceptionnotifications.samples.listener; |
配置自动注册 StartExceptionNotificationsListener
在 resources/META-INF/spring.factories
中进行以下配置
1 | org.springframework.context.ApplicationListener=\ |
创建用于模拟启动过程中出现异常的启动监听器 ApplicationStartingEventListener
,ApplicationEnvironmentPreparedEventListener
等
然后在 SpringApplication
手动添加这些事件
1 | package com.github.ghthou.startexceptionnotifications.samples; |
最后通过测试结果可知
三个事件会进行 ApplicationFailedEvent
事件处理, 不过 ApplicationStartingEvent
事件一般不会产生异常, 而 ApplicationReadyEvent
是启动完成后触发的事件
目前是使用 email 通知, 如果希望使用 短信,微信,钉钉 通知的话, 在 StartExceptionNotificationsListener
中自定义通知处理即可
标准的 Java 库不能提供足够的方法来操纵其核心类,所以 Apache Commons Lang 为我们提供了这些额外的方法
本文便介绍 Apache Commons Lang 中 concurrent 包的使用说明
JDK 版本需要大于等于 1.7
1 | <dependency> |
1 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.7' |
Interface | Description |
---|---|
CircuitBreaker | 描述断路器 Circuit Breaker 的接口 |
Computable<I,O> | 为单个参数的计算提供定义一个包装接口,并返回一个结果 |
ConcurrentInitializer | 定义线程安全的初始化接口 |
Class | Description |
---|---|
AbstractCircuitBreaker | 断路器基础类 |
AtomicInitializer | ConcurrentInitializer 接口基于 AtomicReference 变量的实现类 |
AtomicSafeInitializer | ConcurrentInitializer 接口基于 AtomicReference 变量的实现类,但是初始化方法 AtomicSafeInitializer.initialize() 只会执行一次 |
BackgroundInitializer | ConcurrentInitializer 接口实现类,通过后台线程完成初始化 |
BasicThreadFactory | ThreadFactory 接口实现类,提供一些配置获取方法 |
BasicThreadFactory.Builder | BasicThreadFactory 的 Builder 模式实例 |
CallableBackgroundInitializer | BackgroundInitializer 实现类,通过传入 Callable 进行实现 |
ConcurrentUtils | 提供 java.util.concurrent 包相关功能的工具类 |
ConstantInitializer | ConcurrentInitializer 接口实现类,直接返回构造器中传入的参数 |
EventCountCircuitBreaker | 计算特定事件的 Circuit Breaker 断路器模式的简单实现 |
LazyInitializer | ConcurrentInitializer 接口实现类,通过双重检查锁进行实现 |
Memoizer<I,O> | 为单个参数的计算定义一个包装的接口,并返回一个结果 |
MultiBackgroundInitializer | BackgroundInitializer 实现类,可以在后台进行多个初始化 |
MultiBackgroundInitializer.MultiBackgroundInitializerResults | 一个数据类,用于存储MultiBackgroundInitializer 初始化后的结果 |
ThresholdCircuitBreaker | 通过给定阈值开启 Circuit Breaker 断路器模式的简单实现 |
TimedSemaphore | 一个专门的信号量实现,在给定的时间内提供许可 |
Enum | Description |
---|---|
AbstractCircuitBreaker.State | 表示断路器不同状态的内部枚举 |
Exception | Description |
---|---|
CircuitBreakingException | 用于报告与 Circuit Breaker 断路器相关的运行时错误条件的异常类 |
ConcurrentException | 用于报告与访问后台任务数据相关的错误条件的异常类 |
ConcurrentRuntimeException | 用于报告与访问后台任务数据有关的运行时错误条件的异常类 |
描述断路器的接口
一个断路器可用来防止不可靠的服务或意外负载的应用程序。 它通常监视特定的资源。 只要这个资源按照预期工作,它就处于关闭状态,这意味着资源可以被使用。 如果使用该资源时遇到问题时,断路器可以切换到打开状态 。 那么访问这个资源是被禁止的。 根据具体的实施方式,当资源再次可用时,断路器可能切换到关闭状态。
该接口定义了断路器组件的通用协议
断路器的基础类,实现了 CircuitBreaker
大部分接口
计算特定事件的断路器模式的简单实现
一个断路器可用来防止不稳定的服务或遭遇意外高峰负载的应用程序。 新创建的EventCountCircuitBreaker
对象最初处于关闭状态,意味着没有检测到任何问题。 当应用程序遇到特定事件(如错误或服务超时)时,它会通知断路器增加一个内部计数器。 如果在一个特定的时间间隔中报告的事件的数量超过配置的阈值时,断路器将被打开。 这意味着相关的子系统有问题; 应用程序不应该再使用它,而是给它一点时间让它安顿下来。 如果接收的事件数量低于阈值,断路器可以配置为在一定的时间之后切换回关闭状态。
当一个EventCountCircuitBreaker
对象被构造时,可以提供下列参数:
构造方法如下
1 | // threshold 改变断路器状态的阈值; 如果在检查间隔内收到的事件数量大于此值,则断路器打开; 如果它低于这个值,它会再次关闭 |
这个类支持以下典型用例:
防止负载高峰
想象一下你有一个服务器可以每分钟处理一定数量的请求。 突然之间,请求数量显著增加,可能是遭到 DDoS(拒绝服务攻击)。 EventCountCircuitBreaker
可以配置为在检测到突然的高峰负载时停止应用程序处理请求,并在事情平静时再开始请求处理。 以下代码片段显示了这种情况的典型示例。 这里EventCountCircuitBreaker
在断路器打开之前允许每分钟最多 1000 个请求。 当负载再次下降到每秒 800 个请求时,它会切换回关闭状态:
1 | EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(1000, 1, TimeUnit.MINUTE, 800); |
处理不稳定的服务
假如应用程序是一个不稳定的第三方服务。 如果错误太多,服务应被视为关闭,停止服务一段时间。 可以使用以下方式来实现,在这个具体的例子中,我们在 2 分钟内允许 5 个错误; 如果达到这个限制,服务会有 10 分钟的休息时间:
1 | EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(5, 2, TimeUnit.MINUTE, 5, 10, TimeUnit.MINUTE); |
除了自动状态转换,断路器的状态可以使用 open()
和 close()
方法进行手动更改。 同时也可以使用 addChangeListener(final PropertyChangeListener listener)
方法注册监听器,当发生状态转换时,可以得到事件改变对象 PropertyChangeEvent
,此时可以根据状态更改的情况做出反应
实现说明:
如果请求的次数大于给定阈值,则会打开 Circuit Breaker 模式的简单实现。
它包含一个从零开始的内部计数器,每次调用将计数器增加一个给定的数量。 如果阈值是零,断路器将永远处于打开状态。
一个内存断路器示例
1 | long threshold = 10L; |
为单个参数的计算定义一个包装的接口,并返回一个结果。
为单个参数的计算定义一个包装的接口,并返回一个结果。 计算结果将被缓存
这不是一个全功能的缓存,一旦生成结果,就无法限制或删除结果。 但是,如果在上一次计算过程中抛出错误,则可以通过在构造方法中设置一个选项来实现重新生成给定参数的结果。 如果没有设置或设置为 false,将抛出缓存的异常
1 | public class Memoizer<I, O> implements Computable<I, O> { |
ConcurrentInitializer 及其子类主要是用来进行数据安全初始化的,开头的 Concurrent 表示支持并发操作
它只定义了一个接口
1 | public interface ConcurrentInitializer<T> { |
调用 get()
方法即可获取完成初始化的对象.同时提供了多种实现,可根据需要进行选择
ConstantInitializer 是最简单的 ConcurrentInitializer 实现
在构造器中传入一个对象,在它的 get()
方法中直接返回这个对象.适用于单元测试及替换其他实现类
1 | public class ConstantInitializer<T> implements ConcurrentInitializer<T> { |
1 | ConcurrentInitializer<String> initializer = new ConstantInitializer<>("test"); |
顾名思义,使用懒加载的方式进行初始化,只有在第一次调用时才进行初始化操作,通过实现 initialize
方法返回初始化后的对象
使用双重检查锁进行实现
1 | public abstract class LazyInitializer<T> implements ConcurrentInitializer<T> { |
1 | ConcurrentInitializer<Properties> initializer = new LazyInitializer<Properties>() { |
与 LazyInitializer 功能,用法一致,不过实现方式不同,同时其 initialize
方法会存在调用多次的问题
使用 CAS 方式进行实现
1 | public abstract class AtomicInitializer<T> implements ConcurrentInitializer<T> { |
1 | ConcurrentInitializer<Properties> initializer = new AtomicInitializer<Properties>() { |
AtomicInitializer 存在一个 initialize
执行多次的问题.而 AtomicSafeInitializer 则是为了解决这个问题而诞生,使用它,initialize
只会被执行一次
1 | public abstract class AtomicSafeInitializer<T> implements |
1 | ConcurrentInitializer<Properties> initializer = new AtomicSafeInitializer<Properties>() { |
使用后台线程进行初始化,在创建对象后需要先执行 start()
保证后台线程运行,再调用 get()
方法
默认Executors.newFixedThreadPool(1)
创建一个线程数量为 1 的线程池,在 initialize
方法执行完成之后对线程池进行关闭
同时提供一个 protected BackgroundInitializer(final ExecutorService exec) {}
构造器方法,此时使用传入的 ExecutorService
对象执行 initialize
方法,执行完成后不会该关闭线程池
1 | public abstract class BackgroundInitializer<T> implements |
1 | BackgroundInitializer<Properties> initializer = new BackgroundInitializer<Properties>() { |
BackgroundInitializer 的子类,使用方式为在构造器中传入一个 Callable
对象,在初始化时调用该参数的 call()
方法
1 | public class CallableBackgroundInitializer<T> extends BackgroundInitializer<T> { |
1 | BackgroundInitializer<Properties> initializer = new CallableBackgroundInitializer<>(new Callable<Properties>() { |
BackgroundInitializer 多后台任务的实现,通过该类,可以并行执行多个 BackgroundInitializer
1 | public class MultiBackgroundInitializer |
1 | MultiBackgroundInitializer initializer = new MultiBackgroundInitializer(); |
如果初始化程序执行时间过长且不希望在第一次调用时等待太久请选择 BackgroundInitializer
与 CallableBackgroundInitializer
否则使用 LazyInitializer
与 AtomicSafeInitializer
ThreadFactory
接口的实现,,提供一些配置获取方法
ThreadFactory
用于 ExecutorService
创建执行任务的线程。 在许多情况下,用户并不关心,因为 ExecutorService
会使用一个默认的 ThreadFactory
。 但是,如果对线程有特殊要求,则必须创建一个自定义的ThreadFactory
这个类为它创建的线程提供了一些经常需要的配置选项,这些是:
namingPattern
线程的名称模式。 如果希望日志输出或异常跟踪更容易阅读,可以为线程起一个有意义的名称。 在线程命名时使用 thread.setName(String.format(namingPattern, count));
方式,count
即线程池当前创建的线程数量,从 1 开始。 namingPattern
中的 %d
会被替换为 count
。 如 : namingPattern
为 "My %d. worker thread"
,那么生成的线程名称为 "My 1. worker thread"
,"My 2. worker thread"
daemonFlag
守护线程标志。 该工厂创建的线程是否是守护线程。 这会影响当前 Java 应用程序的退出行为,当正在运行的线程都是守护线程时,Java 虚拟机将退出,默认为 false ,即用户线程priority
线程的优先级. Integer
类型,值区间为 [1,10]uncaughtExceptionHandler
线程由于未捕获到异常而突然终止时调用的处理程序。 如果线程内发生未捕获的异常,则调用此处理程序BasicThreadFactory
不是直接创建实例,而是使用内部类 Builder
类实现此目的,使用 Builder
只需要设置应用程序感兴趣的配置选项。 以下示例显示了 BasicThreadFactory
是如何创建并配置在在ExecutorService
中:
1 | // 创建一个线程工厂,配置名称模式,守护线程标志,优先级 |
提供 java.util.concurrent
包相关功能的工具方法
该类涉及到 ConcurrentException
的方法存在一个 原方法名+Unchecked
的相同功能方法,用于将受检异常 ConcurrentException
转换为运行时异常 ConcurrentRuntimeException
用法如下
1 | Future<Object> future = ...; |
Modifier and Type | Method and Description |
---|---|
static <T> Future<T> | constantFuture(T value) 创建一个已完成的 Future ,该 Future 的返回值及参数中的 T value |
static <K,V> V | createIfAbsent(ConcurrentMap<K,V> map, K key, ConcurrentInitializer<V> init) 检查 ConcurrentMap 是否包含 key ,如果未包含,为 ConcurrentMap 设置 key : init.get() |
static <K,V> V | createIfAbsentUnchecked(ConcurrentMap<K,V> map, K key, ConcurrentInitializer<V> init) createIfAbsent 的非受检异常版本 |
static ConcurrentException | extractCause(ExecutionException ex) ExecutionException 异常原因转换, RuntimeException 与 Error 进行抛出,否则转换为受检异常 ConcurrentException 并返回 |
static ConcurrentRuntimeException | extractCauseUnchecked(ExecutionException ex) extractCause 的非受检异常版本 |
static void | handleCause(ExecutionException ex) ExecutionException 异常处理,RuntimeException 与 Error 进行抛出,否则转换为 ConcurrentException 在进行抛出 |
static void | handleCauseUnchecked(ExecutionException ex) handleCauseUnchecked 的非受检异常版本 |
static <T> T | initialize(ConcurrentInitializer<T> initializer) 获取 initializer 的初始化返回值 |
static <T> T | initializeUnchecked(ConcurrentInitializer<T> initializer) initialize 的非受检异常版本 |
static <K,V> V | putIfAbsent(ConcurrentMap<K,V> map, K key, V value) 检查 ConcurrentMap 是否包含 key ,如果未包含,为 ConcurrentMap 设置 key : value |
一个专门的信号量实现,在给定的时间内提供许可,到期自动释放
该类的功能与 java.util.concurrent.Semaphore
有点类似,通过调用 acquire()
方法获取许可,但是没有提供 release()
方法进行许可释放,因为它会在时间到期后释放所有的许可
如果在许可已经用尽的情况下调用acquire()
,那么当前线程会一直阻塞,直到时间到期,所有许可被释放.此时再重新获取许可.这意味着可以在规定的时间范围内,限制给定操作的次数
用法示例
假如存在一个通过后台线程查询数据库以进行收集统计信息的应用程序.这种后台处理不应该对数据库产生太多负载,防止影响系统的功能和性能.所以可以使用 TimedSemaphore
来限制该线程每秒只能发出一定数量的数据库查询
执行查询的线程类伪代码可能如下
1 | public class StatisticsThread extends Thread { |
下面的代码片段显示了如何创建一个每秒只允许 10 次的 TimedSemaphore
并传递给统计线程 StatisticsThread
1 | TimedSemaphore sem = new TimedSemaphore(1, TimeUnit.SECONDS, 10); |
构造方法如下
1 | // timePeriod 时间段 |
所以 new TimedSemaphore(1, TimeUnit.SECOND, 10);
含义是在 timePeriod(1) * timeUnit(TimeUnit.SECOND) = 1秒
的时间内只发放 limit(10)
个许可
在使用时需要在限制操作前调用 acquire()
方法。 TimedSemaphore
会统计调用 acquire()
的次数,并在许可达到上限后阻塞当前线程,直到时间周期结束后释放所有许可,此时再进行许可获取
另外提供 tryAcquire()
方法,该方法尝试获取许可,如果获取成功,返回true
,否者返回false
,使用这种方法不会造成当前线程的阻塞
同时在运行过程中你可以随时调用 setLimit(final int limit)
修改许可数量。 如果一个操作的次数限制需要动态调整,比如白天减少许可,晚上增加许可。 如果设置的许可小于原本许可,那么会立即生效,但是设置的许可大于原来的许可,阻塞的线程依然阻塞,直到所有许可释放,阻塞的线程被唤醒.此时按照新设置的许可进行处理.如果许可数量设置小于等于 0,那么 acquire()
操作不会阻塞,直接放行
当TimedSemaphore
不需要时,可以调用 shutdown()
方法,该方法会取消释放所有许可的周期任务,如果执行周期任务的 ScheduledExecutorService
不是外部传入,同时也会结束该线程池
FindBugs 是一个使用静态分析来 查找 Java 代码中的错误 的程序。它是免费软件
当前版本的 FindBugs 是 3.0.1
FindBugs 运行需要 1.7 或更高版本的 JRE(或 JDK)。但是,它可以分析从任何版本的 Java 编译的程序,从 1.0 到 1.8
以上是来自官网的介绍,核心内容为查找 Java 代码中的错误
Ctrl+Alt+S
快捷键打开设置选项Plugins
Browse repositories
按钮打开插件中心findbugs
FindBugs-IDEA
Install
按钮 (下面这张图是已经安装的情况),点击安装即可,安装完成后需要重启 IDEACtrl+Shift+A
快捷键打开 Find Action 搜索面板findbugs
进行搜索FindBugs-IDEA
即可打开插件面板创建一个带有一些问题的类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Random;
public class FindBugsDemo {
private static final DateFormat yyyyMMdd = new SimpleDateFormat("yyyy-MM-dd");
public static String yyyyMMddForMat(Date date) {
return yyyyMMdd.format(date);
}
public static int getRanDom() {
return new Random().nextInt();
}
public static int round(int num) {
return Math.round(num);
}
public static void printMap(Map<?, ?> map) {
if (map != null && map.size() > 0) {
for (Object key : map.keySet()) {
System.out.println("key--->" + key);
System.out.println("value--->" + map.get(key));
}
}
}
public static String trimString(String str) {
str.trim();
return str;
}
public boolean equals(Object obj) {
return super.equals(obj);
}
}
选中该类,点击上面插件面板序号为 1 对应的按钮,进行文件分析,结果如下
我们是使用 BUG 严重级别进行分组
分组类型对应如下 (红色编号)
Of Concren
中的问题也一并修复下面对具体提示的 BUG 进行分析 (黄色编号)1.Random object created and used only once (Random 对象创建后只使用一次)
该方法每次运行都会创建一个新的 Random 对象,执行一次后就会被回收。 但是在多线程情况获取随机数方法也能正常使用,所以可以定义一个 Random 对象常量,然后使用该常量对象进行方法调用。 能减少创建对象的性能开销
2.Class defines equals()and uses Object.hashCode()(覆写了 equals 方法但是没有覆写 hashCode 方法)
在 Set,Map 中会使用对象的 hashCode 方法,如果覆写了 equals 方法但是没有覆写 hashCode 方法会导致在 Set,Map 对象中出现问题
3.Inefficient use of keySet iterator instead of entrySet iterator (keySet 迭代器低效,应该使用 entrySet 进行替换)
如果需要获取 Map 中的 key 和 value,使用 Map.entrySet()方法返回 Set<Map.Entry<K, V>> 对象,然后迭代该 Set,在使用 Entry 对象获取 key 和 value 更为高效
4.Method ignores return value (方法忽略返回值)
String 对象是不可变的,当调用 String.trim()后,是返回一个新的 String 对象,不会对调用者的内容进行改动
5.int value cast to float and then passed to Math.round (将 int 值转换为 float,然后传递给 Math.round)
Math.round()方法只接收 float 和 double 类型,然后转换为 int 和 long 类型,如果传递 int 类型,会先将其转换为 float 类型,然后再转换为 int 类型,所以导致该操作返回值与参数内容一致
6.Call to static DateFormat (调用静态的 DateFormat 对象)
DateFormat 对象是线程不安全的,如果多线程调用同一个 DateFormat 对象会导致结果异常
FindBugs 只是一款静态代码分析工具,虽然分析大多数的问题,但是如果希望编写更为健壮的程序,还需进行更多的测试操作,切不可认为 FindBugs 没有分析出问题便认为没有问题了
]]>对于 POJO,我们需要为其中的每个字段生成 getter,setter 方法,虽然能够使用 IDE 快速为我们生成。 但如果需要修改字段名称及字段类型,那么就需要删除在重新进行生成,终究还是有一些不方便。 如果使用 lombok,可以通过一些简单的注解直接生成我们所需要的代码,能极大的提高开发体验
1 | <dependency> |
使用 @NonNull 注解修饰的字段 通过 set 方法设置时如果为 null,将抛出 NullPointerException
主要用来修饰 IO 流相关类,会在 finally 代码块中对该资源进行 close();
为字段生成 getter,setter 方法,标记到类上表明为所有字段生成
生成 toString 方法,默认打印所有非静态字段
生成 equals 和 hashCode 方法
@NoArgsConstructor,@RequiredArgsConstructor,@AllArgsConstructor
NoArgsConstructor 无参构造函数
RequiredArgsConstructor 为未初始化的 final 字段和使用 @NonNull 标注的字段生成构造函数
AllArgsConstructor 为所有字段生成构造函数
相当于同时使用 @Getter,@Setter,@ToString,@EqualsAndHashCode,@RequiredArgsConstructor
使用后,类将使用 final 进行修饰,同时使用 @ToString,@EqualsAndHashCode,@AllArgsConstructor,@Getter
创建一个静态内部类,使用该类可以使用链式调用创建对象
如 User 对象中存在 name,age 字段,User user=User.builder().name(“姓名”).age(20).build()
对标注的方法进行 try catch 后抛出异常,可在 value 输入需要 catch 的异常数组,默认 catch Throwable
在标注的方法内 使用 synchronized($lock) {} 对代码进行包裹 ,$lock 为 new Object[0]
生成一个当前类的日志对象,可以使用 topic 指定要获取的日志名称
虽然 lombok 能为我们快速生成代码,但是有一些生成的代码依然无法满足我们的需求。 此时可配置 lombok.config 来解决问题
以下列出一些常用的配置1
2
3
4lombok.getter.noIsPrefix=true(默认: false) #lombok 默认对 boolean 类型字段生成的 get 方法使用 is 前缀, 通过此配置则使用 get 前缀
lombok.accessors.chain=true(默认: false) #默认的 set 方法返回 void 设置为 true 返回调用对象本身
lombok.accessors.fluent=true(默认: false) #如果设置为 true. get,set 方法将不带 get,set 前缀, 直接以字段名为方法名
lombok.log.fieldName=logger(默认: log) #设置 log 类注解返回的字段名称
注 :在 IDEA 中,lombok.config
文件 请放置于 src\main\java
目录下,在 src\main\resources
中将不生效
在网络攻击日益泛滥的今天,用户的密码可能会因为各种原因泄漏。 而一些涉及用户重要数据的服务,如 QQ,邮箱,银行,购物等等。 一但被有心人利用,那么除了自己隐私泄漏的风险外,还存在自己身份被冒充的危害,更有可能而导致极其严重的结果。 为此谷歌推出了 Google Authenticator
服务,其原理是在登录时除了输入密码外,还需根据 Google Authenticator APP
输入一个实时计算的验证码。 凭借此验证码,即使在密码泄漏的情况下,他人也无法登录你的账户
Google Authenticator
使用了一种基于 时间 的 TOTP
算法,其中时间的选取为自 1970-01-01 00:00:00
以来的毫秒数除以 30
与 客户端及服务端约定的 密钥 进行计算,计算结果为一个 6 位数的字符串 (首位数可能为 0,所以为字符串 ),所以在 Google Authenticator
中我们可以看见验证码每个 30 秒就会刷新一次。 更多详情可查看 Google 账户两步验证的工作原理 一文
由上可知,生成验证码有俩个重要的参数,其一为 客户端与服务端约定的密钥 ,其二便为 30 秒的个数 1
2
3
4
5
6
7
8
9/**
* 创建一个密钥
*/
public static String createSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
return new Base32().encodeToString(bytes).toLowerCase();
}
1 | //1970-01-01 00:00:00 以来的毫秒数除以 30 |
根据这两个参数就可以生成一个验证码1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 根据密钥获取验证码
* 返回字符串是因为数值有可能以0开头
*
* @param secretKey 密钥
* @param time 第几个30秒 System.currentTimeMillis() / 1000 / 30
*/
public static String generateTOTP(String secretKey, long time) {
byte[] bytes = new Base32().decode(secretKey.toUpperCase());
String hexKey = Hex.encodeHexString(bytes);
String hexTime = Long.toHexString(time);
return TOTP.generateTOTP(hexKey, hexTime, "6");
}
因为 Google Authenticator
(以下简称 App)计算验证码也需要 密钥 的参与,而时间 App 则会在本地获取,所以我们需要将 密钥保存在 App 中 ,同时为了与其他账户进行区分,除了密钥外,我们还需要录入 服务名称 ,用户账户 信息。 而为了方便用户信息的录入,我们一般将所有信息生成一张二维码图片,让用户通过扫码自动填写相关信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 生成 Google Authenticator Key Uri
* Google Authenticator 规定的 Key Uri 格式: otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
* https://github.com/google/google-authenticator/wiki/Key-Uri-Format
* 参数需要进行 url 编码 +号需要替换成%20
*
* @param secret 密钥 使用 createSecretKey 方法生成
* @param account 用户账户 如: example@domain.com
* @param issuer 服务名称 如: Google,GitHub
*/
public static String createKeyUri(String secret, String account, String issuer) {
String qrCodeStr = "otpauth://totp/${issuer}:${account}?secret=${secret}&issuer=${issuer}";
Builder<String, String> mapBuilder = ImmutableMap.builder();
mapBuilder.put("account", URLEncoder.encode(account, "UTF-8").replace("+", "%20"));
mapBuilder.put("secret", URLEncoder.encode(secret, "UTF-8").replace("+", "%20"));
mapBuilder.put("issuer", URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
return StringSubstitutor.replace(qrCodeStr, mapBuilder.build());
}
此时再根据上述信息生成二维码,二维码生成方式可参考以下两种方案
此时选择使用 Java
的方式返回一个二维码图片流1
2
3
4
5
6
7
8
9
10
11/**
* 将二维码图片输出到一个流中
* @param content 二维码内容
* @param stream 输出流
* @param width 宽
* @param height 高
*/
public static void writeToStream(String content, OutputStream stream, int width, int height) throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
MatrixToImageWriter.writeToStream(bitMatrix, format, stream);
}
扫描二维码
扫描成功后会新增一栏验证码信息
再让用户输入验证码,与服务端进行校验,如果校验通过,则表明用户可以完好使用该功能
因为验证码是使用基于时间的 TOTP
算法,依赖于客户端与服务端时间的一致性。 如果客户端时间与服务端时间相差过大,那在用户没有同步时间的情况下,永远与服务端进行匹配。 同时服务端也有可能出现时间偏差的情况,这样反而导致时间正确的用户校验无法通过
为了解决这种情况,我们可以使用 时间偏移量 来解决该问题,GoogleAuthenticator
验证码的时间参数为 1970-01-01 00:00:00 以来的毫秒数除以 30
,所以每 30 秒就会更新一次。 但是我们在后台进行校验时,除了与当前生成的二维码进行校验外,还会对当前时间参数 前后偏移量 生成的验证码进行校验,只要其中任意一个能够校验通过,就代表该验证码是有效的
1 | /** 时间前后偏移量 */ |
根据以上代码我们可以简单的创建一个 Google Authenticator
的应用。 但是与此同时,我们也发现 Google Authenticator
严重依赖手机,又因为 Google Authenticator
没有同步功能 ,所以如果用户一不小心删除了记录信息,或者 App 被卸载,手机系统重装等情况。 就会导致 Google Authenticator
成为使用者的障碍。 此时我们可以使用 Authy 这款支持 同步功能 的 App 以解决删除,卸载,重装等问题。 同时 Authy 也存在 Chrome 插件 版本,用于解决在手机丢失的情况下获取验证码。除了 Authy 这个选择外,我们还可以使用 备用验证码 的机制用户用于解决上述问题。 即在用户绑定 Google Authenticator
成功后自动为用户生成多个 备用验证码 ,然后在前台显示。 并让用户进行保存,再让用户使用备用验证码进行校验,以确保用户保存成功,可以参考 印象笔记 的用法 如何开启印象笔记登录两步验证?
另外本文同时参考了以下资料
主要研究的类为
源码版本为jdk1.8.0_91
首先需要了解 AtomicInteger
类型的 ctl
变量,这个变量以32位二进制的方式描述俩种信息
[0,(2^29)-1]
)任务执行有俩种方法,其中 execute
方法由 ThreadPoolExecutor
提供submit
方法继承自 AbstractExecutorService
的实现
1 | public void execute(Runnable command) { |
1 | private boolean addWorker(Runnable firstTask, boolean core) { |
1 | final void runWorker(Worker w) { |
1 | private Runnable getTask() { |
1 | public <T> Future<T> submit(Callable<T> task) { |
存在一个 int
类型的 state
变量,改变量的至可能为以下六种
状态变更存在以下几种顺序
NEW -> COMPLETING -> NORMAL
NEW -> COMPLETING -> EXCEPTIONAL
NEW -> CANCELLED
NEW -> INTERRUPTING -> INTERRUPTED
1 | public V get() throws InterruptedException, ExecutionException { |
1 | private V report(int s) throws ExecutionException { |
1 | private int awaitDone(boolean timed, long nanos) |
在 java.util.concurrent.ThreadPoolExecutor#runWorker(Worker w)
方法中任务执行是直接调用 run
方法,因为FutureTask
需要获取任务运行结果及收集异常,所以对 run
方法进行了包装
在构造 FutureTask 时参数允许接受 Callable 与 Runnable 类型,实际上他会将 Runnable 类型转为一个 Callable 类型,然后使用 call()
方法进行调用
1 | public void run() { |
1 | protected void set(V v) { |
setException(Throwable t)
方法请参考 set(V v)
1 | private void finishCompletion() { |
HashMap
的工作原理及实现,但是对实现的具体过程,思路尚未贯通,所以对于其中的几个核心方法按照每个步骤进行研究,注释源码版本为jdk1.8.0_91
put(K key, V value)
1
2
3
4
5
6
7
8
9
10public V put(K key, V value) {
// 调用 putVal 方法
return putVal(hash(key), key, value, false, true);
}
// 对 key 进行 hash 操作
static final int hash(Object key) {
int h;
// 如果 key 为 null,返回0,否则调用 hashCode() 方法,然后对 hashCode 高16bit不变,低16bit和高16bit做了一个异或处理
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果 table 为 null,或者 table 的长度为0,进行初始化操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据 (table长度-1) 与 hash 计算得出该 hash 在table中的索引
// 根据索引获取对应的值,如果该值为 null,在此位置插入一个 Node 对象
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判断该值的 hash,key 与要插入的 hash,key 是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果相等,表明该key为当前节点的第一个,将原值设置为当前 e 对象
e = p;
else if (p instanceof TreeNode)
// 判断当前节点是否为 TreeNode 类型
// 如果是 TreeNode 类型,使用红黑树的方式找出对应节点或新增节点并返回
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果是链表类型
for (int binCount = 0; ; ++binCount) {
// 如果下一个节点为 null,进行节点追加操作
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果当前节点的数量大于等于 8,将链表转换为 TreeNode
treeifyBin(tab, hash);
break;
}
// 如果链表中存在该 key,因为已经将该节点赋值给 e,所以直接结束循环,等待下面的方法对值进行更新
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果 e 不等于 null,证明存在旧节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 更新原本旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 空实现
afterNodeAccess(e);
return oldValue;
}
}
// 操作数加1
++modCount;
// 如果总数加1大于threshold,进行扩容
if (++size > threshold)
resize();
// 空实现
afterNodeInsertion(evict);
return null;
}
get(K key)
1
2
3
4
5public V get(Object key) {
Node<K,V> e;
// 调用 getNode 方法进行获取 Node 对象
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode(int hash, Object key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果 table 为 null 或 table 的长度为0 或 根据 hash 计算的节点为 null,返回null,否则进行查找
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查第一个节点是否为当前 key
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果第二个节点不是 null
if ((e = first.next) != null) {
// 如果是 TreeNode 类型
if (first instanceof TreeNode)
// 使用红黑树的查找方法进行查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 循环判断 hash,key是否相等,如果相等,返回否则一直到链表结束
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove(Object key)
1
2
3
4
5
6public V remove(Object key) {
Node<K,V> e;
// 调用 removeNode 方法
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 如果 table 为 null 或 table 的长度为0 或 根据 hash 计算的节点为 null,返回null,否则进行查找
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 检查第一个节点是否为当前 key,如果是将其赋值给 node 变量
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 如果第二个节点不为空
else if ((e = p.next) != null) {
// 如果是 TreeNode 类型
if (p instanceof TreeNode)
// 使用红黑树的查找方法进行查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 循环判断 hash,key 是否相等,如果相等,赋值给 node 变量
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果根据 key 找到对应节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 根据该节点的类型进行对应的删除操作
if (node instanceof TreeNode)
// 如果是 TreeNode 类型,按照红黑树的方式删除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果是第一个,将table中的该索引指向第二个节点
tab[index] = node.next;
else
//如果是在链表中,将 node 的前一个节点的 next 指向 node 的节点的 next
p.next = node.next;
// 操作数加1
++modCount;
// 总数减1
--size;
// 空实现
afterNodeRemoval(node);
return node;
}
}
return null;
}
resize()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115final Node<K,V>[] resize() {
// 原数组
Node<K,V>[] oldTab = table;
// 原容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原threshold值(容量*负载因子)
int oldThr = threshold;
int newCap, newThr = 0;
// 如果原容量大于 0
if (oldCap > 0) {
// 如果数组长度达到最大上限,更新 threshold,不进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 否则容量*2 threshold*2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果在构造函数中设置了初始 threshold 使用 HashMap(int initialCapacity, float loadFactor)创建 Map
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 如果原容量且原threshold 都为0,进行初始化操作
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果 newThr == 0 ( oldThr > 0 为 true 时该判断才会为 true)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 计算新的 threshold
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 更新 threshold
threshold = newThr;
"rawtypes","unchecked"}) ({
// 根据newCap 构造一个新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 更显table引用
table = newTab;
// 如果 oldTab 不为 null,表明为扩容操作,否则为table初始化操作
if (oldTab != null) {
// 遍历原数组中的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果该元素不为 null
if ((e = oldTab[j]) != null) {
// 将原数组该索引设置为null,方便回收
oldTab[j] = null;
// 如果该节点下一个元素为null,表明该节点只存在一个元素
if (e.next == null)
// 将该节点设置到新数组中去
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果节点为 TreeNode 类型,按照对应方式设置到新数组中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 如果是数量大于1的链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 此处操作跟hash计算索引有关
// 在 HashMap 中,索引的计算方法为 (n - 1) & hash
// 所以,在进行扩容操作 (n*2) 后,该计算结果可能导致变更
// 例如
// 有一个值为 111001 的 hash
// 扩容前 n=16(10000) n-1=15(1111) (n - 1) & hash = 1111 & 111001= 001001
// 扩容后 n=32(100000) n-1=31(11111) (n - 1) & hash = 11111 & 111001= 011001
// 假如 hash 值为 101001
// 那么会发现扩容前 1111 & 101001 = 001001
// 扩容后 11111 & 101001 = 001001
// 所以可知,在进行扩容操作时,主要按照 hash 与 原数组长度中1的对应位置有关
// 如果 hash 中对应的位置为0,扩容后索引结果不变
// 不为0,表示索引结果为原结果+原数组长度
// 而 hash 中该对应位置的值只存在俩种可能 0,1
// 所以在该节点中的数据大约有一半索引不变,一半为原索引+原数组长度
// 通过 e.hash & oldCap 的方式可以得知 hash 在 oldCap 1对应的位置是否为0或1
if ((e.hash & oldCap) == 0) {
// 如果为0,证明扩容后索引的计算依然与扩容前一致
// 组装链表
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//如果不为0,则表明扩容后索引的计算依然与扩容不一致,所以需要移动到新索引,新索引的位置为旧索引加oldCap
// 组装链表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 如果链表不为 null,设置到新数组中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
服务器处理富文本编辑器提交的内容时,因排版的需求不能对 HTML 标签进行转义,但为了防止 XSS 攻击,又必须过滤掉其中的 JS 代码,在 Java 中使用 Jsoup 正好可以满足此要求
Jsoup 使用标签 白名单 的机制用来进行防止 XSS 攻击,假设白名单中只允许 p 标签存在,此时在一段 HTML 代码中,只能存在 p 标签 ,其他标签将会被清除只保留被标签所包裹的内容,具体详情可查看参考资料
1 | <dependency> |
创建进行测试的 HTML 代码
1 | String testHtml = "<div class='div'style='height: 100px;'>div 标签的内容 </div><p class='div'style='width: 50px;'>p 标签的内容 </p>"; |
创建一个白名单对象
1 | Whitelist whitelist = new Whitelist(); |
添加允许使用的标签,此时只允许 p 标签存在
1 | whitelist.addTags("p"); |
对测试代码进行过滤,过滤规则就是创建的白名单
1 | String result1 = Jsoup.clean(testHtml, whitelist); |
此时我们发现 div 标签已经被过滤掉了,但是 p 标签中的属性也同时也被过滤掉了,因为白名单只允许了 p 标签,但是并未对属性加入白名单,此时将 p 标签中的 class 属性加入白名单中,再进行一次过滤
1 | whitelist.addAttributes("p","class"); |
此时可见 class 属性已被允许存在,另外 whitelist.addAttributes(String tag, String... keys)
中的 keys 是一个可变数组,由此可知我们可以同时添加多个属性,如 whitelist.addAttributes("p","class","style","title")
,whitelist.addTags(String... tags)
方法同理
此时假如我在白名单中添加了多个标签,那么如何才能快速对所有标签添加共同属性
1 | whitelist.addAttributes(":all","style","title"); |
:all
表明给白名单中的所有标签添加 style,title 属性,此时我们将 div,h1 标签放入白名单,再进行测试
1 | whitelist.addTags("div","h1"); |
结果分析
白名单对象 | 标签 | 说明 |
---|---|---|
none | 无 | 只保留标签内文本内容 |
simpleText | b,em,i,strong,u | 简单的文本标签 |
basic | a,b,blockquote,br,cite,code,dd, dl,dt,em,i,li,ol,p,pre,q,small,span, strike,strong,sub,sup,u,ul | 基本使用的标签 |
basicWithImages | basic 的基础上添加了 img 标签 及 img 标签的 src,align,alt,height,width,title 属性 | 基本使用的加上 img 标签 |
relaxed | a,b,blockquote,br,caption,cite, code,col,colgroup,dd,div,dl,dt, em,h1,h2,h3,h4,h5,h6,i,img,li, ol,p,pre,q,small,span,strike,strong, sub,sup,table,tbody,td,tfoot,th,thead,tr,u,ul | 在 basicWithImages 的基础上又增加了一部分部分标签 |
如果没有图片上传的需求,使用 basic
,否则使用 basicWithImages
在刚才测试的时候,会发现 Jsoup.clean()方法返回的代码已经被进行格式化,在标签及标签内容之间添加了 \n 回车符,如果不需要的话,可以使用 Jsoup.clean(testHtml, "", whitelist, new Document.OutputSettings().prettyPrint(false));
进行过滤
1 | import org.jsoup.Jsoup; |
这是我在 慕课网 观看 SpringMVC 数据绑定入门 所做的学习笔记
其中包含对 List,Set,Map,JSON,XML 的数据绑定以及 PropertyEditor、Formatter、Converter 三种自定义类型转换器
特点
List 对象绑定需要建立一个 List 集合包装类
1 | public class User { |
List 的长度为前台传入的 集合最大下标加 1
1 |
|
测试数据
http://localhost/list2?userList[0].name=a&userList[1].name=b
listUserWrapSize:2 listUserWrap:ListUserWrap(userList=[User(age=0, name=a), User(age=0, name=b)])
http://localhost/list2?userList[0].name=a&userList[1].name=b&userList[3].name=c
listUserWrapSize:4 listUserWrap:ListUserWrap(userList=[User(age=0, name=a), User(age=0, name=b), User(age=0, name=null), User(age=0, name=c)])
特点
设置 Set 长度时需要注意重写对象的 hashCode,equals 方法,否者后面的会掩盖前面的
1 | public class SetUserWrap { |
测试数据
http://localhost/set?userSet[0].name=a 因为预先定义的第二个对象的 name 为 b,所以此处返回 b
setUserWrapSize:2 setUserWrap:SetUserWrap(userSet=[User(age=0, name=a), User(age=0, name=b)])
http://localhost/set?userSet[0].name=a&userSet[1].name=bbb
setUserWrapSize:2 setUserWrap:SetUserWrap(userSet=[User(age=0, name=a), User(age=0, name=bbb)])
特点
建立一个 Map 包装类
1 | public class MapUserWrap { |
测试数据
http://localhost/map?userMap["a"].name=a&userMap["b"].name=b
mapUserWrapSize:2 mapUserWrap:MapUserWrap(userMap={a=User(age=0, name=a), b=User(age=0, name=b)})
http://localhost/map?userMap["a"].name=a&userMap["a"].name=b
mapUserWrapSize:1 mapUserWrap:MapUserWrap(userMap={a=User(age=0, name=a,b)})
前台在 body 区域传入以下类型格式字符串
1 | { |
1 |
|
前台在 body 区域传入以下类型格式字符串
1 | <xmluser> |
建立一个 XML 包装类
1 | "xmluser")// 根节点标签名 (name = |
1 |
|
在 Controller 中编写一个带有 InitBinder 注解的方法,传入 WebDataBinder 对象,使用该对象注册指定类型的转换关系,对该方法所在 Controller 中使用该类型的方法参数有效
1 |
|
根据
String
类型自定义转换规则转换成需要的类型,需要实现org.springframework.format.Formatter<T>
接口,T
为想要转换的结果类型
创建自定义 Formatter
1 | public class DateFormatter implements Formatter<Date> { |
将自定义的 Formatter 注入到 SpringMVC 默认的 FormattingConversionServiceFactoryBean 中,同时将默认转换规则服务类配置为已经被注入的 bean 对象
1 | <mvc:annotation-driven conversion-service="myFormattingConversionService"/> |
自己指定数据来源类型及转换结果类型,相比 Formatter 更为灵活,需要实现
org.springframework.core.convert.converter.Converter<S, T>
接口,S
为来源类型,T
为结果类型
创建自定义 Converter
1 | public class DateConverter implements Converter<String, Date> { |
将自定义的 Converter 注入到 SpringMVC 默认的 FormattingConversionServiceFactoryBean 中,同时将默认转换规则服务类配置为已经被注入的 bean 对象
1 | <mvc:annotation-driven conversion-service="myFormattingConversionService"/> |
java.util.concurrent.locks
中,为我们提供了可重入锁,读写锁,及超时获取锁的方法。 为我们提供了完好的支持,但是在分布式系统中,当多个应用需要共同操作某一个资源时。 我么就无法使用 JDK 来实现了,这时就需要使用一个外部服务来为此进行支持,现在我们选用 ZooKeeper + Curator 来完成分布式锁如果 ZooKeeper 版本为 3.4.x,请进行兼容处理
下载、安装、启动 ZooKeeper,可以查看这篇博文 ZooKeeper 的安装、配置、启动和使用(一)——单机模式
如果想跳过这一步的话请参考最下面的便捷测试
创建一个 Maven 工程,然后引入所需资源1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
在 src/test/java 下创建一个 DistributedLockDemo 类
基本代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class DistributedLockDemo {
// ZooKeeper 锁节点路径, 分布式锁的相关操作都是在这个节点上进行
private final String lockPath = "/distributed-lock";
// ZooKeeper 服务地址, 单机格式为:(127.0.0.1:2181), 集群格式为:(127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183)
private String connectString;
// Curator 客户端重试策略
private RetryPolicy retry;
// Curator 客户端对象
private CuratorFramework client;
// client2 用户模拟其他客户端
private CuratorFramework client2;
// 初始化资源
public void init() throws Exception {
// 设置 ZooKeeper 服务地址为本机的 2181 端口
connectString = "127.0.0.1:2181";
// 重试策略
// 初始休眠时间为 1000ms, 最大重试次数为 3
retry = new ExponentialBackoffRetry(1000, 3);
// 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为链接超时时间
client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
client2 = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
// 创建会话
client.start();
client2.start();
}
// 释放资源
public void close() {
CloseableUtils.closeQuietly(client);
}
}
1 |
|
1 | public void sharedReentrantLock() throws Exception { |
1 |
|
测试结果如下:
写入数据 1
写入数据 2
读取数据 2
写入数据 3
读取数据 3
写入数据 4
读取数据 4
读取数据 4
写入数据 5
读取数据 5
读取数据线程总是能看到最新写入的数据
1 |
|
1 |
|
分布式锁服务宕机,ZooKeeper 一般是以集群部署,如果出现 ZooKeeper 宕机,那么只要当前正常的服务器超过集群的半数,依然可以正常提供服务
持有锁资源服务器宕机,假如一台服务器获取锁之后就宕机了,那么就会导致其他服务器无法再获取该锁。 就会造成 死锁 问题,在 Curator 中,锁的信息都是保存在临时节点上,如果持有锁资源的服务器宕机,那么 ZooKeeper 就会移除它的信息,这时其他服务器就能进行获取锁操作
为了测试上面的代码,我们需要下载、安装、启动一个 ZooKeeper 服务,然后将该服务地址配置为 connectString。 如果更换环境的话又需要重新安装,未免麻烦了点。 Curator 为我们提供一个专门用于开发、测试的便捷方法,让我们更加专注于编写与 ZooKeeper 相关的程序。首先需要导入 curator-test 测试包1
2
3
4
5
6<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-test</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
在这个包中为我们提供了一个 TestingServer 类,主要用法如下
构造方法有多个,但是主要使用到的有这两个1
2TestingServer()
TestingServer(int port, File tempDirectory)
port 为端口
tempDirectory 为临时的 dataDir 目录
如果调用 TestingServer()
方法构造,会获取一个空闲端口,同时在 java.io.tmpdir
创建一个临时目录当作本次的 dataDir
目录
然后使用以下方法创建客户端1
2
3TestingServer server=new TestingServer();
// server.getConnectString() 方法会返回可用的服务链接地址, 如: 127.0.0.1:2181
CuratorFramework client=CuratorFrameworkFactory.newClient(server.getConnectString(), retry);
另外在测试完成记得进行资源释放1
2
3
4
5
public void close() {
CloseableUtils.closeQuietly(client);
CloseableUtils.closeQuietly(server);
}
TestingServer 能为我们简单的启动一个 ZooKeeper 服务器,但是如果需要进行集群测试呢?这个时候我们可以使用 TestingCluster 启动 ZooKeeper 集群
TestingCluster 同样提供多个构造器,但是主要使用以下两个1
2TestingCluster(int instanceQty)
TestingCluster(InstanceSpec... specs)
instanceQty 是集群的数量
specs 是 InstanceSpec 的变长参数
InstanceSpec 的创建方法可以参考 TestingServer 的构造方法实现
然后创建客户端使用以下方法1
2
3TestingCluster server=new TestingCluster(3);
// server.getConnectString() 方法会返回可用的服务链接地址, 如: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
CuratorFramework client=CuratorFrameworkFactory.newClient(server.getConnectString(), retry);
同样请记得释放资源
]]>通过 使用 zxing 生成二维码 我们可以实现简单二维码的生成, 但是二维码显示却过于单调, 本文变讲述如何利用 thumbnailator 为我们的二维码添加 LOGO
thumbnailator 是一个缩略图工具类库, 但它除了能缩略图片外, 还提供裁剪, 旋转, 水印等功能, 此次我们便借助它的水印 API 实现以上需求
1 | <dependency> |
1 | String qrCodeFilePath = "src/qrCode.jpg"; |
根据文件路径生成一个图片构造对象( Thumbnails.of()
方法还可以接收 BufferedImage
,InputStream
,URL
,String
的可变参数类型)
1 | Thumbnails.Builder<File> builder = Thumbnails.of(new File(qrCodeFilePath)); |
创建一个水印对象, 水印对象需要三个参数 Position position
, BufferedImage watermarkImg
,float opacity
, 其中 Position
是水印的坐标,BufferedImage
是水印图片,opacity
是不透明度, 值为 [0-1] 之间, 1 代表不透明.
1 | BufferedImage bufferedImage = ImageIO.read(new File(logoFilePath)); |
为二维码设置水印, 并设置缩略比例为 1(即不压缩), 输出到一个新文件中(outputFormat()
为指定输出格式, 如: jpg,png)
1 | builder.watermark(watermark).scale(1F).toFile(new File("src/logoQrCode.png")); |
生成后的二维码
通过以上方法我们能够简单的为二维码添加 LOGO, 但是实际使用时远没有这样简单, 有以下几个问题
logo 图片的尺寸可能并不固定, 可能有大有小, 这样就会导致 logo 在二维码中太小或太大
这时我们可以在创建 BufferedImage
时将原图压缩 / 放大至指定尺寸, 也可以使用二维码的尺寸乘以一定比例
1 | //forceSize(int width, int height) 指将图片强制压缩为指定宽高, 如不强制, 可使用 size(int width, int height) |
图片显示, logo 图片可能不是一个图片文件, 同时生成好之后的图片希望直接输出在页面上Thumbnails.of()
可以接收 BufferedImage
,InputStream
,URL
,String
,File
的可变 ( 批量处理需要 ) 参数类型ImageIO.read()
可以接收 File
,ImageInputStream
,InputStream
,URL
参数类型
以下为 SpringMVC 动态生成带 LOGO 二维码示例,QRCodeUtil.toBufferedImage()
具体实现请看 生成二维码之 Java (Google zxing) 篇
1 | /** |
1 | /** |
以上案例可知 thumbnailator 水印功能的强大. 然而除了实现以上案例外, 我们也可以根据水印功能实现另外一种需求.
比如公司现在有一个推广需求, 让用户为我们的产品进行宣传, 达到多少次就进行奖励. 传统的实现方式是一段文字再配上一个专属链接. 但是此方式往往太过单调, 枯燥. 这个时候如果我们让设计提供一张内容丰富, 带有二维码的宣传图片, 然后根据专属链接生成二维码与宣传图片进行组合. 这时用户就有了一张自己的专属宣传图片. 此时通过直接宣传专属图片往往会比文字加链接有着更好的效果.
注: 此功能在图片合成时需要对二维码的位置进行定位, 此时可使用 Position
的实现类 Coordinate
完成
使用如下:1
2
3//Coordinate 存在一个 Coordinate(int x, int y) 构造函数, x 为水印距离底图左边的像素, y 为上边
BufferedImage bufferedImage = ImageIO.read(new File(logoFilePath));
Watermark watermark = new Watermark(new Coordinate(100, 100), bufferedImage, 1F);
zxing-javase
1 | <dependency> |
1 | import com.google.zxing.BarcodeFormat; |
1 | "/qrcode") (value = |
在一些论坛,博客等项目中。 用户发送的帖子,文章内容可能会存在太长的情况。 这时如果用户的网速不佳,或者网络不稳定。 那么将会面临 响应过慢、发送失败 的情况。 如果网站还有自动保存的功能的话,这种情况会明显增多。 这时如果将传输的内容在本地进行压缩上传,然后在服务器进行解压。 对长文本的处理能够得到完好解决,同时极大减少了移动端用户的网络开销。
本文创作思路来源于 Jerry Qu 的博客 如何压缩 HTTP 请求正文
在前台对请求正文使用 pako_deflate.js
进行本地 gzip
格式压缩
在后台使用 Java
对请求正文进行解压
因为只在前台进行压缩,所以只需引用 pako 的压缩专用文件 pako_deflate.min.js
又因为我在项目中主要使用 jQuery 发送 Ajax 请求,所以引入 jQuery1
2<script src="jquery-2.2.4.min.js"></script>
<script src="pako_deflate.min.js"></script>
将发送的参数转换为 JSON 字符串1
2
3
4var params = encodeURIComponent(JSON.stringify({
title: "标题",
content: "内容"
}));
gzip 虽然能极大的压缩请求正文。 但是如果内容过小,压缩后内容反而会增大,经测试,对于 params.length
大于 1000 的文本压缩效果能够达到 60% 以上,所以在压缩前,需要对内容进行判断1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var params = encodeURIComponent(JSON.stringify({
title: title,
content: content
}));
var compressBeginLen = params.length;
if (compressBeginLen > 1000) {
// 对 JSON 字符串进行压缩
// pako.gzip(params) 默认返回一个 Uint8Array 对象, 如果此时使用 Ajax 进行请求, 参数会以数组的形式进行发送
// 为了解决该问题, 添加 {to: "string"} 参数, 返回一个二进制的字符串
params = pako.gzip(params, {to: "string"});
}
$.ajax({
url: "/gzip",
data: params,
dataType: "text",
type: "post",
headers: {
// 如果 compressBeginLen 大于 1000, 标记此次请求的参数使用了 gzip 压缩
"Content-Encoding": params.length>1000?"gzip":""
},
success: function (data) {
//dosomething
}
})
首先获取 Content-Encoding
请求头,根据该请求头中的内容进行逻辑处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"/gzip") (value =
public String gzip(HttpServletRequest request) {
String params = "";
try {
// 获取 Content-Encoding 请求头
String contentEncoding = request.getHeader("Content-Encoding");
if (contentEncoding != null && contentEncoding.equals("gzip")) {
// 获取输入流
BufferedReader reader = request.getReader();
// 将输入流中的请求实体转换为 byte 数组, 进行 gzip 解压
byte[] bytes = IOUtils.toByteArray(reader, "iso-8859-1");
// 对 bytes 数组进行解压
params = GZIPUtil.uncompress(bytes);
} else {
BufferedReader reader = request.getReader();
params = IOUtils.toString(reader);
}
if (params != null && params.trim().length() > 0) {
// 因为前台对参数进行了 url 编码, 在此进行解码
params = URLDecoder.decode(params, "utf-8");
// 将解码后的参数转换为 json 对象
JSONObject json = JSONObject.fromObject(params);
// 从 json 对象中获取参数进行后续操作
System.out.println("title:\t" + json.getString("title"));
System.out.println("content:\t" + json.getString("content"));
}
} catch (IOException e) {
e.printStackTrace();
}
return params;
}
Java gzip 解压方法 GZIPUtil.uncompress
参考 Java 使用 GZIP 进行压缩和解压缩(GZIPOutputStream,GZIPInputStream)一文而成1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/**
* 解压 gzip 格式 byte 数组
* @param bytes gzip 格式 byte 数组
* @param charset 字符集
*/
public static String uncompress(byte[] bytes, String charset) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream byteArrayOutputStream = null;
ByteArrayInputStream byteArrayInputStream = null;
GZIPInputStream gzipInputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayInputStream = new ByteArrayInputStream(bytes);
gzipInputStream = new GZIPInputStream(byteArrayInputStream);
// 使用 org.apache.commons.io.IOUtils 简化流的操作
IOUtils.copy(gzipInputStream, byteArrayOutputStream);
return byteArrayOutputStream.toString(charset);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放流资源
IOUtils.closeQuietly(gzipInputStream);
IOUtils.closeQuietly(byteArrayInputStream);
IOUtils.closeQuietly(byteArrayOutputStream);
}
return null;
}
另外 Jerry Qu 实现了一个服务器使用 Node.js 解压的 DEMO 并提供 deflate,zlib,gzip 三种压缩,解压方式
在网页开发中,大部分网页都具有相同的页头,页尾,菜单等模块。 一般情况下我们会将这些共用的代码单独抽取成一个页面,然后进行包含。 虽然这样能够达到代码复用的效果,但是如果引入的页面过多,一来会带来修改不变的效果,二来依然会形成多个页面使用相同的代码 (页面包含代码),此时我们可以使用 SiteMesh3 来妥善解决这个问题
进行页面包含后,能达到一些共用页面的代码复用,但是如果页面布局复杂的话,会存在大量的页面包含代码,依然给我们带来了不便
使用 SiteMesh3 时需要先定义一个装饰器,在这个装饰器中我们可以定义页面的布局,然后配置动态内容输出位置即可
示例如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<html lang="zh-cmn-Hans">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<!--<sitemesh:write property="title"/> 会输出原始页面的 title 标签里面的内容 -->
<title><sitemesh:write property="title"/></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="renderer" content="webkit"/>
<link rel="stylesheet" type="text/css" href="/static/css/web.css"/>
<!--<sitemesh:write property="head"/> 会输出原始页面 head 标签里面的内容 (不包括 title 标签)-->
<sitemesh:write property="head"/>
</head>
<body>
<header>header</header>
<!--<sitemesh:write property="body"/> 会输出原始页面 body 标签里面的内容 -->
<sitemesh:write property="body"/>
<footer>footer</footer>
<script type="text/javascript" src="/static/js/web.js"></script>
</body>
</html>
假如此时存在一个这样的原始页面1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>title</title>
<meta name="keywords" content="SiteMesh3">
</head>
<body>
hello,world
</body>
</html>
经过 SiteMesh3 装饰后的页面如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html lang="zh-cmn-Hans">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>title</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="renderer" content="webkit"/>
<link rel="stylesheet" type="text/css" href="/static/css/web.css"/>
<meta name="keywords" content="SiteMesh3">
</head>
<body>
<header>header</header>
hello,world
<footer>footer</footer>
<script type="text/javascript" src="/static/js/web.js"></script>
</body>
</html>
可以发现使用 SiteMesh3 进行装饰能够让我们更加专注于一些与页面独有的代码逻辑,能避免相同的代码在多个页面重复出现
通过上面的一个小例子,我们可以发现使用 SiteMesh3 需要一个装饰器页面。 由此可以牵扯出另外几个问题,对哪些页面进行装饰? 使用哪个装饰页面装饰?通过以下配置可以完成这些疑问
首先我们需要引入 SiteMesh3 相关的 jar 包1
2
3
4
5<dependency>
<groupId>org.sitemesh</groupId>
<artifactId>sitemesh</artifactId>
<version>3.0.1</version>
</dependency>
其次,SiteMesh3 会对一些页面进行装饰,所以我们需要添加一个过滤器来进行页面过滤1
2
3
4
5
6
7
8
9<filter>
<filter-name>sitemesh</filter-name>
<filter-class>org.sitemesh.config.ConfigurableSiteMeshFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>sitemesh</filter-name>
<!-- 如果项目中的页面地址以 .do 或者 .action 结尾可以使用 *.do 或 *.action-->
<url-pattern>/*</url-pattern>
</filter-mapping>
同时我们还需要配置装饰器,要装饰的页面,不需要装饰的页面等信息
在 WEB-INF
目录下创建一个 sitemesh3.xml
文件,请确保路径正确,SiteMesh3 会根据 /WEB-INF/sitemesh3.xml
加载文件
文件内容如下1
2
3
4
5
6
7
<sitemesh>
<!-- path 是指要进行装饰的页面 如 /index.jsp 对应 http://localhost:8080/index.jsp -->
<!-- decorator 是指装饰器页面 /decorators/default.jsp 对应位置为 src/main/webapp/decorators/default.jsp-->
<!-- sitemesh3 会在 index.jsp 页面返回时提取 title head body 中的内容, 然后在 default.jsp 根据输出标签进行对应输出 -->
<mapping path="/index.jsp" decorator="/decorators/default.jsp"/>
</sitemesh>
此时我们可以将上面演示中的文件内容建立 index.jsp
和 default.jsp
页面,然后进行测试访问,会发现能达到演示中的效果
通过上面的简单配置我们可以实现一个最基本的页面装饰,与此同时 SiteMesh3 还支持一些更为高级的配置
默认装饰器
1 | <!-- 如果不填写 path 路径, 则 SiteMesh3 会在找不到匹配的装饰器时, 使用这个装饰器进行装饰 --> |
多个装饰器
1 | <mapping> |
装饰器会按照配置的先后顺序进行装饰
假如
multi.jsp 中 body 内容为 0,multi_1.jsp 中内容为 1 同时在下面输出原始页面中的 body 内容1
2
3
4<body>
<h3>1</h3>
<sitemesh:write property="body"/>
</body>
multi_2.jsp 中内容为 2 同时在下面输出原始页面中的 body 内容
最终返回的页面内容为
2
1
0
不需要装饰
1 | <!-- SiteMesh3 会先判断是否不需要装饰, 然后再判断是否存在匹配的装饰器 --> |
MIME 类型
1 | <!-- |
自定义输出标签
1 | <!-- |
ExpandTagRuleBundle 代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14public class ExpandTagRuleBundle implements TagRuleBundle {
public void install(State defaultState, ContentProperty contentProperty, SiteMeshContext siteMeshContext) {
defaultState.addRule("header", new ExportTagToContentRule(siteMeshContext, contentProperty.getChild("header"), false));
defaultState.addRule("menu", new ExportTagToContentRule(siteMeshContext, contentProperty.getChild("menu"), false));
defaultState.addRule("footer", new ExportTagToContentRule(siteMeshContext, contentProperty.getChild("footer"), false));
}
public void cleanUp(State defaultState, ContentProperty contentProperty, SiteMeshContext siteMeshContext) {
}
}
此时可以使用 <sitemesh:write property="header"/>
标签在装饰器中输出原始页面的 header 标签值
其他说明
title
, head
, body
标签的实现请参考 org.sitemesh.content.tagrules.html.CoreHtmlTagRuleBundle
title
, head
, body
等自定义标签只会提取原始页面中的第一个找到的标签默认配置文件路径为 /WEB-INF/sitemesh3.xml
,如果需要自定义配置路径,请在配置 filter
时配置 configFile
属性,如将 sitemesh3.xml
配置文件放在 resources
文件夹中
1 | <filter> |
如果装饰器是 html
文件会存在中文乱码的问题,在 SpringMVC 中可在 web.xml
文件配置如下过滤器
1 | <filter> |
在 SpringMVC 中因为 SiteMesh3 需要直接访问到装饰页面,所以需要增加如下标签
1 | <mvc:default-servlet-handler/> |
如果装饰器为 JSP 页面,则可以使用 EL 表达式获取原始页面中的属性
假如在 SpringMVC 中设置如下属性
1 | "/index") (value = |
则可以在方法返回页面 /index.jsp
和该 url 的装饰页面 /decorators/default.jsp
中使用 EL 表达式获取 date
属性
SiteMesh3 官网文档
SiteMesh3 GitHub
本文讲述如何使用 jquery-qrcode 生成二维码
1 | <!-- 引入 jQuery 与 jquery.qrcode--> |
由于 qrcode 的编码原因会导致中文乱码
如果能确保二维码中的内容为 链接 , 那么在使用前对内容进行 encodeURI 编码即可1
$("#qrcode").qrcode(encodeURI("https://google.com"));
如果不是链接, 需要对二维码内容进行编码, 方法来源于 http://justcoding.iteye.com/blog/22130341
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function toUtf8(str) {
var out, i, len, c;
out = "";
len = str.length;
for(i = 0; i < len; i++) {
c = str.charCodeAt(i);
if ((c>= 0x0001) && (c <= 0x007F)) {
out += str.charAt(i);
} else if (c> 0x07FF) {
out += String.fromCharCode(0xE0 | ((c>> 12) & 0x0F));
out += String.fromCharCode(0x80 | ((c>> 6) & 0x3F));
out += String.fromCharCode(0x80 | ((c>> 0) & 0x3F));
} else {
out += String.fromCharCode(0xC0 | ((c>> 6) & 0x1F));
out += String.fromCharCode(0x80 | ((c>> 0) & 0x3F));
}
}
return out;
}
$("#qrcode").qrcode(toUtf8("https://google.com"));