聊聊如何玩转spring-boot-admin

前言

1、何为spring-boot-admin?

Spring Boot Admin 是一个监控工具,旨在以良好且易于访问的方式可视化 Spring Boot Actuators 提供的信息

快速开始

如何搭建spring-boot-admin-server

1、在服务端项目的POM引入相应的GAV

  <dependency><groupId>de.codecentric</groupId><artifactId>spring-boot-admin-starter-server</artifactId><version>${spring-boot-admin.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>

2、新建springboot启动类并加上@EnableAdminServer

@SpringBootApplication
@EnableAdminServer
public class MonitorApplication {public static void main(String[] args) {SpringApplication.run(MonitorApplication.class);}
}

配置完,访问一下页面

虽然可以访问,但是这样不安全,接下来我们和spring security做个整合

3、整合spring security

a、 在服务端项目的pom引入security GAV

 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

b、 在服务端项目的application.yml配置相关用户名和密码

spring:security:user:name: ${MONITOR_USER:admin}password: ${MONITOR_PWD:admin}

c、 定制security config

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityMonitorConfig extends WebSecurityConfigurerAdapter {private final AdminServerProperties adminServer;private final WebEndpointProperties webEndpointProperties;@Overrideprotected void configure(HttpSecurity http) throws Exception {SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();successHandler.setTargetUrlParameter("redirectTo");successHandler.setDefaultTargetUrl(this.adminServer.path("/"));http.authorizeRequests().requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/assets/**"))).permitAll().requestMatchers(new AntPathRequestMatcher(this.adminServer.path(webEndpointProperties.getBasePath() + "/info"))).permitAll().requestMatchers(new AntPathRequestMatcher(adminServer.path(webEndpointProperties.getBasePath() + "/health"))).permitAll().requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/login"))).permitAll().anyRequest().authenticated().and().formLogin().loginPage(this.adminServer.path("/login")).successHandler(successHandler).and().logout().logoutUrl(this.adminServer.path("/logout")).and().httpBasic().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).ignoringRequestMatchers(new AntPathRequestMatcher(this.adminServer.path("/instances"), POST.toString()),new AntPathRequestMatcher(this.adminServer.path("/instances/*"), DELETE.toString()),new AntPathRequestMatcher(this.adminServer.path(webEndpointProperties.getBasePath() + "/**")));http.rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600));}}

配置完访问一下页面


输入用户名和密码 admin/admin


如果对整合安全认证还有疑问,可以直接参考官网
https://docs.spring-boot-admin.com/current/security.html

4、页面定制

如果我们觉得登录的springboot admin logo个性化不强,我们可以简单定制一下

在application.yml做如下配置

spring:boot:admin:ui:title: ${UI_TITLE:LYB-GEEK Monitor}brand: <img src="assets/img/icon-spring-boot-admin.svg"><span>${spring.boot.admin.ui.title}</span>

配置好访问一下


如果有些导航栏,我们觉得不需要,比如去掉关于我们

spring:boot:admin:ui:view-settings:- name: "about"enabled: false

注: view-settings这个配置需要是2.3.1以上版本才有的属性

配置好访问一下


发现关于我们已经去掉了,以上只是简单定制,更多定制可以参考如下链接
https://docs.spring-boot-admin.com/current/customize_ui.html

5、与注册中心集成

a、 在服务端项目中pom引入eureka-client GAV

  <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>

b、 在application.yml文件引入eureka 客户端相关配置

eureka:instance:instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}prefer-ip-address: ${PREFER_IP:true}  #是否选择IP注册#   ip-address: ${IP_ADDRESS:localhost}   #指定IP地址注册lease-renewal-interval-in-seconds: 5  #续约更新时间间隔(默认30秒),使得eureka及时剔除无效服务lease-expiration-duration-in-seconds: 10 #续约到期时间(默认90秒)hostname: ${HOSTNAME:${spring.application.name}}client:service-url:defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}#缩短延迟向服务端注册的时间、默认40sinitial-instance-info-replication-interval-seconds: 10#提高Eureka-Client端拉取Server注册信息的频率,默认30sregistry-fetch-interval-seconds: 5

访问eureka控制面板


服务端的配置暂且说到这边,接下来我们说下客户端集成

如何搭建spring-boot-admin-client

1、在客户端项目的POM配置相关GAV

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>de.codecentric</groupId><artifactId>spring-boot-admin-starter-client</artifactId><version>${spring-boot-admin-client.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>

2、客户端暴露actuator相关端点

management:endpoints:web:exposure:include: "*" endpoint:health:show-details: ALWAYS

3、配置spring-boot-admin服务端地址

spring:boot:admin:client:url: http://localhost:8080

启动观察控制台,会发现有如下信息

原因是因为我们服务端配置了鉴权,因此我们客户端还需做如下配置

spring:boot:admin:client:url: http://localhost:8080username: adminpassword: admin

配置好,观察控制台,发现没异常信息,此时我们访问服务端监控面板


如图说明客户端搭建成功

4、配置应用信息

默认我们查看服务端监控面板–应用列表详情,会发现


这个信息是空的,我们可以在yml配置形如下内容

info:groupId: @project.groupId@artifactId: @project.artifactId@version: @project.version@describe: 这是一个微服务应用

再次访问服务端监控面板

其实这个采的就是actuator/info端点。当然可以像官方介绍的示例,在项目的POM引入springboot插件,并指定goal为build-info

<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><goals><goal>build-info</goal></goals></execution></executions></plugin></plugins>
</build>

5、在服务端监控面板集成客户端日志

默认是没集成客户端日志,如图


通过官网
在这里插入图片描述
我们知道要配置logging.file.path或者logging.file.name

示例配置

logging:file:path: ${LOG_FILE_PATH:/data/logs/cloud-mini-comsumer}

logback-spring相关配置如下

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false"><include resource="org/springframework/boot/logging/logback/defaults.xml"/><property name="serviceName" value="cloud-mini-comsumer"/><property name="logHome" value="/data/logs/${serviceName}"/><contextName>${serviceName}</contextName><!--输出到控制台--><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${CONSOLE_LOG_PATTERN}</pattern></encoder></appender><!--按天生成日志--><appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender"><Prudent>true</Prudent><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><FileNamePattern>${logHome}/%d{yyyy-MM-dd}/%d{yyyy-MM-dd}.log</FileNamePattern><maxHistory>30</maxHistory></rollingPolicy><layout class="ch.qos.logback.classic.PatternLayout"><Pattern>%d{yyyy-MM-dd HH:mm:ss} -%msg%n</Pattern></layout></appender><root level="info"><appender-ref ref="console"/><appender-ref ref="logFile"/></root></configuration>

我们配置后,出现日志文件按钮,点击后出现

那就很诡异,明明按官网配置了,后面排查发现,其他服务可以出现日志,他们配置日志目录底下,都会生成一个spring.log日志,那意味着只要能生成spring.log即可。于是我们调整一下logback-spring,将

 <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

调整为

 <include resource="org/springframework/boot/logging/logback/base.xml" />

然后重新访问服务端监控面板


发现有日志出来了。那为毛加了这个base.xml就有用,那是因为这个日志采集的端点是actuator/logfile。因为本文不是讲解源码,我就把相关核心源码,贴在下面,感兴趣的朋友可以根据下面提供的源码,进行debug调试

核心源码

@WebEndpoint(id = "logfile")
public class LogFileWebEndpoint {private static final Log logger = LogFactory.getLog(LogFileWebEndpoint.class);private File externalFile;private final LogFile logFile;public LogFileWebEndpoint(LogFile logFile, File externalFile) {this.externalFile = externalFile;this.logFile = logFile;}@ReadOperation(produces = "text/plain; charset=UTF-8")public Resource logFile() {Resource logFileResource = getLogFileResource();if (logFileResource == null || !logFileResource.isReadable()) {return null;}return logFileResource;}private Resource getLogFileResource() {if (this.externalFile != null) {return new FileSystemResource(this.externalFile);}if (this.logFile == null) {logger.debug("Missing 'logging.file.name' or 'logging.file.path' properties");return null;}return new FileSystemResource(this.logFile.toString());}}
public class LogFile {/*** The name of the Spring property that contains the name of the log file. Names can* be an exact location or relative to the current directory.* @deprecated since 2.2.0 in favor of {@link #FILE_NAME_PROPERTY}*/@Deprecatedpublic static final String FILE_PROPERTY = "logging.file";/*** The name of the Spring property that contains the directory where log files are* written.* @deprecated since 2.2.0 in favor of {@link #FILE_PATH_PROPERTY}*/@Deprecatedpublic static final String PATH_PROPERTY = "logging.path";/*** The name of the Spring property that contains the name of the log file. Names can* be an exact location or relative to the current directory.* @since 2.2.0*/public static final String FILE_NAME_PROPERTY = "logging.file.name";/*** The name of the Spring property that contains the directory where log files are* written.* @since 2.2.0*/public static final String FILE_PATH_PROPERTY = "logging.file.path";private final String file;private final String path;/*** Create a new {@link LogFile} instance.* @param file a reference to the file to write*/LogFile(String file) {this(file, null);}/*** Create a new {@link LogFile} instance.* @param file a reference to the file to write* @param path a reference to the logging path to use if {@code file} is not specified*/LogFile(String file, String path) {Assert.isTrue(StringUtils.hasLength(file) || StringUtils.hasLength(path), "File or Path must not be empty");this.file = file;this.path = path;}/*** Apply log file details to {@code LOG_PATH} and {@code LOG_FILE} system properties.*/public void applyToSystemProperties() {applyTo(System.getProperties());}/*** Apply log file details to {@code LOG_PATH} and {@code LOG_FILE} map entries.* @param properties the properties to apply to*/public void applyTo(Properties properties) {put(properties, LoggingSystemProperties.LOG_PATH, this.path);put(properties, LoggingSystemProperties.LOG_FILE, toString());}private void put(Properties properties, String key, String value) {if (StringUtils.hasLength(value)) {properties.put(key, value);}}@Overridepublic String toString() {if (StringUtils.hasLength(this.file)) {return this.file;}return new File(this.path, "spring.log").getPath();}

加了那个logback-base可以的原因是,点开base.xml

6、客户端与注册中心集成

说实话spring-boot-admin我看过的,基本上都是用在微服务场景比较多,因此后面的内容,我以集成注册中心为核心来讲解示例,通过url配置服务端监控地址就不再论述。

a、 在客户端项目的pom引入eureka-client GAV

 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>

b、 配置eureka 客户端相关信息

eureka:instance:instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${random.uuid}}prefer-ip-address: ${PREFER_IP:false}  #是否选择IP注册#   ip-address: ${IP_ADDRESS:localhost}   #指定IP地址注册lease-renewal-interval-in-seconds: 5  #续约更新时间间隔(默认30秒),使得eureka及时剔除无效服务lease-expiration-duration-in-seconds: 10 #续约到期时间(默认90秒)hostname: ${HOSTNAME:${spring.application.name}}metadata-map:ipAddress: ${spring.cloud.client.ip-address}management:address: ${spring.cloud.client.ip-address}client:service-url:defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}#缩短延迟向服务端注册的时间、默认40sinitial-instance-info-replication-interval-seconds: 10#提高Eureka-Client端拉取Server注册信息的频率,默认30sregistry-fetch-interval-seconds: 5

注: 客户端和服务端集成的eureka地址必须得同一个

客户端和服务端同时配置好注册中心后,我们访问一下服务端监控面板


和用url配置服务端地址的效果一样,到这边大体就差不多了。但是实际使用,没那么简单。我们列举几种场景

场景一:客户端的默认端点不是actuator

因为公司有时候会有等保要求,正常是不能直接暴露actuator端点,所以我们客户端,可能会将端点路径改个名字,比如改成如下

management:endpoints:web:base-path: ${MONINTOR_BASE_PATH:/lyb-geek}exposure:include: "*"

此时通过服务端监控面板访问

会发现爆红了,点击爆红的面板进去

健康检测404,我们可以通过配置注册中心的元数据,示例如下

eureka:instance:metadata-map:management:context-path: ${management.endpoints.web.base-path:/actuator}

此时我们再访问服务端监控面板


发现可以正常访问了。

场景二:客户端的actuator需要认证才能访问

当我们没有通过认证,直接访问服务端监控面板时



会出现401,未授权访问,此时我们在注册中心配置形如下内容

eureka:instance:metadata-map:user.name: ${spring.security.user.name}user.password: ${spring.security.user.password}

访问服务端监控面板

已经可以正常访问

场景三:客户端通过hostName注册到注册中心,服务端监控面板只显示一个实例

这个场景出现在容器化部署,因为此时hostName和port都一样,因此这个客户端就被当成是同一个。此时通过如下配置

eureka:instance:metadata-map:management:address: ${spring.cloud.client.ip-address}

通过配置management.address指定ip

注: 想知道spring-boot-admin可以支持哪些注册中心元数据,可以查看官网
https://docs.spring-boot-admin.com/current/server.html


也看可以查看源码

de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter

如何为spring-boot-admin集成告警

以集成邮件告警为例,在服务端的POM引入邮件发送的GAV

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>

在服务端的application.yml配置邮件发送配置

spring:mail:host: ${MAIL_HOST:邮箱服务器地址}port:username: ${MAIL_USERNAME:邮箱服务器用户名}password: ${MAIL_PWD:邮箱服务器密码}protocol: ${MAIL_PROTOCOL:smtp}default-encoding: UTF-8properties:mail.smtp.auth: truemail.smtp.starttls.enable: truemail.smtp.starttls.required: truemail.smtp.socketFactory.port: ${MAIL_SMTP_SOCKETFACTORY_PORT:465}mail.smtp.socketFactory.class: javax.net.ssl.SSLSocketFactorymail.smtp.socketFactory.fallback: falsemail.smtp.ssl.protocols: ${MAIL_SMTP_SSL_PROTOCOLS:TLSv1}

配置邮件通知接收人和发送人

spring:boot:admin:notify:mail:to: ${NOTIFY_MAIL_TO:邮箱接收人,多个用,隔开}from: ${NOTIFY_MAIL_FROM:邮箱发送人}

当客户端出现异常时,会收到形如下告警

更多告警的玩法可以参考官网
https://docs.spring-boot-admin.com/current/server-notifications.html

总结

spring-boot-admin其实核心就做了一件事,就是把Spring Boot Actuators 可视化。本文就不提供demo了,因为官网文档写得很详细,大部分内容都可以从官网找到https://docs.spring-boot-admin.com/current/。除了那个日志稍微有点坑

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/121785.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kali 安装cpolar内网穿透实现 ssh 远程连接

文章目录 1. 启动kali ssh 服务2. kali 安装cpolar 内网穿透3. 配置kali ssh公网地址4. 远程连接5. 固定连接SSH公网地址6. SSH固定地址连接测试 简单几步通过cpolar 内网穿透软件实现ssh 远程连接kali! 1. 启动kali ssh 服务 默认新安装的kali系统会关闭ssh 连接服务,我们通…

接入 NVIDIA A100、吞吐量提高 10 倍!Milvus GPU 版本使用指南

Milvus 2.3 正式支持 NVIDIA A100&#xff01; 作为为数不多的支持 GPU 的向量数据库产品&#xff0c;Milvus 2.3 在吞吐量和低延迟方面都带来了显著的变化&#xff0c;尤其是与此前的 CPU 版本相比&#xff0c;不仅吞吐量提高了 10 倍&#xff0c;还能将延迟控制在极低的水准。…

【sgLazyTree】自定义组件:动态懒加载el-tree树节点数据,实现增删改、懒加载及局部数据刷新。

特性 可以自定义主键、配置选项支持预定义节点图标&#xff1a;folder文件夹|normal普通样式多个提示文本可以自定义支持动态接口增删改节点 sgLazyTree源码 <template><div :class"$options.name" v-loading"rootLoading"><div class&qu…

我使用的Vim插件

2023年9月5日&#xff0c;周二下午 为了方便以后还原自己的Vim插件配置&#xff0c;于是写这篇博客来记录一下 不定期更新 目录 语法检查Syntastic文件树The NERD tree自动补全括号auto-pairs超轻量级自动补全vim-auto-popmenu 我使用的插件管理器是vim-plug 语法检查Syntas…

Java学习笔记之----I/O(输入/输出)二

【今日】 孩儿立志出乡关&#xff0c;学不成名誓不还。 文件输入/输出流 程序运行期间&#xff0c;大部分数据都在内存中进行操作&#xff0c;当程序结束或关闭时&#xff0c;这些数据将消失。如果需要将数据永久保存&#xff0c;可使用文件输入/输出流与指定的文件建立连接&a…

GPT引领前沿与应用突破之GPT4科研实践技术与AI绘图

GPT对于每个科研人员已经成为不可或缺的辅助工具&#xff0c;不同的研究领域和项目具有不同的需求。如在科研编程、绘图领域&#xff1a;1、编程建议和示例代码: 无论你使用的编程语言是Python、R、MATLAB还是其他语言&#xff0c;都可以为你提供相关的代码示例。2、数据可视化…

操作系统强化认识之Shell编程学习与总结

目录 1.Shell的概述 2.Shell脚本入门 3.变量 3.1.系统预定义变量 3.2.自定义变量 3.3.特殊变量 4.运算符 5.条件判断 6.流程控制 6.1.if判断 6.2.case语句 6.3.for循环 6.4.while循环 7.read读取控制台输入 8.函数 8.1.系统函数 8.2.自定义函数 9.正则表示式入…

Python,如何安装lap,pip安装lap出现问题

Linux可以&#xff1a; pip install cpython pip install gitgit://github.com/gatagat/lap.gitwindows可以&#xff1a; 下载https://github.com/gatagat/lap 后解压&#xff0c; 安装pip install cpython 安装VS2019企业版&#xff1a; key BF8Y8-GN2QH-T84XB-QVY3B-RC4D…

Python 之 match 表达式

Python 从 3.10 版本开始增加了 match 语句&#xff0c;和其他语言常见的 switch 语句极其相似&#xff0c;但功能更加强大。 本文通过实例&#xff0c;了解下其用法。 基本的 match 语句 def http_code(status): match status: case 400 | 404 | 418: …

1801. 积压订单中的订单总数;1567. 乘积为正数的最长子数组长度;923. 三数之和的多种可能

1801. 积压订单中的订单总数 核心思想&#xff1a;维护一个最小堆sell和一个最大堆buy&#xff0c;然后模拟即可。 1567. 乘积为正数的最长子数组长度 核心思想:动态规划&#xff0c;z表示以当前数字num结尾的乘积为正的最长子数组长度&#xff0c;f表示以当前数字num结尾的乘…

C#循环定时上传数据,失败重传解决方案,数据库标识

有些时候我们需要定时的上传一些数据库的数据&#xff0c;在数据不完整的情况下可能上传失败&#xff0c;上传失败后我们需要定时在重新上传失败的数据&#xff0c;该怎么合理的制定解决方案呢&#xff1f;下面一起看一下&#xff1a; 当然本篇文章只是提供一个思路&#xff0…

postman json复杂数据的模拟

先设置路径 然后可以定义下边数据&#xff08;Key value&#xff09; 也可以不定义 看你的情况 [{"mac": "4C-77-66-19-50-65","addressPattern": "98jd","platform": "ios","registrationId": "…

android studio安卓模拟器高德SDK定位网络连接异常

背景 使用了高德SDK创建了一个 project, 下面是运行界面: 点击 "开始定位"按钮, 结果并没有返回定位信息, 而是报错了: 根据错误提示打开这个网址: https://lbs.amap.com/api/android-location-sdk/guide/utilities/errorcode, 并且找到错误码 4 的信息, 显示的是网…

【transformer】自注意力源码解读和复杂度计算

Self-attention A t t e n t i o n ( Q , K , V ) s o f t m a x ( Q K T d k ) V Attention(Q,K,V) softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)softmax(dk​ ​QKT​)V 其中&#xff0c; Q Q Q为查询向量&#xff0c; K K K和 V V V为键向量和值向量&#xff0c;…

油田钻井平台三维应急仿真系统降低事故发生概率

海上钻井是供海上作业人员进行生产作业或者其他活动使用的仿陆地区域&#xff0c;被称为“流动的国土”&#xff0c;主要由上部平台、下浮体(沉垫浮箱)和中部立柱三部分组成&#xff0c;平台上安装钻井、动力、通讯、导航等设备&#xff0c;以及安全救生和人员生活等设施&#…

OB Cloud 初体验

文章来源&#xff1a;韩锋频道 韩锋 数据库行业资深从业者&#xff0c;著有《SQL 优化最佳实践》、《数据库高效优化》等数据库相关著作。 OceanBase&#xff08;下文简称OB&#xff09; 作为国内一款优秀的分布式数据库&#xff0c;这些年来发展很快&#xff0c;在金融、电商…

Java之文件操作与IO

目录 一.认识文件 1.1文件是什么&#xff1f; 1.2文件的组织 1.3文件路径 1.4文件的分类 二.文件操作 2.1File概述 三.文件内容操作--IO 3.1JavaIO的认识 3.2Reader和Writer ⭐Reader类 ⭐Writer类 3.2FileInputStream和FileOutputStream ⭐FileInputStream类 …

继承(个人学习笔记黑马学习)

1、基本语法 #include <iostream> using namespace std; #include <string>//普通实现页面//Java页面 //class Java { //public: // void header() { // cout << "首页、公开课、登录、注册...(公共头部)" << endl; // } // void footer() …

【广州华锐互动】VR全景工厂虚拟导览,虚拟现实技术提升企业数字化信息管理水平

随着工业4.0的到来&#xff0c;VR工厂全景制作成为了越来越多工业企业的选择。传统的工厂管理方式往往存在诸多问题&#xff0c;如信息不对称、安全隐患等。为了解决这些问题&#xff0c;VR工厂全景制作应运而生&#xff0c;它通过结合虚拟现实现实技术和数据采集技术&#xff…

设计模式-迭代器

文章目录 1. 引言1.1 概述1.2 设计模式1.3 迭代器模式的应用场景1.4 迭代器模式的作用 2. 基本概念2.1 迭代器 Iterator2.2 聚合 Aggregate2.3 具体聚合 ConcreteAggregate 3. Java 实现迭代器模式3.1 Java 集合框架3.2 Java 迭代器接口3.3 Java 迭代器模式实现示例 4. 迭代器模…