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

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


  • 首页

  • 归档

  • 搜索

Spring Cloud Gateway过滤器精确控制异常返

发表于 2021-11-25

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):github.com/zq2599/blog…

本篇概览

  • 在《Spring Cloud Gateway修改请求和响应body的内容》一文中,咱们通过filter成功修改请求body的内容,当时留下个问题:在filter中如果发生异常(例如请求参数不合法),抛出异常信息的时候,调用方收到的返回码和body都是Spring Cloud Gateway框架处理后的,调用方无法根据这些内容知道真正的错误原因,如下图:

在这里插入图片描述

  • 本篇任务就是分析上述现象的原因,通过阅读源码搞清楚返回码和响应body生成的具体逻辑

提前小结

  • 这里将分析结果提前小结出来,如果您很忙碌没太多时间却又想知道最终原因,直接关注以下小结即可:
  1. Spring Cloud Gateway应用中,有个ErrorAttributes类型的bean,它的getErrorAttributes方法返回了一个map
  2. 应用抛出异常时,返回码来自上述map的status的值,返回body是整个map序列化的结果
  3. 默认情况下ErrorAttributes的实现类是DefaultErrorAttributes
  • 再看上述map的status值(也就是response的返回码),在DefaultErrorAttributes是如何生成的:
  1. 先看异常对象是不是ResponseStatusException类型
  2. 如果是ResponseStatusException类型,就调用异常对象的getStatus方法作为返回值
  3. 如果不是ResponseStatusException类型,再看异常类有没有ResponseStatus注解,
  4. 如果有,就取注解的code属性作为返回值
  5. 如果异常对象既不是ResponseStatusException类型,也没有ResponseStatus注解,就返回500
  • 最后看map的message字段(也就是response body的message字段),在DefaultErrorAttributes是如何生成的:
  1. 异常对象是不是BindingResult类型
  2. 如果不是BindingResult类型,就看是不是ResponseStatusException类型
  3. 如果是,就用getReason作为返回值
  4. 如果也不是ResponseStatusException类型,就看异常类有没有ResponseStatus注解,如果有就取该注解的reason属性作为返回值
  5. 如果通过注解取得的reason也无效,就返回异常的getMessage字段
  • 上述内容就是本篇精华,但是并未包含分析过程,如果您对Spring Cloud源码感兴趣,请允许欣宸陪伴您来一次短暂的源码阅读之旅

Spring Cloud Gateway错误处理源码

  • 首先要看的是配置类ErrorWebFluxAutoConfiguration.java,这里面向spring注册了两个实例,每个都非常重要,咱们先关注第一个,也就是说ErrorWebExceptionHandler的实现类是DefaultErrorWebExceptionHandler:

在这里插入图片描述

  • 处理异常时,会通过FluxOnErrorResume调用到这个ErrorWebExceptionHandler的handle方法处理,该方法在其父类AbstractErrorWebExceptionHandler.java中,如下图,红框位置的代码是关键,异常返回内容就是在这里决定的:

在这里插入图片描述

  • 展开这个getRoutingFunction方法,可见会调用renderErrorResponse来处理响应:
1
2
3
4
java复制代码@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);
}
  • 打开renderErrorResponse方法,如下所示,真相大白了!
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
// 取出所有错误信息
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));

// 构造返回的所有信息
return ServerResponse
// 控制返回码
.status(getHttpStatus(error))
// 控制返回ContentType
.contentType(MediaType.APPLICATION_JSON)
// 控制返回内容
.body(BodyInserters.fromValue(error));
}
  • 通过上述代码,咱们得到两个重要结论:
  1. 返回给调用方的状态码,取决于getHttpStatus方法的返回值
  2. 返回给调用方的body,取决于error的内容
  • 都已经读到了这里,自然要看看getHttpStatus的内部,如下所示,status来自入参:
1
2
3
java复制代码protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("status");
}
  • 至此,咱们可以得出一个结论:getErrorAttributes方法的返回值是决定返回码和返回body的关键!
  • 来看看这个getErrorAttributes方法的庐山真面吧,在DefaultErrorAttributes.java中(回忆刚才看ErrorWebFluxAutoConfiguration.java的时候,前面曾提到里面的东西都很重要,也包括errorAttributes方法):
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复制代码public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = this.getErrorAttributes(request, options.isIncluded(Include.STACK_TRACE));
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(new Include[]{Include.EXCEPTION});
}

if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}

if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}

if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}

if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}

return errorAttributes;
}
  • 篇幅所限,就不再展开上述代码了,直接上结果吧:
  1. 返回码来自determineHttpStatus的返回
  2. message字段来自determineMessage的返回
  • 打开determineHttpStatus方法,终极答案揭晓,请关注中文注释:
1
2
3
4
5
6
7
8
9
10
11
java复制代码private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
// 异常对象是不是ResponseStatusException类型
return error instanceof ResponseStatusException
// 如果是ResponseStatusException类型,就调用异常对象的getStatus方法作为返回值
? ((ResponseStatusException)error).getStatus()
// 如果不是ResponseStatusException类型,再看异常类有没有ResponseStatus注解,
// 如果有,就取注解的code属性作为返回值
: (HttpStatus)responseStatusAnnotation.getValue("code", HttpStatus.class)
// 如果异常对象既不是ResponseStatusException类型,也没有ResponseStatus注解,就返回500
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
}
  • 另外,message字段的内容也确定了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码    private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
// 异常对象是不是BindingResult类型
if (error instanceof BindingResult) {
// 如果是,就用getMessage作为返回值
return error.getMessage();
}
// 如果不是BindingResult类型,就看是不是ResponseStatusException类型
else if (error instanceof ResponseStatusException) {
// 如果是,就用getReason作为返回值
return ((ResponseStatusException)error).getReason();
} else {
// 如果也不是ResponseStatusException类型,
// 就看异常类有没有ResponseStatus注解,如果有就取该注解的reason属性作为返回值
String reason = (String)responseStatusAnnotation.getValue("reason", String.class).orElse("");
if (StringUtils.hasText(reason)) {
return reason;
} else {
// 如果通过注解取得的reason也无效,就返回异常的getMessage字段
return error.getMessage() != null ? error.getMessage() : "";
}
}
}
  • 至此,源码分析已完成,最终的返回码和返回内容究竟如何控制,相信聪明的您心里应该有数了,下一篇《实战篇》咱们趁热打铁,写代码试试精确控制返回码和返回内容
  • 提前剧透,接下来的《实战篇》会有以下内容呈现:
  1. 直接了当,控制返回码和body中的error字段
  2. 小小拦路虎,见招拆招
  3. 简单易用,通过注解控制返回信息
  4. 终极方案,完全定制返回内容
  • 以上内容敬请期待,欣宸原创必不辜负您

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

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

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

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

本文转载自: 掘金

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

Spring Boot的前世今生以及它和Spring Clo

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

要了解Spring Boot的发展背景,还得从2004年Spring Framework1.0版本发布开始说起,不过大家都是从开始学习Java就使用Spring Framework了,所以就不做过多展开。

随着使用Spring Framework进行开发的企业和个人越来越多,Spring 也慢慢从一个单一简洁的小框架编程了一个大而全的开源软件,Spring Framework的边界不断进行扩张,到了现在Spring 几乎可以做任何事情。目前市面上绝大部分的开源组件和中间件,都有Spring对应组件的支持,

你们如果去关注Spring目前的官网,你会发现他的slogan是:Spring makes Java Simple。它让Java的开发变得更加简单。

虽然Spring的组件代码是轻量级的,但是它的配置却是重量级的,Spring 每集成一个开源软件,就需要增加一些基础配置,慢慢的随着我们开发的项目越来越庞大,往往需要集成很多开源软件,因此后期使用 Spirng 开发大型项目需要引入很多配置文件,太多的配置非常难以理解,并容易配置出错,这个给开发人员带来了不少的负担。

大家想象一个场景,就是假如你需要用spring开发一个简单的Hello World Web应用程序,应该要做哪些动作呢?

  • 创建一个项目结构,必然包含依赖Maven或者Gradle的构建文件。
  • 至少需要添加spring mvc和servlet api的依赖
  • 一个web.xml,声明spring的DispatcherServlet
  • 一个启用了Spring MVC的spring 配置
  • 一个控制器类,“以HelloWord”为响应的http请求
  • 一个用于部署应用程序的web应用服务器,比如Tomcat

在整个过程中,我们发现只有一个东西和Hello Word功能相关,那就是控制器(controller),剩下的都是Spring 开发的Web应用程序必须要的通用模版,既然所有Spring Web应用程序都要用到他们,那为什么还要你来提供这些东西呢?

所以,直到2012年10月份,一个叫Mike Youngstrom(扬斯特罗姆)在Spring Jira中创建了一个功能请求,要求在Spring Framework中支持无容器Web应用程序体系结构,他谈到了在主容器引导 spring 容器内配置 Web 容器服务。

jira.spring.io/browse/SPR-…

1
2
txt复制代码I think that Spring's web application architecture can be significantly simplified if it were to provided tools and a reference architecture that leveraged the Spring component and configuration model from top to bottom. Embedding and unifying the configuration of those common web container services within a Spring Container bootstrapped from a simple main() method.
我认为,如果要提供从上到下充分利用Spring组件和配置模型的工具和参考体系结构,则可以大大简化Spring的Web应用程序体系结构。在通过简单main()方法引导的Spring容器中嵌入和统一那些通用Web容器服务的配置。

而且Spring 开发团队也意识到了这些问题,急需要一套软件来解决这个问题,而这个时候微服务的概念也慢慢的起来,快速开发微小独立的应用也变得很急迫。

而Spring恰好处在这样一个交叉点上,所以顺势而为在2013年初的时候,开始投入Spring Boot项目的研发,直到2014年4月,Spring Boot1.0版本发布。从那以后,Spring Boot开启了一些列的迭代和升级的过程。

经过7年时间的发展,到目前为止,Spring Boot最新稳定版为2.6.0版本。

Spring Boot的发展

Spring Boot刚出生的时候,引起了很多开源社区的关注,并且也有个人和企业开始尝试使用Spring Boot。 其实直到2016年,Spring Boot才真正在国内被使用起来。我之前在挖财的时候,2015年公司就开始采用Spring Boot来构建基于Dubbo的微服务架构。到现在,Spring Boot几乎是所有公司的第一选择。

Build Anything

Spring Boot被官方定位为“BUILD ANYTHING”,Spring Boot官方的概述是这么描述Spring Boot的。

1
2
3
4
java复制代码Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".
// 通过Spring Boot可以轻松的创建独立的、生产级别的基于Spring 生态下的应用,你只需要运行即可。
We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.
//对于Spring平台和第三方库,我们提供了一个固化的视图,这个视图可以让我们在构建应用是减少很多麻烦。大部分spring boot应用只需要最小的Spring 配置即可。

如果大家不习惯看英文文档,可能理解起来比较复杂,翻译成人话就是:Spring Boot能够帮助使用Spring Framework生态的开发者快速高效的构建一个基于Spring以及spring 生态体系的应用。

为了让大家对这句话的理解更加深刻,我们来做两个小实验,一个是基于传统的Spring MVC框架构建一个项目、另一种是使用Spring Boot。

Spring MVC With Spring Boot

通过Spring MVC项目搭建过程来对比Spring Boot的差异和优势。

Spring MVC项目搭建过程

  • 创建一个maven-webapp项目
  • 添加jar包依赖
1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
1
2
3
4
5
6
r复制代码spring-context
spring-context-support
spring-core
spring-expression
spring-web
spring-webmvc
  • 修改web.xml文件
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复制代码<context-param><!--配置上下文配置路径-->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--配置监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
<!--配置Spring MVC的请求拦截-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:dispatcher-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-patter>
</servlet-mapping>
  • 在resources目录下添加dispatcher-servlet.xml文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 扫描 controller -->
<context:component-scan base-package="com.gupaoedu.controller" />
<!--开启注解驱动-->
<mvc:annotation-driven/>
<!-- 定义视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"/>
<property name="suffix" value=".jsp"/>
</bean>
  • 创建一个Controller
1
2
3
4
5
6
7
8
9
java复制代码@Controller
public class HelloController {

@RequestMapping(method = RequestMethod.GET,path = "/index")
public String index(Model model){
model.addAttribute("key","Hello Gupao");
return "index";
}
}
  • 修改默认的index.jsp,设置el表达式的解析
1
2
3
html复制代码<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8" isELIgnored="false" %>
${key}
  • 运行项目

Spring Boot搭建过程

直接基于start.spring.io这个脚手架搭建即可。

思考和总结

咱们再回到最开始Spring Boot的定义部分,Spring Boot能够帮助使用Spring Framework生态的开发者快速高效的构建一个基于Spring以及spring 生态体系的应用。

再对比两种构建过程,似乎也能够理解Spring Boot的作用了吧。当然它的作用不仅于此,后续会逐步揭开它的真实面目。

通过上面这个案例我们发现,如果没有spring boot,要去构建一个Spring MVC的web应用,需要做的事情很多

  • 引入jar包
  • 修改web.xml,添加监听和拦截
  • 创建spring mvc核心配置文件dispatcher-servlet.xml
  • 创建controller
  • 部署到tomcat

这个过程如果不熟悉,很可能需要1~2个小时,如果是新手,可能需要更长时间。但是spring boot,不管是新手还是老手,都能够分分钟解决问题。

理解约定优于配置

我们知道,Spring Boot是约定由于配置理念下的产物,那么什么是约定由于配置呢?

约定优于配置是一种软件设计的范式,主要是为了减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。

简单来说,就是你所使用的工具默认会提供一种约定,如果这个约定和你的期待相符合,就可以省略那些基础的配置,否则,你就需要通过相关配置来达到你所期待的方式。

约定优于配置有很多地方体现,举个例子,比如交通信号灯,红灯停、绿灯行,这个是一个交通规范。你可以在红灯的时候不停,因为此时没有一个障碍物阻碍你。但是如果大家都按照这个约定来执行,那么不管是交通的顺畅度还是安全性都比较好。

而相对于技术层面来说,约定有很多地方体现,比如一个公司,会有专门的文档格式、代码提交规范、接口命名规范、数据库规范等等。这些规定的意义都是让整个项目的可读性和可维护性更强。

Spring Boot Web应用中约定优于配置的体现

那么在前面的案例中,我们可以思考一下,Spring Boot为什么能够把原本繁琐又麻烦的工作省略掉呢? 实际上这些工作并不是真正意义上省略了,只是Spring Boot帮我们默认实现了。

而这个时候我们反过来思考一下,Spring Boot Web应用中,相对Spring MVC框架的构建而言,它的约定由于配置体现在哪些方面呢?

  • Spring Boot的项目结构约定,Spring Boot默认采用Maven的目录结构,其中

src.main.java 存放源代码文件

src.main.resource 存放资源文件

src.test.java 测试代码

src.test.resource 测试资源文件

target 编译后的class文件和jar文件

  • 内置了嵌入式的Web容器,在Spring 2.2.6版本的官方文档中3.9章节中,有说明Spring Boot支持四种嵌入式的Web容器

Tomcat

Jetty

Undertow

Reactor

  • Spring Boot默认提供了两种配置文件,一种是application.properties、另一种是application.yml。Spring Boot默认会从该配置文件中去解析配置进行加载。
  • Spring Boot通过starter依赖,来减少第三方jar的依赖。

这些就是Spring Boot能够方便快捷的构建一个Web应用的秘密。当然Spring Boot的约定优于配置还不仅体现在这些地方,在后续的分析中还会看到Spring Boot中约定优于配置的体现。

Spring Boot整合Mybatis

实际上Spring Boot的本质就是Spring,如果一定要从技术发展的过程中找到一些相似的对比的话,你们可以对比一下Jsp/Servlet和Spring MVC, 两者都可以用来开发Web项目,但是在使用上,Spring MVC的使用会更加简单。

而Spring Boot和Spring 就相当于当年的JSP/Servlet和Spring MVC的关系。所以它本身并没有所谓新的技术,接下来,我带着大家来通过Spring Boot整合Mybatis实现数据的基本操作的案例,来继续认识一下Spring Boot。

创建Spring Boot 应用

创建一个Web项目

引入项目中需要的starter依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

创建数据库表

1
2
3
4
5
6
7
sql复制代码DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`address` varchar(80) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

配置数据库连接

1
2
3
4
5
6
yaml复制代码spring:
datasource:
url: jdbc:mysql://192.168.13.106:3306/test_springboot
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver

开发数据库访问层

1587536145803

创建实体对象

1
2
3
4
5
java复制代码public class User {
private int id;
private String name;
private String address;
}

创建Mapper

1
2
3
4
5
6
7
8
9
10
11
java复制代码//@Repository可以支持在你的持久层作为一个标记,可以去自动处理数据库操作产生的异常
@Repository
@Mapper
public interface UserMapper {

User findById(int id);
List<User> list();
int insert(User user);
int delete(int id);
int update(User user);
}

编写mapper文件

在resource文件目录下创建UserMapper.xml文件,内容如下

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
xml复制代码<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD com.example.Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<resultMap id="resultMap" type="com.example.demo.entity.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="address" column="address"/>
</resultMap>

<select id="findById" resultMap="resultMap" parameterType="java.lang.Integer">
select * from t_user where id=#{id}
</select>
<select id="list" resultMap="resultMap">
select * from t_user
</select>
<insert id="insert" parameterType="com.example.demo.entity.User" keyProperty="id" useGeneratedKeys="true">
insert into t_user(name,address) values(#{name,jdbcType=VARCHAR},#{address,jdbcType=VARCHAR})
</insert>
<delete id="delete" parameterType="java.lang.Integer">
delete from t_user where id=#{id}
</delete>
<update id="update" parameterType="com.example.demo.entity.User">
update t_user set name=#{name,jdbcType=VARCHAR},address=#{address,jdbcType=VARCHAR} where id=#{id,jdbcType=INTEGER}
</update>
</mapper>

定义service及实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public interface IUserService {

User findById(int id);
List<User> list();
int insert(User user);
int delete(int id);
int update(User user);
}

@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
}

创建Controller

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
java复制代码@RestController
public class Controller {

@Autowired
private IUserService userService;

@GetMapping("/user/{id}")
public User user(@PathVariable("id") int id){
return userService.findById(id);
}

@GetMapping("/users")
public List<User> users(){
return userService.list();
}

@PostMapping("/user")
public String insertUser(User user){
int row=userService.insert(user);
return row>0?"SUCCESS":"FAILED";
}

@PutMapping("/user")
public String updateUser(User user){
int row=userService.update(user);
return row>0?"SUCCESS":"FAILED";
}
@DeleteMapping("/user/{id}")
public String deleteUser(@PathVariable("id") int id){
return userService.delete(id)>0?"SUCCESS":"FAILED";
}

}

修改配置

  • 在Spring 的Main方法上增加以下注解,用来扫描Mybatis的Mapper文件
1
java复制代码@MapperScan("com.example.demo.mapper")
  • 配置Mapper配置文件的地址,在application.yml中
1
2
yml复制代码mybatis:
mapper-locations: classpath:*Mapper.xml

id int,

name varchar(20),

address varchar(20)

)

项目打包

  • mvn -Dmaven.test.skip -U clean install
  • java -jar xxx.jar

简单总结

这个代码,我想,大家应该写过无数遍了,而在基于Spring Boot集成Mybatis这个案例中,核心的业务逻辑并没有减少,它只减少了一些繁琐的配置,使得我们更聚焦在业务开发层面。

简单来说,基于Spring Boot的项目中,我们只需要写Controlelr、Service、Dao即可。甚至很多情况下我们dao都不需要管,比如使用mybatis-plus这个插件,就可以省去很多固定的dao层逻辑。

所以实际上,Spring Boot并没有新鲜的东西,因此你看到市面上大部分讲spring boot的书,这些书我几乎都看过,基本上都是讲解Spring Boot的应用,以及Spring Boot的一些特性分析。因为一旦你想讲Spring Boot的原理,就必然会回归到Spring这块的内容上。比如小马哥的Spring Boot编程思想着本书,大篇幅的都是在讲Spring Framework。因为Spring Boot的内核还是Spring Framework。

Spring Boot与微服务

接下来,给大家讲讲spring boot与微服务这块的内容。

什么是Spring Cloud

首先,我们要简单了解一下什么是微服务,按照我的理解来说,微服务就是微粒度的服务,它是面向服务架构(SOA)的进一步优化。如果大家不是很好理解,翻译成白话就是

一个业务系统,原本是在一个独立的war包中。现在为了更好的维护和提高性能,把这个war包按照业务纬度拆分成了一个个独立的业务子系统,每个子系统提供该业务领域相关的功能,并暴露API接口。

这些服务彼此之间进行数据交换和通信来实现整个产品的功能。

而这些业务子系统,实际上代表的就是一个服务,那么所谓的微服务,说的是这个服务的粒度。至于服务的粒度什么样才叫微,其实没有一个固定的衡量标准。更多的还是在每个公司具体的业务粒度的把控上。

微服务化遇到的问题

在为服务化之后,会面临很多的问题,比如服务注册、服务路由、负载均衡、服务监控等等。这些问题都需要有相应的技术来解决,这个时候,Spring Cloud就出现了。

简单来说,Spring Cloud 提供了一些可以让开发者快速构建微服务应用的工具,比如配置管理、服务发现、熔断、智能路由等,这些服务可以在任何分布式环境下很好地工作。Spring Cloud 主要
致力于解决如下问题:

  • Distributed/versioned configuration,分布式及版本化配置。
  • Service registration and discovery,服务注册与发现。
  • Routing,服务路由。
  • Service-to-service calls,服务调用。
  • Load balancing,负载均衡。
  • Circuit Breakers,断路器。
  • Global locks,全局锁。
  • Leadership election and cluster state,Leader 选举及集群状态。
  • Distributed messaging,分布式消息。

需要注意的是,Spring Cloud 并不是 Spring 团队全新研发的框架,它只是把一些比较优秀的解决微服务架构中常见问题的开源框架基于 Spring Cloud 规范进行了整合,通过 Spring Boot 这个
框架进行再次封装后屏蔽掉了复杂的配置,给开发者提供良好的开箱即用的微服务开发体验。不难看出,Spring Cloud 其实就是一套规范,而 Spring Cloud Netflix、Spring Cloud Consul、Spring CloudAlibaba 才是 Spring Cloud 规范的实现。

为什么Spring Cloud是基于Spring Boot

那为什么Spring Cloud会采用Spring Boot来作为基础框架呢?原因很简单

  1. Spring Cloud它是关注服务治理领域的解决方案,而服务治理是依托于服务架构之上,所以它仍然需要一个承载框架
  2. Spring Boot 可以简单认为它是一套快速配置Spring应用的脚手架,它可以快速开发单个微服务

在微服务架构下,微服务节点越来越多,需要一套成熟高效的脚手架,而Spring Boot正好可以满足这样的需求,如下图所示。

image-20211124135348046

Spring Boot的四大核心机制

如果一定要基于Spring Boot的特性去说,那么只能去说Spring Boot的四大核心机制,分别是@EnableAutoConfiguration 、 Starter开箱即用组件、Actuator应用监控、Spring Boot CLI 命令行工具。

EnableAutoConfiguration

Starter

告诉Spring Boot需要什么功能,它就能引入需要的库。

Actuator

让你能够深入运行中的Spring Boot应用程序

Spring Boot CLI

Spring Boot CLI 为Spring Cloud 提供了Spring Boot 命令行功能。我们可以通过编写groovy脚本来运行Spring Cloud 组件应用程序。步骤如下、

  • 下载spring-boot-cli

Spring Boot CLI:repo.spring.io/release/org…

  • 配置环境变量
  • 在控制台spring --version查看CLI版本
  • 使用CLI运行应用。我们可以使用run命令编译和运行Groovy源代码。Spring Boot CLI中包含所有运行Groovy所需要的依赖。
  • 创建一个hello.groovy文件
1
2
3
4
5
6
7
8
groovy复制代码@RestController
class HelloController {

@GetMapping("/hello")
String hello(){
return "Hello World";
}
}
  • 在控制台执行spring run hello.groovy,如果需要传递参数,比如端口,和JVM参数类似
1
shell复制代码spring run hello.groovy -- --server.port=9000

Spring Boot的四大核心特性

  • EnableAutoConfiguration
  • Starter
  • Actuator
  • Spring Boot CLI

Spring Boot CLI 为Spring Cloud 提供了Spring Boot 命令行功能。我们可以通过编写groovy脚本来运行Spring Cloud 组件应用程序。步骤如下、

+ 下载spring-boot-cli


Spring Boot CLI:[repo.spring.io/release/org…](https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/2.2.6.RELEASE/spring-boot-cli-2.2.6.RELEASE-bin.zip)
+ 配置环境变量
+ 在控制台`spring --version`查看CLI版本
+ 使用CLI运行应用。我们可以使用run命令编译和运行Groovy源代码。Spring Boot CLI中包含所有运行Groovy所需要的依赖。
+ 创建一个`hello.groovy`文件



1
2
3
4
5
6
7
8
groovy复制代码@RestController
class HelloController {

@GetMapping("/hello")
String hello(){
return "Hello World";
}
}
+ 在控制台执行`spring run hello.groovy`,如果需要传递参数,比如端口,和JVM参数类似
1
shell复制代码spring run hello.groovy -- --server.port=9000

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

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

基于IDEA Plugin插件开发,撸一个DDD脚手架

发表于 2021-11-25

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

  • 最近很感兴趣结合 IDEA Plugin 开发能力,扩展各项功能。也基于此使用不同的案例,探索 IDEA Plugin 插件开发技术。希望这样的成体系学习和验证总结,能给更多需要此技术的伙伴,带来帮助。
  • 源码地址:github.com/fuzhengwei/…

一、前言

研发,要避免自嗨!

你做这个东西的价值是什么?有竞品调研吗?能赋能业务吗?那不已经有同类的了,你为什么还自己造轮子?

你是不是也会被问到这样的问题,甚至可能还有些头疼。但做的时候挺嗨,研究技术嘛,还落地了,多刺激。不过要说价值,好像一时半会还体现不出来,能不能赋能业务就不更不一定了。

可谁又能保证以后不能呢,技术的点是一个个攻克尝试的才有机会再深度学习后把这些内容连成一片,就像单说水、单说沙子、单说泥巴,好像并没有啥用,但把它们凑到一块再给把火,就烧成了砖,砖就码成了墙,墙就盖成房。

二、需求目的

我们这一章节把 freemarker 能力与 IDEA Plugin 插件能力结合,开发一个DDD 脚手架 IDEA 插件,可能你会想为什么要把脚手架开发到插件里呢?还有不是已经有了成型的脚手架可以用吗?

首先我们目前看到的脚手架基本都是网页版的,也就是一次性创建工程使用,不过在我们实际使用的时候,还希望在工程创建过程中把数据库、ES、Redis等生成对应的 ORM 代码,减少开发工作量。并且在使用的工程骨架的过程中,还希望可以随着开发需要再次补充新的功能进去,这个时候网页版的脚手架都不能很好的支持了。此外一些大厂都会自己的技术体系,完全是使用市面的脚手架基本很难满足自身的需求,所以就需要有一个符合自己场景的脚手架了。

那么,我们本章节就把脚手架的开发放到 IDEA 插件开发中,一方面学习脚手架的建设,另外一方面学习如何改变工程向导,创建出自己需要的DDD结构脚手架。

三、案例开发

1. 工程结构

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
java复制代码guide-idea-plugin-scaffolding
├── .gradle
└── src
├── main
│ └── java
│ └── cn.bugstack.guide.idea.plugin
│ ├── domain
│ │ ├── model
│ │ │ └── ProjectConfigVO.java
│ │ └── service
│ │ ├── impl
│ │ │ └── ProjectGeneratorImpl.java
│ │ ├── AbstractProjectGenerator.java
│ │ ├── FreemarkerConfiguration.java
│ │ └── IProjectGenerator.java
│ ├── factory
│ │ └── TemplateFactory.java
│ ├── infrastructure
│ │ ├── DataSetting.java
│ │ ├── DataState.java
│ │ ├── ICONS.java
│ │ └── MsgBundle.java
│ ├── module
│ │ ├── DDDModuleBuilder.java
│ │ └── DDDModuleConfigStep.java
│ └── ui
│ ├── ProjectConfigUI.java
│ └── ProjectConfigUI.form
├── resources
│ ├── META-INF
│ │ └── plugin.xml
│ └── template
│ ├── pom.ftl
│ └── yml.ftl
├── build.gradle
└── gradle.properties

源码获取:#公众号:bugstack虫洞栈 回复:idea 即可下载全部 IDEA 插件开发源码

在此 IDEA 插件工程中,主要分为5块区域:

  • domain:领域层,提供创建 DDD 模板工程的服务,其实这部分主要使用的就是 freemarker
  • factory:工厂层,提供工程创建模板,这一层的作用就是我们在 IDEA 中创建新工程的时候,可以添加上我们自己的内容,也就是创建出我们定义好的 DDD 工程结构。
  • infrastructure:基础层,提供数据存放、图片加载、信息映射这些功能。
  • module:模块层,提供 DDD 模板工程的创建具体操作和步骤,也就是说我们创建工程的时候是一步步选择的,你可以按需添加自己的步骤页面,允许用户选择和添加自己需要的内容。比如你需要连库、选择表、添加工程所需要的技术栈等
  • ui:界面层,提供Swing 开发的 UI 界面,用于用户图形化选择和创建。

2. UI 工程配置窗体

1
2
3
4
5
6
7
8
9
java复制代码public class ProjectConfigUI {

private JPanel mainPanel;
private JTextField groupIdField;
private JTextField artifactIdField;
private JTextField versionField;
private JTextField packageField;

}
  • 使用 Swing UI Designer 创建一个配置工厂信息的 UI 窗体,通过这样的方式创建可以直接拖拽。
  • 在这个 UI 窗体中我们主要需要;roupId、artifactId、version、package

3. 配置工程步骤创建

3.1 数据存放

cn.bugstack.guide.idea.plugin.infrastructure.DataSetting

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
java复制代码@State(name = "DataSetting",storages = @Storage("plugin.xml"))
public class DataSetting implements PersistentStateComponent<DataState> {

private DataState state = new DataState();

public static DataSetting getInstance() {
return ServiceManager.getService(DataSetting.class);
}

@Nullable
@Override
public DataState getState() {
return state;
}

@Override
public void loadState(@NotNull DataState state) {
this.state = state;
}

public ProjectConfigVO getProjectConfig(){
return state.getProjectConfigVO();
}

}
  • 在基础层提供数据存放的服务,把创建工程的配置信息存放到服务中,这样比较方便设置和获取。

3.2 扩展步骤

cn.bugstack.guide.idea.plugin.module.DDDModuleConfigStep

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
java复制代码public class DDDModuleConfigStep extends ModuleWizardStep {

private ProjectConfigUI projectConfigUI;

public DDDModuleConfigStep(ProjectConfigUI projectConfigUI) {
this.projectConfigUI = projectConfigUI;
}

@Override
public JComponent getComponent() {
return projectConfigUI.getComponent();
}

@Override
public boolean validate() throws ConfigurationException {
// 获取配置信息,写入到 DataSetting
ProjectConfigVO projectConfig = DataSetting.getInstance().getProjectConfig();
projectConfig.set_groupId(projectConfigUI.getGroupIdField().getText());
projectConfig.set_artifactId(projectConfigUI.getArtifactIdField().getText());
projectConfig.set_version(projectConfigUI.getVersionField().getText());
projectConfig.set_package(projectConfigUI.getPackageField().getText());

return super.validate();
}

}
  • 继承 ModuleWizardStep 开发一个自己需要的步骤,这个步骤就会出现到我们创建新的工程中。
  • 同时在重写的 validate 方法中,把从工程配置 UI 窗体中获取到信息,写入到数据配置文件中。

3.3 配置步骤

cn.bugstack.guide.idea.plugin.module.DDDModuleBuilder

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
java复制代码public class DDDModuleBuilder extends ModuleBuilder {

private IProjectGenerator projectGenerator = new ProjectGeneratorImpl();

@Override
public Icon getNodeIcon() {
return ICONS.SPRING_BOOT;
}

/**
* 重写 builderId 挂载自定义模板
*/
@Nullable
@Override
public String getBuilderId() {
return getClass().getName();
}

@Override
public ModuleWizardStep[] createWizardSteps(@NotNull WizardContext wizardContext, @NotNull ModulesProvider modulesProvider) {

// 添加工程配置步骤,可以自己定义需要的步骤,如果有多个可以依次添加
DDDModuleConfigStep moduleConfigStep = new DDDModuleConfigStep(new ProjectConfigUI());

return new ModuleWizardStep[]{moduleConfigStep};
}
}
  • 在 createWizardSteps 方法中,把我们已经创建好的 DDDModuleConfigStep 添加工程配置步骤,可以自己定义需要的步骤,如果有多个可以依次添加。
  • 同时需要注意,只有重写了 getBuilderId() 方法后,你新增加的向导步骤才能生效。

4. 开发脚手架服务

cn.bugstack.guide.idea.plugin.domain.service.AbstractProjectGenerator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public abstract class AbstractProjectGenerator extends FreemarkerConfiguration implements IProjectGenerator {

@Override
public void doGenerator(Project project, String entryPath, ProjectConfigVO projectConfig) {

// 1.创建工程主POM文件
generateProjectPOM(project, entryPath, projectConfig);

// 2.创建四层架构
generateProjectDDD(project, entryPath, projectConfig);

// 3.创建 Application
generateApplication(project, entryPath, projectConfig);

// 4. 创建 Yml
generateYml(project, entryPath, projectConfig);

// 5. 创建 Common
generateCommon(project, entryPath, projectConfig);
}

}
  • 在 domain 领域层添加用于创建脚手架框架的 FreeMarker 服务,它是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。FreeMarker 在线手册:freemarker.foofun.cn
  • 按照 DDD 工程结构,分层包括:application、domain、infrastructure、interfaces,那么我们把这些创建过程抽象到模板方法中,具体交给子类来创建。

5. 调用脚手架服务

cn.bugstack.guide.idea.plugin.module.DDDModuleBuilder

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
java复制代码public class DDDModuleBuilder extends ModuleBuilder {

private IProjectGenerator projectGenerator = new ProjectGeneratorImpl();

@Override
public Icon getNodeIcon() {
return ICONS.SPRING_BOOT;
}

@Override
public void setupRootModel(@NotNull ModifiableRootModel rootModel) throws ConfigurationException {

// 设置 JDK
if (null != this.myJdk) {
rootModel.setSdk(this.myJdk);
} else {
rootModel.inheritSdk();
}

// 生成工程路径
String path = FileUtil.toSystemIndependentName(Objects.requireNonNull(getContentEntryPath()));
new File(path).mkdirs();
VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
rootModel.addContentEntry(virtualFile);

Project project = rootModel.getProject();

// 创建工程结构
Runnable r = () -> new WriteCommandAction<VirtualFile>(project) {
@Override
protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
projectGenerator.doGenerator(project, getContentEntryPath(), DataSetting.getInstance().getProjectConfig());
}
}.execute();

}

}
  • 在 DDDModuleBuilder#setupRootModel 中,添加创建 DDD工程框架的服务,projectGenerator.doGenerator(project, getContentEntryPath(), DataSetting.getInstance().getProjectConfig());
  • 另外这里需要用到 IDEA 提供的线程调用方法,new WriteCommandAction 才能正常创建。

6. 配置模板工程

6.1 模板工厂

cn.bugstack.guide.idea.plugin.factory.TemplateFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class TemplateFactory extends ProjectTemplatesFactory {

@NotNull
@Override
public String[] getGroups() {
return new String[]{"DDD脚手架"};
}

@Override
public Icon getGroupIcon(String group) {
return ICONS.DDD;
}

@NotNull
@Override
public ProjectTemplate[] createTemplates(@Nullable String group, WizardContext context) {
return new ProjectTemplate[]{new BuilderBasedTemplate(new DDDModuleBuilder())};
}

}
  • 模板工厂的核心在于把我们用于创建 DDD 的步骤添加 createTemplates 方法中,这样算把整个创建自定义脚手架工程的链路就串联完成了。

6.2 文件配置

plugin.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<idea-plugin>
<id>cn.bugstack.guide.idea.plugin.guide-idea-plugin-scaffolding</id>
<name>Scaffolding</name>
<vendor email="184172133@qq.com" url="https://bugstack.cn">小傅哥</vendor>

<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
on how to target different products -->
<depends>com.intellij.modules.platform</depends>

<extensions defaultExtensionNs="com.intellij">
<projectTemplatesFactory implementation="cn.bugstack.guide.idea.plugin.factory.TemplateFactory"/>
<applicationService serviceImplementation="cn.bugstack.guide.idea.plugin.infrastructure.DataSetting"/>
</extensions>

</idea-plugin>
  • 接下来还需要把我们创建的工程模板以及数据服务配置到 plugin.xml 中,这样在插件启动的时候就可以把我们自己插件启动起来了。

四、测试验证

  • 点击 Plugin 启动 IDEA 插件,之后创建工程如下:

  • 快拿去试试吧,启动插件,点击创建工程,傻瓜式点击,就可以创建出一个 DDD 工程结构了。

五、总结

  • 学习使用 IDEA Plugin 开发技术,改变创建工程向导,添加自己需要的工程创建模板,这样就可以创建出一个 DDD 脚手架工程骨架了,接下来你还可以结合自己实际的业务场景添加自己需要的一些技术栈到脚手架中。
  • 如果你愿意尝试可以在工程创建中链接到数据库,把数据库中对应的表生成Java代码,这样一些简单的配置、查询、映射,就不用自己动手写了。
  • 在开发 DDD 脚手架的源码中还有一些细节过程,包括图标的展示、文案的信息、Freemarker的使用细节,这些你都可以在源码中学习并调试验证。

六、系列推荐

  • 使用 Freemarker,创建 SpringBoot 脚手架
  • 发布Jar包到Maven中央仓库(为开发开源中间件做准备)
  • DDD 领域层决策规则树服务设计
  • 工作两三年了,整不明白架构图都画啥?
  • CodeGuide Github 仓库开源啦!

本文转载自: 掘金

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

SpringBoot 实战:自定义 Filter 优雅获取请

发表于 2021-11-25

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」

本文被《Spring Boot 实战》专栏收录。

你好,我是看山。

一个系统上线,肯定会或多或少的存在异常情况。为了更快更好的排雷,记录请求参数和响应结果是非常必要的。所以,Nginx 和 Tomcat 之类的 web 服务器,都提供了访问日志,可以帮助我们记录一些请求信息。

本文是在我们的应用中,定义一个Filter来实现记录请求参数和响应结果的功能。

有一定经验的都知道,如果我们在Filter中读取了HttpServletRequest或者HttpServletResponse的流,就没有办法再次读取了,这样就会造成请求异常。所以,我们需要借助 Spring 提供的ContentCachingRequestWrapper和ContentCachingRequestWrapper实现数据流的重复读取。

定义 Filter

通常来说,我们自定义的Filter是实现Filter接口,然后写一些逻辑,但是既然是在 Spring 中,那就借助 Spring 的一些特性。在我们的实现中,要继承OncePerRequestFilter实现我们的自定义实现。

从类名上推断,OncePerRequestFilter是每次请求只执行一次,但是,难道Filter在一次请求中还会执行多次吗?Spring 官方也是给出定义这个类的原因:

Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) method with HttpServletRequest and HttpServletResponse arguments.

As of Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore sub-classes can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types.

Subclasses may use isAsyncDispatch(HttpServletRequest) to determine when a filter is invoked as part of an async dispatch, and use isAsyncStarted(HttpServletRequest) to determine when the request has been placed in async mode and therefore the current dispatch won’t be the last one for the given request.

Yet another dispatch type that also occurs in its own thread is ERROR. Subclasses can override shouldNotFilterErrorDispatch() if they wish to declare statically if they should be invoked once during error dispatches.

也就是说,Spring 是为了兼容不同的 Web 容器,所以定义了只会执行一次的OncePerRequestFilter。

接下来开始定义我们的Filter类:

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
java复制代码public class AccessLogFilter extends OncePerRequestFilter {
    //... 这里有一些必要的属性

    @Override
    protected void doFilterInternal(final HttpServletRequest request,
                                    final HttpServletResponse response,
                                    final FilterChain filterChain)
            throws ServletException, IOException {
        // 如果是被排除的 uri,不记录 access_log
        if (matchExclude(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }

        final String requestMethod = request.getMethod();
        final boolean shouldWrapMethod = StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.PUT.name())
                || StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.POST.name());

        final boolean isFirstRequest = !isAsyncDispatch(request);

        final boolean shouldWrapRequest = isFirstRequest && !(request instanceof ContentCachingRequestWrapper) && shouldWrapMethod;
        final HttpServletRequest requestToUse = shouldWrapRequest ? new ContentCachingRequestWrapper(request) : request;

        final boolean shouldWrapResponse = !(response instanceof ContentCachingResponseWrapper) && shouldWrapMethod;
        final HttpServletResponse responseToUse = shouldWrapResponse ? new ContentCachingResponseWrapper(response) : response;

        final long startTime = System.currentTimeMillis();
        Throwable t = null;
        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } catch (Exception e) {
            t = e;
            throw e;
        } finally {
            doSaveAccessLog(requestToUse, responseToUse, System.currentTimeMillis() - startTime, t);
        }
    }

    // ... 这里是一些必要的方法

这段代码就是整个逻辑的核心所在,其他的内容从源码中找到。

分析

这个代码中,整体的逻辑没有特别复杂的地方,只需要注意几个关键点就可以了。

  1. 默认的HttpServletRequest和HttpServletResponse中的流被读取一次之后,再次读取会失败,所以要使用ContentCachingRequestWrapper和ContentCachingResponseWrapper进行包装,实现重复读取。
  2. 既然我们可以自定义Filter,那我们依赖的组件中也可能会自定义Filter,更有可能已经对请求和响应对象进行过封装,所以,一定要先进行一步判断。也就是request instanceof ContentCachingRequestWrapper和response instanceof ContentCachingResponseWrapper。

只要注意了这两点,剩下的都是这个逻辑的细化实现。

运行

接下来我们就运行一遍,看看结果。先定义几种不同的请求:普通 get 请求、普通 post 请求、上传文件、下载文件,这四个接口几乎可以覆盖绝大部分场景。(因为都是比较简单的写法,源码就不赘述了,可以从文末的源码中找到)

先启动项目,然后借助 IDEA 的 http 请求工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码###普通 get 请求
GET http://localhost:8080/index/get?name=howard

###普通 post 请求
POST http://localhost:8080/index/post
Content-Type: application/json

{"name":"howard"}

###上传文件
POST http://localhost:8080/index/upload
Content-Type: multipart/form-data; boundary=WebAppBoundary

--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="history.txt"
Content-Type: multipart/form-data
</Users/liuxh/history.txt
--WebAppBoundary--

###下载文件
GET http://localhost:8080/index/download

再看看打印的日志:

1
2
3
4
perl复制代码2021-04-29 19:44:57.495  INFO 83448 --- [nio-8080-exec-1] c.h.d.s.filter.AccessLogFilter           : time=44ms,ip=127.0.0.1,uri=/index/get,headers=[host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=null,responseContentType=text/plain;charset=UTF-8,params=name=howard,request=,response=
2021-04-29 19:44:57.551 INFO 83448 --- [nio-8080-exec-2] c.h.d.s.filter.AccessLogFilter : time=36ms,ip=127.0.0.1,uri=/index/post,headers=[content-type:application/json,content-length:17,host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=application/json,responseContentType=application/json,params=,request={"name":"howard"},response={"name":"howard","timestamp":"1619696697540"}
2021-04-29 19:44:57.585 INFO 83448 --- [nio-8080-exec-3] c.h.d.s.filter.AccessLogFilter : time=20ms,ip=127.0.0.1,uri=/index/upload,headers=[content-type:multipart/form-data; boundary=WebAppBoundary,content-length:232,host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=multipart/form-data; boundary=WebAppBoundary,responseContentType=application/json,params=,request=,response={"contentLength":"0","contentType":"multipart/form-data"}
2021-04-29 19:44:57.626 INFO 83448 --- [nio-8080-exec-4] c.h.d.s.filter.AccessLogFilter : time=27ms,ip=127.0.0.1,uri=/index/download,headers=[host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=null,responseContentType=application/octet-stream;charset=utf-8,params=,request=,response=

文末总结

自定义Filter是比较简单的,只要能够注意几个关键点就可以了。不过后续还有扩展的空间,比如:

  1. 定义排除的请求 uri,可以借助AntPathMatcher实现 ant 风格的定义
  2. 将请求日志单独存放,可以借助 logback 或者 log4j2 等框架的的日志配置实现,这样能更加方便的查找日志
  3. 与调用链技术结合,在请求日志中增加调用链的 TraceId 等,可以快速定位待查询的请求日志

可以关注公众号「看山的小屋」,回复“spring”获取源码。

推荐阅读

  • SpringBoot 实战:一招实现结果的优雅响应
  • SpringBoot 实战:如何优雅的处理异常
  • SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
  • SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
  • SpringBoot 实战:优雅的使用枚举参数
  • SpringBoot 实战:优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:JUnit5+MockMvc+Mockito 做好单元测试
  • SpringBoot 实战:加载和读取资源文件内容

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

Lombok你也许不知道的 Builder 坑坑!

发表于 2021-11-25

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

🤞 个人主页:@青Cheng序员石头

🤞 粉丝福利:加粉丝群 一对一问题解答,获取免费的丰富简历模板、提高学习资料等,做好新时代的卷卷王!

Lombok,使我们提供我们生产效率的一个强大的利器,其通过简单的注解来实现精简代码,消除冗长代码和提高开发效率的目的。我很喜欢使用的一个注解是@Builder,这个注解能让我很轻松的使用构造器模式,这篇文章记录如何在子类中使用@Builder避免一个常见的坑坑。

一、问题

对于下面这两个类,一个父类,一个子类。两个类都想使用@Builder注解,用于使用构造器模式去构造一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Getter
@ToString
public class Parent {

private long id;

private String name;

@Builder
@Getter
@ToString
static class Child extends Parent{
private String value;
}


}

当我们尝试编译上面的代码时,会报错,报错的内容如下。

1
ini复制代码无法将类 <包路径>.Parent中的构造器 Parent应用到给定类型;

这个原因是,这是由于Lombok未考虑父类的字段,而只考虑到当前子类的字段。

二、解决方案

解决办法有很多种,最简单的一种是我们在子类的构造函数中包含父类的字段,并且对构造函数使用@Builder的注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Getter
@AllArgsConstructor
@ToString
public class Parent {

private long id;

private String name;

@Getter
@ToString
static class Child extends Parent{

private String value;

@Builder
public Child(long id,String name,String value){
super(id,name);
this.value = value;
}
}
}

这种方式能解决上面的问题,但是如果我们想对父类使用@Builder的注解,如下面的代码所示。

1
2
3
4
5
6
7
java复制代码@Getter
@AllArgsConstructor
@ToString
@Builder
public class Parent {
...
}

编译的时候又会报另一个错误。

1
scss复制代码<包路径>.Child()无法覆盖<包路径>.Parent中的builder()

这个原因是,子类尝试申明具有跟父类相同名称的builder,说的通俗一定就是builder重名了。我们可以为子类替换一个构造器名字去解决这个问题,如下面的代码所示,为子类的Builder自定义一个名称。

1
2
3
4
5
java复制代码@Builder(builderMethodName = "childBuilder")
public Child(long id,String name,String value){
super(id,name);
this.value = value;
}

这个问题是解决了,机智的同学又会问了,那么如果我继承更深的层次呢,比如还有另外的类要继承Child类,也想使用Builder模式,怎么办呢?能想到最简单的办法就是像上面一样,将继承类的构造函数改写,支持父类所有的字段。

除了这个办法,还有什么简单快捷的方法呢?答案是Lombok本身提供了行之有效的解决办法。

三、更好的解决方案

Lombok1.18版引入了@SuperBuilder注解,使用这个注解同时注解父类和子类可以解决我们上面遇到的问题。

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
scala复制代码@Getter
@AllArgsConstructor
@ToString
@SuperBuilder
public class Parent {

private long id;

private String name;

@Getter
@ToString
@SuperBuilder
static class Child extends Parent{

private String value;

}

@Getter
@ToString
@SuperBuilder
static class ChildSChild extends Child{

private String personality;
}
}

需要注意两点:

  1. @SuperBuilder 和@Builder在父类和子类中不能混用
  2. @SuperBuilder在所有的父类和子类中都必须使用上,缺一不可。

四、总结

使用Lombok的目的是为了提供我们工作效率,建议在使用这些便捷工具时,要仔细理解一下官方的解释说明,其实很多使用技巧和避坑指南都写在了官方文档上。


少年,没看够?点击石头的详情介绍,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

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

URL URI傻傻分不清楚?dart告诉你该怎么用 简介 d

发表于 2021-11-25

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」

简介

如果我们要访问一个网站,需要知道这个网站的地址,网站的地址一般被称为URL,他的全称是Uniform Resource Locator。那么什么是URI呢?

URI的全程是Uniform Resource Identifier,也叫做统一资源标志符。

URI用来对资源进行标记,而URL是对网络上的资源进行标记,所以URL是URI的子集。

了解了URI和URL之间的关系之后,我们来看看dart语言对URI的支持。

dart中的URI

dart中为URI创建了一个专门的类叫做Uri:

1
kotlin复制代码abstract class Uri

Uri是一个抽象类,他定义了一些对URI的基本操作。它有三个实现类,分别是_Uri,_DataUri和_SimpleUri。

接下来,我们一起来看看,dart中的Uri都可以做什么吧。

encode和decode

为什么要对encode URI?

一般来说URI中可以包含一些特殊字符,像是空格或者中文等等。这些字符在传输中可能不被对方所认识。所以我们需要对Uri进行编码。

但是对于URI中的一些特殊但是有意义的字符,比如: /, :, &, #, 这些是不用被转义的。

所以我们需要一种能够统一编码和解码的方法。

在dart中,这种方法叫做encodeFull() 和 decodeFull():

1
2
3
4
5
6
7
8
ini复制代码var uri = 'http://www.flydean.com/doc?title=dart uri';

var encoded = Uri.encodeFull(uri);
assert(encoded ==
'http://www.flydean.com/doc?title=dart%20uri');

var decoded = Uri.decodeFull(encoded);
assert(uri == decoded);

如果要编码所有的字符,包括那些有意义的字符:/, :, &, #, 那么可以使用encodeComponent() 和 decodeComponent():

1
2
3
4
5
6
7
8
ini复制代码var uri = 'http://www.flydean.com/doc?title=dart uri';

var encoded = Uri.encodeComponent(uri);
assert(encoded ==
'http%3A%2F%2www.flydean.com%2Fdoc%3Ftitle%3Ddart%20uri');

var decoded = Uri.decodeComponent(encoded);
assert(uri == decoded);

解析URI

URI是由scheme,host,path,fragment这些部分组成的。我们可以通过Uri中的这些属性来对Uri进行分解:

1
2
3
4
5
6
7
8
ini复制代码var uri =
Uri.parse('http://www.flydean.com/doc#dart');

assert(uri.scheme == 'http');
assert(uri.host == 'www.flydean.com');
assert(uri.path == '/doc');
assert(uri.fragment == 'dart');
assert(uri.origin == 'http://www.flydean.com');

那么怎么构造Uri呢?我们可以使用Uri的构造函数:

1
2
3
4
5
6
7
less复制代码var uri = Uri(
scheme: 'http',
host: 'www.flydean.com',
path: '/doc',
fragment: 'dart');
assert(
uri.toString() == 'http://www.flydean.com/doc#dart');

总结

dart为我们提供了非常简单的Uri的使用工具。

本文已收录于 <www.flydean.com>

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

AEJoy —— 三分钟了解 AE 相关的颜色空间和颜色管理

发表于 2021-11-25

目标

了解色彩空间和色彩管理器

什么是颜色管理器

色彩管理器(又称 “色彩管理系统” 或 “色彩管理解决方案” )是一种改变图像和视频的色彩空间的应用程序。

在视觉特效制作中,许多颜色空间是根据数码电影相机、渲染 CG 、照片和哑光涂料等工具使用的。色彩空间的统一或转换是必要的。色彩空间转换是视觉特效制作中的一个重要过程。

什么是颜色空间

颜色空间是一种特定的颜色组织,其中颜色被表示为坐标。

颜色模型

颜色模型是用一些通道(如RGB、CMYK)描述颜色的抽象数学模型。虽然颜色模型和颜色空间不是一回事,但一些颜色系统如 Adobe RGB 和 sRGB 是基于颜色模型的。

代表性的颜色空间

image.png

Adobe Photoshop 中颜色选择器的颜色空间

image.png

一些 RGB 和 CMYK 色域 在 CIE 1931 xy 色度图上的比较, 基于下图

image.png【CIE 1931 xy 色度图】

和来自 Blatner 和 Fraser 的 “真实世界的图像处理软件” 的数据, p179:

image.png 【色域比较】

sRGB

sRGB(标准红绿蓝)是一种 RGB 颜色空间,主要用于显示器、打印机和互联网。在这种方法中可以表示的色域是由红、绿、蓝三种原色定义的颜色三角形。

image.png

CIE 1931 xy 色度图显示了 sRGB 颜色空间的色域和原色的位置。

这张图片是在 sRGB 空间着色的,所以三角形外的颜色是插值的

Adobe RGB

Adobe RGB 是 Adobe Systems 提出的一种颜色空间定义。它有一个比 sRGB 更广泛的 RGB 颜色再现范围(特别是绿色),旨在覆盖 CMYK 彩色打印机可实现的 大多数颜色。它包含 CIELAB 颜色空间中指定的大约 50% 的可见颜色。

image.png

CIE 1931 xy 色度图显示了 Adobe RGB(1998)的原色空间。CIE 标准光源 D65 白点显示在中心。

NTSC(BT.601)

NTSC 彩色空间是为电视设计的。它具有比 sRGB 宽得多的色域。虽然在现代显示器中不使用 NTSC ,但它通常用于比较和指定色域。

这也是世界上主要的电视形式之一,包括 PAL , SECAM 。

Rec.2020

ITU-R 建议书 BT.2020 (简称 Rec.2020 或 BT.2020) 用标准动态范围(SDR)和宽色域(WCG)定义了超高清电视(UHDTV)的各个方面。

它定义了图像分辨率、逐行扫描帧速率、位深度、原色、RGB 和流光色度颜色表示、色度子采样和光电传递函数。不支持全高清和 HDR 。

Rec.2100

Rec.2100 向上兼容 Rec.2020 。ITU-R 建议书 BT.2100 (又称 Rec.2100 或 BT.2100 )是处理全高清(2K)、4K 和 8K 分辨率的设备必须满足的规范的国际标准。它由国际电信联盟无线通信部门(ITU-R)设立。

CMYK(或 CMY)

CMYK 采用了在印刷过程中使用的减色法混色。

CMYK 对应油墨颜色,青色、品红、黄色和黑色。

HSV

HSV(色相,饱和度,值)用于在计算机上绘制或颜色样本。画家使用它是因为它是更自然和直观的考虑色彩的色相、色彩和饱和度,相对于添加混合或减色法混合来说。HSV 是 RGB 颜色空间的变换。HSV 也被称为 HSB(色相、饱和度、亮度)。

HSL

HSL(色相、饱和度、明度/亮度)与 HSV 非常相似,用 “明度” 代替了 “亮度” 。差异是一种值计算方法。HSV 采用六角金字塔模型,纯色的亮度等于白色的亮度,HLS 采用双六角金字塔模型,纯色的亮度是白色亮度的 50 %。(请参考下面这张图片)。

image.png

它也被称为 HSI (色相,饱和度,强度)。

LMS

LMS 色位是基于具有正常视力的人眼所具有的三种锥细胞。这些锥细胞能感知光,并在短波长、中波长和长波长有光谱灵敏度峰值。

三种锥体细胞(L、M、S)刺激水平对应的三个参数描述了任何人类的色觉。所以 LMS 颜色空间可以包含所有可见的颜色。

然而,由于L、M、S三个参数在人与环境之间存在差异,因此 LMS 并不是颜色的客观表征。

CIE 1931 颜色空间

CIE (Commission internationale de l’ éclairage,国际照明委员会)是制定有关光和颜色的国际标准的组织。

CIE 1931 颜色空间首次定义了电磁可见光谱中波长分布与生理感知颜色之间的定量关系。

CIE XYZ 是对颜色空间的重新映射,其中三刺激(tristimulus)值被概念化为三个原色的数量。这些原色是人类看不见的。CIE XYZ 颜色空间的设计使 Y 分量对应亮度。

CIE XYZ 可以包含所有可见的颜色。而 RGB 不能在不使用负值的情况下表示某些可见颜色,但 CIE XYZ 可以包含正象限的所有可见颜色。

为什么需要颜色管理

image.png

当您将导出的视频传输到其他媒体时,需要进行色域重映射、伽马校正、设置白色色度(白点)等操作。

什么是 ACES

ACES(Academy Color Encoding System)是由美国电影艺术与科学学院(Academy of Motion Picture Arts and Sciences)赞助创建的彩色图像编码系统。该系统定义自己的原色,包括 CIE xyY 规范定义的可见光谱轨迹。

当你在视频编辑软件中使用 ACES,如 Adobe After Effects, Maya 和 Nuke,是需要颜色管理器的。颜色管理器通常以插件或内置函数的形式提供。

代表性的颜色管理器

OpenColorIO

OpenColorIO (OCIO)是一个完整的色彩管理解决方案,面向电影制作,强调视觉效果和计算机动画。

OpenColorIO 官方网站(opencolorio.org/)

OCIO 与 ACES 兼容,不依赖于 LUT 格式,支持许多流行的格式。

SynColor

SynColor 是 Autodesk 颜色管理组件。

Adobe Color Management Module(CMM)

CMM 是 Adobe 软件(如 Photoshop )的颜色管理器。

补充

Lab

  • L: 表示颜色的亮度(从 0 到 100 )
  • a: 颜色中红色或绿色的范围(从 -128 到 +127 )
  • b: 颜色中蓝色或黄色的范围(从 -128 到 +127 )

本文转载自: 掘金

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

Gradle 系列(5)一键检索未适配 64 位架构的 so

发表于 2021-11-25

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。


前言

  • 最近,各大应用市场都在推动应用支持 64 位架构,你的 App 已经支持了吗?
  • 在这篇文章里,我将带你完成 64 位架构的的适配工作。同时会带你建立关于 ABI 的基本认识,并为你带来我的 Gradle 插件 EasyPrivacy,帮助你检测工程中的 64 位适配问题。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

这篇文章是 Gradle 系列文章第 5 篇,相关 Android 工程化专栏完整文章列表:

一、Gradle 基础:

  • 1、Gradle 基础 :Wrapper、Groovy、生命周期、Project、Task、增量
  • 2、Gradle 插件:Plugin、Extension 扩展、NamedDomainObjectContainer、调试
  • 3、Gradle 依赖管理
  • 4、Maven 发布:SHAPSHOT 快照、uploadArchives、Nexus、AAR
  • 5、Gradle 插件案例:EasyPrivacy、so 文件适配 64 位架构、ABI

二、AGP 插件:

  • 1、AGP 构建过程
  • 2、AGP 常用配置项:Manifest、BuildConfig、buildTypes、壳工程、环境切换
  • 3、APG Transform:AOP、TransformTask、增量、字节码、Dex
  • 4、AGP 代码混淆:ProGuard、R8、Optimize、Keep、组件化
  • 5、APK 签名:认证、完整性、v1、v2、v3、Zip、Wallet
  • 6、AGP 案例:多渠道打包

三、组件化开发:

  • 1、方案积累:有赞、蘑菇街、得到、携程、支付宝、手淘、爱奇艺、微信、美团
  • 2、组件化架构基础
  • 3、ARouter 源码分析
  • 4、组件化案例:通用方案
  • 5、组件化案例:组件化事件总线框架
  • 6、组件化案例:组件化 Key-Value 框架

四、AOP 面向切面编程:

  • 1、AOP 基础
  • 2、Java 注解
  • 3、Java 注解处理器:APT、javac
  • 4、Java 动态代理:代理模式、Proxy、字节码
  • 5、Java ServiceLoader:服务发现、SPI、META-INF
  • 6、AspectJ 框架:Transform
  • 7、Javassist 框架
  • 8、ASM 框架
  • 9、AspectJ 案例:限制按钮点击抖动

五、相关计算机基础

  • 1、Base64 编码
  • 2、安全传输:加密、摘要、签名、CA 证书、防窃听、完整性、认证

目录


  1. 概述

1.1 CPU 和 ABI 的关系

CPU 架构是 CPU 厂商定义的 CPU 规范,目前主流的 CPU 架构分为两大类:

  • 复杂指令集(CISC): 例如 Intel 和 AMD 的 X86 架构;
  • 精简指令集(RISC): 例如 IBM 的 PowerPC 架构、ARM 的 ARM 架构。

应用二进制接口(Application Binary Interface, ABI)定义了机器代码和操作系统的交互,与我们熟知 API 会以一个接口源码实体存在不同,ABI 更应该理解为一种规范。 ABI 包含信息详见 Android ABI —— 官方文档

1.2 Android 支持 的 ABI

不同的 Android 设备使用不同的 CPU,不同 CPU 支持的 ABI 也不同。目前,Android 设备支持的 ABI 类型如下:

ABI 描述
armeabi 第 5 代、第 6 代的ARM 处理器,基本退出历史舞台
armeabiv-v7a 第 7 代及以上的 ARM 处理器,正在逐步退出历史舞台
arm64-v8a 第 8 代、64 位 ARM 处理器,目前是主流
x86 / x86_64 一般是模拟器

不同 CPU 支持的 ABI 情况如下:

. armeabi armeabi-v7a arm64-v8a x86 x86_64 mips mips64
ARMv5 /ARMv6 ✔
ARMV7 ✔ ✔
ARMV8 ✔ ✔ ✔
X86 ✔ ✔ ✔
X86-64 ✔ (不支持) ✔ ✔
MIPS ✔
MIPS64 ✔ ✔

提示: 通过 Build.SUPPORTED_ABIS 可以得到设备支持的 ABI 列表,并且是按照偏好排序的。

1.3 主要 ABI 和辅助 ABI

每个 CPU 架构都有一个主要 ABI 和(可选的)兼容的辅助 ABI,64 位 CPU 可以兼容 32 位 ABI(例如 x86_64 兼容 x86,反过来不行)。 需要注意的是:只有使用主要 ABI 才能获得最佳性能(例如 x86 兼容 armeabi ),这就是应用市场着手推动 64 位架构适配的根本原因。


  1. 为 Android 设备适配 64 位架构

2.1 64 位架构适配的时间节点

海外应用市场早在 19 年就在推进 64 位架构的适配,从 2019 年 8 月 1 日起,在 Google Play 上发布的应用就必须支持 64 位架构。至于国内应用市场,大致的时间节点如下(以 小米、VIVO、OPPO 为例):

  • 至 2021 年 12 月底,在应用市场发布的应用必须支持 64 位架构;
  • 至 2022 年 8 月底,对于支持 64 位的硬件系统,将只接收 64 位版本的 APK;
  • 至 2023 年 12 月底,硬件将仅支持 64 位 APK,

2.2 Android 系统 ABI 管理

在安装应用时,PMS 服务将扫描 APK 文件,从中查找出 APK 中主要 ABI 类型的 so 文件:

1
vbnet复制代码lib/<primary-abi>/lib<name>.so

如果没有找到,则会去查找 APK 文件中辅助 ABI 类型的 so 文件:

1
vbnet复制代码lib/<secondary-abi>/lib<name>.so

完成查找后,PMS 会将它们复制到 app 目录下的 so 库路径(例如:/data/app/[packagename]/lib/arm64),并在应用运行时执行到 System.loadLibrary(…) 时加载到内存中。如果没有查找到匹配的 so 文件,不会中断安装过程,但在运行时会崩溃。

关于加载 so 文件的过程,我们在 《说说 so 库从加载到卸载的全过程》 这篇文章里已经讨论过了。你可以回去看看,主要源码在:DexPathList.java

—— 图片引用自爱奇艺技术团队

可以看到,适配 64 位架构到底是做什么呢?说到底就是为系统提供性能最高的主要 ABI so 文件。 上层应用的重点就是提供 64 位的 so 文件,我们可以将需要做的事情拆解为三部分:

  • 1、检索不支持 64 位 的 so 文件(EasyPrivacy 插件)
  • 2、构建 64 位 APK
  • 3、分发 64 位 APK

  1. EasyPrivacy 插件一键检索 so 文件

关于如何检索 APK 中不支持 64 位 的 so 文件,官方提供了两种方法,具体可参考 官方文档:

  • 1、通过 APK 分析器分析(直接将 APK 拖到 Android Studio 上);
  • 2、解压缩 APK 并通过 grep 命令来分析。

这两种方法基本可以满足要求,但操作上太费时间,也无法直接提示 so 文件是通过哪个组件来集成的 (例如,push.aar 内部集成了 libc++_shared.so,通过 APK 无法知晓该 so 文件是来自 push.aar)。为了快速检索到项目中不支持 64 位 的 so 文件,贴心的我已经帮你实现为一个 EasyPrivacy 插件。源码地址:github.com/pengxurui/E…

3.1 添加依赖

  • 1、依赖 EasyPrivacy 插件

在项目级 build.gradle 中声明远程仓库,并依赖 EasyPrivacy 插件:

项目级 build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码buildscript {
repositories {
...
google()
mavenCentral()
// JitPack 仓库
maven { url "https://jitpack.io" }
}
dependencies {
...
classpath 'com.github.pengxurui:EasyPrivacy:v1.0.2
}
}
  • 2、应用 EasyPrivacy 插件

在应用级或者模块级 build.gradle 中应用 EasyPrivacy 插件:

build.gradle

1
2
arduino复制代码apply plugin: 'com.pengxr.easyprivacy'
...

执行 Sync Gradle 之后,可以在 Gradle 面板中看到新增的检测任务,具体位于 privacy 任务组:

3.2 执行 support 64-bit abi

执行 support 64-bit abi 任务,将检索该模块的 Gradle 依赖树中的 so 文件,从中筛选出其中没有完成 64 位适配的 so 文件。例如, 项目中存在 armeabiv-v7a 类型的 libc++_shared.so 文件,但没有提供对应的 64 位arm64-v8a 类型,就会在分组so in armeabiv-v7a, but not in arm64-v8a:中增加提示。

3.3 分析日志

以下是在 sample 模块的日志输出:

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
ini复制代码...
> Configure project :sample
...
> Task :sampleLib:copyReleaseJniLibsProjectOnly UP-TO-DATE
> Task :sample:mergeReleaseNativeLibs UP-TO-DATE
> Task :sample:support 64-bit abi
EasyPrivacy => Support 64-bit abi start.
EasyPrivacy => so: /Users/pengxurui/.gradle/caches/transforms-2/files-2.1/297c6c751393f4fc48ae19ba461e118a/openDefault-4.2.7/jni/armeabi-v7a/libweibosdkcore.so
EasyPrivacy => so: /Users/pengxurui/.gradle/caches/transforms-2/files-2.1/297c6c751393f4fc48ae19ba461e118a/openDefault-4.2.7/jni/x86/libweibosdkcore.so
EasyPrivacy => so: /Users/pengxurui/.gradle/caches/transforms-2/files-2.1/297c6c751393f4fc48ae19ba461e118a/openDefault-4.2.7/jni/armeabi/libweibosdkcore.so
EasyPrivacy => so: /Users/pengxurui/workspace/public/EasyPrivacy/sample/build/intermediates/merged_jni_libs/release/out/armeabi-v7a/libbsdiff.so
EasyPrivacy => so: /Users/pengxurui/workspace/public/EasyPrivacy/sample/build/intermediates/merged_jni_libs/release/out/x86/libbsdiff.so
EasyPrivacy => so: /Users/pengxurui/workspace/public/EasyPrivacy/sample/build/intermediates/merged_jni_libs/release/out/armeabi/libbsdiff.so
EasyPrivacy => so: /Users/pengxurui/workspace/public/EasyPrivacy/sampleLib/build/intermediates/library_jni/release/jni/armeabi-v7a/libgetuiext3.so
EasyPrivacy => so: /Users/pengxurui/workspace/public/EasyPrivacy/sampleLib/build/intermediates/library_jni/release/jni/armeabi-v7a/libc++_shared.so
EasyPrivacy => so: /Users/pengxurui/workspace/public/EasyPrivacy/sampleLib/build/intermediates/library_jni/release/jni/arm64-v8a/libgetuiext3.so

EasyPrivacy => armeabi size: 2
EasyPrivacy => armeabiv-v7a size: 4
EasyPrivacy => arm64-v8a size: 1
EasyPrivacy => x86 size: 2
EasyPrivacy => x86_64 size: 0
EasyPrivacy => mips size: 0
EasyPrivacy => mips_64 size: 0

so in armeabi, but not in arm64-v8a:
[openDefault-4.2.7:libweibosdkcore.so] [:libbsdiff.so]

so in armeabiv-v7a, but not in arm64-v8a:
[openDefault-4.2.7:libweibosdkcore.so] [:libbsdiff.so] [:libc++_shared.so]

so in x86, but not in x86-64:
[openDefault-4.2.7:libweibosdkcore.so] [:libbsdiff.so]

so in mips, but not in mips-64:

从以上日志可以看出,[openDefault-4.2.7:libweibosdkcore.so]、[:libbsdiff.so]、[:libc++_shared.so] 这三个 so 文件没有提供 arm64-v8a 类型,这部分就是你需要做适配的内容。

其中 openDefault-4.2.7 是 so 文件所处的 aar 的 pom 信息,你可以根据这个信息来判断需要适配的 SDK。另外,像 :libbsdiff.so 这种则属于直接集成在工程中的 so 文件。


  1. 构建 64 位 APK

完成适配工作后,现在需要构建出 64 位的 APK。根据应用市场的要求,你需要构建出三种包:

  • 1、32 位包
  • 2、64 位包
  • 3、32 / 64 位包(同时包含 32 位 和 64 位两种 so 文件)

4.1 ndk.abiFilters 配置

通过 ndk. abiFilters 配置可以过滤出需要打包到 APK 中的 so 文件,例如以下配置将会把 armeabi-v7a 和 arm64-v8a 两种 ABI 类型的 so 文件打包到 APK 中:

应用级 build.gradle

1
2
3
4
5
6
7
8
9
arduino复制代码android {
...
defaultConfig {
...
ndk {
abiFilters "armeabi-v7a","arm64-v8a"
}
}
}

4.2 splits 配置

ndk.abiFilters 配置可以将所有支持的 ABI 的 so 文件都打包进 APK,缺点是包体积增大。其实,应用市场是支持单独分发 32 位和 64 位 APK 包的能力的,我们可以使用 splits 配置。例如以下配置会将每种 ABI 类型单独打包。universalApk 为 ture 时还会额外构建一个包含所有 ABI 类型的 APK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码android {
...
defaultConfig {
...
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a'
universalApk false
}
}
}
}

  1. 总结

EasyPrivacy 框架的源码我已经放在 Github 上了,源码地址:github.com/pengxurui/E… 我也写了一个简单的 Sample Demo,你可以直接运行体验下。欢迎批评,欢迎 Issue~

最近几个月,你是否经常会收到应用市场的隐私整改邮件呢?是的,在中国用户隐私意识得到强化的同时,针对 App 的隐私规范整改也在一步步收紧,海外(Google Play/App Store)走过的老路,最终我们也得走一遍呀!

我们会发现隐私整改是每个 App 都无法规避的问题,具备共性。我想做一个专门针对隐私整改的 Gradle 插件 EasyPrivacy,帮助开发者快速发现工程中隐私问题。市面上目前有类似的工具吗,可以分享给我。或者你可以说说那些最让你头疼的整改问题(给我提 Feature!)


参考资料

  • 支持 64 位架构 —— 官方文档
  • 构建多个 APK —— 官方文档
  • Android ABI —— 官方文档
  • 爱奇艺 App 架构升级之路——64 位适配探索与实践 —— 爱奇艺技术产品团队 著
  • Android 适配 64 位架构 —— callmepeanut 著

我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。

本文转载自: 掘金

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

AEJoy —— AE 插件开发中的 命令选择器(三) 正文

发表于 2021-11-25

「这是我参与11月更文挑战的第 23 天,活动详情查看:2021最后一次更文挑战」。

参加该活动的第 40 篇文章

正文

帧选择器

为插件渲染的每一帧(或一组音频采样)传递。

PF_Cmd_FRAME_SETUP

分配任何帧特定的数据。这是在每一帧渲染之前立即发送的,以允许帧特定的设置(setup)数据。如果您的效果改变了其输出缓冲区的大小,请指定新的输出高度、宽度和相对原点。除了输入层之外的所有参数都是有效的。如果你设置宽度和高度为 0 , After Effects 将忽略你对以下 PF_Cmd_RENDER 的响应。注意:如果设置了PF_Outflag_I_EXPAND_BUFFER,你将接收这个选择器(还有 PF_Cmd_FRAME_SETDOWN*) 两次,一旦在它们之间没有 *PF_Cmd_RENDER 的话。这样我们就可以知道给定的图层是否可见。帧数据可以追溯到机器有 8 MB RAM 的时候。考虑到调用序列(上面),仅在 PF_Cmd_RENDER 期间分配要高效得多。

PF_Cmd_RENDER

根据输入帧和任何参数将效果渲染到输出中。这个渲染调用只能支持 8 位或 16 位每通道渲染。32 位每通道渲染必须在 PF_Cmd_SMART_RENDER 中处理。PF_InData 中的所有字段都是有效的。如果您对该选择器的响应被中断(您对 PF_ABORT 或 PF_PROGRESS 的调用返回一个错误代码),您的结果将不会被使用。你不能在这个选择器中删除 frame_data ; 你必须等待直到PF_Cmd_FRAME_SETDOWN 。

PF_Cmd_FRAME_SETDOWN

释放任何在 PF_Cmd_FRAME_SETUP 期间分配的帧数据

PF_Cmd_AUDIO_SETUP

在每个音频渲染之前发送。请求输入音频的时间跨度(time span)。分配和初始化任何特定序列的数据。如果您的效果需要来自时间跨度的输入而不是输出时间跨度,那么请更新 PF_OutData 中的 startsampL 和 endsampL 字段。

PF_Cmd_AUDIO_RENDER

用效果处理过的音频填充PF_OutData.dest_snd。PF_InData 中的所有字段都是有效的。如果你对这个选择器的响应被中断(你对 PF_ABORT 或 PF_PROGRESS 的调用会返回一个错误代码),你的结果将不会被使用。

PF_Cmd_AUDIO_SETDOWN

释放 PF_Cmd_AUDIO_SETUP 期间分配的内存。

PF_Cmd_SMART_PRE_RENDER

仅限于 SmartFX。根据 effect 实现的任何标准,确定 effect 所需要产生其输出的输入区域。

可能在 MediaCore 托管时发送两次*。第一个将在 *GetFrameDependencies 期间收集输入。

源检出可以在这里返回完整的帧尺寸。一旦渲染了源,如果它们的大小与第一次调用不同,那么这个选择器将使用实际的源大小第二次发出,以获得正确的输出大小。

注意,MediaCore 需要所有的输出,因此将使用 PF_PreRenderOutput::max_result_rect 。

16.0 的新特性

在 PF_PreRenderOutput 中将 PF_RenderOutputFlag_GPU_RENDER_POSSIBLE 设置为 GPU 渲染。

如果没有设置此标志,则由于参数或渲染设置的原因,请求的渲染无法使用所请求的 GPU 。

主机可能会使用另一个 what_gpu 选项(或 PF_GPU_Framework_None )来重新调用 PreRender 。

1
2
3
4
5
6
7
8
cpp复制代码typedef struct
{
PF_RenderRequest output_request; // 被要求渲染的效果
short bitdepth; // 正被驱动的效果(在 bpc 中的)位深
const void *gpu_data; // (new AE 16.0)
PF_GPU_Framework what_gpu; // (new AE 16.0)
A_u_long device_index; // (new AE 16.0) 和 PrSDKGPUDeviceSuite 配合使用
} PF_PreRenderInput;

PF_Cmd_SMART_RENDER

仅限 SmartFX。执行渲染并为 effect 被要求渲染的区域提供输出。

Frame Selectors

Passed for each frame (or set of audio samples) to be rendered by your plug-in.

PF_Cmd_FRAME_SETUP

Allocate any frame-specific data. This is sent immediately before each frame is rendered, to allow for frame-specific setup data. If your effect changes the size of its output buffer, specify the new output height, width, and relative origin. All parameters except the input layer are valid.If you set width and height to 0, After Effects ignores your response to the following PF_Cmd_RENDER.NOTE: If PF_Outflag_I_EXPAND_BUFFER is set, you will receive this selector (and PF_Cmd_FRAME_SETDOWN) twice, once without PF_Cmd_RENDER between them.This is so we know whether or not the given layer will be visible.Frame data dates from the days when machines might have 8MB of RAM. Given the calling sequence (above), it’s much more efficient to just allocate during PF_Cmd_RENDER.

PF_Cmd_RENDER

Render the effect into the output, based on the input frame and any parameters.This render call can only support 8-bit or 16-bit per channel rendering. 32-bit per channel rendering must be handled in PF_Cmd_SMART_RENDER.All fields in PF_InData are valid.If your response to this selector is interrupted (your calls to PF_ABORT or PF_PROGRESS returns an error code), your results will not be used.You cannot delete frame_data during this selector; you must wait until PF_Cmd_FRAME_SETDOWN.

PF_Cmd_FRAME_SETDOWN

Free any frame data allocated during PF_Cmd_FRAME_SETUP.

PF_Cmd_AUDIO_SETUP

Sent before every audio render. Request a time span of input audio. Allocate and initialize any sequence-specific data.If your effect requires input from a time span other than the output time span, update the startsampL and endsampL field in PF_OutData.

PF_Cmd_AUDIO_RENDER

Populate PF_OutData.dest_snd with effect-ed audio. All fields in PF_InData are valid.If your response to this selector is interrupted (your calls to PF_ABORT or PF_PROGRESS returns an error code), your results will not be used.

PF_Cmd_AUDIO_SETDOWN

Free memory allocated during PF_Cmd_AUDIO_SETUP.

PF_Cmd_SMART_PRE_RENDER

SmartFX only. Identify the area(s) of input the effect will need to produce its output, based on whatever criteria the effect implements.maybe sent up to twice when MediaCore is hosting. The first will come during GetFrameDependencies to collect the inputs.The source checkouts can return full frame dimensions here. Once the sources are rendered, if they are different in size than the first call then this selector will be emitted a second time with the actual source sizes in order to get a correct output size.Note that MediaCore wants all of the output, so PF_PreRenderOutput::max_result_rect will be used.New in 16.0Set PF_RenderOutputFlag_GPU_RENDER_POSSIBLE in PF_PreRenderOutput to render on the GPU.If this flag is not set the requested render is not possible with the requested GPU, because of parameters or render settings.The host may re-call PreRender with another what_gpu option (or PF_GPU_Framework_None).

1
2
3
4
5
6
7
8
9
10
> cpp复制代码typedef struct
> {
> PF_RenderRequest output_request; // what the effect is being asked to render
> short bitdepth; // bitdepth the effect is being driven in (in bpc)
> const void *gpu_data; // (new AE 16.0)
> PF_GPU_Framework what_gpu; // (new AE 16.0)
> A_u_long device_index; // (new AE 16.0) For use in conjunction with PrSDKGPUDeviceSuite
> } PF_PreRenderInput;
>
>

PF_Cmd_SMART_RENDER

SmartFX only. Perform rendering and provide output for the area(s) the effect was asked to render.

本文转载自: 掘金

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

Gin 框架 基于云原生环境,区分配置文件 介绍 安装 快

发表于 2021-11-25

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

介绍

通过一个完整例子,在 Gin 框架中,根据环境区分配置文件。也就是如何在【测试】,【线上】等环境中,读取不同的配置文件。

我们将会使用 rk-boot 来启动 Gin 框架微服务。

请访问如下地址获取完整教程:

  • rkdocs.netlify.app/cn

安装

1
2
go复制代码go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-gin

快速开始

我们会创建 config/beijing.yaml, config/shanghai.yaml, config/default.yaml 三个配置文件,然后根据不同的环境变量读取不同的文件。

rk-boot 使用 REALM,REGION,AZ,DOMAIN 环境变量来区分不同的环境。这也是我们推荐的云原生环境分辨法。
比如,REALM=”你的业务”,REGION=”北京”,AZ=”北京一区”,DOMAIN=”测试环境”。

rk-boot 集成了 viper 来处理配置文件。

1.创建配置文件

  • config/beijing.yaml
1
2
yaml复制代码---
region: beijing
  • config/shanghai.yaml
1
2
yaml复制代码---
region: shanghai
  • config/default.yaml
1
2
yaml复制代码---
region: default

2.创建 boot.yaml

boot.yaml 文件告诉 rk-boot 如何启动 Gin 服务。

我们使用 config 作为 boot.yaml 中配置文件的入口,可以提供多个 config 文件路径。

locale 代表 Config 的环境,我们使用 locale 来区分不同的 Config。

为什么 config.name 使用同一个名字?

我们希望使用同一套代码,但是读取不同的文件,并且希望文件的名字也不一样。
所以通过 locale 来区分不同文件。我们在后面具体介绍 locale 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码config:
# 默认
- name: my-config
locale: "*::*::*::*"
path: config/default.yaml
# 如果环境变量 REGION=beijing,读取此文件
- name: my-config
locale: "*::beijing::*::*"
path: config/beijing.yaml
# 如果环境变量 REGION=shanghai,读取此文件
- name: my-config
locale: "*::shanghai::*::*"
path: config/shanghai.yaml
gin:
- name: greeter
port: 8080
enabled: true

3.创建 main.go

设置环境变量:REGION=”beijing”,然后读取配置文件,config/beijing.yaml 会被读取。

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
go复制代码package main

import (
"context"
"fmt"
"github.com/rookie-ninja/rk-boot"
_ "github.com/rookie-ninja/rk-gin/boot"
"os"
)

// Application entrance.
func main() {
// Set REGION=beijing
os.Setenv("REGION", "beijing")

// Create a new boot instance.
boot := rkboot.NewBoot()

// Load config which is config/beijing.yaml
fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

4.文件夹结构

1
2
3
4
5
6
7
8
9
10
go复制代码$ tree
.
├── boot.yaml
├── config
│ ├── beijing.yaml
│ ├── default.yaml
│ └── shanghai.yaml
├── go.mod
├── go.sum
└── main.go

5.验证

1
go复制代码$ go run main.go

我们会得到如下的输出:

1
复制代码beijing

6.未找到匹配的环境变量

如果 REGION=”not-matched”,即未找到匹配的环境变量,则会读取默认的配置文件(config/default.yaml)。因为 config/default.yaml 的 locale 属性为 *::*::*::*

1
2
3
4
5
6
7
8
9
10
go复制代码// Application entrance.
func main() {
// Set REGION=not-matched
os.Setenv("REGION", "not-matched")

...
// Load config which is config/beijing.yaml
fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))
...
}
1
arduino复制代码default

7.环境变量未配置

如果我们没有配置 REGION 环境变量,则会读取 config/default.yaml 文件。

1
2
3
4
5
6
7
go复制代码// Application entrance.
func main() {
...
// Load config which is config/beijing.yaml
fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))
...
}
1
arduino复制代码default

概念

rk-boot 使用 REALM,REGION,AZ,DOMAIN 四个环境变量来区分配置文件。

这四个环境变量可以是任意的值。

最佳实践

举个例子,我们有一个【云相册】业务。此业务在不同环境里使用的 MySQL 的 IP 地址不一样,则可以这么配置。

架构

假定,我们的业务在【北京】,【上海】都有服务器,同时为了提高服务可用性,在【北京】和【上海】又各开了2个区。

这时候,我们可以机器上配置如下的环境变量,可以通过 Ansible 等工具来批量设置。

环境 对应环境变量
北京,一区,测试 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-1”,DOMAIN=”test”
北京,一区,线上 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-1”,DOMAIN=”prod”
北京,二区,测试 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-2”,DOMAIN=”test”
北京,二区,线上 REALM=”cloud-album”,REGION=”bj”,AZ=”bj-2”,DOMAIN=”prod”
上海,一区,测试 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-1”,DOMAIN=”test”
上海,一区,线上 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-1”,DOMAIN=”prod”
上海,二区,测试 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-2”,DOMAIN=”test”
上海,二区,线上 REALM=”cloud-album”,REGION=”sh”,AZ=”sh-2”,DOMAIN=”prod”

同时,如果我们不使用类似 ETCD,Consul 等服务远程拉取配置文件,可以直接在机器中添加如下文件。每个文件都有不同的 MySQL IP 地址。

文件夹结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码.
├── boot.yaml
├── config
│ ├── bj-1-test.yaml
│ ├── bj-1-prod.yaml
│ ├── bj-2-test.yaml
│ ├── bj-2-prod.yaml
│ ├── sh-1-test.yaml
│ ├── sh-1-prod.yaml
│ ├── sh-2-test.yaml
│ ├── sh-2-prod.yaml
│ └── default.yaml
├── go.mod
├── go.sum
└── main.go

boot.yaml

接下来,我们在 boot.yaml 里添加如下 config 入口。

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
yaml复制代码config:
# 默认入口
- name: my-config
locale: "*::*::*::*"
path: config/default.yaml
# 北京,一区,测试环境
- name: my-config
locale: "cloud-album::bj::bj-1::test"
path: config/bj-1-test.yaml
# 北京,一区,线上环境
- name: my-config
locale: "cloud-album::bj::bj-1::prod"
path: config/bj-1-prod.yaml
# 北京,二区,测试环境
- name: my-config
locale: "cloud-album::bj::bj-2::test"
path: config/bj-2-test.yaml
# 北京,二区,线上环境
- name: my-config
locale: "cloud-album::bj::bj-2::prod"
path: config/bj-2-prod.yaml
# 上海,一区,测试环境
- name: my-config
locale: "cloud-album::sh::sh-1::test"
path: config/sh-1-test.yaml
# 上海,一区,线上环境
- name: my-config
locale: "cloud-album::sh::sh-1::prod"
path: config/sh-1-prod.yaml
# 上海,二区,测试环境
- name: my-config
locale: "cloud-album::sh::sh-2::test"
path: config/sh-2-test.yaml
# 上海,二区,线上环境
- name: my-config
locale: "cloud-album::sh::sh-2::prod"
path: config/sh-2-prod.yaml
gin:
- name: greeter
port: 8080
enabled: true

main.go 中读取配置文件。

因为,所有的 Config 都命名为 my-config,在 main.go 中读取的时候,我们可以使用 my-config 获取 ConfigEntry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package main

import (
"context"
"fmt"
"github.com/rookie-ninja/rk-boot"
_ "github.com/rookie-ninja/rk-gin/boot"
"os"
)

// Application entrance.
func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Get viper instance based on environment variable
boot.GetConfigEntry("my-config").GetViper()

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

本文转载自: 掘金

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

1…204205206…956

开发者博客

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