2024年1月

项目代码同步更新至码云
uni-vue3-ts-template

开发前准备

利用
uni-app
开发,有两种方法:

  1. 通过
    HBuilderX
    创建(需安装
    HBuilderX
    编辑器)
  2. 通过命令行创建(需安装
    NodeJS
    环境),推荐使用
    vscode
    编辑器

这里我们使用第2种方法,这两种方法官方都有详细介绍
点击查看官方文档

vscode
安装插件

  1. 安装 Vue3 插件,
    点击查看官方文档
  • 安装
    Vue Language Features (Volar)
    :Vue3 语法提示插件
  • 安装
    TypeScript Vue Plugin (Volar)
    :Vue3+TS 插件
  • 工作区禁用
    Vue2 的 Vetur 插件(Vue3 插件和 Vue2 冲突)
  • 工作区禁用
    @builtin typescript 插件(禁用后开启 Vue3 的 TS 托管模式)
  1. 安装 uni-app 开发插件
  • uni-create-view
    :快速创建 uni-app 页面
  • uni-helper
    (插件套装,安装一个后会有多个插件) :代码提示
  • uniapp小程序扩展
    :鼠标悬停查文档

uni-create-view
插件使用

uni-create-view
安装后,需要修改配置,进入
文件
->
首选项
->
设置
,按以下选项修改即可
image

uni-create-view
使用方法:

src/pages
下右键,选择
新建uni-app页面
,弹出输入框,输入
文件夹名称 页面名称
,然后回车
image

生成如下目录文件
image

并且在
src/pages.json
目录下,已将新界面配置进去
image

vscode
项目配置

项目生成后,在项目的根目录进行
新建
.vscode
文件夹,并创建
settings.json
文件:

{
  // 在保存时格式化文件
  "editor.formatOnSave": true,
  // 文件格式化配置
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  // 配置语言的文件关联
  "files.associations": {
    "pages.json": "jsonc", // pages.json 可以写注释
    "manifest.json": "jsonc" // manifest.json 可以写注释
  }
}

同样,在
.vscode
文件夹内,创建 vue3 模板文件,命名为
vue3-uniapp.code-snippets

{
  "vue3+uniapp快速生成模板": {
    "scope": "vue",
    "prefix": "Vue3",
    "body": [
      "<script setup lang=\"ts\">",
      "$2",
      "</script>\n",
      "<template>",
      "\t<view class=\"\">test</view>",
      "</template>\n",
      "<style lang=\"scss\"></style>",
      "$2"
    ],
    "description": "vue3+uniapp快速生成模板"
  }
}

然后,在空白vue文件中,输入vue3,选择此模板,即可快速生成代码
image

项目初始化

项目创建

拉取
uni-app
官方项目基础架构代码
https://uniapp.dcloud.net.cn/quickstart-cli.html

npx degit dcloudio/uni-preset-vue#vite-ts uni-vue3-ts-shop

cd uni-vue3-ts-shop

或者直接直接克隆国内 gitee 地址,然后修改项目名称,并进入项目根目标

git clone -b vite-ts https://gitee.com/dcloud/uni-preset-vue.git

安装ts扩展

主要是为了增加
uni-app

微信小程序

nodejs
对ts的支持

npm i -D @uni-helper/uni-app-types miniprogram-api-typings @types/node

修改
tsconfig.json

{
  "compilerOptions": {
    "ignoreDeprecations": "5.0",
    "allowJs": true,
    },
    "types": ["@dcloudio/types", "miniprogram-api-typings", "@uni-helper/uni-app-types"]
  },
  "vueCompilerOptions": {
    // experimentalRuntimeMode 已废弃,现调整为 nativeTags,请升级 Volar 插件至最新版本
    "nativeTags": ["block", "component", "template", "slot"]
  }
}

配置环境变量

点我查看官方文档

新增env文件

根目录下新建
.env
文件

VITE_HTTP = https://mock.mengxuegu.com/mock/6598258423a3c638568501db/uniapp_template

使用

获取环境变量

process.env.NODE_ENV          // 应用运行的模式,比如vite.config.ts里
import.meta.env.VITE_HTTP     // src下的vue文件或其他ts文件里

开启
sourcemap

点我查看官方文档
修改
vite.config.ts
文件:

export default defineConfig({
  build: {
    // 开发阶段启用源码映射:https://uniapp.dcloud.net.cn/tutorial/migration-to-vue3.html#需主动开启-sourcemap
    sourcemap: process.env.NODE_ENV === 'development',
  },
  plugins: [uni()],
})

统一代码规范

安装
prettier

npm i -D prettier

根目录下新建
.prettierrc.json

{
  "singleQuote": true,
  "semi": false,
  "printWidth": 120,
  "trailingComma": "all",
  "endOfLine": "auto"
}

安装
eslint

npm i -D eslint eslint-plugin-vue @rushstack/eslint-patch @vue/eslint-config-typescript  @vue/eslint-config-prettier

根目录下新建
.eslintrc.js

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier',
  ],
  // 小程序全局变量
  globals: {
    uni: true,
    wx: true,
    WechatMiniprogram: true,
    getCurrentPages: true,
    getApp: true,
    UniApp: true,
    UniHelper: true,
    App: true,
    Page: true,
    Component: true,
    AnyObject: true,
  },
  parserOptions: {
    ecmaVersion: 'latest',
  },
  rules: {
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true,
        semi: false,
        printWidth: 120,
        trailingComma: 'all',
        endOfLine: 'auto',
      },
    ],
    'vue/multi-word-component-names': ['off'],
    'vue/no-setup-props-destructure': ['off'],
    'vue/no-deprecated-html-element-is': ['off'],
    '@typescript-eslint/no-unused-vars': ['off'],
  },
}

package.json
中新增命令
lint

{
  "scripts": {
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
  }
}

然后运行
npm run lint
,将项目内的文件格式化为
eslint
规定的格式(这个命令可随时运行,以便有新页面/插件加入时,解决代码风格的问题)

规范git提交

非必需,适合多人开发

安装
husky

安装并初始化
husky

npx husky-init

如果是首次安装,会有以下提示,输入
y
回车即可

Need to install the following packages:
husky-init@8.0.0
Ok to proceed? (y)

安装完成后,会多出
.husky
文件夹和
pre-commit
文件

安装
lint-staged

npm i -D lint-staged

安装完成后配置
package.json

{
  "script": {
    // ... 省略 ...
    "lint-staged": "lint-staged"
  },
  "lint-staged": {
    "*.{vue,ts,js}": ["eslint --fix"]
  }
}

修改
pre-commit
文件

- npm test
+ npm run lint-staged

提交规范

至此,已完成
husky
+
lint-staged
的配置。之后,每次提交代码,在提交信息前都要加入以下提交类型之一,譬如:
feat: 首页新增轮播图

提交字段 说明
feat: 增加新功能
fix: 修复问题/BUG
style: 代码风格相关无影响运行结果的
perf: 优化/性能提升
refactor: 重构
revert: 撤销修改
test: 测试相关
docs: 文档/注释
chore: 依赖更新/脚手架配置修改等
workflow: 工作流改进
ci: 持续集成
types: 类型定义文件更改
wip: 开发中

安装
uni-ui
组件库

非必需,也可使用其他组件库

uni-ui
是DCloud提供的一个跨端ui库,它是基于vue组件的、flex布局的、无dom的跨全端ui框架。
查看官方文档

安装
uni-ui
及相关插件

sass sass-loader

uni-ui
的依赖库,
@uni-helper/uni-ui-types
是类型声明文件

npm i  -D sass sass-loader
npm i @dcloudio/uni-ui
npm i -D @uni-helper/uni-ui-types

修改配置

修改
tsconfig.json
,配置类型声明文件

{
  "compilerOptions": {
    "types": ["@dcloudio/types", "miniprogram-api-typings", "@uni-helper/uni-app-types", "@uni-helper/uni-ui-types"]
  }
}

修改
src/pages.json
,配置自动导入组件

{
  "easycom": {
    "autoscan": true,
    "custom": {
      // uni-ui 规则如下配置  
      "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" 
    }
  },
  "pages": [
    // ……
  ]
}

安装配置
pina

安装

pinia-plugin-persistedstate
是持久化
pina
插件

npm i pinia pinia-plugin-persistedstate

使用


src
下新增以下目录和文件

src
├─stores
│  ├─modules
│  │ └─user.ts
|  └─index.ts

user.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'

// 定义 Store
export const useMemberStore = defineStore(
  'user',
  () => {
    // 用户信息
    const userInfo = ref<any>()

    // 保存用户信息,登录时使用
    const setUserInfo = (val: any) => {
      userInfo.value = val
    }

    // 清理用户信息,退出时使用
    const clearUserInfo = () => {
      userInfo.value = undefined
    }

    return {
      userInfo,
      setUserInfo,
      clearUserInfo,
    }
  },
  // TODO: 持久化
  {
    persist: true,
  },
)

index.ts

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

// 创建 pinia 实例
const pinia = createPinia()
// 使用持久化存储插件
pinia.use(persist)

// 默认导出,给 main.ts 使用
export default pinia

// 模块统一导出
export * from './modules/user'

main.ts

import { createSSRApp } from 'vue'
import pinia from './stores'

import App from './App.vue'
export function createApp() {
  const app = createSSRApp(App)

  app.use(pinia)
  return {
    app,
  }
}

持久化

插件默认使用
localStorage
实现持久化,小程序端不兼容,需要替换持久化 API。
修改
stores/modules/user.ts

export const useUserStore = defineStore(
  'member',
  () => {
    //…省略
  },
  {
    // 配置持久化
    persist: {
      // 调整为兼容多端的API
      storage: {
        setItem(key, value) {
          uni.setStorageSync(key, value) 
        },
        getItem(key) {
          return uni.getStorageSync(key) 
        },
      },
    },
  },
)

封装请求

uniapp
拦截器

uni.addInterceptor
的使用参考
官方文档

src
目录下新建
utils
文件夹,并新建
http.ts
文件

import { useUserStore } from '@/stores'

const baseURL = import.meta.env.VITE_HTTP

// 拦截器配置
const httpInterceptor = {
  // 拦截前触发
  invoke(options: UniApp.RequestOptions) {
    // 非 http 开头需拼接地址
    if (!options.url.startsWith('http')) {
      options.url = baseURL + options.url
    }
    options.timeout = 10000
    // 添加请求头标识
    options.header = {
      'request-client': 'wechart-app',
      ...options.header,
    }
    // 添加 token 请求头标识
    const memberStore = useUserStore()
    const token = memberStore.userInfo?.token
    if (token) {
      options.header.Authorization = token
    }
  },
}

// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)

由于
uni-app
的响应拦截器对类型支持并不友好,所以我们自行封装响应拦截器,同一个文件,继续

/**
 * 请求函数
 * @param  UniApp.RequestOptions
 * @returns Promise
 */
// Data类型根据后台返回数据去定义
type Data<T> = {
  code: string
  msg: string
  result: T
}
export const http = <T>(options: UniApp.RequestOptions) => {
  return new Promise<Data<T>>((resolve, reject) => {
    uni.request({
      ...options,
      // 响应成功
      success(res) {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data as Data<T>)
        } else if (res.statusCode === 401) {
          // 401错误  -> 清理用户信息,跳转到登录页
          const userStore = useUserStore()
          userStore.clearUserInfo()
          uni.navigateTo({ url: '/pages/login' })
          reject(res)
        } else {
          // 其他错误 -> 根据后端错误信息轻提示
          uni.showToast({
            icon: 'none',
            title: (res.data as Data<T>).msg || '请求错误',
          })
          reject(res)
        }
      },
      // 响应失败
      fail(err) {
        uni.showToast({
          icon: 'none',
          title: '网络错误,换个网络试试',
        })
        reject(err)
      },
    })
  })
}

使用

为了统一API文件,我们在 src 目录下新建
api
文件夹,并新建
user.ts

import { http } from '@/utils/http'

export const getUserInfoAPI = (data: any) => {
  return http({
    url: '/user/info',
    method: 'POST',
    data,
  })
}

然后在需要的地方调用,比如在
page/my/index.vue
里调用:

<script setup lang="ts">
import { useUserStore } from '@/stores'
import { getUserInfoAPI } from '@/api/user'

const userStore = useUserStore()

const getUserInfo = async () => {
  const res = await getUserInfoAPI({ id: 'weizwz' })
  console.log(res)
  const { result } = res
  userStore.setUserInfo(result)
}
</script>

<template>
  <view class="">
    <view>用户信息: {{ userStore.userInfo }}</view>
    <button
      @tap="
        userStore.setUserInfo({
          userName: 'weizwz',
        })
      "
      size="mini"
      plain
      type="primary"
    >
      保存用户信息
    </button>
    <button @tap="userStore.clearUserInfo()" size="mini" plain type="primary">清理用户信息</button>
    <button @tap="getUserInfo()" size="mini" plain type="primary">发送请求</button>
  </view>
</template>

<style lang="scss"></style>

效果如下,可以看到已经调用成功:
image

如果调用被拦截的话,请检查微信小程序里的项目设置,然后选中
不检验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书
选项

注意事项

开发区别

uni-app
项目每个页面是一个
.vue
文件,数据绑定及事件处理同
Vue.js
规范:

  1. vue文件中的
    div
    标签需替换为
    view
    标签
  2. 属性绑定
    src="{ { url }}"
    升级成
    :src="url"
  3. 事件绑定
    bindtap="eventName"
    升级成
    @tap="eventName"

    支持()传参
  4. 支持 Vue 常用
    指令
    v-for

    v-if

    v-show

    v-model

其他补充

  1. 调用接口能力,
    建议前缀
    wx
    替换为
    uni
    ,养成好习惯,
    支持多端开发
  2. <style>
    页面样式不需要写
    scoped
    ,小程序是多页面应用,
    页面样式自动隔离
  3. 生命周期分三部分
    :应用生命周期(小程序),页面生命周期(小程序),组件生命周期(Vue)
  4. 其他参考
    uniapp-vue语法 官方文档

文章部分内容来自
小兔鲜儿项目
,本文主要是在此基础上补全了完整创建此项目的流程和所需的依赖

最近框架中的可视化界面设计需要使用到表达式引擎(解析代码字符串并动态执行),之前旧框架的实现是将表达式字符串解析为语法树后解释执行该表达式,本文介绍如何使用Roslyn解析表达式字符串,并直接转换为Linq的表达式后编译执行。

一、语法(Syntax)与语义(Semantic)

C#的代码通过Roslyn解析为相应的语法树,并且利用语义分析可以获取语法节点所对应的符号及类型信息,这样利用这些信息可以正确的转换为Linq的表达式。这里作者就不展开了,可以参考Roslyn文档。

二、实现表达式解析器(ExpressionParser)

1. 解析字符串方法

下面开始创建一个类库工程,引用包Microsoft.CodeAnalysis.CSharp.Features,然后参照以下代码创建ExpressionParser类, 静态ParseCode()方法是解析字符串表达式的入口:

using System.Linq.Expressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ExpEngine;

public sealed class ExpressionParser : CSharpSyntaxVisitor<Expression>
{
    private ExpressionParser(SemanticModel semanticModel)
    {
        _semanticModel = semanticModel;
    }

    private readonly SemanticModel _semanticModel;

    /// <summary>
    /// 解析表达式字符串转换为Linq的表达式
    /// </summary>
    public static Expression ParseCode(string code)
    {
        var parseOptions = new CSharpParseOptions().WithLanguageVersion(LanguageVersion.CSharp11);
        var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
            .WithNullableContextOptions(NullableContextOptions.Enable);

        var tree = CSharpSyntaxTree.ParseText(code, parseOptions);
        var root = tree.GetCompilationUnitRoot();
        var compilation = CSharpCompilation.Create("Expression", options: compilationOptions)
            .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location))
            .AddSyntaxTrees(tree);
        var semanticModel = compilation.GetSemanticModel(tree);
        //检查是否存在语义错误
        var diagnostics = semanticModel.GetDiagnostics();
        var errors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error);
        if (errors > 0)
            throw new Exception("表达式存在语义错误");

        var methodDecl = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First();
        if (methodDecl.Body != null && methodDecl.Body.Statements.Count > 1)
            throw new NotImplementedException("Parse block body");

        if (methodDecl.ExpressionBody != null)
            throw new NotImplementedException("Parse expression body");

        var firstStatement = methodDecl.Body!.Statements.FirstOrDefault();
        if (firstStatement is not ReturnStatementSyntax returnNode)
            throw new Exception("表达式方法不是单行返回语句");

        var parser = new ExpressionParser(semanticModel);
        return parser.Visit(returnNode.Expression)!;
    }
}

2. 解析运行时类型的方法

因为转换过程中需要将Roslyn解析出来的类型信息转换为对应的C#运行时的类型,所以需要实现类型转换的方法:

    private readonly Dictionary<string, Type> _knownTypes = new()
    {
        { "bool", typeof(bool) },
        { "byte", typeof(byte) },
        { "sbyte", typeof(sbyte) },
        { "short", typeof(short) },
        { "ushort", typeof(ushort) },
        { "int", typeof(int) },
        { "uint", typeof(uint) },
        { "long", typeof(long) },
        { "ulong", typeof(ulong) },
        { "float", typeof(float) },
        { "double", typeof(double) },
        { "char", typeof(char) },
        { "string", typeof(string) },
        { "object", typeof(object) },
    };

    /// <summary>
    /// 根据类型字符串获取运行时类型
    /// </summary>
    private Type ResolveType(string typeName)
    {
        if (_knownTypes.TryGetValue(typeName, out var sysType))
            return sysType;

        //通过反射获取类型
        var type = Type.GetType(typeName);
        if (type == null)
            throw new Exception($"Can't find type: {typeName} ");

        return type;
    }

3. 解析各类语法节点转换为对应的Linq表达式

这里举一个简单的LiteralExpression转换的例子,其他请参考源码。需要注意的是Linq的表达式严格匹配类型签名,比如方法调用object.Equals(object a, object b), 如果参数a是int类型,需要使用Expression.Convert(int, typeof(object))转换为相应的类型。

    private Type? GetConvertedType(SyntaxNode node)
    {
        var typeInfo = _semanticModel.GetTypeInfo(node);
        Type? convertedType = null;
        if (!SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType))
            convertedType = ResolveType((INamedTypeSymbol)typeInfo.ConvertedType!);

        return convertedType;
    }

    public override Expression? VisitLiteralExpression(LiteralExpressionSyntax node)
    {
        var convertedType = GetConvertedType(node);
        var res = Expression.Constant(node.Token.Value);
        return convertedType == null ? res : Expression.Convert(res, convertedType);
    }

三、测试解析与执行表达式

现在可以创建一个单元测试项目验证一下解析字符串表达式并执行了,当然实际应用过程中应缓存解析并编译的表达式委托:

namespace UnitTests;

using static ExpEngine.ExpressionParser;

public class Tests
{
    [Test]
    public void StaticPropertyTest() => Assert.True(Run<object>("DateTime.Today") is DateTime);

    [Test]
    public void InstancePropertyTest() => Run<int>("DateTime.Today.Year");

    [Test]
    public void MethodCallTest1() => Run<DateTime>("DateTime.Today.AddDays(1 + 1)");

    [Test]
    public void MethodCallTest2() => Run<DateTime>("DateTime.Today.AddDays(DateTime.Today.Year)");

    [Test]
    public void MethodCallTest3() => Run<DateTime>("DateTime.Today.AddDays(int.Parse(\"1\"))");

    [Test]
    public void MethodCallTest4() => Assert.True(Run<bool>("Equals(new DateTime(1977,3,1), new DateTime(1977,3,1))"));

    [Test]
    public void PrefixUnaryTest() => Run<DateTime>("DateTime.Today.AddDays(-1)");

    [Test]
    public void NewTest() => Assert.True(Run<DateTime>("new DateTime(1977,3,16)") == new DateTime(1977, 3, 16));

    [Test]
    public void BinaryTest1() => Assert.True(Run<float>("3 + 2.6f") == 3 + 2.6f);

    [Test]
    public void BinaryTest2() => Assert.True(Run<bool>("3 >= 2.6f"));
}

四、 一些限制与TODO

Linq的表达式本身存在一些限制,请参考文档:
https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/

另上述代码仅示例,比如表达式输入参数等未实现,小伙伴们可以继续自行完善。

今天聊聊
SpringBoot接口:响应时间优化的9个技巧
。在实际开发中,提升接口响应速度是一件挺重要的事,特别是在面临大量用户请求的时候。好了,咱们直接切入正题。

本文,已收录于,我的技术网站
ddkk.com
,有大厂完整面经,工作技术,架构师成长之路,等经验分享

在SpringBoot应用中,接口响应时间的优化是一个永恒的话题。优化接口响应时间不仅能提高用户体验,还能提升系统的处理能力。在这篇文章里,我将和大家分享三个实用的技巧,这些技巧能有效地缩短你的SpringBoot应用接口的响应时间。

1、使用异步处理

异步处理能有效提升接口的响应速度。当接口需要执行长时间的任务时,我们可以把这部分任务异步处理,从而不阻塞主线程。

代码示例:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

    // 标记为异步方法
    @Async
    public void longRunningTask() {
        // 假设这里有一个长时间运行的任务
        System.out.println("开始执行长时间任务");
        try {
            Thread.sleep(5000);  // 模拟长时间任务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("长时间任务执行完毕");
    }
}

这个例子中,我们通过
@Async
注解,让
longRunningTask
方法异步执行。这样就不会阻塞调用它的主线程了。

2、缓存机制

使用缓存是提升响应速度的另一个重要手段。对于那些不经常变化的数据,我们可以将其缓存起来,这样就不需要每次都去数据库或者远程服务取数据了。

代码示例:

javaCopy code
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class CacheService {

    // 应用缓存
    @Cacheable("data")
    public String getCachedData(String param) {
        // 模拟从数据库或远程服务获取数据
        return "从数据库获取的数据:" + param;
    }
}

在这里,
@Cacheable("data")
表示对这个方法的返回值进行缓存,缓存的名字是
data
。这样,当参数
param
相同的时候,就会直接从缓存中获取数据,而不是每次都执行方法体。

3、数据库查询优化

优化数据库查询是减少接口响应时间的关键。合理的索引、减少查询字段、避免复杂的关联查询都是常见的优化方法。

代码示例:

import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.List;

@Service
public class DatabaseOptimizationService {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Object> optimizedQuery() {
        Query query = entityManager.createQuery("SELECT field1, field2 FROM MyTable WHERE condition");
        // 这里进行了优化的查询
        return query.getResultList();
    }
}

在这个例子中,我们只查询需要的字段(
field1

field2
),而不是查询整个表的所有字段。这样可以大大减少数据传输和处理的时间。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

这是大佬写的,
7701页的BAT大佬写的刷题笔记,让我offer拿到手软

4、使用数据压缩技术

在处理大量数据的接口中,使用数据压缩可以减少网络传输时间,从而提升响应速度。特别是在RESTful API中,可以通过压缩JSON或XML响应体来实现。

代码示例:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.zip.GZIPOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

@RestController
public class CompressionController {

    @GetMapping("/compressedData")
    public void getCompressedData(HttpServletResponse response) throws IOException {
        String data = "这是需要被压缩的大量数据...";
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
        gzipOutputStream.write(data.getBytes());
        gzipOutputStream.close();

        response.addHeader("Content-Encoding", "gzip");
        response.getOutputStream().write(byteArrayOutputStream.toByteArray());
    }
}

在这个例子中,我们通过
GZIPOutputStream
对数据进行了GZIP压缩,并在响应头中标明了内容编码方式。

5、使用WebFlux进行响应式编程

Spring 5 引入的Spring WebFlux提供了响应式编程的支持,它可以在处理大量并发请求时提高性能。

代码示例:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class WebFluxController {

    @GetMapping("/reactiveData")
    public Mono<String> getReactiveData() {
        // 异步地返回数据
        return Mono.just("响应式编程的数据");
    }
}

在这个例子中,我们使用了
Mono
来异步地返回数据。这种方式在处理大量请求时可以保持较低的资源占用。

6、优化日志记录

过多或不必要的日志记录会影响接口的响应时间。合理配置日志级别,以及在生产环境中关闭调试日志,可以提升接口性能。

代码示例:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoggingController {

    private static final Logger logger = LoggerFactory.getLogger(LoggingController.class);

    @GetMapping("/efficientLogging")
    public String getEfficientLogging() {
        // 只在必要时记录日志
        if (logger.isInfoEnabled()) {
            logger.info("高效的日志记录");
        }
        return "日志优化示例";
    }
}

在这个例子中,我们通过检查日志级别是否启用,来决定是否记录日志。这样可以避免在生产环境中生成大量的调试信息。

7、利用索引优化数据库查询

合理的数据库索引能大幅提升查询效率。特别是在处理大型数据集或高频查询时,正确的索引可以显著减少查询时间。

代码示例:

假设我们有一个用户表(User),我们经常根据用户名(username)来查询用户。为了优化这个查询,我们可以在
username
字段上创建索引。

CREATE INDEX idx_username ON User(username);

在Java代码中,我们可以这样查询:

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

这个
findByUsername
方法会受益于我们在数据库中创建的索引,从而提高查询速度。

8、使用连接池来管理数据库连接

合理配置和使用数据库连接池是提高数据库操作效率的关键。它可以减少频繁创建和销毁数据库连接的开销。

代码示例:

在SpringBoot的
application.properties
文件中,我们可以这样配置数据库连接池:

spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=myuser
spring.datasource.password=mypassword
spring.datasource.hikari.maximum-pool-size=10

这里,我们使用了HikariCP作为连接池,
maximum-pool-size
设置了池中最大的连接数。这样的配置可以确保在高并发情况下数据库连接的有效管理。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

这是大佬写的,
7701页的BAT大佬写的刷题笔记,让我offer拿到手软

9、使用Content Delivery Network (CDN) 加速静态资源加载

将静态资源(如图片、CSS和JavaScript文件)放在CDN上,可以加快这些资源的加载速度,从而间接提升接口的响应时间。

代码示例:

假设你有一个静态资源,如一张图片,你可以将它上传到CDN,然后在你的应用中这样引用:

<img src="https://your-cdn-url.com/path/to/your/image.jpg" alt="Description">

这样,当用户访问你的应用时,这张图片会从离用户最近的CDN节点加载,从而减少加载时间。

总结优化SpringBoot接口响应时间的技巧:

这些技巧的应用可以帮助你提高SpringBoot应用的性能,尤其是在处理高并发和大数据量场景时更为有效。不过记得,最好是根据具体的应用场景和需求来选择合适的优化策略。

1、使用异步处理
通过标记方法为异步,例如使用
@Async
注解,可以让长时间运行的任务在后台执行,从而不阻塞主线程。

2、缓存机制
利用
@Cacheable
等注解实现缓存,可以减少重复数据的处理和查询时间,特别是对于频繁请求的数据。

3、数据库查询优化
通过只查询必要的字段、使用合理的索引和避免复杂的关联查询,可以显著提高数据库操作的效率。

4、使用数据压缩技术
对大数据量的响应进行压缩处理,比如使用GZIP,可以减少网络传输的数据量,加快响应速度。

5、使用WebFlux进行响应式编程
通过响应式编程模型,如Spring WebFlux,可以更高效地处理并发请求,特别适合大规模的数据流操作。

6、优化日志记录
合理配置日志级别并在生产环境中关闭不必要的日志,可以减少日志记录对性能的影响。

7、利用索引优化数据库查询
正确地创建和使用数据库索引,特别是在经常查询的字段上,可以加速查询操作,提高整体性能。

8、使用连接池管理数据库连接
通过配置如HikariCP等数据库连接池,可以优化数据库连接的创建和管理,提升数据库操作的效率。

9、使用CDN加速静态资源加载
将静态资源部署到CDN上,可以加快这些资源的加载速度,减少服务器的负载,间接提升接口响应时间。

求一键三连:点赞、分享、收藏

点赞对我真的非常重要!在线求赞,加个关注我会非常感激!

本文,已收录于,我的技术网站
ddkk.com
,有大厂完整面经,工作技术,架构师成长之路,等经验分享

实验内容

  1. 设计并实现一个基本 HTTP 代理服务器。要求在指定端口(例如 8080)接收来自客户的 HTTP 请求并且根据其中的 URL 地址访问该地址 所指向的 HTTP 服务器(原服务器),接收 HTTP 服 务器的响应报文,并 将响应报文转发给对应的客户进行浏览。
  2. 设计并实现一个支持 Cache 功能的 HTTP 代理服务器。要求能缓 存原服务器响应的对象,并 能够通过修改请求报文(添加 if-modified-since 头行),向原服务器确认缓存对象是否是最新版本。 (选作内容)
  3. 扩展 HTTP 代理服务器,支持如下功能: (选作内容)
    1. 网站过滤:允许/不允许访问某些网站;
    2. 用户过滤:支持/不支持某些用户访问外部网站;
    3. 网站引导:将用户对某个网站的访问引导至一个模拟网站(钓鱼)。

代理服务器的概念

代理服务器,允许一个网络终端(一般为客户端)通过这个服务与另一 个网络终端(一般为服务器)进行非直接的连接。普通 Web 应用通信方式与采用代理服务器的 通信方式的对比如下图所示:

代理服务器在指定端口(本实验中所指定的是666端口)监听浏览器的访问请求(需要在客户端浏览器进行相应的设置),接收到浏览器对远程网站的浏览请求时,代理服务器开始在代理服务器的缓存中检索 URL 对应的对象(网页、 图像等对象),找到对象文件后,提取该对象文件的最新被修改时间;代理服务器程序在客户的请求报文首部插入,并向原 Web 服务器转发修改后的请求报文。如果代理服务器没有该对象的缓存,则会直接向原服务器转发请求报文,并将原服务器返回的响应直接转发给客户端,同时将对象缓存到代理服务器中。代理服务器程序会根据缓存的时间、大小和提取记录等对缓存进行清理。

代码结构

代码中共实现 3个类,分别为
WebsiteDetector
类、
Cache
类和
HttpProxyServer
类。

WebsiteDetector
类:

该类实现了网站过滤和网站引导功能。通过构造函数直接静态设置了钓鱼网站和屏蔽的网站:

WebsiteDetector::WebsiteDetector() 
{
	AddValidURL("http://jwc.hit.edu.cn/","http://jwts.hit.edu.cn/");
	AddBlockedURL("http://xltj.hit.edu.cn/");
}

可知,屏蔽了心理网站。将教务处网站引导到本科教学管理与服务平台。

Cache

该类在当前目录下创建文件夹
.cache/
,在其中存储浏览缓存对象。同时该类中,保存着对象与文件名的映射关系,对象和LastModified字段的映射关系。

class Cache {

public:

std::string GetDate(const std::string& url); // 获取url对应保存的LastModified字段

bool Get(const std::string& url, char* response, size_t& start, size_t& responseSize); // 读取缓存

bool Put(const std::string& url, const char* response, size_t responseSize, size_t& start); // 保存缓存

private:

​      std::string cacheDirectory_; // 存放缓存的文件目录

​      std::map<std::string, std::string> cacheMap_; // 对象和LastModified字段的映射关系

​      std::map<std::string, std::string> fileMap_; / 对象与文件名的映射关系

​      std::mutex mutex_; // 多线程同时读写文件的互斥锁

};

HttpProxyServer

该类是代理服务器的实现类。是一个多用户代理服务器。首先该类创建HTTP代理服务的TCP主套接字,该套接字监听等待客户端的连接请求。当客户端连接之后,创建一个子线程,由子线程行上述一对一的代理过程,服务结束之后子线程终止。

class HttpProxyServer

{

public:

	HttpProxyServer(int port); // 构造函数,参数为端口号

	void Start(); // 监听客户端连接请求

private:

	int serverSocket_; // 代理服务Socket

	int port_; // 端口号

	struct sockaddr_in serverAddr_; // 代理服务地址

	Cache cache_; // Cache类

	WebsiteDetector websiteDetector_; // websiteDetector类

	void HandleClient(int clientSocket); // 子线程调用函数

	std::string ExtractUrl(const std::string &httpRequest); // 解析URL

	int CreateServerSocket(const std::string &host); // 创建与原服务器连接的Socket

	bool ServerToClient(const std::string &url, int clientSocket); // 转发数据

	void ParseUrl(const std::string &url, std::string &host, std::string &path); // 解析主机名与路径名

};

程序基本流程

(1)
初始化服务器Socket,监听等待客户端的连接请求。

(2)
当客户端连接后,创建子线程处理请求。

(3)
子线程接收请求,解析
HTTP
请求的首部行和请求头。然后提取Url,Url作为参数通过websiteDetector类判断是否屏蔽或者引导。

(4)
然后进入转发的过程,首先进行域名解析,然后创建Socket先原服务器发送请求,接收响应,将数据转发到客户端。

(5)
在转发的过程中,涉及保存缓存和读取缓存。

网站引导功

利用首部行中的location字段,实现引导。

std::string locationResponse = std::string("HTTP/1.1 302 Found") + MY_CRLF + "Location: " + newUrl + MY_CRLF + MY_CRLF;

send(clientSocket, locationResponse.c_str(), locationResponse.size(), 0);

用户过滤功能

设置服务器地址信息时实现。

serverAddr_.sin_addr.s_addr = INADDR_ANY;

// serverAddr_.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //只允许本机用户访问服务器

Cache
功能

1.
代理服务器处理客户端请求时,对于第一次出现的对象,会保存下。当客户端再次请求时,代理服务器就会在请求中添加If-Modified-Since首部行。

date = cache_.GetDate(url);
std::string cacheRequest = httpRequest + "If-Modified-Since: " + date + MY_CRLF + MY_CRLF;
  1. 发送该请求后,等待原服务器响应,并判断是否回应304状态码。
if (IsResponseNotModified(responseNotModified) ) {
​      // std::cout << "304 Not Modified" << std::endl;
​      sel = false;
}else {
​      cache_.ClearFileContent(url); //清空
​      sel = true;
}
  1. sel为false时,则读取Cache转发到客户端。若为true,则发送HTTP请求到原服务器,再接收响应,转发到客户端,再保存到Cache。

修改
Chrome浏览器代理配置

--proxy-server="http://127.0.0.1:666"

VScode编译运行


该代理服务器成功在666端口启动,并输出了cache目录。

验证

验证基础的代理功能

访问今日哈工大网站:
http://today.hit.edu.cn

可以看到,网站资源顺利加载,输出栏中,输出了请求的各个资源对象的url。

验证网站引导功能

输入网址:
http://jwc.hit.edu.cn/

最后直接跳转到到了,
http://jwts.hit.edu.cn/

验证网站过滤功能

输入网址:
http://xltj.hit.edu.cn/

可以看到,无法访问。

验证用户过滤功能

验证
Cache
功能

将在Cache中的资源
http://jwts.hit.edu.cn/resources/css/common/ydy.css
,修改一下。

把色彩均改为红色,再次访问
http://jwts.hit.edu.cn/

可以看到,字体颜色变为红色。可知,HTTP代理服务器这次使用的是Cache中的资源。

源代码

点击查看代码
//g++ your_code.cpp -o your_executable -lws2_32

#include <fstream>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <thread>
#include <vector>
#include <mutex>
#include <list>
#include <map>
#include <sstream>
#include <winsock2.h>
#include <WS2tcpip.h>



#define MAX_CLIENTS 6
#define BUFSIZE 655360
#define HEADSIZE 128
#define MY_CRLF "\r\n"

class WebsiteDetector {
public:
    WebsiteDetector() 
    {
        AddValidURL("http://jwc.hit.edu.cn/", "http://jwts.hit.edu.cn/");
        AddBlockedURL("http://xltj.hit.edu.cn/");
    }
    // 钓鱼
    std::string IsURLPhishing(const std::string& url) {
        auto it = validURLs_.find(url);
        if (it != validURLs_.end()) {
            return it->second;
        } else {
            return "Phishing";
        }
    }
    // 屏蔽
    bool IsURLBlocked(const std::string& url) {
        for (const std::string& blockedURL : blockedURLs_) {
             if (url.find(blockedURL) != std::string::npos) {
                return true;
            }
        }
        return false;
    }
private:
    std::map<std::string, std::string> validURLs_;
    std::vector<std::string> blockedURLs_;

    void AddValidURL(const std::string& srcURL, const std::string& dstURL) 
    {
        validURLs_[srcURL] = dstURL;
    }

    void AddBlockedURL(const std::string& url) {
        blockedURLs_.push_back(url);
    }
};

class Cache {
public:
   
    Cache() : cacheDirectory_("H:\\cppwork\\CS-networking\\.cache") {
        std::cout << cacheDirectory_ << std::endl;
        std::system(("mkdir -p " + cacheDirectory_).c_str());
    }
    bool Check(const std::string& url) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = cacheMap_.find(url);
        if (it != cacheMap_.end()) 
        {
            return true;
        }
        return false;
    }
    // 清空文件内容
    bool ClearFileContent(const std::string& url) {
        std::lock_guard<std::mutex> lock(mutex_);

        // Generate a unique filename based on the URL
        std::string fileName = GetFileNameFromUrl(url);
        auto it = fileMap_.find(fileName);
        std::string fileTag = it->second;

        std::string filePath = cacheDirectory_ + "\\" + fileTag;
        // 打开文件并使用 std::ios::trunc 模式来清空文件内容
        std::ofstream file(filePath, std::ios::out | std::ios::trunc);
        if (!file) {
            std::cerr << "无法打开文件:" << filePath << std::endl;
            return false;
        }
        // 关闭文件
        file.close();
        return true;
    }
    std::string GetDate(const std::string& url) 
    {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = cacheMap_.find(url);
        return it->second;
    }
    bool Get(const std::string& url, char* response, size_t& start, size_t& responseSize) {
        std::lock_guard<std::mutex> lock(mutex_);

        // Generate a unique filename based on the URL
        std::string fileName = GetFileNameFromUrl(url);
        std::string fileTag = fileMap_[fileName];

        std::cout << "Get() url: " << url << std::endl;
        std::cout << "Get() fileTag: " << fileTag << std::endl;
        // If found, read the response from the file
        std::ifstream file(cacheDirectory_ + "\\" + fileTag, std::ios::binary);
        if (file) {
            file.seekg(start, std::ios::beg);
            file.read(response, BUFSIZE);
            // Get the number of bytes read in this chunk
            size_t bytesRead = static_cast<size_t>(file.gcount());
            start += bytesRead;
            responseSize = bytesRead;
            response[bytesRead] = '\0';
            file.close();
            return true;
        }
        return false; // URL not found in the cache
    }

    bool Put(const std::string& url, const char* response, size_t responseSize, size_t& start) {
        std::lock_guard<std::mutex> lock(mutex_);

        // Generate a unique filename based on the URL
        std::string fileName = GetFileNameFromUrl(url);
        auto it = fileMap_.find(fileName);
        std::string fileTag;
        if (it == fileMap_.end()) 
        {
            fileTag = std::to_string(cnt);
            fileMap_[fileName] = fileTag;
            cnt ++;
        }else 
        {
            fileTag = it->second;
        }

        // Store the response in a file
        std::ofstream file(cacheDirectory_ + "\\" + fileTag, std::ios::binary | std::ios::app );
        if (!file) {
            fprintf(stderr, "file open error: %s(errno: %d)\n", strerror(errno),errno);
            return false; // Unable to open file for writing
        }
        file.seekp(start);
        file.write(response, responseSize);
        file.close();
        start += responseSize;
        return true; // Failed to store response in the cache
    }

    void PutDate(const std::string& url)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        // Generate a unique filename based on the URL
        std::string fileName = GetFileNameFromUrl(url);
        std::string fileTag = fileMap_[fileName];
        // 拼接完整的文件路径
        std::string filePath = cacheDirectory_ + "\\" + fileTag;

        // 打开文件并读取 Last-Modified 首部内容
        std::ifstream file(filePath);
        if (!file) {
            fprintf(stderr, "file open error: %s(errno: %d)\n", strerror(errno),errno);
        }
        std::string line;
        while (std::getline(file, line)) {
            // 查找包含 Last-Modified 首部的行
            if (line.find("Last-Modified:") != std::string::npos) {
                // 提取 Last-Modified 的值并存储到 cacheMap_
                size_t startPos = line.find(":") + 2;
                size_t endPos = line.find(MY_CRLF);
                std::string date = line.substr(startPos, endPos);
                // std::cout << "line: " << line << std::endl;
                // std::cout << "date: " << date << std::endl;
                cacheMap_[url] = date;
                break; // 找到后可以退出循环
            }else
            {
                if (line == MY_CRLF)
                {   
                    break;
                }
            }
        }
        file.close();
    }

    
private:
    std::string cacheDirectory_;
    std::map<std::string, std::string> cacheMap_;
    std::map<std::string, std::string> fileMap_;
    std::mutex mutex_;
    int cnt = 1;

    std::string GetFileNameFromUrl(const std::string& url) {
        // Replace characters in the URL to create a valid filename
        std::string fileName = url;
        for (char& c : fileName) {
            if (c == '/' || c == '?' || c == '&' || c == '=') {
                c = '_';
            }
        }
        return fileName;
    }
};


// 定义HTTP代理服务器类
class HttpProxyServer
{
public:
    HttpProxyServer(int port) : port_(port)
    {
        
        // 初始化服务器
        // 创建主套接字并绑定端口
        serverSocket_ = socket(AF_INET, SOCK_STREAM, 0);
        if (serverSocket_ == -1)
        {
            fprintf(stderr, "Constructor(): create socket error: %s(errno: %d)\n", strerror(errno),errno);
            exit(EXIT_FAILURE);
        }

        // 设置服务器地址信息
        // 初始化 serverAddr_
        memset(&serverAddr_, 0, sizeof(serverAddr_));
        serverAddr_.sin_family = AF_INET;
        serverAddr_.sin_addr.s_addr = INADDR_ANY;
        // serverAddr_.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //只允许本机用户访问服务器
        serverAddr_.sin_port = htons(port_);

        // 绑定套接字到指定端口
        if (bind(serverSocket_, (struct sockaddr *)&serverAddr_, sizeof(serverAddr_)) == -1)
        {
            fprintf(stderr, "Constructor(): bind socket error: %s(errno: %d)\n",strerror(errno), errno);
            closesocket(serverSocket_);
            exit(EXIT_FAILURE);
        }

        // 开始监听客户端连接请求
        if (listen(serverSocket_, MAX_CLIENTS) == -1)
        {
            fprintf(stderr, "Constructor(): listen socket error: %s(errno: %d)\n",strerror(errno),errno);
            closesocket(serverSocket_);
            exit(EXIT_FAILURE);
        }

        std::cout << "Proxy server started on port " << port_ << std::endl;
    }

    void Start()
    {
        // 启动服务器,监听客户端连接请求
        while (true)
        {
            struct sockaddr_in clientAddr;
            int clientAddrLen = sizeof(struct sockaddr);

            // 接受客户端连接
            int clientSocket = accept(serverSocket_, (struct sockaddr *)&clientAddr, &clientAddrLen);
            if (clientSocket == INVALID_SOCKET)
            {
                fprintf(stderr, "Start(): accept socket error: %s(errno: %d)",strerror(errno),errno);
                continue; // 继续等待下一个连接
            }

            // std::cout << "Start(): Accepted a client connection" << std::endl;

            // 创建子线程处理客户端请求
            std::thread clientThread(&HttpProxyServer::HandleClient, this, clientSocket);
            clientThread.detach(); // 不等待
        }
    }

private:
    int serverSocket_;
    int port_;
    struct sockaddr_in serverAddr_;
    Cache cache_;
    WebsiteDetector websiteDetector_;

    void HandleClient(int clientSocket)
    {
        // 读取客户端的HTTP请求
        char buffer[BUFSIZE];
        memset(buffer, 0, BUFSIZE);
        ssize_t bytesRead = recv(clientSocket, buffer, BUFSIZE - 1, 0);
        if (bytesRead == -1)
        {
            perror("HandleClient(): Error reading from client socket");
            closesocket(clientSocket);
            return;
        }

        // 解析请求,提取URL
        std::string request(buffer);
        std::string url = ExtractUrl(request);

        std::cout << "<" << url << ">" << std::endl;
        
        // Website Filter; User Filter ; Website phishing
        if (websiteDetector_.IsURLBlocked(url))
        {
            std::cout << "Url Blocked Success: " << url << std::endl;
        }else
        {
            std::string newUrl = websiteDetector_.IsURLPhishing(url);
            if (newUrl == "Phishing")
            {
                // 向服务端请求,向客户端发送
                if( ServerToClient(url, clientSocket) )
                {
                    std::cout << "Transmit Success!" << std::endl;
                }else
                {
                    std::cout << "Transmit Fail!" << std::endl;
                }
            }else
            {
                std::cout << "Phishing" << std::endl;
                std::string locationResponse = std::string("HTTP/1.1 302 Found") + MY_CRLF + "Location: " + newUrl + MY_CRLF + MY_CRLF;
                std::cout << locationResponse << std::endl;
                send(clientSocket, locationResponse.c_str(), locationResponse.size(), 0);
            }
            
        }

        std::cout << "----------------------" << std::endl;
        // 关闭连接
        closesocket(clientSocket);
    }

    // 提取URL
    std::string ExtractUrl(const std::string &httpRequest)
    {
        std::string url;
        // Debug
        // std::cout << "ExtractUrl(): httpRequest = " << std::endl << httpRequest << std::endl;

        // 在HTTP请求中查找"GET ",通常URL紧随其后
        size_t getPos = httpRequest.find("GET ");
        if (getPos != std::string::npos)
        {
            // 找到"GET "后,查找下一个空格,该空格之后是URL
            size_t spacePos = httpRequest.find(' ', getPos + 4);
            if (spacePos != std::string::npos)
            {
                url = httpRequest.substr(getPos + 4, spacePos - (getPos + 4));
            }
        }
        return url;
    }
    
    void ParseUrl(const std::string &url, std::string &host, std::string &path)
    {
        // 查找 URL 中的 "http://",并获取其后的部分
        size_t httpPos = url.find("http://");
        if (httpPos != std::string::npos)
        {
            std::string urlWithoutHttp = url.substr(httpPos + 7); // 7 是 "http://" 的长度
            // 查找 "/",分隔主机名和路径
            size_t slashPos = urlWithoutHttp.find('/');
            if (slashPos != std::string::npos)
            {
                host = urlWithoutHttp.substr(0, slashPos);
                path = urlWithoutHttp.substr(slashPos);
            }
            else
            {
                // 如果没有找到 "/",则整个剩余部分都是主机名
                host = urlWithoutHttp;
                path = "/";
            }
        }
        else
        {
            // 如果没有 "http://" 前缀,则默认协议为 HTTP,整个 URL 都是主机名
            host = url;
            path = "/";
        }

        // Debug
        // std::cout << "url: " + url << std::endl;
        // std::cout << "host: " + host << std::endl;
        // std::cout << "path: " + path << std::endl;

    }
    int CreateServerSocket(const std::string &host)
    {
        // 域名解析
        addrinfo* result = NULL;
        addrinfo hints;

        ZeroMemory(&hints, sizeof(hints));
        hints.ai_family = AF_INET;  // 使用IPv4地址
        hints.ai_socktype = SOCK_STREAM;

        if (getaddrinfo(host.c_str(), "http", &hints, &result) != 0)
        {
            fprintf(stderr, "CreateServerSocket(): Failed to resolve the host: %s\n", host.c_str());
            return -1; // 返回-1表示连接失败
        }

        // 创建Socket
        int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
        if (serverSocket == -1)
        {
            fprintf(stderr, "CreateServerSocket(): create socket error: %s(errno: %d)\n", strerror(errno), errno);
            freeaddrinfo(result);  // 释放内存
            return -1; // 返回-1表示连接失败
        }

        // 设置服务器地址信息
        struct sockaddr_in serverAddr;
        memset(&serverAddr, 0, sizeof(serverAddr));
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_port = htons(80); // 设置端口号为80,可以根据需要修改
        serverAddr.sin_addr.s_addr = ((struct sockaddr_in *)(result->ai_addr))->sin_addr.s_addr;

        // 连接到原服务器
        if (connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)
        {
            fprintf(stderr, "CreateServerSocket(): connect error: %s(errno: %d)\n",strerror(errno),errno);
            closesocket(serverSocket); // 在Windows中使用closesocket关闭套接字
            freeaddrinfo(result);  // 释放内存
            return -1; // 返回-1表示连接失败
        }
        freeaddrinfo(result);  // 释放内存
        return serverSocket; // 返回连接成功的套接字描述符
    }

    bool ServerToClient(const std::string &url, int clientSocket)
    {
        // 解析URL,获取主机名和路径
        std::string host, path;
        ParseUrl(url, host, path);

        // 创建Socket连接到原服务器
        int serverSocket = CreateServerSocket(host);
        if (serverSocket == -1)
        {
            return FALSE; // 处理连接失败的情况
        }  
        // 构建HTTP请求
        std::string httpRequest = "GET " + path + " HTTP/1.1" + MY_CRLF + "Host: " + host + MY_CRLF + "Connection: close" + MY_CRLF;

        std::string date;
        bool sel;

        if (cache_.Check(url)) 
        {
            sel = false;
            date = cache_.GetDate(url);
            std::string cacheRequest = httpRequest + "If-Modified-Since: " + date + MY_CRLF + MY_CRLF;
            // 发送HTTP, 带有If-Modified-Since 首部行
            if (send(serverSocket, cacheRequest.c_str(), cacheRequest.size(), 0) == -1)
            {
                perror("Error sending request to server");
                closesocket(serverSocket);
                return FALSE;
            }
            std::string cacheResponse;
            char cacheBuffer[HEADSIZE];
            ssize_t cacheBytesRead;
            cacheBytesRead = recv(serverSocket, cacheBuffer, HEADSIZE - 1, 0);
            std::string responseNotModified(cacheBuffer);

    
            // std::cout << "responseNotModified: " << responseNotModified << std::endl;

            if (IsResponseNotModified(responseNotModified) ) 
            {
                // std::cout << "304 Not Modified" << std::endl;
                sel = false;
            }else
            {
                cache_.ClearFileContent(url); //清空
                sel = true;
            }

        }else
        {
            sel = true;
        }
    
        if (sel == false) 
        {

            // std::cout << "cache hit!" << std::endl;
            // 接收缓存,转发到客户端
            char buffer[BUFSIZE];
            size_t start = 0;
            size_t bytesRead;
            while (1)
            {
                if (cache_.Get(url, buffer, start, bytesRead) == false)
                {
                    perror("Error sending response to client");
                }

                // std::cout << "bytesRead: " << bytesRead << std::endl;
                if (bytesRead == 0) break;

                if (send(clientSocket, buffer, bytesRead, 0) == -1)
                {
                    perror("Error sending response to client");
                    closesocket(serverSocket);
                    return FALSE;
                }
            }
           
        }else
        {
            httpRequest += MY_CRLF;
            // 发送HTTP请求到原服务器
            if (send(serverSocket, httpRequest.c_str(), httpRequest.size(), 0) == -1)
            {
                perror("Error sending request to server");
                closesocket(serverSocket);
                return FALSE;
            }

            // 接收原服务器的HTTP响应
            char buffer[BUFSIZE];
            size_t start = 0;
            ssize_t bytesRead;
            while ((bytesRead = recv(serverSocket, buffer, BUFSIZE - 1, 0)) > 0)
            {
                buffer[bytesRead] = '\0';
                // 发送接收到的数据到客户端
                if (send(clientSocket, buffer, bytesRead, 0) == -1)
                {
                    perror("Error sending response to client");
                    closesocket(serverSocket);
                    return FALSE;
                }
                if(cache_.Put(url, buffer, bytesRead, start) == false)
                {
                    std::cerr << "Cache put error" << std::endl;
                }
            }
            cache_.PutDate(url);
            if (! cache_.Check(url))
            {
                cache_.ClearFileContent(url);
            }
        }
        // 关闭原服务器连接
        closesocket(serverSocket);
        return TRUE;
    }
    bool IsResponseNotModified(const std::string& response) {
       // 查找第一个空格,定位到状态码的开始
        size_t spacePos = response.find(' ');
        if (spacePos != std::string::npos) {
            // 提取状态码部分
            std::string statusCode = response.substr(spacePos + 1, 3);
            // 检查状态码是否为 "304"
            return (statusCode == "304"); // HTTP/1.1 304 Not Modified
        }
        return false; // 未找到状态码
    }
};



bool InitWinsock()
{
    // 加载套接字库(必须)
    WORD wVersionRequested;
    WSADATA wsaData;
    // 套接字加载时错误提示
    int err;
    // 版本 2.2
    wVersionRequested = MAKEWORD(2, 2);
    // 加载 dll 文件 Scoket 库
    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0)
    {
        // 找不到 winsock.dll
        printf("加载 winsock 失败,错误代码为: %d\n", WSAGetLastError());
        return FALSE;
    }
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
    {
        printf("不能找到正确的 winsock 版本\n");
        return FALSE;
    }

    return TRUE;
}

int main()
{
    if (!InitWinsock())
    {
        WSACleanup();
        return -1; // 初始化失败,退出程序
    }
    int port = 666; // 设置端口
    HttpProxyServer proxyServer(port);
    proxyServer.Start();
    WSACleanup(); // 在程序结束时清理Winsock库
    return 0;
}

你好呀,我是歪歪。

前两天在一个技术群里看到有人抛出一张图片,提出了这样的一个问题:

请教一下,线程池可以做到根据任务的类型,来指定特定线程执行吗?

了解了一下背景,是批量任务触发,从订单表中查询出“处理中”状态的订单,订单可能属于不同的通道,所以需要调用不同通道的接口。

现在的方案是把订单查出来之后,往线程池里面扔,在异步任务里面判断当前订单是属于哪个通道,就调用哪个通道的查询接口:

这是常规做法,看起来没有毛病。

但是现在提问的这个哥们遇到了一个问题:有一个通道的查询接口特别慢,会占着线程池里面的线程资源,影响了其他两个通道的订单查询。

举个极端的例子,比如你的线程池核心线程数就三个。

假设一共有 5 笔数据,前 3 笔是通道 A 的,后面两笔分别是通道 B 和通道 C 的。

结果现在通道 A 出问题了:

直接把你的核心线程都占满了,剩下的两笔对应 B 和 C 通道的数据就在队列里面排着队,等着。

你说这个合不合理?

非常不合理,对不对?

但是这个问题确实也是很常规,常规到它甚至没有资格作为一个场景面试题出现在面试环节中。

问题在于不同的通道在共用同一个线程池,从而导致的相互影响。所以解决思路主要就是怎么把资源隔离开来。

一般来说,大家能想到的第一个解决方案就是用 MQ 嘛:

利用不同的队列,天然就把不同通道的订单给区分开了,在监听侧各自处理各自通道的数据,这样就达到了资源隔离的效果。

这个方案应该是很常规了,但是这个常规方案立马就被毙了。

因为:

需要注意的是,他这里说的“系统内部”是指同一个微服务,也就是不允许一个微服务使用 MQ 来做“自产自销”。

我个人认为是“自产自销”没有任何问题的,在这个场景下我完全可以借助它的特性帮我做数据分隔、异步处理数据,而且代码简单,逻辑清晰。

但是既然是公司规定,可能有一些因地制宜的考虑,我们也不好去做过多的批判。

反正就是 MQ 可以解决这个问题,但是老板并不采取这个方案。

没关系,小脑壳一转,大多数同学就能立马就掏出了另外一个解决方案。

你前面出问题的原因不是因为不同的通道在共用同一个线程池吗?

那很简单,每个通道各自搞一个线程池。然后和 MQ 的方案类似,根据不同的通道扔到对应的线程池中去,自己玩自己的:

这样即使某个通道出问题了,由于在线程池层面做了线程资源隔离,所以也不影响另外的通道进行数据处理。

这个就是线程池隔离的方案。

其实关于这个方案,我当时还想到了另外一种原理一致,实现形式不一样,但是最终被认为是比较 low 的一个回答。

因为他抛出的这个图片,我第一眼理解错了,我以为是按照通道分组,然后用单线程一个个的去调用查询接口,避免并发调用:

所以我提到了一个叫做 KeyAffinityExecutor 的魔改线程池:

这个线程池,它有一个比较厉害的特性,可以确保投递进来的任务按某个维度划分出任务,然后按照任务提交的顺序依次执行。这个线程池可以通过并行处理(多个线程)来提高吞吐量、又能保证一定范围内的任务按照严格的先后顺序来运行。

对比到当前的这个问题中。

可以按照通道维度进行任务划分,然后把任务往线程池扔的时候,就会被分配到不同的线程中去。

关于这个线程池,我之前写了这篇文章,有兴趣的可以去了解一下,不赘述了:
《看到一个魔改线程池,面试素材加一!》

本质上还是线程池隔离的思路,只不过一个是分多个不同的业务线程池,线程池和业务绑定。一个是一个大线程池里面包了多个线程池,线程池可以通过分配规则的方式指定。

同一个思路的不同实现方案而已。

但是为什么我说我提出的这个魔改线程池的方案 low 呢?

因为人家只是需要分组的特性,而不需要“按照任务提交的顺序依次执行”的特性。

反而会出现如果一个通道的订单多,只有一个线程来处理,导致性能不够,任务堆积的情况。

但是,话说回来,你也可以魔改一下这个魔改线程池,把里面的小线程池的核心线程数搞多点,就行了。

总之,都是线程池隔离的思路。

好了,这个方案我又讲完了,谁赞成,谁反对?

看着没有任何问题,但是实际情况是:

卧槽,50 多个?

确实,如果是只有三个通道,或者多说点,五个通道嘛,我觉得用上面这个方案做线程池维度的隔离,都是可以接受的。

但实际情况是 50 多个通道,一想起项目里面有 50 多个线程池在跑,这个就有点难受了。

好了,现在 MQ 和线程池隔离的方案都被否决了,接下来的思路是什么?

没有思路没有关系,我们再来读读题:批量任务触发,从订单表中查询出“处理中”状态的订单,订单可能属于不同的通道,所以需要调用不同通道的接口。但是某个通道慢,导致影响了其他通道订单的查询。

问怎么办?

某个通道慢,该怎么办?

有的通道慢,有的通道快,我该怎么办?

等等...

前面我们按照通道维度分线程池被否了的原因是通道太多了。

但是其实针对响应快的通道,我们完全不需要做线程池隔离,他们完全可以使用同一个线程池嘛,反正都是唰唰唰的就查回来了。

所以,我们只需要搞两个线程池,一个处理通道响应快的,比如把接口调用的超时时间设置为 1s。另外一个处理通道响应慢的,超时时间直接拉满到 30s,自己慢慢玩去:

至于怎么去判断通道到底是快是慢呢?

这里又可以大致分为三个不同的方案了。

第一个方案就是已知某几个通道是慢的,那就代码里面写硬编码都行。虽然不优雅,但是这确实也是一个在实际生产中常常被提及的一个快速解决问题的方案。

第二个方案就是配置化,可以做个配置表,来配置通道的快慢标识。程序里面根据当前订单的通道,来表里面获取当前通道的快慢标识,从而把订单扔到不同的线程池中去。

在这个方案中,用配置表代替了硬编码,但是还是需要人工基于线下沟通或者数据监控的方式去调整通道的快慢标识。

你知道的,线上程序这玩意,一旦涉及到人工介入,就遭老罪了,很不爽。

所以这个方案,有一点优雅,但是不多。

第三个方案就是配置化加自动化这一套组合拳。

配置化还是指前面提到的配置表。

但是这个表中通道的快慢标识,就不需要人工来介入了,完全由程序自己收集信息,进行判断。

比如,我们可以假设一开始的时候所有的通道都能快速响应。但是突然某个通道开始“扯拐”,响应时长出现波动,1s 内没有响应成功,那么这个任务就会超时,就可以把这个任务扔到慢通道线程池中去处理,同时对该通道的失败次数进行记录。

当某个时间段失败次数超过某个阈值之后,则在配置表中标识该通道为慢通道。

这样当下一个属于该通道的订单过来时,就会直接被扔到慢通道线程池中去。

这样,就由程序完成了通道由“快标识”到“慢标识”的处理。

那么当这个通道的问题解决之后,它又变成一个快通道时,怎么去修改它在配置表中的标识呢?

很简单,同样的逻辑,在慢通道线程池处理的过程中,记录某个时间段某个通道的平均响应时长,如果低于指定阈值,比如 1s,则在配置表中重新标识该通道为快通道。

整个过程,不管标识怎么变化,都是基于程序自动的数据统计来的,完全不需要人工介入。

甚至你还可以加一个逻辑:当配置表中的通道都是快通道时,两个线程池都可以用起来,实现资源利用的最大化。

优雅,非常优雅。

至于怎么去统计线程池中的任务“某个时间段失败次数”和“某个时间段某个通道的平均响应时长”这样的统计信息,在线程池里面,专门留了这两个方法给你去在任务执行之前和之后搞事情,完全可以基于这两个方法做一些统计工作:

java.util.concurrent.ThreadPoolExecutor#runWorker

就目前提出的方案来说,把通道分为快慢通道,然后划分为线程池是最满足提问者的需求的。

最后应该就拿着这个方案去汇报了。

汇报题目我都帮忙想好了:

《基于通道关键指标收集分析的全自适应、高敏感度、资源利用最大化的调度方案汇报》

剩下的,就看你怎么去吹了。

除去前面的方案外,其实我还想到一个“比较奇葩”的解决方案。

因为他的业务场景是定时任务嘛,所以我想起了之前写过的这篇文章:
《又被夺命连环问了!从一道关于定时任务的面试题说起。》

既然能区分出来通道的快慢,那么在定时任务启动之后,我们就可以把“快慢标识”传递到服务器中去,服务器就能把订单分为快慢两大类,然后一台机器处理通道慢的订单数据,一台处理快的:

这样我就能从服务器这个物理层面就把数据区分开了。

所以只要能标识开区分数据,那么理论上不仅可以在代码中区分,也可以往上抽离一层,通过服务器维度区分。

但是好处是什么呢?

呃...

看起来确实没什么好处,只是这个方案比较奇葩,一般没人想到,我就是顺便提一嘴,主要是显摆一下。

不显摆一下,装装逼,总感觉不得劲。


类似的场景

基于提问者的这个问题,歪师傅也想起了两个类似的场景。

一个是我参与开发过的一个对客发送短信的消息系统,简化一下整个流程大概是这样的:

上面这个图片会出现什么问题呢?

就是消息堆积。

当某个业务系统调用短信发送接口,批量发送消息的时候,比如发送营销活动时,大量的消息就在队列里面堆着,慢慢消费。

其实堆积也没有关系,毕竟营销活动的实时性要求不是那么高,不要求立马发送到客户手机上去。

但是,如果在消息堆积起来之后,突然有用户申请了验证码短信呢?

需要把前面堆积的消费完成后,才会发送验证码短信,这个已经来不及了,甚至验证码已经过期很久了你才发过去。

客户肯定会骂娘,因为获取不到验证码,他就不能进行后续业务。

如果大量客户因为收不到验证码不能进行后续业务,引起群体性的客诉,甚至用户恐慌,这个对于企业来说是一个非常严重的事件。

怎么办呢?

解决方案非常简单,再搞一个“高速”队列出来:

验证码消息直接扔到“高速”队列中去,这个队列专门用来处理验证码、动账通知这一类时效性要求极高的消息,从业务场景上分析,也不会出现消息堆积。

不是特别复杂的方案,大道至简,问题得到了解决。

类比到前面说的“快慢”线程池,其实是一样的思想,都是从资源上进行隔离。

只不过我说的这个场景更加简单,不需要去收集信息进行动态判断。业务流程上天然的就能区分出来,哪些消息实时性比较高,应该走“高速”队列;哪些消息慢慢 发没关系,可以应该走“常规”队列。

而这个所谓的“高速”和“常规”,只是开发人员给一个普通队列赋予的一个属性而已,站在 MQ 的角度,这两个队列没有任何区别。

另外一个场景是我想起了之前写过的这篇文章:
《我试图给你分享一种自适应的负载均衡。》

我们还是先看看前面出现的这个图:

图中的线程池,不管是快的还是慢的,本质上他们处理的请求都是一样的,即拿着订单去对应的通道查询订单结果。

那我们是不是可以把这两个线程池抽象一下,理解为部署了同一个服务的两个不同的服务器,一个服务器的性能好,一个服务器的性能差。

现在有一个请求过来了,理论上这两个服务器都能处理这个请求,所以我们通过某个逻辑选一个服务器出来,把请求发过去。

这个“某个逻辑”不就是我们常说的负载均衡算法吗?

负载均衡算法的算法有很多:

其中这几个都是需要统计服务端的相关数据,基于数据进行分析,最终觉得把当前请求发个哪个服务器:

这个逻辑,和我们前面提到的这句话,其实是一脉相承的,都是信息收集、指标分析、阈值设定:

去统计线程池中的任务“某个时间段失败次数”和“某个时间段某个通道的平均响应时长”这样的统计信息

你想想我们最开始的问题是“一个通道慢了,影响了其他通道的数据,怎么办?”

现在我带着你扯到了“负载均衡策略”。

这两个场景不能说八竿子打不着吧,但是它们确实在一定程度上有相似性,转好几个弯之后,也能联系到一起。

你要是再发散一点,你甚至能想到 Serverless 的弹性场景,通过收集 CPU、Mem 指标、QPS、RT、TCP 连接数等指标,进行综合判断,弹性扩容,也无需人工介入,手动扩容。

所以,朋友,这个事情告诉我们一个什么道理?

向上抽象问题的能力,把看看似不一样的场景抽离成类似的问题模型的能力很重要。

还有,“一个通道慢需要进行资源隔离”这个问题的关键不在于“一个通道”上,虽然可以在通道层面做隔离,但是这样并没有抓住问题的关键。问题的关键在于“通道慢”,所以可以在“快慢”的维度上做隔离,这才是问题的关键。

关键问题,就是要找到问题的关键。

这也是我在这一次群聊的讨论中学习到的东西。

好啦,本文的技术部分就到这里了。

下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。


荒腔走板

我们家之前有一个花市,距离只有 3 km,基本上每个月我们都会骑自行车过去逛一圈,采购点鲜切花。由于花市规模比较大,所以整体上物美价廉,量大管够。

自从花市从去年年初的时候统一迁移到稍远一点的地方后,因为交通不便,我们就从来没有去过了,家里也很久没有买鲜花了。

2024 年的第一个周末,终于把车提了。从去年 10 月到现在,也算是等得望穿秋水了。

所以,提车后的第一天,就带着 Max 同学,也就是我老婆,以练车的名义往花市跑了一趟。

新的花市开在高架桥下,一字排开,长长的一条街,分为了好几个区。我们直奔鲜切花区,逛了一小时,花了 50 元钱,买了一束鲜花,一把腊梅,两个花瓶。

还有熟悉的物美价廉,还是熟悉的量大管够。

就是过去的路上有一段小路,虽然我已经是拿了驾照有十年的“老司机”了,但是确实没怎么开过车,手上汗水都开出来了。Max 同学坐在副驾,神情严肃,目光如炬,从头到尾也就一句话:注意注意注意,慢点慢点慢点,刹车刹车刹车...