日志基础

1、日志原理简述

1.0、记录日志

每个Java程序员都很熟悉在有问题的代码中插入一些System,out.println方法调用来帮助我们观察程序裕兴的操作过程;当然,一旦发现问题的根源,就要将这些语句从代码中删除。如果接下来又出现另外问题,就需要插入几个调用System.out.println方法的语句。

记录日志API就是为了解决这个问题而设计的,下面先讨论这些API的优点:

  • 可以很容易的取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭哦这个操作也很容易;
  • 可以很简单的禁止日志记录的输出,因此,将这些日志代码流在程序中的开销很小
  • 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等
  • 日志记录器和处理器都可以对记录进行过滤。过滤可以根据过滤实现器指定的标准丢弃哪些无用的记录项;
  • 日志记录可以采用不同的方式格式化,例如:纯文本或XML
  • 应用程序可以使用多个日志记录器,他们使用类似包名的这种具有层次结构的名字:例如.com.mycompany.myapp
  • 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置

1.1、概念理解

  1. 日志门面:一般采用facade设计模式(外光设计模式:外光设计模式定义了一个高层的功能,为子系统中的多个模块协同的完成某种功能需求提供简单的对外功能调用方式,使得这一个子系统更加容易被外部使用)设计的一组接口应用

  2. 日志实现:接口的实现

日志原理五花八门,但是大道至简,其本质应属一致,日志实现底层基本组成如下:

  • Loggers:Logger负责捕捉事件并将其发送给合适的Appender

  • Appenders也被称为Handlers,负责将Logger中取出 日志信息并将消息发送出去,比如发送到控制台,文件网络上其他服务或操作系统日志

  • Layouts:也被称为Formatters,它负责对日志事件中进行中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式

实现如下:

Logger记录一个事件时,它将事务转发给适当的Appender。然后将Appender使用Layout来对日志记录进行格式化,并将其发送给控制台文件或其他目标位置。另外,Filters可以让你进一步制定一个Appender是否可以应用在一条特定的日志记录上。在日志配置中,Filters并不是必须的,但可以让你灵活的控制日志消息的流动。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AAZH4pX3-1580562142515)(images/02.png)]

1.2、总结

1.日志实现与日志门面结合实现日志系统的功能

2.日志是java开发很重要的工具,其在不断发展中也在不断地解耦和抽象化,使得日志系统更加强大便捷

3.日志组件有logger、handler、formatters组成,利用这几个基本组件能实现很多不同的日志功能

2、日志框架

转载:

1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j。后来Log4j成为Apache基金会项目中的一员。期间Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到java的标准库中,但Sun拒绝了。

2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,Log4j就已经成为一项成熟的技术,使得Log4j在选择上占据了一定的优势。

接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。

后来(2006年),Ceki Gülcü不适应Apache的工作方式,离开了Apache。然后先后创建了Slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。

现今,Java日志领域被划分为两大阵营:Commons Logging阵营和Slf4j阵营。
Commons Logging在Apache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出Slf4j的发展趋势更好
————————————————
版权声明:本文为CSDN博主「赖胖子的廖小明」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Laiguanfu/article/details/86258745

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POx0m1mV-1580562142517)(images/03.png)]

  • 由上所述可得目前主要的日志门面包括:Apache Commons-Loggingslf4j,主要的日志实现包括:log4j、jul,logback

  • 一般而言,使用日志实现即可实现一些简单程序需求,但是为了避免直接依赖日志实现儿导致耦合过密,一般会使用日志门面+日志实现的方式开发,其中,最经典的搭配是:Commons-logging+log4j,而虽然slf4j适配所有目前的日志实现,兼容性极强,但是最为适配的还是:SLF4j+logback 的组合,毕竟logback天然支持slf4j

2.1、JUL(Java.util.logging)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3NtiA5U-1580562142518)(images/11.png)]

/**
 * A Logger object is used to log messages for a specific
 * system or application component.  Loggers are normally named,
 * using a hierarchical dot-separated namespace.  Logger names
 * can be arbitrary strings, but they should normally be based on
 * the package name or class name of the logged component, such
 * as java.net or javax.swing.  In addition it is possible to create
 * "anonymous" Loggers that are not stored in the Logger namespace.
 * <p>
 * Logger objects may be obtained by calls on one of the getLogger
 * factory methods.  These will either create a new Logger or
 * return a suitable existing Logger. It is important to note that
 * the Logger returned by one of the {@code getLogger} factory methods
 * may be garbage collected at any time if a strong reference to the
 * Logger is not kept.
 * <p>
 * Logging messages will be forwarded to registered Handler
 * objects, which can forward the messages to a variety of
 * destinations, including consoles, files, OS logs, etc.
 * <p>
 * Each Logger keeps track of a "parent" Logger, which is its
 * nearest existing ancestor in the Logger namespace.
 * <p>
 * Each Logger has a "Level" associated with it.  This reflects
 * a minimum Level that this logger cares about.  If a Logger's
 * level is set to <tt>null</tt>, then its effective level is inherited
 * from its parent, which may in turn obtain it recursively from its
 * parent, and so on up the tree.
 * <p>
 * The log level can be configured based on the properties from the
 * logging configuration file, as described in the description
 * of the LogManager class.  However it may also be dynamically changed
 * by calls on the Logger.setLevel method.  If a logger's level is
 * changed the change may also affect child loggers, since any child
 * logger that has <tt>null</tt> as its level will inherit its
 * effective level from its parent.
 * <p>
 * On each logging call the Logger initially performs a cheap
 * check of the request level (e.g., SEVERE or FINE) against the
 * effective log level of the logger.  If the request level is
 * lower than the log level, the logging call returns immediately.
 * <p>
 * After passing this initial (cheap) test, the Logger will allocate
 * a LogRecord to describe the logging message.  It will then call a
 * Filter (if present) to do a more detailed check on whether the
 * record should be published.  If that passes it will then publish
 * the LogRecord to its output Handlers.  By default, loggers also
 * publish to their parent's Handlers, recursively up the tree.
 * <p>
 * Each Logger may have a {@code ResourceBundle} associated with it.
 * The {@code ResourceBundle} may be specified by name, using the
 * {@link #getLogger(java.lang.String, java.lang.String)} factory
 * method, or by value - using the {@link
 * #setResourceBundle(java.util.ResourceBundle) setResourceBundle} method.
 * This bundle will be used for localizing logging messages.
 * If a Logger does not have its own {@code ResourceBundle} or resource bundle
 * name, then it will inherit the {@code ResourceBundle} or resource bundle name
 * from its parent, recursively up the tree.
 * <p>
 * Most of the logger output methods take a "msg" argument.  This
 * msg argument may be either a raw value or a localization key.
 * During formatting, if the logger has (or inherits) a localization
 * {@code ResourceBundle} and if the {@code ResourceBundle} has a mapping for
 * the msg string, then the msg string is replaced by the localized value.
 * Otherwise the original msg string is used.  Typically, formatters use
 * java.text.MessageFormat style formatting to format parameters, so
 * for example a format string "{0} {1}" would format two parameters
 * as strings.
 * <p>
 * A set of methods alternatively take a "msgSupplier" instead of a "msg"
 * argument.  These methods take a {@link Supplier}{@code <String>} function
 * which is invoked to construct the desired log message only when the message
 * actually is to be logged based on the effective log level thus eliminating
 * unnecessary message construction. For example, if the developer wants to
 * log system health status for diagnosis, with the String-accepting version,
 * the code would look like:
 <pre><code>

   class DiagnosisMessages {
     static String systemHealthStatus() {
       // collect system health information
       ...
     }
   }
   ...
   logger.log(Level.FINER, DiagnosisMessages.systemHealthStatus());
</code></pre>
 * With the above code, the health status is collected unnecessarily even when
 * the log level FINER is disabled. With the Supplier-accepting version as
 * below, the status will only be collected when the log level FINER is
 * enabled.
 <pre><code>

   logger.log(Level.FINER, DiagnosisMessages::systemHealthStatus);
</code></pre>
 * <p>
 * When looking for a {@code ResourceBundle}, the logger will first look at
 * whether a bundle was specified using {@link
 * #setResourceBundle(java.util.ResourceBundle) setResourceBundle}, and then
 * only whether a resource bundle name was specified through the {@link
 * #getLogger(java.lang.String, java.lang.String) getLogger} factory method.
 * If no {@code ResourceBundle} or no resource bundle name is found,
 * then it will use the nearest {@code ResourceBundle} or resource bundle
 * name inherited from its parent tree.<br>
 * When a {@code ResourceBundle} was inherited or specified through the
 * {@link
 * #setResourceBundle(java.util.ResourceBundle) setResourceBundle} method, then
 * that {@code ResourceBundle} will be used. Otherwise if the logger only
 * has or inherited a resource bundle name, then that resource bundle name
 * will be mapped to a {@code ResourceBundle} object, using the default Locale
 * at the time of logging.
 * <br id="ResourceBundleMapping">When mapping resource bundle names to
 * {@code ResourceBundle} objects, the logger will first try to use the
 * Thread's {@linkplain java.lang.Thread#getContextClassLoader() context class
 * loader} to map the given resource bundle name to a {@code ResourceBundle}.
 * If the thread context class loader is {@code null}, it will try the
 * {@linkplain java.lang.ClassLoader#getSystemClassLoader() system class loader}
 * instead.  If the {@code ResourceBundle} is still not found, it will use the
 * class loader of the first caller of the {@link
 * #getLogger(java.lang.String, java.lang.String) getLogger} factory method.
 * <p>
 * Formatting (including localization) is the responsibility of
 * the output Handler, which will typically call a Formatter.
 * <p>
 * Note that formatting need not occur synchronously.  It may be delayed
 * until a LogRecord is actually written to an external sink.
 * <p>
 * The logging methods are grouped in five main categories:
 * <ul>
 * <li><p>
 *     There are a set of "log" methods that take a log level, a message
 *     string, and optionally some parameters to the message string.
 * <li><p>
 *     There are a set of "logp" methods (for "log precise") that are
 *     like the "log" methods, but also take an explicit source class name
 *     and method name.
 * <li><p>
 *     There are a set of "logrb" method (for "log with resource bundle")
 *     that are like the "logp" method, but also take an explicit resource
 *     bundle object for use in localizing the log message.
 * <li><p>
 *     There are convenience methods for tracing method entries (the
 *     "entering" methods), method returns (the "exiting" methods) and
 *     throwing exceptions (the "throwing" methods).
 * <li><p>
 *     Finally, there are a set of convenience methods for use in the
 *     very simplest cases, when a developer simply wants to log a
 *     simple string at a given log level.  These methods are named
 *     after the standard Level names ("severe", "warning", "info", etc.)
 *     and take a single argument, a message string.
 * </ul>
 * <p>
 * For the methods that do not take an explicit source name and
 * method name, the Logging framework will make a "best effort"
 * to determine which class and method called into the logging method.
 * However, it is important to realize that this automatically inferred
 * information may only be approximate (or may even be quite wrong!).
 * Virtual machines are allowed to do extensive optimizations when
 * JITing and may entirely remove stack frames, making it impossible
 * to reliably locate the calling class and method.
 * <P>
 * All methods on Logger are multi-thread safe.
 * <p>
 * <b>Subclassing Information:</b> Note that a LogManager class may
 * provide its own implementation of named Loggers for any point in
 * the namespace.  Therefore, any subclasses of Logger (unless they
 * are implemented in conjunction with a new LogManager class) should
 * take care to obtain a Logger instance from the LogManager class and
 * should delegate operations such as "isLoggable" and "log(LogRecord)"
 * to that instance.  Note that in order to intercept all logging
 * output, subclasses need only override the log(LogRecord) method.
 * All the other logging methods are implemented as calls on this
 * log(LogRecord) method.
 *
 * @since 1.4
 */
public class Logger {
    
}

2.1.1、基本日志

要生成简单的日志记录,可以使用全局日志记录器(gloable logger)并调用其info方法

Logger.getGloable().info("File->Open menu item selected")

如果在适当的地方(如main开始)调用

Logger.getGloable().setLevel(Level.OFF)

将会取消所有日志

2.1.2、高级日志

在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器

可以调用getLogger方法创建或获取记录器

private static final Logger mylogger = Logger.getLogger("com.mycompany.myapp");
//未被任何变量引用的日志记录器可能会被垃圾回收。
//为了防止这种情况的发生,要像上面的例子一样,用一个静态变量存储日志记录器的一个引用

与包名类似,日志记录器名也具有层次结构。

事实上,与包名相比,日志记录器的层次性更强

对于包名来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器的父与子之间将共享某些属性。

例如,如果对com.mycompay日志记录器设置了日志级别,它的子记录器也会继承这个级别

2.1.3 日志级别

通常,有以下7个日志记录器级别:

  • SERVER(最高级别)
  • WARNING
  • INFO(默认级别)
  • CONFIG
  • FINE
  • FINER
  • FINEST(最低级别)

还有两个特殊的级别:

  • OFF:用来关闭日志记录

  • ALL:启用所有消息的日志记录

在默认情况下,只记录三个级别,也可以设置其他的级别,例如:

logger.setLevel(Level.FINE)

现在,FINE和更高级别记录都可以记录下来


对于所有的级别由下面几种记录方法;

logger.warning(message);

logger.fine(message);

//同时,还可以使用log方法指定的级别
logger.log(Level.FINE,message)
  • 默认的日志配置记录了INFO或者更高级别的所有记录;因此,应该使用CONFIG,FINE,FINER和FINEST级别来记录那些有助于判断,但对于程序员又没有太大意义的调试信息
  • 如果将记录级别设计为INFO或更低,则需要修改日志处理器的配置。默认的日志处理器不会处理低于INFO级别的信息

记录日志的常见用途是记录那些不可预料的异常;可以使用下面两个方法提供日志记录中包含的一异常描述内容:

 /**
     * Log throwing an exception.
     * <p>
     * This is a convenience method to log that a method is
     * terminating by throwing an exception.  The logging is done
     * using the FINER level.
     * <p>
     * If the logger is currently enabled for the given message
     * level then the given arguments are stored in a LogRecord
     * which is forwarded to all registered output handlers.  The
     * LogRecord's message is set to "THROW".
     * <p>
     * Note that the thrown argument is stored in the LogRecord thrown
     * property, rather than the LogRecord parameters property.  Thus it is
     * processed specially by output Formatters and is not treated
     * as a formatting parameter to the LogRecord message property.
     * <p>
     * @param   sourceClass    name of class that issued the logging request
     * @param   sourceMethod  name of the method.
     * @param   thrown  The Throwable that is being thrown.
     */
    public void throwing(String sourceClass, String sourceMethod, Throwable thrown) {
        if (!isLoggable(Level.FINER)) {
            return;
        }
        LogRecord lr = new LogRecord(Level.FINER, "THROW");
        lr.setSourceClassName(sourceClass);
        lr.setSourceMethodName(sourceMethod);
        lr.setThrown(thrown);
        doLog(lr);
    }

典型用法是:

{
    IOException exception = new IOException(",,,,")
    logger.throwing("com.mycompany.mylib.Reader","read","exception");
    throw exception;
}

2.1.4、修改日志管理器配置

可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件位于:

jre/lib/logging.properties

要想使用另一个配置文件,就要将java.util.logging.config.file特性设置为配置文件的存储位置,并用下列命令启动应用程序:

java -Djava.util.logging.config.file = configFile Mainclass

日志管理器在VM启动过程中初始化,这在main执行之前完成

要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行;

.level=INFO

可以通过添加以下内容来指定自己的日志记录级别

com.mycompany.myapp.level=FINE

日志记录并不总是将信息发送到控制台上,这是处理器的任务。另外处理器也有级别,要想在控制台上看到FINE级别的消息,就需要进行设置

java.util.logging.ConsoleHandler.level = FINE

2.1.5、处理器

抽象类Handler

/**
 * A <tt>Handler</tt> object takes log messages from a <tt>Logger</tt> and
 * exports them.  It might for example, write them to a console
 * or write them to a file, or send them to a network logging service,
 * or forward them to an OS log, or whatever.
 * <p>
 * A <tt>Handler</tt> can be disabled by doing a <tt>setLevel(Level.OFF)</tt>
 * and can  be re-enabled by doing a <tt>setLevel</tt> with an appropriate level.
 * <p>
 * <tt>Handler</tt> classes typically use <tt>LogManager</tt> properties to set
 * default values for the <tt>Handler</tt>'s <tt>Filter</tt>, <tt>Formatter</tt>,
 * and <tt>Level</tt>.  See the specific documentation for each concrete
 * <tt>Handler</tt> class.
 *
 *
 * @since 1.4
 */

public abstract class Handler {
    
}



//StreamHandler
public class StreamHandler extends Handler {}


//ConsoleHandler
public class ConsoleHandler extends StreamHandler {}

//FileHandler
public class FileHandler extends StreamHandler {
    private MeteredStream meter;
    private boolean append;
    private int limit;       // zero => no limit.
    private int count;
    private String pattern;
    private String lockFileName;
    private FileChannel lockFileChannel;
    private File files[];
    private static final int MAX_LOCKS = 100;
    private static final java.util.HashMap<String, String> locks = new java.util.HashMap<>();
}

日志处理器将记录发送到ConsoleHandler中,并由它输出到Sytem.err流中。

特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为" ")有一个ConsoleHandler.

与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为:

java.util.logging.ConsoleHandler.level =INFO

要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别

另外,还可以绕过配置文件,安装自己的处理器:

Logger logger  = Logger.getLogger("com.mycompany.myapp");

logger.setLevel(Level.FINE)
    
logger.setUserParentHanders(false);

Handler handler = new ConsoleHandler();

handler.setLevel(Level.FINE);

logger.addHandler(hanler);

在默认情况下,日志记录器将记录发送到自己的处理器和父处理器。

我们的日志记录器是原始日志记录器(命名为"") 的子类

而原始日志记录器将会把所有等于或高于INFO级别的记录发送到控制台。然而,我们并不想两次看到这些记录。鉴于这个原因,应该将useParentHandlers属性设置为false

要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个很有用的处理器:一个是FileHandler(它可以收集文件中的记录);另一个是SocketHandler(将记录发送到特定的主机和端口)

Logger类中的addHandler方法

  /**
     * Add a log Handler to receive logging messages.
     * <p>
     * By default, Loggers also send their output to their parent logger.
     * Typically the root Logger is configured with a set of Handlers
     * that essentially act as default handlers for all loggers.
     *
     * @param   handler a logging Handler
     * @throws  SecurityException if a security manager exists,
     *          this logger is not anonymous, and the caller
     *          does not have LoggingPermission("control").
     */
    public void addHandler(Handler handler) throws SecurityException {
        // Check for null handler
        handler.getClass();
        checkPermission();
        handlers.add(handler);
    }

可以像下面这样将记录发送到默认的处理器:

FileHandler handler = new FileHandler();
logger.addHandler(handler);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pH3esJ1O-1580562142519)(images/08.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vVW9kQUX-1580562142540)(images/09.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1hoD8NH-1580562142540)(images/10.png)]

2.1.6、过滤器

/**
 * A Filter can be used to provide fine grain control over
 * what is logged, beyond the control provided by log levels.
 * <p>
 * Each Logger and each Handler can have a filter associated with it.
 * The Logger or Handler will call the isLoggable method to check
 * if a given LogRecord should be published.  If isLoggable returns
 * false, the LogRecord will be discarded.
 *
 * @since 1.4
 */
@FunctionalInterface
public interface Filter {

    /**
     * Check if a given log record should be published.
     * @param record  a LogRecord
     * @return true if the log record should be published.
     */
    public boolean isLoggable(LogRecord record);
}

在默认情况下,过滤器根据日志记录的级别进行过滤。

每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的功能。另外,可以通过实现Filter接口并定义下列方法来自定义过滤器

要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了、注意,同一时刻最多只能有一个过滤器

2.1.7、格式化器

/**
 * A Formatter provides support for formatting LogRecords.
 * <p>
 * Typically each logging Handler will have a Formatter associated
 * with it.  The Formatter takes a LogRecord and converts it to
 * a string.
 * <p>
 * Some formatters (such as the XMLFormatter) need to wrap head
 * and tail strings around a set of formatted records. The getHeader
 * and getTail methods can be used to obtain these strings.
 *
 * @since 1.4
 */

public abstract class Formatter {
}

ConsoleHandler类和FileHandler类可以生成文本和Xml格式的日志记录。

但是,也可以自定义格式。这需要扩展Formatter类并覆盖下面这个方法:

String format(LogRecord record)

最后,调用setFormatter方法将格式化器安装到处理器中

2.2、Log4j

2.2.1、日志级别

log4j定义了8个级别的log(除去OFF和ALL,可以说分为6个级别),
优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。

ALL 最低等级的,用于打开所有日志记录。
TRACE 很低的日志级别,一般不会使用。
DEBUG 指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息。
INFO 消息在粗粒度级别上突出强调应用程序的运行过程。这个可以用于生产环境中输出程序运行的一些重要信息。
WARN 表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给开发者的一些提示。
ERROR 指出发生错误的信息,可能会导致系统出错或是宕机等,必须要避免
FATAL 指出每个严重的错误事件将会导致应用程序的退出。这个级别比较高了。重大错误,这种级别你可以直接停止程序了。
OFF 最高等级,用于关闭所有日志记录。

参考

参考

Java日志较为全面的介绍

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值