使用mockito編寫(xiě)測(cè)試用例教程
前言
首先聲明筆者并不是一個(gè)TDD開(kāi)發(fā)的擁躉,我傾向于實(shí)用主義。TDD有他的用武之地,但不是銀彈,同理BDD(行為驅(qū)動(dòng)開(kāi)發(fā))也是如此。筆者堅(jiān)持寫(xiě)測(cè)試的原因只是為了更加方便的重構(gòu),當(dāng)筆者意識(shí)到一個(gè)模塊會(huì)被大范圍使用且會(huì)面臨多次迭代的時(shí)候,筆者就會(huì)認(rèn)證寫(xiě)測(cè)試,并且每次出bug都會(huì)用測(cè)試先復(fù)現(xiàn)。如果一個(gè)項(xiàng)目只是一次性的demo那些個(gè)啥的測(cè)試,一把梭哈得了。
什么是TDD
TDD是測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test-Driven Development)的英文簡(jiǎn)稱,是敏捷開(kāi)發(fā)中的一項(xiàng)核心實(shí)踐和技術(shù),也是一種設(shè)計(jì)方法論。TDD的原理是在開(kāi)發(fā)功能代碼之前,先編寫(xiě)單元測(cè)試用例代碼,測(cè)試代碼確定需要編寫(xiě)什么產(chǎn)品代碼。TDD雖是敏捷方法的核心實(shí)踐,但不只適用于XP(Extreme Programming),同樣可以適用于其他開(kāi)發(fā)方法和過(guò)程。
為什么要使用mockito
Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.
Mockito 是非常不錯(cuò)框架。它使您可以使用干凈簡(jiǎn)單的 API 編寫(xiě)漂亮的測(cè)試。 Mockito 不會(huì)給你帶來(lái)宿醉,因?yàn)闇y(cè)試非常易讀并且會(huì)產(chǎn)生干凈的驗(yàn)證錯(cuò)誤
在開(kāi)發(fā)程序的時(shí)候我們需要測(cè)試的類不可能都是簡(jiǎn)單的類,很多復(fù)雜的邏輯往往需要多個(gè)前置條件才能測(cè)試到。如果為了測(cè)試去滿足這些前置條件未免顯得太繁瑣。比如有的邏輯需要進(jìn)行HTTP請(qǐng)求某個(gè)特定服務(wù),我不想在測(cè)試的時(shí)候去單獨(dú)啟動(dòng)這個(gè)服務(wù)。這時(shí)候我們就可以mock這個(gè)http請(qǐng)求,讓其返回一個(gè)特定值,以此簡(jiǎn)化測(cè)試流程。
如何使用mockito
前期準(zhǔn)備
引包
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency>
靜態(tài)導(dǎo)入
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.mockito.Mockito.*;
包裝你要模擬的類
使用mock方法
LinkedList mockedList = mock(LinkedList.class);
使用spy方法
PDStateMachineNode mock = spy(new PDStateMachineNode());
mock和spy的區(qū)別
mock方法和spy方法都可以對(duì)對(duì)象進(jìn)行mock。但是前者是接管了對(duì)象的全部方法,而后者只是將有樁實(shí)現(xiàn)(stubbing)的調(diào)用進(jìn)行mock,其余方法仍然是實(shí)際調(diào)用。大家先這樣理解后面會(huì)具體舉例子。
什么插樁
其實(shí)就是對(duì)要模擬方法進(jìn)行包裝,設(shè)定返回值或者拋異常之類,直接看官方例子。這里就是對(duì)get(0) 和get(1) 進(jìn)行插樁了
//You can mock concrete classes, not just interfaces LinkedList mockedList = mock(LinkedList.class); //插樁 when(mockedList.get(0)).thenReturn("first"); when(mockedList.get(1)).thenThrow(new RuntimeException()); //following prints "first" System.out.println(mockedList.get(0)); //following throws runtime exception System.out.println(mockedList.get(1)); //following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999)); //Although it is possible to verify a stubbed invocation, usually it's just redundant //If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed). //If your code doesn't care what get(0) returns, then it should not be stubbed. verify(mockedList).get(0);
驗(yàn)證行為
可以用來(lái)驗(yàn)證某個(gè)方法是否調(diào)用了
這里使用了官網(wǎng)例子,大致用途就是你可以用它來(lái)驗(yàn)證某個(gè)方法被調(diào)用了沒(méi)有??梢院苊黠@在的看到他在驗(yàn)證是否調(diào)用了add和clear
//Let's import Mockito statically so that the code looks clearer import static org.mockito.Mockito.*; //mock creation List mockedList = mock(List.class); //using mock object mockedList.add("one"); mockedList.clear(); //verification verify(mockedList).add("one"); verify(mockedList).clear();
下面看看我自己項(xiàng)目里面得例子。這里可以發(fā)現(xiàn)我在傳入?yún)?shù)得時(shí)候直接是傳入了any(),這也是mockito
提供得,與此對(duì)應(yīng)得還有anyString()
,anyInt()
等等。
大致意思就是驗(yàn)證runAviatorScript這個(gè)方法被調(diào)用的了應(yīng)該有一次。
@Test void testOnEntry2() throws NodeExecuteTimeoutException { PDStateMachineNode mock = spy(PDStateMachineNode.class); mock.onEntry(any()); // 默認(rèn)第二個(gè)參數(shù)就是times(1),因此這里可以不寫(xiě) verify(mock,times(1)).runAviatorScript(any(),any(),anyString()); }
參數(shù)匹配
方便得模擬一次方法得參數(shù)
直接上官方例子,上述代碼也有使用
//stubbing using built-in anyInt() argument matcher when(mockedList.get(anyInt())).thenReturn("element"); //stubbing using custom matcher (let's say isValid() returns your own matcher implementation): when(mockedList.contains(argThat(isValid()))).thenReturn(true); //following prints "element" System.out.println(mockedList.get(999)); //you can also verify using an argument matcher verify(mockedList).get(anyInt()); //argument matchers can also be written as Java 8 Lambdas verify(mockedList).add(argThat(someString -> someString.length() > 5));
如果你使用的參數(shù)匹配器,那么所有參數(shù)都必須提供參數(shù)匹配器,否則會(huì)拋異常。
//正確 verify(mock).someMethod(anyInt(), anyString(), eq("third argument")); //錯(cuò)誤 verify(mock).someMethod(anyInt(), anyString(), "third argument");
驗(yàn)證調(diào)用次數(shù)
就是驗(yàn)證某個(gè)方法調(diào)用了多少次
//using mock mockedList.add("once"); mockedList.add("twice"); mockedList.add("twice"); mockedList.add("three times"); mockedList.add("three times"); mockedList.add("three times"); //following two verifications work exactly the same - times(1) is used by default verify(mockedList).add("once"); verify(mockedList, times(1)).add("once"); //exact number of invocations verification verify(mockedList, times(2)).add("twice"); verify(mockedList, times(3)).add("three times"); //verification using never(). never() is an alias to times(0) verify(mockedList, never()).add("never happened"); //verification using atLeast()/atMost() verify(mockedList, atMostOnce()).add("once"); verify(mockedList, atLeastOnce()).add("three times"); verify(mockedList, atLeast(2)).add("three times"); verify(mockedList, atMost(5)).add("three times");
筆者項(xiàng)目中得例子
@Test void testDELETE() { Assertions.assertTimeout(Duration.ofSeconds(10), () -> { mockStatic.when(() -> HttpRequest.delete("test").execute().body()).thenReturn("success"); String execute = (String) AviatorEvaluator.execute("return DELETE("test");"); Assertions.assertTrue(execute.contains("success")); //在這里 mockStatic.verify(() -> HttpRequest.delete(anyString()), times(2)); }); }
模擬void方法
項(xiàng)目中的例子
當(dāng)調(diào)用onExit的時(shí)候啥也不干
@Test void onExit() throws NodeExecuteTimeoutException { StateMachineInterpreter interpreter = new StateMachineInterpreter(); StateMachineNode spy = spy(new StateMachineNode()); //啥也不干 doNothing().when(spy).onExit(any()); }
驗(yàn)證調(diào)用順序
有時(shí)候在需要驗(yàn)證某個(gè)方法內(nèi)部調(diào)用其他方法的順序。筆者例子如下:
這段測(cè)試意思就是模擬 PDStateMachineNode
這個(gè)類調(diào)用verify()方法得邏輯。分兩種情況
onCheck方法返回true(doReturn(true).when(mock).onCheck(any())
)
此種情況下,方法調(diào)用順序應(yīng)為:onEntry,onCheck,onMatch,onExit
onCheck返回false(doReturn(false).when(mock).onCheck(any())
)
此種情況下,方法調(diào)用順序應(yīng)為:onEntry,onCheck,onFail,onExit
PDStateMachineNode mock = spy(new PDStateMachineNode()); doReturn(true).when(mock).onCheck(any()); mock.verify(any()); InOrder inOrder = inOrder(mock); inOrder.verify(mock).onEntry(any()); inOrder.verify(mock).onCheck(any()); inOrder.verify(mock).onMatch(any()); inOrder.verify(mock).onExit(any()); doReturn(false).when(mock).onCheck(any()); mock.verify(any()); InOrder inOrder2 = inOrder(mock); inOrder2.verify(mock).onEntry(any()); inOrder2.verify(mock).onCheck(any()); inOrder2.verify(mock).onFail(any()); inOrder2.verify(mock).onExit(any());
doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod()
官方建議大部分情況下你應(yīng)該使用when(),二者區(qū)別后文再說(shuō)。
doReturn()
List list = new LinkedList(); List spy = spy(list); //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty) when(spy.get(0)).thenReturn("foo", "bar", "qix"); //You have to use doReturn() for stubbing: doReturn("foo", "bar", "qix").when(spy).get(0);
List list = new LinkedList(); List spy = spy(list); //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty) when(spy.get(0)).thenReturn("foo", "bar", "qix"); //You have to use doReturn() for stubbing: doReturn("foo", "bar", "qix").when(spy).get(0);
doThrow()
doThrow(new RuntimeException()).when(mock).someVoidMethod(); doThrow(RuntimeException.class).when(mock).someVoidMethod();
doAnswer()
doAnswer(new Answer() { public Object answer(InvocationOnMock invocation) { Object[] args = invocation.getArguments(); Mock mock = invocation.getMock(); return null; }}) .when(mock).someMethod();
doNothing()
doNothing(). doThrow(new RuntimeException()) .when(mock).someVoidMethod(); //does nothing the first time: mock.someVoidMethod(); //throws RuntimeException the next time: mock.someVoidMethod();
doCallRealMethod()
Foo mock = mock(Foo.class); doCallRealMethod().when(mock).someVoidMethod(); // this will call the real implementation of Foo.someVoidMethod() mock.someVoidMethod();
筆者項(xiàng)目中的使用的例子
@Test void step() throws NodeExecuteTimeoutException, NextNodesNotExistException { PDStateMachineNode spy = spy(new PDStateMachineNode()); PDStateMachineNode node = new PDStateMachineNode(); PDStateMachineNode subSpy = spy(node); doReturn(true).when(subSpy).verify(any()); doReturn(List.of(subSpy)).when(spy).getNextNodes(); PDStateMachineNode step = spy.step(any(PointData.class)); Assertions.assertEquals(subSpy, step); when(spy.getNextNodes()).thenReturn(new ArrayList<>()); doReturn(true).when(spy).isTerminalNode(); Assertions.assertThrows(NextNodesNotExistException.class, () -> spy.step(any(PointData.class))); doReturn(new ArrayList<>()).when(spy).getNextNodes(); doReturn(false).when(spy).isTerminalNode(); Assertions.assertEquals(spy, spy.step(any(PointData.class))); }
靜態(tài)方法模擬
靜態(tài)有返回值且存在鏈?zhǔn)秸{(diào)用
直接看筆者項(xiàng)目中的例子,注意這里下面的例子有些許不同,RETURNS_DEEP_STUBS其實(shí)是用來(lái)進(jìn)行嵌套模擬的。因?yàn)?strong>HttpRequest.get("test").execute().body() 是一個(gè)鏈?zhǔn)降恼{(diào)用,實(shí)際上涉及了多個(gè)類的模擬。如果你沒(méi)有這個(gè)需求就可以不加。
@BeforeAll void init() { mockStatic = mockStatic(HttpRequest.class, RETURNS_DEEP_STUBS); } @Test void testGET() { Assertions.assertTimeout(Duration.ofSeconds(10), () -> { mockStatic.when(() -> HttpRequest.get("test").execute().body()).thenReturn("success"); String execute = (String) AviatorEvaluator.execute("return GET("test");"); Assertions.assertTrue(execute.contains("success")); mockStatic.verify(() -> HttpRequest.get(anyString()), times(2)); }); }
靜態(tài)無(wú)返回值
someMock.when(() -> Files.delete(fileToDelete)).thenAnswer((Answer<Void>) invocation -> null); // 也可以是下面這個(gè) // someMock.when(() -> Files.delete(fileToDelete)).thenAnswer(Answers.RETURNS_DEFAULTS);
進(jìn)階
mock和spy的區(qū)別
mock方法和spy方法都可以對(duì)對(duì)象進(jìn)行mock。但是前者是接管了對(duì)象的全部方法,而后者只是將有樁實(shí)現(xiàn)(stubbing)的調(diào)用進(jìn)行mock,其余方法仍然是實(shí)際調(diào)用。
使用mock
PDStateMachineNode mock = mock(PDStateMachineNode.class); mock.onEntry(any()); verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
拋錯(cuò)如下,意思就是runAviatorScript沒(méi)有被調(diào)用,因此驗(yàn)證失敗。實(shí)際上筆者在onEntry內(nèi)部是調(diào)用了runAviatorScript方法的
Wanted but not invoked: pDStateMachineNode.runAviatorScript( <any>, <any>, <any string> ); -> at core.state.GenericStateMachineNode.runAviatorScript(GenericStateMachineNode.java:78)
使用spy,則無(wú)任務(wù)錯(cuò)誤。
@Test void testOnEntry2() throws NodeExecuteTimeoutException { PDStateMachineNode mock = spy(PDStateMachineNode.class); mock.onEntry(any()); verify(mock, times(1)).runAviatorScript(any(), any(), anyString()); }
從上述對(duì)比就可以理解mock和spy的區(qū)別,對(duì)于未指定mock的方法,spy默認(rèn)會(huì)調(diào)用真實(shí)的方法,有返回值的返回真實(shí)的返回值,而mock默認(rèn)不執(zhí)行,有返回值的,默認(rèn)返回null。具體細(xì)節(jié)筆者也沒(méi)有深究比如實(shí)際上mock也能做到類似psy的效果
when(...).thenReturn(...)和doReturn(...).when(...)的區(qū)別
● when(...) thenReturn(...)會(huì)調(diào)用真實(shí)的方法,如果你不想調(diào)用真實(shí)的方法而是想要mock的話,就不要使用這個(gè)方法。
● doReturn(...) when(...) 不會(huì)調(diào)用真實(shí)方法
因此針對(duì)區(qū)別一般情況下如果時(shí)第三方庫(kù)得代碼在需要測(cè)試得方法則可以使用 do...return
進(jìn)行略過(guò),自己調(diào)用自己得方法則建議使用 when...return
。但是有時(shí)候調(diào)用得方法需要一些特殊得環(huán)境才能起作用,那么也能使用 do..return
,亦或者被調(diào)用得方法已經(jīng)測(cè)試過(guò)了也可以使用 do..return
。下面看二者區(qū)別得例子。
例子:
@Override public void onEntry(T event) throws NodeExecuteTimeoutException { System.out.println("hello"); this.runAviatorScript(this.onEntry, event, "onEntry"); }
測(cè)試when..return...:
@Test void testOnEntry2() throws NodeExecuteTimeoutException { PDStateMachineNode mock = spy(PDStateMachineNode.class); when(mock.onCheck(any())).thenReturn(true); mock.onEntry(any()); verify(mock, times(1)).runAviatorScript(any(), any(), anyString()); }
結(jié)果可以看到輸出得hello
測(cè)試do...return...
@Test void testOnEntry2() throws NodeExecuteTimeoutException { PDStateMachineNode mock = spy(PDStateMachineNode.class); doNothing().when(mock).onEntry(any()); mock.onEntry(any()); verify(mock, times(1)).runAviatorScript(any(), any(), anyString()); }
結(jié)果可以看到不僅沒(méi)輸出還報(bào)錯(cuò)了,為什么呢?因?yàn)?do..return
實(shí)際上不執(zhí)行包裝得方法,也就沒(méi)有執(zhí)行onEntry方法,自然里面 runAviatorScript
也就沒(méi)有執(zhí)行,因此就會(huì)導(dǎo)致驗(yàn)證錯(cuò)誤。
BDDMockito(行為驅(qū)動(dòng)測(cè)試)
什么是BDD
行為驅(qū)動(dòng)開(kāi)發(fā)(英語(yǔ):Behavior-driven development,縮寫(xiě)BDD)是一種敏捷軟件開(kāi)發(fā)的技術(shù),它鼓勵(lì)軟件項(xiàng)目中的開(kāi)發(fā)者、QA和非技術(shù)人員或商業(yè)參與者之間的協(xié)作。BDD最初是由Dan North在2003年命名,它包括驗(yàn)收測(cè)試和客戶測(cè)試驅(qū)動(dòng)等的極限編程的實(shí)踐,作為對(duì)測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的回應(yīng)。在過(guò)去數(shù)年里,它得到了很大的發(fā)展。行為驅(qū)動(dòng)測(cè)試的開(kāi)發(fā)風(fēng)格使用//given//when//then 作為測(cè)試方法的基本部分。
其實(shí)還是比較簡(jiǎn)單的,粗淺的理解就是換了幾個(gè)API。
舉個(gè)例子
直接看幾個(gè)官方的例子
import static org.mockito.BDDMockito.*; Seller seller = mock(Seller.class); Shop shop = new Shop(seller); public void shouldBuyBread() throws Exception { //given given(seller.askForBread()).willReturn(new Bread()); //when Goods goods = shop.buyBread(); //then assertThat(goods, containBread()); }
如何模擬異常
可以發(fā)現(xiàn)willThrow就像之前的doThrow差不多
//given willThrow(new RuntimeException("boo")).given(mock).foo(); //when Result result = systemUnderTest.perform(); //then assertEquals(failure, result);
驗(yàn)證調(diào)用次數(shù)
person.ride(bike); person.ride(bike); then(person).should(times(2)).ride(bike); then(person).shouldHaveNoMoreInteractions(); then(police).shouldHaveZeroInteractions();
驗(yàn)證調(diào)用順序
InOrder inOrder = inOrder(person); person.drive(car); person.ride(bike); person.ride(bike); then(person).should(inOrder).drive(car); then(person).should(inOrder, times(2)).ride(bike);
實(shí)戰(zhàn)中使用
這里不僅模擬了方法的返回值,還模擬了springboot中controller的調(diào)用
@Test void shouldNotListRoles() throws Exception { given(roleService.findAll()).willReturn(new ArrayList<>()); ResultActions actions = this.mvc.perform(get("/api/role/getRoles")); actions.andExpect(status().isOk()).andReturn().getResponse().setCharacterEncoding("UTF-8"); actions.andDo(print()).andExpect(jsonPath("$.data.length()").value(Matchers.is(0))); } @Test void shouldCreateRole() throws Exception { objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS); Role role = new Role(null, "mfine", LocalDateTime.now(), "", 0, null, "admin"); // 這里 也使用了參數(shù)匹配器 given(roleService.insertSelective(BDDMockito.any())).willReturn(1); ResultActions actions = this.mvc.perform(post("/api/role/createRole").content(objectMapper.writeValueAsString(role)) .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON)); actions.andExpect(status().isOk()).andReturn().getResponse().setCharacterEncoding("UTF-8"); actions.andExpect(ResultMatcher.matchAll(result -> { Assert.assertTrue(result.getResponse().getContentAsString().contains("success")); })); }
總結(jié)
完整的測(cè)試時(shí)重構(gòu)得底氣,當(dāng)沒(méi)有測(cè)試得時(shí)候一切重構(gòu)都是扯淡。程序出bug之后第一件事應(yīng)該是復(fù)現(xiàn)bug,增補(bǔ)測(cè)試然后再是修復(fù)bug,如果是線上緊急情況那也應(yīng)該在時(shí)候補(bǔ)充測(cè)試。但是測(cè)試也不是銀彈,所謂的TDD和BDD也要看情況使用,沒(méi)有萬(wàn)能方案只有適合方法。
以上就是使用mockito編寫(xiě)測(cè)試用例教程的詳細(xì)內(nèi)容,更多關(guān)于mockito測(cè)試用例教程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中ShardingSphere 數(shù)據(jù)分片的實(shí)現(xiàn)
其實(shí)很多人對(duì)分庫(kù)分表多少都有點(diǎn)恐懼,我們今天用ShardingSphere 給大家演示數(shù)據(jù)分片,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09Springboot項(xiàng)目基于Devtools實(shí)現(xiàn)熱部署步驟詳解
這篇文章主要介紹了Springboot項(xiàng)目基于Devtools實(shí)現(xiàn)熱部署,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06SpringBoot2.6.x默認(rèn)禁用循環(huán)依賴后的問(wèn)題解決
由于SpringBoot從底層逐漸引導(dǎo)開(kāi)發(fā)者書(shū)寫(xiě)規(guī)范的代碼,同時(shí)也是個(gè)憂傷的消息,循環(huán)依賴的應(yīng)用場(chǎng)景實(shí)在是太廣泛了,所以SpringBoot 2.6.x不推薦使用循環(huán)依賴,本文給大家說(shuō)下SpringBoot2.6.x默認(rèn)禁用循環(huán)依賴后的應(yīng)對(duì)策略,感興趣的朋友一起看看吧2022-02-02Java后端SSM框架圖片上傳功能實(shí)現(xiàn)方法解析
這篇文章主要介紹了Java后端SSM框架圖片上傳功能實(shí)現(xiàn)方法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06Spring Validator接口校驗(yàn)與全局異常處理器
這篇文章主要介紹了Spring Validator接口校驗(yàn)與全局異常處理器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11java實(shí)現(xiàn)導(dǎo)出Excel的功能
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)導(dǎo)出Excel的功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05關(guān)于yml文件字符串,List,Map的書(shū)寫(xiě)方式并使用@ConfigurationProperties注入配置類
這篇文章主要介紹了關(guān)于yml文件字符串,List,Map的書(shū)寫(xiě)方式并使用@ConfigurationProperties注入配置類,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12