轻量级 Bean 实体校验器

news2024/11/24 3:55:29

简介

概述

利用 Spring 自带校验器结合 JSR 注解实现轻量级的 Bean 实体校验器。轻捷、简单、很容易上手,也容易扩展。

三个核心类ValidatorInitializingValidatorImplValidatorEnum去掉注释不超过共200行源码实现 10多m 的 Hibernate Validator 多数功能。

后端依赖的话,是我的框架 AJAXJS,当然是非常轻量级的。如果你不打算依赖 AJAXJS,把这三个类抠出来也是非常简单的。

另外,该组件在 Spring MVC 5 下调试通过,无须 Spring Boot 亦可。

源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-backend/aj-framework/aj-framework/src/main/java/com/ajaxjs/framework/spring/validator。

回顾一下

关于实体校验,笔者很早就进行研究了,有以往几篇博客都探讨过:

  • 数据验证框架 Apache BVal 简介
  • 简单实现 Bean 字段校验
  • Java 的业务逻辑验证框架 fluent-validator

大路货 Hibernate Validator 肯定不行,早有定论了;相对苗条得多的 Apache BVal 其实也可以,笔者之前一直在用,但直到本组件出来之后,笔者也抛弃 Apache BVal 了……这是笔者早期说的:

在这里插入图片描述

其实,Spring 验证器 Validatior 可以绑定 JSR 注解的,不需要你手工编码 if (null) else warn("不能空数据")。不了解 JSR 规范的同学可以看看以下 JSR 介绍:

javax.validation 2.0 是 JSR 380 的版本。JSR 380 是 Java 规范请求的缩写,它定义了 Java Bean 验证 API(Java Bean Validation API)。Java Bean 验证 API
提供了一组用于验证对象属性的注解和接口,帮助开发人员进行数据验证和约束。

javax.validation 2.0 是 JSR 380 中定义的规范的实现版本,它引入了一些新的特性和改进,以增强 Java Bean 验证功能。例如,javax.validation 2.0 支持对集合参数进行验证、支持原始类型的装箱、增加了针对日期和时间类型的约束注解等。在 Java 9 及之后的版本中,javax.validation 已经被整合到了 Java SE 标准库中,因此无需额外的依赖就可以直接使用。

于是,基于上述思想,更轻量级的校验器就此诞生了。

用法

配置默认的出错提示信息

首先要在 YAML 增加默认的出错提示信息。

javax-validation:
  javax.validation.constraints.AssertTrue.message: 值必须为 true
  javax.validation.constraints.AssertFalse.message: 值必须为 false
  javax.validation.constraints.DecimalMax.message: 值不能大于 {value}
  javax.validation.constraints.DecimalMin.message: 值不能小于 {value}
  javax.validation.constraints.Digits.message: 数字值超出范围(应为 <{integer} digits>.<{fraction} digits>javax.validation.constraints.Email.message: 值必须为有效的电子邮箱地址
  javax.validation.constraints.Future.message: 值必须为将来的日期
  javax.validation.constraints.FutureOrPresent.message: 值必须为当前或将来的日期
  javax.validation.constraints.Max.message: 值不能大于 {value}
  javax.validation.constraints.Min.message: 值不能小于 {value}
  javax.validation.constraints.Negative.message: 值必须为负数
  javax.validation.constraints.NegativeOrZero.message: 值必须为非正数
  javax.validation.constraints.NotBlank.message: 值不能为空值或空白字符串
  javax.validation.constraints.NotEmpty.message: 值不能为空值、null 或空集合
  javax.validation.constraints.NotNull.message: 值不能为空
  javax.validation.constraints.Null.message: 值必须为空
  javax.validation.constraints.Past.message: 值必须为过去的日期
  javax.validation.constraints.PastOrPresent.message: 值必须为当前或过去的日期
  javax.validation.constraints.Positive.message: 值必须为正数
  javax.validation.constraints.PositiveOrZero.message: 值必须为非负数
  javax.validation.constraints.Pattern.message: 值必须与指定正则表达式匹配
  javax.validation.constraints.Size.message: 大小必须小于 {max},大于 {min}

可见我们完全拥抱 YAML,抛弃了.propperties文件(痛苦的中文转码)。

初始化校验组件

接着注入ValidatorContextAware。这是在 Spring 应用程序上下文初始化完成后设置验证器和参数解析器。这个类的作用是在 Spring 启动时,拦截并修改RequestMappingHandlerAdapter的行为。通过设置自定义的验证器和参数解析器,可以对路径变量进行验证。

@Bean
public ValidatorContextAware ValidatorContextAware() {
    return new ValidatorContextAware();
}

校验 Bean 实体

首先在 controller 里面方法参数上添加@Validated注解,注意是org.springframework.validation.annotation.Validated

@PostMapping("/test")
public boolean test(@Validated JvmInfo info) {
    System.out.println(info);
    return true;
}

在参数实体属性上添加对应的注解。

import javax.validation.constraints.NotNull;

@Data
public class JvmInfo implements IBaseModel {

    private String name;

    @NotNull
    private String classPath;
    
    ……
}    

遗憾的是当前 Map 入参的校验,无从入手:(

路径参数的校验

这是基于 POST 方法提交实体的校验,那么对于路径上的参数是否支持校验呢?答案是支持的。

在 controller 里面方法参数上直接添加你要校验的注解:

@RequestMapping("/test/{mobileNo}/{idNo}")
public Map<String, Object> test(@PathVariable @MobileNo String mobileNo, @PathVariable @IdCard String idNo) {

便可完成对路径参数的校验了。一般来说既然是路径的参数,那么就是必填非空的了。

值得注意的是,这里的@MobileNo@IdCard都是自定义的注解,而非标准的 JSR 380 所提供的。这里顺便说说自定义的校验注解的写法。

自定义的校验注解

首先定义注解。

import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface IdCard {
    String message() default "身份证号格式不正确";

    boolean required() default true;
}

然后在枚举类ValidatorEnum中增加具体的校验方法,如果不通过就抛出ValidatorException异常。

在这里插入图片描述
至此就完成了自定义注解的定义。

原理分析

初始化

我们了解,既然是校验入参,那么肯定有种机制提前拦截控制器的执行,获取所有的参数进行校验,不通过的话则不会继续走下面控制器方法的逻辑。

具体的拦截机制就是修改RequestMappingHandlerAdapter的行为——还是 Spring 惯用的套路,在应用程序上下文初始化ApplicationContextAware, InitializingBean完成后得到ApplicationContext从而能够进行配置。详见ValidatorInitializing类:

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 在 Spring 应用程序上下文初始化完成后设置验证器和参数解析器
 * 这个类的作用是在 Spring MVC 启动时,拦截并修改 RequestMappingHandlerAdapter 的行为。通过设置自定义的验证器和参数解析器,可以对路径变量进行验证
 */
public class ValidatorInitializing implements ApplicationContextAware, InitializingBean {
    private ApplicationContext cxt;

    @Override
    public void setApplicationContext(ApplicationContext cxt) throws BeansException {
        this.cxt = cxt;
    }

    @Override
    public void afterPropertiesSet() {
        /*
            在 afterPropertiesSet 方法中,我们从应用程序上下文中获取 RequestMappingHandlerAdapter 对象。
            然后,我们将自定义的验证器 ValidatorImpl 设置为 ConfigurableWebBindingInitializer 对象的验证器。
            接着,我们获取到当前的参数解析器列表,并排除了 PathVariableMethodArgumentResolver 类型的解析器。
            然后,我们将自定义的 PathVariableArgumentValidatorResolver 解析器添加到解析器列表的开头。最后,将更新后的解析器列表设置回 RequestMappingHandlerAdapter 对象
         */
        RequestMappingHandlerAdapter adapter = cxt.getBean(RequestMappingHandlerAdapter.class);
        ConfigurableWebBindingInitializer init = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();

        assert init != null;
        init.setValidator(new ValidatorImpl());
        List<HandlerMethodArgumentResolver> resolvers = Objects.requireNonNull(adapter.getArgumentResolvers())
                .stream().filter(r -> !(r.getClass().equals(PathVariableMethodArgumentResolver.class)))
                .collect(Collectors.toList());

        // 路径变量时进行参数验证
        resolvers.add(0, new PathVariableMethodArgumentResolver() {
            @Override
            protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
                Object value = super.resolveName(name, parameter, request);
                // validateIfApplicable
                new ValidatorImpl().resolveAnnotations(parameter.getParameterAnnotations(), value);

                return value;
            }
        });

        adapter.setArgumentResolvers(resolvers);
        System.out.println("init done");
    }
}

init.setValidator(new ValidatorImpl());设置对 Bean 实体的校验;另外,实现PathVariableMethodArgumentResolver接口设置了对路径参数的校验。总的来说,核心是ValidatorImpl这个校验实现类。

ValidatorImpl

下面我们看看ValidatorImpl。它首先实现了 Spring 标准接口Validator,重写了validate(Object target, Errors errors)方法——肯定是参与了 Spring 某种机制才能有得让你参与进来“玩”。

@Override
public void validate(Object target, Errors errors) {
    Field[] declaredFields = target.getClass().getDeclaredFields();

    try {
        for (Field field : declaredFields) {
            if (!Modifier.isStatic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers())) {// isPrivate
                field.setAccessible(true);
                resolveAnnotations(field.getDeclaredAnnotations(), field.get(target));
            }
        }
    } catch (Exception e) {
        if (e instanceof ValidatorException)
            throw (ValidatorException) e;

        throw new ValidatorException(e);
    }
}

这里就是获取了入参 Bean,得到其 Class 解析内部的私有字段,看看有没有要校验的注解,传入到resolveAnnotations()进一步处理。

遍历所有的字段,得到值进行校验,还有出错信息。

在这里插入图片描述

总体过程比较简单的说,但是过程中还是有不少技巧的,下面我们看看。

枚举另类的玩法

没想到 Java 枚举还可以这样玩:
在这里插入图片描述
下面还可以设置抽象方法
在这里插入图片描述
annotationName名字跟注解匹配的话,就执行validated方法。感觉是个方便的单例+key/value 结构,本身枚举的意义不强,就好像有人用枚举做单例模式那样。

遇到小问题:怎么获取 YAML 配置呢?

平时用@value可以方便地获取 yaml 配置,但是当前环境下是一个集合的,最好是返回 Map 给我获取的。但翻遍了 YAML 没有一个公开的方法。但是 Spring 的 PropertySourcesPlaceholderConfigurer类中找到一个私有属性localProperties,这里面有配置的集合,可惜就是private的,但通过下面方法可以巧妙地获取这个localProperties集合。

1、创建一个继承自PropertySourcesPlaceholderConfigurer的子类,并重写postProcessBeanFactory()方法

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

import java.io.IOException;
import java.util.Properties;

/**
 * PropertySourcesPlaceholderConfigurer 是一个由 Spring 提供的用于解析属性占位符的配置类,
 * 它没有提供直接获取私有属性 localProperties 的公开方法。但是,可以通过以下步骤获取 localProperties 的值
 */
public class CustomPropertySourcesPlaceholderConfigure extends PropertySourcesPlaceholderConfigurer {
    private Properties localProperties;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        super.postProcessBeanFactory(beanFactory);

        try {
            localProperties = mergeProperties();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public Properties getLocalProperties() {
        return localProperties;
    }
}

2、配置的时候将默认的PropertySourcesPlaceholderConfigurer改下:

/**
 * YAML 配置文件
 *
 * @return YAML 配置文件
 */
@Bean
public PropertySourcesPlaceholderConfigurer properties() {
    PropertySourcesPlaceholderConfigurer cfger = new CustomPropertySourcesPlaceholderConfigure();
    cfger.setIgnoreUnresolvablePlaceholders(true);// Don't fail if @Value is not supplied in properties. Ignore if not found
    YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
    ClassPathResource c = new ClassPathResource("application.yml");

    if (c.exists()) {
        yaml.setResources(c);
        cfger.setProperties(Objects.requireNonNull(yaml.getObject()));
    } else System.err.println("未设置 YAML 配置文件");

    return cfger;
}

3、通过CustomPropertySourcesPlaceholderConfigure.getLocalProperties()就可以获取所有的配置了。如下ValidatorImpl类里面的getValue()通过DiContextUtil.getBean()获取CustomPropertySourcesPlaceholderConfigure

/**
 * 从注解上获取错误信息,如果没有则从默认的 YAML 配置获取
 */
private String getValue(Annotation annotation) {
    String message = (String) AnnotationUtils.getValue(annotation, "message");
    assert message != null;

    if (message.indexOf('{') > -1) { // 注解上没设置 message,要读取配置
        CustomPropertySourcesPlaceholderConfigure bean = DiContextUtil.getBean(CustomPropertySourcesPlaceholderConfigure.class);
        assert bean != null;
        String key = "javax-validation." + message.replaceAll("^\\{|}$", "");
        Object o = bean.getLocalProperties().get(key);

        if (o != null)
            message = o.toString();
    }

    return message;
}

旧时代码

之前玩弄的代码,弃之无味,就留存这里吧。

<dependency>
    <groupId>org.apache.bval</groupId>
    <artifactId>bval-jsr</artifactId>
    <version>2.0.6</version>
    <scope>compile</scope>
</dependency>
/**
 * 数据验证框架
 *
 * @return
 */
@Bean
LocalValidatorFactoryBean localValidatorFactoryBean() {
    LocalValidatorFactoryBean v = new LocalValidatorFactoryBean();
    v.setProviderClass(ApacheValidationProvider.class);

    return v;
}

// Bean 验证前
置拦截器
@Bean
BeanValidation beanValidation() {
    return new BeanValidation();
}

BeanValidation 源码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Set;

/**
 * Bean 校验拦截器
 */
public class BeanValidation {
    @Autowired
    LocalValidatorFactoryBean v;

    public boolean check(Object bean) {
        Set<ConstraintViolation<Object>> violations = v.getValidator().validate(bean);

        if (!CollectionUtils.isEmpty(violations)) {
            StringBuilder sb = new StringBuilder();

            for (ConstraintViolation<Object> v : violations) {
                sb.append("输入字段[").append(v.getPropertyPath()).append("],当前值[").append(v.getInvalidValue()).append("],校验失败原因[");
                sb.append(v.getMessage()).append("];");
            }

            sb.append("请检查后再提交");

            throw new IllegalArgumentException(sb.toString());
        }

        return true;
    }


    public boolean before(Method beanMethod, Object[] args) {
        Parameter[] parameters = beanMethod.getParameters();
        int i = 0;

        for (Parameter parameter : parameters) {
            Annotation[] annotations = parameter.getAnnotations();

            for (Annotation annotation : annotations) {
                if (annotation instanceof Valid) {
                    Validator validator = v.getValidator();
                    Set<ConstraintViolation<Object>> violations = validator.validate(args[i]);

                    if (!CollectionUtils.isEmpty(violations)) {
                        StringBuilder sb = new StringBuilder();

                        for (ConstraintViolation<Object> v : violations) {
                            sb.append("输入字段[").append(v.getPropertyPath()).append("],当前值[").append(v.getInvalidValue()).append("],校验失败原因[");
                            sb.append(v.getMessage()).append("];");
                        }

                        sb.append("请检查后再提交");

                        throw new IllegalArgumentException(sb.toString());
                    }
                }
            }

            i++;
        }

        return true;
    }
}

参考

  • 严重感谢 easyvalidator,就是受到其启动,再重构并优化之的!
  • Jakarta Bean Validation specification
  • Fluent-validation framework

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/902620.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用VS code 编辑器 导出、导入和运行Excel中的VBA代码

使用VS code 编辑器 导出、导入和运行Excel中的VBA代码 前言 Excel自带的 Microsoft Visual Basic for Applications 编辑器常被人称为上古编辑器&#xff0c;的确不适合代码编辑&#xff0c;这是其一&#xff0c;其二是当系统语言与Excel的安装语言不一致时&#xff0c;往往出…

网络安全---负载均衡案例

一、首先环境配置 1.上传文件并解压 2.进入目录下 为了方便解释&#xff0c;我们只用两个节点&#xff0c;启动之后&#xff0c;大家可以看到有 3 个容器&#xff08;可想像成有 3 台服务器就成&#xff09;。 二、使用蚁剑去连接 因为两台节点都在相同的位置存在 ant.jsp&…

从关键新闻和最新技术看AI行业发展(2023.7.10-7.23第三期) |【WeThinkIn老实人报】

Rocky Ding 公众号&#xff1a;WeThinkIn 写在前面 【WeThinkIn老实人报】本栏目旨在整理&挖掘AI行业的关键新闻和最新技术&#xff0c;同时Rocky会对这些关键信息进行解读&#xff0c;力求让读者们能从容跟随AI科技潮流。也欢迎大家提出宝贵的优化建议&#xff0c;一起交流…

字符设备驱动实例(ADC驱动)

四、ADC驱动 ADC是将模拟信号转换为数字信号的转换器&#xff0c;在 Exynos4412 上有一个ADC&#xff0c;其主要的特性如下。 (1)量程为0~1.8V。 (2)精度有 10bit 和 12bit 可选。 (3)采样时钟最高为5MHz&#xff0c;转换速率最高为1MSPS (4)具有四路模拟输入&#xff0c;同一时…

深入浅出解析Stable Diffusion中U-Net的核心知识与价值 | 【算法兵器谱】

Rocky Ding 公众号&#xff1a;WeThinkIn 写在前面 【算法兵器谱】栏目专注分享AI行业中的前沿/经典/必备的模型&论文&#xff0c;并对具备划时代意义的模型&论文进行全方位系统的解析&#xff0c;比如Rocky之前出品的爆款文章Make YOLO Great Again系列。也欢迎大家提…

FFmpeg中avfilter模块简介及测试代码(overlay)

FFmpeg中的libavfilter模块(或库)用于filter(过滤器), filter可以有多个输入和多个输出。为了说明可能发生的事情&#xff0c;考虑以下filtergraph(过滤器图): 该filtergraph将输入流(stream)分成两个流&#xff0c;然后通过crop过滤器和vflip过滤器发送一个流&#xff0c;然后…

Android SDK 上手指南|| 第三章 IDE:Android Studio速览

第三章 IDE&#xff1a;Android Studio速览 Android Studio是Google官方提供的IDE&#xff0c;它是基于IntelliJ IDEA开发而来&#xff0c;用来替代Eclipse。不过目前它还属于早期版本&#xff0c;目前的版本是0.4.2&#xff0c;每个3个月发布一个版本&#xff0c;最近的版本…

非常适合大学附近的校园跑腿和自习室订座小程序

推荐两款非常适合在大学内和大学周边的项目 这两款小程序分别是校园跑腿系统和自习室在线订座系统 1、校园跑腿系统&#xff0c;第一张图所示&#xff0c;支持多校运营、快递代取、校园跑腿、租借服务、代理中心、跑腿中心、人员管理、订单抽成、数据统计、众包接单、消息通…

微信消息没通知iphone can‘t show notifications

小虎最近手机微信消息没通知&#xff0c;本来以为要卸载&#xff0c;但是发现原来是多客户端登录导致消息被其他平台截取&#xff0c;所有没有通知。 解决方法 小虎是在手机和电脑端同时登录的&#xff0c;所有退出电脑端后手机新消息就有提示了。可能是一个bug。

Docker版本号说明:安装不同版本看文档变化|遇错不求人

docker实战(一):centos7 yum安装docker docker实战(二):基础命令篇 docker实战(三):docker网络模式(超详细) docker实战(四):docker架构原理 docker实战(五):docker镜像及仓库配置 docker实战(六):docker 网络及数据卷设置 docker实战(七):docker 性质及版本选择 认知升…

Java:集合框架:Set集合、LinkedSet集合、TreeSet集合、哈希值、HashSet的底层原理

Set集合 创建一个Set集合对象&#xff0c;因为Set是一个接口不能直接new一个对象&#xff0c;所以要用一个实现类来接 HashSet来接 无序性只有一次&#xff0c;只要第一次运行出来后&#xff0c;之后再运行的顺序还是第一次的顺序。 用LinkedSet来接 有序 不重复 无索引 用Tree…

嵌入式入门教学——C51(下)

嵌入式入门教学汇总&#xff1a; 嵌入式入门教学——C51&#xff08;上&#xff09;嵌入式入门教学——C51&#xff08;中&#xff09;嵌入式入门教学——C51&#xff08;下&#xff09; 十三、AT24C02&#xff08;I2C总线&#xff09; 1、存储器 RAM、ROM各有优势&#xff…

pandas(pd)数据的一些操作( np数据转成pd数据、pd数据保存csv文件)

一. np数据转成pd数据 import pandas as pd import numpy as np# 第一种 data {Category: [A, B, C],Value: [10, 20, 15]}df pd.DataFrame(data) print(df)# 第二种 data np.array([[0, 1, 2, 5],[0, 3, 4, 5],[0, 5, 6, 5]]) df pd.DataFrame(data,columns[num1, num2, …

手撕vector容器

一、vector容器的介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素&#xff0c;但是又不像数组&#xff0c;它的大小是可以动态改变的&#xff0c;而且它的大小会被容器自动处理。 总结&#xff1a;vector是一个动态…

OpenAI Function calling

开篇 原文出处 最近 OpenAI 在 6 月 13 号发布了新 feature&#xff0c;主要针对模型进行了优化&#xff0c;提供了 function calling 的功能&#xff0c;该 feature 对于很多集成 OpenAI 的应用来说绝对是一个“神器”。 Prompt 的演进 如果初看 OpenAI 官网对function ca…

计算机网络-物理层(三)编码与调制

计算机网络-物理层&#xff08;三&#xff09;编码与调制 在计算机网络中&#xff0c;计算机需要处理和传输用户的文字、图片、音频和视频&#xff0c;它们可以统称为消息 数据是运输信息的实体&#xff0c;计算机只能处理二进制数据&#xff0c;也就是比特0和比特1。计算机中…

不含数字的webshell绕过

异或操作原理 1.首先我们得了解一下异或操作的原理 在php中&#xff0c;异或操作是两个二进制数相同时&#xff0c;异或(相同)为0&#xff0c;不同为1 举个例子 A的ASCII值是65&#xff0c;对应的二进制值是0100 0001 的ASCII值是96&#xff0c;对应的二进制值是 0110 000…

CSS加载失败的6个原因

有很多刚刚接触 CSS 的新手有时会遇到 CSS 加载失败这个问题&#xff0c;但测试时&#xff0c;网页上没有显示该样式的问题&#xff0c;这就说明 CSS 加载失败了。出现这种状况一般是因为的 CSS 路径书写错&#xff0c;或者是在浏览器中禁止掉了 CSS 的加载&#xff0c;可以重新…

Linux/Ubuntu 的日常更新,如何操作?

我安装的是Ubuntu 20.04.6 LTS的Windows上Linux子系统版本&#xff0c;启动完成后显示&#xff1a; Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.15.90.4-microsoft-standard-WSL2 x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.c…

使用mysql:5.6和owncloud镜像构建个人网盘

一、拉取镜像 使用docker拉取mysql:5.6和owncloud的镜像 [rootexam ~]# docker pull mysql:5.6 [rootexam ~]# docker pull owncloud 运行镜像生成容器实例 [rootexam ~]# docker run -d --name mydb1 --env MYSQL_ROOT_PASSWORD123456 mysql:5.6 a184c65b73ff993cc5cf86f…