2024年7月

在Java中,循环调用一个具有事务的方法时,需要特别注意事务的边界和管理。通常,事务的边界是由框架(如Spring)来控制的,确保方法执行时数据的完整性和一致性。然而,在循环中调用事务方法时,每个调用都可以被视为独立的事务,除非特别配置以允许跨多个方法调用共享同一事务。

1. Java 方法中循环调用具有事务的具体方法示例

下面,我将提供一个使用Spring框架的示例,其中包含一个服务层方法,该方法在循环中调用另一个具有事务注解的方法。请注意,默认情况下,Spring的
@Transactional
注解在每个方法调用时都会开启一个新的事务,除非配置为使用不同的传播行为。

1.1 示例环境

(1)Spring Boot 2.x;

(2)Maven 项目。

1.2 Maven 依赖

首先,确保我们的
pom.xml
文件中包含必要的Spring Boot和数据库相关依赖。这里只列出核心依赖:

<dependencies>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-data-jpa</artifactId>  
    </dependency>  
    <dependency>  
        <groupId>com.h2database</groupId>  
        <artifactId>h2</artifactId>  
        <scope>runtime</scope>  
    </dependency>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
</dependencies>

1.3 实体类

假设我们有一个简单的
User
实体类:

import javax.persistence.Entity;  
import javax.persistence.GeneratedValue;  
import javax.persistence.GenerationType;  
import javax.persistence.Id;  
  
@Entity  
public class User {  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
    private String name;  
  
    // 省略构造方法、getter和setter  
}

1.4 仓库接口

import org.springframework.data.jpa.repository.JpaRepository;  
  
public interface UserRepository extends JpaRepository<User, Long> {  
}

1.5 服务层

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
import org.springframework.transaction.annotation.Transactional;  
  
@Service  
public class UserService {  
  
    @Autowired  
    private UserRepository userRepository;  
  
    // 假设这个方法需要在循环中调用另一个事务方法  
    public void processUsers() {  
        for (int i = 0; i < 10; i++) {  
            // 每次循环调用一个事务方法  
            createUser("User" + i);  
        }  
    }  
  
    @Transactional  
    public void createUser(String name) {  
        User user = new User();  
        user.setName(name);  
        userRepository.save(user);  
        // 这里可以模拟一些业务逻辑,如果抛出异常,则当前事务会回滚  
        // throw new RuntimeException("Failed to create user");  
    }  
}

1.6 控制器

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
public class UserController {  
  
    @Autowired  
    private UserService userService;  
  
    @GetMapping("/process")  
    public String processUsers() {  
        userService.processUsers();  
        return "Users processed successfully!";  
    }  
}

1.7 注意

(1)在上面的例子中,
createUser
方法被
@Transactional
注解标记,意味着它将在自己的事务中运行。由于
processUsers
方法没有
@Transactional
注解,所以循环中的每次
createUser
调用都将独立开启和关闭事务。

(2)如果需要所有
createUser
调用在同一个事务中执行(例如,要求所有用户创建成功或全部失败),我们需要将
@Transactional
注解移动到
processUsers
方法上,并可能调整传播行为(尽管在这种情况下,默认的传播行为
REQUIRED
应该就足够了)。

1.8 结论

这个示例演示了如何在Java(特别是Spring框架中)循环调用具有事务的方法。根据我们的具体需求,我们可能需要调整事务的传播行为或边界。

2.其他方法示例

在Java中,特别是在使用Spring框架时,管理事务的方式不仅仅是通过
@Transactional
注解。虽然
@Transactional
是Spring中最常用和推荐的方式,但还有其他几种方法可以实现类似的功能。以下是一些替代方案:

2.1 编程式事务管理

编程式事务管理允许我们通过代码直接控制事务的开始、结束以及异常时的回滚。Spring提供了
TransactionTemplate

PlatformTransactionManager
来帮助进行编程式事务管理。

示例:使用
TransactionTemplate

@Autowired  
private TransactionTemplate transactionTemplate;  
  
@Autowired  
private UserRepository userRepository;  
  
public void processUsers() {  
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
        @Override  
        protected void doInTransactionWithoutResult(TransactionStatus status) {  
            for (int i = 0; i < 10; i++) {  
                try {  
                    createUser("User" + i);  
                } catch (RuntimeException ex) {  
                    // 可以在这里决定是回滚整个事务还是只处理当前异常  
                    status.setRollbackOnly();  
                    throw ex; // 可选,根据需要抛出或处理异常  
                }  
            }  
        }  
  
        private void createUser(String name) {  
            User user = new User();  
            user.setName(name);  
            userRepository.save(user);  
        }  
    });  
}

注意:在这个例子中,整个循环被包裹在一个事务中,这意味着如果循环中的任何
createUser
调用失败,整个事务将回滚。

2.2 声明式事务管理(除了
@Transactional

虽然
@Transactional
是声明式事务管理的典型方式,但Spring也支持通过XML配置来实现相同的功能。不过,在现代Spring应用中,这种方式已经不太常见了。

示例XML配置
(简化版):

<beans ...>  
  
    <!-- 事务管理器配置 -->  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource"/>  
    </bean>  
  
    <!-- 声明事务代理 -->  
    <tx:advice id="txAdvice" transaction-manager="transactionManager">  
        <tx:attributes>  
            <tx:method name="*" propagation="REQUIRED"/>  
        </tx:attributes>  
    </tx:advice>  
  
    <!-- 定义切入点 -->  
    <aop:config>  
        <aop:pointcut id="serviceOperation" expression="execution(* com.example.service.*.*(..))"/>  
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>  
    </aop:config>  
  
    <!-- 其他bean定义... -->  
  
</beans>

注意:这个XML配置需要与Spring的AOP命名空间一起使用,并且
dataSource
bean 也需要被定义。

2.3 使用AOP(面向切面编程)手动创建事务

我们可以通过Spring的AOP框架手动拦截方法调用,并在调用前后添加事务管理逻辑。这通常比直接使用
@Transactional

TransactionTemplate
更复杂,因此不推荐除非有特殊需求。

使用AOP手动管理事务通常不是推荐的做法,因为它涉及到底层事务API的直接调用,这可能会使代码变得复杂且难以维护。不过,为了说明目的,我们可以想象一个切面,它在方法调用前后分别开启和提交/回滚事务。

示例AOP切面
(概念性):

@Aspect  
@Component  
public class TransactionAspect {  
  
    @Autowired  
    private PlatformTransactionManager transactionManager;  
  
    @Around("execution(* com.example.service.*.*(..))")  
    public Object manageTransaction(ProceedingJoinPoint pjp) throws Throwable {  
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());  
        try {  
            Object result = pjp.proceed(); // 执行方法  
            transactionManager.commit(status);  
            return result;  
        } catch (RuntimeException e) {  
            transactionManager.rollback(status);  
            throw e;  
        }  
    }  
}

注意
:这个示例非常简化,并且没有处理事务传播行为、只读事务等高级特性。此外,它也没有考虑事务的同步和并发问题。

2.4 数据库层面的事务

在某些情况下,我们也可以依赖数据库本身的事务支持。例如,使用JDBC时,我们可以手动管理
Connection
对象的
setAutoCommit(false)
来开启事务,并在完成所有数据库操作后调用
commit()

rollback()
。然而,这种方法通常与Spring的事务管理集成不佳,并且容易出错。

在数据库层面管理事务通常涉及使用JDBC的
Connection
对象。这不是Spring特有的,但Spring提供了对JDBC的封装(如
JdbcTemplate
),尽管它通常与Spring的事务管理一起使用。

JDBC示例
(非Spring特有):

Connection conn = dataSource.getConnection();  
try {  
    conn.setAutoCommit(false);  
    // 执行SQL语句...  
    conn.commit();  
} catch (SQLException e) {  
    if (conn != null) {  
        try {  
            conn.rollback();  
        } catch (SQLException ex) {  
            // 处理回滚异常  
        }  
    }  
    throw e; // 重新抛出异常  
} finally {  
    if (conn != null) {  
        try {  
            conn.close();  
        } catch (SQLException e) {  
            // 处理关闭连接异常  
        }  
    }  
}

2.5 使用分布式事务

如果我们的应用需要跨多个数据库或服务进行事务管理,那么我们可能需要使用分布式事务解决方案,如JTA(Java Transaction API)、Atomikos、Bitronix或Spring Cloud的分布式事务支持(如通过Spring Cloud Data Flow)。

分布式事务涉及多个服务或数据库之间的协调。Spring Cloud提供了对分布式事务的支持,但通常依赖于外部服务(如Atomikos、Bitronix或基于JTA的实现)。

示例
(概念性,使用Spring Cloud和Atomikos):

这通常涉及在Spring Boot应用中配置Atomikos作为JTA事务管理器,并在需要分布式事务的服务中注入该事务管理器。然后,我们可以使用
@Transactional
注解来标记需要分布式事务支持的方法。但是,具体的配置将取决于我们使用的Spring Cloud版本和分布式事务解决方案。

2.6 结论

对于大多数Spring应用来说,
@Transactional
注解是管理事务的首选方法。然而,根据我们的具体需求和场景,我们可能需要考虑上述其他方法。在选择时,请权衡每种方法的优缺点,并选择最适合我们应用需求的方案。

由于篇幅和复杂性的限制,我将为每种方法提供一个简化的代码示例框架或思路,而不是完整的可运行代码。这些示例旨在说明概念和方法,而不是直接用于生产环境。

前言

公司项目是是微服务项目,网关是手撸的一个.net core webapi 项目,使用 refit 封装了 20+ 服务 SDK,在网关中进行统一调用和聚合等处理,以及给前端提供 swagger 文档
在我两年前进公司的时候,文档还能够顺滑的打开,在去年的时候文档只能在本地打开,或者访问原始的 swagger 页面,knife4j 的页面更是打不开一点,于是想办法对此进行了优化

.net core 项目中使用 Swashbuckle.AspNetCore 生成 SwaggerUI

首先再记录一下安装及使用,之前也分享过 Swashbuckle.AspNetCore 的使用,不过版本比较老了,本次演示用的示例版本为 .net core 8.0,从安装使用开始分享一二

安装包

  • 新建.net core 项目
  • 添加 Swashbuckle.AspNetCore 相关包引用
  • 设置项目 xml 生成路径,组件将根据 xml 解析接口相关信息
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
    <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.2" />
  </ItemGroup>
	<PropertyGroup>
		<DocumentationFile>bin\$(MSBuildProjectName).xml</DocumentationFile>
	</PropertyGroup>

服务配置

  • 一些基础配置使用备忘
    • 配置文档信息
      c.SwaggerDoc
    • 配置环境
      c.AddServer
    • 配置模型标识
      c.CustomSchemaIds
    • 配置唯一标识
      c.CustomOperationIds
    • 配置解析 xml
      c.IncludeXmlComments
    • 启用数据注解
      c.EnableAnnotations [SwaggerOperation]
  • 完整配置如下
//框架初始化巴拉巴拉xxx
builder.Services.AddControllers();
//配置 swagger
UseSwagger(builder.Services);

/// <summary>
/// Swagger 注入配置
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
void UseSwagger(IServiceCollection services)
{
    services.AddSwaggerGen(c =>
    {
        //配置文档信息
        c.SwaggerDoc("v1", new OpenApiInfo
        {
            Title = "swagger接口文档测试",
            Description = "这是一个文档",
            Version = "v1",
        });
        //配置环境
        c.AddServer(new OpenApiServer()
        {
            Url = "",
            Description = "本地"
        });
        //配置模型标识,默认type.Name,名称一样,不同明明空间会报错,所以改成FullName,加上命名空间区分
        c.CustomSchemaIds(type => type.FullName);
        //配置唯一标识
        c.CustomOperationIds(apiDesc =>
        {
            var controllerAction = apiDesc.ActionDescriptor as ControllerActionDescriptor;
            return controllerAction.ControllerName + "-" + controllerAction.ActionName;
        });
        //解析站点下所有xml,一般加自己项目的引用的即可
        foreach (var file in Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
        {
            c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, file));
        }
        //启用数据注解
        c.EnableAnnotations(true, true);
    });
}
  • 启用 swagger

RunSwagger(app);

/// <summary>
/// 启用swagger
/// </summary>
/// <param name="app"></param>
void RunSwagger(IApplicationBuilder app)
{
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/v1/api-docs", "V1 Docs");
    });
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapSwagger("{documentName}/api-docs");
        endpoints.MapGet("/v3/api-docs/swagger-config", async (httpContext) =>
        {
            JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                IgnoreNullValues = true
            };
            jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));
            SwaggerUIOptions _options = new SwaggerUIOptions()
            {
                ConfigObject = new ConfigObject()
                {
                    Urls = new List<UrlDescriptor>
                        {
                            new UrlDescriptor()
                            {
                                Url="/v1/api-docs",
                                Name="V1 Docs"
                            }
                        }
                }
            };
            await httpContext.Response.WriteAsync(JsonSerializer.Serialize(_options.ConfigObject, jsonSerializerOptions));
        });
    });
}

运行

  • 运行后可以看到配置成功,swagger文档已经生成

到这里基础的 swagger 配置已可以使用,更深层次的参考官方文档使用即可,接下来才是不一样的东西
随着我们的项目发展,当我们的服务越来越多,接口也越来越多的时候,swagger 就从慢,到打开超时偶尔能打开,到每次都打不开(/api-docs 过大返回超时,渲染卡顿)
这个时候,或者一开始就应该对 swagger 进行分组返回了,优化 /api-docs 接口返回的数据
当然,除了这种方式,还有可以加特效标记的方式,但是几百个服务,加不了一点

分模块返回文档

一开始并没有想到分组显示,因为在本地运行的时候是可以打开的,只是 json 文件较大,于是做了一个优化是每次在发布应用后,请求一个接口去将 swagger 的 json 文件生成到本地,后续访问直接读取,算是暂时解决了打不开的问题,这样用了大半年,实在受不了这个速度,然后平时在看一些开源项目的时候发现是完全可以按自己的规则进行分组的,于是有了这篇文章

为了兼容之前的文档路由,所以还是在原有配置的基础上,配置了其他模块的接口文档
可有两种方式

  • 一种是在原有基础上显示其他分组
  • 一种是单独的 swagger 进行显示

优化修改

  • 先定义好需要分组显示的模块
//设置需要分组的api接口
var groupApis = new List<string>() { "SwaggerTest.Controllers.Test", "SwaggerTest.Controllers.Demo" };
  • UseSwagger 修改部分
  • 重点是这块的自定义,去分组中匹配路由 c.DocInclusionPredicate
    官方文档
//配置文档信息
c.SwaggerDoc("v1", new OpenApiInfo
{
    Title = "swagger接口文档测试",
    Description = "这是一个文档",
    Version = "v1",
});
//配置环境
c.AddServer(new OpenApiServer()
{
    Url = "",
    Description = "本地"
});
//模型标识配置,默认type.Name,名称一样,不同明明空间会报错,所以改成FullName,加上命名空间区分
c.CustomSchemaIds(type => type.FullName);
c.CustomOperationIds(apiDesc =>
{
    var controllerAction = apiDesc.ActionDescriptor as ControllerActionDescriptor;
    return controllerAction.ControllerName + "-" + controllerAction.ActionName;
});
//加载注释文件
foreach (var file in Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
{
    c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, file));
}
//增加模块接口的注册
groupApis.ForEach(s =>
{
    c.SwaggerDoc(s, new OpenApiInfo
    {
        Title = "api-" + s,
        Description = "api " + s,
        Version = "v1",
    });
});

//启用数据注解
c.EnableAnnotations(true, true);
//自定义分组匹配
c.DocInclusionPredicate((docName, apiDes) =>
{
    if (groupApis.Contains(docName))
    {
        var displayName = apiDes.ActionDescriptor?.DisplayName?.ToLower() ?? string.Empty;
        var existGroup = groupApis.FirstOrDefault(s => displayName.Contains(s.ToLower()));
        return docName == existGroup;
    }

    return true;
});
  • RunSwagger 修改部分
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/v1/api-docs", "V1 Docs");
    //默认页支持分组
    groupApis.ForEach(s =>
    {
        c.SwaggerEndpoint($"/{s}/api-docs", s);
    });
});
//单独的页面
groupApis.ForEach(s =>
{
    app.UseSwaggerUI(c =>
    {
        c.RoutePrefix = s;
        c.SwaggerEndpoint($"/{s}/api-docs", s);
    });
});
app.UseEndpoints(endpoints =>
{
    SwaggerUIOptions _options = new SwaggerUIOptions()
    {
        ConfigObject = new ConfigObject()
        {
            Urls = new List<UrlDescriptor>
                {
                    new UrlDescriptor()
                    {
                        Url="/v1/api-docs",
                        Name="V1 Docs"
                    }
                }.Concat(groupApis.Select(s => new UrlDescriptor()
                        {
                            Url = $"/{s}/api-docs",
                            Name = s
                        }).ToList())
        }
    };
})

修改完成后,可以结合自己业务来定义需要单独显示分组,最近又基于此加了一个开放平台的接口,独立于正常网关,单独提供出去,一切都是刚刚好~

后语

如果有更好的方式,欢迎分享
若有错误,欢迎指出,谢谢

相关文档

大家好,我是独孤风。又到了好书推荐的时间。近几年来,国内数据治理蓬勃发展,数据的价值不断放大,数据正成为一种资产,也是新型的生产要素。数据血缘一词作为数据治理的一个核心概念,更是被频频提及。 但是国内数据治理方面的书籍还是少之又少,大多数还停留在纯理论阶段,与实践,行业联系不够紧密。不过好消息来了,由国内两位专家作者 成于念与赛助力老师创作的《数据血缘分析原理与实践 》一书终于发表了,通读之后收获非常大,后续我也会分享我的读书笔记给大家。两位专家在世界500强企业从事数据相关工作,本书做了丰富的经验总结,值得一看。

在当今大数据时代,数据血缘分析已经成为数据治理的核心和基础。《数据血缘分析原理与实践》是一部能够帮助读者从0开始理解、建设并深度实践数据血缘及其系统的专业指导手册。本书内容丰富、结构清晰。本文从几个方面详细介绍这本书的精华内容和实际应用价值。
一、概念篇:理解数据血缘的基础
企业面临的数据问题
在概念篇中,作者首先从企业面临的主要数据问题入手,逐渐延伸到对数据血缘的相关定义、特征、价值,以及数据组成的深度解读。无论是互联网行业的严峻数据安全挑战,还是能源化工行业的数据共享互通能力不足,或者是零售行业的数据分析需求,本书都详细剖析了这些问题,并提出了数据血缘作为解决方案的潜力和应用前景。
数据血缘的基本概念
什么是数据血缘?数据血缘分析又是什么?这些基本概念在本书中得到了详细阐述。数据血缘是指数据从来源到目的地的流动路径及其间的关联关系。通过数据血缘分析,企业能够追踪数据的来源、变更和流转情况,确保数据的完整性和准确性。
数据血缘的特征与价值
数据血缘具有多个特征,包括数据的历史性、关联性、可追溯性等。本书从多个维度深度解析了数据血缘的价值,如破除数据质疑、快速评估数据变更影响范围、数据资产价值评估等。这部分内容是真正落地数据血缘项目的基础,帮助读者充分理解数据血缘的本质和应用场景。

二、建设篇:构建数据血缘的实践指南
“1355”框架模型
建设篇展示了一个可落地的数据血缘框架模型——“1355”框架模型,即1个周期、3种实体、5个类型、5个层级。这是数据血缘建设的基础模型,为读者提供了一个全面、系统的建设蓝图。
数据血缘实施路径
在详细介绍数据血缘实施路径时,作者列举了可能会面临的问题、具体建设方式和步骤。例如,如何确保血缘质量高、实施路径清晰,以及如何自动解析数据血缘关系等。这些实际操作指南将理论与实践相结合,帮助读者在实施过程中少走弯路。
具体建设步骤
明确数据血缘目标、制定需求范围、构建系统、完成数据血缘收集和初始化、实现数据血缘的可视化,这些步骤详细展示了如何一步步构建一个有效的数据血缘系统。
三、技术篇:深入探讨数据血缘相关技术
数据血缘应用场景
技术篇重点介绍了数据血缘相关技术和产品,以及其在数据治理中的主要应用场景。作者分析了数据开发、数据资产管理和数据安全等多个场景中的数据血缘应用,提供了实际操作中的技术参考。
核心技术与产品
数据采集技术(如ETL技术、SQL解析)、数据建模(概念建模、逻辑建模、物理建模)、数据可视化技术以及其他相关技术(如数据挖掘、区块链、人工智能、大数据技术),这些技术在书中都得到了详细介绍。此外,作者还介绍了9款主流的数据血缘产品,如Apache Atlas、LinkedIn的数据平台、马哈鱼数据血缘平台等,帮助读者了解市场上的主流工具。
四、案例篇:典型行业的数据血缘实践
行业案例分析
案例篇分享了互联网、服务、制造、零售快消等行业中的数据血缘建设案例。例如,字节跳动、Airbnb、Amazon、Netflix、Uber、通用电气、西门子、沃尔玛、Zara等全球知名企业的数据实践。这些案例为读者展示了数据血缘在不同企业和行业中的具体应用情况,提供了宝贵的经验和启发。
实践中的挑战与解决方案
每个案例不仅展示了成功的经验,也探讨了在实践中遇到的挑战及其解决方案。例如,如何确保数据血缘的准确性、如何应对数据源的复杂性、如何实现跨部门的数据协作等。

本书也是PowerData数据之力技术丛书的首本书籍,也是首本国内作者编写的数据血缘图书。感谢各位大佬的付出,期待未来社区更多的优秀书籍! 我们会陆续出版一系列的数据相关书籍的。 现在国内数据书籍太少了,我们努力填补这片空白。你们有需求也可以随时反馈哈 需要学习数据血缘相关知识的同学赶紧行动吧!

面试连环call

  1. Java类是如何被加载到内存中的?
  2. Java类的生命周期都有哪些阶段?
  3. JVM加载的class文件都有哪些来源?
  4. JVM在加载class文件时,何时判断class文件的格式是否符合要求?

类生命周期

一个类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历
加载、验证、准备、解析、初始化、使用和卸载
七个阶段,其中
验证、准备、解析
三个部分统称为
连接

类的生命周期

  • 加载、验证、准备、初始化和卸载
    这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按步就班地开始,
  • 解析阶段
    顺序则不固定,它可以在初始化之前,而在某些情况下也可以在初始化阶段之后,这是为了支持Java语言的
    运行时绑定特性

类加载过程

系统加载 Class 类型的文件主要三步:
加载->连接->初始化
。连接过程又可分为三步:
验证->准备->解析

img

加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情

  • 通过一个类的全限定名来获取其定义的
    二进制字节流
  • 将这个字节流所代表的静态存储结构转化为
    方法区的运行时数据结构

  • Java堆
    中生成一个代表这个
    类的java.lang.Class对象
    ,作为对方法区中这些数据的访问入口。

java_jvm_classload_1

加载.class文件的方式


  • 本地文件
    系统中直接加载
  • 通过
    网络下载
    .class文件

  • zip,jar等归档文件
    中加载.class文件
  • 从专有
    数据库
    中提取.class文件

  • Java源文件
    动态编译为.class文件

链接

验证

确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证
    (Class 文件格式检查)
  • 元数据验证
    (字节码语义检查)
  • 字节码验证
    (程序语义检查)
  • 符号引用验证
    (类的正确性检查)

验证阶段示意图

准备

为类的静态变量分配内存,并将其初始化为默认值

  • 这时候进行内存分配的仅包括
    类变量(
    static
    )

    ,而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 这里所设置的初始值通常情况下是
    数据类型默认的零值
    (如
    0

    0L

    null

    false
    等),而不是被在Java代码中被显式地赋予的值。

基本数据类型的零值

注意
:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,类变量所使用的内存都应当在
永久的
中进行分配。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的
字符串常量池、静态变量
等移动到

中,这个时候类变量则会随着 Class 对象一起存放在
Java 堆
中。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程

解析动作主要针对
类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符
7 类符号引用进行

  1. 符号引用
  • 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可
  1. 直接引用
  • 直接指向目标的指针(比如,指向Class类型、类变量、类方法的直接引用可能是指向方法区的指针)
  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  • 一个能间接定位到目标的句柄

类的初始化

当一个JVM在启动之后,其中可能包含的类非常多,是不是每个类都会被初始化呢?答案是
否定
的。

JVM对类的初始化是一个
延迟机制
,当一个类在首次使用的时候才会被初始化,在同一个运行时package下,一个Class只会被初始化一次。

《Java虚拟机规范》则是严格规定了
有且只有6种情况
下必须立即对类进行
初始化
(而加载、验证、准备自然需要在此之前开始)

  1. 通过
    new关键字
    会导致类的初始化

  2. 访问
    类的静态变量
    ,包括读取和更新会导致类的初始化

  3. 访问
    类的静态方法
    ,也会导致类初始化

  4. 初始化子类
    会导致父类被初始化

  5. 对某个类进行
    反射操作
    。会导致类被初始化

  6. 启动类
    ,就是执行main函数所在的类会导致该类被初始化

注意:构造某个类的数组时并不会导致该类的初始化

参考内容

1.简单使用实例

1.1 添加log4net.dll的引用。

在NuGet程序包中搜索log4net并添加,此次我所用版本为2.0.17。如下图:

image

1.2 添加配置文件

右键项目,添加新建项,搜索选择应用程序配置文件,命名为log4net.config,步骤如下图:

image

1.2.1 log4net.config简单配置示例

下面是一个简单的配置示例,详细节点及相应的说明详见
2. 配置文件节点详解

点击查看log4net.config配置
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<log4net>
	<logger name="DefaultLog">
		<!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF-->
		<!--如果没有定义LEVEL的值,则缺省为DEBUG-->
		<level value="ALL" />
		<appender-ref ref="FileAppenderDefault"></appender-ref>
	</logger>
	
	<!-- appender 定义日志输出方式   将日志以回滚文件的形式写到文件中。-->
	<appender name="FileAppenderDefault" type="log4net.Appender.RollingFileAppender">
		<!--绝对路径-->
		<!--<file value="D:\KangarooLog.txt"></file>-->
		<!--日志输出到exe程序这个相对目录下-->
		<file value="../../Log/DefalutLog" />
		<!--相对路径,在项目的根目录下-->
		<!--以最后一个路径为准,所以上面的绝对路径下不会写日志-->
		<!--<file value="./Log/Kangaroo.txt"></file>-->
		<!--防止多线程时不能写Log,官方说线程非安全-->
		<!--实际使用时,本地测试正常,部署后没有不能写日志的情况-->
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<!--追加日志内容,true后续输出的日志会追加到之前的日志文件-->
		<appendToFile value="true" />
		<!--可以为:Once|Size|Date|Composite-->
		<!--Composite为Size和Date的组合-->
		<rollingStyle value="Composite" />
		<!--日志最大个数,都是最新的-->
		<!--rollingStyle节点为Date时,该节点不起作用-->
		<!--rollingStyle节点为Size时,只能有value个日志-->
		<!--rollingStyle节点为Composite时,每天有value个日志-->
		<maxSizeRollBackups value="10" />
		<!--当备份文件时,为文件名加的后缀-->
		<!--后缀为*.txt时,例:AX.txt_2008-07-24.PxP  应该是程序上的一个bug-->
		<!--后缀为*.TXT时,例:AX.txt_2008-07-25.TXT-->
		<datePattern value="_yyyy-MM-dd'.log'" />
		<!--每个文件的大小。只在混合方式与文件大小方式下使用。超出大小后在所有文件名后自动增加正整数重新命名,数字最大的最早写入。可用的单位:KB|MB|GB。不要使用小数,否则会一直写入当前日志-->
		<maximumFileSize value="10MB" />
		<!--置为true,当前最新日志文件名永远为file节中的名字-->
		<staticLogFileName value="false" />
		<!--输出级别在INFO和ERROR之间的日志-->
		<!--<filter type="log4net.Filter.LevelRangeFilter">
			<param name="LevelMin" value="INFO" />
			<param name="LevelMax" value="ERROR" />
		</filter>-->
		<!--必须结合起来用,第一个只过滤出WARN,第二个拒绝其它其它日志输出-->
		<filter type="log4net.Filter.LevelMatchFilter">
			<param name="LevelToMatch" value="WARN" />
		</filter>
		<filter type="log4net.Filter.DenyAllFilter" />
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%n==========
			%n【日志级别】%-5level
			%n【记录时间】%date
			%n【执行时间】[%r]毫秒
			%n【执行Log分类的名称】%logger
			%n【传入信息内容】%message
			%n=========="/>
		</layout>
	</appender>
</log4net>
</configuration>
<!--==================================layout节点的配置说明==================================-->
<!--                                                                        Made By YSL      -->
<!--        %m(message):输出的日志消息,如ILog.Debug(…)输出的一条消息                          -->
<!--        %n(new line):换行                                                                   -->
<!--        %d(datetime):输出当前语句运行的时刻                                                 -->
<!--        %r(run time):输出程序从运行到执行到当前语句时消耗的毫秒数                           -->
<!--        %t(thread id):当前语句所在的线程ID                                                  -->
<!--        %p(priority): 日志的当前优先级别,即DEBUG、INFO、WARN…等                           -->
<!--        %c(class):当前日志对象的名称,例如:                                                -->
<!--               模式字符串为:%-10c -%m%n                                                    -->
<!--               代码为:                                                                     -->
<!--        ILog log=LogManager.GetLogger(“Exam.Log”);                                        -->
<!--        log.Debug(“Hello”);                                                               -->
<!--            则输出为下面的形式:                                                            -->
<!--        Exam.Log       - Hello                                                              -->
<!--        %L:输出语句所在的行号                                                              -->
<!--        %F:输出语句所在的文件名                                                            -->
<!--        %-数字:表示该项的最小长度,如果不够,则用空格填充                                  -->

1.2.2 设置log4net.config配置文件属性

点击log4net.config,将其文件属性设为始终复制,如下图:

image

1.3 在项目中引入该配置文件

这里有两种方式引入配置文件。

1.3.1 在项目的 AssemblyInfo.cs 中引入配置文件

首先在项目中新建一个Config文件夹,将之前创建的log4net.config文件放入其中,随后在 AssemblyInfo.cs 中添加如下语句:

image

1.3.2 在项目运行时动态引入配置文件

使用固定语句引入配置文件,如下所示,其中,configFilePath 为配置文件的绝对路径。

log4net.Config.XmlConfigurator.ConfigureAndWatch(new System.IO.FileInfo(configFilePath));

1.4 创建帮助类使用日志进行记录

我们首先创建名为 Log4Helper 的类,并使用固定的
log4net.LogManager.GetLogger()
语句实例化对应的Log对象,然后调用其对应的方法即可写入日志。示例代码如下:

点击查看Log4Helper代码
 public class Log4Helper
 {
     private static readonly log4net.ILog logDefault = log4net.LogManager.GetLogger("DefaultLog");

     /// <summary>
     /// 测试默认配置信息输出(输出范围做了限制)
     /// </summary>
     public static void TestDefaultLog()
     {
         logDefault.Debug("这是条调试信息");
         logDefault.Info("这是条提示信息");
         logDefault.Warn("这是条警告信息");
         logDefault.Error("这是条错误信息");
         logDefault.Fatal("这是条致命错误信息");
     }

     /// <summary>
     /// Log4Net初始化(可读取自定义配置)
     /// </summary>
     /// <param name="configFilePath"></param>
     public static void Log4NetInit(string configFilePath)
     {
         log4net.Config.XmlConfigurator.ConfigureAndWatch(new System.IO.FileInfo(configFilePath));
     }
     /// <summary>
     /// 返回指定名称的日志对象
     /// </summary>
     public static log4net.ILog Log(string appenderName)
     {
         return log4net.LogManager.GetLogger(appenderName);
     }
 }

1.5 在主程序中引用

最后一步就是在主程序中引用 Log4Helper 中的日志记录方法,如下所示:

private void Application_Startup(object sender, StartupEventArgs e)
 {
     Log4Helper.TestDefaultLog();
 }

根据上面 log4net.config 配置中 file 节点中的地址,在项目中会生成 Log 文件夹,该文件夹下会生成类似 DefalutLog_2024-07-01.log 的文件。该文件名由 file 节点和 datePattern 节点两部分组合而成,其中,
staticLogFileName
节点需要被设置为 false ,若为 true ,则当前最新日志文件名永远为 file 节点中的名字,其余日志会根据 datePattern 节点自动添加后缀。

打开日志文件,会发现里面只有 WARN 警告信息,例如:

==========                     
【日志级别】WARN                      
【记录时间】2024-07-01 00:18:28,828                     
【执行时间】[53]毫秒                     
【执行Log分类的名称】DefaultLog              
【传入信息内容】这是条警告信息             
==========

这是因为配置中的 filter 过滤节点,详见注释,将其注释再次运行,则会正常显示全部日志信息。

2. 配置文件节点详解

log4net的主要组成有四部分,分别是 Logger、Appender、Layout、Filter等,详见下方。

2.1 Logger 节点配置详解

以上文的配置为例解释:

<logger name="DefaultLog">
	<!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF-->
	<!--如果没有定义LEVEL的值,则缺省为DEBUG-->
	<level value="ALL" />
	<appender-ref ref="FileAppenderDefault"></appender-ref>
</logger>

level 定义记录的日志级别,就是说,你要记录哪个级别以上的日志,级别由低到高依次是:

ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF

如果你 level 定义 INFO,那么低于 INFO 级别以下的信息,将不会记入日志,啥意思呢?
就是说,就算你在程序里,用 log.Debug() 来写入一个日志信息,可是你在配置中指定 level 为 INFO,由于 DEBUG 级别低于 INFO,所以,不会被记入日志。这样的处理非常灵活。

在具体写日志时,一般可以这样理解日志等级:

FATAL(致命错误):记录系统中出现的能使用系统完全失去功能,服务停止,系统崩溃等使系统无法继续运行下去的错误。例如,数据库无法连接,系统出现死循环。

ERROR(一般错误):记录系统中出现的导致系统不稳定,部分功能出现混乱或部分功能失效一类的错误。例如,数据字段为空,数据操作不可完成,操作出现异常等。

WARN(警告):记录系统中不影响系统继续运行,但不符合系统运行正常条件,有可能引起系统错误的信息。例如,记录内容为空,数据内容不正确等。

INFO(一般信息):记录系统运行中应该让用户知道的基本信息。例如,服务开始运行,功能已经开户等。

DEBUG (调试信息):记录系统用于调试的一切信息,内容或者是一些关键数据内容的输出。

appender-ref,要引用的 appender 的名字,由 Layout 控制输出格式。

最后还要说一个LogManager类,它用来管理所有的Logger。它的GetLogger静态方法,可以获得配置文件中相应的Logger:

log4net.ILog log = log4net.LogManager.GetLogger("logger-name");

2.2 Appender 节点配置详解

以上文的 FileAppenderDefault 节点为例:

点击查看 FileAppenderDefault 节点配置
	<!-- appender 定义日志输出方式   将日志以回滚文件的形式写到文件中。-->
	<appender name="FileAppenderDefault" type="log4net.Appender.RollingFileAppender">
		<!--绝对路径-->
		<!--<file value="D:\KangarooLog.txt"></file>-->
		<!--日志输出到exe程序这个相对目录下-->
		<file value="../../Log/DefalutLog" />
		<!--相对路径,在项目的根目录下-->
		<!--以最后一个路径为准,所以上面的绝对路径下不会写日志-->
		<!--<file value="./Log/Kangaroo.txt"></file>-->
		<!--防止多线程时不能写Log,官方说线程非安全-->
		<!--实际使用时,本地测试正常,部署后没有不能写日志的情况-->
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<!--追加日志内容,true后续输出的日志会追加到之前的日志文件-->
		<appendToFile value="true" />
		<!--可以为:Once|Size|Date|Composite-->
		<!--Composite为Size和Date的组合-->
		<rollingStyle value="Composite" />
		<!--日志最大个数,都是最新的-->
		<!--rollingStyle节点为Date时,该节点不起作用-->
		<!--rollingStyle节点为Size时,只能有value个日志-->
		<!--rollingStyle节点为Composite时,每天有value个日志-->
		<maxSizeRollBackups value="10" />
		<!--当备份文件时,为文件名加的后缀-->
		<!--后缀为*.txt时,例:AX.txt_2008-07-24.PxP  应该是程序上的一个bug-->
		<!--后缀为*.TXT时,例:AX.txt_2008-07-25.TXT-->
		<datePattern value="_yyyy-MM-dd'.log'" />
		<!--每个文件的大小。只在混合方式与文件大小方式下使用。超出大小后在所有文件名后自动增加正整数重新命名,数字最大的最早写入。可用的单位:KB|MB|GB。不要使用小数,否则会一直写入当前日志-->
		<maximumFileSize value="10MB" />
		<!--置为true,当前最新日志文件名永远为file节中的名字-->
		<staticLogFileName value="false" />
		<!--输出级别在INFO和ERROR之间的日志-->
		<!--<filter type="log4net.Filter.LevelRangeFilter">
			<param name="LevelMin" value="INFO" />
			<param name="LevelMax" value="ERROR" />
		</filter>-->
		<!--必须结合起来用,第一个只过滤出WARN,第二个拒绝其它其它日志输出-->
		<filter type="log4net.Filter.LevelMatchFilter">
			<param name="LevelToMatch" value="WARN" />
		</filter>
		<filter type="log4net.Filter.DenyAllFilter" />
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%n==========
			%n【日志级别】%-5level
			%n【记录时间】%date
			%n【执行时间】[%r]毫秒
			%n【执行Log分类的名称】%logger
			%n【传入信息内容】%message
			%n=========="/>
		</layout>
	</appender>

每个节点均写有注释,这里额外介绍下 appender 的输出方式( type 属性),如下所示:

点击查看 appender 输出方式
	AdoNetAppender              将日志记录到数据库中。可以采用SQL和存储过程两种方式。
	AnsiColorTerminalAppender   将日志高亮输出到ANSI终端。
	AspNetTraceAppender         能用asp.net中Trace的方式查看记录的日志。
	BufferingForwardingAppender 在输出到子Appenders之前先缓存日志事件。
	ConsoleAppender             将日志输出到应用程序控制台。
	EventLogAppender            将日志写到Windows Event Log。
	FileAppender                将日志输出到文件。
	ForwardingAppender          发送日志事件到子Appenders。
	LocalSyslogAppender         将日志写到local syslog service (仅用于UNIX环境下)。
	MemoryAppender              将日志存到内存缓冲区。
	NetSendAppender             将日志输出到Windows Messenger service.这些日志信息将在用户终端的对话框中显示。
	OutputDebugStringAppender   将日志输出到Debuger,如果程序没有Debuger,就输出到系统Debuger。如果系统Debuger也不可用,将忽略消息。
	RemoteSyslogAppender        通过UDP网络协议将日志写到Remote syslog service。
	RemotingAppender            通过.NET Remoting将日志写到远程接收端。
	RollingFileAppender         将日志以回滚文件的形式写到文件中。
	SmtpAppender                将日志写到邮件中。
	SmtpPickupDirAppender       将消息以文件的方式放入一个目录中,像IIS SMTP agent这样的SMTP代理就可以阅读或发送它们。
	TelnetAppender              客户端通过Telnet来接受日志事件。
	TraceAppender               将日志写到.NET trace 系统。
	UdpAppender                 将日志以无连接UDP数据报的形式送到远程宿主或用UdpClient的形式广播。

2.3 Filter 节点配置详解

filter只能作为 appender 的子元素,type 属性表示 Filter 的类型。常用子元素 param 数量0个或多个,作用设置一些参数。具体例子详见上文
2.2 Appender 节点配置详解

额外补充下 filter 的类型说明:

	DenyAllFilter       阻止所有的日志事件被记录
	LevelMatchFilter    只有指定等级的日志事件才被记录
	LevelRangeFilter    日志等级在指定范围内的事件才被记录
	LoggerMatchFilter   Logger名称匹配,才记录
	PropertyFilter      消息匹配指定的属性值时才被记录
	StringMathFilter    消息匹配指定的字符串才被记录

2.4 Layout 节点配置详解

layout 节点只能作为 appender 的子元素。type 属性表示 Layout 的类型。具体例子详见上文
2.2 Appender 节点配置详解

额外补充 layout 节点的 type 属性取值:

	ExceptionLayout         只呈现日志事件中异常的文本信息
	PatternLayout           可以通过类型字符串来配置的布局
	RawPropertyLayout       从日志事件中提取属性值
	RawTimeStampLayout      从日志事件中提取日期
	RawUtcTimeStampLayout   从日志事件中提取UTC日期
	SimpleLayout            很简单的布局
	XmlLayout               把日志事件格式化为XML元素的布局

这其中我们主要使用的还是PatternLayout 类型,而在 ConversionPattern 节点中,我们可以进一步的配置日志输出格式,以 PatterLayout 的格式化字符串输出为例:

点击查看 PatterLayout 的格式化字符串
    %m、%message         输出的日志消息
    %d、%datetime        输出当前语句运行的时刻,格式%date{yyyy-MM-dd HH:mm:ss,fff}
    %r、%timestamp       输出程序从运行到执行到当前语句时消耗的毫秒数
    %p、%level           日志的当前优先级别
    %c、%logger          当前日志对象的名称
    %L、%line            输出语句所在的行号
    %F、%file            输出语句所在的文件名,警告:只在调试的时候有效,调用本地信息会影响性能
    %a、%appdomain       引发日志事件的应用程序域的名称。
    %C、%class、%type    引发日志请求的类的全名,警告:会影响性能
    %exception           异常信息
    %u、%identity        当前活动用户的名字,我测试的时候%identity返回都是空的。警告:会影响性能
    %l、%location        引发日志事件的名空间、类名、方法、行号。警告:会影响性能,依赖pdb文件
    %M、%method          发生日志请求的方法名,警告:会影响性能
    %n、%newline         换行符
    %x、%ndc             NDC(nested diagnostic context)
    %X、%mdc、%P、%properties  等介于 %property
    %property           输出{log4net:Identity=, log4net:UserName=, log4net:HostName=} 
    %t、%thread         引发日志事件的线程,如果没有线程名就使用线程号。
    %w、%username       当前用户的WindowsIdentity,类似:HostName/Username。警告:会影响性能
    %utcdate            发生日志事件的UTC时间。例如:%utcdate{HH:mm:ss,fff}
    %%                  输出一个百分号

额外补充下 PatterLayout 格式修饰符:

格式修饰符 对齐 最小宽 最大宽 说明
%20logger 右对齐 20 如果logger名不足20个字符,就在左边补空格
%-20logger 左对齐 20 如果logger名不足20个字符,就在右边补空格
%.30logger 左对齐 30 超过30个字符将截断
%20.30logger 右对齐 20 30 logger名要在20到30之间,少了在左边补空格,多了截断
%-20.30logger 左对齐 20 30 logger名要在20到30之间,少了在右边补空格,多了截断

3. 如何输出自定义类

自定义扩展输出,通过继承 log4net.Layout.PatternLayout 和 log4net.Layout.Pattern.PatternLayoutConverter 类,使用 log4net.Core.LoggingEvent 类的方法得到了要输出的 LogEntity 类的名称。

然后通过反射得到各个属性的值,使用 PatternLayout 类 AddConverter 方法传入得到的值,在 PatternLayoutConverter 中对其进行处理。注意配置文件 Appender 中的 Layout type 用到的类的命名空间以及类名,要同步更改为自定义类的。详见示例:

点击查看自定义拓展类的代码
namespace WPFPractice
{
    public class CustomLayout : log4net.Layout.PatternLayout
    {
        public CustomLayout()
        {
            this.AddConverter("Custom", typeof(CustomConvert));
        }
    }
    public class CustomConvert : log4net.Layout.Pattern.PatternLayoutConverter
    {
        protected override void Convert(System.IO.TextWriter writer, log4net.Core.LoggingEvent loggingEvent)
        {
            if (!string.IsNullOrEmpty(Option))
            {
                object obj = loggingEvent.MessageObject;
                if (obj != null)
                {
                    PropertyInfo info = obj.GetType().GetProperty(Option);
                    if (info != null)
                    {
                        object cusMsg = info.GetValue(obj, null);
                        writer.Write(cusMsg);
                    }
                }
            }
        }
    }
}

与之对应的,我们的配置文件也添加以下代码:

点击查看配置文件新增代码
	<logger name="CustomLog">
		<!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF-->
		<!--如果没有定义LEVEL的值,则缺省为DEBUG-->
		<level value="ALL" />
		<appender-ref ref="FileAppenderCustom"></appender-ref>
	</logger>
	
	<appender name="FileAppenderCustom" type="log4net.Appender.RollingFileAppender">
		<file value="../../Log/CustomLog" />
		<appendToFile value="true" />
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<rollingStyle value="Composite" />
		<maxSizeRollBackups value="5" />
		<datePattern value="_yyyy-MM-dd'.log'" />
		<maximumFileSize value="10MB" />
		<staticLogFileName value="false" />
		<layout type="WPFPractice.CustomLayout">
			<conversionPattern value="%n==========
			%n【日志级别】%-5level
			%n【记录时间】%date
			%n【执行时间】[%r]毫秒
			%n【执行线程ID】[%thread]
			%n【执行Log分类的名称】%logger
			%n【耗材类型名】%Custom{LabTypeName}
			%n【耗材名】%Custom{LabName}
			%n【耗材编号】%Custom{LabNumber}
			%n【是否吸头】%Custom{IsTip}
			%n【传入信息内容/类型】%message
			%n==========" />
		</layout>
	</appender>

接下来我们新建一个类 LabwareModel,并在帮助类 Log4Helper 中添加调用 CustomLog 的方法,来测试能否输出自定义类 LabwareModel 中的内容。

点击查看自定义类 LabwareModel
public class LabwareModel
{
    public string LabTypeName { get; set; } = "采样管";
    public string LabName { get; set; } = "Custom_2000ul";
    public double LabNumber { get; set; } = 200;
    public int IsTip { get; set; } = 1;
}

帮助类 Log4Helper 如下:

点击查看帮助类 Log4Helper
 private static readonly log4net.ILog logDefault = log4net.LogManager.GetLogger("DefaultLog");

 /// <summary>
 /// 生成默认实例
 /// </summary>
 /// <returns></returns>
 public static LabwareModel GetLabwareModel() 
 {
     LabwareModel labwareModel = new LabwareModel();
     return labwareModel;
 }

 /// <summary>
 /// 测试自定义配置信息输出
 /// </summary>
 public static void TestCustomLog()
 {
     var labware = GetLabwareModel();
     logCustom.Debug("这是条调试信息");
     logCustom.Info("这是条提示信息");
     logCustom.Warn("这是条警告信息");
     logCustom.Error(labware);
     logCustom.Fatal("这是条致命错误信息");
 }

在主程序中调用进行测试:

private void Application_Startup(object sender, StartupEventArgs e)
{
    Log4Helper.TestCustomLog();
    Log4Helper.TestDefaultLog();
}

根据上面 FileAppenderCustom 配置中 file 节点中的地址,在项目中会生成 Log 文件夹,该文件夹下会生成类似 CustomLog_2024-07-01.log 的文件。打开文件会看到输出内容如下:

点击查看日志输出内容
==========           
【日志级别】DEBUG                  
【记录时间】2024-07-01 00:18:28,812                  
【执行时间】[37]毫秒           
【执行线程ID】[1]           
【执行Log分类的名称】CustomLog           
【耗材类型名】           
【耗材名】           
【耗材编号】           
【是否吸头】           
【传入信息内容/类型】这是条调试信息           
==========
==========           
【日志级别】INFO                   
【记录时间】2024-07-01 00:18:28,823                  
【执行时间】[48]毫秒           
【执行线程ID】[1]           
【执行Log分类的名称】CustomLog           
【耗材类型名】           
【耗材名】           
【耗材编号】           
【是否吸头】           
【传入信息内容/类型】这是条提示信息           
==========
==========           
【日志级别】WARN                   
【记录时间】2024-07-01 00:18:28,825                  
【执行时间】[50]毫秒           
【执行线程ID】[1]           
【执行Log分类的名称】CustomLog           
【耗材类型名】           
【耗材名】           
【耗材编号】           
【是否吸头】           
【传入信息内容/类型】这是条警告信息           
==========
==========           
【日志级别】ERROR                  
【记录时间】2024-07-01 00:18:28,826                  
【执行时间】[51]毫秒           
【执行线程ID】[1]           
【执行Log分类的名称】CustomLog           
【耗材类型名】采样管           
【耗材名】Custom_2000ul           
【耗材编号】200           
【是否吸头】1           
【传入信息内容/类型】WPFPractice.LabwareModel           
==========
==========           
【日志级别】FATAL                  
【记录时间】2024-07-01 00:18:28,828                  
【执行时间】[53]毫秒           
【执行线程ID】[1]           
【执行Log分类的名称】CustomLog           
【耗材类型名】           
【耗材名】           
【耗材编号】           
【是否吸头】           
【传入信息内容/类型】这是条致命错误信息           
==========

以上就是关于 Log4Net 配置详解。