220824-基于引入包选择具体实现类的使用姿势小结

最近遇到一个需求场景,开源的工具包,新增了一个高级特性,会依赖json序列化工具,来做一些特殊操作;但是,这个辅助功能并不是必须的,也就是说对于使用这个工具包的业务方而言,正常使用完全不需要json相关的功能;如果我强引用某个json工具,一是对于不适用高级特性的用户而言没有必要;二则是我引入的json工具极有可能与使用者的不一致,会增加使用者的成本

因此我希望这个工具包对外提供时,并不会引入具体的json工具依赖;也就是说maven依赖中的<scope>设置为provided;具体的json序列化的实现,则取决于调用方自身引入了什么json工具包

那么可以怎么实现上面这个方式呢?

220809-实战小技巧20:巧用函数方法实现二维数组遍历

对于数组遍历,基本上每个开发者都写过,遍历本身没什么好说的,但是当我们在遍历的过程中,有一些复杂的业务逻辑时,将会发现代码的层级会逐渐加深

211231-Java调用本地程序的几种姿势

作为一个后端同学,经常被安全的小伙伴盯上,找一找安全漏洞;除了常说的注入之外,还有比较吓人的执行远程命令,唤醒本地应用程序等;然后有意思的问题就来了,写了这么多年的代码,好像还真没有尝试过用java来唤醒本地应用程序的

比如说一个最简单的,打开本地的计算器,应该怎么搞?

接下来本文将介绍一下如何使用java打开本地应用,以及打开mac系统中特殊一点的处理方式(直白来说就是不同操作系统,使用姿势不一样)

211228-Jar文件提取与查看

最近遇到一个奇怪的问题,一个jar包无法解压,直接使用jar xvf xxx.jar没有任何响应

因为实际想看的只是jar包中的某个class文件,基于此可以通过 jar tf 查看文件列表,在通过 jar xf xxx.jar xxxfile的方式来解压指定文件来实现目的

接下来记录一下jar包的几个操作case

211220-JDNI注入:RMI之绕过trustURLCodebase配置的注入实例演示

上一篇博文介绍了RMI绑定一个Reference,导致加载远程class文件时导致的注入问题,当时有提到对于高级的版本,对于默认的配置为java.rmi.server.useCodebaseOnly=false,对于远程的class文件做了安全校验的,但是即便如此,也并没能完全限制住注入

接下来我们来实例演示一下

211216-JDNI注入:RMI Reference注入问题

前面一篇介绍了基础的RMI的使用case JDNI注入:RMI基本知识点介绍 - 一灰灰Blog,其中有说到客户端通过rmi访问server时,表现和我们常见的rpc也一致,客户端拿到代理执行的方法,也是在远程服务端执行的,怎么就存在注入问题呢?

接下来我们再来看一个知识点,RMI + Reference,利用反序列化来实现注入

211213-JDNI注入:RMI基本知识点介绍

远程方法调用,现在更多的使用RPC来处理,至于RMI好像没有那么多了,最近闹的火热的log4j2漏洞,又让几个关键词jndi,rmi,ldap频繁出现;对于我这种面向Spring编程的javer而言,这些是啥? 干嘛用的?为啥漏洞这么多?

接下来简单学习下RMI的基本知识点

211209-Java使用URI.create()注意事项

记录一个在实际使用过程中遇到的问题,在解析一个url格式的字符串中的域名时,直接使用下面这种姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static ImmutablePair</**host*/String, /**uri*/String> foramtUri(String uri) {
// uri中空格去除,避免转换异常
URI u = URI.create(uri);
String host = u.getHost();
if (u.getPort() > 0 && u.getPort() != 80) {
host = host + ":80";
}

String baseUri = u.getPath();
if (u.getFragment() != null) {
baseUri = baseUri + "#" + u.getFragment();
}

if (StringUtils.isNotBlank(baseUri)) {
baseUri = host + baseUri;
} else {
baseUri = host;
}

return ImmutablePair.of(host, baseUri);
}

正常使用上面这种进行解析,没啥问题,结果某天突然抛了个异常

211112-Java实现图片灰度化

本文通过一个简单的实例,演示如何使用java来实现图片灰度化处理,主要借助下面两种策略来处理颜色

灰度化公式

1
avgColor = red * 0.299f + green * 0.587f + blue * 0.114f

均值方式

1
avgColor = (red + green + blue) / 3.0f

211108-封装一个根据路径获取文件资源的工具类

通常我们最多的场景是从本地资源中读取文件,这个时候我们经常需要注意的是相对路径、绝对路径问题;

除了从本地获取文件之外,从网络中获取文件资源(如图片)也属于相对常见的场景,接下来我们封装一个工具类,可以支持以上各种类型的数据读取

211011-构建一个创建Map的工具类来辅助理解可变传参

虽说java作为编译语言,但是它本身也提供了很多运行时能力,今天介绍一个非常基础的知识点,可变参数传递

210518-String#format数量不匹配抛异常

偶然发现一个问题,在使用String.format进行格式化输出时,发现参数个数不匹配时,会抛出异常,如

1
String msg = String.format("hello %s, %s", "a");

上面这个执行之后,会抛MissingFormatArgumentException异常,提示信息如

1
java.util.MissingFormatArgumentException: Format specifier '%s'

210225-分页遍历的两种使用姿势

在日常开发中,分页遍历迭代的场景可以说非常普遍了,比如扫表,每次捞100条数据,然后遍历这100条数据,依次执行某个业务逻辑;这100条执行完毕之后,再加载下一百条数据,直到扫描完毕

那么要实现上面这种分页迭代遍历的场景,我们可以怎么做呢

本文将介绍两种使用姿势

  • 常规的使用方法
  • 借助Iterator的使用姿势

210129-Java对象内存地址输出

当一个对象没有重写hascode方法时,它返回的内存地址,当覆盖之后,我们有什么办法获取对象的内存地址么?

  • 使用 System.identityHashCode() 输出内存地址

210123-Quick-Media 前置渲染模板支持

Java端”最强”二维码渲染工具又双叒叕升级了,新增前置模板渲染,简单几行代码即可实现酷炫的二维码生成

项目源码:https://github.com/liuyueyi/quick-media

210121-StringUtils字符串分割使用姿势异常记录

字符串分割,属于比较常见的case了,在实际开发中,相信很多小伙伴会借助common-lang工具包中的StringUtils来实现,使用姿势也很简单

1
String[] ans = StringUtils.split("a,b,c", ",");

一般来讲上面这种使用方式没有问题,但是当分隔符前后为空时,需要特别注意,可能会和你预期的返回不一致

200303-如何优雅的在java中统计代码块耗时

在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下

1
2
3
4
5
6
long start = System.currentTimeMillis();
try {
// .... 具体的代码段
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}

上面的写法没有什么毛病,但是看起来就不太美观了,那么有没有什么更优雅的写法呢?

191204-Ognl 使用实例手册

上一篇博文介绍了ongl的基础语法,接下来进入实际的使用篇,我们将结合一些实际的case,来演示ognl究竟可以支撑到什么地步

在看本文之前,强烈建议先熟悉一下什么是ognl,以及其语法特点,减少阅读障碍,五分钟入门系列: 191129-Ognl 语法基础教程

191129-Ognl 语法基础教程

本文将力求用最简单的语言是和示例,介绍一下OGNL的语法规则,文章主要内容参考自官方文档http://commons.apache.org/proper/commons-ognl/language-guide.html

  • 本篇主要是语法介绍篇,实战放在一下篇

191107-InputStream重复使用小技巧

在日常的编码中,有时会遇到,需要重复获取InputStream中的数据的需求;然后一般的流,只能读一次,读完就没了;那么如果我希望有一个可以重复读取数据的InputStream,可以怎么操作?

190909-Java之输出异常堆栈信息

在代码出现异常时,堆栈信息可以有效的辅助定位和排除问题,异常堆栈一般是怎么打印的呢

190605-记录BigDecimal转int四舍五入的姿势

从db中查了一个BigDecimal数据,希望按照四舍五入的方式进行取整,发现直接使用 intValue 不太对,特此记录一下正确姿势

1
new BigDecimal(4.51).setScale(0, RoundingMode.HALF_EVEN).intValue()

190529-Java之HashMap迭代删除使用方法小结

map的迭代删除,和我们常见的list,set不太一样,不能直接获取Iteraotr对象,提供的删除方法也是单个的,根据key进行删除,如果我们有个需求,将map中满足某些条件的元素删除掉,要怎么做呢?

190108-Quick-Fix 如何优雅的实现应用内外交互之接口设计篇

如何实现应用内外交互,是Quick-Fix框架的核心之一,我们常见的应用有提供web服务的(如Spring应用),有进行大数据计算的(如Storm应用),有提供rpc的后台服务(如通过dubbo提供rpc的数据服务),有纯jar服务等;基本上我们可以划分为两类

  • 应用本身,有一套健全的与外界交互的机制(这里不包括db/redis等数据的读写)
  • 应用只关注自己的服务功能(接收数据,产生数据,保存数据),本身不与第三方的应用进行交互

针对上面这两种case,我们应该怎么来设计一套应用内外交互的方案,来实现接收外部请求,执行应用内部方法或访问应用内部数据,并返回结果的目的?

190104-Quick-Fix 纯Jar应用及扩展手册

目前Quick-Fix框架提供了两种类型,三中不同场景下的Fixer,一种是以Jar方式启动的,一个是基于Spring生态体系玩法的,下面主要介绍这jar方式,如何使用QuickFix来实现应用内服务调用和数据订正

190102-Quick-Fix 从0到1构建一个应用内服务/数据访问订正工具包

I. 背景说明

Builder

case1: 程序出bug了

在我们的实际工作中,当我们遇到别人反馈代码出问题了吧,怎么返回的数据不对?

当应用持续跑了一段时间之后,这个时候我们的第一个反应基本是确认能复现么?如果能复现,那么调用的姿势是不是对的?如果确认姿势没问题,那么就是请求参数不对了!!! 如果请求参数还没有问题,卧槽,这下完了,真可能有bug了,这下怎么办?

接下来,一般的讨论是在测试环境复现一下,如果能复现,那么开启debug(或者远程debug),一行行调试,相信很快就能搞定了;

但是,最怕的就是但是,测试环境没法复现,至于线上环境才有问题,这下怎么搞?

case2: 缓存数据有问题

另外一个场景就是为了提升服务性能,缓存基本上会被大量的使用在各个系统之间;有缓存,那么就会有缓存不一致的问题,如果缓存用的是外部的如(redis/memcache)之类的,那么缓存数据的查询和订正,就相对简单了;但是,如果我们使用了内存作为数据的缓存,比如(hashmap, guava),这种时候,我想知道这个内存中的数据怎么办?我想修改这个内存的中的数据怎么办?

3. 小结

上面两个场景,归纳一下主要是两个问题

  • 如何知道线上应用中,某个服务的方法的执行结果;
  • 如何知道线上应用中,某些内存数据的结果

181230-使用Java Socket实现一个http服务器

作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomcat的底层是怎么支持http服务的呢?大名鼎鼎的Servlet又是什么东西呢,该怎么使用呢?

在初学java时,socket编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为0,本篇博文将主要介绍如何使用socket来实现一个简单的http服务器功能,提供常见的get/post请求支持,并再此过程中了解下http协议

180910-Java根据路径获取文件内容

给出一个资源路径,然后获取资源文件的信息,可以说是非常常见的一种需求场景了,当然划分一下,本文针对最常见的三种状况进行分析

  • 网络地址
  • 本地绝对路径
  • 本地相对路径

180827-Java获取类路劲的几种姿势小结

I. Java获取类路劲的几种姿势小结

在Java环境中,如何获取当前类的路径,如何获取项目根路径,可以说是比较常见的需求场景了,下面简单的记录一下

180808-Java实现一个MD5工具类

I. MD5工具类

利用JDK封装一个简易的MD5工具类,逻辑比较简单,直接贴下具体实现

180718-jar包执行传参使用小结

logo

jar包执行时传参的使用姿势

虽说我们现在大多不太直接使用jar包运行方式,目前比较主流的是将自己的服务丢在某个容器中(如tomcat,jetty等)运行,比如我之前所属的电商公司,就是将项目打包为war包,丢到tomcat容器中运行的

在使用SpringBoot时,可能会出现直接打包一个可执行的jar,然后运行,这种时候,通过java命令执行时,时可以传参的,那么问题来了,main方法可以如何优雅的解析这些传参呢?

170709-Java实现获取本机Ip工具类

logo

Java实现获取本机Ip的工具类

获取本机Ip算是比较常见的一个需求场景了,比如业务报警,可能就会带上出问题的机器IP,方便直接上去看日志定位问题,那么问题来了,如何获取机器IP呢?

180706-BigDecimal除法的精度问题

BigDecimal除法的精度问题

在使用BigDecimal的除法时,遇到一个鬼畜的问题,本以为的精度计算,结果使用返回0,当然最终发现还是自己的使用姿势不对导致的,因此记录一下,避免后面重蹈覆辙

180615-精度计算BigDecimal

180615-精度计算BigDecimal

目前接触的业务中,对数据的精度要求比较高,因此不再使用基本的float,double,改为用BigDecimal进行存储和相关的计算,端午前的这一篇博文,则简单的介绍下BigDecimal的使用姿势,早点回家早点放假

180607-手写定长数组

手写定长数组

有个背景场景如下:

一天划分为1440分钟,每分钟记录一个数据块,然后用一个数据结构存储着1440个数据块,随着时间的推移,每过一分钟,向这个数据结构中添加一块,并移除最前的那个;其次就是我希望根据当前的时间,可以获取往前n分钟的数据块

简单来说,上面的需求解析如下:

  • 一个数组,容量为1440
  • 频繁的新增和删除
  • 随机的访问

后面两个就限制了ArrayList和LinkedList的使用场景了,所以为了满足这个场景,然后写了一个简单的数据结构

180604-之时间戳的取整小TIP

时间戳的取整小TIP

一个简单的背景,持有ms为单位的时间戳,需要判断两个时间戳是否为同一分钟

180530-通过反射获取泛型类的实际参数

反射获取泛型类的实际参数

泛型用得还是比较多的,那么如何获取泛型类上实际的参数类型呢?

ConcurrentHashMap之1.7与1.8小结

I. ConcurrentHashMap 两种实现方式小结

1. 锁分段机制

HashMap的底层数据结构是数组+hash链表的方式,非线程安全

ConcurrentHashMap 采用锁分段机制,底层数据结构为二维数组,其中第一层是Segment的数组,每个Segment持有一把独立的锁,而Segment的结构和HashMap很相似;这就是锁分段机制;线程安全

关注几个点:

  • ConcurrentHashMap 如何定位 Segment, 如何定位 HashEntry
  • 修改的加锁逻辑,如何进行扩容
  • 读数据时,如何做到不加锁但保证线程安全的?

Spring学习之事务管理与传播属性

Spring 事务管理与传播属性

在博文 《Spring学习之事务的使用姿势》 中,演示了基于注解和xml的事务使用姿势,以@Transactional注解为例,其中很多的参数都没有详细说明

本篇博文,则主要目的是弄懂这些参数有啥用,以及在实际项目中如何选择

Spring学习之事务的使用姿势

Spring + mybatis + mysql 使用事务的几种姿势

主要记录下spring是如何支持事务的,以及在Spring结合mybatis时,可以怎么简单的实现数据库的事务功能

I. 前提

case1:两张表的的事务支持情况

首先准备两张表,一个user表,一个story表,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`pwd` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `story` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`userId` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者的userID',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '作者名',
`title` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
`story` text COMMENT '故事内容',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `userId` (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

我们的事务场景在于用户修改name时,要求两张表的name都需要一起修改,不允许出现不一致的情况

case2:单表的事务支持

转账,一个用户减钱,另一个用户加钱

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

相比上面那个case,这个更加简单了,下面的实例则主要根据这个进行说明,至于case1,则留待扩展里面进行

首先是实现对应的dao和entity

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
@Data
public class MoneyEntity implements Serializable {

private static final long serialVersionUID = -7074788842783160025L;

private int id;

private String name;

private int money;

private int isDeleted;

private int created;

private int updated;
}


public interface MoneyDao {
MoneyEntity queryMoney(@Param("id") int userId);

// 加钱,负数时表示减钱
int incrementMoney(@Param("id") int userId, @Param("addMoney") int addMoney);
}

对应的mapper文件为

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.demo.mybatis.mapper.MoneyDao">

<sql id="moneyEntity">
id, `name`, `money`, `isDeleted`, `created`, `updated`
</sql>


<select id="queryMoney" resultType="com.git.hui.demo.mybatis.entity.MoneyEntity">
select
<include refid="moneyEntity"/>
from money
where id=#{id}

</select>

<update id="incrementMoney">
update money
set money=money + #{addMoney}
where id=#{id}
</update>
</mapper>

对应的mybatis连接数据源的相关配置

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
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath*:jdbc.properties</value>
</property>
</bean>


<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>

<property name="filters" value="stat"/>

<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>

<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>

<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>

<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="50"/>
</bean>


<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 指定mapper文件 -->
<property name="mapperLocations" value="classpath*:mapper/*.xml"/>
</bean>


<!-- 指定扫描dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.git.hui.demo.mybatis"/>
</bean>

II. 实例演示

通过网上查询,Spring事务管理总共有四种方式,下面逐一进行演示,每种方式是怎么玩的,然后看实际项目中应该如何抉择

1. 硬编码方式

编程式事务管理,既通过TransactionTemplate来实现多个db操作的事务管理

a. 实现

那么,我们的转账case可以如下实现

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
@Repository
public class CodeDemo1 {

@Autowired
private MoneyDao moneyDao;


@Autowired
private TransactionTemplate transactionTemplate;


/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*/
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);

// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}
}
});
}


// 下面都是测试用例相关
private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if(status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


public void addMoney(final int userId) {
System.out.printf("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println(" sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}
}

主要看上面的transfor方法,内部通过 transactionTemplate 来实现事务的封装,内部有三个db操作,一个查询,两个更新,具体分析后面说明

上面的代码比较简单了,唯一需要关注的就是transactionTemplate这个bean如何定义的,xml文件中与前面重复的就不贴了,直接贴上关键代码, 一个是根据DataSource创建的TransactionManager,一个则是根据TransactionManager创建的TransactionTemplate

1
2
3
4
5
6
7
8
<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>

b. 测试用例

正常演示情况, 演示没有任何异常,不考虑并发的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource1.xml"})
public class CodeDemo1Test {
@Autowired
private CodeDemo1 codeDemo1;

@Autowired
private MoneyDao moneyDao;

@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


codeDemo1.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

输出如下,两个账号的钱都没有问题

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526130394266
---------after----------
id: 1 money = 10010
id: 2 money = 49990

转账过程中出现异常,特别是转账方钱已扣,收款方还没收到钱时,也就是case中的status为1的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 内部抛异常的情况
@Test
public void testTransforException() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


try {
codeDemo1.transfor(1, 2, 10, 1);
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}

对此,我们希望把转账方的钱还回去, 输出如下,发现两个的钱都没有变化

1
2
3
4
5
6
7
8
---------before----------
id: 1 money = 10010
id: 2 money = 49990
---------after----------
id: 1 money = 10010
java.lang.IllegalArgumentException: 转账异常!!!
... // 省略异常信息
id: 2 money = 49990

当status为2,表示在转账人钱已扣,收款人钱没收到之间,又有人给收款人转了200,此时根据mysql的锁机制,另外人的转账应该是立马到的(因为收款人账号没有被锁住),且金额不应该有问题

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
---------before----------
id: 1 money = 10010
id: 2 money = 49990
## 右边是注释: 转账过程中,另外存钱立马到账,没有被锁住
内部加钱: 1526130827480
sub modify success! now: 1526130827500
## 存钱结束
转账完成! now: 1526130830488
---------after----------
id: 1 money = 10220
id: 2 money = 49980

当status为3, 表示在转账人钱已扣,收款人钱没收到之间,又有人给转账人转了200,这时因为转账人的记录以及被加了写锁,因此只能等待转账的事务提交之后,才有可能+200成功,当然最终的金额也得一致

输出结果如下

1
2
3
4
5
6
7
8
9
10
11
---------before----------
id: 1 money = 10220
id: 2 money = 49980
## 右边是注释:内部存钱了,但没有马上成功
## 直到转账完成后,才立马存成功,注意两个时间戳
内部加钱: 1526131101046
转账完成! now: 1526131104051
sub modify success! now: 1526131104053
---------after----------
id: 1 money = 10230
id: 2 money = 50170

c. 小结

至此,编程式事务已经实例演示ok,从上面的过程,给人的感觉就和直接写事务相关的sql一样,

1
2
3
4
5
6
start transaction;

-- 这中间就是 TransactionTemplate#execute 方法内部的逻辑
-- 也就是需要事务管理的一组sql

commit;

2. 基于TransactionProxyFactoryBean方式

接下来的三个就是声明式事务管理,这种用得也比较少,因为需要每个事务管理类,添加一个TransactionProxyFactoryBean

a. 实现

除了将 TransactionTemplate 干掉,并将内部的sql逻辑移除之外,对比前面的,发现基本上没有太多差别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class FactoryBeanDemo2 {

@Autowired
private MoneyDao moneyDao;

/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*/
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {

MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);


// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}


}


private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if (status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


public void addMoney(final int userId) {
System.out.println("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println("sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}
}

重点来了,主要是需要配置一个 TransactionProxyBeanFactory,我们知道BeanFactory就是我们自己来创建Bean的一种手段,相关的xml配置如下

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
<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="factoryBeanDemo2" class="com.git.hui.demo.mybatis.repository.transaction.FactoryBeanDemo2"/>

<!-- 配置业务层的代理 -->
<bean id="factoryBeanDemoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 配置目标对象 -->
<property name="target" ref="factoryBeanDemo2" />
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<!-- 注入事务的属性 -->
<property name="transactionAttributes">
<props>
<!--
prop的格式:
* PROPAGATION :事务的传播行为
* ISOTATION :事务的隔离级别
* readOnly :只读
* -EXCEPTION :发生哪些异常回滚事务
* +EXCEPTION :发生哪些异常不回滚事务
-->
<!-- 这个key对应的就是目标类中的方法-->
<prop key="transfor">PROPAGATION_REQUIRED</prop>
<!-- <prop key="transfer">PROPAGATION_REQUIRED,readOnly</prop> -->
<!-- <prop key="transfer">PROPAGATION_REQUIRED,+java.lang.ArithmeticException</prop> -->
</props>
</property>
</bean>

通过上面的配置,大致可以了解到这个通过TransactionProxyFactoryBean就是创建了一个FactoryBeanDemo2的代理类,这个代理类内部封装好事务相关的逻辑,可以看做是前面编程式的一种简单通用抽象

b. 测试

测试代码与前面基本相同,唯一的区别就是我们使用的应该是上面BeanFactory生成的Bean,而不是直接使用FactoryBeanDemo2

正常演示case:

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource2.xml"})
public class FactoryBeanDemo1Test {

@Resource(name = "factoryBeanDemoProxy")
private FactoryBeanDemo2 factoryBeanDemo2;

@Autowired
private MoneyDao moneyDao;


@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


factoryBeanDemo2.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526132058886
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status为1,内部异常的情况下,我们希望钱也不会有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testTransforException() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


try {
factoryBeanDemo2.transfor(1, 2, 10, 1);
} catch (Exception e) {
System.out.println(e.getMessage());;
}

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}

输出为

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status为2 时,分析结果与上面应该相同,输出如下

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10010
id: 2 money = 49950
内部加钱: 1526133325376
sub modify success! now: 1526133325387
转账完成! now: 1526133328381
---------after----------
id: 1 money = 10220
id: 2 money = 49940

status为3时,输出

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10220
id: 2 money = 49940
内部加钱: 1526133373466
转账完成! now: 1526133376476
sub modify success! now: 1526133376480
---------after----------
id: 1 money = 10230
id: 2 money = 50130

c. 小结

TransactionProxyFactoryBean 的思路就是利用代理模式来实现事务管理,生成一个代理类,拦截目标方法,将一组sql的操作封装到事务中进行;相比较于硬编码,无侵入,而且支持灵活的配置方式

缺点也显而易见,每个都要进行配置,比较繁琐

3. xml使用方式

Spring有两大特点,IoC和AOP,对于事务这种情况而言,我们可不可以使用AOP来做呢?

对于需要开启事务的方法,拦截掉,执行前开始事务,执行完毕之后提交事务,出现异常时回滚

这样一看,感觉还是蛮有希望的,而下面两种姿势正是这么玩的,因此需要加上aspect的依赖

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

a. 实现

java类与第二种完全一致,变动的只有xml

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
<!-- 首先添加命名空间 -->
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="...
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"



<!--对应的事务通知和切面配置-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
propagation :事务传播行为
isolation :事务的隔离级别
read-only :只读
rollback-for:发生哪些异常回滚
no-rollback-for :发生哪些异常不回滚
timeout :过期信息
-->
<tx:method name="transfor" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>



<!-- 配置切面 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(* com.git.hui.demo.mybatis.repository.transaction.XmlDemo3.*(..))" id="pointcut1"/>
<!-- 配置切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/>
</aop:config>

观察上面的配置,再想想第二种方式,思路都差不多了,但是这种方式明显更加通用,通过切面和切点,可以减少大量的配置

b. 测试

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource3.xml"})
public class XmlBeanTest {
@Autowired
private XmlDemo3 xmlDemo;

@Autowired
private MoneyDao moneyDao;


@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


xmlDemo.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

这个测试起来,和一般的写法就没啥两样了,比第二种的FactoryBean的注入方式简单点

正常输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526135301273
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status=1 出现异常时,输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status=2 转账过程中,又存钱的场景,输出,与前面预期一致

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10010
id: 2 money = 49990
内部加钱: 1526135438403
sub modify success! now: 1526135438421
转账完成! now: 1526135441410
---------after----------
id: 1 money = 10220
id: 2 money = 49980

status=3 的输出,与前面预期一致

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10220
id: 2 money = 49980
内部加钱: 1526135464341
转账完成! now: 1526135467349
sub modify success! now: 1526135467352
---------after----------
id: 1 money = 10230
id: 2 money = 50170

4. 注解方式

这个就是消灭xml,用注解来做的方式,就是将前面xml中的配置用 @Transactional注解替换

a. 实现

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
@Repository
public class AnnoDemo4 {

@Autowired
private MoneyDao moneyDao;


/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*
*
* Transactional注解中的的属性 propagation :事务的传播行为 isolation :事务的隔离级别 readOnly :只读
* rollbackFor :发生哪些异常回滚 noRollbackFor :发生哪些异常不回滚
* rollbackForClassName 根据异常类名回滚
*/
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {

MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);


// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}


}


private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if (status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


private void addMoney(final int userId) {
System.out.println("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println("sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}

}

因此需要在xml中配置,开启事务注解

1
2
3
4
5
6
7
8

<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>


<tx:annotation-driven transaction-manager="transactionManager"/>

这样一看,就更加清晰了,实际项目中,xml和注解方式也是用得最多的场景了

b. 测试case

和第三种测试case完全相同, 输出结果也一样,直接省略

III. 小结

上面说了Spring中四种使用事务的姿势,其中硬编码方式可能是最好理解的,就相当于将我们写sql中,使用事务的方式直接翻译成对应的java代码了;而FactoryBean方式相当于特殊情况特殊对待,为每个事务来一个代理类来增强事务功能;后面的两个则原理差不多都是利用事务通知(AOP)来实现,定义切点及相关信息

编程式:

  • 注入 TransactionTemplate
  • 将利用事务的逻辑封装到 transactionTemplate#execute方法内

代理BeanFactory:

  • 利用 TransactionProxyFactoryBean 为事务相关类生成代理
  • 使用方通过FactoryBean获取代理类,作为使用的Bean

xml配置:

  • 利用 tx标签 + aop方式来实现
  • <tx:advice> 标签定义事务通知,内部可有较多的配置信息
  • <aop:config> 配置切点,切面

注解方式:

  • 在开启事务的方法or类上添加 @Transactional 注解即可
  • 开启事务注解 <tx:annotation-driven transaction-manager="transactionManager"/>

IV. 其他

1. 参考

文档

源码

2. 个人博客: 一灰灰Blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

3. 声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

4. 扫描关注

QrCode

Java实现邮件发送

Java实现邮件发送

在日常工作中,通过邮件或短信做报警或者信息推送的场景还是挺多的,而java中,常用的就是JavaMail来做这个事情了,到网上搜索了一把,发现apache有个commons email 的开源包,现在借助它来尝鲜一把

基于ForkJoin构建一个简单易用的并发组件

基于ForkJoin构建一个简单易用的并发组件

在实际的业务开发中,需要用到并发编程的知识,实际使用线程池来异步执行任务的场景并不是特别多,而且一般真的遇到了需要并发使用的时候,可能更加常见的就是直接实现Runnable/Callable接口,丢到Thread中执行了;或者更高级一点,定义一个线程池,扔进去执行;本片博文,将从另一个角度,借助JDK提供的ForkJoin,来设计一个简单易用的并发框架

Java学习之NIO相关

Java NIO学习小结

前面一篇主要学习了下IO的流式操作,接下来就是重头戏了,NIO,又称为New IO

当然也是得抱着问题来学习这个东西了,希望可以通过本文,可以学习到:

  • 什么是NIO
  • NIO相比较与IO有什么特点
  • 同步,非同步,阻塞,非阻塞是什么鬼
  • 几种IO模型

Java学习之IO相关

Java IO学习小结

IO操作算是java的一个基本知识点了,比如我们常见的网络IO,文件读写等,而且这一块基本上大家并不会频繁的来操作,大多会用一些封装得好用的工具来代替,某些时候真的需要做的时候,基本上也很难一下子很顺利的写完

本篇将主要集中在:

  • 几种IO分类
  • 字节IO和字符IO的转换
  • 装饰类IO是什么
  • 序列化的实现机制

JVM学习之垃圾回收机制

jvm的垃圾回收算法,除了我们熟悉的引用计数判断对象是否活着之外,其他还有那些有意思的东西呢?

总是听到的年轻代年老代又是啥?

传说中的YoungGC(MinorGC) 和 FullGC的时机是什么,又干了些啥?

熔断Hystrix使用尝鲜

熔断Hystrix使用尝鲜

当服务有较多外部依赖时,如果其中某个服务的不可用,导致整个集群会受到影响(比如超时,导致大量的请求被阻塞,从而导致外部请求无法进来),这种情况下采用hystrix就很有用了

出于这个目的,了解了下hystrix框架,下面记录下,框架尝新的历程

I. 原理探究

通过官网和相关博文,可以简单的说一下这个工作机制,大致流程如下

首先是请求过来 -> 判断熔断器是否开 -> 服务调用 -> 异常则走fallback,失败计数+1 -> 结束

下面是主流程图

流程图

1
2
3
4
5
6
7
8
9
10
graph LR
A(请求)-->B{熔断器是否已开}
B --> | 熔断 | D[fallback逻辑]
B --> | 未熔断 | E[线程池/Semphore]
E --> F{线程池满/无可用信号量}
F --> | yes | D
F --> | no | G{创建线程执行/本线程运行}
G --> | yes | I(结束)
G --> | no | D
D --> I(结束)

熔断机制主要提供了两种,一个是基于线程池的隔离方式来做;还有一个则是根据信号量的抢占来做

线程池方式 : 支持异步,支持超时设置,支持限流

信号量方式 : 本线程执行,无异步,无超时,支持限流,消耗更小

基本上有上面这个简单的概念之后,开始进入我们的使用测试流程

II. 使用尝鲜

1. 引入依赖

1
2
3
4
5
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.12</version>
</dependency>

2. 简单使用

从官方文档来看,支持两种Command方式,一个是基于观察者模式的ObserverCommand, 一个是基本的Command,先用简单的看以下

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
public class HystrixConfigTest extends HystrixCommand<String> {

private final String name;

public HystrixConfigTest(String name, boolean ans) {
// 注意的是同一个任务,
super(Setter.withGroupKey(
// CommandGroup是每个命令最少配置的必选参数,在不指定ThreadPoolKey的情况下,字面值用于对不同依赖的线程池/信号区分
HystrixCommandGroupKey.Factory.asKey("CircuitBreakerTestGroup"))
// 每个CommandKey代表一个依赖抽象,相同的依赖要使用相同的CommandKey名称。依赖隔离的根本就是对相同CommandKey的依赖做隔离.
.andCommandKey(HystrixCommandKey.Factory.asKey("CircuitBreakerTestKey_" + ans))
// 当对同一业务依赖做隔离时使用CommandGroup做区分,但是对同一依赖的不同远程调用如(一个是redis 一个是http),可以使用HystrixThreadPoolKey做隔离区分
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("CircuitBreakerTest_" + ans))
.andThreadPoolPropertiesDefaults( // 配置线程池
HystrixThreadPoolProperties.Setter()
.withCoreSize(12) // 配置线程池里的线程数,设置足够多线程,以防未熔断却打满threadpool
)
.andCommandPropertiesDefaults( // 配置熔断器
HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(3)
.withCircuitBreakerErrorThresholdPercentage(80)
// .withCircuitBreakerForceOpen(true) // 置为true时,所有请求都将被拒绝,直接到fallback
// .withCircuitBreakerForceClosed(true) // 置为true时,将忽略错误
// .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) // 信号量隔离
.withExecutionIsolationSemaphoreMaxConcurrentRequests(20)
.withExecutionTimeoutEnabled(true)
.withExecutionTimeoutInMilliseconds(200)
.withCircuitBreakerSleepWindowInMilliseconds(1000) //熔断器打开到关闭的时间窗长度
// .withExecutionTimeoutInMilliseconds(5000)
)
);
this.name = name;
}

@Override
protected String run() throws Exception {
System.out.println("running run():" + name + " thread: " + Thread.currentThread().getName());
int num = Integer.valueOf(name);
if (num % 2 == 0 && num < 10) { // 直接返回
return name;
} else if (num < 40) {
Thread.sleep(300);
return "sleep+"+ name;
} else { // 无限循环模拟超时
return name;
}
}
//
// @Override
// protected String getFallback() {
// Throwable t = this.getExecutionException();
// if(t instanceof HystrixRuntimeException) {
// System.out.println(Thread.currentThread() + " --> " + ((HystrixRuntimeException) t).getFailureType());
// } else if (t instanceof HystrixTimeoutException) {
// System.out.println(t.getCause());
// } else {
// t.printStackTrace();
// }
// System.out.println(Thread.currentThread() + " --> ----------over------------");
// return "CircuitBreaker fallback: " + name;
// }

public static class UnitTest {

@Test
public void testSynchronous() throws IOException, InterruptedException {
for (int i = 0; i < 50; i++) {
if (i == 41) {
Thread.sleep(2000);
}
try {
System.out.println("===========" + new HystrixConfigTest(String.valueOf(i), i % 2 == 0).execute());
} catch (HystrixRuntimeException e) {
System.out.println(i + " : " + e.getFailureType() + " >>>> " + e.getCause() + " <<<<<");
} catch (Exception e) {
System.out.println("run()抛出HystrixBadRequestException时,被捕获到这里" + e.getCause());
}
}

System.out.println("------开始打印现有线程---------");
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread thread : map.keySet()) {
System.out.println("--->name-->" + thread.getName());
}
System.out.println("thread num: " + map.size());

System.in.read();
}
}
}

使用起来还是比较简单的,一般步骤如下:

  • 继承 HsytrixCommand
  • 重载构造方法,内部需要指定各种配置
  • 实现run方法,这个里面主要执行熔断监控的方法

写上面的代码比较简单,但是有几个地方不太好处理

  1. 配置项的具体含义,又是怎么生效的?
  2. 某些异常不进入熔断逻辑怎么办?
  3. 监控数据如何获取?
  4. 如何模拟各种不同的case(超时?服务异常?熔断已开启?线程池满?无可用信号量?半熔断的重试?)

3. 实测理解

根据上面那一段代码的删删改改,貌似理解了以下几个点,不知道对误

a. 配置相关

  • groupKey 用于区分线程池和信号量,即一个group对应一个
  • commandKey 很重要,这个是用于区分业务
    • 简单来讲,group类似提供服务的app,command则对应app提供的service,一个app可以有多个service,这里就是将一个app的所有请求都放在一个线程池(or共享一个信号量)
  • 开启熔断机制,指定触发熔断的最小请求数(10s内),指定打开熔断的条件(失败率)
  • 设置熔断策略(线程池or信号量)
  • 设置重试时间(默认熔断开启后5s,放几个请求进去,看服务是否恢复)
  • 设置线程池大小,设置信号量大小,设置队列大小
  • 设置超时时间,设置允许超时设置

b. 使用相关

run方法是核心执行服务调用,如果需要某些服务不统计到熔断的失败率(比如因为调用姿势不对导致服务内部的异常抛上来了,但是服务本身是正常的),这个时候,就需要包装下调用逻辑,将不需要的异常包装到 HystrixBadRequestException 类里

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected String run() {
try {
return func.apply(route, parameterDescs);
} catch (Exception e) {
if (exceptionExcept(e)) {
// 如果是不关注的异常case, 不进入熔断逻辑
throw new HystrixBadRequestException("unexpected exception!", e);
} else {
throw e;
}
}
}

c. 如何获取失败的原因

当发生失败时,hystrix会把原生的异常包装到 HystrixRuntimeException 这个类里,所以我们可以在调用的地方如下处理

1
2
3
4
5
6
7
try {
System.out.println("===========" + new HystrixConfigTest(String.valueOf(i), i % 2 == 0).execute());
} catch (HystrixRuntimeException e) {
System.out.println(i + " : " + e.getFailureType() + " >>>> " + e.getCause() + " <<<<<");
} catch (Exception e) {
System.out.println("run()抛出HystrixBadRequestException时,被捕获到这里" + e.getCause());
}

当定义了fallback逻辑时,异常则不会抛到具体的调用方,所以在 fallback 方法内,则有必要获取对应的异常信息

1
2
// 获取异常信息
Throwable t = this.getExecutionException();

然后下一步就是需要获取对应的异常原因了,通过FailureType来表明失败的根源

1
((HystrixRuntimeException) t).getFailureType()

d.如何获取统计信息

hystrix自己提供了一套监控插件,基本上公司内都会有自己的监控统计信息,因此需要对这个数据进行和自定义,目前还没看到可以如何优雅的处理这些统计信息

4. 小结

主要是看了下这个东西可以怎么玩,整个用下来的感觉就是,设计的比较有意思,但是配置参数太多,很多都没有完全摸透

其次就是一些特殊的case(如监控,报警,特殊情况过滤)需要处理时,用起来并不是很顺手,主要问题还是没有理解清楚这个框架的内部工作机制的问题

III. 其他

个人博客: Z+|blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode

JVM学习之内存结构

JVM学习之内存结构

java运行时对象创建在什么地方?堆和栈空间又有什么区别?听闻已久的Young,Old区又是什么鬼?听说有个常量池,这个又是啥

要想在脑海中清晰的布局一个java类在加载到使用的过程中,整个类生命周期中,各项数据究竟最终落在哪个板块上,就需要了解下JVM的内存区域了

JVM学习之Java类的加载机制

JVM学习之Java类的加载机制

平常我们使用java的多,深入到jvm层的机会却很少,平时若不关注,也不会清楚java文件编译后的class文件是如何被jvm加载到内存,如何进行初始化,如何进行运行的

因此这里主要学习的目标就是class文件的加载,会包含以下内容:

  • 什么是类加载
  • 类加载的过程
  • 什么时候触发类加载
  • 类加载器
  • 双亲委托机制

Java并发学习之线程池ThreadPoolExecutor的小结

Java并发学习之线程池ThreadPoolExecutor的小结

本篇博文将带着问题来回顾小结多线程池相关的知识点

  1. 线程池的几种创建方式
  2. 线程池的优点是什么
  3. 应用场景
  4. 如何使用
  5. 实现原理
  6. 异常状况怎么处理
  7. 线程池中任务的提交执行后,到线程执行,执行完成的整个流程逻辑
  8. 线程池中的线程回收机制

7. 报警系统QuickAlarm之默认报警规则扩展

本篇主要是扩展默认的报警规则,使其能更加友好的支持同时选择多种报警方式

扩展遵循两个原则

  • 不影响原有的配置文件格式
  • 简化规则解析复杂度
Your browser is out-of-date!

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

×