<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>404 Not Found</title><description>Lost &amp; Found</description><link>https://blog.wavelee.top/</link><language>zh_CN</language><item><title>国际时区转换</title><link>https://blog.wavelee.top/posts/global-timezone-converter/</link><guid isPermaLink="true">https://blog.wavelee.top/posts/global-timezone-converter/</guid><description>海外部署服务前后端交互时区转换</description><pubDate>Mon, 25 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1，概述&lt;/h2&gt;
&lt;h3&gt;1.1，需求场景&lt;/h3&gt;
&lt;p&gt;海外部署服务，用户界面展示当地时间，后端及数据库采用北京时间GMT+8时区，前端采用浏览器定位当地时区，前后端交互时需对时间进行时区转换。&lt;/p&gt;
&lt;h3&gt;1.2，实现原理&lt;/h3&gt;
&lt;p&gt;前端增加请求头 ISrmTimeZone，传入当前浏览器获取时区；
后端自定义消息转换器 HttpMessageConverter，修改 Jackson 序列化配置，根据请求头 ISrmTimeZone 设置时区。&lt;/p&gt;
&lt;h2&gt;2，代码实现&lt;/h2&gt;
&lt;h3&gt;2.1，自定义 HttpMessageConverter&lt;/h3&gt;
&lt;p&gt;继承 MappingJackson2HttpMessageConverter，原逻辑基础上修改 objectMapper 部分，从请求头/响应头上获取 ISrmTimeZone 设置时区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.midea.srm.core.timezone;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonInputMessage;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.TypeUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 国际时区消息转换器
 * 请求头/响应头包含 ISrmTimeZone 时生效
 * 根据 ISrmTimeZone 动态设置 objectMapper 时区
 * {@link org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter}
 *
 * @author liwf106
 * @since 2023/12/7
 */
@Slf4j
@SuppressWarnings(&quot;all&quot;)
public class TimeZoneMessageConverter extends MappingJackson2HttpMessageConverter {
    public static final String TIME_ZONE_HEADER = &quot;ISrmTimeZone&quot;;

    public TimeZoneMessageConverter() {
        super();
    }

    @NonNull
    @Override
    protected Object readInternal(@NonNull Class&amp;lt;?&amp;gt; clazz, HttpInputMessage inputMessage) throws IOException,
        HttpMessageNotReadableException {
        List&amp;lt;String&amp;gt; timeZone = inputMessage.getHeaders().get(TIME_ZONE_HEADER);
        ObjectMapper objectMapper = getObjectMapper(timeZone);
        JavaType javaType = getJavaType(clazz, null);
        return readJavaType(objectMapper, javaType, inputMessage);
    }

    @NonNull
    @Override
    public Object read(@NonNull Type type, Class&amp;lt;?&amp;gt; contextClass, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {
        List&amp;lt;String&amp;gt; timeZone = inputMessage.getHeaders().get(TIME_ZONE_HEADER);
        ObjectMapper objectMapper = getObjectMapper(timeZone);
        JavaType javaType = getJavaType(type, contextClass);
        return readJavaType(objectMapper, javaType, inputMessage);
    }

    @Override
    public boolean canRead(Class&amp;lt;?&amp;gt; clazz, MediaType mediaType) {
        HttpServletResponse response =
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        String timezone = response.getHeader(TimeZoneMessageConverter.TIME_ZONE_HEADER);
        return super.canRead(clazz, mediaType) &amp;amp;&amp;amp; StringUtils.isNotBlank(timezone);
    }

    @Override
    public boolean canWrite(Class&amp;lt;?&amp;gt; clazz, MediaType mediaType) {
        HttpServletResponse response =
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        String timezone = response.getHeader(TimeZoneMessageConverter.TIME_ZONE_HEADER);
        return super.canWrite(clazz, mediaType) &amp;amp;&amp;amp; StringUtils.isNotBlank(timezone);
    }

    @Override
    protected void writeInternal(@NonNull Object object, @Nullable Type type, @NonNull HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotWritableException {
        List&amp;lt;String&amp;gt; timeZone = outputMessage.getHeaders().get(TIME_ZONE_HEADER);
        ObjectMapper objectMapper = getObjectMapper(timeZone);

        MediaType contentType = outputMessage.getHeaders().getContentType();
        JsonEncoding encoding = getJsonEncoding(contentType);
        JsonGenerator generator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
        try {

            Class&amp;lt;?&amp;gt; serializationView = null;
            FilterProvider filters = null;
            Object value = object;
            JavaType javaType = null;
            if (object instanceof MappingJacksonValue) {
                MappingJacksonValue container = (MappingJacksonValue) object;
                value = container.getValue();
                serializationView = container.getSerializationView();
                filters = container.getFilters();
            }
            if (type != null &amp;amp;&amp;amp; TypeUtils.isAssignable(type, value.getClass())) {
                javaType = getJavaType(type, null);
            }
            ObjectWriter objectWriter;
            if (serializationView != null) {
                objectWriter = objectMapper.writerWithView(serializationView);
            } else if (filters != null) {
                objectWriter = objectMapper.writer(filters);
            } else {
                objectWriter = objectMapper.writer();
            }
            if (javaType != null &amp;amp;&amp;amp; javaType.isContainerType()) {
                objectWriter = objectWriter.forType(javaType);
            }
            SerializationConfig config = objectWriter.getConfig();
            if (contentType != null &amp;amp;&amp;amp; contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &amp;amp;&amp;amp;
                config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
                DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
                prettyPrinter.indentObjectsWith(new DefaultIndenter(&quot;  &quot;, &quot;\ndata:&quot;));
                objectWriter = objectWriter.with(prettyPrinter);
            }
            objectWriter.writeValue(generator, value);

            generator.flush();

        } catch (InvalidDefinitionException ex) {
            throw new HttpMessageConversionException(&quot;Type definition error: &quot; + ex.getType(), ex);
        } catch (JsonProcessingException ex) {
            throw new HttpMessageNotWritableException(&quot;Could not write JSON: &quot; + ex.getOriginalMessage(), ex);
        }

    }

    // 修改 objectMapper 时区
    private ObjectMapper getObjectMapper(List&amp;lt;String&amp;gt; timeZone) {
        ObjectMapper timeZoneObjectMapper = objectMapper.copy();
        SimpleDateFormat dateFormat = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        timeZoneObjectMapper.setDateFormat(dateFormat);

        if (Objects.nonNull(timeZone) &amp;amp;&amp;amp; !timeZone.isEmpty()) {
            timeZoneObjectMapper.setTimeZone(TimeZone.getTimeZone(timeZone.get(0)));
            log.info(&quot;set srm timezone {}&quot;, TimeZone.getTimeZone(timeZone.get(0)));
        } else {
            timeZoneObjectMapper.setTimeZone(TimeZone.getTimeZone(&quot;Asia/Shanghai&quot;));
        }
        return timeZoneObjectMapper;
    }

    private Object readJavaType(ObjectMapper objectMapper, JavaType javaType, HttpInputMessage inputMessage)
        throws IOException {
        try {
            if (inputMessage instanceof MappingJacksonInputMessage) {
                Class&amp;lt;?&amp;gt; deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
                if (deserializationView != null) {
                    return objectMapper.readerWithView(deserializationView).forType(javaType).
                        readValue(inputMessage.getBody());
                }
            }
            return objectMapper.readValue(inputMessage.getBody(), javaType);
        } catch (InvalidDefinitionException ex) {
            throw new HttpMessageConversionException(&quot;Type definition error: &quot; + ex.getType(), ex);
        } catch (JsonProcessingException ex) {
            throw new HttpMessageNotReadableException(&quot;JSON parse error: &quot; + ex.getOriginalMessage(), ex);
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2，自定义 ResponseBodyAdvice&lt;/h3&gt;
&lt;p&gt;为兼容 mcomponent 的 JsonResponseBodyAdvice，仅修改 supports 方法以适配自定义 MessageConverter。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.midea.srm.core.timezone;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import com.midea.mcomponent.core.config.Settings;
import com.midea.mcomponent.core.response.Response;
import com.midea.mcomponent.core.response.SuccessResponse;
import com.midea.mcomponent.core.response.SuccessResponseData;
import com.midea.mcomponent.core.util.IPUtils;
import com.midea.mcomponent.core.util.TraceContext;
import org.apache.tomcat.util.buf.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * {@link com.midea.mcomponent.web.common.advice.JsonResponseBodyAdvice}
 *
 * @author liwf106
 * @since 2023/12/7
 */
@Order(-1)
@ControllerAdvice
@SuppressWarnings(&quot;all&quot;)
public class TJsonResponseBodyAdvice implements ResponseBodyAdvice&amp;lt;Object&amp;gt; {
    protected final Logger logger = LoggerFactory.getLogger(TJsonResponseBodyAdvice.class);
    private static final List&amp;lt;String&amp;gt; DEFAULT_UN_WARP_PATHS =
        Arrays.asList(&quot;/**/configuration/ui&quot;, &quot;/**/swagger-resources&quot;, &quot;/**/v2/api-docs&quot;, &quot;/**/configuration/security&quot;);
    @Autowired
    private Settings settings;

    public TJsonResponseBodyAdvice() {
    }

    public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class clazz,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse()
            .setHeader(&quot;Cache-Control&quot;, &quot;no-cache,no-store&quot;);
        String uri = request.getURI().getPath();
        boolean isRecord = new Boolean(TraceContext.getLocaleWeb());
        if (this.isUnWarpPath(uri)) {
            return object;
        } else {
            if (object instanceof SuccessResponseData) {
                ((SuccessResponseData) object).setProviderSpans(
                    isRecord ? StringUtils.join(TraceContext.getSpans()) : null);
                ((SuccessResponseData) object).setWebIpAddress(IPUtils.getLocalIpAddress());
                ((SuccessResponseData) object).setTraceId(TraceContext.getTraceId());
            }
            if (object instanceof Response) {
                return object;
            } else if (object == null) {
                return SuccessResponse.newInstance();
            } else {
                SuccessResponseData data = SuccessResponseData.newInstance(object);
                data.setProviderSpans(isRecord ? StringUtils.join(TraceContext.getSpans()) : null);
                data.setWebIpAddress(IPUtils.getLocalIpAddress());
                data.setTraceId(TraceContext.getTraceId());
                return data;
            }
        }
    }

    private boolean isUnWarpPath(String path) {
        AntPathMatcher matcher = new AntPathMatcher();
        Iterator var4 = DEFAULT_UN_WARP_PATHS.iterator();
        String pattern;
        while (var4.hasNext()) {
            pattern = (String) var4.next();
            if (org.apache.commons.lang.StringUtils.isNotEmpty(pattern) &amp;amp;&amp;amp; matcher.match(pattern, path)) {
                return true;
            }
        }
        if (this.settings.getUnwarpUrls() != null &amp;amp;&amp;amp; this.settings.getUnwarpUrls().size() &amp;gt; 0) {
            var4 = this.settings.getUnwarpUrls().iterator();
            while (var4.hasNext()) {
                pattern = (String) var4.next();
                if (org.apache.commons.lang.StringUtils.isNotEmpty(pattern) &amp;amp;&amp;amp; matcher.match(pattern, path)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean supports(MethodParameter methodParameter, Class clazz) {
        return clazz.equals(TimeZoneMessageConverter.class);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3，替换 MappingJackson2HttpMessageConverter&lt;/h3&gt;
&lt;p&gt;使用自定义 MessageConverter 替换掉项目原来默认处理 json 类型报文的 MappingJackson2HttpMessageConverter。
若不确定项目默认消息转换器可在此方法下断点判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.midea.srm.core.timezone;

import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 配置国际时区转换器
 *
 * @author liwf106
 * @since 2023/12/7
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converters) {
        int index = converters.size();
        for (int i = 0; i &amp;lt; converters.size(); i++) {
            if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                index = i;
                break;
            }
        }
        converters.add(index, new TimeZoneMessageConverter());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4，响应头设置 ISrmTimeZone &amp;amp; 特殊传参处理&lt;/h3&gt;
&lt;p&gt;使用 AOP 拦截需要进行时区转换的接口，将请求头 ISrmTimeZone 设置到响应头中以供自定义 messageConverter 获取时区。
修改 objectMapper 时区仅适用于 Java 对象传参，若使用形如 Map&amp;lt;String, String&amp;gt;的方式传参则无法获取到 Date 类型而无法转换时区，因此可在 AOP 内酌情判断并手动转换时区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.midea.srm.core.timezone;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 国际时区转换处理
 * 处理 Map 等特殊入参形式
 *
 * @author liwf106
 * @since 2023/12/6
 */
@Slf4j
@Aspect
@Component
@SuppressWarnings(&quot;all&quot;)
public class TimeZoneConvertAspect {
    // 国内时区
    public static final SimpleDateFormat SH_SDF = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
    // 国内时区-日期
    public static final SimpleDateFormat SH_SDF_DATE = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);
    // 国际时区
    public static final SimpleDateFormat GLOBAL_SDF = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
    // 国际时区-日期
    public static final SimpleDateFormat GLOBAL_SDF_DATE = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);

    @Autowired
    private ObjectMapper objectMapper;

    public TimeZoneConvertAspect(ObjectMapper objectMapper) {
        SH_SDF.setTimeZone(TimeZone.getTimeZone(&quot;Asia/Shanghai&quot;));
        SH_SDF_DATE.setTimeZone(TimeZone.getTimeZone(&quot;Asia/Shanghai&quot;));
        this.objectMapper = objectMapper;
    }

    @Pointcut(&quot;@within(org.springframework.web.bind.annotation.RestController) &amp;amp;&amp;amp; within(com.midea.srm..*)&quot;)
    public void point() {
    }

    @Around(&quot;point()&quot;)
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String timeZone = request.getHeader(TimeZoneMessageConverter.TIME_ZONE_HEADER);
            if (StringUtils.isNotBlank(timeZone)) {
                HttpServletResponse response =
                    ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                response.addHeader(TimeZoneMessageConverter.TIME_ZONE_HEADER, timeZone);
                // 根据请求头设置国际时区
                GLOBAL_SDF.setTimeZone(TimeZone.getTimeZone(timeZone));
                GLOBAL_SDF_DATE.setTimeZone(TimeZone.getTimeZone(timeZone));
                // 入参
                Object[] args = joinPoint.getArgs();
                log.info(&quot;原始请求参数为{}&quot;, args);
                for (Object arg : args) {
                    log.info(&quot;参数:{}&quot;, JSONUtil.toJsonStr(arg));
                    // 仅处理 Map 形式入参
                    if (arg instanceof Map) {
                        ((Map) arg).forEach((k, v) -&amp;gt; {
                            if (v instanceof String &amp;amp;&amp;amp; StringUtils.isNotBlank((String) v) &amp;amp;&amp;amp; isDateFieldName((String) k)) {
                                try {
                                    log.info(&quot;传参含date字段:[{}],[{}]&quot;, k, v);
                                    Date date;
                                    String format;
                                    // 仅年月日
                                    if ((((String) v)).length() == 10) {
                                        date = GLOBAL_SDF_DATE.parse((String) v);
                                        format = SH_SDF_DATE.format(date);
                                    } else {
                                        date = GLOBAL_SDF.parse((String) v);
                                        format = SH_SDF.format(date);
                                    }
                                    ((Map) arg).put(k, format);
                                    log.info(&quot;传参含date字段转换结果:[{}],[{}]&quot;, k, format);
                                } catch (ParseException e) {
                                    log.error(&quot;解析失败&quot;, e);
                                }
                            }
                        });
                    }
                }
            }
        } catch (Exception e) {
            log.error(&quot;时区转换失败&quot;, e);
        }
        return joinPoint.proceed();
    }

    /**
     * 根据字段名猜测是否为日期
     *
     * @param fieldName 字段名
     * @return isDateFieldName
     */
    private boolean isDateFieldName(String fieldName) {
        return fieldName.contains(&quot;date&quot;) || fieldName.contains(&quot;Date&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3，逻辑验证&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;前端验证可修改浏览器位置信息模拟海外场景&lt;/li&gt;
&lt;li&gt;后端验证手动添加请求头，若不生效可关注所有已注册的 HttpMessageConverter 响应的 contentType 和注册顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;附：注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;以上方案仅处理前后端交互报文，未处理 excel 导入导出场景&lt;/li&gt;
&lt;li&gt;以上方案会替换处理 json 的默认 HttpMessageConverter，建议根据实际情况评估影响范围（例如本例中影响了 com.midea.mcomponent.web.common.advice.JsonResponseBodyAdvice）&lt;/li&gt;
&lt;li&gt;2.4 中特殊传参处理仅考虑了传参为 Map 的场景&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>硬盘挂载</title><link>https://blog.wavelee.top/posts/hard-disk-mount/</link><guid isPermaLink="true">https://blog.wavelee.top/posts/hard-disk-mount/</guid><description>Linux挂载硬盘操作流程</description><pubDate>Tue, 12 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1.查看硬盘挂载情况&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fdisk -l
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.查看当前分区情况&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;df -l
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.给新硬盘添加新分区（可选）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;_fdisk _/dev/vdb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.分区完成，查询所有设备的文件系统类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;blkid
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.格式化分区&lt;/h3&gt;
&lt;p&gt;先查看当前系统支持格式化成什么类型，输入mkfs，然后按两下tab键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkfs.xfs /dev/vdb1
blkid
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.挂载&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir /mnt/storage
mount /dev/vdb1 /mnt/storage/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.设置自动挂载&lt;/h3&gt;
&lt;p&gt;磁盘被手动挂载之后都必须把挂载信息写入 /etc/fstab 这个文件中，否则下次开机启动时仍然需要重新挂载。系统开机时会主动读取 /etc/fstab 这个文件中的内容，根据文件里面的配置挂载磁盘。这样我们只需要将磁盘的挂载信息写入这个文件中我们就不需要每次开机启动之后手动进行挂载了。&lt;/p&gt;
&lt;p&gt;首先通过 blkid 命令将分区的 uuid 查询出来并复制 uuid（往/etc/fstab中追加挂载信息时建议使用uuid）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/fstab
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>MinIO 安装运行</title><link>https://blog.wavelee.top/posts/minio-quick-start/</link><guid isPermaLink="true">https://blog.wavelee.top/posts/minio-quick-start/</guid><description>对象存储 MinIO 简单安装配置与运行</description><pubDate>Mon, 11 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;使用Docker 运行单点模式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker run -p 9000:9000 -di \
  --name minio \
  -v /data/minio:/data \
  -e &quot;MINIO_ACCESS_KEY=HI3UNHSHXG2A0IR35YZO&quot; \
  -e &quot;MINIO_SECRET_KEY=bzXKoRrrqbpQ+1ezmJkBKxusVc5JZc02cBNm5vEd&quot; \
  minio/minio server /data
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-p&lt;/td&gt;
&lt;td&gt;端口映射，将外部端口映射到容器内部端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--name&lt;/td&gt;
&lt;td&gt;自定义容器名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-di&lt;/td&gt;
&lt;td&gt;后台运行的方式运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--restart=always&lt;/td&gt;
&lt;td&gt;一旦docker重启或者开启时，也自动启动镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e&lt;/td&gt;
&lt;td&gt;设置系统变量，在这里是设置Minio的ACCESS_KEY和SECRET_KEY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-v&lt;/td&gt;
&lt;td&gt;挂载文件，将系统文件映射到容器内部对应的文件夹&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;以普通用户身份运行MinIO Docker&lt;/h4&gt;
&lt;p&gt;Docker提供了标准化的机制，可以以非root用户身份运行docker容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ${HOME}/data
docker run -p 9000:9000 \
  --user $(id -u):$(id -g) \
  --name minio1 \
  -e &quot;MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE&quot; \
  -e &quot;MINIO_SECRET_KEY=wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY&quot; \
  -v ${HOME}/data:/data \
  minio/minio server /data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用二进制文件运行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
./minio server /data
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;MinIO客户端&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;下载二进制文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wget -P /bin https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x /bin/mc
mc --help
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;添加一个云存储服务，也可直接修改~/.mc/config.json&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# API签名为可选项 默认S3v4
mc alias set &amp;lt;ALIAS&amp;gt; &amp;lt;YOUR-S3-ENDPOINT&amp;gt; &amp;lt;YOUR-ACCESS-KEY&amp;gt; &amp;lt;YOUR-SECRET-KEY&amp;gt; --api &amp;lt;API-SIGNATURE&amp;gt; --path &amp;lt;BUCKET-LOOKUP-TYPE&amp;gt;
# 示例 MinIO本地存储
mc alias set minio http://localhost:9000 HI3UNHSHXG2A0IR35YZO bzXKoRrrqbpQ+1ezmJkBKxusVc5JZc02cBNm5vEd
# 示例 MinIO云存储
mc alias set minio http://192.168.1.51 BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
# 示例 Amazon S3云存储
mc alias set s3 https://s3.amazonaws.com BKIKJAA5BMMU2RHO6IBB V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
# 示例 Google云存储
mc alias set gcs  https://storage.googleapis.com BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.min.io/docs/minio-admin-complete-guide.html&quot;&gt;MinIO Admin命令指南&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.min.io/docs/minio-docker-quickstart-guide.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>异步场景日志链路追踪</title><link>https://blog.wavelee.top/posts/distributed-log-trace/</link><guid isPermaLink="true">https://blog.wavelee.top/posts/distributed-log-trace/</guid><description>分布式架构下的日志全链路追踪</description><pubDate>Wed, 06 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1，概述&lt;/h2&gt;
&lt;p&gt;线上环境排查问题需要对某次请求的所有日志进行链路追踪。logback 的 MDC 提供的日志追踪方案仅支持单个线程内的追踪，这是因为 MDC 是基于 ThreadLocal 实现，无法跨线程传递 traceId。阿里开源的TransmittableThreadLocal 基于 InheritableThreadLocal 扩展，支持线程池跨线程传递变量。借此重写 MDCAdapter 即可实现异步场景下的日志链路追踪。&lt;/p&gt;
&lt;h2&gt;2，MDC 改造&lt;/h2&gt;
&lt;h3&gt;2.0，依赖引入&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;transmittable-thread-local&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.14.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.1，重写 MDCAdapter&lt;/h3&gt;
&lt;p&gt;MDC 存储线程变量的逻辑位于 MDCAdapter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.slf4j;


import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.spi.MDCAdapter;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 重写logback的LogbackMDCAdapter，用TransmittableThreadLocal替换ThreadLocal，解决异步线程traceId传递
 *
 * @author liwf106
 * @since 2024/1/25
 */
@SuppressWarnings(&quot;all&quot;)
public class TtlMDCAdapter implements MDCAdapter {

    private final ThreadLocal&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; copyOnInheritThreadLocal = new TransmittableThreadLocal&amp;lt;&amp;gt;();

    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;

    static TtlMDCAdapter mtcMDCAdapter;

    static {
        mtcMDCAdapter = new TtlMDCAdapter();
        // 替换MDC的MDCAdapter
        MDC.mdcAdapter = mtcMDCAdapter;
    }

    public static MDCAdapter getInstance() {
        return mtcMDCAdapter;
    }

    final ThreadLocal&amp;lt;Integer&amp;gt; lastOperation = new ThreadLocal&amp;lt;Integer&amp;gt;();

    private Integer getAndSetLastOperation(int op) {
        Integer lastOp = lastOperation.get();
        lastOperation.set(op);
        return lastOp;
    }

    private boolean wasLastOpReadOrNull(Integer lastOp) {
        return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;
    }

    private Map&amp;lt;String, String&amp;gt; duplicateAndInsertNewMap(Map&amp;lt;String, String&amp;gt; oldMap) {
        Map&amp;lt;String, String&amp;gt; newMap = Collections.synchronizedMap(new HashMap&amp;lt;String, String&amp;gt;());
        if (oldMap != null) {
            // we don&apos;t want the parent thread modifying oldMap while we are
            // iterating over it
            synchronized (oldMap) {
                newMap.putAll(oldMap);
            }
        }

        copyOnInheritThreadLocal.set(newMap);
        return newMap;
    }

    /**
     * Put a context value (the &amp;lt;code&amp;gt;val&amp;lt;/code&amp;gt; parameter) as identified with the
     * &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; parameter into the current thread&apos;s context map. Note that
     * contrary to log4j, the &amp;lt;code&amp;gt;val&amp;lt;/code&amp;gt; parameter can be null.
     * &amp;lt;p/&amp;gt;
     * &amp;lt;p/&amp;gt;
     * If the current thread does not have a context map it is created as a side
     * effect of this call.
     *
     * @throws IllegalArgumentException in case of the &quot;key&quot; parameter is null
     */
    @Override
    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException(&quot;key cannot be null&quot;);
        }

        Map&amp;lt;String, String&amp;gt; oldMap = copyOnInheritThreadLocal.get();
        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
            Map&amp;lt;String, String&amp;gt; newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
            oldMap.put(key, val);
        }
    }

    /**
     * Remove the the context identified by the &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; parameter.
     * &amp;lt;p/&amp;gt;
     */
    @Override
    public void remove(String key) {
        if (key == null) {
            return;
        }
        Map&amp;lt;String, String&amp;gt; oldMap = copyOnInheritThreadLocal.get();
        if (oldMap == null)
            return;

        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp)) {
            Map&amp;lt;String, String&amp;gt; newMap = duplicateAndInsertNewMap(oldMap);
            newMap.remove(key);
        } else {
            oldMap.remove(key);
        }
    }

    /**
     * Clear all entries in the MDC.
     */
    @Override
    public void clear() {
        lastOperation.set(WRITE_OPERATION);
        copyOnInheritThreadLocal.remove();
    }

    /**
     * Get the context identified by the &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; parameter.
     * &amp;lt;p/&amp;gt;
     */
    @Override
    public String get(String key) {
        final Map&amp;lt;String, String&amp;gt; map = copyOnInheritThreadLocal.get();
        if ((map != null) &amp;amp;&amp;amp; (key != null)) {
            return map.get(key);
        } else {
            return null;
        }
    }

    /**
     * Get the current thread&apos;s MDC as a map. This method is intended to be used
     * internally.
     */
    public Map&amp;lt;String, String&amp;gt; getPropertyMap() {
        lastOperation.set(MAP_COPY_OPERATION);
        return copyOnInheritThreadLocal.get();
    }

    /**
     * Returns the keys in the MDC as a {@link Set}. The returned value can be
     * null.
     */
    public Set&amp;lt;String&amp;gt; getKeys() {
        Map&amp;lt;String, String&amp;gt; map = getPropertyMap();

        if (map != null) {
            return map.keySet();
        } else {
            return null;
        }
    }

    /**
     * Return a copy of the current thread&apos;s context map. Returned value may be
     * null.
     */
    @Override
    public Map&amp;lt;String, String&amp;gt; getCopyOfContextMap() {
        Map&amp;lt;String, String&amp;gt; hashMap = copyOnInheritThreadLocal.get();
        if (hashMap == null) {
            return null;
        } else {
            return new HashMap&amp;lt;&amp;gt;(hashMap);
        }
    }

    @Override
    public void setContextMap(Map&amp;lt;String, String&amp;gt; contextMap) {
        lastOperation.set(WRITE_OPERATION);

        Map&amp;lt;String, String&amp;gt; newMap = Collections.synchronizedMap(new HashMap&amp;lt;String, String&amp;gt;());
        newMap.putAll(contextMap);

        // the newMap replaces the old one for serialisation&apos;s sake
        copyOnInheritThreadLocal.set(newMap);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2，加载TtlMDCAdapter&lt;/h3&gt;
&lt;p&gt;定义初始化方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.midea.escm.core.config;

import org.slf4j.TtlMDCAdapter;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * 初始化自定义MDCAdapter
 *
 * @author liwf106
 * @since 2024/1/25
 */
@SuppressWarnings(&quot;all&quot;)
public class TtlMDCAdapterInitializer implements ApplicationContextInitializer&amp;lt;ConfigurableApplicationContext&amp;gt; {

    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        // 加载自定义的MDCAdapter
        TtlMDCAdapter.getInstance();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动类里增加 initializer &lt;em&gt;（ApplicationContextInitializer另有两种使用方法，在此不赘述）&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication(VbpApplication.class);

    springApplication.addInitializers(new TtlMDCAdapterInitializer());

    springApplication.run(args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3，MDC 使用&lt;/h2&gt;
&lt;h3&gt;3.1 添加 traceId&lt;/h3&gt;
&lt;p&gt;在web请求拦截器用调用 MDC.put 添加traceId&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void addInterceptors(InterceptorRegistry registry) {

    registry.addWebRequestInterceptor(new WebRequestInterceptor() {

        @Override
        public void preHandle(WebRequest request) throws Exception {
            String cookie = request.getHeader(&quot;Cookie&quot;);
            TokenUtil.setToken(cookie);
            MDC.put(&quot;traceId&quot;, TraceContext.getTraceId());
        }

        @Override
        public void postHandle(WebRequest request, ModelMap model) throws Exception {
        }

        @Override
        public void afterCompletion(WebRequest request, Exception ex) throws Exception {
            TokenUtil.removeToken();
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 日志打印&lt;/h3&gt;
&lt;p&gt;在 logback-spring.xml 的输出格式中使用 %X{traceId} 获取 traceId 的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property name=&quot;log.colorPattern&quot;
          value=&quot;%date{yyyy-MM-dd HH:mm:ss} | %cyan(%-5level) | %yellow(%thread) | %red([TraceId:%X{traceId}] | %magenta(%file:%line) | %green(%logger{96}) | (%msg%n%n)&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;附：
&lt;a href=&quot;https://github.com/alibaba/transmittable-thread-local&quot;&gt;https://github.com/alibaba/transmittable-thread-local&lt;/a&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>