目 录CONTENT

文章目录

Junit5技术

FatFish1
2024-11-01 / 0 评论 / 0 点赞 / 65 阅读 / 0 字 / 正在检测是否收录...

引入

Junit5用于构建单元化测试能力,maven依赖如下:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>xxx</version>
    <scope>test</scope>
</dependency>

常用注解

@BeforAll、@AfterAll

  • @BeforeAll:当前的方法需要在当前类下所有用例执行之前执行一次,且被该注解修饰的方法必须为静态方法。

  • @AfterAll:当前的方法需要在当前类下所有用例执行之后执行一次,且被该注解修饰的方法必须为静态方法。

需要注意:这个方法配合MockStatic使用,会使mock对象在测试类间互相影响。

@BeforEach、@AfterEach

  • @BeforeEach:当前的方法需要在每个用例执行之前都执行一次

  • @AfterEach:当前的方法需要在每个用例执行之后都执行一次

@Disabled

表示忽略,被这个注解的测试方法不被执行

@ExtendWith

提供扩展相关能力

参数化

参数化就是尽可能的通过一个用例,多组参数来模拟用户的行为;在使用参数化注解之前需要先用 @parameterizedTest 声明该方法为参数化方法,然后再通过注解提供数据来源。需集成param包。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.9.2</version>
</dependency>

单参数

使用@ValueSource获取参数

@ValueSource(数据类型方法={参数1,参数2…})

@ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    void Test01(int num) {
        System.out.println(num);
    }

从csv获取参数

@CsvFileSource(resources = "____.csv")

这个时候我们需要在 main 文件下 resources 中创建一个 test.csv 文件(.csv 和 参数必须相同):

@ParameterizedTest
    @CsvFileSource(resources = "test.csv")
    void Test03(String name) {
        System.out.println(name);
    }

从方法获取参数

@CsvFileSource@ValueSource 只能传递同种类型的参数,那么我们想要传多种参数,那么可以用方法获取参数。@MethodSource("Generator")

    public static Stream<Arguments> Generator() {
        return Stream.of(Arguments.arguments(1, "张三"),
                Arguments.arguments(2, "李四"),
                Arguments.arguments(3, "王五")
        );
    }
 
    @ParameterizedTest
    @MethodSource("Generator")
    void Test04(int num, String name) {
        System.out.println(num + ":" + name);
    }

多参数

多参数:@CsvSource({“数据组合1”,“数据组合2”…}),每个双引号是一组参数(测试用例)

    //多参数:@CsvSource({“数据组合1”,“数据组合2”…})
    @ParameterizedTest
    @CsvSource({"1, 张三", "2, 李四", "3, 王五"})
    void manyTest(int num, String name) {
        System.out.println("num:" + num + ", name:" + name);
    }

用例执行顺序

  • 顺序执行:@TestMethodOrder(MethodOrderer.OrderAnnotation.class),然后再每次执行的时候添加 @order(int)

  • 随机执行:@TestMethodOrder(MethodOrderer.class)

Assertions - 断言

org.junit.jupiter.api.Assertions提供断言能力,常用包括:

  • 断言匹配/不匹配:assertEquals()assertNotEquals()

  • 断言结果为真/为假:assertTrue()assertFalse()

  • 断言结果为空/非空:assertNull()assertNotNull()

  • 断言抛出异常:assertThrows()

测试套件

Mockito与打桩

提供成员方法和静态方法的mock能力,它的作用就是模拟一个可以满足测试的假想对象

maven依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>${org.mockito.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>${org.mockito.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>${org.mockito.version}</version>
    <scope>test</scope>
</dependency>

设置测试类依托mockito管理

Mock 需要先设置测试类被 Mockito 管理,有两种方式:

  • 在spring boot项目中,必须在测试类上添加注解Junit5以下使用 @RunWith(MockitoJUnitRunner.class),Junit5使用@ExtendWith(MockitoExtension.class)

  • @RunWith(MockitoJUnitRunner.Silent.class)标签可以用于排除一些cleanCode错误时使用

  • @Before方法中,调用 MockitoAnnotations.initMocks(this)

mock类替身对象技术

对类构造替身对象

对类构造替身对象有如下几种方式:

  • 在测试用例中直接使用 Mockito.mock(Class) 方法,生成替身对象

  • 在测试类中声明需生成替身的依赖类,使用 @Mock 注解。

  • 将替身注入到被测试类。注入可以使用 @InjectMocks 注解标识需要被注入的类。依赖的其他类 就用@Mock 注解标识,Mockito 自动将替身注入到被测试类。

第二种方式是该测试类中所有单元测试唯一的对象,为了防止每个单元测试 stubbing 的先后顺序对其他单元测试的影响,最好写一个 @After 方法,在该方法中使用 Mockito.reset() 方法清空 stubbing 规则

stubbing - 模拟替身对象的行为

最常用的一种方式,就是设置某种方法根据某个入参的返回值是什么,或者设置抛出什么异常:

  • 设置返回对象方式一:when().thenReturn();

Mockito.when(mock.action(Mockito.anyMap())).thenReturn(mock);
  • 设置返回对象方式二:doReturn().when(object).method();

  • 设置抛出异常:when().thenThrow();

  • void方法什么都不做执行空逻辑:Mockito.doNothing().when(对象).方法

  • mock多次返回

@Test
public void testMockReturnMultiple2() {
    Mockito.when(list.size()).thenReturn(1).thenReturn(2).thenReturn(3).thenReturn(4);

    assertThat(list.size(), equalTo(1));
    assertThat(list.size(), equalTo(2));
    assertThat(list.size(), equalTo(3));
    assertThat(list.size(), equalTo(4));
}

mock类的替身对象案例

@ExtendWith(MockitoExtension.class)
public class SchoolTest {
    // 使用第二种方式
    @Mock
	 Student student;

    @InjectMocks
    SchoolClass schoolClass;

    @BeforeEach
    public void setUp() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        MockitoAnnotations.openMocks(SchoolTest.class);       
    }

    @Test
    public void test1() {
        // 使用第一种方式
        Teacher teacher = Mockito.mock(Teacher.class);
        Mockito.when(teacher.teach(student)).thenReturn("I'm teaching");
        Mockito.doReturn(“XiaoMing”).when(student).getNameByNo(any());
        List<String> nameList = schoolClass.listNameByClassNo(“No1”);
        Assertions.assertEquals(10, nameList.size());
    }
}

mock静态方法技术

private MockedStatic<StaticClass> beanFactoryMockedStatic;

beanFactoryMockedStatic = Mockito.mockStatic(StaticClass.class);
beanFactoryMockedStatic.when(() -> StaticClass.staticMethod(入参)).thenReturn(返回值);

可以用any()代表传入任何参数

静态类mock完后必须要关闭,否则会持续留在内存中影响后续测试

    @AfterEach
    public void after() {
        beanFactoryMockedStatic.close();
    }

通用参数匹配增强

  • Matchers.any():匹配任意对象,带着 any 名称的方法 还有 anyInt,anyObject 等都是一样的作用。

  • Matchers.eq():when(list.get(eq(0)) 跟 when(list.get(0) 没区别。提供这个主要是可以在通用参数匹配中,指定特殊的具体value匹配。

  • Matchers.isA():表示匹配参数为任意某个类及其子类的对象。

  • anyBoolean():用于匹配布尔类型,anyMap()用于匹配任意map

值得注意的事使用通用匹配的话 需要都用通用匹配函数,如果想指定某个参数是特殊值 用eq() 包装一下

案例:

@ExtendWith(MockitoExtension.class)
public class SchoolTest {
    @Mock
	 Student student;

    @InjectMocks
    SchoolClass schoolClass;

    @BeforeEach
    public void setUp() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        MockitoAnnotations.openMocks(SchoolTest.class);       
    }

    @Test
    public void test1() {
        Mockito.doReturn(“XiaoMing”).when(student).getNameByNo(any());
        List<String> nameList = schoolClass.listNameByClassNo(“No1”);
        Assertions.assertEquals(10, nameList.size());
    }
}

上面的案例中,假设SchoolClass中有内部类Student,SchoolClass有方法listNameByClassNo,要测试这个方法的逻辑,内部依赖school对象调用getNameByNo方法,因此mock内部类Student对象,并注入SchoolClass对象中,同时对student的getNameByNo方法进行方法mock。

更优雅的断言 - HamcrestMatchers

  • equalTo() 实际值等于期望对象

  • not()不如何如何 可以组装 equalTo() 例如 not(equalTo()) 表示实际值不等于期望对象

  • is()equalTo()一样

  • greaterThan() 实际值大于期望对象

  • lessThan() 实际值小于期望对象

  • either().or() 或关系,满足其一即可

  • both().and() 且关系,两个条件都得满足

  • anyOf() 满足其中之一即可,相当于IN

  • allOf() 所有都满足 相当于一堆 AND

跨线程mock技术

Mockito进行跨线程mock存在难点,其原理是:MockStatic是通过一个Map管理的,每个线程构造一个对象,负责的管理自己的Map,因此在主线程Mock好的,子线程感知不到

基于这个原理,有两种跨线程mock的方案:

  • mock线程池,让其中的线程在执行前首先进行MockStatic,再执行原有方法

  • 直接获取到子线程,将主线程的ThreadLocal中的mockMap传递给子线程

假设有一个mock,需要,mock在主线程中执行,但是方法执行的时候是在new ThreadPoolExecutor().submit()中:

public static void mockStart() {
    MockedStatic<ServiceSwitchUtil> ServiceSwitchUtilMock = Mockito.mockStatic(ServiceSwitchUtil.class);
    ServiceSwitchUtilMock.when(ServiceSwitchUtil::getSwitches).thenReturn(ImmutableMap.of(……));
}

方案一

  • 首先提供一个mock线程池的方法,思路就是对任意线程返回一个指定的Answer

public static ThreadPoolExecutor mockExecutor(Answer<Runnable> answer) {
    ThreadPoolExecutor mockExecutor = Mockito.mock(ThreadPoolExecutor.class);
    Mockito.lenient().doAnswer(answer).when(mockExecutor).submit(any(Runnable.class));
    Mockito.lenient().doAnswer(answer).when(mockExecutor).execute(any(Runnable.class));
    return mockExecutor;
}
  • 选择特定的线程,对线程进行mock

// 首先构造一个Answer对象,这个对象的思路是先执行mock,然后执行线程自己的runnable方法
Answer<Runnable> answer = (invocation) -> {
    // 先取出这个线程原本的runnable可执行对象
    final Runnable runnable = invocation.getArgument(0);
    // 构造一个新的runnable
    Runnable wrapperRunnable = () -> {
        // 先执行mock
        mockStart();
        MockUtils.mockSftpFileSystem();
        // 再执行线程中原本的runnable
        runnable.run();
  
    };
    // 构造一个新线程并执行
    Thread thread = new Thread(wrapperRunnable, "junit-mock-consumer-thread-"+count.incrementAndGet());
    thread.start();
    return null;
};
// 将构造的Answer作为参数mock到线程池中,并用mock好的线程池替换主线程构造线程池的时候构造的变量
ThreadPoolExecutor mockExecutor = MockUtils.mockExecutor(answer);
MockUtils.setField(consumerScheduler, "pool", mockExecutor);

方案二

  • 启动线程,执行mock,完成新线程的mock配置,同时通过线程组获取所有线程,定义一个match方法从所有线程中找到需要mock的子线程,执行mockSubThread方法

new Thread(() -> {
    // 执行mock
    mockStart();
    
    // 获取线程组
    ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
    int count = threadGroup.activeCount();
    Thread[] threads = new Thread[count];
    threadGroup.enumerate(threads);
    for (Thread thread : threads) {
        if (match(thread)) {
            mockSubThread(thread)
        }
    }
  • 完成ThreadLocal的传递,这里先在当前线程中取出了InlineByteBuddyMockMaker中配置好的mapMap,然后通过pushTo方法把它推送到了子线程中,从而完成了ThreadLocal的传递

Field field = MockUtil.class.getDeclaredField("mockMaker");
field.setAccessible(true);
InlineByteBuddyMockMaker inlineByteBuddyMockMaker = (InlineByteBuddyMockMaker) field.get(MockUtil.class);
Field inlineDelegateByteBuddyMockMaker = inlineByteBuddyMockMaker.getClass().getDeclaredField("inlineDelegateByteBuddyMockMaker");
inlineDelegateByteBuddyMockMaker.setAccessible(true);
InlineDelegateByteBuddyMockMaker inlineDelegateByteBuddyMockMaker2 = (InlineDelegateByteBuddyMockMaker) inlineDelegateByteBuddyMockMaker.get(inlin

Mockito源码分析

成员方法mock源码思路分析

……

静态类mock源码思路分析

MockedStatic<ServiceSwitchUtil> ServiceSwitchUtilMock = Mockito.mockStatic(ServiceSwitchUtil.class);
ServiceSwitchUtilMock.when(ServiceSwitchUtil::getSwitches).thenReturn(ImmutableMap.of(……));

通过这段示例代码,对一个服务的开关工具类进行了Mock,从而实现调用ServiceSwitchUtil::getSwitches方法,获得一个自定义的开关列表

分析这段逻辑,首先是构建Mock的Mockito#mockStatic方法,然后是.when().thenReturn()方法

Mockito

核心成员变量

// mockitoCore,执行Mock的真正核心代码
static final MockitoCore MOCKITO_CORE = new MockitoCore();
// 如果 mock 没有存根,则每个 mock 的默认值Answer。通常它只返回一些空值
public static final Answer<Object> RETURNS_DEFAULTS = Answers.RETURNS_DEFAULTS;

mockStatic

return mockStatic(classToMock, withSettings());

简单看下withSettings()方法

return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);

其实是给定一个默认返回

关键内容在下层的mockStatic()

return MOCKITO_CORE.mockStatic(classToMock, mockSettings);

委托给MockitoCore#mockStatic执行

MockitoCore

mockStatic - 核心骨架方法

MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
// 构造一些mockStatic的默认配置,包括mock名等
MockCreationSettings<T> creationSettings = impl.buildStatic(classToMock);
// 构造静态mock
MockMaker.StaticMockControl<T> control = createStaticMock(classToMock, creationSettings);
// 向构造的静态mock中添加代理执行器
control.enable();
mockingProgress().mockingStarted(classToMock, creationSettings);
// 封装返回
return new MockedStaticImpl<>(control);

静态mock的核心框架方法,经历以下几个核心步骤,补充跳转

  1. impl.buildStatic:构造mockstatic的默认配置

  2. createStaticMock:构造静态mock,跳转

  3. enbable:激活静态mock,跳转

其中MockSettings  的实现类 MockSettingsImplCreationSettings  承载着相关配置信息,比如要为待 mock 的对象生成怎样的返回值

MockUtil

核心成员变量

// mock构造器
private static final MockMaker mockMaker = Plugins.getMockMaker();

跟进下看看MockMaker取的是哪里,找到PluginRegistry#mockMaker

private final MockMaker mockMaker =
        new PluginLoader(
                            pluginSwitch,
                            DefaultMockitoPlugins.INLINE_ALIAS,
                            DefaultMockitoPlugins.PROXY_ALIAS)
                .loadPlugin(MockMaker.class);

这里可以看到默认的Plugin是一个map存放的

DEFAULT_PLUGINS.put(
        INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker");

INLINE_ALIAS对应的是InlineByteBuddyMockMaker这个类

createStaticMock

MockHandler<T> handler = createMockHandler(settings);
return mockMaker.createStaticMock(type, settings, handler);

这里是两个步骤,首先构造MockHandler(Mock对象的代理),然后通过MockMaker启动静态mock

这里MockMaker

createMockHandler

createMockHandler是通过MockHandlerFactory方法返回handler对象

MockHandler<T> handler = new MockHandlerImpl<T>(settings);
MockHandler<T> nullResultGuardian = new NullResultGuardian<T>(handler);
return new InvocationNotifierHandler<T>(nullResultGuardian, settings);

MockHandler接口为Mock类提供代理执行的能力,InvocationNotifierHandler是MockHandler接口实现类的封装

MockHandlerImpl - mock代理对象核心

handle

handle方法是匹配mock和方法并执行的核心,比较复杂

if (invocationContainer.hasAnswersForStubbing()) {
    // stubbing voids with doThrow() or doAnswer() style
    InvocationMatcher invocationMatcher =
            matchersBinder.bindMatchers(
                    mockingProgress().getArgumentMatcherStorage(), invocation);
    invocationContainer.setMethodForStubbing(invocationMatcher);
    return null;
}

首先判断如果stubbing是doAnswer或doThrow两种,优先执行

// 获取验证模式
VerificationMode verificationMode = mockingProgress().pullVerificationMode();
// 获取调用匹配器对象
InvocationMatcher invocationMatcher =
        matchersBinder.bindMatchers(
                mockingProgress().getArgumentMatcherStorage(), invocation);
mockingProgress().validateState();
// if verificationMode is not null then someone is doing verify()
if (verificationMode != null) {
  ……
}

这一部分如果需要verify,在if中执行校验方法

// prepare invocation for stubbing
invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
mockingProgress().reportOngoingStubbing(ongoingStubbing);

构造OngoingStubbingImpl对象,这里可以看到handle每次调用都会创建新的对象存在mockingProgress上下文中,取用流程可以参考后面MockedStaticImpl#when方法

StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);
……
if (stubbing != null) {
  ……
  try {
    return stubbing.answer(invocation);
} else {
    // 如果打桩对象为空,也就是mock对象的默认行为,由于mock对象的每一个操作都是“无效”的,因此都会返回一个相应类型的默认值(stub除外) 
    Object ret = mockSettings.getDefaultAnswer().answer(invocation);
    DefaultAnswerValidator.validateReturnValueFor(invocation, ret);
    invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
    return ret;
}

为当前拦截的调用对象查找存在的返回对象answer,执行返回对应的stubbing,如果大壮对象为空,则返回默认值

InlineDelegateByteBuddyMockMaker - MockMaker接口实现类

核心成员变量

private final DetachedThreadLocal<Map<Class<?>, MockMethodInterceptor>> mockedStatics =
        new DetachedThreadLocal<>(DetachedThreadLocal.Cleaner.INLINE);

Mockito包下定义的一个ThreadLocal,标志着mock是线程唯一的,这里导致了MockStatic不能在一个线程中多次执行

createStaticMock

if (type == ConcurrentHashMap.class) {
    throw new MockitoException(
            ……
} else if (type == Thread.class
        || type == System.class
        || type == Arrays.class
        || ClassLoader.class.isAssignableFrom(type)) {
    throw new MockitoException(
            ……
}

首先可以看到对于ConcurrentHashMap的Mock、对Thread、System、Arrays的mock会报错

bytecodeGenerator.mockClassStatic(type);

基于ByteCodeGenerator生成代理

Map<Class<?>, MockMethodInterceptor> interceptors = mockedStatics.get();
if (interceptors == null) {
    interceptors = new WeakHashMap<>();
    mockedStatics.set(interceptors);
}

可以看到构造静态mock实际上是向DetachedThreadLocal中设置值

return new InlineStaticMockControl<>(type, interceptors, settings, handler);

构造一个封装了mock代理类的封装并返回,这里可以看到,每个线程返回各自的封装类,存放各自的Mock库,因此决定了名每个mock静态类只能mock一次

InlineStaticMockControl - 静态mock封装

构造函数

private InlineStaticMockControl(
        Class<T> type,
        Map<Class<?>, MockMethodInterceptor> interceptors,
        MockCreationSettings<T> settings,
        MockHandler handler) {
    this.type = type;
    this.interceptors = interceptors;
    this.settings = settings;
    this.handler = handler;
}

用interceptors变量存储传入的map;用handler对象承接传入的

enable - 激活mock

if (interceptors.putIfAbsent(type, new MockMethodInterceptor(handler, settings))
        != null) {
    throw new MockitoException(
            join(
                    "For "
                            + type.getName()
                            + ", static mocking is already registered in the current thread",
                    "",
                    "To create a new mock, the existing static mock registration must be deregistered"));
}

可以看到,如果一个线程多次mock一个静态类,即这里多次执行putIfAbsent,会导致抛出异常,同时输出这段日志

disable - 注销mock

if (interceptors.remove(type) == null) {
    throw new MockitoException(
            join(
                    "Could not deregister "
                            + type.getName()
                            + " as a static mock since it is not currently registered",
                    "",
                    "To register a static mock, use Mockito.mockStatic("
                            + type.getSimpleName()
                            + ".class)"));
}

对于在一个线程中用完的静态类,执行close方法,最终调用到这里,实际执行的是map的remove方法

MockStatic模拟stubbing源码分析

ServiceSwitchUtilMock.when(ServiceSwitchUtil::getSwitches).thenReturn(ImmutableMap.of(……));

回顾上面的案例,一个静态mockstubbing的执行实际上就是.when().thenReturn()的流程

MockedStaticImpl

when

// 初始化MockProgressImpl对象
MockingProgress mockingProgress = mockingProgress();
// 标记stub开始
mockingProgress.stubbingStarted();
// 获取最新的ongoingStubbing对象
@SuppressWarnings("unchecked")
OngoingStubbing<S> stubbing = (OngoingStubbing<S>) mockingProgress.pullOngoingStubbing();
if (stubbing == null) {
    mockingProgress.reset();
    throw missingMethodInvocation();
}
// 返回ongoingStubbing对象
return stubbing;

其中OngoingStubbing对象会在每次MockHandlerImpl调用handle方法时创建一个,然后set到ThreadLocal的mockingProgress中,所以这里取出来的就是上一次的调用,这里也证明了其实when的参数是没用的,只要mock对象有方法调用就可以了。因此,when方法就是返回上次mock方法调用封装好的OngoingStubbing

BaseStubbing

thenReturn

通过链式展示thenRetrun方法中调用的顺序

    // OngoingStubbing#thenReturn
    @Override
    public OngoingStubbing<T> thenReturn(T value) {
        return thenAnswer(new Returns(value));
    }

封装Returns,向下调用

OngoingStubbingImpl#thenAnswer

    @Override
    public OngoingStubbing<T> thenAnswer(Answer<?> answer) {
        if(!invocationContainer.hasInvocationForPotentialStubbing()) {
            throw incorrectUseOfApi();
        }
        //把这个answer加入到invocationContainer对象中保存
        invocationContainer.addAnswer(answer);
        return new ConsecutiveStubbing<T>(invocationContainer);
    }    

通过invocationContainer保存answer

InvocationContainerImpl#addAnswer

    public StubbedInvocationMatcher addAnswer(Answer answer, boolean isConsecutive) {
        // 1.获取stub的调用
        Invocation invocation = invocationForStubbing.getInvocation();
        // 2.标记stub完成
        mockingProgress().stubbingCompleted();
        if (answer instanceof ValidableAnswer) {
            ((ValidableAnswer) answer).validateFor(invocation);
        }
        // 3.把stub的调用和answer加到StubbedInvocationMatcher的list
        // 这里的意思就是把调用和返回绑定,如果下次调用匹配到了,就返回对应的answer
        synchronized (stubbed) {
            if (isConsecutive) {
                stubbed.getFirst().addAnswer(answer);
            } else {
                stubbed.addFirst(new StubbedInvocationMatcher(invocationForStubbing, answer));
            }
            return stubbed.getFirst();
        }
    }

完成answer的添加,这里在InvocationContainerImpl中可以看出来stubbed是一个LinkedList

private final LinkedList<StubbedInvocationMatcher> stubbed = new LinkedList<>();

0

评论区