畅购 业务与技术(通用Mapper 跨域访问 FastDFS 网关过滤器 鉴权JWT 分布式id)

day01

一、通用mapper

业务和技术

通用mapper用来实现单表的增删改查,使用方便而且通用,不需要写sql了,通过调用实体类的mapper来实现,注意只能实现单表的,如果想多表可以用在mapper类下手动加入一些自定义的。

具体实现

maven起步依赖

 <!--通用mapper起步依赖-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>
        <!--MySQL数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis分页插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>

数据库表

CREATE TABLE tb_brand (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
  name varchar(100) NOT NULL COMMENT '品牌名称',
  image varchar(1000) DEFAULT '' COMMENT '品牌图片地址',
  letter char(1) DEFAULT '' COMMENT '品牌的首字母',
  seq int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=325416 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='品牌表'

这里拿实体类Brand来作为实例,需要在实体类上加入注解
@Table 来和数据库中的表做个对应关系
@Id 指定表中的主键
@Column 如果数据库字段和实体类字段不一致可以用这个来做个关系映射

package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Table(name="tb_brand")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Brand implements Serializable {
    @Id
    private Integer id;//品牌id
    @Column(name = "name")
    private String name;//品牌名称
    @Column(name = "image")
    private String image;//品牌图片地址
    private String letter;//品牌的首字母
    private Integer seq;//排序
}

dao层就很简单了,因为通用的关系,继承Mapper类,把实体对象放进去

public interface BrandMapper extends Mapper<Brand> {
}

后续dao会很多,记得在启动类中记得配置包扫描

@SpringBootApplication
@EnableEurekaClient //声明当前的工程是eureka客户端
@MapperScan(basePackages = {"com.changgou.service.goods.dao"})
public class GoodsApplication {

    public static void main(String[] args) {
        SpringApplication.run(GoodsApplication.class,args);
    }
}

配置

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.210.128:3306/changgou_goods?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: root

2.单表

接下来是自己做的一些测试,包千万别导错了

brandMapper.insert(brand);
拼接的sql是这样的 insert into xxx (id,name,age…) values(1,null,null)
brandMapper.insertSelective(brand);
拼接的sql是这样的insert into xxx(id) values(1)
下面一个更好些,会根据不同的条件进行动态的拼接参数。
Example这个条件需要先生成一个类,再用这个类去加入需要的条件

2.1查询所有

    @Autowired
    private BrandMapper brandMapper;

    @Test
    public void testSelectAll(){
        List<Brand> brands = brandMapper.selectAll();
        System.out.println(brands);
    }

2.2根据主键查询

    @Test
    public void testSelectById(){
        Brand brand = brandMapper.selectByPrimaryKey(2032);
        System.out.println(brand);
    }

2.3插入数据

insert和insertSelective方法的区别具体看注释。一般我们用insertSelective比较多

    @Test
    public void testInsert(){
        Brand brand = new Brand();
        brand.setName("戴森");
        /**
         * 相当于: insert into tb_brand (id,name,image,letter,seq) value (null,"戴森",null,null,null);
         */
        int insert = brandMapper.insert(brand);
        System.out.println(insert);
    }
    @Test
    public void testInsertSelective(){
        Brand brand = new Brand();
        brand.setName("戴森2");
        /**
         * 相当于: insert into tb_brand (name) value ("戴森");
         */
        int insert = brandMapper.insertSelective(brand);
        System.out.println(insert);

    }

2.4根据主键更新数据

具体区别看注释,我们一般用updateByPrimaryKeySelective

    @Test
    public void testUpdateByPrimaryKeySelective(){
        Brand brand = new Brand();
        brand.setId(325419);
        brand.setName("戴森3");
        /**
         * 相当于只修改了我们设置的字段
         * UPDATE tb_brand SET NAME = '戴森3' WHERE id = 325419
         */
        brandMapper.updateByPrimaryKeySelective(brand);
    }
    @Test
    public void testUpdateByPrimaryKey(){
        Brand brand = new Brand();
        brand.setId(325419);
        brand.setName("戴森3");
        /**
         * 相当于
            UPDATE tb_brand SET NAME = '戴森3',image = NULL,letter=NULL,seq=NULL WHERE id = 325419
         */
        brandMapper.updateByPrimaryKey(brand);
    }

2.5删除

    @Test
	//根据主键删除
    public void testDeleteByPrimaryKey(){
        int i = brandMapper.deleteByPrimaryKey(325418);
        System.out.println(i);
    }

    @Test
	//根据条件删除
    public void testDelete(){
        Brand brand = new Brand();
        brand.setId(325417);
        int i = brandMapper.delete(brand);
        System.out.println(i);
    }

2.6条件查询

/**
     * 条件查询
     */
    @Test
    public void testSelectByExample(){
        Brand brand = new Brand();
        brand.setName("迷你");
        brand.setLetter("M");
        Example example = new Example(Brand.class);
        Example.Criteria criteria = example.createCriteria();
        if (brand != null) {
            if(!StringUtils.isEmpty(brand.getName())){
                criteria.andLike("name","%"+brand.getName()+"%");
            }
            if(!StringUtils.isEmpty(brand.getLetter())){
                criteria.andEqualTo("letter",brand.getLetter());
            }
        }
        //SELECT id,name,image,letter,seq FROM tb_brand WHERE ( name like ? and letter = ? )  
        //Parameters: %迷你%(String), M(String)
        List<Brand> brands = brandMapper.selectByExample(example);
        System.out.println(brands);
    }

    //条件查询
    @Test
    public void selectExample(){
//        User u = new User();
//        u.setUserName("heima");
//        u.setName("黑马");
        Example example = new Example(User.class);
        //使用criteria去设置条件
        Example.Criteria criteria = example.createCriteria();
        //   and   user_name like ‘cj%’
        criteria.andLike("userName","ma%");
        // id in (1,2,3,4)
        criteria.andGreaterThan("age",28);
      
        //SELECT id,user_name,password,name,age,sex,birthday,created,updated,note FROM tb_user WHERE ( user_name like ? and age > ? )
        List<User> list = userMapper.selectByExample(example);
        System.out.println(list);
    }

2.7分页查询

    @Test
    /**
     * 分页查询
     */
    public void testSelectForPage(){
        PageHelper.startPage(1,10);
        Page<Brand> page = (Page<Brand>) brandMapper.selectAll();
        List<Brand> result = page.getResult();
        for (Brand brand : result) {
            System.out.println(brand);
        }
    }

2.8查询符合条件的个数

selectCount方法,根据条件查询符合条件的数据行数。

int count = categoryBrandMapper.selectCount(categoryBrand);

2.9插入数据时获取自增id

在pojo的id属性上加上@GeneratedValue(strategy= GenerationType.IDENTITY)
或者使用@KeySql(useGeneratedKeys = true)

public class User {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Integer id;
}

2.10忽略实体类中的自动

在字段上增加@Transient

3.多表/自定义sql

3.1在接口中增加方法

public interface UserMapper extends Mapper<User> {

    User selectMy();
}

3.2 在接口方法上使用注解定义sql /创建xml文件

注解方式(不推荐)

    @Select("select * from user where id = 7")
    User selectMy();

xml方式 注意:在配置文件中配置映射文件的位置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.itheima.mapper.UserMapper">
    <select id="selectMy" resultType="com.itheima.pojo.User">
        select * from user where id = 7
    </select>
</mapper>
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径

二、异常处理类

基于springboot搭建的环境,有定义好的异常处理功能,加入注解,包扫描到即可

/**
 * 统一异常处理类
 */
@ControllerAdvice //声明该类是一个增强类
public class BaseExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result error(Exception e){
        e.printStackTrace();
        return new Result(false, StatusCode.ERROR,"当前系统繁忙,请您稍后重试");
    }
}

day02

一、CORS跨域资源共享

CORS是一个“跨域资源共享”,CORS需要浏览器和服务器同时支持。
特别是微服务之间,经常会有跨域请求。
SpringMVC版本4.2以上使用@CrossOrigin注解即可实现

二、多表联查 多种方式对比

在这里插入图片描述

在这里插入图片描述

 @Select("SELECT name,options FROM tb_spec WHERE template_id IN 
 ( SELECT template_id FROM tb_category WHERE NAME=#{categoryName}) order by seq")
    public List<Map> findListByCategoryName(@Param("categoryName") String categoryName);  

这是三种实现方式
1.关联查询
2.分段查询
3.子查询
总来说1.3性能差不多,但是尽量还是用分段查询,因为mysql会有缓存机制,一样的查询会保存一起,做一个缓存,这样相对消耗的性能也会少一些。

三、FastDFS文件系统

做文件存储可以用七牛云,阿里云。这是云存储,而FastDFS是可以自己搭建分布式文件系统,对文件进行存储。
在这里插入图片描述
在这里插入图片描述
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
在这里插入图片描述
组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。

虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了

store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。

数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据

文件。

文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储

服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

3.1Linux搭建

拉取镜像

docker pull morunchang/fastdfs

运行tracker

 docker run -d --name tracker --net=host morunchang/fastdfs sh
 tracker.sh

运行storage

docker run -d --name storage --net=host -e TRACKER_IP=<your tracker server address>:22122 -e GROUP_NAME=<group name> morunchang/fastdfs sh storage.sh

使用的网络模式是–net=host, 替换为你机器的Ip即可
是组名,即storage的组
如果想要增加新的storage服务器,再次运行该命令,注意更换 新组名
(4)修改nginx的配置

进入storage的容器内部,修改nginx.conf

docker exec -it storage  /bin/bash

进入后

vi /data/nginx/conf/nginx.conf

添加以下内容

location /group1/M00 {
   proxy_next_upstream http_502 http_504 error timeout invalid_header;
     proxy_cache http-cache;
     proxy_cache_valid  200 304 12h;
     proxy_cache_key $uri$is_args$args;
     proxy_pass http://fdfs_group1;
     expires 30d;
 }

(5)退出容器

exit

(6)重启storage容器

docker restart storage

3.2代码实战

在这里插入图片描述

<dependency>
        <groupId>net.oschina.zcx7878</groupId>
        <artifactId>fastdfs-client-java</artifactId>
        <version>1.27.0.0</version>
    </dependency>

fdfs_client.conf

connect_timeout = 60
network_timeout = 60
charset = UTF-8
http.tracker_http_port = 8080
tracker_server = 192.168.23.129:22122
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
server:
  port: 9008
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

FileController
拿到参数后需要做一个拼接来返回整个路径信息

package com.changgou.file.controller;

import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.changgou.file.util.FastDFSClient;
import com.changgou.file.util.FastDFSFile;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/file")
public class FileController {

    @PostMapping("/upload")
    public Result uploadFile(MultipartFile file){
        try{
            //判断文件是否存在
            if (file == null){
                throw new RuntimeException("文件不存在");
            }
            //获取文件的完整名称
            String originalFilename = file.getOriginalFilename();
            if (StringUtils.isEmpty(originalFilename)){
                throw new RuntimeException("文件不存在");
            }

            //获取文件的扩展名称  abc.jpg   jpg
            String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);

            //获取文件内容
            byte[] content = file.getBytes();

            //创建文件上传的封装实体类
            FastDFSFile fastDFSFile = new FastDFSFile(originalFilename,content,extName);

            //基于工具类进行文件上传,并接受返回参数  String[]
            String[] uploadResult = FastDFSClient.upload(fastDFSFile);
            结果的url http://192.168.23.129:8080/ybb/M00/00/00/wKgXgV9walmATnFDAAF9JumPvag381.jpg
            //封装返回结果
            String url = FastDFSClient.getTrackerUrl()+uploadResult[0]+"/"+uploadResult[1];
            return new Result(true,StatusCode.OK,"文件上传成功",url);
        }catch (Exception e){
            e.printStackTrace();
        }
        return new Result(false, StatusCode.ERROR,"文件上传失败");
    }
}

FastDFSClient
这里工具类的实现会把得到的组名和存储路径返还回去。

package com.changgou.file.util;

import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FastDFSClient {

    private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class);

    /***
     * 初始化加载FastDFS的TrackerServer配置
     */
    static {
        try {
            String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
            ClientGlobal.init(filePath);
        } catch (Exception e) {
            logger.error("FastDFS Client Init Fail!",e);
        }
    }

    /***
     * 文件上传
     * @param file
     * @return 1.文件的组名  2.文件的路径信息
     */
    public static String[] upload(FastDFSFile file) {
        //获取文件的作者
        NameValuePair[] meta_list = new NameValuePair[1];
        meta_list[0] = new NameValuePair("author", file.getAuthor());

        //接收返回数据
        String[] uploadResults = null;
        StorageClient storageClient=null;
        try {
            //创建StorageClient客户端对象
            storageClient = getTrackerClient();

            /***
             * 文件上传
             * 1)文件字节数组
             * 2)文件扩展名
             * 3)文件作者
             */
            uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
        } catch (Exception e) {
            logger.error("Exception when uploadind the file:" + file.getName(), e);
        }

        if (uploadResults == null && storageClient!=null) {
            logger.error("upload file fail, error code:" + storageClient.getErrorCode());
        }
        //获取组名
        String groupName = uploadResults[0];
        //获取文件存储路径
        String remoteFileName = uploadResults[1];
        return uploadResults;
    }

    /***
     * 获取文件信息
     * @param groupName:组名
     * @param remoteFileName:文件存储完整名
     * @return
     */
    public static FileInfo getFile(String groupName, String remoteFileName) {
        try {
            StorageClient storageClient = getTrackerClient();
            return storageClient.get_file_info(groupName, remoteFileName);
        } catch (Exception e) {
            logger.error("Exception: Get File from Fast DFS failed", e);
        }
        return null;
    }

    /***
     * 文件下载
     * @param groupName
     * @param remoteFileName
     * @return
     */
    public static InputStream downFile(String groupName, String remoteFileName) {
        try {
            //创建StorageClient
            StorageClient storageClient = getTrackerClient();

            //下载文件
            byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
            InputStream ins = new ByteArrayInputStream(fileByte);
            return ins;
        } catch (Exception e) {
            logger.error("Exception: Get File from Fast DFS failed", e);
        }
        return null;
    }

    /***
     * 文件删除
     * @param groupName
     * @param remoteFileName
     * @throws Exception
     */
    public static void deleteFile(String groupName, String remoteFileName)
            throws Exception {
        //创建StorageClient
        StorageClient storageClient = getTrackerClient();

        //删除文件
        int i = storageClient.delete_file(groupName, remoteFileName);
    }

    /***
     * 获取Storage组
     * @param groupName
     * @return
     * @throws IOException
     */
    public static StorageServer[] getStoreStorages(String groupName)
            throws IOException {
        //创建TrackerClient
        TrackerClient trackerClient = new TrackerClient();
        //获取TrackerServer
        TrackerServer trackerServer = trackerClient.getConnection();
        //获取Storage组
        return trackerClient.getStoreStorages(trackerServer, groupName);
    }

    /***
     * 获取Storage信息,IP和端口
     * @param groupName
     * @param remoteFileName
     * @return
     * @throws IOException
     */
    public static ServerInfo[] getFetchStorages(String groupName,
                                                String remoteFileName) throws IOException {
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
    }

    /***
     * 获取Tracker服务地址
     * @return
     * @throws IOException
     */
    public static String getTrackerUrl() throws IOException {
        return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port()+"/";
    }

    /***
     * 获取Storage客户端
     * @return
     * @throws IOException
     */
    private static StorageClient getTrackerClient() throws IOException {
        TrackerServer trackerServer = getTrackerServer();
        StorageClient storageClient = new StorageClient(trackerServer, null);
        return  storageClient;
    }

    /***
     * 获取Tracker
     * @return
     * @throws IOException
     */
    private static TrackerServer getTrackerServer() throws IOException {
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        return  trackerServer;
    }
}

FastDFSFile

package com.changgou.file.util;

public class FastDFSFile {
    //文件名字
    private String name;
    //文件内容
    private byte[] content;
    //文件扩展名
    private String ext;
    //文件MD5摘要值
    private String md5;
    //文件创建作者
    private String author;

    public FastDFSFile(String name, byte[] content, String ext, String height,
                       String width, String author) {
        super();
        this.name = name;
        this.content = content;
        this.ext = ext;
        this.author = author;
    }

    public FastDFSFile(String name, byte[] content, String ext) {
        super();
        this.name = name;
        this.content = content;
        this.ext = ext;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }

    public String getExt() {
        return ext;
    }

    public void setExt(String ext) {
        this.ext = ext;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

FileAppiction

package com.changgou.file;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class FileApplication {

    public static void main(String[] args) {
        SpringApplication.run(FileApplication.class,args);
    }
}

day03

一、网关限流

多服务之间调用,安全,认证等一些问题,应用了网关做一个统一的入口管理。
网关:介于客户端和服务端之间的中间层,所有外部请求都会先经过网关。
安全:对外只暴露网关
易于监控:手机监控数据
统一认证:方便相关的鉴权,安全控制,日志统一处理
在这里插入图片描述

1.依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
2.把网关服务注册到注册中心
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class,args);
    }
}
server:
  port: 9999
spring:
  application:
    name: gateway
eureka:
  client:
    service-url:
      # eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
      defaultZone: http://127.0.0.1:10086/eureka
    # 是否注册到服务端
    register-with-eureka: true
    # 是否拉取服务列表
    fetch-registry: true
  instance:
    ip-address: 127.0.0.1 # 服务的ip地址
    prefer-ip-address: true # 启用ip地址访问
3.静态路由配置
server:
  port: 8082
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        # 路由id,可以随意写
        - id: user-service-route
        # 代理的服务地址
          uri: http://127.0.0.1:8082
        # 路由断言,可以配置映射路径
          predicates:
            - Path=/user/**

服务提供方的访问地址如下:

http://localhost:8082/user/findList?id=2

4.测试

http://localhost:9999/user/findList?id=2

动态路由(更好)
1.在静态路由的基础上修改路由配置 的uri
server:
  port: 9999
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        # 路由id,可以随意写
        - id: user-service-route
        # 代理的服务地址
          uri: lb://user-server
        # 路由断言,可以配置映射路径
          predicates:
            - Path=/user/**

user-server为要访问的服务名

过滤器

参考:官方文档

局部过滤器
添加前缀

假设要访问的服务请求路径为:

http://localhost:8082/api/user/findList?id=2

网关配置如下:

server:
  port: 9999
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        # 路由id,可以随意写
        - id: user-service-route
        # 代理的服务地址
          uri: lb://user-server
        # 路由断言,可以配置映射路径
          predicates:
            - Path=/user/**
          filters:
            - PrefixPath=/api #为请求增加一个前缀

当测试访问的路径为: http://localhost:9999/user/findList?id=2 的时候可以正确请求到服务。

删除前缀

假设要访问的服务请求路径为:

http://localhost:8082/user/findList?id=2

网关配置如下:

server:
  port: 9999
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        # 路由id,可以随意写
        - id: user-service-route
        # 代理的服务地址
          uri: lb://user-server
        # 路由断言,可以配置映射路径
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1 #为请求删除一个前缀  如果为2就是去除2个前缀

当测试访问的路径为: http://localhost:9999/api/user/findList?id=2 的时候可以正确请求到服务。

全局过滤器

添加在default-filters配置里面的都是全局生效的过滤器

server:
  port: 9999
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        # 路由id,可以随意写
        - id: user-service-route
        # 代理的服务地址
          uri: lb://user-server
        # 路由断言,可以配置映射路径
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1 #为请求删除一个前缀  如果为2就是去除2个前缀
      default-filters:
        - AddResponseHeader=X-Response-Foo, Bar

自定义过滤器

自定义全局过滤器

定义一个类实现GlobalFilter, Ordered接口

@Component
public class TokenFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("-----------------全局过滤器MyGlobalFilter---------------------");
                String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (StringUtils.isBlank(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        //值越小越先执行
        return 0;
    }
}

注意记得加上@Component注解

如果要进行放行:

 return chain.filter(exchange);

如果需要拦截请求,过滤器直接给出响应

return exchange.getResponse().setComplete();

如果在过滤器中需要获取请求中的相关数据

ServerHttpRequest request = exchange.getRequest();
//获取发起请求方的IP地址
 InetSocketAddress remoteAddress = request.getRemoteAddress();
 System.out.println("ip:"+remoteAddress.getHostName());
//获取请求URI
request.getURI();

如果在过滤器中需要获取响应

ServerHttpResponse response = exchange.getResponse();

如果需要对请求进行增强,增加一个请求头

request.mutate().header("请求头name","值");
过滤器获取真实ip
/**

 * 获取客户端的访问ip
 */
@Component
public class IpFilter implements GlobalFilter, Ordered {

    private static List<String> ipList;

    static {
        ipList = new ArrayList<>();
        //        ipList.add("192.168.199.211");
        //        ipList.add("192.168.199.21");
    }

    //具体业务逻辑
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取客户端的访问ip
        System.out.println("经过了第一个过滤器");
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String ip = getIpAddress(request);
        System.out.println("真实ip:"+ip);
        //判断是否在黑名单中如果在就拦截
        if(ipList.contains(ip)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //        System.out.println("ip:"+remoteAddress.getHostName());
        //        System.out.println("ip:"+request.getRemoteAddress().getHostString());
        //放行

        return chain.filter(exchange);

    }
    /**

     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     *

     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,

     * 而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?

     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     *

     * 如:X-Forwarded-For:192.168.1.110,
     *

     * 用户真实IP为: 192.168.1.110
     *

     * @param request

     * @return
     */
    public  String getIpAddress(ServerHttpRequest request) {

        String ip = request.getHeaders().getFirst("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().toString();
        }
        return ip;
    }
    //过滤器的执行优先级,返回值越小,执行优先级越高
    @Override
    public int getOrder() {
        return 1;
    }
}
自定义局部过滤器

①定义一个类实现AbstractGatewayFilterFactory接口

注意类名后缀必须为 GatewayFilterFactory

注意记得加上@Component注解

@Component
public class MyParamGatewayFilterFactory extends
        AbstractGatewayFilterFactory<MyParamGatewayFilterFactory.Config> {

    public static final String PARAM_NAME = "param";
    public MyParamGatewayFilterFactory() {
        super(Config.class);
    }
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(PARAM_NAME);
    }
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            if (request.getQueryParams().containsKey(config.param)) {
                request.getQueryParams().get(config.param)
                        .forEach(value -> System.out.printf("----------局部过滤器-----%s= %s-----",
                                config.param, value));
            }
            return chain.filter(exchange);
        };
    }
    public static class Config {
        private String param;
        public String getParam() {
            return param;
        }
        public void setParam(String param) {
            this.param = param;
        }
    }

}

②配置

因为类名为MyParamGatewayFilterFactory 所以配置的时候属性名为 MyParam

server:
  port: 9999
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        # 路由id,可以随意写
        - id: user-service-route
        # 代理的服务地址
          uri: lb://user-server
        # 路由断言,可以配置映射路径
          predicates:
            - Path=/api/user/**
          filters:
            - MyParam=name
负载均衡和熔断

GateWay集成了Ribbon和Hystrix。如果需要配置相关的负载均衡和熔断请参考对应文档

配置跨域请求
spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            #allowedOrigins: * # 这种写法或者下面的都可以,*表示全部
            allowedOrigins:
              - "http://docs.spring.io"
            allowedMethods:
              - GET
             

上述配置表示:可以允许来自 http://docs.spring.io 的get请求方式获取服务数据。 allowedOrigins 指定允许访问的服务器地址,如:http://localhost:10000 也是可以的。 ‘[/**]’ 表示对所有访问到网关服务器的请求地址 官网具体说明:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.1.RELEASE/multi/mul ti__cors_configuration.html

高可用

启动多个Gateway服务,自动注册到Eureka,形成集群。如果是服务内部访问,访问Gateway,自动负载均衡,没 问题。 但是,Gateway更多是外部访问,PC端、移动端等。它们无法通过Eureka进行负载均衡,那么该怎么办? 此时,可以使用其它的服务网关,来对Gateway进行代理。比如:Nginx

二、鉴权

基于gateway网关去加入一个鉴权的功能,来确定用户是否登录。
在这里插入图片描述
这里用令牌桶来实现
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

配置

依赖

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

在启动引导类下注入一个Bean

    //定义一个KeyResolver
    @Bean
    public KeyResolver ipKeyResolver() {
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
            //这里是根据ip来限流,可以根据不同业务根据不同情况进行限流
                return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
            }
        };
    }

修改application.yml

spring:
  application:
    name: sysgateway
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
            - GET
            - POST
            - PUT
            - DELETE
      routes:
      - id: goods
        uri: lb://goods
        predicates:
        - Path=/goods/**
        filters:
        - StripPrefix= 1
        - name: RequestRateLimiter #请求数限流 名字不能随便写 
          args:
            key-resolver: "#{@ipKeyResolver}"
            redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
            redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
      - id: system
        uri: lb://system
        predicates:
        - Path=/system/**
        filters:
        - StripPrefix= 1
  # 配置Redis 127.0.0.1可以省略配置
  redis:
    host: 192.168.200.128
    port: 6379
server:
  port: 9101
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true

解释:

burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

JWT加密

快速入门(后面有封装用法)

依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

生成令牌并解析

    public static void main(String[] args) {
        JwtBuilder builder= Jwts.builder()
                .setId("888")   //设置唯一编号
                .setSubject("小白")//设置主题  可以是JSON数据
                .setIssuedAt(new Date())//设置签发日期
                .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
        System.out.println( builder.compact() );


        String jwt = builder.compact();
        Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(jwt).getBody();
        System.out.println(claims);
    }

输出结果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1ODcwNDEzMDd9.vLUDlR86I--WrdvT708KN6OHFKWCj7PwZU--ZLqUYzI
{jti=888, sub=小白, iat=1587041307}

注意加密和解密必须使用同一个签名才可以

设置JWT过期时间

//当前时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis+10000);
JwtBuilder builder= Jwts.builder()
    .setId("888")   //设置唯一编号
    .setSubject("小白")//设置主题  可以是JSON数据
    .setIssuedAt(new Date())//设置签发日期
    .setExpiration(date)
    .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );

自定义claims

如果你想存储更多的信息(例如角色)可以定义自定义claims。

public void createJWT(){
    //当前时间
    long currentTimeMillis = System.currentTimeMillis();
    currentTimeMillis+=1000000L;
    Date date = new Date(currentTimeMillis);
    JwtBuilder builder= Jwts.builder()
        .setId("888")   //设置唯一编号
        .setSubject("小白")//设置主题  可以是JSON数据
        .setIssuedAt(new Date())//设置签发日期
        .setExpiration(date)//设置过期时间
        .claim("roles","admin")//设置角色
        .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
    //构建 并返回一个字符串
    System.out.println( builder.compact() );
}

封装后的工具类

package com.xxx.xxx2.util;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "xxx";

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        SecretKey secretKey = generalKey();

        JwtBuilder builder = Jwts.builder()
                .setId(id)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("admin")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
生成token
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), "token中携带的数据", null);
解析token
JwtUtil.parseJWT(token);

使用非对称加密生成JWT

1.依赖

<dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-data</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

2.生成私钥

keytool -genkeypair -alias 密钥别名 -keyalg 加密算法的名称(RSA) -keypass 密钥的访问密码 -keystore 最终生成的文件名称一般后缀是jks -storepass 密钥库的访问密码

keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou

3.生成公钥

keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey

4.生成jwt

package com.changgou.oauth;

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;

public class CreateJWTTest {

    @Test
    public void createJWT(){
        //基于私钥生成jwt
        //1. 创建一个秘钥工厂
        //1: 指定私钥的位置
        ClassPathResource classPathResource = new ClassPathResource("changgou.jks");
        //2: 指定秘钥库的密码
        String keyPass = "changgou";
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keyPass.toCharArray());

        //2. 基于工厂获取私钥
        String alias = "changgou";
        String password = "changgou";
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
        //将当前的私钥转换为rsa私钥
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();

        //3.生成jwt
        Map<String,String> map = new HashMap();
        map.put("company","heima");
        map.put("address","beijing");

        Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey));
        String jwtEncoded = jwt.getEncoded();
        System.out.println(jwtEncoded);
    }
}

5.解密jwt

package com.changgou.oauth;

import org.junit.Test;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;

public class ParseJwtTest {

    @Test
    public void parseJwt(){
        //基于公钥去解析jwt
        String jwt ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiYmVpamluZyIsImNvbXBhbnkiOiJoZWltYSJ9.iijApV8zarqmkAUCvg8-te2WbXV3W4i9aN5cafw-tPjMnDIXXQXBdyQzKA2arUTQ9zaI6hOevW7XrbEFn-WHm1XJDWx-jt_HLqc0IRi1B5OSzD5ZgVoe3cqCqIVLYcfe-1f2Q5z78ZTZ5206KBTa-h1SA0799ETygmG_PA3Tv_gSn0R2NriDxj7OQS529IvQtwO4yesUcwVzjhjkFHECLuvrKfvHjtUXblSiZd_xMaqqHfcZwG2FEht5_MfqvcdbdcnV5O8VdsSKvxvX3ujSqSKvH8Ezi6YnlTZe0gbrWFyKjsZAM_FCfrqz47kmBEppyFaQq6yAqaOf2O0Di37Q_g";
		//使用自己私钥对应的公钥
        String publicKey ="-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjwGOEtoYeGyigK3CzThHP7C3uNJKTde5/Is0c/RPMCFaZIb/iLn8R4MZ77pMsUjYF/5Ab93ksryq8IaRCO7ZUYKl1WsvkWgVoNBxbl+KjrA65LUmB+azpiQipZOvBc+iSXvbF2HuT1ArVtpGkWUE0rtAENf41xUrPew2nCOWt/6DTpH+wmuuUCbW6bH1nBxHsW4OqZ3qwfv6d4oXdOyTifaZ8mpht8Tv52gP983Kno9lZTrIBJvtr5S0ALQ9nZ8TXHpl7oVD4ZnSHqu2pMxMndKMN/ouJ8orLqpHAO0v7FXgnEgX/o0wYhsXW/UnAtOB9jeFiomwj06a/w2tYkDDswIDAQAB-----END PUBLIC KEY-----";

        Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey));

        String claims = token.getClaims();
        System.out.println(claims);
    }
}

系统运用

上面工具类copy使用,这里查询用通用Mapper做一个查询

AuthorizeFilter 重点全局过滤访问配置

全局过滤器,检测访请求是否登录,token是否正确

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取请求对象
        ServerHttpRequest request = exchange.getRequest();
        //2.获取响应对象
        ServerHttpResponse response = exchange.getResponse();

        //3.判断当前的请求是否为登录请求,如果是,则直接放行
        if (request.getURI().getPath().contains("/admin/login")){
            //放行
            return chain.filter(exchange);
        }
        //4.获取当前的所有请求头信息
        HttpHeaders headers = request.getHeaders();

        //5.获取jwt令牌信息
        String jwtToken = headers.getFirst("token");

        //6.判断当前令牌是否存在,
        if (StringUtils.isEmpty(jwtToken)){
            //如果不存在,则向客户端返回错误提示信息
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //6.1 如果令牌存在,解析jwt令牌,判断该令牌是否合法,如果令牌不合法,则向客户端返回错误提示信息
        try {
            //解析令牌
            JwtUtil.parseJWT(jwtToken);
        }catch (Exception e){
            e.printStackTrace();
            //令牌解析失败
            //向客户端返回错误提示信息
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //6.2 如果令牌合法,则放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

服务层

service

  @Override
    public boolean login(Admin admin) {
        //根据登录名获取管理员信息
        Admin admin1 = new Admin();
        admin1.setLoginName(admin.getLoginName());
        admin1.setStatus("1");
        //传参查对象,这里1状态码需要确定该用户现在是否还可用
        Admin adminResult = adminMapper.selectOne(admin1);

        if (adminResult == null){
            return false;
        }else{
            //对密码进行校验
            return BCrypt.checkpw(admin.getPassword(),adminResult.getPassword());
        }
        //返回结果
    }

controller

@PostMapping("/login")
    public Result login(@RequestBody Admin admin){
        boolean result = adminService.login(admin);
        if (result){
            //密码是正确的
            //生成jwt令牌,返回到客户端
            Map<String,String> info = new HashMap<>();
            info.put("username",admin.getLoginName());
            //基于工具类生成jwt令牌
            String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null);
            info.put("token",jwt);
            //把生成好的两个参数传给前端,这样前端以后就会带着名字和token过来访问了
            return new Result(true, StatusCode.OK,"登录成功",info);
        }else{
            return new Result(false, StatusCode.ERROR,"登录失败");
        }
    }

day04

分布式id

问题:
分库分表后,导致订单数据存储在不同的数据库中,可能会出现重复的主键
解决:

UUID

优点:

1)简单,代码方便。

2)生成ID性能非常好,基本不会有性能问题。

3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

缺点:

1)没有排序,无法保证趋势递增。

2)UUID往往是使用字符串存储,查询的效率比较低。

3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。

4)传输数据量大

5)不可读。

Redis

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。

优点:

1)不依赖于数据库,灵活方便,且性能优于数据库。

2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。

2)需要编码和配置的工作量比较大。

3)网络传输造成性能下降。

开源算法snowflake

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
在这里插入图片描述

综合比较下来snowflake更好些,内置在项目中,不需要通过网络传输速度更快,而且是long类型可以排序,更好。

雪花算法工具类

package com.changgou.util;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
 * <p>名称:IdWorker.java</p>
 * <p>描述:分布式自增长ID</p>
 * <pre>
 *     Twitter的 Snowflake JAVA实现方案
 * </pre>
 * 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
 * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
 * 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
 * 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
 * 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
 * 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
 * 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
 * <p>
 * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
 *
 * @author Polim
 */
public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    private final static long twepoch = 1288834974657L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
            /*
             * GET jvmPid
             */
            mpid.append(name.split("@")[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }


    public static void main(String[] args) {

        IdWorker idWorker=new IdWorker(0,0);

        for(int i=0;i<10000;i++){
            long nextId = idWorker.nextId();
            System.out.println(nextId);
        }
    }
}

注入和运用
在启动类上注入IdWorker

@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.goods.dao"})
public class GoodsApplication {

    @Value("${workerId}")
    private Integer workerId;

    @Value("${datacenterId}")
    private Integer datacenterId;

    @Bean
    public IdWorker idWorker(){
        return new IdWorker(workerId,datacenterId);
    }

    public static void main(String[] args) {
        SpringApplication.run( GoodsApplication.class);
    }
}

yml中注入属性
datactenterId 中心的编号 这里比如杭州1,宁波2,这样分区
workerId 工作机器编号

workerId: 0
datacenterId: 0

这样哪里需要用@Autowired注入使用即可

商品SPU和SKU

SPU = Standard Product Unit (标准产品单位)

概念 : SPU 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

通俗点讲,属性值、特性相同的货品就可以称为一个 SPU

例如:华为P30 就是一个 SPU

SKU=stock keeping unit( 库存量单位)

SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。

SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。

在服装、鞋类商品中使用最多最普遍。

例如:华为P30 红色 64G 就是一个 SKU

字段名称 字段含义 字段类型 字段长度 备注
id 主键 VARCHAR
sn 货号 VARCHAR
name SPU名 VARCHAR
caption 副标题 VARCHAR
brand_id 品牌ID INT
category1_id 一级分类 INT
category2_id 二级分类 INT
category3_id 三级分类 INT
template_id 模板ID INT
freight_id 运费模板id INT
image 图片 VARCHAR
images 图片列表 VARCHAR
sale_service 售后服务 VARCHAR
introduction 介绍 TEXT
spec_items 规格列表 VARCHAR
para_items 参数列表 VARCHAR
sale_num 销量 INT
comment_num 评论数 INT
is_marketable 是否上架 CHAR
is_enable_spec 是否启用规格 CHAR
is_delete 是否删除 CHAR
status 审核状态 CHAR

tb_sku 表(SKU商品表)

字段名称 字段含义 字段类型 字段长度 备注
id 商品id VARCHAR
sn 商品条码 VARCHAR
name SKU名称 VARCHAR
price 价格(分) INT
num 库存数量 INT
alert_num 库存预警数量 INT
image 商品图片 VARCHAR
images 商品图片列表 VARCHAR
weight 重量(克) INT
create_time 创建时间 DATETIME
update_time 更新时间 DATETIME
spu_id SPUID BIGINT
category_id 类目ID INT
category_name 类目名称 VARCHAR
brand_name 品牌名称 VARCHAR
spec 规格 VARCHAR
sale_num 销量 INT
comment_num 评论数 INT
status 商品状态 1-正常,2-下架,3-删除 CHAR

定义一个goods对象来存储

/**
 * 商品组合实体类
 */
public class Goods implements Serializable {private Spu spu;
    private List<Sku> skuList;public Spu getSpu() {
        return spu;
    }public void setSpu(Spu spu) {
        this.spu = spu;
    }public List<Sku> getSkuList() {
        return skuList;
    }public void setSkuList(List<Sku> skuList) {
        this.skuList = skuList;
    }
}

在这里插入图片描述
中间表的实体类

@Table(name="tb_category_brand")
public class CategoryBrand implements Serializable {@Id
    private Integer categoryId;@Id
    private Integer brandId;
​
​
    public Integer getCategoryId() {
        return categoryId;
    }public void setCategoryId(Integer categoryId) {
        this.categoryId = categoryId;
    }public Integer getBrandId() {
        return brandId;
    }public void setBrandId(Integer brandId) {
        this.brandId = brandId;
    }
}

记得创建访问接口

public interface CategoryBrandMapper extends Mapper<CategoryBrand> {
  
}

spu中加入添加字段的方法

    /***
     * 新增
     * @param goods
     */
    void add(Goods goods);

在这里插入图片描述

    @Autowired
    private CategoryMapper categoryMapper;@Autowired
    private SkuMapper skuMapper;@Autowired
    private BrandMapper brandMapper;@Autowired
    private IdWorker idWorker;/**
     * 保存商品 SPU+SKU列表
     * @param goods 商品组合实体类
     */
    @Transactional
    @Override
    public void add(Goods goods) {
    //根据传过来的goods对象,拿到spu对象,工具类生成id赋值,设置未删除,未上架,当前状态
        Spu spu = goods.getSpu();
        long spuId = idWorker.nextId();
        spu.setId(String.valueOf(spuId));
        spu.setIsDelete("0");
        spu.setIsMarketable("0");
        spu.setStatus("0");
        spuMapper.insertSelective(spu);//保存sku集合数据到数据库
        saveSkuList(goods);
    }/**
     * 保存sku列表
     * @param goods
     */
    private void saveSkuList(Goods goods){
        //获取spu对象
        Spu spu = goods.getSpu();
        //当前日期
        Date date = new Date();
        //获取品牌对象
        Brand brand = brandMapper.selectByPrimaryKey(spu.getBrandId());
        //获取分类对象,这里拿三级分类的id
        Category category = categoryMapper.selectByPrimaryKey(spu.getCategory3Id());
        //获取sku集合对象
        List<Sku> skuList = goods.getSkuList();
        if (skuList != null) {
            for (Sku sku : skuList) {
                //设置sku主键ID
                sku.setId(String.valueOf(idWorker.nextId()));
                //设置sku规格
                if (sku.getSpec() == null || "".equals(sku.getSpec())) {
                //如果为空需要传入一个空对象
                    sku.setSpec("{}");
                }
                //设置sku名称(商品名称 + 规格)
                String name = spu.getName();
                //将规格json字符串转换成Map
                Map<String, String> specMap = JSON.parseObject(sku.getSpec(), Map.class);
                if (specMap != null && specMap.size() > 0) {
                //sku的名称为: spu名称+规格1值+“ ”+规格2的值+“ ” +规格3的值.....
                    for(String value : specMap.values()){
                        name += " "+ value;
                    }
                }
​
                sku.setName(name);//名称
                sku.setSpuId(spu.getId());//设置spu的ID
                sku.setCreateTime(date);//创建日期
                sku.setUpdateTime(date);//修改日期
                sku.setCategoryId(category.getId());//商品分类ID
                sku.setCategoryName(category.getName());//商品分类名称
                sku.setBrandName(brand.getName());//品牌名称
                skuMapper.insertSelective(sku);//插入sku表数据
            }
        }
    }

商品审核上下架

商品新增后,审核状态为0(未审核),默认为下架状态。

审核商品,需要校验是否是被删除的商品,如果未删除则修改审核状态为1,并自动上架

@Transactional
    public void audit(String id) {
        //查询spu对象
        Spu spu = spuMapper.selectByPrimaryKey(id);
        if (spu == null){
            throw new RuntimeException("当前商品不存在");
        }
        //判断当前spu是否处于删除状态
        if ("1".equals(spu.getIsDelete())){
            throw new RuntimeException("当前商品处于删除状态");
        }
        //不处于删除状态,修改审核状态为1,上下架状态为1
        spu.setStatus("1");
        spu.setIsMarketable("1");
        //执行修改操作
        spuMapper.updateByPrimaryKeySelective(spu);
    }
    /**
     * 审核
     * @param id
     * @return
     */
    @PutMapping("/audit/{id}")
    public Result audit(@PathVariable String id){
        spuService.audit(id);
        return new Result();
    }

下架商品,需要校验是否是被删除的商品,如果未删除则修改上架状态为0

@Transactional
    public void pull(String id) {
        //查询spu
        Spu spu = spuMapper.selectByPrimaryKey(id);
        if (spu == null){
            throw new RuntimeException("当前商品不存在");
        }
        //判断当前商品是否处于删除状态
        if ("1".equals(spu.getIsDelete())){
            throw new RuntimeException("当前商品处于删除状态");
        }
        //商品处于未删除状态的话,则修改上下架状态为已下架(0)
        spu.setIsMarketable("0");
        spuMapper.updateByPrimaryKeySelective(spu);
    }
   /**
     * 下架
     * @param id
     * @return
     */
    @PutMapping("/pull/{id}")
    public Result pull(@PathVariable String id){
        spuService.pull(id);
        return new Result();
    }

上架商品,需要审核状态为1,如果为1,则更改上下架状态为1

    /**
     * 上架商品
     * @param id
     */
    @Override
    public void put(String id) {
        Spu spu = spuMapper.selectByPrimaryKey(id);
        if(!spu.getStatus().equals("1")){
            throw new RuntimeException("未通过审核的商品不能上架!");
        }
        spu.setIsMarketable("1");//上架状态
        spuMapper.updateByPrimaryKeySelective(spu);
    }
    /**
     * 上架
     * @param id
     * @return
     */
    @PutMapping("/put/{id}")
    public Result put(@PathVariable String id){
        spuService.put(id);
        return new Result();
    }

删除与还原

商品列表中的删除商品功能,并非真正的删除(物理删除),而是采用逻辑删除将删除标记的字段设置为1.

在回收站中有还原商品的功能,将删除标记的字段设置为0

    /**
     * 恢复数据
     * @param id
     */
    @Override
    public void restore(String id) {
        Spu spu = spuMapper.selectByPrimaryKey(id);
        //检查是否删除的商品
        if(!spu.getIsDelete().equals("1")){
            throw new RuntimeException("此商品未删除!");
        }
        spu.setIsDelete("0");//未删除
        spu.setStatus("0");//未审核
        spuMapper.updateByPrimaryKeySelective(spu);
    }    
    /**
     * 恢复数据
     * @param id
     * @return
     */
    @PutMapping("/restore/{id}")
    public Result restore(@PathVariable String id){
        spuService.restore(id);
        return new Result();
    }

在回收站中有删除商品的功能,是真正的物理删除,将数据从数据库中删除掉。
逻辑删除

    /**
     * 删除
     * @param id
     */
    @Override
    public void delete(String id){
        Spu spu = spuMapper.selectByPrimaryKey(id);
        //检查是否下架的商品
        if(!spu.getIsMarketable().equals("0")){
            throw new RuntimeException("必须先下架再删除!");
        }
        spu.setIsDelete("1");//删除
        spu.setStatus("0");//未审核
        spuMapper.updateByPrimaryKeySelective(spu);
    }
  @Override
    public void realDelete(String id) {
        Spu spu = spuMapper.selectByPrimaryKey(id);
        //检查是否删除的商品
        if(!spu.getIsDelete().equals("1")){
            throw new RuntimeException("此商品未删除!");
        }
        spuMapper.deleteByPrimaryKey(id);
        
    }

物理删除


    /**
     * 物理删除
     * @param id
     * @return
     */
    @DeleteMapping("/realDelete/{id}")
    public Result realDelete(@PathVariable String id){
        spuService.realDelete(id);
        return new Result();
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值