主頁 > 軟體設計 > 寶塔面板批量封IP---node.js增量式封鎖腳本(定時每日自動封新IP)

寶塔面板批量封IP---node.js增量式封鎖腳本(定時每日自動封新IP)

2021-07-31 16:15:55 軟體設計

一句話需求

  • 現在是七月, 從三月開始我的一個網站一直受到幾百個IP的流量攻擊, 具體表現就是日志里面出現大量訪問一個固定url網址的不帶reffer的手機端的國內IP段的大量請求. 每秒請求超過50次.
  • 一開始用寶塔面板免費WAF nginx防火墻, 能防住, 但是效果不好, 依然會有大量額外的圖片等資源請求.
  • 事件的經過在沒查明IP之前我是不想封的, 因為有些站群的操作手法就是克隆我的網站來引流到他們自己的網站, 這種手法會造成大量訪問我IP的請求都是來自真實用戶的手機. 然后根絕我長期采樣觀察發現這些雖然是真人IP但是應該是來自他們手機里面的后門, 所以流量對我沒有意義, 可以直接封殺掉了.
  • 定性之后就要封IP了. nginx那個防火墻不好用, 實際上請求還是請求了nginx的只是nginx回傳了錯誤資訊直接擋住了.所以nginx依然是有負載的.并且防不住變幻請求其他的鬼才知道的哪個頁面.
  • 我們要用linux自帶的防火墻來直接封IP才能有效抵御住DDOS. 我使用的是 centos 7.6 系統自帶防火墻是 firewalld , 我禁用了iptable
  • 可視化管理是寶塔面板的系統防火墻3.0在這里插入圖片描述

在這里插入圖片描述
這是利用centos最新的系統級firewalld來封鎖IP的高級防火墻, 比iptable更好.

  • 從圖片上的匯入規則我們可以批量匯入要封鎖的IP地址
  • 匯入格式是{"id": 1, "types": "drop", "address": "171.8.172.145", "brief": "", "addtime": "2021-07-28 17:26:50"}

需求落地:

  • 分析nginx的web日志
  • 提取要封鎖的IP
  • 生成JSON規則串列, 并保留歷史記錄, 下一次分析日志就只需要添加新規則
  • 一次性匯入即可

專案命名 digIP.js

檔案夾結構

在這里插入圖片描述
原始碼:

/**
* ./utils/datetime-format.js
 * 計算指定時間到當前時間的時間間隔
 * @param {*} time 指定時間
 * @return 時間間隔或年月日時分
 *
 * example:
 * time2MinuteOrHour('2020-04-25T13:42:00.000Z')
 */
const time2MinuteOrHour = time => {
  const now = new Date()
  const pass = new Date(time)
  const result = now - pass
  // 分鐘差小于60分鐘
  if (parseInt(parseInt(result / 1000, 0) / 60, 0) < 60) {
    return `${Math.ceil(result / 1000 / 60)}分鐘前`
  }
  // 小時差小于16小時
  if (parseInt(parseInt(parseInt(result / 1000, 0) / 60, 0) / 60, 0) < 16) {
    return `${Math.ceil(result / 1000 / 60 / 60)}小時前`
  }
  // 超過16個小時展示 年月日時分
  return time.replace(/T/, ' ').replace(/Z/, '').substring(0, 16)
}

/**
 * 時間轉換為年月日時分
 * @param {*} originTime 原始時間
 * @return 年月日 時分
 *
 * example:
 * time2DateAndHM('2020-04-25T11:54:17+08:00')
 */
const time2DateAndHM = originTime => {
  const time = originTime.replace(/T/, ' ').replace(/Z/, '')
  return `${time.substring(0, 10)} ${time.substring(11, 16)}`
}

/**
 * 有效期(時間戳減去當前時間戳再轉換為天)
 * @param {*} timestamp 時間戳
 * @return 天
 *
 * example:
 * timestamp2day(1589785128)
 */
const timestamp2day = timestamp => {
  const interval = timestamp - Math.round(new Date() / 1000)
  return parseInt(interval / (60 * 60 * 24), 0)
}

// yyyy-MM-dd hh:mm:ss.SSS 所有支持的型別
function pad(str, length = 2) {
  str += ''
  while (str.length < length) {
    str = '0' + str
  }
  return str.slice(-length)
}

const parser = {
  yyyy: dateObj => {
    return pad(dateObj.year, 4)
  },
  yy: dateObj => {
    return pad(dateObj.year)
  },
  MM: dateObj => {
    return pad(dateObj.month)
  },
  M: dateObj => {
    return dateObj.month
  },
  dd: dateObj => {
    return pad(dateObj.day)
  },
  d: dateObj => {
    return dateObj.day
  },
  hh: dateObj => {
    return pad(dateObj.hour)
  },
  h: dateObj => {
    return dateObj.hour
  },
  mm: dateObj => {
    return pad(dateObj.minute)
  },
  m: dateObj => {
    return dateObj.minute
  },
  ss: dateObj => {
    return pad(dateObj.second)
  },
  s: dateObj => {
    return dateObj.second
  },
  SSS: dateObj => {
    return pad(dateObj.millisecond, 3)
  },
  S: dateObj => {
    return dateObj.millisecond
  }
}

// 這都n年了iOS依然不認識2020-12-12,需要轉換為2020/12/12
function getDate(time) {
  if (time instanceof Date) {
    return time
  }
  switch (typeof time) {
    case 'string':
      return new Date(time.replace(/-/g, '/'))
    default:
      return new Date(time)
  }
}

function formatDate(date, format = 'yyyy/MM/dd hh:mm:ss') {
  if (!date && date !== 0) {
    return '-'
  }
  date = getDate(date)
  const dateObj = {
    year: date.getFullYear(),
    month: date.getMonth() + 1,
    day: date.getDate(),
    hour: date.getHours(),
    minute: date.getMinutes(),
    second: date.getSeconds(),
    millisecond: date.getMilliseconds()
  }
  const tokenRegExp = /yyyy|yy|MM|M|dd|d|hh|h|mm|m|ss|s|SSS|SS|S/
  let flag = true
  let result = format
  while (flag) {
    flag = false
    result = result.replace(tokenRegExp, function (matched) {
      flag = true
      return parser[matched](dateObj)
    })
  }
  return result
}

function friendlyDate(
  time,
  {
    locale = 'zh',
    threshold = [60000, 3600000],
    format = 'yyyy/MM/dd hh:mm:ss'
  }
) {
  if (!time && time !== 0) {
    return '-'
  }
  const localeText = {
    zh: {
      year: '年',
      month: '月',
      day: '天',
      hour: '小時',
      minute: '分鐘',
      second: '秒',
      ago: '前',
      later: '后',
      justNow: '剛剛',
      soon: '馬上',
      template: '{num}{unit}{suffix}'
    },
    en: {
      year: 'year',
      month: 'month',
      day: 'day',
      hour: 'hour',
      minute: 'minute',
      second: 'second',
      ago: 'ago',
      later: 'later',
      justNow: 'just now',
      soon: 'soon',
      template: '{num} {unit} {suffix}'
    }
  }
  const text = localeText[locale] || localeText.zh
  const date = getDate(time)
  let ms = date.getTime() - Date.now()
  const absMs = Math.abs(ms)
  if (absMs < threshold[0]) {
    return ms < 0 ? text.justNow : text.soon
  }
  if (absMs >= threshold[1]) {
    return formatDate(date, format)
  }
  let num
  let unit
  let suffix = text.later
  if (ms < 0) {
    suffix = text.ago
    ms = -ms
  }
  const seconds = Math.floor(ms / 1000)
  const minutes = Math.floor(seconds / 60)
  const hours = Math.floor(minutes / 60)
  const days = Math.floor(hours / 24)
  const months = Math.floor(days / 30)
  const years = Math.floor(months / 12)
  switch (true) {
    case years > 0:
      num = years
      unit = text.year
      break
    case months > 0:
      num = months
      unit = text.month
      break
    case days > 0:
      num = days
      unit = text.day
      break
    case hours > 0:
      num = hours
      unit = text.hour
      break
    case minutes > 0:
      num = minutes
      unit = text.minute
      break
    default:
      num = seconds
      unit = text.second
      break
  }

  if (locale === 'en') {
    if (num === 1) {
      num = 'a'
    } else {
      unit += 's'
    }
  }

  return text.template
    .replace(/{\s*num\s*}/g, num + '')
    .replace(/{\s*unit\s*}/g, unit)
    .replace(/{\s*suffix\s*}/g, suffix)
}

module.exports = {
  formatDate
}

./utils/write-file.js

const fs = require('fs').promises
const path = require('path')

/**
 * Write file from path
 * @param {String} _path Relative path
 * @param {[String]} file File data
 */
const writeFile = async (_path, file, flag = 'w+') => {
  const relativePath = path.resolve(__dirname, _path)
  const { dir } = path.parse(relativePath)
  await fs.mkdir(path.resolve(__dirname, dir), { recursive: true })
  // console.log('file write to ', relativePath)
  return fs.writeFile(relativePath, file, { flag: 'w+' })
}

module.exports = writeFile

./utils/wirte-log.js

const fs = require('fs').promises
const writeLog = async (file, str, isAppend = true) => {
  str && isAppend
    ? await fs.appendFile(file, str + '\n', 'utf-8')
    : await fs.writeFile(file, str + '\n', 'utf-8')
}

module.exports = writeLog

./digIP.js 主程式代碼

const fs = require('fs').promises
const path = require('path')
const writeLog = require('./utils/write-log.js')
const formatDate = require('./utils/datetime-format.js').formatDate

/*
分析網站日志 main 組
提取非法IP串列
輸出指定格式JSON
**********************使用放方法:
配置config里面的weblog路徑地址, 必須是main路徑或者IP在第一位(日志分隔符是空格), main配置格式見后
首次執行, 執行即可在當前目錄下生成一個JSON檔案, 復制這個檔案內容到寶塔的系統防火墻匯入IP界面即可(匯入之前先洗掉老的JSON, 700個IP耗時半小時, 這個匯入時間很長, 是寶塔的問題)
二次執行, 會生成全新JSON, 包含老的(這里要優化下, 提供只生成新IP的策略JSON)
之后對新weblog分析(含老日志或者全新日志都可以), 會自動對比老組態檔和新weblog, 自動去重已添加的IP并分配新的序列號

*/
const config = {
  newIpNewFile: true, // true 單獨輸出一份新IP輸出為獨立json檔案; false 不單獨輸出一份
  logFile: String.raw`F:\my_download\你的網站nginx訪問日志檔案(單行資料里面是空格分隔,其中IP是第一個資料).log`,
  outputFile: path.resolve(__dirname, 'output-policy.json'),
  policy: {
    keyword: '/tag/特殊請求url格式/return%20false'
  }
}

const getIPList = async () => {
  let list = (await fs.readFile(config.logFile, { encoding: 'utf-8' })).split(
    '\n'
  )
  console.log(`總計 ${list.length} 條記錄`)
  list = list.filter(item => ~item.indexOf(config.policy.keyword))
  console.log(`找到 ${list.length} 條非法記錄`)
  list = list.map(item => item.split(' ')[0])
  list = [...new Set(list)]
  list = list.sort()
  console.log(`本次攻擊IP總計: ${list.length}`)
  return list
}

// 生成封鎖模板
// {"id": 1, "types": "drop", "address": "171.8.172.145", "brief": "", "addtime": "2021-07-28 17:26:50"}
const outputJSON = async (ipList, JsonInfo = 1, type = 'bt') =>
  ipList.map((item, index, arr) => ({
    id:
      index +
      (typeof JsonInfo === 'number'
        ? JsonInfo
        : JsonInfo.length > 0
        ? JsonInfo.slice(-1)[0].id + 1
        : 1),
    types: 'drop',
    address: item.trim(),
    brief: '',
    addtime: formatDate(Date.now(), 'yyyy-MM-dd hh:mm:ss')
  }))

// 對比本地list 檢測新的
const getNewJsonDeltaBundle = async ipList => {
  const oldJSON = JSON.parse(
    await fs.readFile(config.outputFile, { encoding: 'utf-8' })
  )
  if (oldJSON.length > 0) {
    const oldIPs = oldJSON.map(item => item.address)
    return [ipList.filter(item => !oldIPs.includes(item)), oldJSON]
  } else return [ipList, oldJSON]
}

;(async () => {
  const timeElapseFlag = '🎈All Done'
  console.time(timeElapseFlag)
  const ipList = await getIPList()
  let jsonList = await outputJSON(ipList)

  try {
    await fs.access(config.outputFile)
  } catch (err) {
    await writeLog(config.outputFile, JSON.stringify([]), false)
  }

  const [newIPList, oldJSON] = await getNewJsonDeltaBundle(ipList)
  if (newIPList.length) {
    console.log('寫入新IP', newIPList)
    if (config.newIpNewFile) {
      jsonList = [...(await outputJSON(newIPList, oldJSON))]
      await writeLog(
        `${config.outputFile.slice(
          0,
          -5
        )}_${Date.now()}${config.outputFile.slice(-5)}`,
        JSON.stringify(jsonList),
        false
      )
    }

    jsonList = [...oldJSON, ...(await outputJSON(newIPList, oldJSON))]
  } else {
    console.log('沒有找到需要添加的新IP')
  }

  await writeLog(config.outputFile, JSON.stringify(jsonList), false)
  console.timeEnd(timeElapseFlag)
})()


代碼執行

node digIP.js

  • 初次運行
    在這里插入圖片描述
  • 二次運行(無新IP)
    在這里插入圖片描述
  • 三次運行, 加入增量IP
  • 在這里插入圖片描述

附上nginx的main節點日志格式

log_format main '$remote_addr - $remote_user [$time_local] requesthost:"$http_host"; "$request" requesttime:"$request_time"; $status $body_bytes_sent "$http_referer" - $request_body "$http_user_agent" "$http_x_forwarded_for"';

后記

生成的JSON檔案用寶塔里面的系統防火墻IP屏蔽匯入配置即可
在這里插入圖片描述
流量刷一下就下去了
清空web日志
第二天再次觀察還有幾個漏網之魚
再次執行程式得到新的組態檔
寶塔匯入之
妥妥封死了

[ 2021年7月31日更新 ] 拋棄了寶塔系統防火墻, 直接注入linux系統級firewalld規則, 速度提升千倍(2秒封1000個IP)

  • 增加服務器全自動每日凌晨定時封自動封IP功能

  • 其實就是在上述代碼末尾的timeEnd列印之前插入一段代碼即可, 老代碼可以保留不影響
    在這里插入圖片描述

  • 本質上就是批量一次性添加上萬條IP到bloclist規則,所以, 要每日生效之前需要進行blocklist規則的初始化,如下:
    在這里插入圖片描述
    初始化完成我們就可以利用ipset名為blocklist的規則從代碼里呼叫了.
    - 每天都會生成一個ip-block-list-時間.txt的檔案(內含新封鎖IP,一行一條) 向blocklist里增加IP,所有增加的IP被自動屏蔽(需要reload, 所以代碼末尾執行了reload, reload這一步比較費時間超時時間拉長到2分鐘)

  • 最后寶塔的"計劃任務"里面增加一個bash腳本每日定時執行就可以了
    在這里插入圖片描述

  • 實測增加1000個IP耗時12秒,其中分析日志,插入腳本需要2秒,剩下10多秒都是reload防火墻消耗,這個沒辦法變快. 相比一條一條的增加寶塔的防火墻規則耗時2個小時多, 這個批量增加ipset的辦法只用了12秒實在是快太多了. 后來又測驗了使用firewalld的rich規則一條一條注入1000條耗時在30秒左右, 所以最終保留了最優方案的代碼.

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/291156.html

標籤:其他

上一篇:鏡像的概念

下一篇:牛客網的編程初學者入門訓練第二十題:kiki算數

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more