Spring 練功場#3筆記-IOC加上介紹一點點測試

YungHsin
17 min readAug 31, 2020

--

本次的練功場重點:

(1)複習上次的Basic-3-Layer

(2)練習Update跟Delete資料

(3)IOC範例介紹

https://blog.csdn.net/UmGsoil/article/details/74987807

(4)稍微提一下TDD測試

第一部分就略過,有來練功場的就能再溫習一下上次教得進度!這次就延續前一堂學過的,本來是只有做到進行查詢跟新增使用者資料,這次補齊了修改Update跟刪除Delete資料的動作。

練習Update跟Delete資料

service

/service/UserService.kt

照慣例,先把實作的interface寫進來,分別是modifyUser跟removeById

fun modifyUser(userDto: UserDto): UserDto

fun removeById(id: Long)

/service/impl/RealUserService.kt

(1)fun modifyUser 的動作:將呼叫傳入的使用者資料userDto:UserDto,透過id用finById找出資料庫裡頭的資料User,再將此查到的User的name跟age塞入userDto.name跟userDto.age,最後再用userpository.save(this)進行資料庫的更新。

  • 這裡特別注意: JPA的方式沒有insert/update區分,一律都是用save,所以在update之前要先將資料query出來,取得id之後再寫回資料庫去更新。如果你沒有先給User的id,直接使用save反而會變成再新增一筆User資料了。

(2)fun removeById 的動作:將想要刪除的使用者id,透過UserRepository的deleteById方法,把資料刪掉

override fun modifyUser(userDto: UserDto) =
userRepository.findById(userDto.id).orElseThrow {
RuntimeException()
}.apply {
name = userDto.name
age = userDto.age
userRepository.save(this)
}.run {
UserDto(this.id!!, "${name}", this.age)
}

override fun removeById(id: Long) = userRepository.deleteById(id)

controller

/controller/UserApi.kt

一樣先加interface, modifyUser跟removeById

(1) 變更使用者 fun modifyUser

@PutMapping(“/user”) 表示http request用Put方式進行

(@RequestBody userDto:UserDto ) 表示將以UserDto的資料格式傳入

(2)刪除使用者fun removeById

@DeleteMapping(“/user”) 表示http request用Delete方式進行

(@RequestParm id:Long):UserDto 表示用id傳入參數

interface UserApi{

@PutMapping("/user")
fun modifyUser(@RequestBody userDto: UserDto): UserDto

@DeleteMapping("/user")
fun removeById(@RequestParam id: Long)
}

/controller/impl/UserController.kt

(1) fun modifyUser 的動作:將傳入的使用者資料UserDto,傳遞給UserService,接著再從UserService傳遞給UserRepository達到更新使用者的功能。

(2)fun removeById 的動作:將傳入的查詢使用者id,接著再從UserService傳遞給UserRepository達到刪除使用者的功能

@RestController
class UserController(@Qualifier("RealUserService") val userService: UserService) : UserApi {

override fun modifyUser(userDto: UserDto) = userService.modifyUser(userDto)
override fun removeById(id: Long) = userService.removeById(id)
}

測試執行

一樣使用上次產生的UserRequest.http檔案,更新的http request的寫法如下,我們要把在資料庫的id=1的User資料更新為下列

PUT http://localhost:8080/user
Content-Type: application/json

{
"id": 1,
"name": "update demo",
"age": 777

}

按下綠色執行箭頭,開始執行更新

跑出結果回傳200 OK了!

刪除的寫法如下,我們要刪除id=2的User

DELETE http://localhost:8080/user?id=2

按下綠色執行箭頭,開始執行刪除

跑出結果回傳200 OK,回頭看資料庫,也只剩剛剛更新的一筆資料了!

IOC介紹

這次解說怎麼將我們學到的Basic-3-Layer用applicationContext.xml跟annotation來做到IOC原則。

據說以前都幾乎是用xml去寫這些設定,但是自從Spring一直不斷地演進(黑魔法),現在不太用xml去寫,用annotation一樣能做到一樣的效果。

首先我們先下載這次練功場的sample code:

https://github.com/b2etw/Spring-Boot-Kotlin-Dojo/tree/master/stage3/demo-ioc-spring/src/main/java/org/yfr

假設我們沒有利用application context.xml或是使用annotation建立對應依賴的的bean,在Controller呼叫findName將不會成功,因為沒有透過IOC來幫忙把相依的Service幫我們建立,而且程式也沒指定要使用的Sevice,所以會出錯。而Service2Lower跟Service2Uper也一樣,有相依Repository,所以呼叫findName也會出錯。

applicationContext.xml設定

所以我們回頭看applicationContext.xml裡頭的設定

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd">

<!-- Scan for spring annotated components -->
<context:component-scan base-package="org.yfr"/>
<!-- Process annotations on registered beans like @Autowired... -->
<context:annotation-config/>

<bean id="repositoryQ" class="org.yfr.RepositoryQ"/>
<bean id="repositoryV" class="org.yfr.RepositoryV"/>

<bean id="service2Lower" class="org.yfr.Service2Lower">
<property name="repository" ref="repositoryQ"/>
</bean>
<bean id="service2Upper" class="org.yfr.Service2Upper">
<property name="repository" ref="repositoryV"/>
</bean>

<bean id="controller" class="org.yfr.Controller">
<property name="service" ref="service2Lower"/>
</bean>
</beans>

透過xml檔案的設定來建立RepositoryQ 跟RepositoryV的class

<bean id="repositoryQ" class="org.yfr.RepositoryQ"/>
<bean id="repositoryV" class="org.yfr.RepositoryV"/>

再來是實作的Service2Lower跟service2Upper class我們也要設定起來,而且要設定property name=repository,把裡頭會相依到的repository進行reference,Service2Lower我們會參考repositoryQ而service2Upper會參考repositoryV。

到了最上層的Controller要呼叫service取用功能時,也要設定bean來建立class,然後property name=service,要用的service可以是service2Lower或是service2Upper都行。

<bean id="controller" class="org.yfr.Controller">
<property name="service" ref="service2Lower"/>
</bean>

如果是用service2Lower,執行TestMain時就會把RepositoryQ的findName內容用小寫印出” findname test repositoryq”

試著把controller指定給service2Upper

<bean id="controller" class="org.yfr.Controller">
<pr
operty name="service" ref="service2Upper"/>
</bean>

同理,執行TestMain時就會把RepositoryV的findName內容用大寫印出” FINDNAME TEST REPOSITORY”

這樣的話,改xml設定,就能很快替換直接實作不同的方法了,我想這就是IOC帶來的好處吧,都把new跟依賴的動作都交給框架處理,我們只要專注在實現功能面。

Annotation設定

接下來換用annotation來練習,先把Controller.java 加上@org.springframework.stereotype.Controller跟幫Service加上@Autowired

@org.springframework.stereotype.Controller
public class Controller {

@Autowired
private Service service;

public String findName() {
return service.findName();
}
}

RepositoryQ.java加上@org.springframework.stereotype.Repository

@org.springframework.stereotype.Repository
public class RepositoryQ implements Repository {

@Override
public String findName() {
return "findName Test RepositoryQ";
}
}

RepositoryV.java加上@org.springframework.stereotype.Repository

@org.springframework.stereotype.Repository
public class RepositoryV implements Repository {

@Override
public String findName() {
return "findName Test RepositoryV";
}
}

我們先讓Controller使用Service2Upper讓測試的字印出大寫,所以先在Service2Upper.java加上@Component,然後將原本的命名的repository改成repositoryV,會自動幫我們使用repossitoryV去將findName內容印出(我試過改成其他命名方式就會出錯,看來真的是黑魔法幫我們去找到respositoryV了)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service2Upper implements Service {

@Autowired
private Repository repositoryV;

@Override
public String findName() {
return repositoryV.findName().toUpperCase();
}

}

執行TestMain大寫印出” FINDNAME TEST REPOSITORY

如果要換成小寫印出,就把Service2Upper.java的@Component拿掉,換放在Service2Lower.java

@Component
public class Service2Lower implements Service {

@Autowired
private Repository repositoryQ;

@Override
public String findName() {
return repositoryQ.findName().toLowerCase();
}
}

執行TestMain時小寫印出” findname test repositoryq”

眼尖的就會看到console Log,Spring幫我們定義了controller, repositoryQ, respositryV跟service2Lower

INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@55ca8de8: defining beans [controller,repositoryQ,repositoryV,service2Lower,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy

自己練習下來的感想是,感覺好像寫xml比較快,因為只要設定一個檔案,不用在每個檔案都要寫annotation(難道我是古時代還沒進化嗎…? 或是code寫不夠多!?),結果詢問講者大大,真的是我Code寫不過多,因為測試的sample規模太小,如果專案變大,加上大家的xml的style都不一樣,可能又會變成難維護的災難了,所以才有annotaion,據說之後黑魔法又要把annotation消滅,就看Spring怎麼幫我們進化了XD。

接下來的測試部分從缺…

測試的部份因為沒寫過,我還需要花一些時間來理解,自己練習過才知道怎麽寫,有興趣的可以去講者大大提供的Unit Test範例先練習

https://github.com/b2etw/Spring-Boot-Kotlin-Dojo/tree/master/stage3/demo-unit-test

而且下一次也會再介紹地更深入,就先趁這時間先學TDD一下,會隨時有空更新。

以上就是練功場的第三堂體驗,學會IOC的概念,還有帶領進入測試的世界(只是我還沒踏進去…還在新手村練習)。

歡迎各位也一起來練功場學習Spring囉!

2020–08–2~2020–09–27相約在線上! 隔週一起來練功!

--

--

YungHsin
YungHsin

No responses yet