發(fā)布于:2021-01-25 11:01:19
0
96
0
javaee應用服務器和單片軟件體系結構的時代幾乎一去不復返了。硬件不再變得更快,但互聯(lián)網(wǎng)流量仍在增加。平臺必須支持向外擴展。負載必須分配到多個主機?;谖⒎盏捏w系結構可以為這一需求提供解決方案。除了更好的可擴展性之外,微服務還提供了更快的開發(fā)周期、基于負載的動態(tài)可擴展性和改進的故障轉移行為。
在微服務體系結構中,只需要擴展需要更多資源的部分,而不是復制一個完整的系統(tǒng)來處理更高的需求。軟件可以解耦,軟件的維護也越來越容易。每一個不得不在一個單一應用程序中更新hibernate版本的開發(fā)人員都知道讓一個單一應用程序保持最新和減少技術債務的痛苦。使用microservices,您可以一步一步地完成這項工作。隨著服務數(shù)量的增加,每個開銷都需要最小化。繁重的應用服務器不是用于此目的的工具。Web應用程序或服務可以通過使用嵌入式Web服務器實現(xiàn)一段時間。不用安裝完整的JEE概要文件應用服務器,也不用部署EAR或WAR文件,一個簡單的自運行jar就可以完成這項工作。這并不新鮮,但為了快速創(chuàng)建此類服務,需要一個模板或框架。這種框架在其他語言中非常突出,但對于Java來說,只有少數(shù)幾種可用的選項。情況變了。在Dropwizard、Play Framework或Spring Boot中,至少有3個框架在Java微服務世界中大量使用。
在本教程中,我將使用一個簡單的示例來演示如何使用springboot設置基于REST的springboot微服務。此示例基于一個服務,該服務是為某些移動應用程序構建的后端。本教程中顯示的代碼已簡化。該服務本身通過使用其他已經(jīng)存在于后臺的服務,為移動應用程序提供了restapi。這意味著該服務僅充當其他內(nèi)部服務的包裝器類型。該服務需要限制未經(jīng)授權的訪問,在這種情況下,意味著該服務需要用戶和客戶端(在這種情況下是移動應用程序)的授權。
這就是我們添加OAuth2和JWT作為授權系統(tǒng)的原因。我們還為API的文檔添加了Swagger。服務本身是使用Docker部署到生產(chǎn)環(huán)境的。
如何設置初始Spring引導結構
springboot是一個旨在簡化新服務創(chuàng)建的框架。對于最簡單的用例,所需的庫已經(jīng)捆綁在所謂的spring starters中的fitting組合和版本中。我們不必將應用程序部署到應用程序服務器中,相反,我們可以獨立運行應用程序或在Docker容器中運行應用程序,因為應用程序已經(jīng)包含服務器。
為了創(chuàng)建一個簡單的REST服務,只需要幾行代碼。從一個可用的Spring啟動示例或Spring初始化器開始(http://start.spring.io),我們只需要添加一個javadto和注釋一個控制器,我們就有了第一個端點。
@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顯示了一個控制器??刂破靼ㄓ糜谧缘姆椒?,該方法可由POST請求觸發(fā)。該方法處理JSON并返回JSON。從JSON到javadto的轉換對Java開發(fā)人員來說是完全透明的,反之亦然。解析器的配置由springboot處理。彈簧靴支持Maven和Gradle。在Gradle的情況下,bootRun命令將啟動服務。
...
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中,只要在主類中添加一個簡單的注釋,就可以完成應用程序的許多擴展。注釋背后的底層基礎結構是隱藏的。這很好,因為可以在實現(xiàn)業(yè)務邏輯而不是技術上投入更多的時間,但有時spring引導特性背后的魔力可能會很可怕,調(diào)試意外行為可能需要很多時間。注解@SpringBootApplication足以在嵌入式tomcat中啟動應用程序。至少對于小型服務來說,通過混合使用XML片段、注釋和代碼來設置應用程序上下文的復雜性已經(jīng)消失了。關于spring作為一個沉重而復雜的框架的舊印象已經(jīng)不再突出。從本例中啟動服務后,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)碼的簡單方法。這樣我們就可以很容易地保證某種類型的異??偸菍е孪嗤腻e誤代碼。在本例中(清單1),異常處理程序捕獲異常并返回符合HTTP標準的響應。參數(shù)錯誤的請求不會導致500錯誤,相反,將返回一個400狀態(tài)代碼,其中包含有用的錯誤ID。清單5顯示了這樣一個響應。當然,我們可以用同樣的方法實現(xiàn)GET、PUT、DELETE請求或處理XML而不是JSON。從一個模板、一個現(xiàn)有的示例或初始化器開始,我們可以立即開始編寫代碼。整個基礎設施,如JAR的打包、HTTP服務器的啟動、庫的設置和其他初始化工作都從一開始就得到了解決。新rest服務的初始設置需要幾分鐘。
{
"timestamp":1458746952449,
"status":400,
"error":"Bad Request",
"exception":"java.lang.IllegalArgumentException",
"message":"error.username",
"path":"/register"
}
如何保護restapi
因為我們的測試服務接受用戶注冊,所以我們必須考慮數(shù)據(jù)的保護。在用戶可以檢索其數(shù)據(jù)之前,他必須對自己進行授權。在休息服務的世界里,古典意義上的會話是不存在的。每個呼叫都必須經(jīng)過授權才能訪問資源。
有幾種做法很常見。通常,服務受基本身份驗證的保護。Basic Auth要求客戶端在每個請求中發(fā)送用戶名和密碼(Base64編碼為頭信息的一部分)。嗅探器可以利用這些信息來授權他的通話。即使我們將通過SSL保護我們的服務,我們也認為Basic Auth不是我們服務的正確方法。另一種方法是使用令牌,它將隨每個請求而更改。成功的請求將返回下一個令牌和響應。在這種情況下,并行請求或錯誤很容易導致注銷。這就是為什么我們決定使用OAuth2.0。OAuth2.0僅在初始登錄時使用密碼。OAuth登錄將返回2個令牌。以后的請求必須使用第一個令牌(訪問令牌)執(zhí)行。此令牌在給定的時間段內(nèi)替換密碼。如果有人能夠攔截流量,他可以在該時間段內(nèi)使用該令牌,直到令牌過期。一旦令牌過期,客戶機就可以使用第二個令牌(刷新令牌)檢索新令牌。
這個概念并不強迫客戶機一直發(fā)送真正的密碼,并行執(zhí)行調(diào)用也是可行的。即使訪問令牌過期,客戶端也可以通過使用刷新令牌確保用戶永久登錄。發(fā)送給客戶機的令牌需要持久化,以便將客戶機發(fā)送的令牌與生成的令牌進行比較。在我們的用例中,我們希望去掉任何數(shù)據(jù)庫,因為這個服務只是包含真正邏輯的其他微服務的包裝。
為了解決這個問題,我們有三個選擇。我們可以使用內(nèi)存中的數(shù)據(jù)庫,它在多個實例之間共享。第二種選擇是使用負載平衡,它總是將來自會話的所有請求發(fā)送到本地(例如內(nèi)存中)存儲令牌的同一實例。第一種選擇將導致不必要的努力,第二種選擇打破了云本地微服務架構的整體概念,因為這樣我們就不再有無狀態(tài)的應用程序了。每次停機都意味著客戶注銷。所以我們決定用另一種方法。OAuth之上的JWT(jsonwebtokens)擴展允許在不存儲令牌的情況下進行授權。訪問令牌不僅僅是隨機生成的,而是使用私鑰對用戶ID、到期日期和其他元云本機進行簽名,并作為Oauth令牌添加到頭信息中。這樣每個實例都可以在不存儲令牌的情況下驗證令牌的有效性并檢索用戶信息,并且信息是加密的。JWT完全符合OAuth格式,這意味著所有oauth2客戶機都應該能夠使用JWT,即使不知道該令牌是JWT令牌而不是經(jīng)典的oauth2.0令牌。格式保持不變,令牌只是稍微長一點。在我們的例子中,客戶機不必做任何需要的更改,即使在REST服務中,所需的自適應也是最小的。不是將接收到的令牌與存儲的令牌進行比較,而是調(diào)用JWT存儲庫來驗證令牌。存儲庫解密令牌,其行為與使用數(shù)據(jù)庫的存儲庫相同。令牌包含用戶ID,但不包含用戶數(shù)據(jù)本身。這意味著用戶數(shù)據(jù)的存儲必須獨立解決。在我們的例子中,服務通過rest將用戶數(shù)據(jù)路由到用戶服務。清單6顯示了將OAuth2.0與JWT結合使用所需的Spring引導配置。它還顯示了一些額外的配置選項。
@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支持第三方的授權。即使在這種情況下未使用此功能,我們也可以將REST服務的使用限制為某些客戶機或合作伙伴。在登錄階段,不僅要傳輸用戶的用戶名和密碼,還需要客戶端和客戶端密碼。在我們的案例中,客戶端是不同的應用程序。springboot提供了一個簡單的角色和權限模型。但在本教程中我們將不詳細介紹。在我們的示例中,注冊是不安全的,但是只有在成功登錄之后才能訪問用戶數(shù)據(jù)。在本例中,登錄是檢索訪問OAuth-2.0令牌的請求。清單7顯示了使用OAuth令牌登錄后的登錄和用戶信息檢索。清單8是OAuth登錄的可能響應,包括訪問令牌和刷新令牌。為了完成這個示例,清單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的可維護文檔需要盡可能接近代碼,并且在理想情況下,應該從代碼生成(或者應該從API描述生成代碼)。Swagger是一個功能強大的框架,它包括圍繞API文檔主題的多個工具和庫。例如,工具集的一部分是從API描述生成代碼的工具。
但對于我們的用例來說更重要的是lib,它在運行時根據(jù)代碼生成JSON文檔。另一個工具用JSON文檔創(chuàng)建可執(zhí)行的HTML文檔。即使使用默認設置,Swagger庫通常也能提供很好的結果。為RESTAPI添加可執(zhí)行文檔可以使用單個注釋(@EnableSwagger2)完成。清單10就是一個例子。默認情況下,Swagger搜索應用程序中所有現(xiàn)有的REST定義。在清單10中,我們還可以看到如何將API文檔限制為現(xiàn)有API的一個子集。在這種情況下,文檔中將只顯示用戶信息和登錄名。此外,我還向文檔中添加了標題和版本信息。
@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可以自動使用其他端點來豐富自定義API,例如健康檢查、度量或調(diào)試信息。這些端點將由Swagger自動檢測(如果不受限制)。這是非常強大的,但是您應該考慮保護這些信息(例如,通過將其移動到防火墻后面的其他端口)。
圖1和圖2是清單10中配置的結果。Swagger幾乎檢測所有端點。OAuth2.0的配置不是在控制器中完成的,而是在配置類中完成的。Swagger無法完全檢測OAuth所需的所有信息。這就是為什么我在清單11中的方法中添加了頭描述。如果沒有這個描述,頭信息將不會顯示在可執(zhí)行的Swagger文檔中(圖2)。圖3顯示了包含頭信息的結果。通過向靜態(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 沒有標題信息的API文檔 包含標題信息的API文檔。
如何將應用程序嵌入Docker容器
將自動運行的jar嵌入Docker容器很簡單。彈簧靴支持Maven和Gradle。因為Gradle更精簡、更易于擴展、更易于使用和更快,所以我更喜歡Gradle用于我所有的項目。清單12展示了如何使用Gradle Docker插件將Spring引導應用程序打包到Docker容器中。Docker容器只需要一個Java運行時來運行jar。
清單13是一個簡單的Dockerfile。Docker容器基于另一個容器,該容器已經(jīng)包含Java運行時,并將我們的應用程序添加為Jar。如果您啟動容器,那么應用程序?qū)⒈O(jiān)聽端口8080,并且在我們的示例中,相同的端口將公開。清單14展示了如何構建和啟動容器。
清單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的微服務,包括身份驗證、文檔以及如何將其嵌入Docker容器。配置Spring引導應用程序的最簡單方法是屬性文件(應用程序?qū)傩?在應用程序的資源文件夾中。您可以在應用程序啟動時重寫屬性(如果需要的話),例如通過java-jar這樣的命令示例.jar–spring.config.location=/configuration.屬性。更改Docker容器中應用程序的配置可以通過將配置文件裝入容器來完成。這可能是部署自動化的一部分。在bean中使用配置可以通過使用單個注釋來完成。
準備好應用云
將應用程序嵌入Docker容器后,可以使用所有可用的Docker工具將應用程序部署到云中(例如Kubernetes)。彈簧靴由樞軸驅(qū)動。Pivotal是企業(yè)PAAS解決方案Pivotal Cloud Foundry背后的驅(qū)動程序。SpringBoot應用程序可以很容易地集成到云計算解決方案中,而無需Docker。除了一些元信息,這些應用程序可以部署到CloFoundrydry而無需修改。Cloud Foundry可以作為開放源代碼或不同的企業(yè)版本(例如,F(xiàn)oundry)提供,并且可以安裝在您的數(shù)據(jù)中心中。還有公共云代工產(chǎn)品(如Pivotal Web Services和IBM Bluemix)。
結論
對于大多數(shù)用例,springboot簡化了基于Java的微服務的構建。與Dropwizard等框架不同,它更易于使用,并提供了更豐富的功能集。springboot提供了大量的附加庫和集成,比如Ribbon、Zuul、Hystrix,以及與MongoDB、Redis、GemFire、Elasticsearch、Cassandra或Hazelcast等數(shù)據(jù)庫的集成。
Maven和Gradle為Java開發(fā)人員提供了強大且廣泛支持的構建系統(tǒng),與Play框架等框架的專用構建系統(tǒng)相比,這些構建系統(tǒng)更常見,更容易集成到現(xiàn)有結構中。與經(jīng)典的Web應用程序相比,springboot是精簡的。對于大多數(shù)項目,向項目中添加依賴項足以從一開始就獲得良好的結果,而無需調(diào)整默認配置。
但并非所有的一切都是完美的春季開機生態(tài)系統(tǒng)。如果要調(diào)整庫的設置,很可能還必須調(diào)整其他庫的設置。其中一個例子是OAuth的集成。Swagger沒有自動檢測到標題信息。為了嵌入Hystrix,您只需添加兩個注釋,依賴項和所有度量都會被自動檢測到。例如,如果您更改了健康檢查的URL,那么您也必須更改Hystrix的配置。調(diào)試隱藏在底層Spring魔術中的這些問題可能需要時間,但是Spring Boot提供的優(yōu)勢是值得的。