RPC项目笔记
实习经历
分布式锁
分布式锁框架
ETL架构
异步获取埋点数据
Redis和Mysql数据一致性
Redis高可用性
API防护8/19
API攻击有哪些种
API攻击可以分为以下几种常见的类型:
- 认证和授权攻击:攻击者尝试使用未经授权的凭据或者利用弱密码等方式来绕过认证机制,获取对API的访问权限。
- 参数篡改攻击:攻击者修改API请求中的参数,尝试绕过输入验证和过滤机制,以获取未经授权的数据或者执行未授权的操作。
- SQL注入攻击:攻击者通过在API请求中插入恶意的SQL语句,来执行未经授权的数据库操作或者获取敏感数据。
- XSS攻击:攻击者通过在API响应中插入恶意的脚本代码,来窃取用户的敏感信息或者进行其他恶意操作。
- CSRF攻击:攻击者通过诱使用户在浏览器中执行恶意操作或者点击恶意链接,来利用用户的身份执行未经授权的API请求。
- DoS和DDoS攻击:攻击者通过发送大量的请求或者恶意请求,使API服务器过载或者崩溃,导致服务不可用。
- API泄露和信息收集攻击:攻击者通过分析API的响应或者利用错误处理机制,来获取敏感信息或者了解API的内部结构和功能。
这只是一些常见的API攻击类型,实际上还有很多其他的攻击方式。为了保护API的安全,需要采取综合的安全措施,并进行定期的安全评估和漏洞修复。
API防护有哪些种
API防御是确保API安全性和保护API免受恶意攻击的关键措施。以下是一些常见的API防御措施及其选择的原因:
- 认证和授权:通过实施认证和授权机制,只允许授权用户或应用程序访问API。这可以防止未经授权的访问和滥用。选择这个措施是因为它可以确保只有经过身份验证和授权的用户才能使用API,并提供访问控制和权限管理的能力。
- API密钥:为每个用户或应用程序分配唯一的API密钥,用于标识和验证其身份。API密钥可以用于追踪API使用情况,并提供对API的访问控制。选择这个措施是因为它可以帮助识别和跟踪使用API的用户,并提供更细粒度的访问控制。
- 访问限制和频率限制:限制API的访问频率和请求限制,以防止恶意用户或应用程序对API进行滥用和攻击。选择这个措施是因为它可以减少服务器的负载,防止DDoS攻击和暴力破解,并保护API的可用性。
- 输入验证和过滤:对API的输入参数进行验证和过滤,以防止恶意用户通过注入攻击、跨站脚本(XSS)等方式对API进行攻击。选择这个措施是因为它可以防止恶意输入破坏API的功能或盗取敏感信息。
- 数据加密:对通过API传输的敏感数据进行加密,以防止数据在传输过程中被窃取或篡改。选择这个措施是因为它可以保护数据的机密性和完整性。
- 日志和监控:记录API的访问日志并进行实时监控,以及时发现异常行为和安全事件。选择这个措施是因为它可以帮助及时发现和应对潜在的攻击,并提供安全审计和合规性方面的支持。
- 更新和漏洞管理:定期更新和修补API,以纠正已知的漏洞和安全问题。选择这个措施是因为它可以保持API的安全性,并及时响应新的安全威胁和漏洞。
选择这些API防御措施的原因是因为它们涵盖了API安全的关键方面,并提供了多层次的保护。通过综合使用这些措施,可以增强API的安全性,减少潜在的风险和攻击。然而,具体选择哪些措施还需要根据实际情况评估,考虑到应用程序的特点、敏感性和风险承受能力等因素。
重放攻击是什么
重放攻击是一种常见的网络攻击方式,攻击者通过重复或延迟发送已经捕获到的有效请求来欺骗服务器。以下是几种常见的重放攻击类型:
- 简单重放攻击:攻击者直接重复发送已经捕获到的有效请求,以期望服务器再次执行相同的操作。
- 延迟重放攻击:攻击者在一定时间延迟后重放已经捕获到的有效请求,以期望服务器在延迟后再次执行相同的操作。
- 反射式重放攻击:攻击者从一次请求的响应中提取关键信息,并将其作为新的请求参数进行重放。
- 重放攻击预演:攻击者通过重放已经捕获到的有效请求来模拟或预演可能的攻击路径,以寻找潜在的安全漏洞。
- 逻辑重放攻击:攻击者重放已经捕获到的有效请求,但在重放过程中对请求进行了一些修改,以达到不同的目的或绕过某些安全机制。
为了防止重放攻击,可以采取以下措施:
- 使用唯一标识符或令牌:为每个请求生成唯一的标识符或令牌,并在服务器端进行验证,确保请求不会被重放。
- 使用时间戳和序列号:在请求中加入时间戳和序列号等信息,服务器端进行验证,拒绝重放过期或重复的请求。
- 加密和数字签名:对请求进行加密或者数字签名,确保请求的完整性和真实性,并防止被篡改或重放。
- 使用单次性令牌:使用一次性的令牌或票据来验证请求的合法性,确保每个请求只能被执行一次。
- 强化认证和授权机制:使用强大的认证和授权机制,确保只有经过授权的用户才能发送有效请求。
综合采用这些措施可以有效地防止重放攻击,并提升API的安全性。
选择的加密算法的优缺点
通过时间戳和随机数加密是一种常见的API防御方式,它的优点包括:
- 随机性高:通过使用随机数,可以增加加密的难度,减少攻击者的猜测和破解概率。
- 防止重放攻击:使用时间戳可以防止攻击者重复使用之前的请求,增加了安全性。
- 简单易实现:时间戳和随机数加密是比较简单的加密方式,实现起来相对容易。
然而,该方式也存在一些缺点:
- 加密强度有限:时间戳和随机数加密虽然可以增加攻击者的破解难度,但其加密强度相对较低,对于高级攻击手段可能不够安全。
- 安全性依赖于算法和密钥管理:加密算法和密钥的选择和管理对于安全性至关重要,如果算法或密钥被攻击者获取,加密的安全性将大打折扣。
- 需要额外的校验和验证:使用时间戳和随机数加密后,需要在服务器端对请求进行验证和校验,增加了开发和维护的复杂性。
因此,虽然时间戳和随机数加密是一种有效的API防御方式,但在实际应用中还需要结合其他的安全措施,如认证和授权、输入验证和过滤、数据加密、日志和监控等,以增强API的安全性。
RPC项目
项目中哪里使用了设计模式?
rpc 传输数据安全性
时间戳校验
在发送请求中绑定时间戳,然后服务端收到后校验,如果是4秒内并且检查时间戳是否是唯一的。
空闲检测(应用层keep alive)/心跳检测
实现:
server端超过10s没收到client信息,连接断开
client超过5s没有写事件发生时,则发送 keep alive,防止连接被断开
如何加强安全性:
- 防止连接劫持:通过定期检测连接的空闲状态,可以及时发现异常情况,如连接被劫持或中间人攻击等。如果连接长时间处于空闲状态,可能意味着连接已被攻击者劫持或被中间人截获并篡改,空闲检测可以及时关闭这类异常连接,减少安全风险。
- 防止资源浪费:RPC框架中的连接是有限的资源,如果连接一直处于空闲状态而不被释放,会造成资源的浪费。
- 预防重放攻击:在RPC通信中,重放攻击是一种安全威胁,攻击者可以重复发送已捕获的请求,导致重复执行操作。通过空闲检测,可以设置连接的最大空闲时间,一旦连接超过该时间仍然保持空闲状态,就可以主动断开连接,防止重放攻击的发生。
当一定时间内没有接收到心跳消息时,不能确定一定存在重放攻击,但可以将其作为一种可能性进行警觉。为了更准确地判断是否存在重放攻击,可以结合其他安全机制,如加密传输、身份认证和访问控制等,进行综合分析和判定。
黑白名单
IpSubnetFilterRule ipSubnetFilterRule = new IpSubnetFilterRule("127.0.0.1", 8, IpFilterRuleType.REJECT);
IpSubnetFilter ipSubnetFilter = new IpSubnetFilter(ipSubnetFilterRule);
自定义授权
@Slf4j
@Sharable
public class AuthHandler extends SimpleChannelInboundHandler<RequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RequestMessage msg) throws Exception {
Operation messageBody = msg.getMessageBody();
try {
if (messageBody instanceof AuthOperation) {
AuthOperation authOperation = AuthOperation.class.cast(messageBody);
AuthOperationResult result = authOperation.execute();
if (result.isPassAuth()) {
log.info("successfully pass auth");
} else {
log.error("fail to pass auth");
ctx.close();
}
}
} finally {
ctx.pipeline().remove(this);
}
}
}
SSL生成证书
SelfSignedCertificate certificate = new SelfSignedCertificate();
SslContext sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build();
添加sslHandler到pipline
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
pipeline.addLast("sslHandler", sslHandler);
RequestId如何递增?
如何保证发送后返回的future已经有数据?
自定义注解?@RpcReference @RpcService
线程池的区别?
线程池中的线程如何复用?
-
哪里使用了泛型/多态? ExtensionLoader中的Holder用于单例对象的创建
-
Netty设计模式:模板方法(ByteToMessageDecoder) 责任链模式(ChannelInBoundHandler)
-
Netty零拷贝如何体现:
- 直接内存
- FileChannel.transferTo
- CompositeByteBuf
-
Netty是基于NIO实现的,那么什么是NIO?Netty中的NIO相比原始NIO的优势在哪儿?戳:如何从BIO演进到NIO,再到Netty
-
Netty的线程模型是基于Reactor线程模型的,那么什么是Reactor线程模型呢?戳:Netty源码分析系列之Reactor线程模型
-
Netty是异步非阻塞的,异步和非阻塞体现在什么地方?
首先Netty非阻塞就是因为NIO中的Selector,可以轮询监听多条通道(一个客户端对应一条通道),而不像传统的BIO,一个线程只能负责一个客户端,只要客户端没发请求过来,线程就一直阻塞着傻傻等待,浪费资源。第二个异步的概念是当一个异步过程调用发出后会立即返回,调用者不能立刻得到结果。实际处理完成后,通过状态、通知或者回调的形式来告诉调用者,异步的优势是在高并发情形下会更稳定,具有更高吞吐量。Netty中的IO操作都是异步的,异步的实现是依靠ChannelFuture,它可以添加ChannelFutureListener监听器,当IO操作真正完成的时候,客户端会得到操作成功或失败的通知。
-
Netty对于TCP粘包、半包问题的解决,戳:Netty源码分析系列之TCP粘包、半包问题以及Netty是如何解决的
-
Kyro线程不安全 用ThreadLocal初始化
-
客户端连接五次尝试 用CountDownLatch ,connect用异步的ChannelFuture并添加Listener来通过另一个线程异步返回结果
Kyro序列化的优缺点
Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
序列化:将各种数据类型(基本类型、包装类型、对象、数组、集合)等序列化为byte数组的过程。
反序列化:将byte数组转换为各种数据类型(基本类型、包装类型、对象、数组、集合)。
为什么kryo比其它的序列化方案要快?
序列化后的二进制序列大小
- 由于Kryo没有将类field的描述信息序列化,所以Kryo需要以自己加载该类的filed。这意味着如果该类没有在kryo中注册,或者该类是第一次被kryo序列化时,kryo需要时间去加载该类
- kryo会为每一个类分配一个id。类注册机制,主要是在写入类全路径名时,如果该类注册了,会分配一个int类型的值代替字符串的全面,能减少序列化后二进制流的大小。
- 使用变长int、变长long存储int、long类型,大大节省空间
- 字符串类型使用UTF-8存储,但会使用ascii码进一步优化空间。
序列化、反序列化的速率
- 实现了自己的IntMap接口
- 利用变量memoizedRegistration和memoizedType记录上一次的调用writeObject函数的Class,则如果两次写入同一类型时,可以直接拿到,不再查找HashMap。
所以该类如果已经被kryo加载过,那么kryo保存了其类的信息,就可以很快的将byte数组填入到类的field中。
kryo序列化支持的数据类型较多。
除了常见的 JDK 类型、以及这些类型组合而来的普通 POJO,Kryo 还支持以下的类型:
- 枚举
- 集合、数组
- 子类/多态
- 循环引用
- 内部类
- 泛型
其中部分特性的支持,需要使用者手动设定 Kryo 的某些配置(KryoUtil 已经进行了这些配置)。
kryo的缺点
-
Kryo 是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有一定的优势,但正因如此,其使用也只能限制在基于 JVM 的语言上。
-
Kryo 不是线程安全的。每个线程都应该有自己的 Kryo 对象、输入和输出实例。因此在多线程环境中,可以考虑使用 ThreadLocal 或者对象池来保证线程安全性。
-
Kryo 不支持增加或删除 Bean 中的字段
增加或删除 Bean 中的字段;举例来说,某一个 Bean 使用 Kryo 序列化后,结果被放到 Redis 里做了缓存,如果某次上线增加/删除了这个 Bean 中的一个字段,则缓存中的数据进行反序列化时会报错;作为缓存功能的开发者,此时应该 catch 住异常,清除这条缓存,然后返回 “缓存未命中” 信息给上层调用者。
kyro源码
-
变长Int、Long序列化节省空间
第一位也就是最高位记录是否前面还有其他字节buffer[0],如果为1继续读buffer[1],并将当前数据左移七位与buffer[0]或运算,先读到的是int中的低位。 -
String序列化
String序列化的整体结构为 length + 内容,注意,这里的length不是内容字节的长度,而是String字符的长度。
1、如果是null,则用1个字节表示,其二进制为 1000 0000。
2、如果是""空字符串,则用1个字节表示,其二进制为1000 0001。
3、如果字符长度大于1·且小于64,并且字符全是ascii字符(小等于127),则每个字符用一个字节表示,最后一个字节的高位置1,表示String字符的结束。【优化点,如果是ascii字符,编码时不需要使用length+内容的方式,而是直接写入内容】
4、如果不满足上述条件,则需要使用length + 内容的方式。
1)用一个变长int写入字符的长度,每一字节,高两位分别为 编码标记(1:utf8)、是否结束标记(1:否;0:结束)
2)将内容用utf-8编码写入字节序列中,utf8,用变长字节(1-3)个字节表示一个字符(英文、中文)。每一个字节,使用6为,高两位为标志位。【16位】
3字节的存储为 【4位】 + 【6位】 + 【6位】,根据第一个字节高4位判断得出 需要几个字节来存储一个字符。 -
class序列化
序列化类的全路径名,反序列化时根据Class.forName生成对应的实例。
kryo序列化Class实例的编码规则:
1、如果为null,用变长int,实际使用1个字节,存储值为0。
2、如果该类通过类注册机制注册到kryo时,则序列化 (nameId + 2),用变长int存储。
3、如果该类未通过类注册机制注册到kryo,在一次序列化过程中时,类型第一次出现时,会分配一个nameId,将nameId+type全路径序列化,后续再出现该类型,则只序列化nameId即可。
其他的序列化框架
无需跨语言嘿嘿
Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用.
像 Protobuf、 ProtoStuff、hessian 这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。
SPI机制几种有什么区别?
SPI涉及到双亲委派机制,SPI接口的类加载器一般为BootStrapClassLoader,而实现类一般为AppClassLoader,SPI接口调用的时候需要加载实现类再去调用,而两个类加载器不通。这时需要使用Thread.currentThread().getContextClassLoader(),可以自己设置类加载器是哪个,浅破坏了双亲委派机制,不过更方便了。
-
Java的Spi:文件名是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。通过ServiceLoader.load(Logger.class)获取全部实现类
-
dubbo的Spi:文件名是接口的全类名,然后里面的内容是键值对:zk=全类名/nacos=全类名 这样可以通过名字进行判断和筛选。通过 ExtensionLoader.getExtensionLoader(ServiceDiscovery.class).getExtension("zk");获取指定的实现类。
Hoder泛型方便实现单例模式 -
SpringBoot的Spi:
在Spring中提供了SPI机制,我们只需要在 META-INF/spring.factories 中配置接口实现类名,即可通过服务发现机制,在运行时加载接口的实现类:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
在 spring-boot-autoconfigure 模块下,SpringBoot默认就配置了很多接口的服务实现:
其实我们在使用三方 spring-boot-starter就是使用这种机制,我们的Application显然不会和三方jar处于同包或者子包。三方Bean Configuration需要加载就需要使用spring 的spi机制。
包扫描器和自定义注解
@RpcScanner
- 包路径赋值:在这个注解中@RpcScan(basePackage = {"github.javaguide"})来配置到底扫描哪个包路径。
- 在这个注解的实现中通过@Import(CustomScannerRegistrar.class)将CustomScannerRegistrar类注入到Spring容器中。
- 这个类实现了registerBeanDefinitions方法,在被注入到Spring中的时候自动调用,来加载扫描包过程。
- 获取到包路径后,使用自定义的CustomScanner扫描对应类,先通过传入beanDefinitionRegistry和注解类初始化CustomScanner,让这个扫描器只扫描我们关注的注解类。
- 然后扫描指定的路径,其会自动加载到Spring中,并记录扫描的类的个数。
@RpcService
- @RpcService类的加载是通过@RpcScanner加载到Spring中的。
- 实现BeanPostProcessor接口,实现里面的postProcessBeforeInitialization方法,在Bean被初始化之前处理服务,同时暴露服务
- 判断如果这个Bean标记着这个注解,获取注解中的group和version值组成RpcServiceConfig。
- 通过拼接"my-rpc/"+"接口名+group+version"+"ip/port"存入到zookeeper中完成服务的注解发布。
@RpcReference
- @RpcReference(version = "version1", group = "test1") private HelloService helloService; 该注解标记到成员变量上,然后对整个类使用@Component来注入到Spring中
- 实现BeanPostProcessor接口,实现里面的postProcessAfterInitialization方法,在Bean被初始化之后处理服务引用,生成代理对象
- 遍历Bean的成员变量,如果有@RpcReference则通过注解值获取RpcServiceConfig,然后传入RpcClient和RpcServiceConfig初始化RpcClientProxy。
- 通过代理类获取接口的代理对象,使用反射将Controller里的对象替换为我们的代理对象,再调用方法时,会调用到我们的invoke函数里,来实现发送request收到response的目的。
动态代理技术是怎么实现的?
代理模式是一种比较好理解的设计模式。代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
姨妈在这里就可以看作是代理你的代理对象,代理的行为(方法)是接收和回复新郎的提问。
代理模式有静态代理和动态代理两种实现方式,我们 先来看一下静态代理模式的实现
静态代理
静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
实现步骤
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情
实现案例
-
定义发送短信的接口
public interface SmsService { String send(String message); }
-
实现发送短信的接口
public class SmsServiceImpl implements SmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
-
创建代理类并同样实现发送短信的接口
public class SmsProxy implements SmsService { private final SmsService smsService; public SmsProxy(SmsService smsService) { this.smsService = smsService; } @Override public String send(String message) { //调用方法之前,我们可以添加自己的操作 System.out.println("before method send()"); smsService.send(message); //调用方法之后,我们同样可以添加自己的操作 System.out.println("after method send()"); return null; } }
-
实际使用
public class Main { public static void main(String[] args) { SmsService smsService = new SmsServiceImpl(); SmsProxy smsProxy = new SmsProxy(smsService); smsProxy.send("java"); } }
运行上述代码之后,控制台打印出:
before method send()
send message:java
after method send()
可以输出结果看出,我们已经增加了 SmsServiceImpl
的send()
方法。
从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
动态代理
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
JDK 动态代理机制
介绍
在 Java 动态代理机制中 InvocationHandler
接口和 Proxy
类是核心。
Proxy
类中使用频率最高的方法是:newProxyInstance()
,这个方法主要用来生成一个代理对象
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
......
}
这个方法一共有 3 个参数:
- loader :类加载器,用于加载代理对象。
- interfaces : 被代理类实现的一些接口;
- h : 实现了
InvocationHandler
接口的对象;
要实现动态代理的话,还必须需要实现InvocationHandler
来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler
接口类的 invoke
方法来调用
public interface InvocationHandler {
/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
invoke()
方法有下面三个参数:
- proxy :动态生成的代理类
- method : 与代理类对象调用的方法相对应
- args : 当前 method 方法的参数
也就是说:你通过Proxy
类的 newProxyInstance()
创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler
接口的类的 invoke()
方法。 你可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
JDK 动态代理类使用步骤
- 定义一个接口及其实现类;
- 自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; - 通过
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象;
代码示例
-
定义发送短信的接口
public interface SmsService { String send(String message); }
-
实现发送短信的接口
public class SmsServiceImpl implements SmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
-
定义一个 JDK 动态代理类
import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * @author shuang.kou * @createTime 2020年05月11日 11:23:00 */ public class DebugInvocationHandler implements InvocationHandler { /** * 代理类中的真实对象 */ private final Object target; public DebugInvocationHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { //调用方法之前,我们可以添加自己的操作 System.out.println("before method " + method.getName()); Object result = method.invoke(target, args); //调用方法之后,我们同样可以添加自己的操作 System.out.println("after method " + method.getName()); return result; } }
invoke()
方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是invoke()
方法,然后invoke()
方法代替我们去调用了被代理对象的原生方法。 -
获取代理对象的工厂类
public class JdkProxyFactory { public static Object getProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), // 目标类的类加载 target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个 new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler ); } }
getProxy()
:主要通过Proxy.newProxyInstance()
方法获取某个类的代理对象 -
实际使用
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl()); smsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
CGLIB 动态代理机制
介绍
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
CGLIBopen in new window(Code Generation Library)是一个基于ASMopen in new window的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIBopen in new window, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
在 CGLIB 动态代理机制中 MethodInterceptor
接口和 Enhancer
类是核心。
你需要自定义 MethodInterceptor
并重写 intercept
方法,intercept
用于拦截增强被代理类的方法。
public interface MethodInterceptor extends Callback{
// 拦截被代理类中的方法
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable;
}
- obj : 被代理的对象(需要增强的对象)
- method : 被拦截的方法(需要增强的方法)
- args : 方法入参
- proxy : 用于调用原始方法
你可以通过 Enhancer
类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor
中的 intercept
方法.
CGLIB 动态代理类使用步骤
- 定义一个类;
- 自定义
MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke
方法类似; - 通过
Enhancer
类的create()
创建代理类;
代码示例
不同于 JDK 动态代理不需要额外的依赖。CGLIBopen in new window(Code Generation Library) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖.
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
-
实现一个使用阿里云发送短信的类
public class AliSmsService { public String send(String message) { System.out.println("send message:" + message); return message; } }
-
自定义
MethodInterceptor
(方法拦截器)import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * 自定义MethodInterceptor */ public class DebugMethodInterceptor implements MethodInterceptor { /** * @param o 被代理的对象(需要增强的对象) * @param method 被拦截的方法(需要增强的方法) * @param args 方法入参 * @param methodProxy 用于调用原始方法 */ @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { //调用方法之前,我们可以添加自己的操作 System.out.println("before method " + method.getName()); Object object = methodProxy.invokeSuper(o, args); //调用方法之后,我们同样可以添加自己的操作 System.out.println("after method " + method.getName()); return object; } }
-
获取代理类
import net.sf.cglib.proxy.Enhancer; public class CglibProxyFactory { public static Object getProxy(Class<?> clazz) { // 创建动态代理增强类 Enhancer enhancer = new Enhancer(); // 设置类加载器 enhancer.setClassLoader(clazz.getClassLoader()); // 设置被代理类 enhancer.setSuperclass(clazz); // 设置方法拦截器 enhancer.setCallback(new DebugMethodInterceptor()); // 创建代理类 return enhancer.create(); } }
-
实际使用
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class); aliSmsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
JDK 动态代理和 CGLIB 动态代理对比
- JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
静态代理和动态代理的对比
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
CompletableFuture异步
- CompletableFuture原理解析
- 异步编程利器:CompletableFuture详解
- CompletableFuture 和 Future 的区别
Zookeeper注册中心优缺点
半包粘包问题和编解码器
杂物
addShutdownHook
ava程序运行时,有时会因为一些原因会导致程序死掉。也有些时候需要将程序对应的进程kill掉。
这些情况发生时,可能会导致有些需要保存的信息没能够保存下来,还有可能我们需要进程交代一些后事再被销毁。那要怎么办呢?
这就该ShutdownHook登场了。他是怎么完成我们上面描述的需要完成的事情呢?看看下面的例子吧。
-
正常运行结束
上面代码中“Runtime.getRuntime().addShutdownHook”方法就是添加了一个ShutdownHook。这个方法的参数是一个Thread对象,在程序退出时就会执行这个Thread对象了。
-
因异常结束运行
比之前多添加了一行会抛出异常的代码。因为抛出异常,“结束运行”这个内容是肯定不会输出了。那还会输出“交代后事”吗?我们来看看运行的结果是什么吧。
看来有异常时,进程也可以完成“交代后事”。
-
程序通过System.exit()自行结束
说明通过System.exit()结束程序时,也可以“交代后事”。 -
使用kill命令结束程序
其实这个“-9”就是个必杀令,如果是执行“kill -9 pid”这样的命令,程序就不能“交代后事”了。我觉得就是为了防止程序的ShutdownHook磨磨蹭蹭的交代不完后事,甚至是通过一直“交代后事”而达到不被“kill”的目的。
课程进阶篇
RPC架构
整个架构就变成了一个微内核架构,我们将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离并提供接口的默认实现。这样的架构相比之前的架构,有很多优势。首先它的可扩展性很好,实现了开闭原则,用户可以非常方便地通过插件扩展实现自己的功能,而且不需要修改核心功能的本身;其次就是保持了核心包的精简,依赖外部包少,这样可以有效减少开发人员引入 RPC 导致的包版本冲突问题。
服务发现
- 服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
- 服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用
健康检测机制
路由策略
负载均衡
重试机制
优雅关闭
优雅启动
熔断限流
业务分组
课程高级篇
通过 RPC 的异步去压榨单机的吞吐量
RPC 框架的异步策略主要是调用端异步与服务端异步。
- 调用端的异步就是通过 Future 方式实现异步,调用端发起一次异步请求并且从请求上下文中拿到一个 Future,之后通过Future 的 get 方法获取结果,如果业务逻辑中同时调用多个其它的服务,则可以通过Future 的方式减少业务逻辑的耗时,提升吞吐量。
- 服务端异步则需要一种回调方式,让业务逻辑可以异步处理,之后调用 RPC 框架提供的回调接口,将最终结果异步通知给调用端。
另外,我们可以通过对 CompletableFuture 的支持,实现 RPC 调用在调用端与服务端之间的完全异步,同时提升两端的单机吞吐量。
安全体系
-
靠猜测调用的概率很小,但是当调用方在其它新业务场景里面要用之前项目中使用过的接口,就很有可能真的不跟服务提供方打招呼就直接调用了。这种行为对于服务提供方来说就很危险了,因为接入了新的调用方就意味着承担的调用量会变大,有时候很有可能新增加的调用量会成为压倒服务提供方的“最后一根稻草”,从而导致服务提供方无法正常提供服务,关键是服务提供方还不知道是被谁给压倒的。
不可逆加密算法吗?HMAC 就是其中一种具体实现。服务提供方应用里面放一个用于 HMAC 签名的私钥,在授权平台上用这个私钥为申请调用的调用方应用进行签名,这个签名生成的串就变成了调用方唯一的身份。服务提供方在收到调用方的授权请求之后,我们只要需要验证下这个签名跟调用方应用信息是否对应得上就行了,这样集中式授权的瓶颈也就不存在了。
-
服务提供方会把接口 Jar 发布到私服上,以方便调用方能引入到项目中快速完成 RPC 调用,那有没有可能有人拿到你这个 Jar后,发布出来一个服务提供方呢?这样的后果就是导致调用方通过服务发现拿到的服务提供方 IP 地址集合里面会有那个伪造的提供方。
那怎么实现呢?在[第 08 讲] 我们提到过,服务提供方启动的时候,需要把接口实例在注册中心进行注册登记。我们就可以利用这个流程,注册中心可以在收到服务提供方注册请求的时候,验证下请求过来的应用是否跟接口绑定的应用一样,只有相同才允许注册,否则就返回错误信息给启动的应用,从而避免假冒的服务提供者对外提供错误服务。
分布式环境下如何快速定位问题?
- 封装合适的异常
- 分布式链路追踪
详解时钟轮在RPC中的应用
- 调用端请求超时处理,这里我们就可以应用到时钟轮,我们每发一次请求,都创建一个处理请求超时的定时任务放到时钟轮里,在高并发、高访问量的情况下,时钟轮每次只轮询一个时间槽位中的任务,这样会节省大量的 CPU。
- 调用端与服务端启动超时也可以应用到时钟轮,以调用端为例,假设我们想要让应用可以快速地部署,例如 1 分钟内启动,如果超过 1 分钟则启动失败。我们可以在调用端启动时创建一个处理启动超时的定时任务,放到时钟轮里。
- 定时心跳。RPC 框架调用端定时向服务端发送心跳,来维护连接状态,我们可以将心跳的逻辑封装为一个心跳任务,放到时钟轮里。在定时任务的执行逻辑的最后,我们可以重设这个任务的执行时间,把它重新丢回到时钟轮里。
在 RPC 框架中,只要涉及到定时任务,我们都可以应用时钟轮,比较典型的就是调用端的超时处理、调用端与服务端的启动超时以及定时心跳等等。
流量回放
动态分组:超高效实现秒级扩缩容
在[第 16 讲],我们讲了分组后带来的收益,它可以帮助服务提供方实现调用方的隔离。但是因为调用方流量并不是一成不变的,而且还可能会因为突发事件导致某个分组的流量溢出,而在整个大集群还有富余能力的时候,又因为分组隔离不能为出问题的集群提供帮助。为了解决这种突发流量的问题,我们提供了一种更高效的方案,可以实现分组的快速扩缩容。事实上我们还可以利用动态分组解决分组后给每个分组预留机器冗余的问题,我们没有必要把所有冗余的机器都分配到分组里面,我们可以把这些预留的机器做成一个共享的池子,从而减少整体预留的实例数量。
如何在没有接口的情况下进行RPC调用?
如何在没有接口的情况下进行 RPC 调用,泛化调用的功能可以实现这一目的。
这个功能的实现原理,就是 RPC 框架提供统一的泛化调用接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,通过调用GenericService 代理的 $invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行RPC 调用的功能。而通过泛化调用的方式发起调用,由于调用端没有服务端提供方提供的接口 API,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。
专业能力
Java集合
Mysql
事务、索引、日志以及查询优化
SQL优化经验
并发编程
AQS、Unsafe、ReentrantLock、线程池、CompletableFuture
Spring
IOC、AOP
JVM
垃圾回收、类加载机制、对象创建过程
Redis
RDB、AOF、主从复制、Sentinel
计算机网络
TCP、HTTP、HTTPS
操作系统
进程线程、死锁、调度算法