zhanghan

6年Java互联网研发经验,坐标北京;擅长微服务和中间件。
6年Java互联网研发经验,坐标北京;擅长微服务和中间件。

BigDecimal 转 String

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:
https://zhanghan.blog.csdn.net/article/details/97621028

强烈推荐一个大神的人工智能的教程:http://www.captainbed.net/zhanghan

【前言】

        最近遇到一个需求,是给用户发送带钱的短信,本来自己以为挺简单,很快可以搞定,不过在实现的时候遇到一些小问题,自己辗转查和实验最终解决该问题,在此记录一下。

【BigDecimal如何转成String】

        一、业务需求
               1、数据库中有相应的用户金额,需要发短信通知告诉用户;
               2、短信内容设计采用:固定内容+变量(占位符{0})具体参考《Java 巧用占位符》;
               3、用户金额(BigDecimal)是其中的变量,需要将金额BigDecimal转换成String类型;
               4、产品要求金额在短信中的显示样式:
                  (1)数据库:1888.00—>短信中显示:1888
                  (2)数据库:1888.10—>短信中显示:1888.1
                  (3)数据库:1888.11—>短信中显示:1888.11
        二、相关代码及测试
               1、尝试一:首先想到的直接是BigDecimal.toString方式;
                  (1)代码

    public static void main(String[] args) {

        //短信模版
        String mod = "{0} 先生/女士,您好!您的尾号({1})的银行卡,余额是({2})元";

        //模拟从数据库中取出金额
        BigDecimal bigDecimalValue = new BigDecimal(1888.10);

        //拼接短信变量
        String variable = "张三;567;" + bigDecimalValue.toString();

        //组合短信内容:模版+变量
        String context = MessageFormat.format(mod, variable.split(";"));

        //测试输出 短信内容
        System.out.println(context);

    }

                  (2)测试结果

张三 先生/女士,您好!您的尾号(567)的银行卡,余额是(1888.09999999999990905052982270717620849609375)元

                  (3)结论:金额转为科学计数法,很显然不符合我们的要求;
               2、尝试二:针对尝试一问题,想到保留小数位方式
                  (1)代码

    public static void main(String[] args) {

        //短信模版
        String mod = "{0} 先生/女士,您好!您的尾号({1})的银行卡,余额是({2})元";

        //模拟从数据库中取出金额
        BigDecimal bigDecimalValue = new BigDecimal(1888.10);

        //保留两位小数
        String strValue = bigDecimalValue.setScale(2, BigDecimal.ROUND_HALF_DOWN).toString();

        //拼接短信变量
        String variable = "张三;567;" + strValue;

        //组合短信内容:模版+变量
        String context = MessageFormat.format(mod, variable.split(";"));

        //测试输出 短信内容
        System.out.println(context);

    }

                  (2)测试结果

张三 先生/女士,您好!您的尾号(567)的银行卡,余额是(1888.10)元

                  (3)结论:虽然比尝试一显示好很多,但是仍然不符合产品需求(产品需求是1888.10要显示成1888.1)
               3、尝试三:将BigDecimal转换为double再进行取值
                  (1)代码

    public static void main(String[] args) {

        //短信模版
        String mod = "{0} 先生/女士,您好!您的尾号({1})的银行卡,余额是({2})元";

        //模拟从数据库中取出金额
        BigDecimal bigDecimalValue = new BigDecimal(1888.10);

        //保留两位小数
        String strValue =bigDecimalToString(bigDecimalValue);

        //拼接短信变量
        String variable = "张三;567;" + strValue;

        //组合短信内容:模版+变量
        String context = MessageFormat.format(mod, variable.split(";"));

        //测试输出 短信内容
        System.out.println(context);

    }

    //将BigDecimal取为Double然后转为String方式
    private static String bigDecimalToString(BigDecimal bigDecimalValue){
        NumberFormat cf = NumberFormat.getInstance();
        double taxD = bigDecimalValue.doubleValue();
        String strValue = cf.format(taxD).replace(",", "");
        return strValue;
    }

                  (2)测试结果

张三 先生/女士,您好!您的尾号(567)的银行卡,余额是(1888.1)元

                  (3)结论:
                           A、测了一些数据符合我们的要求;
                           B、如果大家看这种方式的实现方式不难发现将BigDecimal转为Double时精度会有丢失;当数据位数很多时会有问题;例如:188888888888888888888.10 转换后为 188888888888888900000  
                           C、针对位数过多数据错乱,查看一下我们数据库字段类型为 decimal(20,8) 也金钱是保留小数点后两位,也就是存的最大数值为 999999999999.99 而将最大值用转换程序测试一下是支持的,故满足需求;测试结果如下:

张三 先生/女士,您好!您的尾号(567)的银行卡,余额是(999999999999.99)元

        

【总结】

        1、思想上移,行动下移;
        2、方法总比困难多;
        3、逐渐体会到英语重要性(在找计算机专业方案时,百度出来的结果和Google出来的准确度还是有一些)。

Posted by zhanghan in 技术, 0 comments

灰度实战(六):SpringCloud灰度(2)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://zhanghan.blog.csdn.net/article/details/94906100  

【前言】

       在上篇博文中《灰度实战(五):SpringCloud灰度(1)》讲解了SpringCloud项目的灰度实战,其中在zuul中拦截到是灰度请求时采用重定向再次请求网关然后分发至灰度服务,在本篇将zuul针对灰度请求分发策略进行优化,由重定向优化为直接转发提高性能。

【SpringCloud灰度实战】

         一、项目简介
               1、重定向方式的灰度转发策略示意图:

               2、直接转发方式的灰度转发策略示意图:

               3、对比:
                     从示意图中不难发现,zuul网关在进行灰度分发时直接转发比重定向少了一次请求,直接分发到业务系统。
         二、代码调整
               1、增加直接转发拦截器—GrayDirectZuulFilter 

/*
 * Copyright (c) 2019. zhanghan_java@163.com All Rights Reserved.
 * 项目名称:灰度实战
 * 类名称:GrayDirectZuulFilter.java
 * 创建人:张晗
 * 联系方式:zhanghan_java@163.com
 * 开源地址: https://github.com/dangnianchuntian/springboot
 * 博客地址: https://blog.csdn.net/zhanghan18333611647
 */
package com.zhanghan.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.zhanghan.zuul.bean.GrayBean;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.nio.charset.Charset;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.*;
public class GrayDirectZuulFilter extends ZuulFilter {
    @Autowired
    private GrayBean grayBean;
    @Autowired
    private RouteLocator routeLocator;
    @Autowired
    private UrlPathHelper urlPathHelper;
    @Override
    public String filterType() {
        return PRE_TYPE;
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public boolean shouldFilter() {
        return grayBean.isEnabled();
    }
    @Override
    public Object run() {
        Boolean isGray = shouldBeRedirected();
        if (isGray) {
            String grayServiceId = getServiceId() + grayBean.getSuffix();
            String url = RequestContext.getCurrentContext().getRequest().getRequestURI();
            url = replaceUrl(url, grayServiceId);
            try {
                RequestContext.getCurrentContext().put(REQUEST_URI_KEY, url);
                RequestContext.getCurrentContext().put(SERVICE_ID_KEY, grayServiceId);
            } catch (Exception e) {
                return null;
            }
        }
        return null;
    }
    /**
     * 是否为灰度请求  依据条件同时满足1和2为灰度请求: 1.请求在灰度列表中 2.companyNo为配置的灰度companyNo
     *
     * @return true灰度 false不灰度
     */
    private Boolean shouldBeRedirected() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        if (grayBean.getGraylist().isEmpty()) {
            return false;
        }
        String serviceId = getServiceId();
        if (!grayBean.getGraylist().contains(serviceId)) {
            return false;
        }
        Object companyNo = getParment(request, "companyNo");
        if (null != companyNo && companyNo.toString().equals(grayBean.getCompanyNo())) {
            return true;
        }
        return false;
    }
    /**
     * 转换为灰度url
     *
     * @param url           原url
     * @param grayServiceId 灰度服务id
     * @return
     */
    private String replaceUrl(String url, String grayServiceId) {
        grayServiceId = grayServiceId.replace(grayBean.getSuffix(), "");
        return url.replace("/" + grayServiceId, "");
    }
    /**
     * 获取本次请求的serviceId
     *
     * @return
     */
    private String getServiceId() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        Object object = RequestContext.getCurrentContext().get(SERVICE_ID_KEY);
        String serviceId = object == null ? null : object.toString();
        if (StringUtils.isEmpty(serviceId)) {
            String requestURI = urlPathHelper.getPathWithinApplication(request);
            serviceId = routeLocator.getMatchingRoute(requestURI).getLocation();
        }
        return serviceId;
    }
    /**
     * 获取post请求的body
     *
     * @param request
     * @param parmentKey
     * @return
     */
    private static Object getParment(HttpServletRequest request, String parmentKey) {
        try {
            String charset = request.getCharacterEncoding();
            InputStream inputStream = request.getInputStream();
            String body = StreamUtils.copyToString(inputStream, Charset.forName(charset));
            if (StringUtils.isEmpty(body)) {
                return null;
            }
            JSONObject json = JSONObject.fromObject(body);
            return json.get(parmentKey);
        } catch (Exception e) {
            return null;
        }
    }
}

               2、启动类关闭GrayRedirectZuulFilter的注入,启用GrayDirectZuulFilter注入

/*
 * Copyright (c) 2019. zhanghan_java@163.com All Rights Reserved.
 * 项目名称:灰度实战
 * 类名称:ZhZuulApplication.java
 * 创建人:张晗
 * 联系方式:zhanghan_java@163.com
 * 开源地址: https://github.com/dangnianchuntian/springboot
 * 博客地址: https://blog.csdn.net/zhanghan18333611647
 */
package com.zhanghan.zuul;
import com.zhanghan.zuul.filter.GrayDirectZuulFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZhZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZhZuulApplication.class, args);
    }
    /**
     * 重定向方式的Filter---性能低
     */
//    @Bean
//    public GrayRedirectZuulFilter grayRedirectZuulFilter() {
//        return new GrayRedirectZuulFilter();
//    }
    /**
     * 直接跳转方式的Filter---性能高建议采用
     *
     * @return
     */
    @Bean
    public GrayDirectZuulFilter grayDirectZuulFilter() {
        return new GrayDirectZuulFilter();
    }
}

               3、其他代码
                     灰度实战:https://github.com/dangnianchuntian/gray
                     项目模块:gray-cloud
        三、演示灰度
               效果和上篇博文效果一致,在此不再赘述;

【总结】

         1、本节将SpringCloud中利用zuul灰度重定向优化为直接调用至灰度服务。
         2、在接下来会为大家演示SpringCloud链式调用情况下如何做灰度。

Posted by zhanghan in 灰度实战, 0 comments

灰度实战(五):SpringCloud灰度(1)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://zhanghan.blog.csdn.net/article/details/94896149 强烈推荐一个大神的人工智能的教程:http://www.captainbed.net/zhanghan

【前言】

       在上四篇博文中讲解了Apollo如何动态配置,以及Apollo的对灰度发布支持;在本篇博文中为大家带来我们项目(Spring Cloud)的灰度实战。

【SpringCloud灰度实战】

         一、项目简介
               1、项目目录:

gray-cloud
├── gray-eureka -- 注册中心
├── gray-service-sms -- 短信服务代码
└── gray-zuul -- 网关

               2、项目架构图:

               3、网关路由分发图:

         二、代码展示
               1、重要代码展示—灰度拦截器(GrayRedirectZuulFilter)

/*
 * Copyright (c) 2019. zhanghan_java@163.com All Rights Reserved.
 * 项目名称:灰度实战
 * 类名称:GrayRedirectZuulFilter.java
 * 创建人:张晗
 * 联系方式:zhanghan_java@163.com
 * 开源地址: https://github.com/dangnianchuntian/springboot
 * 博客地址: https://blog.csdn.net/zhanghan18333611647
 */
package com.zhanghan.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.zhanghan.zuul.bean.GrayBean;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.StreamUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.nio.charset.Charset;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
public class GrayRedirectZuulFilter extends ZuulFilter {
    @Autowired
    private GrayBean grayBean;
    @Override
    public String filterType() {
        return PRE_TYPE;
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public boolean shouldFilter() {
        return grayBean.isEnabled();
    }
    @Override
    public Object run() {
        if (shouldBeRedirected()) {
            RequestContext.getCurrentContext().setSendZuulResponse(false);
            String redirectUrl = generateRedirectUrl(grayBean.getSuffix());
            sendRedirect(redirectUrl);
        }
        return null;
    }
    /**
     * 是否为灰度请求 依据条件同时满足1和2: 1.请求在灰度列表中 2.companyNo为配置的灰度companyNo
     *
     * @return
     */
    private boolean shouldBeRedirected() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        if (grayBean.isEnabled()) {
            if (grayBean.getGraylist().isEmpty()) {
                return false;
            }
            String requestUrl = request.getRequestURI();
            if (grayBean.getGraylist().contains(requestUrl.split("/")[1])) {
                Object companyNo = getParment(request, "companyNo");
                if (null != companyNo && companyNo.toString().equals(grayBean.getCompanyNo())) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * 获取post请求的body
     *
     * @param request
     * @param parmentKey
     * @return
     */
    private static Object getParment(HttpServletRequest request, String parmentKey) {
        try {
            String charset = request.getCharacterEncoding();
            InputStream inputStream = request.getInputStream();
            String body = StreamUtils.copyToString(inputStream, Charset.forName(charset));
            if (StringUtils.isEmpty(body)) {
                return null;
            }
            JSONObject json = JSONObject.fromObject(body);
            return json.get(parmentKey);
        } catch (Exception e) {
            return null;
        }
    }
    /**
     * 获取重定向URL
     *
     * @param graySuffix 灰度后缀
     * @return
     */
    private static String generateRedirectUrl(String graySuffix) {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        String queryParams = request.getQueryString();
        Object originalRequestPath = request.getRequestURI();
        String[] modifiedRequestPathArr = (originalRequestPath.toString().split("/", 3));
        modifiedRequestPathArr[1] = modifiedRequestPathArr[1] + graySuffix;
        StringBuilder stringBuilder = new StringBuilder();
        for (String str : modifiedRequestPathArr) {
            if (StringUtils.isNotEmpty(str)) {
                stringBuilder.append("/");
                stringBuilder.append(str);
            }
        }
        return stringBuilder.toString() + (queryParams == null ? "" : ("?" + queryParams));
    }
    /**
     * 进行重定向
     *
     * @param redirectUrl 重定向URL
     */
    private static void sendRedirect(String redirectUrl) {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletResponse response = ctx.getResponse();
        HttpServletRequest request = ctx.getRequest();
        try {
            if (request.getMethod().toUpperCase().equals("POST")) {
                response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
            } else {
                response.setStatus(HttpStatus.MOVED_PERMANENTLY.value());
            }
            response.setHeader(HttpHeaders.LOCATION, redirectUrl);
            response.flushBuffer();
        } catch (Exception ex) {
        }
    }
}

               2、启动类(启用GrayRedirectZuulFilter)

/*
 * Copyright (c) 2019. zhanghan_java@163.com All Rights Reserved.
 * 项目名称:灰度实战
 * 类名称:ZhZuulApplication.java
 * 创建人:张晗
 * 联系方式:zhanghan_java@163.com
 * 开源地址: https://github.com/dangnianchuntian/springboot
 * 博客地址: https://blog.csdn.net/zhanghan18333611647
 */
package com.zhanghan.zuul;
import com.zhanghan.zuul.filter.GrayRedirectZuulFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZhZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZhZuulApplication.class, args);
    }
    /**
     * 重定向方式的Filter---性能低
     */
    @Bean
    public GrayRedirectZuulFilter grayRedirectZuulFilter() {
        return new GrayRedirectZuulFilter();
    }
    /**
     * 直接跳转方式的Filter---性能高建议采用
     *
     * @return
     */
//    @Bean
//    public GrayDirectZuulFilter grayDirectZuulFilter() {
//        return new GrayDirectZuulFilter();
//    }
}

               3、其他代码
                     灰度实战:https://github.com/dangnianchuntian/gray
                     项目模块:gray-cloud
         三、项目部署
               1、Apollo配置中心配置
                   (1)eureka注册中心配置

# Spring cloud config
spring.application.name=zh_eureka
server.port=8090
# Eureka config for discovery
eureka.instance.metadataMap.group=eureka-group
eureka.client.serviceUrl.defaultZone=http://172.16.11.230:8090/eureka/
eureka.instance.preferIpAddress=true

                   (2)短信业务配置

spring.application.name = service_sms
server.port = 1100
eureka.client.serviceUrl.defaultZone = http://172.16.11.230:8090/eureka/
eureka.instance.preferIpAddress = true

                   (3)zuul网关配置

spring.application.name = zh_zuul
server.port = 8099
zuul.debug.request = true
zuul.routes.servicesms.path = /service_sms/**
zuul.routes.servicesms.serviceId = service_sms
zuul.routes.servicesms_gray.path = /service_sms_gray/**
zuul.routes.servicesms_gray.serviceId = service_sms_gray
eureka.client.serviceUrl.defaultZone = http://172.16.11.230:8090/eureka/
management.endpoints.web.exposure.include = *
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 60000
feign.hystrix.enabled = false
ribbon.ReadTimeout = 60000
ribbon.ConnectTimeout = 5000
# 灰度相关配置  close gray:false  open gray:true
gray.bean.enabled = false
gray.bean.suffix = _gray
gray.bean.companyNo = dmsd
gray.bean.graylist[0] = service_sms

                2、部署相关服务
                   (1)部署eureka注册中心服务(服务器172.16.11.230)
                       a.启动命令

nohup java -jar -Xmx128m  -Dapollo_meta=http://172.16.11.226:8088 -Dapp_ip="gray_cloud" -Dapollo_namespaces=eureka gray-eureka.jar &

                       b.启动正常验证

                   (2)部署短信业务服务
                       a.部署 172.16.11.223 服务器
                            ①启动命令:

# 通过sid区分不同服务器,以验证请求是分发到哪台服务器
nohup java -jar -Xmx128m -Dsid=sms-223  -Dapollo_meta=http://172.16.11.226:8088 -Dapp_ip="gray_cloud" -Dapollo_namespaces=sms gray-service-sms.jar &

                            ②启动正常验证:

                       b.部署172.16.11.230  服务器
                            ①启动命令:

# 通过sid区分不同服务器,以验证请求是分发到哪台服务器
nohup java -jar -Xmx128m -Dsid=sms-230  -Dapollo_meta=http://172.16.11.226:8088 -Dapp_ip="gray_cloud" -Dapollo_namespaces=sms gray-service-sms.jar &

                            ②启动正常验证:

                   (3)部署zuul网关服务(服务器172.16.11.223)
                     a.启动命令

nohup java -jar -Xmx128m -Dapollo_meta=http://172.16.11.226:8088 -Dapp_ip="gray_cloud" -Dapollo_namespaces=zuul gray-zuul.jar &

                     b.启动正常验证

        三、演示灰度
                1、正常情况:
                   (1)访问示意图
                   (2)验证(默认zuul分发策略为轮循)
                     a.单数次访问(请求均落在223机器上) 

                     b.双数次访问(请求均落在230机器上) 

                2、项目升级(灰度发布;将230置为灰度服务)
                   (1)访问最终示意图
                   (2)灰度步骤
                     a.修改230服务名称(将230服务的流量摘除)
                            ①本步操作示意图

                            ②操作步骤:
                                  (①)在Apollo上进行灰度发布(详细步骤参考上篇博文《灰度实战(四):Apollo配置中心(4)》)

                                  (②)查看eureka发现230服务名已经更改

                                  (③)这时多次访问zuul发现请求一直落在223上:

                     b. 更新230代码(将短信代码由V1.0升级到V2.0)
                          ①本步操作示意图

                          ②操作步骤:一般由公司运维执行(从git上拉最新的代码,打包,发布)
                     c.将230服务挂载到网关上(此时正常流量全部分发到223服务器上,灰度流量【本例灰度流量标记companyNo=dmsd  可在zuul配置文件中灵活配置】分发到230上)
                        ①访问示意图

                        ②操作步骤
                                  (①)打开zuul灰度开关

                                  (②)正常流量验证:正常流量一直分发到223服务器

                                  (③)灰度流量验证:灰度流量一直分发到230服务器【本例灰度流量标记companyNo=dmsd  可在zuul配置文件中灵活配置】

                   d.灰度测试完毕后(一般为测试发起请求),说明新的代码在生产环境没有问题;
                   e.让230服务接收正常流量
                        ①本步操作示意图

                        ②操作步骤
                                  (①)在Apollo的sms中撤销230的灰度发布

                                  (②)在Apollo的zuul中将灰度开关关闭

                                  (③)此时再访问发现请求又换成轮循
                                          ·一次访问落在223机器上

                                          ·一次访问落在230机器上 

                   g.摘掉223的流量
                        ①本步操作示意图

                        ②操作步骤(针对223进行灰度)

               h.升级223代码(示意图)

               i.让223重新接收请求
                        ①本步操作示意图

                        ②操作步骤

 

【总结】

         1、本节为大家演示SpringCloud的灰度。
         2、在接下来会为大家演示本版本的升级版—zuul接收到灰度请求时,将重定向优化为直接调用至灰度服务。

Posted by zhanghan in 灰度实战, 0 comments

灰度实战(四):Apollo配置中心(4)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://zhanghan.blog.csdn.net/article/details/94734557 强烈推荐一个大神的人工智能的教程:http://www.captainbed.net/zhanghan

【前言】

       在上一篇博文《灰度实战(三):Apollo配置中心(3)》中讲解了Apollo如何动态更改程序中连接第三方中间件连接池,在本篇博文中为大家带来如何通过Apollo实现灰度配置。

【灰度配置演示】

         一、环境准备
               1、演示代码:
                    (1)以《灰度实战(二):Apollo配置中心(2)》中@value注入为例;
                    (2)代码地址:https://github.com/dangnianchuntian/gray
               2、环境说明:
                    (1)灰度发布针对是集群(至少两台服务器),在此以机器:172.16.11.223 (以下简称223)和 172.16.11.230(以下简称230)为例子;
                    (2)代码启动脚本(利用命令指定优点:应对多环境部署):

// apollo_meta 为服务器的Apollo地址
java -jar -Dapollo_meta=http://172.16.11.226:8088 -Dapp_ip="gray_test" -Dapollo_namespaces=grayapollo  gray-apollo.jar

         二、灰度展示
               1、程序启动从Apollo上读取配置文件
                     (1)示意图

                     (2)在swagger上调用接口验证
                              a.调用223服务器

                              b.调用230服务器

               2、部分灰度(对230进行灰度)
                     (1)示意图

                     (2)进行Apollo进行相关配置
                              a.点击灰度

                              b.创建灰度

                              c.修改需要进行灰度的灰度项配置(将zh.int值由8改为230)

                              d.新增灰度规则

                              e.选择230ip进行灰度

                              f.进行灰度发布

                              g.查看230服务器的日志(发现更新已经推送过来)

                              h.查看223服务器的日志(未推送)

                     (3)调用swagger接口进行验证
                              a.调用230(发现已经为灰度值)

                              b.调用233

               3、全量灰度
                     (1)示意图

                     (2)Apollo操作

                     (3)验证同部分灰度,不再赘述
               4、放弃灰度
                     (1)示意图

                     (2)Apollo操作

                     (3)验证同部分灰度,不再赘述;         
         三、项目地址
               灰度实战:https://github.com/dangnianchuntian/gray

【总结】

         1、本节为大家演示如何用Apollo进行灰度调整程序的配置文件值。
         2、在接下来的灰度程序中会对这个功能的应用体会更加深刻。

Posted by zhanghan in 灰度实战, 0 comments

灰度实战(三):Apollo配置中心(3)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://zhanghan.blog.csdn.net/article/details/94601303 强烈推荐一个大神的人工智能的教程:http://www.captainbed.net/zhanghan

【前言】

       在上一篇博文《灰度实战(二):Apollo配置中心(2)》中讲解了Apollo如何动态更改程序中通过@value配置值,在本篇博文中为大家带来如何通过Apollo动态更新程序和中间件的连接。

【实时推送演示】

         一、程序和第三方组件连接动态更改(在此以连接redis为例)—失败版
               1、演示代码(增加redis操作)

package com.zhanghan.grayapollo.controller;
import com.zhanghan.grayapollo.util.wrapper.WrapMapper;
import com.zhanghan.grayapollo.util.wrapper.Wrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@Api(value = "演示Apollo控制器",tags = {"演示Apollo控制器"})
public class DynamicController {
    @Value("${zh.int}")
    private Integer zhInt;
    @Autowired
    private RedisTemplate strRedisTemplate;
    @ApiOperation(value="测试通过@value注入数据",tags = {"演示Apollo控制器"})
    @RequestMapping(value = "/test/value", method = RequestMethod.POST)
    public Wrapper testValue() {
        Map map = new HashMap();
        map.put("zhTest", zhInt);
        return WrapMapper.ok(map);
    }
    @ApiOperation(value="演示向redis中放数据",tags = {"演示Apollo控制器"})
    @RequestMapping(value = "/post/rdb", method = RequestMethod.POST)
    public Wrapper postDB() {
        strRedisTemplate.opsForValue().set("zh-test",zhInt.toString());
        Map map = new HashMap();
        map.put("putZhTest", zhInt.toString());
        return WrapMapper.ok(map);
    }
    @ApiOperation(value="演示从redis中获取数据",tags = {"演示Apollo控制器"})
    @RequestMapping(value = "/get/rdb", method = RequestMethod.POST)
    public Wrapper getDB() {
        String getZhInt = strRedisTemplate.opsForValue().get("zh-test");
        Map map = new HashMap();
        map.put("getZhTest", getZhInt);
        return WrapMapper.ok(map);
    }
}

               2、访问swagger给redis设置值

               3、访问swagger读取redis设置的值

               4、用redis可视化工具查看,此时存储在redis的1库中(此时2库中没有值)

               5、在Apollo上修改redis的库(由1改为2),并发布

               6、查看系统的日志,发现将变动已经推送到程序

               7、此时再次访问swagger的从redis中读取值接口

               8、小结:
                   竟然还可以读取到值,说明redis数据库并未切换;说明刚刚的配置变更只是推送到程序,而程序在第一次启动时已经和redis建立连接(其中含redis的database),推送并没有改变连接;
         二、程序和第三方组件连接动态更改(在此以连接redis为例)—成功版
               1、如何动态的更改那?
                     (1)增加Apollo的监听类并刷新程序配置

package com.zhanghan.grayapollo.refresh;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class RedisPropertiesRefresher implements ApplicationContextAware {
    private static final Logger logger = LoggerFactory.getLogger(RedisPropertiesRefresher.class);
    private ApplicationContext applicationContext;
    @Autowired
    private RefreshScope refreshScope;
    //此处配置的value值为Apollo的namespace名称
    @ApolloConfigChangeListener(value = "grayapollo")
    public void onChange(ConfigChangeEvent changeEvent) {
        boolean propertiesChanged = false;
        for (String changedKey : changeEvent.changedKeys()) {
            logger.info("===============================================================");
            logger.info("changedKey:{} value:{}", changedKey, changeEvent.getChange(changedKey));
            ConfigChange configChange = changeEvent.getChange(changedKey);
            configChange.getOldValue();
            propertiesChanged = true;
            break;
        }
        refreshProperties(changeEvent);
        if (propertiesChanged) {
            refreshProperties(changeEvent);
        }
    }
    private void refreshProperties(ConfigChangeEvent changeEvent) {
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        refreshScope.refreshAll();
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

                     (2)redis工场增加@RefreshScope

package com.zhanghan.grayapollo.config;
import com.zhanghan.grayapollo.properties.RedisProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPoolConfig;
@Component
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Autowired
    private RedisProperties redisProperties;
    @Bean
    @RefreshScope  //此注解用于动态刷新
    public JedisConnectionFactory jedisConnectionFactory() {
        RedisStandaloneConfiguration rf = new RedisStandaloneConfiguration();
        rf.setDatabase(redisProperties.getDatabase());
        rf.setHostName(redisProperties.getHost());
        rf.setPort(redisProperties.getPort());
        rf.setPassword(RedisPassword.of(redisProperties.getPassword()));
        JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jpb =
                (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(redisProperties.getMaxIdle());
        jedisPoolConfig.setMinIdle(redisProperties.getMinIdle());
        jedisPoolConfig.setMaxTotal(redisProperties.getMaxActive());
        jedisPoolConfig.setMaxWaitMillis(redisProperties.getMaxWait());
        jedisPoolConfig.setEvictorShutdownTimeoutMillis(redisProperties.getTimeout());
        jpb.poolConfig(jedisPoolConfig);
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(rf, jpb.build());
        return jedisConnectionFactory;
    }
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
    @Bean
    RedisTemplate redisTemplate() {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory());
        return redisTemplate;
    }
    @Bean
    RedisTemplate strRedisTemplate() {
        final RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericToStringSerializer<>(String.class));
        template.setValueSerializer(new GenericToStringSerializer<>(String.class));
        return template;
    }
    @Bean
    RedisTemplate longRedisTemplate() {
        final RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericToStringSerializer<>(Long.class));
        template.setValueSerializer(new GenericToStringSerializer<>(Long.class));
        return template;
    }
    @Bean
    RedisTemplate booleanRedisTemplate() {
        final RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
        return template;
    }
    @Bean
    RedisTemplate intRedisTemplate() {
        final RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
        return template;
    }
}

               2、此时清空本地redis,再重新做实验,进行验证;(程序启动时Apollo配置的redis的数据库为1,程序启动后动态改成连库2)
               3、访问swagger给redis设置值

               4、访问swagger读取redis设置的值

               5、用redis可视化工具查看,此时存储在redis的1库中(此时2库中没有值)

               6、在Apollo上修改redis的库(由1改为2),并发布

               7、此时再次访问swagger的从redis中读取值接口(惊喜出现了),说明换库了

               8、究竟是换的是不是1库那?我们此时再在swagger上调用放入redis值接口

               9、此时通过redis客户端查看库2中惊奇发现已经有刚才设置的值;说明已经成功的切库。

               10、至此实验成功;
         三、项目地址
               灰度实战:https://github.com/dangnianchuntian/gray

【总结】

         1、本节第一部分为大家证实,如果不配置Apollo监听以及redis连接工程的动态更新,不能动态的更改程序与中间件连接。
         2、本节第二部分为大家演示如何接收Apollo动态更新连第三方中间件的连接池(以redis连接池为例)。

Posted by zhanghan in 灰度实战, 0 comments