2024年11月


title: Nuxt.js 应用中的 webpack:compile 事件钩子
date: 2024/11/22
updated: 2024/11/22
author:
cmdragon

excerpt:
webpack:compile 钩子是 Nuxt.js 和 Webpack 集成中的一个重要部分,它允许开发者在实际编译过程开始之前执行一些自定义逻辑。通过这一钩子,您可以获取编译的选项并进行相应的修改,为构建定制化处理。

categories:

  • 前端开发

tags:

  • Nuxt.js
  • Webpack
  • 编译
  • 钩子
  • 自定义
  • 逻辑
  • 构建


image
image

扫描
二维码
关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

webpack:compile
钩子是 Nuxt.js 和 Webpack 集成中的一个重要部分,它允许开发者在实际编译过程开始之前执行一些自定义逻辑。通过这一钩子,您可以获取编译的选项并进行相应的修改,为构建定制化处理。

使用
webpack:compile
钩子

定义与作用

  • webpack:compile
    是一个钩子,在 Webpack 开始编译之前被调用。
  • 这使得开发者可以在编译期间执行特定的逻辑,比如记录信息、修改编译选项、或打印日志等。

调用时机

webpack:compile
钩子在 Webpack 开始实际构建之前被调用,此时您可以访问编译选项以及其他相关信息。

参数说明

这个钩子接收一个参数:

  • options
    : 一个对象,包含编译的选项。您可以根据需要读取和修改这些选项。

示例用法

下面是一个简单的示例,展示如何使用
webpack:compile
钩子。


plugins/webpackCompile.js
中的实现

// plugins/webpackCompile.js

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('webpack:compile', (options) => {
    // 打印当前编译选项
    console.log('即将开始编译,当前编译选项:', options);

    // 如果需要,可以根据条件修改编译选项
    if (process.env.NODE_ENV === 'development') {
      options.mode = 'development';
      console.log('设置编译模式为开发模式');
    }
  });
});

应用场景

1. 记录编译信息

您可以在编译开始时记录一些信息,以便后续调试或分析。

nuxtApp.hooks('webpack:compile', (options) => {
  console.log(`编译开始于: ${new Date().toISOString()}`);
  console.log('使用的编译选项: ', options);
});

2. 修改编译模式

根据特定的条件,您可能需要在编译过程中动态修改选项。

nuxtApp.hooks('webpack:compile', (options) => {
  if (process.env.CI) {
    options.mode = 'production'; // 在 CI 环境下强制使用生产模式
  }
});

3. 环境变量的设置

在开始编译之前,您可以根据不同的环境设置相应的参数。

nuxtApp.hooks('webpack:compile', (options) => {
  options.customEnv = process.env.CUSTOM_ENV || 'default';
});

注意事项

  • 影响性能
    : 虽然可以在编译开始之前修改选项,过于复杂的逻辑可能会影响编译性能,因此应注意编写的代码效率。
  • 理解选项
    : 对编译选项的修改应该基于对 Webpack 和项目需求的充分理解,以免引入不必要的问题。
  • 测试修改
    : 对编译选项的任何修改后,都应该进行构建并测试,确保构建输出符合预期。

总结

webpack:compile
钩子是一个强大而灵活的工具,能够帮助您在 Webpack 开始编译之前自定义很多方面。无论是记录编译时间、环境变量的设置,还是编译模式的动态调整,这一钩子都能提供必要的功能支持。

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt.js 应用中的 webpack:compile 事件钩子 | cmdragon's Blog

往期文章归档:

专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应 用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信

应用场景:

  • 多个组件依赖于同一状态、共享数据
  • 来自不同组件的行为需要变更同一状态
  • vuex

vuex原理

image-20241122100314789

每一个 Vuex 应用的核心就是 store,里面又包括:

  1. State(状态):用于数据的存储(对象类型数据),是store中唯一数据源
  2. Actions(行为):类似于mutation,用于提交mutation来改变状态,而不直接变更状态,可以包含任意异步事件
  3. Mutations(转变):类似函数,改变state数据的唯一途径,且不能用于处理异步事件,Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方
  4. Getter(数据加工):如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关计算
  5. Module:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块

vuex环境

  1. 安装vuex

    vue2:使用vuex3版本

    vue3:使用vuex4版本

    # 安装3版本
     npm i vuex@3    
    
  2. 创建Store


    创建store文件夹,下面创建index.js (官方推荐) or 创建vuex文件夹,创建store.js文件,自定义写法


    import Vue from "vue"
    import Vuex from "vuex"
    
    
    // 使用vuex插件,使用之后,创建vm的时候,可以传入store配置项
    // new Vuex.Store 必须使用了Vuex之后,否则报错
    // 如果在main.js 使用 然后引包,脚手架会先执行所以import代码再执行其他代码 也会报错
    Vue.use(Vuex)
    
    
    // 定义actions-用于响应组件中的动作(mutations)
    const actions = {}
    
    // 定义mutations-用于操作数据(state)
    const mutations = {}
    
    // 定义state,用于存储数据
    const state = {}
    
    
    
    // 创建Store
    const store = new Vuex.Store({
        actions,
        mutations,
        state
    })
    
    // 暴露store
    export  default store
    

  3. 配置store

    // main.js
    
    import  store from "./store"
    
    new Vue({
        render: h => h(App),
        store // 配置导入的store,在vm和vc身上可以通过.$store访问
    
    }).$mount('#app')
    

基本使用

1.初始化数据
// 定义state,用于存储数据
// 数据被mutations修改
const state = {
    // 定义一个count
    count: 0
}
2. 配置actions
// 定义actions-用于响应组件中的动作(mutations)
// 该对象中的方法用于commit响应mutations中的方法操作数据
// 该对象被dispatch触发
const actions = {
    // 定义add方法
    // context:上下文对象,包含commit、dispatch、getter、state、roomGetter、rootState 
    // value是调用方传递的值  
    add: function (context, value) {
        // 将数据提交到ADD方法(mutations中定义的),此时不会直接修改数据
        // 可以通过context.dispatch触发其他actions
        context.commit("ADD", value)


    }
}
3. 配置mutations
// 定义mutations-用于操作数据(state)
// 该对象被actions中的commit and 其他地方的commit触发
const mutations = {
    ADD(state, value) {
        // state是数据对象,包含getter、setter等
        // value是调用方传递的值
        // 通过数据对象操作数据,此时会直接修改值
        state.count += value

    }

}
4.操作store.js
methods: {
  increment(){
    // 触发回调的时候 调用$store.dispatch,调用actions里的add方法,参数是n
    // actions相当于一个中转,如果参数需要做其他逻辑处理调用dispatch

    this.$store.dispatch("add",this.n)
    
    // 也可以直接操作this.$store.commit("ADD",this.n),调用mutations里的ADD方法
    // 如果不需要其他逻辑处理,直接修改数据调用commit
    this.$store.commit("ADD",this.n)

  }
5.模版中访问
<!-- 模版中直接通过$store访问 -->
{{$store.state.count}}

getters配置项

getters的功能类似于 Vue 组件中的计算属性。它们都是用于对已有的数据(在 Vuex 中是state)进行计算或者过滤后返回一个新的值

  1. 定义getters

    // 用于将state中的数据进行加工
    const getters = {
        // 定义一个doubleCount属性
        doubleCount(state){
            // state 是数据对象
            return state.count * 2
    
        }
    }
    
    const store = new Vuex.Store({
        actions,
        mutations,
        state,
        getters
    })
    
    
  2. 使用getters

    this.getters.doubleCount
    

    <!-- 模版中使用 -->
    {{$store.getters.doubleCount}}
    

store中的map方法

mapState

用于帮助我们映射state中的数据为计算属性

   ```js

import {mapState} from "vuex";

computed: {
// 对象写法 等同于 count(){{return this.$store.state.count}}
// 如果要修改state中的值 可以写箭头函数返回
...mapState({count: "count", count2: (state)=> state.count * 2}),
// 数组写法

// 等同于 count(){return this.$store.state.count}
// 相当于把stage中的数据以同名的方式映射到计算属性中
...mapState(["count"])

}
```

mapGetters

用于帮助我们映射getters中的数据为计算属性

      ```js

import {mapGetters} from "vuex"

computed: {
// 数组写法 doubleCount(){ return this.$store.getters.doubleCount}
...mapGetters(["doubleCount"]),
// 对象写法 doubleCount(){ return this.$store.getters.doubleCount}
...mapGetters({doubleCount: "doubleCount"})

}
```

mapActions

用于帮助我们生成与actions对话的方法,即:包含$store.dispatch()的函数

import {mapActions} from 'vuex'


	methods: {

      //  等同于 addCount(){this.$store.dispatch("add",value)}
      // 这里的value 需要在使用此回调函数的地方绑定 @click="addCount(要传递的参数)"
      // 如果不在使用的地方绑定,则ADD方法内部收到的value 是点击事件
      ...mapActions({addCount: "add"}),
        
        // 数组写法,原理与mapState一致
      ...mapActions(["add"]),
    },
mapMutations

用于帮助我们生成与mutations对话的方法,即:包含$store.commit()的函数

import {mapMutations} from 'vuex'

	methods: {

      //  等同于 increment(){this.$store.commit("ADD",value)}
      // 这里的value 需要在使用此回调函数的地方绑定 @click="increment(要传递的参数)"
      // 如果不在使用的地方绑定,则increment方法内部收到的value 是点击事件
      ...mapMutations({increment: "ADD"}),
        
        // 数组写法,原理与mapState一致
      ...mapMutations(["ADD"]),

    },

vuex模块化&命名空间

当 Vue 应用规模变大时,
store
中的状态(
state
)、变更操作(
mutations
)、异步操作(
actions
)和获取器(
getters
)也会变得复杂繁多。Vuex 模块化允许将
store
分割成多个模块,每个模块都有自己独立的
state

mutations

actions

getters
,就像有多个小的 Vuex
store
一样。

模块化使得代码结构更加清晰,便于团队开发和维护。不同的功能模块可以有各自独立的状态管理逻辑,例如一个电商应用中,可以有用户模块、商品模块、订单模块等,每个模块管理自己相关的状态和操作

模块化
  • 包含多个 module ,一个 module 是一个 store 的配置对象 与一个组件(包含有共享数据)对应
  1. 定义模块

    // 自定义一个对象,里面包含vuex的对方方法
    const sumOptions = {
        actions: {},
        mutations:{},
        state:{},
        getters:{sum:10}
    
    }
    
    // 自定义一个对象,里面包含vuex的对方方法
    const countOptions = {
        actions: {},
        mutations:{},
        state:{count:20}
    
    }
    
    
    //创建并暴露store
    export default new Vuex.Store({
        // 模块配置
        modules: {
            // store里面有一个sum模块,对应的是sumOptions对象里面的方法
            sumModules: sumOptions,
    
            // store里面有一个count模块,对应的是countOptions对象里面的方法
            countModules: countOptions
        }
    
    })
    
  2. 使用模块store数据

    {{$store.state.sumModules.sum}}
    

    this.$store.state.sumModules.sum
    
  3. 通过map方法映射模块对象访问

        // 映射的是模块对象
        ...mapState(["sumModules","countModules"])
    

    <!--在模版中可以使用 {{模块名.属性访问}}-->
    {{sumModules.sum}}
    
命名空间

可以开启模块的命名空间,使用map方法的时候通过指定模块名字和属性、方法名字进行映射

  1. 通过namespaced开启命名空间

    const sumOptions = {
        namespaced:true, // 开启命名空间
        actions: {},
        mutations:{},
        state:{sum:10},
    
    }
    
  2. 读取指定模块的数据

    // 使用map读取
    // new store的时候指定的模块名、要映射的属性,其他map方法方法映射同理
    ...mapState("sumModules",["sum"])
    // {{sum}}
    

    // 自己直接读取  state.模块名.属性名
    this.$store.state.sumModules.sum
    
  3. 读取getters数据

    // 使用map读取
    // 模块名,[要映射的数据]
    ...mapGetters('countAbout',['bigSum'])
    

    // 直接读取  模块名/属性名
    this.$store.getters['sumModules/doubleSum']
    
  4. commit和dispatch

    // 直接读取
    //  模块名/方法名,参数
    this.$store.commit("sumModules/ADD",100)
    this.$store.dispatch("sumModules/add",100)
    

    // 使用map映射   模块名  [要映射的方法]
    ...mapMutations('sumModules',{setSum:'ADD'}),
      
    ...mapActions('sumModules',{commitSum:'add'})
    
    // 传参原理与未开启命名空间原理一致
    
模块化推荐设计
  • store 文件夹:在项目的根目录下创建一个专门的
    store
    文件夹,用于存放所有与 Vuex 相关的代码。这个文件夹是整个 Vuex 模块管理的核心区域。
  • 模块文件夹:在
    store
    文件夹内,为每个主要的功能模块创建一个单独的文件夹。例如,如果是一个电商应用,可以有
    user
    模块文件夹、
    product
    模块文件夹、
    order
    模块文件夹等。每个模块文件夹用于存放该模块相关的
    state

    mutations

    actions

    getters
    的定义文件,export暴露给外部
  • index中用于注册vuex插件、new vuex对象、导入所有模块,统一管理

大家好,我是程序员鱼皮。我发现很多程序员都不打日志,有的是
不想
打、有的是
意识不到
要打、还有的是
真不会
打日志啊!

前段时间的模拟面试中,我问了几位应届的 Java 开发同学 “你在项目中是怎么打日志的”,得到的答案竟然是 “支支吾吾”、“阿巴阿巴”,更有甚者,竟然表示:直接用
System.out.println()
打印一下吧。。。

要知道,日志是我们系统出现错误时,最快速有效的定位工具,没有日志给出的错误信息,遇到报错你就会一脸懵逼;而且日志还可以用来记录业务信息,比如记录用户执行的每个操作,不仅可以用于分析改进系统,同时在遇到非法操作时,也能很快找到凶手。

因此,对于程序员来说,日志记录是重要的基本功。但很多同学并没有系统学习过日志操作、缺乏经验,所以我写下这篇文章,分享自己在开发项目中记录日志的方法和最佳实践,希望对大家有帮助~

一、日志记录的方法

日志框架选型

有很多 Java 的日志框架和工具库,可以帮我们用一行代码快速完成日志记录。

在学习日志记录之前,很多同学应该是通过
System.out.println
输出信息来调试程序的,简单方便。

但是,
System.out.println
存在很严重的问题!

首先,
System.out.println
是一个同步方法,每次调用都会导致 I/O 操作,比较耗时,频繁使用甚至会严重影响应用程序的性能,所以不建议在生产环境使用。此外,它只能输出简单的信息到标准控制台,无法灵活设置日志级别、格式、输出位置等。

所以我们一般会选择专业的 Java 日志框架或工具库,比如经典的 Apache Log4j 和它的升级版 Log4j 2,还有 Spring Boot 默认集成的 Logback 库。不仅可以帮我们用一行代码更快地完成日志记录,还能灵活调整格式、设置日志级别、将日志写入到文件中、压缩日志等。

可能还有同学听说过 SLF4J(Simple Logging Facade for Java),看英文名就知道了,这玩意并不是一个具体的日志实现,而是为各种日志框架提供简单统一接口的日志门面(抽象层)。

啥是门面?

举个例子,现在我们要记录日志了,先联系到前台接待人员 SLF4J,它说必须要让我们选择日志的级别(debug / info / warn / error),然后要提供日志的内容。确认之后,SLF4J 自己不干活,屁颠屁颠儿地去找具体的日志实现框架,比如 Logback,然后由 Logback 进行日志写入。

这样做有什么好处呢?无论我们选择哪套日志框架、或者后期要切换日志框架,调用的方法始终是相同的,不用再去更改日志调用代码,比如将 log.info 改为 log.printInfo。

既然 SLF4J 只是玩抽象,那么 Log4j、Log4j 2 和 Logback 应该选择哪一个呢?

值得一提的是,SLF4J、Log4j 和 Logback 竟然都是同一个作者(俄罗斯程序员 Ceki Gülcü)。

首先,Log4j 已经停止维护,直接排除。Log4j 2 和 Logback 基本都能满足功能需求,那么就看性能、稳定性和易用性。

  • 从性能来说,Log4j 2 和 Logback 虽然都支持异步日志,但是 Log4j 基于 LMAX Disruptor 高性能异步处理库实现,性能更高。

  • 从稳定性来说,虽然这些日志库都被曝出过漏洞,但 Log4j 2 的漏洞更为致命,姑且算是 Logback 得一分。

  • 从易用性来说,二者差不多,但 Logback 是 SLF4J 的原生实现、Log4j2 需要额外使用 SLF4J 绑定器实现。

再加上 Spring Boot 默认集成了 Logback,如果没有特殊的性能需求,我会更推荐初学者选择 Logback,都不用引入额外的库了~

使用日志框架

日志框架的使用非常简单,一般需要先获取到 Logger 日志对象,然后调用 logger.xxx(比如 logger.info)就能输出日志了。

最传统的方法就是通过 LoggerFactory 手动获取 Logger,示例代码如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {
   private static final Logger logger LoggerFactory.getLogger(MyService.class);

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

上述代码中,我们通过调用日志工厂并传入当前类,创建了一个 logger。但由于每个类的类名都不同,我们又经常复制这行代码到不同的类中,就很容易忘记修改类名。

所以我们可以使用
this.getClass
动态获取当前类的实例,来创建 Logger 对象:

public class MyService {
   private final Logger logger LoggerFactory.getLogger(this.getClass());

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

给每个类都复制一遍这行代码,就能愉快地打日志了。

但我觉得这样做还是有点麻烦,我连复制粘贴都懒得做,怎么办?

还有更简单的方式,使用 Lombok 工具库提供的
@Slf4j
注解,可以自动为当前类生成一个名为
log
的 SLF4J Logger 对象,简化了 Logger 的定义过程。示例代码如下:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyService {
   public void doSomething() {
       log.info("执行了一些操作");
  }
}

这也是我比较推荐的方式,效率杠杠的。

此外,你可以通过修改日志配置文件(比如
logback.xml

logback-spring.xml
)来设置日志输出的格式、级别、输出路径等。日志配置文件比较复杂,不建议大家去记忆语法,随用随查即可。

二、日志记录的最佳实践

学习完日志记录的方法后,再分享一些我个人记录日志的经验。内容较多,大家可以先了解一下,实际开发中按需运用。

1、合理选择日志级别

日志级别的作用是标识日志的重要程度,常见的级别有:

  • TRACE:最细粒度的信息,通常只在开发过程中使用,用于跟踪程序的执行路径。

  • DEBUG:调试信息,记录程序运行时的内部状态和变量值。

  • INFO:一般信息,记录系统的关键运行状态和业务流程。

  • WARN:警告信息,表示可能存在潜在问题,但系统仍可继续运行。

  • ERROR:错误信息,表示出现了影响系统功能的问题,需要及时处理。

  • FATAL:致命错误,表示系统可能无法继续运行,需要立即关注。

其中,用的最多的当属 DEBUG、INFO、WARN 和 ERROR 了。

建议在开发环境使用低级别日志(比如 DEBUG),以获取详细的信息;生产环境使用高级别日志(比如 INFO 或 WARN),减少日志量,降低性能开销的同时,防止重要信息被无用日志淹没。

注意一点,日志级别未必是一成不变的,假如有一天你的程序出错了,但是看日志找不到任何有效信息,可能就需要降低下日志输出级别了。

2、正确记录日志信息

当要输出的日志内容中存在变量时,建议使用参数化日志,也就是在日志信息中使用占位符(比如
{}
),由日志框架在运行时替换为实际参数值。

比如输出一行用户登录日志:

// 不推荐
logger.debug("用户ID:" userId " 登录成功。");

// 推荐
logger.debug("用户ID:{} 登录成功。", userId);

这样做不仅让日志清晰易读;而且在日志级别低于当前记录级别时,不会执行字符串拼接,从而避免了字符串拼接带来的性能开销、以及潜在的 NullPointerException 问题。所以建议在所有日志记录中,使用参数化的方式替代字符串拼接。

此外,在输出异常信息时,建议同时记录上下文信息、以及完整的异常堆栈信息,便于排查问题:

try {
   // 业务逻辑
catch (Exception e) {
logger.error("处理用户ID:{} 时发生异常:", userId, e);
}

3、控制日志输出量

过多的日志不仅会占用更多的磁盘空间,还会增加系统的 I/O 负担,影响系统性能。

因此,除了根据环境设置合适的日志级别外,还要尽量避免在循环中输出日志。

可以添加条件来控制,比如在批量处理时,每处理 1000 条数据时才记录一次:

if (index 1000 == 0) {
   logger.info("已处理 {} 条记录", index);
}

或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:

StringBuilder logBuilder new StringBuilder("处理结果:");
for (Item item : items) {
   try {
       processItem(item);
       logBuilder.append(String.format("成功[ID=%s], ", item.getId()));
  } catch (Exception e) {
       logBuilder.append(String.format("失败[ID=%s, 原因=%s], ", item.getId(), e.getMessage()));
  }
}
logger.info(logBuilder.toString());

如果参数的计算开销较大,且当前日志级别不需要输出,应该在记录前进行级别检查,从而避免多余的参数计算:

if (logger.isDebugEnabled()) {
   logger.debug("复杂对象信息:{}", expensiveToComputeObject());
}

此外,还可以通过更改日志配置文件整体过滤掉特定级别的日志,来防止日志刷屏:

<!-- Logback 示例 -->
<appender name="LIMITED" class="ch.qos.logback.classic.AsyncAppender">
<!-- 只允许 INFO 级别及以上的日志通过 -->
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFO</level>
   </filter>
   <!-- 配置其他属性 -->
</appender>

4、把控时机和内容

很多开发者(尤其是线上经验不丰富的开发者)并没有养成记录日志的习惯,觉得记录日志不重要,等到出了问题无法排查的时候才追悔莫及。

一般情况下,需要在系统的关键流程和重要业务节点记录日志,比如用户登录、订单处理、支付等都是关键业务,建议多记录日志。

对于重要的方法,建议在入口和出口记录重要的参数和返回值,便于快速还原现场、复现问题。

对于调用链较长的操作,确保在每个环节都有日志,以便追踪到问题所在的环节。

如果你不想区分上面这些情况,我的建议是尽量在前期多记录一些日志,后面再慢慢移除掉不需要的日志。比如可以利用 AOP 切面编程在每个业务方法执行前输出执行信息:

@Aspect
@Component
public class LoggingAspect {

   @Before("execution(* com.example.service..*(..))")
   public void logBeforeMethod(JoinPoint joinPoint) {
       Logger logger LoggerFactory.getLogger(joinPoint.getTarget().getClass());
       logger.info("方法 {} 开始执行", joinPoint.getSignature().getName());
  }
}

利用 AOP,还可以自动打印每个 Controller 接口的请求参数和返回值,这样就不会错过任何一次调用信息了。

不过这样做也有一个很重要的点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。

5、日志管理

随着日志文件的持续增长,会导致磁盘空间耗尽,影响系统正常运行,所以我们需要一些策略来对日志进行管理。

首先是设置日志的滚动策略,可以根据文件大小或日期,自动对日志文件进行切分。比如按文件大小滚动:

<!-- 按大小滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeBasedRollingPolicy">
   <maxFileSize>10MB</maxFileSize>
</rollingPolicy>

如果日志文件大小达到 10MB,Logback 会将当前日志文件重命名为
app.log.1
或其他命名模式(具体由文件名模式决定),然后创建新的
app.log
文件继续写入日志。

还有按照时间日期滚动:

<!-- 按时间滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>

上述配置表示每天创建一个新的日志文件,
%d{yyyy-MM-dd}
表示按照日期命名日志文件,例如
app-2024-11-21.log

还可以通过
maxHistory
属性,限制保留的历史日志文件数量或天数:

<maxHistory>30</maxHistory>

这样一来,我们就可以按照天数查看指定的日志,单个日志文件也不会很大,提高了日志检索效率。

对于用户较多的企业级项目,日志的增长是飞快的,因此建议开启日志压缩功能,节省磁盘空间。

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>

上述配置表示:每天生成一个新的日志文件,旧的日志文件会被压缩存储。

除了配置日志切分和压缩外,我们还需要定期审查日志,查看日志的有效性和空间占用情况,从日志中发现系统的问题、清理无用的日志信息等。

如果你想偷懒,也可以写个自动化清理脚本,定期清理过期的日志文件,释放磁盘空间。比如:

# 每月清理一次超过 90 天的日志文件
find /var/log/myapp/ -type f -mtime +90 -exec rm {} \;

6、统一日志格式

统一的日志格式有助于日志的解析、搜索和分析,特别是在分布式系统中。

我举个例子大家就能感受到这么做的重要性了。

统一的日志格式:

2024-11-21 14:30:15.123 [main] INFO com.example.service.UserService - 用户ID:12345 登录成功
2024-11-21 14:30:16.789 [main] ERROR com.example.service.UserService - 用户ID:12345 登录失败,原因:密码错误
2024-11-21 14:30:17.456 [main] DEBUG com.example.dao.UserDao - 执行SQL:[SELECT * FROM users WHERE id=12345]
2024-11-21 14:30:18.654 [main] WARN com.example.config.AppConfig - 配置项 `timeout` 使用默认值:3000ms
2024-11-21 14:30:19.001 [main] INFO com.example.Main - 应用启动成功,耗时:2.34秒

这段日志整齐清晰,支持按照时间、线程、级别、类名和内容搜索。

不统一的日志格式:

2024/11/21 14:30 登录成功 用户ID: 12345
2024-11-21 14:30:16 错误 用户12345登录失败!密码不对
DEBUG 执行SQL SELECT * FROM users WHERE id=12345
Timeout = default
应用启动成功

emm,看到这种日志我直接原地爆炸!

建议每个项目都要明确约定和配置一套日志输出规范,确保日志中包含时间戳、日志级别、线程、类名、方法名、消息等关键信息。

<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
       <!-- 日志格式 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
   </encoder>
</appender>

也可以直接使用标准化格式,比如 JSON,确保所有日志遵循相同的结构,便于后续对日志进行分析处理:

<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
   <!-- 配置 JSON 编码器 -->
</encoder>

此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如用户 ID、请求 ID 等,方便追踪。在 Java 代码中,可以为 MDC 变量设置值:

MDC.put("requestId", "666");
MDC.put("userId", "yupi");
logger.info("用户请求处理完成");
MDC.clear();

对应的日志配置如下:

<!-- 文件日志配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <encoder>
       <!-- 包含 MDC 信息 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
   </encoder>
</appender>

这样,每个请求、每个用户的操作一目了然。

7、使用异步日志

对于追求性能的操作,可以使用异步日志,将日志的写入操作放在单独的线程中,减少对主线程的阻塞,从而提升系统性能。

除了自己开线程去执行 log 操作之外,还可以直接修改配置来开启 Logback 的异步日志功能:

<!-- 异步 Appender -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>500</queueSize<!-- 队列大小 -->
   <discardingThreshold>0</discardingThreshold<!-- 丢弃阈值,0 表示不丢弃 -->
   <neverBlock>true</neverBlock<!-- 队列满时是否阻塞主线程,true 表示不阻塞 -->
   <appender-ref ref="CONSOLE" /> <!-- 生效的日志目标 -->
   <appender-ref ref="FILE" />
</appender>

上述配置的关键是配置缓冲队列,要设置合适的队列大小和丢弃策略,防止日志积压或丢失。

8、集成日志收集系统

在比较成熟的公司中,我们可能会使用更专业的日志管理和分析系统,比如 ELK(Elasticsearch、Logstash、Kibana)。不仅不用每次都登录到服务器上查看日志文件,还可以更灵活地搜索日志。

但是搭建和运维 ELK 的成本还是比较大的,对于小团队,我的建议是不要急着搞这一套。


OK,就分享到这里,洋洋洒洒 4000 多字,希望这篇文章能帮助大家意识到日志记录的重要性,并养成良好的日志记录习惯。学会的话给鱼皮点个赞吧~

日志不是写给机器看的,是写给未来的你和你的队友看的!

更多编程学习资源

本地widows(win11)docker环境安装

安装Docker Desktop

Windows只需要安装Docker Desktop就可以完成docker和docker-compose的环境;系统要求至少win10版本号19041 以上

  1. 检查是否启动了虚拟平台;控制面板》程序》程序和功能》启动或关闭Windows功能
  • win10
    在这里插入图片描述
    在这里插入图片描述

  • win11
    在这里插入图片描述

  1. 安装之前先检查系统是否已经有wsl2
  • 打开cmd命令窗口执行
    wsl --version
    ,出现类似下图表示已经有
    在这里插入图片描述
  • 如果没有
    wsl2就得先安装,打开PowerShell(管理员)运行
    wsl --install
  • 如果有
    存在了需要升级到最新版本
    wsl --update
  • 执行命令设置wsl版本
    wsl --set-default-version 2
  1. 下载
    Docker Desktop
    根据自己系统选择下载版本一般情况下应该是AMD64
    在这里插入图片描述
  2. 修改配置
    在这里插入图片描述
  • 完整的配置内容
{
  "builder": {
    "gc": {
      "defaultKeepStorage": "20GB",
      "enabled": true
    }
  },
  "experimental": false,
  "insecure-registries": [
    "http://47.xx.xx.46:2201",
    "http://xx.106.79.1xx:8080",
    "http://39.xx.170.xxx:6633",
    "http://127.0.0.1:6633"
  ],
  "registry-mirrors": [
    "https://mirrors.cloud.tencent.com",
    "https://registry.docker-cn.com",
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com",
    "https://mirror.baidubce.com",
    "https://ccr.ccs.tencentyun.com"
  ]
}
  • nsecure-registries上面配置里这个是配置非安全链接的私有仓库,就是私有仓库部署的时候没有用https直接是http的需要加在这里面跟白名单一个意思。
  • registry-mirrors这个是镜像源配置,下面服务器配置的时候也是一样。

服务器liunx(ubuntu)docker环境安装

安装nginx

  1. 执行安装
    apt-install nginx
    安装过程遇到提示直接选y
  2. 验证是否安装成功
    nginx -v

安装docker环境

  1. 执行安装
    apt-install docker.io
  2. 验证安装是否成功
    docker -v
  3. 配置镜像源和私有仓库编辑文件
    vim /etc/docker/daemon.json
    内容改为如下

{
  "registry-mirrors": [
     "https://l0xxxx85.mirror.aliyuncs.com",
     "https://mirrors.cloud.tencent.com",
     "https://registry.docker-cn.com",
     "https://docker.mirrors.ustc.edu.cn",
     "https://hub-mirror.c.163.com",
     "https://mirror.baidubce.com",
     "https://ccr.ccs.tencentyun.com"
     ],
  "insecure-registries": ["http://47.1xx.xx.xxx:6633"]
}

  1. 加载配置
    sudo systemctl daemon-reload
    然后重启docker
    sudo systemctl restart docker

安装docker compose

  1. 执行下载安装命令
    sudo curl -L "https://github.com/docker/compose/releases/download/v2.21.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  2. 赋予执行权限
    sudo chmod +x /usr/local/bin/docker-compose
  3. 验证是否安装成功
    docker-compose --version
  4. 找个目录创建通用执行脚本,此脚本为了根据配置自动拉取镜像更新后续会常用,如在/home下创建一个文件夹dockersh,然后创建脚本文件名为
    update_docker_compose3.0.sh
    内容如下:
#!/bin/bash

# 检查是否提供了参数
if [ -z "$1" ] || [ -z "$2" ]; then
  echo "Usage: $0 <docker-compose-file> <image-version>"
  exit 1
fi

# 使用传入的参数作为 docker-compose 文件和镜像版本号
COMPOSE_FILE=$1
IMAGE_VERSION=$2

# 提取项目名称(从文件名中提取或根据文件名设置)
PROJECT_NAME=$(basename "$COMPOSE_FILE" .yml)

# 更新镜像
echo "Pulling the latest images using ${COMPOSE_FILE} and image version ${IMAGE_VERSION}..."
if ! IMAGE_VERSION=$IMAGE_VERSION docker-compose -f $COMPOSE_FILE -p $PROJECT_NAME pull; then
  echo "Failed to pull the latest images. Exiting..."
  exit 1
fi

# 停止并删除当前的容器
echo "Stopping and removing current containers for project ${PROJECT_NAME}..."
if ! IMAGE_VERSION=$IMAGE_VERSION docker-compose -f $COMPOSE_FILE -p $PROJECT_NAME down --remove-orphans; then
  echo "Failed to stop and remove current containers. Exiting..."
  exit 1
fi

# 重新创建并启动服务
echo "Starting containers with the latest images and image version ${IMAGE_VERSION} for project ${PROJECT_NAME}..."
if ! IMAGE_VERSION=$IMAGE_VERSION docker-compose -f $COMPOSE_FILE -p $PROJECT_NAME up -d; then
  echo "Failed to start containers with the latest images. Exiting..."
  exit 1
fi

# 清理旧镜像
echo "Cleaning up old images..."
if ! docker image prune -f; then
  echo "Failed to clean up old images. Exiting..."
  exit 1
fi

echo "Update complete!"

  1. 给脚本执行权限
    chmod +x update_docker_compose3.0.sh

docker私有仓库部署

  1. 安装htpasswd-tool来生成密码文件
# Ubuntu 或 Debian
sudo apt-get update
sudo apt-get install apache2-utils

# CentOS 或 Red Hat
sudo yum install httpd-tools

  1. 创建存放密码文件的目录
    mkdir /var/auth
  2. 创建用户名和密码
htpasswd -Bc /var/auth/registry.password admin
  • -B 表示使用 bcrypt 算法加密密码(推荐使用)。
  • -c 表示创建新的 .htpasswd 文件。如果你要为其他用户添加密码,可以去掉 -c,这样可以避免覆盖文件。
  • 上面的命令执行之后会让你输入密码表示设置这个账号的密码
  1. 查看是否创建成功
    cat /var/auth/registry.password
  2. 编写一个容器编排脚本比如放在/home目录下名称叫docker-compose-rep.yml内容如下
version: '3'

services:
  registry:
    image: registry:2
    ports:
      - "6633:5000"
    environment:
      REGISTRY_AUTH: "htpasswd"
      REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
      REGISTRY_AUTH_HTPASSWD_PATH: "/auth/registry.password" #容器密码文件路径下面做映射
    volumes:
      - /var/auth:/auth  # 挂载存放密码文件的目录
      - /var/docker/registry/data:/var/lib/registry  # 存放镜像数据的目录

  1. 启动私有仓库
    docker-compose -f docker-compose-rep.yml up -d
  2. 登录私有仓库
    docker login http://47.xxx.xx.xx:6633
    ,执行之后根据提示输入账号和密码,看到提示如果显示成功就可以。
  • 特别说明如果没有配置https那docker请求的时候会报错这时候应该

docker部署mysql5.7

  1. 获取镜像
    docker pull mysql:5.7
    ;上面已经配置了国内镜像源应该是直接执行就可以,如果获取不了镜像需要外网请求那就需要有一个可以获取镜像的环境,获取之后放到自己的私有仓库再去拉取。
  2. 创建存放数据的目录
    mkdir /var/mysql_data
  3. 运行镜像
    docker run --name=mysql57 --restart=unless-stopped -e MYSQL_ROOT_PASSWORD=123456 -v /var/mysql_data:/var/lib/mysql -p 3306:3306 -d mysql:5.7 --lower-case-table-names=1
    ;关键字说明:(MYSQL_ROOT_PASSWORD=密码,-v 设置持久化映射路径,-p设置端口号,命令行里冒号前面的是主机路径或端口号冒号后面的是容器路径或端口号,lower-case-table-names=1不区分大小写)
  • 常用的 --restart策略:
    no:默认值。容器不会在重启后自动启动。
    always:无论容器的退出状态如何,都会在服务器重启后自动启动容器。
    unless-stopped:与 always 类似,但手动停止的容器不会自动重启。
    on-failure[
    ]:仅在容器因错误退出时自动重启,可以设置最大重启次数。

docker部署Redis

  1. 拉取Redis镜像
    docker pull redis
  2. 找个目录建一下配置文件文件名叫redis.conf内容为
################################ SNAPSHOTTING  ################################
 
# 持久化保存策略配置
# 在900s内,如果至少有1个key进行了修改,就进行持久化操作
save 900 1
# 在300s内,如果至少有10个key进行了修改,就进行持久化操作
save 300 10
# 在60s内,如果至少有10000个key进行了修改,就进行持久化操作
save 60 10000
# 配置如果持久化出错,Redis是否禁止写入命令 yes:禁止写入命令,no:允许写入命令(存在数据丢失风险)
stop-writes-on-bgsave-error yes
# 配置是否压缩rdb文件。[开启(yes)的话,会消耗一定的cpu资源]
rdbcompression yes
# 保存rdb文件的时候,进行错误的检查校验
rdbchecksum yes
# 默认持久化保存后的文件名
dbfilename dump.rdb
# rdb文件保存的目录
dir ./
 
# 设置访问、登录的密码,设置requirepass 您的密码
requirepass wyb123456
 
 
############################## APPEND ONLY MODE ###############################
 
 
# 是否开启aof持久化模式,默认值:no,不开启。redis的默认持久化策略是edb模式
appendonly no
 
# 持久化文件名称
appendfilename "appendonly.aof"
 
# 持久化策略设置
# appendfsync always # 每次修改都进行持久化操作
# 每秒执行一次持久化操作
appendfsync everysec
# appendfsync no     # 不执行持久化操作,相当于未开启aof持久化策略
 
# 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes
no-appendfsync-on-rewrite no
 
# AOF自动重写配置,默认值为100
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
 
# Redis在以AOF方式恢复数据时,对最后一条可能出问题的指令的处理方式,默认值yes
aof-load-truncated yes
 
# 当重写AOF文件时,Redis能够在AOF文件中使用RDB前导码,以便更快地重写和恢复,启用此选项时,重写的AOF文件由两个不同的节组成:
# [RDB file][AOF tail],当加载AOF文件时,Redis通过以 “REDIS” 字符串开头的AOF文件识别出此文件是由RDB和AOF组合而成的,Redis会先加载RDB部分,然后再加载AOF部分,默认值yes
# aof-use-rdb-preamble yes
 
 
 
################################### CLIENTS ####################################
 
# 设置客户端最大连接数,该配置一般无需修改,使用默认值即可
# maxclients 10000
 
############################## MEMORY MANAGEMENT ################################
 
# redis配置的最大内存容量
# maxmemory <bytes>
 
# 到达内存容量限制之后的处理策略
# maxmemory-policy noeviction
  • requirepass 以上配置内容这个是修改密码的
  1. 运行redis容器
    docker run --restart=always -p 6379:6379 --name myredis -v /var/docker/redis/redis.conf:/etc/redis/redis.conf -v /var/docker/redis/data:/data -d redis redis-server /etc/redis/redis.conf
  • -v是目录映射如果在windows那这个/var应该改为如C:/等其他目录都行
  1. 查看是否成功运行
    docker ps
    在这里插入图片描述
  2. 连接redis

各项目的发布及运行

旧的NET Framework项目

由于比较旧的非netcore项目没有办法直接支持docker发布需要依赖mono,这边直接采用比较方便的jexus来部署以下是部署过程的一些记录:

  1. 在本地创建一个文件夹专门用于这些已经发布好的旧项目把Windows服务器上的已发布项目复制下来如下图所示:
    在这里插入图片描述

  2. 在此目录下创建分别创建dockerfile和default内容如下:
    default

######################
# Web Site: Default 
########################################

port=80
root=/ /var/website/default
hosts=*    #OR your.com,*.your.com


# addr=0.0.0.0
# CheckQuery=false
NoLog=true
# AppHost={CmdLine=/usr/local/x/xx;AppRoot=/usr/local/x;Port=5000}
# NoFile=/index.aspx
# Keep_Alive=false
# UseGZIP=false

# UseHttps=true
# ssl.certificate=/var/mallapi.chunqiulihe.com/1_mallapi.chunqiulihe.com_bundle.crt  #or pem
# ssl.certificatekey=/var/mallapi.chunqiulihe.com/2_mallapi.chunqiulihe.com.key
# ssl.protocol=TLSv1.0,TLSv1.1,TLSv1.2  # TLSv1.1 or  TLSv1.2...
# ssl.ciphers= 

# DenyFrom=192.168.0.233, 192.168.1.*, 192.168.2.0/24
# AllowFrom=192.168.*.*
# DenyDirs=~/cgi, ~/upfiles
# indexes=myindex.aspx
# rewrite=^/.+?\.(asp|php|cgi|pl|sh)$ /index.aspx
# reproxy=/bbs/ http://192.168.1.112/bbs/
# host.Redirect=abc.com www.abc.com  301
# ResponseHandler.Add=myKey:myValue


# Jexus php fastcgi address is '/var/run/jexus/phpsvr'
#######################################################
# fastcgi.add=php|socket:/var/run/jexus/phpsvr

# php-fpm listen address is '127.0.0.1:9000'
############################################
# fastcgi.add=php|tcp:127.0.0.1:9000

dockerfile

FROM ubuntu:latest
LABEL "name"="支付系统管理端" 
LABEL "version"="1.0.0"
LABEL "describe"="初始化部署"

#安装更新必要的库
RUN apt-get update; apt-get -y upgrade; apt-get -y install wget curl vim libicu-dev
#设置时区
RUN apt-get install -y tzdata
RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && dpkg-reconfigure --frontend noninteractive tzdata
# 安装字符集
RUN apt-get install -y language-pack-zh-hans;
# 添加中文支持
# zh_CN.UTF-8 等于 zh_CN.utf8
RUN locale-gen zh_CN.UTF-8;
ENV LANG zh_CN.UTF-8
ENV LC_ALL zh_CN.UTF-8
#安装jexus
RUN wget --no-check-certificate https://linuxdot.net/down/jexus-6.4.x-x64.tar.gz
RUN tar -zxvf jexus-6.4.x-x64.tar.gz
RUN mv -f jexus /usr/
RUN cd /usr/jexus
RUN chmod +x /usr/jexus/jws

RUN apt-get -y autoremove; apt-get -y autoclean; apt-get -y clean

COPY . /var/website/default
COPY default /usr/jexus/siteconf
#开放端口
EXPOSE 80 443
WORKDIR /usr/jexus
ENTRYPOINT ["/usr/jexus/jws", "start", "-D"]
  1. 打包docker镜像,镜像打标签,镜像推送到私有仓库,此处不懂可以看前面的文章
    docker build -t zetapay-cms:1.0.0 .
    docker tag zetapay-cms:1.0.0 47.01.01.100:8080/zaoxu/zetapay-cms:1.0.0
    docker push 47.01.01.100:8080/zaoxu/zetapay-cms:1.0.0
  2. 在服务器
    创建docker镜像运行脚本内容如下:
version: '3'
services:
  zetapay-cms: #服务名称
    container_name: zetapay-cms
    image: 47.01.01.100:8080/zaoxu/zetapay-cms:${IMAGE_VERSION}
    ports:
      - "2401:80" #端口号2401是映射后主机的端口号nginx配置的时候使用这个
    environment:
      - ENV=production
    volumes: #这边是目录映射主机目录在前:容器目录在后,根据自己实际项目需要映射的目录去增减
      - /var/website/activity/cms/view:/var/website/default/view
      - /var/website/activity/cms/attachments:/var/website/default/attachments
      - /var/website/activity/cms/Models:/var/website/default/Models
      - /var/website/activity/cms/log:/var/website/default/log
    restart: unless-stopped #除非用户手动停止容器,否则始终重启

  1. 到服务器放脚本的目录下执行脚本
    ./update_docker_compose3.0.sh docker-compose-zetapay-cms.yml 1.0.0
  2. 配置nginx,切换到目录
    cd /etc/nginx/conf.d
    ,创建文件
    vim test-cms.conf
    ,这里最好是每个项目一个配置文件不要混在一起比较方便管理。
    test-cms.conf的内容如下:
server {
   server_name  test.cms.aaa.com.cn;
   #charset koi8-r;

   #access_log  logs/host.access.log  main;
   client_max_body_size    128m;  

   location / {
   	# 这里的端口号2401就是上面运行镜像映射到主机对应的端口号
       proxy_pass http://127.0.0.1:2401;
       proxy_redirect off;
       proxy_set_header Host $host;   
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       client_max_body_size 10m; 
       client_body_buffer_size 128k; 
       proxy_connect_timeout 90;
       proxy_read_timeout 90;
       proxy_buffer_size 4k;
       proxy_buffers 6 32k;
       proxy_busy_buffers_size 64k;
       proxy_temp_file_write_size 64k;
   }
   error_page  404              /404.html;
   location = /404.html {
       return 404;
   }

   # redirect server error pages to the static page /50x.html
   #
   error_page   500 502 503 504  /50x.html;
   location = /50x.html {
       return 503;
   }
}
  1. 重启nginx
    nginx -s reload

netcore项目-有源码

  1. 在用vs在运行项目添加docker支持,右键项目》添加》docker支持,容器os为liunx,容器生成类型为dockerfile,发行版本默认就行。

  2. 右键项目》发布》新建配置文件》docker容器注册表》》其他docker容器注册表(这里是本地仓所以选择这个,如果是其他的按照实际情况选择)》输入私有仓库地址、用户名、密码》docker desktop
    在这里插入图片描述
    在这里插入图片描述

  3. 点发布之前先修改一下镜像版本号,然后直接点发布
    在这里插入图片描述

  4. 发布成功之后可以在docker镜像列表中看到如下图
    在这里插入图片描述

  5. 以上已经完成镜像打包上传到了私有仓库,接下来就是要在服务器做发布操作,按照上面的旧的NET Framework项目迁移的第4-7的步骤。

netcore项目-无源码

  1. 在项目目录下创建一个dockerfile内容如下:
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base

LABEL "name"="商城API" 
LABEL "version"="1.0.0"
LABEL "describe"="首次初始化"

WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY . .
ENV ASPNETCORE_URLS=http://+:80
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENTRYPOINT ["dotnet", "Shop.Web.Entry.dll"]
  1. 用命令窗口打开到目录下,执行build打包,tag给标签,push上传到私有仓库
  2. 到服务器部署按照旧的NET Framework项目迁移第4-7步骤执行

java项目

这里对java的idea工具还不是很熟悉,所以没有采用像vs那样直接添加docker支持的方式;使用先打包成jar的方式然后dockerfile镜像方式

  1. 打包成jar,idea最右边点开maven》package等待执行完之后会在target文件夹下生成一个jar文件
    在这里插入图片描述
  2. 这边我们可以把.jar文件复制到自己新创建的目录下,在这个目录下还可以放项目里需要读取的文件如秘钥pem或者引用的其他dll文件,如下图
    在这里插入图片描述
    dockerfile的内容为:
# 使用 JDK 1.8 作为基础镜像
FROM openjdk:8-jdk-alpine

# 设置时区为北京时间
RUN apk add --no-cache tzdata \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone


# 创建应用目录
WORKDIR /app

# 将打包好的 JAR 文件复制到容器中
COPY . /app


# 暴露应用所使用的端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["java", "-jar", "/app/ca.test.jar"]
  1. 参照上面其他项目的打包镜像,标签镜像,上传镜像,部署完成最后的发布。

nodejs项目

与上面相同在发布后的目录下创建dockerfile,然后操作步骤一样;dockerfile的内如如下:

FROM node:lts-alpine3.17
LABEL "name"="商城服务" 
LABEL "version"="1.0.1"
LABEL "describe"=""
#设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN npm i pm2 -g

COPY . /var/website/default
#暴露端口号
EXPOSE 80 443
WORKDIR /var/website/default
ENTRYPOINT ["pm2-runtime", "start", "nodeserver.js"]

静态项目

跟发布后的部署方式一样,多了一个nginx的配置文件,就是在发布目录建两个文件如下
dockerfile

FROM nginx:latest
COPY . /usr/share/nginx/html
COPY  default.conf /etc/nginx/conf.d/default.conf
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

default.conf

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;


    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;  #解决刷新出现404问题
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

其他部署执行参考上面的项目。

https免费证书配置

  1. 安装snapd
    sudo apt update
    sudo apt install snapd
  2. 安装Certbot
    sudo snap install --classic certbot
  3. 软连接(环境变量),让随意目录下都能执行certbot
    sudo ln -s /snap/bin/certbot /usr/bin/certbot
  4. 根据域名生成证书
    sudo certbot certonly --nginx -d test.cms.wyb.com.cn
  • 如果多个域名就-d 域名 -d 域名
  • 成功之后会出现存放证书的路径大概如下
    Successfully received certificate.
    Certificate is saved at: /etc/letsencrypt/live/test.cms.wyb.com.cn/fullchain.pem
    Key is saved at: /etc/letsencrypt/live/test.cms.wyb.com.cn/privkey.pem
  1. 配置nginx使用这个证书
  • 打开上面nginx的配置文件在net framework部署的第6步 添加433端口的监听并且配置证书内容如下
server {
    server_name  test.cms.wyb.com.cn;
    #charset koi8-r;

    #access_log  logs/host.access.log  main;
    client_max_body_size    128m;  

    location / {
        proxy_pass http://127.0.0.1:2410;
        proxy_redirect off;
        proxy_set_header Host $host;   
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size 10m; 
        client_body_buffer_size 128k; 
        proxy_connect_timeout 90;
        proxy_read_timeout 90;
        proxy_buffer_size 4k;
        proxy_buffers 6 32k;
        proxy_busy_buffers_size 64k;
        proxy_temp_file_write_size 64k;
    }
    error_page  404              /404.html;
    location = /404.html {
        return 404;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        return 503;
    }
}
server {
		listen 443 ssl; 
		server_name test.cms.wyb.com.cn;
    ssl_certificate /etc/letsencrypt/live/test.cms.wyb.com.cn/fullchain.pem; # Certbot生成的证书
    ssl_certificate_key /etc/letsencrypt/live/test.cms.wyb.com.cn/privkey.pem; # Certbot生成的证书      
		ssl_session_timeout 5m;
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 
		ssl_ciphers AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL;
		ssl_prefer_server_ciphers   on;
		client_max_body_size    128m;  
		 location / {
        proxy_pass http://127.0.0.1:2410;
        proxy_redirect off;
        proxy_set_header Host $host;   
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size 10m; 
        client_body_buffer_size 128k; 
        proxy_connect_timeout 90;
        proxy_read_timeout 90;
        proxy_buffer_size 4k;
        proxy_buffers 6 32k;
        proxy_busy_buffers_size 64k;
        proxy_temp_file_write_size 64k;
    }

    error_page  404              /404.html;
    location = /404.html {
        return 404;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        return 503;
    }
}
  1. 重启nginx
    nginx -s reload
    ,然后就可以访问https了。
  2. 设置证书到期自动更新(以下二选一即可)
    7.1 第一种方式使用Cron
    打开文件
    crontab -e
    ,如果是第一次打开会让你选择编辑器这边熟悉vim所以选择vim.basic
    添加自动续签任务
    0 3 * * * certbot renew --quiet && systemctl reload nginx
    ;保存并退出编辑之后cron会自动加载
  • 含义:


    • 0 3 * * *: 每天凌晨 3 点运行一次。
    • certbot renew --quiet: 检查是否需要续订证书并自动续订。
    • && systemctl reload nginx: 在续订成功后,重新加载 Nginx 配置以应用新证书。

    7.2 第二种方式使用certbot.timer定时器
    创建
    /etc/systemd/system/certbot.timer
    内容如下:

[Unit]
Description=Run certbot twice daily

[Timer]
OnCalendar=*-*-* 03:00:00  # 定义触发时间:每天的 03:00:00
Persistent=true               # 在重启后继续保持计时

[Install]
WantedBy=timers.target         # 定时器作为 systemd 的计时目标启动

创建
/etc/systemd/system/certbot.service
内容如下:

[Unit]
Description=Certbot renewal
Documentation=https://eff-certbot.readthedocs.io/en/stable/

[Service]
Type=oneshot            # 服务运行一次就退出
ExecStart=/usr/bin/certbot renew --quiet --renew-hook "systemctl reload nginx" # 具体执行 Certbot 的续签命令重启nginx

创建定时器和服务之后执行下面命令启动定时器

sudo systemctl daemon-reload
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

验证是否成功启动
systemctl list-timers | grep certbot

最后

如果上面的步骤忘了设置
容器的重启策略
可以执行以下命令一次性全部设置
docker ps -q | xargs -n 1 docker update --restart=unless-stopped

  • 常用的 --restart策略:
    no:默认值。容器不会在重启后自动启动。
    always:无论容器的退出状态如何,都会在服务器重启后自动启动容器。
    unless-stopped:与 always 类似,但手动停止的容器不会自动重启。
    on-failure[
    ]:仅在容器因错误退出时自动重启,可以设置最大重启次数。

一、异步FIFO需要注意的问题

所谓异步FIFO,指的是写时钟与读时钟可以不同步,读时钟可以比写时钟快,反之亦然。思考一下,这样会直接地造成两个问题:

1. 读满或者写满

由于异步FIFO的基本存储单元是双端口RAM,因此读写速率不一致,就会造成读满或者写满的问题。

2. 跨时钟域的同步

为了判断读满、写满的情况,势必需要将写指针(告诉读模块,写到哪个位置了,我还可不可以继续读?)同步到读模块,(或者读指针同步到写模块,通知写模块,现在读到哪里了,我还能不能继续写啦?如果还没读,我再写一轮不就把数据覆盖了啊?),这样就会存在跨时钟域的同步问题。
因此,针对上述问题,我们解决办法如下:

3. 针对问题一,将读指针与写指针进行比较,产生读空、写满标志。

思考一下如何判断读空、写满标志呢,假设有一个深度为8的RAM,那么其地址线的宽度为3,这里我们扩展一位,让最高位作为读空、写满标志,(实际给到RAM的只有[2:0]),其原理如下
假设写指针写到了0111,此时读指针也读到了0111,意味着读指针追上了写写指针,那么此时就是读空了;
假设写指针写到了1000(实际上是第二轮的000),此时读指针读到了000(第一轮的000),那么就是写满。
image

因此,可以总结:
当最高位相同,其余位相同认为是读空
当最高位不同,其余位相同认为是写满

4. 针对问题二:两级寄存器同步 + 格雷码

我们将读写指针编码为Gray码并打两拍进行同步。采用Gray码的原因可参考上一篇博客,简单来说就是Gray码相邻两个编码之间只存在一个bit变化,避免多个bit同时跳变的问题。再进行两拍同步就可以将读写指针进行异步时钟同步化了。

5. 由于问题二,采用Gray码,导致我们判断读空及写满的逻辑需要稍微改变下了:

我们观察一下基于gray码的读空、写满的情况:
image

因此,用格雷码判断是否为读空或写满时应看最高位和次高位是否相等:即:
当最高位和次高位相同,其余位相同认为是读空
当最高位和次高位不同,其余位相同认为是写满

二、异步FIFO架构

根据上述讨论,我们可以总结一个异步FIFO的架构包括以下几个部分:

  1. 双端口RAM,作为FIFO的存储体。可以采用硬件描述的方式描述一个RAM,也可以采用IP核、原语的方式。
  2. FIFO写模块,用于产生写地址,写使能,写满等信号
  3. FIFO读模块,用于产生读地址,读使能、读空等信号。
  4. Gray码转换模块,用于自然二进制与Gray码转换
  5. 时钟同步,用于将读写指针打拍同步。
    结构框架如下:
    image

三、reference

《Simulation and Synthesis Techniques for Asynchronous FIFO Design》Clifford E. Cummings, Sunburst Design, Inc.
cliffc@sunburst-design.com