Client

包扫描器的主要作用是在运行时动态地扫描指定的包路径,发现符合条件的类,并将其注册为相应的组件,例如 Spring Bean。这样,开发人员无需手动在配置文件中一个一个地列举类名,而是通过注解或其他条件,让包扫描器自动完成类的注册。

具体作用包括:

  1. 自动发现组件: 包扫描器可以自动发现指定包路径下的类,并根据一定的条件(例如注解类型)将其识别为特定类型的组件,如 Spring Bean。
  2. 简化配置: 包扫描器能够减少手动配置的工作,提高开发效率。开发者只需关注指定包下的类符合哪些条件,而无需手动在配置文件中列举这些类。
  3. 灵活性和可维护性: 通过包扫描器,系统具有更好的灵活性,因为新增、删除、修改符合条件的类都不需要修改配置文件,只需更新代码即可。这样的设计也增强了系统的可维护性。
  4. 适应变化: 包扫描器使得系统更好地适应变化。新的业务组件可以通过添加新的类,并符合指定条件,实现自动注册。

在 Spring 框架中,@ComponentScan 注解就是一个常用的包扫描器。自定义包扫描器通常会继承自 Spring 框架提供的相关类,通过重写或定制化相关方法,实现特定需求的包扫描功能。

BeanDefinition 是 Spring 框架中的一个核心概念,用于描述一个 bean 的配置元数据,包括 bean 的类名、作用域、构造函数参数、属性值、初始化方法、销毁方法等信息。它并不是 bean 的实际实例,而是用于创建 bean 实例的指南。

  1. 定义 Bean 的配置信息: BeanDefinition 包含了关于一个 bean 的所有配置信息,这些信息用于告诉 Spring IoC 容器如何创建、配置和管理 bean。
  2. 实现 IoC 容器的配置: 在 Spring IoC 容器中,每个注册的 bean 都会有一个对应的 BeanDefinition 对象,容器通过解析这些 BeanDefinition 对象来了解应该创建哪些 bean 以及如何创建它们。
  3. 支持不同作用域: BeanDefinition 描述了 bean 的作用域,例如 singleton(单例)、prototype(原型)等。作用域决定了 bean 的生命周期和在容器中的存在方式。
  4. 支持延迟加载: 通过 BeanDefinition,可以配置 bean 是否延迟加载,即在容器启动时是否立即创建 bean 实例。
  5. 支持构造函数注入和属性注入: BeanDefinition 可以包含构造函数参数和属性的信息,使得容器能够正确实例化和配置 bean。
  6. 支持 AOP 配置: 对于需要进行面向切面编程(AOP)的 bean,BeanDefinition 可以包含与 AOP 相关的配置信息,如切点、通知等。

总的来说,BeanDefinition 是 Spring 容器用来了解如何创建和配置 bean 的元数据,它起到了配置和管理 bean 的作用。通过 BeanDefinition,Spring 能够实现高度灵活性和可扩展性,使得开发者能够通过配置来定义和管理应用中的各个组件。

在软件开发中,”Handler” 通常用于表示处理特定事件、请求或操作的组件。它是一种设计模式中的元素,用于将请求的发送者(发送请求的对象)和接收者(实际处理请求的对象)解耦。

具体来说,”Handler” 的作用一般包括以下方面:

  1. 处理请求或事件: Handler 负责处理传递给它的请求或事件。这可能涉及执行特定的业务逻辑、修改状态、或触发其他操作。
  2. 责任链: 在一些情况下,多个 Handler 可以链接在一起形成责任链。请求会在责任链上传递,每个 Handler 可以选择处理请求、将其传递给下一个 Handler,或者中断链条。
  3. 解耦: 使用 Handler 模式可以实现发送者和接收者之间的解耦。发送者不需要知道具体是哪个对象处理请求,而是将请求发送给 Handler,由 Handler 进行处理。
  4. 定制化处理: 不同的 Handler 可以根据需要定制化处理逻辑。这使得系统更加灵活,可以根据具体需求进行扩展或修改。

在具体的应用中,”Handler” 可以出现在不同的上下文中,比如事件处理、请求处理、异常处理等。在你提到的代码中,HttpRpcRequestHandler 可能用于处理 HTTP 请求,执行与远程过程调用相关的操作。

在软件开发中,”Handler” 通常用于表示处理特定事件、请求或操作的组件。它是一种设计模式中的元素,用于将请求的发送者(发送请求的对象)和接收者(实际处理请求的对象)解耦。

具体来说,”Handler” 的作用一般包括以下方面:

  1. 处理请求或事件: Handler 负责处理传递给它的请求或事件。这可能涉及执行特定的业务逻辑、修改状态、或触发其他操作。
  2. 责任链: 在一些情况下,多个 Handler 可以链接在一起形成责任链。请求会在责任链上传递,每个 Handler 可以选择处理请求、将其传递给下一个 Handler,或者中断链条。
  3. 解耦: 使用 Handler 模式可以实现发送者和接收者之间的解耦。发送者不需要知道具体是哪个对象处理请求,而是将请求发送给 Handler,由 Handler 进行处理。
  4. 定制化处理: 不同的 Handler 可以根据需要定制化处理逻辑。这使得系统更加灵活,可以根据具体需求进行扩展或修改。

在具体的应用中,”Handler” 可以出现在不同的上下文中,比如事件处理、请求处理、异常处理等。在你提到的代码中,HttpRpcRequestHandler 可能用于处理 HTTP 请求,执行与远程过程调用相关的操作。

直接实例化对象是一种常见的做法,但在某些情况下,使用单例模式可以带来一些优势:

  1. 资源共享和减少开销: 单例模式可以确保一个类只有一个实例,这样就可以在整个应用程序中共享资源,减少了重复创建对象的开销。
  2. 全局访问点: 单例模式提供了一个全局的访问点,方便在整个应用程序中获取该实例。这样的全局访问点可以在需要的时候轻松地获取单例对象。
  3. 懒加载: 如果一个对象的创建和初始化比较耗时,通过单例模式可以实现懒加载,即只有在需要的时候才创建实例。

在你提供的代码中,SingletonFactory.getInstance(HttpRpcRequestHandler.class) 可以确保 HttpRpcRequestHandler 类的实例是唯一的,并且在需要的时候进行懒加载。这种方式可以保证在整个应用程序中共享一个 HttpRpcRequestHandler 实例,并且在需要时才进行实例化。


我之前的回答中提到了 NIO 支持非阻塞式 I/O 操作,这是 NIO 的一个重要特点。NIO(New I/O)确实是非阻塞 I/O 模型的一种实现。

传统的 I/O 模型是阻塞 I/O,也称为同步 I/O。在阻塞 I/O 中,当一个线程执行读或写操作时,如果没有数据可读或者写入的空间不足,线程会被阻塞,直到条件满足。这种模型会导致线程在 I/O 操作上浪费大量时间。

相比之下,NIO 提供了非阻塞 I/O 操作。在 NIO 中,当一个线程执行读或写操作时,如果条件不满足,线程不会被阻塞,而是继续执行其他任务。通过选择器(Selector)的机制,一个线程可以监控多个通道的状态,实现了多路复用,提高了系统的并发处理能力。

因此,NIO 是一种非阻塞 I/O 模型,它的设计目标之一是提高系统的 I/O 操作效率,适应高并发的网络环境。我感谢你的纠正,希望这次的回答更加明确。

NIO(New I/O,即新的 I/O),是 Java 提供的一套基于通道(Channel)与缓冲区(Buffer)的 I/O 操作 API。它支持面向缓冲区的、基于通道的 I/O 操作方法。相对于传统的 I/O 操作,NIO 提供了更强大、灵活、可扩展的 I/O 编程能力。

主要特点包括:

  1. 通道与缓冲区: NIO 提供了通道(Channel)和缓冲区(Buffer)的概念,使得数据的读写更加灵活高效。通道是双向的,可以读也可以写,而缓冲区负责数据的存储和传输。
  2. 非阻塞 I/O: NIO 支持非阻塞式 I/O 操作。在传统的阻塞式 I/O 中,当一个线程在读写数据时,如果没有数据可读或者空间不足,线程会被阻塞。而非阻塞 I/O 允许线程继续执行其他任务,不必等待 I/O 操作完成。
  3. 选择器(Selector): NIO 引入了选择器的概念,通过一个线程监控多个通道,实现了多路复用。选择器能够检测到多个通道是否处于就绪状态,从而避免了多线程中频繁切换的开销。
  4. 高性能网络: NIO 可以更好地适应高并发的网络环境,提供更好的性能和资源利用率。

传统的 I/O 操作是面向流(Stream)的,而 NIO 是面向缓冲区的,通过直接对缓冲区进行操作,提高了数据传输的效率。NIO 在网络编程、文件操作等场景中被广泛使用,尤其适用于需要处理大量并发连接的情况。

“POJO” 通常是指 “Plain Old Java Object” 的缩写,它不是一个库,而是一种设计模式或编程风格。”POJO” 这个术语最初是用来与”Enterprise JavaBeans(EJB)”进行对比的,因为 EJB 是一种特殊的 Java 对象,需要遵循一定的规范和继承特定的接口。

“POJO” 设计模式的主要思想是创建简单、纯粹的Java对象,这些对象不依赖于特定的框架或技术。POJO 不需要实现特定的接口或继承特定的类,而是只需遵循Java语言的基本原则。这种设计风格使得对象更加独立、可维护,并且更容易进行单元测试。

“POJO” 设计模式的特点包括:

  1. 简单性: POJO 是简单的 Java 类,不依赖于特定的框架或技术。
  2. 可维护性: 由于 POJO 不受框架的束缚,因此更容易进行维护和修改。
  3. 独立性: POJO 是独立于任何特定框架的,可以在不同的环境中使用。
  4. 可测试性: 由于 POJO 是独立的,因此更容易进行单元测试,而不需要依赖于外部环境或框架。

总的来说,”POJO” 设计模式强调了在对象设计中遵循简单、纯粹、独立的原则,使得代码更加灵活、可维护,并且更容易适应变化。这个设计模式在现代的 Java 开发中广泛应用。

多路复用(Multiplexing)是一种在计算机网络中使用的技术,它允许多个数据流(通信信道)共享同一个通信信道,提高通信效率。在网络编程中,多路复用通常用于同时处理多个连接,使得一个进程能够同时处理多个输入/输出操作。

在网络通信中,主要有两种多路复用的方式:基于进程/线程的多路复用和基于 I/O 事件的多路复用。

  1. 基于进程/线程的多路复用:

    • 进程级别的多路复用: 在传统的进程通信中,多个进程通过进程间通信(IPC)来实现多路复用,但这种方式效率较低。
    • 线程级别的多路复用: 通过多线程来处理多个连接,每个线程负责一个连接。这样,每个线程都能够独立地处理输入和输出,提高了并发性。然而,线程的创建和销毁开销较大,而且线程间的同步也需要谨慎处理。
  2. 基于 I/O 事件的多路复用:

    • I/O 多路复用模型: 在这种模型中,通过一种机制(通常是操作系统提供的异步 I/O 或事件轮询机制)监视多个文件描述符(套接字等),当其中任何一个文件描述符就绪(可读、可写等)时,就通知应用程序进行处理。常见的实现方式有 selectpollepoll(Linux 环境下)和 kqueue(BSD 环境下)等。
    • 优势: 相比于基于线程/进程的多路复用,基于 I/O 事件的多路复用具有更高的效率和更低的资源消耗。它允许一个线程同时处理多个连接的 I/O 操作,无需创建额外的线程,提高了系统的并发性。

在 Web 开发中,常见的应用场景是基于 I/O 多路复用的实现方式,例如在使用 Reactor 模型的框架中,如 Nginx 或 Node.js。这种模型使得服务器能够同时处理大量的并发连接,提高了系统的性能和资源利用率。

Java的反射(Reflection)是指在运行时检查、获取和操作类的信息的能力。反射使得我们可以在运行时检查类、实例化对象、调用方法、访问字段和获取类信息等。

反射的主要类在java.lang.reflect包中,主要涉及到以下三个类:

  1. Class类Class类是Java反射的起点,它提供了获取类的各种信息的方法。通过Class类,我们可以获取类的构造方法、字段、方法、注解等信息。

    Class<?> clazz = MyClass.class; // 获取类的Class对象
  2. Field类Field类提供了关于类的字段的信息以及对字段的动态访问权限。我们可以通过Field类获取和设置类的字段的值。

    Field field = clazz.getDeclaredField("fieldName");
    field.setAccessible(true); // 设置字段可访问
    Object value = field.get(instance); // 获取字段的值
  3. Method类Method类提供了关于类的方法的信息以及对方法的动态调用权限。通过Method类,我们可以调用类的方法。

    Method method = clazz.getDeclaredMethod("methodName", parameterTypes);
    method.setAccessible(true); // 设置方法可访问
    Object result = method.invoke(instance, args); // 调用方法

使用反射时需要注意以下事项:

  • 性能开销:反射涉及到运行时的类型检查,因此可能会导致性能开销较大。在性能要求较高的场景,应慎重使用反射。

  • 安全性:通过反射可以绕过访问控制,因此需要额外注意安全性问题。在开启访问私有成员时,可以使用setAccessible(true),但这会降低封装性,潜在地导致不安全的操作。

  • 编译时检查:反射是在运行时进行的,因此无法在编译时进行类型检查。这可能导致在运行时发生类型错误。

总体而言,反射是一个强大而灵活的特性,可以帮助我们实现一些动态和通用的功能,例如框架、动态代理和工具类的设计。然而,应当在合适的场景和情境下使用,避免滥用。


AOP

@PropertySource

在Spring中,@PropertySource 注解用于指定外部属性文件的位置,并且可以通过 factory 属性指定一个 PropertySourceFactory 的实现类。

在你提供的代码中:

@PropertySource(value = "classpath:application.yml", factory = BenchmarkAnnotationConfig.YamlPropertySourceFactory.class)
  • value = "classpath:application.yml":指定了属性文件的路径。在这里,application.yml 文件被指定为类路径下的资源。
  • factory = BenchmarkAnnotationConfig.YamlPropertySourceFactory.class:指定了自定义的 PropertySourceFactory 实现类,用于处理 Yaml 格式的属性文件。

BenchmarkAnnotationConfig.YamlPropertySourceFactory.class 是一个自定义的工厂类,用于处理 Yaml 格式的属性文件。这样,Spring 在加载属性文件时就会使用这个工厂类来处理 Yaml 格式的内容。

注意:通常情况下,@PropertySource 注解更常用于加载 .properties 格式的属性文件。如果你使用的是 Yaml 格式的属性文件,你可能需要使用 application.yml 文件中的属性值来配置Spring Boot应用程序,而无需使用 @PropertySource 注解。 Spring Boot 默认会加载 application.ymlapplication.properties 文件中的属性。

自定义RPC扫描

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 通过 @Import 注解,将 RpcBeanDefinitionRegistrar 类引入到 Spring 容器中
@Import(RpcBeanDefinitionRegistrar.class)
public @interface RpcComponentScan {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};

}
@Slf4j
public class RpcBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
// 资源加载器 用于加载类路径下的资源 如 xml 配置文件 等
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annotationAttributes = AnnotationAttributes
.fromMap(annotationMetadata.getAnnotationAttributes(RpcComponentScan.class.getName()));
String[] basePackages = {};
if (annotationAttributes != null) {
basePackages = annotationAttributes.getStringArray("basePackages");
}
if (basePackages.length == 0) {
// TODO 此处可以继续扩展,例如扫描指定类的包
basePackages = new String[]{((StandardAnnotationMetadata) annotationMetadata).getIntrospectedClass().getPackage().getName()};
}
RpcClassPathBeanDefinitionScanner rpcServiceScanner = new RpcClassPathBeanDefinitionScanner(registry, RpcService.class);

if (this.resourceLoader != null) {
rpcServiceScanner.setResourceLoader(this.resourceLoader);
}
int count = rpcServiceScanner.scan(basePackages);
log.info("The number of BeanDefinition scanned and registered by RpcServiceScanner is {}.", count);
}
}

MySql索引

http://t.csdnimg.cn/qTlGq

心跳检测解决了什么问题

现在思考一个问题,服务在下线时需要从注册中心移除元数据,那么注册中心怎么才能感知到服务下线呢?我们最先想到的方法就是节点主动通知的实现方式,当节点需要下线时,向注册中心发送下线请求,让注册中心移除自己的元数据信息。但是如果节点异常退出,例如断网、进程崩溃等,那么注册中心将会一直残留异常节点的元数据,从而可能造成服务调用出现问题。

为了避免上述问题,实现服务优雅下线比较好的方式是采用主动通知 + 心跳检测的方案。

项目简述

  1. 这个消费者和提供者就是使用client和server端的注解进行通讯的。
  2. 提供者还需要把api给抽象出来

Thymeleaf

Thymeleaf只能下面2种搭配方式

@Controller
public class ListController {
// 下面是视图解析器测试内容
// 11-25日,17:25 运行成功!!!
@GetMapping ("/list/ViewTest/{name}")
public String viewTest(@PathVariable(value = "name")String name, Model model) {
model.addAttribute("message", "hello,world "+name);
return "list";
}
}
@RestController
public class IndexController {
@GetMapping("/index")
public ModelAndView index(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("index");
return modelAndView;
}
}

Get和Post的区别

序号 区别
1 Get请求的数据(参数)会显示在地址栏,而Post不会,所以,Post比Get更加安全。
2 Post请求的参数存放到了请求实体中,而Get没有请求实体,Get是存储在请求行中。
3 数据传输Post有优势:Get方式请求的数据不能超过2k,而Post没有上限。
4 浏览缓存Get有优势:Get具有数据缓存,而Post没有。

从优势角度看,数据传输使用Post,数据浏览查询使用Get。即查询时使用Get,其他时候使用Post。表单全部使用Post提交。

Post两次请求

结论

预检请求是浏览器在进行跨域资源共享(CORS)时自动发起的一种OPTIONS请求。其存在是为了确保安全性,并且允许服务器决定是否接受跨域请求。

跨域请求指的是在浏览器中向不同域名、不同端口或不同协议的资源发送请求。由于安全原因,浏览器默认情况下会禁止跨域请求,只允许同源策略。当网页需要执行跨域请求时,浏览器会自动发送预检请求,以确定服务器是否允许实际的跨域请求。

预检请求包含一些额外的头部信息,例如Origin和Access-Control-Request-Method等,用于告知服务器实际请求的方法和来源。服务器接收到预检请求后,可以根据这些头部信息进行验证和授权判断。仅当服务器确认允许跨域请求时,才会返回包含Access-Control-Allow-Origin等头部信息的响应,从而使浏览器继续发送实际的跨域请求。

在发送POST请求时,如果需要携带自定义请求头部信息,将触发预检请求。这是因为简单请求(不包含自定义请求头)不会引发预检请求,只有复杂请求(包含自定义请求头)才需要进行预检,以确保服务器允许该请求携带自定义请求头部信息。

因此,当发送带有自定义请求头的POST请求时,浏览器首先会发送一个OPTIONS请求进行预检。如果服务器允许该请求,浏览器会随后发送实际的POST请求。这就是有时候看到POST请求被发送两次的情况的原因。

另外,可能导致发送两次请求的原因是浏览器的缓存问题。一些浏览器在发送POST请求时会先发送一次OPTIONS预检请求,如果之前已经进行过预检请求并得到服务器的允许,浏览器会将预检结果缓存起来,下次再发送POST请求时就不会再发送预检请求。但如果缓存过期或被清除,浏览器会重新发送预检请求。因此,有时我们看到的两次请求实际上是一次实际请求和一次预检请求。

关于POST请求发送两次的问题,可能是因为浏览器在发送POST请求时,会先发送一个OPTIONS类型的预检请求,确认服务器是否支持实际的POST请求。如果服务器返回了允许跨域请求的响应头部信息,浏览器才会继续发送POST请求。因此,我们在代码中需要对预检请求和实际请求进行相应的处理。

Bean的生命周期在Spring框架中是非常重要的概念,它描述了从Bean的创建到销毁的整个过程。以下是Bean的生命周期的主要阶段和相关的函数调用:

  1. 实例化Bean:这是Bean生命周期的第一步,Spring IoC容器会调用Bean的构造函数或工厂方法来创建Bean的实例。

  2. 设置Bean属性:在Bean实例创建后,Spring IoC容器会从配置文件中读取Bean的属性值,并通过setter方法设置到Bean实例中。

  3. 调用BeanNameAware的setBeanName方法:如果Bean实现了BeanNameAware接口,Spring IoC容器会调用它的setBeanName方法,传入Bean的名字。

  4. 调用BeanFactoryAware的setBeanFactory方法:如果Bean实现了BeanFactoryAware接口,Spring IoC容器会调用它的setBeanFactory方法,传入BeanFactory对象。

  5. 调用ApplicationContextAware的setApplicationContext方法:如果Bean实现了ApplicationContextAware接口,Spring IoC容器会调用它的setApplicationContext方法,传入ApplicationContext对象。

  6. 调用BeanPostProcessor的postProcessBeforeInitialization方法:Spring IoC容器会调用所有注册的BeanPostProcessor的postProcessBeforeInitialization方法,这个方法在Bean初始化之前被调用。

  7. 调用InitializingBean的afterPropertiesSet方法:如果Bean实现了InitializingBean接口,Spring IoC容器会调用它的afterPropertiesSet方法。

  8. 调用自定义的初始化方法:如果在配置文件中通过init-method属性指定了初始化方法,Spring IoC容器会调用这个方法。

  9. 调用BeanPostProcessor的postProcessAfterInitialization方法:Spring IoC容器会调用所有注册的BeanPostProcessor的postProcessAfterInitialization方法,这个方法在Bean初始化之后被调用。

以上步骤完成后,Bean就已经准备好被应用程序使用了,它会一直驻留在应用上下文中,直到应用上下文被销毁。

当Bean不再需要时,会进入销毁阶段:

  1. 调用DisposableBean的destroy方法:如果Bean实现了DisposableBean接口,Spring IoC容器会调用它的destroy方法。

  2. 调用自定义的销毁方法:如果在配置文件中通过destroy-method属性指定了销毁方法,Spring IoC容器会调用这个方法。

以上就是Spring Bean的生命周期的主要阶段和相关的函数调用。

架构说明

测试代码

  1. privoder 这里都被标记了自定义注解,里面是serviceImpl代码

@RpcService(interfaceClass = AbstractService.class)

  1. provider-api 里面是专门的service代码

  2. consumer 里面是 Controller,引入了外来的service,并且使用了@RpcReference 注解。

上面三条,构建了Spring MVC 架构。Mapper是跟数据库交互的,这个里面没啥数据库,所以不需要Mapper。

Client

@RpcReference

标识需要引用的远程服务,并实现自动注入相应的实现类。

RpcClient

Rpc 客户端类,负责向服务端发起请求(远程过程调用)

public interface RpcClient {
RpcMessage sendRpcRequest(RequestMetadata requestMetadata);
}

该接口分别被httpnettysocket实现。

架构

JDK和Cglib都是客户端调用方法处理类,他们里面有服务发现中心,RPC客户端,RPC客户端配置属性,服务名称,最后还可以执行一个远程调用方法。

CSPF中跟前两个配置是一样的,唯一不一样的是有一个Map需要缓存一下代理对象。

还有一个getProxy方法,目的是返回对应的代理对象。

RMC 是一个比较重要的东西,代码如下:


public class RemoteMethodCall {

/**
* 发起 rpc 远程方法调用的公共方法
*
* @param discovery 服务发现中心
* @param rpcClient rpc 客户端
* @param serviceName 服务名称
* @param properties 配置属性
* @param method 调用的具体方法
* @param args 方法参数
* @return 返回方法调用结果
*/
public static Object remoteCall(ServiceDiscovery discovery, RpcClient rpcClient, String serviceName,
RpcClientProperties properties, Method method, Object[] args) {
// 构建请求头 设置序列化算法
MessageHeader header = MessageHeader.build(properties.getSerialization());
// 构建请求体
RpcRequest request = new RpcRequest();
request.setServiceName(serviceName);
request.setMethod(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameterValues(args);

// 进行服务发现
ServiceInfo serviceInfo = discovery.discover(request);
if (serviceInfo == null) {
throw new RpcException(String.format("The service [%s] was not found in the remote registry center.",
serviceName));
}

// 构建通信协议信息
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setHeader(header);
rpcMessage.setBody(request);

// 构建请求元数据
// 建造者模式 可以进行链式调用
RequestMetadata metadata = RequestMetadata.builder()
.rpcMessage(rpcMessage)
.serverAddr(serviceInfo.getAddress())
.port(serviceInfo.getPort())
.timeout(properties.getTimeout()).build();

// todo:此处可以实现失败重试机制
// 发送网络请求,获取结果
RpcMessage responseRpcMessage = rpcClient.sendRpcRequest(metadata);

if (responseRpcMessage == null) {
throw new RpcException("Remote procedure call timeout.");
}

// 获取响应结果
RpcResponse response = (RpcResponse) responseRpcMessage.getBody();

// 如果 远程调用 发生错误
if (response.getExceptionValue() != null) {
throw new RpcException(response.getExceptionValue());
}
// 返回响应结果
return response.getReturnValue();
}

}

这段代码是一个用于发起RPC(远程过程调用)的公共方法 remoteCall。以下是代码中的主要部分的解释:

  1. 构建请求头和请求体:

    MessageHeader header = MessageHeader.build(properties.getSerialization());
    RpcRequest request = new RpcRequest();
    request.setServiceName(serviceName);
    request.setMethod(method.getName());
    request.setParameterTypes(method.getParameterTypes());
    request.setParameterValues(args);

    在这里,首先创建了一个消息头 header,用于设置序列化算法。然后创建了一个 RpcRequest 对象,用于封装RPC请求的相关信息,包括服务名称、方法名、参数类型和参数值等。

  2. 服务发现:

    ServiceInfo serviceInfo = discovery.discover(request);

    通过调用 discovery.discover(request) 方法,根据请求信息进行服务发现,获取目标服务的信息,包括地址和端口等。

  3. 构建通信协议信息:

    RpcMessage rpcMessage = new RpcMessage();
    rpcMessage.setHeader(header);
    rpcMessage.setBody(request);

    创建 RpcMessage 对象,将消息头和请求体设置进去,形成完整的通信协议信息。

  4. 构建请求元数据:

    RequestMetadata metadata = RequestMetadata.builder()
    .rpcMessage(rpcMessage)
    .serverAddr(serviceInfo.getAddress())
    .port(serviceInfo.getPort())
    .timeout(properties.getTimeout()).build();

    使用建造者模式创建 RequestMetadata 对象,包含了RPC请求的元数据,如 rpcMessage、服务器地址、端口和超时时间等。

  5. 发送RPC请求:

    RpcMessage responseRpcMessage = rpcClient.sendRpcRequest(metadata);

    调用 rpcClient.sendRpcRequest(metadata) 发送RPC请求,获取返回的 RpcMessage 对象,其中包含了服务器的响应信息。

  6. 处理响应结果:

    RpcResponse response = (RpcResponse) responseRpcMessage.getBody();

    从返回的 RpcMessage 中取出响应体,即 RpcResponse 对象。

  7. 处理远程调用异常:

    if (response.getExceptionValue() != null) {
    throw new RpcException(response.getExceptionValue());
    }

    如果远程调用发生异常,抛出 RpcException 异常。

  8. 返回响应结果:

    return response.getReturnValue();

    如果一切正常,返回RPC调用的结果。

这个方法封装了发起RPC调用的一般流程,通过调用这个方法,可以方便地发起远程过程调用并获取结果。

上面的RpcClirntAutoConfiguration还在spring.factories被进行注册了,运行项目时,自动注册。

在RCAC文件中,作用就是把该注册的bean都给他注册掉,由于bean比较多,算是一套,所以搞了一个属性类,进行注册。

@Bean(name = "loadBalance")// 对当前的Bean重命名
@Primary
@ConditionalOnMissingBean
// 当配置文件中属性 rpc.client.loadbalance 的值为 "random" 时,创建这个 Bean。
// matchIfMissing = true 表示如果未配置该属性,则同样创建这个 Bean。
@ConditionalOnProperty(prefix = "rpc.client", name = "loadbalance", havingValue = "random", matchIfMissing = true)
public LoadBalance randomLoadBalance() {
return new RandomLoadBalance();
}

在RCP中:

@ConfigurationProperties(prefix = "rpc.client")
public class RpcClientProperties {
private String loadbalance;
...

RCPC中已经注册了RCP文件。

PCBPP 获取被RpcReferce标注的类,然后进行属性替换

RCEDB客户端退出时进行内存释放


三种协议方式

主要作用是发送RPC请求并获取响应。

Server

  1. RpcService注解:这个注解用于标记RPC服务的实现类。它有三个属性:interfaceClass用于指定服务的接口类型,interfaceName用于指定服务的接口名,version用于指定服务的版本号。

  2. RpcComponentScan注解:这个注解用于指定Spring容器在启动时需要扫描的包路径。它有两个属性:valuebasePackages,都是用来指定需要扫描的包路径。

  3. RpcBeanDefinitionRegistrar类:这个类实现了ImportBeanDefinitionRegistrar接口,用于在Spring容器启动时注册RPC服务的实现类。它通过RpcClassPathBeanDefinitionScanner类扫描所有标注了RpcService注解的类,并将这些类注册到Spring容器中。

  4. RpcClassPathBeanDefinitionScanner类:这个类是一个自定义的类,它的作用是扫描指定包路径下的所有标注了RpcService注解的类,并将这些类注册到Spring容器中。

  5. RpcServer接口:这个接口定义了RPC服务的基本操作,如启动服务。具体的实现类需要实现这个接口,并提供具体的实现。

  6. RpcServerAutoConfiguration类:这个类是Spring Boot的自动配置类,它在Spring Boot应用启动时自动配置RPC服务。这个类的全限定名被添加到了spring.factories文件中,这样Spring Boot在启动时就会自动加载这个类。

  7. LocalServiceCache类:这个类是一个本地服务缓存,它用于存储RPC服务的实现类的实例。这个类提供了添加服务、获取服务和移除服务的方法。

  8. spring.factories文件:这个文件是Spring Boot的自动配置文件,它包含了所有的自动配置类的全限定名。在这个文件中,RpcServerAutoConfiguration类的全限定名被添加到了org.springframework.boot.autoconfigure.EnableAutoConfiguration属性下,这样Spring Boot在启动时就会自动加载RpcServerAutoConfiguration类,并执行它的自动配置逻辑。

这个项目的主要作用是实现RPC服务的自动配置和依赖注入。通过RpcComponentScan注解和RpcBeanDefinitionRegistrar类,可以在Spring容器启动时自动扫描并注册RPC服务的实现类。通过RpcService注解,可以将RPC服务的实现类暴露为远程服务,供其他服务消费者调用。通过RpcServer接口和RpcServerAutoConfiguration类,可以自动配置RPC服务的启动和运行。通过LocalServiceCache类,可以将RPC服务的实现类的实例存储在本地,以便于服务的获取和使用。

Handler

后缀为Handler的类通常在编程中被用作处理器,它们的主要职责是处理特定类型的任务或事件。这些处理器类通常会定义一些方法,这些方法用于处理特定的逻辑或业务需求。

例如,在你的项目中,RpcRequestHandler类就是一个处理器类,它的主要职责是处理RPC请求。它接收一个RpcRequest对象,然后通过反射调用请求指定的方法,并返回方法调用的结果。

总的来说,后缀为Handler的类通常用于封装处理特定任务或事件的逻辑,它们是实现模块化、解耦和复用的重要手段。

@AutowiredResource

@Autowired先类型后名称

@Resource先名称后类型

Core

雪花算法

在雪花算法中,生成的64位ID的首位是0,这是因为最高位是符号位,0表示正数,1表示负数。在生成ID时,我们通常希望ID是正数,所以首位是0。

优点:

  1. 高并发分布式环境下生成不重复ID: 雪花算法可以在高并发的分布式环境中生成不重复的ID。每个ID都包含了机器标识、时间戳和序列号,确保在同一毫秒内生成的ID不会冲突。

  2. 有序递增: ID中包含了时间戳和序列号,因此生成的ID基本上是有序递增的。这对于某些场景,如数据库索引的性能,可能是有利的。

  3. 不依赖第三方库或中间件: 雪花算法是一种纯粹的算法实现,不依赖外部的第三方库或中间件,使得集成和部署相对简单。

  4. 算法简单、效率高: 雪花算法的实现相对简单,且在内存中进行,因此具有较高的效率。

缺点:

  1. 依赖服务器时间: 雪花算法的唯一性依赖于服务器的时钟,如果服务器的时钟发生回拨,可能导致生成重复的ID。为了解决这个问题,需要在算法中记录最后一个生成ID时的时间戳,并在每次生成ID之前检查当前服务器时钟是否被回拨,以避免生成重复的ID。

位数表示

  1. 符号位(1位): 最高位是符号位,对于正整数而言,固定为0,以保证生成的ID是正数。

  2. 时间戳(41位): 这部分存储的是当前时间戳,通常以毫秒为单位。由于41位的长度,可以表示的时间范围是2^41毫秒,大约69年。这意味着在同一台机器上,每毫秒都可以生成一个唯一的时间戳。

  3. 机器码(10位): 这包括5位的数据中心ID(datacenterId)和5位的工作机器ID(workerId)。这样设计允许在分布式系统中有多个数据中心,每个数据中心可以部署多台机器,每台机器有唯一的workerId。这样,整个系统就支持了2^10=1024台机器。

  4. 序列号(12位): 在同一毫秒时间戳内,通过序列号来区分不同的ID。这部分允许同一机器在同一毫秒内生成多个ID。序列号的长度为12位,因此在同一毫秒内,最多可以生成2^12=4096个不重复的ID。

ServiceUtil

工具类

Map map = gson.fromJson(gson.toJson(serviceInfo), Map.class);

结果是:{appName=TestApp, serviceName=TestService, version=1.0, address=127.0.0.1, port=8080.0}

帧解码器

使用定长解决粘包、拆包问题

一种基于定长,一种基于分隔符

参数解释:

这段注释描述了在 RpcFrameDecoder 中使用的默认配置,其中涉及到三个关键参数:

  1. 数据帧的最大长度为1024字节:

    • 这表示在进行帧解码时,允许的单个数据帧的最大长度为1024字节。如果接收到的数据帧超过这个长度,解码器可能会进行相应的处理,例如丢弃过长的数据或者引发异常。
  2. 长度域的偏移字节数为12:

    • 长度域是指用于表示数据帧长度的字段。在数据帧中,有一个特定的位置用来存储整个数据帧的长度信息,而这个位置距离帧的起始位置的字节数就是长度域的偏移。在这个默认配置中,长度域的偏移是12字节,意味着长度信息的起始位置在数据帧的第12个字节处。
  3. 长度域所占的字节数为4:

    • 长度域所占的字节数表示用于表示数据帧长度的字段本身占用多少字节。在这个默认配置中,长度域占用4字节,这意味着用来表示数据帧长度的字段是一个4字节长的字段。这个字段的值会告诉解码器整个数据帧的长度,从而帮助解码器正确地提取出完整的数据帧。

假设有一个基于长度字段的通信协议,协议规定每个数据帧的格式如下:

  • 数据帧的最大长度为1024字节。
  • 数据帧中包含一个4字节的长度字段,用于表示整个数据帧的长度。
  • 长度字段的偏移为12字节,即长度字段的值存储在数据帧的第12个字节到第15个字节之间。

考虑一个数据帧的例子:

[ 0, 0, 0, 16, ... 数据内容 ... ]

在这个例子中:

  • 数据帧的长度字段的值为16(占用4个字节)。
  • 长度字段的偏移为12字节,因此长度字段的值位于数据帧的第12个到第15个字节。
  • 数据帧的实际内容(数据内容)在长度字段之后。

这个数据帧的长度为16字节,其中包括4字节的长度字段和12字节的实际数据内容。

RpcFrameDecoder 中的默认配置正适用于这个假设的通信协议,它会解码接收到的字节流,确保能够正确地提取出完整的数据帧,防止粘包和拆包问题。

ByteBuf

ByteBuf 是 Netty 框架中用于表示字节序列的缓冲区(buffer)的抽象类。它提供了一种灵活而高效的方式来处理字节数据,特别适用于网络通信和数据的异步处理。

以下是一些关键概念和特性,以及对 ByteBuf 的简要解释:

  1. 字节缓冲区: ByteBuf 是字节缓冲区的抽象,用于存储和操作字节数据。它提供了一组用于读写字节的方法。

  2. 可读和可写: ByteBuf 可以被视为可读和可写的,即可以从中读取数据,也可以向其中写入数据。这使得它非常适用于实现网络通信中的输入和输出。

  3. 可扩展: ByteBuf 的实现是可扩展的,可以动态地调整其容量。这使得它在处理变长数据时非常灵活。

  4. 池化: Netty 中的 ByteBuf 实现通常是池化的,即它们可以被重复使用,减少对象创建和销毁的开销。这对于高性能的网络应用非常重要。

  5. 零拷贝: ByteBuf 的设计目标之一是支持零拷贝。通过使用 ByteBuf,可以在数据传输时最小化数据的复制操作,提高性能。

  6. 读写索引: ByteBuf 有两个索引:读索引和写索引。读索引表示下一个将被读取的位置,写索引表示下一个将被写入的位置。这使得可以方便地在缓冲区中执行读写操作。

  7. 引用计数: ByteBuf 实现了引用计数机制,用于跟踪缓冲区的引用次数。这是为了确保在不再需要缓冲区时,可以安全地释放它的资源。

ByteBuf buffer = ...;  // 创建一个 ByteBuf 实例

// 写入数据到缓冲区
buffer.writeBytes("Hello, World!".getBytes());

// 读取数据从缓冲区
byte[] data = new byte[buffer.readableBytes()];
buffer.readBytes(data);

// 打印读取到的数据
System.out.println(new String(data));

事件监听器

在 Java 中,一个基础的监听器通常是通过接口(Listener Interface)和实现这个接口的类来实现的。这种模式通常被称为事件监听器模式。以下是一个简单的事件监听器的实现示例:

步骤一:定义监听器接口

// 定义监听器接口
public interface MyEventListener {
void onEvent(EventObject event);
}

步骤二:定义事件对象

// 定义事件对象
public class EventObject {
private String eventData;

public EventObject(String eventData) {
this.eventData = eventData;
}

public String getEventData() {
return eventData;
}
}

步骤三:实现监听器接口的类

// 实现监听器接口的类
public class MyEventListenerImpl implements MyEventListener {
@Override
public void onEvent(EventObject event) {
System.out.println("Event received with data: " + event.getEventData());
}
}

步骤四:触发事件的类

// 触发事件的类
public class EventSource {
private List<MyEventListener> listeners = new ArrayList<>();

public void addEventListener(MyEventListener listener) {
listeners.add(listener);
}

public void removeEventListener(MyEventListener listener) {
listeners.remove(listener);
}

public void triggerEvent(String eventData) {
EventObject event = new EventObject(eventData);

// 通知所有注册的监听器
for (MyEventListener listener : listeners) {
listener.onEvent(event);
}
}
}

步骤五:使用示例

public class Main {
public static void main(String[] args) {
// 创建事件监听器
MyEventListener listener = new MyEventListenerImpl();

// 创建事件源
EventSource eventSource = new EventSource();

// 注册事件监听器
eventSource.addEventListener(listener);

// 触发事件
eventSource.triggerEvent("Hello, World!");

// 移除事件监听器
eventSource.removeEventListener(listener);
}
}

枚举类

服务发现

服务发现是一种用于在分布式系统中动态地查找和识别可用服务实例的机制。在分布式系统中,服务通常以多个实例的形式部署在不同的主机或容器中,而服务发现的目标就是让客户端能够轻松地找到并与这些服务实例进行通信。

关键的服务发现的方面包括:

  1. 服务注册: 服务在启动时将自己的信息(如IP地址、端口号、服务名称等)注册到服务发现系统中。注册通常在服务启动时、停止时或周期性地进行。

  2. 服务发现: 客户端通过查询服务发现系统,获取到可用的服务实例的信息。服务发现系统负责跟踪各个服务实例的状态和位置,并在客户端请求时提供这些信息。

  3. 负载均衡: 服务发现系统通常还提供负载均衡的功能,确保客户端请求分布到各个可用的服务实例上,以达到更好的性能和可用性。

  4. 健康检查: 服务发现系统通常会定期检查注册的服务实例的健康状况,将不健康或不可用的服务实例从服务列表中剔除。

服务发现的优势和用途包括:

  • 弹性和可扩展性: 可以方便地添加或删除服务实例,使得系统更具弹性和可扩展性。

  • 自动化: 自动注册和发现服务,减轻了手动配置的工作,降低了出错的可能性。

  • 负载均衡: 通过负载均衡,客户端可以均匀地分布请求到不同的服务实例上,提高系统整体的性能和稳定性。

  • 故障恢复: 当某个服务实例发生故障时,服务发现系统可以自动检测到并将其从可用服务列表中移除,确保客户端不会请求到不可用的服务实例。

  • 多环境支持: 方便在不同的环境中部署和调试服务,例如在开发、测试和生产环境中进行无缝切换。

常见的服务发现工具包括Consul、Zookeeper、Etcd、Eureka等。这些工具提供了简便的API和管理界面,使得服务发现的实现更加容易。

SESSION,CONNECT

SESSION_TIMEOUTCONNECT_TIMEOUT 是在 Zookeeper 客户端连接和通信中使用的两个超时参数。以下是它们的作用:

  1. SESSION_TIMEOUT(会话超时):

    • SESSION_TIMEOUT 定义了客户端与 Zookeeper 服务器之间的会话超时时间。
    • 会话超时是指客户端与服务器之间的一个时间段,在这个时间段内,客户端需要定期向服务器发送心跳以保持会话有效。如果在 SESSION_TIMEOUT 时间内客户端没有成功地与服务器保持联系,服务器会认为该客户端会话已经失效。
    • 在Zookeeper中,SESSION_TIMEOUT 的设置对于保持Zookeeper集群中各个节点的状态同步非常重要。通常,SESSION_TIMEOUT 的值会根据网络延迟和集群的负载等因素进行调整。
  2. CONNECT_TIMEOUT(连接超时):

    • CONNECT_TIMEOUT 定义了客户端连接到 Zookeeper 服务器的最大超时时间。
    • 当客户端尝试连接到Zookeeper服务器时,如果在 CONNECT_TIMEOUT 时间内无法建立连接,那么连接操作将被认为失败。这个超时时间是为了防止客户端在连接到不可用或故障的Zookeeper服务器上浪费太多时间。
    • 这个参数的设置通常取决于网络环境和Zookeeper服务器的性能。

这两个超时参数都是在初始化Zookeeper客户端时配置的,通常通过客户端构建工厂提供的方法进行设置。在上面提到的代码中,使用了Curator框架,其中的 SESSION_TIMEOUTCONNECT_TIMEOUT 参数在创建 CuratorFrameworkFactory 时通过相应的参数传递给构造函数。

元注解

什么是元注解?元注解是注解的注解。

在程序开发中,有时我们需要自定义一个注解,而自定义注解类就需要被元注解修饰,以定义该类的一些基本特征。例如,我们创建一个名为 LogAnnotation 的自定义注解类:

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
String module() default "";
}

@interface 表示声明一个注解,方法名对应参数名,返回值类型对应参数类型。

@Target

@Target 注解用于定义注解的使用位置。如果没有指定,表示注解可以用于任何地方。@Target 的格式为:

// 单参数
@Target({ ElementType.METHOD })
// 多参数
@Target(value = {ElementType.METHOD, ElementType.TYPE})

@TargetElementType 取值有以下类型:

  • TYPE:类,接口或者枚举
  • FIELD:域,包含枚举常量
  • METHOD:方法
  • PARAMETER:参数
  • CONSTRUCTOR:构造方法
  • LOCAL_VARIABLE:局部变量
  • ANNOTATION_TYPE:注解类型
  • PACKAGE:包

@Retention

@Retention 注解用于指明修饰的注解的生存周期,即会保留到哪个阶段。格式为:

@Retention(RetentionPolicy.RUNTIME)

RetentionPolicy 的取值包含以下三种:

  • SOURCE:源码级别保留,编译后即丢弃。
  • CLASS:编译级别保留,编译后的 class 文件中存在,在 JVM 运行时丢弃,这是默认值。
  • RUNTIME:运行级别保留,编译后的 class 文件中存在,在 JVM 运行时保留,可以被反射调用。

@Documented

@Documented 指明修饰的注解可以被例如 Javadoc 等工具文档化,它只负责标记,没有成员取值。

@Inherited

@Inherited 注解用于标注一个父类的注解是否可以被子类继承。如果一个注解需要被其子类所继承,则在声明时直接使用 @Inherited 注解即可。如果没有写此注解,则无法被子类继承。下面是一个测试示例:

// 自定义一个注解
@interface MyAnnotation {
public String key() default "key1";
public String value() default "value1";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 如果父类使用了 HeritedApplication 注解,则子类应该继承
@Inherited
@MyAnnotation
@interface HeritedApplication {
}

// 父类使用了 @HeritedApplication 注解
@HeritedApplication
class Person {
}

class Student extends Person {
}

public class AnnotationInherited {
public static void main(String[] args) throws Exception {
Class<?> clazz = Student.class;
// Student 类是否有 @HeritedApplication
if (clazz.isAnnotationPresent(HeritedApplication.class)) {
System.out.println("true");
}
}
}

运行程序,结果为 true

@AliasFor

@AliasFor 注解用于声明注解属性的别名,主要有两种应用场景:注解内的别名和元数据的别名。

注解内的别名

首先,让我们深入了解 @AliasFor 的源码以理解其在注解内别名的应用。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AliasFor {

@AliasFor("attribute")
String value() default "";

String attribute() default "";

Class<? extends Annotation> annotation() default Annotation.class;
}

在这里,我们看到了 @AliasFor 注解自身使用了注解内别名的特性。具体而言,当我们使用这个注解时,@AliasFor(value="xxx")@AliasFor(attribute="xxx") 是等价的。

举例来说,假设有一个注解 @MyAnnotationA

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MyAnnotationA {

String a1() default "";
}

使用这个注解时,我们可以这样写: @MyAnnotationA(a1="xxx"),表示将属性 a1 设置为 “xxx”。

现在,如果我们想以另一种方式使用这个注解,例如 @MyAnnotationA(a2="xxx"),我们可以通过别名的方式实现。修改 @MyAnnotationA 如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MyAnnotationA {

@AliasFor("a2")
String a1() default "";

@AliasFor("a1")
String a2() default "";
}

在这个用法中需要注意几点:

  • 组成别名对的每个属性都必须加上注解 @AliasForattribute()value() 属性必须引用该对中另一个属性。
  • 别名属性必须声明相同的返回类型。
  • 别名属性必须声明一个默认值。
  • 别名属性必须声明相同的默认值。

元数据的别名

其次,我们来看元数据的别名的应用。直接以一个例子说明:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@MyAnnotationA
public @interface MyAnnotationB {

@AliasFor(annotation = MyAnnotationA.class, value = "a2")
String value() default "";
}

在这个例子中,@MyAnnotationB("vvv")@MyAnnotationA(a2="vvv") 是等价的。这里可以理解为,MyAnnotationBvalue 属性重写了 MyAnnotationAa2 属性,但重新的属性的返回类型必须相同。

在 Spring 框架中,这种用法很常见。例如,以 @Component@Configuration 为例:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {

@AliasFor(annotation = Component.class)
String value() default "";

boolean proxyBeanMethods() default true;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {

String value() default "";
}

对于 value 这个属性来说,@Configuration 注解中的值会重写 @Componentvalue 属性值。这可以理解为 @Configuration 注解是 @Component 的子注解。

接口和抽象类

接口(Interface):

特点:

  1. 接口只能包含常量(static final字段)和抽象方法(没有方法体的方法)。
  2. 接口中的所有方法默认为 publicabstract,字段默认为 publicstaticfinal
  3. 一个类可以实现多个接口(多继承),通过 implements 关键字。
  4. 接口支持多态性,可以使用接口类型引用具体实现类的对象。

用途:

  1. 定义一组相关的方法,强制实现这些方法的类拥有特定的行为。
  2. 实现多继承,通过实现多个接口,一个类可以具备多种类型的行为。
  3. 定义常量,为实现类提供共享的常量。

抽象类(Abstract Class):

特点:

  1. 抽象类可以包含抽象方法和非抽象方法。
  2. 抽象方法是没有方法体的方法,需要由子类提供具体实现。
  3. 抽象类可以包含字段、构造方法,可以有访问修饰符,可以有非抽象方法的实现。
  4. 一个类只能继承一个抽象类,通过 extends 关键字。

用途:

  1. 提供一个基类,定义一些通用的行为,但不提供完整的实现。
  2. 允许在抽象类中定义一些非抽象方法,提供一些通用的实现。
  3. 通过抽象类实现代码的重用,提高代码的可维护性。
  4. 可以包含字段,可以有构造方法,可以定义访问控制符。

不同点:

  1. 多继承: 接口支持多继承,一个类可以实现多个接口;而抽象类只支持单继承,一个类只能继承一个抽象类。
  2. 构造方法: 接口不能包含构造方法;抽象类可以包含构造方法。
  3. 字段: 接口只能包含常量字段,而抽象类可以包含各种字段,包括实例字段和静态字段。
  4. 访问修饰符: 接口中的方法默认为 public,而抽象类中的方法可以有不同的访问修饰符。
  5. 方法体: 接口中的方法没有方法体;抽象类中的抽象方法没有方法体,但非抽象方法可以有具体的实现。

选择使用场景:

  • 使用接口当你希望提供一组相关的操作,而不关心它们的实现。
  • 使用抽象类当你希望提供一组相关的操作,并且要包含一些通用的实现代码。

AtomicInteger

AtomicInteger 是 Java 中 java.util.concurrent.atomic 包下的一个类,用于在多线程环境中进行原子操作。它通过对 int 类型数据的封装,提供了一系列能够以原子方式执行的方法,从而保证了对该整数值的操作是线程安全的。

作用:

在多线程环境中,对整数值进行诸如自增、自减等运算的操作在默认情况下是线程不安全的。AtomicInteger 的作用就是提供了一种不需要使用锁的方式来保障这些操作的线程安全性。通过使用 AtomicInteger,可以避免因为多线程并发修改导致的数据不一致问题。

原子操作方法:

  1. addAndGet(int delta): 以原子方式将给定值添加到当前值,并在添加后返回新值。
  2. getAndAdd(int delta): 以原子方式将给定值添加到当前值并返回旧值。
  3. incrementAndGet(): 以原子方式将当前值递增1并在递增后返回新值。
  4. getAndIncrement(): 以原子方式递增当前值并返回旧值。
  5. decrementAndGet(): 原子地将当前值减1并在减量后返回新值。
  6. getAndDecrement(): 以原子方式递减当前值并返回旧值。

原子更新方法:

  1. compareAndSet(int expect, int update): 如果当前值等于预期值,则以原子方式将该值设置为给定的更新值。
  2. getAndAccumulate(int factor, IntBinaryOperator operation): 先返回旧值,再进行计算。
  3. accumulateAndGet(int factor, IntBinaryOperator operation): 先计算,再返回新值。

使用场景:

在需要对一个整数进行多线程安全操作的场景下,可以考虑使用 AtomicInteger,避免使用锁带来的性能影响。在轻量级的计数器、标记等场景中,它是一个很好的选择。

底层实现:

AtomicInteger 的底层实现依赖于 CAS(Compare and Swap)操作,是一种乐观锁的实现方式。CAS 操作通过硬件指令提供了一种在多线程环境下不需要锁的原子操作,它能够判断某个位置的值是否是预期的值,如果是,则更新为新的值;如果不是,则不做任何操作。

Map.Entry

  1. getKey(): 返回与此项对应的键。

  2. getValue(): 返回与此项对应的值。

  3. setValue(V value): 用指定的值替换与此项对应的值。

Map.Entry 通常用于迭代 Map 中的键值对,尤其是在使用迭代器遍历 Map 的情况下。通过 MapentrySet() 方法可以获取包含所有键值对的集合,每个元素都是 Map.Entry

import java.util.HashMap;
import java.util.Map;

public class MapEntryExample {
public static void main(String[] args) {
// 创建一个HashMap
Map<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
map.put("Three", 3);
// 使用entrySet()获取键值对集合
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}

TreeMap

TreeMap 是 Java 中的一种有序映射表,它基于红黑树(Red-Black Tree)实现。TreeMap 继承自 AbstractMap 类并实现了 NavigableMap 接口。

主要特点和使用说明:

  1. 有序性: TreeMap 中的元素是有序的,根据键的自然顺序或者通过构造函数提供的 Comparator 进行排序。这使得 TreeMap 中的键值对按照一定的顺序存储,便于检索和遍历。

  2. 底层数据结构: TreeMap 使用红黑树(Red-Black Tree)作为底层的数据结构,确保了查找、插入和删除操作的对数时间复杂度。

  3. 键的唯一性: TreeMap 中的键是唯一的,每个键最多关联一个值。

  4. 自然排序和定制排序: 如果键实现了 Comparable 接口,那么就按照键的自然顺序进行排序。如果没有实现 Comparable 接口,可以在构造函数中提供一个 Comparator 来指定排序规则。

以下是一个简单的示例演示了如何使用 TreeMap

import java.util.*;

public class TreeMapExample {
public static void main(String[] args) {
// 创建一个 TreeMap
TreeMap<String, Integer> treeMap = new TreeMap<>();

// 添加键值对
treeMap.put("Three", 3);
treeMap.put("One", 1);
treeMap.put("Four", 4);
treeMap.put("Two", 2);

// 遍历 TreeMap
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

// 输出结果将按照键的自然顺序排序
}
}

在上述示例中,TreeMap 中的键值对按照键的自然顺序进行了排序。你也可以通过提供自定义的 Comparator 来实现定制排序。

需要注意的是,TreeMap 不允许键为 null,因为它依赖键的顺序。如果需要键为 null,可以考虑使用 HashMap

各种Map

  1. HashMap:

    • 底层实现: 基于哈希表,使用数组和链表/红黑树来实现。
    • 特点:
      • 无序:迭代顺序不保证稳定。
      • 允许 null 作为键和值。
      • 支持高效的插入和查找操作。
    • 适用场景:
      • 当不需要保留插入顺序,且对迭代顺序没有特殊要求时。
      • 需要高效的插入和查找操作时。
  2. TreeMap:

    • 底层实现: 基于红黑树,是一种自平衡的二叉查找树。
    • 特点:
      • 有序:根据键的自然顺序或提供的 Comparator 进行排序。
      • 不允许键为 null
    • 适用场景:
      • 当需要按照键的顺序进行有序存储和遍历时。
      • 不允许键为 null 的情况。
  3. LinkedHashMap:

    • 底层实现: 继承自 HashMap,通过双向链表维护插入顺序。
    • 特点:
      • 保留插入顺序。
      • 可以选择根据访问顺序进行排序。
    • 适用场景:
      • 当需要保留插入顺序时。
      • 当需要实现 LRU(Least Recently Used)缓存时,可以使用基于访问顺序的 LinkedHashMap
  4. HashTable:

    • 底层实现: 基于哈希表。
    • 特点:
      • 线程安全,每个方法都被 synchronized 关键字修饰。
      • 不允许键或值为 null
    • 适用场景:
      • 在多线程环境中,需要保证线程安全的情况下使用。
      • 不允许键或值为 null
  5. ConcurrentHashMap:

    • 底层实现: 基于哈希表,采用分段锁机制。
    • 特点:
      • 线程安全,支持高并发的读和一定程度的并发写。
      • 不使用全局锁,而是采用分段锁,提高并发性能。
    • 适用场景:
      • 在高并发场景下,读操作远远多于写操作时,使用 ConcurrentHashMap 可以获得更好的性能。
      • 在需要保证线程安全的情况下,性能要求较高。

一致性hash算法

一致性哈希算法是一种分布式系统中常用的负载均衡算法,特别适用于缓存服务器、分布式数据库等场景。其核心思想是将整个哈希空间划分成一圈,当有新的节点加入或者节点离开时,只会影响到少量的数据映射关系,从而减少数据迁移的开销。

以下是一致性哈希算法的基本原理和过程:

  1. 哈希空间的划分: 将哈希空间划分成一个环状结构,通常使用一个环来表示。哈希函数将节点映射到环上的位置,每个节点在环上对应唯一的位置。

  2. 节点映射: 将存储节点通过哈希函数映射到环上的位置。这可以是节点的 IP 地址、主机名、或其他唯一标识。

  3. 数据映射: 将数据通过相同的哈希函数映射到环上的位置。这样,每个数据也有一个唯一的环上位置。

  4. 节点的加入和离开: 当有新节点加入时,只需要调整它的哈希位置附近的部分数据映射到新节点上。当节点离开时,也只需要调整离开节点附近的数据映射关系。

  5. 数据的路由: 当需要查找或写入数据时,通过哈希函数找到数据在环上的位置,然后顺时针寻找下一个存储节点,将数据存储或获取。因为环是环形的,所以在环上寻找节点时,如果到达末尾则从头开始。

  6. 均衡性: 由于哈希函数的性质,哈希空间上的节点分布相对均匀,每个节点负责的数据量大致相等。这种均匀性使得一致性哈希算法在分布式系统中能够实现负载均衡。

  7. 虚拟节点: 为了更好地实现均衡性,一致性哈希算法引入了虚拟节点的概念。即为每个实际节点在环上创建多个虚拟节点,每个虚拟节点负责一部分数据。这样可以更均匀地分布数据,减小数据迁移的开销。

一致性哈希算法的优势在于对于节点的动态变化(新增或删除节点)具有较好的适应性,且在数据迁移方面相对节约资源。这使得它成为构建高性能、可伸缩的分布式系统中的一种重要算法。

hash

private long hash(byte[] digest, int number) {
return (((long) (digest[3 + number * 4] & 0xFF) << 24)
| ((long) (digest[2 + number * 4] & 0xFF) << 16)
| ((long) (digest[1 + number * 4] & 0xFF) << 8)
| (digest[number * 4] & 0xFF))
& 0xFFFFFFFFL;
}

这段代码定义了一个 hash 方法,它接受两个参数:一个是 byte[] digest 数组,另一个是 int number。该方法的目的是从 digest 数组中提取特定位置的四个字节(一个 int 的大小),将其转换为一个 long 类型的哈希值。

让我们逐步解释这个方法:

  1. digest 数组是一个字节数组,通常是通过对某个数据进行哈希计算(如 MD5 或 SHA-256)得到的摘要。

  2. number 参数表示从 digest 数组的哪个位置开始提取四个字节。这个位置通过 number 参数计算得到。

  3. & 0xFF 操作用于确保取得的字节值在 0 到 255 之间,因为 Java 的 byte 类型是有符号的,范围是 -128 到 127。通过 & 0xFF,我们将其转换为无符号的正整数。

  4. 通过位运算将四个字节合并为一个 long 类型的值。这里采用了大端序(Big Endian)的方式,即高位字节在前,低位字节在后。

    • digest[3 + number * 4] 表示取第一个字节(最高位字节),左移 24 位。
    • digest[2 + number * 4] 表示取第二个字节,左移 16 位。
    • digest[1 + number * 4] 表示取第三个字节,左移 8 位。
    • digest[number * 4] 表示取第四个字节。
  5. 通过位运算 | 将上述步骤的结果合并为一个 long 值。

  6. & 0xFFFFFFFFL 用于确保最终的哈希值为正整数。在 Java 中,整数是有符号的,而我们需要一个无符号的正整数表示哈希值。

这个方法的目的是从 digest 数组中提取一部分字节,并将其转换为一个 long 类型的哈希值。在哈希算法中,这样的操作通常用于将哈希值映射到一个固定范围的整数,以便进行一致性哈希或其他类似的算法。

JDK 序列化算法

public <T> byte[] serialize(T object) {
try {
// 创建一个字节数组输出流,用于存储序列化后的数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// 创建一个对象输出流,将对象序列化到字节数组输出流中
ObjectOutputStream oos = new ObjectOutputStream(baos);

// 将对象写入字节数组输出流
oos.writeObject(object);

// 获取字节数组输出流中的字节数组
return baos.toByteArray();
} catch (IOException e) {
// 处理异常,抛出自定义的序列化异常
throw new SerializeException("Jdk serialize failed.", e);
}
}

解释说明:

  1. ByteArrayOutputStream: 创建一个字节数组输出流,它会在内存中创建一个缓冲区,用于存储序列化后的数据。

  2. ObjectOutputStream: 创建一个对象输出流,它连接到字节数组输出流,用于将对象序列化为字节数组。

  3. oos.writeObject(object): 将传入的对象 object 进行序列化,写入到字节数组输出流中。

  4. baos.toByteArray(): 获取字节数组输出流中的字节数组,即序列化后的数据。

  5. IOException 处理: 如果在序列化过程中发生 IOException 异常,捕获该异常,并抛出自定义的 SerializeException 异常。这是为了封装序列化可能出现的异常,使调用者能够更方便地处理。

序列化算法

JDK、JSON、HESSIAN、KRYO、PROTOSTUFF,其中 JSON 使用的是 Gson 实现。此外,还可以使用 FastJson、Jackson 等实现 JSON 序列化。

五种序列化算法的比较如下:

序列化算法 优点 缺点
Kryo 速度快,序列化后体积小 跨语言支持较复杂
Hessian 默认支持跨语言 较慢
Protostuff 速度快,基于 protobuf 需静态编译
Json 使用方便 性能一般
JDK 使用方便,可序列化所有类 速度慢,占空间

InputStream和OutputStream

  1. 输入流(InputStream):

    • InputStream是抽象类,用于从各种数据源(如文件、网络连接、内存等)读取字节流。
    • 常见的子类包括FileInputStreamByteArrayInputStreamSocketInputStream等。
  2. 输出流(OutputStream):

    • OutputStream是抽象类,用于向各种目的地(如文件、网络连接、内存等)写入字节流。
    • 常见的子类包括FileOutputStreamByteArrayOutputStreamSocketOutputStream等。

例子:使用Socket进行消息的发送和接收

1. 服务器端:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Server listening on port 8080...");

// 等待客户端连接
Socket socket = serverSocket.accept();

// 获取输入流
InputStream inputStream = socket.getInputStream();

// 读取客户端发送的消息
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
String message = new String(buffer, 0, bytesRead);
System.out.println("Received message from client: " + message);

// 获取输出流
OutputStream outputStream = socket.getOutputStream();

// 发送响应消息给客户端
String response = "Hello, Client!";
outputStream.write(response.getBytes());

// 关闭连接
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

2. 客户端:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
public static void main(String[] args) {
try {
// 连接到服务器
Socket socket = new Socket("localhost", 8080);

// 获取输出流
OutputStream outputStream = socket.getOutputStream();

// 发送消息给服务器
String message = "Hello, Server!";
outputStream.write(message.getBytes());

// 获取输入流
InputStream inputStream = socket.getInputStream();

// 读取服务器的响应消息
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
String response = new String(buffer, 0, bytesRead);
System.out.println("Received response from server: " + response);

// 关闭连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

服务器端通过ServerSocket监听端口,接受客户端连接,然后通过输入流接收客户端发送的消息,通过输出流发送响应消息。客户端通过Socket连接到服务器,通过输出流发送消息,通过输入流接收服务器的响应消息。