错题本

……尝试治一下自己的毛病

大致就是,写代码的时候做出许多假设,测试的时候发现假设并不总是成立。。

很多都是非常细小的问题,交付一个模块的时候记一下,不要再犯就是了

2019-11-14

微商城斑马二期用户分组、花型

  • MyBatis Plus默认不更新为null字段,导致无法将面料修改为花型

  • 重构商品校验Service时,取错集合,拼错字符串,导致花型无法设置为前10商品时报错文案错误

  • 商品占库业务没有考虑到库存可以为空,订单业务应该是花型订单才不占库,写反了

  • JRebel会导致修改Spring配置文件的配置项后不生效

  • 审核订单需要计算幅宽克重,没有跳过

  • 销售系统创建订单会根据类来推断订单类型,还有一个强行将生产数量置0的设定

  • 管理后端商品详情应该写sql 用嵌套查询查出子表内的完整记录,然后返回id和名称给前端

  • 商品详情没有返回折扣价,而且从登录Token信息取用户组id不一定是最新的

解决方案:

  • 单个用户认证并且变更所在用户组的时候,需要踢出该用户
  • 删除整个用户组时,需要踢出该用户组所有用户
  • 商品详情使用用户组id从数据库获取并返回当前用户的折扣信息,由前端计算实时的商品价格/价格配置价格
  • 订单详情的花型订单需要返回单品的单价
  • 商品列表也需要返回折扣

购物车展示折扣价

2019-9-16

从前往后

标签模块

  • 表名先用了复数,手写sql没改过来
  • 没考虑添加商品时没有带标签的情况
  • 删除接口Controller的请求方法写错
  • TagDTO用于传输公司id,使用BeanUtils.copy时将公司id复制过去,导致返回结果多出公司id来
  • 根据标签名搜索商品时 标签名与商品名条件应用OR排列,标签id应用and排列

2019-9-11

  • 商品列表我认为是空列表,但是没有再swagger里写清楚,前端不知是传空数组还是不传
    包括活动页Banner的必传性我也没有写清楚
    而我后台做了许多没有判null就进行的操作

  • 是否需要主标题为空的校验写错了类型,写到了个人中心页的Banner

  • 手动把模块表的某条记录从逻辑删除恢复,然后忘了恢复图片。。导致这个类型的模块拿不到,手动修了数据

  • 哎呦,吐了,设计出现重大问题
    之前做小程序banner一对多是按图片id做group,因为不想改原有数据结构
    现在出现问题了,用两张相同的图片发现商品id被group到一个图片里去了

得改改,用json字符串存这个图片-商品一对多好了

2019-08-28

Banner图模块

  • 关联查询时,由于习惯Mybatis Plus的自动过滤逻辑删除特性,自己撰写SQL时没有过滤关联查询表的已经逻辑删除的记录、以及子表中不存在的记录
    代码:
1
2
3
4
@Select("select m.id as module_id,m.is_required,m.title,m.subtitle,m.type,i.file_id,i.product_id,i.pno " +
"from wsc_pc_banner_module m left join wsc_pc_banner_image i on i.module_id = m.id " +
"${ew.customSqlSegment} and (i.is_deleted is NULL or i.is_deleted = 0) and m.is_deleted = 0 order by m.id")
List<WscPcBannerDO> selectBanner(@Param(Constants.WRAPPER) Wrapper wrapper);

重点在于and (i.is_deleted is NULL or i.is_deleted = 0)

  • 根据类型筛选Banner图,前端应该这么传值:
1
2
3
GET {{adminHost}}/microManage/pcBanner?type=0&type=3&type=4&type=5
Content-Type: application/json
Authorization: {{adminToken}}

后端应该这么接受:

1
2
3
4
@GetMapping("/pcBanner")
public JsonResult pcBanner(@RequestParam(required = false) Integer[] type) {
//...
}

前端直接用[0,3,4,5]这样的后端是接收不到的。。后端也不能用List来接受。。感觉这个设计有一点丑,但又不想额外写代码转换。。

记一次死锁

业务场景:微信支付模块,每一次请求都会在pay_log表里插入订单号等记录,支付回调时会更新pay_log表的记录

同时还有一个定时任务用来定时将所有1小时前的订单标记为交易超时

编写单元测试,模拟支付回调通知,结果出了异常,更新记录时出现死锁

大致SQL如下:

事务A

1
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

事务B

1
2
INSERT pay_log VALUES(NULL,?,NOW(),?,?,?,?,?,?,?,?,?,?,?,?) 
Update pay_log SET tradeState =? WHERE outTradeNo = ? AND tradeState = ? AND payType = ?

然后事务B被回滚了

解决方案:要么禁用定时任务,要么去掉单元测试的事务手动回滚,要么单元测试不要打断点调试(定时任务设置了项目启动后10秒后启动)

附上MySQL死锁日志,其实这玩意只能大致的定位故障,因为可以看出事务2的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
2019-08-20 16:23:07 0x7ff1b2039700
*** (1) TRANSACTION:
TRANSACTION 43869195, ACTIVE 3 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 269 row lock(s)
MySQL thread id 746830, OS thread handle 140676071700224, query id 184518127 192.168.3.108 root updating
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869195 lock_mode X waiting
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0


*** (2) TRANSACTION:
TRANSACTION 43869181, ACTIVE 7 sec starting index read
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 747008, OS thread handle 140676050425600, query id 184518193 192.168.3.49 root updating
Update pay_log SET tradeState =1 WHERE outTradeNo = 'T9744695806' AND tradeState = 0 AND payType = 0

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X locks rec but not gap
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 4 n bits 168 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** WE ROLL BACK TRANSACTION (2)

记一次本人代码提交丢失事故

前情提要:
公司代码提交规范是Merge Request,平时push在自己fork的远程仓库,开发完成后用Merge Request合并到上游仓库。

之前我负责的功能是在wsc分支上工作的,push到了自己的远程仓库,并且发起Merge Request,然后被后续可能切换分支为理由打回。

今天项目负责人通知我要切换到saas2.2分支,目前该分支与wsc分支保持一致,仅存在于上游仓库。

于是我使用git fetch upstream拉取上游仓库saas2.2分支信息,并且checkout到本地。

1
2
3
10:10:23.544: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false checkout -b saas2.2 upstream/saas2.2 --
Branch 'saas2.2' set up to track remote branch 'saas2.2' from 'upstream'.
Switched to a new branch 'saas2.2'

然后将本地saas2.2分支push到远程仓库saas2.2分支(在IDEA中选择了origin作为remote):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10:10:42.123: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false push --progress --porcelain origin refs/heads/saas2.2:saas2.2
Enumerating objects: 67, done.
Delta compression using up to 4 threads
Total 33 (delta 12), reused 0 (delta 0)
remote: hooks/pre-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote: hooks/update:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
To http://192.168.3.112:8888/qhs/***.git
remote: hooks/post-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote:
* refs/heads/saas2.2:refs/heads/saas2.2 [new branch]
remote: To create a merge request for saas2.2, visit:
remote: http://192.168.3.112:8888/qhs/***/merge_requests/new?merge_request%5Bsource_branch%5D=saas2.2
remote:
Done

然后将本地wsc分支合并到本地saas2分支,然后再次push。

然后这次我忘了设置remote:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
10:10:57.803: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false merge wsc
10:11:05.785: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false push --progress --porcelain upstream refs/heads/saas2.2:saas2.2
Enumerating objects: 25, done.
Delta compression using up to 4 threads
Total 9 (delta 2), reused 0 (delta 0)
remote: hooks/pre-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote: hooks/update:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote: hooks/post-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote:
remote: To create a merge request for saas2.2, visit:
remote: http://192.168.3.112:8888/root/***/merge_requests/new?merge_request%5Bsource_branch%5D=saas2.2
remote:
To http://192.168.3.112:8888/root/***.git
refs/heads/saas2.2:refs/heads/saas2.2 a4cc8e2..de8714c
Done

然后我hard reset了本地的wsc分支,

1
2
3
4
10:11:15.153: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false checkout wsc --
Your branch is up to date with 'origin/wsc'.
Switched to branch 'wsc'
10:12:01.366: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false reset --hard c84bdf0753dd540fcb8312b324d1f848aae938a8

并且force push到了远程仓库。至此本地、远程仓库的wsc分支丢失了本次提交。

看起来一切都很美好,直到我看到了我之前忘记设置remote的push引发的WebHook钉钉机器人提醒。。

于是我将本地saas2.2分支 hard reset,并且force push到了上游仓库。。

1
2
3
4
10:15:00.241: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false checkout saas2.2 --
Your branch is up to date with 'upstream/saas2.2'.
Switched to branch 'saas2.2'
10:15:42.094: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false reset --hard a4cc8e2b7ce5dfdc0c25c98b63a77a12c3aa09f3

至此本地、上游仓库的saas2.2分支丢失了本次提交,并且我在合并前就将saas2.2分支push到远程仓库,因此远程仓库也没有此提交记录。。

万幸Gitlab上有关闭的Merge Request记录,可以查看到代码改动。。

得到的经验教训是:使用git push -f之前,一定要仔细看一下提交记录,push之前也要仔细看一下remote到底是什么。。

幸好干了两年活只丢过这么一次代码,还是丢的我自己的。。

关于单测的笔记

之前没有写单元测试的意识,把测试框架看成不用启动项目的带Spring框架的类。。

后来项目有了单元测试覆盖率要求,于是开始根据数据库的现有数据写用例

然后被教育了,单元测试应该是和外部环境无关的对现有逻辑的测试,所有数据要在@Before方法构建,并且测试完之后所有操作都要回滚。。

灵异事件二则

……调微信支付的时候碰到两个灵异事件

首先是按Spring文档 添加了如下依赖:

1
2
3
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

然后写了一个微信支付AppID的配置类

结果代码里跑起来死活为null

想了一下,把wxpay相关的配置拿到了spring相关配置下方(按理应该没影响)

然后配置文件里log后面莫名其妙多了一个192.168.3.49,没注意到,启动测试类读取微信支付配置时提示连不上数据库192.168.3.49……

删掉之后 配置文件里的数据库密码突然变成了之前调试本地数据库时候的本地数据库密码。。

改掉配置文件,invalid cache and restart之后测试类能读到配置类的数据了。。然后配置类的@Data注解上还多了个断点,感觉电脑被远程控制了一样……

最骚的是,IDEA 2019.1.1不支持Gradle4.9+SpringBoot读配置文件的组合,如果依赖写的是annotationProcessor会提示class path里没有spring配置相关的processor。。


2019-5-21 17:12:36
更骚的出现了,测试类能拿到appID,引用了common模块的其他模块跑起来的时候拿不到

最后是因为多个application.yml冲突了……

微信小程序支付对接开发笔记

2019-9-23更新
试了一下支付宝

发现支付宝网页有个收银台,PC网站下单完事之后会给一个自动提交的form,然后302跳转到另一个URL

感觉实际对接的时候可以把那个URL返回给前端


新单位新项目,微信小程序对接微信支付+退款,由于开发测试是真的繁琐,在此记录一下笔记。

下载微信SDK,编写后端代码

……随便吐槽一下,微信SDK被人挖出来漏洞之后就不放github了,只能从微信官网下载,然后阿里规约扫描报了一大堆错……也没法改,万一后续SDK更新了呢

2019-8-28更新:
我真是认识鹅厂微信支付Java SDK作者的美,你弄个WxPayConfig的抽象类给我们扩展,然后getAppId()这种方法的访问权限是default,用Maven等依赖管理引入之后,根本没法扩展,因为必须得在同一个包下。。

然后其他WxPay之类的类又必须依赖这个类的子类才能干活。。 难怪官方都只给源码下载,而不是托管到什么依赖仓库。。

配置类

首先创建一个SDK内WxPayConfig类的实现类,由于项目名是微商城,取名叫WscWxConfig

使用Spring配置读取的方式,在类里声明AppIdMchId(商户号),Key,证书内容(字节数组)几个成员变量,然后AppId MchId keygetter直接用lombok生成。

getCertStream这个方法需要重写为return new ByteArrayInputStream(this.certData);的形式,getWxPayDomain更麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
IWXPayDomain getWXPayDomain() {
return new IWXPayDomain() {
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {

}
@Override
public DomainInfo getDomain(WXPayConfig config) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
}

2019-8-28更新:
后续改成按公司独立设置微信支付参数,所以用了ThreadLocal维护这些东西。。支付退款证书也放在了OSS上,反正是流,无所谓……

顺带给WxPay类做一个单例,声明一个WxPay类型的变量,然后用@PostConstruct注解初始化方法:
注意这里的构造函数传参第三个参数,表示使用沙箱环境

1
2
3
4
5
6
7
public void initWxPay(){
try {
this.wxPay = new WXPay(this, false, true);
} catch (Exception e) {
log.error("inital wxpay failed ", e);
}
}

这样配置类就写完了。

Service

微信SDK给了轻度封装,至少使用Map<String, String>就可以完成参数填写了

统一下单

由于负责用户模块的同事的设计是不存储OpenId,因此该方法直接接受code,订单号,金额,公司名(用于微信订单描述)作为参数。

接下来是调用微信SDK发送请求,并且将拿到的prepay_id返回前端。

接受支付通知

一定要校验签名和金额!!!!

幂等性处理后,将接收到的支付信息持久化一份到数据库,并且根据业务结果+支付金额调订单Service。

扯一下我们的幂等性方案吧,防重入一开始打算用悲观锁锁住订单号对应的行,但是怕应用意外崩溃然后锁无法释放,最后用的是redis的setnx。防重入之后就做一下状态判断就行了。

退款

这回是由商户服务器发起请求,所以不用OpenId了,指定订单号、金额、退款金额、售后单号即可

接受退款通知

类似支付通知的幂等性处理,持久化一份到数据库,并根据业务结果调售后Service。

Controller

主要是两个接收通知的接口,以及退款接口信息的解密方法。

先从request.getInputStream()中获取XML,然后用微信SDK转成Map,再用微信SDK做签名校验。

测试

后端码狗自测微信小程序支付接口简直要人命……居然没找到描述全流程的博文
调个微信支付和玩tmd解谜RPG一样,到处找线索

小程序支付交付需要在沙箱模拟过一遍之后向微信提交验收通过申请,当然如果已经有通过微信支付验收的小程序就不必走一遍沙箱流程了。

写完代码自己测试时,一般还是直接用一个能用的小程序支付参数,而不是沙箱,理由后面讲。。

数据准备

AppId

小程序所属的公司账号将开发者的微信号加入小程序开发者后,开发者使用微信扫码登录微信小程序后台即可看到。

AppSecret

需要超级管理员在小程序后台开发设置生成,用于小程序Code换OpenId用。。

MchId

在商户后台将开发者账号加入商户的员工账号后,员工账号会收到包含MchId的通知。

Key

商户后台API安全处填写,需要超级管理员验证,需要商户账号开通操作密码
其中沙箱开发要用到SignKey,使用Key请求微信的API生成。

解析退款信息时使用。

证书

商户后台API安全处可以重设,最好能找到第一次申请的证书文件。
解析退款信息时使用。

内网穿透

本地请求支付的接口需要一个带https的内网穿透wx.request用,即使有CI/CD,也懒得改一行代码提一个pr……图省事直接买了NATAPP。

吐槽一下,要调试小程序需要买他的国内隧道+二级域名,15元一年的域名+9元一个月的隧道;

倒是可以选择用自己的域名,但是我域名没备案……而阿里云买的域名要备案需要配阿里云的实例……

还有香港流量包月的隧道,买完才发现这个不能开https……香港流量不包月的倒是可以配自己域名,但是自己域名443端口必须空着,我域名有个开了https的小网盘跑着……

幸好微信的支付通知回调可以不用https,就用免费隧道顶上。

2019-8-28更新:
后续在自己的服务器上搭了个ngrok,不用额外付冤枉钱了。。

统一下单

在微信开发者工具中新建一个小程序,修改AppId为公司小程序的AppId(也可以直接调公司小程序的代码,但是单独测一个支付我还是选择新起一个小程序)。

加一个按钮,测试支付,绑定一个函数,把返回的requestPayment用的数据打印出来,顺带请求支付:

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
testPay: function (e) {
wx.login({
success(res) {
if (res.code) {
// 发起网络请求
wx.request({
url: 'https://qxkjwxpay.mynatapp.cc/micro/order/payOrder',
header: {
'Authorization': '登录接口生成的JWT内容'
},
method: 'POST',
data: {
code: res.code,
amount: '1.01',
orderNum: '2',
paymentType: 10
},
success(res) {
console.log(res);
wx.requestPayment(
{
'timeStamp': res.data.data.timestamp,
'nonceStr': res.data.data.nonceStr,
'package': res.data.data.prepayId,
'signType': 'MD5',
'paySign': res.data.data.sign,
'success': function (res) { },
'fail': function (res) { },
'complete': function (res) { }
})
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
}

结果是这样的:

1
2
3
4
5
6
7
8
9
code:200
data:{
nonceStr:"klrv5kgx5dJdc7ILG9Uj9k70hUKzXbF8"
prepayId:"prepay_id=wx20190522095426670409"
sign:"A118797887A33C961594D149285EDCEE"
timestamp:1558490065
}
msg:"成功"
subCode:null

调起支付

果↓然↑啊,扫描开发者工具给的二维码时提示错误:调用支付JSAPI缺少参数:total_fee

我寻思统一下单都成功了你在说你吗呢,会报错你就多报几句,是不是把你妈杀了你也只能呜咽着说出你缺少total_fee?真的憨批一样

package传的也带了prepay_id=,后端生成随机数+签名用的是微信JavaSDK的工具类,签名字段大小写也没问题

发现后端生成签名时,工具类会自动拼上key=key,而我手动在map里加了一个key……也就是现在的签名内容有问题。。

改掉之后还是报错,看到微信开放社区里一个帖子 说即使报错也受到了微信支付的成功通知,又看到segmentfault里的一个帖子沙箱就是这样。。

我真是艹了

接受回调

坑点:微信SDK送了一个判断支付结果通知中的sign是否有效的方法isPayResultNotifySignatureValid(),这方法默认没有传入签名类型的时候,选的是MD5.

但是微信支付主工具类 初始化签名方式时,根据传入是否沙箱来切换加密方式和URL,此时加密方式是HMAC!!

1
2
3
4
5
6
7
8
9
10
11
12
13
public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
this.config = config;
this.notifyUrl = notifyUrl;
this.autoReport = autoReport;
this.useSandbox = useSandbox;
if (useSandbox) {
this.signType = SignType.MD5; // 沙箱环境
}
else {
this.signType = SignType.HMACSHA256;
}
this.wxPayRequest = new WXPayRequest(config);
}

于是支付回调返回的sign也当然是HMAC,但是支付回调是没有写明签名类型的……这里一定做处理,比较优雅的做法是接受回调时读取配置加入签名类型。。不过我当时是直接改了微信的SDK……

然后是支付回调里没有trade_state,只有result_code,因为只推送成功结果,这点要和主动拉取支付结果做区分。

退款回调

发起退款的流程和发起支付类似,略过不表

退款回调没有sign字段,而且需要JDK装密钥长度无限扩展包

直接解密req_info字段即可,解密过程有几个坑,MD5哈希之后要转成小写,算法名是AES/ECB/PKCS7Padding不能写错,如果抛出需要IV的异常就是算法名写错了……

解密代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    private String decrypt(String reqInfo) throws Exception {
// (1)对加密串A做base64解码,得到加密串B
byte[] encryptB = Base64.getDecoder().decode(reqInfo);
// (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
StringBuilder hexString = new StringBuilder();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(wscWxConfig.getKey().getBytes());
byte[] hash = md.digest();
for (byte b : hash) {
if ((0xff & b) < 0x10) {
hexString.append("0").append(Integer.toHexString((0xFF & b)));
} else {
hexString.append(Integer.toHexString(0xFF & b));
}
}
String md5Key = hexString.toString().toLowerCase();
// (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
// 需要JDK中添加JCE:https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
Key sKeySpec = new SecretKeySpec(md5Key.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
byte[] result = cipher.doFinal(encryptB);
return new String(result);
}

Spring Boot项目公用模块单元测试踩坑

现在手头的项目分为几个子模块,前台一个,后台一个,计划任务一个,Service、bo、dao等放在common模块里,而common模块作为其他几个模块的依赖存在。

在加入Spring环境的单元测试时报了错:

1
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

按提示在测试类加入@SpringBootConfiguration后,报错变了:

1
Parameter 0 of method setXXService in com.XXX.common.XXXServiceImplTest required a bean of type 'com.XXX.common.XXXService' that could not be found.

Google了一下,发现这种情况是Application启动类不存在导致的,可是既然是依赖,自然就没有Application启动类,于是我尝试加入@SpringBootTest(classes="要测试的Service"),然后发现该Service注入到测试类成功了,但是Service中的几个成员注入失败,报错是找不到Bean。

查了一下发现,单元测试执行时,Spring会扫描这个class指定的类所在的包的所有子包,也就是一定要有一个打了@SpringBootApplication注解的类存在于common包下!

先建一个Root.class应付了事,然后慢慢研究:

在测试类加入注解@ComponentScan(basePackages = {"com.XXX.common"}),这回报错又变了:

1
Parameter 0 of method setXXXMapper in com.XXX.common.service.impl.XXXServiceImpl required a bean of type 'com.XXX.common.domain.repository.XXXMapper' that could not be found.

项目中用了Mybatis-Plus,该Mapper接口继承了BaseMapper<>,加了@Mapper和@Repository注解,难道@Repository注解不认??

暂且先改为@Component,报错变成了这样:

1
2
Parameter 0 of method setObjectMapper in com.XXX.common.service.impl.XXXServiceImpl required a bean of type 'com.fasterxml.jackson.databind.ObjectMapper' that could not be found.

原来是忘记把Jackson的ObjectMapper纳入Spring 管理了:

1
2
3
4
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}

接下来依然是Mapper接口报错找不到Bean……无奈先用Root.class顶过去,日后再说)

2019-8-28更新

最后的解决方案是不在Common模块里写测试用例……

动态代理笔记

以前我眼里动态代理一直是个很神秘的东西,面试一旦问到必然摇头回答不知道

想办法理一下,目前动态代理有使用JDK类库 和使用CGLIB 两种方式

JDK类库的大概用法是这样的:

写一个类 写一个接口 叫MyClassMyClassImpl

写一个Handler类,实现InvocationHandler这个JDK反射包里的接口,构造器传入一个MyClass的实例,并且覆盖invoke方法,然后在invoke方法中实现具体的逻辑

如果要使用动态代理,就需要使用JDK的Proxy.newProxyInstance()方法,传入类加载器、代理需要实现的接口、你写的Handler类的一个新实例,并且传入一个MyClassImpl作为这个新实例的构造函数参数。

就可以在方法调用时 动态生成Proxy类,并且实际执行Handler中的逻辑了。

这一套看起来很眼熟,Spring的AOP相关类库 就是对使用动态代理的简化。


但是JDK动态代理 要求被代理的类必须实现接口,碰到没有接口的类 就必须使用CGLIB来操作字节码了。

其实和JDK库类似,只不过实际使用的类库不太一样:

首先写一个MyClass类,再写一个Interceptor类 实现MethodInterceptor这个CGLIB的接口,覆盖intercept方法,然后在intercept方法里实现具体的逻辑

而实际使用动态代理时,new 一个 Enhancer对象(CGLIB提供这个类),设置要代理的类和Interceptor对象,再使用create()方法创建被代理后的对象。

这样做,所有非final方法都会被代理。而由于生成的代理类是继承了原类的,所以final类被代理时会抛出异常。

对于Object类,JDK只代理hashCode()equals()toString(),按官方文档的说法,是“和接口方法一样处理”;

而CGLIB则是代理wait(),notify()notifyAll(),getClass()四个方法,因为它们是final的。

面经读后感-类加载顺序

讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字

段,当new的时候,他们的执行顺序。

死板答案:父类静态字段/静态块 子类静态字段/静态块 父类字段/代码块 父类构造器 子类字段/代码块 子类构造器

解释:
编译器会收集所有字段赋值和静态块 搞成一个方法,顺序由源码顺序决定,并且保证父类的比子类的先执行。
但是常量赋值会在之前执行。
所以必然是父类的静态字段/块,子类静态字段/块。
然后是方法,初始化所有字段,并且调用其他方法。
分几种情况:
如果对应的源码构造器内明确从调用另一个构造器开始,则该方法会先调用对应的方法。
否则该方法会调用父类的方法,初始化字段,然后是该方法本体的字节码)

简要回顾各垃圾回收器的核心思想

人一老 就容易忘事


Serial 新生代单线程 复制到老年代

ParNew 新生代多线程 复制到老年代

Parallel Scavenge 新生代多线程 复制到老年代
关注点在 停顿时间 ,因此可以设置最大回收停顿时间,自动调节各代大小
和G1都没有用传统的GC框架,因此无法和老年代CMS共用


Serial Old 老年代单线程 标记整理

Parallel Old Parallel Scavenge的老年版,标记整理
配合Parallel Scavenge 可以只关注吞吐量

CMS 老年代并发 标记清除 1.4.1
初始标记,标记GC Root(初始标记的root并不包括年轻代,结合并发标记阶段看实际上年轻代也是GC Root的一部分)直达对象,阻塞
并发标记,从初始标记的结果开始爬可达性树,非阻塞
重新标记,并发标记完成后,修正在并发标记期间而变化的记录,阻塞
并发清除

缺点:
需要多核环境
并发清除时产生的垃圾只能留到下次,如果并发清除时内存不足只能Full GC
标记清除带来空间碎片,可以设置在Full GC前整理(默认开启)


G1 管理整个GC堆 但是依然有分代概念 6u14面世 7u4正式推出
准备替代CMS
G1暂停时需要复制对象,CMS暂停时只需要扫描对象,因此6G以下CMS不一定比G1差。推荐6G以上堆,可达到0.5s以下GC时间。

可用对象占用50%以上堆时
对象分配/变老速率显著变化时(CMS并发标记时,如果依然在高速分配内存,会导致很久的remark),
较长时间的GC/压缩发生时(0.5-1s以上)
建议切到G1,可以获得收益

设计理念:
停顿可预测:可以避免收集整个堆,而是跟踪每个Region中的垃圾大小和回收所需时间(也就是价值),优先回收价值高的Region,也是名字的来由
无碎片:内存分为等大的Region,整体为标记整理,实际是复制Region。
并行:扫描对象和复制对象分开运行,并且扫描对象的【初始标记】会借用复制对象的【年轻代复制】步骤。
空间换时间:使用Remembered Set记录老年代指向年轻代的指针,以及使用Collection Set记录需要收集的Region。

收集器部分

复制对象的两种运行模式:
Young gc:新生代满时运行,扫描所有年轻代的Region,找出半死的和全死的Region构成Collection Set。
并行的干掉死透的,并且把半死的Region中活对象复制到别的Region中。通过控制年轻代个数来控制开销

Mixed gc:扫描所有年轻代Region,和 全局并发标记 得出的高价值老年代Region构成Collection Set
。根据用户指定开销来调节价值范围,当mixed gc也清不出足够内存,老年代填满,就会用Searial Old 的核心代码 来Full GC。System.gc()也是Full GC,XX:+ExplicitGCInvokesConcurrent 会改为强行启动一次全局并发标记。

每个Region有对应的Remembered Set,只记录老年代到年轻代的 别的Region对本Region对象的引用,在写引用的时候阻塞,如果Region改变就把引用信息改到新Region的RSet中

全局并发标记的步骤:
初始标记,同CMS只扫描GC Root直达对象,压入扫描栈,阻塞

并发标记,弹栈,递归扫描对象图,还会扫描Write Barrier记录的引用(这些引用是每次改变引用时的老引用),非阻塞

重新标记,标记每个线程各自的Write Barrier(这个Barrier满了之后会丢到全局的去,而每个线程还有一个没满的Barrier),顺带处理弱引用,阻塞。只扫描SATB Buffer,而不是和CMS一样重新扫描整个GC Root,整个年轻代都会被扫描,可能会很慢

清理,类似于标记清理的清理阶段,但是不清理实际对象,而是计算每个Region的价值,根据用户要求的性能水平( -XX:MaxGCPauseMillis)优先清理价值高的,阻塞,所以如果要求的性能太高,反而容易造成垃圾堆积进而Full GC。

CSet永远包括年轻代,因此G1不维护年轻代出发的引用涉及的RSet更新

记录引用变化的部分

SATB Barrier
G1的设计思路是,一次GC开始时 活的对象认为是活的,并且记录为SATB(理解成快照) ,GC过程中新分配的都当做活对象。

GC中新分配的对象容易找,每个Region会记录两个TAMS指针(top-at-mark-start),此后的对象视为新分配的。

但是全局并发标记的并发标记过程中,由于和其他线程并行执行,会出现这种情况:

首先假设有一个对象的引用没有被标记过,记为A(其实就是白色状态)
前提1:给一个已经标记过、并且所有字段被标记完(黑色状态)的对象的字段赋值为A
前提2:并且所有 字段没有被标记完的引用(灰色状态) 到A的引用被删除了

这时候会出现A明明活着 却没有被标记到的情况,因此G1引入了两个WriteBarrier,在改变引用 前后 都会把老引用记下来,哪怕发生了前提2的事,A也会被标记下来。

Logging Barrier

G1为了尽量避免降低改变引用的性能,改变引用时 其实是将老引用加入一个队列,满了之后会被移到一个全局的SATB队列集合,然后换一个新的空队列。

而并发标记会定期检查全局SATB队列集合,当超过一定量时就把它们全部标记上,并且把它们压到标记栈上等后面进一步标记。

Remember Set

传统GC的
G1给每个Region维护一个Remembered Set,它记录别的Region指向本体的指针,并且这些指针分别在哪些Card Table的范围内。

维护Remembered Set的逻辑在改变引用 做,过滤掉从年轻代出发的引用 涉及的RSet维护。

维护RSet时也会采用Logging Barrier的设计思路,在全局队列集合超过一定量时,会取出若干个队列,并且更新RSet。


ZGC
并不是新货,而是Azul很久之前的Pauseless GC,而不如Zing VM的C4。
所有阶段都可以并发,很容易最大暂停控制在1ms内。

不标记对象,而是标记引用,访问引用时有Read Barrier,消耗读取引用时的性能,而干掉了STW。
Region有多种尺寸,根据对象大小分配
每次清理整个Region,因此没有RSet
支持NUMA,提高整体效率
没有分代(暂不完善,还在考虑是分代还是Thread Local GC作为前端),因此只有PGC的水平,遇到高速分配对象只能调大堆内存来喘息

初始扫描:只扫描全局变量和线程栈的指针,不扫描GC堆的指针
并发标记:递归对象图
移动对象:在移动过程中有forward table记录移动,并且活的对象移走后可以立即被释放,可以被其他扫描过程用来复制
修正指针:在修正时同时进行标记