Spring引导教程:REST服务和微服务

docker 微服务 spring boot 教程
2021-01-25 11:01:19
8 0 0

javaee应用服务器和单片软件体系结构的时代几乎一去不复返了。硬件不再变得更快,但互联网流量仍在增加。平台必须支持向外扩展。负载必须分配到多个主机。基于微服务的体系结构可以为这一需求提供解决方案。除了更好的可扩展性之外,微服务还提供了更快的开发周期、基于负载的动态可扩展性和改进的故障转移行为。

在微服务体系结构中,只需要扩展需要更多资源的部分,而不是复制一个完整的系统来处理更高的需求。软件可以解耦,软件的维护也越来越容易。每一个不得不在一个单一应用程序中更新hibernate版本的开发人员都知道让一个单一应用程序保持最新和减少技术债务的痛苦。使用microservices,您可以一步一步地完成这项工作。随着服务数量的增加,每个开销都需要最小化。繁重的应用服务器不是用于此目的的工具。Web应用程序或服务可以通过使用嵌入式Web服务器实现一段时间。不用安装完整的JEE概要文件应用服务器,也不用部署EAR或WAR文件,一个简单的自运行jar就可以完成这项工作。这并不新鲜,但为了快速创建此类服务,需要一个模板或框架。这种框架在其他语言中非常突出,但对于Java来说,只有少数几种可用的选项。情况变了。在Dropwizard、Play Framework或Spring Boot中,至少有3个框架在Java微服务世界中大量使用。

在本教程中,我将使用一个简单的示例来演示如何使用springboot设置基于REST的springboot微服务。此示例基于一个服务,该服务是为某些移动应用程序构建的后端。本教程中显示的代码已简化。该服务本身通过使用其他已经存在于后台的服务,为移动应用程序提供了restapi。这意味着该服务仅充当其他内部服务的包装器类型。该服务需要限制未经授权的访问,在这种情况下,意味着该服务需要用户和客户端(在这种情况下是移动应用程序)的授权。

这就是我们添加OAuth2和JWT作为授权系统的原因。我们还为API的文档添加了Swagger。服务本身是使用Docker部署到生产环境的。

如何设置初始Spring引导结构

springboot是一个旨在简化新服务创建的框架。对于最简单的用例,所需的库已经捆绑在所谓的spring starters中的fitting组合和版本中。我们不必将应用程序部署到应用程序服务器中,相反,我们可以独立运行应用程序或在Docker容器中运行应用程序,因为应用程序已经包含服务器。

为了创建一个简单的REST服务,只需要几行代码。从一个可用的Spring启动示例或Spring初始化器开始(http://start.spring.io),我们只需要添加一个javadto和注释一个控制器,我们就有了第一个端点。

清单1

@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请求触发。该方法处理JSON并返回JSON。从JSON到javadto的转换对Java开发人员来说是完全透明的,反之亦然。解析器的配置由springboot处理。弹簧靴支持Maven和Gradle。在Gradle的情况下,bootRun命令将启动服务。

清单2

...

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中,只要在主类中添加一个简单的注释,就可以完成应用程序的许多扩展。注释背后的底层基础结构是隐藏的。这很好,因为可以在实现业务逻辑而不是技术上投入更多的时间,但有时spring引导特性背后的魔力可能会很可怕,调试意外行为可能需要很多时间。注解@SpringBootApplication足以在嵌入式tomcat中启动应用程序。至少对于小型服务来说,通过混合使用XML片段、注释和代码来设置应用程序上下文的复杂性已经消失了。关于spring作为一个沉重而复杂的框架的旧印象已经不再突出。从本例中启动服务后,POST调用可以触发注册。清单4显示了一个简单的示例。

清单3

...
@SpringBootApplication
public class Application {

   public static void main(String[] args) {
       SpringApplication.run(Application.class, args);
   }
}

清单4

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状态码的简单方法。这样我们就可以很容易地保证某种类型的异常总是导致相同的错误代码。在本例中(清单1),异常处理程序捕获异常并返回符合HTTP标准的响应。参数错误的请求不会导致500错误,相反,将返回一个400状态代码,其中包含有用的错误ID。清单5显示了这样一个响应。当然,我们可以用同样的方法实现GET、PUT、DELETE请求或处理XML而不是JSON。从一个模板、一个现有的示例或初始化器开始,我们可以立即开始编写代码。整个基础设施,如JAR的打包、HTTP服务器的启动、库的设置和其他初始化工作都从一开始就得到了解决。新rest服务的初始设置需要几分钟。

清单5

{
      "timestamp":1458746952449,
      "status":400,
      "error":"Bad Request",
      "exception":"java.lang.IllegalArgumentException",
      "message":"error.username",
      "path":"/register"
   }

如何保护restapi

因为我们的测试服务接受用户注册,所以我们必须考虑数据的保护。在用户可以检索其数据之前,他必须对自己进行授权。在休息服务的世界里,古典意义上的会话是不存在的。每个呼叫都必须经过授权才能访问资源。

有几种做法很常见。通常,服务受基本身份验证的保护。Basic Auth要求客户端在每个请求中发送用户名和密码(Base64编码为头信息的一部分)。嗅探器可以利用这些信息来授权他的通话。即使我们将通过SSL保护我们的服务,我们也认为Basic Auth不是我们服务的正确方法。另一种方法是使用令牌,它将随每个请求而更改。成功的请求将返回下一个令牌和响应。在这种情况下,并行请求或错误很容易导致注销。这就是为什么我们决定使用OAuth2.0。OAuth2.0仅在初始登录时使用密码。OAuth登录将返回2个令牌。以后的请求必须使用第一个令牌(访问令牌)执行。此令牌在给定的时间段内替换密码。如果有人能够拦截流量,他可以在该时间段内使用该令牌,直到令牌过期。一旦令牌过期,客户机就可以使用第二个令牌(刷新令牌)检索新令牌。

这个概念并不强迫客户机一直发送真正的密码,并行执行调用也是可行的。即使访问令牌过期,客户端也可以通过使用刷新令牌确保用户永久登录。发送给客户机的令牌需要持久化,以便将客户机发送的令牌与生成的令牌进行比较。在我们的用例中,我们希望去掉任何数据库,因为这个服务只是包含真正逻辑的其他微服务的包装。

为了解决这个问题,我们有三个选择。我们可以使用内存中的数据库,它在多个实例之间共享。第二种选择是使用负载平衡,它总是将来自会话的所有请求发送到本地(例如内存中)存储令牌的同一实例。第一种选择将导致不必要的努力,第二种选择打破了云本地微服务架构的整体概念,因为这样我们就不再有无状态的应用程序了。每次停机都意味着客户注销。所以我们决定用另一种方法。OAuth之上的JWT(jsonwebtokens)扩展允许在不存储令牌的情况下进行授权。访问令牌不仅仅是随机生成的,而是使用私钥对用户ID、到期日期和其他元云本机进行签名,并作为Oauth令牌添加到头信息中。这样每个实例都可以在不存储令牌的情况下验证令牌的有效性并检索用户信息,并且信息是加密的。JWT完全符合OAuth格式,这意味着所有oauth2客户机都应该能够使用JWT,即使不知道该令牌是JWT令牌而不是经典的oauth2.0令牌。格式保持不变,令牌只是稍微长一点。在我们的例子中,客户机不必做任何需要的更改,即使在REST服务中,所需的自适应也是最小的。不是将接收到的令牌与存储的令牌进行比较,而是调用JWT存储库来验证令牌。存储库解密令牌,其行为与使用数据库的存储库相同。令牌包含用户ID,但不包含用户数据本身。这意味着用户数据的存储必须独立解决。在我们的例子中,服务通过rest将用户数据路由到用户服务。清单6显示了将OAuth2.0与JWT结合使用所需的Spring引导配置。它还显示了一些额外的配置选项。

清单6

@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提供了一个简单的角色和权限模型。但在本教程中我们将不详细介绍。在我们的示例中,注册是不安全的,但是只有在成功登录之后才能访问用户数据。在本例中,登录是检索访问OAuth-2.0令牌的请求。清单7显示了使用OAuth令牌登录后的登录和用户信息检索。清单8是OAuth登录的可能响应,包括访问令牌和刷新令牌。为了完成这个示例,清单9显示了控制器。

清单7

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

清单8

{ "access_token":"eyJh...Fpao",
 "token_type":"bearer",
 "refresh_token":"eyJh...4clI",
 "expires_in":43199,
 "scope":"read write",
 "jti":"6e0...b31"
}

清单9

@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,它在运行时根据代码生成JSON文档。另一个工具用JSON文档创建可执行的HTML文档。即使使用默认设置,Swagger库通常也能提供很好的结果。为RESTAPI添加可执行文档可以使用单个注释(@EnableSwagger2)完成。清单10就是一个例子。默认情况下,Swagger搜索应用程序中所有现有的REST定义。在清单10中,我们还可以看到如何将API文档限制为现有API的一个子集。在这种情况下,文档中将只显示用户信息和登录名。此外,我还向文档中添加了标题和版本信息。

清单10

@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,例如健康检查、度量或调试信息。这些端点将由Swagger自动检测(如果不受限制)。这是非常强大的,但是您应该考虑保护这些信息(例如,通过将其移动到防火墙后面的其他端口)。

图1和图2是清单10中配置的结果。Swagger几乎检测所有端点。OAuth2.0的配置不是在控制器中完成的,而是在配置类中完成的。Swagger无法完全检测OAuth所需的所有信息。这就是为什么我在清单11中的方法中添加了头描述。如果没有这个描述,头信息将不会显示在可执行的Swagger文档中(图2)。图3显示了包含头信息的结果。通过向静态资源的文件夹中添加自定义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容器基于另一个容器,该容器已经包含Java运行时,并将我们的应用程序添加为Jar。如果您启动容器,那么应用程序将监听端口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
       }
   }
}

...

清单13

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

配置

我们已经了解了如何建立基于REST的微服务,包括身份验证、文档以及如何将其嵌入Docker容器。配置Spring引导应用程序的最简单方法是属性文件(应用程序属性)在应用程序的资源文件夹中。您可以在应用程序启动时重写属性(如果需要的话),例如通过java-jar这样的命令示例.jar–spring.config.location=/configuration.属性。更改Docker容器中应用程序的配置可以通过将配置文件装入容器来完成。这可能是部署自动化的一部分。在bean中使用配置可以通过使用单个注释来完成。

准备好应用云

将应用程序嵌入Docker容器后,可以使用所有可用的Docker工具将应用程序部署到云中(例如Kubernetes)。弹簧靴由枢轴驱动。Pivotal是企业PAAS解决方案Pivotal Cloud Foundry背后的驱动程序。SpringBoot应用程序可以很容易地集成到云计算解决方案中,而无需Docker。除了一些元信息,这些应用程序可以部署到CloFoundrydry而无需修改。Cloud Foundry可以作为开放源代码或不同的企业版本(例如,Foundry)提供,并且可以安装在您的数据中心中。还有公共云代工产品(如Pivotal Web Services和IBM Bluemix)。

结论

对于大多数用例,springboot简化了基于Java的微服务的构建。与Dropwizard等框架不同,它更易于使用,并提供了更丰富的功能集。springboot提供了大量的附加库和集成,比如Ribbon、Zuul、Hystrix,以及与MongoDB、Redis、GemFire、Elasticsearch、Cassandra或Hazelcast等数据库的集成。

Maven和Gradle为Java开发人员提供了强大且广泛支持的构建系统,与Play框架等框架的专用构建系统相比,这些构建系统更常见,更容易集成到现有结构中。与经典的Web应用程序相比,springboot是精简的。对于大多数项目,向项目中添加依赖项足以从一开始就获得良好的结果,而无需调整默认配置。

但并非所有的一切都是完美的春季开机生态系统。如果要调整库的设置,很可能还必须调整其他库的设置。其中一个例子是OAuth的集成。Swagger没有自动检测到标题信息。为了嵌入Hystrix,您只需添加两个注释,依赖项和所有度量都会被自动检测到。例如,如果您更改了健康检查的URL,那么您也必须更改Hystrix的配置。调试隐藏在底层Spring魔术中的这些问题可能需要时间,但是Spring Boot提供的优势是值得的。

作者介绍

用微信扫一扫

收藏