目录
前言
一、基础环境搭建
1、安装Linux虚拟机
2、安装docker
2.1、安装MySQL
2.2、docker 安装 redis
2.3、docker 安装 Es和Kibana
2.4、docker 安装 Nginx
二、SpringClound Alibaba
1、Nacos[注册中心、配置中心]
2、Gataway
2.1、简介
2.2、核心概念
2.3、使用
2.4、配置统一的全局跨域
2.5、Gataway配合Nginx进行请求转发
3、Feign 声明式远程调用
3.1、简介
3.2、使用
3.3、Feign远程调用丢失请求头问题
3.4、Feign异步情况丢失上下文问题
三、Java生态知识点
1、JSR303 数据校验
2、统一异常处理
3、ES
3.1、基本概念:
3.2、分词(IK分词器安装)
3.3、整合SpringBoot(Elasticsearch-Rest-Client)
3.4、商城商品检索代码(可当做参考案例)
4、Java虚拟机监控
4.1、jconsole 与 jvisualvm
4.2、项目设值堆内存大小
5、缓存
1、缓存使用
2、本地缓存
3、整合 redis 作为缓存
4、高并发下缓存失效问题
4.1、缓存穿透
4.2、缓存雪崩
4.3、缓存击穿
4.4、加锁(本地锁)解决缓存击穿问题
4.5、加锁(分布式锁)解决缓存击穿问题
5、Redisson 整合
5.1、原生Redisson整合
5.2、使用
6、缓存数据一致性问题
6.1、双写模式
6.2、失效模式
6.3、延迟双删
6.4、总结
6.5、异步更新缓存(基于Mysql binlog的同步机制 Canal)
7、SpringCache
7.1、整合
7.2、使用细节
6、异步和线程
6.1、初始化线程的 4 种方式
6.2、线程池的七大参数
6.3、线程池运行流程
6.4、开发中为什么使用线程池
6.5、CompletableFuture异步编排
7、MD5&盐值&BCrypt
8、社交登陆
1.1、OAuth2.0
1.2、微博登陆准备工作
1、进入微博开放平台,API使用
2、微博登陆流程图和时序图
3、微博登陆代码
1.3、Session共享问题
1.3.1、session原理
1.3.2、分布式下session共享问题
1.3.3、Session共享问题解决
1.3.4、不同服务,子域session共享问题
1.4、整合SpringSession
9、单点登陆(SSO)
1.1、前置概念
1.2、gitee基础代码演示
1.3、单点登录流程
1.4、谷粒商城项目单点登录代码
10、ThreadLocal-同一个线程共享数据编辑
11、消息中间件(MQ)
11.1、用途
11.2、重要概述
11.3、RabbitMQ概念
11.4、Docker安装RabbitMQ
11.5、RabbitMQ运行机制
11.6、SpringBoot 整合 RabbitMQ
11.7、RabbitMQ消息确认机制-可靠抵达
11.8、RabbitMQ延时队列(实现定时任务)
11.8.1、延时队列场景
11.8.2、Schedule 定时任务的时效性问题
11.8.3、消息的TTL(Time To Live)和死信队列(Dead Letter Exchanges(DLX))
11.8.4、延时队列实现
11.9、注解方式创建队列和交换机和绑定关系
11.10、如何保证消息可靠性
1、保证消息不丢失
2、保证消息不重复
3、保证消息不积压
12、接口幂等性
12.1、什么是幂等性
12.2、哪些情况需要防止
12.3、什么情况下需要幂等
12.4、幂等解决方案
12.4.1、token 机制
12.4.2、各种锁机制
1、数据库悲观锁
2、数据库悲观锁
3、业务层分布式锁
12.4.3、各种唯一约束
1、数据库唯一约束
2、redis set 防重
12.4.4、防重表
12.5、token机制解决案例
13、本地事务&分布式事务
13.1、本地事务
13.1.1、事务的基本性质
13.1.2、事务的隔离级别
13.1.3、事务的传播行为
13.1.4、SpringBoot 事务
13.2、分布式事务
13.2.1、CAP 定理与 BASE 理论
1、CAP 定理
2、BASE 理论
3、强一致性、弱一致性、最终一致性
13.2.2、分布式事务几种方案
1、2PC 模式
2、柔性事务-TCC 事务补偿型方案
3、柔性事务-最大努力通知型方案
4、柔性事务-可靠消息+最终一致性方案(异步确保型)
13、Seata
13.1、Seata术语
13.2、整体机制
13.3、Seata使用
13.2.1、整合
四、运维知识点
1、Nginx的应用
1.1、正向代理与反向代理概念
1.2、Nginx配置文件
1.3、Nginx+Windows搭建域名访问环境
1.3、Nginx 动静分离
2、内网穿透
2.1、简介
2.2、内网穿透图解
2.3、内网穿透的几个常用软件
2.4、花生壳使用流程
五、整合第三方平台
1、阿里云OSS
2、阿里云短信服务
3、支付宝支付
3.1、加密-对称加密
3.2、加密-非对称加密
3.3、什么是公钥、私钥、加密、签名和验签
3.4、支付宝加密验签流程图
3.5、支付宝配置沙箱环境
六、测试
1、压力测试
1.1、概念
1.2、性能指标
1.3、JMeter 安装和使用
vi /etc/ssh/sshd_config
#修改 PasswordAuthentication yes/no
#重启服务
service sshd restart
docker 安装配置
docker pull mysql:5.7
docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
参数说明
-p 3306:3306:将容器的 3306 端口映射到主机的 3306 端口
-v /mydata/mysql/conf:/etc/mysql:将配置文件夹挂载到主机
-v /mydata/mysql/log:/var/log/mysql:将日志文件夹挂载到主机
-v /mydata/mysql/data:/var/lib/mysql/:将配置文件夹挂载到主机
-e MYSQL_ROOT_PASSWORD=root:初始化 root 用户的密码
vi /mydata/mysql/conf/my.cnf
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci' init_connect='SET NAMES utf8' character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
#跳过域名解析
skip-name-resolve
docker exec -it mysql mysql -uroot -proot
grant all privileges on *.* to 'root'@'%' identified by 'root' with grant option;
flush privileges;
docker pull redis
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
docker exec -it redis redis-cli
#存储和检索数据
docker pull elasticsearch:7.4.2
#可视化检索数据
docker pull kibana:7.4.2
#创建文件夹 用于挂载容器内部文件
mkdir -p /mydata/elasticsearch/config
#创建文件夹 用于挂载容器内部文件
mkdir -p /mydata/elasticsearch/data
#允许外部ip访问
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
#保证权限
chmod -R 777 /mydata/elasticsearch/
#启动容器
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
解释:容器名:--name elasticsearch暴露端口:-p 9200:9200(Http访问连接端口) -p 9300:9300(ES集群之间的访问端口)单节点运行:-e "discovery.type=single-node"
设置内存大小:-e ES_JAVA_OPTS="-Xms64m -Xmx512m"挂载配置文件目录:-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
挂载数据存储目录:-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data
挂载插件目录:-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins启动后:访问http://虚拟机ip:9200 端口特别注意:-e ES_JAVA_OPTS="-Xms64m -Xmx256m" \ 测试环境下,设置 ES 的初始内存和最大内存,否则导 致过大启动不了 ES
#http://192.168.56.10:9200 一定改为自己虚拟机的地址
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601 \
-d kibana:7.4.2
#启动一个实例
docker run -p 80:80 --name nginx -d nginx:1.10
#将容器内的配置文件拷贝到当前目录 别忘了后面的点
docker container cp nginx:/etc/nginx .
mkdir /mydata/nginx
#修改文件名称,
mv nginx conf
#把这个 conf 移动到/mydata/nginx 下
mv conf /mydata/nginx
#停止容器
docker stop nginx
#删除容器
docker rm 容器id
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
双击 bin 中的 startup.cmd 文件访问 http://localhost:8848/nacos/使用默认的 nacos/nacos 进行登录
spring:
cloud:
nacos:
discovery:
#nacos 地址:端口
server-addr: 127.0.0.1:8848
# Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置,文件后缀名,必须相同,不能是yml和yaml
group: TEST_GROUP #指定分组,会读取namespace下分组为 DEV_GROUP 的 ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}文件,如果没有配置namespace,则读取的是public,下分组为 DEV_GROUP 的 ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}文件
namespace: 4027cd05-15b8-4c4b-ab8c-2cab3d14df06 #指定命名空间,读取的就是该空间下的某个文件
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
# nacos-config-client-test.yaml ----> config.info
org.springframework.cloud
spring-cloud-starter-gateway
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates: - Query=baz
filters: - AddRequestParameter=foo, bar
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsConfig(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许所有请求头、请求方式都进行跨域处理
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
//允许携带cookie的请求
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
upstream gulimall{
#转发至网关,有网关转发到指定的服务
server 192.168.56.1:80;
}
server {
listen 80;
#多个域名映射同一个上游路径
server_name gulimall.com search.gulimall.com item.gulimall.com auth.gulimall.com cart.gulimall.com order.gulimall.com member.gulimall.com 4448227kv1.imdo.co seckill.gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /payed/ {
#Nginx转发请求时会丢失头部信息,需要自己设置头部信息
proxy_set_header Host order.gulimall.com;
#proxy_pass http://192.168.56.1:10000;
#路由到指定upstream上,需要在server块之上配置
proxy_pass http://gulimall;
}
#static请求路径转发到Nginx的html目录下,实现动静分离
location /static/{
root /usr/share/nginx/html;
}
location / {
#Nginx转发请求时会丢失头部信息,需要自己设置头部信息
proxy_set_header Host $host;
#proxy_pass http://192.168.56.1:10000;
#路由到指定upstream上,需要在server块之上配置
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: gulimall-product
uri: lb://gulimall-product
predicates:
- Path=/api/product/**,/testHello
filters:
- RewritePath=/api/?(?.*),/$\{segment}
- id: gulimall-coupon
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**,/testHello
filters:
- RewritePath=/api/?(?.*),/$\{segment}
- id: gulimall-member
uri: lb://gulimall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/?(?.*),/$\{segment}
- id: gulimall-ware
uri: lb://gulimall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/?(?.*),/$\{segment}
- id: gulimall-third-party
uri: lb://gulimall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/?(?.*),/$\{segment}
#path作用范围大的要写咋最后面
- id: gulimall-admin
uri: lb://renren-fast
predicates:
#断言,路径相匹配的进行路由
- Path=/api/**
filters:
#重写路径 /api/.. 重写成 /...
# http://localhost:9900/api/renren-fast/list 会映射到 lb://renren-fast:端口号/api/renren-fast/list
#但会出现404 原因是真实的访问地址是 http://localhost:8080/api/renren-fast/list 要是过滤器进行路由重写
- RewritePath=/api/?(?.*),/renren-fast/$\{segment}
#客户端发送请求->nginx转发到网关(会丢失头信息,需要在Nginx进行设置)->网关接收请求通过断言请求头信息,转发到对应服务
- id: gulimall-product-host
uri: lb://gulimall-product
predicates:
#断言请求头匹配,能够匹配上 gulimall.com的请求头的转发到对应服务
- Host=gulimall.com,item.gulimall.com
#客户端发送请求->nginx转发到网关(会丢失头信息,需要在Nginx进行设置)->网关接收请求通过断言请求头信息,转发到对应服务
- id: gulimall-search-host
uri: lb://gulimall-search
predicates:
#断言请求头匹配,能够匹配上 search.gulimall.com的请求头的转发到对应服务
- Host=search.gulimall.com
#客户端发送请求->nginx转发到网关(会丢失头信息,需要在Nginx进行设置)->网关接收请求通过断言请求头信息,转发到对应服务
- id: gulimall-auth-server
uri: lb://gulimall-auth-server
predicates:
#断言请求头匹配,能够匹配上 search.gulimall.com的请求头的转发到对应服务
- Host=auth.gulimall.com
#客户端发送请求->nginx转发到网关(会丢失头信息,需要在Nginx进行设置)->网关接收请求通过断言请求头信息,转发到对应服务
- id: gulimall-cart
uri: lb://gulimall-cart
predicates:
#断言请求头匹配,能够匹配上 search.gulimall.com的请求头的转发到对应服务
- Host=cart.gulimall.com
- id: gulimall-order
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.com
- id: gulimall-member
uri: lb://gulimall-member
predicates:
- Host=member.gulimall.com
- id: gulimall-seckill
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.com
org.springframework.cloud
spring-cloud-starter-openfeign
@EnableFeignClients(basePackages = "com.atguigu.gulimall.pms.feign")
@FeignClient("gulimall-ware")//指定远程调用的服务名(spring:application:name):gulimall-ware
public interface WareFeignService {
@PostMapping("/ware/waresku/skus") //gulimall-ware服务的请求全路径
public Resp> skuWareInfos(@RequestBody List skuIds);
}
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @Description: feign拦截器功能 解决feign 远程调用解决请求头丢失问题(远程调用其他服务会创建一个新的请求,该请求没有携带任何cookie信息,另外一个服务不知道已经进行了登陆,改拦截后拦截后,设置携带cookie信息,到新的请求中,被远程调用的服务就知道那个用户进行了登陆,能获取到登陆信息)
**/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//开启第一个异步任务
CompletableFuture addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据 Feign异步情况丢失上下文问题 异步执行后,不同的线程,获取的上下文不同 所以设置为统一的上下文
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收获地址列表
List address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//开启第二个异步任务
CompletableFuture cartInfoFuture = CompletableFuture.runAsync(() -> {
//RequestContextHolder上下文 底层是ThreadLocal共享数据 每一个线程都来共享之前的请求数据 Feign异步情况丢失上下文问题 异步执行后,不同的线程,获取的上下文不同 所以设置为统一的上下文
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项 解决feign 远程调用解决请求头丢失问题(远程调用其他服务会创建一个新的请求,该请求没有携带任何cookie信息,另外一个服务不知道已经进行了登陆,改拦截后拦截后,设置携带cookie信息,到新的请求中,被远程调用的服务就知道那个用户进行了登陆,能获取到登陆信息) 具体代码见:GuliFeignConfig
List currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, threadPoolExecutor).thenRunAsync(() -> {
List items = confirmVo.getItems();
//获取全部商品的id
List skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List skuStockVos = skuHasStock.getData("data", new TypeReference>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌名
* 给Bean添加校验注解:javax.validation.constraints包下的,并定义自己的message提示
*/
@NotBlank(message = "品牌名不能为空")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "品牌logo必须是一个URL")
private String logo;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空")
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "^[a-zA-Z]$",message = "首字母必须是a-z或者A-Z")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须是大于等于0的数字")
private Integer sort;
}
/**
* bindingResult 绑定了不合校验规则的参数
* 给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
*/
@RequestMapping("/save")
public R save(@Valid() @RequestBody BrandEntity brand, BindingResult bindingResult){
if (bindingResult.hasErrors()){
List allErrors = bindingResult.getAllErrors();
Map map = new HashMap<>();
allErrors.forEach(item -> {
map.put(item.getObjectName(),item.getDefaultMessage());
});
return R.error(400,"参数不合法").put("data",map);
}else {
brandService.save(brand);
return R.ok();
}
brandService.save(brand);
return R.ok();
}
/**
* 字段验证分组,更细分组
* @author pengjun
*/
public interface UpdateValidGroup {
}
/**
* 字段验证分组,保存分组
* @author pengjun
*/
public interface SaveValidGroup {
}
/**
* 字段验证分组,更细分组
* @author pengjun
*/
public interface UpdateStatusValidGroup {
}
public class BrandEntity implements Serializable {
/**
* 品牌id
*/
@NotNull(message = "更新时,需要传递品牌id",groups = {UpdateValidGroup.class}) //更新时,需要传递品牌id
@Null(message = "保存时无需传递品牌id",groups = {SaveValidGroup.class}) //指定分组,在保存分组时,无需传递品牌id
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名不能为空",groups = {UpdateValidGroup.class, SaveValidGroup.class}) //指定分组,在保存更新时,都需要需传递品牌名称
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "品牌logo必须是一个URL",groups = {UpdateValidGroup.class, SaveValidGroup.class})//指定分组,在保存更新时,都需要符合URL
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空",groups = {SaveValidGroup.class, UpdateStatusValidGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty //未指定分组的,在指定了 @Validated(value = {SaveValid.class}) 指定分组验证时,不会进行验证
@Pattern(regexp = "^[a-zA-Z]$",message = "首字母必须是a-z或者A-Z",groups = {SaveValidGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须是大于等于0的数字")
private Integer sort;
}
//指定不同的分组标识 会验证不同的规则
@RequestMapping("/save")
public R save(/*@Valid(无法应用于分组)*/@Validated(value = {SaveValidGroup.class})/*指定分组时验证,如果字段验证未指定分组,验证不生效*/ @RequestBody BrandEntity brand, BindingResult bindingResult){
if (bindingResult.hasErrors()){
List allErrors = bindingResult.getAllErrors();
Map map = new HashMap<>();
allErrors.forEach(item -> {
map.put(item.getObjectName(),item.getDefaultMessage());
});
return R.error(400,"参数不合法").put("data",map);
}else {
brandService.save(brand);
return R.ok();
}
brandService.save(brand);
return R.ok();
}
//指定不同的分组标识 会验证不同的规则
@RequestMapping("/update")
public R update(@Validated(value = {UpdateValidGroup.class, UpdateStatusValidGroup.class}) @RequestBody BrandEntity brand){......}
javax.validation
validation-api
2.0.1.Final
com.pj.common.valid.ListValue.message=必须提交指定的值
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {ListValueConstraintValidator.class} //指定校验拦截器,可以指定多个,不同类型的属性,都可以指定不同的拦截器,如果没有配置,则需要在初始化的时候进行校验拦截
)
public @interface ListValue {
String message() default "{com.pj.common.valid.ListValue.message}"; //指定默认提示信息,需要配置一个ValidationMessages.properties配置文件
Class>[] groups() default {}; //分组
Class extends Payload>[] payload() default {};
int[] vals() default {};
}
/**
* 自定义检验拦截器
* @author pengjun
*/
public class ListValueConstraintValidator implements ConstraintValidator{
private Set set = new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
//拿到指定的值,放入set集合中
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 进行拦截校验
* @param integer 属性的传入过来的值
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(integer);
}
}
//@Constraint(validatedBy = {ListValueConstraintValidator.class} //指定校验拦截器,可以指定多个,不同类型的属性,都可以指定不同的拦截器,如果没有配置,则需要在初始化的时候进行校验拦截)
@Data
public class BrandEntity implements Serializable {
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空",groups = {SaveValidGroup.class, UpdateStatusValidGroup.class})
@ListValue(vals = {0,1},message = "显示状态只能为0和1",groups = {SaveValidGroup.class, UpdateStatusValidGroup.class})
private Integer showStatus;
}
/**
* 公共处理异常类
*
* @author
*/
//@ControllerAdvice
//@ResponseBody
@Slf4j
@RestControllerAdvice //ControllerAdvice 和 ResponseBody的合体
public class GulimallExceptionAdvice {
//指定拦截的异常类,子类也会将被拦截
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handlerVException(MethodArgumentNotValidException exception) {
BindingResult bindingResult = exception.getBindingResult();
List fieldErrors = bindingResult.getFieldErrors();
Map map = new HashMap<>();
fieldErrors.forEach(item -> map.put(item.getField(), item.getDefaultMessage()));
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data", map);
}
@ExceptionHandler(value = Throwable.class)
public R handlerException(Throwable throwable) {
log.error("错误:",throwable);
return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
}
}
Elasticsearch官网地址
开发中文文档
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
#进入 es 容器内部 plugins 目录
docker exec -it 容器 id /bin/bash
#进入 /usr/share/elasticesearch/bin 目录,列出系统的分词器
elasticsearch-plugin list
#使用默认
POST _analyze
{
"text": "我是中国人"
}
#使用分词器
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
mkdir es
vi fenci.txt
中国
中国人
#修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml
cd /usr/share/elasticsearch/plugins/ik/config
vi IKAnalyzer.cfg.xml
IK Analyzer 扩展配置
http://192.168.56.10/es/fenci.txt
org.elasticsearch.client
elasticsearch-rest-high-level-client
7.17.10
@Configuration
public class ElasticSearchConfig {
//RequestOptions类包含请求的部分,这些部分应该在同一应用程序中的多个请求之间共享。您可以创建一个singleton实例,并在所有请求之间共享它
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
//权限设置
// builder.addHeader("Authorization", "Bearer " + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}
/**
* 配置连接es客户端
* @return
*/
@Bean
public RestHighLevelClient restHighLevelClient(){
RestClientBuilder builder = RestClient.
builder(new HttpHost("192.168.56.10",9200,"http"));
// ,new HttpHost("192.168.56.10",9200,"http")); 如果有集群可以配置多个
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
/**
* 保存和更新 文档
* 具体API文档地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-document-index.html
*/
@Test
public void saveEsIndex() throws IOException {
//指定索引(库名) 也可以指定 type(表,弃用了) 和 文档id(一行行数据的id)
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
//构造存储的数据
//indexRequest.source("name","zhangsan","age",16,"gender","F");
//构造成map进行存储
// Map map = new HashMap<>();
// map.put("name","zs");
// map.put("age",15);
// map.put("gender","F");
// indexRequest.source(map);
//构造成对象转为json后进行存储
User user = new User();
user.setName("张三");
user.setAge(18);
user.setGender("男");
String s = JSON.toJSONString(user);
//指定存储类型 为JSON 不指定会报错
indexRequest.source(s, XContentType.JSON);
//设置请求超时时间,还可以设置其他的,自己从官网查看
indexRequest .timeout(TimeValue.timeValueSeconds(1));
//发送请求个es存储数据
IndexResponse index = restHighLevelClient.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(index);
}
@Data
class User {
private String name;
private Integer age;
private String gender;
}
/**
* #查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资
*/
@Test
public void searchIndex() throws IOException {
//1. 创建检索请求
SearchRequest searchRequest = new SearchRequest();
//1.1)指定索引
searchRequest.indices("bank");
//1.2)构造检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// QueryBuilders.matchAllQuery(); 查询所有
// sourceBuilder.query(QueryBuilders.matchQuery("address", "Mill"));
sourceBuilder.query(QueryBuilders.matchAllQuery());
//1.2.1)按照年龄分布进行聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
//在ageAgg分组下构建子聚合 求这个年龄段内的男女情况
TermsAggregationBuilder genderAgg = AggregationBuilders.terms("genderAgg").field("gender.keyword");
ageAgg.subAggregation(genderAgg);
//在ageAgg分组下构建子聚合 求这个年龄段的平均工资
AvgAggregationBuilder balanceAvg2 = AggregationBuilders.avg("balanceAvg2").field("balance");
ageAgg.subAggregation(balanceAvg2);
sourceBuilder.aggregation(ageAgg);
// 构造builder
/*
//构造多个聚合
sourceBuilder
.aggregation(AggregationBuilders
.composite("buckets", Arrays.asList(
new TermsValuesSourceBuilder("genderAgg").field("gender.keyword"),
new TermsValuesSourceBuilder("balanceAvg2").field("balance"))));*/
/*
TermsAggregationBuilder genderAgg = AggregationBuilders.terms("genderAgg").field("gender.keyword");
aggregation.aggregation(genderAgg);
AvgAggregationBuilder balanceAvg2 = AggregationBuilders.avg("balanceAvg2");
aggregation.aggregation(balanceAvg2);
*/
//1.2.2)计算查询的数据平均年龄
AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg").field("age");
sourceBuilder.aggregation(ageAvg);
//1.2.3)计算查询的数据的平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
sourceBuilder.aggregation(balanceAvg);
System.out.println("检索条件:" + sourceBuilder);
searchRequest.source(sourceBuilder);
//检索结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println("检索结果:" + searchResponse);
//获取最外层查询结果,包括查询的数据,查询的索引、类型、命中率等
SearchHits hits = searchResponse.getHits();
//获取查询数据
SearchHit[] searchHits = hits.getHits();
for (SearchHit searchHit : searchHits) {
//获取到每一条查询的数据,输出为json串
String sourceAsString = searchHit.getSourceAsString();
//json串转为对象
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println(account);
}
//4. 获取聚合信息
Aggregations aggregations = searchResponse.getAggregations();
//获取聚合名称为:ageAgg 的聚合结果
Terms ageAgg1 = aggregations.get("ageAgg");
//遍历聚合结果,获取每个年龄段下的的人数
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:" + keyAsString + " ==> " + bucket.getDocCount());
//获取子聚合的值
Aggregations aggregations2 = bucket.getAggregations();
Terms genderAgg2 = aggregations2.get("genderAgg");
for (Terms.Bucket genderBucket : genderAgg2.getBuckets()) {
long docCount = genderBucket.getDocCount();
System.out.println(keyAsString + "该年龄的性别为:" + genderBucket.getKeyAsString() + "的人数为:" + docCount);
}
//获取子聚合的值
Avg balanceAvgAggregation = aggregations2.get("balanceAvg2");
System.out.println(keyAsString +"改年龄的平均工资:" + balanceAvgAggregation.getValue());
}
//获取所有人的平均年龄
Avg ageAvg1 = aggregations.get("ageAvg");
System.out.println("平均年龄:" + ageAvg1.getValue());
//获取所有人的平均工资
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValue());
}
//插入数据
PUT my-index-000001/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
//检索数据
GET my-index-000001/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
//删除索引
DELETE my-index-000001
//设置索引映射 嵌入式处理
PUT my-index-000001
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
//插入数据
PUT my-index-000001/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
//重新检索
GET my-index-000001/_search
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
}
}
//获取索引类型映射
GET my-index-000001/_mapping
#ES 检索dtl
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "HUAWEI"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"9",
"12"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"terms": {
"attrs.attrId": [
"15",
"16"
]
}
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"terms": {
"attrs.attrValue": [
"骁龙855"
]
}
}
}
},
{
"term": {
"hasStock": "true"
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 5000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 1,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "",
"post_tags": ""
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 100
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_image_agg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
/**=====================================================
* 商品检索服务dto
* 排序的条件:?keyword="abc"&catalog3Id=123&sort=skuPrice_desc&hasStock=0&skuPrice=1_500&brandId=1&brandId=2&attrs=1_5寸&attrs=2_8GB&pageNum=5
* sort=skuPrice_asc 按照价格排序
* sort=saleCount_desc 按照销量排序
* sort=saleCount_asc
* sort=hasScore_desc 按照热度分排序
* sort=hasScore_asc
* @author pengjun
*/
@Data
public class SearchProductDto {
/**
* 标题查找
*/
private String keyword;
/**
* 三级分类id查找
*/
private Long catalog3Id;
/**
* 排序规则
*/
private String sort;
/**
* 是否只显示有库存的 0无 1有
*/
private Integer hasStock;
/**
* 价格区间 skuPrice=1_500/_500/500_
* 1_500:1-500之间的价格
* _500:小于500的价格
* 500_:大于500的价格
*/
private String skuPrice;
/**
* 品牌。可以选择多个品牌 &brandId=1&brandId=2
*/
private List brandId;
/**
* 多个属性 id_属性值 如 &attrs=1_5寸:8寸&attrs=2_8GB:16G
* 属性id为1的,5寸、8寸的商品
* 属性id为2的,8G和16G的商品
*/
private List attrs;
/**
* 页码
*/
private Integer pageNum = 1;
/**
* 请求参数
*/
private String uri;
}
//=================检索代码=========
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
*
* @param searchProductDto
* @return
*/
@Override
public SearchResult searchProduct(SearchProductDto searchProductDto) {
SearchRequest searchRequest = builderSearchCondition(searchProductDto);
SearchResponse search;
SearchResult searchResult = null;
try {
search = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
searchResult = builderSearchResult(search, searchProductDto);
} catch (IOException e) {
e.printStackTrace();
}
return searchResult;
}
/**
* 构造检索条件 模糊匹配,过滤(按照属性、分类、品牌,价格区间,库存),完成排序、分页、高亮,聚合分析功能
*
* @param searchProductDto
* @return
*/
private SearchRequest builderSearchCondition(SearchProductDto searchProductDto) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//1. 构建bool-query
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//1.1 bool-must 根据副标题,模糊匹配
String keyword = searchProductDto.getKeyword();
if (StringUtils.isNotBlank(keyword)) {
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", keyword));
}
//1.2 bool-fiter
//1.2.1 catelogId 根据分类id检索
Long catalog3Id = searchProductDto.getCatalog3Id();
if (catalog3Id != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", catalog3Id));
}
//1.2.2 brandId 根据品牌id查找
List brandId = searchProductDto.getBrandId();
if (brandId != null && !brandId.isEmpty()) {
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", brandId));
}
//1.2.3 attrs
//attrs=1_5寸:8寸&2_16G:8G
List attrs = searchProductDto.getAttrs();
if (attrs != null && !attrs.isEmpty()) {
for (String attr : attrs) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
String[] s = attr.split("_");
//分类id
String attrId = s[0];
//这个属性检索用的值
String[] attrValues = s[1].split(":");
//一个分类id有多个对应不同的值
boolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
//构建扁平化处理的查询条件数据
boolQueryBuilder.filter(QueryBuilders.nestedQuery("attrs", boolQuery, ScoreMode.None));
}
}
Integer hasStock = searchProductDto.getHasStock();
if (hasStock != null) {
//1.2.4 hasStock
boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", searchProductDto.getHasStock() == 1));
}
//1.2.5 skuPrice
//skuPrice形式为:1_500或_500或500_
String skuPrice = searchProductDto.getSkuPrice();
if (StringUtils.isNotBlank(skuPrice)) {
String[] s = skuPrice.split("_");
//1_500
if (s.length == 2) {
String startPrice = s[0];
String endPrice = s[1];
boolQueryBuilder.filter(QueryBuilders.rangeQuery("skuPrice").gte(startPrice).lte(endPrice));
} else if (s.length == 1) {
String price = s[0];
//_500
if (skuPrice.startsWith("_")) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("skuPrice").lte(price));
} else if (skuPrice.endsWith("_")) {
//500_
boolQueryBuilder.filter(QueryBuilders.rangeQuery("skuPrice").gte(price));
}
}
}
//封装所有的查询条件
searchSourceBuilder.query(boolQueryBuilder);
//构造分页
/**
* 从第几条开始:(页码 - 1 * size)
* 第一页 5 条数据 0,1,2,3,4
* 第二页 5 条数据 5,6,7,8,9
*/
searchSourceBuilder.from((searchProductDto.getPageNum() - 1) * ElasticSearchIndex.PRODUCT_PAGESIZE);
searchSourceBuilder.size(ElasticSearchIndex.PRODUCT_PAGESIZE);
//构造高亮
if (StringUtils.isNotBlank(keyword)) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
searchSourceBuilder.highlighter(highlightBuilder);
}
//排序 形式为sort=hotScore_asc/desc
String sort = searchProductDto.getSort();
if (StringUtils.isNotBlank(sort)) {
String[] s = sort.split("_");
SortOrder sortOrder = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
searchSourceBuilder.sort(s[0], sortOrder);
}
/**
* 聚合分析
*/
//1. 按照品牌进行聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId");
//1.1 品牌的子聚合- 按照品牌id聚合后,在按照品牌名字聚合,一个id对应一个名称
TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
brand_agg.subAggregation(brand_name_agg);
//1.2 品牌的子聚合- 按照品牌id聚合后,在按照品牌图片聚合,一个id对应一个图片
TermsAggregationBuilder brand_image_agg = AggregationBuilders.terms("brand_image_agg").field("brandImg").size(1);
brand_agg.subAggregation(brand_image_agg);
searchSourceBuilder.aggregation(brand_agg);
//2. 按照分类信息进行聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
//2.1 分类信息子聚合-按照分类名称聚合,一个id对应一个分类名称
TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
catalog_agg.subAggregation(catalog_name_agg);
searchSourceBuilder.aggregation(catalog_agg);
//3. 按照属性信息进行聚合
//3.1 进行数据扁平化处理
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//3.2 在扁平化处理之后,在对属性id进行聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId").size(50);
//3.3 在id聚合下 按照名字聚合,一个id对应一个属性名
TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
attr_id_agg.subAggregation(attr_name_agg);
//3.3 在id聚合下 按照属性值聚合,一个id对应多个属性名
TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
attr_id_agg.subAggregation(attr_value_agg);
attr_agg.subAggregation(attr_id_agg);
searchSourceBuilder.aggregation(attr_agg);
System.out.println(searchSourceBuilder.toString());
SearchRequest searchRequest = new SearchRequest(new String[]{ElasticSearchIndex.PRODUCT_INDEX}, searchSourceBuilder);
return searchRequest;
}
//=================检索数据解析=========
/**
* 构建检索之后的数据封装成的对应的数据返回
* @param search
* @param searchProductDto
* @return
*/
private SearchResult builderSearchResult(SearchResponse search, SearchProductDto searchProductDto) {
SearchResult searchResult = new SearchResult();
SearchHits hits = search.getHits();
List skuEsModels = new ArrayList<>();
//1、返回的所有查询到的商品
SearchHit[] datas = hits.getHits();
if (datas != null && datas.length > 0) {
for (SearchHit data : datas) {
//存储的时候就是按照 SkuEsModel 对象存储,可直接转化为对象
String sourceAsString = data.getSourceAsString();
SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//带上keyword才有高亮
if (StringUtils.isNotBlank(searchProductDto.getKeyword())) {
//获取高亮数据
Map highlightFields = data.getHighlightFields();
if (highlightFields != null && highlightFields.size() > 0) {
//获取高亮 skuTitle
HighlightField skuTitle = highlightFields.get("skuTitle");
//获取高亮之后的结果
String highlightSkuTitle = skuTitle.getFragments()[0].string();
skuEsModel.setSkuTitle(highlightSkuTitle);
}
}
skuEsModels.add(skuEsModel);
}
}
searchResult.setProduct(skuEsModels);
//获取聚合信息
Aggregations aggregations = search.getAggregations();
//2、当前商品涉及到的所有属性信息
List attrVos = new ArrayList<>();
ParsedNested attr_agg = aggregations.get("attr_agg");
//获取扁平化处理的聚合信息下的 通过属性id聚合的结果
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
//遍历通过属性id聚合的结果
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//1、得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
attrVo.setAttrId(attrId);
//2、获取通过属性id聚合下的通过属性名字聚合的结果,一个id只会对应一个名称,可以直接.get(0) 得到属性的名字
ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(attrName);
//3、获取通过属性id聚合下的通过属性值聚合的结果,一个id只会对应多个属性值,提取所有的属性值 得到属性的所有值
ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
List attrValues = attr_value_agg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
searchResult.setAttrs(attrVos);
List brandVos = new ArrayList<>();
//3、当前商品涉及到的所有品牌信息
//获取品牌的聚合,遍历聚合信息
ParsedLongTerms brand_agg = aggregations.get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
//获取品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
brandVo.setBrandId(brandId);
//获取品牌id聚合下的子聚合,品牌名称
ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
String brandName = brand_name_agg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandName(brandName);
//获取品牌id聚合下的子聚合,默认图片
ParsedStringTerms brand_image_agg = bucket.getAggregations().get("brand_image_agg");
String brandImage = brand_image_agg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandImg(brandImage);
brandVos.add(brandVo);
}
searchResult.setBrands(brandVos);
//4、当前商品涉及到的所有分类信息
List catalogVos = new ArrayList<>();
//获取分类的聚合,遍历聚合信息
ParsedLongTerms catalog_agg = aggregations.get("catalog_agg");
for (Terms.Bucket bucket : catalog_agg.getBuckets()) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
//获取分类id
long catalogId = bucket.getKeyAsNumber().longValue();
catalogVo.setCatalogId(catalogId);
//获取分类id聚合下的子聚合,分类的名称
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalogName);
catalogVos.add(catalogVo);
}
searchResult.setCatalogs(catalogVos);
//5、分页信息-页码
searchResult.setPageNum(searchProductDto.getPageNum());
//获取分页信息
TotalHits totalHits = hits.getTotalHits();
//获取总记录数
Long total = totalHits.value;
//5、1分页信息、总记录数
searchResult.setTotal(total);
//5、2分页信息-总页码-计算 总记录/size 有余数 总记录/size+1 否则 总记录/size
int totalPages = (int) (total % ElasticSearchIndex.PRODUCT_PAGESIZE == 0 ? total / ElasticSearchIndex.PRODUCT_PAGESIZE : total / ElasticSearchIndex.PRODUCT_PAGESIZE + 1);
searchResult.setTotalPages(totalPages);
//5.3 设置页码
List pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
searchResult.setPageNavs(pageNavs);
//6、面包屑导航数据
if (searchProductDto.getAttrs() != null && searchProductDto.getAttrs().size() > 0) {
List navVos = searchProductDto.getAttrs().stream().map(attr -> {
SearchResult.NavVo navVo = new SearchResult.NavVo();
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
SearchResult.AttrVo attrVo = attrVos.stream().filter(item -> item.getAttrId().equals(Long.valueOf(s[0]))).findFirst().orElse(null);
if (attrVo != null){
navVo.setNavName(attrVo.getAttrName());
}else {
navVo.setNavName("");
}
//2、取消了这个面包屑以后,我们要跳转到哪个地方,将请求的地址url里面的当前的品牌条件置空,剩余的url就是需要取消之后去的地方
//拿到所有的查询条件,去掉当前
String encode = null;
try {
encode= URLEncoder.encode(attr, "UTF-8");//将空格转成 +
encode.replace("+","%20"); //浏览器对空格的编码和Java不一样,差异化处理
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String uri = searchProductDto.getUri();
String replace;
if (uri.contains("&attrs")){
replace = searchProductDto.getUri().replace("&attrs=" + encode, "");
}else {
replace = searchProductDto.getUri().replace("attrs=" + encode, "");
}
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
searchResult.getAttrsId().add(Long.valueOf(s[0]));
return navVo;
}).collect(Collectors.toList());
searchResult.setNavs(navVos);
}
if (searchProductDto.getBrandId() != null && searchProductDto.getBrandId().size() > 0){
List navs = searchResult.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌:");
List brandIds = searchProductDto.getBrandId();
StringBuffer stringBuffer = new StringBuffer();
for (Long brandId : brandIds) {
SearchResult.BrandVo brandVo1 = brandVos.stream().filter(brandVo -> brandVo.getBrandId().equals(brandId)).findFirst().orElse(null);
if (brandVo1 != null){
stringBuffer.append(brandVo1.getBrandName()+";");
String encode = null;
try {
encode= URLEncoder.encode(brandId+"", "UTF-8");//将空格转成 +
encode.replace("+","%20"); //浏览器对空格的编码和Java不一样,差异化处理
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
navVo.setNavValue(stringBuffer.toString());
String uri = searchProductDto.getUri();
String replace;
if (uri.contains("&brandId")){
replace = searchProductDto.getUri().replace("&brandId=" + encode, "");
}else {
replace = searchProductDto.getUri().replace("brandId=" + encode, "");
}
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
navs.add(navVo);
}
}
}
return searchResult;
}
data = cache.load(id);//从缓存加载数据
If(data == null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//保存到 cache 中
}
return data;
注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题
1、本地缓存 只能获取自己部署的服务下的缓存,不能获取其他服务器内存存储的缓存,因为本地缓存存储的是本地服务中的内存。
2、数据一致性的问题:假设数据需要进行更新了。负载到第一个服务器,更新了第一服务器的本地缓存,其余两个节点未更新,下次负载到第二个服务器,导致数据不一致。
org.springframework.boot
spring-boot-starter-data-redis
spring:
redis:
host: 192.168.56.10
port: 6379
# password: **** #未设置密码可不配置
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate(){
ValueOperations ops = stringRedisTemplate.opsForValue();
ops.set("hello","world_"+ UUID.randomUUID().toString());
String hello = ops.get("hello");
System.out.println(hello);
}
注意:lettuce操作redis的客户端,产生堆外内存溢出OutOfDirectMemoryError。SpringBoot2,0以后默认使用 Lettuce作为操作 redis的客户端。它使用 netty进行网络通。lettuce的bug导致netty堆外内存溢出,可设置:-Dio.netty.maxDirectMemory
解决方案:不能直接使用-Dio.netty.maxDirectMemory去调大堆外内存
1)、升级lettuce客户端。 2)、切换使用jedisLettuce、 jedis都是操作 redis的底层客户端。 Spring再次封装 redis Template
/**
* spring是单例模式,所有项目启动就是一个对象,当前类的service都能用,本地缓存(如果有多个商品的微服务,而且占用的是本地内存,缓存就无法共享)
*/
private Map cache = new HashMap<>();
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 本地锁
*
* @return
*/
public Map> catalogLevel23WithLock() {
synchronized (this) {
return catalogLevel23DB();
}
}
private Map> catalogLevel23DB() {
//先从redis缓存找,找到直接返回,否则查询数据库
String catalogLevel23String = redisTemplate.opsForValue().get("catalogLevel23");
if (StringUtils.isNotBlank(catalogLevel23String)) {
Map> result = JSON.parseObject(catalogLevel23String, new TypeReference
#网关配置
- id: gulimall-product-host
uri: lb://gulimall-product
predicates:
#断言请求头匹配,能够匹配上 gulimall.com的请求头的转发到对应服务
- Host=gulimall.com,item.gulimall.com
/**
* redis分布式锁
* 从数据库查找2、3级分类数据
*
* @return
*/
public Map> catalogLevel23RedisLock() {
/**
* 加锁:this,springboot所有的组件在容器中都是单例的,可以锁住
* 使用本地锁出现的问题:假设有多个商品服务的微服务,this锁只能锁住当前的微服务没其他的微服务还是有对应的锁,需要使用分布式锁
*/
//加锁 setIfAbsent:如果存在当前的lock键,就返回false,不存在返回true
//Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "0");
//原子命令:设置值和设置时间,是原子性操作,要么完成要么不完成
String uuid = UUID.randomUUID().toString();
//出现假设4的问题
// Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "0",30, TimeUnit.SECONDS);
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (lock) {
//抢占锁成功
/**
* 出现的问题:
* 假设1:catalogLevel23DB抛出异常,导致锁永远无法删除,其他服务永远无法占用到该锁,一直循环,解决办法:try-finally
* 假设2:catalogLevel23DB执行完毕后,服务器突然断电,还未删除完毕,其他服务永远无法占用到该锁,一直循环;解决办法:可以设置过期时间
* 假设3:在设置锁自动过期时间时,还未设置上时,突然断电,导致为设置上过期时间就退出了,其他服务永远无法占用到该锁,一直循环;解决办法:设置值和设置时间,是原子性操作,要么完成要么不完成
* 假设4:线程1执行业务1的过程超过了lock锁的时间,lock锁就会自动删除,导致其他服务线程2抢占到锁,执行业务1,执行业务逻辑,线程1执行完逻辑后,进行锁的删除操作,结果删除了线程2设置的锁(误删除锁),导致很多线程又可以同时访问业务,可能会多次查询数据库,出现问题(没锁住)。解决办法:只能删除自己的锁,设置一个值为uuid,判断值相同才可以删除,否则不能删除
* 假设5:锁过期时间为10秒,业务逻辑执行了9.5秒,获取lock锁的时间0.3秒,值已经获取到了,在进入 if判读 时,花了0.2秒,此时redis lock锁过期,已经更新成了别的线程的lock值,但此时已经进入了if,此时还是删除掉了别人的锁,导致很多线程又可以同时访问业务,可能会多次查询数据库,出现问题(没锁住)。解决办法:获取值和删除锁需要原子操作,lua脚本和redis命令一起操作。
*
*/
//设置锁自动过期时间 出现假设2、3的问题
//redisTemplate.expire("lock",30, TimeUnit.SECONDS);
//出现假设1的问题
Map> map;
try {
System.out.println("获取redis锁成功=======");
map = catalogLevel23DB(); //redis锁自动续期,暂时没做,在删除之前,锁的过期时间就删除了,需要在业务层面继续续锁,或者把锁的过期时间加长
} finally {
//出现假设4的问题
//redisTemplate.delete("lock");
/*
String lockValue = redisTemplate.opsForValue().get("lock");
//值相同才可以删除:出现假设5的问题
if (uuid.equals(lockValue)){
redisTemplate.delete("lock");
}*/
//lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//执行脚本 返回值为 Long类型,KEYS[1] 对应 lock ARGV[1] 对应uuid的值,如果redis存lock的值和uuid相同,则进行删除,保证了原子性
Long lock1 = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);
}
return map;
} else {
System.out.println("获取redis锁失败成功=======进行重试");
//抢占锁失败
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//自旋的方式获取锁
return catalogLevel23RedisLock();
}
}
官方文档
org.redisson
redisson
3.12.0
/**
* Redisson的配置类
* @author pengjun
*/
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
Config config = new Config();
//设置集群模式
// config.useClusterServers()
// .addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
//设置单个节点模式
//Redis url should start with redis:// or rediss:// (for SSL connection) 配置路径必须配置redis: 开头
// config.useSingleServer().setAddress("192.168.56.10:6379").setPassword();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
@Autowired
RedissonClient redissonClient;
@Test
public void redisson(){
System.out.println(redissonClient);
}
@Autowired
private RedissonClient redissonClient;
/**
* 测试简单请求
* @return
*/
@ResponseBody
@GetMapping({"testHello"})
public String testHello(){
//RLock 集成了Lock api都是一样的
RLock lock = redissonClient.getLock("my-lock");
//redis存储的是 my-lock : uuid:线程id , 看门狗:过期时间是自动续期的(如果业务执行过长,过期时间会自动增加,默认30s)
//加锁的业务只要运行完成,就不会给当前锁续期,即使不手动释放锁,锁默认也在30s以后自动删除
//lock.lock();//阻塞住,只能得到锁才能往下走,其他的都只能等待,我们自己写的redis分布式锁,则是自旋的方式获取锁 阻塞式等待。默认加的锁都是30s
//指定自动解锁时间,10s后自定删除锁,不会自动续期,自动解锁时间一定要大于业务员执行时间,会其他线程抢占到锁,当前线程删除锁时,抛出异常,删除的是别人的锁
lock.lock(10, TimeUnit.SECONDS);
/**
* 1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
* 2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
* 3)、myLock.lock(10,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
* 问题:在锁时间到了以后,不会自动续期
*/
//最佳实战: lock.lock(30, TimeUnit.SECONDS); 省掉了自动续期,30s的业务执行不完,也就是业务有问题
/**
* 1、如果我们传递了锁的时间,就发送redis执行脚本,进行站锁,默认超时就是我们指定的时间
* 2、如果我们未指定锁的超时时间,就是使用 30 * 1000 【lockWatchdogTimeout看门狗的默认时间】
* 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
* 定时任务 internalLockLeaseTime【看门狗的默认时间】 / 3 ,10s中执行一次, 10s后自动续期,续期到看门狗时间
*/
try {
System.out.println("获取锁成功,执行业务需求--"+Thread.currentThread().getName() + Thread.currentThread().getId());
//模拟业务超长,发送两次请求,一个只能等待,执行完成后,另一个才能获取锁
Thread.sleep(30000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//假设:有两个商品服务,一个请求 10000端口,一个请求10001端口,请求10000端口时,突然宕机,还未释放锁,10001端口的线程也能获取到锁,redisson会自己解锁。
lock.unlock();
System.out.println("解锁锁成功"+Thread.currentThread().getName() + Thread.currentThread().getId());
}
return "hello";
}
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 读写锁:保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读就必须等待,读取的是最新的数据
* http://localhost:10000/write http://localhost:10000/read
* 先发送写锁请求,在发送读锁请求,读锁请求需要等待写锁请求写完之后,读锁才可以读取数据,处于阻塞状态
* 读锁可以共享,不会阻塞
*
* 读 + 读 :相当于无锁,只会在redis中记录好,所有当前的读锁,他们都是会同时加锁成功 (模拟:写发送写请求,在四个浏览器窗口发读请求,都一起获取到了锁)
* 读 + 写 :有读锁,写也需要等待 (模拟:发送读请求,在发送写请求,处理读业务20s,才能写入成功)
* 写 + 读 :等待写锁释放,才可以读取
* 写 + 写 :阻塞方式,只能一个个写
* 只要有写的存在,必须等待
*/
/**
* 写锁
* @return
*/
@ResponseBody
@GetMapping({"write"})
public String write(){
String s = UUID.randomUUID().toString();
//获取读写锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
//获取写锁
RLock wLock = readWriteLock.writeLock();
try {
System.out.println("加写锁成功===========");
//上写锁 写锁没释放,读锁就不能读
wLock.lock();
redisTemplate.opsForValue().set("write",s);
//模拟业务执行20s
Thread.sleep(20000);
} catch (Exception e) {
e.printStackTrace();
} finally {
wLock.unlock();
System.out.println("解写锁成功===========");
}
return s;
}
/**
* 读锁
* @return
*/
@ResponseBody
@GetMapping({"read"})
public String read(){
//获取读写锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
//juc的读写锁
// ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//获取写锁
RLock rLock = readWriteLock.readLock();
String s = "";
try {
System.out.println("加读锁成功===========");
//上写锁 写锁没释放,读锁就不能读
rLock.lock();
s = redisTemplate.opsForValue().get("write");
//模拟业务执行20s
Thread.sleep(20000);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("解读锁成功===========");
}
return s;
}
/**
* CountDownLatch :减少计数
* CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
* 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
* 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
*
* CyclicBarrier:循环栅栏
* CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。
* 它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
* 线程进入屏障通过CyclicBarrier的await()方法。
*
* Semaphore信号灯
* 在信号量上我们定义两种操作:
* acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
* release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
* 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
* https://blog.csdn.net/weixin_43947102/article/details/123417002
*/
/**
* 信号量
* 模拟停车位:3个停车位,只能停3辆车,在来了车,就需要等待,其他的开走。
* 三次调用 停车请求,第四调用时,则需等待阻塞,等待开出停车位请求完成,才可以获取到停车位
* 可以做分布式的限流
*/
@ResponseBody
@GetMapping({"park"})
public String park() throws InterruptedException {
// ReentrantLock reentrantLock = new ReentrantLock();
RSemaphore park = redissonClient.getSemaphore("park");
/*
juc信号量
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
semaphore.release();*/
//获取信号量,如果能获取到,则往下执行,获取不到则需要等待
//park.acquire();
//尝试获取一个信号量,如果能够获取到就返回ture,获取不到也不会等待,返回false
boolean b = park.tryAcquire();
if (b){
//获取到了信号量,处理业务需求
}else {
return "error"+ b;
}
return "ok"+ b;
}
/**
* 开出停车位
*/
@ResponseBody
@GetMapping({"go"})
public String go(){
RSemaphore park = redissonClient.getSemaphore("park");
//释放信号量
park.release();
return "ok";
}
/**
* 模拟:5个班级,人全部走完了,门卫才可以关门
* 先访问 lockDoor请求,直接等待,需要访问 gogogo/{id}请求5次之后,lockDoor请求才会继续往下执行
*/
@ResponseBody
@GetMapping({"lockDoor"})
public String lockDoor() throws InterruptedException, BrokenBarrierException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
//JUC:CountDownLatch countDownLatch = new CountDownLatch(5);
/* CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () ->{
System.out.println("七龙珠汇合");
});
cyclicBarrier.await();*/
door.trySetCount(5);
//等待,只有等待5个班级的人走光了才会继续往下执行
door.await();
return "ok";
}
/**
* 班级走
*/
@ResponseBody
@GetMapping({"gogogo/{id}"})
public String gogogo(@PathVariable("id") Integer id) {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
//班级走一个,减一个,减到位0,lockDoo()才会继续往下走
door.countDown();
return "班级"+id+"走";
}
org.springframework.session
spring-session-data-redis
org.springframework.boot
spring-boot-starter-cache
spring:
cache:
type: redis
redis:
time-to-live: 360000 #${random.int} #key的过期时间,单位ms
#key-prefix: CACHE_ #key的所有前缀,如果指定了前缀就用我们指定的前缀,如果没有就默认使用,注解上配置的 key 属性的值作为缓存的名字,以value的值作为分组
#use-key-prefix: true #是否开启前缀,开启了就会添加上述前缀CACHE_缓存名,不开启就只有 缓存名:value
cache-null-values: true #是否缓存空置,防止缓存穿透
#指定缓存的名称,存储缓存时可以从指定的缓存名拿
# cache-names:
//===============================用法========================
//@Cacheable(value = {"category"}) //value 指定在那个区 可以指定多个区内 自定义使用字符串作为key
//@Cacheable(value = {"category"},key = "'myCategory'") //自定义使用字符串作为key,必须加单引号,不加单引号会当做表达式解析
//@Cacheable(value = {"category"},key = "#root.method.name",sync = true) //获取当前方法名作为key sync 加本地锁的缓存
public List queryCategoryLevel1(){}
//@CacheEvict(value = "category" ,key = "'queryCategoryLevel1'") //Redis和MySQL数据同步问题,失效模式:删除key为 queryCategoryLevel 的缓存
// @CacheEvict(value = "category",allEntries = true) //更新了分类数据,将所有category分组下的缓存全部删除 最好开启默认前缀
//@CachePut(value = "category",key = "'catalogLevel23'") //Redis和MySQL数据同步问题,双写模式:更新后,如果当前更新了,方法返回了的对象(可以再次去查找新的分类数据),在把该对象缓存起来
//@CachePut(value = "category",key = "'queryCategoryLevel1'")
@Caching(
//组合多个springcatch注解,evict:标记多个更新删除缓存后,删除多个key
evict = {
@CacheEvict(value = "category",key = "'catalogLevel23'"),
@CacheEvict(value = "category",key = "'queryCategoryLevel1'")
}
)
public void updateCascade(CategoryEntity category){}
//============================ 缓存的配置=======================
/**
* 缓存的配置(包括key和value的格式化,读取配置文件信息)
* @author pengjun
*/
@Configuration
@EnableCaching //开启spring cache 缓存功能
//@EnableConfigurationProperties(CacheProperties.class)
public class MyRedisCacheConfiguration {
// @Autowired
// CacheProperties cacheProperties;
/**
* 1、缓存的配置文件中的bean没有注入到容器中
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties
* 2、要让他注入到容器中
* @EnableConfigurationProperties(CacheProperties.class)
* @return
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
//获取系统默认的缓存配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config.serializeKeysWith都是返回一个new的新的RedisCacheConfiguration,需要使用config = 去接收反回来的新对象,配置才可以全设置上
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//获取缓存的配置文件信息,设置当前的缓存配置应用上配置文件的配置
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//读取配置文件中的配置信息,如果不配置,配置文件配置的信息就会失效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
每一个需要缓存的数据我们都来指定要放到那个分区的缓存。【相当于缓存分区(按照业务类型分,也可以放到多个分组中)】
代表当前方法的结果需要缓存: 如果缓存中有,方法不用调用;如果缓存中没有,就调用方法。先从缓存中获取,缓存中有直接返回,缓存中没有,调用目标方法(查找数据库),在把执行的目标方法返回值存储到缓存中。
默认行为
Key的默人自动生成:缓存的名字::SimpleKey{}(自动生成的key值)。
缓存的value的值,默认使用Jdk序列化机制,将序列化的数据存到redis。
默认ttl时间 为 -1 ,永不过期。
自定义
指定生成的缓存的使用的Key:key属性指定,可以接收一个SpEL表达式。
定缓存的数据的存活时间 :配置文件中修改配置属性(spring.cache.redis.time-to-live: 360000 #${random.int} #key的过期时间,单位ms)。
将数据保存为Json格式:需要自定义 缓存管理器。
Spring-Cache的不足:CacheManager (RedisCacheManager) -> Cache(RedisCache)
读模式:
解决:加随机过期时间, SpringCache 配置文件 time-to-live: 360000 #${random.int}。
缓存雪崩:大量的key同时过期。
解决:加锁,默认是无锁的,sync = true 就是加锁状态,是本地锁,但不影响(多服务也就多查几次),不是分布式锁。没指定sync = true, RedisCache 就不会调用同步的 synchronized 的方法指定了sync = true 会调用 RedisCache 的 public synchronized
缓存击穿:大量并发进来同时查询一个数据,此时该数据整好达到过期时间,请求直接达到数据库。
解决:缓存空数据;springCache 配置文件 cache-null-values: true
缓存穿透:查询一个数据库和Redis都不存在的数据,每次请求都进入数据库查询
写模式:(缓存与数据库一致,SpringCache没有管)
读写加锁:读多,写少的情况加锁。缓存的数据本就不应该是实时性、一致性要求超高的,遇到实时性、一致性要求高的数据,就应该查数据库。
引入Canal,感知到MySQL的更新去更新数据库(使用Canal订阅Binlog的方式)。
读多写多,直接去数据库查询就行。
读多写少读数据直接用SpringCache,写数据可以加读写锁,读多写多直接查数据库
总结:
常规数据(读多写少,及时性,一致性要求不高的数据),可以使用SpringCatch。
写模式:只要设置缓存时间即可。
特殊数据(及时性、一致性要求搞,又要求缓存的数据)特殊设计。
自定义缓存管理器原理:CacheAutoConfiguration -> RedisCacheConfiguration ->自动配置了 RedisCacheManager -> 初始化所有的缓存设置的名称(指定缓存的名称,存储缓存时可以从指定的缓存名拿 cache-names: ) -> 每个缓存决定使用什么配置 -> 如果RedisCacheConfiguration 有就用自己的配置,没有就用默认配置->想改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration 即可->就会应用到当前 RedisCacheManager 管理的所有缓存分区中。
public class Thread01 extends Thread {
@Override
public void run() {
System.out.println("Thread01"+Thread.currentThread().getId() + "执行了");
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
Thread thread = new Thread01();
thread.start();
System.out.println("main......end.....");
}
public class Runnable01 implements Runnable{
@Override
public void run() {
System.out.println("Runnable01"+Thread.currentThread().getId() + "执行了");
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
Runnable01 runnable01 = new Runnable01();
new Thread(runnable01).start();
System.out.println("main......end.....");
}
public class Callable01 implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("Callable01"+Thread.currentThread().getId() + "执行了");
return 2;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
FutureTask futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
System.out.println(futureTask.get());//获取结果 会暂停当前线程 获取到线程结果后继续往下执行
System.out.println("main......end.....");
}
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
executor.execute(new Runnable01());
Future submit = executor.submit(new Callable01());
//阻塞状态,没获取到结果,就会阻塞主线程
Integer integer = submit.get();
System.out.println("main......start....." + integer);
}
/**
* 线程池开启线程
* 详细查看此文章:https://blog.csdn.net/weixin_43947102/article/details/123417002
*/
public void threadPool() {
//固定的线程数 core = max
Executors.newFixedThreadPool(10);
//缓存线程池 core =0 ;max = Integer.MAX_VALUE
Executors.newCachedThreadPool();
//定时线程池 core = 10 ;max = Integer.MAX_VALUE
Executors.newScheduledThreadPool(10);
//一个线程数 core = max = 1
Executors.newSingleThreadExecutor();
/*
自定义线程池的七个参数:
public ThreadPoolExecutor(int corePoolSize,// 线程池中的常驻核心线程数 除非设置了 allowCoreThreadTimeOut
int maximumPoolSize, //线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
long keepAliveTime,// 多余的控线线程的存活时间 当前线程池中数量超过corePoolSize时,当空闲时间达到KeepAliveTime时,多余线程会被销毁直到只剩下corePooleSize个线程为止
TimeUnit unit, //KeepAliveTime的单位
BlockingQueue workQueue,// 阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了 corePoolSize大小,就会放在这里等待空闲线程执行。
ThreadFactory threadFactory, //表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可,比如指定线程名等
RejectedExecutionHandler handler)//拒绝策略,表示当队列名满了,并且工作线程大于等于线程池的最大线程数时,如何来拒绝请求执行的runnable的策略
}
*/
}
corePoolSize:池中一直保持的线程的数量,即使线程空闲。除非设置了allowCoreThreadTimeOut
maximumPoolSize:pool池中允许的最大的线程数
keepAliveTime:当线程数大于核心线程数的时候,线程在最大多长时间没有接到新任务就会终止释放,最终线程池维持在 corePoolSize 大小
unit:时间单位
workQueue:阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了 corePoolSize大小,就会放在这里等待空闲线程执行。
threadFactory:创建线程的工厂,比如指定线程名等
handler:拒绝策略,如果线程满了,线程池就会使用拒绝策略。
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
/**
* 没有返回值的异步回调
*/
CompletableFuture future0 = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
/**
* 有返回值的,且抛出异常后的方法的处理
*/
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res,exception) -> {
//res 返回结果 没异常返回null exception:异常信息 有异常无法回结果
//虽然能得到异常信息,但是没法修改返回数据
System.out.println("异步任务成功完成了...结果是:" + res + "异常是:" + exception);
}).exceptionally(throwable -> {
//可以感知异常,同时返回默认值,抛出异常返回默认值
return 10;
});
//获取结果,也会阻塞主线程
System.out.println(future.get()); //10
System.out.println("main......end.....");
}
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
/**
* 统一处理结果和异常信息
*/
CompletableFuture future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).handle((result,thr) -> {
//直接处理异常和结果,不需要上面那样分开处理
if (result != null) {
return result * 2;
}
if (thr != null) {
System.out.println("异步任务成功完成了...结果是:" + result + "异常是:" + thr);
return 0;
}
return 0;
});
//获取结果,也会阻塞主线程
System.out.println(future2.get()); //抛出异常返回0 否则返回10
System.out.println("main......end.....");
}
/**
* 线程串行化:一步一步执行 run、accept、apply 就这三种,记住
* 1、thenRunAsync:不能获取上一步的执行结果,无返回值,继续调用其他业务
* 2、thenAcceptAsync:能接受上一步结果,但是无返回值
* 3、thenApplyAsync:能接受上一步结果,有返回值
*
*/
CompletableFuture future3 = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
System.out.println("任务2启动了..." + res);
return "Hello" + res;
}, executor);
System.out.println("main......end....." + future3.get());
/**
* 两个线程都完成了任务后在完成另外一个任务
*/
CompletableFuture
CompletableFuture f1 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("任务1运行结果:" + i);
return i;
}, executor);
CompletableFuture f2 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
try {
Thread.sleep(3000);
System.out.println("任务2运行结果:");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
}, executor);
/**
* 两个任务有一个执行就执行任务3
*/
//任务1或者2完成一个任务,任务3就执行, 不能获取任务1、2的返回值结果,任务3本身无返回值结果
f1.runAfterEitherAsync(f2,()->{
System.out.println("任务3线程:" + Thread.currentThread().getId());
},executor);
//任务1或者2完成一个任务,任务3就执行, 能获取任务1、2先执行完任务的返回值结果,任务3本身无返回值结果
/* f1.acceptEitherAsync(f2,(res)->{
System.out.println("任务3线程:" + Thread.currentThread().getId() + "====》上次任务执行结果"+res);
},executor);*/
//任务1或者2完成一个任务,任务3就执行, 能获取任务1、2先执行完任务的返回值结果,任务3有返回值结果
/* CompletableFuture f3 = f1.applyToEitherAsync(f2, (res) -> {
System.out.println("任务3线程:" + Thread.currentThread().getId() + "====》上次任务执行结果" + res);
return res + "hehe";
}, executor);
System.out.println(f3.get());
/**
* 所有异步任务都处理完后才可以往下走 默认线程池为守护线程,主线程结束就会结束
*/
CompletableFuture f11 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取商品默认图片");
return "hehe.jpg";
}, executor);
CompletableFuture f12 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取商品属性");
return "256G + 8G";
}, executor);
CompletableFuture f13 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
System.out.println("获取商品分类");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "华为";
}, executor);
/* CompletableFuture allOf = CompletableFuture.allOf(f11, f12, f13);
//阻塞等待所有的异步线程执行完成,才能往下执行
allOf.get();
//获取结果
System.out.println(f11.get()+"->"+f12.get()+"->"+f13.get());*/
CompletableFuture anyOf = CompletableFuture.anyOf(f11, f12, f13);
//只有有一个异步线程完成后,就可以继续往下执行
Object o = anyOf.get();
System.out.println(o);
System.out.println("main......end....." + future3.get());
@Test
public void tests(){
//无盐的MD5
String s = DigestUtils.md5Hex("123456");
System.out.println(s);
//随机盐值
System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));
//指定盐值加密
System.out.println(Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq"));
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
//$2a$10$cR3lis5HQQsQSSh8/c3L3ujIILXkVYmlw28vLA39xz4mHDN/NBVUi
//盐值加密 使用同一个铭文 加密出来的数据都不同
String encode = bCryptPasswordEncoder.encode("123456");
//使用加密后的密文 进行验证 无需存储盐值 盐值已经存在了加密后的密文中 会自动算出盐值后匹配
boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");
System.out.println(encode+"==>" + matches);
}
-- 返回值 access_token:用于调用API 传递的token uid:用户id expires_in过期时间
{
"access_token": "xxxxxxxxx",
"remind_in": "157679999",
"expires_in": 157679999,
"uid": "6541850998",
"isRealName": "true"
}
@Slf4j
@Controller
@RequestMapping("/oauth2.0")
public class Oauth2Controller {
@Autowired
private MemberFeignService memberFeignService;
/**
* 微博回调地址
* @param code
* @param session
* @return
*/
@SneakyThrows
@GetMapping("/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) {
//1、通过 code 换取accessToken
Map map = new HashMap<>();
map.put("client_id", "xxxx");//自己的app key
map.put("client_secret", "xxx");//自己的 app secret
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
HttpResponse post = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "POST", new HashMap<>(), map, new HashMap<>());
if (post.getStatusLine().getStatusCode() == 200) {
String json = EntityUtils.toString(post.getEntity());
//获取accessToken
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
//知道了哪个社交用户
//1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息,以后这个社交账号就对应指定的会员)
//登录或者注册这个社交用户
System.out.println(socialUser.getAccess_token());
//调用远程服务 方法在下面
R oauthLogin = memberFeignService.oauth2Login(socialUser);
if (oauthLogin.getCode() == 0) {
MemberResponseVo data = oauthLogin.getData(new TypeReference() {
});
log.info("登录成功:用户信息:{}", data.toString());
//1、第一次使用session,命令浏览器保存卡号,JSESSIONID这个cookie 具体看 谷粒商城-分布式基础-图.pdf
//以后浏览器访问哪个网站就会带上这个网站的cookie
//子域之间:gulimall.com auth.gulimall.com order.gulimall.com
//发卡的时候(指定域名为父域名),即使是子域系统发的卡,也能让父域直接使用
//TODO 1、默认发的令牌。当前域(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象到Redis中
// new Cookie("JSESSOINUD","AAA").setDomain(".gulimall.com"); //这样吧cookie设置域之后 对应的域名都能够使用了当前这个cookie值
session.setAttribute(AuthServerConstant.LOGIN_USER, data);
//2、登录成功跳回首页
return "redirect:http://gulimall.com";
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
}
}
/**
* 微博社交登陆
* @param socialUser
* @return
*/
@SneakyThrows
@Override
public MemberEntity login(SocialUser socialUser) {
//具有登录和注册逻辑
String uid = socialUser.getUid();
//1、判断当前社交用户是否已经登录过系统
MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper().eq("social_uid", uid));
if (memberEntity != null) {
//这个用户已经注册过
//更新用户的访问令牌的时间和access_token
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
//2、没有查到当前社交用户对应的记录我们就需要注册一个
MemberEntity register = new MemberEntity();
//3、查询当前社交用户的社交账号信息(昵称、性别等)
Map query = new HashMap<>();
query.put("access_token",socialUser.getAccess_token());
query.put("uid",socialUser.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap(), query);
if (response.getStatusLine().getStatusCode() == 200) {
//查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
String profileImageUrl = jsonObject.getString("profile_image_url");
register.setNickname(name);
register.setGender("m".equals(gender)?1:0);
register.setHeader(profileImageUrl);
register.setCreateTime(new Date());
register.setSocialUid(socialUser.getUid());
register.setAccessToken(socialUser.getAccess_token());
register.setExpiresIn(socialUser.getExpires_in());
//把用户信息插入到数据库中
this.baseMapper.insert(register);
}
return register;
}
}
优点: web-server(Tomcat)原生支持,只需要修改配置文件。
缺点:
session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力。
任意一台web-server保存的数据都是所有web- server的session总和,受到内存限制无法水平扩展更多的web-server。
大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可。
优点:服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源。
缺点:都是缺点,这只是一种思路。具体如下
每次http请求,携带用户在cookie中的完整信息,浪费网络带宽。
session数据放在cookie中,cookie有长度限制4K,不能保存大量信息。
session数据放在cookie中,存在泄漏、篡改、窃取等安全隐患。
hash一致性
优点:
只需要改nginx配置,不需要修改应用代码
负载均衡,只要hash属性的值分布是均匀的,多台web-server的负载是均衡的
可以支持web-server水平扩展(session同步法是不行的,受内存限制)
缺点
session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录。
如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session
但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用。
优点:
没有安全隐患。
可以水平扩展,数据库/缓存水平切分即可。
web-server重启或者扩容都不会有session丢失。
不足
增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替
换为从Redis查数据的方式。redis获取数据比内存慢很多。
上面缺点可以用SpringSession完美解决
/**
*子域之间:gulimall.com auth.gulimall.com order.gulimall.com
*这样吧cookie设置域之后 对应的域名都能够使用了当前这个cookie值 设置父级域名即可,子域名即可拥有
* springsession 都可解决
**/
new Cookie("JSESSOINUD","AAA").setDomain(".gulimall.com");
官方地址
官方案例文档
org.springframework.session
spring-session-data-redis
# Session 存储方式 以Redis缓存存储 需要配置Redis连接用户名密码等
spring.session.store-type=redis
# Session超时时间
server.servlet.session.timeout= 30m
# Sessions 刷新策略
spring.session.redis.flush-mode=on_save
# Sessions 前缀命名中心
spring.session.redis.namespace=spring:session
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* 配置springsession
* @author Administrator
*/
@Configuration
public class GulimallSessionConfig {
/**
* 设置spring session cookie的作用范围
* @return
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//提高cookie的作用域 gulimall.com结尾的都能用 可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。
cookieSerializer.setDomainName("gulimall.com");
//更改cookie的名字
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
/**
* redis 存储序列化
* @return
*/
@Bean
public RedisSerializer redisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
@EnableRedisHttpSession //整合redis作为session的存储
public class GulimallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallAuthServerApplication.class, args);
}
}
服务端代码
登录页
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
/**
* 从缓存中获取用户信息
* @param token
* @return
*/
@ResponseBody
@GetMapping("/userinfo")
public String userinfo(@RequestParam(value = "token") String token) {
String s = redisTemplate.opsForValue().get(token);
return s;
}
/**
* 跳转到登录页,如果有cookie 证明有人登陆过,就直接跳转url页面
* @param url 需要转发的url
* @param model
* @param sso_token 有人登陆过,登陆后会存储 sso_token的cookie值,证明以及登陆路了系统,就无须去往登录页登陆,直接携带 token 转发到 url上 ,没人登陆过跳转到登陆页面
* @return
*/
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model, @CookieValue(value = "sso_token", required = false) String sso_token) {
if (!StringUtils.isEmpty(sso_token)) {
//之前有人登陆过,浏览器留下了痕迹
return "redirect:" + url + "?token=" + sso_token;
}
model.addAttribute("url", url);
return "login";
}
/**
* 登陆接口
* @param username 用户名
* @param password 密码
* @param url 登陆成功后需要转发的url
* @param response
* @return
*/
@PostMapping(value = "/doLogin")
public String doLogin(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("redirect_url") String url, HttpServletResponse response) {
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
String uuid = UUID.randomUUID().toString().replace("_", "");
//生成令牌 表示登陆成功
redisTemplate.opsForValue().set(uuid, username);
//设置cookie 当前服务器 就有了这个cookie 证明就登陆过了
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
//登录成功跳转 携带当前的uuid 到对应的转发地址
return "redirect:" + url + "?token=" + uuid;
}
//跳回到登录页
return "login";
}
}
@Controller
public class HelloController {
/**
* 无需登录就可访问
*
* @return
*/
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
return "hello";
}
/**
*
* @param model
* @param session 获取登陆信息
* @param token 是否携带token 参数 携带了 就证明登陆过了 未携带证明还未登陆直接转发到 gulimall-test-sso-server 的登录页
* @return
*/
@GetMapping(value = "/employees")
public String employees(Model model, HttpSession session, @RequestParam(value = "token", required = false) String token) {
if (!StringUtils.isEmpty(token)) {
RestTemplate restTemplate=new RestTemplate();
//调用 gulimall-test-sso-server 服务的方法 通过token获取对应的Redis中的存储的值
ResponseEntity forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userinfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser", body);
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null) {
//redirect_url 跳转到登陆服务器后,登陆成功后的 在跳回当前域名上
return "redirect:" + "http://ssoserver.com:8080/login.html"+"?redirect_url=http://client1.com:8081/employees";
} else {
List emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
}
@Controller
public class HelloController {
/**
* 无需登录就可访问
*
* @return
*/
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
return "hello";
}
/**
*
* @param model
* @param session 获取登陆信息
* @param token 是否携带token 参数 携带了 就证明登陆过了 未携带证明还未登陆直接转发到 gulimall-test-sso-server 的登录页
* @return
*/
@GetMapping(value = "/boss")
public String boss(Model model, HttpSession session, @RequestParam(value = "token", required = false) String token) {
if (!StringUtils.isEmpty(token)) {
RestTemplate restTemplate=new RestTemplate();
//调用 gulimall-test-sso-server 服务的方法 通过token获取对应的Redis中的存储的值
ResponseEntity forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userinfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser", body);
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null) {
//redirect_url 跳转到登陆服务器后,登陆成功后的 在跳回当前域名上
return "redirect:" + "http://ssoserver.com:8080/login.html"+"?redirect_url=http://client2.com:8082/boss";
} else {
List emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
}
/**
* 在执行目标方法之前,判断用户的登录状态.并封装传递给controller目标请求
* @author Administrator
*/
public class CartInterceptor implements HandlerInterceptor {
/**
* 存储每个线程保存的 登录人信息
* 一个请求 CartInterceptor ---> Controller ---> Service 都是同一个线程 可以直接在后面调用时通过线程直接获取对应的用户信息 UserInfoTo info = toThreadLocal.get();
*/
public static ThreadLocal toThreadLocal = new ThreadLocal<>();
/***
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
//获得当前登录用户的信息
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (memberResponseVo != null) {
//用户登录了
userInfoTo.setUserId(memberResponseVo.getId());
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
//user-key
String name = cookie.getName();
if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
userInfoTo.setUserKey(cookie.getValue());
//标记为已是临时用户
userInfoTo.setTempUser(true);
}
}
}
//如果没有临时用户一定分配一个临时用户
if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//目标方法执行之前
toThreadLocal.set(userInfoTo);
return true;
}
/**
* 业务执行之后,分配临时用户来浏览器保存
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//获取当前用户的值
UserInfoTo userInfoTo = toThreadLocal.get();
//如果没有临时用户一定保存一个临时用户
if (!userInfoTo.getTempUser()) {
//创建一个cookie
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
//扩大作用域
cookie.setDomain("gulimall.com");
//设置过期时间
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
/**
* @Description: 注册拦截器 拦截路径
**/
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor())//注册拦截器
.addPathPatterns("/**");
}
}
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p
25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
org.springframework.boot
spring-boot-starter-amqp
spring:
rabbitmq:
host: 192.168.56.10
port: 5672
#指定虚拟主机
virtual-host: /
password: admin
username: admin
#开启java发送消息到mq服务器的确认回调 java发送消息 -> 交换机上收到消息后的确认回调
publisher-confirms: true
#开启消息抵达队列确认回调 交换机 -> 消息未发送到队列上的回调
publisher-returns: true
template:
#只要抵达队列,就以异步发送优先回调我们的回调函数
mandatory: true
listener:
simple:
#消费者消费消息后,主动通知 mq服务端进行了消息消费,我们手动告知mq服务端将此消息移除 默认是auto 自动应答,消费完成mq服务器自动移除
acknowledge-mode: manual
@Autowired
private AmqpAdmin amqpAdmin;
/**
* amq 交换机代码创建
*/
@Test
public void createExchanges(){
//DirectExchange(String name, boolean durable, boolean autoDelete)
//direct类型的 交换机名称 是否持久化(mq重启后是否需要存在) 是否自动删除(没有绑定队列后,自动删除)
DirectExchange directExchange = new DirectExchange("hello-java-exchanges",true,false);
//创建交换机
amqpAdmin.declareExchange(directExchange);
log.info("{}创建成功","hello-java-exchanges");
}
/**
* amq 队列代码创建
*/
@Test
public void createQueue(){
//Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments)
//队列名称 是否持久化(队列重启后是否需要存在) 是否排他的(只能由一条连接使用 其他连接无法使用 独占) 是否自动删除(没有消息就自动删除)
Queue queue = new Queue("hello-java-queue",true,false,false);
//创建交换机
amqpAdmin.declareQueue(queue);
log.info("{}创建成功","hello-java-queues");
}
/**
* amq 队列绑定交换机
*/
@Test
public void queueBingingExchanges(){
//Binding(String destination【目的地】, Binding.DestinationType destinationType,【目的类型】 String exchange【交换机】, String routingKey【路由key】, Map arguments【自定义参数】)
//将 exchange指定的交换机和 destination目的地进行绑定,使用 routingKey 作为指定的路由
Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE,"hello-java-exchanges","hello.java",null);
//创建交换机
amqpAdmin.declareBinding(binding);
log.info("{}交换机绑定{}队列成功","hello-java-exchanges","hello-java-queues");
// amqpAdmin.deleteExchange();
// amqpAdmin.deleteQueue()
}
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* amq 发送消息
*/
@Test
public void sendSms(){
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(111L);
orderReturnReasonEntity.setCreateTime(new Date());
orderReturnReasonEntity.setSort(12);
orderReturnReasonEntity.setStatus(1);
//将换机名称 路由名称 消息值
// rabbitTemplate.convertAndSend("hello-java-exchanges","hello.java","Hello Word");
//发送的消息是对象 会使用序列号写出去 所以对象必须实现 Serializable 发送消息默认是jdk序列化 不是json 可以配置 MessageConverter 消息转换器(配置成Jackson2JsonMessageConverter就是json序列化)
for (int i = 0; i < 10; i++) {
if (i%2==0){
orderReturnReasonEntity.setName("AAA" + i);
rabbitTemplate.convertAndSend("hello-java-exchanges","hello.java",orderReturnReasonEntity);
}else {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchanges","hello.java",orderEntity);
}
}
log.info("消息发送成功");
}
public class OrderReturnReasonEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* 退货原因名
*/
private String name;
/**
* 排序
*/
private Integer sort;
/**
* 启用状态
*/
private Integer status;
/**
* create_time
*/
private Date createTime;
}
public class OrderEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* 订单号
*/
private String orderSn;
}
/**
* mq 的配置类
*/
@Configuration
public class MyRabbitConfig {
/**
* 如果容器中有 converter的组件 就用我们自己的 如果没有默认使用 SimpleMessageConverter 进行数据序列化
* 如果有 就用我们自己指定的进行序列化
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
@RabbitListener(queues = {"hello-java-queue"})
@Service("mqMessageService")
public class MqMessageServiceImpl extends ServiceImpl implements MqMessageService {
/**
* MQ监听消息
* @param
* @return
* queues:指定监听那个队列 可以监听多个 只要收到消息,队列删除消息,而且只能有一个收到此消息
* 1、Message msg:原生的消息详细信息内容 包括头和体
* 2、T<发送的消息类型>:内容,会根据发送的消息类型自动跳转不同的方法
* 3、通道 channel 当前传输的数据的通道
*
* 场景:
* 1、假设是集群部署当前项目 都有这段代码 同一个消息只能由一个服务消费
* 2、只有一个消息处理完成之后(方法执行完)才能继续接受下一个消息
*
* RabbitListener (可以标记在方法(表示当前方法监听队列)和类上(配合 RabbitHandler,整个类中标记了改注解的都会监听指定的队列)):指定监听的队列,当发送的消息类型不同时,可以使用RabbitHandler标记不同的方法,接收的类型不同,进入的监听方法就不同
* RabbitHandler 只能标记在方法上,用于监听接收同一个队列中的不同的消息类型的消息
*/
// @RabbitListener(queues = {"hello-java-queue"})
@RabbitHandler()
public void recieveMessage(Message msg, OrderReturnReasonEntity orderReturnReasonEntity, Channel channel) throws InterruptedException {
//(Body:'{"id":111,"name":"AAA","sort":12,"status":1,"createTime":1673935752934}' MessageProperties [headers={__TypeId__=com.pj.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchanges, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-UxdUZmW8q_vtHgHDtYww-Q, consumerQueue=hello-java-queue])
// byte[] body = msg.getBody();
//org.springframework.amqp.core.Message
System.out.println("接收到消息....消息内容" + msg +"类型:"+ msg.getClass());
Thread.sleep(3000);
System.out.println(orderReturnReasonEntity);
//channel内按顺序自增的序列
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag = " + deliveryTag);
//手动应答消息 告知MQ服务器 当前消息进行了消费 false 非批量模式
try {
if (deliveryTag % 2 == 0) {
channel.basicAck(deliveryTag,false);
System.out.println("签收成功!");
}else {
//long deliveryTag, boolean multiple, boolean requeue
//手动拒绝签收消息 消息自增序列号 是否批量签收 签收未成功是否重新入队
//如果重新入队了 当前会继续接收被拒绝且重新入队的消息 重新入队的消息的 deliveryTag 会继续累加
//如果手动拒绝签收 不重新入队列 就会将此消息丢弃
channel.basicNack(deliveryTag,false,true);
//long deliveryTag, boolean requeue
//消息自增序列号 签收未成功是否重新入队
// channel.basicReject(deliveryTag,false);
System.out.println("没有签收!" + deliveryTag);
}
} catch (IOException e) {
//网络中断异常
e.printStackTrace();
}
}
@RabbitHandler()
public void recieveMessage(Message msg, OrderEntity orderEntity, Channel channel) throws InterruptedException {
System.out.println("接收到消息....消息内容" + msg +"类型:"+ msg.getClass());
Thread.sleep(3000);
System.out.println(orderEntity);
}
}
spring:
rabbitmq:
host: 192.168.56.10
port: 5672
#指定虚拟主机
virtual-host: /
password: admin
username: admin
#开启java发送消息到mq服务器的确认回调 java发送消息 -> 交换机上收到消息后的确认回调
publisher-confirms: true
#开启消息抵达队列确认回调 交换机 -> 消息未发送到队列上的回调
publisher-returns: true
template:
#只要抵达队列,就以异步发送优先回调我们的回调函数
mandatory: true
listener:
simple:
#消费者消费消息后,主动通知 mq服务端进行了消息消费,我们手动告知mq服务端将此消息移除 默认是auto 自动应答,消费完成mq服务器自动移除
acknowledge-mode: manual
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.annotation.PostConstruct;
/**
* mq 的配置类
*/
@Configuration
public class MyRabbitConfig {
private RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
/**
* 如果容器中有 converter的组件 就用我们自己的 如果没有默认使用 SimpleMessageConverter 进行数据序列化
* 如果有 就用我们自己指定的进行序列化
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 定制 rabbitTemplate
*
* @PostConstruct //java自己提供的 该注解被用来修饰一个非静态的void方法,被修饰的方法会在服务器加载servlet时候运行,并且当前对象被初始化之后的回调函数,且只会被服务器执行一次,该注解在构造函数之后执行,inti()方法之前执行
*/
public void initRabbitTemplate() {
/**
* 消息发送成功以后,基于生产者的消息回执,来确保生产者的可靠性
* 1、服务收到消息就会回调
* 开启java发送消息到mq服务器的确认回调 java发送消息 -> mq服务器上收到消息后的确认回调
* 1、spring.rabbitmq.publisher-confirms: true
* 2、设置确认回调
* 2、消息正确抵达队列就会进行回调
* 开启消息抵达队列确认回调 mq服务器 -> 发送消息到队列上后的确认回调
* 1、spring.rabbitmq.publisher-returns: true
* 只要抵达队列,就以异步发送优先回调我们的回调函数
* spring.rabbitmq.template.mandatory: true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
* listener:
* simple:
* #消费者消费消息后,主动通知 mq服务端进行了消息消费,我们手动告知mq服务端将此消息移除 默认是auto 自动应答,消费完成mq服务器自动移除
* acknowledge-mode: manual
* 1、默认是自动确认模式,当消费者消费消息后,客户端会自动像服务器发送确认消息,服务端就会移除这个消息
* 问题:收到很多个消息,自动回复给服务器ack,只有一个消息成功,客户端宕机了,其他消息还未进行消费,但又是服务器自动确认的方式,导致消息丢失
* 解决:手动确认:当客户端消费消息后,需要自己手动通知mq服务器将此消息移除,我们如果没有明确告知MQ,当前消息被消费了,消息就一直处于unacked状态,即使客户端宕机了,消息也不会丢失,消息会重新变为Ready状态,下次有客户端连接这个队列,消息将会通知到这个客户端
* long deliveryTag = msg.getMessageProperties().getDeliveryTag(); channel内按顺序自增的序列
* channel.basicAck(deliveryTag,false); 手动应答消息 告知MQ服务器 当前消息进行了消费 false 非批量模式
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id) 发送消息时可以设置此值,如果需要记录,可以存储到数据库中,记录状态值
* ack:消息是否成功收到
* cause:失败的原因
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
System.out.println("setConfirmCallback" + correlationData + ";ack = " + ack + ";cause=" + cause);
});
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* message:投递失败的消息详细信息
* replyCode:回复的状态码
* replyText:回复的文本内容
* exchange:当时这个消息发给哪个交换机
* routingKey:当时这个消息用哪个路邮键
*/
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("Fail Message[" + message + "]==>replyCode[" + replyCode + "]" +
"==>replyText[" + replyText + "]==>exchange[" + exchange + "]==>routingKey[" + routingKey + "]");
});
rabbitTemplate.setMandatory(true);
}
}
@RequestMapping("sendMessage")
public String sendMessage(@RequestParam(value = "number",defaultValue = "10") Integer number){
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(111L);
orderReturnReasonEntity.setCreateTime(new Date());
orderReturnReasonEntity.setSort(12);
orderReturnReasonEntity.setStatus(1);
//将换机名称 路由名称 消息值 消息的唯一值主键
// rabbitTemplate.convertAndSend("hello-java-exchanges","hello.java","Hello Word");
//发送的消息是对象 会使用序列号写出去 所以对象必须实现 Serializable 发送消息默认是jdk序列化 不是json 可以配置 MessageConverter 消息转换器(配置成Jackson2JsonMessageConverter就是json序列化)
for (int i = 0; i < number; i++) {
if (i%2==0){
orderReturnReasonEntity.setName("AAA" + i);
//CorrelationData 给每个消息设置一个唯一id
rabbitTemplate.convertAndSend("hello-java-exchanges","hello.java",orderReturnReasonEntity,new CorrelationData(UUID.randomUUID().toString()));
}else {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
// rabbitTemplate.convertAndSend("hello-java-exchanges","hello.java",orderEntity);
//测试发送消息失败 触发回调函数
rabbitTemplate.convertAndSend("hello-java-exchanges","hello22.java",orderEntity,new CorrelationData(UUID.randomUUID().toString()));
}
}
return "ok";
}
@RabbitListener(queues = {"hello-java-queue"})
@Service("mqMessageService")
public class MqMessageServiceImpl extends ServiceImpl implements MqMessageService {
/**
* MQ监听消息
* @param
* @return
* queues:指定监听那个队列 可以监听多个 只要收到消息,队列删除消息,而且只能有一个收到此消息
* 1、Message msg:原生的消息详细信息内容 包括头和体
* 2、T<发送的消息类型>:内容,会根据发送的消息类型自动跳转不同的方法
* 3、通道 channel 当前传输的数据的通道
*
* 场景:
* 1、假设是集群部署当前项目 都有这段代码 同一个消息只能由一个服务消费
* 2、只有一个消息处理完成之后(方法执行完)才能继续接受下一个消息
*
* RabbitListener (可以标记在方法(表示当前方法监听队列)和类上(配合 RabbitHandler,整个类中标记了改注解的都会监听指定的队列)):指定监听的队列,当发送的消息类型不同时,可以使用RabbitHandler标记不同的方法,接收的类型不同,进入的监听方法就不同
* RabbitHandler 只能标记在方法上,用于监听接收同一个队列中的不同的消息类型的消息
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
* listener:
* simple:
* #消费者消费消息后,主动通知 mq服务端进行了消息消费,我们手动告知mq服务端将此消息移除 默认是auto 自动应答,消费完成mq服务器自动移除
* acknowledge-mode: manual
* 1、默认是自动确认模式,当消费者消费消息后,客户端会自动像服务器发送确认消息,服务端就会移除这个消息
* 问题:收到很多个消息,自动回复给服务器ack,只有一个消息成功,客户端宕机了,其他消息还未进行消费,但又是服务器自动确认的方式,导致消息丢失
* 解决:手动确认:当客户端消费消息后,需要自己手动通知mq服务器将此消息移除,我们如果没有明确告知MQ,当前消息被消费了,消息就一直处于unacked状态,即使客户端宕机了,消息也不会丢失,消息会重新变为Ready状态,下次有客户端连接这个队列,消息将会通知到这个客户端
* long deliveryTag = msg.getMessageProperties().getDeliveryTag(); channel内按顺序自增的序列
* channel.basicAck(deliveryTag,false); 手动应答消息 告知MQ服务器 当前消息进行了消费 false 非批量模式
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id) 发送消息时可以设置此值,如果需要记录,可以存储到数据库中,记录状态值
* ack:消息是否成功收到
* cause:失败的原因
*/
// @RabbitListener(queues = {"hello-java-queue"})
@RabbitHandler()
public void recieveMessage(Message msg, OrderReturnReasonEntity orderReturnReasonEntity, Channel channel) throws InterruptedException {
//(Body:'{"id":111,"name":"AAA","sort":12,"status":1,"createTime":1673935752934}' MessageProperties [headers={__TypeId__=com.pj.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchanges, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-UxdUZmW8q_vtHgHDtYww-Q, consumerQueue=hello-java-queue])
// byte[] body = msg.getBody();
//org.springframework.amqp.core.Message
System.out.println("接收到消息....消息内容" + msg +"类型:"+ msg.getClass());
Thread.sleep(3000);
System.out.println(orderReturnReasonEntity);
//channel内按顺序自增的序列
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag = " + deliveryTag);
//手动应答消息 告知MQ服务器 当前消息进行了消费 false 非批量模式
try {
if (deliveryTag % 2 == 0) {
channel.basicAck(deliveryTag,false);
System.out.println("签收成功!");
}else {
//long deliveryTag, boolean multiple, boolean requeue
//手动拒绝签收消息 消息自增序列号 是否批量签收 签收未成功是否重新入队
//如果重新入队了 当前会继续接收被拒绝且重新入队的消息 重新入队的消息的 deliveryTag 会继续累加
//如果手动拒绝签收 不重新入队列 就会将此消息丢弃
channel.basicNack(deliveryTag,false,true);
//long deliveryTag, boolean requeue
//消息自增序列号 签收未成功是否重新入队
// channel.basicReject(deliveryTag,false);
System.out.println("没有签收!" + deliveryTag);
}
} catch (IOException e) {
//网络中断异常
e.printStackTrace();
}
}
@RabbitHandler()
public void recieveMessage(Message msg, OrderEntity orderEntity, Channel channel) throws InterruptedException {
System.out.println("接收到消息....消息内容" + msg +"类型:"+ msg.getClass());
Thread.sleep(3000);
System.out.println(orderEntity);
}
}
消息级别的TTL:当发布消息时,可以为特定的消息设置TTL。这意味着这条消息从进入队列开始只有指定的时间可以存活。如果在这段时间内消息没有被消费,则消息会从队列中被移除。
队列级别的TTL:可以为整个队列设置默认的TTL。这会影响进入该队列的所有消息,进入该队列的消息的过期时间就是队列设置的TTL。如果消息没有单独设置TTL,它会继承队列的TTL。如果队列中的消息设置了不同的TTL,每条消息都会根据其自身的TTL被独立处理。
消息A在10:00进入队列,TTL为5分钟。
消息B在10:03进入队列,TTL为2分钟。
在10:05时,消息A会过期并从队列中移除;而消息B会在10:05之前的10:05时过期并移除。
这就是说,即使两条消息都在队列中,它们也可以有不同的过期时间,并根据各自的TTL独立地被处理。
/**
* mq 创建队列和交换机 和绑定关系 死信和延时队列
* @author Administrator
*/
@Configuration
public class MyRabbitMQConfig {
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name, 交换机名称
* boolean durable, 持久化
* boolean autoDelete, 是否自动删除
* Map arguments 参数
* */
return new TopicExchange("order-event-exchange", true, false);
}
/**
* 死信队列
* @Beab 直接把创建的队列或者交换机或者绑定关系 创建到mq服务器上,如果mq服务器上有不会再次创建(属性变化的也不会更改,只能删除后重新创建)
* @return
*/@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map arguments) 属性
*/
HashMap arguments = new HashMap<>();
//死信队列
arguments.put("x-dead-letter-exchange", "order-event-exchange");
//路由键
arguments.put("x-dead-letter-routing-key", "order.release.order");
//消息过期时间
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange, 交换机
* String routingKey, 路由key
* Map arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 测试监听 order.release.order.queue 消息
*/
@RabbitListener(queues = "order.release.order.queue")
public void listenerReleaseOrderQueue(OrderEntity orderEntity, Channel channel, Message message){
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
System.out.println("收到订单消息,需要关闭的订单信息为:"+ orderEntity);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
/**
* 商品秒杀队列
* @return
*/
@Bean
public Queue orderSecKillOrrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
/**
* 监听随便的队列 使创建出对应的交换机和队列 没有消费者 不会自动创建
* @param message
*/
@RabbitListener(queues = "stock.release.stock.queue")
public void handle(Message message) {
}
}
/**
* 测试发送消息 1分钟后,消息过期,进入死信队列,监听死信队列的消费消息
* @return
*/
@ResponseBody
@GetMapping(value = "/test/createOrder")
public String createOrderTest() {
//订单下单成功
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给MQ发送消息 路由key order.create.order
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "ok";
}
CREATE TABLE `mq_message` (
`message_id` char(32) NOT NULL,
`content` text,
`to_exchane` varchar(255) DEFAULT NULL,
`routing_key` varchar(255) DEFAULT NULL,
`class_type` varchar(255) DEFAULT NULL,
`message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`message_id`)
)
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】 0:校验失败 1:校验(删除)通过
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
//主启动类打上注解
@EnableAspectJAutoProxy(exposeProxy = true) //开启了aspect动态代理模式,对外暴露代理对象
@EnableRedisHttpSession //开启springsession
@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = GlobalTransactionAutoConfiguration.class)
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
/**
* 本地事务回顾
* @param
* @return
*/
@Transactional(propagation = Propagation.REQUIRED) //事务的传播行为 REQUIRED 使用当前方法的对象 REQUIRES_NEW 使用新的事务
public void a(){
//问题:同一个对象内事务方法互调默认失效,都是使用a的事务(如果想使用b、c方法设置的事务传播行为,就得使用代理对象调用),原因:事务是需要代理对象进行实现,直接调用绕过了代理对象 无法实现
b(); //使用a的事务 a抛出异常 b也回滚
c(); //不使用a的事务成 自己创建新的事务 抛出异常 c不回滚
//需要代理对象调用其方法后,事务配置才可生效
// bService.a();
// cService.c();
/**
* 如果需要自己本类互调事务生效 需要使用到本类的代理对象 调用方法
* 引入 starter-aop 开启 @EnableAspectJAutoProxy(exposeProxy = true) 所有的动态代理都是 AspectJ代理出来的对象(即使没有接口也可以创建动态代理)
* exposeProxy = true :对外暴露代理对象
*/
//使用aop直接获取当前类的代理对象
OrderServiceImpl o = (OrderServiceImpl) AopContext.currentProxy();
o.a();
o.c();
}
@Transactional(propagation = Propagation.REQUIRED)
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {
}
seata官方文档
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-all
io.seata
seata-all
1.4.1
#registry.config
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 配置注册中心
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
serverAddr = "127.0.0.1"
namespace = ""
group="DEFAULT_GROUP"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3 配置seata文件加载路径
type = "file"
nacos {
serverAddr = "127.0.0.1"
namespace = ""
group="DEFAULT_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
apolloAccesskeySecret = ""
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
transport {
# tcp, unix-domain-socket
type = "TCP"
#NIO, NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = false
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThreadPrefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
## transaction log store, only used in seata-server 日志保存记录位置
store {
## store mode: file、db、redis
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "mysql"
password = "mysql"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
#配置seata分组
service {
vgroup_mapping.my-test-tx-group = "default"
##default.grouplist = "127.0.0.1:8091"
}
## server configuration, only used in server side
server {
recovery {
#schedule committing retry period in milliseconds
committingRetryPeriod = 1000
#schedule asyn committing retry period in milliseconds
asynCommittingRetryPeriod = 1000
#schedule rollbacking retry period in milliseconds
rollbackingRetryPeriod = 1000
#schedule timeout retry period in milliseconds
timeoutRetryPeriod = 1000
}
undo {
logSaveDays = 7
#schedule delete expired undo_log in milliseconds
logDeletePeriod = 86400000
}
#check auth
enableCheckAuth = true
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
maxCommitRetryTimeout = "-1"
maxRollbackRetryTimeout = "-1"
rollbackRetryTimeoutUnlockEnable = false
}
## metrics configuration, only used in server side
metrics {
enabled = false
registryType = "compact"
# multi exporters use comma divided
exporterList = "prometheus"
exporterPrometheusPort = 9898
}
#配置setat配置文件
spring:
cloud:
alibaba:
seata:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: "DEFAULT_GROUP"
namespace: "public"
username: "nacos"
password: "nacos"
# 事务分组配置(在v1.5之后默认值为default_tx_group) 事务组的命名不要用下划线’_‘,可以用’-'因为在seata的高版本中使用underline下划线 将导致service not to be found。
tx-service-group: my-test-tx-group
enabled: true
service:
#指定事务分组至集群映射关系(等号右侧的集群名需要与Seata-server注册到Nacos的cluster保持一致)
vgroup_mapping:
my-test-tx-group: default
@Configuration
public class MySeataConfig {
/* @Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}*/
@Autowired
DataSourceProperties dataSourceProperties;
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
* 将现有的数据源 使用seata 数据源进行包装
* @param dataSourceProperties
* @return
*/
@Bean
@Primary
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
正向代理是一个位于客户端和目标服务器之间的代理服务器(中间服务器)。为了从原始服务器取得内容,客户端向代理服务器发送一个请求,并且指定目标服务器,之后代理向目标服务器转交并且将获得的内容返回给客户端。正向代理的情况下客户端必须要进行一些特别的设置才能使用。
反向代理正好相反。对于客户端来说,反向代理就好像目标服务器。并且客户端不需要进行任何设置。客户端向反向代理发送请求,接着反向代理判断请求走向何处,并将请求转交给客户端,使得这些内容就好似他自己一样,一次客户端并不会感知到反向代理后面的服务,也因此不需要客户端做任何设置,只需要把反向代理服务器当成真正的服务器就好了。
正向代理是代理客户端,为客户端收发请求,使真实客户端对服务器不可见;而反向代理是代理服务器端,为服务器收发请求,使真实服务器对客户端不可见。
从上面的描述也能看得出来正向代理和反向代理最关键的两点区别:
是否指定目标服务器
客户端是否要做设置
用一张图来表示两者的差异:正向代理中,proxy和client同属一个LAN,对server透明; 反向代理中,proxy和server同属一个LAN,对client透明。 实际上proxy在两种代理中做的事都是代为收发请求和响应,不过从结构上来看正好左右互换了下,所以把前者那种代理方式叫做正向代理,后者叫做反向代理。
"废话文学解释":
正向代理:A同学在大众创业、万众创新的大时代背景下开启他的创业之路,目前他遇到的最大的一个问题就是启动资金,于是他决定去找马云爸爸借钱,可想而知,最后碰一鼻子灰回来了,情急之下,他想到一个办法,找关系开后门,经过一番消息打探,原来A同学的大学老师王老师是马云的同学,于是A同学找到王老师,托王老师帮忙去马云那借500万过来,当然最后事成了。不过马云并不知道这钱是A同学借的,马云是借给王老师的,最后由王老师转交给A同学。这里的王老师在这个过程中扮演了一个非常关键的角色,就是代理,也可以说是正向代理,王老师代替A同学办这件事,这个过程中,真正借钱的人是谁,马云是不知道的,这点非常关键。
大家都有过这样的经历,拨打10086客服电话,可能一个地区的10086客服有几个或者几十个,你永远都不需要关心在电话那头的是哪一个,叫什么,男的,还是女的,漂亮的还是帅气的,你都不关心,你关心的是你的问题能不能得到专业的解答,你只需要拨通了10086的总机号码,电话那头总会有人会回答你,只是有时慢有时快而已。那么这里的10086总机号码就是我们说的反向代理。客户不知道真正提供服务人的是谁。
#配置到http块下
#上游服务器路径
upstream gulimall{
#转发至网关,由网关转发到指定的服务
server 192.168.56.1:80;
}
server {
listen 80;
#会读取请求头中携带的host 路径进行域名匹配,多个域名映射同一个上游路径
server_name gulimall.com search.gulimall.com item.gulimall.com auth.gulimall.com cart.gulimall.com order.gulimall.com member.gulimall.com 4448227kv1.imdo.co;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /payed/ {
#接收内网穿透穿过来的 host主机头不匹配,导致无法转发指定服务,只要是/payed开始的请求,直接转到订单服务商,需要指定固定的订单的host主机头,转发到对应的服务上
proxy_set_header Host order.gulimall.com;
#路由到指定路径和端口
#proxy_pass http://192.168.56.1:10000;
#路由到指定upstream上,需要在server块之上配置
proxy_pass http://gulimall;
}
#static请求路径转发到Nginx的html目录下,实现动静分离,一个请求只会对应一个location处理,当前location能处理的就不会往下走
location /static/{
root /usr/share/nginx/html;
}
location / {
#Nginx转发请求时会丢失头部信息,需要自己设置头部信息 网关需要使用对于的请求头信息进行路由跳转
proxy_set_header Host $host;
#proxy_pass http://192.168.56.1:10000;
#路由到指定upstream上,需要在server块之上配置
proxy_pass http://gulimall;
}
}
spring:
cloud:
gateway:
routes:
#客户端发送请求->nginx转发到网关(会丢失头信息,需要在Nginx进行设置)->网关接收请求通过断言请求头信息,转发到对应服务
- id: gulimall-product-host
uri: lb://gulimall-product
predicates:
#断言请求头匹配访问的域名地址,能够匹配上 gulimall.com,item.gulimall.com的请求头的转发到对应服务
- Host=gulimall.com,item.gulimall.com
官方开发文档
com.aliyun.oss
aliyun-sdk-oss
3.15.1
@Test
public void uploadYuanShengTest() {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写,创建OOS实例桶后的Endpoint。
String endpoint = "自己的endpoint ";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。 不是阿里云登陆密码,需要自己创建。
String accessKeyId = "自己的accessKeyId ";
String accessKeySecret = "自己的accessKeySecret ";
// 填写 Bucket 名称,例如examplebucket。
String bucketName = "自己创建OOS桶实例";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。 文件名
String objectName = "2e3a0b5ee45d446193118ecd6d987df5.png";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath = "文件全路径名.文件后缀";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
com.alibaba.cloud
aliyun-oss-spring-boot-starter
// application.properties
alibaba.cloud.access-key=your-ak
alibaba.cloud.secret-key=your-sk
alibaba.cloud.oss.endpoint=***
@Autowired
public OSSClient ossClient;
//使用springcloud alibaba 的API上传 配置文件配置如下内容:
/**
* alicloud:
* oss:
* sts:
* access-key: **
* security-token: **
* endpoint: **
*/
@Test
public void uploadAlibabaTest() {
String bucketName = "自己创建的oss实例桶名称";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "上传文件名";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath ="文件全路径地址.后缀";
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
/**
* https://help.aliyun.com/document_detail/31926.html
* 服务端签名后直传
*
* @return
*/
@Autowired
OSS ossClient;
@RequestMapping("/oss/policy")
protected R policy() {
// 填写Host地址,格式为https://bucketname.endpoint。
String host = "https://" + bucket + "." + endpoint;
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format + "/";
Map respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap();
respMap.put("accessId", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
System.out.println(e.getMessage());
}
return R.ok().put("data",respMap);
}
阿里云短信服务开通链接
public static void main(String[] args) {
String host = "https://cxkjsms.market.alicloudapi.com";
String path = "/chuangxinsms/dxjk";
String method = "POST";
String appcode = "你自己的AppCode";//开通服务后 买家中心-查看AppCode
Map headers = new HashMap();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map querys = new HashMap();
querys.put("content", "【创信】你的验证码是:5873,3分钟内有效!");
querys.put("mobile", "13800138001");
Map bodys = new HashMap();
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map headers,
Map querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map headers,
Map querys,
Map bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List nameValuePairList = new ArrayList();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map headers,
Map querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map headers,
Map querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map headers,
Map querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map headers,
Map querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map headers,
Map querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
@Override
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
spring:
cloud:
alicloud:
sms:
host: https://fesms.market.alicloudapi.com
path: /sms/
skin: 1
sign: 175622
appcode: xxxxxxxxx
@ConfigurationProperties(value = "spring.cloud.alicloud.sms")
@Data
@Component
public class SmsComponent {
private String host;
private String path;
private String skin;
private String sign;
private String appcode;
public void sendCode(String phone,String code) {
String method = "GET";
Map headers = new HashMap();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE xxxxx
headers.put("Authorization", "APPCODE " + appcode);
Map querys = new HashMap();
querys.put("code", code);
querys.put("phone", phone);
querys.put("skin", skin);
querys.put("sign", sign);
//JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 或者直接下载:
* http://code.fegine.com/HttpUtils.zip
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
* 相关jar包(非pom)直接下载:
* http://code.fegine.com/aliyun-jar.zip
*/
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
//System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。
//状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
//获取response的body
System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ResponseBody
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone) {
//1、接口防刷
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(redisCode)) {
//活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
long currentTime = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - currentTime < 60000) {
//60s内不能再发
return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
}
}
//2、验证码的再次效验 redis.存key-phone,value-code
int code = (int) ((Math.random() * 9 + 1) * 100000);
String codeNum = String.valueOf(code);
String redisStorage = codeNum + "_" + System.currentTimeMillis();
//存入redis,防止同一个手机号在60秒内再次发送验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, redisStorage, 10, TimeUnit.MINUTES);
//远程调用第三方服务发送短信
thirdPartFeignService.sendCode(phone, codeNum);
return R.ok();
}
蚂蚁金服开放平台
开发者文档
沙箱支付(支付宝官方Demo)
package com.alipay.config;
import java.io.FileWriter;
import java.io.IOException;
/* *
*类名:AlipayConfig
*功能:基础配置类
*详细:设置帐户有关信息及返回路径
*修改日期:2017-04-05
*说明:
*以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
*该代码仅供学习和研究支付宝接口使用,只是提供一个参考。
*/
public class AlipayConfig {
//↓↓↓↓↓↓↓↓↓↓请在这里配置您的基本信息↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
public static String app_id = "自己的沙箱支付应用id";
// 商户私钥,您的PKCS8格式RSA2私钥
public static String merchant_private_key = "自己的私钥";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
public static String alipay_public_key = "支付宝公钥";
// 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String notify_url = "服务器异步通知";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//支付宝成功支付之后跳转的地址
public static String return_url = "支付宝成功支付之后跳转的地址";
// 签名方式
public static String sign_type = "RSA2";
// 字符编码格式
public static String charset = "utf-8";
// 支付宝网关
public static String gatewayUrl = "支付宝网关";
// 支付宝日志
public static String log_path = "C:\\";
//↑↑↑↑↑↑↑↑↑↑请在这里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
/**
* 写日志,方便测试(看网站需求,也可以改成把记录存入数据库)
* @param sWord 要写入日志里的文本内容
*/
public static void logResult(String sWord) {
FileWriter writer = null;
try {
writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis()+".txt");
writer.write(sWord);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
//获得初始化的AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
//设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(AlipayConfig.return_url);
alipayRequest.setNotifyUrl(AlipayConfig.notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = new String(request.getParameter("WIDout_trade_no").getBytes("ISO-8859-1"),"UTF-8");
//付款金额,必填
String total_amount = new String(request.getParameter("WIDtotal_amount").getBytes("ISO-8859-1"),"UTF-8");
//订单名称,必填
String subject = new String(request.getParameter("WIDsubject").getBytes("ISO-8859-1"),"UTF-8");
//商品描述,可空
String body = new String(request.getParameter("WIDbody").getBytes("ISO-8859-1"),"UTF-8");
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
//若想给BizContent增加其他可选请求参数,以增加自定义超时时间参数timeout_express来举例说明
//alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
// + "\"total_amount\":\""+ total_amount +"\","
// + "\"subject\":\""+ subject +"\","
// + "\"body\":\""+ body +"\","
// + "\"timeout_express\":\"10m\","
// + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
//请求参数可查阅【电脑网站支付的API文档-alipay.trade.page.pay-请求参数】章节
//请求
String result = alipayClient.pageExecute(alipayRequest).getBody();
//输出
out.println(result);
/* *
* 功能:支付宝服务器同步通知页面
* 日期:2017-03-30
* 说明:
* 以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
* 该代码仅供学习和研究支付宝接口使用,只是提供一个参考。
*************************页面功能说明*************************
* 该页面仅做页面展示,业务逻辑处理请勿在该页面执行
*/
//获取支付宝GET过来反馈信息
Map params = new HashMap();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.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] + ",";
}
//乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type); //调用SDK验证签名
//——请在这里编写您的程序(以下代码仅作参考)——
if(signVerified) {
//商户订单号
String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");
//支付宝交易号
String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");
//付款金额
String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8");
out.println("trade_no:"+trade_no+"
out_trade_no:"+out_trade_no+"
total_amount:"+total_amount);
}else {
out.println("验签失败");
}
//——请在这里编写您的程序(以上代码仅作参考)——
//获得初始化的AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
//设置请求参数
AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
//商户订单号,商户网站订单系统中唯一订单号
String out_trade_no = new String(request.getParameter("WIDTCout_trade_no").getBytes("ISO-8859-1"),"UTF-8");
//支付宝交易号
String trade_no = new String(request.getParameter("WIDTCtrade_no").getBytes("ISO-8859-1"),"UTF-8");
//请二选一设置
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," +"\"trade_no\":\""+ trade_no +"\"}");
//请求
String result = alipayClient.execute(alipayRequest).getBody();
//输出
out.println(result);
com.alipay.sdk
alipay-sdk-java
4.9.28.ALL
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
/**
非对称加密: 加密解密使用不同钥匙
支付宝 -> 商家 支付宝一把公钥(alipay_public_key) 商家一把私钥(merchant_private_key) 支付宝给商家数据时,会带上公钥, 只能由私钥解开 给支付宝的数据
商家 -> 支付宝 支付宝一把私钥(未知的) 商家一把公钥(公钥配置在支付宝账号中),商家给支付宝发送数据时,会携带公钥, 需有支付宝私钥解密才能解开,但支付宝私钥永远不知道,其他人是无法处理数据的
*/
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
public String app_id;
// 商户私钥,您的PKCS8格式RSA2私钥
public String merchant_private_key;
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
public String alipay_public_key;
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
public String notify_url;
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
public String return_url;
// 签名方式
private String sign_type;
// 字符编码格式
private String charset;
//订单超时时间
private String timeout = "1m";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
public String gatewayUrl;
public String pay(PayVo vo) throws AlipayApiException {
//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
//付款金额,必填
String total_amount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\""+timeout+"\"," //相对超时时间 距离当前时间的timeout分钟后,订单失效,就无法支付 属于关单状态 更多的参数可以参考支付宝的文档 https://opendocs.alipay.com/open/028r8t?pathHash=8e24911d&ref=api&scene=22
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
}
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
#支付宝相关的配置
alipay:
app_id: 应用id
merchant_private_key: 商户私钥
alipay_public_key:支付宝公钥
#通知地址 必须外网可以正常访问
notify_url: https://4448227kv1.imdo.co/payed/notify
#支付成功后返回地址
return_url: http://member.gulimall.com/memberOrder.html
sign_type: RSA2
charset: utf-8
gatewayUrl: https://openapi.alipaydev.com/gateway.do
@Autowired
private AlipayTemplate alipayTemplate;
@Autowired
private OrderService orderService;
/**
* 用户下单:支付宝支付
* 1、让支付页让浏览器展示
* 2、支付成功以后,跳转到用户的订单列表页
* @param orderSn
* @return
* @throws AlipayApiException
* produces = MediaType.TEXT_HTML_VALUE 告诉返回的是HTML页面
*/
@ResponseBody
@GetMapping(value = "/aliPayOrder", produces = MediaType.TEXT_HTML_VALUE)
public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
//构造payVo 直接模板调用支付请求 渲染页面(就是支付页)
PayVo payVo = orderService.getOrderPay(orderSn);
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
/**
* @Description: 订单支付成功监听器
**/
@RestController
public class OrderPayedListener {
@Autowired
private OrderService orderService;
@Autowired
private AlipayTemplate alipayTemplate;
//支付宝回调 接口 使用了内网穿透(配置了查看图片:支付宝回调地址调用请求,转发至Nginx,Nginx设置请求头后转发到网关,网关根据host信息设置跳转对应的服务.png):https://4448227kv1.imdo.co/payed/notify
@PostMapping(value = "/payed/notify")
public String handleAlipayed(PayAsyncVo asyncVo,HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
// 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
// 获取支付宝POST过来反馈信息
System.out.println("收到支付宝的异步通知 " + asyncVo);
/*Map parameterMap = request.getParameterMap();
parameterMap.keySet().forEach(item ->{
System.out.println(item + " : " + request.getParameter(item));
});*/
//TODO 需要验签
Map params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
//TODO 需要验签 验证是不是支付宝发的请求 防止恶意修改其他订单数据
if (signVerified) {
System.out.println("签名验证成功...");
//去修改订单状态
String result = orderService.handlePayResult(asyncVo);
return result;
}else {
System.out.println("签名验证失败...");
return "error";
}
}
}
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date notify_time;
private String subject;
private String sign;
private String buyer_id;//支付者的id
private String body;//订单的信息
private String invoice_amount;//支付金额
private String version;
private String notify_id;//通知id
private String fund_bill_list;
private String notify_type;//通知类型; trade_status_sync
private String out_trade_no;//订单号
private String total_amount;//支付的总额
private String trade_status;//交易状态 TRADE_SUCCESS
private String trade_no;//流水号
private String auth_app_id;//
private String receipt_amount;//商家收到的款
private String point_amount;//
private String app_id;//应用id
private String buyer_pay_amount;//最终支付的金额
private String sign_type;//签名类型
private String seller_id;//商家的id
}
HPS(Hits Per Second) :每秒点击次数,单位是次/秒。
TPS(Transaction per Second):系统每秒处理交易数(整个业务执行完成),单位是笔/秒。
QPS(Query per Second):系统每秒处理查询次数,单位是次/秒。对于互联网业务中,如果某些业务有且仅有一个请求连接,那么 TPS=QPS=HPS,一 般情况下用 TPS 来衡量整个业务流程,用 QPS 来衡量接口查询次数,用 HPS 来表示对服务器单击请求。