大家好,又见面了,我是你们的朋友全栈君。
前言
本章主要介绍Log4j2的几个例子与使用场景。
- Log4j2 基础样例
- Log4j2 文件时间&文件大小 Appender设置
- Log4j2 日志脱敏 (重写Layout实现)
- Log4j2 日志脱敏 (Layout Replace表达式实现)
本文所用的Demo皆可在我的git项目
https://github.com/SeanYanxml/log4j-demos 内找到。(如果觉得项目写的不错,不妨给我一个star)
Log4j2 Demos
Log4j2 Demos(普通设置)
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug">
<Appenders>
<Console name="Console">
<PatternLayout pattern="%d [%t] %-5p [%c] - %m%n" />
</Console>
<RollingFile name="RollingAppender" fileName="logs/hello.log" filePattern="logs/hello-{yyyy-MM-dd}-%i.log">
<!-- logs/hello-{yyyy-MM-dd HH:mm:ss}-%i.log -->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%d [%t] %-5p [%c] - %m%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<SizeBasedTriggeringPolicy size="100KB" />
</Policies>
<DefaultRolloverStrategy max="99999">
<Delete basePath="logs" maxDepth="2">
<IfFileName glob="hello*.log" />
<!-- <IfLastModified age="2m" /> -->
<IfLastModified age="30d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="com.yanxml" level="DEBUG" additivity="false">
<AppenderRef ref="Console" />
<AppenderRef ref="RollingAppender" />
</Logger>
<Root level="INFO">
<AppenderRef ref="Console" />
<!-- <AppenderRef ref="RollingAppender" /> --> </Root>
</Loggers>
</Configuration>
- 其中
<DefaultRolloverStrategy max="99999">
表示目录可以生成的最大文件个数为99999,默认为7. <delete/>
表示删除文件的策略,上述例子表示保留30天数据.<TimeBasedTriggeringPolicy interval="1" modulate="true" />
表示新日志生成间隔时间。<SizeBasedTriggeringPolicy size="100KB" />
表示文件到达多大生成新日志文件或回滚。
#HelloWroldLog4j2
package com.yanxml.log4j2.demos.origin;
import org.apache.logging.log4j.LogManager;
//import java.util.logging.LogManager;
import org.apache.logging.log4j.Logger;
//import org.apache.logging.log4j.LogManager;
//import org.apache.logging.log4j.Logger;
/** * The first demo to show the log4j2. * Author Sean * Date 20180529 * */
public class HelloWroldLog4j2 {
public static Logger logger = LogManager.getLogger(HelloWroldLog4j2.class);
// Logger logger = LogManager.getLogger(this.getClass().getName());
public static void main(String[] args) {
System.out.println("System Out: HelloWorld!");
// logger.trace("Logger Level: TRACE");
// logger.debug("Logger Level: DEBUG");
// logger.info("Logger Level: INFO");
// logger.warn("Logger Level: WARN");
logger.error("Error1 , error2, Error3");
// logger.error("Logger Level: ERROR 123");
// logger.fatal("Logger Level: FATAL");
}
}
- Log4jTimer
package com.yanxml.log4j2.demos.timer;
import java.util.TimerTask;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jTimer extends TimerTask{
static Logger logger = LogManager.getLogger(Log4jTimer.class);
@Override
public void run() {
logger.info("Info Log ,Time:"+System.currentTimeMillis()+".");
}
}
- Log4jOutTimerDemo
package com.yanxml.log4j2.demos.timer;
import java.util.Timer;
/** * Set the timer to uptdae the xx.log . * * */
public class Log4jOutTimerDemo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.scheduleAtFixedRate(new Log4jTimer(),1,1);
}
}
- log4j.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug">
<Appenders>
<Console name="Console">
<PatternLayout pattern="%d [%t] %-5p [%c] - %m%n" />
</Console>
<RollingFile name="RollingAppender" fileName="logs/hello.log" filePattern="logs/hello-%d{yyyy-MM-dd HH:mm:ss}-%i.log">
<!-- logs/hello-{yyyy-MM-dd HH:mm:ss}-%i.log -->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%d [%t] %-5p [%c] - %m%n" />
<Policies>
<!-- interval的单位 与filePattern内的最小参数有关 -->
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<!-- <SizeBasedTriggeringPolicy size="1KB" /> --> </Policies>
<DefaultRolloverStrategy max="99999">
<Delete basePath="logs" maxDepth="2">
<IfFileName glob="hello*.log" />
<!-- <IfLastModified age="2m" /> -->
<IfLastModified age="30d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="com.yanxml" level="DEBUG" additivity="false">
<AppenderRef ref="Console" />
<AppenderRef ref="RollingAppender" />
</Logger>
<Root level="INFO">
<AppenderRef ref="Console" />
<!-- <AppenderRef ref="RollingAppender" /> --> </Root>
</Loggers>
</Configuration>
PS: 日志生成后会每秒生成一个新的日志文件。(interval根据需要进行设置)
Log4j2 Demos(敏感日志设置)
通过查看log4j2的文档,发现Layout支持Replace功能。
需要注意的是
1. 这Replace只支持内容,不支持e(即catch捕获的eroor直接输出).
2. %replace{%msg}{error|ERROR|Error}{Err**} %n
就不能再设置为%msg %replace{%msg}{error|ERROR|Error}{Err**} %n
,即%msg
不能设置两次,否则替换失效。
3. 此法不能涵盖复杂处理的情况。
<Appenders>
<Console name="Console">
<PatternLayout pattern="%d [%t] %-5p [%c] - %replace{%msg}{error|ERROR|Error}{Err**} %n" />
</Console>
<RollingFile name="RollingAppender" fileName="logs/hello.log" filePattern="logs/hello-{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%d [%t] %-5p [%c] - %replace{%msg}{error|ERROR|Error}{Err**} %n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<SizeBasedTriggeringPolicy size="100KB" />
</Policies>
<DefaultRolloverStrategy max="99999">
<Delete basePath="logs" maxDepth="2">
<IfFileName glob="hello*.log" />
<!-- <IfLastModified age="2m" /> -->
<IfLastModified age="30d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
### Log4j2 Demos(敏感日志设置2 自定义Layout)
- CustomPatternLayout
package com.yanxml.log4j2.demos.sensitive;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.layout.ByteBufferDestination;
import org.apache.logging.log4j.core.layout.Encoder;
import org.apache.logging.log4j.core.layout.PatternSelector;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternFormatter;
import org.apache.logging.log4j.core.pattern.PatternParser;
import org.apache.logging.log4j.core.pattern.RegexReplacement;
import org.apache.logging.log4j.util.Strings;
/** * 定制的输出层,可自由组织要输出的text文本 * */
@Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class CustomPatternLayout extends AbstractStringLayout {
/** * 定制输出文本 * * @param text * @return */
public static String customize(String text) {
return LogUtil.customize(text);
}
/** * Default pattern string for log output. Currently set to the string * <b>"%m%n"</b> which just prints the application supplied message. */
public static final String DEFAULT_CONVERSION_PATTERN = "%m%n";
/** * A conversion pattern equivalent to the TTCCLayout. Current value is <b>%r * [%t] %p %c %notEmpty{%x }- %m%n</b>. */
public static final String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %notEmpty{%x }- %m%n";
/** * A simple pattern. Current value is <b>%d [%t] %p %c - %m%n</b>. */
public static final String SIMPLE_CONVERSION_PATTERN = "%d [%t] %p %c - %m%n";
/** Key to identify pattern converters. */
public static final String KEY = "Converter";
/** * Conversion pattern. */
private final String conversionPattern;
private final PatternSelector patternSelector;
private final Serializer eventSerializer;
/** * Constructs a LayoutTest using the supplied conversion pattern. * * @param config * The Configuration. * @param replace * The regular expression to match. * @param eventPattern * conversion pattern. * @param patternSelector * The PatternSelector. * @param charset * The character set. * @param alwaysWriteExceptions * Whether or not exceptions should always be handled in this * pattern (if {
@code true}, exceptions will be written even if * the pattern does not specify so). * @param noConsoleNoAnsi * If {
@code "true"} (default) and {
@link System#console()} is * null, do not output ANSI escape codes * @param headerPattern * header conversion pattern. * @param footerPattern * footer conversion pattern. */
private CustomPatternLayout(final Configuration config, final RegexReplacement replace,
final String eventPattern, final PatternSelector patternSelector, final Charset charset,
final boolean alwaysWriteExceptions, final boolean noConsoleNoAnsi,
final String headerPattern, final String footerPattern) {
super(config, charset,
createSerializer(config, replace, headerPattern, null, patternSelector,
alwaysWriteExceptions, noConsoleNoAnsi),
createSerializer(config, replace, footerPattern, null, patternSelector,
alwaysWriteExceptions, noConsoleNoAnsi));
this.conversionPattern = eventPattern;
this.patternSelector = patternSelector;
this.eventSerializer = createSerializer(config, replace, eventPattern,
DEFAULT_CONVERSION_PATTERN, patternSelector, alwaysWriteExceptions,
noConsoleNoAnsi);
}
public static Serializer createSerializer(final Configuration configuration,
final RegexReplacement replace, final String pattern, final String defaultPattern,
final PatternSelector patternSelector, final boolean alwaysWriteExceptions,
final boolean noConsoleNoAnsi) {
if (Strings.isEmpty(pattern) && Strings.isEmpty(defaultPattern)) {
return null;
}
if (patternSelector == null) {
try {
final PatternParser parser = createPatternParser(configuration);
final List<PatternFormatter> list = parser.parse(
pattern == null ? defaultPattern : pattern, alwaysWriteExceptions,
noConsoleNoAnsi);
final PatternFormatter[] formatters = list.toArray(new PatternFormatter[0]);
return new PatternSerializer(formatters, replace);
} catch (final RuntimeException ex) {
throw new IllegalArgumentException("Cannot parse pattern '" + pattern + "'", ex);
}
}
return new PatternSelectorSerializer(patternSelector, replace);
}
/** * Gets the conversion pattern. * * @return the conversion pattern. */
public String getConversionPattern() {
return conversionPattern;
}
/** * Gets this LayoutTest's content format. Specified by: * <ul> * <li>Key: "structured" Value: "false"</li> * <li>Key: "formatType" Value: "conversion" (format uses the keywords * supported by OptionConverter)</li> * <li>Key: "format" Value: provided "conversionPattern" param</li> * </ul> * * @return Map of content format keys supporting LayoutTest */
@Override
public Map<String, String> getContentFormat() {
final Map<String, String> result = new HashMap<>();
result.put("structured", "false");
result.put("formatType", "conversion");
result.put("format", conversionPattern);
return result;
}
/** * Formats a logging event to a writer. * * @param event * logging event to be formatted. * @return The event formatted as a String. */
@Override
public String toSerializable(final LogEvent event) {
return eventSerializer.toSerializable(event);
}
@Override
public void encode(final LogEvent event, final ByteBufferDestination destination) {
if (!(eventSerializer instanceof Serializer2)) {
super.encode(event, destination);
return;
}
final StringBuilder text = toText((Serializer2) eventSerializer, event, getStringBuilder());
final Encoder<StringBuilder> encoder = getStringBuilderEncoder();
encoder.encode(text, destination);
trimToMaxSize(text);
}
/** * Creates a text representation of the specified log event and writes it * into the specified StringBuilder. * <p> * Implementations are free to return a new StringBuilder if they can detect * in advance that the specified StringBuilder is too small. */
private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
final StringBuilder destination) {
return serializer.toSerializable(event, destination);
}
/** * Creates a PatternParser. * * @param config * The Configuration. * @return The PatternParser. */
public static PatternParser createPatternParser(final Configuration config) {
if (config == null) {
return new PatternParser(config, KEY, LogEventPatternConverter.class);
}
PatternParser parser = config.getComponent(KEY);
if (parser == null) {
parser = new PatternParser(config, KEY, LogEventPatternConverter.class);
config.addComponent(KEY, parser);
parser = config.getComponent(KEY);
}
return parser;
}
@Override
public String toString() {
return patternSelector == null ? conversionPattern : patternSelector.toString();
}
/** * Creates a pattern layout. * * @param pattern * The pattern. If not specified, defaults to * DEFAULT_CONVERSION_PATTERN. * @param patternSelector * Allows different patterns to be used based on some selection * criteria. * @param config * The Configuration. Some Converters require access to the * Interpolator. * @param replace * A Regex replacement String. * @param charset * The character set. The platform default is used if not * specified. * @param alwaysWriteExceptions * If {
@code "true"} (default) exceptions are always written even * if the pattern contains no exception tokens. * @param noConsoleNoAnsi * If {
@code "true"} (default is false) and * {
@link System#console()} is null, do not output ANSI escape * codes * @param headerPattern * The footer to place at the top of the document, once. * @param footerPattern * The footer to place at the bottom of the document, once. * @return The LayoutTest. */
@PluginFactory
public static CustomPatternLayout createLayout(
@PluginAttribute(value = "pattern", defaultString = DEFAULT_CONVERSION_PATTERN) final String pattern,
@PluginElement("PatternSelector") final PatternSelector patternSelector,
@PluginConfiguration final Configuration config,
@PluginElement("Replace") final RegexReplacement replace,
// LOG4J2-783 use platform default by default, so do not specify
// defaultString for charset
@PluginAttribute(value = "charset") final Charset charset,
@PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) final boolean alwaysWriteExceptions,
@PluginAttribute(value = "noConsoleNoAnsi", defaultBoolean = false) final boolean noConsoleNoAnsi,
@PluginAttribute("header") final String headerPattern,
@PluginAttribute("footer") final String footerPattern) {
return newBuilder().withPattern(pattern).withPatternSelector(patternSelector)
.withConfiguration(config).withRegexReplacement(replace).withCharset(charset)
.withAlwaysWriteExceptions(alwaysWriteExceptions)
.withNoConsoleNoAnsi(noConsoleNoAnsi).withHeader(headerPattern)
.withFooter(footerPattern).build();
}
private static class PatternSerializer implements Serializer, Serializer2 {
private final PatternFormatter[] formatters;
private final RegexReplacement replace;
private PatternSerializer(final PatternFormatter[] formatters,
final RegexReplacement replace) {
super();
this.formatters = formatters;
this.replace = replace;
}
@Override
public String toSerializable(final LogEvent event) {
final StringBuilder sb = getStringBuilder();
try {
return toSerializable(event, sb).toString();
} finally {
trimToMaxSize(sb);
}
}
@Override
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
final int len = formatters.length;
for (int i = 0; i < len; i++) {
formatters[i].format(event, buffer);
}
// 对数据进行脱敏处理
String strCustomize = customize(buffer.toString());
buffer.setLength(0);
buffer.append(strCustomize);
if (replace != null) { // creates temporary objects
String str = buffer.toString();
str = replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append(super.toString());
builder.append("[formatters=");
builder.append(Arrays.toString(formatters));
builder.append(", replace=");
builder.append(replace);
builder.append("]");
return builder.toString();
}
}
private static class PatternSelectorSerializer implements Serializer, Serializer2 {
private final PatternSelector patternSelector;
private final RegexReplacement replace;
private PatternSelectorSerializer(final PatternSelector patternSelector,
final RegexReplacement replace) {
super();
this.patternSelector = patternSelector;
this.replace = replace;
}
public String toSerializable(final LogEvent event) {
final StringBuilder sb = getStringBuilder();
try {
return toSerializable(event, sb).toString();
} finally {
trimToMaxSize(sb);
}
}
@Override
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
final PatternFormatter[] formatters = patternSelector.getFormatters(event);
final int len = formatters.length;
for (int i = 0; i < len; i++) {
formatters[i].format(event, buffer);
}
if (replace != null) { // creates temporary objects
String str = buffer.toString();
str = replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append(super.toString());
builder.append("[patternSelector=");
builder.append(patternSelector);
builder.append(", replace=");
builder.append(replace);
builder.append("]");
return builder.toString();
}
}
/** * Creates a LayoutTest using the default options. These options include * using UTF-8, the default conversion pattern, exceptions being written, * and with ANSI escape codes. * * @return the LayoutTest. * @see #DEFAULT_CONVERSION_PATTERN Default conversion pattern */
public static CustomPatternLayout createDefaultLayout() {
return newBuilder().build();
}
/** * Creates a LayoutTest using the default options and the given * configuration. These options include using UTF-8, the default conversion * pattern, exceptions being written, and with ANSI escape codes. * * @param configuration * The Configuration. * * @return the LayoutTest. * @see #DEFAULT_CONVERSION_PATTERN Default conversion pattern */
public static CustomPatternLayout createDefaultLayout(final Configuration configuration) {
return newBuilder().withConfiguration(configuration).build();
}
/** * Creates a builder for a custom LayoutTest. * * @return a LayoutTest builder. */
@PluginBuilderFactory
public static Builder newBuilder() {
return new Builder();
}
/** * Custom LayoutTest builder. Use the * {
@link CustomPatternLayout#newBuilder() builder factory method} to create * this. */
public static class Builder
implements org.apache.logging.log4j.core.util.Builder<CustomPatternLayout> {
@PluginBuilderAttribute
private String pattern = CustomPatternLayout.DEFAULT_CONVERSION_PATTERN;
@PluginElement("PatternSelector")
private PatternSelector patternSelector;
@PluginConfiguration
private Configuration configuration;
@PluginElement("Replace")
private RegexReplacement regexReplacement;
// LOG4J2-783 use platform default by default
@PluginBuilderAttribute
private Charset charset = Charset.defaultCharset();
@PluginBuilderAttribute
private boolean alwaysWriteExceptions = true;
@PluginBuilderAttribute
private boolean noConsoleNoAnsi;
@PluginBuilderAttribute
private String header;
@PluginBuilderAttribute
private String footer;
private Builder() {
}
// TODO: move javadocs from PluginFactory to here
public Builder withPattern(final String pattern) {
this.pattern = pattern;
return this;
}
public Builder withPatternSelector(final PatternSelector patternSelector) {
this.patternSelector = patternSelector;
return this;
}
public Builder withConfiguration(final Configuration configuration) {
this.configuration = configuration;
return this;
}
public Builder withRegexReplacement(final RegexReplacement regexReplacement) {
this.regexReplacement = regexReplacement;
return this;
}
public Builder withCharset(final Charset charset) {
// LOG4J2-783 if null, use platform default by default
if (charset != null) {
this.charset = charset;
}
return this;
}
public Builder withAlwaysWriteExceptions(final boolean alwaysWriteExceptions) {
this.alwaysWriteExceptions = alwaysWriteExceptions;
return this;
}
public Builder withNoConsoleNoAnsi(final boolean noConsoleNoAnsi) {
this.noConsoleNoAnsi = noConsoleNoAnsi;
return this;
}
public Builder withHeader(final String header) {
this.header = header;
return this;
}
public Builder withFooter(final String footer) {
this.footer = footer;
return this;
}
@Override
public CustomPatternLayout build() {
// fall back to DefaultConfiguration
if (configuration == null) {
configuration = new DefaultConfiguration();
}
return new CustomPatternLayout(configuration, regexReplacement, pattern,
patternSelector, charset, alwaysWriteExceptions, noConsoleNoAnsi, header,
footer);
}
}
}
- LogUtil
package com.yanxml.log4j2.demos.sensitive;
import lombok.extern.log4j.Log4j2;
/**日志工具类 * @author dongp * */
@Log4j2
public class LogUtil {
private static final String REGEX = "[\\d]{8,15}";
private static final String REPLACEMENT = "********";
public static void main(String[] args) throws InterruptedException {
String text = "asdsad13800005001asd";
// String text = "asdsad12345678asd";
log.info(text);
//Thread.sleep(100000);
}
/** * 定制输出文本 可以根据个人需求进行更改。 * * @param text * @return */
public static String customize(String text) {
return text.replaceAll(REGEX, REPLACEMENT);
}
}
//2018-07-27 16:40:14 [main] INFO LogUtil:com.yanxml.log4j2.demos.sensitive.LogUtil.main(LogUtil.java:17) - asdsad********asd
- log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="DEBUG" packages="com.yanxml.log4j2.demos.sensitive">
<properties>
<property name="logPath">log</property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<CustomPatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1}:%l - %msg%n" />
</Console>
<RollingFile name="RollingFile" filename="${logPath}/automation.log" filepattern="${logPath}/%d{yyyyMMddHHmmss}-automation.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1}:%L - %msg%n" />
<Policies>
<SizeBasedTriggeringPolicy size="100 MB" />
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
<!-- <AppenderRef ref="LogFile" /> <AppenderRef ref="RollingFile" /> -->
<!-- <AppenderRef ref="Rewrite" /> -->
</Root>
</Loggers>
</Configuration>
另有更改Log4j2源码包的设置,不推荐。
Reference
[1]. log4j2 java日志脱敏
[2]. 使用log4j2实现日志数据脱敏
[3]. log4j 日志脱敏处理 + java properties文件加载
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/139688.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...