2023年4月

前言

《腾讯云 x Elasticsearch三周年》活动来了。文章写之前的思路是:在腾讯云服务器使用docker搭建ES。但是理想很丰满,显示很骨感,在操作过程中一波三折,最后还是含着泪美滋滋地,白嫖了一个月的腾讯云ES服务。

最后就是利用腾讯云的Elasticsearch和Kibana,和我在腾讯云服务器上搭建MySQL进行了一波联动,完成了数据库内部指标的展示。

<服务器搭建ES>
部分只用作记录,可以忽略直接看
<腾讯云领取ES的活动>
进行后续操作。

一. 服务器搭建ES

1.拉取ES镜像

本来想自己编写dockerfile生成镜像,没成想ES官网提供了docker镜像,这里就使用官方镜像,操作文档参考链接:

https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.6.2

2. 修改Linux系统句柄

# 在 /etc/sysctl.conf 末尾添加
vm.max_map_count=262144

修改之后重启或者使用以下命令使其生效:

/sbin/sysctl -p

3. 修改文件句柄和进程限制

# 在 /etc/security/limits.conf 末尾添加
* soft nofile 65536
* hard nofile 65536

4. docker安装

docker我也是提前安装好了,可以参考我之前的文章

yum -y install docker-ce

如果安装集群,还需要安装docker-compose,官网提供了此种安装方式,但是我的服务内存实在太小,尝试几次之后就果断放弃了。

单节点ES搭建

启动ES的时候,会自动启用安全认证配置,包括:

  1. 生成安全证书:http_ca.crt
  2. elasticsearch.yml.:TLS(Transport Layer Security)配置
  3. ES用户密码
  4. Kibana的注册token

1. 启动单节点ES

# 创建es网络
docker network create elastic

2. 启动单节点ES

# 启动单节点ES
docker run --name es01 --net elastic -p 9200:9200 -it docker.elastic.co/elasticsearch/elasticsearch:8.6.2

在启动单节点ES的时候,终端会打印一个用户密码(用户名:
elastic
),和一个注册Kibana用的token。因为只会在第一次启动时输出,所以要保存好。

这里使用的是前台启动,在保存好密码之后,再使用docker start后台启动容器。

3. 安全证书

执行命令将http_ca.crt拷贝到宿主机。

docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .

4. 验证

使用下载的安全证书和用户密码访问ES服务。

curl --cacert http_ca.crt -u elastic https://localhost:9200


至此,ES搭建完成。

外网无法访问服务器Docker中的ES

测试外网无法访问服务器中的ES,需要修改以下配置:

#  /etc/sysctl.conf 默认为0,修改成1
net.ipv4.ip_forward = 1

重启网络和es:

systemctl restart network
docker restart es01

这样就可以外网访问到ES了。

Kibana安装

docker pull docker.elastic.co/kibana/kibana:8.6.2
docker run --name kib-01 --net elastic -p 5601:5601 docker.elastic.co/kibana/kibana:8.6.2

启动Kibana的时候会输出一个地址,去浏览器中配置。

复制ES启动时生成的token填入。

token过期了,可以再生成一个。

docker exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana

花了老大的劲才忙活到这儿,™然后启动kibana就CPU狂飙,丐版服务器终究错付。搞了一下午无果,决定还是去腾讯云看看能不能白嫖。


二. 腾讯云ES0元活动

果然到有ES白嫖一月的活动,呜呜呜呜。觉得自己是个铁憨憨。


购买付款。


两分钟完事儿,进入控制台。

可以看到集群已经分配好,查询集群架构可以发现一共三个ES、一个Kibana。


可视化配置
中添加本机的IP到白名单,就可以访问Kibana。

点击公网访问地址,进入Kibana。

三.实践应用

1. 添加数据

在Kibana首页,进入
Add data
,可以添加很多种类数据源。这里挑选进行操作

2. MySQL Metrics

选择
MySQL metrics
,这个可以获取MySQL的内部指标。首先下载
metricbeat
。然后根据官方步骤完成配置:

  1. 修改
    metricbeat.yml
    中的es和kibana的配置

  1. 修改
    modules.d/msyql.yml
    中的mysql的配置,把query那行注释掉,否则会报错。

官方操作文档:

数据加载完成之后,点击
check data
会提示数据成功接收。

3. Dashboard

在Kibana首页找到
Dashboard

选择导入的MySQL指标数据。

如图,MySQL各种指标就展示出来了。

结语

在控制台点击几下,就完成了一下午的工作量,不得不感叹SaaS有点东西。如果对Elasticsearch和Kibana有兴趣的,在腾讯云白嫖一个挺合适的。

大家好,我是飘渺。

在日常的接口开发中,为了保证接口的稳定安全,我们一般需要在接口逻辑中处理两种校验:

  1. 参数校验
  2. 业务规则校验

首先我们先看看参数校验。

参数校验

参数校验很好理解,比如登录的时候需要校验用户名密码是否为空,创建用户的时候需要校验邮件、手机号码格式是否准确。

而实现参数校验也非常简单,我们只需要使用
Bean Validation
校验框架即可,借助它提供的校验注解我们可以非常方便的完成参数校验。

常见的校验注解有:

@Null、@NotNull、@AssertTrue、@AssertFalse、@Min、@Max、@DecimalMin、@DecimalMax、@Negative、@NegativeOrZero、@Positive、@PositiveOrZero、@Size、@Digits、@Past、@PastOrPresent、@Future、@FutureOrPresent、@Pattern、@NotEmpty、@NotBlank、@Email

在SpringBoot中集成参数校验我特意写了一篇文章,感兴趣的可以点击阅读。
SpringBoot 如何进行参数校验,老鸟们都这么玩的!

接下来我们再看看业务规则校验。

业务规则校验

业务规则校验指接口需要满足某些特定的业务规则,举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。

这就要求在
创建用户时需要校验用户名称、手机号码、邮箱是否被注册

编辑用户时不能将信息修改成已有用户的属性

95%的程序员当面对这种业务规则校验时往往选择写在service逻辑中,常见的代码逻辑如下:

public void create(User user) {
    Account account = accountDao.queryByUserNameOrPhoneOrEmail(user.getName(),user.getPhone(),user.getEmail());
    if (account != null) {
        throw new IllegalArgumentException("用户已存在,请重新输入");
    }
}

虽然我在上一篇文章中介绍了使用
Assert来优化代码
可以使其看上去更简洁,但是将简单的校验交给 Bean Validation,而把复杂的校验留给自己,这简直是买椟还珠故事的程序员版本。

image-20210716084136689

最优雅的实现方法应该是参考 Bean Validation 的标准方式,借助自定义校验注解完成业务规则校验。

接下来我们通过上面提到的用户接口案例,通过自定义注解完成业务规则校验。

代码实战

需求很容易理解,注册新用户时,应约束不与任何已有用户的关键信息重复;而修改自己的信息时,只能与自己的信息重复,不允许修改成已有用户的信息。

这些约束规则不仅仅为这两个方法服务,它们可能会在用户资源中的其他入口被使用到,乃至在其他分层的代码中被使用到,在 Bean 上做校验就能全部覆盖上述这些使用场景。

自定义注解

首先我们需要创建两个自定义注解,用于业务规则校验:

  • UniqueUser
    :表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidation.UniqueUserValidator.class)
public @interface UniqueUser {

    String message() default "用户名、手机号码、邮箱不允许与现存用户重复";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

  • NotConflictUser
    :表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidation.NotConflictUserValidator.class)
public @interface NotConflictUser {
    String message() default "用户名称、邮箱、手机号码与现存用户产生重复";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

实现业务校验规则

想让自定义验证注解生效,需要实现
ConstraintValidator
接口。接口的第一个参数是
自定义注解类型
,第二个参数是
被注解字段的类
,因为需要校验多个参数,我们直接传入用户对象。需要提到的一点是
ConstraintValidator
接口的实现类无需添加
@Component
它在启动的时候就已经被加载到容器中了。

@Slf4j
public class UserValidation<T extends Annotation> implements ConstraintValidator<T, User> {

    protected Predicate<User> predicate = c -> true;

    @Resource
    protected UserRepository userRepository;

    @Override
    public boolean isValid(User user, ConstraintValidatorContext constraintValidatorContext) {
        return userRepository == null || predicate.test(user);
    }

    /**
     * 校验用户是否唯一
     * 即判断数据库是否存在当前新用户的信息,如用户名,手机,邮箱
     */
    public static class UniqueUserValidator extends UserValidation<UniqueUser>{
        @Override
        public void initialize(UniqueUser uniqueUser) {
            predicate = c -> !userRepository.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
        }
    }

    /**
     * 校验是否与其他用户冲突
     * 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
     */
    public static class NotConflictUserValidator extends UserValidation<NotConflictUser>{
        @Override
        public void initialize(NotConflictUser notConflictUser) {
            predicate = c -> {
                log.info("user detail is {}",c);
                Collection<User> collection = userRepository.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
            };
        }
    }

}

这里使用Predicate函数式接口对业务规则进行判断。

使用

@RestController
@RequestMapping("/senior/user")
@Slf4j
@Validated
public class UserController {
    @Autowired
    private UserRepository userRepository;
    

    @PostMapping
    public User createUser(@UniqueUser @Valid User user){
        User savedUser = userRepository.save(user);
        log.info("save user id is {}",savedUser.getId());
        return savedUser;
    }

    @SneakyThrows
    @PutMapping
    public User updateUser(@NotConflictUser @Valid @RequestBody User user){
        User editUser = userRepository.save(user);
        log.info("update user is {}",editUser);
        return editUser;
    }
}

使用很简单,只需要在方法上加入自定义注解即可,业务逻辑中不需要添加任何业务规则的代码。

测试

调用接口后出现如下错误,说明业务规则校验生效。

{
  "status": 400,
  "message": "用户名、手机号码、邮箱不允许与现存用户重复",
  "data": null,
  "timestamp": 1644309081037
}

小结

通过上面几步操作,业务校验便和业务逻辑就完全分离开来,在需要校验时用
@Validated
注解自动触发,或者通过代码手动触发执行,可根据你们项目的要求,将这些注解应用于控制器、服务层、持久层等任何层次的代码之中。

这种方式比任何业务规则校验的方法都优雅,推荐大家在项目中使用。在开发时可以将不带业务含义的格式校验注解放到 Bean 的类定义之上,将带业务逻辑的校验放到 Bean 的类定义的外面。
这两者的区别是放在类定义中的注解能够自动运行,而放到类外面则需要像前面代码那样,明确标出注解时才会运行。

老鸟系列源码已经上传至GitHub,需要的在公号【JAVA日知录】回复关键字
0923
获取源码地址。

前言

一、人物简介

  • 第一位闪亮登场,有请今后会一直教我们C语言的老师 —— 自在。

  • 第二位上场的是和我们一起学习的小白程序猿 —— 逍遥。

二、算数运算符简介

C语言的算数运算符,是用来完成基本的算术运算的符号。

  • 按操作数个数可分为一元运算符(含一个操作数)和二元运算符(含两个操作数)。

  • 一元运算符的优先级一般高于二元运算符。

三、一元运算符

  • 一元运算符如下表所示
一元运算符 描述
+ 正号
- 负号
++ 自增1
-- 自减1
  • 实例演示
#include <stdio.h>
 
int main()
{
   int a = 61;
   int b = 10;
   int result ;
 
   result = +a;
   printf("+a 的值是 %d\n", result);

   result = -b;
   printf("-b 的值是 %d\n", result);
    
   result = ++a;
   printf("++a 的值是 %d\n", result);
    
   result = --b;
   printf("--b 的值是 %d\n", result);
    
   return 0;
}
  • 程序运行结果如下图

a++与++a的区别

a++

++a
都是自增运算符,它们的区别在于对变量a的值进行自增的时机不同。
a--

--a
同理。

前端性能优化——图片优化

一、图片优化措施

优化图片是 Web 前端优化的重要一环,因为图片是 Web 页面中最耗费带宽和加载时间的资源之一。以下是一些通过优化图片来优化 Web 前端的方法:

  1. 压缩图片:压缩图片可以减少图片的文件大小,从而减少加载时间。
  1. 使用矢量图形:使用矢量图形(如 SVG)可以减少文件大小,并且可以在不失真的情况下无限缩放。

  2. 使用 WebP 格式图片:WebP 是一种由 Google 开发的图片格式,可以大幅减少文件大小,从而减少加载时间。WebP 格式图片可以在 Chrome、Firefox、Edge 和 Opera 等现代浏览器中使用。

  3. 使用适当的图片格式:使用适当的图片格式可以大大减少图片的文件大小。对于需要透明背景的图片,使用 PNG 格式;对于颜色较少的图像,使用 GIF 格式;对于照片和复杂图像,使用 JPEG 格式。

  4. 移除图片元数据:图片通常包含元数据,例如拍摄日期、相机型号等。移除这些元数据可以减少图片的文件大小。

  5. 缓存图片:将图片缓存在浏览器中可以减少页面加载时间。你可以使用浏览器缓存机制或者使用 CDN 来缓存图片。

  6. 使用 CSS Sprites:使用 CSS Sprites 可以将多个图片合并成一个大图,减少页面中的图片数量,从而减少 HTTP 请求次数,减少页面的加载时间。

  7. 图片预加载:通过预加载图片,可以提高用户体验并减少加载时间。你可以使用 preload 或 prefetch 标签来预加载图片。

  8. 使用响应式图片:使用响应式图片可以根据设备的分辨率提供不同大小的图片,从而减少加载时间和带宽消耗。你可以使用 srcset 属性和 sizes 属性来实现响应式图片。

  9. 使用 Lazyload 延迟加载图片:Lazyload 是一种懒加载技术,它可以将页面中的图片延迟加载,直到用户滚动到页面中的相应位置。这可以减少页面的初始加载时间,并且可以提高用户的体验。可以使用懒加载技术或 Intersection Observer API 来实现延迟加载图片。

  10. 使用 CDN 加速图片加载:使用 CDN 可以将图片缓存到离用户最近的节点,从而提高图片的加载速度和用户的体验。

二、图片优化实践

2.1、压缩图片

  1. 基于 Vue 3 + Webpack 5 的 vue.config.js 配置示例:

    需要安装 image-webpack-loader:

    npm install image-webpack-loader --save-dev
    

    在 vue.config.js 文件中添加以下代码:

    const { defineConfig } = require("@vue/cli-service");
    
    module.exports = defineConfig({
      transpileDependencies: true,
      devServer: {
        port: 8123,
        // 调试时启用 gzip 压缩
        compress: true,
        // 调试时允许内网穿透,让外网的人访问到本地调试的 H5 页面
        disableHostCheck: true,
      },
      configureWebpack: {
        plugins: [],
        module: {
          rules: [],
        },
      },
      chainWebpack: (config) => {
        const imagesRule = config.module.rule("images");
        imagesRule
          .use("image-webpack-loader")
          .loader("image-webpack-loader")
          .options({
            mozjpeg: {
              progressive: true,
              quality: 65,
            },
            optipng: {
              enabled: false,
            },
            pngquant: {
              quality: [0.65, 0.9],
              speed: 4,
            },
            gifsicle: {
              interlaced: false,
            },
            webp: {
              quality: 75,
            },
          });
      },
    });
    

    优化效果:

    优化前:

    img

    优化后:

    img

2.2、使用 SVG 格式图片

优化思路:由于 JPEG、PNG 和 GIF 等格式图片无法被 gzip 等压缩算法压缩,所以通过使用 SVG 矢量图(文本类型),即可通过 gzip 等压缩算法对文本进行压缩。

如果需要使用 SVG 图片,最好是使用专门的 SVG 编辑器或绘图工具来创建或编辑它们,例如,
Inkscape
是一个流行的开源矢量图形编辑器。

2.3、使用 WebP/AVIF 格式图片

采用新一代格式提供图片:WebP 和 AVIF 等图片格式的压缩效果通常优于 PNG 或 JPEG,因而下载速度更快,消耗的数据流量更少。

在 HTML 文件中使用 picture 元素来加载图片,以支持不同的图片格式:

<picture>
  <source srcset="example.webp" type="image/webp" />
  <img src="example.jpg" alt="Example image" />
</picture>

通过使用 WebP/AVIF 格式图片 ,并且在 HTML 文件中使用 picture 元素加载图片,以支持不同的图片格式。这样可以有效地减少带宽的使用和加快图片的加载速度,从而提高网站的性能和用户体验。

将 PNG/JPEG 格式图片转换为 Webp 格式图片

  1. 使用在线工具:
    jpg-to-webp

  2. 使用 Python 对图片进行批量转换处理:
    《Python 实现图像尺寸和转换处理》

2.4、使用图片 Base64 编码的 DataURI

优化思路:由于 JPEG、PNG 和 GIF 等格式图片无法被 gzip 等压缩算法压缩,所以通过配置 webpack,在打包时,将 JPEG、PNG 和 GIF 等格式图片转换为 图片 Base64 编码的 DataURI,即可通过 gzip 等压缩算法对文本进行压缩。

需要注意的是,将图片转换为图片 Base64 编码的 DataURI 字符串可能会导致文件大小变大,并且会影响到应用程序的加载速度。因此,建议只将小文件转换为 base64 编码的字符串,而将大文件编译成普通的图片文件。

  1. 基于 Vue 2 + Webpack 4 的 vue.config.js 配置示例:

    // vue.config.js
    
    module.exports = {
      devServer: {
        host: "0.0.0.0",
        port: 9999,
        https: false,
        // 调试时启用 gzip 压缩
        compress: true,
        // 调试时允许内网穿透,让外网的人访问到本地调试的 H5 页面
        disableHostCheck: true,
      },
      configureWebpack: {
        rules: [],
        plugins: [],
      },
      configureWebpack: (config) => {},
      chainWebpack: (config) => {
        config.module
          .rule("images")
          .test(/\.(png|jpe?g|gif)$/i)
          .use("url-loader")
          .loader("url-loader")
          .tap((options) => {
            // 将文件大小限制为100kb以内,超过100kb的文件将被编译成普通的图片文件。
            options.limit = 100 * 1024;
            return options;
          });
      },
    };
    
  2. 基于 Vue 3 + Webpack 5 的 vue.config.js 配置示例:

    // vue.config.js
    
    const path = require("path");
    const { defineConfig } = require("@vue/cli-service");
    
    module.exports = defineConfig({
      transpileDependencies: true,
      devServer: {
        port: 8123,
        // 调试时启用 gzip 压缩
        compress: true,
        // 调试时允许内网穿透,让外网的人访问到本地调试的 H5 页面
        disableHostCheck: true,
      },
      configureWebpack: {
        module: {
          rules: [
            {
              test: /\.(png|jpe?g|gif)$/i,
              type: "asset",
              parser: {
                dataUrlCondition: {
                  maxSize: 100 * 1024, // 将文件大小限制为 100kb 以内,超过 100kb 的文件将被编译成普通的图片文件。
                },
              },
            },
          ],
        },
      },
      chainWebpack: (config) => {
        config.resolve.alias.set("@", path.resolve(__dirname, "src"));
      },
    });
    

    上面的代码展示了如何使用 Webpack 5 的 asset 模块来将图片转换为 base64 编码的字符串。通过配置 dataUrlCondition.maxSize,可以指定将多大以下的文件转换为 base64 编码的字符串。超过这个大小的文件将被编译成普通的图片文件。

    另外,为了避免在 Vue 3 中使用 import 语句时出现相对路径的问题,可以通过 chainWebpack 来配置别名,使得我们可以使用@代替 src 目录路径。

    优化效果:

    优化前:

    img

    img

    优化后:

    img

    img

    由上述对比可知,累积布局偏移(CLS,Cumulative Layout Shift)指标得到了优化,是一项意料之外的优化,所以还无法肯定该优化措施能达到预期。