开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring Boot 配置中的敏感信息如何保护?

发表于 2021-08-16

在之前的系列教程中,我们已经介绍了非常多关于Spring Boot配置文件中的各种细节用法,比如:参数间的引用、随机数的应用、命令行参数的使用、多环境的配置管理等等。

这些配置相关的知识都是Spring Boot原生就提供的,而今天我们将介绍的功能并非Spring Boot原生就支持,但却非常有用:配置内容的加密。

为什么要加密?

可能很多初学者,对于配置信息的加密并不敏感,因为开始主要接触本地的开发,对于很多安全问题并没有太多的考虑。而现实中,我们的配置文件中,其实包含着大量与安全相关的敏感信息,比如:数据库的账号密码、一些服务的密钥等。这些信息一旦泄露,对于企业的重要数据资产,那是相当危险的。 所以,对于这些配置文件中存在的敏感信息进行加密,是每个成熟开发团队都一定会去的事。

如果您是DD的老读者,也许马上会想到Spring Cloud Config就提供配置的加密功能,之前在我的Spring Cloud系列教程和《Spring Cloud微服务实战》一书中都有详细的介绍,感兴趣的话可以点击《Spring Cloud构建微服务架构:分布式配置中心(加密解密)》一探究竟。

既然以前写过类似内容,那为什么还要写呢?因为并不是所有的开发场景都会搭建Spring Cloud的那套基础设施,同时也不一定会使用Spring Cloud Config作为配置中心。所以,本文主要说说,当我们只使用Spring Boot的时候,如何实现对配置中敏感信息的加密。

动手试试

下面我们将使用https://github.com/ulisesbocchio/jasypt-spring-boot这个开源项目提供的实现和插件,来帮助我们轻松的完成配置信息的加密。

赶紧跟着我下面的步骤动手试试吧!

第一步:创建一个基础的Spring Boot项目(如果您还不会,可以参考这篇文章:快速入门

第二步:设计一个参数和单元测试,用来输出这个配置信息

准备加密的配置:

1
properties复制代码datasource.password=didispace.com

用来输出配置信息的单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码@Slf4j
@SpringBootTest
public class PropertiesTest {

@Value("${datasource.password:}")
private String password;

@Test
public void test() {
log.info("datasource.password : {}", password);
}

}

执行这个单元测试,会输出:

1
bash复制代码2021-08-13 22:28:45.506  INFO 70405 --- [           main] com.didispace.chapter15.PropertiesTest   : datasource.password : didispace.com

这里还没开始加密,下面我们开始引入加密的操作!

第三步:在pom.xml中引入jasypt提供的Spring Boot Starter

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

在插件配置中加入:

1
2
3
4
5
xml复制代码<plugin>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-maven-plugin</artifactId>
<version>3.0.3</version>
</plugin>

第四步:在配置文件中加入加密需要使用的密码

1
properties复制代码jasypt.encryptor.password=didispace

同时,修改要加密的内容,用DEC()将待加密内容包裹起来,比如:

1
properties复制代码datasource.password=DEC(didispace.com)

第五步:使用jasypt-maven-plugin插件来给DEC()包裹的内容实现批量加密。

在终端中执行下面的命令:

1
bash复制代码mvn jasypt:encrypt -Djasypt.encryptor.password=didispace

注意:这里-Djasypt.encryptor.password参数必须与配置文件中的一致,不然后面会解密失败。

执行之后,重新查看配置文件,可以看到,自动变成了

1
2
3
properties复制代码datasource.password=ENC(/AL9nJENCYCh9Pfzdf2xLPsqOZ6HwNgQ3AnMybFAMeOM5GphZlOK6PxzozwtCm+Q)

jasypt.encryptor.password=didispace

其中,ENC()跟DEC()一样都是jasypt提供的标识,分别用来标识括号内的是加密后的内容和待加密的内容。

如果当前配置文件已经都是ENC()内容了,那么我们可以通过下面的命令来解密配置文件,查看原始信息:

1
bash复制代码mvn jasypt:decrypt -Djasypt.encryptor.password=didispace

该操作不会修改配置文件,只会在控制台输出解密结果,比如:

1
2
3
properties复制代码datasource.password=DEC(didispace.com)

jasypt.encryptor.password=didispace

第六步:此时,我们的配置文件中的敏感信息已经被ENC()修饰了,再执行一下单元测试,不出意外的话,依然可以得到之前一样的结果:

1
yaml复制代码2021-08-13 22:50:00.463  INFO 76150 --- [           main] com.didispace.chapter15.PropertiesTest   : datasource.password : didispace.com

而此时,配置文件中已经是加密内容了,敏感信息得到了保护。

本系列教程《Spring Boot 2.x基础教程》点击直达! ,欢迎收藏与转发!如果学习过程中如遇困难?可以加入我们Spring技术交流群 ,参与交流与讨论,更好的学习与进步!

进一步思考

根据上面的步骤,爱思考的你,也许会发现这样的问题:虽然敏感信息是加密了,但是我们通过配置文件也能看到jasypt.encryptor.password信息,我们是不是通过利用这个再把原始信息解密出来,这样的话岂不是还是不安全?

上面的实现方式的确是会有这样的问题!所以,在实际应用的过程中,jasypt.encryptor.password的配置,可以通过运维小伙伴在环境变量或启动参数中注入,而不是由开发人员在配置文件中指定。

同时,为了应对更高的安全要求,jasypt也提供自定义的加密解密方式,这里就不做具体展开了,有兴趣的小伙伴可以前往jasypt的仓库查看使用细节。

代码示例

本文的完整工程可以查看下面仓库中2.x目录下的chapter1-5工程:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,分享外面看不到的干货与思考!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot:三十五道SpringBoot面试题及答

发表于 2021-08-16

本文已参与掘金创作者训练营第三期,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

前言

手执烟花以谋生 心怀诗意以谋爱

祝全世界的男孩子脸上没有痘痘,头发没有油,王者不掉星,吃鸡不被杀,联盟拿S,穿越全点头,有一个超级暖,超级听好,超级好的女朋友。

今天博主将为大家分享三十五道SpringBoot面试题及答案,不喜勿喷,如有异议欢迎讨论!

Spring Boot 是微服务中最好的 Java 框架之一. 我们建议你能够成为一名 Spring Boot 的专家。本文精选了三十五个常见的 Spring Boot 知识点,祝你一臂之力!


问题一:

Spring Boot、Spring MVC 和 Spring 有什么区别?
  1. Spring
    Spring最重要的特征是依赖注入。所有 SpringModules 不是依赖注入就是 IOC 控制反转。
    当我们恰当的使用 DI 或者是 IOC 的时候,我们可以开发松耦合应用。松耦合应用的单元测试可以很容易的进行。
  2. Spring MVC
    Spring MVC 提供了一种分离式的方法来开发 Web 应用。通过运用像 DispatcherServelet,MoudlAndView 和 ViewResolver 等一些简单的概念,开发 Web 应用将会变的非常简单。
  3. SpringBoot
    Spring 和 SpringMVC 的问题在于需要配置大量的参数。**
    在这里插入图片描述Spring Boot 通过一个自动配置和启动的项来目解决这个问题。为了更快的构建产品就绪应用程序,Spring Boot 提供了一些非功能性特征。

问题二:

什么是自动配置?

Spring 和 SpringMVC 的问题在于需要配置大量的参数。
在这里插入图片描述

我们能否带来更多的智能?当一个 MVC JAR 添加到应用程序中的时候,我们能否自动配置一些 beans?

Spring 查看(CLASSPATH 上可用的框架)已存在的应用程序的配置。在此基础上,Spring Boot 提供了配置应用程序和框架所需要的基本配置。这就是自动配置。


问题三:

什么是自动配置?

什么是 Spring Boot Stater ?

启动器是一套方便的依赖没描述符,它可以放在自己的程序中。你可以一站式的获取你所需要的 Spring 和相关技术,而不需要依赖描述符的通过示例代码搜索和复制黏贴的负载。

例如,如果你想使用 Sping 和 JPA 访问数据库,只需要你的项目包含 spring-boot-starter-data-jpa 依赖项,你就可以完美进行。


问题四:

你能否举一个例子来解释更多 Staters 的内容?

让我们来思考一个 Stater 的例子 -Spring Boot Stater Web。

如果你想开发一个 web 应用程序或者是公开 REST 服务的应用程序。Spring Boot Start Web 是首选。让我们使用 Spring Initializr 创建一个 Spring Boot Start Web 的快速项目。

Spring Boot Start Web 的依赖项。

在这里插入图片描述
下面的截图是添加进我们应用程序的不同的依赖项

在这里插入图片描述
依赖项可以被分为:

  • Spring - core,beans,context,aop
  • Web MVC - (Spring MVC)
  • Jackson - for JSON Binding
  • Validation - Hibernate,Validation API
  • Enbedded Servlet Container - Tomcat
  • Logging - logback,slf4j

任何经典的 Web 应用程序都会使用所有这些依赖项。Spring Boot Starter Web 预先打包了这些依赖项。

作为一个开发者,我不需要再担心这些依赖项和它们的兼容版本。


问题五:

Spring Boot 还提供了其它的哪些 Starter Project Options?
  • spring-boot-starter-web-services - SOAP Web Services;
  • spring-boot-starter-web - Web 和 RESTful 应用程序;
  • spring-boot-starter-test - 单元测试和集成测试;
  • spring-boot-starter-jdbc - 传统的 JDBC;
  • spring-boot-starter-hateoas - 为服务添加 HATEOAS 功能;
  • spring-boot-starter-security - 使用 SpringSecurity 进行身份验证和授权;
  • spring-boot-starter-data-jpa - 带有 Hibeernate 的 Spring Data JPA;
  • spring-boot-starter-data-rest - 使用 Spring Data REST 公布简单的 REST 服务;

问题六:

Spring 是如何快速创建产品就绪应用程序的?

Spring Boot 致力于快速产品就绪应用程序。为此,它提供了一些譬如高速缓存,日志记录,监控和嵌入式服务器等开箱即用的非功能性特征。

  • spring-boot-starter-actuator - 使用一些如监控和跟踪应用的高级功能
  • spring-boot-starter-undertow, spring-boot-starter-jetty, spring-boot-starter-tomcat - 选择您的特定嵌入式 Servlet 容器
  • spring-boot-starter-logging - 使用 logback 进行日志记录
  • spring-boot-starter-cache - 启用 Spring Framework 的缓存支持
  • ###Spring2 和 Spring5 所需要的最低 Java 版本是什么?
  • Spring Boot 2.0 需要 Java8 或者更新的版本。Java6 和 Java7 已经不再支持。

推荐阅读:

github.com/spring-proj…


问题七:

创建一个 Spring Boot Project 的最简单的方法是什么?

Spring Initializr是启动 Spring Boot Projects 的一个很好的工具。

  • 登录 Spring Initializr,按照以下方式进行选择:
  • 选择 com.in28minutes.springboot 为组
  • 选择 studet-services 为组件
  • 选择下面的依赖项
  • Web
  • Actuator
  • DevTools
  • 点击生 GenerateProject
  • 将项目导入 Eclipse。文件 - 导入 - 现有的 Maven 项目

问题八:

Spring Initializr 是创建 Spring Boot Projects 的唯一方法吗?

Spring Initiatlizr 让创建 Spring Boot 项目变的很容易,但是,你也可以通过设置一个 maven 项目并添加正确的依赖项来开始一个项目。

在我们的 Spring 课程中,我们使用两种方法来创建项目。

第一种方法是 start.spring.io 。

另外一种方法是在项目的标题为“Basic Web Application”处进行手动设置。

手动设置一个 maven 项目

这里有几个重要的步骤:

  • 在 Eclipse 中,使用文件 - 新建 Maven 项目来创建一个新项目
  • 添加依赖项。
  • 添加 maven 插件。
  • 添加 Spring Boot 应用程序类。

到这里,准备工作已经做好!


问题九:

为什么我们需要 spring-boot-maven-plugin ?

spring-boot-maven-plugin 提供了一些像 jar 一样打包或者运行应用程序的命令。

  • spring-boot:run 运行你的 SpringBooty 应用程序。
  • spring-boot:repackage 重新打包你的 jar 包或者是 war 包使其可执行
  • spring-boot:start 和 spring-boot:stop 管理 Spring Boot 应用程序的生命周期(也可以说是为了集成测试)。
  • spring-boot:build-info 生成执行器可以使用的构造信息。

问题十:

如何使用 SpringBoot 自动重装我的应用程序?

使用 Spring Boot 开发工具。

把 Spring Boot 开发工具添加进入你的项目是简单的。

把下面的依赖项添加至你的 Spring Boot Project pom.xml 中

在这里插入图片描述

重启应用程序,然后就可以了。

同样的,如果你想自动装载页面,有可以看看 FiveReload

www.logicbig.com/tutorials/s….

在我测试的时候,发现了 LiveReload 漏洞,如果你测试时也发现了,请一定要告诉我们。


问题十一:

什么是嵌入式服务器?我们为什么要使用嵌入式服务器呢?

思考一下在你的虚拟机上部署应用程序需要些什么。

第一步: 安装 Java

第二部: 安装 Web 或者是应用程序的服务器(Tomat/Wbesphere/Weblogic 等等)

第三部: 部署应用程序 war 包

如果我们想简化这些步骤,应该如何做呢?

让我们来思考如何使服务器成为应用程序的一部分?

你只需要一个安装了 Java 的虚拟机,就可以直接在上面部署应用程序了,
是不是很爽?

这个想法是嵌入式服务器的起源。

当我们创建一个可以部署的应用程序的时候,我们将会把服务器(例如,tomcat)嵌入到可部署的服务器中。

例如,对于一个 Spring Boot 应用程序来说,你可以生成一个包含 Embedded Tomcat 的应用程序 jar。你就可以想运行正常 Java 应用程序一样来运行 web 应用程序了。

嵌入式服务器就是我们的可执行单元包含服务器的二进制文件(例如,tomcat.jar)。


问题十二:

如何在 Spring Boot 中添加通用的 JS 代码?

在源文件夹下,创建一个名为 static 的文件夹。然后,你可以把你的静态的内容放在这里面。

例如,myapp.js 的路径是 resources\static\js\myapp.js,你可以参考它在 jsp 中的使用方法:

在这里插入图片描述

错误:HAL browser gives me unauthorized error - Full authenticaition is required to access this resource.,该如何来修复这个错误呢?

在这里插入图片描述

两种方法:

  • 方法 1:关闭安全验证
    application.properties
    management.security.enabled:FALSE
  • 方法二:在日志中搜索密码并传递至请求标头中

问题十三:

什么是 Spring Data?

来自:https//projects.spring.io/spring-data/

Spring Data 的使命是在保证底层数据存储特殊性的前提下,为数据访问提供一个熟悉的,一致性的,基于 Spring 的编程模型。这使得使用数据访问技术,关系数据库和非关系数据库,map-reduce 框架以及基于云的数据服务变得很容易。

为了让它更简单一些,Spring Data 提供了不受底层数据源限制的 Abstractions 接口。
在这里插入图片描述

你可以定义一简单的库,用来插入,更新,删除和检索代办事项,而不需要编写大量的代码。


问题十四:

什么是 Spring Data REST?

Spring Data TEST 可以用来发布关于 Spring 数据库的 HATEOAS RESTful 资源。

在这里插入图片描述

不需要写太多代码,我们可以发布关于 Spring 数据库的 RESTful API。

下面展示的是一些关于 TEST 服务器的例子

POST:

  • URL:http://localhost:8080/todos
  • Use Header:Content-Type:Type:application/json
  • Request Content

在这里插入图片描述

在这里插入图片描述


问题十五:

path=”users”, collectionResourceRel=”users” 如何与 Spring Data Rest 一起使用?

在这里插入图片描述

  • path- 这个资源要导出的路径段。
  • collectionResourceRel- 生成指向集合资源的链接时使用的 rel 值。在生成 HATEOAS 链接时使用。

问题十六:

当 Spring Boot 应用程序作为 Java 应用程序运行时,后台会发生什么?

如果你使用 Eclipse IDE,Eclipse maven 插件确保依赖项或者类文件的改变一经添加,就会被编译并在目标文件中准备好!在这之后,就和其它的 Java 应用程序一样了。

当你启动 java 应用程序的时候,spring boot 自动配置文件就会魔法般的启用了。

  • 当 Spring Boot 应用程序检测到你正在开发一个 web 应用程序的时候,它就会启动 tomcat。

问题十七:

我们能否在 spring-boot-starter-web 中用 jetty 代替 tomcat?

在 spring-boot-starter-web 移除现有的依赖项,并把下面这些添加进去。

在这里插入图片描述


问题十八:

如何使用 Spring Boot 生成一个 WAR 文件?

  • 推荐阅读:
  • spring.io/guides/gs/c…
  • 下面有 spring 说明文档直接的链接地址:
  • docs.spring.io/spring-boot…

问题十九:

如何使用 Spring Boot 部署到不同的服务器?

  • 在一个项目中生成一个 war 文件。
  • 将它部署到你最喜欢的服务器(websphere 或者 Weblogic 或者 Tomcat and so on)。

第一步:这本入门指南应该有所帮助:

  • spring.io/guides/gs/c…

第二步:取决于你的服务器。


问题二十:

RequestMapping 和 GetMapping 的不同之处在哪里?

  • RequestMapping 具有类属性的,可以进行 GET,POST,PUT 或者其它的注释中具有的请求方法。
  • GetMapping 是 GET 请求方法中的一个特例。它只是 ResquestMapping 的一个延伸,目的是为了提高清晰度。

问题二十一:

为什么我们不建议在实际的应用程序中使用 Spring Data Rest?

我们认为 Spring Data Rest 很适合快速原型制造!在大型应用程序中使用需要谨慎。

通过 Spring Data REST 你可以把你的数据实体作为 RESTful 服务直接发布。

当你设计 RESTful 服务器的时候,最佳实践表明,你的接口应该考虑到两件重要的事情:

  • 你的模型范围。
  • 你的客户。

通过 With Spring Data REST,你不需要再考虑这两个方面,只需要作为 TEST 服务发布实体。

这就是为什么我们建议使用 Spring Data Rest 在快速原型构造上面,或者作为项目的初始解决方法。对于完整演变项目来说,这并不是一个好的注意。


问题二十二:

在 Spring Initializer 中,如何改变一个项目的包名字?

好消息是你可以定制它。点击链接“转到完整版本”。你可以配置你想要修改的包名称!


问题二十三:

可以配置 application.propertierde 的完整的属性列表在哪里可以找到?

  • docs.spring.io/spring-boot…

问题二十四:

JPA 和 Hibernate 有哪些区别?

  • JPA 是一个规范或者接口
  • Hibernate 是 JPA 的一个实现

当我们使用 JPA 的时候,我们使用 javax.persistence 包中的注释和接口时,不需要使用 hibernate 的导入包。

我们建议使用 JPA 注释,因为哦我们没有将其绑定到 Hibernate 作为实现。后来(我知道 - 小于百分之一的几率),我们可以使用另一种 JPA 实现。


问题二十五:

业务边界应该从哪一层开始?

我们建议在服务层管理义务。商业业务逻辑在商业层或者服务层,与此同时,你想要执行的业务管理也在该层。


问题二十六:

使用 Spring Boot 启动连接到内存数据库 H2 的 JPA 应用程序需要哪些依赖项?

在 Spring Boot 项目中,当你确保下面的依赖项都在类路里面的时候,你可以加载 H2 控制台。

  • web 启动器
  • h2
  • jpa 数据启动器

在这里插入图片描述
需要注意的一些地方:

  • 一个内部数据内存只在应用程序执行期间存在。这是学习框架的有效方式。
  • 这不是你希望的真是世界应用程序的方式。
  • 在问题“如何连接一个外部数据库?”中,我们解释了如何连接一个你所选择的数据库。

问题二十七:

如何不通过任何配置来选择 Hibernate 作为 JPA 的默认实现?

在这里插入图片描述

spring-boot-stater-data-jpa 对于 Hibernate 和 JPA 有过渡依赖性。

当 Spring Boot 在类路径中检测到 Hibernate 中,将会自动配置它为默认的 JPA 实现。


问题二十八:

指定的数据库连接信息在哪里?它是如何知道自动连接至 H2 的?

这就是 Spring Boot 自动配置的魔力。

来自:docs.spring.io/spring-boot…

Spring Boot auto-configuration 试图自动配置你已经添加的基于 jar 依赖项的 Spring 应用程序。比如说,如果 HSQLDBis 存在你的类路径中,并且,数据库连接 bean 还没有手动配置,那么我们可以自动配置一个内存数据库。

  • 进一步的阅读:www.springboottutorial.com/spring-boot…

当 Spring Boot 在类路径中检测到 Hibernate 中,将会自动配置它为默认的 JPA 实现。


问题二十九:

我们如何连接一个像 MySQL 或者Orcale 一样的外部数据库?

第一步 - 把 mysql 连接器的依赖项添加至 pom.xml

在这里插入图片描述

第二步 - 从 pom.xml 中移除 H2 的依赖项

或者至少把它作为测试的范围。

在这里插入图片描述

第三步 - 安装你的 MySQL 数据库

  • 更多的来看看这里 github.com/in28minutes…
  • 进一步的阅读:www.springboottutorial.com/spring-boot…

第四步 - 配置你的 MySQL 数据库连接

配置 application.properties

1
2
3
4
ini复制代码spring.jpa.hibernate.ddl-auto=none 
spring.datasource.url=jdbc:mysql://localhost:3306/todo_example
spring.datasource.username=todouser
spring.datasource.password=YOUR_PASSWORD

第五步 - 重新启动,你就准备好了!


问题三十:

Spring Boot 配置的默认 H2 数据库的名字是上面?为什么默认的数据库名字是 testdb?

在 application.properties 里面,列出了所有的默认值

  • docs.spring.io/spring-boot…

找到下面的属性

1
2
3
ini复制代码#Name of the datasource.

spring.datasource.name=testdb

如果你使用了 H2 内部存储数据库,它里面确定了 Spring Boot 用来安装你的 H2 数据库的名字。


问题三十一:

如果 H2 不在类路径里面,会出现上面情况?

  • 将会报下面的错误

Cannot determine embedded database driver class for database type NONE

把 H2 添加至 pom.xml 中,然后重启你的服务器

在这里插入图片描述


问题三十二:

你能否举一个以 ReadOnly 为事务管理的例子?

  • 当你从数据库读取内容的时候,你想把事物中的用户描述或者是其它描述设置为只读模式,以便于 Hebernate 不需要再次检查实体的变化。这是非常高效的。

问题三十三:

发布 Spring Boot 用户应用程序自定义配置的最好方法是什么?

@Value 的问题在于,您可以通过应用程序分配你配置值。更好的操作是采取集中的方法。
你可以使用 @ConfigurationProperties 定义一个配置组件。

在这里插入图片描述

你可以在 application.properties 中配置参数。

1
2
3
4
5
yaml复制代码basic.value: true 

basic.message: Dynamic Message

basic.number: 100

问题三十四:

配置文件的需求是什么?

企业应用程序的开发是复杂的,你需要混合的环境:

  • Dev
  • QA
  • Stage
  • Production

在每个环境中,你想要不同的应用程序配置。

Spring 和 Spring Boot 提供了你可以制定的功能。

  • 不同配置文件中,不同环境的配置是什么?
  • 为一个制定的环境设置活动的配置文件。

Spring Boot 将会根据特定环境中设置的活动配置文件来选择应用程序的配置。


问题三十五:

如何使用配置文件通过 Spring Boot 配置特定环境的配置?

配置文件不是设别环境的关键。

在下面的例子中,我们将会用到两个配置文件

  • dev
  • prod

缺省的应用程序配置在 application.properties 中。让我们来看下面的例子:

application.properties

1
2
3
4
5
ini复制代码basic.value= true 

basic.message= Dynamic Message

basic.number= 100

我们想要为 dev 文件自定义 application.properties 属性。我们需要创建一个名为 application-dev.properties 的文件,并且重写我们想要自定义的属性。

application-dev.properties

1
makefile复制代码basic.message: Dynamic Message in DEV

一旦你特定配置了配置文件,你需要在环境中设定一个活动的配置文件。

有多种方法可以做到这一点:

  • 在 VM 参数中使用 Dspring.profiles.active=prod
  • 在 application.properties 中使用 spring.profiles.active=prod

到这里三十五道SpringBoot面试题及答案分享完毕了!


🎉最后

  • 更多参考精彩博文请看这里:陈永佳的博客
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flink 从0-1实现 电商实时数仓 - 项目概览

发表于 2021-08-16

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

概览

一、离线计算和实时计算的差别

离线计算:计算前就已知所以输入数据,且输入数据不会发生变化,一般计算量很大,计算时间也比较长。

一般都是根据前一日的数据生成报表,虽然统计指标、报表繁多,但是对着时效性不敏感。从技术角度来看,这属于批处理操作,就是确定好范围一次性计算。

实时计算:输入数据可以是一个一个输入,也就是说计算前并不知道所有的输入数据,与离线计算相比,运行时间短,计算量相对小。强调计算过程的时间要短,给出结果块。

主要侧重于对当日数据的实时监控,通常业务逻辑相对离线需求简单一下,统计指标也少一些,但是更注重数据的时效性,以及用户的交互性。从技术操作的角度,这部分属于流处理的操作。根据数据源源不断地到达进行实时的运算。

二、实时需求种类

1. 日常报表

  对于日常企业、网站的运营管理如果仅仅依靠离线计算,数据的时效性往往无法满足。通过实时计算获得当日、分钟级、秒级甚至亚秒的数据更加便于企业对业务进行快速反应与调整。

  所以实时计算结果往往要与离线数据进行合并或者对比展示在BI或者统计平台中。

2. 实时大屏展示

  数据大屏,相对于BI工具或者数据分析平台是更加直观的数据可视化方式。尤其是一些大促活动,已经成为必备的一种营销手段。

  另外还有一些特殊行业,比如交通、电信的行业,那么大屏监控几乎是必备的监控手段。

3. 数据预警或提示

  经过大数据实时计算得到的一些风控预警、营销信息提示,能够快速让风控或营销部分得到信息,以便采取各种应对。

  比如,用户在电商、金融平台中正在进行一些非法或欺诈类操作,那么大数据实时计算可以快速的将情况筛选出来发送风控部门进行处理,甚至自动屏蔽。 或者检测到用户的行为对于某些商品具有较强的购买意愿,那么可以把这些“商机”推送给客服部门,让客服进行主动的跟进。

4.实时推荐系统

  实时推荐就是根据用户的自身属性结合当前的访问行为,经过实时的推荐算法计算,从而将用户可能喜欢的商品、新闻、视频等推送给用户。

  这种系统一般是由一个用户画像批处理加一个用户行为分析的流处理组合而成。

三、架构图

实时-mini.png
后续补齐完整流程图

关注专栏持续更新 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数组、链表、队列和栈,四大基础数据结构详解

发表于 2021-08-16

本文为掘金社区首发签约文章,未获授权禁止转载。

序

开新坑了,这次是数据结构与算法专题,保证不鸽,此专题将会分为三部分:

  1. 基础数据结构 :除了本章标题中这些还会有哈希表、树、堆等数据结构。
  2. 排序算法 :单独介绍一些常用常见算法如,冒泡、选择、插入、归并、快排、堆排序等。
  3. 高级数据结构 :高级数据结构不是说它更高级,主要是在前文的基础数据结构上的扩展如,B+树(树中N树的一种)、红黑树(业界常用的自平衡树)和一些改进的散列如布谷鸟之类的。

当然上面这些只是我目前的写作计划,可能会加入更多内容,请大家拭目以待吧。


本章正式开始之前,先聊聊我对数据结构的看法吧,老生常谈的一句话是:

程序 = 数据结构 + 算法。

学习数据结构与算法对我们写程序的人有多么重要我举几个例子,大家就明白了。

首先大家要知道,数据结构和算法这些东西都是为了解决现实问题而生的,比如我们的数据库中有1000w条数据吧,我想瞬间查询到一个我想要的数据,这能做到吗?

如果你没有学过数据结构与算法,对查询这种事第一时间的反应应该是遍历,但是对1000w 的数据量进行遍历将会极慢极慢。

有更好的办法来解决这个问题吗?

有,可以通过哈希表数据结构组织这些数据,哈希表能够让我们以O(1) 的时间复杂度快速查询到我们任何想要的数据,无论是1000w还是2000w,都能在瞬间查询到任何我们想查询到的数据,这是这么令人惊叹的效果,中间件Redis 和国产数据库新秀TiDB中的查询都是通过哈希表做的。

除了哈希表,我们也可以通过树型数据结构来做这件事,比如关系型数据库中常用的B+索引,它就是一个树型结构,是一个N树,它能以O(logN) 的速度进行查询,虽然速度上比不上哈希表,但是它支持范围查询,这对数据库很重要,所以数据库一般选它为主键索引。

上面我简单举了两个数据结构的例子,那算法呢?很多时候,算法与数据结构是相辅相成的。

就比如树吧,它为什么查询快呢?

它使用的一种分治的算法思想,在数据查询中会使用二分算法不断缩小数据规模,达到一个O(logN) 时间复杂度。

通过树的变形,还催生了堆这一数据结构,堆就是我们常说的优先队列,通过堆这一数据结构的特点,还催生了堆排序这一说法,它在求 topK 的问题上,有着不错的效果。

说了树,那哈希表呢?

    哈希表的效果好不好,其实很大程度上取决于它的哈希算法好不好,在基本上所有语言中都内置了哈希表这种数据结构,如Java的HashMap,和一些脚本语言中的字典(Dict),它们都采用哈希表实现,其中Python2.7的字典还因为实现问题,导致过漏洞。

所以,数据结构和算法很重要,虽然我们平常不会直接使用,但是我们接触的很多类库和中间件中都有它们的影子,再从功利的角度讲,进大厂,算法已经是必考了。

废话不多说,开始吧~

注:在整个数据结构与算法专题中,我都会尽量以实际应用来举例,同时程序实现将会采用Java,相信前端读者们也都能看懂,JS都会了,Java有什么难的呢?

  1. 大O 表示法

任何一位开发工程师,哪怕没有科班背景,应该也或多或少的听说过大O 表示法。

这是一种用来衡量时间复杂度与空间复杂度的计数表示,说白了就是衡量算法时间长不长和占用内存多不多,在本文中主要用它来表示时间复杂度,暂不涉及空间复杂度。

比方说,我们要衡量一个算法快不快,那么你的第一反应可能是通过它的运行时间长不长来判断,但是同一段算法在老式奔腾处理器和最新的I9-9700K处理器上,运行时间肯定不同。

所以我们需要一个更精确一点的表示法,就是大O表示法,在大O表示法中,我们通过步数来衡量一个算法的快不快 。

1
2
3
java复制代码    public void test(int[] items) {

}

假设这是一个遍历算法,它需要访问items中的每一个元素,所以如果items的长度为N,那么遍历算法就需要执行N步,它的时间复杂度就是O(N)。

假设这是一个遍历算法,那么有一个输入N,它势必要访问每一个元素,所以它需要执行N步,那么它的时间复杂度就是O(N)。

大O表示法的时间复杂度大致可以分为以下几个级别:

  1. O(1) :常数级别,无论输入多大,它执行的步数都是恒定的,不会因为输入变大而变大,哈希表的查找就是这个级别。
  2. O(N) :线性级别,随着输入变大耗费的步数也正向相关,遍历算法就是这个级别。
  3. O(logN) :对数级别,输入每变大一倍,耗费步骤则增加1,二分查找算法属于这个级别。
  4. O(N²) :平方级别,随着输入的变大所耗费的步数会成倍增加,一般当你的算法使用双层for循环就是这个级别,比如冒泡排序。

此外还有立方级别,就不过多叙述,和平方级别一样,只不过更大。

上面的四种级别除了对数级别其他的应该都很好理解,对数级别涉及到一个数学符号log,它的底数是2,默认省略了。

可以通过一个小例子来理解对数级别:我们有一个排好序的数组,通过二分算法寻找其中一个数字。

假设数组中有16个数字,则需要查找4次,因为2的4次方是4。

假设数组中有2147483647个数字(21亿),则需要查找31次,因为2的31次方是2147483648,这已经是一个相当快的速度了。

所以对数级别你可以理解当规模翻一倍的时候,所需步数只用+1。


关于大O 表示法,我还有一点要强调,它并不像数学一样准确 。

假设你有一个算法每次需要进行2N次的遍历才能执行完,那么它的时间复杂度也是O(N),常数在大O 表示法中会被忽略,因为常数是一定的,不会变化。

再比如O(1),就算你没学过哈希表应该也能明白,不可能有任何算法只执行一个步骤就能查找到某个数字,应该是执行好几步,但是无论是执行5步也好,6步也罢,因为它的步数是恒定的且和输入规模无关,所以它是O(1)。

最后一点,同一段算法在不同输入下可能可能耗费的步数完全不同,所以大O表示法一般取一个一般值来表示,比如某些排序算法在输入逆序和顺序的情况下会呈现完全不同的时间复杂度,一般取耗费更多的那个,这点在排序篇再说吧。

  1. 数组

数组 是一种可以快速访问的数据结构,它也是数据结构的基石之一,所有语言都内置支持数组,它在内存中是一块连续的内存地址。

在现代编程语言中,往往使用数组作为一个容器,它可以方便的存储的成百上千个元素,如果没有数组,存储这么多的元素则需要成百上千个引用。

在数组中,每一个数组都有一个地址,同时可以通过数组的下标方便的算出每个元素的内存地址,从而实现快速访问和赋值,所以它通过下标查找的效率是O(1) 级别。

在数组的插入和删除中,如果是在数组尾部插入或删除一个元素则可以直接进行,但如果在其他位置插入或者删除则需要调整其他元素的位置,比如你删除数组的第一个元素之后,需要把后面的所有元素都往前移动一位。

数组还有一个巨大弱点就是当这个数组已经装满的时候,就无法再继续装入元素了。

在高级编程语言中,往往都使用动态数组来解决这个问题,所谓动态数组就是自动会进行数组扩容,当数组容量达到某个临界点的时候,动态数组会开辟一个更大的数组,然后将原来的元素复制过去。

Java中的ArrayList类就是这样做的。

接下来我会简单实现一个动态数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码public class DiyList<T> {

private Object[] items;

private int size = 0;

public DiyList() {
items = new Object[16];
}

public T get(int index) {
if (index > size) {
throw new NoSuchElementException();
}
return (T) items[index];
}

public boolean add(T item) {
if (Objects.isNull(item)) {
throw new NullPointerException();
}

if (size >= items.length / 2) {
grow();
}

items[size++] = item;
return true;
}

private void grow() {
Object[] newItems = new Object[items.length * 2];
System.arraycopy(items, 0 , newItems, 0 , items.length);
items = newItems;
}

public int size() {
return size;
}

}

由于Java中的范型会有泛型擦除,所以我无法定义一个范型数组,导致只能实现Object数组,在进行get的时候将此元素强转为泛型元素。

在这个例子中,我只实现了基本的get和add方法,动态扩容的最主要方法就是grow方法,它负责了数组的扩容,扩容方式也能简单,建立一个更大的数组,然后把原来的复制过来即可。

动态数组在帮你自动扩容的同时,也带来了复制元素的代价:

  1. 当数组需要扩容时,需要进行元素复制。
  2. 当删除数组元素时也需要进行元素复制,因为当一删除某一个元素后,数组中间就会有一个空位置,需要把后面的元素都往前挪一格。

所以动态数组比较适合使用下标查询,插入和删除都需要额外的时间消耗和空间消耗。

2.1 课后练习

在上述示例的基础上,补充delete方法,示例已经过自测,可直接copy运行。

  1. 链表

上一节数组是访问元素比较方便,这一节的链表 则是插入与删除更方便。

数组的元素地址是强制连续的,但是链表的元素内存地址可以是不连续的 ,它通过地址引用的方式指向下一个元素的位置,所以链表的数据结构看起来比较像一个串。

链表的代码结构一般是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class DiyLinked<T> {
private int size = 0;
private Node<T> item;

private static class Node<T> {
private T item;
private Node<T> next;
public Node(T t) {
item = t;
}
}
}

在Node元素中,除了本身的数据item之外,往往还会持有一个next对象,它用来指向下一个节点的引用。

链表的数据结构也注定了它无法快速的访问某个元素,只能依靠遍历的方式慢慢查找,所以说链表的查询数据是比较慢的,是O(N) 这个级别,但是它在头节点(第一个节点)进行插入和删除比较快,只需要O(1) 就可以了,因为你只需要改变一下第一个元素的引用。

我为什么要特意提到头节点呢, 因为如果你想要在尾节点进行插入和删除,势必需要遍历这个链表,找到最后这个链表节点再进行插入,遍历链表的花费是O(N)。

上面提到的这种只带一有一个next引用的链表叫做单向链表,为了解决尾插的问题,又出现了双向链表。

双向链表,也就是链表中有前后两个指针引用,分别指向前面的元素和后面的元素。

画作逐渐崩坏~

双向链表的代码一般是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class DiyLinked<T> {
private int size = 0;
private Node<T> first;
private Node<T> last;

private static class Node<T> {
private T item;
private Node<T> next;
private Node<T> prev;
public Node(T t) {
item = t;
}
}
}

双向链表会同时维护first 和 last 两个节点,所以当你需要尾插的时候,直接通过last节点进行插入或者删除。

Java中的LinkedList就是一个双向链表,实现也很简单,链表是数据结构的另一个基石,很多数据结构都可以在链表的基础上进行变形得来。

3.1 课后练习

实现一个翻转链表,给你一个链表的头节点,将其翻转一遍,返回一个新的链表,新链表的元素顺序相对于原先的来说应当是逆序的。

  1. 队列

队列 是一个先入先出的数据结构,它在数据结构上可以使用数组或者链表去实现。

队列的特性就像我们日常排队买东西一样,先到先得。

通过图示大家可以看出队列只有两个动作:入队(enqueue)和出队(dequeue),一般呢是在尾部插入,在头部取出。

队列可以分为两种:

  1. 有界队列 :能装入的元素有限。
  2. 无界队列 :能装入的元素无限,只要内存还有,就能一直装下去。

我将利用数组和链表的特性,分别构造这两种队列。

4.1 数组构造队列

数组在前文中也提到过了,它有两个缺点:

  1. 数组是恒定的,想要变大只能使用动态数组。
  2. 删除数组元素会迫使其他元素移位。

如果我们想要使用数组构造队列,第一个问题只能通过动态数组来解决,不过实际中很多队列都是恒定的,是不能变大变小的,因为计算机的内存它不是无限的,所以用数组构造有界队列是比较合适的。

第二个问题,在构造队列时要通过复用数组位置来解决,也就是给数组安上两个指针,一个指向尾节点,一个指向头节点,也就是所谓的环形数组。

环形数组队列的两种操作:

  1. 出队:出队直接拿first节点的数据即可,然后将first下标元素置为null,并将first指向的下标+1即可。
  2. 入队:入队之前需要先判断一下end+1下标上是否有元素,没有才能正常入队,如果有元素则队列已满。

4.2 链表构造队列

使用链表构造队列只需要使用我们上文提到过的双向链表即可,它简直是天生可以作为队列使用,因为它同时维护了头节点和尾节点,而且由于它可以一直链接下去,所以做无界队列正合适。

Java中常用的LinkedList就同时实现了Queue 这个接口,也就是说LinkedList也支持队列操作。

由于链表在上文已经讲过,也很简单,这里我就不实现了~

4.3 课后练习

用数组实现一个有界队列。

  1. 栈

栈 ,是一种先入后出/后入先出的数据结构,如果要举一个生活中的例子,我觉得最形象应该就是弹夹了,最先放进去的子弹往往最后被打出。

栈,同样也可以使用数组和链表两种方式来构造数据结构,因为其实它会上文中提到过的队列是极其类似的。

在计算机世界中,栈常常被用于程序的调用帧栈,在程序中有时候出现的StackOverFlow异常就是由于程序调用栈用尽了,所以栈一般都是有相对固定的深度,从这点上看用数组构造栈是一个比较好的主意。(栈深往往也被设定的内存影响)

栈使用push插入元素,使用pop读取元素,最后被push的那个元素往往就是第一个被pop的元素。

同样的,栈这个数据结构在Java里面也有,它的名字就叫做Stack,但是它不光具有栈的两个操作,还具有数组的一些可用操作,因为它继承了一个List类,像这样的宽接口设计是类库设计失败的典范,希望大家不要模仿~

5.1 课后练习

用数组实现一个栈。

  1. 结语

最后的结语部分,带大家复习一下这四种数据结构:

  1. 数组:下标查找快,插入和删除慢。
  2. 链表:头尾插入和删除快,查找慢。
  3. 队列:尾插和头出,时间复杂度是O(1),非常快。
  4. 栈:尾插尾出,时间复杂度是O(1),非常快。

在如今的编程中,更多的是只用到前两种,选用什么容器取决于你的业务属性。

  1. 在选用数组时可以预估一下集合规模然后初始化一个比较合适的动态数组,避免多次扩容。
  2. 选用链表时可以先问问自己是查询的场景多还是插入的场景多。
  3. 使用队列和栈时也可以先了解一下自己使用的类库的底层实现是数组还是链表,当你面对的东西对你来说不再是黑盒时,就可以轻松的作出判断了~

看完本文后,相信读者对这四种基础数据结构的掌握应该已经不在话下了,但是无论是数组做容器还是链表做容器,当我想往容器中间插入的时候都有一次比较大的查找消耗,下一章准备引入新的数据结构:树 。

树的查找插入删除都是O(logN),属于各方面都比较有优势的数据结构,树可以解决在中间插入的效率问题,我会在下一篇里给大家深入讲解一下,包括有些算法书中没讲到的AVL树。

最后,向大家求个点赞,有什么疑问也可以在评论区留言,我都会及时回复。


参考书目 :

  1. 算法第四版
  2. 数据结构与算法分析
  3. 数据结构与算法图解
  4. 计算机科学导论

推荐阅读

  1. 延迟执行与不可变,系统讲解JavaStream数据处理
  2. 归约、分组与分区,深入讲解JavaStream终结操作

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

蚂蚁金服一面:十道经典面试题解析

发表于 2021-08-16

前言

大家好,我是捡田螺的小男孩。最近编程讨论群有位小伙伴去蚂蚁金服面试了,以下是面试的真题,跟大家一起来讨论怎么回答。

  • 公众号:捡田螺的小男孩
  • github地址
  1. 用到分布式事务嘛?为什么用这种方案,有其他方案嘛?

什么是分布式事务

谈到事务,我们就会想到数据库事务,很容易就想到原子性、一致性、持久性、隔离性。

分布式事务跟数据库事务有点不一样,它是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。

分布式事务基础

分布式事务需要需要知道CAP理论和BASE理论。

CAP理论

  • 一致性(C:Consistency):一致性是指数据在多个副本之间能否保持一致的特性。例如一个数据在某个分区节点更新之后,在其他分区节点读出来的数据也是更新之后的数据。
  • 可用性(A:Availability):可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是”有限时间内”和”返回结果”。
  • 分区容错性(P:Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务。

一个分布式系统中,CAP理论它只能同时满足(一致性、可用性、分区容错性)中的两点。

BASE 理论

BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。

  • 基本可用是指,通过支持局部故障而不是系统全局故障来实现的;
  • Soft State表示状态可以有一段时间不同步;
  • 最终一致,最终数据是一致的就可以了,而不是实时保持强一致。

分布式事务的几种解决方案

  • 2PC(二阶段提交)方案,事务的提交分为两个阶段:准备阶段和提交执行方案。
  • TCC(即Try、Confirm、Cancel),它采用了补偿机制,核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
  • 本地消息表,它的核心思想就是将分布式事务拆分成本地事务进行处理。
  • 最大努力通知,实现最大努力通知,可以采用MQ的ack机制。
  • Saga事务,它的核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

业界目前使用本地消息表这种方案是比较多的,它的核心思想就是将分布式事务拆分成本地事务进行处理。可以看一下基本的实现流程图吧:

对于消息发送方:

  • 首先需要有一个消息表,记录着消息状态相关信息。
  • 业务数据和消息表在同一个数据库,即要保证它俩在同一个本地事务。
  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到MQ消息队列。
  • 消息会发到消息消费方,如果发送失败,即进行重试。

消息消费方:

  • 处理消息队列中的消息,完成自己的业务逻辑。
  • 此时如果本地事务处理成功,则表明已经处理成功了。
  • 如果本地事务处理失败,那么就会重试执行。
  • 如果是业务上面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

2.JDK6、7、8分别提供了哪些新特性

JDK 6 新特性

  • Desktop类(它允许一个Java应用程序启动本地的另一个应用程序去处理URI或文件请求)
  • 使用JAXB2来实现对象与XML之间的映射
  • 轻量级 Http Server API
  • 插入式注解处理API(lombok框架基于这个特性实现)
  • STAX(是JDK6中一种处理XML文档的API)

JDK 7的新特性

  • switch 支持String字符串类型
  • try-with-resources,资源自动关闭
  • 整数类型如(byte,short,int,long)能够用二进制来表示
  • 数字常量支持下划线
  • 泛型实例化类型自动推断,即”<>”
  • 一个catch中捕获多个异常类型,用(|)分隔开
  • 增强的文件系统
  • Fork/join 框架

JDK8 的新特性

  • lambada表达式
  • 函数式接口
  • 方法引用
  • 默认方法
  • Stream API
  • Optional
  • Date Time API(如LocalDate)
  • 重复注解
  • Base64
  • JVM的新特性(如元空间Metaspace代替持久代)
  1. https原理,工作流程

  • HTTPS = HTTP + SSL/TLS,即用SSL/TLS对数据进行加密和解密,Http进行传输。
  • SSL,即Secure Sockets Layer(安全套接层协议),是网络通信提供安全及数据完整性的一种安全协议。
  • TLS,即Transport Layer Security(安全传输层协议),它是SSL3.0的后续版本。

  1. 客户端发起Https请求,连接到服务器的443端口。
  2. 服务器必须要有一套数字证书(证书内容有公钥、证书颁发机构、失效日期等)。
  3. 服务器将自己的数字证书发送给客户端(公钥在证书里面,私钥由服务器持有)。
  4. 客户端收到数字证书之后,会验证证书的合法性。如果证书验证通过,就会生成一个随机的对称密钥,用证书的公钥加密。
  5. 客户端将公钥加密后的密钥发送到服务器。
  6. 服务器接收到客户端发来的密文密钥之后,用自己之前保留的私钥对其进行非对称解密,解密之后就得到客户端的密钥,然后用客户端密钥对返回数据进行对称加密,酱紫传输的数据都是密文啦。
  7. 服务器将加密后的密文返回到客户端。
  8. 客户端收到后,用自己的密钥对其进行对称解密,得到服务器返回的数据。
  1. 讲讲java jmm volatile的实现原理

volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。

volatile是如何保证可见性的呢?我们先来看下java内存模型(jmm)

  • Java虚拟机规范试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。
  • 为了更好的执行性能,java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存打交道,也没有限制编译器进行调整代码顺序优化。所以Java内存模型会存在缓存一致性问题和指令重排序问题的。
  • Java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。这里的变量包括实例变量和静态变量,但是不包括局部变量,因为局部变量是线程私有的。
  • 线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作操作主内存。并且每个线程不能访问其他线程的工作内存。
    Java内存模型

volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。

指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。volatile是如何禁止指令重排的?在Java语言中,有一个先行发生原则(happens-before)

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
  • 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

实际上volatile保证可见性和禁止指令重排都跟内存屏障有关。我们来看一段volatile使用的demo代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class Singleton {  
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个「内存屏障」

lock指令相当于一个内存屏障,它保证以下这几点:

  • 1.重排序时不能把后面的指令重排序到内存屏障之前的位置
  • 2.将本处理器的缓存写入内存
  • 3.如果是写入动作,会导致其他处理器中对应的缓存无效。

第2点和第3点就是保证volatile保证可见性的体现嘛,第1点就是禁止指令重排列的体现。内存屏障又是什么呢?

内存屏障四大分类:(Load 代表读取指令,Store代表写入指令)

内存屏障类型 抽象场景 描述
LoadLoad屏障 Load1; LoadLoad; Load2 在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障 Store1; StoreStore; Store2 在Store2写入执行前,保证Store1的写入操作对其它处理器可见
LoadStore屏障 Load1; LoadStore; Store2 在Store2被写入前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障 Store1; StoreLoad; Load2 在Load2读取操作执行前,保证Store1的写入对所有处理器可见。

为了实现volatile的内存语义,Java内存模型采取以下的保守策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:

内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈~

  1. 讲一讲7层网络模型,tcp的为什么要三次握手

计算机网路体系结构有三层:OSI七层模型、TCP/IP四层模型、五层体系结构,如图:

七层模型,亦称OSI(Open System Interconnection),国际标准化组织(International Organization for Standardization)制定的一个用于计算机或通信系统间互联的标准体系。

  • 应用层:网络服务与最终用户的一个接口,常见的协议有:HTTP FTP SMTP SNMP DNS.
  • 表示层:数据的表示、安全、压缩。,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
  • 会话层:建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话.
  • 传输层:定义传输数据的协议端口号,以及流控和差错校验,协议有TCP UDP.
  • 网络层:进行逻辑地址寻址,实现不同网络之间的路径选择,协议有ICMP IGMP IP等.
  • 数据链路层:在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路。
  • 物理层:建立、维护、断开物理连接。

6.说说线程池的工作原理

面试官如果要我们讲下线程池工作原理的话,大家讲下以下这个流程图就可以啦:

为了形象描述线程池执行,加深大家的理解,我打个比喻:

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求

  • 当产品提个需求,正式员工(核心线程)先接需求(执行任务)
  • 如果正式员工都有需求在做,即核心线程数已满),产品就把需求先放需求池(阻塞队列)。
  • 如果需求池(阻塞队列)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程)来做。
  • 如果所有员工(最大线程数也满了)都有需求在做了,那就执行拒绝策略。
  • 如果外包员工把需求做完了,它经过一段(keepAliveTime)空闲时间,就离开公司了。

7.你们数据库的高可用是怎么实现的?

高可用,即High Availability,是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。单机部署谈不上高可用,因为单点故障问题。高可用都是多个节点的,我们在考虑MySQL数据库的高可用的架构时,需要考虑这几个方面:

  • 如果数据库节点宕机,需要尽快回复,保证业务不受宕机影响。
  • 从数据库节点的数据,尽可能跟主节点数据实时保持一致,至少保证最终一致性。
  • 数据库节点切换时,数据不能缺失。

7.1 主从或主主半同步复制

用双节点数据库,搭建单向或者双向的半同步复制。架构如下:

通常会和proxy、keepalived等第三方软件同时使用,即可以用来监控数据库的健康,又可以执行一系列管理命令。如果主库发生故障,切换到备库后仍然可以继续使用数据库。

这种方案优点是架构、部署比较简单,主机宕机直接切换即可。缺点是完全依赖于半同步复制,半同步复制退化为异步复制,无法保证数据一致性;另外,还需要额外考虑haproxy、keepalived的高可用机制。

7.2 半同步复制优化

半同步复制机制是可靠的,可以保证数据一致性的。但是如果网络发生波动,半同步复制发生超时会切换为异步复制,异复制是无法保证数据的一致性的。因此,可以在半同复制的基础上优化一下,尽可能保证半同复制。如双通道复制方案

  • 优点:这种方案架构、部署也比较简单,主机宕机也是直接切换即可。比方案1的半同步复制,更能保证数据的一致性。
  • 缺点:需要修改内核源码或者使用mysql通信协议,没有从根本上解决数据一致性问题。

7.3 高可用架构优化

保证高可用,可以把主从双节点数据库扩展为数据库集群。Zookeeper可以作为集群管理,它使用分布式算法保证集群数据的一致性,可以较好的避免网络分区现象的产生。

  • 优点:保证了整个系统的高可用性,扩展性也较好,可以扩展为大规模集群。
  • 缺点:数据一致性仍然依赖于原生的mysql半同步复制;引入Zookeeper使系统逻辑更复杂。

7.4 共享存储

共享存储实现了数据库服务器和存储设备的解耦,不同数据库之间的数据同步不再依赖于MySQL的原生复制功能,而是通过磁盘数据同步的手段,来保证数据的一致性。

DRBD磁盘复制

DRBD是一个用软件实现的、无共享的、服务器之间镜像块设备内容的存储复制解决方案。主要用于对服务器之间的磁盘、分区、逻辑卷等进行数据镜像,当用户将数据写入本地磁盘时,还会将数据发送到网络中另一台主机的磁盘上,这样的本地主机(主节点)与远程主机(备节点)的数据就可以保证实时同步。常用架构如下:

当本地主机出现问题,远程主机上还保留着一份相同的数据,即可以继续使用,保证了数据的安全。

  • 优点:部署简单,价格合适,保证数据的强一致性
  • 缺点:对IO性能影响较大,从库不提供读操作

7.5 分布式协议

分布式协议可以很好解决数据一致性问题。常见的部署方案就是MySQL cluster,它是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性。如下:

  • 优点:不依赖于第三方软件,可以实现数据的强一致性;
  • 缺点:配置较复杂;需要使用NDB储存引擎;至少三节点;
  1. 读写分离的场景下,怎么保证从数据库读到最新的数据?

数据库读写分离,主要解决高并发时,提高系统的吞吐量。来看下读写分离数据库模型:

  • 写请求是直接写主库,然后同步数据到从库
  • 读请求一般直接读从库,除飞强制读主库

在高并发场景或者网络不佳的场景,如果存在较大的主从同步数据延迟,这时候读请求去读从库,就会读到旧数据。这时候最简单暴力的方法,就是强制读主库。实际上可以使用缓存标记法。

  • A发起写请求,更新主库数据,并在缓存中设置一个标记,表示数据已更新,标记格式为:userId+业务Id。
  • 设置此标记,设置过期时间(估值为主库和从库同步延迟的时间)
  • B发起读请求,先判断此请求,在缓存中有没有更新标记。
  • 如果存在标记,走主库;如果没有,请求走从库。

这个方案,解决了数据不一致问题,但是每次请求都要先跟缓存打交道,会影响系统吞吐。

  1. 如何保证MySQL数据不丢?

MySQL这种关系型数据库,是日志先行策略(Write-Ahead Logging),只要binlog和redo log日志能保证持久化到磁盘,我们就能确保MySQL异常重启后,数据不丢失。

binlog日志

binlog,又称为二进制日志,它会记录数据库执行更改的所有操作,但是不包括查询select等操作。一般用于恢复、复制等功能。它的格式有三种:statement、mixed和row。

  • statement:每一条会修改数据的sql都会记录到binlog中,不建议使用。
  • row:基于行的变更情况记录,会记录行更改前后的内容,推荐使用。
  • mixed:混合statement和row两个模式,不建议使用。

binlog 的写入机制是怎样的呢?

事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把binlog cache写到binlog文件中
。

系统为每个客户端线程分配一个binlog cache,其大小值控制参数是binlog_cache_size。如果binlog cache的值超过阀值,就会临时持久化到磁盘。当事务提交的时候,再将 binlog cache中完整的事务持久化到磁盘中,并且清空binlog cache。

binlog写文件

binlog写文件分write和fsync两个过程:

  • write:指把日志写到文件系统的page cache,并没有把数据持久化到磁盘,因此速度较快。
  • fsync,实际的写盘操作,即把数据持久化到磁盘。

write和fsync的写入时机,是由变量sync_binlog控制的:

如果IO出现性能瓶颈,可以将sync_binlog设置成一个较大的值。比如设置为(100~1000)。但是,会存在数据丢失的风险,当主机异常重启时,会丢失N个最近提交的事务binlog。

redo log日志

redo log,又称为重做日志文件,只记录事务对数据页做了哪些修改,它记录的是数据修改之后的值。redo 有三种状态

  • 物理上是在MySQL进程内存中,存在redo log buffer中,
  • 物理上在文件系统的page cache里,写到磁盘 (write),但是还没有持久化(fsync)。
  • 存在hard disk,已经持久化到磁盘。

日志写到redo log buffer是很快的;wirte到page cache也很快,但是持久化到磁盘的速度就慢多了。

为了控制redo log的写入策略,Innodb根据innodb_flush_log_at_trx_commit参数不同的取值采用不同的策略,它有三种不同的取值:

    1. 设置为0时,表示每次事务提交时都只是把redo log留在redo log buffer 中 ;
    1. 设置为1时,表示每次事务提交时都将 redo log 直接持久化到磁盘;
    1. 设置为2时,表示每次事务提交时都只是把redo log 写到page cache。

三种模式下,0的性能最好,但是不安全,MySQL进程一旦崩溃会导致丢失一秒的数据。1的安全性最高,但是对性能影响最大,2的话主要由操作系统自行控制刷磁盘的时间,如果仅仅是MySQL宕机,对数据不会产生影响,如果是主机异常宕机了,同样会丢失数据。

  1. 高并发下如何设计秒杀系统?

设计一个秒杀系统,需要考虑这些问题:

如何解决这些问题呢?

  • 页面静态化
  • 按钮至灰控制
  • 服务单一职责
  • 秒杀链接加盐
  • 限流
  • 分布式锁
  • MQ异步处理
  • 限流&降级&熔断

页面静态化

秒杀活动的页面,大多数内容都是固定不变的,如商品名称,商品图片等等,可以对活动页面做静态化处理,减少访问服务端的请求。秒杀用户会分布在全国各地,有的在上海,有的在深圳,地域相差很远,网速也各不相同。为了让用户最快访问到活动页面,可以使用CDN(Content Delivery Network,内容分发网络)。CDN可以让用户就近获取所需内容。

按钮至灰控制

秒杀活动开始前,按钮一般需要置灰的。只有时间到了,才能变得可以点击。这是防止,秒杀用户在时间快到的前几秒,疯狂请求服务器,然后秒杀时间点还没到,服务器就自己挂了。

服务单一职责

我们都知道微服务设计思想,也就是把各个功能模块拆分,功能那个类似的放一起,再用分布式的部署方式。

如用户登录相关的,就设计个用户服务,订单相关的就搞个订单服务,再到礼物相关的就搞个礼物服务等等。那么,秒杀相关的业务逻辑也可以放到一起,搞个秒杀服务,单独给它搞个秒杀数据库。

服务单一职责有个好处:如果秒杀没抗住高并发的压力,秒杀库崩了,服务挂了,也不会影响到系统的其他服务。

秒杀链接加盐

链接如果明文暴露的话,会有人获取到请求Url,提前秒杀了。因此,需要给秒杀链接加盐。可以把URL动态化,如通过MD5加密算法加密随机的字符串去做url。

限流

一般有两种方式限流:nginx限流和redis限流。

  • 为了防止某个用户请求过于频繁,我们可以对同一用户限流;
  • 为了防止黄牛模拟几个用户请求,我们可以对某个IP进行限流;
  • 为了防止有人使用代理,每次请求都更换IP请求,我们可以对接口进行限流。
  • 为了防止瞬时过大的流量压垮系统,还可以使用阿里的Sentinel、Hystrix组件进行限流。

分布式锁

可以使用redis分布式锁解决超卖问题。

使用Redis的SET EX PX NX + 校验唯一随机值,再删除释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
  }
  finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}

在这里,判断是不是当前线程加的锁和释放锁不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。

为了更严谨,一般也是用lua脚本代替。lua脚本如下:

1
2
3
4
5
vbnet复制代码if redis.call('get',KEYS[1]) == ARGV[1] then 
return redis.call('del',KEYS[1])
else
return 0
end;

MQ异步处理

如果瞬间流量特别大,可以使用消息队列削峰,异步处理。用户请求过来的时候,先放到消息队列,再拿出来消费。

限流&降级&熔断

  • 限流,就是限制请求,防止过大的请求压垮服务器;
  • 降级,就是秒杀服务有问题了,就降级处理,不要影响别的服务;
  • 熔断,服务有问题就熔断,一般熔断降级是一起出现。

参考与感谢

  • 五大常见的MySQL高可用方案
  • 读写分离数据库如何保持数据一致性
  • 《我们一起进大厂》系列-秒杀系统设计
  • 《极客时间:MySQL45讲实战》
  • MySQL是如何保证不丢数据的(一)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java版gRPC实战之七:基于eureka的注册发现

发表于 2021-08-16

欢迎访问我的GitHub

github.com/zq2599/blog…

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

《java版gRPC实战》全系列链接

  1. 用proto生成代码
  2. 服务发布和调用
  3. 服务端流
  4. 客户端流
  5. 双向流
  6. 客户端动态获取服务端地址
  7. 基于eureka的注册发现

关于eureka

前面咱们在开发客户端应用时,所需的服务端地址都是按如下步骤设置的:

  • 在application.yml中配置,如下图:

在这里插入图片描述

  • 在用到gRPC的bean中,使用注解GrpcClient即可将Stub类注入到成员变量中:

在这里插入图片描述

  • 上述操作方式的优点是简单易用好配置,缺点也很明显:服务端的IP地址或者端口一旦有变化,就必须修改application.yml并重启客户端应用;
  • 聪明的您一定想到了应对之道:注册中心!没错,有了注册中心,咱们的客户端只要能从注册中心取得最新的服务端地址,就不再需要手动配置了,以下是常规的eureka作用说明:

在这里插入图片描述

本篇概览

  • 如果您有Spring Cloud的开发经验,对resttemplate和feign等应该很熟悉,但是Spring Cloud环境下的gRPC调用却没有那么常用,本篇的目标是通过实战与大家一起掌握Spring Cloud环境下的gRPC调用,分为以下章节:
  1. eureka应用开发
  2. gRPC服务端开发
  3. gRPC客户端开发
  4. 验证
  5. 一点疑惑

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(github.com/zq2599/blog…%EF%BC%9A)
名称 链接 备注
项目主页 github.com/zq2599/blog… 该项目在GitHub上的主页
git仓库地址(https) github.com/zq2599/blog… 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,《java版gRPC实战》系列的源码在grpc-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

  • grpc-tutorials文件夹下有多个目录,本篇文章对应的eureka代码在cloud-eureka目录,服务端代码在cloud-server-side目录,客户端代码在cloud-client-side目录,如下图:

在这里插入图片描述

eureka应用开发

  • 在父工程grpc-turtorials下面新建名为cloud-eureka的模块,其build.gradle内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
groovy复制代码// 使用springboot插件
plugins {
id 'org.springframework.boot'
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
// 依赖eureka
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
// 状态暴露需要的依赖
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 依赖自动生成源码的工程
implementation project(':grpc-lib')
}
  • 配置文件bootstrap.yml,设置自己的web端口号和应用名,另外eureka.client.serviceUrl.defaultZone的配置请改成自己的IP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
yml复制代码server:
port: 8085

spring:
application:
name: cloud-eureka

eureka:
instance:
hostname: localhost
prefer-ip-address: true
status-page-url-path: /actuator/info
health-check-url-path: /actuator/health
lease-expiration-duration-in-seconds: 30
lease-renewal-interval-in-seconds: 30
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://192.168.50.5:8085/eureka/
server:
enable-self-preservation: false

endpoints:
shutdown:
enabled: true
  • 这个模块只有一个类CloudEurekaApplication.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.bolingcavalry.grpctutorials;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class CloudEurekaApplication {

public static void main(String[] args) {
SpringApplication.run(CloudEurekaApplication.class, args);
}
}
  • 以上就是一个简单通用的eureka服务了;

gRPC服务端开发

  • 依赖eureka的gRPC服务端,其重点在于:第一,配置使用eureka,第二,不要指定端口;
  • 在父工程grpc-turtorials下面新建名为cloud-server-side的模块,其build.gradle内容如下,注意要引入gRPC服务端相关的starter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
groovy复制代码// 使用springboot插件
plugins {
id 'org.springframework.boot'
}

dependencies {
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'
// 作为gRPC服务提供方,需要用到此库
implementation 'net.devh:grpc-server-spring-boot-starter'
// 作为eureka的client
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// 状态暴露需要的依赖
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 依赖自动生成源码的工程
implementation project(':grpc-lib')
// annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
annotationProcessor 'org.projectlombok:lombok'
}
  • 配置文件application.yml,设置自己的应用名,另外值得注意的是server.port和grpc.server.port这两个配置的值都是0,这样两个端口就会被自动分配未被占用的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yml复制代码spring:
application:
name: cloud-server-side

server:
port: 0
grpc:
server:
port: 0
eureka:
instance:
prefer-ip-address: true
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://192.168.50.5:8085/eureka/
  • 启动类CloudServerSideApplication.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.bolingcavalry.grpctutorials;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@EnableDiscoveryClient
@SpringBootApplication
public class CloudServerSideApplication {

public static void main(String[] args) {
SpringApplication.run(CloudServerSideApplication.class, args);
}
}
  • 提供gRPC服务的类GrpcServerService,和local-server模块中的一样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.HelloReply;
import com.bolingcavalry.grpctutorials.lib.SimpleGrpc;
import net.devh.boot.grpc.server.service.GrpcService;
import java.util.Date;

@GrpcService
public class GrpcServerService extends SimpleGrpc.SimpleImplBase {

@Override
public void sayHello(com.bolingcavalry.grpctutorials.lib.HelloRequest request,
io.grpc.stub.StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("1. Hello " + request.getName() + ", " + new Date()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
  • 以上就是服务端代码了,可见除了将gRPC端口设置为0,以及常规使用eureka的配置,其他部分和local-server模块是一样的;

gRPC客户端开发

  • 依赖eureka的gRPC客户端,其重点在于:第一,配置使用eureka,第二,配置中的gRPC配置项的名字要等于gRPC服务端在eureka注册的名字,如下图红框所示:

在这里插入图片描述

  • 在父工程grpc-turtorials下面新建名为cloud-client-side的模块,其build.gradle内容如下,注意要引入gRPC客户端相关的starter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
groovy复制代码// 使用springboot插件
plugins {
id 'org.springframework.boot'
}

dependencies {
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'
// 作为gRPC服务使用方,需要用到此库
implementation 'net.devh:grpc-client-spring-boot-starter'
// 作为eureka的client
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// 状态暴露需要的依赖
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 依赖自动生成源码的工程
implementation project(':grpc-lib')
// annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
annotationProcessor 'org.projectlombok:lombok'
}
  • 配置文件application.yml,设置自己的web端口号,另外值得注意的是gRPC配置项cloud-server-side的名字要等于gRPC服务端在eureka注册的名字,并且不需要address配置项:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yml复制代码server:
port: 8086
spring:
application:
name: cloud-client-side
eureka:
instance:
prefer-ip-address: true
status-page-url-path: /actuator/info
health-check-url-path: /actuator/health
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://192.168.50.5:8085/eureka/
grpc:
client:
# gRPC配置的名字,GrpcClient注解会用到
cloud-server-side:
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext
  • 启动类CloudClientSideApplication.java,使用了eureka相关的注解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.bolingcavalry.grpctutorials;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@EnableDiscoveryClient
@SpringBootApplication
public class CloudClientSideApplication {

public static void main(String[] args) {
SpringApplication.run(CloudClientSideApplication.class, args);
}
}
  • 封装gRPC调用的服务类GrpcServerService,和local-server模块中的一样,GrpcClient注解对应配置中的gRPC配置项:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.HelloReply;
import com.bolingcavalry.grpctutorials.lib.HelloRequest;
import com.bolingcavalry.grpctutorials.lib.SimpleGrpc;
import io.grpc.StatusRuntimeException;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;

@Service
public class GrpcClientService {

@GrpcClient("cloud-server-side")
private SimpleGrpc.SimpleBlockingStub simpleStub;

public String sendMessage(final String name) {
try {
final HelloReply response = this.simpleStub.sayHello(HelloRequest.newBuilder().setName(name).build());
return response.getMessage();
} catch (final StatusRuntimeException e) {
return "FAILED with " + e.getStatus().getCode().name();
}
}
}
  • 再做一个web接口类,这样我们就能通过web调用验证gRPC服务了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.bolingcavalry.grpctutorials;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GrpcClientController {

@Autowired
private GrpcClientService grpcClientService;

@RequestMapping("/")
public String printMessage(@RequestParam(defaultValue = "will") String name) {
return grpcClientService.sendMessage(name);
}
}
  • 客户端开发完毕,接下来可以验证了;

验证

  • 启动cloud-eureka:

在这里插入图片描述

  • 启动cloud-server-side,可见gRPC服务端口自动分配了65141,不过我们无需关心这个值,因为客户端可以从eureka获取到:

在这里插入图片描述

  • 接下来启动cloud-client-side,启动成功后eureka上可见两个服务的注册信息:

在这里插入图片描述

  • 浏览器访问cloud-client-side提供的web接口,响应如下,可见cloud-client-side成功调用了cloud-server-side的gRPC服务:

在这里插入图片描述

一点疑惑

  • 如果您对eureka有所了解,可能会产生一点疑惑:cloud-client-side从eureka取得的cloud-server-side信息,应该是http服务的地址和端口,不应该有gRPC的端口号,因为eureka的注册发现服务并不包含gRPC有关的!
  • 篇幅所限,这里不适合将上述问题展开分析,咱们来关注最核心的地方,相信聪明的您看上一眼就会豁然开朗;
  • DiscoveryClientNameResolver来自grpc-client-spring-boot-autoconfigure.jar,用来保存从eureka取得的服务端信息,该类的注释已经说得很清楚了,从metadata的gRPC.port配置项中取得gRPC端口号:

在这里插入图片描述

  • 在DiscoveryClientNameResolver的代码中打上断点,查看成员变量instanceList,可见metadata中确实有gRPC端口的信息:

在这里插入图片描述

  • 至于cloud-server-side如何将端口号提交到eureka,以及cloud-client-side为何会使用DiscoveryClientNameResolver来处理eureka的服务列表信息,就不在本文中讨论了,您要是有兴趣深入研究eureka,可以参考《程序员欣宸文章汇总(Spring篇)》中的Eureka源码分析专题,如下图:

在这里插入图片描述

  • 至此,基于eureka的gRPC服务注册发现的开发和验证就完成了,希望本文可以给您带来一些参考,让您的服务在注册中心的加持下更加灵活和可靠;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

毕业设计-宿舍管理系统

发表于 2021-08-15

前言

​ 本期项目是宿舍管理系统,主要包括数据监控大盘、宿舍楼管理、宿舍管理、宿舍成员管理、借用管理、卫生管理、缴费管理、保修管理、日志管理、用户管理、角色管理以及各个模块的导出功能。以企业级的开发标准来完成整个前后端代码,无论是用来作为毕业设计还是拿来学习,相信对初学者都会有很大帮助。

(想要源码和视频教程的同学私信我~)

工程架构

应用分层

image-20201226111957265

上面的分层架构摘自阿里巴巴java开发手册,我对此做了一些调整,实际分层结构如下:

image-20210815201245445

领域模型

  • DO(DataObject):与数据库表结构一一对应,通过DAO层向上传输数据源对象
  • BO(BusinessObject):业务对象。由Service层输出的封装业务逻辑的对象
  • VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象

BO和VO领域模型又分为BoRequest(输入模型)、BoResponse(输出模型)、VoRequest(输入模型)、VoResponse(输出模型)

技术栈

前端:vue + element

后端:jdk1.8 + springboot + redis + mysql

系统设计

接口设计

​ 整个项目接口采用的目前互联网比较流行的restful风格设计,每个接口、每个参数都有详细的文档说明。因为企业中开发必然是团队协作,必然前后端分离的开发模式,你得先把接口定义出来,然后前端可以和后端同步开发。还有一种就是对外提供接口,比如你们隔壁团队也想调用你这个服务的接口,但是你两排期是同一周,这时候你得先把接口定义出来给人家,然后大家同步开发,开发完了之后再进行联调。

image-20210815201414004

运行效果

系统登录

image-20210815200810020

dashboard

首页数据大盘,按最近7天饼图占比、最近30天折线图走势、最近一年柱状图分析、最近7天各个时间段占比分析全方位可视化分析数据。

image-20210815201449587

宿舍楼管理

image-20210815201611075

宿舍管理

image-20210815201650962

宿舍成员管理

image-20210815201724764

借用管理

image-20210815201800342

卫生管理

宿舍长可以对寝室值日人员进行排班,每人值日一天,便于卫生管理。

image-20210815201858392

缴费管理

image-20210815202028668

保修管理

image-20210815202105839

Excel导出

​ 所有模块都支持数据导出Excel,方便进行数据分析

借用记录导出

image-20210815202209538

保修单导出

image-20210815202256382

日志管理

​ 日志管理默认是开给管理员的,在系统中的所有操作都会被记录,在系统出现异常时也便于管理员进行问题排查。

image-20210815202324059

用户管理

​ 默认也是只有管理员拥有用户管理菜单的权限,可以新建/编辑用户、分配用户角色、禁用/启用等操作

image-20210815202342016

编辑用户信息

image-20210815082407636

角色管理

​ 极其灵活的权限管理,系统中的所有按钮都可以单独分配权限,你可以给A角色只分配了查询和导出权限,也可以给B角色分配查询、编辑、新建权限,还可以给C角色只分配查询权限。可以满足几乎所有的业务需求,大家可以自由发挥定义权限组合。

​ 默认有‘管理员角色’、‘寝室长角色’、‘普通用户角色’

image-20210815202425058

页面不存在时提示页面

image-20210815082839734

个人信息修改

image-20210815202603698

密码修改

​ 管理员创建完用户之后的默认密码是“123456”,用户可以登录系统自己修改密码

image-20210815202628405

权限设计

​ 权限基于security和spring-session实现。权限可以分为认证和授权,认证其实就是登录,用户登录时会进行账号密码的校验,校验成功后会,会把session存入redis中。授权指的是用户是否拥有访问后端资源的权限,每个新用户在创建后都会分配角色,角色其实就是一个权限集合,这里的权限可以理解为访问后端一个个接口(资源)的权限。

​ 这里权限设计的非常灵活,细粒度到按钮级别,比如新增、删除、修改、查询、借阅动作,普通用户可能就只有查询权限,管理员则拥有新增、删除、修改的权限。普通用户即使通过接口直接访问后端的修改或者删除接口,后端也会返回授权失败错误,因为后端每个需要权限的接口都打了权限标识,只有拥有资源权限用户才能访问。

​ 比如下面的车辆修改接口,只有拥有“CAR_UPDATE”这个权限标识的用户才能访问这个接口,否则返回“未授权”的错误。

1
2
3
4
5
java复制代码@PutMapping("/{id}")
@PreAuthorize("hasAuthority(T(com.senior.book.console.api.security.Authority).BOOK_UPDATE.name())")
public Result<Boolean> update(@PathVariable("id") Long id, @Valid @RequestBody BookUpdateVoRequest request) {

}

日志方案

​ 日志采用lombok注解+slf4j+log4j2的实现方案,基于profile实现了多环境的日志配置,因为不同环境的日志打印策略是不一样,比如开发环境我可能需要打印到console控制台,需要debug级别的日志以便于本地开发调试,测试环境可能就需要打印到日志文件里,线上环境可能需要打印到文件的同时将日志发送到kafka然后收集到es中,这样当线上部署了多台机器后我们查日志不用一台一台机器去查日志了,因为都收集到es了,我们只需要登录kibana去搜索,这样就非常方便。这里说到的kafka+es+kibana这样一套日志解决方案也是目前互联网公司比较常用的一套解决方案。如果你动手能力够强,你可以本地搭一套kafka、es、kibana,然后只需要在配置文件中加入几行配置就实现了这么一套企业级的日志解决方案(默认是输出到日志文件)。

下面是部分关键配置,如果要配置kafka,只需要在标签中配置配置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码    <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" xmlns:xi="http://www.w3.org/2001/XInclude">
<Properties>
<Property name="LOG_FILE">system.log</Property>
<Property name="LOG_PATH">./logs</Property>
<Property name="PID">????</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATE_FORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATE_FORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}
</Property>
<Property name="FILE_LOG_PATTERN">%d{${LOG_DATE_FORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.}:%L : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}
</Property>
</Properties>
<Appenders>
<xi:include href="log4j2/file-appender.xml"/>
</Appenders>
<Loggers>
<logger name="com.senior.park" level="info"/>
<Root level="info">
<AppenderRef ref="FileAppender"/>
</Root>
</Loggers>
</Configuration>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

开发人员,怎能没有个人博客!域名ICP备案

发表于 2021-08-15

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

前言

  上一章已经给大家介绍了域名的相关知识点和怎么选择一个适合自己的域名,其实有了域名之后,并不能直接进行访问网站,还需要对域名进行实名认证,然后对域名进行备案,在服务器中对域名进行解析之后,才能使用域名进行访问。本文将针对域名备案进行介绍。

什么是域名备案

  域名备案DNICP(Domain Name Internet Content Provider)的目的就是为了防止在网上从事非法的网站经营活动,打击不良互联网信息的传播,如果网站不备案的话,很有可能被查处以后关停。根据中华人民共和国信息产业部第十二次部务会议审议通过的《非经营性互联网信息服务备案管理办法》条例 】,在中华人民共和国境内提供非经营性互联网信息服务,应当办理备案。未经备案,不得在中华人民共和国境内从事非经营性互联网信息服务。而对于没有备案的网站将予以罚款或关闭。

为什么要进行域名备案

  根据《互联网信息服务管理办法》以及 《非经营性互联网信息服务备案管理办法》,国家对非经营性互联网信息服务实行备案制度,对经营性互联网信息服务实行许可制度。未取得许可或者未履行ICP备案手续的,不得从事互联网信息服务。即所有对中国内地(大陆)提供服务的网站都必须先进行ICP备案,才可开通服务。

哪些域名可以备案

  国内支持的域名还是比较多的。
未获工信部批复的域名后缀无法进行ICP备案;国别域名及托管在非中国内地的域名无需申请ICP备案。ICP备案前您需根据以下步骤,检查您的域名是否支持ICP备案。

  • 1、检查域名是否已获批复。
    点击进入
  • 2、单击中国互联网域名体系。
    图片.png
  • 3、在中国互联网域名体系列表中,查看当前工信部已经批复的域名后缀。
    如果在 中国互联网域名体系列表中未查看到您的域名后缀类型,则您的域名无法进行ICP备案。

域名备案准备

  域名备案准备
1、准备ICP备案的实名认证域名。
2、准备备案服务器,例如:ECS实例、云虚拟主机、轻量应用服务器实例、弹性Web托管实例、NAT网关、IPv6转换服务、ENS等都可以进行备案
3、ICP备案主体为个人的,需要:中国内地居民身份证、域名证书、手持个人证件照片、网站建设方案书(个别省份需要)、暂住证或居住证(个别省份需要)。

域名备案流程

  本次域名ICP备案在阿里云进行,作者在阿里云已经备案多个域名,因此熟悉其备案整体流程。在其他服务商的备案方式与阿里云基本相同。本文以阿里云进行备案介绍。
图片.png

基础信息校验

  进行网站域名备案时,通过填写ICP备案的部分主体信息和网站域名来校验基础信息,并根据您ICP备案所在地域为您关联当地管局规则,您需根据管局的要求填写ICP备案信息并提交备案订单,不同的备案类型需填写的信息略有差异。

  • 注册账号并登录ICP备案系统
  • ICP备案消息通知设置
  • 网站域名准备与检查
  • ICP备案所需资料
  • ICP备案服务器(接入信息)准备与检查
  • 各地区管局ICP备案规则
  • 前置审批

填写主办者信息

  进行ICP备案申请时,需要填写真实的主办者信息,后续ICP备案审核人员会审核所填信息的真实性。介绍ICP备案审核的主办者信息填写规范,正确填写,避免备案审核被驳回。期中主要有通讯地址、投资人或主管单位、备注三个主要信息。通信地址无具体门牌号,请在备注中说明主办单位通信地址已为最详细的地址。

填写网站信息

  进行行ICP备案申请时,需要填写真实的网站信息,后续ICP备案审核人员会审核所填信息的真实性,ICP备案审核的网站信息的填写规范,指导正确填写,避免备案审核被驳回。
期中网站名称需要符合规范,要求由三个及三个以上汉字组成,且网站名称需与主办单位名称有关联性(建议网站命名为与主体相关的名称,关联性无需提供材料证明,ICP备案初审时审核人员会判断并核实)。

个人网站命名要求
  • 网站命名基本要求
    网站名称需大于3个及3个以上汉字。
    禁止使用纯数字、英文或域名作为网站名称,且名称中不能包含特殊符号。
    网站名称需与实际网站内容一致。
    不可使用人物名称、地点名称、成语等内容作为网站名称。
  • 网站名称禁止涉及的类别
    禁止涉及行业信息、企业信息、产品信息、国家信息、敏感信息(如反腐、赌博、廉政、色情等)等。
    禁止涉及博客、论坛、在线、社区、交流等信息。
  • 网站名称禁止使用的词汇
    全国网站命名时禁止使用如下词汇:
  • 禁止使用XXX的个人空间、爱好者、博客、导航、工作室、论坛、平台、热线、社区、社团、网络、网站、网址、主页、资讯、作品展示等词汇。

快速开始

点击快速开始

图片.png

填写主办者信息

  按上面的要求,填写主办者信息。填写完成点击信息校验,校验通过进行下一步。
图片.png

填写网站信息

  按管局的要求,填写所有的网站相关的信息,必须符合相关规范,并校验域名、服务器是否正常。由于没有需要备案的域名,暂时无截图

上传资料

  上传上述准备材料、包含身份证、域名证书、手持身份证照片等
  网站负责人在进行人脸识别验证其真实性前,必须满足以下拍摄要求。

  • 必须为白色背景且背景不能太亮或太暗。
  • 必须穿戴整齐且面部无帽子、刘海、眼镜等遮挡。

信息确认

  完成ICP备案信息填写及所需资料的上传、认证后,需再最终仔细确认填写的ICP备案信息是否正确,以免因备案信息填写错误或资料不合格导致备案申请被驳回。确认无误之后提交备案订单。

ICP备案审核

  提交了ICP备案申请,并根据系统提示完成信息填写及资料上传后,您的ICP备案申请将进入审核阶段,各省管局审核时间不同,实际审核时长会根据ICP备案场景有所不同,一般为1~20个工作日,具体以实际审核时间为准。

完成备案

  各省管局审核完成之后,会收到短信通知,大家收到短信消息之后,就可以针对自己的域名解析访问啦。恭喜各位小主完成备案。作者已经完成备案信息如下:

图片.png

结语

  域名ICP备案是建立个人正式网站中的重要一步,预计从申请到审核通过在20天左右,作者申请备案最快的10天左右,最慢的20多天。当认证通过之后,会有很强的成就感,如果读者在备案中遇到了相关问题,欢迎联系作者,进行相关解答,很愿意与大家交流学习。

  如果大家有个人技术网站或者博客,也可以一起交流学习,加个友情链接。作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

验证码这样做,瞬间高出一个逼格

发表于 2021-08-15

前言

行为验证码通过用户的操作来完成验证,常见的行为验证码有拖动式和点触式。

拖动式验证就是根据图片显示,将指定的图形拖动到指定位置完成验证。而点触式验证码就是通过鼠标点击出示例中出现的图形完成验证。

行为验证码应用

今天推荐一款非常优秀的行为验证码AJ-Captcha(项目地址**gitee.com/anji-plus/c…**),这个项目包含了滑动拼图和文字点选两种类型的验证码,除了嵌入式交互,还提供了弹出式交互的方式,完全不影响原UI布局。

AJ-Captcha的验证流程如下:

  1. 用户访问登录页面,发送请求显示行为验证码
  2. 用户按照提示要求完成验证码拼图/点击
  3. 用户提交表单,前端将第二步的输出一同提交到后台
  4. 验证数据随表单提交到后台后,后台需要调用captchaService.verification做二次校验。
  5. 第4步返回校验通过/失败到产品应用后端,再返回到前端。如下图所示。

如果你是Maven开发者,使用起来非常方便,项目的维护人员已经将依赖推送至中央仓库。只需要引入依赖就完成了90%的工作量。接下来只需要在登录接口中进行二次验证就可以了。

项目集成了包括html、vue、flutter、uni-app、Android Kotlin、IOS、php等多种前端语言,可以轻松将AJ_Captcha集成到项目中。

接下来我们以Spring Boot+html为例看看如何快速集成AJ_Captcha完成行为验证码的交互流程。

第一步、Spring Boot中引入AJ_Captcha依赖

1
2
3
4
5
xml复制代码<dependency>
   <groupId>com.anji-plus</groupId>
   <artifactId>spring-boot-starter-captcha</artifactId>
   <version>1.2.9</version>
</dependency>

AJ_Captcha默认实现了验证码生成和验证接口,验证码生成接口的默认请求地址是/captcha/get,验证接口的默认请求地址为/captcha/check。也就是说完成以上步骤,就可以提供给前端获取和验证验证码的接口了。如果你还想让你的验证码生成的个性一点,可以配置以下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
ini复制代码# 滑动验证,底图路径,不配置将使用默认图片
# 支持全路径
# 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/jigsaw
aj.captcha.jigsaw=classpath:images/jigsaw
# 滑动验证,底图路径,不配置将使用默认图片
# 支持全路径
# 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/pic-click
aj.captcha.pic-click=classpath:images/pic-click
​
# 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis或者memcache,
# 参考CaptchaCacheServiceRedisImpl.java
# 如果应用是单点的,也没有使用redis,那默认使用内存。
# 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
# !!! 注意啦,如果应用有使用spring-boot-starter-data-redis,
# 请打开CaptchaCacheServiceRedisImpl.java注释。
# redis -----> SPI: 在resources目录新建META-INF.services文件夹(两层),参考当前服务resources。
# 缓存local/redis...
aj.captcha.cache-type=local
# local缓存的阈值,达到这个值,清除缓存
#aj.captcha.cache-number=1000
# local定时清除过期缓存(单位秒),设置为0代表不执行
#aj.captcha.timing-clear=180
#spring.redis.host=10.108.11.46
#spring.redis.port=6379
#spring.redis.password=
#spring.redis.database=2
#spring.redis.timeout=6000
​
# 验证码类型default两种都实例化。
aj.captcha.type=default
# 汉字统一使用Unicode,保证程序通过@value读取到是中文,可通过这个在线转换;yml格式不需要转换
# https://tool.chinaz.com/tools/unicode.aspx 中文转Unicode
# 右下角水印文字(我的水印)
aj.captcha.water-mark=\u6211\u7684\u6c34\u5370
# 右下角水印字体(不配置时,默认使用文泉驿正黑)
# 由于宋体等涉及到版权,我们jar中内置了开源字体【文泉驿正黑】
# 方式一:直接配置OS层的现有的字体名称,比如:宋体
# 方式二:自定义特定字体,请将字体放到工程resources下fonts文件夹,支持ttf\ttc\otf字体
# aj.captcha.water-font=WenQuanZhengHei.ttf
# 点选文字验证码的文字字体(文泉驿正黑)
# aj.captcha.font-type=WenQuanZhengHei.ttf
# 校验滑动拼图允许误差偏移量(默认5像素)
aj.captcha.slip-offset=5
# aes加密坐标开启或者禁用(true|false)
aj.captcha.aes-status=true
# 滑动干扰项(0/1/2)
aj.captcha.interference-options=2
​
aj.captcha.history-data-clear-enable=false
​
# 接口请求次数一分钟限制是否开启 true|false
aj.captcha.req-frequency-limit-enable=false
# 验证失败5次,get接口锁定
aj.captcha.req-get-lock-limit=5
# 验证失败后,锁定时间间隔,s
aj.captcha.req-get-lock-seconds=360
# get接口一分钟内请求数限制
aj.captcha.req-get-minute-limit=30
# check接口一分钟内请求数限制
aj.captcha.req-check-minute-limit=60
# verify接口一分钟内请求数限制
aj.captcha.req-verify-minute-limit=60

第二步、前端伪代码调用接口

  1. 引入验证码的样式以及验证等文件
  2. 验证码获取及验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
php复制代码<script>
   $('#content').slideVerify({
   baseUrl:'http://localhost:8080/',  //服务器请求地址, 默认地址为安吉服务器;
   containerId:'btn',//pop模式 必填 被点击之后出现行为验证码的元素id
   mode:'pop',     //展示模式
   imgSize : {       //图片的大小对象,有默认值{ width: '310px',height: '155px'},可省略
       width: '400px',
       height: '200px',
  },
   barSize:{          //下方滑块的大小对象,有默认值{ width: '310px',height: '50px'},可省略
       width: '400px',
       height: '40px',
  },
   beforeCheck:function(){  //检验参数合法性的函数 mode ="pop"有效
       let flag = true;
       //实现: 参数合法性的判断逻辑, 返回一个boolean值
       return flag
  },
   ready : function() {},  //加载完毕的回调
   success : function(params) { //成功的回调
       // params为返回的二次验证参数 需要在接下来的实现逻辑回传服务器
       例如: login($.extend({}, params))
  },
   error : function() {}        //失败的回调
});
</script>

验证码验证成功之后,会返回一个用于二次验证的串码。

第三步,用户登录,二次验证

客户端登录的时候携带验证成功后返回的串码,在登录接口中进行二次验证,验证流程完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码@Autowired
private CaptchaService captchaService;
/**
 * 页面获取token
 * 大屏数据校验
 * @param user
 * @return
 */
@PostMapping("getWebToken")
public ResultBean getWebToken(@RequestBody LoginUser user,String captchaVerification){
   ResultBean resultBean = new ResultBean();
   CaptchaVO captchaVO = new CaptchaVO();
   captchaVO.setCaptchaVerification(captchaVerification);
   ResponseModel responseModel = captchaService.verification(captchaVO);
   if(!responseModel.isSuccess()){
       resultBean.fillCode(0,responseModel.getRepMsg());
       return resultBean;
  }
   // 验证通过后,继续登录流程
}

今天的内容就介绍到这里了,趁这个机会,试着使用这款高颜值的行为验证码来替换项目中的图形验证码吧。

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

synchronized和lock的区别

发表于 2021-08-15

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

两者对比

  • synchronized是属于jvm层面的的关键字,底层通过monitorenter、monitorexit指令实现的;而lock是属于一个类。
  • synchronized在代码执行异常时或正常执行完毕后,jvm会自动释放锁;而lock不行使用lock必须加上异常处理,而且必须在finally块中写上unlock()释放锁。
  • synchronized不可中断,只能等待程序执行完毕或者异常退出;而lock可通过interrupt来中断,可参考示例。
  • synchronized不能精确唤醒指定的线程;而lock可以通过Condition精确唤醒。可参考示例。
  • synchronized无法判断锁的状态,从而无法知道是否获取锁;而lock可以判断锁的状态,可参考示例。

中断响应

lock可以通过interrupt中断,而isInterrupted可以判断线程是否被中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码package com.wangscaler.lock;
​
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/**
* @author WangScaler
* @date 2021/8/14 15:41
*/
class Resource {
   private Lock lock = new ReentrantLock();
   private int num = 1;
​
   protected void getLock() throws InterruptedException {
       lock.lockInterruptibly();
       try {
           System.out.println(Thread.currentThread().getName() + "得到了锁");
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println(Thread.currentThread().getName() + num + "次执行");
               ++num;
               if (num == 10) {
                   System.out.println(Thread.currentThread().getName() + "即将中断");
                   Thread.currentThread().interrupt();
              }
          }
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           lock.unlock();
           System.out.println(Thread.currentThread().getName() + "释放了锁");
      }
  }
}
​
public class LockDemo {
   public static void main(String[] args) {
       Resource resource = new Resource();
       new Thread(() -> {
           try {
               resource.getLock();
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
      }, "A").start();
  }
}

首先启动线程,如果num小于10则不中断线程,此时循环执行,直到num自增到10,则执行中断Thread.currentThread().interrupt();,此时循环条件变为false,线程结束。而synchronized一旦执行不能中断,要么执行完毕,要么程序异常。

lock精确唤醒示例

lock可以通过Condition精确唤醒。

比如我们有三个线程A、B、C,我们需要保证他们的执行顺序是A-B-C那么我们可以这样写当A线程执行完通过signal();方法来唤醒B,同理一次类推循环唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
java复制代码package com.wangscaler.lock;
​
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/**
* @author WangScaler
* @date 2021/8/14 15:41
*/
class Resource {
   private int num = 1;
   private Lock lock = new ReentrantLock();
   private Condition condition = lock.newCondition();
   private Condition condition1 = lock.newCondition();
   private Condition condition2 = lock.newCondition();
​
   protected void startRunA() {
       lock.lock();
       try {
           while (num != 1) {
               condition.await();
          }
           for (int i = 0; i < num; i++) {
               int number = i + 1;
               System.out.println(Thread.currentThread().getName() + number + "次执行");
          }
           ++num;
           condition1.signal();
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           lock.unlock();
      }
  }
​
   protected void startRunB() {
       lock.lock();
       try {
           while (num != 2) {
               condition1.await();
          }
           for (int i = 0; i < num; i++) {
               int number = i + 1;
               System.out.println(Thread.currentThread().getName() + number + "次执行");
          }
           ++num;
           condition2.signal();
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           lock.unlock();
      }
  }
​
   protected void startRunC() {
       lock.lock();
       try {
           while (num != 3) {
               condition2.await();
          }
           for (int i = 0; i < num; i++) {
               int number = i + 1;
               System.out.println(Thread.currentThread().getName() + number + "次执行");
          }
           num = 1;
           condition.signal();
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           lock.unlock();
      }
  }
}
​
public class LockDemo {
   public static void main(String[] args) {
       Resource resource = new Resource();
       new Thread(() -> {
           for (int i = 0; i < 4; i++) {
               resource.startRunA();
          }
      }, "A").start();
​
       new Thread(() -> {
           for (int i = 0; i < 4; i++) {
               resource.startRunB();
          }
      }, "B").start();
       new Thread(() -> {
           for (int i = 0; i < 4; i++) {
               resource.startRunC();
          }
      }, "C").start();
  }
}

当然这些线程之间就像存在依赖关系一样,只有A能唤醒B,B唤醒C,C唤醒A。就像上述的案例三个线程都是执行4次,可以保证程序的正确执行,但是当B线程改为3次,程序就无法终止,因为C线程一直处于await状态,等待B线程的唤醒,然而B线程已经结束了。不过lock能精确的控制线程的执行顺序,而synchronized则做不到这点,synchronized只能随机唤醒线程。

获取锁的状态

而lock可以通过tryLock判断锁的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码package com.wangscaler.lock;
​
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/**
* @author WangScaler
* @date 2021/8/14 15:41
*/
class Resource {
   private Lock lock = new ReentrantLock();
​
   protected void testLock() {
       if (lock.tryLock()) {
           System.out.println(Thread.currentThread().getName() + "获取锁成功");
           try {
               Thread.sleep(3000);
          } catch (Exception e) {
               e.printStackTrace();
          } finally {
               System.out.println(Thread.currentThread().getName() + "释放锁");
               lock.unlock();
          }
      } else {
           System.out.println(Thread.currentThread().getName() + "获取锁失败");
      }
  }
}
​
public class LockDemo {
   public static void main(String[] args) {
       Resource resource = new Resource();
       new Thread(() -> {
           resource.testLock();
      }, "A").start();
​
       new Thread(() -> {
           resource.testLock();
      }, "B").start();
       new Thread(() -> {
           resource.testLock();
      }, "C").start();
  }
}

tryLtrock方法尝试去获取锁,如果获取成功则返回布尔值true,如果获取失败则返回false。所以可以根据tryLtrock()来判断线程是否获的锁。

总结:

在资源竞争不是很激烈的情况下,可以选择synchronized,反之选择lock。synchronized是由jvm管理的,对程序员的要求较低,而lock则相反,如果操作不当,反而会带来严重的后果。

synchronized已经加入了线程自旋和适应性自旋以及锁消除、锁粗化、偏向锁,慢慢的从重量级锁转换成轻量级锁,优势也越来越明显。总之如何选择,大家根据实际情况进行选择吧。

来都来了,点个赞再走呗!

关注WangScaler,祝你升职、加薪、不提桶!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…562563564…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%