Spring Boot项目如何实现分布式日志链路追踪

简介: 作为一名后端开发工程师,排查系统问题用得最多的手段之一就是查看系统日志,在当下主要的分布式集群环境中一般使用`ELK(Elasticsearch , Logstash, Kibana)`来统一收集日志,以便后续查看日志定位追踪相关问题。但是在并发情况下,大量的系统用户即多线程并发访问后端服务导致同一个请求的日志记录不再是连续相邻的,此时多个请求的日志是一起串行输出到文件中,所以我们筛选出指定请求的全部相关日志还是比较麻烦的,同时当后端异步处理功能逻辑以及微服务的下游服务调用日志追踪也有着相同的问题。

1.概述

作为一名后端开发工程师,排查系统问题用得最多的手段之一就是查看系统日志,在当下主要的分布式集群环境中一般使用ELK(Elasticsearch , Logstash, Kibana)来统一收集日志,以便后续查看日志定位追踪相关问题。但是在并发情况下,大量的系统用户即多线程并发访问后端服务导致同一个请求的日志记录不再是连续相邻的,此时多个请求的日志是一起串行输出到文件中,所以我们筛选出指定请求的全部相关日志还是比较麻烦的,同时当后端异步处理功能逻辑以及微服务的下游服务调用日志追踪也有着相同的问题。

为了快速排查、定位、解决日常反馈的系统问题,我们就必须解决上面所说的查看请求日志的痛点。解决方案就是:每个请求都使用一个唯一标识traceId来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵),然后使用使用Logback的MDC机制日志模板中加入traceId标识,取值方式为%X{traceId}。这样在收集的日志文件中就可以看到每行日志有一个tracceId值,每个请求的值都不一样,这样我们就可以根据traceId查询过滤出一次请求的所有上下文日志了。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

2.实现方案

MDC(Mapped Diagnostic Context,映射调试上下文)log4jlogback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从MDC 中获取所需的信息即可。MDC的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

由于MDC内部使用的是ThreadLocal所以只有本线程才有效,子线程和下游的服务MDC里的值会丢失;所以方案主要的难点是解决traceId值的传递问题,需要重点关注一下两点:

  • MDCtraceId数据如何传递给下游服务,下游服务如何接收traceId并放入MDC

  • 异步的情况下(线程池)如何把traceId值传给子线程。

2.1 设置日志模板

无论是我们的项目使用的是log4j还是logback框架,我们都需要先调整日志配置文件的日志格式如下:

<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="[%X{traceId}] [%-5p] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t@${PID}]  %c %M : %m%n"/>

这样才能有效地把traceId收集到日志文件中。

2.2 请求上下文设置traceId并有效传递下游服务

按照上面说的,每个请求使用一个唯一标识traceId来追踪一次请求的全部日志,这就要求我们的traceId必须保证唯一性,不然就会出现请求日志混乱问题,是绝对不允许的。这里我们利用hutool框架的生成id工具IdUtil来生成唯一值,可以生成uuid或者使用雪花算法Snowflake生成唯一id都可以,因为这里id是记录在日志文件中做唯一标识用的,所以对id字符类型,递增性那些没啥要求,只要唯一标识即可,按照之前习惯,我就用雪花算法生成唯一id标识了。

生成traceId并放入到MDC上下文中

public class WebTraceFilter extends OncePerRequestFilter {
   


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws IOException, ServletException {
   
        try {
   
            String traceId = request.getHeader(MDCTraceUtils.TRACE_ID_HEADER);
            if (StrUtil.isEmpty(traceId)) {
   
                MDCTraceUtils.addTrace();
            } else {
   
                MDCTraceUtils.putTrace(traceId);
            }
            filterChain.doFilter(request, response);
        } finally {
   
            MDCTraceUtils.removeTrace();
        }
    }
}

这里通过一个过滤器来设置traceId放入到MDC中,可以将该过滤器的执行优先级设置比较靠前,这样就可以有效保证我们一次请求上下文的日志中都有traceId了。同时这个过滤器我们是集成在自定义实现的web starter中,公司的所有服务都会引用web starter集成该过滤器,意味着只要我们请求下游服务时添加了traceId这个header,下游服务执行到该过滤器时就会拿到上游服务传递过来的traceId值放入到当前服务的MDC中。MDCTraceUtils工具类代码如下:

public class MDCTraceUtils {
   
    /**
     * 追踪id的名称
     */
    public static final String KEY_TRACE_ID = "traceId";

    /**
     * 日志链路追踪id信息头
     */
    public static final String TRACE_ID_HEADER = "x-traceId-header";


    /**
     * 创建traceId并赋值MDC
     */
    public static void addTrace() {
   
        String traceId = createTraceId();
        MDC.put(KEY_TRACE_ID, traceId);
    }

    /**
     * 赋值MDC
     */
    public static void putTrace(String traceId) {
   
        MDC.put(KEY_TRACE_ID, traceId);
    }

    /**
     * 获取MDC中的traceId值
     */
    public static String getTraceId() {
   
        return MDC.get(KEY_TRACE_ID);
    }

    /**
     * 清除MDC的值
     */
    public static void removeTrace() {
   
        MDC.remove(KEY_TRACE_ID);
    }

    /**
     * 创建traceId
     */
    public static String createTraceId() {
   
        return IdUtil.getSnowflake().nextIdStr();
    }

}

接下来我们就来演示下traceId如何在服务间有效传递。无论是微服务间的服务调用还是单体项目的调用下游服务,我都建议使用Spring Cloud框架中的openfeign组件进行服务间接口调用,如果对组件openfeign不太熟悉的,可以看看之前我总结的 openfeign实现原理进行了解。这里就用openFeign进行模拟服务间调用下游服务获取车间列表的接口

@FeignClient(name = "workshopService", url = "http://127.0.0.1:16688/textile", path = "/workshop")
public interface WorkshopService {
   
    @GetMapping("/list/temp")
    ResponseVO<List<WorkshopDTO>> getList();
}

增加feign拦截器,继续把当前服务的traceId值传递给下游服务

public class FeignInterceptor implements RequestInterceptor {
   
    @Override
    public void apply(RequestTemplate requestTemplate) {
   
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 传递请求相关header
        if (requestAttributes != null) {
   
            HttpServletRequest request = requestAttributes.getRequest();
            Enumeration<String> headerNames = request.getHeaderNames();
            if (headerNames != null) {
   
                while (headerNames.hasMoreElements()) {
   
                    String name = headerNames.nextElement();
                    // 跳过 content-length
                    if (Objects.equals("content-length", name)){
   
                        continue;
                    }
                    String value = request.getHeader(name);
                    requestTemplate.header(name, value);
                }
            }
        }
        // 传递日志追踪的traceId
        String traceId = MDCTraceUtils.getTraceId();
        if (StringUtils.isNotBlank(traceId)) {
   
            requestTemplate.header(MDCTraceUtils.TRACE_ID_HEADER, traceId);
        }
    }
}

可以看到这里主要完成传递请求的header,traceId这个header单独处理,这是因为webTraceFilter过滤器中只把traceId放入了MDC中,并没有吧traceId放入到请求的header中,servlet层的filter过滤器Spring不建议修改请求的参数,包括header,改起来也比较麻烦,所以这里需要单独处理传递。当然这里的拦截器FeignInterceptor和上面的过滤器WebTraceFilter都需要注入到Spring容器中。

编写代码进行接口调用测试:

    @GetMapping("/trace")
    public void testTrace() {
   
        log.info("开始执行咯");
        BaseQuery query = new BaseQuery();
        ResponseVO<List<WorkshopDTO>> responseVO = workshopService.getList();
        log.info("接口返回结果:{}", responseVO);
    }

执行日志打印如下,当前服务的日志:

[1675794072381583360] [INFO ] [2023-07-03 17:10:16.289] [http-nio-18888-exec-5@24417]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:18888/fds/test/trace","httpMethod":"GET","classMethod":"com.plasticene.fast.controller.TestController.testTrace","requestParams":null}
[1675794072381583360] [INFO ] [2023-07-03 17:10:16.299] [http-nio-18888-exec-5@24417]  com.plasticene.fast.controller.TestController testTrace$original$mZGAheRd : 开始执行咯
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.087] [http-nio-18888-exec-5@24417]  com.plasticene.fast.controller.TestController testTrace$original$mZGAheRd : 接口返回结果:ResponseVO(code=200, msg=OK, data=[WorkshopDTO(id=3, orgId=4, name=检验车间, location=杭州市西湖区, remark=这里是最严格的, machineCount=null)])
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.088] [http-nio-18888-exec-5@24417]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result:  null
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.089] [http-nio-18888-exec-5@24417]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost:  805

traceId为:1675794072381583360,看看下游服务textile的日志如下:

[1675794072381583360] [INFO ] [2023-07-03 17:10:16.438] [http-nio-16688-exec-1@24461]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:16688/textile/workshop/list/temp","httpMethod":"GET","classMethod":"com.plasticene.textile.controller.WorkshopController.getAllList","requestParams":null}
[1675794072381583360] [DEBUG] [2023-07-03 17:10:16.939] [http-nio-16688-exec-1@24461]  com.plasticene.textile.dao.WorkshopDAO.selectList debug : ==>  Preparing: SELECT id, org_id, name, location, remark, create_time, update_time, creator, updater FROM workshop WHERE (org_id = ?) ORDER BY id DESC
[1675794072381583360] [DEBUG] [2023-07-03 17:10:16.972] [http-nio-16688-exec-1@24461]  com.plasticene.textile.dao.WorkshopDAO.selectList debug : ==> Parameters: 4(Integer)
[1675794072381583360] [DEBUG] [2023-07-03 17:10:17.008] [http-nio-16688-exec-1@24461]  com.plasticene.textile.dao.WorkshopDAO.selectList debug : <==      Total: 1
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.029] [http-nio-16688-exec-1@24461]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result:  [{"id":3,"orgId":4,"name":"检验车间","location":"杭州市西湖区","remark":"这里是最严格的","machineCount":null}]
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.040] [http-nio-16688-exec-1@24461]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost:  621

可以看到两个服务的traceId都是一样的,这就说明我们的traceId有效传递了。

当然我们也可以使用Spring自带的RestTemplate、或者httpClient、OkHttp3等框架进行接口调用,只要请求接口时设置traceId这个header即可,使用restTemplate客户端调接口时,还可以通过扩展点ClientHttpRequestInterceptor接口的实现类对请求进行拦截处理进行统一traceIdheader设置,这样就不用每个接口请求都要设置一遍,尽量减少重复劳动做到优雅不过时。这里不在展示详细,请自我去实现。

2.3 异步父子线程traceId传递

上面说过MDC内部使用的是ThreadLocal,所以只有本线程才有效,子线程和下游的服务MDC里的值会丢失。我们项目服务使用的logback日志框架,所以我们需要重写logback的LogbackMDCAdapter,由于logback的MDC实现内部使用的是ThreadLocal不能传递子线程,所以需要重写替换为阿里的TransmittableThreadLocalTransmittableThreadLocal 是Alibaba开源的、用于解决在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。官方文档地址:https://github.com/alibaba/transmittable-thread-local

重写logback的LogbackMDCAdapter,自定义实现TtlMDCAdapter类,其实就是把LogbackMDCAdapterThreadLocal换成TransmittableThreadLocal即可,其他代码都是一样的。

/**
 *重构{@link LogbackMDCAdapter}类,搭配TransmittableThreadLocal实现父子线程之间的数据传递
 *
 * @author fjzheng
 * @version 1.0
 * @date 2022/7/14 13:50
 */
public class TtlMDCAdapter implements MDCAdapter {
   
    private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();

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

    private static TtlMDCAdapter mtcMDCAdapter;

    /**
     * keeps track of the last operation performed
     */
    private final ThreadLocal<Integer> lastOperation = new ThreadLocal<>();

    static {
   
        mtcMDCAdapter = new TtlMDCAdapter();
        MDC.mdcAdapter = mtcMDCAdapter;
    }

    public static MDCAdapter getInstance() {
   
        return mtcMDCAdapter;
    }

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

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

    private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
   
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap<>());
        if (oldMap != null) {
   
            // we don'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 <code>val</code> parameter) as identified with the
     * <code>key</code> parameter into the current thread's context map. Note that
     * contrary to log4j, the <code>val</code> parameter can be null.
     * <p/>
     * <p/>
     * If the current thread does not have a context map it is created as a side
     * effect of this call.
     *
     * @throws IllegalArgumentException in case the "key" parameter is null
     */
    @Override
    public void put(String key, String val) {
   
        if (key == null) {
   
            throw new IllegalArgumentException("key cannot be null");
        }

        Map<String, String> oldMap = copyOnInheritThreadLocal.get();
        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
   
            Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
   
            oldMap.put(key, val);
        }
    }

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

        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp)) {
   
            Map<String, String> 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 <code>key</code> parameter.
     * <p/>
     */
    @Override
    public String get(String key) {
   
        final Map<String, String> map = copyOnInheritThreadLocal.get();
        if ((map != null) && (key != null)) {
   
            return map.get(key);
        } else {
   
            return null;
        }
    }

    /**
     * Get the current thread's MDC as a map. This method is intended to be used
     * internally.
     */
    public Map<String, String> 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<String> getKeys() {
   
        Map<String, String> map = getPropertyMap();

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

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

    @Override
    public void setContextMap(Map<String, String> contextMap) {
   
        lastOperation.set(WRITE_OPERATION);

        Map<String, String> newMap = Collections.synchronizedMap(new HashMap<>());
        newMap.putAll(contextMap);

        // the newMap replaces the old one for serialisation's sake
        copyOnInheritThreadLocal.set(newMap);
    }
}

接下来只需要实现程序启动时加载上我们自己实现的TtlMDCAdapter:

/**
 *
 * 初始化TtlMDCAdapter实例,并替换MDC中的adapter对象
 *
 * @author fjzheng
 * @version 1.0
 * @date 2022/7/14 13:55
 */
public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        //加载TtlMDCAdapter实例
        TtlMDCAdapter.getInstance();
    }
}

这样我们在异步多线程情况下MDCtraceId值就能正常传递,下面来看看测试示例:

   // 定义线程池
   private ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
            .setNameFormat("letter-pool-%d").build();
   private ExecutorService fixedThreadPool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors()*2,
            Runtime.getRuntime().availableProcessors() * 40,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(Runtime.getRuntime().availableProcessors() * 20),
            namedThreadFactory);

    // 测试接口
    @GetMapping("/async")
    public void testAsync() {
   
        log.info("打印日志了");
        fixedThreadPool.execute(()->{
   
            log.info("异步执行了");
            try {
   
                Student student = null;
                String name = student.getName();
            } catch (Exception e) {
   
                log.error("异步报错了:", e);
            }

        });
    }

执行结果日志打印如下:

[1675805796950241280] [INFO ] [2023-07-03 17:56:51.683] [http-nio-18888-exec-8@24417]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:18888/fds/test","httpMethod":"GET","classMethod":"com.plasticene.fast.controller.TestController.test","requestParams":null}
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.698] [http-nio-18888-exec-8@24417]  com.plasticene.fast.controller.TestController test$original$mZGAheRd : 打印日志了
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.700] [http-nio-18888-exec-8@24417]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result:  null
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.700] [http-nio-18888-exec-8@24417]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost:  24
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.700] [letter-pool-1@24417]  com.plasticene.fast.controller.TestController lambda$test$0 : 异步执行了
[1675805796950241280] [ERROR] [2023-07-03 17:56:51.704] [letter-pool-1@24417]  com.plasticene.fast.controller.TestController lambda$test$0 : 异步报错了:
java.lang.NullPointerException: null
    at com.plasticene.fast.controller.TestController.lambda$test$0(TestController.java:93)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

3.总结

以上全部就是关于Spring Boot如何实现分布式日志链路追踪的相关知识点。工欲善其事,必先利其器,我们要想快速通过日志定位系统问题,就必须通过traceId高效查找一次请求的全部上下文日志,包括异步执行的逻辑。

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
目录
相关文章
|
3月前
|
人工智能 监控 Java
零代码改造 + 全链路追踪!Spring AI 最新可观测性详细解读
Spring AI Alibaba 通过集成 OpenTelemetry 实现可观测性,支持框架原生和无侵入探针两种方式。原生方案依赖 Micrometer 自动埋点,适用于快速接入;无侵入探针基于 LoongSuite 商业版,无需修改代码即可采集标准 OTLP 数据,解决了原生方案扩展性差、调用链易断链等问题。未来将开源无侵入探针方案,整合至 AgentScope Studio,并进一步增强多 Agent 场景下的观测能力。
2021 59
|
4月前
|
Prometheus 监控 Java
日志收集和Spring 微服务监控的最佳实践
在微服务架构中,日志记录与监控对系统稳定性、问题排查和性能优化至关重要。本文介绍了在 Spring 微服务中实现高效日志记录与监控的最佳实践,涵盖日志级别选择、结构化日志、集中记录、服务ID跟踪、上下文信息添加、日志轮转,以及使用 Spring Boot Actuator、Micrometer、Prometheus、Grafana、ELK 堆栈等工具进行监控与可视化。通过这些方法,可提升系统的可观测性与运维效率。
503 1
日志收集和Spring 微服务监控的最佳实践
|
4月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
780 5
|
10月前
|
存储 Java 文件存储
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
2605 1
|
9月前
|
前端开发 安全 Java
Spring Boot 便利店销售系统项目分包设计解析
本文深入解析了基于Spring Boot的便利店销售系统分包设计,通过清晰的分层架构(表现层、业务逻辑层、数据访问层等)和模块化设计,提升了代码的可维护性、复用性和扩展性。具体分包结构包括`controller`、`service`、`repository`、`entity`、`dto`、`config`和`util`等模块,职责分明,便于团队协作与功能迭代。该设计为复杂企业级应用开发提供了实践参考。
377 0
|
10月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
193 0
|
10月前
|
Java 微服务 Spring
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——使用Logger在项目中打印日志
本文介绍了如何在项目中使用Logger打印日志。通过SLF4J和Logback,可设置不同日志级别(如DEBUG、INFO、WARN、ERROR)并支持占位符输出动态信息。示例代码展示了日志在控制器中的应用,说明了日志配置对问题排查的重要性。附课程源码下载链接供实践参考。
1210 0
|
6月前
|
机器学习/深度学习 XML Java
【spring boot logback】日志logback格式解析
在 Spring Boot 中,Logback 是默认的日志框架,它支持灵活的日志格式配置。通过配置 logback.xml 文件,可以定义日志的输出格式、日志级别、日志文件路径等。
1237 5
|
6月前
|
Java 关系型数据库 数据库连接
Spring Boot项目集成MyBatis Plus操作PostgreSQL全解析
集成 Spring Boot、PostgreSQL 和 MyBatis Plus 的步骤与 MyBatis 类似,只不过在 MyBatis Plus 中提供了更多的便利功能,如自动生成 SQL、分页查询、Wrapper 查询等。
706 3
|
6月前
|
前端开发 Java API
酒店管理系统基于 JavaFX Spring Boot 和 React 经典项目重构实操
本文介绍了基于现代技术栈的酒店管理系统开发方案,整合了JavaFX、Spring Boot和React三大技术框架。系统采用前后端分离架构,JavaFX构建桌面客户端,React开发Web管理界面,Spring Boot提供RESTful API后端服务。核心功能模块包括客房管理和客户预订流程,文中提供了JavaFX实现的客房管理界面代码示例和React开发的预订组件代码,展示了如何实现客房信息展示、添加修改操作以及在线预订功能。
414 0