当前位置: 首页 > news >正文

日志技术、框架

目录

门面模式

核心思想

结构

Java日志体系

常用的日志技术组合

主流日志框架与技术详解

区别与优点对比

日志门面:SLF4J vs JCL

字符串拼接 vs 参数化

字符串拼接(String Concatenation)

参数化(Parameterization)

在日志中的影响

 在SQL中的完全不同的概念

日志实现:Logback vs Log4j 2

在SpringBoot中如何操作?

日志 vs AOP

两者的关系

1. 独立使用日志(常见做法)

2. 使用AOP统一记录日志(可选做法)

核心区别

实际应用建议

完整的AOP日志切面示例

配套的自定义注解(可选)

关键点说明

1. @Pointcut的作用

2. 常用的切入点表达式

各种通知类型的适用场景


门面模式

门面模式(Facade Pattern)是一种经典的设计模式。它是结构型设计模式的一种

核心思想

定义:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

通俗理解:就像一个公司的前台接待员,你不需要知道公司内部各个部门如何协调工作,只需要通过前台就能完成你想要办理的业务。

结构

// 复杂的子系统A
class SubSystemA {public void operationA() {System.out.println("执行子系统A的操作");}
}// 复杂的子系统B  
class SubSystemB {public void operationB() {System.out.println("执行子系统B的操作");}
}// 复杂的子系统C
class SubSystemC {public void operationC() {System.out.println("执行子系统C的操作");}
}// 门面类 - 提供统一的简单接口
class Facade {private SubSystemA systemA;private SubSystemB systemB;private SubSystemC systemC;public Facade() {this.systemA = new SubSystemA();this.systemB = new SubSystemB();this.systemC = new SubSystemC();}// 统一的简单方法,隐藏了内部的复杂调用public void simpleOperation() {System.out.println("门面开始协调各个子系统...");systemA.operationA();systemB.operationB();systemC.operationC();System.out.println("门面完成所有操作");}
}// 客户端使用
public class Client {public static void main(String[] args) {// 不使用门面模式 - 复杂SubSystemA a = new SubSystemA();SubSystemB b = new SubSystemB();SubSystemC c = new SubSystemC();a.operationA();b.operationB();c.operationC();// 使用门面模式 - 简单Facade facade = new Facade();facade.simpleOperation();  // 一行代码完成所有操作}
}

Java日志体系

现代Java日志体系的核心思想:“门面模式” 或 “抽象层”。它将日志的接口和实现分离开。

  • 日志门面/抽象层:提供了一套统一的日志API,你的应用程序代码只依赖于这个抽象层,而不关心底层具体使用哪种日志实现。这使得更换日志实现变得非常容易。

  • 日志实现/框架:具体的日志库,负责实现实际的日志记录功能(如输出到控制台、文件、数据库等)。

常用的日志技术组合

在SpringBoot中,最常见的组合是:SLF4J + Logback

  • 日志门面SLF4J

  • 日志实现Logback

为什么是这个组合?
因为SpringBoot的默认日志 starter spring-boot-starter-logging 就是采用这个组合。
当你引入 spring-boot-starter-web 或其他核心starter时,它已经被自动引入了。

<!-- 查看 spring-boot-starter-logging 的依赖树,你会看到 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 它内部依赖了: -->
<!-- slf4j-api (门面) -->
<!-- logback-classic (实现,并且它本身也桥接到了slf4j) -->
<!-- 以及一些其他库的桥接器 -->

主流日志框架与技术详解

类别名称角色与描述
日志门面SLF4J (Simple Logging Facade for Java)业界标准。提供统一的API,让程序与具体日志实现解耦。
JCL (Jakarta Commons Logging) / Apache Commons Logging旧时代的标准,现在已被SLF4J取代。Spring早期使用它。
日志实现LogbackSLF4J的天然继任者,由SLF4J的作者设计。性能好,功能丰富,是SpringBoot的默认实现。
Log4j 2Log4j 1.x的重大升级版。在异步日志性能上极其出色,功能强大,是Logback的强大竞争对手。
Log4j 1.x已被淘汰,存在性能和安全问题。
JUL (java.util.logging)JDK自带的日志工具,功能相对简单,一般不选它。

区别与优点对比

日志门面:SLF4J vs JCL
特性SLF4JJCL (Commons Logging)
绑定机制在编译时通过静态绑定找到日志实现。更稳定,不易出现类加载问题。在运行时动态发现日志实现。容易出现类加载器内存泄漏问题。
性能更好,尤其使用占位符{}时,避免了不必要的字符串拼接。稍差。
社区与趋势现代Java开发的事实标准,是SpringBoot等主流框架的选择。老旧,已基本被SLF4J取代。

SLF4J的占位符优势:

// SLF4J - 推荐:只有当日志级别是DEBUG时,才会进行字符串拼接
logger.debug("User id is: {}, name is: {}", userId, userName);// 传统方式 - 不推荐:无论级别如何,都会先进行字符串拼接,浪费性能
logger.debug("User id is: " + userId + ", name is: " + userName);
字符串拼接 vs 参数化
字符串拼接(String Concatenation)
// 字符串拼接:在代码中直接连接字符串
String message = "User id is: " + userId + ", name is: " + userName;
logger.debug(message);// 或者直接拼接
logger.debug("User id is: " + userId + ", name is: " + userName);
参数化(Parameterization)
// 参数化:使用占位符,由框架在内部处理
logger.debug("User id is: {}, name is: {}", userId, userName);

注意:日志参数化 = 立即求值 + 条件拼接(根据日志级别决定是否进行字符串拼接)

SLF4J的日志级别从低到高是:
TRACE → DEBUG → INFO → WARN → ERROR

上面代码实际执行顺序:
 1. ✔ 立即求值:userId 和 userName被立即计算
 2. ✔ 检查级别:判断当前是否允许DEBUG级别日志
 3. ×/✔ 条件拼接:只有级别允许时,才进行 User id is: " + userId + ", name is: " + userName的拼接

在日志中的影响

1.性能影响

// × 字符串拼接:总是创建新字符串对象
logger.debug("Processing user: " + user.getName() + " with id: " + user.getId());
// 即使DEBUG级别关闭,也会创建:"Processing user: John with id: 123"// ✔ 参数化:延迟字符串创建
logger.debug("Processing user: {} with id: {}", user.getName(), user.getId());
// 如果DEBUG关闭,不会创建最终的日志字符串,节省内存和CPU

但是 参数化 没有避免了不必要的方法执行(立即求值)

传统字符串拼接(最差)logger.debug("User: " + userName + ", Data: " + fetchUserData());
// 1. 立即求值:fetchUserData() 执行
// 2. 立即拼接:创建完整的日志字符串
// 3. 检查级别:如果DEBUG关闭,丢弃已经拼接好的字符串
// × 浪费:方法执行 + 字符串拼接参数化日志(中等)logger.debug("User: {}, Data: {}", userName, fetchUserData());
// 1. 立即求值:fetchUserData() 执行
// 2. 检查级别:如果DEBUG关闭,直接返回
// 3. 条件拼接:只有DEBUG开启时才拼接
// ✔ 节省:避免了不必要的字符串拼接
// × 仍然浪费:方法执行条件检查(最优)if (logger.isDebugEnabled()) {logger.debug("User: {}, Data: {}", userName, fetchUserData());
}
// 1. 检查级别:如果DEBUG关闭,直接跳过
// 2. 条件求值:只有DEBUG开启时才执行fetchUserData()
// 3. 条件拼接:只有DEBUG开启时才拼接
// ✔ 完全节省:避免了不必要的方法执行和字符串拼接

为什么设计成这样?
Java语言限制

// Java的方法调用机制决定了参数必须立即求值
method(argument1, argument2);
// 在进入method方法体之前,argument1和argument2必须已经计算完成// 无法实现真正的"延迟参数求值",除非使用lambda
method(() -> expensiveOperation());  // 这样才可以延迟

早期的SLF4J设计时,Java还没有lambda表达式,所以无法实现真正的延迟求值。

现代改进

Log4j2的解决方案

// Log4j2支持lambda,实现真正的延迟
logger.debug("Data: {}", () -> expensiveOperation());
// expensiveOperation()只在DEBUG开启且需要输出时才执行

为什么Log4j2能做到

// Log4j2的API设计使用了Supplier
void debug(String message, Supplier<?>... paramSuppliers);// 参数传递的是"代码"而不是"值"
logger.debug("Data: {}", () -> expensiveOperation());
// 传递的是:一个可以执行expensiveOperation()的lambda表达式
// 而不是:expensiveOperation()的执行结果

2.可读性影响

// × 字符串拼接:冗长,难以阅读
logger.debug("User " + userName + " (ID: " + userId + ") from " + city + ", " + country + " performed action: " + action);// ✔ 参数化:清晰,易于阅读  
logger.debug("User {} (ID: {}) from {}, {} performed action: {}", userName, userId, city, country, action);
 在SQL中的完全不同的概念

1.日志参数化(例如SLF4J的{}占位符):

目的:主要是为了性能(避免不必要的字符串拼接)和代码可读性。

机制:在日志框架内部,如果日志级别足够(例如DEBUG级别开启),才会将参数替换到占位符中,生成完整的日志字符串。如果日志级别不够,则不会进行字符串拼接,但请注意,参数仍然会被求值(即如果参数是方法调用,该方法会被执行),只是不会拼接字符串。

2.SQL参数化(例如MyBatis的#{}):

目的:主要是为了安全(防止SQL注入)和性能(数据库可以缓存执行计划)。

机制:使用预编译语句,将参数值通过占位符(?)传递给数据库,数据库将参数值视为数据,而不是SQL代码的一部分。因此,即使参数值中包含SQL代码,也不会被数据库执行。

SQL注入风险

// × 危险的字符串拼接(动态SQL)
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// 如果 username = "admin' OR '1'='1",就变成:
// SELECT * FROM users WHERE username = 'admin' OR '1'='1'// ✔ MyBatis参数化(使用#{})
@Select("SELECT * FROM users WHERE username = #{username}")
User findByUsername(String username);
// 会被编译为:SELECT * FROM users WHERE username = ?
// 然后安全地设置参数值

MyBatis中的#{} vs ${}

// ✔ #{}:参数化,防止SQL注入
@Select("SELECT * FROM users WHERE username = #{username} AND age = #{age}")
User findUser(@Param("username") String username, @Param("age") int age);
// 生成:SELECT * FROM users WHERE username = ? AND age = ?// × ${}:字符串替换,有SQL注入风险(但有时必要)
@Select("SELECT * FROM ${tableName} ORDER BY ${columnName}")
List<User> dynamicQuery(@Param("tableName") String tableName, @Param("columnName") String columnName);
// 生成:SELECT * FROM users ORDER BY create_time
// 注意:${}只能用于表名、列名等标识符,不能用于用户输入的值!
日志实现:Logback vs Log4j 2

这是选择的重点,也是SpringBoot中常见的“切换”场景。

        LogbackLog4j 2
出身SLF4J作者的“亲儿子”,为SLF4J原生设计。重建版,吸收了Logback的优点并加以改进。
性能优秀,比Log4j 1.x快很多。极其优秀,尤其是在异步日志场景下,性能可以比Logback高数倍甚至十倍。
配置方式主要使用XMLXML, JSON, YAML, Properties。
特性亮点- 自动重新加载配置文件。
- 更平滑的过滤功能。
- 从Spring Boot“开箱即用”。
强大的异步日志器 (AsyncLogger),是其性能杀手锏。
- 支持自定义日志级别。
- 插件化架构,扩展性强。
- 防止因日志阻塞应用线程。
缺点异步性能不如Log4j 2。配置相对复杂一些。需要排除默认的Logback并显式引入。
适用场景绝大多数常规应用,对性能要求不是极端苛刻。高并发、低延迟的系统,如金融交易、游戏服务器、大数据处理等。
在SpringBoot中如何操作?

1. 使用默认配置 (SLF4J + Logback)

你什么都不用做,直接在你的类中使用即可:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;@RestController
public class MyController {// 1. 声明Logger,通常使用当前类作为参数private static final Logger logger = LoggerFactory.getLogger(MyController.class);public void someMethod() {// 2. 使用日志logger.info("This is an info message.");logger.error("This is an error message.", e); // 记录异常logger.debug("Debug with placeholder: {}", someValue);}
}

配置文件是 src/main/resources/application.yml 或 src/main/resources/logback-spring.xml

application.yml 示例:

logging:level:com.yourpackage: DEBUG # 设置特定包的日志级别org.springframework.web: WARNfile:name: myapp.log # 输出到文件pattern:console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" # 控制台输出格式

2.切换为 Log4j 2

如果你需要Log4j 2的高性能,可以很容易地切换。

  • 排除默认的 Logback
  • 引入 Log4j 2 的 Starter

在 pom.xml 中:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><!-- 排除默认的日志starter --><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions>
</dependency><!-- 添加 Log4j 2 的 Starter -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

然后,你的代码完全不用改,因为使用的依然是SLF4J的门面API。只需要创建一个Log4j 2的配置文件,如 src/main/resources/log4j2.xml 或 src/main/resources/log4j2-spring.xml

推荐选择理由
绝大多数SpringBoot项目SLF4J + Logback (默认)简单、省心、功能足够、与SpringBoot生态无缝集成。
高性能、高并发项目SLF4J + Log4j 2追求极致的异步日志性能,对系统吞吐量和响应时间有极高要求。

核心建议:

  1. 在代码中,永远使用 SLF4J 的API(即 LoggerFactory.getLogger),这样你的应用就与具体实现解耦了。

  2. 除非有明确的性能瓶颈,否则SpringBoot的默认配置(Logback)已经非常强大。

  3. 如果遇到性能问题,特别是I/O成为瓶颈时,优先考虑切换到Log4j 2并启用其异步日志功能。

日志 vs AOP

日志(Logging)

  • 目的:记录应用程序运行时的状态、事件、错误等信息
  • 技术:SLF4J、Logback、Log4j2等
  • 使用方式:在代码中直接调用日志API

AOP(面向切面编程)

  • 目的:将横切关注点(如日志、安全、事务)从业务逻辑中分离
  • 技术:Spring AOP、AspectJ
  • 使用方式:通过切面统一处理

两者的关系

1. 独立使用日志(常见做法)

@Service
public class UserService {private static final Logger logger = LoggerFactory.getLogger(UserService.class);public User findUserById(Long id) {logger.debug("开始查询用户,ID: {}", id);  // 手动记录日志// 业务逻辑User user = userRepository.findById(id);logger.info("用户查询完成: {}", user.getName());return user;}
}

2. 使用AOP统一记录日志(可选做法)

@Aspect
@Component
public class LoggingAspect {@Before("execution(* com.example.service.*.*(..))")public void logBefore(JoinPoint joinPoint) {// AOP自动记录日志,业务代码中不需要写日志语句logger.info("方法执行: {}", joinPoint.getSignature().getName());}@AfterReturning("execution(* com.example.service.*.*(..))")public void logAfter(JoinPoint joinPoint) {logger.info("方法执行完成: {}", joinPoint.getSignature().getName());}
}@Service
public class UserService {// 业务代码很干净,没有日志语句public User findUserById(Long id) {return userRepository.findById(id);  // 纯粹的业务逻辑}
}

核心区别

方面日志AOP
目的记录信息解耦横切关注点
使用方式代码中直接调用通过切面自动织入
侵入性侵入业务代码非侵入式
关注点"记录什么""在哪里记录"

实际应用建议

业务日志:建议在代码中直接使用日志框架记录

// 业务相关的日志最好在代码中明确写出
logger.info("订单{}状态更新为: {}", orderId, newStatus);

技术日志:可以使用AOP统一处理

// 方法执行时间、入参出参等通用日志可以用AOP
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();Object result = joinPoint.proceed();long duration = System.currentTimeMillis() - start;logger.debug("方法 {} 执行耗时: {}ms", joinPoint.getSignature(), duration);return result;
}

总结:日志是记录内容的技术,AOP是实现方式的技术。它们可以结合使用,但本质上是不同的概念。在SpringBoot项目中,您完全可以在不使用AOP的情况下正常使用日志功能。

完整的AOP日志切面示例

@Aspect
@Component
public class LoggingAspect {private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);// 1. 定义切入点 - 方式一:使用注解标记@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +"@annotation(org.springframework.web.bind.annotation.RequestMapping)")public void controllerMethods() {}// 2. 定义切入点 - 方式二:匹配包路径@Pointcut("execution(* com.example.service.*.*(..))")public void serviceMethods() {}// 3. 定义切入点 - 方式三:组合多个切入点@Pointcut("controllerMethods() || serviceMethods()")public void loggableMethods() {}// 4. 环绕通知 - 最常用的日志记录方式@Around("loggableMethods()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().toShortString();Object[] args = joinPoint.getArgs();// 方法执行前日志logger.info("开始执行方法: {}, 参数: {}", methodName, Arrays.toString(args));long startTime = System.currentTimeMillis();try {// 执行目标方法Object result = joinPoint.proceed();long executionTime = System.currentTimeMillis() - startTime;// 方法执行成功日志logger.info("方法执行成功: {}, 耗时: {}ms, 返回结果: {}", methodName, executionTime, result);return result;} catch (Exception e) {long executionTime = System.currentTimeMillis() - startTime;// 方法执行异常日志logger.error("方法执行异常: {}, 耗时: {}ms, 异常信息: {}", methodName, executionTime, e.getMessage(), e);throw e;}}// 5. 前置通知@Before("controllerMethods()")public void logBeforeController(JoinPoint joinPoint) {logger.debug("Controller方法调用: {}", joinPoint.getSignature().getName());}// 6. 后置通知(无论是否异常都执行)@After("serviceMethods()")public void logAfterService(JoinPoint joinPoint) {logger.debug("Service方法执行完毕: {}", joinPoint.getSignature().getName());}// 7. 返回通知(只有正常返回时执行)@AfterReturning(pointcut = "loggableMethods()", returning = "result")public void logAfterReturning(JoinPoint joinPoint, Object result) {logger.debug("方法正常返回: {}, 返回值: {}", joinPoint.getSignature().getName(), result);}// 8. 异常通知@AfterThrowing(pointcut = "loggableMethods()", throwing = "ex")public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {logger.error("方法执行异常: {}, 异常类型: {}, 异常信息: {}", joinPoint.getSignature().getName(), ex.getClass().getSimpleName(), ex.getMessage());}
}

配套的自定义注解(可选)

// 自定义日志注解,用于标记需要记录日志的方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {String value() default "";boolean logParameters() default true;boolean logResult() default true;Level level() default Level.INFO;enum Level {DEBUG, INFO, WARN, ERROR}
}// 在切面中使用自定义注解
@Aspect
@Component
public class CustomLoggingAspect {@Pointcut("@annotation(com.example.annotation.Loggable)")public void loggableAnnotation() {}@Around("loggableAnnotation() && @annotation(loggable)")public Object logWithCustomAnnotation(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {// 根据注解配置进行日志记录if (loggable.logParameters()) {logger.info("方法参数: {}", Arrays.toString(joinPoint.getArgs()));}Object result = joinPoint.proceed();if (loggable.logResult()) {logger.info("方法返回: {}", result);}return result;}
}// 在业务方法上使用自定义注解
@Service
public class UserService {@Loggable(logParameters = true, logResult = false, level = Loggable.Level.DEBUG)public User findUserById(Long id) {return userRepository.findById(id);}
}

关键点说明

1. @Pointcut的作用
  • 定义可重用的切入点表达式

  • 提高代码的可读性和可维护性

  • 避免在多个通知中重复编写相同的表达式

@Pointcut确实可以省略

// 方式1:使用组合Pointcut(可以省略)
@Pointcut("controllerMethods() || serviceMethods()")
public void loggableMethods() {}@Around("loggableMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {// ...
}// 方式2:直接组合(省略组合Pointcut)
@Around("controllerMethods() || serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {// ...
}

推荐省略组合@Pointcut的情况:

  1. 组合只在一个地方使用

  2. 表达式简单明了

  3. 项目规模较小

  4. 团队成员对AOP熟悉

推荐使用组合@Pointcut的情况:

  1. 相同的组合在多个通知中重复使用

  2. 组合表达式很复杂

  3. 需要提高代码可读性

  4. 可能需要在运行时动态改变切入点

2. 常用的切入点表达式
// 匹配包下所有方法
@Pointcut("execution(* com.example.service.*.*(..))")// 匹配特定注解的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")// 匹配实现特定接口的类
@Pointcut("within(com.example.service.BaseService+)")// 匹配Controller层
@Pointcut("within(@org.springframework.stereotype.Controller *)")
各种通知类型的适用场景
  • @Around: 最强大,可以控制方法执行,适合记录完整的方法调用信息(参数、返回值、执行时间、异常等)
  • @Before: 简单记录方法开始执行
  • @AfterReturning: 记录方法正常返回的结果
  • @AfterThrowing: 专门记录异常情况
  • @After: 无论成功失败都执行,适合资源清理
特性Spring AOPAspectJ
架构层次基于代理的运行时AOP完整的AOP解决方案
织入时机运行时织入编译时、编译后、加载时织入
性能有运行时开销接近原生性能
功能范围方法级别拦截完整的AOP支持
依赖轻量,Spring框架内需要AspectJ编译器/织入器
学习曲线简单易用相对复杂
http://www.dtcms.com/a/505002.html

相关文章:

  • css使用 :where() 来简化大型 CSS 选择器列表
  • 海报在线制作免费网站创办网站公司
  • 网站建设服务商怎么收费wordpress主题著作权
  • ResponseEntity - Spring框架的“标准回复模板“
  • 京东网站开发费用济南市住房和城乡建设局网站
  • 赛车网站开发做a的视频在线观看网站
  • 如何替换网站ico图标做网站需要哪些技能
  • mysql基础【事务】
  • 网络前端开发招聘搜索引擎优化报告
  • 龙岗网站建设推广报价广西桂林为什么穷
  • 网站开发学什么数据库龙海市城乡规划建设局网站
  • 烟台网站建设技术支持wordpress多媒体导入
  • 网站做跳转怎么做菏泽 网站建设
  • 06数据采集:Prometheus的基本介绍、架构与组件
  • 商城网站建设协议软件开发公司规章制度
  • 执业医师变更注册网站跨境电商运营平台
  • 门头沟做网站公司国内建网站流程
  • 北京网站制作郑州电子商务网站建设需要知识
  • RK3568学习笔记
  • 大规模网站开发语言微信小程序可以自己开发吗
  • wordpress 导出用户天津seo网络营销
  • 【数据结构与算法基础】05. 栈详解(C++ 实战)
  • 做网站要有什么团队雨发建设集团有限公司网站
  • seo建站优化asp.net+mvc+网站开发
  • 网站图片一般分辨率做多大赣州网站建设服务
  • 2017招远网站建设云兰装潢公司总部地址电话
  • 网站服务器查询工具wordpress 返回顶部插件
  • 做网站推广弊端青岛胶南做网站的
  • 2018年网站建设工作总结北京市建设工程信息网登录流程
  • 网站首页背景代码旅游类网站开发毕业设计