“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

如果您要处理大量流数据,反应式应用程序可让您更好地扩展。 它们是非阻塞的,并且往往效率更高,因为它们在等待发生事情时不会占用处理能力。

反应系统包含异步I / O。 异步I / O背后的概念很简单:通过回收本来会在等待I / O活动时处于空闲状态的资源来缓解资源利用效率低下的问题。 异步I / O颠倒了I / O处理的常规设计:向客户端通知新数据而不是请求新数据; 这使客户可以在等待时做其他事情。

如果要构建反应式应用程序,则需要它一直到整个数据库都是反应式的。 将阻塞的JDBC驱动程序与Spring WebFlux一起使用,您会对其性能感到失望。 使用反应性NoSQL数据库(例如Cassandra,MongoDB,Couchbase和Redis),其性能将给您留下深刻的印象。

在本教程中,您将学习如何使用Spring Boot,Spring WebFlux和Spring Data创建与NoSQL数据库后端(在本例中为MongoDB)通信的反应式Web服务。

我只是对你说了几句。 让我们越过它们。

如果您已经了解NoSQL和Reactive编程,并且只想看一些代码,请随时跳过前两部分,并从“构建Spring Boot资源服务器”开始。

注意:本系列的第一部分演示了如何将Spring Boot和Spring Data与关系数据库PostgreSQL一起使用。 您可以在此处查看该帖子。

什么是NoSQL?为什么使用MongoDB?

NoSQL是任何非关系数据库的术语。 在关系数据库(例如SQL,MySQL等)中,数据存储在具有强类型且表列之间关系明确的表中。 关系数据库的紧密,定义明确的结构既是优点也是缺点。 这是一个权衡。 NoSQL数据库使该模型爆炸式增长,并提供了其他模型,这些模型可提供更大的灵活性并易于扩展。

扩展的微服务/集群模型为关系数据库带来了很多问题。 它们不是为在多台计算机上运行和保持同步而设计的。 开发NoSQL数据库部分是为了解决此问题。 通常,它们是在考虑群集和水平缩放的情况下构建的。 为了以另一种方式展示这种方式(通常是使用SQL数据库),如果需要更多功能,则必须调整数据库运行所在的服务器的大小。 它几乎是单片的,并且即使当今有现代的虚拟服务器功能,也很难动态地做到这一点。 在Internet规模上,更好的模型是拥有一个灵活的数据库集群,它们可以在数据库之间自动同步,并允许您根据需求增加实例(并在需求减少时减少实例)。 这意味着增加功率并不需要更昂贵的机器。 您可以根据需要简单地添加更多,相对便宜的计算机。

NoSQL数据库的另一个潜在好处是它们的灵活性。 像MongoDB这样的基于文档的NoSQL数据库可以在文档中存储任意数据。 可以将字段动态添加到存储的文档中,而无需增加表迁移的开销。 当然,这并不能解决版本控制的问题,仍然取决于应用程序来处理不断变化的数据结构(并不总是琐碎的),但是至少您并没有与数据库打架。

综上所述,请记住,SQL /关系数据库不会走到任何地方。 它们经过验证,快速且超级可靠。 在某些情况下,它们更便宜,更容易。 例如,对于简单的网站或博客,MySQL很难被击败。 但是即使在企业环境中, 有时您也想要关系数据库强制执行的结构。 如果您有一个相当静态的数据模型并且不需要扩展到Internet规模,则SQL可能是最佳选择。 这些类型的设计注意事项在您深入数据库选择之前值得深思,因为它是新的且浮华的。

我在本教程中使用的是MongoDB,因为一开始它很容易。 如果您使用Spring Data MongoDB,它甚至更容易!

积极反应!

反应性是另一种行话。 感觉就像人们喜欢在聚会和会议上随意表达的那种字眼,只是含糊其词。 就像“存在的”或“ ennui”。 让我们定义它。

如果您看一下Spring WebFlux文档 ,他们会很好地概述什么是反应性

术语“反应性”是指围绕响应变化而构建的编程模型-网络组件响应I / O事件,UI控制器响应鼠标事件等。 从这个意义上说,非阻塞是反应性的,因为随着操作完成或数据可用,我们现在处于响应通知的模式,而不是被阻塞。

因此,反应式意味着:非阻塞,异步且以流处理为中心。

构建一个Spring Boot资源服务器

从GitHub仓库克隆启动项目,并检出start分支:

git clone -b start https://github.com/oktadeveloper/okta-spring-boot-mongo-webflux-example.git

入门项目是一个简单的Spring Boot入门项目,在build.gradle文件中已经具有必需的依赖build.gradle

让我们快速看一下相关性:

compile('org.springframework.boot:spring-boot-starter-webflux')
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
compileOnly('org.projectlombok:lombok')
compile('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

第一个是针对Spring WebFlux(Spring MVC的响应版本)。 第二个引入了Spring需要的反应性MongoDB依赖关系。 第三个是名为Lombok的项目,该项目使我们免于在Java代码中键入一堆构造函数,getter和setter(您可以在其网页上查看该项目)。 最后一个依赖项是嵌入式的内存MongoDB数据库。 这个数据库非常适合测试,像这样的简单教程,并且不持久。

可以使用简单的Gradle命令运行该应用程序:

./gradlew bootRun

当然,如果您此时运行该应用程序,则不会做太多事情。 Spring Boot将加载,但是还没有定义任何控制器,资源或存储库,因此没有太多事情发生。

为MongoDB定义模型类

为了清楚起见,本教程将与我之前提到的本系列的第一部分并行。 您将要构建一个存储皮划艇类型的简单服务器。 我总是建议首先定义数据结构来开始任何项目。

com.okta.springbootmongo包中创建一个Kayak.java类文件:

package com.okta.springbootmongo;  import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;  @Document
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Kayak {  private String name;  private String owner;  private Number value;  private String makeModel;
}

@Document注释与@Entity的NoSQL等效。 它告诉Spring Boot此类正在定义数据模型。 在NoSQL世界中,这意味着创建文档而不是表条目。 其他三个注释是Lombok帮助器,它们自动生成getter,setter和构造函数。

皮划艇文档具有五个属性:名称,所有者,值和类型。 这些会自动映射到MongoDB的适当BSON类型。 什么是BSON类型? 看看有关该主题的MongoDB文档 。 它们是用于将数据保留在MongoDB文档中的二进制序列化类型。 它们定义了可以存储在MongoDB数据库中的原始类型。

将ReactiveMongoRepository添加到您的Spring Boot应用程序

使用@Document批注定义Kayak类可以使Spring Boot知道数据的结构,但实际上并没有提供任何从数据库保存或加载数据的方法。 为此,您需要定义一个存储库。

这样做的代码非常简单。 在com.okta.springbootmongo包中创建一个KayakRepository.java类文件:

package com.okta.springbootmongo;  import org.springframework.data.mongodb.repository.ReactiveMongoRepository;  public interface KayakRepository extends ReactiveMongoRepository<Kayak, Long> {
}

实际上,这为您提供了从数据库创建,更新,读取和删除文档所需的所有基本方法。 要了解操作方法,请深入研究ReactiveMongoRepository类和其他各种超类,尤其是ReactiveCrudRepository 。 查看ReactiveCrudRepository 的文档以查看已实现的方法。

ReactiveCrudRepository实际上提供了一组基本而完整的CRUD方法。 ReactiveMongoRepository在此基础上构建,以提供一些特定于MongoDB的查询功能。

使用Spring WebFlux实现控制器

添加存储库后,您有足够的能力以编程方式处理数据。 但是,没有定义Web端点。 在前面的教程中,添加REST端点,这是需要的所有是对加@RepositoryRestResource注释到KayakRepository类。 这通过所有CRUD方法为我们自动生成了功能齐全的REST资源。 但是,此快捷方式不适用于Spring WebFlux。 必须明确定义任何公共Web终结点。

添加以下KayakController.java

package com.okta.springbootmongo;  import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;  @Controller
@RequestMapping(path = "/kayaks")
public class KayakController {  private KayakRepository kayakRepository;  public KayakController(KayakRepository kayakRepository) {this.kayakRepository = kayakRepository;}@PostMapping()  public @ResponseBody  Mono<Kayak> addKayak(@RequestBody Kayak kayak) {  return kayakRepository.save(kayak);  }  @GetMapping()  public @ResponseBody  Flux<Kayak> getAllKayaks() {  return kayakRepository.findAll();  }
}

该控制器添加了两个端点:

  • POST /kayaks增加了新的皮划艇
  • GET /kayaks列出所有皮划艇

您还将注意到,该类使用Spring依赖项注入将KayakRepository实例自动KayakRepository到控制器中,并且您将看到如何使用存储库持久化Kayak域类。

此类看起来非常像关系的,阻止的版本。 许多幕后工作使这种情况成为现实。 不用担心,但是,这是100%无反应的非阻塞代码。

测试您的Spring Boot服务器

至此,您已经可以完全运行皮划艇REST资源服务器。 在测试之前,请将以下方法添加到MainApplication类。 只需在应用程序加载时将一些测试数据注入数据库。

@Bean
ApplicationRunner init(KayakRepository repository) {  Object[][] data = {  {"sea", "Andrew", 300.12, "NDK"},  {"creek", "Andrew", 100.75, "Piranha"},  {"loaner", "Andrew", 75, "Necky"}  };  return args -> {  repository  .deleteAll()  .thenMany(  Flux  .just(data)  .map(array -> {  return new Kayak((String) array[0], (String) array[1], (Number) array[2], (String) array[3]);  })  .flatMap(repository::save)  )  .thenMany(repository.findAll())  .subscribe(kayak -> System.out.println("saving " + kayak.toString()));};
}

HTTPie是一个很棒的命令行实用工具,它使对资源服务器的请求运行变得容易。 如果未安装HTTPie,请使用brew install httpie进行brew install httpie 。 或前往他们的网站并实现它。 或者只是跟随。

确保您的Spring Boot应用正在运行。 如果不是,请使用./gradlew bootRun启动它。

针对您的资源服务器运行GET请求: http :8080/kayaks ,这是http GET http://localhost:8080/kayaks简写。

您会得到:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
transfer-encoding: chunked
[{"makeModel": "NDK","name": "sea","owner": "Andrew","value": 300.12},{"makeModel": "Piranha","name": "creek","owner": "Andrew","value": 100.75},{"makeModel": "Necky","name": "loaner","owner": "Andrew","value": 75}
]

现在尝试将新的皮划艇过帐到服务器。

http POST :8080/kayaks name="sea2" owner="Andrew" value="500" makeModel="P&H"

您应该看到:

HTTP/1.1 200 OK
Content-Length: 62
Content-Type: application/json;charset=UTF-8
{"makeModel": "P&H","name": "sea2","owner": "Andrew","value": 500
}

如果您重复GET请求http :8080/kayaks ,您将在列表中看到新的皮划艇!

设置安全认证

现在,您需要集成Okta for OAuth 2.0并将基于令牌的身份验证添加到资源服务器。 本部分与本教程第1部分中的部分完全相同,因此,如果您已完成此操作,则只需要您的客户ID,就可以跳至下一部分。

如果尚未注册,请访问developer.okta.com并注册一个免费帐户。 拥有帐户后,通过单击“ 应用程序”顶部菜单项,然后单击“ 添加应用程序”按钮,打开开发人员仪表板并创建OpenID Connect(OIDC)应用程序


选择单页应用程序


默认应用程序设置很好,除了您需要添加登录重定向URIhttps://oidcdebugger.com/debug : https://oidcdebugger.com/debug 。 您将在稍后使用它来检索测试令牌。

注意 :如果要实现像Angular或React这样的前端,则可能需要根据所使用的平台来更新默认的登录重定向URI。 由于本教程仅创建没有前端的资源服务器,因此暂时不重要。 我们所有的资源服务器将要做的就是使用授权服务器验证JSON Web令牌,而无需重定向。

另外,请注意您的客户ID ,稍后您将需要它。


配置您的Spring Boot服务器以进行令牌认证

现在,您需要更新一些项目文件以为OAuth 2.0配置Spring Boot。

将以下依赖项添加到您的build.gradle文件中:

dependencies {...compile('com.okta.spring:okta-spring-boot-starter:1.1.0')...
}

创建一个名为src/main/resources/application.yml的新配置文件

okta:oauth2:issuer: https://{yourOktaDomain}/oauth2/defaultgroupsClaim: groupsclientId: {yourClientId}

com.okta.springbootmongo包中创建一个SecurityConfiguration.java类:

package com.okta.springbootmongo;import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;@EnableWebFluxSecurity
public class SecurityConfiguration {@Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt();return http.build();}
}

测试您的受保护服务器

停止您的Spring Boot服务器并使用以下./gradlew bootRun重新启动它: ./gradlew bootRun

从命令行运行一个简单的GET请求。

http :8080/kayaks

您会得到未经授权的401 /。

HTTP/1.1 401 Unauthorized
Cache-Control: no-store
Content-Type: application/json;charset=UTF-8

生成访问令牌

要立即访问服务器,您需要一个有效的访问令牌。 您可以使用OpenID Connect调试器来帮助您完成此任务。 在另一个窗口中,打开oidcdebugger.com 。

  • 授权URIhttps://{yourOktaDomain} /oauth2/default/v1/authorize
  • 重定向URI :不变。 这是您在上面的OIDC应用程序中添加的值。
  • 客户ID :来自您刚创建的OIDC应用程序。
  • 范围openid profile email
  • 状态 :您要通过OAuth重定向过程传递的任何值。 我将其设置为{}
  • Nonce :可以一个人呆着。 Nonce表示“编号已使用一次”,是一种简单的安全措施,用于防止同一请求被多次使用。
  • 响应类型token
  • 响应方式form_post

点击发送请求 。 如果您尚未登录developer.okta.com,则需要登录。如果(可能的话)已经登录,则将为您的登录身份生成令牌。


使用您的访问令牌

您可以通过在Bearer类型的Authorization请求标头中包含令牌来使用令牌。

将令牌存储在shell变量中:

TOKEN=eyJraWQiOiJldjFpay1DS3UzYjJXS3QzSVl1MlJZc3VJSzBBYUl3NkU4SDJfNVJr...

然后使用HTTPie发出GET请求:

http :8080/kayaks "Authorization: Bearer $TOKEN"

请注意上面的双引号。 单引号不起作用,因为shell变量未扩展。

添加基于组的授权

现在,您将通过添加基于组成员身份来控制对特定控制器端点的访问的功能,来使授权方案更加完善。

要在Okta中使用基于组的授权,您需要在访问令牌中添加一个“组”声明。 创建一个Admin组(“ 用户” >“ 组” >“ 添加组” )并将您的用户添加到其中。 您可以使用注册时使用的帐户,也可以创建一个新用户(“ 用户” >“ 添加人” )。 导航到“ API” >“ 授权服务器” ,单击“ 授权服务器”选项卡,然后编辑默认选项卡。 点击索赔标签,然后添加索赔 。 将其命名为“组”,并将其包含在访问令牌中。 将值类型设置为“ Groups”,并将过滤器设置为.*的正则表达式。

声明包含用户分配到的组。 您用来登录developer.okta.com网站的默认用户也将是“所有人”组和“管理员”组的成员。

还需要更新SecurityConfiguration类以使用基于组的授权。 更新Java文件以匹配以下内容:

package com.okta.springbootmongo;import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {@Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {http.authorizeExchange().pathMatchers(HttpMethod.POST, "/kayaks/**").hasAuthority("Admin").anyExchange().authenticated().and().oauth2ResourceServer().jwt();return http.build();}
}

用简单的英语来说,这告诉Spring Boot要求对/kayak端点的任何POST都具有组成员身份Admin ,对于所有其他请求,仅需要有效的JWT。

您的基于组的授权策略由以下两行定义:

.pathMatchers(HttpMethod.POST, "/kayaks/**").hasAuthority("Admin")
.anyExchange().authenticated()

有关更多信息,请参阅ServerHttpSecurity类的文档 。

您可能想知道为什么它说hasAuthority()而不是hasRole()hasGroup() 。 这是因为授权是Spring调用服务器发送的文本字符串以表示权限成员身份的方式,无论是角色还是组。 hasRole()假定角色采用特定格式:“ ROLE_ADMIN”。 可以重写此方法,但是hasAuthority()是直接使用授权字符串的简单方法。 没有hasGroup()方法,因为前两个示例(如果未明确说明)涵盖了该用例。

创建一个非管理员用户

要测试基于组的授权方案,您需要一个不是管理员的用户。 转到developer.okta.com仪表板。

从顶部菜单中,选择“ 用户” >“ 人员” 。 单击添加人按钮。

给用户一个名字姓氏用户名 (也将是主要电子邮件 )。 值无关紧要,并且您将不必检查电子邮件。 您只需要知道电子邮件地址/用户名和密码,即可在一分钟内登录Okta。

密码 :将下拉菜单更改为“ 由管理员设置”

为用户分配密码。

点击保存

您刚刚创建的用户不是Admin组的成员,而是默认组Everyone的成员。

基于测试组的授权

注销您的Okta开发人员仪表板。

返回OIDC调试器并生成一个新令牌。

这次,以新的非管理员用户身份登录。 系统会要求您选择一个安全问题,然后将您重定向到https://oidcdebugger.com/debug页面,您可以在其中复制令牌。

如果愿意,可以转到jsonwebtoken.io并解码新令牌。 在有效内容中, 声明将显示用户的电子邮件/用户名,而声明将仅显示“ 所有人”组。

{"sub": "test@gmail.com","groups": ["Everyone"]
}

根据许可方案,该用户应该能够列出所有皮划艇,但不能添加新的皮划艇。

请记住,将令牌存储在shell脚本中,如下所示:

TOKEN=eyJraWQiOiI4UlE5REJGVUJOTnJER0VGaEExekd6bWJqREpSYTRTT1lhaGpsM3d4...

发出GET请求以列出所有皮划艇:

http :8080/kayaks "Authorization: Bearer $TOKEN"HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...[{"makeModel": "NDK","name": "sea","owner": "Andrew","value": 300.12},{"makeModel": "Necky","name": "loaner","owner": "Andrew","value": 75},{"makeModel": "Piranha","name": "creek","owner": "Andrew","value": 100.75}
]

尝试使用非管理员用户令牌添加新的皮划艇:

http POST :8080/kayaks "Authorization: Bearer $TOKEN" name="sea2" owner="Andrew" value="500" makeModel="P&H"

您将被拒绝!

HTTP/1.1 403 Forbidden
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Expires: 0
...

现在,注销developer.okta.com,并使用OIDC Debugger生成一个新令牌。 这次使用您原来的管理员帐户重新登录。

将新令牌存储在shell变量TOKEN

运行POST请求:

http POST :8080/kayaks "Authorization: Bearer $TOKEN" name="sea2" owner="Andrew" value="500" makeModel="P&H"

AM!

使用Spring Boot和MongoDB构建一个反应式应用程序相关推荐

  1. 使用Spring Boot和MongoDB构建一个React式应用程序

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 如果您要 ...

  2. Spring Boot——2分钟构建spring web mvc REST风格HelloWorld

    Spring Boot--2分钟构建spring web mvc REST风格HelloWorld 之前有一篇<5分钟构建spring web mvc REST风格HelloWorld>介 ...

  3. 使用 Node.js、Express、AngularJS 和 MongoDB 构建一个Web程序

    为什么80%的码农都做不了架构师?>>>    使用 Node.js.Express.AngularJS 和 MongoDB 构建一个实时问卷调查应用程序 2014 年 3 月 20 ...

  4. MongoDB最简单的入门教程之四:使用Spring Boot操作MongoDB

    Spring Boot 是一个轻量级框架,可以完成基于 Spring 的应用程序的大部分配置工作.Spring Boot的目的是提供一组工具,以便快速构建容易配置的Spring应用程序,省去大量传统S ...

  5. 使用Spring Boot和Kubernetes构建微服务架构

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 在本教程 ...

  6. 使用PostgreSQL使用Spring Boot和JPA构建基本应用

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 每个不平 ...

  7. 6.3 Spring Boot集成mongodb开发

    6.3 Spring Boot集成mongodb开发 本章我们通过SpringBoot集成mongodb,Java,Kotlin开发一个极简社区文章博客系统. 0 mongodb简介 Mongo 的主 ...

  8. 使用Spring Boot和GraphQL构建安全的API

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. Grap ...

  9. 使用Spring Boot和MongoDB快速进行Web应用原型设计

    回到我以前的项目之一,我被要求制作一些应急申请. 时间表紧张,范围简单. 内部编码标准是PHP,因此尝试建立经典的Java EE堆栈将是一个真正的挑战. 而且,说实话,完全过大了. 那怎么办 我趁机尝 ...

最新文章

  1. 一个管理者的反思(太深刻了!)
  2. linux下安装expect解决方法
  3. 对象类什么是面向对象(1)
  4. CF 19D Points 【线段树+平衡树】
  5. DockOne微信分享(一一二):Flannel中vxlan backend的原理和实现
  6. 系统集成项目管理工程师-变更管理笔记
  7. 技术久了,偶尔放松下吧,看看这些小样,很美好!
  8. Label 表达式绑定
  9. 对自学还是培训的看法
  10. 查询集 QuerySet
  11. LeetCode刷题——62. 不同路径
  12. js实现checkbox全选、不选与反选
  13. SpringBoot2.0高级案例(02) :整合 RocketMQ ,实现请求异步处理
  14. RSA加密解密及RSA加签验签
  15. 监控一个大事务的回滚
  16. 2060显卡驱动最新版本_如何更新你的显卡驱动程序
  17. java计算机毕业设计用户行为自动化书籍推荐系统MyBatis+系统+LW文档+源码+调试部署
  18. php表格合并_合并表格怎么合并
  19. uni-app前端解决跨域的问题
  20. 数字证书申请流程(双证)

热门文章

  1. AtCoder Beginner Contest 179 总结
  2. GDKOI2021总结
  3. Javafx的WebEngine的url加载不输出结果坑,gc回收了局部变量
  4. Java通过Class的对象来获取泛型的class示例
  5. Vue及React脚手架安装
  6. 什么时候才能都及格呢?
  7. 2017蓝桥杯省赛---java---B---10(k倍区间)
  8. php类常量的特点,php类常量是什么?类常量用法详解
  9. 包+类导入+静态导入+类放入包中+包作用域
  10. 朝着理想坚实迈进_坚实原则:开放/封闭原则