• 优秀开源项目解读(六


    一、Piggy Metrics介绍

    PiggyMetrics是一个模拟的个人记账理财的应用,原作者称其为一个端到端的微服务PoC(Proof of Concept),也就是说他开发这个是为了验证微服务架构和Spring Cloud技术栈。PiggyMetrics目前在github上有超过12k星,是学习微服务架构和Spring Cloud技术栈的一个不错参考。

    项目工程结构

    1. ├─account-service # 账户服务
    2. │ │ Dockerfile
    3. │ │ pom.xml
    4. │ │
    5. │ └─src
    6. │ ├─main
    7. │ │ ├─java
    8. │ │ │ └─com
    9. │ │ │ └─piggymetrics
    10. │ │ │ └─account
    11. │ │ │ │ AccountApplication.java
    12. │ │ │ ├─client
    13. │ │ │ │ AuthServiceClient.java
    14. │ │ │ │ StatisticsServiceClient.java
    15. │ │ │ │ StatisticsServiceClientFallback.java
    16. │ │ │ ├─config
    17. │ │ │ │ ResourceServerConfig.java
    18. │ │ │ │
    19. │ │ │ ├─controller
    20. │ │ │ │ AccountController.java
    21. │ │ │ │ ErrorHandler.java
    22. │ │ │ │
    23. │ │ │ ├─domain
    24. │ │ │ │ Account.java
    25. │ │ │ │ Currency.java
    26. │ │ │ │ Item.java
    27. │ │ │ │ Saving.java
    28. │ │ │ │ TimePeriod.java
    29. │ │ │ │ User.java
    30. │ │ │ │
    31. │ │ │ ├─repository
    32. │ │ │ │ AccountRepository.java
    33. │ │ │ └─service
    34. │ │ │ │ AccountService.java
    35. │ │ │ │ AccountServiceImpl.java
    36. │ │ │ │
    37. │ │ │ └─security
    38. │ │ │ CustomUserInfoTokenServices.java
    39. │ │ │
    40. │ │ └─resources
    41. │ │ bootstrap.yml
    42. ├─auth-service # 授权认证服务
    43. │ │ Dockerfile
    44. │ │ pom.xml
    45. │ │
    46. │ └─src
    47. │ ├─main
    48. │ │ ├─java
    49. │ │ │ └─com
    50. │ │ │ └─piggymetrics
    51. │ │ │ └─auth
    52. │ │ │ │ AuthApplication.java
    53. │ │ │ ├─config
    54. │ │ │ │ OAuth2AuthorizationConfig.java
    55. │ │ │ │ WebSecurityConfig.java
    56. │ │ │ ├─controller
    57. │ │ │ │ UserController.java
    58. │ │ │ ├─domain
    59. │ │ │ │ User.java
    60. │ │ │ ├─repository
    61. │ │ │ │ UserRepository.java
    62. │ │ │ └─service
    63. │ │ │ │ UserService.java
    64. │ │ │ │ UserServiceImpl.java
    65. │ │ │ └─security
    66. │ │ │ MongoUserDetailsService.java
    67. │ │ │
    68. │ │ └─resources
    69. │ │ bootstrap.yml
    70. │ │
    71. │ └─
    72. ├─config # 配置中心
    73. │ │ Dockerfile
    74. │ │ pom.xml
    75. │ │
    76. │ └─src
    77. │ └─main
    78. │ ├─java
    79. │ │ └─com
    80. │ │ └─piggymetrics
    81. │ │ └─config
    82. │ │ ConfigApplication.java
    83. │ │ SecurityConfig.java
    84. │ │
    85. │ └─resources
    86. │ │ application.yml
    87. │ │
    88. │ └─shared
    89. │ account-service.yml
    90. │ application.yml
    91. │ auth-service.yml
    92. │ gateway.yml
    93. │ monitoring.yml
    94. │ notification-service.yml
    95. │ registry.yml
    96. │ statistics-service.yml
    97. │ turbine-stream-service.yml
    98. ├─gateway # API网关
    99. │ │ Dockerfile
    100. │ │ pom.xml
    101. │ │
    102. │ └─src
    103. │ ├─main
    104. │ │ ├─java
    105. │ │ │ └─com
    106. │ │ │ └─piggymetrics
    107. │ │ │ └─gateway
    108. │ │ │ GatewayApplication.java
    109. │ │ │
    110. │ │ └─resources
    111. ├─monitoring
    112. │ │ Dockerfile
    113. │ │ pom.xml
    114. │ │
    115. │ └─src
    116. │ ├─main
    117. │ │ ├─java
    118. │ │ │ └─com
    119. │ │ │ └─piggymetrics
    120. │ │ │ └─monitoring
    121. │ │ │ MonitoringApplication.java
    122. │ │ │
    123. │ │ └─resources
    124. │ │ bootstrap.yml
    125. │ │
    126. │ └─test
    127. │ ├─java
    128. │ │ └─com
    129. │ │ └─piggymetrics
    130. │ │ └─monitoring
    131. │ │ MonitoringApplicationTests.java
    132. │ │
    133. │ └─resources
    134. │ bootstrap.yml
    135. ├─notification-service # 通知服务
    136. │ │ Dockerfile
    137. │ │ pom.xml
    138. │ │
    139. │ └─src
    140. │ ├─main
    141. │ │ ├─java
    142. │ │ │ └─com
    143. │ │ │ └─piggymetrics
    144. │ │ │ └─notification
    145. │ │ │ │ NotificationServiceApplication.java
    146. │ │ │ │
    147. │ │ │ ├─client
    148. │ │ │ │ AccountServiceClient.java
    149. │ │ │ │
    150. │ │ │ ├─config
    151. │ │ │ │ ResourceServerConfig.java
    152. │ │ │ │
    153. │ │ │ ├─controller
    154. │ │ │ │ RecipientController.java
    155. │ │ │ │
    156. │ │ │ ├─domain
    157. │ │ │ │ Frequency.java
    158. │ │ │ │ NotificationSettings.java
    159. │ │ │ │ NotificationType.java
    160. │ │ │ │ Recipient.java
    161. │ │ │ │
    162. │ │ │ ├─repository
    163. │ │ │ │ │ RecipientRepository.java
    164. │ │ │ │ │
    165. │ │ │ │ └─converter
    166. │ │ │ │ FrequencyReaderConverter.java
    167. │ │ │ │ FrequencyWriterConverter.java
    168. │ │ │ │
    169. │ │ │ └─service
    170. │ │ │ EmailService.java
    171. │ │ │ EmailServiceImpl.java
    172. │ │ │ NotificationService.java
    173. │ │ │ NotificationServiceImpl.java
    174. │ │ │ RecipientService.java
    175. │ │ │ RecipientServiceImpl.java
    176. │ │ │
    177. │ │ └─resources
    178. │ │ bootstrap.yml
    179. │ │
    180. ├─registry # 注册服务
    181. │ │ Dockerfile
    182. │ │ pom.xml
    183. │ │
    184. │ └─src
    185. │ └─main
    186. │ ├─java
    187. │ │ └─com
    188. │ │ └─piggymetrics
    189. │ │ └─registry
    190. │ │ RegistryApplication.java
    191. │ │
    192. │ └─resources
    193. │ bootstrap.yml
    194. ├─statistics-service # 统计服务
    195. │ │ Dockerfile
    196. │ │ pom.xml
    197. │ │
    198. │ └─src
    199. │ ├─main
    200. │ │ ├─java
    201. │ │ │ └─com
    202. │ │ │ └─piggymetrics
    203. │ │ │ └─statistics
    204. │ │ │ │ StatisticsApplication.java
    205. │ │ │ │
    206. │ │ │ ├─client
    207. │ │ │ │ ExchangeRatesClient.java
    208. │ │ │ │ ExchangeRatesClientFallback.java
    209. │ │ │ │
    210. │ │ │ ├─config
    211. │ │ │ │ ResourceServerConfig.java
    212. │ │ │ │
    213. │ │ │ ├─controller
    214. │ │ │ │ StatisticsController.java
    215. │ │ │ │
    216. │ │ │ ├─domain
    217. │ │ │ │ │ Account.java
    218. │ │ │ │ │ Currency.java
    219. │ │ │ │ │ ExchangeRatesContainer.java
    220. │ │ │ │ │ Item.java
    221. │ │ │ │ │ Saving.java
    222. │ │ │ │ │ TimePeriod.java
    223. │ │ │ │ │
    224. │ │ │ │ └─timeseries
    225. │ │ │ │ DataPoint.java
    226. │ │ │ │ DataPointId.java
    227. │ │ │ │ ItemMetric.java
    228. │ │ │ │ StatisticMetric.java
    229. │ │ │ │
    230. │ │ │ ├─repository
    231. │ │ │ │ │ DataPointRepository.java
    232. │ │ │ │ │
    233. │ │ │ │ └─converter
    234. │ │ │ │ DataPointIdReaderConverter.java
    235. │ │ │ │ DataPointIdWriterConverter.java
    236. │ │ │ │
    237. │ │ │ └─service
    238. │ │ │ │ ExchangeRatesService.java
    239. │ │ │ │ ExchangeRatesServiceImpl.java
    240. │ │ │ │ StatisticsService.java
    241. │ │ │ │ StatisticsServiceImpl.java
    242. │ │ │ │
    243. │ │ │ └─security
    244. │ │ │ CustomUserInfoTokenServices.java
    245. │ │ │
    246. │ │ └─resources
    247. │ │ bootstrap.yml
    248. └─turbine-stream-service # turbine流服务
    249. │ Dockerfile
    250. │ pom.xml
    251. └─src
    252. ├─main
    253. │ ├─java
    254. │ │ └─com
    255. │ │ └─piggymetrics
    256. │ │ └─turbine
    257. │ │ TurbineStreamServiceApplication.java
    258. │ │
    259. │ └─resources
    260. │ bootstrap.yml
    261. └─
    262. 复制代码

    二、Piggy Metrics服务拆分

    技术栈技术栈选型
    API网关Spring Cloud Zuul
    负载均衡Eurake+Ribbon
    远程调用Feign
    限流熔断Hystrix
    服务注册与发现Spring Cloud Eurake
    授权认证Spring Cloud OAuth2
    配置中心Spring Cloud Config&&Apollo
    分布式调用链监控Spring Cloud Sleuth
    链路追踪Zipkin
    消息队列RabbitMQ
    日志监控ELK

    三、Piggy Metrics业架构设计

    目前PiggMetrics采用前后分离架构,前端是单页SPA,后端采用基于Spring Cloud技术栈的微服务架构。

    3.1 业务服务架构

    Piggy Metrics项目主要有三个服务,Account service 账户服务,存储用户账户和记账信息、Statistics service 统计服务,计算用户财务状况和统计信息、Notification service 通知服务,存储通知和备份等相关配置。

    3.2 原基础服务架构

    • API网关: 基于Spring Cloud Zuul的网关,是调用后台API的聚合入口,实现反向路由和负载均衡(Eureka+Ribbon)、限流熔断(Hystrix)等功能。CLIENT单页应用和ZUUL网关暂住在一起,简化部署。
    • 服务注册和发现: 基于Spring Cloud Eureka的服务注册中心。业务服务启动时通过Eureka注册,网关调用后台服务通过Eureka做服务发现,服务之间调用也通过Eureka做服务发现。
    • 授权认证服务: 基于Spring Security OAuth2的授权认证中心。客户端登录时通过AUTHSERVICE获取访问令牌(走用户名密码模式)。服务之间调用也通过AUTHSERVICE获取访问令牌(走客户端模式)。令牌校验方式、各资源服务器去AUTHSERVICE集中校验令牌。
    • 配置服务: 基于Spring Cloud Config的配置中心,集中管理所有Spring服务的配置文件。
    • 分布式调用链: 基于Spring Cloud Sleuth的调用链监控。网关调用后台服务,服务之间调用,都采用Zipkin进行埋点和跟踪。
    • 软负载和限流熔断: 基于Spring Cloud Ribbon&HystrixZuul调用后台服务,服务之间相互调用,都通过Ribbon实现软负载,也通过Hystrix实现熔断限流保护。
    • METRICS & DASHBOARD: 基于Spring Cloud Turbine + Hystrix Dashboard,对所有Hystrix产生的Metrics流进行聚合,并展示在Hystrix Dashboard上。
    • 日志监控: 采用ELK栈集中收集和分析应用日志。

    3.3 优化后的基础服务架构

    上图是经过造后优化后的架构,浅蓝色标注的都属于基础服务,主要替换的组件如下:

    1. 授权认证服务:替换为使用第8模块为课程定制开发的Gravitee OAuth2服务器。
    2. 配置服务:替换为使用携程Apollo做统一配置中心,集中管理所有Spring微服务的配置。
    3. 分布式调用链:替换为使用大众点评开源的CAT做调用链监控,从网关调后台服务,服务之间相互调用,都采用CAT客户端进行埋点监控。CAT埋点既演示使用拦截器(interceptor)方式,也演示使用AOP非侵入方式。
    4. METRICS&ALERTING:网关和微服务都启用Prometheus Metrics端点,便于集成Prometheus监控和告警。

    其它组件,比如Zuul网关、Eureka服务发现、Ribbon软负载、Hystrix限流熔断,以及ELK集中日志都同原架构,没有太大变化。

    技术亮点

    通过Piggy Metrics项目可以从中学习到以下几点:

    1. 如何使用Apollo集中管理Spring应用的配置?
    2. 网关集中验证令牌token怎么实现?
    3. 基于OAuth2的注册登录和API调用具体是如何实现的?
    4. CAT非侵入式埋点怎么做,如何尽量减少业务研发直接使用CAT进行埋点?

    注册登录流程

    上图展示PiggyMetrics的登录注册流程,简化流程如下:

    1. 客户端应用向后台发起注册请求。
    2. 请求通过网关反向路由到账户服务(Account Svc)。
    3. 账户服务先去授权认证服务(Gravitee OAuth2)创建一个用户(包括用户和密码,这样后续才可以登录获取访问令牌)。账户服务再保存新账户信息到本地MongoDB数据库。
    4. 注册成功以后,客户应用向授权认证服务请求访问令牌(走用户名密码模式),拿到令牌以后缓存本地localstorage

    服务调用流程

    上图展示PiggyMetrics的API调用流程,简化流程如下:

    1. 客户端向后台服务发起API调用,调用时在HTTP授权头上带上访问令牌。
    2. 网关截获API请求,根据安全需求判断是否需要验令牌,如果需要,则向授权服务器发起令牌校验请求。授权服务器校验令牌并返回有效型性信息,如果令牌有效,同时返回用户名等相关信息。网关再判断校验是否通过,如果通过,则将用户名以HTTP HEADER方式向后台服务传递,如果不通过,则直接报授权错到客户端。
    3. 资源服务器从HTTP HEADER请求头获取用户名等信息,可通过用户名进一步查询用户相关信息,实现业务逻辑。

    客户端调用后台服务,经过改造为网关集中校验令牌方式,这样可以简化安全架构,即在企业内网,资源服务器端可直接获取用户名信息,不需要再到授权服务器做集中令牌校验。另外,服务之间的调用也改造为可以直接调用,不需要授权认证和令牌,这种做法也是很多一线企业实际落地的做法,即在生产环境中,内部服务之间调用不授权认证,这样可以简化服务的开发和部署,但是对于安全敏感的服务要求做好生产网段隔离(需运维配合)。

    代码解读

    项目采用Spring Security&OAuth2实现用户认证授权,通过实现UserDetailsService接口实现身份认证:

    UserDetailsService

    1. @Service
    2. public class MongoUserDetailsService implements UserDetailsService {
    3. @Autowired
    4. private UserRepository repository;
    5. /**
    6. * 根据用户名获取用户(用户的角色、权限等信息)
    7. *
    8. * @return UserDetails
    9. */
    10. @Override
    11. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    12. return repository.findById(username).orElseThrow(() -> new UsernameNotFoundException(username));
    13. }
    14. }
    15. 复制代码

    WebSecurityConfig配置类

    security的WebSecurityConfigurerAdapter常用于配置任认证管理器配置、核心过滤器配置、安全过滤器链配置, HttpSecurity使用了builder的构建方式来灵活指定访问策略。

    1. /**
    2. * spring security配置
    3. *
    4. * @return UserDetails
    5. */
    6. @Configuration
    7. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    8. /**
    9. * 自定义身份认证逻辑
    10. */
    11. @Autowired
    12. private MongoUserDetailsService userDetailsService;
    13. /**
    14. * anyRequest | 匹配所有请求路径
    15. * access | SpringEl表达式结果为true时可以访问
    16. * anonymous | 匿名可以访问
    17. * denyAll | 用户不能访问
    18. * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
    19. * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
    20. * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
    21. * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
    22. * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
    23. * hasRole | 如果有参数,参数表示角色,则其角色可以访问
    24. * permitAll | 用户可以任意访问
    25. * rememberMe | 允许通过remember-me登录的用户访问
    26. * authenticated | 用户登录后可访问
    27. */
    28. @Override
    29. protected void configure(HttpSecurity http) throws Exception {
    30. http.authorizeRequests().anyRequest()
    31. .authenticated()
    32. .and()
    33. .csrf().disable();
    34. }
    35. @Override
    36. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    37. auth.userDetailsService(userDetailsService)
    38. .passwordEncoder(new BCryptPasswordEncoder());
    39. }
    40. /**
    41. * 解决 无法直接注入 AuthenticationManager
    42. */
    43. @Override
    44. @Bean
    45. public AuthenticationManager authenticationManagerBean() throws Exception {
    46. return super.authenticationManagerBean();
    47. }
    48. }
    49. 复制代码

    AuthorizationServerConfigurer

    AuthorizationServerConfigurer是配置OAuth2授权配置服务的配置类接口,只需要添加@EnableAuthorizationServer,Spring会实现自动注入,接口有三个方法,可实现客户端的配置、安全功能、以及各个Endpoint(端点)的相关配置。

    1. public interface AuthorizationServerConfigurer {
    2. // 配置授权服务器的安全性
    3. void configure(AuthorizationServerSecurityConfigurer security) throws Exception;
    4. //客户端信息配置
    5. void configure(ClientDetailsServiceConfigurer clients) throws Exception;
    6. //配置授权服务器端点的非安全功能,如令牌存储、令牌定制、用户批准和授权类型。默认情况下你不需要做任何事情,除非你需要密码授权,在这种情况下你需要提供一个 {@link AuthenticationManager}。
    7. void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception;
    8. }
    9. 复制代码

    AuthorizationServerConfigurerAdapter

    AuthorizationServerConfigurerAdapter继承了AuthorizationServerConfigurer接口

    1. public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    2. public AuthorizationServerConfigurerAdapter() {
    3. }
    4. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    5. }
    6. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    7. }
    8. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    9. }
    10. }
    11. 复制代码

    PiggyMetrics定义OAuth2AuthorizationConfig

    OAuth2AuthorizationConfigAuthorizationServerConfigurer默认的实现类,它是Spring Security OAuth2授权服务器的默认配置,当没有自定义配置时,将会使用此配置。

    1. EnableAuthorizationServer
    2. public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    3. //保存 OAuth2 token ,有InMemoryTokenStore、JdbcTokenStore、JwkTokenStore、RedisTokenStore
    4. private TokenStore tokenStore = new InMemoryTokenStore();
    5. private final String NOOP_PASSWORD_ENCODE = "{noop}";
    6. @Autowired
    7. @Qualifier("authenticationManagerBean")
    8. private AuthenticationManager authenticationManager;
    9. @Autowired
    10. private MongoUserDetailsService userDetailsService;
    11. @Autowired
    12. private Environment env;
    13. @Override
    14. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    15. // 在内存中创建一个Oauth2 Client
    16. clients.inMemory()
    17. .withClient("browser")
    18. // 授权类型集合
    19. .authorizedGrantTypes("refresh_token", "password")
    20. .scopes("ui")
    21. .and()
    22. .withClient("account-service")
    23. // 密码
    24. .secret(env.getProperty("ACCOUNT_SERVICE_PASSWORD"))
    25. .authorizedGrantTypes("client_credentials", "refresh_token")
    26. .scopes("server")
    27. .and()
    28. .withClient("statistics-service")
    29. .secret(env.getProperty("STATISTICS_SERVICE_PASSWORD"))
    30. .authorizedGrantTypes("client_credentials", "refresh_token")
    31. .scopes("server")
    32. .and()
    33. .withClient("notification-service")
    34. .secret(env.getProperty("NOTIFICATION_SERVICE_PASSWORD"))
    35. .authorizedGrantTypes("client_credentials", "refresh_token")
    36. // 授权范围
    37. .scopes("server");
    38. }
    39. @Override
    40. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    41. // token存储器
    42. endpoints
    43. .tokenStore(tokenStore)
    44. .authenticationManager(authenticationManager)
    45. .userDetailsService(userDetailsService);
    46. }
    47. @Override
    48. public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    49. oauthServer
    50. .tokenKeyAccess("permitAll()")
    51. .checkTokenAccess("isAuthenticated()")
    52. .passwordEncoder(NoOpPasswordEncoder.getInstance());
    53. }
    54. }
    55. 复制代码

    Feign服务调用

    Piggy Metrics在AccountService调用用户服务来创建用户,通过在账户服务定义FeignClient(AuthServiceClient)实现远程调用:

    1. @FeignClient(name = "auth-service")
    2. public interface AuthServiceClient {
    3. @RequestMapping(method = RequestMethod.POST, value = "/uaa/users", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    4. void createUser(User user);
    5. }
    6. 复制代码

    AuthService#UserController开放创建用户的API:

    1. @RestController
    2. @RequestMapping("/users")
    3. public class UserController {
    4. @Autowired
    5. private UserService userService;
    6. @RequestMapping(value = "/current", method = RequestMethod.GET)
    7. public Principal getUser(Principal principal) {
    8. return principal;
    9. }
    10. @PreAuthorize("#oauth2.hasScope('server')")
    11. @RequestMapping(method = RequestMethod.POST)
    12. public void createUser(@Valid @RequestBody User user) {
    13. userService.create(user);
    14. }
    15. }
    16. 复制代码

    Piggy Metrics扩展点

    实际上这个架构进行生产化,仍需做生产化扩展,下面是一些可能的扩展点:

    1. 安全,采用网关集中令牌校验后,内部服务可以直接调用,不需要授权认证,但在生产环境中,特别是对于安全敏感的服务,需要考虑安全增强,例如生产网段隔离IP白名单等机制。
    2. CAT客户端进一步封装,案例演示中为了简化,使用一些手工埋点,但在实际生产中,一般需要有独立框架团队对CAT客户端进行进一步封装,对常用基础组件(服务框架,数据访问层,MVC框架,消息系统,缓存系统等)进行集中埋点,并提供封装好的客户端(最好做到无侵入,可参考Spring Cloud Sleuth Starter埋点方式),方便业务研发团队接入。基本上,框架层集中埋点以后,业务应用只需引入依赖即可,一般不需要再手工埋点。
    3. 用户服务解耦,演示案例中,用户服务(包括用户数据库)和Gravitee OAuth2集成在一起,但实际企业中用户服务可能是独立不耦合的,Gravitee OAuth2可以扩展集成独立用户服务,账户服务也可以集成对接独立用户服务。
    4. 前后分离部署,演示案例中,为简化部署,前端应用和网关住在一起,但在实际生产中,根据企业业务和团队规模,前端应用和后端微服务可能是完全分离部署的,具体做法可参考波波的视频课程。
    5. Gravitee OAuth2,另外Gravitee OAuth2本身也需要扩展 Gravitee OAuth2

    ) - Piggy Metrics微服务项目

  • 相关阅读:
    el-dialog关闭后表单数据缓存没清空【已解决】
    2022CCPC预选赛C Guess(博弈)
    【CSS】CSS实现三角形(一)
    使用.NET简单实现一个Redis的高性能克隆版(七-完结)
    FreeSWITCH添加h264编码及pcap视频提取
    python计算机毕业设计基于django的空闲教室爬虫系统
    再玩玩B端搭建
    javascript基本语法(持续补充)
    Android Studio快速实现Flutter应用的国际化和多语言支持
    AI绘画软件汇总
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127959180