0%

异常章节摘抄

  • 只针对异常的情况才使用异常
  • 对可恢复的情况使用 checked exception, 不可恢复的使用 runtime exception
  • 避免滥用 checked exception
  • 优先使用 JDK 提供的标准异常
  • 抛出与上下文匹配的异常
  • 在方法注释上,为每个抛出的异常编写文档
  • 异常信息中包含失败信息
  • 让失败保持源字形
  • 不要忽略异常

只针对异常的情况才使用异常

展示一个产品代码中遇到的违反这条建议的的奇葩例子。底层 team 给了一个很奇葩的接口,一个 findPersonIdByPersonUUID 的方法,当找不到结果的时候,竟然会抛出 exception 而不是返回 null, 导致我这个调用方处理起来像是吃了屎一样难受。。。而且这样的逻辑把 ‘找不到’ 和 ‘代码异常’ 两中情况混在一起了,抛出了异常之后都不知道具体的 root cause,简直了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 底层实现
public class BadPracticeServiceA {
public long getPersonIdByPersonUUID(String personUuid) {
Long person = getPersonId();
if (person == null) {
throw new ServiceApplicationException("Can not find PersonID by PersonUUID");
}
return person;
}
}

// 作为上层的调用者,我在调用上述方法的时候不得不显示的处理他抛出的异常
try {
long target = badPracticeServiceA.getPersonIdByPersonUUID("uuid");
} catch (IllegalStateException e) {
log.info("Exception when get person id by person uuid {}, detail log msg: {}", "uuid", e.getMessage());
}

return null;

看 Effective Java 中 Stack 实现的时候又有了新的感悟。这里的 get 方法和 stack 里的 pop() 方法很像,而 pop 在拿不到值的时候会抛异常,和上面找不到的情况也很类似。不过相对以 Stack 的例子,上面如果要在找不到的时候抛异常,也不应该是 Exception 而是更友好的 RuntimeException。或者其他自定义的 NotFoundExcepiton 才对。

对可恢复的情况使用 checked exception, 不可恢复的使用 runtime exception

  • 如果期望调用者能够适当的恢复,对于这种情况应该使用 checked exception
  • runtime exception 和 error 行为上是等同的,没有捕获,当前线程停止
  • runtime exception 表明程序错误

避免滥用 checked exception

滥用 checked exception 会使你的 API 调用极其麻烦。一旦抛出 checked exception,调用者就必须 catch 他或者在方法标签中 throw,传播出去,无论哪种处理方式,都是对调用者不可忽视的负担。

绕过 checked exception 的方法:

  • 使用 runtime exception 代替
  • 重构逻辑

优先使用 JDK 提供的标准异常

常见的标准异常

  • IllegalArgmentException
  • IllegalStateException
  • NullPointException
  • IndexOutOfBoundsException
  • ConcurrentModificationException
  • UnsupportedOperaitonException

抛出与上下文匹配的异常

底层异常,传递到上层是可能和当前上下文不匹配,污染 API。这是需要在上层转译一下,重新抛出异常。如果异常栈对错误排查很有帮助,可以将 exception chain 一并传递给新的异常(Throwable 的 initCause 方法)

异常转译比直接传递底层异常好,但更好的处理方式应该是调用底层方法之前确保他能执行成功,避免抛出一场。有时调用底层之前,先做一下参数监测可能是更好的方式。

如果无法避免底层异常,次选方案是让上层悄悄绕开这些异常,从而使上层调用者和底层问题隔离开来,通常这是还会打个 log 记录,方便回溯问题。

前段时间,组内连续出了两个和 SCA 的 role/feature config 相关的 P1 issue,由于不知道它的实现原理,只能通过对比 PR 来找问题,太 low 了,特意读一下他的源码,学习一下下次再出现这种情况要怎么处理

Unified Authorization Architecture 是什么

这套框架的最终目的是阻止没有授权的用户执行某些 service command。从 MVC 分层上来说,它是在 controller 和 service 之间新加了一层逻辑,每次 service 执行之前都会检测一下,当前用户是否有权执行该 service。

检测分两层,第一层是 company feature test, 第二层是 Role test

怎么配置

每个 repo 的 resource 文件夹中都有两个配置文件,分别叫做 featuer-access.xml 和 role-access.xml。他们就是两层检测的配置文件

Feature-protected Service Command

feature 控制配置文件如下

1
2
3
4
5
6
7
8
<activity-feature-map>
<module>
<activity id="com.path.CreateUser">
<feature id="Manage User"/>
</activity>
<!-- ... -->
</module>
</activity-feature-map>

他表示的意思是,对于当前 company,只有开启的 Manage User 这个 feature 才被允许执行 CreateUser 这个 service。所有可用的 feature 都存在 FeatureEnum.java 中。他位于 provisioning 文件夹下,那我估计他是和 provisioning 里的 company feature 挂钩的,之前还以为是和 RBP 的 permission 有关呢。

PS: 但是当前几乎所有的 feature 配置都会默认配置 <feature id="*"/>,也就是说,feature check 名存实亡

Role-protected Service Command

基于 role 的校验可以达到没有配置的用户不能执行 service 的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<activity-permission-map>
<module>
<activity-group id="myGroup01">
<activity id="com.path.CreateUser"/>
</activity-group>
<activity-group id="myGroup02">
<activity id="com.path.DeleteUser"/>
</activity-group>

<role id="processJob">
<activity-group refid="myGroup01"/>
</role>
<role id="user">
<activity-group refid="myGroup02"/>
</role>
</module>
</activity-permission-map>

按照上述配置,login user 只能执行 delete user 这个 service 而不能执行 create user,如果执行了会抛出异常

所有支持爹 role 定义在 PermissionBean 中,也可以去 Permission table 的 permission_type 中查看

特殊的 role

当一个用户通过 /login, /samllogin 登陆到系统中后,对应的 permission 会存到 PermissionListBean 中。但还有其他特殊情况

User type Role Notes
Provisioners provisioner users authenticated via /provisioning_login.
SFV4 Client or Quartz Job processJob allows the SFV4 client/Quartz Job to execute service
Any Login User user any user authenticated via /login or /samllogin

还有一个很神奇的 User type, Anonymous, ParamBean 有一个方法 getAnonymousPrincipal() 注释说,这个 type 表示一个没有经过授权的 user, 猜测,比如,登陆前需要做一些测试,比如看 account 是否存在,这个时候就要用到这种类型的 user type 了。

工作流程

https://confluence.successfactors.com/display/ENG/Unified+Authorization+Architecture+Technical+Specification

AppSec, short for application secirity. 看他的描述,是参考了 OWASP ESAPI 这个项目。

This API will provide initially just two things:

  • A thread local to store the current user
  • A set of access control APIs

The ThreadLocal principal is designed to be populated and cleared by a Servlet filter

逻辑推测,server 启动的时候会加载分析 role/feature 配置文件,并存到一个静态类中,作为共用部分。后续当 SCA engine 调用 impl 之前,都会有一个 auth check 的过程。过程中会使用 AppSec 作为入口检测权限。大致过程就是这样,再细致的分析就得涉及到 RBP 那部分的内容了。最主要的参考文档应该是这个

https://confluence.successfactors.com/display/ENG/Unified+Authorization+Architecture+Technical+Specification

他告诉这个过程的规范,涉及到的类和关系等

public class AppsecConfigListener implements ServletContextListener 这个 class 被注册到 web.xml 中,看实现的接口应该是 servlet 启动结束后,到各个 module 中的加载 feature 和 role 的配置信息。

ProvisioningLoginServlet.java - process() - provisionerBean.createProvisionerParamBean()

public ParamBean createProvisionerParamBean(CompanyBean cb) {
ParamBean p = ParamBean.createDefaultParamBean(cb, PROVISIONER_PARAMBEAN_ROLE);
p.getAppSecRoles().addAll(getAppSecRoles());
return p;
}

ParamBean role as provisioner, 并将 sf_provisioner 中 flag 这个 column 里解析出来的 permission 加上 APP_SEC_PREFIX 添加到 role 中

UiAuthenticationProcessorImpl.java - process() - start login

GenericAuthorizationFilter 每次结束之后会将 appsec current user 置为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
09:35:44,710 ERROR [AuditConfigExecutor] getConfigsFromDb error, company:QAAUTOCAND_RCMGoldenApp7.
sf_class=com.successfactors.appsec.AbstractAccessController line=39 method=assertAuthorizedFor depth=1
AccessDeniedException{user=[RCMGoldenApp7,RCMGoldenApp7,QAAUTOCAND_RCMGoldenApp7.,dbPool1,null,null,en_US] , activity=Activity{type=SERVICE, name='com.successfactors.auditloggingservice.service.audit.command.AuditConfigQueryCmd', context=null}}
at com.successfactors.appsec.AbstractAccessController.assertAuthorizedFor(AbstractAccessController.java:39)
at com.successfactors.appsec.AppSec.assertAuthorizedForService(AppSec.java:231)
at com.successfactors.sca.ServiceCommandEngine.authorizeService(ServiceCommandEngine.java:312)
at com.successfactors.sca.ServiceCommandEngine.execute(ServiceCommandEngine.java:238)
at com.successfactors.sca.service.spring.ServiceCommandProcessorSpring.execute(ServiceCommandProcessorSpring.java:451)
at com.successfactors.sca.service.handler.spring.SpringAppSCAHandler.execute(SpringAppSCAHandler.java:36)
at com.successfactors.auditloggingservice.service.audit.executor.AuditConfigExecutor.getConfigsFromDb(AuditConfigExecutor.java:85)
at com.successfactors.auditloggingservice.service.audit.executor.AuditConfigExecutor.getConfigValue(AuditConfigExecutor.java:55)
at com.successfactors.auditloggingservice.service.audit.executor.AuditConfigExecutor.getConfigValue(AuditConfigExecutor.java:49)
at com.successfactors.auditloggingservice.service.audit.executor.AuditKafkaSaveExecutor.needSave(AuditKafkaSaveExecutor.java:118)
at com.successfactors.auditloggingservice.service.audit.executor.AuditKafkaSaveExecutor.update(AuditKafkaSaveExecutor.java:80)
at com.successfactors.auditloggingservice.service.audit.executor.AuditKafkaSaveExecutor.lambda$0(AuditKafkaSaveExecutor.java:69)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)

怎么测试

针对之前的两个 P1 issue 应该怎么写测试

市面上的同类型产品

https://confluence.successfactors.com/display/CST/12+SCA+Home 这个页面可以应该是 最全的 sca 文档了

ESAPI

    <role id="processJob">
        <activity-group refid="PendingData"/>
        <activity-group refid="ScimUserResoure"/>
        <activity-group refid="personAssignmentDataHandler"/>
        <activity-group refid="igsintegration"/>
    </role>
    <role id="user">
        <activity-group refid="PendingData"/>
        <activity-group refid="ScimUserResoure"/>
        <activity-group refid="personAssignmentDataHandler"/>
        <activity-group refid="igsintegration"/>
    </role>
    <role id="anonymous">
        <activity-group refid="PendingData"/>
        <activity-group refid="ScimUserResoure"/>
        <activity-group refid="personAssignmentDataHandler"/>
    </role>
</module>

a user which has not been authenticated.

curl –location –request GET ‘http://localhost:8080/rest/iam/scim/v2/Users/0bfa4ec0-a35d-4f64-881f-ed759108ff47'

AppsecConfigListener

ServiceCommandEngine

总结柱状图,分布曲线,平均值等概念

柱状图和分布曲线

柱状图: 通过方块面积表示占比

分布曲线: 是柱状图的一个大致的拟合

正态分布曲线: 分布曲线的一种特殊形式

mean vs median vs mode

  • mean 很好理解,就是平均数,(N0 + N1 + … Nn )/n
  • median 所有数排序,取中间那个即为 median
  • mode, 用单词 most often 来解释好了,出现频率最高的那个数。比如有数组 1,2,3,3,5,5,5,5 那么 mode 为 5

最近在看其他 team 的代码时,看到一个在 Filter 中添加 AutoCloseable 的方式来达到记录 perf log 的目的,听新颖的,记录一下并熟悉一下 AutoCloseable 接口的用法

示例

在这之前,我一直以为,想要在方法(request)执行前后记录执行时间,只能通过新建一个 拦截器 或者使用类似 Aspect 的技术。API team 的这个实现着实让我对 AutoCloseable 的使用有了新认识,之前对这个接口只是看到过的程度,哈哈。大致模型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// perf 类实现 AutoCloseable 接口,在 close
public class SimpleAutoCloseable implements AutoCloseable {

public SimpleAutoCloseable(String name) {
System.out.println("Start to record time cost for scope: " + name);
}

@Override
public void close() throws Exception {
System.out.println("Stop record, print time cost: xx ms");
}
}

public class Client {
public void doFilter() throws Exception {
try(SimpleAutoCloseable auto = new SimpleAutoCloseable("Request")) {
System.out.println("special filter logic...");
System.out.println("filterChain.doFilter(request, response)");
}
}
}

测试类中,我们调用 client 的方法触发 try-with-resource 流程,查看 console log, 可以看到当 chain 的 doFilter 整体执行结束之后,自动打印了 perf 信息,666

1
2
3
4
5
6
7
8
9
10
@Test
public void test_AutoCloseable_class_will_execute_close_after_try_block() throws Exception {
Client client = new Client();
client.doFilter();
}

// Start to record time cost for scope: Request
// special filter logic...
// filterChain.doFilter(request, response)
// Stop record, print time cost: xx ms

在同事的安利下安装完了 Superset, 然后大致浏览了一下他的功能特性,发现并不是我想要的。。。他属于一个 BI(Business Inteligence)工具,定位应该是提供了一整套数据可视化的方案。让你可以很方便的将数据展示给别人看。对于我现在的需求,我想要画一下某组数据的分布曲线,这个需求反而太原始了,用这个工具有点牛刀小用,而且效果不怎么好,可能我还是得回到 jupyter 那边去,哈哈

准备工作

docker 方式安装 Superset 的步骤中有一步是 load_example, 这步的过程中会去 github 上下载案例。不过由于 GFW 的问题,下载会失败。解决方案为,手动下载这个 repo 并在本地起一个服务器挂载这些资源,然后修改 docker 中下载 sample 脚本的配置即可。以下是下载资源,起服务部分配置

1
2
3
4
5
6
7
8
9
10
11
12
13
wget https://github.com/apache-superset/examples-data/archive/refs/heads/master.zip
unzip master.zip
# 或者
git clone https://github.com/apache-superset/examples-data.git

# 如果是 zip 包方式,输入 cd examples-data-master
cd examples-data
# 启动服务器,这是你通过访问 localhost:9999 就能看到这个网站服务了
python -m http.server 9999
# 查看本机 ip 用于后续修改下载地址
# 显示结果中有一行格式类似 inet xx.xx.xx.xx netmask 0xffffe000 broadcast xx.xx.xx.xx
# 第一个 ip 段就是我们想要的
ifconfig | grep inet

安装 Superset

访问 dockerhub 查看安装文档,其实就是跟着指导 CV 一遍指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 拉镜像并启动服务,通过 -p 参数修改外部暴露的端口 -p port_you_want:8088
docker run -d -p 8080:8088 --name superset apache/superset
# 创建账户
docker exec -it superset superset fab create-admin \
--username admin \
--firstname Superset \
--lastname Admin \
--email admin@superset.com \
--password admin
# 升级 DB
docker exec -it superset superset db upgrade
# 修改下载地址
docker exec -it superset /bin/bash
sed -i 's/BASE_URL = .*"/BASE_URL = "http:\/\/xx.xx.xx.xx:9999\/"/g' superset/examples/helpers.py
sed -i 's/https:\/\/github.com\/apache-superset\/examples-data\/raw\/master\//http:\/\/xx.xx.xx.xx:9999\//g' superset/examples/configs/datasets/examples/*.yaml
sed -i 's/https:\/\/github.com\/apache-superset\/examples-data\/raw\/lowercase_columns_examples\//http:\/\/xx.xx.xx.xx:9999\//g' superset/examples/configs/datasets/examples/*.yaml
sed -i 's/https:\/\/raw.githubusercontent.com\/apache-superset\/examples-data\/master\//http:\/\/xx.xx.xx.xx:9999\//g' superset/examples/configs/datasets/examples/*.yaml
exit
# 加载案例
docker exec -it superset superset load_examples
# 官方说是 setup roles, 目测是账号权限之类的东西
docker exec -it superset superset init

到这里安装结束,通过访问 http://localhost:8080/login/ 查看页面,使用 admin/admin 登陆

mean vs median vs mode

  • mean 很好理解,就是平均数,(N0 + N1 + … Nn )/n
  • median 所有数排序,取中间那个即为 median
  • mode, 用单词 most often 来解释好了,出现频率最高的那个数。比如有数组 1,2,3,3,5,5,5,5 那么 mode 为 5

最近这个从 session 中删除 provisioner bean 的工作做的确实不细致,有点懈怠了。在项目开始之前就没有做好工作安排,导致测试环境上出了 bug, 而且被其他 team challenge 了,实力打脸。以后应该吸取这个教训,案件还原如下。

某日 MCAP 的需求提上日程(MCAP 是将现有服务移到公有云上的这么一个项目,所以要减小内存开销),突然要落地这个 remove 的项目。前期有调研过,可行性方面还是没问题的,原始业务如下:

在登陆时,将 provisioner bean 塞入 session, 在后续如果需要用到这个 bean, 通过 context.get("provisionerBean") 或者 @Inject ProvisionerBean provisionerBean 的方式获取。在查看原有业务时,我们发现,在存入 bean 的时候还会将对应的 id 存入 session。 那么作为解决方案,我们可以在现有拿 bean 的地方,通过拿 id 并查 DB 拿到 user 的方式绕过去。

在给 module team 实施修改方案的时候,他们提出一些 concern:多了访问 DB 的过程,有降低 performance 的风险。和 arch review 了这种风险,结合我们的实际使用场景,这个 provisioner 是后台管理人员,作为维护和系统 setup 的角色,使用频率很低,所以可以忽略这种风险。

目前为止都 OK 但是改代码的时候就有点粗暴,原来使用方式

1
2
3
4
5
ProvisionerBean provisionerBean = SFContext.getContext().getInstance(SessionConstants.PROVISIONER_BEAN, false);
// 或者
@Qualifier(SessionConstants.PROVISIONER_BEAN)
@Inject
private ProvisionerBean provisionerBean;

这种使用方式是不会抛异常的,只要 get 就行了,但是用查 DB 的方式代替之后,会额外附带异常处理类似

1
2
3
4
5
6
7
8
String provisionerId = SFContext.getContext().getInstance(SessionConstants.PROVISIONER_ID, false);
ProvisionerDataService provisionerDataService = SFContext.getContext().getInstance(ProvisionerDataService.NAME, true);
ProvisionerBean provisionerBean = null;
try {
provisionerBean = provisionerDataService.findProvisionerById(provisionerId);
} catch (ServiceApplicationException e) {
LOGGER.error("Exception occured while getting provisionerBean",e);
}

module team 反馈这种改法侵入性太强,会对他们的代码结构有很大的改变,涉及到源码和大批的 UT 改动。和他们讨论之后,我们打算将这个改动封装到我们内部的类中,并通过统一的接口暴露给外部使用。接口中处理异常,module 只负责调用而不产生其他副作用

1
2
3
4
5
6
7
8
9
10
11
12
// provisionerServiceImpl.class
fetchProvisionerFromContext() {
String provisionerId = context.get("provisionerId");
ProvisionerBean provisionerBean = null;
if (provisionerId != empty) {
provisionerBean = service.findProvisionerFromDB(provisionerId);
} else {
provisionerBean = context.get(provisionerBean);
}

return provisionerBean
}

上面的只是简化版,实际代码中的逻辑还包括了从 session 中拿 provisioner bean 和 db 中查询结果做比较,如果不同则返回 session 中的结果并打印 log 记录。然后我们有另外一套 Splunk 的系统,我们可以在代码部署后通过检查 log 检测是否有预期外的行为,而且这种方式不会对现有的行为产生任何影响。

然而世事难料,和 Arch review 代码的时候,他提出了一种新的修改方式,由于我们要改的那些东西是在 SFContext 里面的,我们也许可以通过框架层面的修改来改变 bean 的生成方式,这种方式的侵入性是最小的。ProvisionerBean 是通过 factory bean 的方式向 context 的中注入 bean 的,我们可以 refactor 一下生成方式,改为先从 session 中拿到 id 然后调用 service 拿 DB 数据组成 bean。这样的话,所有调用点都不用改了,perfect!

又经过一轮 research,大致确定了通过修改 factory bean 的方案,我们返回 prototype 类型的 bean, 使用过后通过 GC 回收。而且后续其他 module 需要修改的代码量减少了很多, 对比如下

Before After
repo:13, call: 34 repo: 6, call: 9

如果修改点比较多,还需要专门建立一个文档记录修改做追踪用

感觉这才是 Arch 这个角色的价值。contract, 就硬改,也不会看修改的地方是什么feature,不会想测测是不是有可能做回归。我现能达到的程度是,能够将这个 refactor 的任务条理理清楚,做改动的时候有这个 sense 去看看调用栈,看看会不会有什么没考虑到的情况并考虑如何测试改动。Arch 则是看了要改的地方,先看看系统本身是不是有地方提供了统一处理的能里,在达到目的的同时,将影响最小化,做法很优雅。

快速了解 AOP 必要知识并给出 demo。这篇文章是在我系统学了 AOP 的基本使用之后再写的,之前遇到的很多痛点都体现不出来了。花了挺多时间读官方文档的时候,然后很多问题都迎刃而解了,就是比较花时间。之前失败的主要原因是 pointcut 的表达式写错了,tomcat 起了,但是服务都失败了。。。。

简单概括什么是 AOP

不破坏代码结构的情况下,为代码添加功能,典型案例如打印方法的执行时间。

AOP 涉及的专有名词

  • Joinpoint - 要匹配的方法
  • Pointcut - 匹配方法的规则
  • Advice - 检测到匹配方法的时候要执行的 额外 逻辑
  • Aspect - 带 @Aspect 的那个类

详细的指代会在下面的例子中标记出来,光是名词实在难记

怎么写

如果 Spring 整体环境已经搭建完成,如果你想要新建一个 Aspect 只需要做两件事

  1. 写 Aspect 文件,最简单的方式是写 Advice 方法,并在注解中添加 pointcut 表达式
  2. 注册 Aspect, 可以通过 Xml 或者 Annotation 注册

在实际例子中标注 AOP 相关的概念,直接看名词很难记住,理解

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
// @Aspect 指代的就是一个切面,即 aspect 这个概念
@Aspect
@Component
public class MyAspect {

// pointcut expression, 表示匹配规则,哪些方法需要被处理
@Pointcut("target(official.AopTestService)")
public void pointcutName() {}

// @Around 即为 Advice, 表示切入方式,其他还有 @Before, @After 等多种方式
@Around("pointcutName()")
// 可以合并简写为 @Around("target(official.AopTestService)")
// join point 表示被拦截的方法,可以从它里面拿到方法名,参数等信息
public Object aroundSayHello(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before join point...");
Object obj = joinPoint.proceed();
System.out.println("After join point...");
return obj;
}
}

// 目标 service,当 service 方法执行时会触发切面
@Component
public class AopTestService {
public void sayHello(String name) {
System.out.println("Hello, " + name);
}

public void greet() {
System.out.println("Hello, there");
}
}

// 配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "official")
public class AppConfig {}

// 执行方法
public class Client {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
ctx.getBean("aopTestService", AopTestService.class).greet();
ctx.getBean("aopTestService", AopTestService.class).sayHello("Jack");
}
}

原理剖析

所有的实现都是基于代理模式完成的。基础版本,不借助任何 lib, 纯 Java 实现。假设我们有 ISubject 接口,和对应的实现类 SubjectImpl,代理类 SubjectProxy。我们可以通过调用 SubjectProxy 来触发 SubjectImpl 并添加我们自己的定制逻辑。

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
// 抽象的业务逻辑接口
public interface ISubject {
void request();
}

// 具体的实现类
public class SubjectImpl implements ISubject {
@Override
public void request() {
System.out.println("Invoke request in SubjectImpl...");
}
}

// 代理类,可以添加定制的方法
public class SubjectProxy implements ISubject {
private ISubject subject;

// constructor + set method

@Override
public void request() {
System.out.println("Call request in proxy...");
subject.request();
}
}

// 调用场景
public class Client {
public static void main(String[] args) {
ISubject subject = new SubjectProxy(new SubjectImpl());
subject.request();
}
}

进阶使用,如果还有其他接口比如 IRequestable 也有 request 方法,那么为了实现代理,我们还需要为他新建一个代理类,如果类似的类很多,就会有很多代码冗余。JDK提供了动态代理的机制解决这种问题,概括来就是给出时间对象和目标接口,就能实现代理逻辑的统一操作

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
public class RequestCtrlInvocationHandler implements InvocationHandler {
private Object target;

public RequestCtrlInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Invoke method in InvocationHandler...");
if (method.getName().equals("request")) {
return method.invoke(target, args);
}
return null;
}
}

public class InvocationHandlerClient {
public static void main(String[] args) {
SubjectImpl impl = new SubjectImpl();
ISubject subject = (ISubject) Proxy.newProxyInstance(
impl.getClass().getClassLoader(),
new Class[]{ISubject.class},
new RequestCtrlInvocationHandler(impl));
subject.request();
}
}

再进一步,上面的方法只能处理实现了接口的情况,如果类并没有实现接口,还想要增强的话,我们需要借助 Cglib 这个第三方库,在类上派生出一个子类,通过复写方法达到扩展的目的。

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
// 目标类,虽然后 request 方法,但是没有任何的接口
public class Requestable {
public void request() {
System.out.println("Call in Requestable, without interface...");
}
}

// 通过 Cglib 实现的方法扩展
public class RequestCtrlCallBack implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("request")) {
System.out.println("Invoke method in MethodInterceptor...");
return methodProxy.invokeSuper(o, objects);
}
return null;
}
}

// 调用方式
public class CallBackClient {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Requestable.class);
enhancer.setCallback(new RequestCtrlCallBack());

Requestable proxy = (Requestable) enhancer.create();
proxy.request();
}
}

Spring 的实现中,会判断是否有接口,如果有则使用动态代理,如果没有则使用字节码增强。

奇怪的 behavior

最近发现产品上的 AOP 代码在两个 aspect 嵌套的情况下只会执行第一个 aspect,第二个直接跳过了,不知道是公司特有的还是 AOP 本来就有这种设定,特意检测一下

场景重现:
service A 有两个 method01,method02 并且 method01 会调用 method02. 创建 Aspect 同时覆盖这两个方法,当 method01 执行时,method02 并不会被检测到。

搜了一下 stackoverflow, 有人指出这个现象底层原理已经在 5.8.1 中写了。。。。proxy 之后,对自己的调用将会失效
Spring 4.3 之后可以通过 class 中注入本身来绕过这个问题
stackoverflow exp + solution

Name Usage
SMTP simple mail transfer protocal, 相当于中转站,将邮件发送到客户端
POP3 Post Office Protocol 3,将邮件从服务器下载到本地,同时删除邮件。是接收邮件的协。
IMAP Internet Mail Access Protocol, 支持文件夹功能
Exchange server 微软提供的邮件服务

python 案例

1
2
3
4
5
6
7
import smtplib

smtp = smtplib.SMTP("mail.sap.corp", 587)
smtp.starttls() # this seting is required
smtp.login("sf-pla-usermanagement-authentication-mails","Authentication1")
smtp.sendmail('noreply+sf_pla_usermanagement_authentication@sap.corp', 'jiabin.zheng01@sap.com', message)
smtp.quit()
1
2
3
4
5
6
7
8
spring.mail.host=mail.sap.corp
spring.mail.port=587
spring.mail.username=sf-pla-usermanagement-authentication-mails
spring.mail.password=Authentication1
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.from=noreply+sf_pla_usermanagement_authentication@sap.corp

常见的 log 使用方式

今天看到同事给我代码 review 的时候推荐使用 log.info(e.getMessage()) 时,不太清楚推荐的原因,特意 Google 了一下几种 log 记录方式

  • info(ex)
  • info(ex.getMessage())
  • info(“msg”, ex)

总的来说,第三种最好,前两种只会记录当前类的异常抛出记录,之前的信息都 miss 掉了

PS: 在 SCIM API 项目中,实现也是只传了 e.getmessage() 而没有传递 ex 这个对象,导致追踪困难,引以为鉴

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
public class ExpTest {
public static int testMethod() {
return 1 / 0;
}
}

public class ExpClient {
private static final Logger logger = Logger.getLogger(ExpClient.class);

public static void main(String[] args) {
try {
ExpTest.testMethod();
} catch (Exception e) {
System.out.println("------------------------------> msg, e <------------------------------");
logger.info("err...", e);
System.out.println("------------------------------> e <------------------------------");
logger.info(e);
System.out.println("------------------------------> msg <------------------------------");
logger.info(e.getMessage());
}
}
}

// 终端输出如下:
// ------------------------------> msg, e <------------------------------
// INFO [main] (ExpClient.java:14) - err...
// java.lang.ArithmeticException: / by zero
// at sementic.ExpTest.testMethod(ExpTest.java:5)
// at sementic.ExpClient.main(ExpClient.java:11)
// ------------------------------> e <------------------------------
// INFO [main] (ExpClient.java:16) - java.lang.ArithmeticException: / by zero
// ------------------------------> msg <------------------------------
// INFO [main] (ExpClient.java:18) - / by zero

try-catch 执行流程

今天在写一段补偿代码的时候突然想到一个问题,当异常发生时,后续代码是否还会被执行的问题。测试前根据主观猜测,感觉如果有 try-catch, 那么会继续执行;没有则直接跳出了,相当于 return.

Scenario 1

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test1 {
public static void main(String[] args) {
try {
int a = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Log after exception");
}
}

// > Task :idl-sfutil:idl-sfutil-service:Test1.main()
// Log after exception

抛错后继续执行

Scenario 2

1
2
3
4
5
6
7
8
9
public class Test1 {
public static void main(String[] args) {
int a = 1/0;
System.out.println("Log after exception");
}
}

// Exception in thread "main" java.lang.ArithmeticException: / by zero
// at com.sf.sfv4.util.Test1.main(Test1.java:5)

抛错后没有执行。抛出异常后,相当于 return,直接结束当前方法。switch-case 中也是一样

1
2
3
4
5
6
7
8
swithc()
{
case scenario1:
throw new RuntimeException();
// break; break 会有编译错误
default:
doSth();
}