虚拟化容器计数,Docker基于镜像,可以秒级启动各种容器,每一种容器都是一个完整的运行环境,容器之间相互隔离;
安装前卸载原有的docker
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
安装yum-utils
yum install -y yum-utils
设置阿里云镜像仓库地址
yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
安装docker相关引擎
yum makecache fase
yum install docker-ce docker-ce-cli containerd.io
#启动docker
systemctl start docker
#查看docker种正在运行的容器
docker ps
#查看本机的docker镜像
docker images
#使用阿里云镜像加速
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://kskdqwg1.mirror.aliyuncs.com"]
}
EOF
systemctl daemon-reload
systemctl 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
#重启mysql
docker restart mysql
#创建目录结构
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
#安装redis
docker pull redis
#启动redis
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
#运行redis
docker exec -it redis redis-cli
#设置redis持久化
cd /mydata/redis/conf
vi redis.conf
#修改如下属性
appendonly yes
# 将docker里的目录挂载到linux的/mydata目录中
# 修改/mydata就可以改掉docker里的
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
# es可以被远程任何机器访问
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
# 递归更改权限,es需要访问
chmod -R 777 /mydata/elasticsearch/
# 9200是用户交互端口 9300是集群心跳端口
# -e指定是单阶段运行
# -e指定占用的内存大小,生产时可以设置32G
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
# 设置开机启动elasticsearch
docker update elasticsearch --restart=always
es的可视化工具kibana
# kibana指定了了ES交互端口9200 # 5600位kibana主页端口
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.116.128:9200 -p 5601:5601 -d kibana:7.4.2
# 设置开机启动kibana
docker update kibana --restart=always
ik分词器
#首先需要下架wget命令
yum install wget
#下载分词器
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
#解压文件
unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
#移动到目标文件夹
mv ik plugins/
#修改权限
chmod -R 777 plugins/ik
#重启容器
docker restart elasticsearch
#删除安装包
rm -rf elasticsearch-analysis-ik-7.4.2.zip
docker run -p80:80 --name nginx -d nginx:1.10
复制nginx
删除拷贝用的nginx
移动文件夹
创建html和logs文件夹
启动nginx
#启动nginx
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
#设置开机自启
docker update nginx --restart=always
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
开机自启 它的账号密码默认为guest
docker update rabbitmq --restart=always
导入的依赖管理:
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.6.RELEASE
pom
import
导入注册中心环境
1.导入依赖: (导入依赖后需要下载一个nacos的压缩包,startup脚本开启nacos)
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
2.在服务的配置文件中配置nacos注册中心的地址
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
3. 开启服务发现功能,在springBoot项目主类上添加@EnableDiscoveryClient注解
4. Nacos注册中心端口号为localhost:8848/nacos; 账号密码都为nacos
5. 在配置文件中配置当前服务的名字(至此已经将当前服务配置到nacos注册中心了)
spring:
application:
name: gulimall-coupon
导入配置中心环境
1.导入依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
2.作为配置中心的时候,编写配置文件需要在bootstrap配置文件中编写
#此文件会优先于application.properties来加载
#配置中心的信息
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
3.进入Nacos后点击配置列表后的+增加配置,配置的名字默认为项目名.properties,在这里我们添加需要的配置:
4.在代码中我们使用@Value即可操作配置中心里的值,联合注解@RefreshScope这样就可以实现项目在发布上线以后,不修改代码重启项目也能做到一些值的更新
@RestController
@RequestMapping("coupon/coupon")
@RefreshScope//用于刷新的时候动态更改配置中心发布的内容
public class CouponController {
@Value("${coupon.user.name}")
private String name;
@Value("${coupon.user.age}")
private String age;
@RequestMapping("/test")
public R test(){
return R.ok().put("name",name).put("age",age);
}
}
命名空间: 用于不同环境的配置区分隔离,比如开发环境和生产环境的资源隔离
1.新建的配置默认都属于public命名空间,我们可以新建多个命名空间来进行环境隔离
2.在配置文件中通过如下属性和对应的uuid来启用命名空间
配置集:所有配置的集合
配置集ID:类似于文件名,也就是新建配置时输入的Data ID
配置分组:默认所有的配置集都属于Default_Group组
同样的通过如下属性来读入组内容,0 1 2索引对应相应的配置文件,如果搜寻不到nacos里的分组,就会搜索本地的文件加载本地的配置
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# 可以选择对应的命名空间 # 写上对应环境的命名空间ID
spring.cloud.nacos.config.namespace=b176a68a-6800-4648-833b-be10be8bab00
# 更改配置分组
spring.cloud.nacos.config.group=dev
spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true
spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true
spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true
它是一个声明式的HTTP客户端,提供了HTTP请求的模板,通过编写简单的接口和插入注解就可以定义好HTTP请求的参数,格式,地址等信息,Feign整合了Ribbon(负载均衡)和Hystrix(服务熔断),可以让我们不再需要显示的使用这两个组件;
1. 引入依赖
org.springframework.cloud
spring-cloud-starter-openfeign
2.开启feign功能,在SpringBoot主类上使用@EnableFeignClient注解,可以在该注解的属性package中指定接口的位置
3.声明式远程接口,使用@FeignClient("这里填的是需要调用的远程服务名")注解,声明一个远程接口,注意远程接口的路径要写全
@FeignClient("gulimall-coupon")//告诉springcloud这里需要调用远程服务
public interface CouponFeignService {
//远程接口,如果以后这个方法被调用,那么就会去调用coupon里的对应方法
@RequestMapping("coupon/coupon/member/list")
public R memberCouponEntity();
}
网关作为流量的入口常用功能包括路由转发,权限校验,限流控制;它有三个核心概念:
路由:一个路由由一个标识的id,一个目标的URI地址,一个断言的集合和一个过滤器的集合构成;只要断言为真,路由就能到指定服务
断言:判断路由到哪个服务的判断条件
过滤器:在请求前和请求后都可以通过过滤器对请求进行修改
网关环境导入
1.网关作为一个单独的模块,也需要把自己注册到配置中心和注册中心(配置方法如上)
2.网关的xml依赖配置如下:
com.wuyimin.gulimall
gulimall-common
0.0.1-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
3.由于在gulimall-common模块中我们配置了数据源相关的操作,所以我们要排除此操作
//开启服务的注册发现(配置注册中心地址)
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallGatewayApplication.class, args);
}
}
4.yml配置路由
spring:
cloud:
gateway:
routes:
#优先级比下面的哪个路由要高所以要放在上面,不然会被截断
- id: admin_route
#lb表示负载均衡
uri: lb://renren-fast
#规定前端项目必须带有一个api前缀
#原来验证码的uri ...localhost:88/api/captcha.jpg
#应该改成的uri ...localhost:88/renren-fast/captcha.jpg
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?.*),/renren-fast/$\{segment}
- id: member_route
uri: lb://gulimall-member
predicates:
- Path=/api/member/**
filters:
#api前缀去掉剩下的全体保留
- RewritePath=/api/(?.*),/$\{segment}
跨域指的是浏览器不能执行其他网站的脚本,它是由用浏览器的同源策略造成的,是浏览器对js施加的安全限制
跨域请求的实现是通过预检请求实现的,先发送一个OPTIONS探路,收到响应允许跨域后再发送真实的请求,在网关统一配置跨域:
package com.wuyimin.gulimall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
/**
* @author wuyimin
* @create 2021-08-04 20:36
* @description 跨域的配置
*/
@Configuration
public class MyCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
创建一个子模块专门用于与Oss之间的数据传输:勾选两个基础模块SpringWeb和OpengFeign
1.依赖导入(配置注册中心,配置中心)
com.alibaba.cloud
spring-cloud-starter-alicloud-oss
2.2.0.RELEASE
com.wuyimin.gulimall
gulimall-common
0.0.1-SNAPSHOT
2.通过OssClient来测试上传
@SpringBootTest
class GulimallThirdPartyApplicationTests {
@Autowired
OSSClient ossClient;
@Test
void contextLoads() throws FileNotFoundException {
// 上传文件流。
InputStream inputStream = new FileInputStream("C:\\Users\\56548\\Desktop\\1.jpg");
// 上传
ossClient.putObject("gulimall-wuyimin", "1.jpg", inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("上传成功.");
}
}
3.Oss服务直传
/**
* @author wuyimin
* @create 2021-08-06 14:34
* @description 签名信息
*/
@RestController
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
private String bucket="gulimall-wuyimin";
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@RequestMapping("/oss/policy")
public R policy(){
// https://gulimall-hello.oss-cn-beijing.aliyuncs.com/hahaha.jpg host的格式为 bucketname.endpoint
String host = "https://" + bucket + "." + endpoint;
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
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(StandardCharsets.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));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
return R.ok().put("data", respMap);
}
}
4.Oss的跨域需要在Nacos的命名空间中进行修改,在来源中填入*
1.依赖导入
org.springframework.boot
spring-boot-starter-validation
2.在相关的字段上方添加对应注解比如名字不能为空(空字符串也不行),就在name上添加一个@NotBlank注解,该注解的message属性可以写入提示信息;@NotEmpty可以为空串;@URL表示必须是一个url地址,@Pattern可以自定义一个正则表达式,@Min表示大于等于
3.在需要校验的方法参数前加入@Valid注解表示这是一个需要校验的地方,后面紧跟一个BindingResult对象参数可以获得一些校验信息
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){//告诉springMVC这个字段需要校验,后面紧跟一个参数可以获取错误信息
if(result.hasErrors()){
Map map=new HashMap<>();
result.getFieldErrors().forEach((item)->{
String defaultMessage = item.getDefaultMessage();//获取信息
String field=item.getField();//获取错误的名字
map.put(field,defaultMessage);
});
return R.error(400,"提交的数据不合法").put("data",map);
}else{
brandService.save(brand);
return R.ok();
}
}
@RestControllerAdvice注解可以捕获全局异常
@WxceptionHandler可以对特定的异常进行处理
@Slf4j//记录日志
//restController+ControllerAdvice
@RestControllerAdvice(basePackages = "com.wuyimin.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();//之前的BindingResult属性
Map map=new HashMap<>();
bindingResult.getFieldErrors().forEach(item->{
map.put(item.getField(),item.getDefaultMessage());
});
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map);
}
//其他任何异常都默认返回error
@ExceptionHandler(value=Throwable.class)
public R handleException(Throwable throwable){
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(),BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
}
}
为了规范每个错误可以创建异常枚举类,对每种错误进行处理
public enum BizCodeEnum {
/**
* 系统未知异常
*/
UNKNOWN_EXCEPTION(10000, "系统未知异常"),
/**
* 参数校验错误
*/
VALID_EXCEPTION(10001, "参数格式校验失败");
private final int code;
private final String msg;
BizCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
1.引入依赖
javax.validation
validation-api
2.0.1.Final
2.自定义注解
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class})//关联自定义的校验器
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.wuyimin.common.valid.ListValue.message}";//提示信息从ValidationMessages.Properties里拿到,也可以直接定义
Class[] groups() default {};
Class[] payload() default {};
int[] vals() default {};
}
3.实现校验器
public class ListValueConstraintValidator implements ConstraintValidator {
private Set set=new HashSet<>();
//初始化方法 把val值拿到存入集合
@Override
public void initialize(ListValue constraintAnnotation) {
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);//包含就返回true,不包含就返回false
}
}
@Configuration
@EnableTransactionManagement//开启事务功能
@MapperScan("com.wuyimin.gulimall.product.dao")
public class MybatisConfig {
//引入分页插件
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor=new PaginationInterceptor();
paginationInterceptor.setOverflow(true);//请求页面大于最后页面 false为默认-请求到空数据 true--跳到第一页
paginationInterceptor.setLimit(1000);//每页最大受限1000条 -1不受限制
return paginationInterceptor;
}
}
也可以叫做View Object:视图对象,之前我们在数据库表实体类上添加了很多注解,比如@JsonInclude,@TableFiled这样的操作其实是不规范的,正确的应该使用vo对象;它的作用就是用于接收页面传递来的数据,封装对象或者将业务处理完的对象,封装成页面需要使用的工具
vo对象在编程中我们使用new关键字来创建,让gc来回收,在实际操作的时候
@Override
@Transactional//事务原子性
public void saveAttr(AttrVo attr) {
AttrEntity attrEntity = new AttrEntity();//这是一个po持久对象用于保存数据库信息
BeanUtils.copyProperties(attr,attrEntity);//使用BeanUtils拷贝属性,两者属性名必须一一对应
this.save(attrEntity);//保存基本数据
//保存关联关系
AttrAttrgroupRelationEntity entity = new AttrAttrgroupRelationEntity();
entity.setAttrGroupId(attr.getAttrGroupId());
entity.setAttrId(attrEntity.getAttrId());
relationService.save(entity);//最好是注入service
}
增加配置文件spring.jackson.date-formate属性 yyyy-MM-dd HH:mm:ss
1.首先要保证安装了nginx,在nginx/html/下创建es文件夹,添加一个fenci.txt分词文件,里面直接写入需要填写的分词
2.修改elasticsearch/plugins/ik/config/IkAnalyzer.cfg.xml,添加自己分词文件所在的地址
3.修改完之后需要重启es
1.依赖导入
org.elasticsearch.client
elasticsearch-rest-high-level-client
7.4.2
com.wuyimin.gulimall
gulimall-common
0.0.1-SNAPSHOT
2.排除数据源操作
3. 这里要注意的是springboot和es有版本对应关系,并且springboot有内置的es版本号,如果在这里导入了两个不同的版本的es会导致错误
4.es的配置文件,这里主要是防止一个用于增删改查的client
@Configuration
public class ESConfig {
public static final RequestOptions COMMON_OPTIONS;
//默认规则
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient() {
RestClientBuilder builder = null;
// 可以指定多个es
builder = RestClient.builder(new HttpHost("192.168.116.128", 9200, "http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
5.测试es
对应复杂检索的构造条件
@SpringBootTest
class GulimallSearchApplicationTests {
@Autowired
private RestHighLevelClient client;
@Test
void contextLoads() throws IOException {
//测试存储数据
IndexRequest users = new IndexRequest("users");//索引名为user
users.id("1");//id全部都是字符串的形式
users.source("userName","张三","age",18,"gender","男");//第一种方案
User user = new User();
user.setAge(10);
user.setGender("女");
user.setUserName("小小吴");
String s = JSON.toJSONString(user);//需要导入FastJson
users.source(s, XContentType.JSON);//同时也需要传入数据的类型
//调用es执行保存操作
IndexResponse index = client.index(users, ESConfig.COMMON_OPTIONS);
//提取响应数据
System.out.println(index);
}
@Test
void test() throws IOException {
//1.创建检索请求
SearchRequest searchRequest=new SearchRequest();
//2.指定索引
searchRequest.indices("bank");
//3.DSL,检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//构造年龄值分布
searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));//address值必须为mill
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);//名字为ageAgg,年龄进行聚合分组,只显示10种可能
searchSourceBuilder.aggregation(ageAgg);
//同级聚合,计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");//求平均值
searchSourceBuilder.aggregation(balanceAvg);
System.out.println(searchSourceBuilder.toString());
searchRequest.source(searchSourceBuilder);
//4.执行检索
SearchResponse searchResponse = client.search(searchRequest, ESConfig.COMMON_OPTIONS);
//5.分析结果
//获取所有查到的数据
SearchHit[] hits=searchResponse.getHits().getHits();//获得我们里面的hits
for(SearchHit searchHit:hits){
String string = searchHit.getSourceAsString();
Account account = JSON.parseObject(string, Account.class);
System.out.println(account);
}
//获取分析信息
Aggregations aggregations = searchResponse.getAggregations();
Terms agg = aggregations.get("ageAgg");
for (Terms.Bucket bucket : agg.getBuckets()) {
String key = bucket.getKeyAsString();
System.out.println("年龄: "+key+"==>"+bucket.getDocCount());//key为xx的人有xx个
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:"+balanceAvg1.getValue());
}
@Data
class User{
private String userName;
private String gender;
private Integer age;
} //必须是static才能被fastjson parse
@Data
@ToString
static class Account{
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
}
bug出现的原因:人人开源的返回对象R是继承于HashMap的,由于我希望在消费者远程调用生产者方法的时候直接拿到一些数据,在R类里设置一个私有属性对象,我修改了R中如下代码
public class R extends HashMap {
private static final long serialVersionUID = 1L;
private T data;
public T getData(){return data;}
pubblic void setData(T data){this.data=data;}
}
在debug的时候发现,本应该set进R里的私有属性的数据竟然没有显示
这是因为jackson对于HashMap有特殊的处理方式,会将该类直接向上转型为map并且导致私有属性的消失,所以后续使用FastJson以序列化反序列化的方式传递对象
public class R extends HashMap {
private static final long serialVersionUID = 1L;
private R setData(Object o){
put("data",o);
return this;
}
//利用fastJson进行逆转
public T getData(TypeReference typeReference){
Object data=get("data");
String s = JSON.toJSONString(data);
T t=JSON.parseObject(s,typeReference);
return t;
}
生产者提供资源的代码:
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List skuIds){
List skuHasStockVos=wareSkuService.getSkuHasStock(skuIds);
return R.ok().setData(skuHasStockVos);
}
消费者消费资源的代码,注意typeReference这个类的构造器受保护的特性
try{
R r=wareFeignService.getSkuHasStock(skuIds);
TypeReference> typeReference = new TypeReference>() {};//构造器受保护我们拿不到,只能生成一个匿名类对象
//根据skuid和bool值组合成了一个map
List data = r.getData(typeReference);
data.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}
关于请求头Host:
一个IP地址可以对应多个域名,比如假设我有这么几个域名www.qiniu.com,www.taobao.com和www.jd.com然后在域名提供商最终都和我的虚拟机服务器IP 111.111.111.111关联起来,那么我通过任何一个域名去访问最终解析到的都是IP 111.111.111.111
虚拟机111.111.111.111上面其实是可以放很很多网站的我们可以把www.qiniu.com,www.taobao.com和www.jd.com这些网站都假设那台虚拟机上面,但是这样会有一个问题,我们每次访问这些域名其实都是解析到服务器IP 111.111.111.111,我怎么来区分每次根据域名显示出不同的网站的内容呢,其实这就要用到请求头中Host的概念了,每个Host可以看做是我在服务器111.111.111.111上面的一个站点,每次我用那些域名访问的时候都是会解析同一个虚拟机没错,但是我通过不同的Host可以区分出我是访问这个虚拟机上的哪个站点
反向代理:屏蔽服务器信息,负载均衡访问
本处需要实现的逻辑:本机浏览器请求xxx.com,通过配置hosts文件之后,相当于域名解析DNS服务得到ip 192.168.116.128(默认是80端口)
1.(用户==>nginx)首先修改了host文件中域名访问地址,现在我们访问gulimall.com实际访问的是我们的虚拟机
2.(Nginx==>网关)修改nginx/conf/nginx.conf,在upstream块中配置网关服务为nginx的上游服务器(88端口)(这里可以配置多个服务器,后可以跟weight属性决定负载均衡权重)
注意到最后一行我们include了目标文件夹下所有的conf后缀的文件,我们将server块的内容配置在此,他们都会被识别到此文件
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#上游服务器的名字叫gulimall 服务器位于175.10.107.1:88
upstream gulimall{
server 175.10.107.1:88;
}
include /etc/nginx/conf.d/*.conf;
}
修改nginx/conf/conf.d/gulimall.conf
server_name:设置该server块解析的主机地址
listen表示监听的端口为80 也就是所有来源于192.168.116.128的信息它都可以获取
proxy_pass 表示把这个请求转交给谁
proxy_set_header设置的请求头是传递给后端服务器的
由于nginx的转发会丢失请求的Host信息,所以这里要添加一个host头
server {
listen 80;
server_name gulimall.com;
location /static {
root /usr/share/nginx/html;#静态资源位置.../nginx/html/static
}
location /payed/ {
proxy_pass http://gulimall;#代理转发到网关,之前配置的上游服务器的名字叫gulimall
proxy_set_header Host order.gulimall.com;#
}
location / {
proxy_pass http://gulimall;
proxy_set_header Host $host;#$host表示代理服务器本身ip
}
}
3.(网关==>具体服务),这个先放在网关的最下面,因为网关的请求处理顺序是根据配置文件的顺序决定的,而当前的请求范围太宽泛了,会覆盖掉下面的具体请求;-Host表示任意**.gulimall.com为host的请求
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com
进行压测的时候会占用端口,所以要先修改windows文件,预留够足够的端口
新建两个dword值,表示预留端口65534个,每30s回收一个端口
修改完之后需要重启计算机
添加一个线程组:
线程属性参数:如图参数表示200个线程,在10秒内全部启动完成,每个线程发送50个请求
添加线程组下的请求属性:
设置请求路径属性等
查看压测结果:
查看结果树:可以查看线程是否结束,失败等
查看汇总报告:核心参数:吞吐量
查看聚合报告:
通过jvisualvm命令直接打开可使用该工具,线程有五个运行模式
其中驻留表示线程池空闲的线程,监视表示正在阻塞,等待锁的线程,通过安装Visual GC工具来查看GC情况,如下是一个正常健康状态的GC
1.导入依赖
org.springframework.boot
spring-boot-starter-data-redis
2.yml文件中指明redis的host地址
3.引入StringRedisTemplate就可以进行对数据库的操作了
4.Redis自带的分布式锁 setIfAbsent总是不能达到我们预期的效果;原因如下:
5.引入Redisson锁依赖
org.redisson
redisson
3.13.4
6.放入一个bean在配置文件中
// redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
// 创建单节点模式的配置
config.useSingleServer().setAddress("redis://192.168.116.128:6379");
return Redisson.create(config);
}
7.测试加锁代码;
假设解锁代码块没有运行,redisson会不会死锁?==>不会,redisson有一个看门狗,管理锁的自动续期,如果业务超长,看门狗自动续期30s,加锁的业务只要运行完成,就不会给当前的锁续期,即使不手动解锁,锁默认在30s之后自动删除;看门狗的原理是通过定时任务,重新给锁设置过期的时间;
redisson锁还提供了读写锁,信号量,countDownLatch等对标juc包下的内容
@ResponseBody
@GetMapping("/hello")
public String hello(){
//获取一把锁,只要锁的名字一样就是同一把锁
RLock myLock = redissonClient.getLock("myLock");
myLock.lock();//加锁,阻塞式的等待
try{
System.out.println("业务代码"+Thread.currentThread().getId());
Thread.sleep(3000);
}catch (Exception e){
}finally {
System.out.println("释放锁"+Thread.currentThread().getId());
myLock.unlock();//解锁
}
return "hello";
}
1.依赖
org.springframework.boot
spring-boot-starter-cache
2.编写配置(还可以配置key-prefix属性来指定缓存key的前缀,如果不指定就会把缓存的名字作为前缀)
spring:
redis:
host: 192.168.116.128
cache:
#指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
3.注解
//每一个需要缓存的数据我们都来指定要放到哪个名字的缓存(缓存分区--按照业务类型分,可以是数组)
@Cacheable(value = "category",key = "#root.methodName") //现在只需要加上缓存注解就行了,如果缓存中有连方法都不会被调用,指定key属性可以指定缓存的key值,支持spel表达式
@Override
public List getLevel1Categorys() {
List categoryEntities = this.list(new QueryWrapper().eq("parent_cid", 0));
return categoryEntities;
}
在数据库中缓存如图:
默认是JDK序列化;如果需要配置Json序列化的话需要加入配置文件
//默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类
@Configuration
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
//指定缓存序列化方式为json
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//设置配置文件中的各项配置,如过期时间
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;
}
}
在指定方法上使用@CaheEvict注解,指定缓存名和key名可以对缓存进行删除,比如每当我们对Category进行更新的时候,之前的缓存就没用了,此时可以如此使用注解
@CacheEvict(value = "category",key = "'getLevel1Categorys'")//表达式是普通字符串必须加上单引号
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
注解的一些其他使用方式
//Caching组合注解
@Caching(evict = {
@CacheEvict(value = "category",key = "'getLevel1Categorys'"),
@CacheEvict(value = "category",key = "'getCatelogJson'")
})
// 删除分区的所有数据
@CacheEvict(value="category",allEntries=true)
//指定同步
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache)
ElasticSearch是一个分布式,高性能、高可用、可伸缩、RESTful 风格的搜索和数据分析引擎;
我们假设一个场景:我们要买苹果吃,咱们想买天水特产的花牛苹果,然后在搜索框输入天水花牛苹果,这时候咱们希望搜索到所有的售卖天水花牛苹果的商家,但是如果咱们技术上根据这个天水花牛苹果使用sql的like模糊查询,是不能匹配到诸如天水特产花牛苹果,天水正宗,果园直送精品花牛苹果这类的不连续的店铺的。所以sql的like进行模糊查询来搜索商品还真不香!
ES | MySql |
---|---|
字段 | 列 |
文档 | 一行数据 |
类型(已废弃) | 表 |
索引 | 数据库 |
节点:一个节点就是一个ES实例,类型分为以下几种:
分片:Es里的索引可能存储大量数据,这些数据可能会超出单个节点的硬件限制,为了解决这个问题,ES提供了将索引细分为多个碎片的功能,这就是分片
分片的注意事项:
副本分片:为了实现高可用,遇到问题时实现分片的故障转移机制,ES允许将索引分片的一个或者多个复制成所谓的副本分片
注意事项:
创建一个空索引:名字为ropledata,分片数为2,副本分片为0
PUT /ropledata
{
"settings": {
"number_of_shards": "2",
"number_of_replicas": "0"
}
}
修改副本分片数:
PUT ropledata/_settings
{
"number_of_replicas" : "2"
}
删除索引:
DELETE /ropledata
插入数据:插入数据的时候可以指定id,如果不指定,ES会自动生成,如下代码创建了一个101的文档
//指定id
POST /ropledata/_doc/101
{
"id":1,
"name":"111",
"page":"https://www.baidu.com",
"say":"123456"
}
修改数据
ES的文档不可以修改,但是支持覆盖,对他做修改本质上时对他覆盖,它的修改分为全局更新和局部更新
全局:每次全局更新以后,它的_version版本都会+1
PUT /ropledata/_doc/101
{
"id":1,
"name":"222",
"page":"https://www.qq.com",
"say":"11111"
}
局部更新:除了第一次执行,后续不管执行了多少次,_version都不会再发生变化,局部更新效率比全局更新更好
POST /ropledata/_update/101
{
"doc":
{
"say":"奥力给"
}
}
查询数据:原文链接
对应的JavaAPI
在渲染商品详情页的时候使用了异步编排API CompletableFutrue,其中自己编写了一个ThreadPoolExecutor,其中的ThreadPoolConfigProperties是自己配置的一个类,该类的参数使用@ConfigurationProperties注解将配置的具体信息放在了yml文件中
#配置线程池
gulimall:
thread:
core-size: 20
max-size: 200
keep-alive-time: 10
@ConfigurationProperties(prefix = "gulimall.thread")//使用此注解可以在配置文件中修改线程池属性
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
@Configuration
public class MyThreadConfig {
//核心线程数,最大线程数,存活时间,时间单位,阻塞队列,线程工厂,拒绝策略
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(), TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
获取销售属性组合的方法: group_concat用于组连接,mysql还规定,Group By的字段必须要查询,distinct用于去重
##分析当前spu有多少个sku,所有sku涉及的属性组合
其查询效果如图:
//运行的顺序 1 2 6同时 345要在1运行完之后运行
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
//第一个异步任务的结果别人还要用,所以就使用supply
CompletableFuture futureInfo = CompletableFuture.supplyAsync(() -> {
//1.查询当前sku的基本信息 sku_info表
SkuInfoEntity skuInfoEntity = getById(skuId);
skuItemVo.setInfo(skuInfoEntity);
return skuInfoEntity;
}, executor);
//需要接受结果
CompletableFuture futureSaleAttrs = futureInfo.thenAcceptAsync(res -> {
//3.获取spu的销售属性组合
List skuItemSaleAttrsVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttrsVos(skuItemSaleAttrsVos);
}, executor);
CompletableFuture futureInfoDesc = futureInfo.thenAcceptAsync(res -> {
//4.获取spu的介绍
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
CompletableFuture futureItemAttrGroups = futureInfo.thenAcceptAsync(res -> {
//5.获取spu的规格参数
List spuItemAttrGroups = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(spuItemAttrGroups);
}, executor);
//没有什么返回结果
CompletableFuture futureImage = CompletableFuture.runAsync(() -> {
//2.sku的图片信息 pms_sku_image
List skuImagesEntities = imagesService.list(new QueryWrapper().eq("sku_Id", skuId));
skuItemVo.setImages(skuImagesEntities);
}, executor);
//等到任务全部做完,可以不用写info,因为image完了info肯定完了
//6.查询到当前商品是否参加秒杀活动
CompletableFuture futureSeckill = CompletableFuture.runAsync(() -> {
R seckillSkuInfo = seckillFeignService.getSeckillSkuInfo(skuId);
if (seckillSkuInfo.getCode() == 0) {
SeckillSkuRedisTo data = seckillSkuInfo.getData(new TypeReference() {
});
skuItemVo.setSeckillSkuRedisTo(data);
}
}, executor);
CompletableFuture.allOf(futureImage, futureInfoDesc, futureItemAttrGroups, futureSaleAttrs,futureSeckill).get();
return skuItemVo;
}
正常来说需要使用@ConfigurationProperties配置到配置文件里,这里直接抽取不做配置,实际上也就是使用特定的参数发送post请求
@Component
@Data
public class SmsComponent {
private String host;
private String path;
private String templateId="908e94ccf08b4476ba6c876d13f084ad";
private String smsSignId="2e65b1bb3d054466b82f0c9d125465e2";
private String appCode="78442b1006ae490da40cedda6826c7b5";
public void sendSmsCode(String phone,String code){
String host = "https://gyytz.market.alicloudapi.com";
String path = "/sms/smsSend";
String method = "POST";
String appcode = appCode;
Map headers = new HashMap();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map querys = new HashMap();
querys.put("mobile", phone);
querys.put("param", "**code**:"+code+"**minute**:5");
querys.put("smsSignId", smsSignId);
querys.put("templateId", templateId);
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
* 下载
*/
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();
}
}
}
虽然设定了规定时间间隔才能发送验证码,但是其实只要每次刷新网页就可以重新发送验证码了,这里使用redis来实现接口防刷功能
@Slf4j
@Controller
public class LoginController {
@Autowired
ThirdPartyFeignService thirdPartyFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
//TODO 接口防刷
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + phone);
if(!StringUtils.isEmpty(redisCode)){
long l=Long.parseLong(redisCode.split("_")[1]);//拿到时间
if(System.currentTimeMillis()-l<60000){
//60秒内不能再发
return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
}
}
String code = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();//加上系统时间
//验证码的再次校验,存入redis key-手机号 value-code
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CAHE_PREFIX+phone,code,10, TimeUnit.MINUTES);
try {
thirdPartyFeignService.sendCode(phone,code);//第三方服务
} catch (Exception e) {
log.warn("远程调用不知名错误 [无需解决]");
}
return R.ok();
}
}
引入依赖
org.springframework.boot
spring-boot-starter-validation
添加校验注解
@Data
public class UserRegistVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6,max = 18,message = "长度必须在6-18")
private String userName;
@NotEmpty(message = "密码必须提交")
@Length(min = 6,max = 18,message = "长度必须在6-18")
private String password;
//第一个数组必须是1,第二个数字在3-9剩下9个数字在0-9,一共11位
@NotEmpty(message = "手机号必须填写")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
视图映射可以做到收到指定的请求返回指定的视图,但是要注意的一点是它只支持get请求而不支持post请求
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
//视图映射
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
注册成功之后,需要重定向到登录页;失败后继续留在注册页
@PostMapping("/regist")
public String register(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes attributes){//第三个参数是专门用来重定向携带数据的
//注册成功会到登录页
//1.判断校验是否通过
Map errors = new HashMap<>();
if (result.hasErrors()){
//1.1 如果校验不通过,则封装校验结果
result.getFieldErrors().forEach(item->{
// 获取错误的属性名和错误信息
errors.put(item.getField(), item.getDefaultMessage());
//1.2 将错误信息封装到session中
attributes.addFlashAttribute("errors", errors);
});
//校验出错,重定向到注册页
return "redirect:http://auth.gulimall.com/reg.html";//防止刷新的时候表单重复提交,采用重定向
}else{
//真正的注册
//1.校验验证码
String code=vo.getCode();
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + vo.getPhone());
if(!StringUtils.isEmpty(redisCode)){
if(code.equals(redisCode.split("_")[0])){
//删除验证码
redisTemplate.delete(AuthServerConstant.SMS_CODE_CAHE_PREFIX + vo.getPhone());
//验证码通过,调用远程接口进行服务注册
R r = memberFeignService.regist(vo);
if(r.getCode()==0){
//成功
return "redirect:http://auth.gulimall.com/login.html";
}else{
//调用失败,返回注册页并显示错误信息
String msg = (String) r.get("msg");
errors.put("msg", msg);
attributes.addFlashAttribute("errors", errors);
log.error("远程调用会员服务抛出异常");
return "redirect:http://auth.gulimall.com/reg.html";
}
}else{
//验证码没有匹配
errors.put("code","验证码错误");
attributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}else{
//没有验证码
errors.put("code","验证码错误");
attributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}
}
远程调用的Member服务regist方法
自定义异常类
public class PhoneExistException extends RuntimeException {
public PhoneExistException() {
super("手机号已经存在");
}
}
检查手机和用户名是否正确的函数
@Override
public void checkPhone(String phone) throws PhoneExistException {
Integer count = baseMapper.selectCount(new QueryWrapper().eq("mobile", phone));
if (count > 0) {
throw new PhoneExistException();
}
}
@Override
public void checkUserName(String userName) throws UsernameExistException {
Integer count = baseMapper.selectCount(new QueryWrapper().eq("username", userName));
if (count > 0) {
throw new UsernameExistException();
}
}
regist方法,其中密码使用了MD5盐值加密
@Override
public void regist(MemberRegisterVo vo) {
MemberEntity memberEntity = new MemberEntity();
MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(memberLevelEntity.getId());
checkPhone(vo.getPhone());
checkUserName(vo.getUserName());
memberEntity.setMobile(vo.getPhone());
memberEntity.setUsername(vo.getUserName());
//设置密码(密码需要进行加密存储)
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
memberEntity.setPassword(encode);
//其他的默认信息。。
//保存
baseMapper.insert(memberEntity);
}
调用它的Controller,接受自定义的异常并进行处理
@PostMapping("/regist")
public R regist(@RequestBody MemberRegisterVo vo){//远程服务必须要获得json对象
try{
memberService.regist(vo);
}catch (PhoneExistException e){
return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
}catch (UsernameExistException e){
return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(),BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
member服务里的登录逻辑
@Override
public MemberEntity login(MemberLoginVo memberLoginVo) {
String loginacct = memberLoginVo.getLoginacct();
String password = memberLoginVo.getPassword();
//去数据库查询 loginacct有可能是用户名也有可能是手机号
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper().eq("mobile", loginacct)
.or().eq("username", loginacct));
//如果实体类不存在就登录失败
if(memberEntity==null){
return null;
}else{
String passwordDB = memberEntity.getPassword();
//密码匹配
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
boolean matches = bCryptPasswordEncoder.matches(password, passwordDB);
if(matches){
return memberEntity;
}
return null;
}
}
认证服务中远程调用该接口:
@PostMapping("/login")
public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){//页面提交过来的数据是key-value,不能用requestBody接受
R login = memberFeignService.login(vo);
Map errors=new HashMap<>();
if(login.getCode()==0){
return "redirect:http://gulimall.com";
}else{
//登录失败
errors.put("msg",login.getData("msg",new TypeReference(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
社交登录流程:
社交登录远程接口
//具有登录和注册合并逻辑
@Override
public MemberEntity login(SocialUserVo socialUserVo) throws Exception {
long uid = socialUserVo.getUid();
//判断当前社交用户是否已经登录过系统
MemberEntity user = baseMapper.selectOne(new QueryWrapper().eq("social_uid", uid));
if(user!=null){
//这个用户已经注册了
user.setAccessToken(socialUserVo.getAccess_token());
long expires_in = socialUserVo.getExpires_in();
user.setExpiresIn(String.valueOf(expires_in));
baseMapper.updateById(user);
return user;
}else{
//需要注册一个用户
MemberEntity newMember = new MemberEntity();
//查询当前社交用户的性别,信息,这里我设置没给,就直接设置id就行了
newMember.setAccessToken(socialUserVo.getAccess_token());
newMember.setExpiresIn(String.valueOf(socialUserVo.getExpires_in()));
newMember.setSocialUid(String.valueOf(socialUserVo.getUid()));
baseMapper.insert(newMember);
return newMember;
}
}
社交登录具体流程
@Slf4j
@Controller
public class OAuth2Controller {
@Autowired
MemberFeignService memberFeignService;
@GetMapping("/oauth2.0/gitee/success")
public String gitee(@RequestParam("code") String code, HttpSession session) throws Exception {
//根据code换取accessToken
HashMap map = new HashMap<>();
HashMap header = new HashMap<>();
HashMap query = new HashMap<>();
map.put("client_id","8351b1529803f1bca29176b023f2c431c48ffe8cd8398165d7bc26baeb6c6f74");
map.put("client_secret","001a283d5c21b12ffd74cbb63c9968e318abbf07611fbb6b2bb1efa60c959fb0");
map.put("grant_type","authorization_code");
map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/gitee/success");
map.put("code",code);
HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", header, query, map);
if(response.getStatusLine().getStatusCode()==200){
//获取到了token
//这里得到的就是access_token": "48e3a360bab1a289c882b81a2aa75633",
// "token_type": "bearer",
// "expires_in": 86400,
// "refresh_token": "29cc443c3eb07d366850034475862004ba6b71d590a0dd0cb7ffcb85c82df7fa",
// "scope": "user_info",
// "created_at": 1629684682---》我猜这个就是uuid
// 的json字符串
String string = EntityUtils.toString(response.getEntity());
SocialUserVo socialUserVo = JSON.parseObject(string, SocialUserVo.class);
//giee比微博多了一个步骤是需要我们提交一个get请求来获得这个用户的唯一id
//https://gitee.com/api/v5/user?access_token=0a7c505a421334e51bc77cad97860bf8
HashMap getHeader = new HashMap<>();
HashMap getQuery = new HashMap<>();
HttpResponse get = HttpUtils.doGet("https://gitee.com", "/api/v5/user?access_token="+socialUserVo.getAccess_token(), "get", getHeader, getQuery);
String s = EntityUtils.toString(get.getEntity());
//拿到的字符串"id":8442725,....
String id = s.split(",")[0].split(":")[1];
long l = Long.parseLong(id);
socialUserVo.setUid(l);
//当前的用户如果是第一次进网站,那么就需要注册进来(自动注册)
//社交用户关联自己系统的会员
R r = memberFeignService.oauthLogin(socialUserVo);
if(r.getCode()==0){
MemberRespVo data = r.getData("data", new TypeReference() {
});
log.info("登录成功 用户: {}",data.toString());
//把值传入session中带给前端
session.setAttribute("loginUser",data);
//远程调用成功
//成功就跳回首页
return "redirect:http://gulimall.com";
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}
}
使用redisSession方案
1.导入依赖
org.springframework.session
spring-session-data-redis
org.springframework.boot
spring-boot-starter-data-redis
2.修改配置
spring: #配置nacos
session:
store-type: redis
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-auth-server
redis:
host: 192.168.116.128
server:
port: 20000
servlet:
session:
timeout: 30m
3.在主函数上添加@EnableRedisHttpSession注解
4.Vo想要作为数据被存到redis里必须实现Serializable接口
5.由于Cookie的作用域不够,所以要提升Cookie的作用域
@Configuration
public class RedisSessionConfig {
@Bean // redis的json序列化
public RedisSerializer