前言
我是从18年11月份入职的,一直做的是Java开发,起初和大部分人一样都是CRUD,直到去年年底一个小项目让我做了技术经理,虽然我在项目上受到了比较大的打击(做己方的话如果真的遇到一个很难对付的甲方简直让人崩溃),但也确实让我获得很多技术上的成长
写这篇博客主要为了回顾一下自己过去做过的项目,也梳理一下面试中谈到项目这块可以说出的亮点,如果对看到这篇博客的你有帮助那就更好了O(∩_∩)O
下文中提到的项目上的亮点如下:
1)、使用反射+枚举的方式记录变更日志
2)、使用策略模式+工厂模式封装银企直连服务
3)、基于GitLab CI实现CI、CD流程,以及jib打包自签名问题的解决
4)、使用Rancher搭建Kubernetes集群
5)、基于OpenShift平台部署Apollo配置中心遇到的问题,以及对Apollo实现原理、Eureka服务发现原理的思考
6)、对接单点登录,以及OAuth授权码模式的思考
1、使用反射+枚举的方式记录变更日志
业务需求:
对比原单据和变更后的单据生成变更日志,即哪些字段发生了变更并且记录变更前和变更后的值
使用注解的主要达到两个效果:
1)、只有该注解标注的属性才会生成变更日志
2)、标注相应字段来设置属性的描述信息(前台页面上的中文描述)
工具类核心方法传参是两个Object对象,主要实现逻辑如下:
1)、只有两个对象都是同一类型才有可比性
2)、通过反射循环获取父类Class对象,直到父类为null的时候说明到达了最上层的Object类,在循环中获取当前类的Field放入到List中
3)、循环所有Field的List,使用setAccessible()
方法设置对象的访问权限,保证对private的属性的访问,判断只有使用注解标注的属性才会生成变更日志,通过isEquals()
方法判断变更前后的属性值是否一致,不一致生成变更日志对象放入List中
这里调用一个新声明的isEquals()
方法主要是为了方便子类根据不同的业务逻辑去扩展,不是所有场景下都直接调用equals()
方法即可,比如说BigDecimal如果使用equals()
方法比较的是数值+精度,比如1.0和1其实数值相同,但精度不同,equals()
方法返回false,但其实实际金额并未发生改变,所以当时实现的时候子类判断如果是BigDecimal类型,使用compareTo()
方法来判断是否产生了变更
详细解决方案:https://blog.csdn.net/qq_40378034/article/details/104158786
2、使用策略模式+工厂模式封装银企直连服务
业务需求:
资金系统需要对接10多家银企直连,根据传入的支付方式判断走哪一家银行,然后调用相关方法
实现思路:
开发银企直连服务虽然是包含10多家银行,但业务逻辑上需要提供的方法是一致的(支付走单笔、支付走批量、查询支付结果、查询账户余额等等),首先根据业务需求写一个统一的Interface接口,每个Service实现这个统一的Interface
但根据传入的支付方式判断走哪一家银行,调用具体的哪个实现类,是通过了注入Map的方式,Spring会自动将Strategy接口的实现类注入到这个Map中,key为bean id,value值则为对应的策略实现类
@Component
public class StrategyFactory {
//Spring会自动将Strategy接口的实现类注入到这个Map中,key为bean id,value值则为对应的策略实现类
@Autowired
private Map<String, Strategy> strategyMap;
public Strategy getBy(String strategyName) {
return strategyMap.get(strategyName);
}
}
然后做了一个别名转换的处理
@Component
@PropertySource("classpath:application.properties")
@ConfigurationProperties(prefix = "strategy")
public class StrategyAliasConfig {
private HashMap<String, String> aliasMap;
public static final String DEFAULT_STATEGY_NAME = "defaultStrategy";
public HashMap<String, String> getAliasMap() {
return aliasMap;
}
public void setAliasMap(HashMap<String, String> aliasMap) {
this.aliasMap = aliasMap;
}
public String of(String entNum) {
return aliasMap.get(entNum);
}
}
配置文件application.properties
strategy.aliasMap.strategy1=concreteStrategy1
strategy.aliasMap.strategy2=concreteStrategy2
@Component
public class StrategyFactory {
@Autowired
private StrategyAliasConfig strategyAliasConfig;
//Spring会自动将Strategy接口的实现类注入到这个Map中,key为bean id,value值则为对应的策略实现类
@Autowired
private Map<String, Strategy> strategyMap;
//找不到对应的策略类,使用默认的
public Strategy getBy(String strategyName) {
String name = strategyAliasConfig.of(strategyName);
if (name == null) {
return strategyMap.get(StrategyAliasConfig.DEFAULT_STATEGY_NAME);
}
Strategy strategy = strategyMap.get(name);
if (strategy == null) {
return strategyMap.get(StrategyAliasConfig.DEFAULT_STATEGY_NAME);
}
return strategy;
}
}
详细解决方案:https://blog.csdn.net/qq_40378034/article/details/104121363
3、基于GitLab CI实现CI、CD流程,以及jib打包自签名问题的解决
项目上实现的主要流程如下:
提交代码到dev-latest
之后,在GitLab上打tag来触发CI、CD的流程,步骤一先通过jib生成镜像推送到镜像私库(如果是一个完整的流程可以在步骤一之前添加单元测试、Sonarqube代码扫描的流程),步骤二通过Rancher的devops镜像(这里使用的docker runner)使用Rancher的命令行执行发布
主要遇到的坑:
客户提供的Harbor仓库用的是自签名Https证书,而Jib使用JVM的已批准CA证书列表来验证SSL证书,最后是通过一个工具类生成证书相关的文件然后放到JAVA_HOME/jre/lib/security
目录下,达到的效果就是证书在JRE级别受信任
详细解决方案:https://blog.csdn.net/qq_40378034/article/details/104750483
4、使用Rancher搭建Kubernetes集群
使用Ranche搭建Kubernetes集群时,每个主机有三种角色可以选择:Etcd、Control Plane和Worker
1)、Etcd
etcd节点的主要功能是数据存储,它负责存储Rancher Server的数据和集群状态。Kubernetes集群的状态保存在etcd中,etcd节点运行etcd数据库。etcd数据库组件是一个分布式的键值对存储系统,用于存储Kubernetes的集群数据,例如集群协作相关和集群状态相关的数据
etcd更新集群状态前,需要集群中的所有节点通过quorum投票机制完成投票。假设集群中有n个节点,至少需要(向下取整) 个节点同意,才被视为多数集群同意更新集群状态。例如一个集群中有3个etcd节点,quorum投票机制要求至少两个节点同意,才会更新集群状态
2)、Control Plane
Control Plane节点上运行的工作负载包括:Kubernetes API Server、Scheduler和Controller Mananger。这些节点负载执行日常任务,从而确保集群状态和集群配置相匹配。因为etcd节点保存了集群的全部数据,所以Control Plane节点是无状态的
3)、Worker
Worker节点运行以下应用:
- Kubelet:监控节点状态的Agent,确保容器处于健康状态
- 工作负载:承载应用和其他类型的部署的容器和Pod
5、基于OpenShift平台部署Apollo配置中心遇到的问题,以及对Apollo实现原理的思考
部署架构及遇到的问题:
Apollo相关的Config Service、Admin Service和Portal都是封装在一个镜像中,作为一个独立的服务,但没有注册到Apollo自身的Eureka Server,而是和其他应用服务共用一个Eureka Server(为保证Apollo高可用)
当时部署是基于OpenShift平台,我们对K8S相关的知识也不太熟悉,最后达到的效果是服务间可以通过服务名(Service Name)调用,但是不能通过注册到Eureka Server中每个Pod的IP来访问服务
Eureka中注册的信息:
Application | IP:Port |
---|---|
APOLLO-ADMINSERVICE | 192.169.0.1:8090 |
APOLLO-CONFIGSERVICE | 192.169.0.1:8080 |
如果其他服务通过服务名Apollo+端口8080来访问Config Service是可以访问的,但是不能通过192.169.0.1:8080访问
解决方案:
问题1:
实际上如果我们注册到Eureka中的注册的服务地址是服务名Apollo+端口这样的形式,那么问题就解决了
Application | IP:Port |
---|---|
APOLLO-ADMINSERVICE | Apollo:8090 |
APOLLO-CONFIGSERVICE | Apollo:8080 |
Eureka注册时有一个参数可以定制完整的服务URL,这个服务URL就是服务间通过服务名在Eureka中获取的服务地址,配置参数为eureka.instance.homePageUrl=http://Apollo:8080
,这样从Eureka Server中拿到的服务地址就是http://Apollo:8080
问题2:
那本地开发时是要指定Apollo的地址来拿到配置信息,指定的是Meta Server的地址apollo.meta=http://IP:Port
,Meta Server再找到Eureka Server,获取服务地址,由于上面的改造,我们拿到的是http://Apollo:8080这种形式的,但是本地开发并不能访问
这时本地配置改为apollo.configService=http://域名:8080
直接指定Config Service的地址来跳过Meta Server的服务发现
Apollo实现原理:
- Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
- Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal
- Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳
- 在Eureka之上架了一层Meta Server用于封装Eureka的服务发现接口
- Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试
- Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试
1)、客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送
2)、客户端还会定时从Apollo配置中心服务端拉取应用的最新配置(推拉结合)
- 这是一个fallback机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
- 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property:
apollo.refreshInterval
来覆盖,单位为分钟
3)、客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
4)、客户端会把从服务端获取到的配置在本地文件系统缓存一份
在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
Mac/Linux:/opt/data/{appId}/config-cache
Windows:C:\opt\data{appId}\config-cache
文件名格式如下:
{appId}+{cluster}+{namespace}.properties
6、对接单点登录,以及OAuth2.0授权码模式的思考
业务需求:
取消我们系统的登录页,使用统一登录平台进行登录,登录后跳转到系统的首页,采用的是OAuth 2.0的授权码模式
流程图:
下面我们系统简称为A网站,统一登录平台简称为B网站
第一步,A网站提供一个链接,用户点击后就会跳转到B网站,授权用户数据给A网站使用。下面就是A网站跳转B网站的一个示意链接
https://b.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read
response_type
参数表示要求返回授权码(code
),client_id
参数让B知道是谁在请求,redirect_uri
参数是B接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围(这里是只读)
第二步,用户跳转后,B网站会要求用户登录,然后询问是否同意给予A网站授权。用户表示同意,这时B网站就会跳回redirect_uri
参数指定的网址。跳转时,会传回一个授权码,就像下面这样
https://a.com/callback?code=AUTHORIZATION_CODE
code
参数就是授权码
第三步,A网站拿到授权码以后,就可以在后端,向B网站请求令牌
https://b.com/oauth/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=CALLBACK_URL
上面 URL 中,client_id
参数和client_secret
参数用来让B确认A的身份(client_secret
参数是保密的,因此只能在后端发请求),grant_type
参数的值是AUTHORIZATION_CODE
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址
第四步,B网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri
指定的网址,发送一段JSON数据
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
OAuth2.0的四种方式:http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html