课程简介
总体内容
graph LR 专高四 --> C[Spring Cloud微服务架构*] 专高四 --> T[其它技术] 专高四 --> W[维数公寓] 专高四 --> M[外卖抢单] 专高四 --> Q[企业级测试与解决方案]
Spring Cloud
graph LR C[spring_cloud微服务架构] --注册中心--> Eureka C --负载均衡--> Ribbon C --服务容错--> Hystrix C --声明式远程服务调用--> OpenFeign C --网关--> Gateway C --配置服务--> Config C --消息总线--> Bus C --请求链接追踪--> Sleuth C --状态监控--> Admin C --网关安全--> Security
其它技术
graph LR T[其它技术] T --消息队列--> RocketMQ T --消息队列--> RabbitMQ T --缓存--> Redis T --分布式任务平台--> XXL-Job T --接口测试与调试工具--> PostMan T --压力&&性能--> JMeter
维数公寓
graph LR 维数公寓 --> 合同录入 维数公寓 --> 预约看房 维数公寓 --> 支付中心 外卖服务 --> 外卖抢单
作业:
上传作业内容和效果视频(3分钟左右);
作业检查表:
graph LR 组长 --> 组员 讲师 -- 抽检 --> 学员
参考资料:
时间
作业参考
AOP
概念
- 连接点(Joinpoint): 程序运行中的一个点(方法运行、异常抛出)
- 通知(Advice):(拦截到某连接点后要执行的动作)
- 切点(Pointcut):(选择哪些连接点)
- 切面(Aspect):(切点和通知的组合)
实操
使用熟悉的利用注解定义切点的方法实现。
- 定义注解
@Target(value= ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnno {
String action();
}
- 定义切面、切点
@Aspect
@Component
public class MyAspect {
// 切点; 切面;连接点;
@Pointcut("@annotation(LogAnno)")
public void pointCut() {
}
@Around("pointCut() && @annotation(anno)")
public Object process(ProceedingJoinPoint joinPoint, LogAnno anno) throws Throwable {
//
System.out.println("========================");
Object[] args = joinPoint.getArgs();
for(Object object : args) {
System.out.println(object);
}
System.out.println(anno.action());
System.out.println("========================");
return joinPoint.proceed();
}
}
- 方法上添加注解
@GetMapping("/list") //1, 1,2,3,4 加一条后 1,2,3,4,5
@LogAnno(action = "查询合同") // 1,10 2,10
public Result<IPage<Contract>> list(@RequestParam int page, @RequestParam int size) {
System.out.println("查询列表");
}
一,Eureka介绍
注册服务中心
flowchart TD C[服务消费者] --发现--> E[Eureka] P[服务提供者] --注册 --> E[Eureka]
服务消费者 和 服务提供者 都属于 Eureka客户端。
Eureka 就是Eureka服务端。
作用
- 类似于114查号台的作用, Eureka就是114查号台, 服务提供者就是商家,服务消费者就是别的商家或消费者,服务提供者将信息登记到114,服务消费者会去114查询商家的联系方式。
二,搭建Server端(eureka)
具体步骤如下:
1. 添加依赖
在pom中添加依赖
<!--eureka服务依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2. 按开关
//放在启动类上方
@EnableEurekaServer
3. 加配置
#顶格放
eureka:
instance:
ip-address: 127.0.0.1
prefer-ip-address: true
server:
enable-self-preservation: false #是否开启自我保护,防止网络分区故障时删掉微服务
client:
fetch-registry: false #是否从注册中心获取注册表
register-with-eureka: false #是否要注册到注册中心
二,搭建Client端(house,contract等)
搭建步骤:
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--注册中心(eureka)客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.按开关
//放在启动类上方
@EnableEurekaClient
3.加配置
#顶格放
eureka:
instance:
prefer-ip-address: true # 优先使用ip地址
ip-address: 127.0.0.1 # 使用这个ip
# instance-id: user # 显示实例名称
client:
register-with-eureka: true # 注册到eureka
fetch_registry: true #从服务器获取注册信息
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心地址
三,搭建eureka集群
当只有一个Eureka服务的时候,如果这个服务宕机,就会导致整个系统崩溃,所以需要搭建一个Eureka服务集群,提高服务的稳定性。
1. 添加配置文件
假设我们要创建两个服务的集群,则先创建两个配置文件,如下:
- application-01.yml
- application-02.yml
然后修改配置中的eureka的配置,修改后如下所示
eureka:
instance:
ip-address: 127.0.0.1
prefer-ip-address: true
server:
enable-self-preservation: false #是否开启自我保护,防止网络分区故障时删掉微服务
client:
fetch-registry: true #是否从注册中心获取注册表; 这里需要获取,所以改为true
register-with-eureka: true #是否要注册到注册中心; 需要注册到注册中心,所以改为true
service-url:
defaultZone: http://localhost:8762/eureka/,http://localhost:8761/eureka/ #两个服务互相指向对方
修改idea的运行配置文件
在IDEA中配置启动入口时,指定要使用的配置文件,例如下面的配置指定使用application-01.yml配置文件。
四,用security保护 eureka
任何用户都可以访问Eureka服务会有安全性隐患,所以一般会在Eureka服务端添加安全保护措施。
加依赖
在Eureka服务的pom中添加依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置文件中添加用户名和密码
在Eureka的application.yml文件中添加security配置
spring:
application:
name: eureka
security: # 一定要注意对齐关系,security和application平级
user: # 指定spring security的登录
name: admin
password: 123456
添加安全设置
在Eureka服务中添加以下配置类,放开向eureka的注册请求。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//csrf -> Cross-Site Request Forgery 跨站请求伪造
http.csrf().ignoringAntMatchers("/eureka/**"); //ignore是忽略的意思
super.configure(http);
}
}
Eureka客户端添加认证信息
格式: 用户名:密码@url
defaultZone: http://macro:123456@localhost:8761/eureka # 注册中心地址
注意,无论是我们示例中的house,contract还是eureka服务本身,只要他想向被security保护的eureka服务端注册都需要带上上面的用户名和密码配置。
其它eureka常用配置
eureka:
client:
register-with-eureka: true #是否将自己注册到eureka服务
fetch-registry: true #是否从eureka服务获取注册信息
service-url:
defaultZone: http://localhost:8761/eureka/ #服务地址,逗号分隔
registry-fetch-interval-seconds: 30 #多久去服务端取一次注册信息
instance:
lease-renewal-interval-in-seconds: 30 #续约间隔,秒
lease-expiration-duration-in-seconds: 90 #过期间隔 ,秒
prefer-ip-address: true #优先使用ip地址而非主机名称
ip-address: 127.0.0.1
server:
enable-self-preservaation: false #开启自我保护
### 打开services面板
打开service窗口:
五、常见错误:
- 添加security配置时,security和application没有上下对齐。
一,RestTemplate
使用之前需要先定义一个RestTemplate的实例,方便别的地方注入:
@Configuration
public class MyRestTemplateConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
之后通过如下方式使用:
// 通过ip地址(域名)直接访问
AllType allType = restTemplate.getForObject("http://localhost:8081/type/", AllType.class);
举个例子:
首先定义一个被调用服务入口,这里是 http://localhost:8081/getName/2
@RestController
@RequestMapping("/")
public class HouseController {
@GetMapping("/getName/{id}")
public String getName(@PathVariable int id) {
System.out.println("====================");
return "房屋id="+id;
}
}
然后在定义了RestTemplate的项目中定义对被调用服务的访问:
@RestController
@RequestMapping("/contract")
public class ContractController {
@Autowired
RestTemplate restTemplate;
//http://ip:port/conteact/test-rest?id=1
@RequestMapping("test-rest")
public String tesRest(int id) {
String forObject = restTemplate.getForObject("http://localhost:8081/getName/" + id, String.class);
return forObject;
}
}
二,Ribbon
微服务架构中会有服务实例间的互相调用,实例之间的远程调用可能需要使用RestTemplate来进行。当被调用的服务部署多个实例后,请求需要公平的发送到这些实例(负载均衡),这时 就需要Ribbon来帮助RestTemplate来实现。
Ribbon 负载均衡策略
- com.netflix.loadbalancer.RandomRule //随机
- com.netflix.loadbalancer.RoundRobinRule //轮询
- com.netflix.loadbalancer.RetryRule //轮询的基础上加上重试
- com.netflix.loadbalancer.WeightedResponseTimeRule //轮询的基础上加上响应时间权重
- com.netflix.loadbalancer.BestAvailableRule //并发最小
- com.netflix.loadbalancer.AvailabilityFilteringRule //过滤掉故障实例,并发最小
- com.netflix.loadbalancer.ZoneAwareLoadBalancer //考虑到区域相同
添加ribbon依赖
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
增加被调用服务的实例
上节课已经配置过Eureka集群(高可用),步骤大致如下:
- 增加配置文件,例如:application-02.yml
- 添加运行入口,右上角 Edit Configuration
- 运行入口中指定使用的配置文件 Active Profiles
使用负载均衡
修改RestTemplate实例定义,增加负载均衡能力。
@Configuration
public class MyRestTemplateConfig {
@Bean
@LoadBalanced //为RestTemplate添加负载均衡能力,
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
修改restTemplate对远程服务的访问方式
将原来对某ip地址的访问改成对服务的访问,restTemplate会在运行时将服务名称改为服务名对应的ip地址。
// 接入eureka和ribbon后, RestTemplate在发起调用前会将type替换为真是的服务器地址
AllType allType = restTemplate.getForObject("http://type/type/", AllType.class);
更换ribon负载均衡算法
@Bean
IRule myRule() {
return new RandomRule();
}
上述代码需要放在 @Configuration修饰的类中。
三,Hystrix - 熔断与降级
hystrix介绍 Hystrix的作用:
- 保护线程资源
//信号量限流
@HystrixCommand(
commandProperties= {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE"),
@HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests", value="20")
},
fallbackMethod = "errMethod"
)
//线程池限流
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD")
},
threadPoolKey = "createOrderThreadPool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "20"),
@HystrixProperty(name = "maxQueueSize", value = "100"),
@HystrixProperty(name = "maximumSize", value = "30"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "120")
},
fallbackMethod = "errMethod"
)
-
快速失败机制
-
提供降级方案
//在被调用方的方法上放置
@HystrixCommand(fallbackMethod="耗时方法", commandProperties =
{
{@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="5000")}
})
//客户端配置
feign:
hystrix:
enabled: true
######################配置请求超时时间##########################
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 7000
####################配置具体方法超时时间 为 3 秒########################
DeptHystrixService#deptInfo_Timeout(Integer):
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
- 防止故障扩散
- 监控功能 负责服务的熔断与降级,考虑如下的服务调用:
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
按开关
//二选一
//@EnableCircuitBreaker
@EnableHystrix
HystrixCommand
使用方法
在需要实现服务降级的方法上添加注解 @HystrixCommand . 然后配置 fallbackMethod 属性,其参数为发生异常时要降级到的方法的名称,此降级方法需要与 @HystrixCommand修饰的方法返回类型及参数保持一致。
HystrixCommand参数介绍
- fallbackMethod:指定服务降级处理方法;
- ignoreExceptions:忽略某些异常,不发生服务降级;
fallbackMethod (回调方法)
指定降级到的方法,此方法的返回类型及参数需要与@HystrixCommand修饰的方法返回类型及参数保持一致。
ignoreExceptions (忽略异常)
指定忽略掉某些异常,设置忽略这些异常后,在发生这些异常后不会进行降级处理。
@HystrixCommand(fallbackMethod = "fallback", ignoreExceptions = {NullPointerException.class})
public String getName(@PathVariable int id) {
System.out.println("====================");
if(1==1){
throw new NullPointerException();
}
return "房屋id="+id;
}
//降级方法,返回值及参数需要和上面被HystrixCommand修饰的方法保持一致,方法名就是上面fallbackMethod指定的方法名。
public String fallback(@PathVariable int id) {
return "降级方法id="+id;
}
超时配置
hystrix可以配置是否启用超时,以及超多多久时间视为超时。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 500
timeout:
enabled: true
扩展--熔断状态
graph LR; N[关闭状态]--有异常-->V{验证} V--10S_and_50%-->S[启动状态] S--所有请求直接进入-->F((fallback)) R{重试}--重试失败-->F S --5秒后--> R R---一旦请求成功则关闭熔断--->N
Feign简介
相比RestTemplate,Feign是更好更方便的远程访问工具。
一,基本使用
加依赖
<!-- eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Feign依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
按开关
在入口类上添加注解:
@EnableFeignClients
添加配置
如果没有添加eureka配置的需要添加此配置:
eureka:
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
代码
//调用方接口定义
//这里的name="house"表示被调用方的服务名称
@FeignClient(name="house", path = "house", fallback = HouseFeignServiceFallback.class)
public interface HouseFeignService {
@GetMapping("/{id}")
public House getHouse(@PathVariable int id);
}
//被调用方代码
@RestController
@RequestMapping("/house")
public class HouseWithReqMappingController {
@GetMapping("/{id}")
public House getHouse(@PathVariable int id) {
System.out.println("======================");
House house = new House();
house.setId(1);
house.setName("房屋with_mapping");
return house;
}
}
两者之间的对应关系如下图中红色标识:
二,负载均衡
负载均衡使用ribbon来实现,所以修改负载均衡和上一节ribbon的修改方法一样。
//注意,需要放在被@Configuration修饰的类中
@Bean
public IRule myRule() {
return new RandomRule();
}
三,Feign的服务降级
Feign也支持服务降级,本质上它就是使用上节课学习的Hystrix来实现的。
修改Feign接口
添加降级类,降级类会在发生异常时接管处理。
@FeignClient(name="house", fallback = HouseFeignServiceFallback.class)
新建降级fallback类
降级类要实现上面定义的接口,并实现降级方法,另外注意在降级类上添加@Service注解,以纳入spring管理
@Service
public class OrderServiceCallback implements OrderServiceFeign{
@Override
public Integer getOrderNumberOfUser(Long id) {
return 22;
}
@Override
public Map postOrderNumberOfUser(Long id) {
Map result = new HashMap();
result.put("number",23);
return result;
}
}
改配置
因为feign也是使用hystrix实现的降级,且默认没有打开降级支持,所以这里需要打开hystrix功能:
feign:
hystrix:
enabled: true
四,打开日志
打开Feign调用日志,方便查找问题。
//注意,需要放在被@Configuration修饰的类中
@Bean
Logger.Level feignLogLevel() {
return Logger.Level.FULL;
}
同时,需要配置yml文件中的日志等级。
logging:
level:
org.example.service: debug
五,常用配置
压缩
客户端
feign:
hystrix:
enabled: true
compression:
response:
enabled: true
request:
enabled: true
min-request-size: 2048 # 超过2M则进行压缩
超时配置
在Feign配置超时时,真正的超时降级受两个地方的配置约束:
feign:
client:
config:
default:
read-timeout: 10000 # feign配置的读超时配置
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000 #hystrix配置的超时配置
gateway 简介
如果没有gateway,网页需要分别访问 house contract服务。 各服务还需要分别实现安全,流量控制。
一,基础使用
网关的作用-》 路由; 安全; 流量控制
1.添加依赖
<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>
2.路由配置
2.1 通过配置文件配置路由
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: user
# uri: http://localhost:8083 #直接指定服务器地址,不推荐
uri: lb://user # 指定服务器注册实例名
filters: #在请求被转向后端服务之前可以进行更改。
- StripPrefix=1 # 去掉前缀过滤器
predicates: #断言,只有满足断言的请求才会被放过去。
- Path=/services/** # 如果访问路径以services开头,则执行本路由规则
eureka: # 顶格:另外记得入口类添加@EnableEurekaClient
instance:
ip-address: 127.0.0.1
prefer-ip-address: true
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8761/eureka/
2.2 通过代码配置路由
一般不使用,了解就好。
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
StripPrefixGatewayFilterFactory factory = new StripPrefixGatewayFilterFactory();
StripPrefixGatewayFilterFactory.Config config = new StripPrefixGatewayFilterFactory.Config();
config.setParts(1);
GatewayFilter stripFilter = factory.apply(config);
return builder.routes()
.route("user", r-> r.path("/services/**")
.filters(gatewayFilterSpec -> gatewayFilterSpec.filter(stripFilter))
.uri("lb://user"))
.build();
}
}
2.3 概念解析:
路由中涉及到两个重要的新概念,断言和过滤器。
断言
断言就是判断,用来决定某个请求是不是应该由当前配置中的服务器来处理。我们最常用的就是Path断言,它是根据请求路径进行判断的。
过滤器
过滤器是对将要交给后端服务的请求进行进一步处理的地方。常用的是StripPrefix,一般和Path断言配合使用,用来将为了路由请求添加的路径去掉。
2.4 负载均衡
可以使用ribbon对路由进行负载均衡算法的自定义:
message:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3. 实例讲解
假设我们有house服务,有如下配置和代码:
server:
port: 8081
spring:
application:
name: house
以上配置确定house服务的访问端口是8081。 以下代码决定了访问路径为 /house/list
@RestController
@RequestMapping("/house")
public class HouseController {
@GetMapping("/list")
public Result<IPage<House>> list(@RequestParam Integer page, @RequestParam(required = false) Integer size) {
int _size = size != null ? size : defaultSize;
IPage<House> ipage = new Page<>(page, _size);
houseService.page(ipage);
return Result.ok(ipage);
}
综合以上信息我们可以得出,通过house服务自己的端口访问list的url应该为:
http://localhost:8091/house/list?page=1&size=10
如果要通过网关访问,则再假设我们的gateway网关路由配置如下:
server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: house
# uri: http://localhost:8083 #直接指定服务器地址,不推荐
uri: lb://house # 指定服务器注册实例名
filters: #对请求进行进一步处理
- StripPrefix=1 # 去掉前缀过滤器
predicates: #断言
- Path=/order/** # 如果访问路径以services开头,则执行本路由规则
则最终通过网关的访问路径为
http://localhsot:8080/house/house/list?page=1&size=10
当这样一个请求到达网关时具体的处理流程是怎样的呢?
- 首先经过断言判断,因为请求路径 /house/house/list?page=1&size=10 满足 正则表达式 */house/** *,所以该请求将会被转到house服务处理
- 过滤器StripPrefix=1,表示要将请求路径的前1级路径去掉,则经过处理后请求路径变为 /house/list?page=1&size=10
- 因为已经没有别的过滤器,所以请求被转到house服务
- house服务刚好有处理 house/list?page=1&size=10 请求的方法,则访问成功被处理。
二,Route Predicate 一些常见断言
- Path Route Predicate
- Path=/user/{id} # curl http://localhost:8080/services/another/1
- Method Route Predicate
访问方法断言
- Method=GET # curl http://localhost:8080/services/another/1
- Header Route Predicate
请求头路由
- Header=X-Request-Id,\d+ # curl http://localhost:8080/services/another/1 -H X-Request-Id:88
- Cookie Route Predicate
Cookie路由断言
- Cookie=sessionId,test # curl http://localhost:8080/services/another/1 --cookie sessionId=test
- Query Route Predicate
请求参数路由
- Query=name,pu. # http://localhost:8080/services/another/5?name=pu1 参数以name命名,值以pu开始共三位
- RemoteAddr Route Predicate 远端地址路由断言
- RemoteAddr=192.168.1.1/24
- Host Route Predicate 主机地址路由
- Host=**.baidu.com #
- Before Route Predicate
- Before=2022-10-24T16:30:00+08:00[Asia/Shanghai]
- After Route Predicate
- After=2019-09-24T16:30:00+08:00[Asia/Shanghai]
- Between Route Predicate
- Between=2019-09-24T16:30:00+08:00[Asia/Shanghai],2023-09-24T16:30:00+08:00[Asia/Shanghai]
三,Filter
部分过滤器
- AddRequestParameter
cloud:
gateway:
routes:
- id: user
uri: lb://user
filters:
- StripPrefix=1
- AddRequestParameter=username,bw00
- StripPrefix
cloud:
gateway:
routes:
- id: user
uri: lb://user
filters:
- StripPrefix=1
- PrefixPath
cloud:
gateway:
routes:
- id: user
uri: lb://user
filters:
- StripPrefix=1
- PrefixPath=/another
predicates:
- Path=/services/**
- Hystrix 除了之前Hystrix在单独的微服务中使用以外,Hystrix还可以在网关中使用。先添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
编写降级处理代码:
@RestController
@RequestMapping
public class IndexController {
@GetMapping("/badthinghappend")
public ResponseEntity fallback4Another() {
return ResponseEntity.ok("badthinghappend ");
}
}
然后通过配置确定当异常发生时将请求降级到上面的路径来处理:
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/badthinghappend
- RequestRateLimiter 请求速率限制是一个重要的技能点,需要重点学习下。因为速率限制需要redis记录一些数据,所以需要先添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
然后还得添加redis配置:
spring:
redis:
host: localhost
port: 6379
最后在配置中添加过相关配置
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 放令牌的速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶最大放多少个
redis-rate-limiter.requestedTokens: 20 #每次请求消耗几个令牌(3.0以上支持)
key-resolver: "#{@ipKeyResolver}" # 根据userName来限流
上面的ipKeyResolver会引用项目中定义的实例,该实例需要在项目中定义,如下::
@Configuration
public class RedisRateLimiterConfig {
@Bean
KeyResolver ipKeyResolver() {
return new KeyResolver(){
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostString());
}
};
}
}
四,全局过滤器(Global Filters)
添加global Filter定义
@Slf4j
@Component
public class LogGlobalFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return 10;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
log.info("有人访问了: " + path);
return chain.filter(exchange);
}
}
在网关判断用户是否登录:
@Component
public class JWTFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String currentUrl = exchange.getRequest().getPath().value();
if(!"/user/user/login".equals(currentUrl)) {//登录url不要求用户已经登录
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
if(cookies != null && cookies.getFirst("token") != null) {
HttpCookie token = cookies.getFirst("token"); //获取jwt数据
String jwt = token.getValue();
boolean verify = JWTUtil.verify(jwt, "1234".getBytes()); //校验jwt是否合法
if(!verify) {
return needLogin(exchange);
}
}else {
return needLogin(exchange);
}
}
return chain.filter(exchange);
}
private Mono<Void> needLogin(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return 0;
}
}
五,CORS 配置
注意!!!,在网关添加了跨域后,后面微服务自己添加的跨域需要去掉,否则会出错。
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
allowCredentials: true
maxAge: 360000
六,常见错误
- 在gateway的pom文件或父pom文件中引入web依赖:
<!--gateway中不能引入!-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
一,Spring Cloud Config
示意图
git仓库
示例: config-repo 各位同学要创建自己的repo。
创建步骤简要描述:
- 创建仓库
- 创建文件夹
- 在文件夹中创建 user-dev.yml文件
- 回到仓库首页,点击管理
- 在管理页面下方找到开源选项并选中
- 提交后即可访问
二,Config服务端配置
新建config项目
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
入口类加注解
@EnableConfigServer
配置文件添加
spring:
cloud:
config:
server:
git: #配置存储信息的Git仓库
#username: xxx
#password: 123456
uri: https://gitee.com/nixf/config-repo.git # 仓库地址
clone-on-start: true # 启动时直接从gitee获取数据
search-paths: unit_5 # 搜索目录
default-label: master # 默认的分支
label: master #分支信息
测试
测试config服务器配置是否生效,以如下所示格式访问
http://localhost:8888/application_name/profile/branch_name
application_name: 指应用的注册名 profile: 环境(dev,test,product) branch_name: git分支名
以我们创建git仓库时建的文件user-dev.yml为例,对它的内容访问路径为:
http:localhost:8888/user/dev/master
可以在user-dev.yml中添加内容后再查看效果。
三,客户端配置
添加依赖
<!--此jar包会读取bootstrap.yml,给应用使用config服务的机会 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
新建bootstrap.yml
server:
port: 9092 #修改为你安排的端口
spring:
application:
name: message # 修改为你应用的注册名
cloud:
config:
profile: dev # 指定开发环境,还可以是test product等。
label: master # git分支
uri: http://localhost:8888 #config 服务地址
name: user #当前应用名,用于匹配git内的配置文件
演示对git中配置文件定义的属性的访问。
//(6-1)
@RestController
@RefreshScope
public class ConfigController {
//读取配置中的值,此值只在git内有
@Value("${my-name}")
private String myName;
@GetMapping("/my-name")
public String getConfigName() {
return myName;
}
}
刷新
刷新需要使用actuator的refresh端口。 http://localhost:8888/actuator/refresh
添加actuator依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
同时打开refresh端口
management:
endpoints:
web:
exposure:
include: 'refresh'
也可以打开全部端口:
management:
endpoints:
web:
exposure:
include: '*'
@RefreshScope
注意上面例子(6-1)的@RefreshScope注解,它对于使用自定义变量在通过接口通知后自动刷新本地变量至关重要。
四,config 高可用
配置config高可用后,就不能再指定一个url路径了,需要使用eureka的帮助自动匹配服务地址。
cloud:
config:
profile: dev
lable: master
# uri: http://localhost:8888
name: user
discovery: # 启用自动寻址
enabled: true # 打开为TRUE
service-id: config # config的服务ID--即eureka的注册名
eureka:
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
# instance-id: user
client:
register-with-eureka: true # 注册到eureka
fetch_registry: true #从服务器获取注册信息
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心地址
五,Spring Cloud Bus
Spring Cloud Bus需要使用RabbitMQ帮助。 参见 Rabbit安装
RabbitMQ的相关概念
- Broker 消息队列服务
- Connection publisher 和 consumer 与broker的tcp连接
- Channel 逻辑连接
- Exchange 分发消息 p2p topic multicast/fanout
- Queue 真正的消息队列
- Binding exchange和queue之间的虚拟连接
MQ通用概念:
graph LR Publisher --发送消息--> Broker Broker --推消息--> Receiver Receiver --拉消息--> Broker
Config服务和Config客户端接入Spring Cloud Bus后,我们可以只通知Config服务更新配置,即可实现所有的客户端都自动更新配置的目的。 步骤如下:
config server配置
服务端需要接入rammbitMQ,所以增加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
同时,配置文件中添加rabbitMQ的配置。同时打开bus-refresh端口,通过此端口通知Config服务刷新配置。
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: 'bus-refresh' # config服务端开发端口,通过post访问
config client配置
Config 客户端也需要接入RabbitMQ。添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
添加配置文件:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
测试
更改git的配置后,使用Post方法调用 http://localhost:8888/actuator/bus-refresh 即可更新所有通过bus链接到config服务的配置。
六,Config服务器安全
引进安全jar包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
设置服务的用户名和密码:
spring:
application:
name: config
security:
user:
name: admin
password: 123456
放开刷新接口的访问:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest()
.authenticated()
.and().httpBasic().and().csrf().disable();
}
}
客户端添加安全配置:
cloud:
config: # house-dev.yml
profile: dev # 指定开发环境,还可以是test product等。
label: master # git分支
# uri: http://localhost:8888 #config 服务地址
name: house #当前应用名,用于匹配git内的配置文件
discovery: # 启用自动寻址
enabled: true # 打开为TRUE
service-id: config # config的服务ID--即eureka的注册名
username: admin
password: 123456
使用Postman刷新:
Rabbit 安装
rabbitMQ 是基于Erlang语言开发的,因此在安装rabbit之前需要先安装erlang。 安装时需要注意几点:
1,文件路径不要有中文、空格;
2,计算机名不要有中文
Erlang 安装
官网:https://www.erlang.org/downloads
下载地址: http://erlang.org/download/otp_win32_21.3.exe
配置Erlang环境变量
新建系统变量:
ERLANG_HOME
C:\Program Files\erl-24.2
将bin追加到Path变量中
%ERLANG_HOME%\bin
测试Erlang环境
erl
下载RabbitMQ
rabbitMQ下载: https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.14
安装管理插件:
在rabbit的sbin目录下执行:
rabbitmq-plugins enable rabbitmq_management
安装完成后访问: http://localhost:15672/ 用户名和密码: guest/guest
问题解决
再次确认安装路径中没有空格,中文字符。 再次确认当前用户名不是中文。 确认后进行以下操作。
- 以上操作都按照步骤执行了,还是不能访问。 打开系统服务,找到RabbitMQ,重新启动。
- 还是不行。 执行
rabbitmq-plugins list
查看rabbitmq_management 确实安装了。如果确实安装了 在执行 rabbitmq-service stop rabbitmq-service start
Sleuth 简介
请求追踪,问题(性能,异常)定位
一, 给服务添加链路追踪
加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
添加配置
spring:
zipkin: # 指定zipkin服务器地址
base-url: http://localhost:9411
sleuth:
sampler: # 采样器
probability: 0.1 # 设置Sleuth抽样收集的概率
安装zipkin
主页地址: https://zipkin.io/ 下载后使用如下命令启动zipkin:
java -jar zipkin-server-2.23.19-exec.jar
zipkin概念
- span-跨度-范围
- trace-追踪-痕迹
使用ES持久化存储跟踪信息
- 安装ES
- ES Header chrome 安装 /Volumes/workspace/es-head/es-head
- zipkin启动时传入使用ES的参数
# STORAGE_TYPE:表示存储类型 ES_HOSTS:表示ES的访问地址
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES_HOSTS=localhost:9200
Spring Boot Admin
Spring Boot Admin 通过 Actuator 获得应用运行过程中的各项指标。以图形化界面展示出来。监控以下内容:
- 应用概览信息
- 度量指标信息
- 环境变量信息
- 所有创建的Bean信息
- 查看应用中所有配置信息
- 应用运行日志信息
- 查看JVM信息
- 查看可以访问的Web端点
- 查看HTTP跟踪信息
一 admin-server
新建admin项目,按照以下步骤添加依赖和配置。
1.添加依赖
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.4</version>
</dependency>
2.打开开关
@EnableAdminServer
3.配置
server:
port: 9301
spring:
application:
name: admin-server
访问http://localhost:9301查看效果
二 admin-client
选择(或新建)项目,作为admin client接入admin服务。
1. 引依赖
<!--admin客户端-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.4</version> <!--2.6.2不兼容 -->
</dependency>
<!-- 开启监控接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2. 配置
spring.boot.admin.client.url 告诉admin server地址 management 开启查询端口
spring:
boot:
admin:
client:
url: http://localhost:9301 #配置admin-server地址
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
logging:
file: # 开启admin的日志监控
name: admin-client.log
启动后,再次查看admin服务,可以观察到接入服务的相关信息。
三 演示
- 1 访问监控地址 http://localhost:9301
- 2 点击wallboard按钮,选择要监控的实例查看详情
- 3 度量指标信息 JVM,Tomcat及进程信息
- 4 环境变量信息
- 5 所有的Bean信息
- 6 查看应用中的所有配置信息
- 7 查看日志信息
四 结合注册中心
上面的配置中我们指定了admin服务的地址,这样admin服务地址变更时就比较麻烦,所以我们可以使用eureka来实现动态查找admin服务。
Admin Server的修改
首先需要引入eureka client依赖和添加相应配置。
添加eureka-client
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
打开EurekaClient
@EnableEurekaClient
添加注册中心配置
eureka:
client:
fetch-registry: true #指定是否要从注册中心获取服务(注册中心不需要开启)
register-with-eureka: true #指定是否要注册到注册中心(注册中心不需要开启)
# 需要注意,如果eureka服务开始了security保护,这里需要有用户名和密码
# http://username:password@localhost:8761/eureka/
#集群的配置
#defaultZone: http://node1:8761/eureka,http://node2:8762/eureka,http://node3:8763/eureka
#单机的配置
service-url:
defaultZone: http://localhost:8761/eureka/
Admin Client
management: # Admin 通过 actuator提供的端口进行监控系统,因此需要打开各端口
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
logging:
file: # 开启admin的日志监控
name: admin-client.log
五 添加登陆认证
引入安全依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置用户名和密码
spring:
security:
user:
name: xxx
password: 123456
boot:
admin:
discovery:
ignored-services: ${spring.application.name} # 忽略自己的信息
增加security后,对于没有使用eureka注册中心,直接去通知admin的就 会受到影响,需要对security进行配置,以允许admin-client对admin的请求。
Spring Security
@Configuration
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
private final String adminContextPath;
public SecuritySecureConfig(AdminServerProperties adminServerProperties) {
this.adminContextPath = adminServerProperties.getContextPath();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminContextPath + "/");
http.authorizeRequests()
//1.配置所有静态资源和登录页可以公开访问
.antMatchers(adminContextPath + "/assets/**").permitAll()
.antMatchers(adminContextPath + "/login").permitAll()
.antMatchers(adminContextPath+"/instances").permitAll()
.antMatchers(adminContextPath+"/actuator/**").permitAll()
.anyRequest().authenticated()
.and()
//2.配置登录和登出路径
.formLogin().loginPage(adminContextPath + "/login").successHandler(successHandler).and()
.logout().logoutUrl(adminContextPath + "/logout").and()
//3.开启 http basic 支持,admin-client 注册时需要使用
.httpBasic().and()
.csrf()
//4.开启基于 cookie 的 csrf 保护
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//5.忽略这些路径的 csrf 保护以便 admin-client 注册
.ignoringAntMatchers(adminContextPath + "/instances",adminContextPath + "/actuator/**");
}
}
合同录入
环境复杂度
- [合同编号重复]
Snowflake snowflake = IdUtil.getSnowflake();
String id = "HT"+snowlake.nextId();
- [身份证号码格式不正确]
/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|[xX])$/
- 租客身份证和租客姓名不真实 api服务入口
@PostMapping("/verify")
public Result<Boolean> verify(@RequestBody Contract contract) {
Map<String,Object> map = new HashMap<>();
map.put("idNo", contract.getIdNo());
map.put("name", contract.getName());
String body = HttpRequest.post("https://idenauthen.market.alicloudapi.com/idenAuthentication")
.header("Authorization", "APPCODE b30006ab97e44f33b9398fd90094e676")
.form(map)
.execute()
.body();
System.out.println(body);
int index = body.indexOf("身份证信息匹配");
return Result.ok(index>0);
}
- [计算合同金额精度缺失]
- 主要涉及BigDecimal的计算。
BigDecimal first = BigDecimal.valueOf(10.22);
BigDecimal second = BigDecimal.valueOf(8);
BigDecimal sum = first.add(second);
BigDecimal subtract = first.subtract(second);
BigDecimal subtract2 = second.subtract(first);
BigDecimal multiply = first.multiply(second);
BigDecimal divide = first.divide(second);
boolean bigger = first.compareTo(second) > 0;
boolean equal = first.compareTo(BigDecimal.valueOf(10.220)) == 0;
boolean less = second.compareTo(first) < 0;
- [reddssion 分布式锁]
引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.5</version>
</dependency>
添加配置:
spring:
redis:
redisson:
singleServerConfig:
password: 123456
address: "/redis:127.0.0.1:6379"
使用代码:
@GetMapping("/lock/test")
public boolean lock(int id) {
RLock lock = redissonClient.getLock("test-lock-reddison" + id);
try {
lock.lock(1, TimeUnit.SECONDS);
Thread.sleep(10000);
return true;
}catch (InterruptedException e) {
}finally {
lock.unlock();
}
return false;
}
收房合同录入功能
- 合同信息录入
- 合同录入选择房产
- 附件处理
- 付款单生成
表设计
合同信息录入
合同录入下:
- [房间租赁合同和客户身份证上传失败] 大文件时涉及断点上传
- [网络请求被篡改]
服务端验证请求、计算金额
[应收房款分期计算有误差]
使用BigDecimal [租房合同保存失败,数据不完整] 事务
文件上传 MinIO
安装
参考视频 安装MiniO
启动命令:
minio.exe server D:\tmp\minio
其中 D:\tmp\minio 是图片存储根目录。需要根据自己的情况设置。
添加依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.0.2</version>
</dependency>
上传示例代码
private final String BUCKET_NAME = "2010a";
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws InvalidPortException, InvalidEndpointException, IOException, InvalidBucketNameException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
//改写名字,但是不能改写类型;
String name = file.getOriginalFilename(); // a.jpg;
name = name.substring(name.indexOf(".")); //.jpg
name = IdUtil.getSnowflakeNextIdStr()+name;// 随机数 + .jpg
//修改为自己服务器上的key和secret。
MinioClient minioClient = new MinioClient("http://localhost:9000", "ExgAQimtkr6sKLWW", "OyWuId4T1SqzMjJpZ9u8AQX2YL0Rm8Nq");
PutObjectOptions options = new PutObjectOptions(file.getSize(), 11111111);
options.setContentType(file.getContentType());
minioClient.putObject(BUCKET_NAME, name, file.getInputStream(), options);
String url = "http://localhost:9000/"+BUCKET_NAME+"/"+name;
return Result.ok(url);
}
配置servlet支持大文件
spring:
servlet:
multipart:
enabled: true
max-file-size: 500MB
max-request-size: 2048MB
环境介绍
沙箱环境
在我们的学习,考试中主要使用这个环境的信息,包含appId,支付网关地址,签名等信息。 沙箱环境
文档中心
https://opendocs.alipay.com/open/028r8t?ref=api&scene=22
调试中心
调试中心包含各api的调用例子,使我们开发学习的主要参考: https://open.alipay.com/dev/workspace/apidebug
代码示例
jsp示例,这里包含有文档中心及调试中心都不存在的支付回调验签代码。 支付回调验签
管理平台
正式的开发需要创建应用,并使用它的appId等信息,但是现在不是必须使用它。 创建应用
内网穿透工具
支付宝支付成功后有个回调地址,因为支付宝只能访问外网地址,所以我们或者自己部署到云服务器,或者使用内网穿透工具提供外网地址。 如下是两个(类)常用的穿透工具。
花生壳
需要去线上认证下:
ngrok
版本众多,可用的不多。
开发
依赖
首先需要引进依赖的jar包。
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.33.50.ALL</version>
</dependency>
配置
使用从沙箱环境获得的信息配置AliClient。
@Configuration
public class AlipayConfiguration {
@Bean
public static AlipayClient alipayClient() {
AlipayConfig config = new AlipayConfig();
//支付宝测试环境服务器
config.setServerUrl("https://openapi.alipaydev.com/gateway.do");
//支付宝应用id,在沙箱环境内获取
config.setAppId("2016092100560986");
//引用私钥,建议使用《支付宝开放平台开发助手》生成,RSA2
config.setPrivateKey("MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCLq2XQiqUVyxyO+vT3VIXOhIGWvL1SIs9p28kurzgXzV8JR0X2h+TrSjfyjwYRQH9aV5FFy9NIciA1ce0ZPfxniDt4Bs0pHqLJfkRaUhkotVWn8F6kqqoypy1NfLseDx8GsGXnWVST0wV7xpeOlQM6cflVwYTxx5topyZc5u6NbLssdb/DX1ABjsMsinJQpbgutJFYwy+iWIM7MTSOds4FwZpWdowZ824zfqCMyWibiP1JmguXVcl14eWyOv6LDvDe0niXiF2qMDrTUReVVb+XwEsGb1/cdJT9wU0JDreRH1SSwE6Z+sQdQClfqB/WQJZniveugGf4bXnnZRQfo44fAgMBAAECggEAH7dCHYL+Td6bk5RFQEy/PdA1JSeizh39f4pbOvCrCiymohK/PmZJg2yNG9WCiTReNwOfh3vrdI4F5l0CfDLpOBFlf7H7sJS2Xo7/sormD6pt0v7wXvAqSepQjUH/s6m3X+t6mHhejlri6eKE0+nem8z505FoQQcUsUUTnxEJpq9s1f8SxVF0+6v2va2UqyCbPwKIR0bQLLF7K8a2jl2YBaHRntRi7BCqt4z+IQmHkI6M1zDOysSVbeVX3XlAc54nseorSHAY3ywmjkFSPmBc5MDzNSFgSG2IdHzlTU7R2F4wvExbFGPh64UODzlwcyZFYaTVwUdxhS8JxyXsnHTsQQKBgQDjJs/CMee4AWTN9GNj3XfVW29xjta7XGFJh5BaQRuABpI10nfNyadfwHC7PxjauGqHX2TTpc6991m+kaHOdX5nUyjMOAkhIrmJT1xgPHEjr15Enzp5f8L9NgpMdOJlUne3Uf9cA8pww/lQlEHHcdvoofEYSHoAxMT7wwSut8VdcQKBgQCdaFrfmHtN4AOovtauy3AbdfhhkoyCzN6g/sTDyI4CbLIbMIfqlXTG/RaJYCLVevnmSGCryflDQT69JicSNOVseRZEHli3Qr3G1sS2zU495ZlQGFe6DZomCkw3RxJcX5z25rvvcW3ECDRGpEG17SvMxXWwpD2+0WQL87wEnuwcjwKBgGyak96/SZC6ad3mqNaIftDtxJzAtH4kLwee3y+nzWQqwCEnncwwS+wF8GA2TMXWQmiy/VwL/IrrBmeM7ZXuqx7vraPmbsb++UJjRUFl5JoxMJsSnjyVDz9NZSMlB1F2WnK1q6fs0A+WQ095cvHOyFuzgbggfuR7L8tHdKesiZqhAoGAR4OU3cc6JhxjrTPe95Un/uHvEe1x9y866mw1WznwAvv9Q0seRR7X6lwr9AgAa3sutEgn24SswbiP14HQ+H2dylWNHy+mYMRq0j7bKq5GIOsCZ5hXqwjpAuVk0SxyFBPAjZAwzE19cDXGAl44GH6DisofeTx1bQ9W4/M9dd/6J9MCgYBzW54BbochobX23cFy6QTOQ1zEUGVaabC4rx8/2M9qbORjVWKIcNOLNErm/BmIXkwU/7dPGdkL98poIvYP172obCHdR/vWNEfA9SzTFO2Dkgr7zsJcMM0H/DrUWVx0PFJoQX6uPMnkF71jupqbg0uYtzkVfKVBQ2GA294fWHlvew==");
//json
config.setFormat("json");
//支付宝的公钥,不是咱自己的。沙箱应用处获取
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtovPlNX2W2/vY7e9FCyb7WTeR61VbgScDzDMpRo866u8ucubHgj9HTt4ebsvwsi5d20l1tqT77mEbEwjCqA8CtfeDyATYuHB+P7SWnotjIIXfFfIAgeAqyIl+7TlsBKt13XAVwQh2LqP2xpzqUCHlhJrvQrrWckzliL/SaqfkHbXRPHqN3O4vNORfOyu5FqXxO5oZ8ADebVkouzi6Rv/wMTazERXIwg2YXYjqkOOQzrojKuHUdHDL9vlC3b0ht1z7cUPLdQE86UoOZCj55dLUvjoZsyxKM/tsGjwvmKJGxvUnDDX88KbGexxFpUS8aN2t0OkE2yzGZgbDtAO3pDTuQIDAQAB");
//UTF-8
config.setCharset("UTF-8");
//RSA2
config.setSignType("RSA2");
try {
return new DefaultAlipayClient(config);
} catch (AlipayApiException e) {
throw new RuntimeException(e);
}
}
}
支付:
建议参考如下文档:
https://opendocs.alipay.com/open/028r8t?ref=api&scene=22
或者如下代码
@GetMapping("/pay/{id}")
public String pay(@PathVariable int id) throws AlipayApiException {
Pay pay = payService.getById(id);
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(pay.getUuid());
model.setTotalAmount(pay.getTotal()+"");
model.setSubject("房租");
model.setProductCode("FAST_INSTANT_TRADE_PAY");//固定写死这个
request.setBizModel(model);
request.setReturnUrl("http://localhost:8081/order"); // 前端跳转地址
request.setNotifyUrl("https://6217to9865.goho.co/pay/cj/ali_callback"); //支付宝通过后端回调我们的地址。
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
return response.getBody();
}
打开支付页面
在前端页面打开支付页面时,需要在前端el-button的click事件处理中使用类似如下的代码打开
//order 是要支付的订单
pay(order) {
//以下为新开页面打开
window.open("http://localhost:8080/order/pay/pay/"+order.id);
//以下为原页面打开
//window.location.href="http://localhost:8080/order/pay/pay/"+order.id
}
后端支付回调(简化实现--内网穿透工具可用时)
@RequestMapping("/ali_callback")
public String callback(String out_trade_no) { //OutTradeNo --> out_trade_no
Pay pay = payService.getByUUID(out_trade_no);
pay.setStatus(PayStatus.PAID.name());
boolean success = payService.updateById(pay);
// 成功返回 succes 失败 返回 fail
return success ? "success" : "fail";
}
前端支付回调 (简化实现--内网穿透工具不可用时)
//本实现方式为简化实现方式,仅用于内网穿透不可用时,工作中不可这样使用。
@GetMapping("/callback")
public void callback(@RequestParam String out_trade_no, HttpServletResponse response) throws IOException {
QueryWrapper query = new QueryWrapper();
query.eq("uuid", out_trade_no); //根据传入的参数out_trade_no(数据库中的uuid字段)构建查询
Order order = orderService.getOne(query);
order.setStatus("PAID"); //更新支付单为已支付
orderService.updateById(order);
response.sendRedirect("http://localhost:8081/products"); //跳转页面到目的页面
}
支付回调(完整实现--实际工作时):
@RequestMapping("/ali_callback")
public Object aliCallback(HttpServletRequest request) {
Map<String,String> params = combineRequestParams(request);
try {
boolean verified = AlipaySignature.rsaCheckV2(params, aliProperties.getPublicKey(), "utf-8", aliProperties.getSignType());
if(!verified) {
return ServerResponse.badRequest().body("非法请求,验证不通过");
}
String status = params.get("trade_status");
if("TRADE_SUCCESS".equals(status)) {
String out_trade_no = params.get("out_trade_no");
Pay pay = payService.getByUUID(out_trade_no);
pay.setPayDate(new Date());
pay.setStatus(PayStatus.PAID.name());
payService.saveOrUpdate(pay);
return "success";
}
//确认支付状态是支付成功
//确认价格一致
//需要添加更新订单处理。
return "fail";
} catch (AlipayApiException e) {
log.info("支付宝回调异常", e);
return "fail";
}
}
private static Map<String, String> combineRequestParams(HttpServletRequest request) {
Map<String,String> params = new HashMap<>();
Map requestParams = request.getParameterMap();
for(Iterator it = requestParams.keySet().iterator(); it.hasNext();) {
String name = (String) it.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for(int i=0; i<values.length; i++) {
valueStr = i==(values.length-1) ? valueStr+values[i] : valueStr+values[i]+",";
}
params.put(name, valueStr);
}
params.remove("sign_type");
return params;
}
支付宝支付(下)
扫单:
定时查询未支付单,进行处理:
- 已支付更新状态。
- 未支付视情况通知支付。
定时任务实现:
- 在入口类添加注解:
@EnableScheduling
- 在要执行的方法上添加
@Scheduled(cron = "0 * * * * ? ")
// 秒 分 时 日 月 周
- * 表示所有值。 例如:在分的字段上设置 *,表示每一分钟都会触发。
- ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每 月的 10 号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?” 具体 设置为 0 0 0 10 * ?
- - 表示区间。例如 在小时上设置 “10-12”,表示 10,11,12 点都会触发。
- , 表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五 触发
- / 用于递增触发。如在秒上面设置”5/15” 表示从 5 秒开始,每增 15 秒触发 (5,20,35,50)。 在日字段上设置’1/3’所示每月 1 号开始,每隔三天触发一次。
- L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果 是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L” 前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本 月最后一个星期五”
- W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示 离每月 15 号最近的那个工作日触发。如果 15 号正好是周六,则找最近的周五(14 号)触 发, 如果 15 号是周未,则找最近的下周一(16 号)触发.如果 15 号正好在工作日(周一至周 五),则就在该天触发。如果指定格式为 “1W”,它则表示每月 1 号往后最近的工作日触发。 如果 1 号正是周六,则将在 3 号下周一触发。(注,”W”前只能设置具体的数字,不允许区 间”-“)。
- # 序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周 六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再 合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在 本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即 MON 与 mon 相同。
掉单:
@GetMapping("/query/{id}")
public Result<Boolean> query(@PathVariable int id) throws AlipayApiException, JsonProcessingException {
Pay pay = payService.getById(id);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(pay.getUuid());
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
System.out.println(response.getBody());
Map result = objectMapper.readValue(response.getBody(), Map.class);
result = (Map)result.get("alipay_trade_query_response");
if("TRADE_SUCCESS".equals(result.get("trade_status"))) {
if(PayStatus.PAID.name().equals(pay.getStatus())) {
//不做事情
return Result.ok(true);
}else {
pay.setStatus(PayStatus.PAID.name());
payService.updateById(pay);
return Result.ok(true);
}
}else {
return Result.ok(false);
}
//wqt
}
退款:
public boolean refund(int orderId, BigDecimal amount) throws AlipayApiException {
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(String.valueOf(orderId));
model.setRefundAmount(amount.toPlainString());
request.setBizModel(model);
AlipayTradeRefundResponse response = alipayClient.execute(request);
return response.isSuccess();
}
退款查询:
public boolean checkRefund(int id) {
Pay pay = payService.getById(id);
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
model.setOutRequestNo(pay.getUuid());
request.setBizModel(model);
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
.....
}
下载支付单:
public String billDownload(String date) throws AlipayApiException {
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
AlipayDataDataserviceBillDownloadurlQueryModel model = new AlipayDataDataserviceBillDownloadurlQueryModel();
model.setBillType("trade");
model.setBillDate(date);
request.setBizModel(model);
//String content = "{\"bill_type\":\"trade\" , \"bill_date\":\""+date+"\"}";
//System.out.println(content);
//request.setBizContent(content);
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
System.out.println(response.getBody());
String url = response.getBillDownloadUrl();
return url;
}
功能汇总
定义工具类,完成支付所有功能。(不包含支付后回调)
public class PayUtils {
public static String pay(String out_trade_no, String price, String name, String returnUrl, String notifyUrl) throws AlipayApiException {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(out_trade_no);
model.setTotalAmount(price);
model.setSubject(name);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
request.setReturnUrl(returnUrl);
request.setNotifyUrl(notifyUrl);
AlipayTradePagePayResponse response = alipayClient().pageExecute(request);
return response.getBody();
}
public static boolean check(String out_trade_no) throws AlipayApiException {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(out_trade_no);
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient().execute(request);
return response.isSuccess();
}
public static boolean refund(String out_trade_no, BigDecimal amount) throws AlipayApiException {
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(out_trade_no);
model.setRefundAmount(amount.toPlainString());
request.setBizModel(model);
AlipayTradeRefundResponse response = alipayClient().execute(request);
return response.isSuccess();
}
public static String checkRefund(String out_trade_no) throws AlipayApiException {
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
model.setOutRequestNo(out_trade_no);
request.setBizModel(model);
AlipayTradeFastpayRefundQueryResponse response = alipayClient().execute(request);
return response.getBody();
}
public static String billDownload(String date) throws AlipayApiException {
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
AlipayDataDataserviceBillDownloadurlQueryModel model = new AlipayDataDataserviceBillDownloadurlQueryModel();
model.setBillType("trade");
model.setBillDate(date);
request.setBizModel(model);
//String content = "{\"bill_type\":\"trade\" , \"bill_date\":\""+date+"\"}";
//System.out.println(content);
//request.setBizContent(content);
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient().execute(request);
System.out.println(response.getBody());
String url = response.getBillDownloadUrl();
return url;
}
public static AlipayClient alipayClient() {
AlipayConfig config = new AlipayConfig();
//支付宝测试环境服务器
config.setServerUrl("https://openapi.alipaydev.com/gateway.do");
//支付宝应用id,在沙箱环境内获取
config.setAppId("2016092100560986");
//引用私钥,建议使用《支付宝开放平台开发助手》生成,RSA2
config.setPrivateKey("MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCLq2XQiqUVyxyO+vT3VIXOhIGWvL1SIs9p28kurzgXzV8JR0X2h+TrSjfyjwYRQH9aV5FFy9NIciA1ce0ZPfxniDt4Bs0pHqLJfkRaUhkotVWn8F6kqqoypy1NfLseDx8GsGXnWVST0wV7xpeOlQM6cflVwYTxx5topyZc5u6NbLssdb/DX1ABjsMsinJQpbgutJFYwy+iWIM7MTSOds4FwZpWdowZ824zfqCMyWibiP1JmguXVcl14eWyOv6LDvDe0niXiF2qMDrTUReVVb+XwEsGb1/cdJT9wU0JDreRH1SSwE6Z+sQdQClfqB/WQJZniveugGf4bXnnZRQfo44fAgMBAAECggEAH7dCHYL+Td6bk5RFQEy/PdA1JSeizh39f4pbOvCrCiymohK/PmZJg2yNG9WCiTReNwOfh3vrdI4F5l0CfDLpOBFlf7H7sJS2Xo7/sormD6pt0v7wXvAqSepQjUH/s6m3X+t6mHhejlri6eKE0+nem8z505FoQQcUsUUTnxEJpq9s1f8SxVF0+6v2va2UqyCbPwKIR0bQLLF7K8a2jl2YBaHRntRi7BCqt4z+IQmHkI6M1zDOysSVbeVX3XlAc54nseorSHAY3ywmjkFSPmBc5MDzNSFgSG2IdHzlTU7R2F4wvExbFGPh64UODzlwcyZFYaTVwUdxhS8JxyXsnHTsQQKBgQDjJs/CMee4AWTN9GNj3XfVW29xjta7XGFJh5BaQRuABpI10nfNyadfwHC7PxjauGqHX2TTpc6991m+kaHOdX5nUyjMOAkhIrmJT1xgPHEjr15Enzp5f8L9NgpMdOJlUne3Uf9cA8pww/lQlEHHcdvoofEYSHoAxMT7wwSut8VdcQKBgQCdaFrfmHtN4AOovtauy3AbdfhhkoyCzN6g/sTDyI4CbLIbMIfqlXTG/RaJYCLVevnmSGCryflDQT69JicSNOVseRZEHli3Qr3G1sS2zU495ZlQGFe6DZomCkw3RxJcX5z25rvvcW3ECDRGpEG17SvMxXWwpD2+0WQL87wEnuwcjwKBgGyak96/SZC6ad3mqNaIftDtxJzAtH4kLwee3y+nzWQqwCEnncwwS+wF8GA2TMXWQmiy/VwL/IrrBmeM7ZXuqx7vraPmbsb++UJjRUFl5JoxMJsSnjyVDz9NZSMlB1F2WnK1q6fs0A+WQ095cvHOyFuzgbggfuR7L8tHdKesiZqhAoGAR4OU3cc6JhxjrTPe95Un/uHvEe1x9y866mw1WznwAvv9Q0seRR7X6lwr9AgAa3sutEgn24SswbiP14HQ+H2dylWNHy+mYMRq0j7bKq5GIOsCZ5hXqwjpAuVk0SxyFBPAjZAwzE19cDXGAl44GH6DisofeTx1bQ9W4/M9dd/6J9MCgYBzW54BbochobX23cFy6QTOQ1zEUGVaabC4rx8/2M9qbORjVWKIcNOLNErm/BmIXkwU/7dPGdkL98poIvYP172obCHdR/vWNEfA9SzTFO2Dkgr7zsJcMM0H/DrUWVx0PFJoQX6uPMnkF71jupqbg0uYtzkVfKVBQ2GA294fWHlvew==");
//json
config.setFormat("json");
//支付宝的公钥,不是咱自己的。沙箱应用处获取
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtovPlNX2W2/vY7e9FCyb7WTeR61VbgScDzDMpRo866u8ucubHgj9HTt4ebsvwsi5d20l1tqT77mEbEwjCqA8CtfeDyATYuHB+P7SWnotjIIXfFfIAgeAqyIl+7TlsBKt13XAVwQh2LqP2xpzqUCHlhJrvQrrWckzliL/SaqfkHbXRPHqN3O4vNORfOyu5FqXxO5oZ8ADebVkouzi6Rv/wMTazERXIwg2YXYjqkOOQzrojKuHUdHDL9vlC3b0ht1z7cUPLdQE86UoOZCj55dLUvjoZsyxKM/tsGjwvmKJGxvUnDDX88KbGexxFpUS8aN2t0OkE2yzGZgbDtAO3pDTuQIDAQAB");
//UTF-8
config.setCharset("UTF-8");
//RSA2
config.setSignType("RSA2");
try {
return new DefaultAlipayClient(config);
} catch (AlipayApiException e) {
throw new RuntimeException(e);
}
}
}
看房
-
预约看房业务流程
-
预约看房表结构设计
-
预约看房前后端交互实现
-
短信发送
-
环境复杂度
-
短信通知实现
榛子云
//注册
http://sms_developer.zhenzikj.com/zhenzisms_user/register.html
//登录
http://sms_developer.zhenzikj.com/zhenzisms_user/login.html
//SDK
http://smsow.zhenzikj.com/doc/java_sdk_doc.html
依赖
<dependency>
<groupId>com.zhenzikj</groupId>
<artifactId>zhenzisms</artifactId>
<version>2.0.2</version>
</dependency>
配置
# 榛子云短信
zzy:
url: https://sms_developer.zhenzikj.com
appId: 112605
secret: 6a987156-398b-4c3c-9d84-ce6b3921bd18
代码示例
简单的发送短信:
@GetMapping("/send_sms")
public Boolean sendSMS(String phone) throws Exception {
ZhenziSmsClient client = new ZhenziSmsClient("https://sms_developer.zhenzikj.com", "112605", "6a987156-398b-4c3c-9d84-ce6b3921bd18");
Map<String, Object> params = new HashMap<String, Object>();
params.put("number", phone);
params.put("templateId", "10759");
String[] templateParams = new String[2];
templateParams[0] = "3421";
templateParams[1] = "5";
params.put("templateParams", templateParams);
String result = client.send(params);
System.out.println("sms.result = " + result);
JSON json = JSONUtil.parse(result);
Integer code = (Integer) json.getByPath("code");
return code==0;
}
短信发送失败如何处理
-
找到错误的原因 一般可以根据api调用的返回判断,一般发生错误的原因: 格式出错 违禁词汇 区域受限 技术层面出错 达到限额 余额不足
-
出现错误怎么解决:
自己找不到原因时要寻找客服人员的帮助 为预防错误,多使用短信模板或固定模板。
短信接口异常如何处理
可以针对特定异常或异常吗进行重试 记录日志,后期和运营商沟通.
发送短信失败重试,如果三次后仍不成功则打印日志代码示例:
@GetMapping("/send_sms_retry")
public Result<Boolean> sendSms(String phone) throws Exception {
Boolean success = retrySend(phone,4);
return Result.ok(success);
}
private Boolean retrySend(String phone, int num) {
if(num <=0 ){
log.error("发送短信失败,重试后仍旧失败!" + phone);
return false;
}
ZhenziSmsClient client = new ZhenziSmsClient("https://sms_developer.zhenzikj.com", "112605", "6a987156-398b-4c3c-9d84-ce6b3921bd18");
Map<String, Object> params = new HashMap<String, Object>();
params.put("number", phone);
params.put("templateId", "10759");
String[] templateParams = new String[2];
templateParams[0] = "3421";
templateParams[1] = "5";
params.put("templateParams", templateParams);
String result = null;
try {
result = client.send(params);
} catch (Exception e) {
this.retrySend(phone, num-1);
}
System.out.println("sms.result = " + result);
JSON json = JSONUtil.parse(result);
Integer code = (Integer) json.getByPath("code");
if(code != 0){
this.retrySend(phone, num-1);
}
return code==0;
}
RocketMQ 基础
作用
解耦 异步 削峰
问题
系统可用性降低
如何保证MQ的高可用
系统复杂度提高
怎么保证消息没有被重复消费? 消费者保证。 防止消息丢失?
生产者 -- 使用事务消息
消费者 -- 使用消息确认
broker -- 早写盘,多备份
安装
下载地址:
https://rocketmq.apache.org/download
版本选择 4.9.3
windows版本方法:
修改启动脚本
因为RocketMQ的名称服务器和broker占用内存较大,所以需要将内存调低些。具体是将下面划红线部分替换为:
-server -Xms256m -Xmx256m -Xmn128m
- 修改 runserver.cmd
- 修改 runbroker.cmd
设置环境变量:
在系统环境变量中新增
ROCKETMQ_HOME
指向 RocketMQ的安装目录(bin的上一层目录)
启动
启动名称服务器,先跳转到bin目录
mqnamesrv
启动broker服务器:
mqbroker -n localhost:9876
启动时需要指定名称服务器地址,这个是通过-n来指定的。
测试
测试是为了确保服务器功能正常,测试方法如下:
在命令窗口中先运行 set NAMESRV_ADDR=localhost:9876 以设置环境变量, 然后运行生产者
tools.cmd org.apache.rocketmq.example.quickstart.Producer
和消费者
tools.cmd org.apache.rocketmq.example.quickstart.Consumer
看到N多发送消息和接收消息即是正常。
linux版本方法:
修改启动脚本
将namesrv和broker的内存占用修改为: -server -Xms256m -Xmx256m -Xmn128m
-
修改nameServer的启动脚本 找到choose_gc_options后,在下面一行做如下修改 vi ./bin/runserver.sh
-
修改broker的启动脚本 找到如下图 修改为前面所述的内存大小:
vi ./bin/runbroker.sh
启动
启动名称服务器
nohup sh bin/mqnamesrv &
启动broker服务
nohup sh bin/mqbroker -n localhost:9876 &
测试
export NAMESRV_ADDR=localhost:9876
./bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
./bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer s
开发
应用引入jar包:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
配置rocketMQ相关信息
rocketmq:
name-server: 127.0.0.1:9876 # rocketMQ 名称服务器
producer:
group: test # 发送组
topic: test-topic # topic 相当于一个地址
retry-times-when-send-failed: 5 # 错了以后重试几次
consumer:
group: test-consumer #消费者组
topic: test-topic # 消费topic
消息的发送
同步发送
@PostMapping("/send")
public Map<String,String> send(@RequestParam String msg) {
Message<String> message = MessageBuilder.withPayload(msg).build();
SendResult result = rocketMQTemplate.syncSend(topic,message);
if(SendStatus.SEND_OK.equals(result.getSendStatus())) {
return this.of("status", "ok");
}else {
return this.of("status", "error");
}
}
异步发送
public void asyncSend(String msg) {
Message<String> message = MessageBuilder.withPayload(msg).build();
rocketMQTemplate.asyncSend(topic, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
SendStatus sendStatus = sendResult.getSendStatus();
if(SendStatus.SEND_OK.equals(sendStatus)) {
System.out.println("异步发送消息成功");
}else {
System.out.println("异步发送没有成功。"+sendStatus.name());
}
}
@Override
public void onException(Throwable throwable) {
System.out.println("异步发送消息时发生了错误");
}
});
}
延迟发送
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
@PostMapping("/delay_send")
public Map<String,String> send(@RequestParam String msg) {
Message<String> message = MessageBuilder.withPayload(msg).build();
SendResult result = rocketMQTemplate.syncSend(topic,message,500,5);
if(SendStatus.SEND_OK.equals(result.getSendStatus())) {
return this.of("status", "ok");
}else {
return this.of("status", "error");
}
}
消息的接收
@Service
@RocketMQMessageListener(nameServer = "${rocketmq.name-server}", topic="${rocketmq.producer.topic}", consumerGroup = "${rocketmq.consumer.group}")
public class MQListener implements RocketMQListener<String> {
@Override
public void onMessage(String rocketMqMessage) {
System.out.println("=====I have got a message. = " + rocketMqMessage);
}
}
确保消息不丢失
0.失败重试
rocketmq:
name-server: 127.0.0.1:9876 # rocketMQ 名称服务器
producer:
group: test # 发送组
topic: test-topic # topic 相当于一个地址
retry-times-when-send-failed: 5 # 错了以后重试几次
consumer:
group: test-consumer #消费者组
topic: test-topic # 消费topic
1.发送事务消息
public void sendTransactional(String msg) {
Message message = MessageBuilder.withPayload(msg).build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(topic, message, transactionalListener);
}
@Component
@RocketMQTransactionListener
public class TransactionalListener implements RocketMQLocalTransactionListener {
int times = 0;
@Autowired
UserService userService;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try{
userService.deleteUser(6);
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
// return RocketMQLocalTransactionState.UNKNOWN;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
times ++;
System.out.println("=====第"+times+"次重试。");
if(times > 5) {
System.out.println("重试5次后终于成功");
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.UNKNOWN;
}
}
2. 确认接收
@Component
public class AcknowledgeListener implements MessageListenerOrderly {
@Value("${rocketmq.name-server}")
private String nameServer;
@Value("${rocketmq.producer.topic}")
private String topic;
@Value("${rocketmq.consumer.group}")
private String group;
private DefaultMQPushConsumer consumer;
@PostConstruct
public void init() throws MQClientException {
consumer =new DefaultMQPushConsumer();
consumer.setNamesrvAddr(this.nameServer); //设置RocketMQ NameSrv 地址
consumer.subscribe(topic,"*"); // 订阅指定topic的内容
consumer.setConsumerGroup(group); //设置消费者组
consumer.setInstanceName("another"); //设置实例名,确保不重复
consumer.registerMessageListener(this); //注册消息监听者,这里注册自己为监听者
consumer.start();
}
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
if(CollectionUtils.isEmpty(list)){
return ConsumeOrderlyStatus.SUCCESS;
}
for(MessageExt ext : list) {
String msg = new String(ext.getBody());
processMessage(msg);
}
return ConsumeOrderlyStatus.SUCCESS;
}
private void processMessage(String msg) {
System.out.println("receive "+msg);
}
}
启动不起来
- CLASS_PATH 设置不正确。
服务端配置
- 集群配置
- 主从写同步
- 磁盘写同步
RocketMQ 集群
集群部署:
我们要创建一个2主2从的RocketMQ集群,同时NameServer也创建两个。
windows版本
创建store根路径
D:\workspaces\rocketmq-4.9.3\data\a
D:\workspaces\rocketmq-4.9.3\data\as
D:\workspaces\rocketmq-4.9.3\data\b
D:\workspaces\rocketmq-4.9.3\data\bs
修改broker配置文件
# 集群名称
brokerClusterName=DefaultCluster
# broker名称,Master和Slaver要保持统一
brokerName=broker-a
# 0 表示主; 1表示从
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=SYNC_MASTER
flushDiskType=ASYNC_FLUSH
# 创建topic时缺省创建队列数
defaultTopicQueueNums=4
# 自动创建话题,线上关闭
autoCreateTopicEnable=true
autoCreateSubscriptionGroup=true
# IP1 为主用
brokerIP1=127.0.0.1
# IP2 是从找主用
brokerIP2=127.0.0.1
# 端口号,各实例间端口号差距多些更好
listenPort=10931
# 存储主地址
storePathRootDir=D:/workspaces/rocketmq-4.9.3/data/a
# 名称服务器地址
namesrvAddr=127.0.0.1:9876;127.0.0.1:9870
新建namesrv配置文件 namesrv-1.properties
listenPort=9870
和 namesrv-2.properties。
listenPort=9876
启动集群:
start bin\mqnamesrv.cmd -c conf\2m-2s-sync\namesrv-1.properties
start bin\mqnamesrv.cmd -c conf\2m-2s-sync\namesrv-2.properties
start bin\mqbroker.cmd -c conf\2m-2s-sync\broker-a.properties
start bin\mqbroker.cmd -c conf\2m-2s-sync\broker-a-s.properties
start bin\mqbroker.cmd -c conf\2m-2s-sync\broker-b.properties
start bin\mqbroker.cmd -c conf\2m-2s-sync\broker-b-s.properties
linux版本:
创建store根路径
/home/nixinfeng/workspace/rocketmq-4.9.3/data/am
/home/nixinfeng/workspace/rocketmq-4.9.3/data/as
/home/nixinfeng/workspace/rocketmq-4.9.3/data/bm
/home/nixinfeng/workspace/rocketmq-4.9.3/data/bs
创建配置文件:
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=SYNC_MASTER
flushDiskType=ASYNC_FLUSH
listenPort=10911
storePathRootDir=/home/nixinfeng/workspace/rocketmq-4.9.3/data/am
namesrvAddr=127.0.0.1:9876
启动集群:(需要补上namesrv的启动)
nohup bin/mqbroker -c /home/nixinfeng/workspace/rocketmq-4.9.3/conf/2m-2s-sync/broker-a.properties &
nohup bin/mqbroker -c /home/nixinfeng/workspace/rocketmq-4.9.3/conf/2m-2s-sync/broker-a-s.properties &
nohup bin/mqbroker -c /home/nixinfeng/workspace/rocketmq-4.9.3/conf/2m-2s-sync/broker-b.properties &
nohup bin/mqbroker -c /home/nixinfeng/workspace/rocketmq-4.9.3/conf/2m-2s-sync/broker-b-s.properties &
可能遇到的问题汇总
- 无报错直接退出
- 配置文件路径问题,windows和linux不同。
- 存储数据的路径权限不够
- 端口分配,貌似rocket会占用临近的端口号
xxl-job
下载:
https://gitee.com/xuxueli0323/xxl-job/tags
选择一个双数版本,例如:2.2.0版本。
- 初始化数据库 执行脚本
/xxl-job/doc/db/tables_xxl_job.sql
- 修改配置文件
进入到xxl-job-admin项目中,修改配置文件application.properties(在文件夹xxl-job-admin\src\main\resources下)中的端口号, 数据库用户名及密码也改成本地的用户名和密码; - 编译运行xxl-job-admin
在xxl-job-admin目录中运行下面的命令:
mvn spring-boot:run
- 登录xxl-job-admin 地址 http://localhost:9999/xxl-job-admin/ 的缺省密码
admin/123456
介绍:
主要涉及到两个概念
执行器
执行器用来执行任务的对象XxlJobSpringExecutor,XxlJobSpringExecutor定义在我们自己的项目中,它的名称用来在xxl-job-admin中标识某个项目。
设置执行器
设置任务
任务
任务是真正执行逻辑的地方,它的运营受到xxl-job-admin的指挥。
在项目中使用
引入依赖
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0</version>
</dependency>
添加配置
xxl:
job:
port: 9998 # xxl-job服务本地监听端口,如果有多个需要使用不同的端口
accessToken: ""
admin:
addresses: http://localhost:9999/xxl-job-admin # 这里的端口需要跟上文中在xxl-job-admin中配置的端口号一致
executor: # 执行器,负责和xxl-job-admin通信
appName: contract # 执行器名称,在xxl-job-admin中定义执行器时需要使用这里定义的名字。
logpath: D:\tmp\logs # xxl执行器日志存放地址
logretentiondays: 10 # 日志保留多少天
初始化
在这里使用上面的配合信息,构建一个执行器实例。
@Value("${xxl.job.admin.addresses}")
private String adminAddress;
@Value("${xxl.job.executor.appName}")
private String appName;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRententionDays;
@Value("${xxl.job.port}")
private int port;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(this.adminAddress);
xxlJobSpringExecutor.setAppname(this.appName);
xxlJobSpringExecutor.setLogPath(this.logPath);
xxlJobSpringExecutor.setLogRetentionDays(this.logRententionDays);
xxlJobSpringExecutor.setPort(port);
return xxlJobSpringExecutor;
}
Handler
定时任务中的任务,由它来完成真正的工作。
@XxlJob("payCreatorForOld")
public ReturnT<String> updateOldContract(String param) {
List<Contract> contracts = contractService.getOldContract();
contracts.stream().forEach(contract -> {
try {
log.debug("===================>>>>>>>>>");
rocketMQService.syncSend(contract);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
XxlJobLogger.log("XXl-job for old "+ param);
return ReturnT.SUCCESS;
}
ThreadPool的使用
//初始化线程池
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(2);//线程池最多有2个线程
executor.setCorePoolSize(1);//核心线程数为1
executor.setThreadNamePrefix("*_*");//线程名称前缀
executor.setQueueCapacity(100);//线程池等候队列
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//拒绝策略
executor.initialize();//初始化
return executor;
}
}
测试JMeter
安装
下载地址: https://jmeter.apache.org/download_jmeter.cgi
启动
直接双击bin下的jmeter批处理文件
使用
- 新建线程组,模拟用户访问,线程数及模拟用户数
- 配置线程组
- 配置http请求
- 配置httpGet请求信息
- 配置httpPost请求
- 配置header
- 提交json数据时header的内容
- 动态参数
查看结果
结果聚集:
查看整体测试结果
结果树
查看单个情况
命令行测试
bin\jmeter -n -t github.jmx -l testplan\result.txt -e -o testplan\webreport
-n 在命令行模式运行
-t 测试计划文件名称
-l 将结果记录到那个文件
-e 生成仪表板
-o 报告测试地址
合同录入
设计表结构
- 01.1 合理设计表结构至少包括合同表,合同付款计划表,采用spring cloud搭建微服务架构至少包括注册中心,配置中心,网关,合同服务。(要求表及字段命名规范,注释清楚,满足三大范式要求,可以适当运用反范式设计) ??怎么创建付款计划表
- 02.1 合理设计表结构,用户表,患者表,数据字典表等(要求性别,学历,职业统一用数据字典表处理)
- 03.1 合理设计表结构,用户表,企业表,数据字典表等(要求所属行业:来自数据字典表)
- 04.1 合理设计表结构至少包括合同表,合同付款计划表,采用spring cloud搭建微服务架构至少包括注册中心,配置中心,网关,合同服务(要求:Spring Cloud - Eureka注册中心实现高可用功能)。 ??怎么创建付款计划表
服务搭建
- 01.2 Spring Cloud - Eureka注册中心实现高可用功能
- 01.3 Spring Cloud - Gateway网关路由
- 02.2 搭建spring cloud微服务架构,包括注册中心,配置中心,网关,用户服务,患者服务等.(要求:注册中心实现高可用,服务正常启动)
- 02.3 Spring Cloud - Gateway网关路由(要求:前端对服务访问均通过gateway访问;gateway网关配置路由;Gateway网关解决跨域问题)
- 03.2 搭建spring cloud微服务架构,包括注册中心,配置中心,网关,用户服务,企业服务等(注册中心实现高可用,服务正常启动)
- 03.3 Spring Cloud - Gateway网关路由(前端对服务访问均通过gateway访问;gateway网关配置路由;Gateway网关解决跨域问题)
- 03.4 服务监考(使用Admin查看用户服务的准实时日志,并演示实时调整日志等级)
- 04.2 Spring Cloud - Gateway网关路由(前端对服务访问均通过gateway访问;gateway网关配置路由;Gateway网关解决跨域问题)
- 05.6 Spring Cloud - Eureka注册中心(安装Eureka Server组件,并进行配置;启动Eureka Server并验证服务可用性)
- 05.8 Spring Cloud - Gateway网关路由(前端对用户中心,费用等系统的访问均通过gateway进行)
- 06.4 搭建Eureka服务,并将用户,商品等服务注册到Eureka中
- 06.5 使用Spring Cloud Gateway作为服务网关,所有对接口的调用都需要走网关,正确实现跨域配置
- 07.1 完成Eureka服务的搭建和配置,并将用户中心、服务管理系统接入Eureka
- 07.8 搭建网关服务,并合理配置路由,确保用户服务、服务管理服务等
链路追踪
- 01.4 使用Zipkin作为链路追踪的框架,将所有的应用都集成到Zipkin上,并演示应用之间的调用链路和延迟时间等信息。
- 02.4 使用Zipkin作为链路追踪的框架,将所有的应用都集成到Zipkin上,并演示应用之间的调用链路和延迟时间等信息
- 04.3 使用Zipkin作为链路追踪的框架,将所有的应用都集成到Zipkin上,并演示应用之间的调用链路和延迟时间等信息
- 06.6 使用Zipkin作为链路追踪的框架,将所有的应用都集成到Zipkin上,并演示应用之间的调用链路和延迟时间等信息
合同列表
- 01.5 正确分页,要求能够查看每个合同的付款计划列表。
- 02.6 患者列表正确展示(必须包含患者编号),分页正确
- 03.6 企业列表页面(列表正确展示(必须包含企业编号),分页正确)
- 04.5 合同列表(正确分页;用户登录只能查看自己录入的合同信息,非自己录入的不能查看;要求切换不同用户进行展示)
- 04.6 合同列表-搜索功能(根据合同编号精确查询;根据租客名称模糊查询;根据签约和到期日期精确查询)
- 05.1 费用管理 - 列表展示(按照生成日期降序排序;点击某一行可以进入该费用的详情页)
- 06.1 列表展示 使用Vue和ElementUI实现一个商品列表页面,要求包含商品名称、价格、单位、库存等信息,并支持分页
- 07.3 实现服务管理中的列表页,包括列表展示、搜索、分页等功能(完成根据服务类型(精确查询)和服务名称(模糊查询)的查询)
- 08.2 用户可以输入房屋名称进行搜索,后端根据名称过滤查询数据库中的房屋数据,并返回给前端。(缓存搜索列表首页数据,提高页面的响应能力)
- 10.2 2. 在后端实现分页功能,通过接口参数传递页码和每页显示的数量,后端根据这些参数查询数据库,返回相应的结果给前端。(使用Redis缓存第一页的数据,提升系统性能)
@GetMapping("/page/{page}/{size}")
public Result<IPage<Orders>> page(@PathVariable int page, @PathVariable int size) {
IPage<Orders> ipage = new Page(page, size);
if(page == 1) {
System.out.println("=========第一页");
String key = key(page, size);
IPage<Orders> orders = (IPage<Orders>)redisTemplate.opsForValue().get(key);
if(orders == null) {
System.out.println("第一页走数据库");
ordersService.page(ipage); //从数据库中分页读取数据
redisTemplate.opsForValue().set(key,ipage,30, TimeUnit.MINUTES);
return Result.ok(ipage);
}else {
System.out.println("第一页走缓存");
return Result.ok(orders);
}
}else {
System.out.println("其他页走数据库");
ordersService.page(ipage);
return Result.ok(ipage);
}
}
- 09.2 提供一个房屋列表页面,前端可以通过API接口获取所有房屋的数据,并实现分页功能。(确保大数量的情况下,分页性能不受影响)
格式验证:
- 01.6 合同录入 -完成页面原型前端校验(签订日期小于结束日期;签订日期小于结束日期)
<template>
<div class="about">
<el-form ref="form" :model="contract" :rules="rules">
<el-form-item label="开始时间" prop="start">
<el-date-picker v-model="contract.start" type="date"></el-date-picker>
</el-form-item>
<el-form-item label="开始时间" prop="end">
<el-date-picker v-model="contract.end" type="date"></el-date-picker>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "AboutView",
data() {
let validateStart = (rule, value, callback) => {
if(this.contract.end == null) {
callback();
}
if(value >= this.contract.end ) {
callback(new Error("开始不能大于结束"));
}else {
callback();
}
};
let validateEnd = (rule, value, callback) => {
if(this.contract.start == null) {
callback();
}
if(value <= this.contract.start) {
callback(new Error("结束不能大于开始"));
}else {
callback();
}
}
return {
contract: {
start: null,
end: null
},
rules: {
start: [
{required: true, message: "不能为空", trigger: 'blur'},
{validator: validateStart, trigger: 'blur'}
],
end:[
{required: true, message: "不能为空", trigger: 'blur'},
{validator: validateEnd, trigger: 'blur'}
]
},
}
}
}
</script>
-
01.7 合同录入 -完成页面原型后端业务功能(解决合同编号重复问题;合同业务类型来自redis数据缓存;要考虑表单重复提交的解决方案)
-
02.7 患者添加功能(手机号和证件号码(身份证)进行格式校验;年龄必须在1-120岁区间)
//后端判断年龄
@PostMapping("/testAge")
public Result<Boolean> testAge(@RequestBody Contract contract) {
int age = IdcardUtil.getAgeByIdCard(contract.getIdcard());
if(age>=1 && age<=120) {
return Result.ok(true);
}else {
return Result.ok(false);
}
}
- 03.7 企业添加功能-校验功能(手机号进行格式校验;数据字典表中的数据要求采用redis缓存;企业logo只能上传图片功能)
<el-upload
class="avatar-uploader"
action="https://jsonplaceholder.typicode.com/posts/"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"> //在上传之前做一些处理
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
methods: {
beforeAvatarUpload(file) {
let type = file.type;
let canUpload = type === "image/jpeg" || type === "image/png";
if(!canUpload) {//如果不是图片则提醒用户
this.$message.error("只能上传图片");
}
return canUpload;
}
}
- 04.7 合同录入 -完成页面原型前端校验(租约开始日期小于结束日期;租约信息进行非空校验;提交合同录入后端要对用户是否登录进行校验,如果未登录不准录入合同)
// 参见 01.7 解决方案
- 09.2 完成房屋列表,实现分页和根据名称查找功能
用户登录:
- 02.5 用户登录功能(用户名+密码登录;密码必须采用md5加密技术处理;考虑用户1分钟登录三次不正确进行锁定账号功能)
@PostMapping("/login")
public Result<String> login(@RequestBody User user, HttpServletResponse response) {
//因为要判断某用户某分钟输错了几次,所以key要包含用户名称(这里是邮箱)和分钟数
String key = user.getMail();
Object obj = redisTemplate.opsForValue().get(key);
if(obj != null) {
int times = (int)obj;//检查某用户在这一分钟错误尝试了几次
if(times >= 3) {// 如果大于三次,则报错
return Result.err(-3, "错误尝试次数太多");
}
}
User dbUser = userService.findByMail(user.getMail());
if(dbUser == null) {
return Result.err(-1, "用户不存在");
}
if(dbUser.getPassword().equals(encode(dbUser.getSalt(), user.getPassword()))) {
String token = createToken(dbUser);
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
response.addCookie(cookie); //添加cookie
redisTemplate.opsForValue().set(token, dbUser, 30, TimeUnit.MINUTES);
return Result.ok(token);
}else {
// 密码错误,增加一次
long result = redisTemplate.opsForValue().increment(key);
if(result == 1){// 第一次尝试时设置缓存的生命周期为1分钟。
redisTemplate.expire(key, 1,TimeUnit.MINUTES);
}
return Result.err(-2, "密码不正确");
}
}
- 03.5 用户登录功能(用户名+密码登录;密码必须采用md5加密技术处理;考虑用户登录登录成功后采用jwt生成token返回給前端)
- 04.4 用户登录功能(用户名+密码登录;密码必须采用md5加密技术处理;考虑用户登录登录成功后采用jwt生成token返回給前端)
- 05.4 用户服务 - 用户登录(使用Vue和Element UI实现一个用户登录页面;前后端分离cookie跨域,页面跨域,登录成功,把认证标识写入cookie中)
login() {
this.axios.post("/user/user/login", this.user)
.then(res => {
console.log(res.data);
if(res.data.state === 0) {
document.cookie = "token="+res.data.data; //注意 token后面的'='是必须的!!!!
this.$router.push("/");
}
})
}
为了保证前端带cookie到后端需要确保main.js中有下面这行代码:
//它指示了是否该使用类似 cookie、Authorization 标头或者 TLS 客户端证书等凭据进行跨站点访问控制(Acess-Control)请求
axios.defaults.withCredentials=true;
- 07.2 用户登录功能实现,登录成功写入Cookie(前后端分离cookie跨域,登录成功,把用户信息写入cookie中;将用户信息放入redis中,供后面验证用户是否登录)
- 08.1 用户登录(前后端分离cookie跨域,页面跨域,登录成功后把必要信息写入cookie中)
- 09.1 用户登录,后端在登录成功后,将必要的用户信息(如用户ID)写入cookie中,并设置有效期。(设置Cookie跨域,携带Cookie信息到后台)
let now = new Date();
now.setMinutes(now.getMinutes()+ 30);
document.cookie = "token="+res.data.data+";expires="+now.toUTCString();
- 10.1 用户登录在前后端分离的架构中,使用 JWT(JSON Web Token)实现用户认证和授权。用户登录成功后,后端生成一个 JWT 并返回给前端,前端将 JWT 存储在 Cookie 中(保证JWT和Cookie的过期时间一致,Cookie信息携带到后台进行校验)
合同添加:
//引入Hutool依赖,使用其中的雪花算法实现;同时最好数据库中对应字段加上唯一索引。
@GetMapping("/no")
public Result<String> no() {
String no = IdUtil.getSnowflakeNextIdStr();
return Result.ok(no);
}
- 02.8 患者添加功能(性别,学历,职业统一来自数据字典表,且采用redis缓存;患者添加功能要考虑防止表单重复提交功能;每个患者要求生成一个唯一的患者编号)
- 03.8 企业添加功能(添加功能要考虑防止表单重复提交功能;每个企业要求生成一个唯一的患者编号;添加功能考虑用户是否登录进行校验(可以采用网关过滤器实现))
@Component
public class JWTFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1. 哪些url需要进行安全检查
String currentUrl = exchange.getRequest().getPath().value();
if(!"/user/user/login".equals(currentUrl)) {
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
if(cookies != null && cookies.getFirst("token") != null) {
HttpCookie token = cookies.getFirst("token");
String jwt = token.getValue();
boolean verify = JWTUtil.verify(jwt, "1234".getBytes());
if(!verify) {
return needLogin(exchange);
}
}else {
return needLogin(exchange);
}
}
//2, 对于需要安全检查的url进行检验。
//3,合法通过,不合法拒绝。
return chain.filter(exchange);
}
private Mono<Void> needLogin(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return 0;
}
}
- 04.8 合同录入 -完成页面原型后端业务功能(解决合同编号重复问题;要考虑表单重复提交的解决方案;合同录入成功合同录入人员为当前登陆用户;确保金额精度,使用BigDecimal数据类型)
- 05.2 费用管理 - 添加费用(输入客户名称、生成日期、合同金额等字段,选择费用类型信息,并进行表单验证; 点击添加按钮后,将费用信息提交到后端服务保存,并刷新费用列表页 ;解决合同编号重复问题;后台合同金额使用BigDecimal,防止精度丢失)
- 05.3 费用管理 – 事务 (保存费用时,如果费用类型不存在则添加费用类型后再保存费用,否则直接添加费用即可;添加事务防止费用保存失败,导致数据不完整)
- 05.5 用户服务 - 用户注册(校验密码规则,必须包含数字、字母,长度大于6位)
/(?=.*\d)(?=.*[a-zA-Z]).{6,}/
- 06.2 添加商品功能- 要求用户可以输入商品的名称、价格、单位、库存等信息,并能够上传商品图片(确保价格金额精度,使用BigDecimal数据类型;防止保存失败,数据不完整,配置事务;添加商品编号,防止商品编号重复)
- 06.3 用户服务 - 实现一个简单的用户注册页面,要求用户可以输入用户名、密码等信息,并能够进行表单校验(前后端分离cookie跨域,页面跨域,登录成功,把认证标识写入cookie中)
- 06.8 创建商品时要防止表单重复提交
- 07.4 完成服务管理中的创建功能(适当处理防止重复预约;服务编号使用雪花算法防止重复)
- 08.3 前端发送请求到后端API接口,传递选中的房屋ID等必要信息。 ??
- 08.4 创建租赁订单页面中,可以修改租赁时间区间,选择时间后即时获得租赁价格(价格计算必须是后端计算后返回给前端,主要使用BigDecimal计算)
- 08.6 创建租赁订单需要防止用户手误导致创建重复订单(使用Redis实现表单重复提交,代码逻辑正确)
- 08.7 创建租赁订单时,后端生成唯一的订单号,可以使用UUID或者其他唯一标识符生成算法。
- 09.3 在房屋列表页面中,提供一个按钮或链接,当用户点击选中某个房屋时,触发生成租赁订单的操作
- 09.4 后端根据租赁时间区间和房屋信息进行计算,使用BigDecimal进行精确计算,并返回计算后的租赁价格给前端
- 09.6 后端在接收到创建租赁订单的请求时,先进行重复订单的判断(使用Redis实现表单重复提交,代码逻辑正确)
- 09.7 在创建租赁订单时,后端生成唯一的订单号,可以使用UUID或者其他唯一标识符生成算法。
- 10.3 前端选择房屋并提交订单请求,后端接收请求后先进行用户身份验证(通过校验 JWT),确保是登录用户才能创建订单。(JWT工具类校验Token是否合法,然后在创建订单)
- 10.4 创建租赁订单页面中,可以修改租赁时间区间,选择时间后即时获得租赁价格(价格计算必须是后端计算后返回给前端,主要使用BigDecimal计算)
- 10.6 当用户提交订单时,后端先查询数据库判断该用户是否已经存在相同的订单。如果有重复订单,则返回错误提示给前端,阻止创建重复订单(使用Redis实现表单重复提交,代码逻辑正确) ??
- 10.7 在创建租赁订单时,后端可以生成唯一的订单号,确保订单号的唯一性。
Feign
- 05.7 Spring Cloud - OpenFeign服务调用(添加费用时,远程(OpenFeign)调用用户中心,确认当前操作人已经登录系统:服务间调用配置Hystrix熔断防止系统雪崩)
- 07.5 创建服务时需要远程调用(Feign)用户服务,确保只有登录的用户才可以创建服务.
- 07.6 远程调用实现服务降级,当降级时任何用户都可以创建服务.
- 08.8 创建租赁订单时需要首先远程调用用户服务,确保只有登录用户才可以创建订单(校验用户状态,避免系统攻击)
- 09.8 后端通过远程调用用户服务,验证当前用户是否已登录,可以通过检查cookie中的用户信息来进行验证(校验用户状态,避免系统攻击)
- 10.8 创建租赁订单时,前端携带 JWT 发起请求到用户服务接口,后端通过解析 JWT 获取用户信息,验证用户是否登录。
文件上传
- 06.7 实现一个简单的文件上传功能,要求用户可以选择本地的图片进行上传,并在后台进行保存。(商品图片上传失败,提示“上传失败”)
验证身份:
- 08.5 创建租赁订单中需要填入租赁人的身份证和名称并验证是否真实
- 09.5 前端发送请求到后端API接口,传递身份证和姓名等参数
- 10.5 在创建租赁订单页面中,前端通过表单输入租赁人的身份证和名称等信息
@PostMapping("/verify")
public Result<String> verify(@RequestBody Contract contract) {
Map<String,Object> map = new HashMap<>();
map.put("idNo", contract.getOwnerNo());
map.put("name", contract.getOwnerName());
String body = HttpRequest.post("https://idenauthen.market.alicloudapi.com/idenAuthentication")
.header("Authorization", "APPCODE 你的appcode")
.form(map)
.execute()
.body();
System.out.println(body);
return Result.ok(body);
}
Admin
- 07.7 使用Admin查看用户服务的准实时日志,并演示实时调整日志等级.
总体其他:
- 01.8 合同录入 – 事务(合同录入提交后,要求根据分期付款方式完成付款计划的录入;确保付款金额精度,使用BigDecimal数据类型)
支付中心
订单列表:
- 11.1 页面展示:订单列表页面展示正常,包含金额字段和支付状态,使用element-ui实现
- 12.1 完成页面开发:包含必要字段:金额字段和支付状态(设计订单表,满足三大范式和业务要求,模拟模拟数据要尽量真实。)
- 13.1 数据库设计和页面开发:订单信息表结构设计规范,完成服务列表页面开发(设计订单表,满足三大范式和业务要求,模拟模拟数据要尽量真实。)
- 14.1 支付页面设计符合用户习惯,能快速完成支付操作
- 15.1 设计订单表,满足三大范式和业务要求,并使用真实数据进行模拟.
- 16.1 使用Java编程语言生成虚拟订单数据,包括订单号、商品信息、金额等。(设计订单表,满足三大范式和业务要求,模拟模拟数据要尽量真实。)
- 17.1 可以使用随机数或者测试框架来生成模拟数据,确保数据的真实性和合理性
- 18.1 使用Java编程语言生成虚拟订单数据,可以使用随机数或测试框架来生成模拟数据(设计订单表,满足三大范式和业务要求,模拟模拟数据要尽量真实)
- 19.1 设计订单表,模拟数据。(设计订单表,满足三大范式和业务要求,模拟模拟数据要尽量真实)
- 20.1 设计订单表,模拟数据 (设计订单表,满足三大范式和业务要求,模拟模拟数据要尽量真实。)
支付宝集成:
- 11.2 支付宝SDK集成:参考官方文档进行集成,支付请求正常,能够跳转到支付宝页面
- 12.2 支付集成:支付宝支付接口正常集成,在订单页生成合法的支付链接。
- 13.2 支付集成:能够无缝对接支付宝支付
- 14.2 支付集成:集成支付宝支付接口,完成支付功能
- 15.2 Spring Boot集成支付宝,实现支付功能的接入和配置.
- 16.2 使用支付宝提供的SDK或API,结合Spring Boot框架,实现支付宝支付功能的集成
- 17.2 配置支付宝的商户信息、密钥等参数,并使用HTTPS协议与支付宝服务器进行交互
- 18.2 在应用的配置文件中配置支付宝的商户信息、密钥等参数,如商户号、应用ID、私钥等
- 19.2 在支付请求方法中,调用支付宝SDK或API提供的接口,将订单信息发送给支付宝服务器进行支付
- 20.2 创建一个支付服务类或控制器,实现支付功能的相关方法,如订单创建、支付请求发送和支付结果查询等
回调验签:
- 11.3 支付宝回调处理:支付回调接口正确验签,对验签通过的请求,及时更新本地订单状态
- 12.3 回调处理:支付宝回调接口验证测试通过,接收支付宝回调,处理回调成功且正确
- 13.3 支付回调:回调接口能够接收支付宝的回调,验证签名安全,及时更新支付状态
- 14.3 支付回调:回调接口接收支付宝支付信息,验证确实为支付宝回调后再更新订单状态,确保支付信息的准确性
- 15.3 处理支付宝回调验签成功后的后续业务逻辑
- 16.3 在支付宝支付完成后,支付宝会向指定的回调URL发送支付结果通知(回调接口,完成验签功能,并能够修改业务状态)
- 17.3 在Spring Boot应用中配置回调URL,并实现接收和解析支付宝回调数据的逻辑
- 18.3 在接收回调通知的方法中,解析支付宝回调数据,可以使用支付宝SDK或API提供的工具类来解析。
- 18.4 如果支付成功,更新订单状态为已支付,并记录相关支付信息
- 19.3 获取支付宝回调通知中的签名和其他参数,使用支付宝提供的验签规则对回调数据进行验签
- 20.3 验签成功后,验证订单信息的正确性,并处理支付结果。可以更新订单状态、生成支付记录、发送通知等操作
- 20.4 需要注意处理并发情况下的幂等性问题,确保多次回调不会重复处理订单。
掉单处理:
- 11.4 对于‘未支付’订单,系统能够定时进行处理,根据支付宝接口返回的状态更新本地订单状态
- 12.4 对于未能及时处理回调的订单,能够定时查询支付宝,并根据查询结果更新本地订单状态
- 15.4 实现调单处理功能,修改订单状态以确保数据准确性和一致性
- 16.4 检查支付宝回调通知中的支付状态,根据实际需求,处理支付成功或支付失败的订单
- 17.4 可以更新订单状态、生成支付记录、发送通知等操作,确保订单状态与支付宝同步 ??
- 18.4 如果支付成功,更新订单状态为已支付,并记录相关支付信息
关闭订单:
- 11.5 支付关闭处理:对于超过30分钟没有支付成功的订单,及时关闭
- 14.5 支付通知:支付成功后,可通过短信或邮件通知用户支付成功,确保用户体验
- 15.5 使用RocketMQ异步消息通知机制,在订单未支付关闭时发送通知消息。
- 16.5 根据业务需求设置订单的有效支付时间,超过该时间未支付的订单将自动关闭
- 18.5 订单创建时,设置订单的有效支付时间,超过该时间未支付的订单将自动关闭
- 19.5 当订单被关闭时,将相关订单信息发送到RocketMQ的消息队列中
退款:
- 13.5 退款功能:已支付的订单,完成退款功能,退款成功修改订单状态
支付成功通知:
- 12.5 支付成功通知:对于成功支付的客户,及时向客户发送支付成功通知。
- 13.4 支付状态通知:在支付成功的情况下,能够用短信及时通知客户。
- 14.4 支付通知:支付成功后,可通过短信或邮件通知用户支付成功,确保用户体验
其他:
- 17.5 使用RocketMQ作为消息队列,将未支付关闭的订单信息发送到消息队列中
- 19.4 如果支付失败,根据需要进行相应的处理操作,如取消订单、释放库存等
- 20.5 在消费者中实现相应的逻辑,如记录日志、发送提醒等操作,以便及时处理关闭的订单信息 ??
预约看房
集成RocketMQ
- 30.1 集成、配置RocketMQ作为支付模块的消息推送机制,确保消息的可靠传递和处理
消息消费
- 30.2 配置RocketMQ的消费者,订阅"payment_topic"主题,并实现相应的业务逻辑来处理支付相关消息
- 29.2 配置RocketMQ的消费者,订阅"payment_topic"主题,确保消费者能够接收到支付消息,并实现负载均衡和消费者组管理以提高系统的容错性和扩展性 ??
- 28.4 将消息的处理过程异步化,并利用RocketMQ的特性实现并行处理,以提高系统的消息处理速度和吞吐量,优化系统性能和资源利用率 ???
- 22.2 消息消费者从RocketMQ中异步获取消息,并进行处理,例如发送短信、邮件等通知操作。 ???
- 21.2 接收方的消息消费者从RocketMQ中获取消息,之后通过发送短信、邮件通知用户
消息发送
- 30.3 使用RocketMQ的异步发送方式,将支付消息以消息对象的形式发送到"payment_topic"主题,提高系统的并发能力和响应速度
- 29.1 配置RocketMQ的生产者,设置主题为"payment_topic",确保消息的可靠传递,并采用可靠性保证机制增加重试次数和错误处理机制
rocketmq:
name-server: 127.0.0.1:9876 # rocketMQ 名称服务器
producer:
group: test # 发送组
topic: test-topic # topic 相当于一个地址
retry-times-when-send-failed: 5 # 同步发送失败要重试几次
retry-times-when-send-async-failed: 3 # 异步发送失败要重试几次
retry-next-server: true # 发送错误后重试下一台服务
send-message-timeout: 3000 # 发送超时
- 29.4 将用户下单成功、支付成功或取消订单等事件封装成消息对象,并通过异步发送消息的方式减轻直接同步发送短信或邮件对系统性能的影响,提升系统的稳定性和并发能力
- 28.1 配置系统以确保用户下单成功、支付成功或取消订单等事件能够触发相应的短信或邮件通知,提升用户体验和服务效果 ????
- 27.1 通过将相关信息封装成消息对象,并利用RocketMQ的生产者将消息发送到指定的消息主题,实现消息的可靠传递和分发
- 26.1 确保支付模块中用户下单成功、支付成功或取消订单等事件能够触发消息发送,并在消息消费者端正确处理 ?? 都要发送这些消息吗?
- 25.1 当用户下单成功、支付成功或取消订单时,调用RocketMQ的生产者发送相应的支付消息到"payment_topic"
- 25.4 使用RocketMQ的异步发送方式,将消息发送到"payment_topic"主题
- 24.1 在用户下单或支付时,将通知信息封装成消息并通过RocketMQ发送至中间件
- 23.1 用户完成购物行为后,将通知信息封装成消息并通过RocketMQ发送至中间件。
- 21.1 使用RocketMQ中间件实现业务解耦,发送短信和邮件通知
- 30.5 添加消息推送的异常处理机制,如消息发送失败时的错误重试机制和错误日志记录,以保证消息的可靠性和系统稳定性
消息发送配置:
- 22.1 使用RocketMQ进行同步发送消息,并设置合适的重试次数和超时时间,避免网络波动导致消息重复发送
防止重复消费
- 30.4 为避免重复消费,使用Redis或其他缓存工具记录已经处理过的消息的唯一标识符,以实现幂等性,确保数据处理的准确性和一致性
@Data
public class PaySuccessMsg {
private String phone;
private BigDecimal price;
private int orderId;
}
@Service
@RocketMQMessageListener(topic="pay_succes_0704", consumerGroup = "pay_succes_0704_group")
@Slf4j
public class PaySuccessConsumer implements RocketMQListener<PaySuccessMsg> {
@Autowired
RedisTemplate redisTemplate;
@Override
public void onMessage(PaySuccessMsg paySuccessMsg) {
//做法一,在进来的时候设置。这种方法更简单
//做法二,进来时不做判断,发送成功以后再做设置。
String key = key(paySuccessMsg);
if(redisTemplate.opsForValue().setIfAbsent(key,"1",30, TimeUnit.MINUTES)) {
System.out.println("我收到了消息:" + paySuccessMsg.toString());
ZhenziSmsClient client = new ZhenziSmsClient("https://sms_developer.zhenzikj.com", "112605", "6a987156-398b-4c3c-9d84-ce6b3921bd18");
sendMsg(paySuccessMsg, client);
}else {
System.out.println("处理消息重复");
}
}
private static void sendMsg(PaySuccessMsg paySuccessMsg, ZhenziSmsClient client) {
Map<String, Object> params = new HashMap<String, Object>();
params.put("number", paySuccessMsg.getPhone());
params.put("templateId", "10759");
String[] templateParams = new String[2];
templateParams[0] = ""+ paySuccessMsg.getPrice().floatValue();
templateParams[1] = "5分钟";
params.put("templateParams", templateParams);
try {
String result = client.send(params);
System.out.println(result);
} catch (Exception e) {
log.error("发送短信出现异常", e);
}
}
private String key(PaySuccessMsg paySuccessMsg) {
return DigestUtil.md5Hex(paySuccessMsg.toString());
}
}
- 29.3 为避免重复消费,消息体中添加唯一标识符如订单号或支付流水号,并在消费端进行去重处理,同时使用幂等性机制确保数据处理的准确性和一致性
- 28.3 利用RocketMQ内置的消息去重机制和消费者端的幂等性处理功能,避免同一条消息被重复消费,确保数据处理的准确性和一致性 ??内置消息去重
- 27.3 在消费者处理消息之前,通过检查消息的唯一标识符或状态,确保同一条消息不会被重复消费,保证数据的一致性和正确性。
- 26.3 利用RocketMQ内置的消息去重机制,结合消费者端的幂等性处理,确保同一条消息不会被重复消费,保证消息的准确性和一致性 ?? 内置消息去重
- 25.3 可以使用Redis等缓存工具记录已经处理过的消息的唯一标识符,以防止重复消费
- 23.3 在消息消费过程中,注意避免重复消费问题,以避免引发业务异常
- 22.4 为消息添加唯一标识如订单号、支付流水号作为消息ID,确保消息仅被处理一次,避免重复消费
防止消息丢失
- 28.2 正确配置RocketMQ的生产者和消费者实例,以确保消息不会在传递过程中丢失,增强系统的消息可靠性和稳定性 ??
- 27.2 RocketMQ采用高可靠的消息传递机制,确保消息能够准确地传递,且不会丢失,提供可靠性保障 ???
- 26.2 通过将消息发送到RocketMQ消息队列中,确保消息不会丢失,并能够在网络异常或其他问题后恢复正常消费
异步?
- 27.4 将短信发送或邮件发送的动作解耦至异步线程中,将发送操作与主线程分离,提高系统的响应速度和并发能力,优化用户体验和系统性能 ??
- 26.4 利用RocketMQ的异步发送特性,将短信发送或邮件发送的动作从主线程中解耦,提高系统的响应速度和并发能力,优化用户体验和系统性能
- 24.2 消息接收的设计。 消息消费者从RocketMQ中获取消息,将消息异步处理,例如发送短信、邮件通知用户
- 23.2 消息消费者从RocketMQ中异步获取消息,并进行相应的处理,如发送短信、邮件通知用户。???
消息堆积
- 24.3 消息堆积解决。 增加消息消费线程数:通过增加消息消费线程数,可以提高消息消费速度,减少消息堆积的可能性
@RocketMQMessageListener(consumeThreadMax = 128,topic="pay_succes_0704", consumerGroup = "pay_succes_0704_group")
- 22.3 增加消费者数量以提高消息消费速度,降低消息堆积的潜在风险
- 21.3 通过启用多个消息消费者实例进行并行处理,解决消息积压问题
消息补偿
- 24.4 消息补偿。:在消息消费过程中,对于处理失败的消息,进行一定次数的重试操作,并记录错误日志。
private void sendSMS(ZhenziSmsClient client, String phone, String orderId, int num) {
if(num <= 0){
log.error("发送短信重试仍旧失败。");
return;
}
Map<String, Object> params = new HashMap<String, Object>();
params.put("number", phone);
params.put("templateId", "10759");
String[] templateParams = new String[2];
templateParams[0] = orderId;
templateParams[1] = "5分钟";
params.put("templateParams", templateParams);
try {
String result = client.send(params);
System.out.println(result);
if(!(result.indexOf("发送成功") > -1)){
sendSMS(client, phone, orderId, num-1);
}
} catch (Exception e) {
log.error("发送短信失败",e);
sendSMS(client,phone, orderId,num-1);
}
}
- 23.4 实现消息消费补偿机制,用于在消息消费过程中出现异常时进行重试操作,确保消息的可靠性
- 21.4 在消息消费过程中出现异常时,通过重试机制保证业务正确性
消费者集群
- 25.2 将RocketMQ的消费者消费模式设置为集群模式,确保多个消费者能够同时消费消息,提高可靠性 ??
@RocketMQMessageListener(messageModel = MessageModel.BROADCASTING,topic="pay_succes_0704", consumerGroup = "pay_succes_0704_group")
其他
消息持久化:
重试策略:
故障恢复机制
监控手段
顺序消息
报警机制
消费者均衡消费
- 21.5 消息推送系统可靠性设计(4分):包括消息持久化、重试策略和故障恢复机制,确保系统的可靠性 ???
- 22.5 消息可靠性保障(3分):使用持久化机制存储消息,结合监控手段和故障恢复机制,保证消息推送系统的可靠性
- 23.5 对于有序消息,考虑实现消息的顺序处理,确保消息按照特定顺序被消费 ???
- 24.5 建立消息监控机制,实时监测消息堆积情况,并设置相应的报警机制,及时发现和处理异常情况 ???
- 25.5 设置RocketMQ的消息可靠性保障机制,如开启同步刷盘和持久化,确保消息不会丢失。
- 26.5 配置RocketMQ的消费者端负载均衡策略,确保多个消费者能够均衡消费消息,提高系统的可伸缩性和稳定性
- 27.5 利用RocketMQ的顺序消息传递特性,确保同一个消息队列中的消息按照发送顺序进行消费,保证消息的顺序性,适用于有序处理的场景
- 28.5 结合RocketMQ提供的错误消息处理机制和重试策略,及时检测和处理消息处理过程中的异常情况,并进行恰当的重试,以确保系统的稳定运行和消息的可靠发送
- 29.5 在RocketMQ中配置适当的消息存储策略、消息投递超时时间以及消息重试次数,以确保消息的持久化存储、可靠性投递和容错处理,防止消息丢失或延迟。
外卖抢单
线程池的使用
- 40.1 通过合理的线程池参数配置,控制线程数量,避免资源耗尽和性能下降(请自定义线程池,并合理设置核心参数,以控制线程数量,避免线程过多而导致系统整体性能下降)
- 39.1 使用线程池管理和调度线程,确保外卖抢单系统能够高效处理大量请求(请自定义线程池,并设置适当的核心参数,避免线程数量过多导致系统整体性能下降)
//初始化线程池
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(2);//线程池最多有2个线程
executor.setCorePoolSize(1);//核心线程数为1
executor.setThreadNamePrefix("*_*");//线程名称前缀
executor.setQueueCapacity(100);//线程池等候队列
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//拒绝策略
executor.initialize();//初始化
return executor;
}
}
- 38.1 使用Java多线程技术处理外卖抢单模块的并发请求(请自定义线程池,并设置适当的核心参数,以避免线程数量过多对系统整体性能造成负面影响)
- 35.1 基于Spring Boot框架构建外卖抢单模块,使用Java多线程技术处理并发请求(自定义线程池,设置核心参数,避免线程过多,影响系统整体性能)
@RequestMapping("/process")
public Result<Boolean> createOrder(Book book) {
executor.execute(() -> {//使用多线程处理创建订单请求
Order order = createOrder(book, 1,1);
orderService.save(order);
});
return Result.ok(true);
}
- 34.2 使用线程池生成外卖订单(使用自定义线程池创建多个线程,并在每个线程中生成多个外卖订单。请设置合理的线程池参数,避免线程数量过多导致系统整体性能下降)
- 33.3 模拟用户产生大量订单 (使用Java多线程技术实现模拟用户并发产生大量订单。在实现过程中,请自定义线程池,并设置合理的核心参数,以避免线程数量过多导致系统性能下降)
- 31.2 创建多个线程,并在每个线程中生成多个外卖订单
@RequestMapping("/creat_order")
public Result<Boolean> creatOrder() {
Book book = new Book();
book.setId(1);
book.setPrice(BigDecimal.TEN);
for( int i=0;i<3; i++) {
executor.execute(() -> {
while(true) {
Order order = createOrder(book,1, 1);
orderService.save(order);
System.out.println("create order: " + order.toString());
try {
Thread.sleep(RandomUtil.randomInt(100,2000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
return Result.ok(true);
}
限流
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: order
uri: lb://order # 指定服务器注册实例名
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/badthinghappend
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 30 # 放令牌的速率
redis-rate-limiter.burstCapacity: 200 # 令牌桶最大放多少个
redis-rate-limiter.requestedTokens: 1 #每次请求消耗几个令牌
key-resolver: "#{@userKeyResolver}" # 根据userName来限流
statusCode: MOVED_TEMPORARILY # 更改错误状态码
predicates: #断言
- Path=/order/** # 如果访问路径以services开头,则执行本路由规则
- 40.2 当达到限流阈值时,返回合适的错误码或页面提示用户稍后再试 (请使用Hystrix或Gateway网关等技术进行服务降级,以避免系统崩溃,并返回适当的错误码或页面提示,告知用户稍后再试) ?? 怎么实现
- 39.2 设置适当的限流策略,例如限制API接口每秒请求数或并发请求数(使用Gateway网关对外卖抢单系统进行服务降级,以防止系统崩溃,并确保在高负载情况下系统的正常运行)
- 38.2 基于网关做限流处理,以防止系统负载过高(通过配置网关进行限流,以确保系统在高负载情况下不会受到过多请求的影响,保持系统的稳定性)
- 37.2 自定义线程池和设置核心参数,在外卖抢单系统中避免线程过多,影响系统整体性能(通过自定义线程池并设置核心参数来管理外卖抢单系统中的线程,以避免线程过多对系统整体性能造成影响) ???
- 36.2 设置合理的限流策略,如限制API接口每秒请求数或并发请求数 (请描述如何设置合理的限流策略,例如通过Hystrix或Gateway网关进行服务降级,以避免系统崩溃)
- 35.2 使用分布式限流对外卖抢单接口进行访问频率的限制,防止系统负载过高(使用Hystrix或Gateway网关进行服务降级,避免系统崩溃)
- 33.2 限制订单服务的访问频率 (为了控制订单服务的访问频率,需要设定最大的访问次数和并发数量。请确保订单服务每分钟最多处理1800次访问请求,并发数量不超过200)
- 32.4 针对高峰时期流量过大可能引起的系统瘫痪问题实现相应解决方案 ???
- 31.4 针对高峰时期流量过大可能引起的系统瘫痪问题实现相应解决方案
超卖
- 40.3 根据订单状态和库存信息,使用Redis防止商品超卖 (请根据订单状态和库存信息,使用Redis等缓存技术,实现商品超卖的防止机制,确保每个订单都有足够的库存进行抢购)
- 39.3 使用Redis的原子操作,实现防止超卖问题的解决方案 (探索并实施基于Redis的原子操作,确保每个订单的抢单过程中不会出现超卖的情况)
- 38.3 使用Redis缓存热点数据,提高读取速度,减轻数据库压力 (使用Redis作为缓存,存储外卖抢单系统的热点数据,以加快数据的读取速度,减少对数据库的访问,从而减轻数据库的压力)
- 37.3 当外卖抢单系统达到限流阈值时,如何返回适当的错误码或页面提示用户稍后再试(在外卖抢单系统中如何通过Hystrix或Gateway网关进行服务降级,以避免系统崩溃,并给出相应的错误码或页面提示,在达到限流阈值时引导用户稍后重试)
- 36.3 在高并发场景下,使用Redis的原子操作(如incr/decr)实现防止超卖问题的解决方案(请详细说明在高并发场景下,如何利用Redis的原子操作(如incr/decr)来有效解决外卖抢单系统中可能出现的超卖问题)
@GetMapping("qiang_book")
public Result<Boolean> qiangBook(int num) {
Long book_total = redisTemplate.opsForValue().decrement("book_total", num);
System.out.println("还剩"+book_total+"本");
if(book_total >=0) {
//creatOrder();
Product product = productService.getById(1);
product.setStock(product.getStock()-num);
productService.updateById(product);
return Result.ok(true);
}else {
return Result.ok(false);
}
}
- 34.3 保证订单抢单的线程安全(为了防止多个用户同时抢购同一个订单,需要实现线程安全机制。请说明你打算如何保证订单抢单的安全性,确保每个订单只能被一个用户抢到) ??? 怎么说明
- 31.3 考虑多个外卖员同时抢某订单的情况,防止某订单被多人抢到
@RequestMapping("/qiang_order")
public Result<Boolean> qiangOrder(int pid, int uid) {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(pid, "1", 3, TimeUnit.DAYS);
if(aBoolean) {
Orders orders = ordersService.getById(pid);
orders.setStatus("被抢单");
orders.setUserId(uid);
//设置订单状态
ordersService.updateById(orders);
}
return Result.ok(true);
}
JMeter
- 40.4 使用JMeter工具,分析测试结果(请使用JMeter工具对系统进行性能测试,分析测试结果,评估系统的吞吐量、并发能力和响应时间等指标,以便进一步优化系统性能)
- 39.4 评估外卖抢单系统的接口性能和稳定性,包括响应时间、吞吐量和错误率等指标(通过性能测试工具Jmetter,对外卖抢单系统进行接口性能和稳定性测试,分析关键指标包括响应时间、吞吐量和错误率等,以评估系统的性能和稳定性)
- 38.4 使用JMeter工具创建模拟请求,配置合理的线程数和并发用户数来模拟高峰期访(使用JMeter工具,配置合理的线程数和并发用户数,模拟外卖抢单系统在高峰期的访问情况,以评估系统的性能和可靠性)
- 37.4 根据订单状态和库存信息,使用Redis维护每个商品的可售数量,并在抢单时进行原子操作,保证库存的准确性(如何利用Redis缓存热点数据,在外卖抢单系统中根据订单状态和库存信息维护每个商品的可售数量,并确保在抢单过程中进行原子操作,以保证库存的准确性)
- 36.4 使用JMeter测试外卖抢单系统的接口性能 (请介绍如何使用JMeter工具对外卖抢单系统的接口性能进行测试,包括测试的目标、方法和常见指标等) ??
- 35.4 使用JMeter工具创建模拟请求,配置合理的线程数和并发用户数来模拟高峰期访问
- 37.5 使用JMeter工具对外卖抢单系统进行性能分析和优化(请阐述如何使用JMeter工具分析外卖抢单系统的性能,并提出相应的优化方案,例如通过调整线程池大小、数据库连接池参数等来提升系统的处理能力)
事务
- 37.1 使用数据库事务机制确保数据的一致性和并发操作的正确性 (利用数据库事务机制来管理外卖抢单系统中的数据操作,以确保数据的一致性和并发操作的正确性)
缓存
- 35.3 商品加载到Redis缓存中,提高读取速度,减轻数据库压力
- 32.2 完成产品信息的缓存代码 只需要完成后端代码,缓存信息存放在redis中
缓存一致性
- 32.3 完成产品更新功能。考虑到产品运行在并发请求中,请实现一种方案,确保缓存和数据库中的数据保持一致
订单关闭
- 34.4 编写定时任务自动删除过期订单(编写定时任务,用于自动删除过期的订单。请设定合适的时间间隔,确保系统能够及时清理过期的订单数据,并释放相关资源)
- 33.1 为了保证订单系统的数据一致性,需要及时关闭未支付订单。请自行设计数据库表结构,满足三大范式要求,并确保数据一致性。订单创建后,如果30分钟内未完成支付,则需要将该订单状态设置为已关闭
建表
- 36.1 设计合适的数据库表结构,准备用于保存订单的数据表(设计合适的数据库表结构,满足三大范式要求,并解释如何确保数据的一致性)???
- 34.1 创建外卖订单表(请使用Java代码生成外卖订单表,包含以下字段:订单ID、价格、送餐地址、目的地、创建时间和状态等。你可以自行建表,也可以参考提供的takeaway.sql文件,确保满足三大范式要求并保证数据一致性)
- 32.1 创建产品表和相应的代码 参考上文中的“product.sql”创建订单表。并生成相应的java代码以实现对该表操作(自行建表,可以参考product.sql,满足三大范式要求,保证数据一致性)
- 31.1 创建外卖订单表和相应的代码 (5分)参考上文中的“takeaway.sql”创建订单表。并生成相应的java代码以实现对该表操作(自行建表,可以参考takeaway.sql,满足三大范式要求,保证数据一致性)
其他
- 33.4 使用Postman实现创建订单,对订单创建请求进行安全验证 检验是否携带正确的token,只有带有正确token的请求才能创建成功
说明
- 33.5 在使用支付宝接口进行订单处理时,为了确保数据的完整性和安全性,需要进行验签操作。请说明验证签名的步骤,并确保只有通过验签的请求才能被认为是有效的 ???说明
- 34.5 在使用支付宝接口进行订单处理时,为了确保数据的完整性和安全性,需要进行验签操作。请说明验证签名的步骤,并确保只有通过验签的请求才能被认为是有效的
- 36.5 请说明如何使用JMeter工具对外卖抢单系统的稳定性进行评估,包括模拟高负载、长时间运行和异常情况等测试场景。 ??? 异常情况是指??
- 38.5 优化外卖抢单系统的性能和用户体验(MQ异步处理订单,以提高外卖抢单系统的性能和用户体验) ???
- 39.5 优化外卖抢单系统的性能和用户体验 (使用Redis缓存热点数据,以提高外卖抢单系统的性能和用户体验)
- 40.5 如何提高系统性能和用户体验(使用缓存技术或异步处理或负载均衡等手段)
父项目
Spring Cloud有很多版本,本课程选取Hoxton.RELEASE版本,它依赖的Spring boot版本为2.2.5.RELEASE。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<springcloud.version>Hoxton.RELEASE</springcloud.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<!--控制spring cloud系列的版本-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--springboot 插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.6</version>
</plugin>
</plugins>
</build>
子项目
添加web依赖:
<!--gateway中不能引入!-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
引入Mysql数据库支持
- 添加依赖 (mybatis-plus 3.5.2)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 使用druid连接池,一并引入-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--因为是基于mysql开发,所以一并引入-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 配置分页
//注意, 这里配置适合mybatis-plus 3.5.2版本
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
- 在入口类配置mapper扫描
@MapperScan("com.eight.user.mapper")
- 增加数据库配置
datasource:
url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: dev
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
引入Redis支持
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置redis序列化方式
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.json());
return template;
}
- 添加redis配置
spring:
redis:
database: 2
host: 127.0.0.1
port: 6379
全局异常处理
全局异常处理使用两个注解来实现,@ControllerAdvice 和 @ExceptionHandler,如下:
//全局异常处理
@RestControllerAdvice
public class ExceptionProcessor {
@ExceptionHandler({Exception.class})
public Result<Exception> handleException(Exception e) {
return Result.ok(e, -1, "通用异常");
}
@ExceptionHandler({BizException.class})
public Result<BizException> handleException(BizException e) {
return Result.ok(e,e.getState(),e.getMessage());
}
}
```java
//自定义异常类,带状态码和错误信息,其中错误信息复用父类的message字段
public class BizException extends RuntimeException{
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public BizException(int state, String message) {
super(message);
this.state = state;
}
}
日期处理
后端日期处理样例:
public class Contract {
@DateFormat("yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date start;
}
前端vue日期格式, format控制前端显示格式,value-format控制向后端传送的格式(值的格式)
<el-data-picker v-model="start"
type="date"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
placeholder="选择日期" ></el-data-picker>
写入cookie数据
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
response.addCookie(cookie);
document.cookie = "token="+res.data.data;
二周目难点
邮箱验证
\w+@\w(.\w)*
唯一ID号
服务端:
Snowflake snowflake = IdUtil.getSnowflake();
String id = "HT"+snowlake.nextId();
浏览器端:
# install
npm install uuid -S
import { v1 } from 'uuid';
let uid = v1()
输入最大值
<el-input-number v-model="num" :precision="0" :step="1" :max="99"></el-input-number>
保持数据库和缓存一致
# clear cache 清除缓存
# update database 更新数据库
# clear cache again 等会儿再清除一次缓存
三周目难点
手机号正则匹配
// 以1开头,后面接10位数字
let reg = /^1\d{10}$/
发送消息失败重试3次
防止消息重复消费
从Token中获取用户ID
发送短信失败重试三次
Redis
配置:
两种持久化机制:
RDB
RDB (Redis Database)
# save 秒 写请求
save 900 1 #当有一条 Keys 数据被改变时,900 秒刷新到 Disk 一次
save 300 10 #当有 10 条 Keys 数据被改变时,300 秒刷新到 Disk 一次
save 60 10000 #当有 10000 条 Keys 数据被改变时,60 秒刷新到 Disk 一次
AOF
AOF (Append Only File)
#启用 AOF 持久化方式
appendonly yes
#AOF 文件的名称,默认为 appendonly.aof
appendfilename appendonly.aof
#每次收到写命令就立即强制写入磁盘,是最有保证的完全的持久化,但速度也是最慢的,一般不推荐使用。
# appendfsync always
#每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,是受推荐的方式。
appendfsync everysec
#完全依赖 OS 的写入,一般为 30 秒左右一次,性能最好但是持久化最没有保证,不被推荐。
# appendfsync no
常见问题:
redis 链接不上的一个原因:
使用缺省的链接库lettuce会有链接不上的情况,出现这种情况时可以改用jedis库,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
穿透
定义
被访问的数据不存在,导致每次请求得数据都不能在缓存中找到,导致请求都要到数据库里。 最终导致数据库压力过大。
解决方案
每次从数据库中读不到的数据,都在缓存里存放一个空对象。一般要设置一个过期时间, 以防止以后某个时间对应的数据又有了。
击穿
定义
某个非常热点的数据在key失效的瞬间,导致大量的请求直接到了数据库。
解决方案
- 基于redis或zookeeper实现互斥锁
- 将热点数据设置为永不过期
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
else {
return value;
}
}
雪崩
定义
雪崩在不同的语境里有不同的内涵,在redis这里的雪崩就是大量热点数据过期,导致大部分缓存数据 在redis层失效,请求直接到数据库层,进而导致数据库压力堵塞甚至宕机。
解决方案
- 缓存过期时间分散化
- 热点数据均匀分布到不同的redis和数据库 (业务不一定允许)
- 多级缓存 ehcache + redis
redis 和 mysql 保持一致
延时双删
后删除,失败再由MQ的重试机制重复删除。
canal 读取binlog再同步到redis。
修改密码
# 如果是docker,则需要进入docker内部
docker exec -it myredis redis-cli;
# 查询当前密码
config get requirepass
# 设置密码 your_password 替换成你自己的密码
config set requirepass your_password
参考:
[canal:]( https://zhuanlan.zhihu.com/p/177001630)
登录与认证
private String createToken(User dbUser) {
Map<String,Object> map = new HashMap<>();
map.put("uid",dbUser.getId());
map.put("name", dbUser.getName());
return JWTUtil.createToken(map,"1234".getBytes());
}
前端解析jwt:
decodeJWT(jwt) {
return JSON.parse(decodeURIComponent(window.atob(token.split(".")[1])))
}
网关jwt统一检查
@Component
public class JWTFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1. 哪些url需要进行安全检查
String currentUrl = exchange.getRequest().getPath().value();
if(!"/user/user/login".equals(currentUrl)) {
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
if(cookies != null && cookies.getFirst("token") != null) {
HttpCookie token = cookies.getFirst("token");
String jwt = token.getValue();
boolean verify = JWTUtil.verify(jwt, "1234".getBytes());
if(!verify) {
return needLogin(exchange);
}
}else {
return needLogin(exchange);
}
}
//2, 对于需要安全检查的url进行检验。
//3,合法通过,不合法拒绝。
return chain.filter(exchange);
}
private Mono<Void> needLogin(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return 0;
}
}
Spring Security
基础表结构
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
);
前端
工程创建
vue create project
安装插件
npm i element-ui -S
npm i axios -S
编辑main.js, 引入element-ui, axios; 配置axios,
import axios from 'axios'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
Vue.config.productionTip = false
axios.defaults.baseURL = "http://localhost:8080/"
axios.defaults.withCredentials=true;
Vue.prototype.axios = axios;
启动服务
npm run serve -- --port=8081
代码收集
所有请求都带指定的header
使用请求拦截器来实现:
// 请求拦截器
instance.interceptors.request.use(function (config){
config.headers.token = "这里放入需要的token"; //在headers中携带token
return config
},function (error){
// 处理请求错误
return Promise.reject(error)
});
Vue.prototype.axios = instance;
统一处理axios返回内容
//响应拦截器
axios.interceptors.response.use(function (resp){
return resp;
},function (err){
console.log(err.request.status);
if("401" == err.request.status) {
router.push("/login")
}
})
传递参数
以列表页打开详情页为例,打开详情页时需要传递当前列表的id到详情页。做法如下:
// 打开/contract页面,同时传递参数ID
openContract(contract) {
this.$router.push({path:"/contract", query:{id:contract.id}})
}
获取传递的参数
mounted() {
console.log("合同ID="+this.$route.query.id);
}
前端安装唯一ID工具
npm install nanoid
发送邮件
引进依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
添加配置文件
spring:
mail:
host: smtp.qq.com
port: 587
default-encoding: UTF-8
username: 1992651344@qq.com
password: 这里写上你的密码
QQ邮箱获取密码指南:
QQ邮箱不使用密码,而是用授权码,授权码获取如下所示:
QQ授权码
代码实现
@Autowired
JavaMailSender javaMailSender;
private void sendMail(String to, String subject, String content) {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setFrom("1992651344@qq.com");
simpleMailMessage.setTo(to);
simpleMailMessage.setSubject(subject);
simpleMailMessage.setText(content);
simpleMailMessage.setSentDate(new Date());
javaMailSender.send(simpleMailMessage);
}
rocketmq-console
启动报 java.xml 异常
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
alipay
https://openhome.alipay.com/develop/sandbox/app
dermol5132@sandbox.com
mysql
修改docker中的数据库时区 docker exec -it mysql sh
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime