發(fā)布于:2021-01-25 11:01:19
0
96
0
javaee應(yīng)用服務(wù)器和單片軟件體系結(jié)構(gòu)的時代幾乎一去不復(fù)返了。硬件不再變得更快,但互聯(lián)網(wǎng)流量仍在增加。平臺必須支持向外擴(kuò)展。負(fù)載必須分配到多個主機(jī)。基于微服務(wù)的體系結(jié)構(gòu)可以為這一需求提供解決方案。除了更好的可擴(kuò)展性之外,微服務(wù)還提供了更快的開發(fā)周期、基于負(fù)載的動態(tài)可擴(kuò)展性和改進(jìn)的故障轉(zhuǎn)移行為。
在微服務(wù)體系結(jié)構(gòu)中,只需要擴(kuò)展需要更多資源的部分,而不是復(fù)制一個完整的系統(tǒng)來處理更高的需求。軟件可以解耦,軟件的維護(hù)也越來越容易。每一個不得不在一個單一應(yīng)用程序中更新hibernate版本的開發(fā)人員都知道讓一個單一應(yīng)用程序保持最新和減少技術(shù)債務(wù)的痛苦。使用microservices,您可以一步一步地完成這項(xiàng)工作。隨著服務(wù)數(shù)量的增加,每個開銷都需要最小化。繁重的應(yīng)用服務(wù)器不是用于此目的的工具。Web應(yīng)用程序或服務(wù)可以通過使用嵌入式Web服務(wù)器實(shí)現(xiàn)一段時間。不用安裝完整的JEE概要文件應(yīng)用服務(wù)器,也不用部署EAR或WAR文件,一個簡單的自運(yùn)行jar就可以完成這項(xiàng)工作。這并不新鮮,但為了快速創(chuàng)建此類服務(wù),需要一個模板或框架。這種框架在其他語言中非常突出,但對于Java來說,只有少數(shù)幾種可用的選項(xiàng)。情況變了。在Dropwizard、Play Framework或Spring Boot中,至少有3個框架在Java微服務(wù)世界中大量使用。
在本教程中,我將使用一個簡單的示例來演示如何使用springboot設(shè)置基于REST的springboot微服務(wù)。此示例基于一個服務(wù),該服務(wù)是為某些移動應(yīng)用程序構(gòu)建的后端。本教程中顯示的代碼已簡化。該服務(wù)本身通過使用其他已經(jīng)存在于后臺的服務(wù),為移動應(yīng)用程序提供了restapi。這意味著該服務(wù)僅充當(dāng)其他內(nèi)部服務(wù)的包裝器類型。該服務(wù)需要限制未經(jīng)授權(quán)的訪問,在這種情況下,意味著該服務(wù)需要用戶和客戶端(在這種情況下是移動應(yīng)用程序)的授權(quán)。
這就是我們添加OAuth2和JWT作為授權(quán)系統(tǒng)的原因。我們還為API的文檔添加了Swagger。服務(wù)本身是使用Docker部署到生產(chǎn)環(huán)境的。
如何設(shè)置初始Spring引導(dǎo)結(jié)構(gòu)
springboot是一個旨在簡化新服務(wù)創(chuàng)建的框架。對于最簡單的用例,所需的庫已經(jīng)捆綁在所謂的spring starters中的fitting組合和版本中。我們不必將應(yīng)用程序部署到應(yīng)用程序服務(wù)器中,相反,我們可以獨(dú)立運(yùn)行應(yīng)用程序或在Docker容器中運(yùn)行應(yīng)用程序,因?yàn)閼?yīng)用程序已經(jīng)包含服務(wù)器。
為了創(chuàng)建一個簡單的REST服務(wù),只需要幾行代碼。從一個可用的Spring啟動示例或Spring初始化器開始(http://start.spring.io),我們只需要添加一個javadto和注釋一個控制器,我們就有了第一個端點(diǎn)。
@RestController
public class RegistrationController {
@RequestMapping(method = RequestMethod.POST,
value = "/register",
produces = APPLICATION_JSON_VALUE)
public UserData register(@RequestBody User user) {
...
if(usernameAlreadyExists) {
throw new IllegalArgumentException("error.username");
}
...
return new UserData(...);
}
@ExceptionHandler
void handleIllegalArgumentException(
IllegalArgumentException e,
HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
}
清單1顯示了一個控制器??刂破靼ㄓ糜谧缘姆椒ǎ摲椒捎蒔OST請求觸發(fā)。該方法處理JSON并返回JSON。從JSON到j(luò)avadto的轉(zhuǎn)換對Java開發(fā)人員來說是完全透明的,反之亦然。解析器的配置由springboot處理。彈簧靴支持Maven和Gradle。在Gradle的情況下,bootRun命令將啟動服務(wù)。
...
public class User {
private String mail;
private String password;
private String lastName;
private String name;
private String address;
public Registration() {}
//... getter and setter
}
清單3展示了SpringBoot的另一個重要概念。在springboot中,只要在主類中添加一個簡單的注釋,就可以完成應(yīng)用程序的許多擴(kuò)展。注釋背后的底層基礎(chǔ)結(jié)構(gòu)是隱藏的。這很好,因?yàn)榭梢栽趯?shí)現(xiàn)業(yè)務(wù)邏輯而不是技術(shù)上投入更多的時間,但有時spring引導(dǎo)特性背后的魔力可能會很可怕,調(diào)試意外行為可能需要很多時間。注解@SpringBootApplication足以在嵌入式tomcat中啟動應(yīng)用程序。至少對于小型服務(wù)來說,通過混合使用XML片段、注釋和代碼來設(shè)置應(yīng)用程序上下文的復(fù)雜性已經(jīng)消失了。關(guān)于spring作為一個沉重而復(fù)雜的框架的舊印象已經(jīng)不再突出。從本例中啟動服務(wù)后,POST調(diào)用可以觸發(fā)注冊。清單4顯示了一個簡單的示例。
...
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
curl -X POST -H "Content-Type: application/json"
http://localhost:8080/register -d
'{
"mail": "test@test.de",
"password": "password",
"lastName": "lastName",
"name": "name",
"address": "somewhere"
}'
springboot提供了一種將異常映射到HTTP狀態(tài)碼的簡單方法。這樣我們就可以很容易地保證某種類型的異??偸菍?dǎo)致相同的錯誤代碼。在本例中(清單1),異常處理程序捕獲異常并返回符合HTTP標(biāo)準(zhǔn)的響應(yīng)。參數(shù)錯誤的請求不會導(dǎo)致500錯誤,相反,將返回一個400狀態(tài)代碼,其中包含有用的錯誤ID。清單5顯示了這樣一個響應(yīng)。當(dāng)然,我們可以用同樣的方法實(shí)現(xiàn)GET、PUT、DELETE請求或處理XML而不是JSON。從一個模板、一個現(xiàn)有的示例或初始化器開始,我們可以立即開始編寫代碼。整個基礎(chǔ)設(shè)施,如JAR的打包、HTTP服務(wù)器的啟動、庫的設(shè)置和其他初始化工作都從一開始就得到了解決。新rest服務(wù)的初始設(shè)置需要幾分鐘。
{
"timestamp":1458746952449,
"status":400,
"error":"Bad Request",
"exception":"java.lang.IllegalArgumentException",
"message":"error.username",
"path":"/register"
}
如何保護(hù)restapi
因?yàn)槲覀兊臏y試服務(wù)接受用戶注冊,所以我們必須考慮數(shù)據(jù)的保護(hù)。在用戶可以檢索其數(shù)據(jù)之前,他必須對自己進(jìn)行授權(quán)。在休息服務(wù)的世界里,古典意義上的會話是不存在的。每個呼叫都必須經(jīng)過授權(quán)才能訪問資源。
有幾種做法很常見。通常,服務(wù)受基本身份驗(yàn)證的保護(hù)。Basic Auth要求客戶端在每個請求中發(fā)送用戶名和密碼(Base64編碼為頭信息的一部分)。嗅探器可以利用這些信息來授權(quán)他的通話。即使我們將通過SSL保護(hù)我們的服務(wù),我們也認(rèn)為Basic Auth不是我們服務(wù)的正確方法。另一種方法是使用令牌,它將隨每個請求而更改。成功的請求將返回下一個令牌和響應(yīng)。在這種情況下,并行請求或錯誤很容易導(dǎo)致注銷。這就是為什么我們決定使用OAuth2.0。OAuth2.0僅在初始登錄時使用密碼。OAuth登錄將返回2個令牌。以后的請求必須使用第一個令牌(訪問令牌)執(zhí)行。此令牌在給定的時間段內(nèi)替換密碼。如果有人能夠攔截流量,他可以在該時間段內(nèi)使用該令牌,直到令牌過期。一旦令牌過期,客戶機(jī)就可以使用第二個令牌(刷新令牌)檢索新令牌。
這個概念并不強(qiáng)迫客戶機(jī)一直發(fā)送真正的密碼,并行執(zhí)行調(diào)用也是可行的。即使訪問令牌過期,客戶端也可以通過使用刷新令牌確保用戶永久登錄。發(fā)送給客戶機(jī)的令牌需要持久化,以便將客戶機(jī)發(fā)送的令牌與生成的令牌進(jìn)行比較。在我們的用例中,我們希望去掉任何數(shù)據(jù)庫,因?yàn)檫@個服務(wù)只是包含真正邏輯的其他微服務(wù)的包裝。
為了解決這個問題,我們有三個選擇。我們可以使用內(nèi)存中的數(shù)據(jù)庫,它在多個實(shí)例之間共享。第二種選擇是使用負(fù)載平衡,它總是將來自會話的所有請求發(fā)送到本地(例如內(nèi)存中)存儲令牌的同一實(shí)例。第一種選擇將導(dǎo)致不必要的努力,第二種選擇打破了云本地微服務(wù)架構(gòu)的整體概念,因?yàn)檫@樣我們就不再有無狀態(tài)的應(yīng)用程序了。每次停機(jī)都意味著客戶注銷。所以我們決定用另一種方法。OAuth之上的JWT(jsonwebtokens)擴(kuò)展允許在不存儲令牌的情況下進(jìn)行授權(quán)。訪問令牌不僅僅是隨機(jī)生成的,而是使用私鑰對用戶ID、到期日期和其他元云本機(jī)進(jìn)行簽名,并作為Oauth令牌添加到頭信息中。這樣每個實(shí)例都可以在不存儲令牌的情況下驗(yàn)證令牌的有效性并檢索用戶信息,并且信息是加密的。JWT完全符合OAuth格式,這意味著所有oauth2客戶機(jī)都應(yīng)該能夠使用JWT,即使不知道該令牌是JWT令牌而不是經(jīng)典的oauth2.0令牌。格式保持不變,令牌只是稍微長一點(diǎn)。在我們的例子中,客戶機(jī)不必做任何需要的更改,即使在REST服務(wù)中,所需的自適應(yīng)也是最小的。不是將接收到的令牌與存儲的令牌進(jìn)行比較,而是調(diào)用JWT存儲庫來驗(yàn)證令牌。存儲庫解密令牌,其行為與使用數(shù)據(jù)庫的存儲庫相同。令牌包含用戶ID,但不包含用戶數(shù)據(jù)本身。這意味著用戶數(shù)據(jù)的存儲必須獨(dú)立解決。在我們的例子中,服務(wù)通過rest將用戶數(shù)據(jù)路由到用戶服務(wù)。清單6顯示了將OAuth2.0與JWT結(jié)合使用所需的Spring引導(dǎo)配置。它還顯示了一些額外的配置選項(xiàng)。
@Configuration
public class OAuth2ServerConfiguration {
...
@Bean
public JwtAccessTokenConverter getTokenConverter() {
JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
// for asymmetric signing/verification use
// tokenConverter.setKeyPair(...);
tokenConverter.setSigningKey("aTokenSigningKey");
tokenConverter.setVerifierKey("aTokenSigningKey");
return tokenConverter;
}
...
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
...
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/register/**")
.permitAll()
.antMatchers("/user/**")
.access("#oauth2.hasScope('read')");
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends
AuthorizationServerConfigurerAdapter {
...
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients
.inMemory()
.withClient("aClient")
.authorizedGrantTypes("password", "refresh_token")
.authorities("USER")
.scopes("read", "write")
.resourceIds(RESOURCE_ID)
.secret("aSecret");
}
...
}
}
OAuth2.0支持第三方的授權(quán)。即使在這種情況下未使用此功能,我們也可以將REST服務(wù)的使用限制為某些客戶機(jī)或合作伙伴。在登錄階段,不僅要傳輸用戶的用戶名和密碼,還需要客戶端和客戶端密碼。在我們的案例中,客戶端是不同的應(yīng)用程序。springboot提供了一個簡單的角色和權(quán)限模型。但在本教程中我們將不詳細(xì)介紹。在我們的示例中,注冊是不安全的,但是只有在成功登錄之后才能訪問用戶數(shù)據(jù)。在本例中,登錄是檢索訪問OAuth-2.0令牌的請求。清單7顯示了使用OAuth令牌登錄后的登錄和用戶信息檢索。清單8是OAuth登錄的可能響應(yīng),包括訪問令牌和刷新令牌。為了完成這個示例,清單9顯示了控制器。
curl -vu aClient:aSecret -X POST 'http://localhost:8080/oauth/token?username=test@test.de&password=aPassword&grant_type=password'
curl -i -H "Authorization: Bearer eyJh...Fpao" http://localhost:8080/user
{ "access_token":"eyJh...Fpao",
"token_type":"bearer",
"refresh_token":"eyJh...4clI",
"expires_in":43199,
"scope":"read write",
"jti":"6e0...b31"
}
@RestController
public class UserController {
@RequestMapping(method = RequestMethod.GET,
value = "/user",
produces = APPLICATION_JSON_VALUE)
public UserData getUser() {
Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
String userid = auth.getName();
...
}
}
如何記錄restapi
restapi的可維護(hù)文檔需要盡可能接近代碼,并且在理想情況下,應(yīng)該從代碼生成(或者應(yīng)該從API描述生成代碼)。Swagger是一個功能強(qiáng)大的框架,它包括圍繞API文檔主題的多個工具和庫。例如,工具集的一部分是從API描述生成代碼的工具。
但對于我們的用例來說更重要的是lib,它在運(yùn)行時根據(jù)代碼生成JSON文檔。另一個工具用JSON文檔創(chuàng)建可執(zhí)行的HTML文檔。即使使用默認(rèn)設(shè)置,Swagger庫通常也能提供很好的結(jié)果。為RESTAPI添加可執(zhí)行文檔可以使用單個注釋(@EnableSwagger2)完成。清單10就是一個例子。默認(rèn)情況下,Swagger搜索應(yīng)用程序中所有現(xiàn)有的REST定義。在清單10中,我們還可以看到如何將API文檔限制為現(xiàn)有API的一個子集。在這種情況下,文檔中將只顯示用戶信息和登錄名。此外,我還向文檔中添加了標(biāo)題和版本信息。
@Configuration
@EnableSwagger2
@EnableAutoConfiguration
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.regex("/user.*|/register.*|/oauth/token.*"))
//PathSelectors.any() for all
.build().apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
ApiInfo apiInfo = new ApiInfo(
"aTitle",
"aDescription",
"aVersion",
"a url to terms and services",
"aContact",
"a License of API",
"a license URL");
return apiInfo;
}
}
springboot可以自動使用其他端點(diǎn)來豐富自定義API,例如健康檢查、度量或調(diào)試信息。這些端點(diǎn)將由Swagger自動檢測(如果不受限制)。這是非常強(qiáng)大的,但是您應(yīng)該考慮保護(hù)這些信息(例如,通過將其移動到防火墻后面的其他端口)。
圖1和圖2是清單10中配置的結(jié)果。Swagger幾乎檢測所有端點(diǎn)。OAuth2.0的配置不是在控制器中完成的,而是在配置類中完成的。Swagger無法完全檢測OAuth所需的所有信息。這就是為什么我在清單11中的方法中添加了頭描述。如果沒有這個描述,頭信息將不會顯示在可執(zhí)行的Swagger文檔中(圖2)。圖3顯示了包含頭信息的結(jié)果。通過向靜態(tài)資源的文件夾中添加自定義UI,可以覆蓋Swagger UI。
清單11
@ApiImplicitParams({
@ApiImplicitParam(name = "Authorization",
value = "Bearer access_token",
required = true,
dataType = "string",
paramType = "header"),
})
@RequestMapping(method = RequestMethod.GET,
value = "/user",
produces = APPLICATION_JSON_VALUE)
public User getUser() {
Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
User aUser =
userRepository.getUser(auth.getName());
if(auth != null && aUser != null) {
return aUser;
} else {
throw new IllegalArgumentException("error.username");
}
}
生成的招搖UI 沒有標(biāo)題信息的API文檔 包含標(biāo)題信息的API文檔。
如何將應(yīng)用程序嵌入Docker容器
將自動運(yùn)行的jar嵌入Docker容器很簡單。彈簧靴支持Maven和Gradle。因?yàn)镚radle更精簡、更易于擴(kuò)展、更易于使用和更快,所以我更喜歡Gradle用于我所有的項(xiàng)目。清單12展示了如何使用Gradle Docker插件將Spring引導(dǎo)應(yīng)用程序打包到Docker容器中。Docker容器只需要一個Java運(yùn)行時來運(yùn)行jar。
清單13是一個簡單的Dockerfile。Docker容器基于另一個容器,該容器已經(jīng)包含Java運(yùn)行時,并將我們的應(yīng)用程序添加為Jar。如果您啟動容器,那么應(yīng)用程序?qū)⒈O(jiān)聽端口8080,并且在我們的示例中,相同的端口將公開。清單14展示了如何構(gòu)建和啟動容器。
清單12
...
buildscript {
...
dependencies {
...
classpath 'se.transmode.gradle:gradle-docker:1.2'
}
}
...
apply plugin: 'docker'
group = 'agroup'
...
task buildDocker(type: Docker, dependsOn: build) {
//push = true
applicationName = jar.baseName
println('Application:' + applicationName)
println('Group:' + project.group)
dockerfile = file('Dockerfile')
doFirst {
copy {
from jar
into stageDir
}
}
}
...
FROM frolvlad/alpine-oraclejdk8:latest
MAINTAINER <YOUR MAIL>
EXPOSE 8080
ADD spring-boot-app-service-example.jar /app/spring-boot-app-service-example.jar
ENTRYPOINT java -jar /app/spring-boot-app-service-example.jar --server.port=8080
清單14
$ ./gradlew buildDocker
$ docker run -p 8080:8080 -t agroup/spring-boot-app-service-example
配置
我們已經(jīng)了解了如何建立基于REST的微服務(wù),包括身份驗(yàn)證、文檔以及如何將其嵌入Docker容器。配置Spring引導(dǎo)應(yīng)用程序的最簡單方法是屬性文件(應(yīng)用程序?qū)傩?在應(yīng)用程序的資源文件夾中。您可以在應(yīng)用程序啟動時重寫屬性(如果需要的話),例如通過java-jar這樣的命令示例.jar–spring.config.location=/configuration.屬性。更改Docker容器中應(yīng)用程序的配置可以通過將配置文件裝入容器來完成。這可能是部署自動化的一部分。在bean中使用配置可以通過使用單個注釋來完成。
準(zhǔn)備好應(yīng)用云
將應(yīng)用程序嵌入Docker容器后,可以使用所有可用的Docker工具將應(yīng)用程序部署到云中(例如Kubernetes)。彈簧靴由樞軸驅(qū)動。Pivotal是企業(yè)PAAS解決方案Pivotal Cloud Foundry背后的驅(qū)動程序。SpringBoot應(yīng)用程序可以很容易地集成到云計(jì)算解決方案中,而無需Docker。除了一些元信息,這些應(yīng)用程序可以部署到CloFoundrydry而無需修改。Cloud Foundry可以作為開放源代碼或不同的企業(yè)版本(例如,F(xiàn)oundry)提供,并且可以安裝在您的數(shù)據(jù)中心中。還有公共云代工產(chǎn)品(如Pivotal Web Services和IBM Bluemix)。
結(jié)論
對于大多數(shù)用例,springboot簡化了基于Java的微服務(wù)的構(gòu)建。與Dropwizard等框架不同,它更易于使用,并提供了更豐富的功能集。springboot提供了大量的附加庫和集成,比如Ribbon、Zuul、Hystrix,以及與MongoDB、Redis、GemFire、Elasticsearch、Cassandra或Hazelcast等數(shù)據(jù)庫的集成。
Maven和Gradle為Java開發(fā)人員提供了強(qiáng)大且廣泛支持的構(gòu)建系統(tǒng),與Play框架等框架的專用構(gòu)建系統(tǒng)相比,這些構(gòu)建系統(tǒng)更常見,更容易集成到現(xiàn)有結(jié)構(gòu)中。與經(jīng)典的Web應(yīng)用程序相比,springboot是精簡的。對于大多數(shù)項(xiàng)目,向項(xiàng)目中添加依賴項(xiàng)足以從一開始就獲得良好的結(jié)果,而無需調(diào)整默認(rèn)配置。
但并非所有的一切都是完美的春季開機(jī)生態(tài)系統(tǒng)。如果要調(diào)整庫的設(shè)置,很可能還必須調(diào)整其他庫的設(shè)置。其中一個例子是OAuth的集成。Swagger沒有自動檢測到標(biāo)題信息。為了嵌入Hystrix,您只需添加兩個注釋,依賴項(xiàng)和所有度量都會被自動檢測到。例如,如果您更改了健康檢查的URL,那么您也必須更改Hystrix的配置。調(diào)試隱藏在底層Spring魔術(shù)中的這些問題可能需要時間,但是Spring Boot提供的優(yōu)勢是值得的。
作者介紹