智牛股_第9章_CEPH_Swift+文件上传与下载

智牛股_第9章_CEPH_Swift+文件上传与下载

学习目标

目标1:CEPH Swift 服务配置与实践
目标2:CEPH文件操作接口封装与集成使用
目标3:基于SnowFlake算法的全局唯一ID的使用
目标4:统一认证的高性能方案Token增强技术使用
目标5:文件上传与下载接口实现
目标6:Spring Boot多环境配置与打包实践

第1章 CEPH Swift Api实践

1. 目标

  • 掌握Ceph Swift Api 特点与存储结构

  • 掌握Ceph Swift Api 的配置与使用, 完成文件的上传与下载接口, 为下面的项目集成使用与封装做预热。

2. 步骤

  • Ceph Swift Api 实践说明
  • Ceph Swift Api 特点
  • Ceph RGW 介绍
  • Ceph 存储结构
  • Ceph Swift Api 服务配置
  • Ceph Swift Api 调用验证

3. 实现

3.1 Ceph Swift Api 实践说明

前面我们讲过ceph的集群安装与使用,集群有一定操作难度,大家要通过集群安装加强对Ceph的理解与运用,ceph是一个分布式文件存储系统,需要至少准备三至四台虚拟机, 之前有过详细介绍,可参考之前的讲义, 就不再赘述。

在ceph的使用上, 之前我们是采用fs文件系统, 并编写了相应的api来操作演示。 但互联网大规模的文件场景下, fs并不能满足生产的使用要求,rados本地化操作也不便于服务的接入与使用, 这里我们就要采用Ceph Swift Api 来实现文件的存储管理。

3.2 Ceph Swift Api 特点

Swift是由Rackspace开发的用来为云计算提供可扩展存储的项目。专注于对象存储, 并提供一套REST风格的Api来访问, 与Ceph强一致性不同, 它是最终一致性。两者都是优秀的开源项目, 并无明显优劣之分,在使用场景上有所不同, 如果是专注于对象存储, 那么可以选择swift即可满足需要, 如果还有块存储要求, 那么选择Ceph更为合适。这里选择Ceph, 因为通过网关可以适配兼容swift api, 同时在数据访问上具有较强的扩展性:

  • Ceph可通过Rados网关用兼容S3的RESTful API访问,对AWS云环境下的其他内容也能很好的兼容, 比如OpenStack Swift的对象存储访问接口。
  • CephFS:是一个POSIX兼容的文件系统,可以在任何Linux发行版上运行,操作系统可直接访问Ceph存储。
  • RDB:RBD是一个Linux内核级的块设备,允许用户像任何其他Linux块设备一样访问Ceph。
  • ISCSI 网关: 这一增加的功能是SUSE加上去的,它允许管理员在Ceph之上运行iSCSI网关,从而将其转变为任何操作系统都可以访问的SAN文件管理器。

在一些大型互联网公司也开始采用 Ceph Swift 接口来处理海量文件信息, 参考:携程网的Ceph实践之路

3.3 Ceph RGW 介绍

Ceph可以提供块、文件和对象三种形态的存储。RGW就是提供对象存储的网关,也即对象存储网关。所谓对象存储网关,也就是对象存储的入口,本质上是一个HTTP服务器,与Nginx和Apache无特殊差别。通过这个网关入口,用户可以采用HTTP协议,以RESTful的方式访问Ceph的对象存储。Ceph网关能够适配兼容AWS S3和OpenStack Swift 的API接口。

3.4 Ceph 存储结构

在使用对象存储之前, 先要了解桶(container容器)概念及其存储结构:

Ceph Swift Api的调用, 需要先创建相应用户进行认证才能操作, 每个用户下面可以创建多个桶, 桶里面可以存储对象,对象就是各种数据文件, 包括文档, 图片等。传统上传文件的使用, 我们往往会指定路径信息, 在这里, 桶和对象的关系好比文件夹与文件的概念, 不同之处是桶不能再嵌套桶, 也就是没有层级路径的概念。

Ceph存储结构:

在这里插入图片描述

3.5 Ceph Swift Api 服务端的配置
  1. 确保集群正常安装并启动:
[root@CENTOS7-1 ceph-cluster]# ceph -s
  cluster:
    id:     0ec99aa9-e97e-43d3-b5b9-90eb21c4abff
    health: HEALTH_OK
 
  services:
    mon: 3 daemons, quorum CENTOS7-1,CENTOS7-2,CENTOS7-3
    mgr: centos7-1(active), standbys: centos7-3, centos7-2
    mds: fs_test-1/1/1 up  {0=centos7-1=up:active}
    osd: 3 osds: 3 up, 3 in
    rgw: 3 daemons active
 
  data:
    pools:   9 pools, 128 pgs
    objects: 257  objects, 166 KiB
    usage:   3.0 GiB used, 57 GiB / 60 GiB avail
    pgs:     128 active+clean
  1. 验证网关是否正常

    访问地址,http://10.10.20.11:7480

    出现以下提示代表正常

在这里插入图片描述

  1. 创建Swift用户, 用于接口请求认证
sudo radosgw-admin user create --subuser="cephtester:subtester" --uid="cephtester" --display-name="cephtester" --key-type=swift --secret="654321" --access=full

uid 为主用户, subuser为子用户信息, secret指定密钥, 不指定则随机生成, access拥有权限设定。

返回结果:

{
    "user_id": "cephtester",
    "display_name": "cephtester",
    "email": "",
    "suspended": 0,
    "max_buckets": 1000,
    "auid": 0,
    "subusers": [
        {
            "id": "cephtester:subtester",
            "permissions": "full-control"
        }
    ],
    "keys": [],
    "swift_keys": [
        {
            "user": "cephtester:subtester",
            "secret_key": "654321"
        }
    ],
    "caps": [],
    "op_mask": "read, write, delete",
    "default_placement": "",
    "placement_tags": [],
    "bucket_quota": {
        "enabled": false,
        "check_on_raw": false,
        "max_size": -1,
        "max_size_kb": 0,
        "max_objects": -1
    },
    "user_quota": {
        "enabled": false,
        "check_on_raw": false,
        "max_size": -1,
        "max_size_kb": 0,
        "max_objects": -1
    },
    "temp_url_keys": [],
    "type": "rgw",
    "mfa_ids": []
}

记住swift_keys下面的user和secret_key信息, 代码中需使用。

  1. 激活管理后台的对象存储模块:

  2. 创建一个管理用户:

     radosgw-admin user create --uid=mgruser --display-name=mgruser  --system
    
    

    返回结果:

    {
        "user_id": "mgruser",
        "display_name": "mgruser",
        "email": "",
        "suspended": 0,
        "max_buckets": 1000,
        "auid": 0,
        "subusers": [],
        "keys": [
            {
                "user": "mgruser",
                "access_key": "XZDC4Y0AORE01NMYU3VV",
                "secret_key": "me0Z80HLvctkzzBs74ufXS0Wi947NBe6Wpj3MxKP"
            }
        ],
        "swift_keys": [],
        "caps": [],
        "op_mask": "read, write, delete",
        "system": "true",
        "default_placement": "",
        "placement_tags": [],
        "bucket_quota": {
            "enabled": false,
            "check_on_raw": false,
            "max_size": -1,
            "max_size_kb": 0,
            "max_objects": -1
        },
        "user_quota": {
            "enabled": false,
            "check_on_raw": false,
            "max_size": -1,
            "max_size_kb": 0,
            "max_objects": -1
        },
        "temp_url_keys": [],
        "type": "rgw",
        "mfa_ids": []
    }
    

    根据生成的access_key与secret_key, 执行:

    ceph dashboard set-rgw-api-access-key XZDC4Y0AORE01NMYU3VV
    ceph dashboard set-rgw-api-secret-key me0Z80HLvctkzzBs74ufXS0Wi947NBe6Wpj3MxKP
    

    打开管理界面,http://10.10.20.11:18843 可以查看到我们刚才创建的两个用户:

在这里插入图片描述

3.6 Ceph Swift Api 调用验证
  1. 修改ceph-demo工程:
    在这里插入图片描述

    增加SwiftOperator接口:

    @Component
    @Log4j2
    public class SwiftOperator {
    
        /**
         * 用户名信息, 格式: 主用户名:子用户名
         */
        private String username ="cephtester:subtester";
    
        /**
         * 用户密码
         */
        private String password = "654321";
    
        /**
         * 接口访问地址
         */
        private String authUrl = "http://10.10.20.11:7480/auth/1.0";
    
        /**
         * 默认存储的容器名称
         */
        private String defaultContainerName = "user_datainfo";
    
        /**
         * Ceph的账户信息
         */
        private Account account = null;
    
        /**
         * Ceph的容器信息
         */
        private Container container;
    
        /**
         * 进行Ceph的初始化配置
         */
        public SwiftOperator() {
            // 1. Ceph的账户信息配置
            AccountConfig config = new AccountConfig();
            config.setUsername(username);
            config.setPassword(password);
            config.setAuthUrl(authUrl);
            config.setAuthenticationMethod(AuthenticationMethod.BASIC);
            account = new AccountFactory(config).createAccount();
    
            // 2.获取容器信息
            Container newContainer = account.getContainer(defaultContainerName);
            if(!newContainer.exists()) {
                container = newContainer.create();
                log.info("container create ==> " + defaultContainerName);
            }else {
                container = newContainer;
            }
    
        }
    
        /**
         * 文件上传处理
         * @param remoteName
         * @param filePath
         */
        public void createObject(String remoteName, String filePath) {
            // 1. 从容器当中获取远程存储对象信息
            StoredObject object = container.getObject(remoteName);
            // 2. 执行文件上传处理
            object.uploadObject(new File(filePath));
        }
    
    
        /**
         * 文件的下载处理
         * @param objectName
         * @param outPath
         */
        public void retrieveObject(String objectName, String outPath) {
            // 1. 从容器当中获取远程存储对象信息
            StoredObject object = container.getObject(objectName);
            // 2. 执行文件的下载方法
            object.downloadObject(new File(outPath));
    
        }
    
        /**
         * 获取用户下面的所有容器信息
         * @return
         */
        public List listContainer() {
            List list = new ArrayList();
            Collection<Container> containers = account.list();
            for(Container container : containers) {
                list.add(container.getName());
                log.info("current container name : " + container.getName());
         }
            return list;
        }
    
    
    }
    

    这里的用户名和密码填写上面我们所生成的信息。注意路径地址后缀为: /auth/1.0

    CephDemoApplication启动类重新封装:

    @SpringBootApplication
    @ComponentScan(basePackages = {"com.itcast"})
    public class CephDemoApplication {
    
    
        public static void main(String[] args) throws Exception  {
    
            // Swift Api接口调用验证
            swiftApi();
        }
    
        /**
         *  Rados Api的封装处理
         * @throws Exception
         */
        public static void radosApi(String[] args) throws Exception  {
            System.out.println("start....");
            String username = "admin";
            String monIp = "10.10.20.11:6789;10.10.20.12:6789;10.10.20.13:6789";
            String userKey = "AQBZBypdMchvBRAAbWVnIGyYNvxWQZ2UkuiYew==";
            String mountPath = "/";
            CephOperator cephOperate = null;
            try {
                String opt = (args == null || args.length < 1)? "" : args[0];
                cephOperate = new CephOperator(username, monIp, userKey, mountPath);
                if("upload".equals(opt)) {
                    cephOperate.uploadFileByPath("/temp_upload_fs", args[1]);
                }else if("download".equals(opt)) {
                    cephOperate.downloadFileByPath("/temp_download_fs", args[1]);
                }else {
                    System.out.println("Unrecognized Command! Usage  opt[upload|download] filename[path]!");
                }
            }catch(Exception e) {
                e.printStackTrace();
            }finally {
                if(null != cephOperate) {
                    cephOperate.umount();
                }
            }
            System.out.println("end....");
    
        }
    
    
        /**
         * 通过Swift接口操作ceph集群
         * @throws Exception
         */
        public static void swiftApi() throws Exception  {
    
            ConfigurableApplicationContext appContext =  SpringApplication.run(CephDemoApplication.class);
            // 1. 先打印出用户的容器信息
            SwiftOperator swiftOperator = appContext.getBean(SwiftOperator.class);
            swiftOperator.listContainer();
    
            String objName = "test_ceph";
            // 2. 上传指定的文件
            swiftOperator.createObject(objName, "d:/test_swift_ceph.txt");
            // 3. 从ceph下载文件到指定的路径下面
            swiftOperator.retrieveObject(objName, "e:/test.txt");
    
            System.out.println("complete");
    
        }
    }
    
  2. 测试验证

    测试思路, 在d盘创建一个文件, 并上传到ceph系统, 然后从ceph系统下载到指定路径下面。

    这里要注意,我们默认的容器配置的是”user_datainfo“, 从ceph系统上传和下载的文件名称要一致, 启动打印"complete" , 无异常代表执行成功。

在这里插入图片描述

这里简要演示, 接下来会讲解如何在项目中集成使用。

2. 总结

  • Ceph提供了多种API调用方式, 这里采用了兼容性较强的Ceph Swift Api调用方式,了解Ceph RGW的功能作用, 在API调用之前, 必须要做好对应的服务配置, 从该案例中, 对Ceph Api的使用要有一定了解, 接下来会在项目中做具体集成使用。

第2章 用户资料上传与下载功能

1. 目标

  • 掌握Ceph的封装与自动化装配
  • 掌握基于snowflake的全局ID使用
  • 完成上传与下载功能

2. 步骤

  • 用户资料上传与下载的业务功能流程
  • Ceph封装与自动化装配
  • 文件上传功能实现
  • Snowflake全局分布式ID实现
  • 利用增强token技术获取上传用户信息
  • 上传功能的使用验证
  • 文件下载功能的实现
  • 文件下载功能验证

3. 实现

3.1 功能流程

在这里插入图片描述

​ 在用户开户功能中, 需要填写用户资料, 上传身份证等证明文件, 业务上步骤是分离处理, 系统需提供用户的文件上传 与下载接口。

3.2 Ceph封装与自动化装配
  1. 新建stock-starter工程, 用于封装自动化组件。

  2. 创建ceph自动化工程:
    在这里插入图片描述

  3. pom文件依赖:

    <dependencies>
            <!-- ceph 依赖 -->
            <dependency>
                <groupId>com.ceph</groupId>
                <artifactId>rados</artifactId>
                <version>0.6.0</version>
            </dependency>
    
            <!-- ceph fs 操作依赖 -->
            <dependency>
                <groupId>com.ceph</groupId>
                <artifactId>libcephfs</artifactId>
                <version>0.80.5</version>
            </dependency>
    
            <!-- ceph swift 依赖 -->
            <dependency>
                <groupId>org.javaswift</groupId>
                <artifactId>joss</artifactId>
                <version>0.10.2</version>
            </dependency>
        </dependencies>
    
    

    直接采用目前的最新版, 加入以上三个依赖, fs依赖不需要也可以去除。

  4. 代码实现

    封装Ceph操作接口, CephSwiftOperator类:

    public class CephSwiftOperator {
    
        private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
    
        /**
         * 用户名
         */
        private String username;
    
        /**
         * 密码
         */
        private String password;
    
        /**
         * 认证接入地址
         */
        private String authUrl;
    
        /**
         * 默认容器名称
         */
        private String defaultContainerName;
    
        /**
         * Ceph账户对象
         */
        private Account account;
    
        /**
         * Ceph容器对象
         */
        private Container container;
    
    
        public CephSwiftOperator(String username, String password, String authUrl, String defaultContainerName) {
            // 初始化配置信息
            this.username = username;
            this.password = password;
            this.authUrl = authUrl;
            this.defaultContainerName = defaultContainerName;
            init();
    
        }
    
        /**
         * 初始化建立连接
         */
        public void init() {
            try {
                // Ceph用户认证配置
                AccountConfig config = new AccountConfig();
                config.setUsername(username);
                config.setPassword(password);
                config.setAuthUrl(authUrl);
                config.setAuthenticationMethod(AuthenticationMethod.BASIC);
                account = new AccountFactory(config).createAccount();
                // 获取容器
                Container newContainer = account.getContainer(defaultContainerName);
                if (!newContainer.exists()) {
                    container = newContainer.create();
                    log.info("account container create ==> " + defaultContainerName);
                } else {
                    container = newContainer;
                    log.info("account container exists!  ==> " + defaultContainerName);
                }
            }catch(Exception e) {
                // 做异常捕获, 避免服务不能正常启动
                log.error("Ceph连接初始化异常: " + e.getMessage());
            }
        }
    
    
        /**
         * 上传对象
         * @param remoteName
         * @param filepath
         */
        public void createObject(String remoteName, String filepath) {
            StoredObject object = container.getObject(remoteName);
            object.uploadObject(new File(filepath));
        }
    
        /**
         * 上传文件对象(字节数组形式)
         * @param remoteName
         * @param inputStream
         */
        public void createObject(String remoteName, byte[] inputStream) {
            StoredObject object = container.getObject(remoteName);
            object.uploadObject(inputStream);
        }
    
        /**
         * 获取指定对象
         * @param containerName
         * @param objectName
         * @param outpath
         */
        public void  retrieveObject(String objectName,String outpath){
            StoredObject object = container.getObject(objectName);
            object.downloadObject(new File(outpath));
        }
    
        /**
         * 下载文件, 转为文件流形式
         * @param objectName
         * @return
         */
        public InputStream retrieveObject(String objectName){
            StoredObject object = container.getObject(objectName);
            return object.downloadObjectAsInputStream();
        }
    
    
        /**
         * 删除指定文件对象
         * @param containerName
         * @param objectName
         * @return
         */
        public boolean deleteObject(String objectName){
            try {
                StoredObject object = container.getObject(objectName);
                object.delete();
                return !object.exists();
            }catch(Exception e) {
                log.error("Ceph删除文件失败: " + e.getMessage());
            }
            return false;
        }
    
        /**
         * 获取所有容器
         * @return
         */
        public List listContainer() {
            List list = new ArrayList();
            Collection<Container> containers = account.list();
            for (Container currentContainer : containers) {
                list.add(currentContainer.getName());
                System.out.println(currentContainer.getName());
    
            }
            return list;
        }
    
    }
    
    
    • 此封装接口将纳入Spring 容器管理, 即为单例, 构造函数会初始化Ceph认证连接等信息

    • 将Account 与Container 设为成员变量, 便于复用, 减少开销。

    • 初始化会默认一个容器名称, 一般每个服务设置一个容器名称, 如果业务功能比较庞杂, 可以每个业务模块设置一个容器。

    • Swift Api已经做了较完善的封装, 我们内部使用比较简单, 主要封装上传和下载接口, 为便于调用处理, 做了进一步封装。

    AutoCephSwiftConfiguration自动化配置类:

    @Configuration
    @EnableAutoConfiguration
    @ConditionalOnProperty(name = "ceph.authUrl")
    public class AutoCephSwiftConfiguration {
    
        @Value("${ceph.username}")
        private String username;
        @Value("${ceph.password}")
        private String password;
        @Value("${ceph.authUrl}")
        private String authUrl;
        @Value("${ceph.defaultContainerName}")
        private String defaultContainerName;
    
    
        @Bean
        public CephSwiftOperator cephSwiftOperator() {
            return new CephSwiftOperator(username, password, authUrl, defaultContainerName);
        }
    
    }
    
    

    ConditionalOnProperty根据ceph.authUrl属性来决定是否加载配置,如果配置文件中没有设置Ceph相关属性, 即使maven中引用, 启动也不会报错。 该自动化配置, 负责初始化一个Ceph Swift 接口操作实例。

  5. 工程配置:

    要让自定义Ceph Starter真正生效, 必须遵循Spring boot 的SPI扩展机制, 在resources环境中, META-INF目录下, 创建spring.factories文件:

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.itcast.stock.starter.ceph.AutoCephSwiftConfiguration
    

    指定我们上面所写的自动化配置类。

  6. 用户服务工程配置:

    bootstrap.yml增加:

    # ceph swift 认证信息配置
    ceph:
      username: cephtester:subtester
      password: 654321
      authUrl: http://10.10.20.11:7480/auth/1.0
      defaultContainerName: user_datainfo
    
3.3 基于snowflake的全局分布式唯一ID
  1. snowflake算法:

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

在这里插入图片描述

  • 第一位:

    占用1bit,其值始终是0,预置为0。

  • 时间戳

    占用41bit,精确到毫秒,总共可以容纳约140年的时间。

  • 工作机器ID

    占用10bit,其中高位5bit是数据中心ID(datacenterId),低位5bit是工作节点ID(workerId),做多可以容纳1024个节点。

  • 序列号

    占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095。

了解snowflake的原理, 那么同一毫秒, 能生成的全局唯一不重复ID?

计算公式, 2^10 * 2^12 = 1024 * 4096 = 4194304。 完全可以满足高并发的场景使用要求。

  1. Java代码实现:

    /**
     * <p>名称:GlobalIDGenerator.java</p>
     * <p>描述:分布式自增长ID</p>
     * <pre>
     *     Twitter的 Snowflake JAVA实现方案
     * </pre>
     * 核心代码为其GlobalIDGenerator这个类实现,其原理结构如下,我分别用一个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(重复累加))
     * 生成ID示例;
     * 1154628864413139070
     * 1154628864413139071
     * 1154628864413139072
     * 1154628864413139073
     */
    public class GlobalIDGenerator {
        // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
        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;
    
        // 机器标识id, 分布式服务需设置不同编号, 不能超过31
    //    @Value("${snowflake.workerId:1}")
        private final long workerId;
        // 数据标识id部分, 业务编码, 不能超过31
    //    @Value("${snowflake.datacenterId:1}")
        private final long datacenterId;
    
        public GlobalIDGenerator() {
            this.datacenterId = getDatacenterId(maxDatacenterId);
            this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
        }
    
        /**
         * @param workerId     工作机器ID
         * @param datacenterId 序列号
         */
        public GlobalIDGenerator(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;
        }
    
        /**
         * 获取下一个ID, 字符形式
         * @return
         */
        public synchronized  String nextStrId(){
            return String.valueOf(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) {
            GlobalIDGenerator id = new GlobalIDGenerator(0, 1);
            for (int i = 0; i < 10000; i++) {
                System.err.println(id.nextId());
            }
        }
    }
    

    通过nextId方法, 直接生成全局唯一ID,该方法是有加锁处理,基于内存计算,不易产生阻塞, 实测每秒能产生26万ID(不同机器配置存在差别), 这里模拟一万条, 执行非常快。

在这里插入图片描述

采用SnowFlake方案, 实现与配置简单, 能保证全局唯一性, 同时还具有ID序列特征(单节点的有序性)。

  1. 项目集成

在这里插入图片描述

把它作为一个工具类, 放在stock-common-utils工程中。

用户服务工程使用:

在这里插入图片描述

在GlobalSystemConfiguration中, 配置bean, 纳入容器管理, 不用每次调用创建新对象, 减少开销:

    ...
        
    @Value("${snowflake.workerId}")
    private Long workerId;
    @Value("${snowflake.datacenterId}")
    private Long datacenterId;

    /**
     * 全局ID生成器
     * @return
     */
    @Bean
    public  GlobalIDGenerator globalIDGenerator() {
        if(null == workerId || null == datacenterId) {
            return new GlobalIDGenerator();
        }
        return new GlobalIDGenerator(workerId, datacenterId);
    }

如果没有配置workerId与datacenterId, 内部将会自动生成对应值。

bootstrap.yml配置:

# 全局唯一ID分布式标识配置
snowflake:
  workerId: 1
  datacenterId: 1

如果服务需要部署集群,按序号配置对应值, 不能随便配置, 每个服务配置的值不能重复,并且不能超过32, 因为workerId和datacenterId只有5bit,最大值为2^5。

3.4 文件上传功能
  1. 先创建实体:

    @Data
    public class TradeUserFile extends  BaseEntity {
    
        /**
         * 主键标识
         */
        private Long id;
    
        /**
         * 用户ID
         */
        private Long userId;
    
        /**
         * 业务类型(0:身份证, 1:银行卡, 2:信用卡)
         */
        private int bizType;
    
        /**
         * 文件ID
         */
        private String fileId;
    
        /**
         * 文件名称
         */
        private String filename;
    
        /**
         * 文件类型
         */
        private String fileType;
    
        /**
         * 文件路径
         */
        private String filePath;
    
        /**
         * 状态(0:有效, 1:无效)
         */
        private int status;
    
        /**
         * 创建时间
         */
        private Date createTime;
    
        /**
        * 更新时间
         */
        private Date updateTime;
    }
    
    

    与用户ID关联, 文件ID需保持全局唯一(支持分布式场景), 每个文件都有所属业务类型, 可以扩展。

  2. Dao数据层实现

    @Repository
    public interface ITradeUserFileDao {
    
        /**
         * 新增用户文件
         * @param record
         * @return
         */
        int insert(TradeUserFile record);
    
        /**
         * 更新用户文件
         * @param record
         * @return
         */
        int update(TradeUserFile record);
    
        /**
         * 根据用户ID获取对象
         * @param userId
         * @return
         */
        List<TradeUserFile> getByUserId(Long userId);
    
        /**
         * 根据文件标识ID获取对象
         * @param fileId
         * @return
         */
        TradeUserFile getByUserIdAndBizType(@Param("userId") Long userId, @Param("bizType")Integer bizType);
    
        /**
         * 根据文件ID获取对象
         * @param userId
         * @param bizType
         * @return
         */
        TradeUserFile getByFileId(@Param("fileId") Long fileId);
    
    }
    
    

    有五个数据层操作接口, 支持新增, 修改, 查询功能。 注意如果存在多个参数, 需要加上@Param注解,该注解全路径为org.apache.ibatis.annotations.Param,不要引用同名的其他路径注解。

    TradeUserFileMapper文件:

        <select id="getByUserId" resultMap="BaseResultMap">
          select id,
            userId, bizType, fileId,
            filename, fileType, filePath,
            status, createTime,updateTime
          from t_trade_user_file
          where userId = #{userId}
        </select>
    
        <select id="getByFileId" resultMap="BaseResultMap">
          select id,
            userId, bizType, fileId,
            filename, fileType, filePath,
            status, createTime,updateTime
          from t_trade_user_file
          where fileId = #{fileId}
        </select>
    
        <select id="getByUserIdAndBizType" resultMap="BaseResultMap">
          select id,
            userId, bizType, fileId,
            filename, fileType, filePath,
            status, createTime,updateTime
          from t_trade_user_file
          where userId = #{userId} and bizType = #{bizType}
        </select>
    
    
        <insert id="insert" parameterType="com.itcast.bulls.stock.entity.user.TradeUserFile">
            <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
                SELECT LAST_INSERT_ID()
            </selectKey>
            insert into t_trade_user_file (userId, bizType, fileId,
            filename, fileType, filePath,
            status, createTime)
            values (#{userId,jdbcType=BIGINT}, #{bizType,jdbcType=TINYINT}, #{fileId,jdbcType=VARCHAR},
            #{filename,jdbcType=VARCHAR}, #{fileType,jdbcType=VARCHAR}, #{filePath,jdbcType=VARCHAR},
            #{status,jdbcType=TINYINT}, #{createTime,jdbcType=TIMESTAMP})
        </insert>
    
    
        <insert id="update" parameterType="com.itcast.bulls.stock.entity.user.TradeUserFile">
            update t_trade_user_file
            set
                fileId = #{fileId},
                filename = #{filename},
                fileType = #{fileType}
            where
                id = #{id}
        </insert>
    
    

    insert 采用 selectKey方式获取主键, 插入完成之后, 会将ID保存至参数对象TradeUserFile的ID中。

  3. Service层实现:

    @Service
    public class StockUserFileServiceImpl implements IStockUserFileService {
    
        @Autowired
        private CephSwiftOperator cephSwiftOperator;
    
        @Autowired
        private GlobalIDGenerator globalIDGenerator;
    
        @Autowired
        private ITradeUserFileDao tradeUserFileDao;
    
        /**
         * 上传用户文件
         * @return
         */
        @Override
        public String uploadUserIdCard(Long userId, MultipartFile file) throws Exception {
    
            if(null == file) {
                // 文件不能为空
                throw new ComponentException(ApplicationErrorCodeEnum.PARAMS_FILE_NOT_NULL);
            }
    
            // 获取唯一文件ID标识
            String remoteFileId = globalIDGenerator.nextStrId();
    
            // 上传文件至CEPH
            cephSwiftOperator.createObject(remoteFileId, file.getBytes());
    
            // 查找对应用户文件
            TradeUserFile dbTradeUserFile = tradeUserFileDao.getByUserIdAndBizType(userId, GlobalConstants.FILE_BIZ_TYPE_IDCARD);
            if(null == dbTradeUserFile){
                // 新增用户文件
                dbTradeUserFile = new TradeUserFile();
                dbTradeUserFile.setBizType(GlobalConstants.FILE_BIZ_TYPE_IDCARD);
                dbTradeUserFile.setFilename(file.getOriginalFilename());
                dbTradeUserFile.setFileType(file.getContentType());
                dbTradeUserFile.setFileId(remoteFileId);
                dbTradeUserFile.setUserId(userId);
                dbTradeUserFile.setCreateTime(new Date());
                tradeUserFileDao.insert(dbTradeUserFile);
            }else {
    
                // 清理原有文件对象
                cephSwiftOperator.deleteObject(dbTradeUserFile.getFileId());
                // 更新用户文件
                dbTradeUserFile.setFileId(remoteFileId);
                dbTradeUserFile.setFilename(file.getOriginalFilename());
                dbTradeUserFile.setFileType(file.getContentType());
                tradeUserFileDao.update(dbTradeUserFile);
            }
    
            return remoteFileId;
        }
    
    }
    
    • 先做校验, 获取全局文件ID标识, 采用雪花算法实现, 支持分布式, 每秒可以产生26万ID, 能够满足高并发场景要求。

    • 上传文件至Ceph系统, 通过封装的cephSwiftOperator实现, ceph 可以支持文件流和字节数组方式传递, 文件不是很大的情况, 可以直接采用字节数组方式。

    • 在保存数据库之前, 先要查找用户之前是否已经上传过该业务类型文件, 如果为空, 则新增;

    • 如果之前已经上传, 则覆盖更新, 每个用户的每个业务类型, 只能存在一条数据。 在覆盖更新之前,

      要将原来上传的物理文件删除, 再做更新。

    • 注意整体的实现顺序,是有考究的。 这里实现了多个功能, 为尽量保证一致性, 我们把容易出现问题的文件上传功能放在前面执行, 因为依赖第三方ceph服务, 且持续时间较长; 把SQL操作放在最后。 这样就不会出现有数据库记录, 但没有文件存在的情况。 当然, 弊端是如果数据库保存失败, 上传的文件就成了临时文件, 但这种情况不影响业务, 是可以接收的, 因为业务数据的一致性就更为重要, 后面也可以通过补偿机制, 清理无效的临时文件。

  4. Controller层实现:

    在StockUserController下面, 增加用户身份证上传接口:

    @PostMapping("/uploadIdCard")
    public ApiRespResult uploadIdCard(@RequestParam("file") MultipartFile file) {
        ApiRespResult  result = null;
        try {
            // 获取用户ID
            Long userId = getUserId();
            // 保存用户上传文件
            String userFileId = stockUserFileService.uploadUserIdCard(userId, file);
            result = ApiRespResult.success(userFileId);
        }catch(ComponentException e) {
            log.error(e.getMessage(), e);
            result = ApiRespResult.error(e.getErrorCodeEnum());
        }catch(Exception e) {
            log.error(e.getMessage(), e);
            result = ApiRespResult.sysError(e.getMessage());
        }
        return result;
    }
    

    通过Spring boot实现文件上传, 注意以表单form形式提交, 类型为multipart/form-data, 参数名为file, 非直接数据流方式上传, 不然服务不能正常识别接收文件。

    文件上传还有个必要参数是用户ID, 但这里没有设置参数来接收, 我们直接通过增强的TOKEN获取到用户ID, 减少不必要的参数传递。

    getUserId()方法,可以先作为空实现, 完成下面token增强的封装后再做完善.

3.5 利用增强Token高效获取信息
  1. 方案说明

    在微服务中, OATUH2的TOKEN处理方案有多种, 有通过统一网关加工处理, 其他服务不参与, 直接通过网关获取用户信息, 但网关职能变得就不再单一, 且其他服务安全性薄弱;有通过JWT TOKEN方式, 各服务直接从JWT中获取用户信息, 但附带信息有限, 且要加解密处理,占用计算成本, 传递JWT的长度比之前token增加数倍; 还有方案是, 各服务直接拿token远程调用认证服务, 获取用户信息,远程连接开销非常高, 且增加认证服务的压力。

    方案其实是可以灵活多样, 这里我们采用一种更高性能的方案:

在这里插入图片描述

  • 用户认证, 生成增强Token(增加用户ID,账号等主要信息), 存储至Redis缓存中。

  • 在接下来的接口请求当中, 通过Token去Redis缓存里面直接获取增强Token信息, 从而获取用户ID等主要信息。

  • 这种方案相比以上方案, 能减少交互次数,不再需要请求统一认证服务, 减少加密解密等计算成本, 同时减少数据传递, 只要最基本的Token即可。但此方案的弊端, 必须认证服务和各个服务都能直接访问Redis缓存, 如果是跨项目, 跨区域, 需要暴露外网的情形下, 那么不推荐此方案, 毕竟还是要保障数据的安全性。

  1. 项目集成使用

    这里, 需要通过增强token来获取用户的ID信息, 便于我们在接入层使用。

    修改用户服务工程stock-user:

    • 封装BaseController, 作为所有Controller的基类, 这里处理一些公用逻辑,便于复用。

      getUserId() 获取用户ID方法实现:

          /**
           * 获取当前请求的token
           * @return
           */
          protected String getCurrentToken() {
              HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                      .getRequest();
              String token = request.getHeader("Authorization");
              if(null != token) {
                  token = token.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();
              }
              return token;
          }    
      
          /**
           * 返回用户ID
           * @return
           */
          protected Long getUserId() throws ComponentException {
              // 获取TOKEN信息
              OAuth2AccessToken auth2AccessToken = tokenStore.readAccessToken(getCurrentToken());
              if(null == auth2AccessToken) {
                  throw new ComponentException(ApplicationErrorCodeEnum.SYS_NOT_VALID_TOKEN);
              }
      
              // 获取TOKEN的增强信息
              Map<String, Object> additionalInfos = auth2AccessToken.getAdditionalInformation();
              if(null == additionalInfos) {
                  throw new ComponentException(ApplicationErrorCodeEnum.SYS_NOT_ACCESS_USER);
              }
      
              // 获取用户ID信息
              Long userId= (Long)additionalInfos.get(GlobalConstants.OAUTH_DETAILS_USER_ID);
              if(null == userId) {
                  throw new ComponentException(ApplicationErrorCodeEnum.SYS_NOT_ACCESS_USER);
              }
      
              return userId;
       }
      
      • 先获取Token, 每个接口请求,都会在头部附带Bearer Token, 由getCurrentToken方法负责处理。
      • 根据Token, 通过TokenStore从缓存中获取信息, 为空则抛出异常。
      • 拿到缓存信息后, 从Map<String, Object>里面获取用户ID, 同时做相应的异常判断。
    • TokenStore从哪来? 这里要做相应配置

      在GlobalSystemConfiguration中配置:

          ...
      
          @Autowired
          private RedisConnectionFactory redisConnectionFactory;
      
      
          /**
           * TokenStore实现方式, 采用Redis缓存
           * @return
           */
          @Bean
          public TokenStore tokenStore() {
              RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
              tokenStore.setPrefix(GlobalConstants.OAUTH_PREFIX_KEY);   
           return tokenStore;
          }
      

      这里要与认证服务的TokenStore的配置保持一致,否则不能正常拿到Token缓存。

      注意增加依赖:

    org.springframework.boot spring-boot-starter-data-redis ${spring-cloud-starter.version} ``` ```
    • 最后, 在工程配置中增加Redis配置信息:

      spring:
        # Redis 缓存配置
        redis:
          host: 127.0.0.1
          password:
          port: 6379
      
    
      这样, 启动工程之后, 就可以直接BaseController的getUserId方法获取用户ID,   在上面的上传文件功能controller层实现就可以可调用封装的方法。
    
    
3.6 上传功能使用验证
  1. 启动服务

    • 启动ceph集群(ceph -s 确认状态是正常)

    • 启动trade-auth服务

    • 启动stock-user服务

  2. 申请token

    在这里插入图片描述
    在这里插入图片描述

  3. 上传文件

在这里插入图片描述

注意构造参数, 表单为form形式, 参数名称为file, 类型要选择File, 才能选择文件。

可以看到接口成功返回了文件ID: 1155121848879550464。

  1. 查看数据库

    在这里插入图片描述

    查看t_trade_user_file表, 数据成功保存。

    接下来实现文件下载功能, 验证文件能否通过ceph成功获取。

3.7 文件下载功能

新增一个接口, 根据文件ID标识下载文件。

  1. Dao层:

    ITradeUserFileDao接口:

        /**
         * 根据文件ID获取对象
         * @param userId
         * @param bizType
         * @return
         */
        TradeUserFile getByFileId(@Param("fileId") Long fileId);
    

    增加根据文件ID获取文件信息的接口。

  2. Service层:

    StockUserFileServiceImpl:

        /**
         * 根据文件ID查找文件对象
         * @param fileId
         * @return
         */
        @Override
        public TradeUserFile getTradeUserFile(String fileId) {
    
            return tradeUserFileDao.getByFileId(Long.valueOf(fileId));
        }
    
    
  3. Controller层:

        @RequestMapping(value = "/downloadFile", method = {RequestMethod.GET, RequestMethod.POST} )
        public  ApiRespResult downloadFile(@NotBlank(message = "文件ID不能为空!") String fileId) {
    
           HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getResponse();
    
            // 获取用户文件
            TradeUserFile tradeUserFile = stockUserFileService.getTradeUserFile(fileId);
            if (null == tradeUserFile) {
                return ApiRespResult.error(ApplicationErrorCodeEnum.USER_FILE_NOT_FOUND);
            }
            BufferedInputStream bis = null;
            OutputStream os = null;
            try {
                // 获取文件流
                InputStream inputStream = cephSwiftOperator.retrieveObject(fileId);
                if(null == tradeUserFile.getFileType()) {
                    response.setContentType("application/force-download");// 设置强制下载不打开
                }
                // 设置文件响应类型
                response.setContentType(tradeUserFile.getFileType());
                // 设置文件名
                response.addHeader("Content-Disposition", "attachment;fileName=" + tradeUserFile.getFilename());
                // 流式缓冲下载处理
                byte[] buffer = new byte[1024];
                bis = new BufferedInputStream(inputStream);
                os = response.getOutputStream();
                int i = bis.read(buffer);
                while (i != -1) {
                    os.write(buffer, 0, i);
                    i = bis.read(buffer);
                }
                os.flush();
                return null;
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                if (bis != null) {
                    try {
                        // 关闭输入流
                        bis.close();
                    } catch (IOException e) {
                        log.error(e.getMessage(), e);
                    }
                }
            }
    
            return ApiRespResult.sysError("下载失败!");
        }
    
    

    为便于演示, RequestMapping中增加get方法支持。 获取HttpServletResponse对象, 也可通过参数方式获取。 参数@NotBlank做了自动化校验。

    需要增加约束性异常检查, 先检查类头部是否有@Validated注解:

    @Validated
    public class StockUserController extends BaseController 
    

    再检查拦截器中是否存在对ConstraintViolationException异常处理:

        /**
         * 拦截约束性校验异常
         * @param cve
         * @return
         */
        @ExceptionHandler(ConstraintViolationException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ResponseBody
        public ApiRespResult handleConstraintViolationException(ConstraintViolationException cve){
            String msg = "";
            Set<ConstraintViolation<?>> cves = cve.getConstraintViolations();
            for (ConstraintViolation<?> constraintViolation : cves) {
                msg += constraintViolation.getMessage();
            }
            // 组装错误提示返回
            ApiRespResult errorWebResult = ApiRespResult.validError(msg);
            return errorWebResult;
        }
    
    • 通过service方法, 获取用户文件信息, 如果未找到, 抛出异常。

    • 根据文件ID, 从ceph服务器获取文件流,直接输出至客户端,不用再通过临时文件方式输出, 提升处理效率, 上百兆的大文件就不要采用此方式。

    • 设置响应类型与文件名称, 便于浏览器及客户端正确识别处理。

    • 通过流式缓冲下载处理, 如果网络带宽, 服务器内存配置比较高, 可以将缓冲扩大。

    • 最后, 不要忘记关闭流式操作对象, 所有流式操作, 都要养成良好操作习惯, 使用完毕及时释放资源。

      不关闭流不会产生内存泄露, 但是会产生系统资源泄露(file descriptor );虽然finalize方法有调用close来释放fd, 但是执行时间点是存在不确定性, 在高并发场景下, 极易出现问题。

3.8 文件下载功能验证
  1. 重新启动用户服务, 访问新增的用户下载接口, 组装参数:

http://127.0.0.1:10681/user/downloadFile?fileId=1155121848879550464&access_token=01a2a051-19ae-43d7-ab51-bab02c864974

填写上传返回的文件ID, 受认证权限控制, 需要附带access_token参数。

  1. 在浏览器中访问:

在这里插入图片描述

可以看到, 文件成功下载。

4. 总结

  • 回顾整体实现流程, 先做好Ceph的封装与自动化装配, 便于集成使用; 实现文件上传功能需要记录用户信息, 通过Token增强技术来获取, 高效便捷; 采用snowflake全局分布式ID, 支持分布式微服务使用,且性能良好;最后实现文件下载功能, 对整个流程的功能进行验证。

第3章 Spring Boot 多环境配置与打包发布

1. 目标

  • 掌握Spring Boot 多环境配置与打包发布

2. 步骤

  • Spring Boot 多环境配置介绍
  • Spring Boot 多环境配置规则
  • 本地的开发环境配置使用
  • 多环境打包配置实现

3. 实现

3.1 Spring Boot 多环境配置介绍

在项目开发过程中, 会存在多个环境, 每个环境的服务器地址不一样,比如redis,数据库等, 在敏捷迭代式交付中, 需要频发更新打包, 完成新需求与版本缺陷。这就需要根据不同环境编写不同配置, 通过MAVEN生成支持各种环境的运行包。

大型互联网微服务项目, 一般会采用统一配置中心来解决此问题, 循序渐进, 先通过Spring Boot本地化多版本配置文件方式来实现不同环境的配置管理, 后面会通过配置中心方式做生产级的集成实践方案。

3.2 Spring Boot 多配置规则

一般Spring Boot 项目, 支持的配置文件多种多样, 比如application.yml、application.properties、优先级更高的bootstrap.yml和bootstrap.properties等。

多环境配置文件名需要满足application-{profile}.properties的格式,其中{profile}对应的是环境标识, 可以自定义填写。 一般定义规则:

application-dev.properties:开发环境
application-uat.properties:测试集成环境
application-prd.properties:生产环境

定义这么多配置文件,那么Spring boot 怎么知道是读取哪个配置文件?

这就还需再定义一个配置文件, 名称为application.properties(用其他application.yml也可以),通过spring.profiles.active属性来设置对应的{profile}值。

也可以通过参数-Dspring.profiles.active来指定{profile}值, 从易读与维护性考虑,建议采用默认配置文件方式。

3.3 本地的开发环境配置使用

在这里插入图片描述

在本地环境中, 建立了两套配置, 一套是本地的dev环境, 另一套是用于联调集成测试的uat环境。

bootstrap.yml配置:

spring:
  profiles:
    active: ${package.environment}

这里没有明确指定一个环境,设置的是一个MAVEN变量。

最新版本的Idea是可以识别MAVEN变量, 老版本是不能识别, 解决方法:

打开Run->Edit Configurations, 在Active Profile输入框中指定配置环境。

在这里插入图片描述

这样, 每次运行就不用再去指定配置文件, 本地环境专门维护bootstrap-dev.yml即可。

接下来如何打包处理? MAVEN是怎么识别? 是否会存在冲突?

3.4 多环境打包配置
  1. 修改pom.xml文件, 增加以下内容:

    <profiles>
            <profile>
                <id>dev</id>
                <activation>
                    <activeByDefault>true</activeByDefault>
                </activation>
                <properties>
                    <package.environment>dev</package.environment>
                </properties>
            </profile>
            <profile>
                <id>uat</id>
                <properties>
                    <package.environment>uat</package.environment>
                </properties>
            </profile>
            <profile>
                <id>prd</id>
                <properties>
                    <package.environment>prd</package.environment>
                </properties>
            </profile>
        </profiles>
    
    <build>
            <finalName>stock-user-${package.environment}</finalName>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>default-resources</id>
                            <phase>validate</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>target/classes</outputDirectory>
                                <useDefaultDelimiters>false</useDefaultDelimiters>
                                <delimiters>
                                    <delimiter>${*}</delimiter>
                                </delimiters>
                                <resources>
                                    <resource>
                                        <directory>src/main/resources/</directory>
                                        <filtering>true</filtering>
                                    </resource>
                                    <resource>
                                        <directory>src/main/java</directory>
                                        <includes>
                                            <include>**/*.xml</include>
                                        </includes>
                                        <filtering>false</filtering>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
    
        </build>
    
    • profiles标签为配置文件标识。这里设置了三个环境, dev, uat和prd。通过activeByDefault标签, 默认激活的是dev环境, 下面的 properties定义了变量package.environment,如果是最新版的IDEA, 可直接运行程序, 会自动识别dev配置文件。

    • buid标签是打包配置, 要注意resources下面增加src/main/java目录扫描:

      <resource>
          <directory>src/main/java</directory>
          <includes>
              <include>**/*.xml</include>
          </includes>
          <filtering>false</filtering>
      </resource>
      

      此项配置是打包环境变量下, mapper目录下的xml文件, 防止启动报错。

  2. 配置打包命令:

    打开maven视图,找到stock-user用户服务工程, 创建打包命令:
    在这里插入图片描述

    填写clean install -P uat 这里的-P参数是指定profile环境
    在这里插入图片描述

    接下来执行新创建的命令:
    在这里插入图片描述

    打包成功, 并生成jar包:
    在这里插入图片描述

    如果打包失败,根据maven错误提示, 检查所有依赖的组件是否已执行clean install, 在打包之前, 最好将所有模块都编译打包一遍, 在最顶级工程下面, 执行clean install命令进行打包,避免编译过程中, 出现的组件依赖问题。

4. 总结

如果要打包生产环境的运行包, 按照上面配置, 再建立一个名为prd的Profile 和对应的bootstrap-prd.yml配置文件, 通过maven执行命令即可。通过这种方式,开发完成后, 不再需要反复修改配置文件, 每个环境维护独立一套配置文件, 不会产生冲突错误, 便于开发管理。

实际项目中, 往往是多人协作,负责不同模块开发, 在打包发布的时候, 会增加deploy命令,

例: clean install deploy -P uat

该命令作用是, 不仅安装到本地MAVEN库, 还会发布到远程MAVEN仓库中,保证每个人生成的都是最新的包, 集成的是最新版本功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

管程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值