![spring](https://i-blog.csdnimg.cn/blog_migrate/ae4bc06514fe9c37fe31c93abc477728.png)
spring
“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。
在本文中,您将学习微服务架构以及如何使用Spring Boot来实现它。 在使用该技术创建了一些项目之后,您将把工件部署为Docker容器,并使用Docker Compose进行简化以模拟容器编排器(例如Kubernetes)。 锦上添花的是使用Spring Profiles进行身份验证集成。 您将了解如何通过生产资料启用它。
但是首先,让我们谈谈微服务。
了解现代微服务架构
与整体架构相反,微服务要求您将应用程序分成逻辑上相关的小块。 这些程序段是独立的软件,例如,可以使用HTTP或消息与其他程序段进行通信。
有一些关于微型尺寸的讨论。 有人说微服务是可以在单个冲刺中创建的软件。 其他人则说,如果微服务在逻辑上相关,那么它的大小可能会更大(例如,您不能混合使用苹果和橙子)。 我同意马丁·福勒( Martin Fowler)的观点,认为尺寸并没有多大关系,它与款式息息相关。
微服务有许多优点:
- 耦合风险不高–由于每个应用程序都处于不同的进程中,因此无法创建相互对话的类。
- 轻松扩展–如您所知,每个服务都是独立的软件。 因此,它可以按需扩展或缩小。 而且,由于代码比整体代码小,因此启动速度可能更快。
- 多个堆栈–您可以为每个服务使用最佳的软件堆栈。 例如,当Python对您要构建的东西更好时,就不再需要使用Java。
- 更少的合并和代码冲突–由于每个服务都是一个不同的存储库,因此更易于处理和检查提交。
但是,有一些缺点:
- 您有一个新的敌人-网络问题。 服务启动了吗? 如果服务中断,该怎么办?
- 复杂的部署过程– OK CI / CD在这里,但是您现在为每个服务只有一个工作流程。 如果他们使用不同的堆栈,则可能甚至无法为每个堆栈复制工作流程。
- 更复杂且难以理解的体系结构–它取决于您的设计方式,但请考虑以下问题:如果您不知道方法的作用,则可以阅读其代码。 在微服务体系结构中,此方法可能在另一个项目中,甚至可能没有代码。
如今,通常应该首先避免使用微服务架构。 经过一些迭代后,代码划分将变得更加清晰,项目需求也将变得更加清晰。 在您的开发团队开始进行小型项目之前,处理微服务通常过于昂贵。
使用Docker在Spring中构建微服务
在本教程中,您将构建两个项目:一个服务(school-service)和一个UI(school_ui)。 该服务提供持久层和业务逻辑,而UI提供图形用户界面。 只需最少的配置即可连接它们。
初始设置后,我将讨论发现和配置服务。 两种服务都是任何大规模分布式体系结构的重要组成部分。 为了证明这一点,您将其与OAuth 2.0集成在一起,并使用配置项目来设置OAuth 2.0密钥。
最后,每个项目都将转换为Docker映像。 Docker Compose将用于模拟容器协调器,因为Compose将使用服务之间的内部网络来管理每个容器。
最后,将介绍Spring配置文件以根据当前适当分配的环境来更改配置。 这样,您将拥有两个OAuth 2.0环境:一个用于开发,另一个用于生产。
更少的单词,更多的代码! 克隆本教程的资源库,并检出start
分支。
git clone -b start https://github.com/oktadeveloper/okta-spring-microservices-docker-example.git
根pom.xml
文件不是pom.xml
。 但是,一次管理多个项目可能会有所帮助。 让我们看看里面:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.okta.developer.docker_microservices</groupId>
<artifactId>parent-pom</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>parent-project</name>
<modules>
<module>school-service</module>
<module>school-ui</module>
</modules>
</project>
这称为聚合项目,因为它聚合子项目。 这对于在所有声明的模块上运行相同的Maven任务很有用。 这些模块不需要将根模块用作父模块。
有两个可用模块:学校服务和学校UI。
学校服务微服务
school-service
目录包含一个Spring Boot项目,该项目充当项目的持久层和业务规则。 在更复杂的情况下,您将拥有更多这样的服务。 该项目是使用始终出色的Spring Initializr和以下配置创建的:
- 组–
com.okta.developer.docker_microservices
- 神器–
school-service
- 依赖关系– JPA,Web,Lombok,H2
您可以阅读PostgreSQL,Flyway和JSONB的Spring Boot以获得有关此项目的更多详细信息。 总而言之,它具有实体TeachingClass
, Course,
Student
并使用TeachingClassServiceDB
和TeachingClassController
通过REST API公开一些数据。 要测试它,请打开一个终端,导航到school-service
目录,然后运行以下命令:
./mvnw spring-boot:run
该应用程序将从端口8081
(在文件school-service/src/main/resources/application.properties
)启动,因此您应该能够导航到http://localhost:8081
并查看返回的数据。
> curl http://localhost:8081
[
{
"classId":13,
"teacherName":"Profesor Jirafales",
"teacherId":1,
"courseName":"Mathematics",
"courseId":3,
"numberOfStudents":2,
"year":1988
},
{
"classId":14,
"teacherName":"Profesor Jirafales",
"teacherId":1,
"courseName":"Spanish",
"courseId":4,
"numberOfStudents":2,
"year":1988
},
{
"classId":15,
"teacherName":"Professor X",
"teacherId":2,
"courseName":"Dealing with unknown",
"courseId":5,
"numberOfStudents":2,
"year":1995
},
{
"classId":16,
"teacherName":"Professor X",
"teacherId":2,
"courseName":"Dealing with unknown",
"courseId":5,
"numberOfStudents":1,
"year":1996
}
]
基于Spring的School UI微服务
顾名思义,学校UI是利用学校服务的用户界面。 它是使用Spring Initializr使用以下选项创建的:
- 组–
com.okta.developer.docker_microservices
- 神器–
school-ui
- 依赖关系–网络,仇恨,胸腺,Lombok
UI是一个单个网页,列出了数据库上可用的类。 为了获取信息,它通过文件school-ui/src/main/resources/application.properties
的配置与school-service
连接。
service.host=localhost:8081
SchoolController
类具有查询服务的所有逻辑:
package com.okta.developer.docker_microservices.ui.controller;
import com.okta.developer.docker_microservices.ui.dto.TeachingClassDto;
import org.springframework.beans.factory.annotation.*;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/")
public class SchoolController {
private final RestTemplate restTemplate;
private final String serviceHost;
public SchoolController(RestTemplate restTemplate, @Value("${service.host}") String serviceHost) {
this.restTemplate = restTemplate;
this.serviceHost = serviceHost;
}
@RequestMapping("")
public ModelAndView index() {
return new ModelAndView("index");
}
@GetMapping("/classes")
public ResponseEntity<List<TeachingClassDto>> listClasses(){
return restTemplate
.exchange("http://"+ serviceHost +"/class", HttpMethod.GET, null,
new ParameterizedTypeReference<List<TeachingClassDto>>() {});
}
}
如您所见,该服务有一个硬编码的位置。 您可以使用-Dservice.host=localhost:9090
这样的环境变量来更改属性设置。 尽管如此,它仍必须手动定义。 如何拥有许多学校服务申请实例? 在当前阶段不可能。
启用school-service后,启动school-ui
,并在浏览器中浏览至http://localhost:8080
:
./mvnw spring-boot:run
您应该看到如下页面:
使用Spring Cloud和Eureka构建发现服务器
现在,您有了一个可以使用的服务,该应用程序使用两种服务将信息提供给最终用户。 怎么了在现代应用程序中,开发人员(或操作)通常不知道应用程序可能部署在何处或端口上。 部署应该是自动化的,以便没有人关心服务器名称和物理位置。 (除非您在数据中心内工作。否则,希望您在意!)
但是,必须有一个工具来帮助服务发现其对应对象。 有许多可用的解决方案,对于本教程,我们将使用Netflix的Eureka ,因为它具有出色的Spring支持。
返回start.spring.io并创建一个新项目,如下所示:
- 组:
com.okta.developer.docker_microservices
- 神器:
discovery
- 依赖项:Eureka Server
编辑主DiscoveryApplication.java
类,以添加@EnableEurekaServer
批注:
package com.okta.developer.docker_microservices.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryApplication.class, args);
}
}
并且,您需要更新其application.properties
文件,使其在端口8761上运行,并且不会尝试向其自身注册。
spring.application.name=discovery-server
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
让我们定义每个属性:
-
spring.application.name
–应用程序的名称,发现服务也使用它来发现服务。 您会看到每个其他应用程序也都有一个应用程序名称。 -
server.port
–服务器正在运行的端口。 Eureka服务器的默认端口8761
。 -
eureka.client.register-with-eureka
–告诉Spring不要将自己注册到发现服务中。 -
eureka.client .fetch-registry
–指示该实例不应从服务器获取发现信息。
现在,运行并访问http://localhost:8761
。
./mvnw spring-boot:run
上面的屏幕显示了准备注册新服务的Eureka服务器。 现在,该更改学校服务和学校用户界面以使用它了。
注意:如果在启动时收到ClassNotFoundException: javax.xml.bind.JAXBContext
错误,那是因为您在Java 11上运行。您可以将JAXB依赖项添加到pom.xml
以解决此问题。
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
</dependency>
使用服务发现在微服务之间进行通信
首先,添加必需的依赖关系很重要。 将以下内容添加到pom.xml
文件中(在school-service和school-ui项目中):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
该模块是Spring Cloud计划的一部分,因此,需要一个新的依存关系管理节点,如下所示(不要忘记将其添加到两个项目中):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
现在,您需要配置两个应用程序以向Eureka注册。
在两个项目的application.properties
文件中,添加以下行:
eureka.client.serviceUrl.defaultZone=${EUREKA_SERVER:http://localhost:8761/eureka}
spring.application.name=school-service
不要忘了应用程序的名称从改变school-service
于school-ui
在学校的UI项目。 注意第一行中有一种新的参数: {EUREKA_SERVER:http://localhost:8761/eureka}
。 这意味着“如果环境变量EUREKA_SERVER存在,请使用它的值,否则请使用默认值。” 这在以后的步骤中将很有用。 ;)
你知道吗? 两个应用程序都准备好将自己注册到发现服务中。 您无需执行任何其他操作。 我们的主要目标是学校用户界面项目不需要知道学校服务在哪里。 因此,您需要更改SchoolController
(在school-ui
项目中)以在其REST端点中使用school-service
。 您也可以在此类中删除serviceHost
变量。
package com.okta.developer.docker_microservices.ui.controller;
import com.okta.developer.docker_microservices.ui.dto.TeachingClassDto;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/")
public class SchoolController {
private final RestTemplate restTemplate;
public SchoolController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@RequestMapping("")
public ModelAndView index() {
return new ModelAndView("index");
}
@GetMapping("/classes")
public ResponseEntity<List<TeachingClassDto>> listClasses() {
return restTemplate
.exchange("http://school-service/classes", HttpMethod.GET, null,
new ParameterizedTypeReference<List<TeachingClassDto>>() {});
}
}
在集成Eureka之前,您已经进行了配置,指出了学校服务的位置。 现在,您已将服务调用更改为使用其他服务使用的名称:无端口,无主机名。 您需要的服务就在某个地方,而您不需要知道在哪里。
学校服务可能具有的多个实例,最好在这些实例之间进行负载均衡负载。 幸运的是,Spring有一个简单的解决方案:在创建RestTemplate
bean时,如下所示添加@LoadBalanced
批注。 每当您向服务器提出问题时,Spring都会管理多个实例调用。
package com.okta.developer.docker_microservices.ui;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.*;
@SpringBootApplication
public class UIWebApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(UIWebApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if(!registry.hasMappingForPattern("/static/**")) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/", "classpath:/static/js/");
}
}
}
现在,开始重新启动school-service和school-ui (并保持Discovery服务启动)。 再次快速浏览一下http://localhost:8761
:
现在,您的服务正在与Discovery服务器共享信息。 您可以再次测试该应用程序,然后查看它是否可以正常运行。 只需在您喜欢的浏览器中转到http://localhost:8080
。
将配置服务器添加到您的微服务架构
尽管此配置有效,但最好删除项目源代码中任何配置值的痕迹。 首先,配置URL已从项目中删除,并由服务进行管理。 现在,您可以使用Spring Cloud Config对项目中的每个配置执行类似的操作。
首先,使用Spring Initializr和以下参数创建配置项目:
- 组:
com.okta.developer.docker_microservices
- 工件:
config
- 依赖项:配置服务器,Eureka发现
在主类中,添加@EnableConfigServer
:
package com.okta.developer.docker_microservices.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
...
}
在项目的application.properties
添加以下属性和值:
spring.application.name=CONFIGSERVER
server.port=8888
spring.profiles.active=native
spring.cloud.config.server.native.searchLocations=.
eureka.client.serviceUrl.defaultZone=${EUREKA_SERVER:http://localhost:8761/eureka}
有关属性的一些解释:
-
spring.profiles.active=native
表示Spring Cloud Config必须使用本机文件系统来获取配置。 通常使用Git存储库,但是为了简单起见,我们将坚持使用本机文件系统。 -
spring.cloud.config.server.native.searchLocations
–包含配置文件的路径。 如果将其更改为硬盘驱动器上的特定文件夹,请确保并在其中创建school-ui.properties
文件。
现在,您需要一些配置和适用于此示例。 Okta的配置如何? 让我们将school-ui放在授权层后面,并使用配置项目提供的属性值。
您可以注册一个永久免费的开发者帐户,该帐户将使您能够创建需要使用的尽可能多的用户和应用程序! 创建帐户后,在Okta的信息中心中创建一个新的Web应用程序(“应用程序” >“添加应用程序” ):
并用以下值填写下一个表格:
该页面将为您返回一个应用程序ID和一个密钥。 确保安全,然后在config
项目的根文件夹中创建一个名为school-ui.properties
的文件,内容如下。 不要忘记填充变量值:
okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId={yourClientId}
okta.oauth2.clientSecret={yourClientSecret}
现在,运行config
项目并检查其是否正确获取了配置数据:
./mvnw spring-boot:run
> curl http://localhost:8888/school-ui.properties
okta.oauth2.clientId: YOUR_CLIENT_ID
okta.oauth2.clientSecret: YOUR_CLIENT_SECRET
okta.oauth2.issuer: https://YOUR_DOMAIN/oauth2/default
更改School UI以使用Spring Cloud Config和OAuth 2.0
现在,您需要对Spring UI项目进行一些更改。
首先,您需要更改school-ui/pom.xml
并添加一些新的依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>com.okta.spring</groupId>
<artifactId>okta-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
在com.okta...ui.config
包中创建一个新的SecurityConfiguration
类:
package com.okta.developer.docker_microservices.ui;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login();
}
}
更改您的SchoolController
以便只允许具有范围profile
用户使用(每位经过身份验证的用户都拥有)。
import org.springframework.security.access.prepost.PreAuthorize;
....
@GetMapping("/classes")
@PreAuthorize("hasAuthority('SCOPE_profile')")
public ResponseEntity<List<TeachingClassDto>> listClasses(){
return restTemplate
.exchange("http://school-service/class", HttpMethod.GET, null,
new ParameterizedTypeReference<List<TeachingClassDto>>() {});
}
一些配置需要在项目启动时定义。 Spring提供了一个聪明的解决方案,可以在上下文启动之前正确定位并提取配置数据。 您需要像这样创建文件src/main/resources/bootstrap.yml
:
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_SERVER:http://localhost:8761/eureka}
spring:
application:
name: school-ui
cloud:
config:
discovery:
enabled: true
service-id: CONFIGSERVER
引导文件会创建一个预启动的Spring Application Context,用于在实际应用程序启动之前提取配置。 您需要将所有属性从application.properties
移到该文件,因为Spring需要知道Eureka Server的位置以及如何搜索配置。 在上面的示例中,您启用了通过发现服务进行配置( spring.cloud.config.discovery.enabled
)并指定了配置service-id
。
更改application.properties
文件,使其仅具有一个OAuth 2.0属性:
okta.oauth2.redirect-uri=/authorization-code/callback
最后一个要修改的文件是src/main/resources/templates/index.hml
。 对其进行调整,以在用户未通过身份验证时显示登录按钮,在用户登录时显示注销按钮。
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://www.javacodegeeks.com/wp-content/litespeed/localres/stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<nav class="navbar navbar-default">
<form method="post" th:action="@{/logout}" th:if="${#authorization.expression('isAuthenticated()')}" class="navbar-form navbar-right">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<button id="logout-button" type="submit" class="btn btn-danger">Logout</button>
</form>
<form method="get" th:action="@{/oauth2/authorization/okta}" th:unless="${#authorization.expression('isAuthenticated()')}">
<button id="login-button" class="btn btn-primary" type="submit">Login</button>
</form>
</nav>
<div id="content" th:if="${#authorization.expression('isAuthenticated()')}">
<h1>School classes</h1>
<table id="classes">
<thead>
<tr>
<th>Course</th>
<th>Teacher</th>
<th>Year</th>
<th>Number of students</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://www.javacodegeeks.com/wp-content/litespeed/localres/code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script src="https://www.javacodegeeks.com/wp-content/litespeed/localres/cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://www.javacodegeeks.com/wp-content/litespeed/localres/stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script src="static/js/school_classes.js"></script>
</div>
</body>
</html>
您应该在此HTML中了解一些Thymeleaf属性:
-
@{/logout}
–返回在后端定义的注销URL -
th:if="${#authorization.expression('isAuthenticated()')}"
–仅在用户登录时打印HTML -
@{//oauth2/authorization/okta}
–这是Spring Security重定向到Okta的URL。 您也可以链接到/login
,但这只是呈现相同的链接,您必须单击它。 -
th:unless="${#authorization.expression('isAuthenticated()')}"
–仅当用户注销时才在节点内打印HTML
现在,重新启动配置项目和school-ui。 如果导航到输入http://localhost:8080
,则应该看到以下屏幕:
登录后,屏幕应显示如下:
恭喜,您已经使用Spring Cloud config和Eureka创建了微服务架构来进行服务发现! 现在,让我们更进一步,并对每个服务进行Dockerize。
使用Docker打包Spring应用程序
Docker是一项了不起的技术,它允许创建类似于虚拟机的系统映像,但是共享与主机操作系统相同的内核。 此功能可以提高系统性能和启动时间。 此外,Docker提供了一个精巧的内置系统,该系统可以确保一旦创建映像; 它永远不会改变。 换句话说:不再有“它可以在我的机器上运行!”
提示:需要更深的Docker背景吗? 看看我们的《 Docker开发人员指南》 。
您需要为每个项目创建一个Docker映像。 每个映像在每个项目的根文件夹中应具有相同的Maven配置和Dockerfile
内容(例如, school-ui/Dockerfile
)。
在每个项目的pom中,添加dockerfile-maven-plugin
:
<plugins>
...
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.9</version>
<executions>
<execution>
<id>default</id>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
<configuration>
<repository>developer.okta.com/microservice-docker-${project.artifactId}</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
</plugins>
每次运行./mvnw install
时,此XML都会配置Dockerfile Maven插件以构建Docker映像。 将使用名称developer.okta.com/microservice-docker-${project.artifactId}
创建每个图像,其中project.artifactId
因project.artifactId
而异。
在每个项目的根目录中创建一个Dockerfile
文件。
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/*.jar app.jar
ENV JAVA_OPTS="
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar"
Dockerfile
遵循Spring Boot对Docker的建议。
现在,更改school-ui/src/main/resources/bootstrap.yml
以添加新的failFast
设置:
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_SERVER:http://localhost:8761/eureka}
spring:
application:
name: school-ui
cloud:
config:
discovery:
enabled: true
serviceId: CONFIGSERVER
failFast: true
spring.cloud.failFast: true
设置告诉Spring Cloud Config在找不到配置服务器时立即终止应用程序。 这将对下一步很有用。
添加Docker Compose以运行所有内容
创建一个名为docker-compose.yml
的新文件,该文件定义每个项目的启动方式:
version: '3'
services:
discovery:
image: developer.okta.com/microservice-docker-discovery:0.0.1-SNAPSHOT
ports:
- 8761:8761
config:
image: developer.okta.com/microservice-docker-config:0.0.1-SNAPSHOT
volumes:
- ./config-data:/var/config-data
environment:
- JAVA_OPTS=
-DEUREKA_SERVER=http://discovery:8761/eureka
-Dspring.cloud.config.server.native.searchLocations=/var/config-data
depends_on:
- discovery
ports:
- 8888:8888
school-service:
image: developer.okta.com/microservice-docker-school-service:0.0.1-SNAPSHOT
environment:
- JAVA_OPTS=
-DEUREKA_SERVER=http://discovery:8761/eureka
depends_on:
- discovery
- config
school-ui:
image: developer.okta.com/microservice-docker-school-ui:0.0.1-SNAPSHOT
environment:
- JAVA_OPTS=
-DEUREKA_SERVER=http://discovery:8761/eureka
restart: on-failure
depends_on:
- discovery
- config
ports:
- 8080:8080
如您所见,每个项目现在都是Docker中已声明的服务,将文件组成。 它将暴露其端口和其他一些属性。
- 除发现外,所有项目都将具有变量值
-DEUREKA_SERVER=http://discovery:8761/eureka
。 这将告诉您在哪里可以找到发现服务器。 Docker Compose在服务之间创建一个虚拟网络,每个服务使用的DNS名称就是其名称:这就是为什么可以将discovery
用作主机名的原因。 - Config服务将具有用于配置文件的卷。 该卷将映射到docker容器内的
/var/config-data
。 同样,属性spring.cloud.config.server.native.searchLocations
将被覆盖为相同的值。 您必须将文件school-ui.properties
存储在卷映射上指定的同一文件夹中(在上面的示例中,相对文件夹./config-data
)。 - school-ui项目的属性将
restart: on-failure
。 这将Docker Compose设置为在应用程序失败后立即重新启动。 与failFast
属性一起使用可以使应用程序继续尝试启动,直到Discovery和Config项目完全准备好为止。
就是这样! 现在,构建图像:
cd config && ./mvnw clean install
cd ../discovery && ./mvnw clean install
cd .. && ./mvnw clean install
在school-ui
项目中,最后一个命令可能会失败,并出现以下错误:
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: java.lang.IllegalStateException: No instances found of configserver (CONFIGSERVER)
要解决此问题,请创建一个school-ui/src/test/resources/test.properties
文件并添加属性,以使Okta的配置通过,并且在测试时不使用发现或配置服务器。
okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId=TEST
spring.cloud.discovery.enabled=false
spring.cloud.config.discovery.enabled = false
spring.cloud.config.enabled = false
然后修改UIWebApplicationTests.java
以加载此文件以用于测试属性:
import org.springframework.test.context.TestPropertySource;
...
@TestPropertySource(locations="classpath:test.properties")
public class UIWebApplicationTests {
...
}
现在,您应该能够在school-ui
项目中运行./mvnw clean install
。
完成后,运行Docker Compose以启动所有容器(在docker-compose.yml
所在的目录中)。
docker-compose up -d
Starting okta-microservice-docker-post-final_discovery_1 ... done
Starting okta-microservice-docker-post-final_config_1 ... done
Starting okta-microservice-docker-post-final_school-ui_1 ... done
Starting okta-microservice-docker-post-final_school-service_1 ... done
现在,您应该能够像以前一样浏览该应用程序。
使用Spring配置文件来修改您的微服务的配置
现在,您已经到达了微服务之旅的最后阶段。 Spring Profiles是一个功能强大的工具。 使用配置文件,可以通过完全注入不同的依赖项或配置来修改程序行为。
假设您有一个结构良好的软件,其持久层与业务逻辑分离。 例如,您还提供对MySQL和PostgreSQL支持。 每个数据库可能有不同的数据访问类,这些类仅由定义的概要文件加载。
另一个用例是配置:不同的配置文件可能具有不同的配置。 以身份验证为例。 您的测试环境会进行身份验证吗? 如果是这样,则不应使用与生产相同的用户目录。
将您的配置项目更改为在Okta中有两个应用程序:一个默认(用于开发),另一个用于生产。 在Okta网站上创建一个新的Web应用程序,并将其命名为“ okta-docker-production”。
现在,在您的config
项目中,创建一个名为school-ui-production.properties
的新文件。 您已经有了school-ui.properties
,每个School UI实例都将使用它。 在文件末尾添加环境时,Spring将合并两个文件,并优先于最特定的文件。 使用生产应用程序的客户端ID和密码保存文件,如下所示:
school-ui-production.properties
okta.oauth2.clientId={YOUR_PRODUCTION_CLIENT_ID}
okta.oauth2.clientSecret={YOUR_PRODUCTION_CLIENT_SECRET}
现在,使用Maven运行配置项目,然后运行以下两个curl
命令:
./mvnw spring-boot:run
> curl http://localhost:8888/school-ui.properties
okta.oauth2.issuer: https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId: ==YOUR DEV CLIENT ID HERE==
okta.oauth2.clientSecret: ==YOUR DEV CLIENT SECRET HERE==
> curl http://localhost:8888/school-ui-production.properties
okta.oauth2.issuer: https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId: ==YOUR PROD CLIENT ID HERE==
okta.oauth2.clientSecret: ==YOUR PROD CLIENT SECRET HERE==
如您所见,即使文件school-ui-production
具有两个属性, config
项目也会显示三个属性(因为配置已合并)。
现在,您可以在docker-compose.yml
更改school-ui
服务以使用production
配置文件:
school-ui:
image: developer.okta.com/microservice-docker-school-ui:0.0.1-SNAPSHOT
environment:
- JAVA_OPTS=
-DEUREKA_SERVER=http://discovery:8761/eureka
-Dspring.profiles.active=production
restart: on-failure
depends_on:
- discovery
- config
ports:
- 8080:8080
您还需要将school-ui-production.properties
复制到您的config-data
目录中。 然后关闭所有Docker容器并重新启动它们。
docker-compose down
docker-compose up -d
您应该在school-ui
容器的日志中看到以下内容:
The following profiles are active: production
而已! 现在,您可以在生产配置文件中运行微服务架构。 晕!
提示:如果要证明使用了okta-docker-production
应用程序而不是okta-docker
,可以在Okta中停用okta-docker
应用程序,并确认您仍然可以登录http://localhost:8080
。
了解有关微服务,Spring,Docker和现代应用程序安全性的更多信息
在这篇文章中,您了解了有关微服务以及如何部署它们的更多信息,以及:
- 什么是微服务?
- 服务应如何发现其依赖关系而无需事先知道它们的位置。
- 如何以信息的中心点维护分布式配置。 该配置可以管理一个或多个应用程序和环境。
- 如何使用Spring Cloud Config配置OAuth 2.0。
- 如何使用Docker和Docker Compose部署微服务
- 如何使用Spring Profiles在生产环境中进行部署。
您可以在oktadeveloper / okta-spring-microservices-docker-example上的GitHub上找到本教程的完整源代码。
如果您有兴趣在Spring中学习有关微服务或现代应用程序开发的更多信息,建议您查看以下资源:
- 使用Spring Boot 2.0和OAuth 2.0构建并保护微服务
- 使用JHipster和OAuth 2.0开发微服务架构
- 使用Spring Boot为Microbrews构建微服务架构
- Spring Boot 2.1:出色的OIDC,OAuth 2.0和React式API支持
- 使用Spring Boot和MongoDB构建一个React式应用程序
如果您对此帖子有任何疑问,请在下面发表评论。 您可以在Twitter上关注@oktadev以获取更多精彩内容!
Build Spring Microservices和Dockerize Them for Production''最初于2019年2月28日发布在Okta开发者博客上。
“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。
翻译自: https://www.javacodegeeks.com/2019/04/build-spring-microservices-dockerize-production.html
spring