代码提交

This commit is contained in:
liujing33
2025-05-08 18:02:47 +08:00
parent 55fcc338d7
commit ba04a1047b
60 changed files with 1961 additions and 201 deletions

3
.gitignore vendored
View File

@@ -18,6 +18,9 @@
*.zip *.zip
*.tar.gz *.tar.gz
*.rar *.rar
*.iml
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
.idea
/.idea/

201
LICENSE
View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

66
pom.xml Normal file
View File

@@ -0,0 +1,66 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>learning-nexus</name>
<!-- 🌱 管理子模块 -->
<modules>
<module>reactive-programming</module>
<module>spring-data-jpa-read-write-separation</module>
<module>spring-cloud-demo</module>
<module>swing-and-javafx</module>
<module>websocket</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-boot.version>2.6.7</spring-boot.version>
<spring-cloud.version>2021.0.2</spring-cloud.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<thrift.version>0.16.0</thrift.version>
</properties>
<!-- ✅ 统一管理依赖版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 🔧 插件和版本统一配置 -->
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.mangmang</groupId>
<artifactId>reactive-programming</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
</parent>
<dependencies>
<!-- 确保添加 spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven 插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package org.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Hello world!
*
*/
@SpringBootApplication
public class ReactiveProgrammingApplication {
public static void main(String[] args) {
SpringApplication.run(ReactiveProgrammingApplication.class, args);
}
}

View File

@@ -0,0 +1,12 @@
package org.mangmang.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, Spring Boot!";
}
}

View File

@@ -0,0 +1,74 @@
package org.mangmang.controller;
import org.mangmang.entity.Product;
import org.mangmang.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public Flux<Product> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProductById(@PathVariable String id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@GetMapping("/category/{category}")
public Flux<Product> getProductsByCategory(@PathVariable String category) {
return productService.getProductsByCategory(category);
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Product> streamAllProducts() {
return productService.getAllProducts();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Product> createProduct(@RequestBody Product product) {
return productService.saveProduct(product);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Product>> updateProduct(@PathVariable String id, @RequestBody Product product) {
return productService.getProductById(id)
.flatMap(existingProduct -> {
existingProduct.setName(product.getName());
existingProduct.setPrice(product.getPrice());
existingProduct.setCategory(product.getCategory());
return productService.saveProduct(existingProduct);
})
.map(updatedProduct -> ResponseEntity.ok(updatedProduct))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteProduct(@PathVariable String id) {
return productService.getProductById(id)
.flatMap(existingProduct ->
productService.deleteProduct(id)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))
)
.defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}

View File

@@ -0,0 +1,15 @@
package org.mangmang.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document
public class Product {
@Id
private String id;
private String name;
private double price;
private String category;
}

View File

@@ -0,0 +1,9 @@
package org.mangmang.repository;
import org.mangmang.entity.Product;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Flux;
public interface ProductRepository extends ReactiveMongoRepository<Product, String> {
Flux<Product> findByCategory(String category);
}

View File

@@ -0,0 +1,39 @@
package org.mangmang.service;
import org.mangmang.entity.Product;
import org.mangmang.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Flux<Product> getAllProducts() {
return productRepository.findAll();
}
public Mono<Product> getProductById(String id) {
return productRepository.findById(id);
}
public Flux<Product> getProductsByCategory(String category) {
return productRepository.findByCategory(category);
}
public Mono<Product> saveProduct(Product product) {
return productRepository.save(product);
}
public Mono<Void> deleteProduct(String id) {
return productRepository.deleteById(id);
}
}

View File

@@ -0,0 +1,34 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<name>config-server</name>
<artifactId>config-server</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 配置中心
* 功能:统一管理各服务的配置,支持从 Git 仓库中读取配置文件
*/
@SpringBootApplication
@EnableConfigServer
@EnableEurekaClient
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}

View File

@@ -0,0 +1,27 @@
# 服务器配置
server:
# 服务器端口号设置为8888
port: 8888
# Spring应用配置
spring:
application:
# 应用名称设置为config-server
name: config-server
cloud:
config:
server:
git:
# Git仓库的URI用于存储配置文件
uri: https://github.com/liujing33/MangMang
# 默认的Git分支设置为main
default-label: main
# 搜索路径,使用{application}作为占位符,表示根据应用名称查找配置文件
search-paths: /
# Eureka客户端配置
eureka:
client:
serviceUrl:
# Eureka服务器的默认区域URL设置为本地8761端口
defaultZone: http://localhost:8761/eureka/

View File

@@ -0,0 +1,33 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>eureka-server</artifactId>
<packaging>jar</packaging>
<name>eureka-server</name>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,17 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* 服务注册中心
* 功能:用于服务注册和发现,微服务启动时会向 Eureka 注册自己,并通过 Eureka 发现其他服务
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

View File

@@ -0,0 +1,21 @@
# server: 配置服务器相关的参数
server:
# port: 指定服务器监听的端口号默认值为8761
port: 8761
# spring: 配置Spring应用相关的参数
spring:
application:
# name: 指定Spring应用的名称这里设置为eureka-server
name: eureka-server
# eureka: 配置Eureka服务器和客户端相关的参数
eureka:
client:
# register-with-eureka: 是否将当前实例注册到Eureka服务器false表示不注册
register-with-eureka: false
# fetch-registry: 是否从Eureka服务器获取注册表信息false表示不获取
fetch-registry: false
server:
# wait-time-in-ms-when-sync-empty: 当同步空注册表时的等待时间毫秒0表示不等待
wait-time-in-ms-when-sync-empty: 0

View File

@@ -0,0 +1,46 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<name>gateway-service</name>
<artifactId>gateway-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,17 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* Hello world!
*
*/
@SpringBootApplication
@EnableEurekaClient
public class GatewayServiceApplication {
public static void main( String[] args ) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,49 @@
# 服务器配置
server:
# 服务器端口号设置为8080
port: 8080
# Eureka客户端配置
eureka:
client:
serviceUrl:
# Eureka服务器的默认区域URL指向本地8761端口的Eureka服务
defaultZone: http://localhost:8761/eureka/
# Spring Cloud配置
spring:
cloud:
gateway:
discovery:
locator:
# 启用服务发现定位器,允许通过服务名称进行路由
enabled: true
routes:
# 用户服务路由配置
- id: user-service
# 使用负载均衡方式指向user-service服务
uri: lb://user-service
predicates:
# 匹配路径为/api/users/**的请求
- Path=/api/users/**
filters:
# 去除路径中的第一个前缀
- StripPrefix=1
# 订单服务路由配置
- id: order-service
# 使用负载均衡方式指向order-service服务
uri: lb://order-service
predicates:
# 匹配路径为/api/orders/**的请求
- Path=/api/orders/**
filters:
# 去除路径中的第一个前缀
- StripPrefix=1
# 管理端点配置
management:
endpoints:
web:
exposure:
# 暴露所有管理端点
include: "*"

View File

@@ -0,0 +1,18 @@
# Spring 应用程序配置
spring:
# 应用程序相关配置
application:
# 应用程序名称,用于标识当前服务
name: gateway-service
# Spring Cloud 相关配置
cloud:
# 配置中心相关配置
config:
# 配置中心服务发现相关配置
discovery:
# 是否启用配置中心服务发现true 表示启用
enabled: true
# 配置中心服务的服务ID用于服务发现
service-id: config-server
# 是否在配置中心不可用时快速失败true 表示快速失败
fail-fast: true

View File

@@ -0,0 +1,55 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<name>order-service</name>
<artifactId>order-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,16 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,13 @@
package com.mangmang.orderservice.client;
import com.mangmang.orderservice.client.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}

View File

@@ -0,0 +1,15 @@
package com.mangmang.orderservice.client.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Order {
private Long id;
private Long userId;
private String productName;
private Double price;
}

View File

@@ -0,0 +1,10 @@
package com.mangmang.orderservice.client.model;
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private String email;
}

View File

@@ -0,0 +1,65 @@
package com.mangmang.orderservice.controller;
import com.mangmang.orderservice.client.UserClient;
import com.mangmang.orderservice.client.model.Order;
import com.mangmang.orderservice.client.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/orders")
public class OrderController {
@Value("${server.port}")
private String port;
@Autowired
private UserClient userClient;
private List<Order> orders = new ArrayList<>();
public OrderController() {
orders.add(new Order(1L, 1L, "iPhone 13", 5999.99));
orders.add(new Order(2L, 1L, "MacBook Pro", 12999.99));
orders.add(new Order(3L, 2L, "iPad Pro", 4999.99));
}
@GetMapping
public List<Order> getAllOrders() {
return orders;
}
@GetMapping("/{id}")
public Order getOrderById(@PathVariable Long id) {
return orders.stream()
.filter(order -> order.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Order not found"));
}
@GetMapping("/user/{userId}")
public List<Order> getOrdersByUserId(@PathVariable Long userId) {
// 验证用户是否存在
User user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found");
}
return orders.stream()
.filter(order -> order.getUserId().equals(userId))
.collect(Collectors.toList());
}
@GetMapping("/port")
public String getPort() {
return "Order service running on port: " + port;
}
}

View File

@@ -0,0 +1,19 @@
# server配置块用于定义服务器相关的配置
server:
# 服务器端口号,指定应用程序监听的端口
port: 8082
# eureka配置块用于定义Eureka客户端相关的配置
eureka:
client:
serviceUrl:
# Eureka服务器的默认区域URL指定Eureka服务器的地址
defaultZone: http://localhost:8761/eureka/
# management配置块用于定义管理端点相关的配置
management:
endpoints:
web:
exposure:
# 暴露所有管理端点允许通过Web访问所有管理端点
include: "*"

View File

@@ -0,0 +1,18 @@
# Spring 应用程序配置
spring:
# 应用程序相关配置
application:
# 应用程序名称,用于标识当前服务
name: order-service
# Spring Cloud 相关配置
cloud:
# Spring Cloud Config 配置
config:
# 配置服务发现相关设置
discovery:
# 启用配置服务发现,允许通过服务发现机制查找配置服务器
enabled: true
# 配置服务器的服务ID用于在服务注册中心查找配置服务器
service-id: config-server
# 配置客户端在启动时快速失败,如果无法连接到配置服务器,则应用程序将无法启动
fail-fast: true

34
spring-cloud-demo/pom.xml Normal file
View File

@@ -0,0 +1,34 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
</parent>
<name>spring-cloud-demo</name>
<artifactId>spring-cloud-demo</artifactId>
<packaging>pom</packaging>
<modules>
<module>eureka-server</module>
<module>config-server</module>
<module>user-service</module>
<module>order-service</module>
<module>gateway-service</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,53 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<name>user-service</name>
<artifactId>user-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,16 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 用户服务
*/
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,43 @@
package com.mangmang.controller;
import com.mangmang.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Value("${server.port}")
private String port;
private final List<User> users = Arrays.asList(
new User(1L, "张三", "zhangsan@example.com"),
new User(2L, "李四", "lisi@example.com"),
new User(3L, "王五", "wangwu@example.com")
);
@GetMapping
public List<User> getAllUsers() {
return users;
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("User not found"));
}
@GetMapping("/port")
public String getPort() {
return "User service running on port: " + port;
}
}

View File

@@ -0,0 +1,14 @@
package com.mangmang.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private String email;
}

View File

@@ -0,0 +1,19 @@
# server: 配置服务器相关的设置
server:
# port: 指定服务器监听的端口号默认为8081
port: 8081
# eureka: 配置Eureka客户端相关的设置
eureka:
client:
serviceUrl:
# defaultZone: 指定Eureka服务器的默认注册地址默认为http://localhost:8761/eureka/
defaultZone: http://localhost:8761/eureka/
# management: 配置Spring Boot Actuator相关的设置
management:
endpoints:
web:
exposure:
# include: 指定暴露的Actuator端点*表示暴露所有端点
include: "*"

View File

@@ -0,0 +1,19 @@
# Spring 应用配置
spring:
# 应用名称配置
application:
# 设置应用的名称为 user-service
name: user-service
# Spring Cloud 配置
cloud:
config:
# 配置服务发现相关设置
discovery:
# 启用配置服务发现功能
enabled: true
# 指定配置服务的服务ID为 config-server
service-id: config-server
# 启用快速失败机制,当配置服务不可用时,应用将快速失败而不是等待
fail-fast: true

View File

@@ -0,0 +1,41 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>spring-data-jpa-read-write-separation</artifactId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,12 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class SpringDataJpaReadWriteSeparationApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataJpaReadWriteSeparationApplication.class, args);
}
}

View File

@@ -0,0 +1,5 @@
package com.mangmang.annotaion;
public enum DataSource {
MASTER, SLAVE;
}

View File

@@ -0,0 +1,24 @@
package com.mangmang.annotaion;
import java.lang.annotation.*;
/**
* 数据源切换注解,用于在方法或类级别指定使用的数据源
* 该注解可应用于类或方法上运行时生效并会保留在javadoc文档中
* @see DataSource 数据源类型枚举包含可用数据源定义如MASTER主库、SLAVE从库等
* 参数说明:
* @value 指定目标数据源名称默认使用MASTER主库数据源
* 通过该参数实现数据源动态切换需配合AOP或拦截器实现具体切换逻辑
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
/**
* 数据源类型配置参数
* @return 数据源枚举值,默认返回主库数据源标识
*/
DataSource value() default DataSource.MASTER;
}

View File

@@ -0,0 +1,79 @@
package com.mangmang.config;
import com.mangmang.annotaion.DataSource;
import com.mangmang.annotaion.TargetDataSource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 动态数据源切面类,用于实现基于注解的数据源动态切换功能
*
* 本切面通过拦截带有@TargetDataSource注解的方法在执行目标方法前切换数据源
* 执行完成后自动清理数据源上下文,实现主从库的动态路由
*/
@Slf4j
@Order(1)
@Aspect
@Component
public class DataSourceAspect {
/**
* 定义切点:拦截所有标注@TargetDataSource注解的方法
*
* @Pointcut 使用注解表达式定位需要拦截的方法注解全路径为com.mangmang.annotaion.TargetDataSource
*/
@Pointcut("@annotation(com.mangmang.annotaion.TargetDataSource)")
public void dataSourcePointcut() {
}
/**
* 环绕通知方法,实现数据源的动态切换和清理
*
* @param point 连接点对象,提供被拦截方法的相关信息
* @return Object 目标方法的执行结果
* @throws Throwable 目标方法可能抛出的异常
*
* 执行流程:
* 1. 获取方法签名和注解信息
* 2. 根据注解值设置数据源(无注解时默认主库)
* 3. 执行目标方法
* 4. 清理线程数据源上下文
*/
@Around("dataSourcePointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取方法元数据
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 解析目标数据源注解
TargetDataSource ds = method.getAnnotation(TargetDataSource.class);
// 设置数据源策略:有注解使用注解值,无注解使用默认主库
if (ds == null) {
DynamicDataSource.DynamicDataSourceContextHolder.setDataSource(DataSource.MASTER.name());
log.info("使用主库");
} else {
DynamicDataSource.DynamicDataSourceContextHolder.setDataSource(ds.value().name());
}
try {
// 执行被拦截的目标方法
return point.proceed();
} finally {
// 确保线程数据源上下文清理,避免内存泄漏
DynamicDataSource.DynamicDataSourceContextHolder.clearDataSource();
log.info("清除数据源");
}
}
}

View File

@@ -0,0 +1,75 @@
package com.mangmang.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置类,用于配置主从数据源和动态路由数据源
*/
@Configuration
public class DataSourceConfig {
/**
* 创建主数据源Bean
* @return 配置好的主数据源实例自动绑定spring.datasource.master前缀的配置属性
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 创建从数据源Bean
* @return 配置好的从数据源实例自动绑定spring.datasource.slave前缀的配置属性
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 创建动态数据源路由(主数据源作为默认数据源)
* @param masterDataSource 通过@Qualifier注入的主数据源实例
* @param slaveDataSource 通过@Qualifier注入的从数据源实例
* @return 配置好的动态数据源路由实例
*/
@Bean
@Primary
public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 构建数据源映射表
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(com.mangmang.annotaion.DataSource.MASTER.name(), masterDataSource);
dataSourceMap.put(com.mangmang.annotaion.DataSource.SLAVE.name(), slaveDataSource);
// 设置默认数据源和完整数据源集合
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
/**
* 创建事务管理器
* @param dynamicDataSource 动态数据源路由实例
* @return 配置好的事务管理器实例
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}

View File

@@ -0,0 +1,52 @@
package com.mangmang.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源路由实现类继承Spring框架的AbstractRoutingDataSource
* 通过重写determineCurrentLookupKey方法实现多数据源动态切换
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 获取当前线程绑定的数据源标识
* @return String 数据源标识key对应配置的数据源映射
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
/**
* 数据源上下文持有器(线程安全)
* 使用ThreadLocal实现线程级数据源隔离通过set/get/clear方法管理当前线程数据源标识
*/
public static class DynamicDataSourceContextHolder {
// 线程本地变量存储数据源标识
private final static ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 绑定数据源标识到当前线程
* @param dataSource 数据源标识key
*/
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
/**
* 获取当前线程绑定的数据源标识
* @return String 当前数据源标识可能为null
*/
public static String getDataSource() {
return contextHolder.get();
}
/**
* 清除当前线程的数据源绑定
*/
public static void clearDataSource() {
contextHolder.remove();
}
}
}

View File

@@ -0,0 +1,38 @@
package com.mangmang.controller;
import com.mangmang.entity.User;
import com.mangmang.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.saveUser(user));
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.findAllUsers());
}
}

View File

@@ -0,0 +1,22 @@
package com.mangmang.entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
@Data
@Entity
@Table(name = "users")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String email;
}

View File

@@ -0,0 +1,9 @@
package com.mangmang.repository;
import com.mangmang.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

View File

@@ -0,0 +1,15 @@
package com.mangmang.service;
import com.mangmang.entity.User;
import java.util.List;
import java.util.Optional;
public interface UserService {
User saveUser(User user);
Optional<User> findById(Long id);
List<User> findAllUsers();
}

View File

@@ -0,0 +1,45 @@
package com.mangmang.service.impl;
import com.mangmang.annotaion.DataSource;
import com.mangmang.annotaion.TargetDataSource;
import com.mangmang.entity.User;
import com.mangmang.repository.UserRepository;
import com.mangmang.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional
public User saveUser(User user) {
return userRepository.save(user);
}
@Transactional(readOnly = true)
@TargetDataSource(DataSource.SLAVE)
@Override
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
@Transactional(readOnly = true)
@TargetDataSource(DataSource.SLAVE)
@Override
public List<User> findAllUsers() {
return userRepository.findAll();
}
}

View File

@@ -0,0 +1,19 @@
spring:
datasource:
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
jdbc-url: jdbc:mysql://localhost:3307/slave_db?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect

47
swing-and-javafx/pom.xml Normal file
View File

@@ -0,0 +1,47 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
</parent>
<name>swing-and-javafx</name>
<artifactId>swing-and-javafx</artifactId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>21</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.mangmang.HelloWorldFX</mainClass> <!-- 替换成你的主类 -->
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,29 @@
package com.mangmang;
import javax.swing.*;
import java.awt.*;
/**
* Hello world!
*
*/
public class HelloWordSwing {
public static void main(String[] args) {
JFrame jFrame = new JFrame("Hello World Swing");
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setSize(300, 300);
JButton button = new JButton("点击我");
button.addActionListener(e -> {
JOptionPane.showConfirmDialog(jFrame, "你点击了按钮");
});
jFrame.getContentPane().add(button, BorderLayout.CENTER);
jFrame.setLocationRelativeTo(null);
jFrame.setVisible(true);
}
}

View File

@@ -0,0 +1,28 @@
package com.mangmang;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorldFX extends Application {
@Override
public void start(Stage primaryStage) {
Button btn = new Button("点击我");
btn.setOnAction(e -> System.out.println("你好JavaFX"));
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello JavaFX");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,267 @@
package com.mangmang;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class ImageConverterApp extends Application {
private ImageView imagePreview;
private Label statusLabel;
private Label imageInfoLabel;
private ComboBox<String> formatComboBox;
private TextField outputDirField;
private List<File> selectedFiles;
// 支持的图像格式
private final String[] supportedFormats = {"JPEG", "PNG", "BMP", "GIF", "TIFF"};
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("图像格式转换工具");
// 创建主布局
BorderPane mainLayout = new BorderPane();
mainLayout.setPadding(new Insets(10));
// 创建顶部工具栏
HBox toolbar = createToolbar();
mainLayout.setTop(toolbar);
// 创建中间内容区域
VBox centerContent = createCenterContent();
mainLayout.setCenter(centerContent);
// 创建底部状态栏
HBox statusBar = createStatusBar();
mainLayout.setBottom(statusBar);
// 设置场景
Scene scene = new Scene(mainLayout, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private HBox createToolbar() {
HBox toolbar = new HBox(10);
toolbar.setPadding(new Insets(10));
// 导入按钮
Button importButton = new Button("导入图片");
importButton.setOnAction(e -> importImages());
// 格式选择下拉框
Label formatLabel = new Label("转换格式:");
formatComboBox = new ComboBox<>();
formatComboBox.getItems().addAll(supportedFormats);
formatComboBox.setValue(supportedFormats[0]);
// 输出目录
Label outputDirLabel = new Label("输出目录:");
outputDirField = new TextField(System.getProperty("user.home"));
outputDirField.setPrefWidth(200);
Button browseButton = new Button("浏览...");
browseButton.setOnAction(e -> browseOutputDirectory());
// 转换按钮
Button convertButton = new Button("开始转换");
convertButton.setOnAction(e -> convertImages());
toolbar.getChildren().addAll(
importButton, new Separator(javafx.geometry.Orientation.VERTICAL),
formatLabel, formatComboBox, new Separator(javafx.geometry.Orientation.VERTICAL),
outputDirLabel, outputDirField, browseButton, new Separator(javafx.geometry.Orientation.VERTICAL),
convertButton
);
return toolbar;
}
private VBox createCenterContent() {
VBox centerContent = new VBox(10);
centerContent.setPadding(new Insets(10));
// 图片预览区域
Label previewLabel = new Label("预览:");
imagePreview = new ImageView();
imagePreview.setFitHeight(300);
imagePreview.setFitWidth(400);
imagePreview.setPreserveRatio(true);
// 图片信息标签
imageInfoLabel = new Label("未加载图片");
// 质量设置滑块
HBox qualityBox = new HBox(10);
Label qualityLabel = new Label("图像质量:");
Slider qualitySlider = new Slider(0, 100, 80);
qualitySlider.setShowTickLabels(true);
qualitySlider.setShowTickMarks(true);
qualitySlider.setMajorTickUnit(25);
qualitySlider.setMinorTickCount(5);
Label qualityValueLabel = new Label("80%");
qualitySlider.valueProperty().addListener((obs, oldVal, newVal) -> qualityValueLabel.setText(String.format("%.0f%%", newVal.doubleValue())));
qualityBox.getChildren().addAll(qualityLabel, qualitySlider, qualityValueLabel);
// 创建拖放区域
VBox dropZone = new VBox();
dropZone.getChildren().addAll(imagePreview);
dropZone.setStyle("-fx-border-color: #cccccc; -fx-border-style: dashed; -fx-background-color: #f7f7f7;");
dropZone.setPadding(new Insets(20));
dropZone.setAlignment(javafx.geometry.Pos.CENTER);
// 设置拖放功能
setupDragAndDrop(dropZone);
centerContent.getChildren().addAll(previewLabel, dropZone, imageInfoLabel, qualityBox);
return centerContent;
}
private HBox createStatusBar() {
HBox statusBar = new HBox();
statusBar.setPadding(new Insets(5));
statusBar.setStyle("-fx-background-color: #e0e0e0;");
statusLabel = new Label("就绪");
statusBar.getChildren().add(statusLabel);
return statusBar;
}
private void setupDragAndDrop(VBox dropZone) {
dropZone.setOnDragOver(event -> {
if (event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
}
event.consume();
});
dropZone.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasFiles()) {
selectedFiles = db.getFiles();
success = true;
updatePreview(selectedFiles.get(0));
}
event.setDropCompleted(success);
event.consume();
});
}
private void importImages() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("选择图片文件");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("图像文件", "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tiff")
);
selectedFiles = fileChooser.showOpenMultipleDialog(null);
if (selectedFiles != null && !selectedFiles.isEmpty()) {
updatePreview(selectedFiles.get(0));
statusLabel.setText("已选择 " + selectedFiles.size() + " 个文件");
}
}
private void browseOutputDirectory() {
javafx.stage.DirectoryChooser directoryChooser = new javafx.stage.DirectoryChooser();
directoryChooser.setTitle("选择输出目录");
File selectedDirectory = directoryChooser.showDialog(null);
if (selectedDirectory != null) {
outputDirField.setText(selectedDirectory.getAbsolutePath());
}
}
private void updatePreview(File imageFile) {
try {
Image image = new Image(imageFile.toURI().toString());
imagePreview.setImage(image);
// 更新图像信息
String format = imageFile.getName().substring(imageFile.getName().lastIndexOf('.') + 1).toUpperCase();
imageInfoLabel.setText(String.format(
"文件名: %s\n尺寸: %.0fx%.0f\n格式: %s\n大小: %.2f KB",
imageFile.getName(),
image.getWidth(),
image.getHeight(),
format,
imageFile.length() / 1024.0
));
} catch (Exception e) {
statusLabel.setText("加载图片预览失败: " + e.getMessage());
}
}
private void convertImages() {
if (selectedFiles == null || selectedFiles.isEmpty()) {
showAlert("请先选择图片文件");
return;
}
String targetFormat = formatComboBox.getValue().toLowerCase();
String outputDirectory = outputDirField.getText();
File outputDir = new File(outputDirectory);
if (!outputDir.exists() || !outputDir.isDirectory()) {
if (!outputDir.mkdirs()) {
showAlert("创建输出目录失败");
return;
}
}
int convertedCount = 0;
for (File file : selectedFiles) {
try {
BufferedImage image = ImageIO.read(file);
if (image == null) {
statusLabel.setText("无法读取图片: " + file.getName());
continue;
}
String fileName = file.getName().substring(0, file.getName().lastIndexOf('.'));
File outputFile = new File(outputDir, fileName + "." + targetFormat);
// 对于JPEG格式可以设置质量
ImageIO.write(image, targetFormat, outputFile);
convertedCount++;
} catch (IOException e) {
statusLabel.setText("转换失败: " + e.getMessage());
}
}
statusLabel.setText("转换完成,成功转换 " + convertedCount + " 个文件");
showAlert("转换完成", "成功转换 " + convertedCount + " 个文件到 " + outputDirectory, Alert.AlertType.INFORMATION);
}
private void showAlert(String message) {
showAlert("提示", message, Alert.AlertType.WARNING);
}
private void showAlert(String title, String message, Alert.AlertType alertType) {
Alert alert = new Alert(alertType);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
public static void main(String[] args) {
launch(args);
}
}

31
websocket/pom.xml Normal file
View File

@@ -0,0 +1,31 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>websocket</artifactId>
<packaging>jar</packaging>
<name>websocket</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebSocketDemoApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketDemoApplication.class, args);
}
}

View File

@@ -0,0 +1,27 @@
package com.mangmang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP协议的节点(endpoint)并指定使用SockJS协议
registry.addEndpoint("/ws").setAllowedOrigins("http://10.6.212.39:5173/");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 配置消息代理,前缀为/topic的消息将会被路由到消息代理
registry.enableSimpleBroker("/topic");
// 以/app开头的消息将会被路由到@MessageMapping注解的方法中
registry.setApplicationDestinationPrefixes("/app");
}
}

View File

@@ -0,0 +1,36 @@
package com.mangmang.controller;
import com.mangmang.model.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
import java.util.Objects;
@Slf4j
@Controller
public class MessageController {
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public Message sendMessage(@Payload Message message) {
log.info(message.toString());
return message;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public Message addUser(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) {
log.info(message.toString());
// 将用户名添加到WebSocket会话
Objects.requireNonNull(headerAccessor.getSessionAttributes()).put("username", message.getSender());
return message;
}
}

View File

@@ -0,0 +1,43 @@
package com.mangmang.listener;
import com.mangmang.model.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import java.util.Objects;
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
private final SimpMessageSendingOperations messagingTemplate;
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
// 获取会话属性
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(event.getMessage());
// 从会话中获取用户名
String username = (String) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("username");
if (username != null) {
log.info("用户断开连接: {}", username);
// 创建一个离开消息
Message message = Message.builder()
.type(Message.MessageType.LEAVE)
.sender(username)
.content("下线了")
.build();
// 发送消息到公共主题
messagingTemplate.convertAndSend("/topic/public", message);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.mangmang.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String content;
private String sender;
private MessageType type;
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
}

View File

@@ -0,0 +1,6 @@
server:
port: 8099
# application.yml
logging:
level:
org.springframework.web.socket: DEBUG