gossip 服務(三)發現


在〈gossip 服務(二)拆分〉中,已經分出了 Mail、Account 與 Message 服務,實際上,組態伺服器也是個服務,若想要在運用這些服務時有伸縮的彈性,可以建立服務註冊伺服器,將這些服務註冊上去,需要服務的就到服務註冊伺服器上查找服務,也就是接下來,打算採取底下的架構:

gossip 服務(三)發現

服務註冊伺服器就使用〈服務註冊伺服器〉的成果就可以了,因為只是範例,就不特別像〈服務可用性〉中談到的,特別還要建立多個服務註冊伺服器並彼此複製註冊表了。

因為組態伺服器也打算註冊為服務,因此首先要處理的,就是先修改 configsvibootstrap.properties

server.port=8888
spring.application.name=configsvr
spring.cloud.config.server.encrypt.enabled=false

eureka.client.fetchRegistry=false
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

eureka.instance.preferIpAddress=true

Mail、Account 與 Message 服務,基本上也要加上註冊伺服器的設定,以 acctsvibootstrap.properties 為例:

server.port=8084
spring.application.name=acctsvi
spring.profiles.active=default

# spring.cloud.config.uri=http://localhost:8888
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=configsvr

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

eureka.instance.preferIpAddress=true

因為現在要從服務註冊伺服器上取得組態伺服器的資訊,記得使用 spring.cloud.config.discovery 相關設定,而不是 spring.cloud.config.uri

當然,別忘了 build.gradle 上都得加上必要的相依。接下來,gossipbootstrap.properties 加上服務註冊伺服器的設定,以便查找服務:

spring.application.name=gossip
spring.profiles.active=default

eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

然後,AccountServiceEmailService 以及 MessageService 的實作,URI 的部份就可以改成各自對應的服務名稱了。

不過,AccountServiceEmailService 以及 MessageService 的實作,幾乎都像是樣版般的程式碼,這就想到了〈RestTemplate 與 Ribbon/Feign〉中談到,可以使用 Feign 來宣告服務的請求。

因此,可以在 build.gradle 中加入必要的相依,然後,GossipApplication 加註 @EnableFeignClients,因為現在身為 Feign 客戶端的 AccountServiceEmailService 以及 MessageService,不在同一個套件之中,記得使用 basePackages 來指定它們的位置:

...略

@EnableFeignClients(
    basePackages={
        "cc.openhome.model"
    }
)
@PropertySource("classpath:path.properties")
public class GossipApplication {
    ...略
}

然後,就可以分別進行宣告了,以 AccountService 為例:

package cc.openhome.model;

...

@FeignClient("acctsvi")
public interface AccountService {
    @PostMapping("tryCreateUser?email={email}&username={username}&password={password}")
    Resource<Account> tryCreateUser(@PathVariable("email") String email, @PathVariable("username") String username, @PathVariable("password") String password);

    @GetMapping("userExisted?username={username}")
    boolean userExisted(@PathVariable("username") String username);

    @PutMapping("verify?email={email}&token={token}")
    Resource<Account> verify(@PathVariable("email") String email, @PathVariable("token") String token);

    @GetMapping("accountByNameEmail?username={username}&email={email}")
    Resource<Account> accountByNameEmail(@PathVariable("username") String name, @PathVariable("email") String email);

    @PutMapping("resetPassword?username={username}&password={password}")
    void resetPassword(@PathVariable("username") String name, @PathVariable("password") String password);

    @GetMapping("accountByName?username={username}")
    Optional<Account> accountByName(@PathVariable("username") String username);
}

注意,這邊改變了介面上的方法簽署,以符合 Feign 的轉換規則,這就與 Account 服務上頭的 AccountService 不同了,這並不會怎樣,因為 Account 服務已經是個獨立的服務了,重點在於 REST 介面定義好就可以了。

在〈gossip 服務(二)拆分〉談過,Resource<Optional<Account>> 可以被轉換為 JSON,根據該 JSON 格式,服務的客戶端可以用 Resource<Account> 來取得,因此上面看到的 AccountService 中,傳回值是 Resource<Account>,如果 JSON 本身沒有 Account 相關的屬性,那 getContent 時會是 null

因為修改了 AccountService 的方法簽署,相關控制器中也要做出修改,並且可以使用 Optional.ofNullable 來銜接。例如 AccountController

package cc.openhome.controller;

...略

@Controller
@SessionAttributes("token")
public class AccountController {

    ...略

    @PostMapping("register")
    public String register(
            @Valid RegisterForm form,
            BindingResult bindingResult,
            Model model) {

        List<String> errors = toList(bindingResult);

        String path;
        if(errors.isEmpty()) {
            path = REGISTER_SUCCESS_PATH;

            Optional<Account> optionalAcct = Optional.ofNullable(accountService.tryCreateUser(
                    form.getEmail(), form.getUsername(), form.getPassword()).getContent());

            if(optionalAcct.isPresent()) {
                emailService.validationLink(optionalAcct.get());
            } else {
                emailService.failedRegistration(
                    form.getUsername(), form.getEmail());
            }
        } else {
            path = REGISTER_FORM_PATH;
            model.addAttribute("errors", errors);
        }

        return path;
    }

    ...略
}

你可以在 GossipSvi/3rd 中,找到全部修改完成的範例專案。