Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
KnowledgePlanet
road-map
xfg-dev-tech-ratelimiter
提交
1003c0f4
xfg-dev-tech-ratelimiter
项目概览
KnowledgePlanet
/
road-map
/
xfg-dev-tech-ratelimiter
通知
178
Star
11
Fork
8
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
xfg-dev-tech-ratelimiter
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
1003c0f4
编写于
12月 06, 2023
作者:
小傅哥
⛹
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 限流
上级
变更
11
隐藏空白更改
内联
并排
Showing
11 changed file
with
506 addition
and
0 deletion
+506
-0
pom.xml
pom.xml
+104
-0
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/Application.java
...p/src/main/java/cn/bugstack/xfg/dev/tech/Application.java
+41
-0
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/annotation/AccessInterceptor.java
...n/bugstack/xfg/dev/tech/annotation/AccessInterceptor.java
+22
-0
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/aop/RateLimiterAOP.java
...ain/java/cn/bugstack/xfg/dev/tech/aop/RateLimiterAOP.java
+164
-0
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/config/RateLimiterAOPConfig.java
...cn/bugstack/xfg/dev/tech/config/RateLimiterAOPConfig.java
+19
-0
xfg-dev-tech-app/src/main/resources/application-dev.yml
xfg-dev-tech-app/src/main/resources/application-dev.yml
+15
-0
xfg-dev-tech-app/src/main/resources/application-prod.yml
xfg-dev-tech-app/src/main/resources/application-prod.yml
+9
-0
xfg-dev-tech-app/src/main/resources/application-test.yml
xfg-dev-tech-app/src/main/resources/application-test.yml
+9
-0
xfg-dev-tech-app/src/main/resources/application.yml
xfg-dev-tech-app/src/main/resources/application.yml
+5
-0
xfg-dev-tech-app/src/main/resources/logback-spring.xml
xfg-dev-tech-app/src/main/resources/logback-spring.xml
+114
-0
xfg-dev-tech-app/src/test/java/cn/bugstack/xfg/dev/test/ApiTest.java
...h-app/src/test/java/cn/bugstack/xfg/dev/test/ApiTest.java
+4
-0
未找到文件。
pom.xml
0 → 100644
浏览文件 @
1003c0f4
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns=
"http://maven.apache.org/POM/4.0.0"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>
4.0.0
</modelVersion>
<groupId>
cn.bugstack
</groupId>
<artifactId>
xfg-dev-tech-ratelimiter
</artifactId>
<version>
1.0-SNAPSHOT
</version>
<packaging>
pom
</packaging>
<modules>
<module>
xfg-dev-tech-app
</module>
</modules>
<properties>
<java.version>
1.8
</java.version>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
<maven.compiler.source>
8
</maven.compiler.source>
<maven.compiler.target>
8
</maven.compiler.target>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
</properties>
<parent>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-parent
</artifactId>
<version>
2.7.12
</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>
com.alibaba
</groupId>
<artifactId>
fastjson
</artifactId>
<version>
2.0.28
</version>
</dependency>
<dependency>
<groupId>
org.apache.commons
</groupId>
<artifactId>
commons-lang3
</artifactId>
<version>
3.9
</version>
</dependency>
<dependency>
<groupId>
com.google.guava
</groupId>
<artifactId>
guava
</artifactId>
<version>
32.1.3-jre
</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>
xfg-dev-tech
</finalName>
<plugins>
<plugin>
<groupId>
org.apache.maven.plugins
</groupId>
<artifactId>
maven-compiler-plugin
</artifactId>
<version>
3.0
</version>
<configuration>
<source>
${java.version}
</source>
<target>
${java.version}
</target>
<encoding>
${project.build.sourceEncoding}
</encoding>
</configuration>
</plugin>
<plugin>
<groupId>
org.apache.maven.plugins
</groupId>
<artifactId>
maven-resources-plugin
</artifactId>
<version>
2.5
</version>
<configuration>
<encoding>
UTF-8
</encoding>
</configuration>
</plugin>
<plugin>
<groupId>
org.codehaus.mojo
</groupId>
<artifactId>
versions-maven-plugin
</artifactId>
<version>
2.7
</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>
dev
</id>
<activation>
<activeByDefault>
true
</activeByDefault>
</activation>
<properties>
<profileActive>
dev
</profileActive>
</properties>
</profile>
<profile>
<id>
test
</id>
<properties>
<profileActive>
test
</profileActive>
</properties>
</profile>
<profile>
<id>
prod
</id>
<properties>
<profileActive>
prod
</profileActive>
</properties>
</profile>
</profiles>
</project>
\ No newline at end of file
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/Application.java
0 → 100644
浏览文件 @
1003c0f4
package
cn.bugstack.xfg.dev.tech
;
import
cn.bugstack.xfg.dev.tech.annotation.AccessInterceptor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Configurable
;
import
org.springframework.boot.SpringApplication
;
import
org.springframework.boot.autoconfigure.SpringBootApplication
;
import
org.springframework.web.bind.annotation.*
;
@Slf4j
@SpringBootApplication
@Configurable
@RestController
()
@RequestMapping
(
"/api/ratelimiter/"
)
public
class
Application
{
public
static
void
main
(
String
[]
args
)
{
SpringApplication
.
run
(
Application
.
class
);
}
/**
* curl http://localhost:8091/api/ratelimiter/login?fingerprint=uljpplllll01009&uId=1000&token=8790
* <p>
* AccessInterceptor
* key: 以用户ID作为拦截,这个用户访问次数限制
* fallbackMethod:失败后的回调方法,方法出入参保持一样
* permitsPerSecond:每秒的访问频次限制
* blacklistCount:超过多少次都被限制了,还访问的,扔到黑名单里24小时
*/
@AccessInterceptor
(
key
=
"fingerprint"
,
fallbackMethod
=
"loginErr"
,
permitsPerSecond
=
1.0d
,
blacklistCount
=
10
)
@RequestMapping
(
value
=
"login"
,
method
=
RequestMethod
.
GET
)
public
String
login
(
String
fingerprint
,
String
uId
,
String
token
)
{
log
.
info
(
"模拟登录 fingerprint:{}"
,
fingerprint
);
return
"模拟登录:登录成功 "
+
uId
;
}
public
String
loginErr
(
String
fingerprint
,
String
uId
,
String
token
)
{
return
"频次限制,请勿恶意访问!"
;
}
}
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/annotation/AccessInterceptor.java
0 → 100644
浏览文件 @
1003c0f4
package
cn.bugstack.xfg.dev.tech.annotation
;
import
java.lang.annotation.*
;
@Documented
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Target
({
ElementType
.
TYPE
,
ElementType
.
METHOD
})
public
@interface
AccessInterceptor
{
/** 用哪个字段作为拦截标识,未配置则默认走全部 */
String
key
()
default
"all"
;
/** 限制频次(每秒请求次数) */
double
permitsPerSecond
();
/** 黑名单拦截(多少次限制后加入黑名单)0 不限制 */
double
blacklistCount
()
default
0
;
/** 拦截后的执行方法 */
String
fallbackMethod
();
}
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/aop/RateLimiterAOP.java
0 → 100644
浏览文件 @
1003c0f4
package
cn.bugstack.xfg.dev.tech.aop
;
import
cn.bugstack.xfg.dev.tech.annotation.AccessInterceptor
;
import
com.google.common.cache.Cache
;
import
com.google.common.cache.CacheBuilder
;
import
com.google.common.util.concurrent.RateLimiter
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang3.StringUtils
;
import
org.aspectj.lang.JoinPoint
;
import
org.aspectj.lang.ProceedingJoinPoint
;
import
org.aspectj.lang.Signature
;
import
org.aspectj.lang.annotation.Around
;
import
org.aspectj.lang.annotation.Aspect
;
import
org.aspectj.lang.annotation.Pointcut
;
import
org.aspectj.lang.reflect.MethodSignature
;
import
java.lang.reflect.Field
;
import
java.lang.reflect.InvocationTargetException
;
import
java.lang.reflect.Method
;
import
java.util.concurrent.TimeUnit
;
@Slf4j
@Aspect
public
class
RateLimiterAOP
{
// 个人限频记录1分钟
private
final
Cache
<
String
,
RateLimiter
>
loginRecord
=
CacheBuilder
.
newBuilder
()
.
expireAfterWrite
(
1
,
TimeUnit
.
MINUTES
)
.
build
();
// 个人限频黑名单24h - 自身的分布式业务场景,可以记录到 Redis 中
private
final
Cache
<
String
,
Long
>
blacklist
=
CacheBuilder
.
newBuilder
()
.
expireAfterWrite
(
24
,
TimeUnit
.
HOURS
)
.
build
();
@Pointcut
(
"@annotation(cn.bugstack.xfg.dev.tech.annotation.AccessInterceptor)"
)
public
void
aopPoint
()
{
}
@Around
(
"aopPoint() && @annotation(accessInterceptor)"
)
public
Object
doRouter
(
ProceedingJoinPoint
jp
,
AccessInterceptor
accessInterceptor
)
throws
Throwable
{
String
key
=
accessInterceptor
.
key
();
if
(
StringUtils
.
isBlank
(
key
))
{
throw
new
RuntimeException
(
"annotation RateLimiter uId is null!"
);
}
// 获取拦截字段
String
keyAttr
=
getAttrValue
(
key
,
jp
.
getArgs
());
log
.
info
(
"aop attr {}"
,
keyAttr
);
// 黑名单拦截
if
(!
"all"
.
equals
(
keyAttr
)
&&
accessInterceptor
.
blacklistCount
()
!=
0
&&
null
!=
blacklist
.
getIfPresent
(
keyAttr
)
&&
blacklist
.
getIfPresent
(
keyAttr
)
>
accessInterceptor
.
blacklistCount
())
{
log
.
info
(
"限流-黑名单拦截(24h):{}"
,
keyAttr
);
return
fallbackMethodResult
(
jp
,
accessInterceptor
.
fallbackMethod
());
}
// 获取限流 -> Guava 缓存1分钟
RateLimiter
rateLimiter
=
loginRecord
.
getIfPresent
(
keyAttr
);
if
(
null
==
rateLimiter
)
{
rateLimiter
=
RateLimiter
.
create
(
accessInterceptor
.
permitsPerSecond
());
loginRecord
.
put
(
keyAttr
,
rateLimiter
);
}
// 限流拦截
if
(!
rateLimiter
.
tryAcquire
())
{
if
(
accessInterceptor
.
blacklistCount
()
!=
0
)
{
if
(
null
==
blacklist
.
getIfPresent
(
keyAttr
))
{
blacklist
.
put
(
keyAttr
,
1L
);
}
else
{
blacklist
.
put
(
keyAttr
,
blacklist
.
getIfPresent
(
keyAttr
)
+
1L
);
}
}
log
.
info
(
"限流-超频次拦截:{}"
,
keyAttr
);
return
fallbackMethodResult
(
jp
,
accessInterceptor
.
fallbackMethod
());
}
// 返回结果
return
jp
.
proceed
();
}
/**
* 调用用户配置的回调方法,当拦截后,返回回调结果。
*/
private
Object
fallbackMethodResult
(
JoinPoint
jp
,
String
fallbackMethod
)
throws
NoSuchMethodException
,
InvocationTargetException
,
IllegalAccessException
{
Signature
sig
=
jp
.
getSignature
();
MethodSignature
methodSignature
=
(
MethodSignature
)
sig
;
Method
method
=
jp
.
getTarget
().
getClass
().
getMethod
(
fallbackMethod
,
methodSignature
.
getParameterTypes
());
return
method
.
invoke
(
jp
.
getThis
(),
jp
.
getArgs
());
}
private
Method
getMethod
(
JoinPoint
jp
)
throws
NoSuchMethodException
{
Signature
sig
=
jp
.
getSignature
();
MethodSignature
methodSignature
=
(
MethodSignature
)
sig
;
return
jp
.
getTarget
().
getClass
().
getMethod
(
methodSignature
.
getName
(),
methodSignature
.
getParameterTypes
());
}
/**
* 实际根据自身业务调整,主要是为了获取通过某个值做拦截
*/
public
String
getAttrValue
(
String
attr
,
Object
[]
args
)
{
if
(
args
[
0
]
instanceof
String
)
{
return
args
[
0
].
toString
();
}
String
filedValue
=
null
;
for
(
Object
arg
:
args
)
{
try
{
if
(
StringUtils
.
isNotBlank
(
filedValue
))
{
break
;
}
// filedValue = BeanUtils.getProperty(arg, attr);
// fix: 使用lombok时,uId这种字段的get方法与idea生成的get方法不同,会导致获取不到属性值,改成反射获取解决
filedValue
=
String
.
valueOf
(
this
.
getValueByName
(
arg
,
attr
));
}
catch
(
Exception
e
)
{
log
.
error
(
"获取路由属性值失败 attr:{}"
,
attr
,
e
);
}
}
return
filedValue
;
}
/**
* 获取对象的特定属性值
*
* @param item 对象
* @param name 属性名
* @return 属性值
* @author tang
*/
private
Object
getValueByName
(
Object
item
,
String
name
)
{
try
{
Field
field
=
getFieldByName
(
item
,
name
);
if
(
field
==
null
)
{
return
null
;
}
field
.
setAccessible
(
true
);
Object
o
=
field
.
get
(
item
);
field
.
setAccessible
(
false
);
return
o
;
}
catch
(
IllegalAccessException
e
)
{
return
null
;
}
}
/**
* 根据名称获取方法,该方法同时兼顾继承类获取父类的属性
*
* @param item 对象
* @param name 属性名
* @return 该属性对应方法
* @author tang
*/
private
Field
getFieldByName
(
Object
item
,
String
name
)
{
try
{
Field
field
;
try
{
field
=
item
.
getClass
().
getDeclaredField
(
name
);
}
catch
(
NoSuchFieldException
e
)
{
field
=
item
.
getClass
().
getSuperclass
().
getDeclaredField
(
name
);
}
return
field
;
}
catch
(
NoSuchFieldException
e
)
{
return
null
;
}
}
}
xfg-dev-tech-app/src/main/java/cn/bugstack/xfg/dev/tech/config/RateLimiterAOPConfig.java
0 → 100644
浏览文件 @
1003c0f4
package
cn.bugstack.xfg.dev.tech.config
;
import
cn.bugstack.xfg.dev.tech.aop.RateLimiterAOP
;
import
com.google.common.cache.Cache
;
import
com.google.common.util.concurrent.RateLimiter
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
@Slf4j
@Configuration
public
class
RateLimiterAOPConfig
{
@Bean
public
RateLimiterAOP
rateLimiter
(){
return
new
RateLimiterAOP
();
}
}
xfg-dev-tech-app/src/main/resources/application-dev.yml
0 → 100644
浏览文件 @
1003c0f4
server
:
port
:
8091
tomcat
:
max-connections
:
20
threads
:
max
:
20
min-spare
:
10
accept-count
:
10
# 日志
logging
:
level
:
root
:
info
config
:
classpath:logback-spring.xml
\ No newline at end of file
xfg-dev-tech-app/src/main/resources/application-prod.yml
0 → 100644
浏览文件 @
1003c0f4
server
:
port
:
8091
# 日志
logging
:
level
:
root
:
info
config
:
classpath:logback-spring.xml
\ No newline at end of file
xfg-dev-tech-app/src/main/resources/application-test.yml
0 → 100644
浏览文件 @
1003c0f4
server
:
port
:
8091
# 日志
logging
:
level
:
root
:
info
config
:
classpath:logback-spring.xml
\ No newline at end of file
xfg-dev-tech-app/src/main/resources/application.yml
0 → 100644
浏览文件 @
1003c0f4
spring
:
config
:
name
:
xfg-dev-tech
profiles
:
active
:
dev
xfg-dev-tech-app/src/main/resources/logback-spring.xml
0 → 100644
浏览文件 @
1003c0f4
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<configuration
scan=
"true"
scanPeriod=
"10 seconds"
>
<contextName>
logback
</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<springProperty
scope=
"context"
name=
"log.path"
source=
"logging.path"
/>
<!-- 日志格式 -->
<conversionRule
conversionWord=
"clr"
converterClass=
"org.springframework.boot.logging.logback.ColorConverter"
/>
<conversionRule
conversionWord=
"wex"
converterClass=
"org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"
/>
<conversionRule
conversionWord=
"wEx"
converterClass=
"org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"
/>
<!-- 输出到控制台 -->
<appender
name=
"CONSOLE"
class=
"ch.qos.logback.core.ConsoleAppender"
>
<!-- 此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息 -->
<filter
class=
"ch.qos.logback.classic.filter.ThresholdFilter"
>
<level>
info
</level>
</filter>
<encoder>
<pattern>
%d{yy-MM-dd.HH:mm:ss.SSS} [%-16t] %-5p %-22c{0}%X{ServiceId} -%X{trace-id} %m%n
</pattern>
<charset>
UTF-8
</charset>
</encoder>
</appender>
<!--输出到文件-->
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender
name=
"INFO_FILE"
class=
"ch.qos.logback.core.rolling.RollingFileAppender"
>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>
./data/log/log_info.log
</file>
<!--日志文件输出格式-->
<encoder>
<pattern>
%d{yy-MM-dd.HH:mm:ss.SSS} [%-16t] %-5p %-22c{0}%X{ServiceId} -%X{trace-id} %m%n
</pattern>
<charset>
UTF-8
</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy
class=
"ch.qos.logback.core.rolling.TimeBasedRollingPolicy"
>
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>
./data/log/log-info-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class=
"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"
>
<maxFileSize>
100MB
</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>
15
</maxHistory>
<totalSizeCap>
10GB
</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender
name=
"ERROR_FILE"
class=
"ch.qos.logback.core.rolling.RollingFileAppender"
>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>
./data/log/log_error.log
</file>
<!--日志文件输出格式-->
<encoder>
<pattern>
%d{yy-MM-dd.HH:mm:ss.SSS} [%-16t] %-5p %-22c{0}%X{ServiceId} -%X{trace-id} %m%n
</pattern>
<charset>
UTF-8
</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy
class=
"ch.qos.logback.core.rolling.TimeBasedRollingPolicy"
>
<fileNamePattern>
./data/log/log-error-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class=
"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"
>
<maxFileSize>
100MB
</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 日志文件保留天数【根据服务器预留,可自行调整】 -->
<maxHistory>
7
</maxHistory>
<totalSizeCap>
5GB
</totalSizeCap>
</rollingPolicy>
<!-- WARN 级别及以上 -->
<filter
class=
"ch.qos.logback.classic.filter.ThresholdFilter"
>
<level>
WARN
</level>
</filter>
</appender>
<!-- 异步输出 -->
<appender
name=
"ASYNC_FILE_INFO"
class=
"ch.qos.logback.classic.AsyncAppender"
>
<!-- 队列剩余容量小于discardingThreshold,则会丢弃TRACT、DEBUG、INFO级别的日志;默认值-1,为queueSize的20%;0不丢失日志 -->
<discardingThreshold>
0
</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>
8192
</queueSize>
<!-- neverBlock:true 会丢失日志,但业务性能不受影响 -->
<neverBlock>
true
</neverBlock>
<!--是否提取调用者数据-->
<includeCallerData>
false
</includeCallerData>
<appender-ref
ref=
"INFO_FILE"
/>
</appender>
<appender
name=
"ASYNC_FILE_ERROR"
class=
"ch.qos.logback.classic.AsyncAppender"
>
<!-- 队列剩余容量小于discardingThreshold,则会丢弃TRACT、DEBUG、INFO级别的日志;默认值-1,为queueSize的20%;0不丢失日志 -->
<discardingThreshold>
0
</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>
1024
</queueSize>
<!-- neverBlock:true 会丢失日志,但业务性能不受影响 -->
<neverBlock>
true
</neverBlock>
<!--是否提取调用者数据-->
<includeCallerData>
false
</includeCallerData>
<appender-ref
ref=
"ERROR_FILE"
/>
</appender>
<!-- 开发环境:控制台打印 -->
<springProfile
name=
"dev"
>
<logger
name=
"com.nmys.view"
level=
"debug"
/>
</springProfile>
<root
level=
"info"
>
<appender-ref
ref=
"CONSOLE"
/>
<!-- 异步日志-INFO -->
<appender-ref
ref=
"ASYNC_FILE_INFO"
/>
<!-- 异步日志-ERROR -->
<appender-ref
ref=
"ASYNC_FILE_ERROR"
/>
</root>
</configuration>
\ No newline at end of file
xfg-dev-tech-app/src/test/java/cn/bugstack/xfg/dev/test/ApiTest.java
0 → 100644
浏览文件 @
1003c0f4
package
cn.bugstack.xfg.dev.test
;
public
class
ApiTest
{
}
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录