Kotlin 单例模式详解

单例模式很熟悉了,在Java中有各种创建姿势,算是比较麻烦的,这里就不再赘述了。

Object

那么如何在kotlin中实现单例模式呢?请看代码

1
2
object JsonObjectMapper {
}

仅需简单的把class关键字替换为object就完成了!

1
2
3
4
5
// Kotlin 调用
JsonObjectMapper

// Java调用
JsonObjectMapper.INSTANCE

究竟Kotlin封装了什么细节?让我们一探究竟吧:
通过Tools > Kotlin > Show Kotlin Bytecode 显示字节码,看看翻译后的Java代码
image.png

image.png

翻译成Java后,发现就是一个最简单的饿汉式单例而已。

Companion Object

懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SingletonDemo private constructor() {
companion object {
private var instance: SingletonDemo? = null
get() {
if (field == null) {
field = SingletonDemo()
}
return field
}
fun get(): SingletonDemo{
return instance!!
}
}
}

线程安全的懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SingletonDemo private constructor() {
companion object {
private var instance: SingletonDemo? = null
get() {
if (field == null) {
field = SingletonDemo()
}
return field
}
@Synchronized
fun get(): SingletonDemo{
return instance!!
}
}
}

双重校验锁式(Double Check)

1
2
3
4
5
6
class SingletonDemo private constructor() {
companion object {
val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
SingletonDemo() }
}
}

静态内部类式

1
2
3
4
5
6
7
8
class SingletonDemo private constructor() {
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder = SingletonDemo()
}
}

Quartz失火指令、补偿执行机制配置

处理规则

调度(scheduleJob)或恢复调度(resumeTrigger,resumeJob)后不同的misfire对应的处理规则如下:

CronTrigger

  • withMisfireHandlingInstructionDoNothing

    • 不触发立即执行
    • 等待下次Cron触发频率到达时刻开始按照Cron频率依次执行
  • withMisfireHandlingInstructionIgnoreMisfires

    • 以错过的第一个频率时间立刻开始执行
    • 重做错过的所有频率周期后
    • 当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行
  • withMisfireHandlingInstructionFireAndProceed

    • 以当前时间为触发频率立刻触发一次执行
    • 然后按照Cron频率依次执行

SimpleTrigger

  • withMisfireHandlingInstructionFireNow

    • 以当前时间为触发频率立即触发执行
    • 执行至FinalTIme的剩余周期次数
    • 以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
    • 调整后的FinalTime会略大于根据starttime计算的到的FinalTime值
  • withMisfireHandlingInstructionIgnoreMisfires

    • 以错过的第一个频率时间立刻开始执行
    • 重做错过的所有频率周期
    • 当下一次触发频率发生时间大于当前时间以后,按照Interval的依次执行剩下的频率
    • 共执行RepeatCount+1次
  • withMisfireHandlingInstructionNextWithExistingCount

    • 不触发立即执行
    • 等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数
    • 以startTime为基准计算周期频率,并得到FinalTime
    • 即使中间出现pause,resume以后保持FinalTime时间不变
  • withMisfireHandlingInstructionNowWithExistingCount

    • 以当前时间为触发频率立即触发执行
    • 执行至FinalTIme的剩余周期次数
    • 以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
    • 调整后的FinalTime会略大于根据starttime计算的到的FinalTime值
  • withMisfireHandlingInstructionNextWithRemainingCount

    • 不触发立即执行
    • 等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数
    • 以startTime为基准计算周期频率,并得到FinalTime
    • 即使中间出现pause,resume以后保持FinalTime时间不变
  • withMisfireHandlingInstructionNowWithRemainingCount

    • 以当前时间为触发频率立即触发执行
    • 执行至FinalTIme的剩余周期次数
    • 以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
    • 调整后的FinalTime会略大于根据starttime计算的到的FinalTime值MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
    • 此指令导致trigger忘记原始设置的starttime和repeat-count
    • 触发器的repeat-count将被设置为剩余的次数
    • 这样会导致后面无法获得原始设定的starttime和repeat-count值

示例代码

示例代码为CronTrigger

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
interface QuartzSchedulerService {

/**
* 失火指令(cronTrigger)
*/
enum class CronMisfireInstruction {
WithMisfireHandlingInstructionDoNothing, // 忽略,等待下次Cron触发频率到达时刻开始按照Cron频率依次执行
WithMisfireHandlingInstructionFireAndProceed, // 以当前时间为触发频率立刻触发一次执行,然后按照Cron频率依次执行(默认)
WithMisfireHandlingInstructionIgnoreMisfires // 以错过的第一个频率时间立刻开始执行, 重做错过的所有频率周期后, 当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行
}
fun submitJob(jobName: String, jobGroupName: String, jobClazz: Class<out Job>, jobDataMap: JobDataMap,
cronExpression: String, cronMisfireInstruction: CronMisfireInstruction): Boolean

}


@Service
class QuartzSchedulerServiceImpl(
var scheduler: Scheduler
) : QuartzSchedulerService {

private val log: Logger = LoggerFactory.getLogger(this.javaClass)

override fun submitJob(jobName: String, jobGroupName: String, jobClazz: Class<out Job>, jobDataMap: JobDataMap, cronExpression: String,
cronMisfireInstruction: QuartzSchedulerService.CronMisfireInstruction): Boolean {
val jobKey = JobKey.jobKey(jobName, jobGroupName)
scheduler.getJobDetail(jobKey).let {
this.removeJob(jobName, jobGroupName) // 定时任务已存在,先删除。
}

try {
// 任务名,任务组,任务执行类
val jobDetail = JobBuilder.newJob(jobClazz)
.withIdentity(jobName, jobGroupName)
.usingJobData(jobDataMap)
.build()

// 配置失火指令
val cronSchedule = CronScheduleBuilder.cronSchedule(cronExpression)
when (cronMisfireInstruction) {
QuartzSchedulerService.CronMisfireInstruction.WithMisfireHandlingInstructionDoNothing -> {
cronSchedule.withMisfireHandlingInstructionDoNothing()
}
QuartzSchedulerService.CronMisfireInstruction.WithMisfireHandlingInstructionIgnoreMisfires -> {
cronSchedule.withMisfireHandlingInstructionIgnoreMisfires()
}
QuartzSchedulerService.CronMisfireInstruction.WithMisfireHandlingInstructionFireAndProceed -> {
cronSchedule.withMisfireHandlingInstructionFireAndProceed()
}
}

// 触发器名称,触发时间
val cronTrigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, jobGroupName)
.startNow()
.withSchedule(cronSchedule)
.build()

// 加入调度器容器
scheduler.scheduleJob(jobDetail, cronTrigger)
log.info("已向调度服务器添加任务:[$jobGroupName]($jobName)")
} catch (e: Throwable) {
throw BusinessException("添加任务[$jobGroupName]($jobName)到调度服务器失败")
}
// 启动调度器
return this.start()
}
}

解决HttpServletRequest.inputStream复用问题

众所周知,request对象中的inputStream只能读取一次,下次再读就没有了,在一些场景中,这种特性是不适用的:比如需要在拦截器中读取请求体,然后做相关的参数校验,做完校验后请求打到Controller中就读取不到了。

解决的方法很简单,通过HttpServletRequestWrapperHttpServletRequest对象包装一下,保存requestBody的副本,最后通过过滤器将包装后的request对象替换掉。

附完整代码:

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
/**
* 包装HttpServletRequest,使其inputStream可复用。
* @author wuwenze
* @date 2019-06-21
*/
@Configuration
@Order(1)
@WebFilter(urlPatterns = ["/**"])
class HttpServletRequestWrapperFilter : Filter {
override fun doFilter(req: ServletRequest?, resp: ServletResponse?, chain: FilterChain?) {
when (req) {
is HttpServletRequest -> chain?.doFilter(MyHttpServletRequestWrapper(req), resp)
else -> chain?.doFilter(req, resp)
}
}

class MyHttpServletRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
private var body: ByteArray = request.getBodyString().toByteArray(Charset.forName("UTF-8"))

@Throws(IOException::class)
override fun getReader(): BufferedReader {
return BufferedReader(InputStreamReader(inputStream))
}

@Throws(IOException::class)
override fun getInputStream(): ServletInputStream {
val byteArrayInputStream = ByteArrayInputStream(body)
return object : ServletInputStream() {

@Throws(IOException::class)
override fun read(): Int {
return byteArrayInputStream.read()
}

override fun isFinished(): Boolean {
return false
}

override fun isReady(): Boolean {
return false
}

override fun setReadListener(readListener: ReadListener) {

}
}
}
}
}

获取requestBody的扩展函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun HttpServletRequest.getBodyString(): String {
val result = StringBuffer()
var reader: BufferedReader? = null
try {
var line: String? = null
reader = BufferedReader(InputStreamReader(this.inputStream, Charset.forName("UTF-8")))
while ({ line = reader.readLine();line }() != null) {
result.append(line)
}
} catch (e: IOException) {
// ignore
} finally {
reader?.close()
}
return result.toString()
}

这样就能多次使用了,每次读取的其实是存储在MyHttpServletRequestWrapper中的副本。

MockMvc WebFilter不生效问题解决

在SpringBoot项目中,配置了一个@WebFilter,正常启动没问题,但是通过MockMvc进行单元测试死活不生效

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@Order(1)
@WebFilter(urlPatterns = ["/**"])
class HttpServletRequestWrapperFilter : Filter {
override fun doFilter(req: ServletRequest?, resp: ServletResponse?, chain: FilterChain?) {
when (req) {
is HttpServletRequest -> chain?.doFilter(MyHttpServletRequestWrapper(req), resp)
else -> chain?.doFilter(req, resp)
}
}
}

后来搞了半天,原来在构建MockMvc对象时,需要手动添加过滤器,这坑爹的玩意儿。

1
2
3
4
5
6
@Before
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.addFilters<DefaultMockMvcBuilder>(HttpServletRequestWrapperFilter())
.build()
}

JMeter以非GUI模式执行压力测试


为什么要使用非GUI模式执行压力测试呢?

  1. 图形化界面消耗更多资源,CPU和内存
  2. 图形化界面不支持大型的负载测试和性能测试
  3. 命令行测试支持持续集成,例如放到Jenkins这样的CI工具上

相关参数

-h:帮助,打印出有用的信息并退出
-n:以非GUI形式运行Jmeter
-t:Jmeter脚本路径
-l:输出结果路径,如果没有该文件就自动创建,可以生成csv或者jtl文件
-r:远程执行,启动远程服务
-H:代理主机,设置Jmeter使用的代理主机
-P:代理端口,设置Jmeter使用的代理主机的端口号
-e:在脚本运行结束后生成html报告
-o:保存html报告的地址,此文件夹中必须为空
-J:传递动态参数搭配, 在脚本中需要配合__P函数取值,如:-Jthreads=100 脚本取值为: ${__P(`threads,1)}`

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×