[{"title":"WSL","url":"/2023/07/10/WSL/","content":"\n# WSL安装Ubuntu\n\n* 来源\n\n  * 直接在微软商店安装ubuntu即可\n\n* 迁移\n\n  * 由于默认安装到C盘下的`C:\\Users\\<username>\\AppData\\Local\\Packages\\CanonicalGroupLimited.Ubuntu<version>onWindows_<code>`目录下，所以最好进行迁移\n\n  * 迁移步骤：\n\n    1. 查看系统：`wsl -l -v`\n    2. 导出指定系统到指定位置：`wsl --export <Name> <目标压缩包绝对路径>`，例如`wsl --export Ubuntu d:\\ubuntu.tar`\n    3. 卸载当前版本：`wsl --unregister <Name>`\n    4. 重新导入：`wsl --import <NewName> <TargetPath> <目标压缩包位置> --version <WSL版本>`，例如`wsl --import Ubuntu F:\\Linux\\ubuntu D:\\Ubuntu.tar --version 2`\n    5. 设置默认登录用户（否则为root）：`<系统Name> config --default-user <username>`\n\n    然后可以彻底删除微软商店下载的ubuntu app\n\n* 配置apt-get的镜像：\n\n  1. 管理员身份进入：`cd /etc/apt`\n\n  2. 备份自带的源：`cp sources.list sources.list.bak`\n\n  3. 修改源配置：`vim sources.list`\n\n  4. 替换内容为阿里云的源：\n\n     ```\n     deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse\n     deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse\n     deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse\n     deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse\n     deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse\n     deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse\n     deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse\n     deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse\n     deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse\n     deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse\n     ```\n\n  5. 更新软件列表：`apt-get update`\n\n  6. 升级：`apt-get upgrade`\n\n# 配置SSH到外层主机\n\n* 安装openssh-server\n\n  1. 最好先移除先前的版本：`apt remove openssh-server`\n\n  2. 安装：`apt install openssh-server`\n\n  3. 修改配置信息：`vim /etc/ssh/sshd_config`\n\n  4. 配置相关信息：\n\n     ```sh\n     Port 2222                   # 监听的端口，可以是其它的\n     ListenAddress 0.0.0.0       # 0.0.0.0 表示所有的地址\n     PasswordAuthentication yes  # 把原来的no改成yes，意思是可以用密码登录\n     PermitRootLogin yes         # 把原来的prohibit-password改成yes\n     ```\n\n  5. 重启ssh服务：`service ssh restart`\n\n* 连接\n\n  * 在cmd或PowerShell：`ssh <子系统内用户名>@localhost -p <端口>`，例如`ssh taoyyz@localhost -p 2222`\n\n* 此时可以直接通过访问本机的2222端口ssh到WSL内的Ubuntu系统\n\n> 参考自CSDN：[SSH连接WSL2踩坑记录与增加端口转换规则，实现外网与WSL2的连接](https://blog.csdn.net/jasneik/article/details/127993390?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-3-127993390-blog-130334713.235%5Ev38%5Epc_relevant_sort_base3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-3-127993390-blog-130334713.235%5Ev38%5Epc_relevant_sort_base3&utm_relevant_index=6)\n\n# 系统服务操作\n\n> 在 **适用与 Linux 的 Windows 的子系统（WSL）** 默认情况下是无法使用 **systemctl** 命令，使用该命令 [WSL](https://so.csdn.net/so/search?q=WSL&spm=1001.2101.3001.7020) 将抛出如下错误。\n>\n> System has not been booted with systemd as init system (PID 1). Can’t operate.\n\n![image-20230703143944594](/images/WSL/image-20230703143944594.png)\n\n> 参考自CSDN：[WSL 错误 System has not been booted with systemd as init system (PID 1). Can‘t operate](https://blog.csdn.net/qq_44879989/article/details/128527355)\n\n## 切换dash与bash\n\n* 区别：\n  * dash：全称`Debian Almquist Shell`，它主要是为了执行脚本而出现，而不是交互，它速度更快，但功能相比bash要少很多，语法严格遵守POSIX标准。\n  * bash：全称`GNU Bourne-Again Shell`\n* 查看当前系统的shell解释器：\n  * `ls -l /bin/sh`\n    * 输出`/bin/sh -> dash`为dash\n    * 否则输出`/bin/sh -> /bin/bash`为bash\n* 切换解释器：\n  * 切换为bash：`sudo ln -fs /bin/bash /bin/sh`\n  * 切换为dash：`sudo ln -fs /bin/dash /bin/sh`\n\n# 性能与优化\n\n* 开启systemctl\n* 开机自启WSL\n* 设置WSL的cpu和内存（默认全核心以及一半的物理机内存）\n\n> 参考自CSDN：[使用WSL2必看的配置优化](https://blog.csdn.net/mqq2502513332/article/details/129722096)\n\n# 设置frp\n\n* 后台运行：windows下也可以使用`nohup &`包裹启动frp的命令\n* 查看运行的nohup：`jobs -l`\n* 关闭frp，也可以通过任务管理器中进程名称为`frpc.exe`\n\n## 我的当前frp配置\n\n* 服务端：`http://116.63.171.90`\n  * 通信端口：`7000`\n  * 管理台端口：`7002`，使用`http://116.63.171.90:7002`访问\n    * 用户名：`admin`\n    * 密码：`qwer2222`\n* 客户端：\n  * 已开设的端口转发：\n    * `c2222 -> s2222`：ssh\n    * `c2223 -> s99`：nginx\n    * `c9000 -> s9000`：portainer\n* 端口转发个人规则：\n  * 端口从99设置为nginx用\n    * 后端转发：\n      * 通过`远程ip:99/back_端口`即可访问指定端口的后端服务\n  * 端口从100开始：\n    * `100`：redis\n    * `101`：mysql\n    * `9000`：portainer\n      * 用户名：`admin`\n      * 密码：`3344520tjjTJJ`\n\n# 已运行的个人服务\n\n## mysql\n\n* 运行命令\n\n  ```sh\n  docker run \\\n          --name mysql_tjj \\\n          -e MYSQL_ROOT_PASSWORD=taoyyz@028hhh \\\n          -p 101:3306 \\\n          -v /home/taoyyz/software/docker/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \\\n          -v /home/taoyyz/software/docker/mysql/logs:/var/log/mysql \\\n          -v /home/taoyyz/software/docker/mysql/data:/var/lib/mysql \\\n          -e TZ=Asia/Shanghai \\\n          -d mysql:latest\n  ```\n\n## redis\n\n* 运行命令\n\n  ```sh\n  docker run  \\\n      -p 100:6379    \\\n      --name redis_tjj    \\\n      -v /home/taoyyz/software/docker/redis/redis.conf:/etc/redis/redis.conf  \\\n      -v /home/taoyyz/software/docker/redis/data:/data    \\\n      -d redis:latest \\\n      redis-server /etc/redis/redis.conf\n  ```\n\n## portainer\n\n* 运行命令\n\n  ```sh\n  docker run -d --name portainer_zh_tjj -p 9000:9000 --restart=always \\\n              -v /var/run/docker.sock:/var/run/docker.sock \\\n              -v /home/taoyyz/software/docker/portainer-zh/data:/data \\\n              6053537/portainer-ce\n  ```\n\n# Redis性能测试\n\n## redis-benchmark\n\n* 帮助\n\n```sh\n[root@redis-test-slave ~ ]$ redis-benchmark --help\nUsage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]\n\n -h <hostname>      Server hostname (default 127.0.0.1)\n -p <port>          Server port (default 6379)\n -s <socket>        Server socket (overrides host and port)\n -a <password>      Password for Redis Auth\n -c <clients>       Number of parallel connections (default 50)\n -n <requests>      Total number of requests (default 100000)\n -d <size>          Data size of SET/GET value in bytes (default 2)\n --dbnum <db>        SELECT the specified db number (default 0)\n -k <boolean>       1=keep alive 0=reconnect (default 1)\n -r <keyspacelen>   Use random keys for SET/GET/INCR, random values for SADD\n  Using this option the benchmark will expand the string __rand_int__\n  inside an argument with a 12 digits number in the specified range\n  from 0 to keyspacelen-1. The substitution changes every time a command\n  is executed. Default tests use this to hit random keys in the\n  specified range.\n -P <numreq>        Pipeline <numreq> requests. Default 1 (no pipeline).\n -e                 If server replies with errors, show them on stdout.\n                    (no more than 1 error per second is displayed)\n -q                 Quiet. Just show query/sec values\n --csv              Output in CSV format\n -l                 Loop. Run the tests forever\n -t <tests>         Only run the comma separated list of tests. The test\n                    names are the same as the ones produced as output.\n -I                 Idle mode. Just open N idle connections and wait.\n\n```\n\n* 案例\n\n```sh\nExamples:\n\n Run the benchmark with the default configuration against 127.0.0.1:6379:\n   # 运行默认配置下的测试\n   $ redis-benchmark\n\n Use 20 parallel clients, for a total of 100k requests, against 192.168.1.1:\n   # 指定并发数20,总请求数为10W,redis server主机IP为192.168.1.1\n   $ redis-benchmark -h 192.168.1.1 -p 6379 -n 100000 -c 20\n\n Fill 127.0.0.1:6379 with about 1 million keys only using the SET test:\n   # 测试SET随机数性能\n   $ redis-benchmark -t set -n 1000000 -r 100000000\n\n Benchmark 127.0.0.1:6379 for a few commands producing CSV output:\n   # 测试结果输出到csv\n   $ redis-benchmark -t ping,set,get -n 100000 --csv\n\n Benchmark a specific command line:\n   # 执行特定命令下的测试\n   $ redis-benchmark -r 10000 -n 10000 eval 'return redis.call(\"ping\")' 0\n\n Fill a list with 10000 random elements:\n   # 测试list入队的速度\n   $ redis-benchmark -r 10000 -n 10000 lpush mylist __rand_int__\n\n On user specified command lines __rand_int__ is replaced with a random integer\n with a range of values selected by the -r option.\n```\n\n* 浅玩了一下\n\n  * 本地docker内直接运行`redis-benchmark -t set -n 1000000 -r 100000000 -a taoyyz@028hhh`，大约27w/s，平均响应时间0.11ms，最大响应时间3ms\n\n    ```apl\n    Summary:\n      throughput summary: 270051.31 requests per second\n      latency summary (msec):\n              avg       min       p50       p95       p99       max\n            0.112     0.032     0.103     0.183     0.271     3.191\n    ```\n\n  * 服务器远程调用，大约1500rps，平均响应时间32ms\n\n    ```apl\n    Summary:\n      throughput summary: 1503.56 requests per second\n      latency summary (msec):\n              avg       min       p50       p95       p99       max\n           33.243    28.736    32.543    37.471    47.615   264.703\n    ```\n\n\n# 安装Xshell\n\n## 安装包\n\n* Xshell安装包已经下载到我的`E:\\破解软件\\XshellPlus - 7.rar`\n* 云盘：[XshellPlus - 7.rar](https://wwc.lanzouy.com/iD5sv052iane)\n* 安装方法：运行内部的bat脚本即可破解安装\n\n## 可能出现的问题\n\n* 由于找不到MSVCP110.dll，无法继续执行代码\n  * 原因：需要vc++的库\n  * 解决方法：下载vc++相关的库\n  * 库名称：`Visual C++ Redistributable for Visual Studio 2012 Update 4`\n  * 链接：[Surface运行库下载](https://www.microsoft.com/zh-CN/download/details.aspx?id=30679)\n* 踩坑：点击下载后，可选择x86、x64、arm的exe文件，但我只装了x64还不够，还同时需要装x86的库\n* 安装包位置：已下载到`E:\\破解软件\\vc运行时库`\n* 参考了CSDN：[xshell运行报错：由于找不到msvcr110.dll 无法继续执行](https://blog.csdn.net/xiaowu108/article/details/128170930)\n\n# Git配置\n\n## 给Git配置代理\n\n### 当前项目的仓库\n\n#### 给Git当前项目的本地仓库配置代理\n\n* HTTP代理：\n\n  ```sh\n  git config http.proxy 'http://127.0.0.1:10809'\n  git config https.proxy 'https://127.0.0.1:10809'\n  ```\n\n* SOCKS5代理：\n\n  ```sh\n  git config http.proxy 'socks5://127.0.0.1:10808'\n  git config https.proxy 'socks5://127.0.0.1:10808'\n  ```\n\n#### 取消当前项目的Git本地仓库的代理配置\n\n```sh\ngit config --unset http.proxy\ngit config --unset https.proxy\n```\n\n### 全局仓库\n\n#### 给Git全局仓库配置代理\n\n* HTTP代理：\n\n  ```sh\n  git config --global http.proxy 'http://127.0.0.1:10809'\n  git config --global https.proxy 'https://127.0.0.1:10809'\n  ```\n\n* SOCKS5代理：\n\n  ```sh\n  git config --global http.proxy 'socks5://127.0.0.1:10808'\n  git config --global https.proxy 'socks5://127.0.0.1:10808'\n  ```\n\n#### 取消全局仓库的代理配置\n\n```sh\ngit config --global --unset http.proxy\ngit config --global --unset https.proxy\n```\n\n## 查看Git配置\n\n### 查看当前Git仓库的配置\n\n```sh\ngit config --list\n```\n\n### 查看全局配置\n\n```sh\ngit config --list --global\n```\n\n# 安装Miniconda\n\n## Linux下\n\n1. [Miniconda官网](https://docs.conda.io/en/latest/miniconda.html)下载对应系统的sh脚本\n\n2. 下载完成后，把sh脚本放到任意位置，然后执行以下命令添加执行权限并执行：\n\n   ```sh\n   chmod 777 Miniconda3-latest-Linux-x86_64.sh\n   sh Miniconda3-latest-Linux-x86_64.sh\n   ```\n\n3. 一路回车，注意有些地方需要输入yes\n\n4. 安装完成后，`~`目录应该会有`.condarc`，并且`~`目录的`.bashrc`最后应该会有miniconda的路径\n\n5. 此时执行`conda -v`查看，如果命令没有找到，执行`source ~/.bashrc`使配置立即生效\n\n6. 出现`PackagesNotFoundError: The following packages are missing from the target environment`等问题，可能是源不正确，尝试：`conda config --remove-key channels`\n\n> 参考了[此链接](http://www.taodudu.cc/news/show-5865794.html?action=onClick)\n\n## windows下\n\n1. 同样的，在miniconda官网下载安装包\n\n2. 下载完成后打开安装程序，过程中取消勾选了为当前用户安装（不推荐）\n\n   * 为所有用户安装后，可能需要自己配置环境变量，在环境变量中配置如下内容：\n\n     * 一个名为`MINI_CONDA_HOME`的系统变量\n\n     * 在`Path`变量中，**分别**添加以下几条：\n\n       ```bash\n       %MINI_CONDA_HOME%\n       %MINI_CONDA_HOME%\\Library\\mingw-w64\\bin\n       %MINI_CONDA_HOME%\\Library\\usr\\bin\n       %MINI_CONDA_HOME%\\Library\\bin\n       %MINI_CONDA_HOME%\\Scripts\n       ```\n\n       > Windwos下会把Python和一些pip安装的包，映射为exe程序。所以依赖scripts文件夹\n\n   * 此时，在CMD中输入`conda`应该可以访问到`Miniconda`的程序\n\n     * 由于为所有用户安装，导致环境和包会默认下载到`C:\\Users\\当前用户\\.conda`文件夹中\n\n     * 需要修改（没有则创建）`C:\\Users\\当前用户\\.condarc`文件，并填写内容：\n\n       ```yaml\n       envs_dirs:\n         - D:\\env\\miniconda3\\envs\n       ```\n\n     * 此时仍然可能未生效，最好在创建环境时，使用绝对路径：\n\n       ```sh\n       conda create --prefix 目标环境文件夹的绝对路径 python=版本\n       # 例如：conda create --prefix D:\\env\\miniconda3\\envs\\py3.8 python=3.8\n       # 上述将创建一个3.8版本的Python环境到D盘环境的py3.8中去\n       ```\n\n## 常用操作\n\n* 列举环境：`conda env list`\n* 搜索包：`conda search 包名`\n* 创建环境：`conda create -n 环境名 python=版本号`\n* 切换环境：`conda activate 环境名`\n* 退出环境：`conda deactivate`\n* 查看pip包安装位置和其他信息：`pip show 包名`\n\n# 安装homeassistant\n\n## 环境：\n\n* 安装homeassistant2023.8.4需要Python3.11版本，使用conda创建python3.11版本环境并切换\n\n* 安装homeassistant需要的一些依赖：\n\n  * `pip3 install home-assistant-frontend`，核心依赖，缺少会导致启动一大堆报错\n\n    > 参考自[社区](https://community.home-assistant.io/t/setup-failed-for-dependencies-http/145523)\n\n  * `pip3 install pycountry`，国家地图依赖\n\n  * `pip3 install webrtcvad`，核心依赖，缺少会导致cloud、mobile-app等一堆报错\n\n    > 参考自[社区](https://community.home-assistant.io/t/after-the-update-ha-could-not-set-up-cloud-assist-pipeline-mobile-app-and-default-config/569738)\n\n  * `pip3 install homeassistant==2023.8.4`，homeassistant自身\n\n* 运行`hass`启动homeassistant服务\n\n* 访问`localhost:8123`即可\n\n# CMD操作\n\n* 切换盘符：`盘符:`，例如切换到D盘：`d:`回车即可\n* 查看当前文件夹内容：`dir`\n* 查看命令所在位置：`where 命令`","tags":["运维","服务器"],"categories":["运维"]},{"title":"Mac使用技巧","url":"/2022/08/14/Mac技巧/","content":"\n# 调度\n\n* `Ctrl + ↑`：调度中心\n  * 已修改为`Command + ↑`\n\n* `Ctrl + ←或→`：向左/右切换桌面\n  * 已修改为`Command + ←或→`\n\n* `Ctrl + ↓`：当前APP的所有窗口\n  * 已修改为`Command + ↓`\n\n* `Control + Shift + Option + ↑`：启动台\n* `Control + Shift + Option + ↓`：显示桌面\n\n# 窗口\n\n* `Command + Ctrl + F`：切换当前APP的全屏状态\n* `Command + Q`：退出当前APP\n* `Command + W`：关闭当前APP的当前窗口\n* `Command + M`：最小化当前窗口到程序坞\n* `Command + H`：无动画隐藏当前窗口，不会占用程序坞\n  * `Command + H`隐藏的窗口可以通过`Command + Tab`呼出\n  * `Command + M`隐藏的窗口不可再次呼出，需要手动点击程序坞图标\n\n* `Command + Option + H`：隐藏除当前APP外的其他窗口\n* `Command + N`：打开当前APP的一个新窗口\n* `选中文件按Space`：预览文件内容\n  * 可以按住`Space`不动预览，松开`Space`取消预览\n\n* `Command + Option + D`：显示/隐藏程序坞\n* `Command + Option + W`：关闭当前APP的所有窗口\n* `Command + Ctrl + Space`：表情和emoji窗口\n* `Command + Option + Esc`：强制停止APP窗口\n* `Command + ～` ：切换当前APP的窗口\n* `Command + Tab`：切换所有开启的APP\n* `Command + Space`：Spotlight聚焦搜索\n* `Command + ,`：打开当前APP的偏好设置\n* `按住Command键移动窗口`：被移动的窗口会保持层叠层次不会跑到屏幕最前面\n\n# 截图\n\n* `Command + Shift + 3`：全屏截图\n* `Command + Shift + 4`：区域截图\n  * 已被修改为QQ同款热键：`Control + Option + A`\n  * 区域截图后修改选区：\n    * `按住Space`：移动选区\n    * `按住Option`：修改选区\n\n* `Command + Shift + 4 + Space`：选定APP截图\n* `Command + Shift + 5`：完整的截图和录屏APP\n\n# 访达\n\n* `Command + Shift + G`：打开前往文件夹窗口，输入路径跳转\n  * 上传文件等场景时，直接把文件拖到上传窗口即可直接定位\n\n* `Command + Backspace`：删除选中文件\n* `Command + T`：打开新的标签页\n  * 适用于多个支持多标签页的APP\n\n* `Command + Shift + \\`：预览多个打开的标签页\n  * 适用于多个支持多标签页的APP\n\n* `Command + Shift + Backspace`：倾倒废纸篓\n* `Command + Shift + .`：切换隐藏文件夹的显示\n* `Option + 鼠标拖拽`：拷贝文件\n  * 在相同磁盘直接拖动文件是移动文件，不同磁盘拖动文件是复制文件\n\n* `Command + 鼠标拖拽`：移动文件\n* `Command + Option + V`：粘贴并删除源文件，相当于剪贴\n* `Command + D`：复制所选文件\n* `Command + E`：推出所选磁盘或宗卷\n* `Command + F`：搜索\n* `Command + I`：显示文件信息，相当于win的文件属性\n* `Command + `：复制所选文件\n* `Command + [`：上一级文件夹\n* `Command + ]`：下一级文件夹\n* `Command + ↑`：定位到当前文件的文件夹\n* `Command + Option + P`：在底部显示/隐藏路径\n  * 右键底部路径可以选择`在终端中打开`或`复制路径`\n\n* `按住Command拖动文件/文件夹到边栏或顶部`：固定文件/文件夹到访达边栏或顶部\n* `按住Command + Shift + 点击Dock栏应用程序`：在访达的应用程序中定位点击的APP\n\n## 快速打开窗口\n\n| 操作          | 作用                      |\n| :--------------------------------: | :---------------------------------- |\n| `Shift` + `Command` + `C`          | 打开“电脑”窗口                      |\n| `Shift` + `Command` + `D`          | 打开“桌面”文件夹                    |\n| `Shift` + `Command` + `F`          | 打开“最近使用”窗口                  |\n| `Shift` + `Command` + `G`          | 打开“前往文件夹”窗口                |\n| `Shift` + `Command` + `H`          | 打开当前 macOS 用户帐户的个人文件夹 |\n| `Shift` + `Command` + `I`          | 打开 iCloud 云盘                    |\n| `Shift` + `Command` + `K`          | 打开“网络”窗口                      |\n| `Option` + `Command` + `L`         | 打开“下载”文件夹                    |\n| `Shift` + `Command` + `N`          | 新建文件夹                          |\n| `Shift` + `Command` + `O`          | 打开“文稿”文件夹                    |\n| `Shift` + `Command` + `P`          | 在“访达”窗口中显示或隐藏预览面板    |\n| `Shift` + `Command` + `R`          | 打开“隔空投送”窗口                  |\n| `Shift` + `Command` + `T`          | 显示或隐藏“访达”窗口中的标签页栏    |\n| `Ctrl` + `Shift` + `Command` + `T` | 将所选的“访达”项目添加到“程序坞”    |\n| `Shift` + `Command` + `U`          | 打开“实用工具”文件夹                |\n| `Option` + `Command` + `D`         | 显示或隐藏“程序坞”                  |\n| `Control` + `Command` + `T`        | 将所选项添加到边栏                  |\n| `Option` + `Command` + `P`         | 隐藏或显示“访达”窗口中的路径栏      |\n| `Option` + `Command` + `S`         | 隐藏或显示“访达”窗口中的边栏        |\n| `Command` + `/`                    | 隐藏或显示“访达”窗口中的状态栏      |\n\n\n# Safari\n\n* `Command + T`：打开新标签页\n* `Command + Shift + T`：恢复上次关闭的网页\n* `Command + L`：定位光标到地址栏\n* `Command + Option + ←或→ `：切换上/下标签页\n* `Command + ↑`：到页面顶部\n* `Command + ↓`：到页面底部\n\n# 终端\n\n* `open .`：用访达打开当前路径\n\n# 输入\n\n* `Control + T`：交换光标前两个字符的顺序\n\n# 触发角\n\n* 在`设置` - `桌面与程序坞` - `触发角`设置鼠标移动到屏幕四角时触发的操作\n  * 在设置四个角的操作时可以按住修饰键防止误触\n\n# 系统\n\n* 锁屏但不睡眠\n  * `Control + Command + Q`：锁屏但不进入睡眠\n    * 此快捷键默认被QQ的打开联系人占用\n  * 直接按MacBook的锁屏按钮\n* `按住Option键点击状态栏的Wi-Fi图标`：查看Wi-Fi连接的详细信息\n* 聚焦搜索结果在访达中打开：对搜索的结果按`Command + Enter`\n* `按住Option键点击状态栏的时间`：快速开启勿扰模式","tags":["技能","运维"],"categories":["技能"]},{"title":"SpringCloud微服务基础","url":"/2021/10/14/SpringCloud微服务/","content":"\n<font size=7 face=\"微软雅黑\">SpringCloud</font>\n\n# 导学\n\n**微服务技术栈**：\n\n![image-20210922233946164](/images/image-20210922233946164.png)\n\n![image-20210922234059562](/images/image-20210922234059562.png)\n\n![image-20210922234411742](/images/image-20210922234411742.png)\n\n# 概念\n\n## 架构\n\n* **单体架构**：将业务的所有功能集中在一个项目中开发，打成一个包部署\n  * 优点：\n    * 架构简单\n    * 部署成本低\n  * 缺点：\n    * 耦合度高\n* **分布式架构**：根据业务功能对系统进行拆分，每个业务模块作为独立项目开发，称为一个***服务***\n  * 优点：\n    * 降低耦合度\n    * 有利于服务升级拓展\n  * 缺点：\n    * 要考虑***服务治理***\n      1. 服务拆分粒度如何？\n      2. 服务集群地址如何维护？\n      3. 服务之间如何实现远程调用？\n      4. 服务健康状态如何感知？\n    * 架构非常复杂：运维、监控、部署难度提高\n\n## 微服务：\n\n> 微服务是一种经过良好架构设计的分布式架构方案\n\n* 微服务架构的特征：<img src=\"/images/image-20210923001208086.png\" alt=\"image-20210923001208086\" style=\"zoom:50%;\" />\n  * 单一职责：微服务拆分粒度更小，每一个服务都对应唯一的业务能力，做到单一职责，避免重复业务开发\n  * 面向服务：微服务对外暴露业务接口\n  * 自治：团队独立、技术独立、数据独立、部署独立\n  * 隔离性强：服务调用做好隔离、容错、降级，避免出现级联问题\n\n## 微服务结构\n\n> 微服务这种方案需要技术框架来落地，国内最知名的就是SpringCloud和阿里巴巴的Dubbo\n\n|                |        Dubbo        |       SpringCloud        |    SpringCloudAlibaba    |\n| :------------: | :-----------------: | :----------------------: | :----------------------: |\n|    注册中心    |  zookeeper、Redis   |      Eureka、Consul      |      Nacos、Eureka       |\n|  服务远程调用  |      Dubbo协议      |    Feign（http协议）     |       Dubbo、Feign       |\n|    配置中心    |         无          |    SpringCloudConfig     | SpringCloudConfig、Nacos |\n|    服务网关    |         无          | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul |\n| 服务监控和保护 | dubbo-admin，功能弱 |         Hystrix          |         Sentinel         |\n\n## SpringCloud\n\n* SpringCloud是目前国内使用最广泛的微服务框架\n* SpringCloud继承了各种微服务功能组件，并基于SpringBoot实现了这些组件的自动装配，从而提供了良好的开箱即用体验\n\n<img src=\"/images/image-20210923002805853.png\" alt=\"image-20210923002805853\" style=\"zoom: 50%;\" />\n\n* SpringCloud与SpringBoot的版本兼容关系：（本教程使用Hoxton.SR10版本）\n\n<img src=\"/images/image-20210923003141626.png\" alt=\"image-20210923003141626\" style=\"zoom:50%;\" /> \n\n## 服务拆分\n\n* 注意事项：\n  1. 不同微服务，不要重复开发相同的业务\n  2. 微服务数据独立，不要访问其他微服务的数据库\n  3. 微服务可以将自己的业务暴露为接口，供其他微服务调用\n\n## 微服务远程调用\n\n1. 注册RestTemplate为Bean\n\n   ```java\n   @Bean //随意在配置类中注册Bean，建议在SpringBoot启动类\n   public RestTemplate restTemplate() {\n       return new RestTemplate();\n   }\n   ```\n\n2. 服务远程调用RestTemplate\n\n   ```java\n   @Service\n   public class OrderService {\n       @Autowired\n       private OrderMapper orderMapper;\n       @Autowired\n       private RestTemplate restTemplate; //注入此Bean\n       public Order queryOrderById(Long orderId) {\n           // 1.查询订单\n           Order order = orderMapper.findById(orderId);\n   //        2.根据order对象的userId查询对应的用户信息，把userId发送到对应的url查询并封装\n           Long userId = order.getUserId();\n           User user = restTemplate.getForObject(\"http://localhost:8081/user/\" + userId, User.class);\n   //        3.封装数据\n           order.setUser(user);\n           // 4.返回\n           return order;\n       }\n   }\n   ```\n\n**调用方式：**\n\n* 基于RestTemplate发起http请求实现远程调用\n* http请求做远程调用是*与语言无关*的调用，只需要知道对方的url（ip、端口、接口路径、请求参数）即可\n\n# Eureka注册中心\n\n## 提供者与消费者\n\n* 服务提供者：一次业务中，被其他微服务调用的服务\n* 服务消费者：一次业务中，调用其他微服务的服务\n\n提供者与消费者角色是相对的，一个服务即可以是提供者，又可以是消费者\n\n## Eureka的作用\n\n![image-20210923133841456](/images/image-20210923133841456.png)\n\n## 服务调用出现的问题\n\n* 消费者如何获取提供者的具体信息？\n  * 服务提供者启动时向Eureka注册自己的信息\n  * Eureka保存这些信息\n  * 消费者根据服务名称向Eureka拉取提供者信息\n* 如果有多个服务提供者，消费者该如何选择？\n  * 服务消费者利用负载均衡算法，从服务列表中挑选一个\n* 消费者如何感知服务提供者健康状态？\n  * 服务提供者会每隔30秒向EurekaServer发送心跳请求，报告健康状态\n  * Eureka会更新记录服务列表信息，心跳不正常的会被剔除\n  * 消费者可以拉取到最新的信息![image-20210923134248868](/images/image-20210923134248868.png)\n\n\n\n## 快速入门\n\n### 1.搭建EurekaServer服务\n\n1. 创建项目，引入`spring-cloud-starter-netflix-eureka-server`的依赖\n\n   ```xml\n   <dependencies>\n       <dependency>\n           <groupId>org.springframework.cloud</groupId>\n           <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>\n       </dependency>\n   </dependencies>\n   ```\n\n2. 编写启动类，添加`@EnabledEurekaServer`注解\n\n   ```java\n   @EnableEurekaServer\n   @SpringBootApplication\n   public class EurekaApplication {\n       public static void main(String[] args) {\n           SpringApplication.run(EurekaApplication.class, args);\n       }\n   }\n   ```\n\n3. 在SpringBoot核心配置文件配置Eureka服务端\n\n   ```yaml\n   server:\n     port: 8082 # Eureka的SpringBoot服务端口\n   spring:\n     application:\n       name: eurekaserver # Eureka的服务名称\n   eureka:\n     client:\n       service-url:\n         defaultZone: http://localhost:8082/eureka # Eureka的地址信息\n       fetch-registry: false # 关闭从Eureka服务器获取信息\n   ```\n\n### 2.注册service到EurekaServer（服务注册）\n\n1. 在需要注册为Eureka客户端的项目中引入`spring-cloud-starter-netflix-eureka-client`的依赖\n\n   ```xml\n   <dependencies>\n       <dependency>\n           <groupId>org.springframework.cloud</groupId>\n           <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>\n       </dependency>\n   </dependencies>\n   ```\n\n2. 在SpringBoot核心配置文件中配置Eureka客户端\n\n   ```yaml\n   spring:\n     application:\n       name: orderservice\n   eureka:\n     client:\n       service-url:\n         defaultZone: http://localhost:8082/eureka\n   ```\n\n### 3.拉取service从EurekaServer（服务发现）\n\n> 服务拉取是基于服务名称获取服务列表，然后再对服务列表做负载均衡\n\n1. 修改service中要访问的url路径，用服务名代替ip、端口\n\n   ```java\n   User user = restTemplate.\n       getForObject(\"http://userservice/user/\" + userId, User.class);\n   ```\n\n2. 在service的启动类中产生@RestTemplate的Bean上打上@LoadBalanced负载均衡注解\n\n   ```java\n   @Bean\n   @LoadBalanced\n   public RestTemplate restTemplate() {\n       return new RestTemplate();\n   }\n   ```\n\n# Ribbon负载均衡\n\n## 负载均衡原理\n\n![image-20210923164855406](/images/image-20210923164855406.png)\n\n![image-20210923170719968](/images/image-20210923170719968.png)\n\n## 负载均衡策略\n\n![image-20210923170909592](/images/image-20210923170909592.png)\n\n**默认实现是ZoneAvoidanceRule，是一种轮询方案**\n\n**通过定义IRule实现可以修改负载均衡规则，有两种方式：**\n\n* 代码方式：在service启动类中定义一个@Bean方法返回一个IRule实现类\n\n  ```java\n  @Bean\n  public IRule randomRule() {\n      return new RandomRule();\n  }\n  ```\n\n* 配置文件方式：在service所在的SpringBoot核心配置文件中配置规则\n\n  ```yaml\n  userserivce:\n    ribbon:\n      NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡的规则\n  ```\n\n## 懒加载和饥饿加载\n\n> Ribbon默认采用懒加载，即第一次访问时才会创建LoadBalanceClient，请求时间会比较长\n>\n> 而饥饿加载则会在项目启动时创建，降低第一次访问的耗时\n\n开启饥饿加载：\n\n```yaml\nribbon:\n  eager-load:\n    enabled: true #开启饥饿加载\n    clients: #userservice #指定对userservice这个服务进行饥饿加载\n      - userservice\n      #- 其他service ，因为clients是一个List<String>\n```\n\n![image-20210923223212010](/images/image-20210923223212010.png)\n\n# Nacos注册中心\n\n> Nacos是阿里巴巴的产品，现在是SpringCloud的一个组件。相比Eureka功能更丰富\n\n## 快速入门\n\n### 1.安装Nacos\n\n* windows环境：\n\n  1. 解压Nacos的zip包\n\n  2. 启动Nacos安装目录下的bin目录下的startup.cmd\n\n     ```shell\n     startup.cmd -m standalone\n     ```\n\n  3. 访问Nacos管理页面：默认用户名密码均为`nacos`<img src=\"/images/image-20210923225135114.png\" alt=\"image-20210923225135114\" style=\"zoom: 80%;\" />\n  \n* docker环境\n\n  1. 拉取nacos server的镜像\n\n  2. 创建配置文件\n\n     ```sh\n     mkdir /home/nacos/init.d\n     mkdir /home/nacos/logs\n     vim /home/nacos/init.d/custom.properties\n     ```\n\n     配置文件内容：\n\n     ```properties\n     server.contextPath=/nacos\n     server.servlet.contextPath=/nacos\n     server.port=8848\n     \n     spring.datasource.platform=mysql\n     \n     db.num=1\n     db.url.0=jdbc:mysql://120.79.141.53:3307/nacos?#characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true\n     db.user=root\n     db.password=123\n     \n     nacos.cmdb.dumpTaskInterval=3600\n     nacos.cmdb.eventTaskInterval=10\n     nacos.cmdb.labelTaskInterval=300\n     nacos.cmdb.loadDataAtStart=false\n     \n     management.metrics.export.elastic.enabled=false\n     management.metrics.export.influx.enabled=false\n     \n     server.tomcat.accesslog.enabled=true\n     server.tomcat.accesslog.pattern=%h %l %u %t \"%r\" %s %b %D %{User-Agent}i\n     \n     nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/**\n     nacos.naming.distro.taskDispatchThreadCount=1\n     nacos.naming.distro.taskDispatchPeriod=200\n     nacos.naming.distro.batchSyncKeyCount=1000\n     nacos.naming.distro.initDataRatio=0.9\n     nacos.naming.distro.syncRetryDelay=5000\n     nacos.naming.data.warmup=true\n     nacos.naming.expireInstance=true\n     ```\n\n  3. 启动容器\n\n     ```sh\n     docker run \\\n         --name nacos -d \\\n         -p 8848:8848 \\\n         --privileged=true \\\n         --restart=always \\\n         -e JVM_XMS=128m \\\n         -e JVM_XMX=128m \\\n         -e MODE=standalone \\\n         -e PREFER_HOST_MODE=hostname \\\n         -v /home/nacos/logs:/home/nacos/logs \\\n         -v /home/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties \\\n         nacos/nacos-server\n     ```\n\n### 2.服务注册与发现\n\n1. 在父工程添加spring-cloud-alibaba的管理依赖\n\n   ```xml\n   <dependencyManagement>\n   \t<dependency>\n           <groupId>com.alibaba.cloud</groupId>\n           <artifactId>spring-cloud-alibaba-dependencies</artifactId>\n           <version>2.2.5.RELEASE</version>\n           <type>pom</type>\n           <scope>import</scope>\n   \t</dependency>\n   </dependencyManagement>\n   ```\n\n2. 去掉service的Eureka依赖\n\n3. 添加Nacos的客户端依赖\n\n   ```xml\n   <dependencies>\n       <!--Eureka客户端依赖-->\n       <!--<dependency>\n           <groupId>org.springframework.cloud</groupId>\n           <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>\n       </dependency>-->\n       <!--nacos依赖-->\n       <dependency>\n           <groupId>com.alibaba.cloud</groupId>\n           <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n       </dependency>\n   </dependencies>\n   ```\n\n4. 在service的SpringBoot核心配置文件中配置nacos的地址\n\n   ```yaml\n   spring:\n     cloud:\n       nacos:\n         server-addr: localhost:8848 #nacos的服务地址,默认\n   ```\n\n![image-20210923231151320](/images/image-20210923231151320.png)\n\n### 3.服务集群属性\n\n1. 修改service的SpringBoot核心配置文件\n\n   ```yaml\n   spring:\n     cloud:\n       nacos:\n         discovery:\n           cluster-name: shanghai\n   ```\n\n2. Nacos控制台可以查看集群\n\n![image-20210923232925983](/images/image-20210923232925983.png)\n\n### 4.NacosRule服务集群优先级\n\n**配置负载均衡的规则为NacosRule：**\n\n* java配置\n\n```java\n@Bean\npublic IRule nacosRule() {\n\treturn new NacosRule();\n}\n```\n\n* service的SpringBoot核心配置文件中配置\n\n```yaml\nuserserivce:\n  ribbon:\n    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #规则\n```\n\n**NacosRule的规则如下：**\n\n* **优先选择同集群的服务实例列表**\n* 本地集群找不到提供者，才会去其他集群寻找，并且会报警告\n* 确定了可用实例列表后，再采用随机负载均衡挑选实例\n\n### 5.根据权重负载均衡\n\n1. 在Nacos控制台设置实例的权重值\n\n   <img src=\"/images/image-20210924002931392.png\" alt=\"image-20210924002931392\" style=\"zoom: 80%;\" /> \n\n2. 设置此实例的权重（取值[0,1]之间）：<img src=\"/images/image-20210924003021042.png\" alt=\"image-20210924003021042\" style=\"zoom:67%;\" />\n\n![image-20210924003043575](/images/image-20210924003043575.png)\n\n### 6.环境隔离namespace\n\n> Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西，用来做最外层隔离\n\n<img src=\"/images/image-20210924003227310.png\" alt=\"image-20210924003227310\" style=\"zoom: 40%;\" />\n\n1. 在Nacos控制台创建namespace，用来隔离不同环境\n\n   <img src=\"/images/image-20210924003539852.png\" alt=\"image-20210924003539852\" style=\"zoom:80%;\" /> \n\n2. 填写namespace信息\n\n   <img src=\"/images/image-20210924003635303.png\" alt=\"image-20210924003635303\" style=\"zoom:80%;\" /> \n\n3. 上述步骤之后会生成一个命名空间以及id\n\n4. 修改service的SpringBoot核心配置文件，添加namespace\n\n   ```yaml\n   spring:\n     application:\n       name: orderservice\n     cloud:\n       nacos:\n         server-addr: localhost:8848 #nacos的服务地址\n         discovery:\n           cluster-name: hangzhou\n           namespace: a67c6300-08fb-4f64-9e07-4969f8e95474 #命名空间的id\n   ```\n\n5. 此时启动service会导致此service位于指定的命名空间下\n\n6. 访问此orderservice由于namespace不同，会导致找不到userservice\n\n   ![image-20210924004140684](/images/image-20210924004140684.png)\n\n![image-20210924004158405](/images/image-20210924004158405.png)\n\n## Nacos注册中心细节分析\n\n![image-20210924004808150](/images/image-20210924004808150.png)\n\n### 临时实例和非临时实例\n\n服务注册到Nacos时，可以选择临时实例或非临时实例，通过配置SpringBoot核心配置文件：\n\n```yaml\nspring:\n  cloud:\n    nacos:\n      discovery:\n        ephemeral: false #是否临时实例\n```\n\n![image-20210924005316140](/images/image-20210924005316140.png)\n\n## Nacos配置管理\n\n### 配置Nacos配置\n\n1. 在Nacos管理页面添加配置\n\n   ![image-20210924134339815](/images/image-20210924134339815.png)\n\n2. 填写配置信息\n\n   ![image-20210924134537042](/images/image-20210924134537042.png)\n\n### 读取Nacos配置\n\n1. 引入Nacos的配置管理客户端依赖\n\n   ```xml\n   <!--nacos的配置管理依赖-->\n   <dependency>\n       <groupId>com.alibaba.cloud</groupId>\n       <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>\n   </dependency>\n   ```\n\n2. 在service的resources目录中添加一个bootstrap.yml引导文件，优先级高于application.yml\n\n   ```yaml\n   spring:\n     application:\n       name: userservice #服务名\n     profiles:\n       active: dev #配置环境名\n     cloud:\n       nacos:\n         server-addr: localhost:8848 #nacos地址\n         config:\n           file-extension: yaml #配置文件后缀名\n   ```\n\n3. 读取Nacos配置信息\n\n   ```java\n   @Value(\"${pattern.dateformat}\")\n   private String dateFormat;\n   \n   @RequestMapping(\"now\")\n   public String getNow() {\n       return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat));\n   }\n   ```\n\n![image-20210924165637880](/images/image-20210924165637880.png)\n\n### 热更新Nacos配置\n\n> Nacos中的配置文件变更后，微服务无需重启就可以感知。不过需要下面两种配置实现：\n\n* 方式一：在@Value注入的变量所在的类上打上`@RefreshScope`注解\n\n  ```java\n  @Slf4j\n  @RestController\n  @RefreshScope\n  @RequestMapping(\"/user\")\n  public class UserController {\n      @Autowired\n      private UserService userService;\n      @Value(\"${pattern.dateformat}\")\n      private String dateFormat;\n      @RequestMapping(\"now\")\n      public String getNow() {\n          return LocalDateTime.now().\n              format(DateTimeFormatter.ofPattern(dateFormat));\n      }\n  }\n  ```\n\n* 方式二：使用`@ConfigurationProperties`注解读取配置文件到配置类中，然后从配置类取得配置项的值\n\n  1. 读取到配置类中，并标记此配置类是一个Bean以方便其他地方注入使用\n\n     ```java\n     @Data\n     @Component\n     @ConfigurationProperties(prefix = \"pattern\")\n     public class PatternProperties {\n         private String dateFormat;\n     }\n     ```\n\n  2. 注入配置类Bean并使用配置项的值\n\n     ```java\n     @Autowired\n     private PatternProperties properties; //注入配置类的Bean\n     \n     @RequestMapping(\"now\")\n     public String getNow() {\n         return LocalDateTime.now().\n             format(DateTimeFormatter.ofPattern(properties.getDateFormat()));\n     }\n     ```\n\n     **注意：这种方式不需要`@RefreshScope`注解**\n\n![image-20210924171537858](/images/image-20210924171537858.png)\n\n### 多环境配置共享\n\n> 微服务在启动时会从Nacos读取多个配置文件\n>\n> * [spring.application.name]-[spring.profiles.active].yaml，例如userservice-dev.yaml\n> * [spring.application.name].yaml，例如userservice.yaml。这种情况下与环境无关\n>\n> 无论环境如何变化，[spring.application.name]不会变化，所以[spring.application.name].yaml一定会加载\n\n* 服务名.yaml可以被所有spring.application.name相同的服务读取到，属于共享配置\n\n* 多种配置**优先级**：在本地application.yml、Nacos配置的某环境yaml、Nacos配置的通用共享yaml中：\n\n  **服务名-环境名.yaml > 服务名.yaml > 本地配置.yaml**<img src=\"/images/image-20210924174006951.png\" alt=\"image-20210924174006951\" style=\"zoom:50%;\" />\n\n## Nacos集群\n\n### 1.Nacos集群结构图\n\n<img src=\"/images/image-20210925001827900.png\" alt=\"image-20210925001827900\" style=\"zoom:50%;\" />\n\n### 2.**搭建集群**：步骤\n\n* 搭建数据库集群，初始化数据库表结构\n\n  * Nacos默认数据存储在内嵌的Derby数据库中，不属于生产可用的数据库\n  * 官方推荐的最佳实践是使用带有主从的高可用数据库集群，主从模式的高可用数据库自学\n  * 这里以单点的数据库为例：\n    1. 新建一个数据库，命名为nacos\n    2. 导入nacos建表有关SQL（注意字段为datetime类型赋默认值需要MySQL5.6版本以上）\n\n* 下载nacos安装包\n\n* 配置nacos\n\n  * 配置nacos安装目录下的`conf`目录下的`cluster.conf`，设置集群的节点\n\n    ```apl\n    127.0.0.1:8845\n    127.0.0.1.8846\n    127.0.0.1.8847\n    ```\n\n  * 配置nacos安装目录下的`conf`目录下的`application.properties`，配置数据库\n\n    ```properties\n    #*************** Config Module Related Configurations ***************#\n    ### If use MySQL as datasource:\n    spring.datasource.platform=mysql\n    ### Count of DB:\n    db.num=1\n    ### Connect URL of DB:\n    db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC\n    db.user.0=root\n    db.password.0=199988\n    ```\n\n  * 配置多个nacos服务器的端口（8845、8846、8847）\n\n    ```properties\n    ### Default web server port:\n    server.port=8845\n    ```\n\n    <font color=red>**JVM内存不够的问题！**</font>\n\n    配置nacos安装目录下的`bin`目录下的启动脚本，修改`cluster`模式下的JVM参数\n\n    ```bash\n    rem if nacos startup mode is cluster\n    if %MODE% == \"cluster\" (\n        echo \"nacos is starting with cluster\"\n    \t  if %EMBEDDED_STORAGE% == \"embedded\" (\n    \t      set \"NACOS_OPTS=-DembeddedStorage=true\"\n    \t  )\n        set \"NACOS_JVM_OPTS=-server -Xms512m -Xmx512m -Xmn256m 后面省略了\n    )\n    ```\n\n* 启动nacos集群\n\n  * 直接运行`startup.cmd`不需要再设置以单击启动的参数了\n\n* 配置nginx反向代理\n\n  * 编辑nginx安装目录下的`conf`目录，在`http`标签内部加入nginx配置：\n\n    ```nginx\n    upstream nacos-cluster {\n        server 127.0.0.1:8845;\n    \tserver 127.0.0.1:8846;\n    \tserver 127.0.0.1:8847;\n    }\n    server {\n        listen       80;\n        server_name  localhost;\n        location /nacos {\n            proxy_pass http://nacos-cluster;\n        }\n    }\n    ```\n\n  * 启动nginx\n\n    * 直接运行`start nginx.exe`\n    * 停止nginx：`nginx.exe -s stop`或者`nginx.exe -s quit`，建议用quit优雅退出\n\n  * 访问`http:localhost:80/nacos`即可代理到`nacos-cluster`所定义的几个负载均衡结点上\n\n### 3.测试Nacos集群\n\n* 访问`http:localhost:80/nacos`会被代理到负载均衡的节点上\n* 在Nacos管理页面创建配置会保存到本地MySQL的`config_info`数据库中\n\n![image-20210925012202746](/images/image-20210925012202746.png)\n\n# Feign远程调用\n\n## RestTemplate方式调用存在的问题\n\n```java\nUser user = restTemplate.\n    getForObject(\"http://userservice/user/\" + userId, User.class);\n```\n\n* 代码可读性差，编程体验不统一\n* 参数复杂时URL难以维护\n\n## Feign的概念\n\n> Feign是一个声明式的http客户端，其作用就是帮助我们优雅的实现http请求的发送，解决以上的问题\n\n## 快速入门\n\n1. 引入起步依赖\n\n   ```xml\n   <!--Feign客户端-->\n   <dependency>\n       <groupId>org.springframework.cloud</groupId>\n       <artifactId>spring-cloud-starter-openfeign</artifactId>\n   </dependency>\n   ```\n\n2. 在service的启动类上添加`@EnableFeignClients`注解开启Feign的功能\n\n   ```java\n   @EnableFeignClients\n   public class OrderApplication { ... }\n   ```\n\n3. 编写Feign客户端的接口\n\n   ```java\n   @FeignClient(\"userservice\") //服务名称\n   public interface UserClient {\n       @GetMapping(\"/user/{id}\")\n       User findById(@PathVariable(\"id\") Long id);\n   }\n   ```\n\n   基于SpringMVC的注解来声明远程调用的信息，例如：\n\n   * 服务名称：userservice\n   * 请求方式：GET\n   * 请求路径：/user/{id}\n   * 请求参数：Long id\n   * 返回值类型：User\n\n4. 用Feign客户端代替RestTemplate\n\n   ```java\n   @Autowired\n   private UserClient userClient; //注入Feign客户端接口\n   public Order queryOrderById(Long orderId) {\n       // 1.查询订单\n       Order order = orderMapper.findById(orderId);\n   //      2.用Feign远程调用\n       User user = userClient.findById(order.getUserId());\n   //      3.封装数据\n       order.setUser(user);\n       // 4.返回\n       return order;\n   }\n   ```\n\n![image-20210925021404029](/images/image-20210925021404029.png)\n\n## 自定义Feign的配置\n\n> Feign运行自定义配置来覆盖默认配置，可以修改的配置如下：\n\n![image-20210925022012411](/images/image-20210925022012411.png)\n\n### 方式一：配置文件的方式\n\n* 全局生效\n\n  ```yaml\n  feign:\n    client:\n      config:\n        default: #对于所有的feign客户端生效\n          loggerLevel: FULL\n  ```\n\n* 局部生效\n\n  ```yaml\n  feign:\n    client:\n      config:\n        userservice: #只对userservice的feign客户端生效\n          loggerLevel: FULL\n  ```\n\n### 方式二：Java代码的方式，需要声明一个Bean\n\n```java\npublic class DefaultFeignConfiguration {\n    @Bean\n    public Logger.Level logLevel() {\n        //BASIC等级日志只记录请求方法和URL以及响应状态代码和执行时间。\n        return Logger.Level.BASIC;\n    }\n}\n```\n\n* 全局配置：把配置类的class放到启动类的`@EnableFeignClients`注解中\n\n  ```java\n  @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)\n  ```\n\n* 局部配置：把配置类的class放到某个Feign客户端的`@FeignClient`注解中\n\n  ```java\n  @FeignClient(value = \"userservice\",configuration = DefaultFeignConfiguration.class) \n  ```\n\n![image-20210925023446855](/images/image-20210925023446855.png)\n\n## Feign的性能优化\n\n* Feign底层的客户端实现：\n  * URLConnection：默认实现，不支持连接池\n  * Apache HttpClient：支持连接池\n  * OKHttp：支持连接池\n* 优化Feign的性能主要包括：\n  * 使用连接池代替默认的URLConnection\n  * 日志级别最好用BASIC或NONE\n\n**步骤：**\n\n1. 引入HttpClient依赖\n\n   ```xml\n   <!--HttpClient-->\n   <dependency>\n       <groupId>io.github.openfeign</groupId>\n       <artifactId>feign-httpclient</artifactId>\n   </dependency>\n   ```\n\n2. 配置连接池\n\n   ```yaml\n   feign:\n     httpclient:\n       enabled: true #支持httpclient的开关\n       max-connections: 200 #最大连接数\n       max-connections-per-route: 50 #单个路径的最大连接数\n   ```\n\n![image-20210925121436208](/images/image-20210925121436208.png)\n\n## Feign的最佳实践\n\n**方式一（继承）：给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准**\n\n![image-20210925122047188](/images/image-20210925122047188.png)\n\n**方式二（抽取）：将FeignClient抽取为独立模块，并且把接口有关的POJO、默认的Feign配置都放到这个模块，提供给所有消费者使用**\n\n![image-20210925122506478](/images/image-20210925122506478.png)\n\n![image-20210925122544884](/images/image-20210925122544884.png)\n\n**实现方式二（抽取FeignClient）：**\n\n1. 新建一个module，命名为`feign-api`，然后引入feign的starter起步依赖\n\n   ```xml\n   <dependencies>\n       <dependency>\n           <groupId>org.springframework.cloud</groupId>\n           <artifactId>spring-cloud-starter-openfeign</artifactId>\n       </dependency>\n   </dependencies>\n   ```\n\n2. 将service中编写的FeignClient、POJO、DefaultFeignConfiguration复制到`feign-api`项目中\n\n   <img src=\"/images/image-20210925125205808.png\" alt=\"image-20210925125205808\" style=\"zoom: 80%;\" /> \n\n3. 在service中引入编写好的`feign-api`依赖\n\n   ```xml\n   <!--自己写的feign-api-->\n   <dependency>\n       <groupId>cn.itcast.demo</groupId>\n       <artifactId>feign-api</artifactId>\n       <version>1.0</version>\n   </dependency>\n   ```\n\n4. 修改service中与上述三个组件相关的import部分，改为从`feign-api`依赖中引入\n\n   <img src=\"/images/image-20210925125334508.png\" alt=\"image-20210925125334508\" style=\"zoom:67%;\" /> \n\n* <font color=red>此时启动service会导致引入的FeignClient客户端Bean无法被自动装载</font>\n\n  ![image-20210925125513417](/images/image-20210925125513417.png)\n\n  **原因：**引入的`feign-api`依赖下的FeignClient客户端位于`cn.itcast.feign.clients`包下，不属于当前service的@SpringBootApplication默认扫描包范围\n\n  **解决方法：**\n\n  * 方式一：指定FeignClient所在包\n\n    ```java\n    @EnableFeignClients(basePackages = \"cn.itcast.feign.clients\")\n    ```\n\n  * 方式二：指定FeignClient的字节码\n\n    ```java\n    @EnableFeignClients(clients = UserClient.class)\n    ```\n\n![image-20210925131403334](/images/image-20210925131403334.png)\n\n# Gateway服务网关\n\n## 网关的功能\n\n![image-20210925131840527](/images/image-20210925131840527.png)\n\n## 网关技术的实现\n\n在SpringCloud中网关的实现包括两种：\n\n* Gateway\n* Zuul\n\n> Zuul是基于Servlet的实现，属于阻塞式编程。而SpringCloud Gateway则是基于Spring5中提供的WebFlux，属于响应式编程的实现，具备更好的性能\n\n![image-20210925132058849](/images/image-20210925132058849.png)\n\n## 快速入门：搭建网关\n\n1. 创建新的module，引入SpringCloud Gateway的依赖和Nacos依赖，创建网关的SpringBoot启动类\n\n   ```xml\n   <dependencies>\n       <!--Nacos服务发现依赖-->\n       <dependency>\n           <groupId>com.alibaba.cloud</groupId>\n           <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n       </dependency>\n       <!--网关依赖-->\n       <dependency>\n           <groupId>org.springframework.cloud</groupId>\n           <artifactId>spring-cloud-starter-gateway</artifactId>\n       </dependency>\n   </dependencies>\n   ```\n\n2. 编写路由配置以及nacos地址\n\n   ```yaml\n   server:\n     port: 10100\n   spring:\n     application:\n       name: gateway\n     cloud:\n       nacos:\n         server-addr: localhost:80 #nacos地址\n       gateway:\n         routes:\n           - id: user-service #路由标识\n             uri: lb://userservice #路由的目标地址\n             predicates: #断言，判断请求是否符合规则\n               - Path=/user/** #断言是否是以/user/开头的请求\n           - id: order-service\n             uri: lb://orderservice\n             predicates:\n               - Path=/order/**\n   ```\n\n   启动访问`http://localhost:10100/user/{id}`或者`http://localhost:10100/order/{orderId}`即可\n\n   ![image-20210925133523283](/images/image-20210925133523283.png)\n\n![image-20210925133647641](/images/image-20210925133647641.png)\n\n## 路由断言工厂：Route Predicate Factory\n\n网关路由可以配置的内容包括：\n\n* 路由id：路由的唯一标识\n* uri：路由目的地，支持lb（负载均衡）和http两种\n* predicates：路由断言，判断请求是否符合要求，符合则转发到路由目的地\n* filters：路由过滤器，处理请求或响应\n\n我们再配置文件中写的断言规则只是字符串，这些字符串会被Predicate Factory读取并处理，转为路由判断条件\n\n例如：Path=/user/**是按照路径匹配，这个规则是由类：`org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory`处理\n\n像这样的断言工厂还有：\n\n![image-20210925134110998](/images/image-20210925134110998.png)\n\n组合断言：使用多个断言，如果不满足其中一个那么无法路由（404）\n\n```yaml\nspring:\n  application:\n    name: gateway\n  cloud:\n    nacos:\n      server-addr: localhost:80 #nacos地址\n    gateway:\n        - id: order-service #路由的目标地址\n          uri: lb://orderservice\n          predicates: #断言，判断请求是否符合规则\n            - Path=/order/** #断言是否是以/order/开头的请求\n            - Method=GET #请求方式必须为GET\n```\n\n![image-20210925135753346](/images/image-20210925135753346.png)\n\n## 路由过滤器：GatewayFilter\n\n> GatewayFilter是网关中提供的一种过滤器，可以对进入网关的请求和微服务返回的响应做处理\n\n**过滤器工厂GatewayFilterFacotry**\n\nSpring提供了31种不同的路由过滤器工厂：\n\n<img src=\"/images/image-20210925140135506.png\" alt=\"image-20210925140135506\" style=\"zoom: 50%;\" /> \n\n案例：给所有进入userservice的请求添加一个请求头：TAOYYZ=likeccz\n\n实现方式：在gateway的SpringBoot核心配置文件中给userservice的路由添加过滤器\n\n```yaml\nspring:\n  application:\n    name: gateway\n  cloud:\n    nacos:\n      server-addr: localhost:80 #nacos地址\n    gateway:\n      routes:\n        - id: user-service #路由标识\n          uri: lb://userservice #路由的目标地址\n          predicates: #断言，判断请求是否符合规则\n            - Path=/user/** #断言是否是以/user/开头的请求\n          filters: #过滤器\n            - AddRequestHeader=TAOYYZ,likeccz #加入请求头\n```\n\n**默认过滤器：**如果要对所有的路由都生效此过滤器，则可以将过滤器工厂写到`default-filters`下：\n\n```yaml\nspring:\n  application:\n    name: gateway\n  cloud:\n    nacos:\n      server-addr: localhost:80 #nacos地址\n    gateway:\n      routes:\n        - id: user-service #路由标识\n          uri: lb://userservice #路由的目标地址\n          predicates: #断言，判断请求是否符合规则\n            - Path=/user/** #断言是否是以/user/开头的请求\n      default-filters:\n        - AddRequestHeader=TAOYYZ,likeccz #对所有请求都加入请求头\n```\n\n## 全局过滤器：GlobalFilter\n\n> 全局过滤器的作用也是处理一切进入网关的请求和微服务响应，与GatewayFilter的作用一样\n>\n> 区别在于GatewayFilter通过配置定义，处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现\n\n定义方式：实现GlobalFilter接口\n\n```java\npublic interface GlobalFilter {\n\tMono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);\n}\n```\n\n案例：定义全局过滤器，拦截并判断用户身份\n\n需求：定义全局过滤器，判断请求参数是否满足以下条件\n\n* 参数中有lover\n* lover参数值为ccz\n\n如果满足则放行，否则拦截\n\n实现：定义一个全局过滤器的实现类，实现`filter()`方法，并且指定为Spring组件，并定义Order执行顺序\n\n```java\n@Component //标记为Spring的Bean\n//@Order(-1) //设置执行顺序，也可以实现Ordered接口的getOrder()方法返回一个int值\npublic class LoverFilter implements GlobalFilter, Ordered {\n    @Override\n    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {\n        //1.获取请求参数\n        ServerHttpRequest request = exchange.getRequest();\n        //2.获取请求参数中的lover参数\n        MultiValueMap<String, String> queryParams = request.getQueryParams();\n        //3.判断参数值是否为ccz\n        String lover = queryParams.getFirst(\"lover\");\n        //4.是的话放行\n        if (\"ccz\".equals(lover)) {\n            return chain.filter(exchange);\n        }\n        //5.否则拦截\n        //5.1为了给用户一个提示，设置一个响应状态码\n        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);\n        return exchange.getResponse().setComplete();\n    }\n    @Override\n    public int getOrder() {\n        return -1;\n    }\n}\n```\n\n此时访问`http://localhost:10100/order/101`会被拦截并返回401状态码，因为参数没有`lover=ccz`\n\n![image-20210925143511913](/images/image-20210925143511913.png)\n\n## 过滤器执行顺序\n\n请求进入网关会碰到三类过滤器：当前路由的过滤器、DefaultFilter、GlobalFilter\n\n请求路由后，会将当前路由过滤器和DefaultFilter、GlobalFilter合并到一个过滤器链中，**排序**后依次执行每个过滤器\n\n* 当前路由过滤器和`DefaultFilter`都属于`GatewayFilter`\n* `GlobalFilter`通过`GatewayFilterAdapter`适配成`GatewayFilter`\n\n![image-20210925145341333](/images/image-20210925145341333.png)\n\n**排序：**\n\n* 每一个过滤器都必须指定一个int类型的Order值，Order值越小，优先级越高，执行顺序越靠前\n* GlobalFilter通过实现Ordered接口或@Order注解来指定Order值，由我们自己指定\n* 路由过滤器和DefaultFilter的Order值由Spring指定，默认是按照声明顺序从1开始递增\n* 当过滤器的Order值一样时：**DefaultFilter > 路由过滤器 > GlobalFilter**\n\n![image-20210925150025819](/images/image-20210925150025819.png)\n\n![image-20210925150035328](/images/image-20210925150035328.png)\n\n## 跨域问题处理\n\n跨域是域名不一致，主要包括：\n\n* 域名不同：`www.taobao.com`和`www.taobao.org`\n* 域名相同，端口不同：`localhost:8081`和`localhost:8081`\n\n**跨域问题**：**浏览器禁止**请求的发起者与服务器端发生跨域的**AJAX请求**，请求被浏览器拦截的问题\n\n解决方案：CORS\n\n网关处理跨域采用的是CORS方案，只需要简单配置即可实现：\n\n```yaml\nspring:\n  cloud:\n    gateway:\n      globalcors: # 全局的跨域处理\n        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题\n        corsConfigurations:\n          '[/**]':\n            allowedOrigins: # 允许哪些网站的跨域请求\n              - \"http://localhost:8090\"\n              - \"http://www.leyou.com\"\n            allowedMethods: # 允许的跨域ajax的请求方式\n              - \"GET\"\n              - \"POST\"\n              - \"DELETE\"\n              - \"PUT\"\n              - \"OPTIONS\"\n            allowedHeaders: \"*\" # 允许在请求中携带的头信息\n            allowCredentials: true # 是否允许携带cookie\n            maxAge: 360000 # 这次跨域检测的有效期\n```\n\n![image-20210925152048065](/images/image-20210925152048065.png)\n\n# RabbitMQ消息队列\n\n## 同步和异步\n\n### 同步调用存在的问题\n\n![image-20210929010842471](/images/image-20210929010842471.png)\n\n<img src=\"/images/image-20210929010920818.png\" alt=\"image-20210929010920818\" style=\"zoom:50%;\" /> \n\n### 异步调用方案\n\n![image-20210929011140311](/images/image-20210929011140311.png)\n\n### 事件驱动优势\n\n* 服务解耦\n* 性能提升，吞吐量提高\n\n* 服务没有强依赖，不担心级联失败问题\n* 流量削峰<img src=\"/images/image-20210929011726020.png\" alt=\"image-20210929011726020\" style=\"zoom:50%;\" />\n\n![image-20210929011911137](/images/image-20210929011911137.png)\n\n## 常用的消息队列\n\n> MQ（MessageQueue）消息队列，字面上来看就是存放消息的队列。也就是事件驱动架构中的Broker\n\n![image-20210929012919073](/images/image-20210929012919073.png)\n\n## RabbitMQ\n\n> RabbitMQ是基于Erlang语言开发的开源消息通信中间件\n\n<img src=\"/images/image-20210930172348642.png\" alt=\"image-20210930172348642\" style=\"zoom: 67%;\" /> \n\n![image-20210930172431626](/images/image-20210930172431626.png)\n\n## 安装RabbitMQ\n\n* 单机部署：\n\n  1. 加载RabbitMQ镜像到docker：\n\n     * 在线拉取：`docker pull rabbitmq:3-management`\n     * 从本地加载镜像的tar包：`docker load -i mq.tar`\n\n  2. 运行RabbitMQ到容器：\n\n     ```shell\n     docker run \\\n      -e RABBITMQ_DEFAULT_USER=account \\\n      -e RABBITMQ_DEFAULT_PASS=password \\\n      --name mq \\\n      --hostname mq1 \\\n      -p 15672:15672 \\\n      -p 5672:5672 \\\n      -d \\\n      rabbitmq:3-management\n     ```\n\n## 常见消息模型\n\n<img src=\"/images/image-20210930172958391.png\" alt=\"image-20210930172958391\" style=\"zoom: 50%;\" /> \n\n### 1. HelloWorld案例——简单队列模型\n\n![image-20210930173119852](/images/image-20210930173119852.png)\n\n![image-20210930175724961](/images/image-20210930175724961.png)\n\n## SpringAMQP\n\n### 概念\n\n![image-20210930180247019](/images/image-20210930180247019.png)\n\n### 快速入门\n\n**案例：**利用SpringAMQP实现HelloWorld中的简单队列模型的基础消息队列功能\n\n1. 在父工程中引入spring-amqp的**起步依赖**\n\n   ```xml\n   <!--AMQP依赖，包含RabbitMQ-->\n   <dependency>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-starter-amqp</artifactId>\n   </dependency>\n   ```\n\n2. 在publisher服务中利用RabbitTemplate**发送消息**到simple.queue这个队列\n\n   * 在publisher服务的SpringBoot核心配置文件中配置mq连接信息：\n\n     ```yaml\n     spring:\n       rabbitmq:\n         host: 192.168.220.12 #RabbitMQ主机，默认localhost\n         port: 5672 #端口，默认为5672，如果启用SSL，则为5671\n         virtual-host: / #虚拟主机\n         username: itcast #默认guest\n         password: 12345 #默认guest\n     ```\n\n   * 在publisher服务中新建一个类来测试方法\n\n     ```java\n     @SpringBootTest\n     public class SpringAmqpTest {\n         @Autowired\n         private RabbitTemplate rabbitTemplate;\n         @Test\n         public void test() {\n             String queueName = \"simple.queue\";\n             String message = \"hello springAmqp by taoyyz2\";\n             rabbitTemplate.convertAndSend(queueName,message);\n         }\n     }\n     ```\n\n3. 在consumer服务中编写**消费**逻辑，绑定simple.queue这个队列\n\n   * 在consumer服务的SpringBoot核心配置文件中配置mq连接信息：\n\n     ```yaml\n     spring:\n       rabbitmq:\n         host: 192.168.220.12 #RabbitMQ主机，默认localhost\n         port: 5672 #端口，默认为5672，如果启用SSL，则为5671\n         virtual-host: / #虚拟主机\n         username: itcast #默认guest\n         password: 12345 #默认guest\n     ```\n\n   * 在consumer服务中新建一个消息监听器类（注册为Bean），编写消费逻辑：\n\n     ```java\n     @Component\n     public class SpringAmqpListenerTest {\n         @RabbitListener(queues = \"simple.queue\")\n         public void listenSimpleQueueMessage(String msg) {\n             System.out.println(\"接受到的消息：\" + msg);\n         }\n     }\n     ```\n\n<img src=\"/images/image-20210930210805550.png\" alt=\"image-20210930210805550\" style=\"zoom: 50%;\" />  \n\n<img src=\"/images/image-20210930220240837.png\" alt=\"image-20210930220240837\" style=\"zoom: 50%;\" /> \n\n### 2. Work Queue工作队列\n\n> 工作队列可以提高消息处理速度，避免队列消息堆积\n\n<img src=\"/images/image-20210930220634785.png\" alt=\"image-20210930220634785\" style=\"zoom:50%;\" />\n\n**案例：**模拟Work Queue，实现一个队列绑定多个消费者\n\n思路：\n\n* 在publisher服务中定义测试方法，每秒产生50条消息，发送到simple.queue\n\n  ```java\n  @Test\n  public void test2() throws InterruptedException {\n      String queueName = \"simple.queue\";\n      String message = \"hello springAmqp__\";\n      for (int i = 0; i < 50; i++) {\n          rabbitTemplate.convertAndSend(queueName, message + i + \"   \");\n          Thread.sleep(20);\n      }\n      System.out.println(\"推送消息完成\");\n  }\n  ```\n\n* 在consumer服务中定义两个消息监听者，都监听simple.queue队列\n\n* 消费者1每秒处理50条消息，消费者2每秒处理10条消息\n\n  ```java\n  @RabbitListener(queues = \"simple.queue\")\n  public void listenSimpleQueueMessage(String msg) throws InterruptedException {\n      System.out.println(\"【1】接受到的消息是：\" + msg + LocalTime.now());\n      Thread.sleep(20);\n  }\n  \n  @RabbitListener(queues = \"simple.queue\")\n  public void listenSimpleQueueMessage2(String msg) throws InterruptedException {\n      System.err.println(\"【2】接受到的消息是：\" + msg + LocalTime.now());\n      Thread.sleep(100);\n  }\n  ```\n\n<font color=red>**问题**：</font>由于消费者2处理能力差，在获取消息时与消费者1都分别**预取**了25条消息，导致消费者2消费25条消息需要大量的时间\n\n### 消费预取限制\n\n修改SpringBoot核心配置文件，设置preFetch值，可以控制预取消息的上限：\n\n```yaml\nspring:\n  rabbitmq:\n    listener:\n      simple:\n        prefetch: 1 #每个消费者可以未确认的最大未确认消息数。\n```\n\n![image-20210930233018894](/images/image-20210930233018894.png)\n\n### 发布（Publish）、订阅（Subscribe）\n\n> 发布订阅模式与之前的区别就是允许将同一消息发送给多个消费者，实现方式是加入了exchange（交换机）\n\n![image-20211001003507962](/images/image-20211001003507962.png)\n\n#### 3. FanoutExchange\n\n> Fanout Exchange会将接受到的消息路由到每一个跟其绑定的queue，称为广播模式\n\n![image-20211001003712624](/images/image-20211001003712624.png)\n\n**案例：**利用SpringAMQP演示FanoutExchange的使用\n\n思路：![image-20211001004124505](/images/image-20211001004124505.png)\n\n* 在consumer服务中，利用代码声明队列、交换机，并将两者绑定\n\n  * 在consumer服务中声明Exchange、Queue、Binding\n\n    SpringAMQP提供了声明交换机、队列、绑定关系的API，例如：\n\n    <img src=\"/images/image-20211001004924838.png\" alt=\"image-20211001004924838\" style=\"zoom:75%;\" />\n\n  * 在consumer服务中编写一个配置类，用于产生FanoutExchange、Queue和绑定关系对象Binding\n\n  ```java\n  @Configuration\n  public class FanoutConfig {\n      @Bean\n      public FanoutExchange fanoutExchange() {\n          //产生交换机Bean\n          return new FanoutExchange(\"test.fanout\");\n      }\n      \n      @Bean\n      public Queue fanoutQueue1() {\n          //产生第一个队列的Bean\n          return new Queue(\"fanout.q1\");\n      }\n  \n      @Bean\n      public Queue fanoutQueue2() {\n          //产生第二个队列的Bean\n          return new Queue(\"fanout.q2\");\n      }\n  \n      @Bean\n      public Binding fanoutBinding1() {\n          //绑定队列1到交换机，这里可以通过调方法的返回值获取队列和交换机\n          return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());\n      }\n      @Bean\n      public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange) {\n          //绑定队列2到交换机，这里通过参数注入这俩Bean\n          return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);\n      }\n  }\n  ```\n\n* 在consumer服务中，编写两个消费者方法，分别监听fanout.queue1和fanout.queue2\n\n  ```java\n  @RabbitListener(queues = \"fanout.q1\")\n  public void listenSimpleQueueMessage3(String msg) {\n      System.out.println(\"【queue1】接受到的消息是：\" + msg + LocalTime.now());\n  }\n  \n  @RabbitListener(queues = \"fanout.q2\")\n  public void listenSimpleQueueMessage4(String msg) {\n      System.err.println(\"【queue2】接受到的消息是：\" + msg + LocalTime.now());\n  }\n  ```\n\n* 在publisher中编写测试方法，向交换机发送消息\n\n  ```java\n  @Test\n  public void testSendFanoutExchange() {\n      //交换机名称\n      String exchangeName = \"test.fanout\";\n      //消息\n      String msg = \"hello,everyOne!\";\n      //发送消息\n      rabbitTemplate.convertAndSend(exchangeName,\"\", msg);\n      System.out.println(\"发送完成！\");\n  }\n  ```\n\n  **<font color=red>踩坑！！！交换机无法接收到publisher中推送的消息，消费者也无法订阅到消息</font>**\n\n  **原因：**RabbitTemplate对象的`convertAndSend()`方法选择了错误的重载：![image-20211001154451836](/images/image-20211001154451836.png)\n\n![image-20211001164327103](/images/image-20211001164327103.png)\n\n#### 4. DirectExchange\n\n> Direct Exchange会将接受到的消息根据规则路由到指定的Queue，称为路由模式（routes）\n\n![image-20211001165453866](/images/image-20211001165453866.png)\n\n**案例：**利用SpringAMQP演示DirectExchange的使用\n\n<img src=\"/images/image-20211001165830038.png\" alt=\"image-20211001165830038\" style=\"zoom: 60%;\" /> \n\n思路：\n\n* 利用@RabbitListener声明Exchange、Queue、routingKey，*代码同下一步*\n\n* 在consumer服务中，编写两个消费者方法，分别监听direct.queue1和direct.queue2\n\n  ```java\n  @RabbitListener(bindings = @QueueBinding(\n          value = @Queue(name = \"direct.q1\"), //绑定的队列名\n          exchange = @Exchange(name = \"itcast.direct\", type = ExchangeTypes.DIRECT),//默认为direct\n          key = {\"red\", \"blue\"} //routingKey路由键\n  ))\n  public void listenDirectQueue1(String msg) {\n      System.out.println(\"【queue1】接受到的消息是：\" + msg);\n  }\n  \n  @RabbitListener(bindings = @QueueBinding(\n          value = @Queue(name = \"direct.q2\"), //绑定的队列名\n          exchange = @Exchange(name = \"itcast.direct\", type = ExchangeTypes.DIRECT),//默认为direct\n          key = {\"red\", \"yellow\"} //routingKey路由键\n  ))\n  public void listenDirectQueue2(String msg) {\n      System.out.println(\"【queue2】接受到的消息是：\" + msg);\n  }\n  ```\n\n* 在publisher中编写测试方法，向itcast.direct发送消息\n\n  ```java\n  @Test\n  public void testSendDirectExchange() {\n      String exchangeName = \"itcast.direct\";\n      String msg = \"hello direct!!\";\n      rabbitTemplate.convertAndSend(exchangeName, \"red\", msg); //监听了red的routingKey的队列均可收到\n      System.out.println(\"发送完成！\");\n  }\n  ```\n\n  ![image-20211001171259343](/images/image-20211001171259343.png)\n\n#### 5. TopicExchange\n\n> TopicExchange的routingKey必须是多个单词的列表，并且以`.`分隔\n>\n> Queue与Exchange指定BindingKey时可以使用通配符：\n>\n> `#`：代表0个或多个单词\n>\n> `*`：代表一个单词\n\n![image-20211001180909842](/images/image-20211001180909842.png)\n\n**案例：**利用SpringAMQP演示TopicExchange的使用\n\n<img src=\"/images/image-20211001181140583.png\" alt=\"image-20211001181140583\" style=\"zoom:67%;\" />\n\n思路：\n\n* 利用@RabbitListener声明Exchange、Queue、routingKey，*代码同下一步*\n\n* 在consumer服务中，编写两个消费者方法，分别监听topic.queue1和topic.queue2\n\n  ```java\n  @RabbitListener(bindings = @QueueBinding(\n          value = @Queue(name = \"topic.q1\"),\n          exchange = @Exchange(name = \"itcast.topic\", type = ExchangeTypes.TOPIC),\n          key = \"china.#\"\n  ))\n  public void listenTopicQueue1(String msg) {\n      System.out.println(\"【topic.q1】接受到的消息是：\" + msg);\n  }\n  \n  @RabbitListener(bindings = @QueueBinding(\n          value = @Queue(name = \"topic.q2\"),\n          exchange = @Exchange(name = \"itcast.topic\", type = ExchangeTypes.TOPIC),\n          key = \"#.news\"\n  ))\n  public void listenTopicQueue2(String msg) {\n      System.out.println(\"【topic.q2】接受到的消息是：\" + msg);\n  }\n  ```\n\n* 在publisher中编写测试方法，向itcast.topic发送消息\n\n  ```java\n  @Test\n  public void testSendTopicExchange() {\n      String exchangeName = \"itcast.topic\";\n      String msg = \"hello topic china!!\";\n      // china.# 和 #.news都能收到\n      rabbitTemplate.convertAndSend(exchangeName, \"china.news\", msg);\n      \n      // china.# 能收到\n      rabbitTemplate.convertAndSend(exchangeName, \"china.whether\", msg);\n  \n      // #.news 能收到\n      rabbitTemplate.convertAndSend(exchangeName, \"japan.news\", msg);\n      System.out.println(\"发送完成！\");\n  }\n  ```\n\n### 消息转换器\n\n> 在SpringAMQP的发送方法中，消息的类型是Object，也就是说可以发送任意对象的消息，SpringAMQP会序列化为字节后发送\n\nSpring对消息对象的处理是由`org.springframework.amqp.support.converter.MessageConverter`来处理的\n\n而默认实现是SimpleMessageConverter，基于JDK的ObjectOutputStream完成序列化。这种方式性能较差。\n\n自定义MessageConverter的Bean即可，**推荐使用json序列化：**\n\n* 发送消息：\n\n    1. 在publisher服务中引入jackson依赖：\n\n       ```xml\n       <!--jackson依赖-->\n       <dependency>\n           <groupId>com.fasterxml.jackson.core</groupId>\n           <artifactId>jackson-databind</artifactId>\n       </dependency>\n       ```\n\n    2. 在publisher服务中声明MessageConverter的Bean，使得发送消息时可以以JSON格式发送\n\n       ```java\n       @Bean\n       public MessageConverter messageConverter() {\n           return new Jackson2JsonMessageConverter();\n       }\n       ```\n       \n    3. 在publisher服务中发送消息：\n\n       ```java\n       @Test\n       public void testSendObject() {\n           Map<String, Object> msg = new HashMap<>();\n           msg.put(\"name\", \"菜菜子\");\n           msg.put(\"age\", 21);\n           rabbitTemplate.convertAndSend(\"object.queue\",msg);\n       }\n       ```\n\n* 接收消息：\n\n    1. 在consumer服务中也引入jackson依赖\n\n    2. 在consumer服务中声明MessageConverter的Bean，使得接受消息时可以以JSON格式解析\n\n       ```java\n       @Bean\n       public MessageConverter messageConverter() {\n           return new Jackson2JsonMessageConverter();\n       }\n       ```\n\n    3. 在consumer服务中定义一个监听者监听队列接收消息\n\n       ```java\n       @RabbitListener(queues = \"object.queue\")\n       public void listenObjectQueue(Map<String, Object> msg) {\n           msg.forEach((s, o) -> System.out.println(s + \" --> \" + o));\n       }\n       ```\n\n![image-20211001214355743](/images/image-20211001214355743.png)","tags":["Java","Spring"],"categories":["Java"]},{"title":"Mybatis基础","url":"/2021/09/01/MyBatis/","content":"\n# MyBatis入门\n\n## MyBatis概述\n\n* MyBatis 是一款基于Java的优秀的持久层框架，它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO（Plain Old Java Objects，普通老式 Java 对象）为数据库中的记录。\n* 它封装了JDBC操作的很多细节，使开发者只需要关注SQL语句本身，而无需关注驱动、创建连接等繁杂过程\n* 它使用了ORM思想实现了结果集的封装\n\n## MyBatis环境搭建\n\n1. 创建Maven工程\n\n   * 小技巧：为了避免每次不同的模块都要编写一遍pom.xml的依赖，可以第一次创建一个父Maven项目，引入依赖，以后的Maven项目直接继承这个父项目就可以简化重复的依赖：\n     * 建立的Maven项目删除掉src文件夹，然后配置好依赖后直接新建模块，选择Maven项目时就有继承选项了，继承的父项目在此项目的pom.xml的`parent`标签里\n\n2. 导入依赖：MyBatis和MySQL连接器\n\n   ```xml\n   \t\t<dependency>\n               <groupId>org.mybatis</groupId>\n               <artifactId>mybatis</artifactId>\n               <version>3.4.5</version>\n           </dependency>\n           <dependency>\n               <groupId>mysql</groupId>\n               <artifactId>mysql-connector-java</artifactId>\n               <version>5.1.38</version>\n           </dependency>\n   ```\n\n3. 创建domain里的实体类和dao层的接口，定义好接口的方法\n\n   * 创建User实体类，成员属性名要和数据库字段名一一对应\n   * 创建dao层的接口和方法\n\n4. 创建MyBatis的主配置文件并进行配置\n\n   * 在Maven项目的`resources`目录下创建一个`xml`文件\n\n   * 引入约束和环境配置：\n\n     ```xml\n     <?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n     <!DOCTYPE configuration\n             PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\"\n             \"http://mybatis.org/dtd/mybatis-3-config.dtd\"> <!--引入配置文件的约束-->\n     <configuration>\n         <!--配置环境-->\n         <environments default=\"mysql\"> <!--为这个环境起个名字-->\n             <environment id=\"mysql\">\n     \t\t\t<transactionManager type=\"JDBC\"></transactionManager><!--配置事务类型-->\n                 <dataSource type=\"POOLED\"> <!--配置数据源/连接池-->\n                     <!--配置连接数据库的4个信息-->\n                     <property name=\"driver\" value=\"com.mysql.jdbc.Driver\"/>\n                     <property name=\"url\" value=\"jdbc:mysql:///db1/mybatisuser\"/>\n                     <property name=\"username\" value=\"root\"/>\n                     <property name=\"password\" value=\"199988\"/>\n                 </dataSource>\n             </environment>\n         </environments>\n     </configuration>\n     ```\n\n5. 创建MyBatis与dao层接口的映射文件：\n\n   * 在上一步的MyBatis配置文件的`configuration`标签中添加一个`mappers`标签，指定映射配置文件位置\n\n     ```xml\n     <mappers>\n     \t<mapper resource=\"com/taoyyz/dao/UserDao.xml\"></mapper>\n     \t<!--注意这个路径默认是maven项目的resources目录而不是java目录-->\n     </mappers>\n     ```\n\n   * 根据映射文件的位置，在`resources`目录下创建对应的`xml`映射文件并编写SQL语句\n\n     ```XML\n     <?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n     <!DOCTYPE mapper\n             PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n             \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"> <!--引入Mapper的约束-->\n     <mapper namespace=\"com.taoyyz.dao.UserDao\"> <!--命名空间为接口全类名-->\n         <!--配置查询所有select,这里的id为对应mapper命名空间那个接口里的方法名称,结果集类型为实体类全类名-->\n         <select id=\"findAll\" resultType=\"com.taoyyz.domain.User\">\n             select * from mybatisuser #在这里编写SQL\n         </select>\n     </mapper>\n     ```\n\n* **注意事项**：遵从以下约定就可以无需编写dao层实现类（约定大于配置）\n  * UserDao接口对应的映射`xml`文件为`UserDao.xml`，也可以叫做`UserMapper.xml`，相当于接口实现类\n  * UserDao接口对应的`xml`**映射文件的目录层级**必须和**接口的包层级**在名字上相同：`com.taoyyz.dao`\n  * 映射配置文件的`mapper`标签的`namespace`命名空间取值必须是dao层接口的**全限定类名**\n  * 映射配置文件的操作配置（例如`select`标签）的`id`属性取值必须是dao层接口的对应方法名\n\n## MyBatis入门案例\n\n## 自定义MyBatis框架\n\n* MyBatis在使用代理dao层接口的方式实现CRUD时做了什么事？\n  * 创建代理对象\n  * 在代理对象中调用方法\n* 自定义MyBatis需要的类：\n  * `Resources`\n  * `SqlSessionFactoryBuilder`\n  * `SqlSessionFactory`\n  * `SqlSession`\n\n# MyBatis基本使用\n\n## MyBatis的单表CRUD操作\n\n## MyBatis的参数和返回值\n\n## MyBatis的dao编写\n\n## MyBatis配置的细节：标签使用\n\n# MyBatis高级\n\n## MyBatis的连接池\n\n## MyBatis的事务控制和设计方法\n\n## MyBatis的多表查询\n\n### 一对多\n\n### 多对多\n\n# MyBatis缓存和注解开发\n\n## MyBatis的加载时机（查询时机）\n\n## MyBatis的一级缓存和二级缓存\n\n## MyBatis的注解开发\n\n### 单表CRUD\n\n### 多表","tags":["Java","Mybatis"],"categories":["Java"]},{"title":"SpringBoot基础","url":"/2021/08/29/SpringBoot/","content":"\n<font size=7 face=\"微软雅黑\">SpringBoot</font>\n\n# 概述\n\n## SpringBoot简介\n\n> SpringBoot提供了一种快速使用Spring的方式，基于**约定优于配置**的思想，可以让开发人员不必在配置与逻辑业务之间进行思维的切换，全身心投入到逻辑业务的代码编写中，从而大大提高开发效率，一定程度上缩短了项目周期。2014年4月SpringBoot 1.0.0发布，是Spring的顶级项目之一\n\n## Spring的缺点\n\n* **配置繁琐**\n\n  虽然Spring的组件代码是轻量级的，但它的配置却是重量级的。一开始Spring用XML配置，而且是很多XML配置，Spring2.5引入了基于注解的组件扫描，这消除了大量针对应用程序自身组件的显式XML配置。Spring3.0引入了基于Java的配置，这是一种类型安全的可重构配置方式，可以代替XML。\n\n  所有这些配置都代表了开发时的损耗，因为在思考Spring特性配置和解决业务问题之间需要进行思维切换，所以编写配置挤占了编写应用程序逻辑的时间，和所有框架一样，Spring实用，但它要求的回报也不少\n\n* **依赖繁琐**\n\n  项目的依赖管理也是一件耗时耗力的事情。在环境搭建时，需要分析要导入哪些库的坐标，而且还需要分析导入与之有依赖关系的其他库的坐标，一旦选错了依赖的版本，随之而来的不兼容问题就会严重阻碍项目的开发进度\n\n## SpringBoot的功能\n\n* **自动配置**\n\n  SpringBoot的自动配置是一个运行时（应用程序启动时）的过程，考虑了众多因素，才决定Spring配置应该用哪个、不该用哪个。\n\n* **起步依赖（依赖传递）**\n\n  起步依赖本质上是一个Maven项目对象模型（POM），定义了对其他库的传递依赖，这些东西加在一起即支持某项功能。简单来说，起步依赖就是将具备各种功能的坐标打包到一起，并提供一些默认的功能\n\n* **辅助功能**\n\n  提供了一些大型项目中常见的非功能性特性，如嵌入式服务器、安全、指标、健康检测、外部配置等\n\n>SpringBoot并不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式\n\n# SpringBoot快速入门\n\n## Maven项目手动引入SpringBoot依赖方式\n\n1. 引入父坐标\n\n   ```xml\n   <parent>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-starter-parent</artifactId>\n       <version>2.2.6.RELEASE</version>\n   </parent>\n   ```\n\n2. 引入web依赖\n\n   ```xml\n   <dependency>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-starter-web</artifactId>\n   </dependency>\n   ```\n\n3. 引入插件\n\n   ```xml\n   <plugin>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-maven-plugin</artifactId>\n       <version>2.2.6.RELEASE</version>\n   </plugin>\n   ```\n\n4. 创建SpringBootApplication核心启动类\n\n   ```java\n   @SpringBootApplication\n   public class HelloApplication {\n       public static void main(String[] args) {\n           SpringApplication.run(HelloApplication.class, args);\n       }\n   }\n   ```\n\n5. 创建Controller进行测试\n\n   ```java\n   @RestController\n   public class HelloController {\n       @RequestMapping(\"/hello\")\n       public String hello(@RequestParam(value = \"name\", defaultValue = \"tao\") String name) {\n           return \"hello \" + name;\n       }\n   }\n   ```\n\n6. 访问默认的8080端口测试`/hello`返回的结果（不带name参数为`hello tao`，否则为`hello name值`）\n\n**补充：要返回JSP页面需要引入tomcat-embed-jasper依赖，并且此依赖不能与servlet-api依赖冲突**\n\n```xml\n<dependency>\n    <groupId>org.apache.tomcat.embed</groupId>\n    <artifactId>tomcat-embed-jasper</artifactId>\n    <scope>provided</scope>\n</dependency>\n```\n\n## 利用Spring Initialzr快速创建SpringBoot项目\n\n<img src=\"/images/image-20210909213009923.png\" alt=\"image-20210909213009923\" style=\"zoom: 80%;\" />\n\n# SpringBoot起步依赖原理\n\n* 在spring-boot-starter-parent中定义了各种技术的版本信息，组合了一套最优搭配的版本\n\n* 在各种starter中，定义了该功能所需要的坐标合集，其中大部分版本信息来自于父工程\n* 我们的工程继承于parent，引入starter后，通过依赖传递，就可以简单方便的获取需要的jar包，并且不会导致版本冲突等问题\n\n# SpringBoot配置文件\n\n## 配置文件分类\n\n> SpringBoot是基于约定的，所以很多的配置都有默认值，如果想使用自己的配置替换默认配置的话，就可以使用application.properties或者application.yml（也可以是yaml后缀）进行配置\n\n* **properties方式**\n\n  ```bash\n  server.port=8080\n  ```\n\n* **yml方式**\n\n  ```apl\n  server:\n    port: 8080\n  ```\n\n\n**总结：**\n\n* SpringBoot提供了2种配置文件的类型：properties和yml\n* 默认的配置文件名称：application\n* 在同一级目录下的优先级为：properties > yml > yaml\n* <font color=red>自定义配置文件名：</font>\n  * 后缀类型也要是properties或yml\n  * 启动参数`--spring.config.name=自定义配置文件名`（参数的自定义配置文件名不需要加类型后缀）\n\n<font color=red>**注意：**IDEA对`.yaml`的配置文件没有语法提示的解决办法：</font>\n\n* 在IDEA中设置模块的Facet：![image-20211116223539830](images/image-20211116223539830.png)\n* 如果配置文件名称不是application可能导致添加配置时点不了ok按钮，需要设置上图`spring.config.name`\n\n## yaml\n\n> YAML全程是YAML Ain't Markup Language。YAML是一种直观的能够被电脑识别的数据序列化格式，并且容易被人类阅读，容易和脚本语言交互的，可以被支持YAML库的不同编程语言程序导入。比如C/C++，Ruby，Python，Java，Perl，C#，PHP等。YML文件是以数据为核心的，比传统的xml方式更加简洁。扩展名可以为`.yml`或者.`yaml`\n\n* **properties方式：**\n\n  ```bash\n  server.port=8080\n  server.address=127.0.0.1\n  ```\n\n* **xml方式：**\n\n  ```xml\n  <server>\n      <port>8080</port>\n      <address>127.0.0.1</address>\n  </server>\n  ```\n\n* **yml方式：**\n\n  ```yaml\n  server:\n    port: 8080\n    address: 127.0.0.1\n  ```\n\n### YAML基本语法：\n\n  * 大小写敏感\n  * 数据值前面必须有空格\n  * 使用缩进表示层级关系\n  * 缩进时不允许使用Tab键，只允许使用空格（各个系统Tab对应的空格数目可能不同，导致层次混乱）\n  * 缩进的空格数目不重要，只要相同层级的元素左侧对齐即可\n  * #表示注释，从这个字符一直到行尾都会被解析器忽略\n\n### YAML数据格式\n\n* 对象（map）：键值对集合\n\n  ```yaml\n  person:\n    name: zhangsan\n  # 行内写法\n  person: {name: zhangsan}\n  \n  map: {key1: value1,key2: value2}\n  ```\n\n* 数组：一组按次序排序的值\n\n  ```yaml\n  address:\n    - beijing\n    - shanghai\n  # 行内写法\n  address: [beijing,shanghai]\n  ```\n\n* 对象集合：利用`List<User> users`接收\n\n  ```yaml\n  # 两个user对象\n  users:\n    - name: 张三\n      age: 20\n    - name: 李四\n      age: 22\n  ```\n\n* 纯量：单个的、不可再分的值\n\n  ```yaml\n  msg1: 'hello \\ n world' # 单引号或者不加任何引号忽略转义字符\n  msg2: \"hello \\n world\" # 双引号识别转义字符\n  ```\n\n### YAML参数引用\n\n`${配置项名}`\n\n## 读取配置文件内容\n\n### @Value\n\n直接取出配置文件中某个完整字段的值赋值给成员属性，此类必须为Spring组件\n\n```java\n@Value(\"${name}\")\nprivate String name;\n\n@Value(\"${person.name}\")\nprivate String personName;\n@Value(\"${person.age}\")\nprivate String personAge;\n\n# 读取数组可以直接用List或Set接收\n@Value(\"${address[0]}\")\nprivate String address1;\n@Value(\"${address[1]}\")\nprivate String address2;\n\n@Value(\"${msg1}\")\nprivate String msg1;\n@Value(\"${msg2}\")\nprivate String msg2;\n```\n\n**冷知识：**\n\n* `@Value(\"#{beanName}\")`：可用于注入一个bean，类似于@Resource， 表示SpEl表达式通常用来获取bean的属性，或者调用bean的某个方法。当然还有可以表示常量\n\n* `@Value(${配置文件的属性})`：获取配置文件中的属性值\n\n* 它俩可以结合使用：比如：`@Value(\"#{'${spring.hhh}'.split(',')}\")`是一个结合使用的案例，可以把配置文件的属性键spring.hhh对应的值解析为List注入\n\n  ```yaml\n  spring:\n    hh: 1,2,3,a\n  ```\n\n  ```java\n  @Value(\"#{'${spring.hh}'.split(',')}\")\n  private List list;\n  ```\n  \n* `@value(${配置文件的属性:默认值})`：用于给此配置属性设定默认值\n\n### @ConfigurationProperties\n\n|        功能        | @ConfigurationProperties |          @Value          |\n| :----------------: | :----------------------: | :----------------------: |\n|      松散绑定      |           支持           | 仅有限支持驼峰和横杠转换 |\n|        SpEL        |          不支持          |           支持           |\n| 元数据支持（提示） |           支持           |          不支持          |\n|  JSR 303数据校验   |           支持           |          不支持          |\n|    复杂类型封装    |           支持           |          不支持          |\n\n* <font color=red>**作用**：读取配置文件的配置项绑定到此配置类的成员属性上。</font>\n\n* **说明**：此注解**需要配置一个`prefix`\"前缀\"参数**，例如：`@ConfigurationProperties(prefix = \"person\")`，表示读取此前缀下的内容到**此注解修饰的类中**。如果不配置前缀，会导致读取到第一个符合此类中成员属性名的字段时就赋值。\n\n* <font color=red>**注册此配置类为Bean**：</font>\n  \n  * 通过给配置类打上@Component把配置类注册为Spring的Bean\n* 通过在@SpringBootApplication注解所在的启动类上使用**@EnableConfigurationProperties**(参数为配置类的class对象)把配置类注册为Spring的Bean。\n  \n* **获取配置类的Bean（注入）**：\n\n  * **属性注入**：通过@Autowired注入配置类的Bean到成员属性中：<img src=\"/images/image-20210914010646993.png\" alt=\"image-20210914010646993\" style=\"zoom: 80%;\" />\n\n  * **构造器注入**：把配置类作为参数赋值给成员属性<img src=\"/images/image-20210914010703668.png\" alt=\"image-20210914010703668\" style=\"zoom: 67%;\" />\n\n  * **Setter注入**：利用Setter给成员属性赋值：<img src=\"/images/image-20210914170108938.png\" alt=\"image-20210914170108938\" style=\"zoom: 75%;\" />\n\n    *以上这三种方式都可以把配置类注册到Spring容器中，并且通过构造方法注入参数时可以省略@Autowired，但进行属性注入时不能注入到`final`属性中，所以不推荐使用字段（属性）注入*\n\n* **搭配@Bean使用**：\n\n  * @ConfigurationProperties注解也可以用在@Bean修饰的public方法上，给此方法返回值所产生的Bean的成员属性赋值，并且可以省略赋值语句完成自动赋值，只需无脑return new 需要的Bean()就行了\n\n    *这种方式在读取配置文件给第三方依赖的Bean赋值时特别有用，例如配置不含起步依赖的druid连接池时利用此方法产生DruidDataSource的Bean并且赋值一些从配置文件中读取到的配置给此Bean*\n\n* **注意**：\n\n  * 需要提供Setter方法，也可以利用lombok的@Data注解\n  * 也可以通过@ConstructorBinding进行**构造器绑定**，此时需要全参构造，并且产生Bean不能通过常规的@Bean或@Component或@Import，而需要@EnableConfigurationProperties(参数为配置类的class对象)注解来注入配置类的Bean或利用@ConfigurationPropertiesScan配置属性扫描\n\n```java\n@Component\n@ConfigurationProperties(prefix = \"person\")\npublic class Person {\n    private String name;\n    private Integer age;\n    private String[] address;\n    //需要Getter/Setter\n}\n```\n\n可以读取以下yml文件的内容：\n\n```yaml\nperson:\n  name: ${name}\n  age: 20\n  address: [beijing,shanghai]\n```\n\n**建议配置注解处理器以提供配合@ConfigurationProperties注解编写配置文件的语法提示：**\n\n> <font color=red>**松散绑定**</font>：\n>\n> 对于类中的成员属性名，例如：firstName\n>\n> 配置文件中可选的松散绑定名称：firstName、first-name、first_name、FIRST_NAME\n>\n> 但松散匹配注解@ConfigurationProperties和@Value的参数**不能出现大写子母**（实际配置项可以松散绑定）\n\n* 引入注解处理器的依赖：\n\n  ```xml\n  <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-configuration-processor</artifactId>\n      <optional>true</optional>\n  </dependency>\n  ```\n\n* ctrl+F9重新构建项目，使注解处理器生效即可在编写配置文件时触发@ConfigurationProperties语法提示，此操作会编译产生一个`META-INF/spring-configuration-metadata.json`文件，语法提示依赖此文件\n\n**数据校验需要配合JSR 303依赖，然后给类打上@Validated注解，再配合具体注解进行校验。内部类需要@Valid**\n\n### Environment\n\n* 注入Environment对象\n\n  ```java\n  @Autowired\n  private Environment environment;\n  ```\n\n* 使用此对象的`getProperty(String s)`方法获取`s`对应的值\n\n  ```java\n  environment.getProperty(\"name\"));\n  environment.getProperty(\"person2.age\"));\n  environment.getProperty(\"address[0]\"));\n  environment.getProperty(\"msg1\"));\n  ```\n\n## profile\n\n> 我们在开发SpringBoot应用时，通常同一套程序会被安装到不同的环境，例如：开发、测试、生产等。其中数据库地址、服务器端口等配置都不同，如果每次打包时都修改配置会非常麻烦。profile就是来进行动态配置切换的。\n\n* profile配置方式\n\n  * 多profile文件方式（properties或yml都可以）\n\n    * 创建`application-环境名1.properties`\n    * 创建`application-环境名2.properties`\n    * 创建`application-环境名3.properties`\n    * 在application.properties中指定`spring.profiles.active=环境名`即可使用上面创建的多个文件的某一个\n\n  * yml多文档方式\n\n    * 在一个yml文件中利用`---`分割多个配置文档，并利用`spring.config.activate.on-profile`指定环境名\n\n      ```yml\n      ---\n      server:\n        port: 8191\n      spring:\n        config:\n          activate:\n            on-profile: dev\n      ---\n      server:\n        port: 8192\n      spring:\n        config:\n          activate:\n            on-profile: pro\n      ---\n      server:\n        port: 8193\n      spring:\n        config:\n          activate:\n            on-profile: test\n      ```\n\n    * 继续在此yml文件中指定使用哪个环境：\n\n      ```yml\n      ---\n      spring:\n        profiles:\n          active: test #启用上面的test环境\n      ```\n    \n    **值得注意的是：yml配置的优先级低于properties，即使在yml文档中指定环境名也会先找此环境名的properties文件**\n\n* profile激活方式\n\n  * **配置文件**\n\n    * properties文件激活profile：`spring.profiles.active=环境名`\n    * yml文档激活profile：`spring.profiles.active: 环境名`\n\n  * **虚拟机参数**\n\n    * 在IDEA中配置VM参数：<img src=\"/images/image-20210909235501673.png\" alt=\"image-20210909235501673\" style=\"zoom:80%;\" />\n    * <img src=\"/images/image-20210909235542637.png\" alt=\"image-20210909235542637\" style=\"zoom: 80%;\" /> \n    * 虚拟机选项配置为：`-Dspring.profiles.active=环境名`（优先级高于配置文件的方式）\n\n  * **命令行参数**\n\n    * 在IDEA中配置程序参数：<img src=\"/images/image-20210909235501673.png\" alt=\"image-20210909235501673\" style=\"zoom:80%;\" />\n    * <img src=\"/images/image-20210909235830212.png\" alt=\"image-20210909235830212\" style=\"zoom: 80%;\" /> \n    * 程序参数配置为：`--spring.profiles.active=环境名`（优先级最高）\n\n    **在控制台利用命令行启动**：`java -jar ./springboot项目jar包 --spring.profiles.active=环境名`\n\n## 内部配置加载顺序\n\n1. `file:./config/`：当前**项目**的/config目录下\n2. `file:./`：当前**项目**的根目录\n3. `classpath:/config/`：classpath的/config目录（默认classpath为resources目录）\n4. `classpath:/`：classpath的根目录（默认classpath为resources目录）\n\n**优先级从高到低，所有配置文件都会被读取，但会以高优先级的文件属性为准**\n\n## 外部配置加载顺序\n\n**由于maven的package命令不会打包当前项目的pom.xml所在层级的文件，所以要手动指定外部配置**\n\n1. jar包所在目录下的config目录下的application.properties会被自动读取，优先级最高\n2. jar包所在目录下的application.properties会被自动读取，优先级次之\n\n**也可以利用`java -jar ./项目jar包 --配置项名=配置值`在执行时指定配置**\n\n**还可以利用`java -jar ./项目jar包 --spring.config.location=配置文件的绝对地址 `引用磁盘上的配置**\n\n# SpringBoot整合其他框架\n\n## SpringBoot整合Junit\n\n1. 引入test的起步依赖和junit坐标\n\n   ```xml\n   <dependency>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-starter-test</artifactId>\n       <scope>test</scope>\n   </dependency>\n   <dependency>\n       <groupId>junit</groupId>\n       <artifactId>junit</artifactId>\n       <version>4.12</version>\n       <scope>test</scope>\n   </dependency>\n   ```\n\n2. 编写测试类，打上@RunWith(SpringRunner.class)注解和@SpringBootTest注解\n\n   ```java\n   @RunWith(SpringRunner.class)\n   @SpringBootTest\n   ```\n\n3. 为测试方法打上@Test注解\n\n**注意：如果需要注入java目录下的类，需要保证test目录下的目录层次与引导类的包结构层次一致，否则需要给@SpringBootTest注解指定classes属性为引导类的class**\n\n```java\n@SpringBootTest(classes = SpringBootJunitApplication.class)\n```\n\n**如果目录层次一致可以省略classes属性，但要保证类位于SpringBootApplication注解类所在的包结构层次及子包层次，因为默认扫描**\n\n## SpringBoot整合Redis\n\n1. 引入Redis起步依赖\n\n   ```xml\n   <dependency>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-starter-data-redis</artifactId>\n   </dependency>\n   ```\n\n2. 注入使用RedisTemplate模板，调用相应的方法\n\n   ```java\n   @Autowired\n   private RedisTemplate redisTemplate;\n   ```\n\n## SpringBoot整合MyBatis\n\n1. 引入mybatis起步依赖，添加mysql驱动\n\n   ```xml\n   <dependency>\n       <groupId>org.mybatis.spring.boot</groupId>\n       <artifactId>mybatis-spring-boot-starter</artifactId>\n       <version>2.2.0</version>\n   </dependency>\n   <dependency>\n       <groupId>mysql</groupId>\n       <artifactId>mysql-connector-java</artifactId>\n       <scope>runtime</scope>\n   </dependency>\n   ```\n\n2. 编写DataSource和MyBatis相关配置\n\n   ```yaml\n   # 配置datasource\n   spring:\n     datasource:\n       driver-class-name: com.mysql.jdbc.Driver\n       url: jdbc:mysql:///db3\n       username: root\n       password: 199988\n   # 配置MyBatis的XML\n   mybatis:\n     mapper-locations: classpath:mapper/*Mapper.xml #Mapper映射文件的位置\n     #别名扫描的包，例如User类会被扫描为user或User的别名\n     type-aliases-package: com.taoyyz.springbootmybatis.domain\n   ```\n\n3. 定义表和实体类\n\n4. 编写dao/mapper文件或注解开发\n\n   * 注解开发：此时的UserMapper是一个Spring容器中的Bean，可以直接调用方法\n\n     ```java\n     @Mapper\n     public interface UserMapper {\n         @Select(\"select * from user\")\n         List<User> findAll();\n     }\n     ```\n\n   * XML开发：需要在XML中编写SQL，UserXmlMapper也是一个Spring Bean\n     * 编写Mapper接口\n\n         ```java\n         @Mapper\n         public interface UserXmlMapper {\n             List<User> findAll();\n         }\n         ```\n\n     * 配置Mapper.xml\n\n         ```xml\n         <?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n         <!DOCTYPE mapper\n                 PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n                 \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n         <mapper namespace=\"com.taoyyz.springbootmybatis.mapper.UserXmlMapper\">\n             <select id=\"findAll\" resultType=\"user\">\n                 select * from user\n             </select>\n         </mapper>\n         ```\n\n## SpringBoot整合Druid\n\n1. 引入Druid起步依赖\n\n   ```xml\n   <dependency>\n       <groupId>com.alibaba</groupId>\n       <artifactId>druid-spring-boot-starter</artifactId>\n       <version>1.1.21</version>\n   </dependency>\n   ```\n\n2. 配置SpringBoot核心配置文件\n\n   * 方式1：\n\n     ```yaml\n     spring:\n       datasource:\n         url: jdbc:mysql://120.79.141.53:3307/mimile?useSSL=false\n         username: root\n         password: 123\n         driver-class-name: com.mysql.jdbc.Driver\n         type: com.alibaba.druid.pool.DruidDataSource\n     ```\n\n   * 方式2：\n\n     ```yaml\n     spring:\n       datasource:\n         druid:\n           url: jdbc:mysql://120.79.141.53:3307/mimile?useSSL=false\n           username: root\n           password: 123\n           driver-class-name: com.mysql.jdbc.Driver\n     ```\n\n# SpringBoot原理分析\n\n## SpringBoot自动配置\n\n### Conditional条件判断\n\n<img src=\"/images/image-20210910235324694.png\" alt=\"image-20210910235324694\" style=\"zoom:80%;\" /> \n\n> Conditional是Spring 4.0增加的条件判断功能，通过这个功能可以实现选择性的创建Bean操作\n\n**用法**：\n\n1. @Conditional注解打在产生Bean的方法上，常配合@Bean注解实现条件判断创建Bean\n\n2. @Conditional注解的参数属性需要一个实现了`Condition`函数式接口的条件类的Class字节码对象\n\n3. 在这个Condition函数式接口实现类中重写`boolean matches(条件context参数,元注解metadata参数)`方法\n\n4. 根据此`matches()`方法返回的结果进行条件判断，为true就加载Bean，否则false不加载\n\n5. SpringBoot提供了一些@ConditionalOnXxx的注解用于条件判断\n\n   例如：`@ConditionalOnProperty(name = \"tjj\", havingValue = \"22\")`可以判断配置文件是否有tjj=22的属性\n\n|      @Conditional扩展注解       |                作用（条件判断）                |\n| :-----------------------------: | :--------------------------------------------: |\n|       @ConditionalOnJava        |           系统的Java版本是否符合要求           |\n|       @ConditionalOnBean        |              容器中存在指定的Bean              |\n|    @ConditionalOnMissingBean    |             容器中不存在指定的Bean             |\n|    @ConditionalOnExpression     |                 满足SpEL表达式                 |\n|       @ConditionalOnClass       |                系统中有指定的类                |\n|   @ConditionalOnMissingClass    |               系统中没有指定的类               |\n|  @ConditionalOnSingleCandidate  | 容器中只有一个指定的Bean，或这个Bean为首选Bean |\n|     @ConditionalOnProperty      |         系统中指定的属性是否有指定的值         |\n|     @ConditionalOnResource      |          类路径下是否存在指定资源文件          |\n|  @ConditionalOnWebApplication   |                 当前是web环境                  |\n| @ConditionalOnNotWebApplication |                当前不是web环境                 |\n|       @ConditionalOnJndi        |                 JNDI存在指定项                 |\n\n### 切换内置Web服务器\n\n> SpringBoot的web环境默认使用tomcat作为内置服务器，提供了4种内置服务器供我们选择\n\n![image-20210911002022854](/images/image-20210911002022854.png) \n\n* 切换到`Jetty`\n\n  1. 在pom.xml排除`spring-boot-starter-web`起步依赖中的`spring-boot-starter-tomcat`依赖\n\n  2. 引入jetty的依赖\n\n     ```xml\n     <dependency>\n         <groupId>org.springframework.boot</groupId>\n         <artifactId>spring-boot-starter-web</artifactId>\n         <exclusions>\n             <!--排除掉tomcat的依赖-->\n             <exclusion>\n                 <artifactId>spring-boot-starter-tomcat</artifactId>\n                 <groupId>org.springframework.boot</groupId>\n             </exclusion>\n         </exclusions>\n     </dependency>\n     <!--引入jetty的依赖-->\n     <dependency>\n         <groupId>org.springframework.boot</groupId>\n         <artifactId>spring-boot-starter-jetty</artifactId>\n     </dependency>\n     ```\n\n### @Enable*注解\n\n> SpringBoot中提供了很多Enable开头的注解，这些注解都是用于动态启用某些功能的。而**底层原理是使用@Import导入一些配置类**实现Bean的动态加载\n\n* 默认情况下@SpringBootApplication只会扫描**当前包及其子包**的Bean，所以其他模块的Bean获取不到\n\n* 获取其他模块的Bean的几种方式：\n\n  * **使用@ComponentScan指定扫描的包**\n\n    ```java\n    @ComponentScan(\"com.taoyyz.config\")\n    ```\n\n  * **使用@Import注解加载类，被@Import导入的类都会被Spring容器创建和管理**\n\n    ```java\n    @Import(UserConfiguration.class)\n    ```\n\n  * **对@Import注解进行封装**\n\n    * 在其他模块中创建一个EnableXxx的注解，在此注解中利用@Import加载类，只需要在使用时@EnableXxx即可\n\n      ```java\n      @Target({ElementType.TYPE})\n      @Retention(RetentionPolicy.RUNTIME)\n      @Documented\n      @Import(UserConfiguration.class)\n      public @interface EnableUser {\n      }\n      ```\n\n    * 在@SpringBootApplication配置类上调用@EnableUser注解即可\n\n### @Import注解\n\n> @Enable*底层依赖于@Import注解导入一些类，使用@Import导入的类会被Spring加载到Ioc容器中，而@Import提供4种用法\n\n#### 导入Bean\n\n```java\n@Import(User.class) //通过字节码Import的Bean在容器中的名称为全限定名com.taoyyz.domain.User\n@Import(Role.class) //同理，在容器中的名称为全限定名com.taoyyz.domain.Role\n```\n\n#### 导入配置类\n\n* 创建配置类\n\n  ```java\n  public class UserConfiguration {\n      @Bean(\"user\")\n      public User getUser() {\n          return new User();\n      }\n  }\n  ```\n\n* 导入配置类\n\n  ```java\n  @Import(UserConfiguration.class) //导入配置类的字节码，从字节码中获得Bean，Bean的名称为@Bean规定的\n  ```\n\n#### 导入ImportSelector实现类。一般用于加载配置文件中的类\n\n* 创建ImportSelector实现类\n\n  ```java\n  public class MyImportSelector implements ImportSelector {\n      @Override\n      public String[] selectImports(AnnotationMetadata annotationMetadata) {\n          return new String[]{\"com.taoyyz.domain.User\", \"com.taoyyz.domain.Role\"};\n      }\n  }\n  ```\n\n* 导入此实现类\n\n  ```java\n  @Import(MyImportSelector.class)\n  ```\n\n#### 导入ImportBeanDefinitionRegistrar实现类\n\n* 创建ImportBeanDefinitionRegistrar实现类\n\n  ```java\n  public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {\n      @Override\n      public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {\n          AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();\n          registry.registerBeanDefinition(\"user\",beanDefinition);\n      }\n  }\n  ```\n  \n* 导入此实现类\n\n  ```java\n  @Import(MyImportBeanDefinitionRegistrar.class)\n  ```\n\n### @EnableAutoConfiguration注解\n\n> 由于@SpringBootApplication注解被@EnableAutoConfiguration注解修饰，而@EnableAutoConfiguration注解又导入了AutoConfigurationImportSelector的字节码文件\n\n<img src=\"/images/image-20210911151122395.png\" alt=\"image-20210911151122395\" style=\"zoom:80%;\" />\n\n* AutoConfigurationImportSelector类的`getCandidateConfigurations()`方法会读取`spring.factories`配置文件，根据@Conditional条件加载配置类初始化Bean\n* 只有满足了@Conditional条件的Bean才会被初始化\n\n### 自定义starter\n\n* 要求：自定义redis-starter，当导入redis坐标时，SpringBoot自动创建Jedis的Bean\n\n* 实现步骤：\n\n  * 创建redis-spring-boot-autoconfigure模块，初始化Jedis的Bean，并定义META-INF/spring.factories文件\n\n    * 创建RedisAutoConfiguration配置类，用于通过配置信息产生Bean\n\n      ```java\n      @Configuration\n      @EnableConfigurationProperties(RedisProperties.class)\n      @ConditionalOnClass(Jedis.class)\n      public class RedisAutoConfiguration {\n          /**\n           * 提供Jedis的Bean\n           */\n          @Bean(\"jedis\")\n          @ConditionalOnMissingBean(name = \"jedis\") //如果没有叫做jedis的bean才加载\n          public Jedis getJedis(RedisProperties props) {\n              System.out.println(\"config的bean\");\n              return new Jedis(props.getHost(), props.getPort());\n          }\n      }\n      ```\n\n    * 创建RedisProperties配置读取类，读取application.properties或yml文件的属性赋值给配置成员属性\n\n      ```java\n      @ConfigurationProperties(prefix = \"redis\") //绑定以redis开头的properties配置，赋值给成员属性\n      @Data //利用lombok产生Getter/Setter\n      public class RedisProperties {\n          private String host = \"localhost\";\n          private int port = 6379;\n      }\n      ```\n\n    * 在resources目录下创建META-INF目录，编辑`spring.factories`文件\n\n      ```properties\n      org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\n      com.taoyyz.config.RedisAutoConfiguration\n      ```\n\n    * 引入jedis依赖\n\n      ```xml\n      <!--引入jedis依赖-->\n      <dependency>\n          <groupId>redis.clients</groupId>\n          <artifactId>jedis</artifactId>\n          <version>2.9.0</version>\n      </dependency>\n      ```\n\n  * 创建redis-spring-boot-starter模块，依赖redis-spring-boot-autoconfigure模块\n\n    ```xml\n    <!--引入autoconfigure的依赖-->\n    <dependency>\n        <groupId>com.taoyyz</groupId>\n        <artifactId>redis-spring-boot-autoconfigure</artifactId>\n        <version>0.0.1-SNAPSHOT</version>\n    </dependency>\n    ```\n\n  * 在测试模块中引入自定义的redis-starter起步依赖，并测试是否能根据依赖引入自动配置获得Bean操作redis\n\n    ```xml\n    <!--引入自定义的redis起步依赖-->\n    <dependency>\n        <groupId>com.taoyyz</groupId>\n        <artifactId>redis-spring-boot-starter</artifactId>\n        <version>0.0.1-SNAPSHOT</version>\n    </dependency>\n    ```\n\n## SpringBoot监听机制\n\n### Java监听机制\n\n> SpringBoot的监听机制是对Java提供的监听机制的封装\n\nJava的事件监听机制定义了以下几个角色：\n\n* 事件：Event，继承java.util.EventObject类的对象\n* 事件源：Srouce，任意Object对象\n* 监听器：Listener，实现了java.util.EventListener接口的对象\n\n### SpringBoot监听机制\n\nSpringBoot在项目启动时，会对几个监听器进行回调，我们可以实现这些监听器接口，在项目启动时完成一些操作\n\n<img src=\"/images/image-20210911170816982.png\" alt=\"image-20210911170816982\" style=\"zoom:67%;\" />\n\n#### ApplicationContextInitializer\n\n* 位于`contextPrepared()`上下文准备之前，打印SpringBoot的启动banner之后调用此`initialize()`方法\n\n* 需要手动配置META-INF下的`spring.factories`\n\n  ```properties\n  org.springframework.context.ApplicationContextInitializer=\\\n    com.taoyyz.springbootlistener.listener.MyApplicationContextInitializer\n  ```\n\n#### SpringApplicationRunListener\n\n* 贯穿整个SpringBoot启动过程，需要一个带参构造方法\n\n  ```java\n  public class MySpringApplicationRunListener implements SpringApplicationRunListener {\n      public MySpringApplicationRunListener(SpringApplication application, String[] args) {\n          System.out.println(\"application = \" + application);\n          System.out.println(Arrays.toString(args));\n      }\n  }\n  ```\n\n* 需要手动配置META-INF下的`spring.factories`\n\n  ```properties\n  org.springframework.boot.SpringApplicationRunListener=\\\n    com.taoyyz.springbootlistener.listener.MySpringApplicationRunListener\n  ```\n\n#### CommandLineRunner\n\n* 项目启动结束后执行`run()`方法\n\n#### ApplicationRunner\n\n* 项目启动结束后执行`run()`方法\n\n## SpringBoot启动流程分析\n\n![SpringBoot启动流程](images\\SpringBoot启动流程.png)\n\n* `@EnableAutoConfiguration`利用了`@Import(AutoConfigurationImportSelector.class)`\n\n* `AutoConfigurationImportSelector`类实现了`ImportSelector`接口的<font color=red>`selectImports(AnnotationMetadata annotationMetadata)`</font>方法，此方法中调用<font color=red>`getAutoConfigurationEntry(annotationMetadata)`</font>返回一个`AutoConfigurationEntry`对象（此对象含有一个`List<String>`类型的`configurations`和一个`Set<String>`类型的 `exclusions`）\n\n* 在上述的`getAutoConfigurationEntry`方法中调用了<font color=red>`getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes)`</font>方法，来获取候选配置，此方法内部利用`SpringFactoriesLoader`的`loadFactoryNames(Class<?> factoryType, ClassLoader classLoader)`方法中调用`loadSpringFactories(ClassLoader classLoader)`方法去读取`SpringFactoriesLoader`中一个静态常量：`public static final String FACTORIES_RESOURCE_LOCATION = \"META-INF/spring.factories\"`\n\n* 在`loadSpringFactories(ClassLoader classLoader)`方法中加载所有`spring.factories`文件\n\n  ![image-20210913024040772](/images/image-20210913024040772.png)\n  \n* 然后`loadFactoryNames(Class<?> factoryType, ClassLoader classLoader)`方法会把上一步方法的返回值（一个多值`Map<String, List<String>>`）调用`getOrDefault(factoryTypeName, Collections.emptyList())`，这里传入的`factoryTypeName`其实就是`EnableAutoConfiguration`的全类名，也就是筛选出`EnableAutoConfiguration`注解所自动配置的key对应的value（这里value为`List<String>`），此时`getCandidateConfigurations(annotationMetadata, attributes)`方法结束。\n\n* 然后`getAutoConfigurationEntry`方法的下一步调用`removeDuplicates(configurations)`移除重复项目，这里利用了HashSet去重，根据HashSet内容构造一个ArrayList并返回\n\n* 下一步调用`getExclusions(annotationMetadata, attributes)`返回一个排除的`Set<String>`\n\n* 下一步调用`checkExcludedClasses(configurations, exclusions)`检查需要排除的Class是否为无效Class，检查无效首先在`ClassUtils.isPresent()`方法中利用`forName()`尝试加载这个exclusions的className，如果发生异常立即报错并返回`false`，其次即使`forName()`可以成功加载，还需要满足`!configurations.contains(exclusion)`，也就是自动配置列表不包含exclusion这个需要排除的项时取反为`true`就会把此exclusion加入到`invalidExcludes`这个`List<String>`中去统一报错处理\n\n* 如果没有报错，说明上一步成功排除，下一步就调用`configurations.removeAll(exclusions)`移除正确的排除项\n\n* 下一步让所有配置在`META-INF/spring.factories`下的`AutoConfigurationImportListener`执行`AutoConfigurationImportEvent`事件\n\n* 最终`getAutoConfigurationEntry(annotationMetadata)`执行完毕，返回`AutoConfigurationEntry`中的`configurations`的`String[]`形式\n\n# SpringBoot监控\n\n> SpringBoot自带监控功能Actuator，可以实现对程序内部运行情况监控，例如监控状况、Bean加载情况、配置属性、日志信息等\n\n* 使用步骤：\n\n  1. 导入依赖坐标\n\n     ```xml\n     <dependency>\n         <groupId>org.springframework.boot</groupId>\n         <artifactId>spring-boot-starter-actuator</artifactId>\n     </dependency>\n     ```\n\n  2. 访问http://localhost:8080/actuator\n\n* 配置监控：\n\n  ```properties\n  # 开启info监控\n  management.endpoint.info.enabled=true\n  # 暴露所有端口\n  management.endpoints.web.exposure.include=*\n  # 开启健康监控完整信息\n  management.endpoint.health.show-details=always\n  ```\n\n* web图形化监控：admin依赖\n\n  1. 创建server端项目，引入server端依赖：（可能需要依赖管理中指定版本）\n\n     ```xml\n     <dependency>\n         <groupId>de.codecentric</groupId>\n         <artifactId>spring-boot-admin-starter-server</artifactId>\n     </dependency>\n     ```\n\n  2. 在SpringBoot配置文件中配置server端访问端口：\n\n     ```properties\n     server.port=9000\n     ```\n\n  3. 开启server端注解，此时可以启用server端项目了\n\n     ```java\n     @EnableAdminServer\n     @SpringBootApplication\n     public class SpringBootAdminServerApplication {\n         public static void main(String[] args) {\n             SpringApplication.run(SpringBootAdminServerApplication.class, args);\n         }\n     }\n     ```\n\n  4. 在client端中引入client端依赖：\n\n     ```xml\n     <dependency>\n         <groupId>de.codecentric</groupId>\n         <artifactId>spring-boot-admin-starter-client</artifactId>\n     </dependency>\n     ```\n\n  5. 在client端的SpringBoot配置文件中配置client端的url（可根据需要暴露监控端口等）\n\n     ```properties\n     # 开启admin.server图形化监控\n     spring.boot.admin.client.url=http://localhost:9000\n     ```\n\n  6. 访问`http://localhost:9000`即可进入图形化监控\n\n# SpringBoot异常处理\n\n* 使用全局异常处理器\n\n  ```java\n  //@RestControllerAdvice\n  @ControllerAdvice\n  public class MyExceptionHandler {\n      //拦截异常\n      @ExceptionHandler(Exception.class)\n  \tpublic JsonResponse exceptionHandler(Exception e){\n          //处理异常逻辑，可以是记录日志等操作\n          e.printStackTrace();\n         \treturn JsonResponse.failure(\"服务器异常，原因是\" + e.getMessage());\n      }\n  }\n  ```\n\n# SpringBoot项目部署\n\n## 启动SpringBoot项目\n\n### 通过jar包的内置tomcat启动\n\n* SpringBoot的Maven项目默认打jar包，其中内置了tomcat服务器可以直接运行\n  * 利用`mvn package`生成jar包\n  * 利用`java -jar ./项目jar包`即可利用SpringBoot内置tomcat容器运行\n\n### 通过war包在外置tomcat启动\n\n* 修改Maven的pom.xml，设置`<packaging>war</packaging>`打包类型为`war`\n\n* 修改`@SpringBootApplication`注解修饰的启动类，使其继承于`SpringBootServletInitializer`类\n\n* 重写启动类中的`configure()`方法，传入启动类的class字节码对象\n\n  ```java\n  @Override\n  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {\n      return builder.sources(SpringBootDeployApplication.class);\n  }\n  ```\n\n* 利用`mvn package`生成war包\n\n* 复制war包到tomcat安装目录的webapps目录下，然后启动外置tomcat服务器\n\n  **访问路径含有上下文路径，也就是此项目在webapps目录下的文件夹名称**\n\n## 热部署SpringBoot项目\n\n### 方式1：重新编译rebuild项目：ctrl+F9\n\n### 方式2：通过插件自动构建部署\n\n1. 引入devtools依赖\n\n   ```xml\n   <dependency>\n       <groupId>org.springframework.boot</groupId>\n       <artifactId>spring-boot-devtools</artifactId>\n   </dependency>\n   ```\n\n2. 开启IDEA自动构建\n\n   <img src=\"/images/image-20210912175546162.png\" alt=\"image-20210912175546162\" style=\"zoom:80%;\" />\n\n3. 允许在**运行时自动构建**\n\n   * 按`ctrl+shift+alt+/`在`维护`窗口选择`注册表`选项：<img src=\"/images/image-20210912175743818.png\" alt=\"image-20210912175743818\" style=\"zoom: 80%;\" />\n   * 勾选`在运行期编译`：<img src=\"/images/image-20210912175848428.png\" alt=\"image-20210912175848428\" style=\"zoom:80%;\" />\n\n4. 刷新页面访问会rebuild项目并热部署","tags":["Java","Spring"],"categories":["Java"]},{"title":"SSM框架基础","url":"/2021/08/20/SSM框架/","content":"\n# Spring\n\n## Spring入门\n\n### Spring简介\n\n* Spring是分层的Java SE/EE应用full-stack轻量级开源框架，以<font color=red>**IoC**</font>（Inverse Of Control：控制反转）和**<font color=red>AOP</font>**（Aspect Oriented Programming）为内核\n* Spring提供了<font color=red>展现层SpringMVC</font>和<font color=red>持久层Spring JDBCTemplate</font>以及<font color=red>业务层事务管理</font>等众多的企业级应用技术，还能整合开源世界众多著名的第三方框架和类库，逐渐成为使用最多的Java EE企业应用开源框架\n\n### Spring发展历程\n\n* 1997年，IBM提出EJB思想\n* 1998年，SUN制定开发标准规范EJB1.0\n* 1999年，EJB1.1发布\n* 2001年，EJB2.0发布\n* 2003年，EJB2.1发布\n* 2006年，EJB3.0发布\n* 2017年9月发布了Spring的最新版本Spring5.0通用版\n\n### Spring的优势\n\n* **<font color=red>方便解耦，简化开发</font>**\n  * 通过Spring提供的IoC容器，可以将对象间的依赖关系交由Spring进行控制，避免硬编码所造成的过度耦合，用户也不必再为单例模式类、属性文件解析等这些底层需求编写代码，而更专注于上层的应用\n* **<font color=red>AOP编程的支持</font>**\n  * 通过Spring的AOP功能，方便进行面向切面编程，许多不容易用传统OOP实现的功能可以通过AOP实现\n* **<font color=red>声明式事务的支持</font>**\n  * 可以将我们从单调烦闷的事务管理代码中解脱出来，通过声明式方式灵活的进行事务管理，提高开发效率\n* **<font color=red>方便程序的测试</font>**\n  * 可以用非容器依赖的编程方式进行几乎所有的测试工作，测试不再是昂贵的操作，而是随手可做的事情\n* **<font color=red>方便集成各种优秀框架</font>**\n  * Spring对各种优秀的框架（Structs、Hibernate、HEssian、Quartz等）的支持\n* **<font color=red>降低Java EE API的使用难度</font>**\n  * Spring对Java EE API（如JDBC、JavaMail、远程调用等）进行了薄薄的封装层，降低了API的使用难度\n* **<font color=red>Java源码的经典学习典范</font>**\n  * Spring的源代码设计精妙、结构清晰、匠心独到，处处体现着大师对Java设计模式的灵活运用以及对Java技术的高深造诣。\n\n### Spring的体系结构\n\n<img src=\"/images/image-20210817142426719.png\" alt=\"image-20210817142426719\" style=\"zoom:67%;\" /> \n\n### Spring快速入门\n\n#### Spring程序开发步骤\n\n<img src=\"/images/image-20210817143126587.png\" alt=\"image-20210817143126587\" style=\"zoom: 50%;\" />\n\n1. 导入Spring开发的基本包坐标：`spring-context`\n\n   <img src=\"/images/image-20210817145031674.png\" alt=\"image-20210817145031674\" style=\"zoom:80%;\" /> \n\n2. 编写dao层接口（UserDao）和实现类（UserDaoImpl）\n\n   `UserDao`：<img src=\"/images/image-20210817145149356.png\" alt=\"image-20210817145149356\" style=\"zoom: 65%;\" />  `UserDaoImpl`：<img src=\"/images/image-20210817145209302.png\" alt=\"image-20210817145209302\" style=\"zoom: 65%;\" />\n\n3. 创建Spring核心配置文件\n\n   ```xml\n   <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n   <beans xmlns=\"http://www.springframework.org/schema/beans\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://www.springframework.org/schema/beans\n                              http://www.springframework.org/schema/beans/spring-beans.xsd\">\n   \t<bean id=\"userDao\" class=\"com.taoyyz.dao.impl.UserDaoImpl\"/>\n   </beans>\n   ```\n\n4. 在Spring配置文件中配置UserDaoImpl\n\n   ```xml\n   <bean id=\"userDao\" class=\"com.taoyyz.dao.impl.UserDaoImpl\"></bean>\n   ```\n\n5. 使用Spring的API获得Bean的实例（而非`new UserDaoImpl()`）\n\n   ![image-20210817145136182](/images/image-20210817145136182.png)\n\n## Spring配置文件\n\n### Bean标签基本配置\n\n* 概述：\n  * 用于配置对象交由Spring来创建\n  * 默认情况下它调用的是类中的无参构造方法，如果没有无参构造方法则不能创建成功\n* 基本属性：\n  * `id`：Bean实例在Spring容器内的唯一标识\n  * `class`：Bean的全限定名称\n\n### Bean标签范围配置\n\n* `scope`属性：对象的作用范围，取值如下：\n\n<img src=\"/images/image-20210817145915770.png\" alt=\"image-20210817145915770\" style=\"zoom: 60%;\" />\n\n* `scope`属性控制下的Bean的创建时机：\n\n  <img src=\"/images/image-20210817151121696.png\" alt=\"image-20210817151121696\" style=\"zoom:60%;\" /> \n\n### Bean的生命周期配置\n\n* `init-method`：为此bean指定一个**初始化**时的方法\n* `destroy-method`：为此bean指定一个**销毁**时的方法\n  * 值得注意的是，在单元测试时的Spring要正常关闭，需要调用`ClassPathXmlApplicationContext`对象的`close()`方法才能正常关闭Spring，以及调用bean的销毁方法\n\n### Bean实例化的三种方式\n\n* **无参构造**方法实例化\n\n  * 定义一个Bean（默认有无参构造）\n\n  * 在配置文件中配置bean的属性：\n\n    ```xml\n    <bean id=\"userDao\" class=\"com.taoyyz.dao.impl.UserDaoImpl\"></bean>\n    ```\n\n    \n\n* **工厂静态**方法实例化\n\n  * 定义一个工厂类，在静态方法里生产Bean：<img src=\"/images/image-20210817152736007.png\" alt=\"image-20210817152736007\" style=\"zoom: 67%;\" />\n\n  * 在配置文件中配置bean的属性：\n\n    ```xml\n    <bean id=\"userDao\" class=\"com.taoyyz.factory.StaticFactory\" \n          factory-method=\"getUserDao\"></bean>\n    ```\n\n* **工厂实例**方法实例化\n\n  * 定义一个工厂类，在实例方法里生产Bean：<img src=\"/images/image-20210817153348508.png\" alt=\"image-20210817153348508\" style=\"zoom:67%;\" />\n\n  * 在配置文件中配置bean的属性：\n\n    ```xml\n    <!--先加载工厂的实例Bean-->\n    <bean id=\"factory\" class=\"com.taoyyz.factory.DynamicFactory\"></bean> \n    <!--再从工厂的实例里调方法-->\n    <bean id=\"userDao\" factory-bean=\"factory\" factory-method=\"getUserDao\"></bean> \n    ```\n\n### Bean的依赖注入DI\n\n#### 概念\n\n* 依赖注入（Dependency Injection）：它是Spring框架核心IoC的具体实现 \n\n#### 注入的方式：\n\n* **构造方法注入**：\n\n  ```xml\n  <bean id=\"userService\" class=\"com.taoyyz.service.impl.UserServiceImpl\">\n  \t<!--name是注入到哪个成员属性，ref是引用哪个bean-->\n  \t<constructor-arg name=\"userDao\" ref=\"userDao\"></constructor-arg>\n  </bean>\n  ```\n\n* **set方法注入**：切割`setXxx()`方法，取`Xxx`部分首字母变小写作为set到的成员属性名\n\n  ```xml\n  <!--配置被依赖的bean-->\n  <bean id=\"userDao\" class=\"com.taoyyz.dao.impl.UserDaoImpl\"></bean>\n  <!--下面的UserService的bean依赖于上述userDao的bean-->\n  <bean id=\"userService\" class=\"com.taoyyz.service.impl.UserServiceImpl\">\n      <!--ref引用userDao这个bean，注入到userServiceImpl的name指定的成员属性里-->\n      <property name=\"userDao\" ref=\"userDao\"></property>\n  </bean>\n  ```\n\n* **简化set方法的p命名空间注入：**\n\n  * 引入p命名空间的xmlns约束：\n\n    ```assembly\n    xmlns:p=\"http://www.springframework.org/schema/p\"\n    ```\n\n  * 在需要注入的bean上利用p标签注入引用：\n\n    ```xml\n    <!--配置被依赖的bean-->\n    <bean id=\"userDao\" class=\"com.taoyyz.dao.impl.UserDaoImpl\"></bean>\n    <!--下面的UserService的bean依赖于上述userDao的bean-->\n    <bean id=\"userService\" class=\"com.taoyyz.service.impl.UserServiceImpl\" p:userDao-ref=\"userDao\"></bean>\n    ```\n\n#### 注入的数据类型\n\n* 上述注入方式都是注入的Bean。除了对象的引用可以注入外，普通数据类型、集合等都可以在容器中注入\n\n* 注入数据的三种数据类型\n\n  * 普通数据类型\n\n    ```xml\n    <bean id=\"userDao\" class=\"com.taoyyz.dao.impl.UserDaoImpl\">\n        <!--name为要注入的成员属性名，value为此成员属性的字面值-->\n        <property name=\"username\" value=\"taoyyz\"></property>\n        <property name=\"age\" value=\"18\"></property>\n    </bean>\n    ```\n\n  * 引用数据类型\n\n  * 集合数据类型\n\n    * List集合\n\n    ```xml\n    <property name=\"strList\">\n    \t<list>\n    \t\t<value>aaa</value>\n    \t\t<value>bbb</value>\n    \t\t<value>ccc</value>\n    \t</list>\n    </property>\n    ```\n\n    * Map集合\n\n    ```xml\n    <property name=\"userMap\">\n    \t<map>\n    \t\t<!--由于map是由一个一个entry组成，每个entry包含key和value-->\n    \t\t<!--这里key是字符串String直接给字面值，但是value是User对象需要给引用-->\n    \t\t<entry key=\"tao\" value-ref=\"user1\"></entry>\n    \t\t<entry key=\"胖\" value-ref=\"user2\"></entry>\n    \t</map>\n    </property>\n    ```\n\n    * Properties集合\n\n    ```xml\n    <property name=\"properties\">\n    \t<props>\n    \t\t<prop key=\"prop1\">第一个prop</prop>\n    \t\t<prop key=\"prop2\">第二个prop</prop>\n    \t</props>\n    </property>\n    ```\n\n### 配置文件的引入\n\n* 将部分配置拆解到其他配置文件中，在Spring主配置文件通过import标签引入即可\n\n  ```xml\n  <import resource=\"要引入的其他Spring配置文件名\"></import>\n  ```\n\n### Spring配置的重点回顾\n\n<img src=\"/images/image-20210817170600266.png\" alt=\"image-20210817170600266\" style=\"zoom:67%;\" /> \n\n## Spring相关API\n\n<img src=\"/images/image-20210817174502199.png\" alt=\"image-20210817174502199\"  />\n\n### ApplicationContext的实现类：\n\n* **ClassPathXmlApplicationContext**\n\n  * 从类的根路径下加载配置文件推荐使用这种\n\n* **FileSystemXmlApplicationContext**\n\n  * 从磁盘路径上加载配置文件，配置文件可以在磁盘的任意位置\n\n* **AnnotationConfigApplicationContext**\n\n  * 使用注解配置容器对象时，使用此类来创建Spring容器，用来读取注解\n\n    <img src=\"/images/image-20210817175414157.png\" alt=\"image-20210817175414157\" style=\"zoom:67%;\" />\n\n### getBean()方法\n\n* 参数为bean的id的字符串，此时总能获取此id对应的Object类型的bean实例\n\n    ```java\n      public Object getBean(String name) throws BeansException {\n              this.assertBeanFactoryActive();\n              return this.getBeanFactory().getBean(name);\n          }\n    ```\n\n* 参数为T类型的Class对象，从容器中匹配T类型的bean实例并返回，如果有多个此类型的bean时会报错\n    ```java\n      public <T> T getBean(Class<T> requiredType) throws BeansException {\n              this.assertBeanFactoryActive();\n              return this.getBeanFactory().getBean(requiredType);\n          }\n    ```\n\n## Spring配置数据源\n\n### 数据源（连接池）的作用\n\n* 数据源（连接池）是提高程序性能出现的\n* 实例化数据源，初始化部分连接资源\n* 使用连接资源时从数据源中获取\n* 使用完毕后归还连接资源给数据源\n\n常见的数据源（连接池）：DBCP、C3P0、BoneCP、Druid等\n\n### 数据源的使用步骤\n\n* 导入数据源的坐标和数据库驱动坐标\n* 创建数据源对象\n* 设置连接数据源的基本连接信息（驱动、数据库URL、用户名、密码）\n* 使用数据源获取连接资源和归还连接资源\n\n### 数据源的手动创建\n\n* 创建数据源对象\n* 设置数据源参数\n* 获取连接\n* 归还连接\n\n### Spring配置数据源的Bean\n\n* 在Spring配置文件中装载数据源为bean，class为此数据源的全类名\n\n* 配置此bean的参数，也就是数据源需要的一些参数（驱动、数据库URL、用户名、密码）\n\n  ```xml\n  <!--装载ComboPooledDataSource对象为bean-->\n  <bean id=\"dataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\">\n      <property name=\"driverClass\" value=\"com.mysql.jdbc.Driver\"/>\n      <property name=\"jdbcUrl\" value=\"jdbc:mysql:///db1\"/>\n      <property name=\"user\" value=\"root\"/>\n      <property name=\"password\" value=\"199988\"/>\n  </bean>\n  ```\n\n* 即可通过Spring的Context上下文去`getBean()`得到此bean（数据源对象），执行后续操作\n\n### Spring加载properties文件\n\n* 在applicationContext.xml配置文件中加载jdbc.properties文件：\n\n  1. 引入context命名空间和约束路径\n\n     * 命名空间：`xmlns:context=\"http://www.springframework.org/schema/context\"`\n\n     * 约束路径：为`xsi:schemaLocation`添加以下两行约束（注意要保证两个同类型约束挨着）：\n\n       ```xml\n       http://www.springframework.org/schema/context\n       http://www.springframework.org/schema/context/spring-context.xsd\n       ```\n\n  2. 利用`context`命名空间下的`property-placeholder`标签加载properties配置文件\n\n  3. 使用SpEL表达式取值\n\n     ```xml\n     <!--加载外部properties文件，在location中指定位置-->\n     <context:property-placeholder location=\"classpath:jdbc.properties\"></context:property-placeholder>\n     <bean id=\"dataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\">\n         <property name=\"driverClass\" value=\"${jdbc.driver}\"/>\n         <property name=\"jdbcUrl\" value=\"${jdbc.url}\"/>\n         <property name=\"user\" value=\"${jdbc.username}\"/>\n         <property name=\"password\" value=\"${jdbc.password}\"/>\n     </bean>\n     ```\n\n## Spring注解开发\n\n* Spring是一种轻代码而重配置的框架，配置比较繁重，影响开发效率，所以注解开发是一种趋势，注解代替XML配置文件可以简化配置，提高开发效率\n\n### Spring原始注解\n\n* 原始注解主要是替代`<bean>`标签的配置\n\n  **原始注解**：\n\n  <img src=\"/images/image-20210817200449058.png\" alt=\"image-20210817200449058\" style=\"zoom: 60%;\" /> \n\n* 使用方式：\n\n  * 用@Component代替所有的`bean`标签的`class`属性，此注解打在对应的类名上\n\n    UserDao（实现类）的bean：<img src=\"/images/image-20210817202516725.png\" alt=\"image-20210817202516725\" style=\"zoom:80%;\" /> \n\n    UserService（实现类）的bean：<img src=\"/images/image-20210817202617838.png\" alt=\"image-20210817202617838\" style=\"zoom: 80%;\" />\n\n  * 用@Autowired代替所有的`property`标签内的`ref`属性，此注解打在对应的成员属性上\n\n    <img src=\"/images/image-20210817202904688.png\" alt=\"image-20210817202904688\" style=\"zoom:80%;\" /> \n\n  * 在Spring的核心配置文件中指定context命名空间的`component-scan`标签的`base-package`属性：\n\n    ```xml\n    <!--用注解方式进行装配Bean需要配置组件扫描-->\n    <context:component-scan base-package=\"com.taoyyz\">\n        <!--base-package属性是指定从哪个包(及其子包)扫描带注解的组件。-->\n    </context:component-scan>\n    ```\n\n  * 此时调用`getBean(String name)`只需要传入需要获取哪个Bean即可（也就是被打上了@Component注解的类），如果@Component没有传入参数，则默认Bean名称是类名首字母转小写\n\n**说明**：\n\n* 注解方式不同于XML，可以不写set方法，会自动给打上注解的成员属性赋值\n* @Controller、@Service、@Repository只是语义化的@Component注解\n  * @Repository可以捕获特定于平台的异常，并将它们作为Spring统一未检查异常的一部分重新抛出\n* @Autowired可以不搭配@Qualifier，此时会**按照数据类型**从Spring容器进行匹配（例如匹配UserDao类型），如果匹配不到一个bean会报错，此时可以指定@Autowired的`required=false`\n* @Qualifier要结合@Autowired使用，是**按照名称**（`Bean`的`id`）从Spring容器中进行匹配\n* @Resource相当于@Autowired + @Qualifier，此时按照名称匹配要指定@Resource注解的`name`属性值\n  * @Resource在Spring4.2之前不支持@Primary：但是在4.2之后，@Resource已经全面支持了@Primary以及提供了对@Lazy的支持\n\n**Autowired补充，解决byType不唯一的NoUniqueBeanDefinitionException异常**：\n\n* *向容器注入Bean的时候加上@Primary注解*\n* *使用@Qualifier指定beanName*\n* *使得需要注入的对象名，和容器里的beanName一致也可以确定唯一的bean*\n\n### Spring新注解\n\n* 使用原始注解不能全部替代XML配置文件，例如\n\n  * 非自定义的Bean的配置：`<bean>`\n  * 加载properties文件的配置：`<context:property-placeholder>`\n  * 组建扫描的配置：`<context:component-scan>`\n  * 引入其他文件：`<import>`\n\n  **新注解**：\n\n<img src=\"/images/image-20210817212227550.png\" alt=\"image-20210817212227550\" style=\"zoom:67%;\" />\n\n* 使用方式：\n  * 用一个类替代XML配置文件，为这个类打上@Configuration注解，表示这是一个Spring核心配置类\n  * 在打上@Configuration的核心配置类上再打上@ComponentScan(\"包路径\")，指定组建扫描的包\n  * 如果想要引入其他希望产生Bean的配置类，需要在打上@Configuration的核心配置类上再打上@Import(其他配置类的`Class<?>[]`)即可使引入的类的Bean被Spring管理\n  * 用@PropertySource(\"文件路径\")代替context命名空间的`location`属性，表示读取properties配置文件\n    * 在@PropertySource读取到properties配置文件后，可以利用@Value(\"`${SpEL表达式}`\")取出properties配置文件中的`key`赋值给被打上此@Value注解的成员属性\n  * 用@Bean代替`<bean>`标签，此注解打在方法上。此方法的方法名会作为bean的`id`属性，返回值就是bean的`class`属性，也可以指定生成的bean的`id`，即@Bean(\"生成的bean的名称\")\n\n## Spring集成Junit\n\n* 原始的Junit测试Spring存在的问题：\n\n  * 每个测试类都需要先获取上下文，然后调用getBean()\n\n  ![image-20210817220929954](/images/image-20210817220929954.png)\n\n* 解决思路：\n\n  * 让SpringJunit负责创建Spring容器，但是需要将配置文件的名称告诉它\n  * 将需要进行测试的Bean直接在测试类中进行注入\n\n* 集成步骤：\n\n  1. 导入Spring集成Junit的坐标\n\n     ```xml\n     <dependency>\n         <groupId>org.springframework</groupId>\n         <artifactId>spring-test</artifactId>\n         <version>5.0.5.RELEASE</version>\n     </dependency>\n     ```\n\n     * <font color=red>踩坑</font>：Maven中`spring-test`依赖包版本要和`spring-core`版本一致，否则会导致以下报错：\n\n       `java.lang.IllegalStateException: Could not load TestContextBootstrapper`\n\n  2. 使用@Runwith注解替换原来的运行期\n\n     * @Runwith(Class<? extends Runner>)打在测试类上，参数为一个Runner及子类的Class对象\n\n       例如：给测试类打上@RunWith(SpringJUnit4ClassRunner.class)的注解\n\n  3. 使用@ContextConfiguration指定配置文件或配置类\n\n     * 指定XML配置文件：`@ContextConfiguration(\"classpath:applicationContext.xml\")`\n     * 指定配置类：`@ContextConfiguration(classes = SpringConfiguration.class)`\n\n  4. 使用@Autowired注入需要测试的对象\n\n     * 相当于从Spring容器中取出值给成员属性赋值\n\n  5. 创建测试方法进行测试\n\n## Spring集成WEB环境\n\n### ApplicationContext应用上下文获取方式\n\n* 如果在`doGet()`或`doPost()`方法中每次都通过`new ClassPathXmlApplicationContext()`去加载应用上下文很麻烦\n* 所以在WEB项目中，可以通过`ServletContextListener`监听WEB应用的启动，在启动时就加载Spring的配置文件，创建应用上下文`ApplicationContext`对象，并将其存储到最大的`ServletContext`域中，这样就可以在任意位置从域中获得应用上下文`ApplicationContext`对象了\n* 也可以通过在web.xml中设置一个`<context-param>`标签，存入`param-name`与对应的`param-value`获取xml文件名\n\n### Spring提供获取应用上下文的工具\n\n* Spring提供了一个监听器`ContextLoaderListener`就是对上述功能的封装，该监听器内部加载Spring配置文件，创建应用上下文对象，并存储到`ServletContext`域中，提供了一个客户端工具类`WebApplicationContextUtils`供使用者获取应用上下文对象\n\n* 使用方式：\n\n  * 在web.xml中配置`ContextLoaderListener`**监听器**（需要导入spring-web坐标）\n\n    ```xml\n    <!--配置监听器-->\n    <listener>\n        <listener-class>\n            org.springframework.web.context.ContextLoaderListener\n        </listener-class>\n  </listener>\n    ```\n\n  * 使用`WebApplicationContextUtils`的`getWebApplicationContext()`方法即可获得应用上下文对象\n  \n    ```java\n    ApplicationContext app = WebApplicationContextUtils\n     .getWebApplicationContext(request.getServletContext());\n    ```\n\n  <font color=red>踩坑</font>：出现以下错误：\n  \n  ```asciiarmor\n  java.lang.ClassNotFoundException:org.springframework.web.context.ContextLoaderListener\n  ```\n  \n  1. 考虑web.xml的版本过高，Tomcat8.5环境下修改web.xml的4.0版本为低版本，或换用Tomcat9\n  2. 考虑修改IDEA的项目结构：设置工件<img src=\"/images/image-20210818010245791.png\" alt=\"image-20210818010245791\" style=\"zoom:80%;\" />\n\n# SpringMVC\n\n## SpringMVC概述\n\n* SpringMVC是一种基于Java的实现<font color=red>MVC设计模式</font>的请求驱动类型的<font color=red>轻量级WEB框架</font>，属于SpringFrameWork的后续产品，已经融合在Spring Web Flow中\n* SpringMVC已经成为目前<font color=red>最主流的MVC框架之一</font>，并且随着Spring3.0发布，全面超越Struts2，成为最优秀的MVC框架。它通过一套注解，让一个简单的Java类成为处理请求的控制器，而无需实现任何接口，同时还<font color=red>支持RESTful编程风格</font>的请求\n\n## SpringMVC快速入门\n\n* 客户端发起请求，服务器端接收请求，执行逻辑并进行视图跳转\n\n![image-20210818132536592](/images/image-20210818132536592.png)\n\n* 开发步骤：\n\n  1. 导入SpringMVC相关坐标\n\n     ```xml\n     <dependency>\n         <groupId>org.springframework</groupId>\n         <artifactId>spring-webmvc</artifactId>\n         <version>5.0.5.RELEASE</version>\n     </dependency>\n     ```\n\n  2. 配置SpringMVC核心控制器DispatcherServlet\n\n     ```xml\n     <!--配置一个SpringMVC的前端控制器-->\n     <servlet>\n         <servlet-name>DispatcherServlet</servlet-name>\n         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>\n         <init-param>\n             <param-name>contextConfigLocation</param-name>\n             <param-value>classpath:spring-mvc.xml</param-value>\n         </init-param>\n         <load-on-startup>1</load-on-startup>\n     </servlet>\n     <servlet-mapping>\n         <servlet-name>DispatcherServlet</servlet-name>\n         <url-pattern>/</url-pattern>\n     </servlet-mapping>\n     ```\n\n  3. 创建Controller类和视图页面\n\n     ```java\n     @Controller\n     public class UserController {\n         @RequestMapping(\"taoyyz.do\")\n         public String method1() {\n             System.out.println(\"Controller method1执行啦\");\n             return \"success.jsp\"; //返回一个JSP视图给浏览器\n         }\n     \n         @RequestMapping(\"/hhh\") //返回值为void会默认返回名为hhh.jsp的视图\n         public void t() {\n             System.out.println(\"hhh来了\");\n         }\n     }\n     ```\n\n  4. 使用注解配置Controller类中业务方法的映射地址：即@RequestMapping\n\n  5. 配置SpringMVC核心配置文件：`spring-mvc.xml`\n\n     ```xml\n     <!--启动组件扫描-->\n     <context:component-scan base-package=\"com.taoyyz.controller\"/>\n     <!--启动允许注解驱动-->\n     <mvc:annotation-driven/>\n     ```\n\n  6. 客户端发请求测试\n\n## SpringMVC组件解析\n\n### SpringMVC的执行流程\n\n![image-20210818135620250](/images/image-20210818135620250.png)\n\n1. 用户发送请求到前端控制器DispatcherServlet\n2. DispatcherServlet收到请求调用HandlerMapping处理器映射器\n3. 处理器映射器找到具体的处理器（可以根据XML配置或注解进行查找），生成处理器对象及处理器拦截器（如果有）一并返回给DispathcerServlet\n4. DispatcherServlet调用HandlerAdapter处理器适配器\n5. HadlerAdapter适配器经过适配调用具体的处理器（也就是后端控制器Controller）\n6. Controller执行完成返回ModelAndView\n7. HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet\n8. DispatcherServlet将ModelAndView传给ViewResolver视图解析器\n9. ViewResolver解析后返回具体View\n10. DispatcherServlet根据View进行渲染视图（将模型数据填充至视图中），然后响应给用户\n\n### SpringMVC注解配置解析\n\n* **@RequestMapping**\n\n  * 作用：用于建立请求的URL和处理请求的方法之间的对应关系\n\n  * 位置：\n\n    * 类上：请求URL的第一级访问目录，此处不写的话默认为应用上下文根目录\n    * 方法上：请求URL的第二级访问目录，与类上的第一级目录一起组成虚拟访问路径\n\n  * 属性：\n\n    * `value`：用于指定请求的URL，它和`path`属性的作用是一样的\n\n    * `method`：用于指定请求的方式，取值为`RequestMethod`的枚举\n\n    * `params`：用于指定限制请求参数的条件，支持简单的表达式，要求请求参数的key和value必须和配置的一模一样\n\n      例如：`params = {\"accountName\"}` 表示请求参数必须含有`accountName`\n\n      ​\t\t\t`params = {\"money!=100\"}` 表示请求参数中的`money`不能为100\n\n### SpringMVC的XML配置解析\n\n* 视图解析器：\n\n  * SpringMVC组件的默认配置：`DispatcherServlet.properties`位于org.springframework.web.servlet\n  * 此配置指定了诸如`HandlerMapping`、`HandlerAdapter`和`ViewResolver`视图解析器等组件\n\n* 在SpringMVC的XML中配置自定义`ViewResolver`的Bean可以为此Bean的成员属性赋值，例如设置前/后缀：\n\n  ```xml\n  <!--配置解析器的前缀和后缀-->\n  <bean id=\"viewResolver\" class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\">\n      <property name=\"prefix\" value=\"/jsp/\"/>\n      <property name=\"suffix\" value=\".jsp\"/>\n  </bean>\n  ```\n\n  前/后缀属性位于`InternalResourceViewResolver`的父类中：<img src=\"/images/image-20210818151741663.png\" alt=\"image-20210818151741663\" style=\"zoom:80%;\" />\n\n  `UrlBasedViewResolver`还包含了重定向和转发前缀：<img src=\"/images/image-20210818151952895.png\" alt=\"image-20210818151952895\" style=\"zoom:80%;\" />\n\n### 知识要点：\n\n<img src=\"/images/image-20210818152241456.png\" alt=\"image-20210818152241456\" style=\"zoom: 67%;\" /><img src=\"/images/image-20210818152254065.png\" alt=\"image-20210818152254065\" style=\"zoom: 75%;\" />\n\n## SpringMVC数据响应的方式\n\n### 页面跳转\n\n* **直接返回字符串**\n\n  ![image-20210818154946053](/images/image-20210818154946053.png)\n\n  * 如果`@RequestMapping`映射的方法返回`void`，那么会跳转到此方法的映射名作为视图的页面上，如果不希望跳转，可以使返回值类型为`ModelAndView`对象，并且`return null`就不会产生跳转\n\n* **通过ModelAndView对象返回**\n\n  ```java\n  @RequestMapping(\"/method3\")\n  public ModelAndView method3(ModelAndView modelAndView) { //SpringMVC会分配此对象\n      modelAndView.setViewName(\"success\"); //设置视图名称，也就是要跳转到的页面\n      modelAndView.addObject(\"username\", \"method3的\"); //设置模型的内容类似Attribute\n      return modelAndView; //返回此ModelAndView\n  }\n  ```\n\n  * 也可以在映射的方法内部手动`new`一个`ModelAndView`对象，设置内容\n\n    ```java\n    @RequestMapping(\"/method2\")\n    public ModelAndView method2() {\n        ModelAndView modelAndView = new ModelAndView(); //手动new一个模型视图对象\n        modelAndView.setViewName(\"success\"); //设置视图名称，也就是要跳转到的页面\n        modelAndView.addObject(\"username\", \"tao\"); //设置模型的内容类似于Attribute\n        return modelAndView; //返回此ModelAndView\n    }\n    ```\n\n  * 还可以只利用`Model`对象传递参数，方法返回值的字符串作为`View`：\n\n    ```java\n    @RequestMapping(\"/method4\")\n    public String method4(Model model) { //参数只需要Model\n        model.addAttribute(\"username\", \"method4的\"); //设置模型的内容\n        return \"success\"; //这里返回的字符串作为View\n    ```\n\n### 回写数据\n\n* **直接返回字符串**\n\n  * 在Servlet中：`Response`对象可以使用`getWriter()`或`getOutputStream()`回写数据\n\n  * 在SpringMVC中，可以注入response对象回写数据，此时不需要进行视图跳转，所以返回值为`void`\n\n    ```java\n    @RequestMapping(\"/method6\")\n    public void method6(HttpServletResponse response) throws IOException {\n        response.setContentType(\"text/html;charset=utf-8\"); //记得设置响应头防乱码\n        response.getWriter().write(\"来自response的你好\");\n    }\n    ```\n\n  * 建议使用@ResponseBody告诉SpringMVC框架不要进行视图跳转，而是当成响应体返回\n\n    ```java\n    @RequestMapping(value = \"/method7\",produces = \"text/html;charset=utf-8\")\n    @ResponseBody\n    public String method7() {\n        return \"来自SpringMVC的回写的数据\";\n    }\n    ```\n\n    @ResponseBody的响应编码默认ISO-8859-1会导致乱码，在请求映射中使用`produces`属性指定\n\n<font color=red>超级踩坑：IDEA对Maven项目依赖jar包的打包问题</font>：引入了jackson依赖后，启动项目总是报错启动失败\n\n原因：IDEA在对工件进行打包时可能会漏掉一些jar包，在项目结构的工件设置里手动添加到`WEB-INF/lib`\n\n![image-20210818172612181](/images/image-20210818172612181.png)\n* 返回对象或集合\n\n  1. 可以通过jackson的ObjectMapper对象的`writeValueAsString()`方法转换为JSON字符串\n\n  2. 可以通过配置SpringMVC的处理器适配器`RequestMappingHandlerAdapter`来利用jackson的转换器转换：配置一个适配器`RequestMappingHandlerAdapter`的Bean，然后指定`messageConverters`\n\n     ```xml\n     <!--配置请求映射处理器适配器的消息转换器为Jackson-->\n     <bean id=\"handlerAdapter\"    class=\"org.springframework.web.servlet.mvc.method.annotation\n                                         .RequestMappingHandlerAdapter\">\n         <property name=\"messageConverters\">\n             <list>\n                 <bean class=\"org.springframework.http.converter.json\n                              .MappingJackson2HttpMessageConverter\"/>\n             </list>\n         </property>\n     </bean>\n     ```\n\n   **究极方法**：使用`<mvc:annotation-driven/>`注解驱动，搭配@ResponseBody返回的类型就**为所欲为**了\n\n  <img src=\"/images/image-20210818181133229.png\" alt=\"image-20210818181133229\" style=\"zoom: 60%;\" />\n\n## SpringMVC获得请求数据\n\n### 获得请求的参数\n\n#### 1.基本类型参数\n\n* Controller中业务方法的参数要与请求参数中的参数名一致，参数值会自动映射匹配\n\n  例如：`http://localhost/req?username=tom&age=20`请求，可以被Controller中的此方法匹配：\n\n  ```java\n  @RequestMapping(\"/req\")\n  @ResponseBody\n  public void method12(String username,int age,Float f) {\n  \tSystem.out.println(\"method12执行了\");\n      System.out.println(\"username = \" + username); //tom\n  \tSystem.out.println(\"age = \" + age); //20\n  \tSystem.out.println(\"f = \" + f); //null\n  }\n  ```\n\n  **注意：**\n\n  * 如果请求的参数没有匹配映射方法的参数，Java中的引用数据类型会被赋值为null，基本数据类型会报错\n  * 如果对String类型传递了多个参数，它们赋值给String时，会以逗号隔开，而其他类型会以初次赋值为准\n\n#### 2.POJO类型参数\n\n* Controller中的业务方法的POJO参数的属性名要与请求参数中的参数名一致，参数值会自动映射匹配\n\n  例如：`http://localhost/req?username=tom&age=20`请求，可以被Controller中的此方法匹配：\n\n  ```java\n  @RequestMapping(value = \"/method13\")\n  @ResponseBody\n  public void method13(User user) { //User对象含有username和age两个属性\n      System.out.println(\"method13执行了\");\n      System.out.println(\"user = \" + user); //User{username='tom', age=20}\n  }\n  ```\n\n#### 3.数组类型参数\n\n* Controller中的业务方法的数组名称与请求参数的参数名一致，参数值会自动映射匹配\n\n  例如：`http://localhost/req?hobby=唱歌&hobby=跳舞`请求，可以被Controller中的此方法匹配：\n\n  ```java\n  @RequestMapping(value = \"/method14\")\n  @ResponseBody\n  public void method14(String[] hobby) {\n      System.out.println(\"method14执行了\");\n      System.out.println(\"hobbies = \" + Arrays.toString(hobby)); //[唱歌, 跳舞]\n  }\n  ```\n\n#### 4.集合类型参数\n\n##### 方式1：使用VO对象包装集合\n\n将集合参数包装到一个POJO中，这个POJO含有一个集合对象的成员属性，此时的POJO被称为VO对象\n\n例如：定义一个`UserVO`对象，含有一个成员属性：`List<User>`，用Controller以下方法匹配映射：\n\n```java\n@RequestMapping(value = \"/method15\")\n@ResponseBody\npublic void method15(UserVO userVO) { //对象集合需要包装到VO（Value/View Object）对象中\n    System.out.println(\"method15执行了\");\n    System.out.println(\"userVO = \" + userVO);\n}\n```\n\n通过POST请求提交方便编写集合的名称及属性：\n\n```jsp\n<form action=\"${pageContext.request.contextPath}/user/method15\" method=\"post\">\n    <input type=\"text\" name=\"userList[0].username\"> <%--第1个User对象的username--%>\n    <input type=\"text\" name=\"userList[0].age\"> <%--第1个User对象的age--%>\n    <input type=\"text\" name=\"userList[1].username\"> <%--第2个User对象的username--%>\n    <input type=\"text\" name=\"userList[1].age\"> <%--第2个User对象的age--%>\n    <input type=\"submit\" value=\"提交\">\n</form>\n```\n\n##### 方式2：使用JSON格式利用@RequestBody注解直接用集合接收\n\n当使用AJAX提交时，可以指定`contentType`为`JSON`形式，那么在方法参数位置使用`@RequestBody`可以直接接收集合数据而无需使用VO对象进行包装\n\n例如：利用AJAX提交一段JSON数据：\n\n```js\nvar userList = [];\nuserList.push({username: '陶俊杰', age: 22})\nuserList.push({username: '胖', age: 23})\n$.ajax({\n    type: 'POST',\n    url: '${pageContext.servletContext.contextPath}/user/method16',\n    data: JSON.stringify(userList),\n    contentType: 'application/json;charset=utf-8'\n})\n```\n\n提交的userList集合可以被打上@RequestBody注解的集合对象直接接收：\n\n```java\n@RequestMapping(value = \"/method16\")\n@ResponseBody\npublic void method16(@RequestBody List<User> userList) {\n    System.out.println(\"method15执行了\");\n    System.out.println(\"userList = \" + userList);\n    //[User{name='陶俊杰', age=22}, User{name='胖', age=23}]\n}\n```\n\n**注意**：引用JQuery的JS文件可能导致404 not found，因为SpringMVC配置文件中默认用`/`匹配所有的请求映射，而此时没有配置其他的Servlet，导致此请求交给了`DispatcherServlet`处理，在`DispatcherServlet`中JS文件这种静态资源没有找到对应映射路径，所以就匹配不到结果，导致资源找不到\n\n**解决方法**：\n\n* 利用SpringMVC配置文件指定允许的静态资源：\n\n  ```xml\n  <mvc:resources mapping=\"/js/**\" location=\"classpath:/js/\"/>\n  ```\n\n  表示请求映射为`/js/`下的任意文件都可以从`location`属性中指定的`/js/`中找到\n\n* 利用SpringMVC配置文件指定默认处理器：\n\n  ```xml\n  <mvc:default-servlet-handler/>\n  ```\n\n  此时如果没有任何Servlet映射此资源，那么就会交给默认的(Tomcat服务器)找资源\n\n### 请求数据乱码问题\n\n* 问题：当使用`POST`请求时，提交到SpringMVC的数据可能会乱码\n\n* 解决方法：需要在web.xml配置一个`CharacterEncodingFilter`**过滤器**来进行编码的过滤\n\n  ```xml\n  <!--配置编码过滤器-->\n  <filter>\n      <filter-name>characterEncodingFilter</filter-name>\n      <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>\n      <init-param>\n          <param-name>encoding</param-name> <!--设置此过滤器的encoding编码成员属性-->\n          <param-value>UTF-8</param-value> <!--设置编码为UTF-8-->\n      </init-param>\n  </filter>\n  <filter-mapping>\n      <filter-name>characterEncodingFilter</filter-name>\n      <url-pattern>/*</url-pattern> <!--直接过滤所有请求-->\n  </filter-mapping>\n  ```\n\n  类似于Servlet中request对象的`setChracterEncoding()`\n\n### 注解进行参数绑定\n\n* 当请求的参数名称与映射方法中的参数名不一致时，利用@RequestParam注解来绑定到方法中的参数\n\n  例如：`http://localhost/req?name=tom`，需要把参数`name`绑定到映射方法的`username`上：\n\n  ```java\n  @RequestMapping(value = \"/method17\")\n  @ResponseBody\n  public void method17(@RequestParam(\"name\") String username) {\n      System.out.println(\"method17执行了\");\n      System.out.println(\"username = \" + username);\n  }\n  ```\n\n  此@RequestParam注解有以下参数可以使用：\n\n  * `value`：需要用于匹配的可能的参数名\n  * `required`：在指定的请求参数是否是必须的，默认为`true`，即请求如果没有传递此参数会报错\n  * `defaultValue`：当没有指定请求参数时，使用的默认值\n\n### Restful风格的参数\n\n* Restful是一种<font color=red>软件架构风格、设计风格</font>，而不是标准，只是提供了一组设计原则和约束条件。主要用于客户端和服务器交互类的软件，基于这个风格设计的软件可以更简洁、更有层次、更易于实现缓存机制等等\n\n* Restful风格的请求是使用\"**url+请求方式**\"表示一次请求目的，HTTP协议里面四个表示操作方式的动词如下：\n\n  * GET：用于获取资源\n  * POST：用于新建资源\n  * PUT：用于更新资源\n  * DELETE：用于删除资源\n\n* 举个例子：\n\n  * /user/1\tGET\t\t：得到id为1的user\n  * /user/1\tDELETE  ：删除id为1的user\n  * /user/1\tPUT\t\t ：更新id为1的user\n  * /user\t\tPOST\t   ：新增user\n\n* 用法：\n\n  * 修改方法的注解指定映射为：`@RequestMapping(\"/映射路径/{占位符}\")`\n\n  * 访问映射路径时直接带上参数即可自动匹配给占位符\n\n  * 在方法参数里使用`@PathVariable(\"占位符\")`取出请求的参数，被此注解修饰的映射方法参数会获得占位符的值\n\n    ```java\n    @RequestMapping(value = \"/method18/{name}\")\n    @ResponseBody\n    public void method18(@PathVariable(\"name\") String username) {\n        System.out.println(\"method18执行了\");\n        System.out.println(\"username = \" + username);\n    }\n    ```\n\n### 获得Servlet相关API\n\n* SpringMVC支持使用原始ServletAPI对象作为控制器方法的参数进行注入，常用的对象有：\n\n  * HttpServletRequest\n  * HttpServletResponse\n  * HttpSession\n\n  ```java\n  @RequestMapping(value = \"/method20\")\n  @ResponseBody\n  public void method20(HttpServletRequest request, HttpServletResponse response, HttpSession session) {\n      System.out.println(\"request = \" + request);\n      System.out.println(\"response = \" + response);\n      System.out.println(\"session = \" + session); //这俩都一样\n      System.out.println(\"request.getSession() = \" + request.getSession());//就是上面\n  }\n  ```\n\n### 获取请求头\n\n* 使用@RequestHeader可以获得请求头信息，相当于Servlet中`request.getHeader(name)`，含有以下属性\n\n  * `value`：请求头的名称\n  * `required`：是否必须携带此请求头\n\n  ```java\n  @RequestMapping(value = \"/method21\")\n  @ResponseBody\n  public void method21(@RequestHeader(value = \"User-Agent\") String user_agent,\n                       @RequestHeader(\"Accept\") String accept,\n                       @RequestHeader(\"host\") String host) {\n      System.out.println(\"user_agent = \" + user_agent);\n      System.out.println(\"accept = \" + accept);\n      System.out.println(\"host = \" + host);\n  }\n  ```\n\n* 使用@CookieValue获取指定Cookie的值，含有以下属性：\n\n  * `value`：指定cookie的名称\n  * `required`：是否必须携带此cookie\n\n  ```java\n  @RequestMapping(value = \"/method22\")\n  @ResponseBody\n  public void method22(@CookieValue(\"JSESSIONID\") String JSessionID,\n                       @CookieValue(value = \"IDEA\",required = false) String idea) {\n      System.out.println(\"JSessionID = \" + JSessionID); //JSESSIONID的值\n      System.out.println(\"idea = \" + idea); //没有此cookie，为null\n  }\n  ```\n\n  \n\n## 自定义类型转换器\n\n* SpringMVC默认已经提供了一些常用的类型转换器，例如客户端提交的字符串可以自动转为int类型等\n* 但不是所有的数据类型都提供了转换器，没有提供的就需要自定义转换器，例如日期类型就需要自定义转换器\n\n**自定义类型转换器的步骤**：\n\n1. 自定义类型转换器，实现`Converter`接口并重写`convert(String date)`方法，解析String为Date并返回\n\n   ```java\n   public class DateConverter implements Converter<String, Date> {\n       @Override\n       public Date convert(String date) {\n           //在这里把String类型的字符串转换成Date对象然后返回\n           SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n           try {\n               return sdf.parse(date);\n           } catch (ParseException e) {\n               e.printStackTrace();\n           }\n           return null;\n       }\n   }\n   ```\n\n2. 在SpringMVC配置文件中声明转换器\n\n   ```xml\n   <!--配置类型转换器-->\n   <bean id=\"conversionService\" class=\"org.springframework.context.support.ConversionServiceFactoryBean\">\n       <property name=\"converters\"> <!--给这个converters成员属性赋值注入-->\n           <set>\n               <bean class=\"com.taoyyz.converter.DateConverter\"/> <!--自定义的转换器-->\n           </set>\n       </property>\n   </bean>\n   ```\n\n3. 在`<annotation-driven>`中引用转换器\n\n   ```xml\n   <mvc:annotation-driven conversion-service=\"conversionService\"/>\n   ```\n\n## 案例：文件上传\n\n* 前端：\n\n  * 在form表单内以POST方式提交数据\n  * 提交的数据类型是：`<input type=\"file\" name=\"filename\">`时，需要把form表单的`enctype`属性设置为`\"multipart/form-data\"`\n\n* 后端：\n\n  * 当form表单的`enctype`为多部分表单时，request.getParameter()将失效\n\n    * 当`enctype`为`\"application/x-www-form-urlencodedmultipart\"`时，form表单提交的正文内容格式为：`key=value&key=value&key=value`\n\n    * 当`enctype`为`\"multipart/form-data\"`时，form表单提交的正文内容就会变成**多部分形式**，例如：\n\n      ![image-20210819162522038](/images/image-20210819162522038.png)\n\n  * 步骤：\n\n    1. 导入fileupload和io坐标\n\n       ```xml\n       <dependency>\n           <groupId>commons-fileupload</groupId>\n           <artifactId>commons-fileupload</artifactId>\n           <version>1.3.1</version>\n       </dependency>\n       <dependency>\n           <groupId>commons-io</groupId>\n           <artifactId>commons-io</artifactId>\n           <version>1.3.2</version>\n       </dependency>\n       ```\n\n    2. 配置文件上传解析器\n\n       ```xml\n       <!--配置文件上传解析器-->\n       <bean id=\"multipartResolver\" class=\"org.springframework.web.multipart.commons.CommonsMultipartResolver\">\n           <!--上传文件的总大小-->\n           <property name=\"maxUploadSize\" value=\"5242800\"/>\n           <!--上传的单个文件的大小-->\n           <property name=\"maxUploadSizePerFile\" value=\"5242800\"/>\n           <!--上传文件的编码类型-->\n           <property name=\"defaultEncoding\" value=\"UTF-8\"/>\n       </bean>\n       ```\n\n    3. 编写文件上传代码\n\n       ```java\n       @RequestMapping(value = \"/upload\")\n       @ResponseBody\n       public void upload(String name,MultipartFile filename) throws IOException {\n           System.out.println(\"name = \" + name);\n           String originalFilename = filename.getOriginalFilename(); //获得文件名\n           System.out.println(\"文件名 = \" + originalFilename);\n           filename.transferTo(new File(\"E:\"+originalFilename)); //保存到磁盘\n       }\n       ```\n\n    4. 前端通过form表单提交数据\n\n       ```jsp\n       <form action=\"${pageContext.servletContext.contextPath}/user/upload\" method=\"post\" enctype=\"multipart/form-data\">\n           名称: <input type=\"text\" name=\"name\">\n           文件: <input type=\"file\" name=\"filename\">\n           <input type=\"submit\" value=\"提交\">\n       </form>\n       ```\n\n  * **多文件上传**：\n\n    * 方式1：后端定义多个不同名的`MultipartFile`对象接收前端发送对应`name`的文件，然后分别处理\n\n      ```java\n      @RequestMapping(value = \"/uploads\")\n      @ResponseBody\n      public void uploads(String name,\n                          MultipartFile filename1,\n                          MultipartFile filename2) throws IOException {\n          System.out.println(\"name = \" + name);\n          String originalFilename1 = filename1.getOriginalFilename(); //文件名\n          String originalFilename2 = filename2.getOriginalFilename(); //文件名\n          uploadFile1.transferTo(new File(\"E:\" + originalFilename1));\n          uploadFile2.transferTo(new File(\"E:\" + originalFilename2));\n      }\n      ```\n\n    * 方式2：后端用一个`MultipartFile[]`数组接收前端发送的同名`name`文件，当做数组处理\n\n      ```java\n      @RequestMapping(value = \"/uploadArr\")\n      @ResponseBody\n      public void uploads(String name,MultipartFile[] files) throws IOException {\n          System.out.println(\"name = \" + name);\n          for (MultipartFile file : files) {\n              file.transferTo(new File(\"E:\" + file.getOriginalFilename()));\n          }\n      }\n      ```\n\n## Spring JdbcTemplate\n\n### 概述\n\n> Spring JdbcTemplate是Spring框架中提供的一个对象，是对原始JDBC API对象的简单封装，Spring框架为我们提供了很多操作的模板类。例如：操作关系型数据库的JdbcTemplate和HibernateTemplate，操作NoSQL数据库的RedisTemplate，操作消息队列的JmsTemplate等\n\n### JdbcTemplate开发步骤\n\n1. 导入spring-jdbc和spring-tx（事务）坐标\n\n   ```xml\n   <dependency>\n       <groupId>org.springframework</groupId>\n       <artifactId>spring-jdbc</artifactId>\n       <version>5.0.5.RELEASE</version>\n   </dependency>\n   <dependency>\n       <groupId>org.springframework</groupId>\n       <artifactId>spring-tx</artifactId>\n       <version>5.0.5.RELEASE</version>\n   </dependency>\n   ```\n\n2. 创建数据库表和实体\n\n3. 创建JdbcTemplate对象\n\n   * 可以把JdbcTemplate对象和DataSource对象配置为Bean，即可直接从Spring容器拿到对象\n\n4. 执行数据库操作\n\n   * 更新/删除：`int update(String sql,Object... args)`\n   * 查询：\n     * 查询单个对象：`T queryForObject(String sql,RowMapper<T> rowMapper,Object... args)`\n     * 查询多个对象的列表：`<T> query(String sql,RowMapper<T> rowMapper,Object... args)`\n     * 聚合函数查询：`T queryForObject(String sql,Class<T> requiredType)`\n\n## 案例：用户管理系统\n\n### 角色列表的展示\n\n* 步骤：\n  1. 点击角色管理菜单，发送请求到服务器端（修改角色管理菜单栏的URL地址）\n  2. 创建RoleController和showList()方法\n  3. 创建RoleService和showList()方法\n  4. 创建RoleDao和findAll()方法\n  5. 使用JdbcTemplate完成查询操作\n  6. 将查询数据存储到Model中\n  7. 转发到role-list.jsp页面进行展示\n* <font color=red>**踩坑踩大坑！！！**报错`CannotGetJdbcConnectionException: Failed to obtain JDBC Connection`</font>\n  * 原因：竟然是新引入的Maven依赖：`mysql-connector`没有被IDEA打包进项目！\n  * 解决方法：在IDEA的项目结构选项里面把漏掉的jar包添加到`WEB-INF/lib`\n\n### 角色添加\n\n* 步骤：\n\n  1. 点击角色列表的新建按钮跳转到角色添加页面\n  2. 输入角色信息，点击保存按钮，表单数据提交到服务器\n  3. 编写RoleController的save()方法\n  4. 编写RoleService的save()方法\n  5. 编写RoleDao的insert()方法\n  6. 使用JdbcTemplate保存Role数据到sys_role表\n  7. 跳转会角色列表页面\n\n* 可能的问题：乱码问题\n\n  * 原因：添加的表单form是POST方式，对于tomcat来说会乱码\n\n  * 解决方法：配置一个过滤器把编码方式设置为UTF-8\n\n    ```xml\n    <!--配置过滤器，解决乱码问题-->\n    <filter>\n        <filter-name>characterEncodingFilter</filter-name>\n        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>\n        <init-param>\n            <param-name>encoding</param-name>\n            <param-value>utf-8</param-value>\n        </init-param>\n    </filter>\n    <filter-mapping>\n        <filter-name>characterEncodingFilter</filter-name>\n        <url-pattern>/*</url-pattern>\n    </filter-mapping>\n    ```\n\n### 用户列表的展示\n\n### 用户添加\n\n### 用户删除\n\n* 注意要同时处理两张表：用户表、用户与角色的对应关系表，应该先处理关系表再删主表，因为有外键约束\n\n## SpringMVC拦截器\n\n### 拦截器（interceptor）的作用\n\n* SpringMVC的拦截器类似于Servlet开发中的过滤器Filter，用于对处理器进行**预处理**和**后处理**\n* 将拦截器按照一定的顺序连接成一条链，这条链被称为**拦截器链（interceptor chain）**。在访问被拦截的方法或字段时，拦截器链中的拦截器会按照其之前定义的顺序被调用。拦截器也是AOP思想的具体实现\n\n### 拦截器和过滤器的区别\n\n<img src=\"/images/image-20210821164156819.png\" alt=\"image-20210821164156819\" style=\"zoom: 60%;\" />\n\n![image-20211222211806443](images/image-20211222211806443.png)\n\n### 拦截器快速入门\n\n* 步骤\n\n  1. 创建拦截器实现类，实现`HandlerInterceptor`接口\n\n     * 重写`boolean preHandle()`：返回`true`时代表**放行**，返回`false`被**拦截**在这里\n     * 重写`void postHandle()`：这里可以再次修改`ModelAndView`对象\n     * 重写`void afterCompletion`：这里可以用于处理`Exception`异常\n\n  2. 配置拦截器\n\n     * 在web.xml中配置（多个）拦截器\n\n       ```xml\n       <!--配置拦截器-->\n       <mvc:interceptors>\n           <mvc:interceptor>\n               <!--拦截哪些资源？-->\n               <mvc:mapping path=\"/**\"/>\n               <!--拦截器实现类的全类名-->\n               <bean class=\"com.interceptor.MyInterceptor1\"/>\n           </mvc:interceptor>\n       </mvc:interceptors>\n       ```\n\n     * 多个在web.xml中配置的拦截器会按照配置的先后顺序执行\n\n  3. 测试拦截器效果\n\n  ![image-20210821190329982](/images/image-20210821190329982.png)\n\n## 案例：用户登录权限控制\n\n* 需求：用户没有登录的情况下，不能对后台菜单进行访问操作，点击菜单跳转到登录页面，只有用户登录成功后才可以进行后台功能的操作\n\n* 方案：编写一个拦截器类，实现`HandlerInterceptor`接口，重写`preHandle()`方法\n\n  * 类似于过滤器，在这个拦截器的`preHandle()`方法中检测Session中是否含有已经登录的`user`对象\n\n    * 如果有，就`return true`放行\n    * 如果没有，就重定向到登录页面并`return false`\n\n  * 在springMVC配置文件中配置拦截器\n\n    ```xml\n    <!--配置拦截器-->\n    <mvc:interceptors>\n        <mvc:interceptor>\n            <!--配置需要拦截的资源-->\n            <mvc:mapping path=\"/**\"/>\n            <!--配置排除拦截的资源-->\n            <mvc:exclude-mapping path=\"/user/login\"/>\n            <bean class=\"com.taoyyz.interceptor.AccessControlInterceptor\"/>\n        </mvc:interceptor>\n    </mvc:interceptors>\n    ```\n\n    **要注意排除掉登录的访问拦截**\n\n## SpringMVC异常处理机制\n\n### SpringMVC异常处理思路\n\n* 系统中的异常包括两类：预期异常和运行时异常RuntimeException，前者通过捕获异常从而获取异常信息，后者主要通过规范代码开发、测试等手段减少运行时异常的发生。\n* 系统的Dao、Service、Controller出现都通过throws Exception往上层抛出，最后由SpringMVC前端控制器交由异常处理器（HandlerExceptionResolver）进行异常处理\n\n### SpringMVC异常处理的两种方式\n\n* 使用SpringMVC提供的简单异常处理器：**SimpleMappingExceptionResolver**\n* 实现Spring的异常处理接口：**HandlerExceptionResolver**自定义自己的异常处理\n\n#### 简单异常处理器\n\n* SpringMVC已经定义好了该类型的转换器，在使用时可以根据项目情况进行相应异常与视图的映射配置\n\n  ```xml\n  <!--配置简单异常映射处理器-->\n  <bean class=\"org.springframework.web.servlet.handler.SimpleMappingExceptionResolver\">\n      <!--当exceptionMappings匹配不到时匹配此默认-->\n      <property name=\"defaultErrorView\" value=\"error\"/>\n      <property name=\"exceptionMappings\">\n          <map>\n              <!--匹配Exception异常-->\n              <entry key=\"java.lang.Exception\" value=\"genericExp\"/>\n              <!--匹配类型转换异常-->\n              <entry key=\"java.lang.ClassCastException\" value=\"classCastExp\"/>\n          </map>\n      </property>\n  </bean>\n  ```\n\n#### 自定义异常处理\n\n* 步骤\n\n  1. 创建异常处理器实现类，实现HandlerExceptionResolver接口和其中的`resolveException()`方法\n\n     ```java\n     public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) {\n         ModelAndView modelAndView = new ModelAndView();\n         if (e instanceof ClassCastException) {\n             modelAndView.addObject(\"msg\", \"类型转换异常惹\");\n         } else if (e instanceof NullPointerException) {\n             modelAndView.addObject(\"msg\", \"空指针啦\");\n         } else {\n             modelAndView.addObject(\"msg\", \"出现异常惹\");\n         }\n         modelAndView.setViewName(\"error\"); //统一跳转到error.jsp页面\n         return modelAndView;\n     }\n     ```\n\n  2. 配置异常处理器\n\n     ```xml\n     <!--配置自定义异常处理器-->\n     <bean class=\"com.taoyyz.resolver.MyExceptionResolver\"/>\n     ```\n\n  3. 编写异常页面\n\n     ```jsp\n     <body>\n         <h1>出错惹，错误原因：${msg}</h1>\n     </body>\n     ```\n\n  4. 测试异常的跳转\n\n# Spring的AOP\n\n## Spring的AOP简介\n\n* **AOP**是 Aspect Oriented Programming 的缩写，意为<font color=red>面向切面编程</font>，是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术\n* **AOP**是OOP的延续，是软件开发中的一个热点，也是Spring框架中的一个重要内容，是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离，从而使得业务逻辑各部分之间的耦合度降低，提高程序的可重用性，同时提高了开发的效率\n\n## AOP的作用及优势\n\n* 作用：在程序运行期间，在不修改源码的情况下对方法进行功能增强\n* 优势：减少重复代码，提高开发效率，并且便于维护\n\n## AOP的底层实现\n\n* AOP的底层是通过Spring提供的动态代理技术实现的，在运行期间，Spring通过动态代理技术动态的生成代理对象，代理对象方法执行时进行增强功能的介入，再去调用目标对象的方法，完成功能的增强\n\n## AOP的动态代理技术\n\n* JDK代理：基于接口的动态代理技术\n* cglib代理：基于父类的动态代理技术\n\n<img src=\"/images/image-20210821220350274.png\" alt=\"image-20210821220350274\" style=\"zoom: 50%;\" /> \n\n## AOP的相关概念\n\n* Target：代理的**目标对象**\n* Proxy：一个类被AOP增强后，就产生了一个结果的**代理类**\n* Joinpoint：**连接点**，指那些被拦截到的点（方法）。在Spring中，这些点指的是方法，因为Spring只支持方法类型的连接点\n* Pointcuit：**切入点**，指的是要对哪些连接点进行拦截的定义\n* Advice：**通知/增强**，指的是拦截到连接点之后要做的事情（增强）\n* Aspect：**切面**，是切入点和通知的结合\n* Weaving：**织入**，把增强应用到目标对象来创建新的代理对象的过程，Spring采用动态代理织入，而AspectJ采用编译器织入和类装载期织入\n\n## AOP开发明确的事项\n\n* 需要编写的内容：\n  * 编写核心业务代码（目标类的目标方法）\n  * 编写切面类，切面类中有通知（增强功能方法）\n  * 在配置文件中，配置织入关系，即将哪些通知与哪些连接点进行结合\n* AOP技术实现的内容\n  * Spring框架监控切入点方法的执行，一旦监控到切入点方法被运行，就使用代理机制，动态的创建目标对象的代理对象，根据通知的类型，在代理对象的对应位置，将通知对应的功能织入，完成完整的代码逻辑运行\n* AOP底层使用哪种代理方式\n  * Spring框架会根据目标类是否实现了接口来判断采用哪种动态代理的方式\n\n## AOP快速入门\n\n### 基于XML的AOP开发\n\n##### 步骤：\n\n1. 导入AOP相关坐标\n\n   ```xml\n   <dependency>\n       <groupId>org.aspectj</groupId>\n       <artifactId>aspectjweaver</artifactId>\n       <version>1.8.12</version>\n   </dependency>\n   ```\n\n2. 创建目标接口和目标类（ 内部有切点）\n\n3. 创建切面类（内部有增强方法）\n\n4. 将目标类和切面类的对象创建权交给Spring，即配置Bean\n\n   ```xml\n   <!--配置目标对象的bean-->\n   <bean id=\"target\" class=\"com.taoyyz.aop.Target\"/>\n   <!--配置切面对象的bean-->\n   <bean id=\"myAspect\" class=\"com.taoyyz.aop.MyAspect\"/>\n   ```\n\n5. 在applicationContext.xml中配置织入关系\n\n   ```xml\n   <!--配置织入，告诉Spring框架哪些方法（切入点）需要被增强（前置、后置等）需要AOP的命名空间-->\n   <aop:config>\n       <!--声明配置切面-->\n       <aop:aspect ref=\"myAspect\">\n           <!--配置此切面的后置增强，切面 = 切点 + 通知，method是切面中的方法，pointcut是需要切入到哪里的切点表达式-->\n           <aop:before method=\"before\" \n                       pointcut=\"execution(public void com.taoyyz.aop\n                                 .TargetInterface.save())\"/>\n       </aop:aspect>\n   </aop:config>\n   ```\n\n6. 测试代码\n\n   调用被增强之后的Target对象的save()方法，此save()方法已经被织入一个前置增强\n\n#### 切点表达式\n\n* 语法：`execution([修饰符] 返回值类型 包名.类名.方法名(参数))`\n\n  * 访问修饰符可以省略不写\n  * 返回值类型、包名、类名、方法名可以使用`*`星号代表任意\n  * 包名与类名之间可以用一个点`.`代表**当前包**下的类，两个点`..`代表**当前包及子包**下的类\n  * 参数列表可以使用两个点表示任意个数，任意类型的参数列表\n\n* 例如： 以下被切点表达式匹配的情况\n\n  ```java\n  //匹配指定包下的指定类的无返回值的method()方法\n  execution(public void com.taoyyz.aop.Target.method())\n  //匹配指定包下的指定类的无返回值的任意参数的任意方法\n  execution(void com.taoyyz.aop.Target.*(..))\n  //匹配指定包及子包下的任意类的任意方法的任意参数的任意返回值的方法\n  execution(* com.taoyyz.aop.*.*(..))\n  //匹配任意包及其子包下任意类的任意方法的任意参数的任意返回值的方法\n  execution(* *..*.*(..))\n  ```\n\n#### 通知的类型\n\n对于一个通知的配置语法：\n\n```xml\n<aop:通知类型 method=\"方法名\" pointcut=\"execution(public void com.taoyyz.*.*(..))\"/>\n```\n\n可选的**通知类型**有：\n\n![image-20210822154545596](/images/image-20210822154545596.png)\n\n#### 切点表达式的抽取\n\n* 在多个增强的切点表达式相同时，可以将这些切点表达式抽取，在增强中使用pointcut-ref属性代替pointcut属性来引用抽取之后的切点表达式\n\n  ```xml\n  <!--抽取pointcut切点表达式-->\n  <aop:pointcut id=\"myPointCut\" expression=\"execution(void com.taoyyz..*.*(..)))\"/>\n  <!--环绕增强，引用切点表达式-->\n  <aop:around method=\"around\" pointcut-ref=\"myPointCut\"/>\n  <!--异常抛出增强，引用切点表达式-->\n  <aop:after-throwing method=\"afterThrowing\" pointcut-ref=\"myPointCut\"/>\n  ```\n\n### 基于注解的AOP开发\n\n#### 步骤：\n\n1. 创建目标接口和目标类（内部有切点）\n\n2. 创建切面类（内部有增强方法）\n\n3. 将目标类和切面类的对象创建权交给Spring\n\n   * 指定目标类的@Component\n   * 指定切面类的@Component\n\n4. 在切面类中使用注解配置织入关系\n\n   ```java\n   @Component(\"myAspect\") //指定此类是一个Bean\n   @Aspect //指定此类为切面类\n   public class MyAspect {\n       //配置前置增强\n       @Before(\"execution(void com.taoyyz.annoAop..*.*(..))\")\n   //    @Before(\"bean(target)\")\n       void before() {\n           System.out.println(\"前置增强\");\n       }\n   }\n   ```\n\n5. 在配置文件中开启组件扫描和AOP的自动代理\n\n   ```xml\n   <!--开启组件扫描-->\n   <context:component-scan base-package=\"com.taoyyz\"/>\n   <!--开启AOP自动代理-->\n   <aop:aspectj-autoproxy/>\n   ```\n\n6. 测试\n\n#### 通知的类型\n\n对于注解配置通知的语法：\n\n`@通知注解(\"切点表达式\")`，也可以用`@通知注解(\"bean(Bean的id)\")`\n\n可选的**通知类型**有：\n\n![image-20210822165932936](/images/image-20210822165932936.png)\n\n##### 切点表达式的抽取\n\n1. 定义一个任意的方法用于注解抽取切点表达式，切点表达式的名称就是方法名\n\n   ```java\n   @Pointcut(\"execution(* com.taoyyz..*.*(..))\")\n   public void myExp() { }\n   ```\n\n2. 引用此切点表达式，以下4中方式效果相同\n\n   ```java\n   //配置前置增强\n   //@Before(\"execution(* com.taoyyz..*.*(..))\")\n   //@Before(\"bean(target)\") //切入到id为target的bean中\n   //@Before(\"myExp()\") //引入切点表达式的抽取\n   @Before(\"MyAspect.myExp()\") //同上\n   void before() {\n       System.out.println(\"前置增强\");\n   }\n   ```\n\n# Spring的事务控制\n\n## 编程式事务控制相关对象\n\n### `PlatformTransactionManager`\n\n* 此接口是Spring的事务管理器，它提供了常用的操作事务的方法：\n\n  ![image-20210822173527109](/images/image-20210822173527109.png)\n\n* PlatformTransactionManager是接口类型，**不同的Dao层技术有不同的实现类**，例如Dao层技术是jdbc或mybatis时，实现类为`org.springframework.jdbc.datasource.DataSourceTransactionManager`\n\n### `TransactionDefinition`\n\n* 此接口是事务的定义信息对象，含有如下方法：\n\n  <img src=\"/images/image-20210822174251488.png\" alt=\"image-20210822174251488\" style=\"zoom:67%;\" />\n\n* <font color=purple>**事务的隔离级别**：</font>\n\n  * `ISO_DEFAULT`\n  * `ISO_READ_UNCOMMITTED`\n  * `ISO_READ_COMMITTED`\n  * `ISO_REPEATABLE_READ`\n  * `ISO_SERIALIZABLE`\n  \n* <font color=purple>**事务的传播行为**：</font>\n\n  * `REQUIRED`：如果当前没有事务，就新建一个事务；如果已经存在一个事务，则加入这个事务(默认)\n\n    例如：**A调用B，B会看A**有没有事务，如果A**没有就新建**，否则A**有就加入**到A的事务\n\n  * `SUPPORTS`：支持当前事务，如果当前没有事务，就以非事务方式执行（没有事务）\n\n    例如：**A调用B，B会看A**有没有事务，如果A**有事务就支持**A的事务，否则A**没有就以非事务方式执行**\n\n  * `MANDATORY`：使用当前的事务，如果当前没有事务就抛出异常\n\n    例如：**A调用B，B会看A**有没有事务，如果A**有事务就支持**当前事务，否则A**没有就抛出异常**\n\n  * `REQUERS_NEW`：新建事务，如果当前在事务中，把当前事务挂起\n\n  * `NEVER`：以非事务方式执行，如果当前存在事务就抛出异常\n\n  * `NESTED`：如果当前存在事务，则在嵌套事务内执行；如果当前没有事务，则执行REQUIRED操作\n\n  * 超时时间：默认为-1没有超时限制。如果有，则以秒为单位设置\n\n  * 是否只读：建议查询时设置为只读\n\n### `TransactionStatus`\n\n* 此接口提供了事务的运行状态，方法如下：\n\n  <img src=\"/images/image-20210822175929810.png\" alt=\"image-20210822175929810\" style=\"zoom: 67%;\" />\n\n## 基于XML的声明式事务控制\n\n### 概念：\n\nSpring的声明式事务控制就是采用声明的方式来处理事务。**声明**指的是**在配置文件中声明**，用在Spring配置文件中声明式的处理事务来代替代码式的处理事务\n\n### 作用：\n\n* 声明式事务管理不侵入开发的组件，具体来说，业务逻辑对象就不会意识到正在事务管理之中，事实上也应该如此，因为事务管理是属于系统层面的服务，而不是业务逻辑的一部分，如果想要改变事务管理策划的话，也只需要在定义文件中重新配置即可\n* 在不需要事务管理的时候，只需要在设定文件上修改一下，即可移去事务管理服务，无需改变代码重新编译\n* **Spring声明式事务控制底层就是AOP**\n\n### 声明式事务控制的实现\n\n* 明确事项：\n\n  * 谁是切点？需要被事务控制的业务方法\n\n    ```xml\n    <!--切点：目标对象内部需要增强的方法（被事务控制的方法）-->\n    <bean id=\"accountService\" class=\"com.taoyyz.service.impl.AccountServiceImpl\">\n        <property name=\"accountDao\" ref=\"accountDaoImpl\"/>\n    </bean>\n    ```\n\n  * 谁是通知？事务控制\n\n    ```xml\n    <!--通知：事务的增强-->\n    <tx:advice id=\"txAdvice\" transaction-manager=\"transactionManager\">\n        <!--设置事务的属性信息，可以对切入的多个method分别配置-->\n        <tx:attributes>\n    \t\t<!--name为切入到的方法，*表示任意方法-->\n            <tx:method name=\"*\"/>\n        </tx:attributes>\n    </tx:advice>\n    ```\n\n  * 配置切面？\n\n    ```xml\n    <!--配置一个平台事务管理器，需要一个数据源对象以供操作数据库-->\n    <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource\n                                         .DataSourceTransactionManager\">\n        <property name=\"dataSource\" ref=\"dataSource\"/>\n    </bean>\n    <!--配置织入：结合通知和切点-->\n    <aop:config>\n    \t<!--把通知txAdvice切入到accountService这个切入点-->\n        <aop:advisor advice-ref=\"txAdvice\" pointcut=\"bean(accountService)\"/>\n    </aop:config>\n    ```\n\n* **配置的参数**：\n\n  <img src=\"/images/image-20210823030556659.png\" alt=\"image-20210823030556659\" style=\"zoom:67%;\" />\n\n## 基于注解的声明式事务控制\n\n* 谁是切点？需要被事务控制的业务方法，需要把切入点所在的类声明为Bean\n\n* 谁是通知？为切入点加上@Transactional注解，此注解可以有参数，例如只读、隔离级别、超时时间等\n\n  * 如果@Transactional注解打在类上，则此类所有的方法都被此事务管理\n  * 如果@Transactional注解打在方法上，则对此方法生效\n\n* 配置切面？织入切面和切点：在XML中配置事务管理器：\n\n  ```xml\n  <!--配置事务管理器-->\n  <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\">\n      <property name=\"dataSource\" ref=\"dataSource\"/>\n  </bean>\n  ```\n\n* 引入事务的注解驱动：\n\n  ```xml\n  <!--引入事务的注解驱动-->\n  <tx:annotation-driven transaction-manager=\"transactionManager\"/>\n  ```\n  \n* 在多个方法间使用事务可能出现的问题参考我的面试总结的md文件，里面提到了A方法调用B方法的事务问题\n\n# MyBatis\n\n* 原始JDBC开发存在的问题：\n  * 数据库频繁创建连接、释放造成系统资源的浪费从而影响系统性能\n  * SQL语句在代码中硬编码，造成代码不易维护，实际应用SQL变化的可能较大，SQL变动需要改动Java代码\n  * 查询操作时，需要手动将结果集中的数据封装到实体中。插入操作时需要手动将实体的数据设置到SQL语句中\n\n* 解决方案：\n  * 使用数据库连接池初始化连接资源\n  * 将SQL语句抽取到XML配置文件中\n  * 使用反射、内省等底层技术，自动将实体与表进行属性与字段的自动映射\n\n## 概念\n\n* MyBatis是一个优秀的基于Java的持久层框架，它内部封装了JDBC，使开发者只需要关注SQL语句本身，而不需要花费精力去加载驱动、创建连接、创建statement等繁杂的过程\n* MyBatis通过XML或注解方式将要执行的各种statement配置起来，并通过Java对象和statement中SQL的动态参数进行映射生成最终执行的SQL语句\n* 最后MyBatis框架执行SQL并将结果映射为Java对象并返回，采用ORM思想解决了实体和数据库映射的问题，对JDBC进行了封装，屏蔽了JDBC API底层访问细节，使我们不再与JDBC API打交道就可以完成数据库的操作\n\n## MyBatis快速入门\n\n1. 添加MyBatis坐标\n\n   ```xml\n   <dependencies>\n       <dependency>\n           <groupId>mysql</groupId>\n           <artifactId>mysql-connector-java</artifactId>\n           <version>5.1.38</version>\n       </dependency>\n       <dependency>\n           <groupId>org.mybatis</groupId>\n           <artifactId>mybatis</artifactId>\n           <version>3.5.7</version>\n       </dependency>\n   </dependencies>\n   ```\n\n2. 创建数据表\n\n3. 编写实体类\n\n4. 编写XML映射文件\n\n   ```xml\n   <?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n   <!DOCTYPE mapper\n           PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n           \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n   <mapper namespace=\"userMapper\">\n       <select id=\"findAll\" resultType=\"com.taoyyz.domain.User\">\n           select * from user\n       </select>\n   </mapper>\n   ```\n\n5. 编写MyBatis核心配置文件\n\n   ```xml\n   <?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n   <!DOCTYPE configuration\n           PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\"\n           \"http://mybatis.org/dtd/mybatis-3-config.dtd\">\n   <configuration>\n       <!--数据源环境-->\n       <environments default=\"mysql\">\n           <!--配置一个环境-->\n           <environment id=\"mysql\">\n               <transactionManager type=\"JDBC\"/>\n               <!--配置数据源为POOLED池-->\n               <dataSource type=\"POOLED\">\n                   <property name=\"driver\" value=\"com.mysql.jdbc.Driver\"/>\n                   <property name=\"url\" value=\"jdbc:mysql:///db3\"/>\n                   <property name=\"username\" value=\"root\"/>\n                   <property name=\"password\" value=\"199988\"/>\n               </dataSource>\n           </environment>\n       </environments>\n       <!--加载映射文件-->\n       <mappers>\n           <mapper resource=\"com/taoyyz/mapper/UserMapper.xml\"/>\n       </mappers>\n   </configuration>\n   ```\n\n6. 测试\n\n   ```java\n   //1.读取MyBatis核心配置文件\n   InputStream is = Resources.getResourceAsStream(\"mybatis-config.xml\");\n   //2.利用工厂类建造者对象建造一个工厂类对象\n   SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);\n   //3.利用工厂对象生产一个会话对象\n   SqlSession sqlSession = sqlSessionFactory.openSession();\n   //4.利用对话对象执行SQL语句，参数就是mapper的命名空间和id\n   List<User> userList = sqlSession.selectList(\"userMapper.findAll\");\n   //5.打印数据\n   System.out.println(userList);\n   //6.释放资源\n   sqlSession.close();\n   is.close();\n   ```\n\n<font color=red>踩坑！！数据库字段名含有下划线时，无法赋值给Java类中的成员属性（转驼峰）</font>\n\n* 解决方法：\n\n  * 在Java成员属性命名时，考虑数据库字段名下划线转驼峰，也就是字段`dept_id`赋值时匹配的是`deptId`\n\n  * 关掉MyBatis的转驼峰，默认为false\n\n    ```xml\n    <!--关掉驼峰转换-->\n    <setting name=\"mapUnderscoreToCamelCase\" value=\"false\"/>\n    ```\n\n## MyBatis的CRUD\n\n### 插入数据\n\n1. 在映射文件中编写SQL语句\n\n   ```xml\n   <!--配置此insert语句的id和参数类型-->\n   <insert id=\"insert\" parameterType=\"com.taoyyz.domain.User\">\n       insert into user values (#{id},#{username},#{password})\n   </insert>\n   ```\n\n   * 使用`parameterType`属性指定参数的类型的全类名，这里是User类型\n\n   * 使用`#{实体参数的属性名}`取出User类型的成员属性值作为占位符\n\n   * **主键自增：**需要对插入的数据实行主键自增，可以在insert标签利用以下属性（有顺序要求）\n\n     ```xml\n     <insert id=\"save\" parameterType=\"emp\" keyProperty=\"id\" useGeneratedKeys=\"true\">\n     ```\n\n2. 调用SqlSession对象的`int insert(String s,Object o)`方法\n\n   * 参数1：要执行的映射文件中的SQL语句的命名空间和id，例如`userMapper.insert`\n   * 参数2：SQL语句插入所需要的参数对象，例如User对象\n\n3. 调用SqlSession对象的`commit()`方法提交事务\n\n4. 释放连接：关闭SqlSession与输入流\n\n### 修改数据\n\n1. 在映射文件中编写SQL语句\n\n   ```xml\n   <!--配置此update语句的id和参数类型-->\n   <update id=\"update\" parameterType=\"com.taoyyz.domain.User\">\n       update user set username = #{username},password = #{password} where id = #{id}\n   </update>\n   ```\n\n   * 使用`parameterType`属性指定参数的类型的全类名，这里是User类型\n   * 使用`#{实体参数的属性名}`取出User类型的成员属性值作为占位符\n\n2. 调用SqlSession对象的`int update(String s,Object o)`方法\n\n   * 参数1：要执行的映射文件中的SQL语句的命名空间和id，例如`userMapper.update`\n   * 参数2：SQL语句修改所需要的参数对象，例如User对象\n\n3. 调用SqlSession对象的`commit()`方法提交事务\n\n4. 释放连接：关闭SqlSession与输入流\n\n### 删除数据\n\n1. 在映射文件中编写SQL语句\n\n   ```xml\n   <!--配置此delete语句的id和参数类型-->\n   <delete id=\"delete\" parameterType=\"java.lang.Integer\">\n       delete from user where id = #{单个参数名随便写}\n   </delete>\n   ```\n\n   * 使用`parameterType`属性指定参数的类型的全类名，这里是Integer类型\n   * 使用`#{参数的属性名}`取出参数类型的值，这里是单个值随意\n\n2. 调用SqlSession对象的`int delete(String s,Object o)`方法\n\n   * 参数1：要执行的映射文件中的SQL语句的命名空间和id，例如`userMapper.delete`\n   * 参数2：SQL语句修改所需要的参数对象，例如一个Integer对象\n\n3. 调用SqlSession对象的`commit()`方法提交事务\n\n4. 释放连接：关闭SqlSession与输入流\n\n## MyBatis核心配置文件\n\n**值得注意的是，configuration标签中的子标签是有顺序要求的，例如typeAliases必须位于properties后面**\n\n* `configuration`标签的配置\n  * `properties`属性\n  * `settings`设置\n  * `typeAliases`类型别名\n  * `typeHandlers`类型处理器\n  * `objectFactory`对象工厂\n  * `plugins`插件\n  * `environments`环境配置，支持配置多个环境，可以通过`default`属性指定默认的环境名称\n    * `environment`配置某个环境，通过`id`属性区分\n      * `transactionManager`事务管理器，`type`属性设置为JDBC\n      * `dataSource`数据源，`type`属性设置为`POOLED`连接池\n        * driver、url、username、password等属性\n  * `databaseIdProvider`数据库厂商标识\n  * `mappers`映射器，加载Mapper.xml文件\n\n### environments标签\n\n* 对于事务管理器transactionManager，它的`type`属性有两种：\n  * `JDBC`：这个配置就是直接使用了JDBC的提交和回滚设置，它依赖于从数据源得到的连接来管理事务作用域\n  * `MANAGED`：这个配置几乎没做什么，它从来不提交或回滚一个连接，而是让容器来管理事务的整个声明周期（比如JEE应用服务器的上下文）。默认情况下它不会关闭连接，然而一些容器并不希望这样，因此需要将`closeConnection`属性设置为false来组织它默认的关闭行为\n* 对于数据源dataSource，它的`type`属性有三种：\n  * `UNPOOLED`：这个数据源的实现只是每次被请求时打开和关闭连接\n  * `POLLED`：这个数据源的实现利用**池**的概念将JDBC连接对象组织起来\n  * `JNDI`：这个数据源的实现是为了能在如EJB或应用服务器这类容器中使用，容器可以集中或在外部配置数据源，然后放置在一个JNDI上下文的引用\n\n### mapper标签\n\n* `mapper`标签用于加载映射，加载方式有以下几种：\n\n  * 使用相对`resources`资源目录的类路径引用，例如：\n\n    ```xml\n    <mapper resource=\"com/taoyyz/mapper/UserMapper.xml\"/>\n    ```\n\n  * 使用完全限定资源定位符(URL)，例如\n\n    ```xml\n    <mapper url=\"file:///var/mappers/UserMapper.xml\"/>\n    ```\n\n  * 使用映射器接口实现类的全类名，例如：\n\n    ```xml\n    <mapper class=\"org.mybatis.builder.UserMapper.xml\"/>\n    ```\n\n  * 将包内的映射器接口实现全部注册为映射器（类似于扫描包），例如:\n\n    ```xml\n    <package name=\"org.mybatis.builder\"/>\n    ```\n\n### properties标签\n\n* `properties`标签用于加载properties配置文件\n\n  ```xml\n  <!--加载properties配置文件-->\n  <properties resource=\"jdbc.properties\"/>\n  ```\n\n* 使用SpEL表达式即可取出配置文件中的key\n\n### typeAliases标签\n\n* typeAliases标签用于为Java类型的全限定名设置一个别名\n\n  * 定义全类名的别名：`com.taoyyz.domain.User`的别名为`user`\n\n  ```xml\n  <!--定义别名-->\n  <typeAliases>\n      <typeAlias type=\"com.taoyyz.domain.User\" alias=\"user\"/>\n  </typeAliases>\n  ```\n\n* MyBatis框架已经为我们设置好了一些类型的别名\n\n  <img src=\"/images/image-20210823172634343.png\" alt=\"image-20210823172634343\" style=\"zoom: 50%;\" />\n  \n* Mybatis会自动扫描指定包下面的JavaBean，并且默认设置一个别名，**默认的名字为： JavaBean 的首字母小写的非限定类名**来作为它的别名（其实别名是不去分大小写的）。也可在javabean 加上**注解@Alias 来自定义别名**， 例如： **@Alias(user)**，也可以在配置文件中用 `<typeAliases>` 的子元素  `<package>` 来让Mybatis自动扫描\n\n  ```xml\n  <typeAliases>\n      <package name=\"com.domain\"/>\n  </typeAliases>\n  ```\n\n## MyBatis相关API\n\n### SqlSessionFactoryBuilder\n\n* SqlSession的工厂建造器，`build()`方法用于构造一个工厂对象\n\n  * `SqlSessionFactory build(InputStream inputStream)`\n    * 此方法传入一个InputStream输入流对象用于指示MyBatis核心配置文件的路径\n    * 会返回一个SqlSessionFactory工厂对象\n    * 建议使用`org.apache.ibatis.io Resources`类的`getResourceAsStream(String resource)`方法传入一个类加载路径下的文件名，获得此文件名的InputStream输入流\n\n* 用法：\n\n  ```java\n  //1.读取MyBatis核心配置文件\n  InputStream is = Resources.getResourceAsStream(\"mybatis-config.xml\");\n  //2.利用工厂类建造者对象建造一个工厂类对象\n  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);\n  ```\n\n### SqlSessionFactory\n\n* SqlSession的工厂类，用于创建SqlSession的实例，常用方法：\n  * `SqlSession openSession()`：默认开启一个事务，但事务不会自动提交，需要手动`commit()`\n  * `SqlSession openSession(boolean autoCommit)`：参数可以指定是否自动提交\n\n### SqlSession\n\n* 会话对象，用于操作数据库、执行SQL、提交或回滚事务、获取映射器实例等\n* 执行SQL语句的方法主要有：\n  * `T selectOne(String statement,Object parameter)`\n  * `List<E> selectList(String statement,Object parameter)`\n  * `int insert(String statement,Object parameter)`\n  * `int update(String statement,Object parameter)`\n  * `int delete(String statement,Object parameter)`\n* 操作事务的主要方法有：\n  * `void commit()`\n  * `void rollback()`\n\n## MyBatis的Dao层实现\n\n### 传统方式\n\n编写Dao层接口，在方法里利用MyBatis的SqlSession对象查询数据库并返回结果\n\n```java\npublic class UserDaoImpl implements UserDao {\n    @Override\n    public List<User> findAll() throws IOException {\n        InputStream is = Resources.getResourceAsStream(\"mybatis-config.xml\");\n        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);\n        SqlSession sqlSession = sqlSessionFactory.openSession();\n        List<User> userList = sqlSession.selectList(\"userMapper.findAll\");\n        return userList;\n    }\n}\n```\n\n### 代理开发方式\n\n* 采用MyBatis的代理开发方式实现Mapper/Dao层开发是主流方式\n* Mapper/Dao层接口开发方法只需要编写Mapper接口（Dao接口），由MyBatis框架根据接口定义创建接口的动态代理对象，代理对象的方法体同上边Mapper/Dao层接口实现类的方法\n* Mapper接口开发需要遵循以下规范：\n  * Mapper.xml文件中的**`mapper`标签的`namespace`**要与Mapper/Dao层**接口的全类名相同**\n  * Mapper/Dao层**接口的方法名**和Mapper.xml中定义的每个操作(例如`select`)标签的**`id`相同**\n  * Mapper/Dao层**接口方法的参数类型**和Mapper.xml中定义的每个SQL的**parameterType的类型相同**\n  * Mapper/Dao层**接口方法的返回值类型**和Mapper.xml中定义的每个SQL的**resultType的类型相同**\n\n## MyBatis映射文件高级\n\n### 动态SQL\n\n* `if`标签配合`where`标签：可以根据条件决定是否拼接SQL，如果`if`写在`where`中，相当于`where 1 = 1`\n\n  ```xml\n  <select id=\"findByCondition\" resultType=\"user\" pa\n      select * from user\n      <where>\n          <if test=\"id!=0 and id!=null\">\n              and id = #{id}\n          </if>\n          <if test=\"username!=null\">\n              and username = #{username}\n          </if>\n          <if test=\"password!=null\">\n              and password = #{password}\n          </if>\n      </where>\n  </select>\n  ```\n\n* `foreach`标签，用于遍历参数，`collection`属性可以为`list`或`array`\n\n  ```xml\n  <select id=\"findByIds\" resultType=\"user\" parameterType=\"list\">\n      select * from user\n      <where>\n          <foreach collection=\"list\" open=\"id in(\" item=\"id\" separator=\",\" close=\")\">\n              #{id}\n          </foreach>\n      </where>\n  </select>\n  ```\n\n### 抽取\n\n把重复的SQL语句抽取到`mapper`标签内的`sql`标签内：\n\n```xml\n<sql id=\"selectUser\">select * from user</sql>\n<select id=\"findByCondition\" resultType=\"user\" parameterType=\"user\">\n    <!--引用抽取的sql即可-->\n    <include refid=\"selectUser\"/>\n</select>\n```\n\n## MyBatis核心配置文件高级\n\n### typeHandlers标签\n\n* MyBatis在预处理语句（PreparedStatement）中设置参数或从结果集取出数据时，都会用**类型处理器**将获取的值以合适的方式转化为Java的数据类型，以下是一些默认的类型处理器：\n\n![image-20210823211628684](/images/image-20210823211628684.png)\n\n* 可以重写类型处理器或者创建类型处理器来处理不支持的或非标准的类型。\n\n  * 具体做法为：实现`org.apache.ibatis.type.TypeHandler`接口，或继承一个很便利的类`org.apache.ibatis.type.BaseTypeHandler`，然后可以选择将它映射到一个JDBC类型。\n  * 例如需求：一个Java中的Date数据类型，将它存储到数据库时存为一个毫秒值，取出来时转换成Java的Date类型，也就是在Java的Date类型与数据库的varchar毫秒值之间转换\n\n* 开发步骤：\n\n  1. 定义转换类，继承`BaseTypeHandler<T>`\n\n  2. 重写其中的方法，其中\n\n     * `setNonNullParameter`为Java程序设置到数据库的回调方法\n     * `getNullableResult`为查询时MySQL字符串类型转换为Java的Type类型的方法\n\n  3. 在MyBatis核心配置文件中注册此转换类\n\n     ```xml\n     <!--自定义类型处理器-->\n     <typeHandlers>\n         <typeHandler handler=\"com.handler.DateTypeHandler\"/>\n     </typeHandlers>\n     ```\n\n  4. 测试\n\n### plugins标签\n\n* 概念\n\n  * MyBatis可以使用第三方的插件来对功能进行扩展，**分页助手PageHelper**是将分页的复杂操作进行封装，使用简单的方式即可获得分页的相关数据\n\n* 开发步骤：\n\n  1. 导入通用PageHelper的坐标\n\n     ```xml\n     <dependency>\n         <groupId>com.github.pagehelper</groupId>\n         <artifactId>pagehelper</artifactId>\n         <version>4.2.1</version>\n     </dependency>\n     <dependency>\n         <groupId>com.github.jsqlparser</groupId>\n         <artifactId>jsqlparser</artifactId>\n         <version>0.9.5</version>\n     </dependency>\n     ```\n\n  2. 在MyBatis核心配置文件中配置PageHelper插件\n\n     ```xml\n     <!--配置分页插件-->\n     <plugins>\n         <plugin interceptor=\"com.github.pagehelper.PageHelper\">\n             <!--配置方言-->\n             <property name=\"dialect\" value=\"mysql\"/>\n         </plugin>\n     </plugins>\n     ```\n\n  3. 测试：利用`PageHelper`类的静态方法操作分页\n\n     ```java\n     PageHelper.startPage(2,3); //页码为第2页，分页大小为显示3条\n     ```\n\n* 获得分页相关参数：\n\n  ```java\n  //获得分页相关参数，利用一个PageInfo对象，参数为当前结果集\n  PageInfo<User> pageInfo = new PageInfo<>(userList);\n  System.out.println(\"当前页为 \" + pageInfo.getPageNum());\n  System.out.println(\"显示条数 \" + pageInfo.getPageSize());\n  System.out.println(\"总条数 \" + pageInfo.getTotal());\n  System.out.println(\"总分页数 \" + pageInfo.getPages());\n  System.out.println(\"上一页的页码 \" + pageInfo.getPrePage());\n  System.out.println(\"下一页的页码 \" + pageInfo.getNextPage());\n  System.out.println(\"是否第一页 \" + pageInfo.isIsFirstPage());\n  System.out.println(\"是否最后一页 \" + pageInfo.isIsLastPage());\n  ```\n\n## MyBatis多表操作\n\n* 一对一：用`<resultMap>`配置\n* 一对多：用`<resultMap>` + `<collection>`配置\n* 多对多：用`<resultMap>` + `<collection>`配置\n\n### 一对一\n\n`Emp`类中存在一个成员属性：`Dept`对象，表示每一个Emp员工所在的Dept部门，一个员工对应属于一个部门\n\n```xml\n<mapper namespace=\"com.taoyyz.dao.EmpDao\">\n    <!--配置一个结果Map集合，类型是Emp全类名，这里用别名emp-->\n    <resultMap id=\"empMap\" type=\"emp\">\n        <!--配置主键，column是数据库查询出的字段名，property是实体类属性名-->\n        <id column=\"empid\" property=\"id\"/>\n        <!--其他属性用result，column是查询的数据库结果名，property是实体类属性名-->\n        <result column=\"empname\" property=\"name\"/>\n        <result column=\"empdept_id\" property=\"dept_id\"/>\n        <!--<result column=\"deptdept_id\" property=\"dept.dept_id\"/>-->\n        <!--<result column=\"deptname\" property=\"dept.name\"/>-->\n        <!--<result column=\"deptloc\" property=\"dept.loc\"/>-->\n        <!--另一种方式，property是Emp中的属性名称dept，javaType是Emp中的属性类型-->\n\t\t<association property=\"dept\" javaType=\"dept\">\n    \t\t<id column=\"ddept_id\" property=\"dept_id\"/>\n    \t\t<result column=\"dname\" property=\"name\"/>\n    \t\t<result column=\"dloc\" property=\"loc\"/>\n\t\t</association>\n    </resultMap>\n    <select id=\"findAll\" resultMap=\"empMap\">\n        select emp.id       as empid\n             , emp.name     as empname\n             , emp.dept_id  as empdept_id\n             , dept.dept_id as deptdept_id\n             , dept.name    as deptname\n             , dept.loc     as deptloc\n        from emp,\n             dept\n        where emp.dept_id = dept.dept_id\n    </select>\n</mapper>\n```\n\n### 一对多\n\n`Dept`类中存在一个成员属性：`List<Emp>`集合，表示一个Dept部门下可能存在多个Emp员工\n\n```xml\n<mapper namespace=\"com.taoyyz.dao.DeptDao\">\n    <!--配置一个结果Map集合，类型是Dept全类名，这里用别名dept-->\n    <resultMap id=\"deptMap\" type=\"dept\">\n        <id column=\"ddept_id\" property=\"dept_id\"/>\n        <result column=\"dname\" property=\"name\"/>\n        <result column=\"dloc\" property=\"loc\"/>\n        <!--配置集合信息，property是Dept中集合的名称，ofType是Dept中集合装的数据类型-->\n        <collection property=\"empList\" ofType=\"emp\">\n            <id column=\"eid\" property=\"id\"/>\n            <result column=\"ename\" property=\"name\"/>\n            <result column=\"edept_id\" property=\"dept_id\"/>\n        </collection>\n    </resultMap>\n    <select id=\"findAll\" resultMap=\"deptMap\">\n        SELECT dept.dept_id as ddept_id,\n               dept.name    as dname,\n               dept.loc     as dloc,\n               emp.id       as eid,\n               emp.name     as ename,\n               emp.dept_id  as edept_id\n        FROM emp,\n             dept\n        WHERE emp.dept_id = dept.dept_id\n    </select>\n</mapper>\n```\n\n### 多对多\n\n`Emp`类中存在一个成员属性：`List<Role>`集合，表示每个Emp员工可能有多个角色，关系由一个中间表维护\n\n```xml\n<mapper namespace=\"com.taoyyz.dao.EmpDao\">\n\t<resultMap id=\"empRoleMap\" type=\"emp\">\n        <id column=\"eid\" property=\"id\"/>\n        <result column=\"name\" property=\"name\"/>\n        <result column=\"dept_id\" property=\"dept_id\"/>\n        <collection property=\"roleList\" ofType=\"role\">\n            <id column=\"roleid\" property=\"id\"/>\n            <id column=\"roleName\" property=\"roleName\"/>\n            <id column=\"roleDesc\" property=\"roleDesc\"/>\n        </collection>\n    </resultMap>\n    <select id=\"findAllWithRole\" resultMap=\"empRoleMap\">\n        SELECT *, emp.id as eid\n        FROM emp,\n             role,\n             emp_role\n        where emp.id = emp_role.empid\n          AND role.id = emp_role.roleid\n    </select>\n</mapper>\n```\n\n## MyBatis注解开发\n\n### MyBatis注解实现简单的CRUD\n\n* 在Dao/Mapper接口的方法上打上相应的注解\n\n  ```java\n  public interface UserDao {\n      @Select(\"select * from user\")\n      List<User> findAll() throws IOException;\n      \n      @Insert(\"insert into user values (#{id}, #{username},#{password},#{birthday})\")\n      void insert(User user);\n      \n      @Update(\"update user set username = #{username}, \n              password = #{password} where id = #{id}\")\n      void update(User user);\n              \n      @Delete(\"delete from user where id = #{单个参数名随便写}\")\n      void delete(Integer id);\n              \n      @Select(\"select * from user where id = #{单个参数名随便写}\")\n      User findUserById(Integer id);\n  }\n  ```\n  \n* 在MyBatis核心配置文件中开启注解接口扫描的包\n\n  ```xml\n  <!--从Dao层的包加载映射-->\n  <mappers>\n      <!--加载映射关系的注解的包-->\n      <package name=\"com.taoyyz.dao\"/>\n  </mappers>\n  ```\n\n### MyBatis注解实现多表复杂映射\n\n![image-20210824180725774](/images/image-20210824180725774.png)\n\n![image-20210824180818367](/images/image-20210824180818367.png)\n\n#### 一对一\n\n* 数据库字段与Java成员属性名的对应关系：\n\n  * `@Result(column = \"did\", property = \"dept_id\")`：把查询结果字段dept_id映射赋值给成员属性名\n\n    * 可以使用多个`@Result`来映射多个字段与成员属性名\n      * 区分多个表的成员属性在`property`属性用`成员属性.该成员自身的属性`赋值\n    * 也可以用`@Results({@Result(column = \"did\", property = \"dept_id\")...})`\n\n  * ```java\n    @Select(\"select * from emp\")\n    @Result(column = \"dept_id\", property = \"dept_id\")\n    @Result(\n            //javaType要封装到Emp的成员属性的类，property是要封装到的Emp的成员属性名\n            javaType = Dept.class, property = \"dept\",\n            //column是根据哪个字段查询Dept表的数据，然后用此dept_id去select查Dept表\n            column = \"dept_id\",one = @One(select = \"com.taoyyz.dao.DeptDao\n                                          .findById\")\n    )\n    List<Emp> findAll();\n    ```\n\n    * 这样相当于先查询`@Select(\"select * from emp\")`，然后用查询的结果中包含的`dept_id`去再次调用@One注解传入一个打上了@Select注解的select语句的`全类名.方法名`即可二次封装\n\n#### 一对多\n\n* 也可以采用先`@Select(select * from dept)`查出一张表的数据，然后在@Result里封装映射结果到成员属性里\n\n* 封装的成员属性是一个集合时，也就是一对多查询，此时@Result就要指定`javaType`和`many`\n\n  ```java\n  @Select(\"select * from dept\")\n  @Results({\n          @Result(id = true, column = \"dept_id\", property = \"dept_id\"),\n          @Result(column = \"name\", property = \"name\"),\n          @Result(column = \"loc\", property = \"loc\"),\n          @Result(\n                  column = \"dept_id\", property = \"empList\",\n                  javaType = List.class, many = @Many(select = \"com.taoyyz.dao.EmpDao.findByDeptId\")\n          )\n  })\n  List<Dept> findAllWithEmp();\n  ```\n\n* 关键就是javaType此时是List集合的class，并且由于结果是多个，所以用`many`再查其他表中的记录\n\n#### 多对多\n\n* 关键是第一次查询的emp.id要作为中间表的条件，找到对应的role.id，再根据role.id查询role表中的id\n\n  ```java\n  @Select(\"select *,emp.id as empid from emp\")\n  @Result(column = \"empid\",property = \"id\")\n  @Result(\n          javaType = List.class,property = \"roleList\",\n          column = \"empid\",many = @Many(select = \"com.taoyyz.dao.RoleDao.findById\")\n  )\n  List<Emp> findAllWithRole();\n  ```\n\n  ```java\n  @Select(\"SELECT * FROM emp_role,role where emp_role.roleid = role.id and emp_role.empid = #{empid}\")\n  List<Role> findById(Integer empid);\n  ```\n\n# MyBatis-Plus\n\n## 概述\n\n### 简介\n\n> MyBatis-Plus（简称MP）是一款MyBatis的增强工具，在MyBatis的基础上只做增强不做改变，为简化开发、提高效率而生\n\n## 特性\n\n* **无侵入**：只做增强不做改变，引入它不会对现有工程产生影响，如丝般顺滑\n* **损耗小**：启动即会自动注入基本 CURD，性能基本无损耗，直接面向对象操作\n* **强大的 CRUD 操作**：内置通用 Mapper、通用 Service，仅仅通过少量配置即可实现单表大部分 CRUD 操作，更有强大的条件构造器，满足各类使用需求\n* **支持 Lambda 形式调用**：通过 Lambda 表达式，方便的编写各类查询条件，无需再担心字段写错\n* **支持主键自动生成**：支持多达 4 种主键策略（内含分布式唯一 ID 生成器 - Sequence），可自由配置，完美解决主键问题\n* **支持 ActiveRecord 模式**：支持 ActiveRecord 形式调用，实体类只需继承 Model 类即可进行强大的 CRUD 操作\n* **支持自定义全局通用操作**：支持全局通用方法注入（ Write once, use anywhere ）\n* **内置代码生成器**：采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码，支持模板引擎，更有超多自定义配置等您来使用\n* **内置分页插件**：基于 MyBatis 物理分页，开发者无需关心具体操作，配置好插件之后，写分页等同于普通 List 查询\n* **分页插件支持多种数据库**：支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库\n* **内置性能分析插件**：可输出 SQL 语句以及其执行时间，建议开发测试时启用该功能，能快速揪出慢查询\n* **内置全局拦截插件**：提供全表 delete 、 update 操作智能分析阻断，也可自定义拦截规则，预防误操作\n\n## 框架结构\n\n<img src=\"/images/image-20210918162712849.png\" alt=\"image-20210918162712849\" style=\"zoom:80%;\" />\n\n## 快速开始及3种用法\n\n1. 创建Maven工程\n\n2. 导入相关坐标\n\n   ```xml\n   <dependencies>\n       <!-- mybatis-plus插件依赖 -->\n       <dependency>\n           <groupId>com.baomidou</groupId>\n           <artifactId>mybatis-plus</artifactId>\n           <version>3.1.1</version>\n       </dependency>\n       <!--mysql驱动-->\n       <dependency>\n           <groupId>mysql</groupId>\n           <artifactId>mysql-connector-java</artifactId>\n           <version>5.1.38</version>\n       </dependency>\n       <!-- 连接池 -->\n       <dependency>\n           <groupId>com.alibaba</groupId>\n           <artifactId>druid</artifactId>\n           <version>1.2.3</version>\n       </dependency>\n       <!--简化bean代码的工具包-->\n       <dependency>\n           <groupId>org.projectlombok</groupId>\n           <artifactId>lombok</artifactId>\n           <optional>true</optional>\n           <version>1.18.20</version>\n       </dependency>\n       <dependency>\n           <groupId>junit</groupId>\n           <artifactId>junit</artifactId>\n           <version>4.12</version>\n       </dependency>\n       <dependency>\n           <groupId>org.slf4j</groupId>\n           <artifactId>slf4j-log4j12</artifactId>\n           <version>1.6.4</version>\n       </dependency>\n   </dependencies>\n   ```\n\n### 用法1：MyBatis + MP\n\n1. 将UserMapper继承`BaseMapper<T>`\n\n   ```java\n   public interface UserMapper extends BaseMapper<User>\n   ```\n\n   此时UserMapper获得了BaseMapper中的方法：![image-20210918171312644](/images/image-20210918171312644.png)\n\n2. 利用`MybatisSqlSessionFactoryBuilder`构造`SqlSessionFactory`，然后用此工厂产生`SqlSession`\n\n3. 调用mapper对象继承自BaseMapper的方法，例如`List<T> selectList(Wrapper<T> queryWrapper);`\n\n   * 此处的Wrapper是条件构造器，如果没有条件，直接传入null即可\n   * MyBatisPlus默认以实体类名首字母小写作为表名，如果需要自己指定，在类上打上`@TableName(\"tb_user\")`注解指定表名\n   * MyBatisPlus默认开启转驼峰\n\n### 用法2：Spring + MyBatis + MP\n\n1. 引入相关依赖\n\n   ```xml\n   <!-- mybatis-plus插件依赖 -->\n   <dependency>\n       <groupId>com.baomidou</groupId>\n       <artifactId>mybatis-plus</artifactId>\n       <version>3.1.1</version>\n   </dependency>\n   <!--mysql驱动-->\n   <dependency>\n       <groupId>mysql</groupId>\n       <artifactId>mysql-connector-java</artifactId>\n       <version>5.1.38</version>\n   </dependency>\n   <!-- 连接池 -->\n   <dependency>\n       <groupId>com.alibaba</groupId>\n       <artifactId>druid</artifactId>\n       <version>1.2.3</version>\n   </dependency>\n   ```\n\n2. 配置spring的核心配置文件\n\n   * 配置数据源`dataSource`的bean\n   * 配置`sqlSessionFactory`的bean为`MybatisSqlSessionFactoryBean`的类，指定`dataSource`的`property`\n   * 配置mapper包扫描的bean为`MapperScannerConfigurer`的类，指定`basePackage`的`property`\n\n3. 直接在测试类中利用@Autowired注入对应mapper的bean即可使用\n\n### 用法3：SpringBoot + MyBatis + MP\n\n1. 引入起步依赖\n\n   ```xml\n   <dependency>\n       <groupId>com.baomidou</groupId>\n       <artifactId>mybatis-plus-boot-starter</artifactId>\n       <version>3.1.1</version>\n   </dependency>\n   <dependency>\n       <groupId>mysql</groupId>\n       <artifactId>mysql-connector-java</artifactId>\n       <scope>runtime</scope>\n   </dependency>\n   ```\n\n2. 为mapper接口打上@Mapper注解\n\n3. 直接利用@Autowired注入mapper层接口的bean即可\n\n   **注**：SpringBoot使用log4j进行日志显示配置springboot核心配置文件：\n\n   ```yaml\n   logging:\n     level:\n       com:\n         taoyyz:\n           mybatisplusspringboot: debug\n   ```\n\n## 通用CRUD\n\n### 插入\n\n利用mapper层接口的`int insert(T entity)`插入数据，返回值是受影响的行数\n\n* 获取**主键自增**后的结果，只需要在实体类的主键属性打上`@TableId(type = IdType.AUTO)`注解即可把自增主键回写到实体对象\n* **@TableFiled**注解：用于指定字段的一些属性，通常有2种用法：\n  * 对象中的属性名和字段名不一致的问题（非驼峰）\n    * 利用`@TableField(\"数据库端字段名\")`来匹配字段名到属性上\n  * 对象中的属性名在表中不存在的问题\n    * 利用`@TableField(exist = false)`来声明此属性不存在于数据库中\n  * 查询时忽略此属性\n    * 利用`@TableField(select = false)`不查询此字段\n* **@TableName**注解：用于指定实际数据库的表名\n\n### 更新\n\n#### 1.根据id更新\n\n* 利用`int updateById(T entity)`更新此实体对象id对应的数据库内容\n\n#### 2.根据条件更新\n\n* 利用`int update(T entity,Wrapper<T> updateWrapper)`更新满足`updateWrapper`条件的值为此实体对象的值\n\n  `Wrapper`对象可以为：\n\n  * `QueryWrapper`：满足此**查询条件构造器**的数据会被更新为`entity`的属性\n  * `UpdateWrapper`：满足此**更新条件构造器**的数据可以直接利用此构造器的`set(R column, Object val)`方法赋值，此时`entity`传入`null`也可以，在`set()`方法中指定更新的具体字段的值优先级更高\n\n### 删除\n\n#### 1.根据id删除\n\n* 利用`int deleteById(Serializable id)`传入主键删除\n\n#### 2.根据id批量删除\n\n* 利用`int deleteBatchIds(Collection<? extends Serializable> idList)`传入需要删除的id的集合\n\n#### 3.根据字段键值的Map删除\n\n* 利用`int deleteByMap(Map<String, Object> columnMap)`删除满足key对应的字段的值为value的数据\n\n#### 4.根据条件删除\n\n* 利用`int delete(Wrapper<T> wrapper)`删除满足条件选择器的数据\n\n### 查询\n\n#### 1.根据id查询\n\n* 利用`T selectById(Serializable id)`查询此id对应的实体对象\n\n#### 2.根据id批量查询\n\n* 利用`List<T> selectBatchIds(Collection<? extends Serializable> idList)`传入需要查询的id的集合\n\n#### 3.根据字段值的Map查询\n\n* 利用`List<T> selectByMap(Map<String, Object> columnMap)`查询满足key对应字段的值为value的数据\n\n#### 4.根据条件查询一条数据\n\n* 利用`T selectOne(Wrapper<T> queryWrapper)`根据条件查询一条记录，返回封装的实体对象。不止一条结果会异常\n\n#### 5.根据条件查询条数\n\n* 利用`Integer selectCount(Wrapper<T> queryWrapper)`根据条件查询记录的数量\n\n#### 5.根据条件批量查询\n\n* 利用`List<T> selectList(Wrapper<T> queryWrapper)`根据条件查询记录的集合\n\n#### 6.分页查询\n\n* 利用`IPage<T> selectPage(IPage<T> page,Wrapper<T> queryWrapper)`\n  * 需要配置一个拦截器Bean：可以通过`@Configuration`所在的配置类产生`PaginationInterceptor`的Bean。（新版MyBatis-Plus推荐产生`MyBatisPlusInterceptor`的Bean并且添加一个`PaginationInnerInterceptor`作为InnerInterceptor）\n  * 调用查询方法，传入的参数为IPage接口的实现类对象以及条件查询包装器。如`list(page,wrapper)`\n  * 返回一个IPage分页对象\n  * 通过IPage分页对象的方法可以得到总页码等数据，以及`getRecords()`方法得到查询的结果集合\n\n## MyBatis-Plus配置文件\n\n### 基本配置\n\n#### configLocation：指定mybatis核心配置文件的位置\n\n* SpringBoot：\n\n  ```yaml\n  mybatis-plus:\n    config-location: classpath:mybatis-config.xml #指定mybatis核心配置文件的位置\n  ```\n\n* Spring：\n\n  ```xml\n  <bean id=\"sqlSessionFactoryBean\" class=\"com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\">\n      <property name=\"dataSource\" ref=\"dataSource\"/>\n      <property name=\"configLocation\" value=\"classpath:mybatis-config.xml\"/>\n  </bean>\n  ```\n\n#### mapperLocations：配置XML映射文件的位置\n\n* SpringBoot：\n\n  ```yaml\n  mybatis-plus:\n    mapper-locations: classpath*:mapper/*.xml #指定映射文件的位置，这里classpath*是加载所有文件\n  ```\n\n* Spring：\n\n  ```xml\n  <bean id=\"sqlSessionFactoryBean\" class=\"com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\">\n      <property name=\"dataSource\" ref=\"dataSource\"/>\n      <property name=\"mapperLocations\" value=\"classpath:mapper/*.xml\"/>\n  </bean>\n  ```\n\n#### type-aliases-package：配置别名扫描包\n\n* SpringBoot：\n\n  ```yaml\n  mybatis-plus:\n    type-aliases-package: com.taoyyz.mybatisplusspringboot.domain #设置别名扫描包，包下的类会被设置别名\n  ```\n\n* Spring：\n\n  ```xml\n  <bean id=\"sqlSessionFactoryBean\" class=\"com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\">\n      <property name=\"dataSource\" ref=\"dataSource\"/>\n      <property name=\"typeAliasesPackage\" value=\"com.taoyyz.mybatisplusspringboot.domain\"/>\n  </bean>\n  ```\n\n#### table-prefix：配置表名前缀\n\n* SpringBoot\n\n  ```yaml\n  mybatis-plus:\n    global-config:\n      db-config:\n        table-prefix: tb_ #设置表前缀，例如实体类Book对应的表会被认为是tb_book\n  ```\n\n### 进阶配置\n\n#### mapUnderscoreToCamelCase：驼峰映射\n\n* 类型：`boolean`\n\n* MyBatis默认：`false`\n\n* MyBatisPlus默认：`true`\n\n* 用法：配置SpringBoot核心配置文件：\n\n  ```yaml\n  mybatis-plus:\n    configuration:\n      map-underscore-to-camel-case: true # 驼峰映射\n  ```\n\n  **注意：**在SpringBoot核心配置文件中不能指定configLocation，否则会冲突，建议配置MyBatis核心配置文件\n\n#### cacheEnabled：设置全局映射器的缓存\n\n* 类型：`boolean`\n\n* 默认值：`true`\n\n* 用法：配置SpringBoot核心配置文件：\n\n  ```yaml\n  mybatis-plus:\n    configuration:\n      cache-enabled: true # 配置全局缓存\n  ```\n\n# SSM整合\n\n## 原始方式整合\n\n1. 创建数据库和表\n\n![image-20210825133345997](/images/image-20210825133345997.png)![image-20210825133420192](/images/image-20210825133420192.png)\n\n2. 创建Maven工程\n3. 导入Maven依赖项的坐标在pom.xml中\n4. 编写数据库表的实体类domain\n5. 编写操作数据库的Dao/Mapper层接口\n6. 编写操作业务逻辑的Service层接口及实现\n7. 编写Controller层\n8. 编写前端页面\n9. 测试\n\n## Spring整合MyBatis\n\n* 整合思路：\n\n  <img src=\"/images/image-20210825153818996.png\" alt=\"image-20210825153818996\" style=\"zoom:67%;\" />\n\n* 将SqlSessionFactory配置到Spring容器中作为Bean，配置Spring核心配置文件：\n\n  ```xml\n  <!--加载properties文件-->\n  <context:property-placeholder location=\"classpath:jdbc.properties\"/>\n  <!--配置数据源-->\n  <bean id=\"dataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\">\n      <property name=\"driverClass\" value=\"${jdbc.driver}\"/>\n      <property name=\"jdbcUrl\" value=\"${jdbc.url}\"/>\n      <property name=\"user\" value=\"${jdbc.username}\"/>\n      <property name=\"password\" value=\"${jdbc.password}\"/>\n  </bean>\n  <!--配置SqlSessionFactory，需要引入mybatis-spring的1.3.1版本maven依赖-->\n  <bean id=\"sqlSessionFactory\" class=\"org.mybatis.spring.SqlSessionFactoryBean\">\n      <!--设置数据源为bean的引用-->\n      <property name=\"dataSource\" ref=\"dataSource\"/>\n      <!--加载MyBatis核心配置文件-->\n      <property name=\"configLocation\" value=\"classpath:mybatis-config-spring.xml\"/>\n  </bean>\n  <!--扫描mapper所在的包，相当于MyBatis为我们创建实现类-->\n  <bean class=\"org.mybatis.spring.mapper.MapperScannerConfigurer\">\n      <property name=\"basePackage\" value=\"com.taoyyz.mapper\"/>\n  </bean>\n  ```\n\n* <font color=red>**踩坑1：**MyBatis操作找不到Dao/Mapper层接口中的方法</font>\n  * 例如：`com.taoyyz.dao.UserMapper.findAll`找不到\n  * 原因：`resources`目录下的Mapper.xml文件**目录层级**要和`java`下的Dao/Mapper**接口层级相同**\n  * 解决方法：\n    * 检查Mapper.xml的`namespace`命名空间属性是否跟Dao/Mapper接口全类名对应\n    * 检查Mapper.xml的标签`id`属性是否跟Dao/Mapper接口中的方法名对应\n    * 修改目录层级，使得**他们被打包后可以被输出到同一目录**![image-20210825164447435](/images/image-20210825164447435.png)\n  \n* <font color=red>**踩坑2：**`java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy`</font>\n  \n  * 原因：spring-jdbc的jar包依赖没有导入：<img src=\"/images/image-20210825160947082.png\" alt=\"image-20210825160947082\" style=\"zoom:67%;\" />\n  \n* <font color=red>**踩坑3：**`Method com/mchange/v2/c3p0/impl/NewProxyResultSet.isClosed()Z is abstract`</font>\n  * 原因：c3p0版本太老，需要换到0.9.2之后的版本\n  * 解决方法：把maven依赖的c3p0版本从`0.9.1.2`换到`0.9.5.2`\n  * **注意：**可能会从仓库找不到0.9.5.2版本，需要自己去Maven仓库下载jar然后手动执行`mvn install`安装\n    * 在利用`mvn install`手动安装jar包时需要保证当前目录下有pom.xml文件且文件内部不能有错误\n  \n* <font color=red>**踩坑4：**`Failed to introspect Class [com.mchange.v2.c3p0.ComboPooledDataSource] from ClassLoader [ParallelWeb`</font>\n  * 原因：换到c3p0的0.9.2以上版本，内部不再集成`mchange-commons-java`的jar包依赖\n  * 解决方法：手动引入Maven依赖：`mchange-commons-java`的`0.2.20`版本\n  \n* 配置声明式事务控制\n\n  * 引入`tx`和`aop`约束\n\n  * ```xml\n    <!--声明式事务控制-->\n    <!--平台事务管理器-->\n    <bean id=\"transactionManager\" class=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\">\n        <property name=\"dataSource\" ref=\"dataSource\"/>\n    </bean>\n    <!--配置事务增强-->\n    <tx:advice id=\"txAdvice\">\n        <tx:attributes>\n            <tx:method name=\"*\"/>\n        </tx:attributes>\n    </tx:advice>\n    <!--配置事务AOP织入-->\n    <aop:config>\n        <aop:advisor advice-ref=\"txAdvice\" pointcut=\"execution(* com.taoyyz.service.impl..*.*(..))\"/>\n    </aop:config>\n    ```\n","tags":["Java","Mybatis","Spring"],"categories":["Java"]},{"title":"数据库入门","url":"/2021/08/16/数据库/","content":"\n<font size=7 face=\"微软雅黑\">数据库</font>\n\n## 数据库的基本概念：\n\n**数据库：DataBase，简称为DB**\n\n* 数据库：用于存储和管理数据的仓库\n\n* 数据库的**特点**：\n\n  1. 持久化存储数据：数据库是一个文件系统\n  2. 方便存储和管理数据\n  3. 使用了统一的方式操作数据库 —— SQL\n\n* 常见的数据库软件：\n\n  ![image-20210726225240780](/images/image-20210726225240780.png)\n\n  ![image-20210726225800471](/images/image-20210726225800471.png)\n\n  * `MySQL`：开源的免费小型数据库，已经被Oracle收购，MySQL6.x版本开始收费。\n  * `Oracle`：收费的大型数据库，Oracle公司的产品\n  * `DB2`：IBM公司的收费数据库产品，常应用在银行系统中\n  * `SQLServer`：微软公司的中型收费数据库，常用于`C#`、`.NET`等语言\n  * `SyBase`：已淡出历史舞台，提供了一个专业的数据建模工具PowerDesigner\n  * `SQLite`：嵌入式的小型数据库，应用在手机端\n\n## MySQL数据库软件：\n\n* 安装：参见教程\n\n* 卸载：\n\n  1. 在MySQL的安装目录下找到`my.ini`文件，复制其中的`datadir=\"路径\"`这一行\n  2. 控制面板卸载`MySQL`\n  3. 删除掉第1步`datadir`路径的那个目录及所有文件\n\n* 配置：\n\n  * **启动**MySQL服务\n\n    1. 在windows服务里手动启动\n    2. 在`cmd`窗口下输入`services.msc`打开windows的**服务**窗口再启动\n    3. 以管理员身份运行`cmd`，输入`net start mysql`即可启动服务\n\n  * **关闭**MySQL服务\n\n    1. 在windows服务里手动关闭\n    2. 在`cmd`窗口下输入`services.msc`打开windows的**服务**窗口再关闭\n\n    3. 以管理员身份运行`cmd`，输入`net stop mysql`即可启动服务\n\n  * **登录**MySQL：\n\n    * 控制台输入`mysql -u用户名 -p密码`，也可以`-p`不跟密码，然后再密文输入密码\n\n    * 远程登录：\n\n      1. `mysql -h主机地址 -u用户名 -p密码`\n\n         例如登录本机：`mysql -hlocalhost -uroot -p123456`\n\n      2. `mysql --host=主机地址 --user=用户名 --password=密码`\n\n         例如登录本机`mysql --host=localhost --user=root --password=123456`\n\n  * **退出**MySQL：\n\n    * 控制台输入`exit`\n    * 控制台输入`quit`\n\n* MySQL目录结构：\n\n  * MySQL安装目录：\n\n  * MySQL数据目录：\n\n    ![image-20210726234508663](/images/image-20210726234508663.png) \n\n  * 几个概念：\n\n    * 数据库：文件夹\n    * 表：文件\n    * 字段：表中的每一行记录\n\n## SQL\n\n### 概念\n\n结构化查询语言（Structured Query Language）简称SQL，是一种特殊目的的编程语言，是一种数据库查询和[程序设计语言](https://baike.baidu.com/item/程序设计语言/2317999)，用于存取数据以及查询、更新和管理[关系数据库系统](https://baike.baidu.com/item/关系数据库系统)。\n\nSQL定义了操作所有关系型数据库的规则，但是不同的数据库的操作方式可能存在差异，称为\"方言\"\n\n### 通用语法\n\n* SQL语句可以写单行或多行，**以分号结尾**\n* 可使用空格和缩进来增强语句的可读性<img src=\"/images/image-20210726235040097.png\" alt=\"image-20210726235040097\"  />\n* MySQL数据库的SQL语句**不区分大小写**，但关键字建议大写\n* 注释：\n  * 单行注释：`-- 注释内容` 或 `#注释内容`(MySQL特有)，`-- 注释内容`的形式注意空格\n  * 多行注释：`/* 注释内容 */`\n\n### SQL分类\n\n![image-20210727001101914](/images/image-20210727001101914.png)\n\n#### DDL(Data Definition Language)数据定义语言\n\n用来定义数据库对象：数据库、表、列等。\n\n关键字：`create`、`drop`、`alter`等\n\n* **操作数据库：CRUD**\n\n  * `C(Create)`：创建\n\n    * <font color=OrangeRed>创建</font>数据库：`create database 数据库名称;`如果数据库已存在会报错\n    * 创建数据库，判断数据库不存在才创建：`create database if not exists 数据库名称;`\n    * 创建数据库，并且指定字符集：`create database 数据库名称 character set 字符集;`\n\n  * `R(Retrieve)`：查询\n\n    * <font color=OrangeRed>查询所有</font>数据库的名称列表：`show databases;`\n\n      对于MySQL自带的几个数据库：\n\n      1. `information_schema`：存放*视图*，并不对应真正的物理文件\n      2. `mysql`：核心数据库，存放核心数据\n      3. `performance_schema`：性能相关\n      4. `test`：测试数据库，可以随意修改\n\n    * <font color=OrangeRed>查看某个</font>数据库的创建语句(以及字符集)：`show create database 数据库名称;`\n\n  * `U(Update)`：修改\n\n    * <font color=OrangeRed>修改</font>数据库的字符集：`alter database 数据库名称 character set 字符集名称;`\n\n  * `D(Delete)`：删除\n\n    * <font color=OrangeRed>删除</font>数据库：`drop database 数据库名称;`如果数据库不存在会报错\n    * 删除数据库，判断数据库存在才会删除：`drop database if exists 数据库名称;`\n\n  * 使用数据库\n\n    * <font color=OrangeRed>查询当前正在使用的数据库</font>名称：`select database();`：可能返回NULL或正在使用的数据库名称\n    * <font color=OrangeRed>使用某个数据库</font>：`use 数据库名称;`\n\n* **操作表**\n\n  * `C(Create)`：创建\n\n    * 语法：\n\n      1. <font color=OrangeRed>创建表</font>，同时指定列名和数据类型：\n\n         `crate table 表名(列名1 数据类型1,列名2 数据类型2,...,列名N 数据类型N)`\n\n         **注意：**最右一个列名和数据类型不能加`,`逗号\n\n         **数据类型：**(除了数字类型，其他类型要使用引号引起来(单双都可以))\n\n         ​\t常用的数据类型：\n\n          1. `int(integer)`：整数类型，例如`age int`\n\n          2. `double`：小数类型，需要指定小数位数，例如`score double(5,2)`表示一共5位，小数点后2位\n\n          3. `date`：日期类型，只包含年月日，格式为`yyyy-MM-dd`\n\n          4. `datetime`：日期类型，包含年月日时分秒，格式为`yyyy-MM-dd HH:mm:ss`\n\n          5. `timestamp`：时间戳类型，包含年月日时分秒，格式为`yyyy-MM-dd HH:mm:ss`\n\n             **`datetime`和`timestamp`的区别**：如果不给`timestamp`赋值，或给`timestamp`赋值为`null`，则默认使用系统当前时间赋值，适用于添加当前时间\n\n         \t6. `varchar`：字符串类型，需要指定最大字符长度，例如`name varchar(20)`\n\n      2. <font color=OrangeRed>从其他表复制表</font>：`create table 表名 like 被复制的表名;`\n\n  * `R(Retrieve)`：查询\n\n    * <font color=OrangeRed>查询某个数据库中所有的表的名称</font>：`show tables;`\n    * <font color=OrangeRed>查询表结构</font>：`desc 表名;`\n\n  * `U(Update)`：修改\n\n    * <font color=OrangeRed>修改表名</font>：`alter table 表名 rename to 新表名`;\n    * <font color=OrangeRed>修改表的字符集</font>：`alter table 表名 character set 字符集名称`;\n    * <font color=OrangeRed>**修改表中的字段**</font>：\n      1. <font color=DeepSkyBlue>添加一列</font>：`alter table 表名 add 添加的列名 添加的数据类型;`\n      2. <font color=DeepSkyBlue>修改列名称和类型</font>：\n         * 只修改列的类型：`alter table 表名 modify 列名 修改之后的数据类型;`\n         * 同时修改列名和数据类型：`alter table 表名 change 原列名 新列名 新数据类型;`\n      3. <font color=DeepSkyBlue>删除列</font>：`alter table 表名 drop 被删除的列名;`\n\n  * `D(Delete)`：删除\n\n    * <font color=OrangeRed>删除表</font>：`drop table 表名`如果表不存在会报错\n    * 删除表，判断表存在才会删除：`drop table if exists 表名`\n\n#### DML(Data Manipulation Language)数据操作语言\n\n用来对数据库中表的数据进行增删改。\n\n关键字：`insert`、`delete`、`update`等\n\n* 添加数据\n\n  * 语法\n\n    * <font color=OrangeRed>添加数据到某些对应的列</font>：`insert into 表名(列名1,列名2,...,列名N) values(值1,值,...,值N);`\n  * 添加数据到<font color=OrangeRed>每一列</font>：`insert into 表名 values(值1,值,...,值N);` **此时必须为每一列赋值**\n    \n    **注意：**\n    \n    1. 列名要和值一一对应\n    2. 如果要给所有的列添加数据，可以省略表名后`(列名...)`的内容\n\n* 删除数据\n\n  * 语法\n\n    * <font color=OrangeRed>按条件删除</font>：`delete from 表名 where 条件;`\n\n    * <font color=OrangeRed>删除表中所有记录</font>：`delete from 表名;`:warning:\n\n      另一种方式：`truncate 表名;`或`truncate table 表名;`:warning:\n\n    **注意：**\n\n    1. :warning:如果不加`where`条件，则删除表中的所有记录\n\n    2. :warning:如果要删除所有记录，不加`where`条件即可，相当于对每一行删除，效率低\n\n       :warning:推荐使用`truncate`删除表，相当于直接删除原表，再创建一个一模一样的空表，效率高\n\n       ![image-20210727172303571](/images/image-20210727172303571.png)\n\n* 修改数据\n\n  * 语法：\n    * :warning:<font color=OrangeRed>更新指定列名的所有位置数据</font>：`update 表名 set 列名1 = 值1,列名2 = 值2,...,列名N = 值N;`\n    * <font color=OrangeRed>按条件更新指定列名的数据</font>：`update 表名 set 列名1 = 值1,...列名N = 值 N where 条件;`\n\n#### DQL(Data Query Language)数据查询语言\n\n用来查询数据库中表的记录（数据）。\n\n关键字：`select`、`where`等\n\n* 查询数据：\n\n  * <font color=red>基础查询</font>\n\n    * <font color=DeepSkyBlue>查询表中所有字段的记录</font>：`select * from 表名;`\n\n    * 查询需要的字段：`select 字段1,字段2,...字段N from 表名;`\n\n    * <font color=DeepSkyBlue>去除重复</font>结果：`select distinct 字段 from 表名`\n\n      **注意：**`DISTINCT`去重的依据是查询出来的字段那一行**所有字段值完全相同，否则不认为是重复**\n\n    * <font color=DeepSkyBlue>引入表达式</font>，例如计算两个字段的和可以写成：`select num1 + num2 from 表名;`\n\n      **注意：**如果`num1`或`num2`有一个为`NULL`，那么结果都为`NULL`\n\n      解决方法：利用`IFNULL(exp1,exp2)`判空，如果`exp1`的值为`NULL`，则表达式的值为`exp2`\n\n    * <font color=DeepSkyBlue>给字段起别名</font>：`select 字段1 as 别名1,字段2 as 别名2,...,字段N as 别名N from 表名;`\n\n      **注意**：利用`as`起别名可以省略as，用空格代替即可\n\n  * <font color=red>条件查询</font>：`select * from 表名 where 条件;`\n\n    * where字句后跟查询条件\n\n    * 运算符\n\n      * `>`、`<`、`<=`、`>=`、`=`、`<>`\n\n        **注意**：在`SQL`中：\n\n        1. <font color=orange>等于</font>就是`=`，而不是Java中的`==`\n        2. <font color=orange>不等于</font>可以用`!=`表示，也可以用`<>`表示\n\n      * `between value1 and value2`：选取满足`value1`到`value2`之间值的行，`mysql`中包括边界\n\n      * `in`：简化对于同一个值的选取，例如`where age=18 or age=20 or age=22;`\n\n        简化之后可以写作：`where age in (18,20,22);`\n\n      * **`like`：模糊查询**\n\n        * 占位符：\n          1. `_`：单个任意字符\n          2. `%`：0个或多个任意字符\n\n      * `is NULL`：判断某字段的值为`NULL`不能直接用`=`号，要用字段值`is NULL`判断\n\n      * `and`或`&&`：逻辑与\n\n      * `or`或`||`：逻辑或，同一个字段的多次连续`||`推荐使用`in(值的集合)`\n\n      * `not`或`!`：逻辑非\n\n        **not可应用于：**\n\n        1. 对`between...and`取反：`not between...and`，不介于...之间\n        2. 对`is NULL`取反：`is not NULL`，某字段的值不为空\n\n  * <font color=red>排序查询</font>：`select * from 表名 order by 排序字段1 排序方式1,排序字段2 排序方式2,...;`\n\n    * 排序方式：默认为`ASC`顺序排序，`DESC`逆序排序\n\n      **注意**：如果有多个排序字段和方式，则只在满足第一个排序字段和方式产生相等结果时，按顺序按照后面的排序字段和方式排序。\n\n  * <font color=red>聚合函数</font>：将一列数据作为一个整体，进行纵向计算\n\n    * `count`：计算个数（计数器）\n\n    * `max`：计算最大值\n\n    * `min`：计算最小值\n\n    * `sum`：计算和\n\n    * `avg`：计算平均值，对于包含`NULL`的字段，`avg`不会此字段计入平均值计算\n\n      **注意**：聚合函数的计算会排除参数字段值为NULL的那一行，特别是`count()`\n\n      解决方案：\n\n      1. 尽量选择不含有`NULL`的列进行聚合函数的计算，例如**主键**\n      2. 利用`IFNULL(exp1,exp2)`把可能含有`NULL`的字段作为`exp1`替换为`exp2`\n\n  * <font color=red>分组查询</font>：`select * from 表名 group by 分组字段;`\n\n    **注意**：\n\n    1. 分组查询之后的字段只能为：分组字段、聚合函数，不推荐为其他字段，包括`*`\n    2. 对分组查询之后的结果进行条件判断，利用**`having`**子句\n       * `where` 和 `having` 的<font color=green>区别</font>：\n         1. `where` 在分组之前进行限定，如果不满足`where`的条件，则不参与分组\n         2. `having`在分组之后进行限定，如果不满足`having`后聚合函数的条件，则不显示\n         3. `where`后不可以跟聚合函数，`having`可以进行聚合函数的判断\n\n  * <font color=red>分页查询</font>：`select * from 表名 limit 开始的索引,要显示的条数;`\n\n    * 分页计算公式：\t`开始的索引 = (当前页码 - 1) * 每页的条数` \n\n    <img src=\"/images/image-20210728175417542.png\" alt=\"image-20210728175417542\"  />\n\n    * 分页操作是一个**方言**，例如`LIMIT`只适用于`MySQL`数据库\n\n#### DCL(Data Control Language)数据控制语言\n\n用来定义数据库的访问权限和安全级别，及创建用户。DCL主要由DBA（数据库管理员）进行管理。\n\n关键字：`GRANT`、`REVOKE`等\n\n* 管理用户\n\n  * 添加用户：`create user '用户名'@'主机名' identified by '密码';`\n\n  * 删除用户：`drop user '用户名'@'主机名';`\n\n  * 修改用户密码：\n\n    1. 修改表字段内容：`update user set Password = password('新密码') where User = '用户名';`\n    2. DCL简化语法：`set password for '用户名'@'主机名' = password('新密码');`\n\n    **忘记了root密码？**\n\n    1. 管理员运行cmd：`net stop mysql` 停止mysql服务\n    2. 使用无验证方式启动mysql服务：`mysqld --skip-grant-tables` 此时当前cmd会陷入阻塞\n    3. 在另一个cmd直接输入`mysql`回车，登录进去修改user表的root用户密码，然后关掉所有cmd\n    4. 在任务管理器杀掉`mysqld`的进程\n    5. 管理员运行cmd：`net start mysql` 启动mysql服务\n\n  * 查询用户：\n\n    1. 切换到`mysql`数据库：`use mysql;`\n\n    2. 查询`user`表：`select * from user;`\n\n       Host字段通配符：`%`表示可以在任意主机使用用户登录数据库（包含了`localhost`）\n\n* 授权\n\n  * 查询权限：`show grants for '用户名'@'主机名';`\n  * 授予权限：`grant 权限列表 on 数据库名.表名 to '用户名'@'主机名';`\n    * 授予所有权限到所有数据库的所有表：`grant all on *.* to 用户名;`\n  * 撤销权限：`revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名';`\n  \n* 建议刷新：`flush privileges;`\n\n### 约束\n\n* 作用：对表中的数据进行限定，保证数据的正确性、有效性和完整性。\n\n* 约束的分类：\n\n  * <font color=red>主键约束</font>：`primary Key`\n\n    **作用**：非空且唯一，一张表只能有一个字段为主键，主键是表中记录的唯一标识\n\n    * 添加主键：\n\n      1. 创建表时指定主键约束：`id int primary key`\n      2. 修改表字段属性时添加主键约束：`alter table 表名 modify 字段名 数据类型 primary key;`\n\n    * 删除主键约束：`alter table 表名 drop primary key`，因为主键只有一个，所以不需要指定字段\n\n      **注意**：使用`alter table 表名 modify 字段`的方式不能删除掉主键约束\n\n      主键约束使得一列的字段值不能相同且非空，相当于`unique`和`not null`\n\n    * 主键<font color=red>自动增长</font>：\n\n      1. 创建表时对主键指定自动增长：`id int primary key auto_increment`\n\n      2. 修改表字段时添加自动增长：`alter table 表名 modify 字段名 数据类型 auto_increment;`\n\n         **注意**：\n\n         1. 只有当自动增长的字段值赋值为0或null时，会触发自动增长，产生自增数值并插入\n         2. 后续才添加自动增长时，必须保证自动增长字段没有值为1，否则添加自动增长不成功\n         3. :warning:后续才添加自动增长会导致自动增长字段中为0的变成1，除非手动配置`ini`文件\n         4. 在发生外键值不存在时，自动增长值依然会`+1`，但主键冲突不会导致`+1`\n\n      3. 删除自动增长：`alter table 表名 modify 字段 属性值;`，可以省略主键设置，因为主键不会被`modify`移除\n\n  * <font color=red>非空约束</font>：`not null`\n\n    * 添加非空约束：\n      1. 创建表时指定非空约束：直接在字段的数据类型后指定约束：`name varchar(20) not null`\n      2. 修改表字段属性时添加非空约束：`alter table 表名 modify 字段名 数据类型 not null;`\n    * 删除非空约束：`alter table 表名 modify 字段名 数据类型;`此时没有给该字段指定约束\n\n  * <font color=red>唯一约束</font>：`unique`\n\n    * 添加唯一约束：\n\n      1. 创建表时指定唯一约束：直接在字段的数据类型后指定约束：`phone varchar(20) unique`\n\n    * 删除唯一约束：`alter table 表名 drop index 要删除唯一约束的字段名;`\n\n      **注意**：使用`alter table 表名 modify 字段`的方式不能删除掉唯一约束\n\n      ​\t唯一约束使得一列的字段值不能相同，但不包括`null`（`null`可重复）\n\n  * <font color=red>外键约束</font>：`foreign key`\n\n    **作用**：让表与表之间事实的依赖关系由外键链接，从而保证数据的正确性\n\n    * 添加外键约束：\n\n      1. 创建表时指定外键约束：\n\n         ```sql\n         create table 表名(\n         \t...,\n         \t外键字段 外键属性,\n             constraint 随便起个外键名称 foreign key (本表外键字段名称)\n             references 外键连接的表名(外键连接的表的唯一约束列名称)\n         );\n         ```\n\n      2. 修改表字段属性时添加外键约束：\n\n         ```sql\n         alter table 要添加外键的本表名\n         add constraint 随便起个外键名称 foreign key (本表外键字段名称) \n         references 外键链接的表名(外键连接的表的唯一约束列名称);\n         ```\n\n      **注意**：\n\n      1. 对于本表中作为其他表外键的约束字段，如果不考虑外键操作会导致操作失败，例如删除一个作为其他表中外键的本表字段会删除失败，因为其他表外键依赖了本表这个字段\n      2. 对于本表中被其他表外键约束的字段，如果本表的值不存在于这个外键对应的字段的值，则操作失败，只能为外键对应的表中被约束的字段值之一，但是**外键字段值可以为`NULL`**\n\n    * 删除外键：`alter table 表名 drop foreign key 要删除的外键名称;`\n\n    * <font color=green>级联操作</font>：使得修改外键对应的表的字段值，可以自动影响本表外键字段\n\n      1. <font color=orange>级联更新</font>：\n\n         * 在创建表时指定外键的级联更新：\n\n           ```sql\n           create table 表名(\n           \t...,\n           \t外键字段 外键属性,\n               constraint 随便起个外键名称 foreign key (本表外键字段名称)\n               references 外键连接的表名(外键连接的表的唯一约束列名称)\n               on update cascade\n           );\n           ```\n\n         * 修改表字段属性时添加外键的级联更新：\n\n           ```sql\n           alter table 要添加外键的本表名\n           add constraint 随便起个外键名称 foreign key (本表外键字段名称) \n           references 外键链接的表名(外键连接的表的唯一约束列名称)\n           on update cascade;\n           ```\n\n      2. <font color=orange>级联删除</font>：\n\n         * 在创建表时指定外键的级联删除：\n\n           ```sql\n           create table 表名(\n           \t...,\n           \t外键字段 外键属性,\n               constraint 随便起个外键名称 foreign key (本表外键字段名称)\n               references 外键连接的表名(外键连接的表的唯一约束列名称)\n               on delete cascade\n           );\n           ```\n\n         * 修改表字段属性时添加外键的级联删除：\n\n           ```sql\n           alter table 要添加外键的本表名\n           add constraint 随便起个外键名称 foreign key (本表外键字段名称) \n           references 外键链接的表名(外键连接的表的唯一约束列名称)\n           on delete cascade;\n           ```\n\n## 数据库的设计\n\n### 多表之间的关系\n\n* 分类：\n\n  * **一对一**：例如：人和身份证号：一个人只有一个身份证号，一个身份证号只对应一个人\n  * **一对多**：例如：部门和员工：一个部门含有多个员工，一个员工只属于一个部门\n    \n  * **多对多**：例如：学生和课程：一个学生可以选修多个课程，一个课程可以被多个学生选择\n  \n* 实现关系：\n\n  * 一对一：在**任意一方**添加**外键**（这个外键必须是`unique`的），**指向另一方**的主键。多在一张表实现\n  * **一对多**：在**多的一方**建立**外键**，**指向一的一方**的主键\n  * **多对多**：需要**借助第三方中间表**，至少包含两个字段作为**外键**（*联合主键*），**分别指向两张表的主键**\n\n  **案例**：一条路线的分类只有一种，一个用户可以选择多条路线，一个路线也可以被多个用户选择。\n\n  ![image-20210729235017508](/images/image-20210729235017508.png)\n\n### 范式\n\n* 概念：设计数据库时需要遵循的一些规范。遵循越高的范式要求遵循较低的所有范式\n\n  * 来自百度百科：\n\n    ​\t\t设计关系数据库时，遵从不同的规范要求，设计出合理的关系型数据库，这些不同的规范要求被称为不同的范式，各种范式呈递次规范，越高的范式数据库冗余越小。目前关系数据库有六种范式：第一范式（1NF）、第二范式（2NF）、第三范式（3NF）、巴斯-科德范式（BCNF）、第四范式(4NF）和第五范式（5NF，又称完美范式）。\n\n* 分类：\n\n  * <font color=red>第一范式（1NF）</font>：每一列都是不可分割的原子数据项\n\n    <img src=\"/images/image-20210730011505571.png\" alt=\"image-20210730011505571\" style=\"zoom:67%;\" />\n\n    可能存在的问题：\n\n    1. 存在非常严重的数据冗余（重复）\n    2. 添加数据存在问题\n    3. 删除数据存在问题\n\n  * <font color=red>第二范式（2NF）</font>：在1NF的基础上，非码属性必须完全依赖于候选码（在1NF基础上**消除**非主属性对主码的**部分函数依赖**，可以解决数据冗余问题）\n\n    <img src=\"/images/image-20210730011619411.png\" alt=\"image-20210730011619411\" style=\"zoom:80%;\" />\n\n    概念：\n\n    * <font color=orange>函数依赖</font>：A-->B，通过A属性（属性组）的属性值可以确定唯一B属性的值，则B依赖于A\n\n      1. A**属性**的值确定B属性的值：例如学号A可以确定唯一的学生姓名B，则学生姓名B依赖于学号A\n      2. A**属性组**的值确定B属性值：例如学号和课程为一组A，可以确定成绩B，则成绩B依赖于属性组A\n\n    * <font color=orange>完全函数依赖</font>：A-->B，如果A是一个属性组，则B属性值的确定需要依赖于A属性组中**所有**属性值\n\n    * <font color=orange>部分函数依赖</font>：A-->B，如果A是一个属性组，则B属性值的确定只需要依赖A属性组中**某些**属性值\n\n    * <font color=orange>传递函数依赖</font>：A-->B，B-->C，如果通过A属性（属性组）的值可以确定唯一B属性的值，再通过B属性（属性组）的值可以确定唯一C属性的值，则称C传递函数依赖于A。例如学号可以确定系名，系名又可以确定系主任，则系主任是传递依赖于学号的\n\n    * <font color=orange>码</font>：在一张表中，如果一个属性或属性组被其他所有属性完全依赖，则这个属性或属性组为该表的码\n\n      <img src=\"/images/image-20210730002712176.png\" alt=\"image-20210730002712176\" style=\"zoom:80%;\" />\n\n      该表中，学号并不能确定唯一的分数，只有**（学号+课程）**可以确定**唯一的分数**，**此时的码为属性组**\n\n      * 主属性：码属性组中的所有属性\n      * 非主属性：除去码属性组的属性\n\n  * <font color=red>第三范式（3NF）</font>：在2NF的基础上，任何非码属性不依赖于其他非主属性（在2NF基础上**消除传递依赖**）\n\n    <img src=\"/images/image-20210730011726163.png\" alt=\"image-20210730011726163\" style=\"zoom:67%;\" />\n\n### 数据库的备份与还原\n\n* 命令行：\n  * 备份：`mysqldump -u用户名 -p密码 要备份的数据库名称 > 要保存到的路径`\n  * 还原：\n    1. 登录数据库`mysql -u用户名 -p密码`\n    2. 创建数据库`create database 数据库名称`\n    3. 使用数据库`use 数据库名称`\n    4. 执行文件：`source 保存的文件路径`\n* 图形化工具：\n  * `DataGrip`：\n    1. 数据库列表右键需要备份的数据库\n    2. 选择：![image-20210730142551600](/images/image-20210730142551600.png)\n    3. 配置`mysqldump`的路径：![image-20210730142611142](/images/image-20210730142611142.png)\n    4. 配置输出位置：![image-20210730142644773](/images/image-20210730142644773.png)\n\n## 多表查询\n\n* 基本语法：`select 字段列表 from 表名列表 where 条件...;`，直接查询的结果为笛卡尔积\n  * 笛卡尔积：全称*笛卡尔乘积*，两个集合X和Y的笛卡尔积（Cartesian product），又称直积，表示为X × Y，第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员。也就是两个集合的所有组成情况。要完成多表查询需要消除无用数据。\n\n### 多表查询的分类：\n\n#### 内连接查询：\n\n> 内连接只查询出符合条件的数据\n\n基本思路：\n\n1. 从哪些表中查询数据\n2. 条件是什么\n3. 查询哪些字段\n\n* **隐式内连接**：`select 字段列表 from 表名1,表名2 where 条件;`使用where条件消除无用数据\n\n  例如：1. 查询所有员工信息和对应的部门名称：\n\n  ​\t\t`select * from emp,dept where emp.dept_id = dept.id; -- 隐式内连接`\n\n  ![image-20210730150424971](/images/image-20210730150424971.png)\n\n  ​\t\t\t2. 查询员工表所有的员工名称、性别和对应部门表中的部门名称：\n\n  ![image-20210730150811208](/images/image-20210730150811208.png)<img src=\"/images/image-20210730150824252.png\" alt=\"image-20210730150824252\" style=\"zoom: 80%;\" />\n\n* **显式内连接**：`select 字段列表 from 表名1 inner join 表名2 on 条件;`其中`inner`可省略\n\n  例如：查询所有员工信息和对应的部门名称：\n\n  ​\t\t`select * from emp inner join dept on emp.dept_id = dept.id`\n\n#### 外连接查询：\n\n> 外连接可以查询某张表的数据，包括不满足条件的\n\n* 左外连接：`select 字段列表 from 表1 left outer join 表2 on 条件;`其中`outer`可省略\n  * 查询到的结果是**所有的左表（表1）记录**和交集内容\n* 右外连接：`select 字段列表 from 表1 right outer join 表2 on 条件;`其中`outer`可省略\n  * 查询到的结果是**所有的右表（表2）记录**和交集内容\n\n#### 子查询：\n\n* 概念：查询中嵌套查询，称嵌套查询为子查询\n* 子查询不同情况：\n  * 子查询的结果是**单行单列**：子查询可以作为条件，使用运算符去判断\n    * 运算符：`>`、`<`、`>=`、`<=`、`=`\n  * 子查询的结果是**多行单列**：子查询可以作为条件，使用运算符`in`\n  * 子查询的结果是**多行多列**：子查询可以作为一张虚拟表进行表的查询\n\n## 事务\n\n### 事务的基本介绍\n\n* 概念：如果一个包含多个步骤的业务操作被事务管理，那么这些操作**要么同时成功，要么同时失败**\n* 操作：\n  * 开启事务：`start transaction;`\n    * 开启事务后的DML语句不会立即更新结果到数据库上，除非`commit`\n  * 回滚：`rollback;`\n  * 提交：`commit;`\n* MySQL数据库中事务默认自动提交（Oracle数据库默认手动提交事务）\n  * 一条DML语句会自动提交一次事务\n  * 事务提交的两种方式：\n    * 自动提交：DML语句\n    * 手动提交：需要手动先开启事务，然后手动提交事务\n  * 查看事务的默认提交方式：`select @@autocommit;`结果为0表示手动提交，为1表示自动提交\n  * 修改事务的默认提交方式：`set @@autocommit = 0;`修改为手动提交\n\n### 事务的四大特征ACID\n\n1. 原子性(Atomicity)：是不可分割的最小操作单位，要么同时成功，要么同时失败\n2. 一致性(Consistency)：事务操作前后数据总量不变\n3. 隔离性(Isolation)：多个事务之间相互独立\n4. 持久性(Durability)：当事务提交或回滚后，数据库会持久化的保存数据\n\n### 事务的隔离级别\n\n* 概念：多个事务之间是隔离的、相互独立的。但是如果多个事务操作同一批数据，就会引发一些问题，通过设置不同的隔离级别就可以解决这些问题\n\n* 存在的问题：\n\n  1. 脏读：一个事务读取到另一个事务中没有提交的数据\n  2. 不可重复读（虚读）：在同一个事务中，两次读取到的数据不一样\n  3. 幻读：一个事务操作（DML）数据表中所有记录，另一个事务添加了一条数据，则第一个事务查询不到自己的修改，好像产生了幻觉一样\n\n* 隔离级别：\n\n  1. `read uncommitted`：读未提交\n\n     产生的问题：脏读、不可重复读、幻读\n\n  2. `read committed`：读已提交（Oracle默认）\n\n     产生的问题：不可重复读、幻读\n\n  3. `repeatable read`：可重复读（MySQL默认）\n\n     产生的问题：幻读\n\n  4. `serializable`：串行化\n\n     可以解决所有的问题\n\n     **注意：从小到大安全性越来越高，但是效率越来越低**\n\n* 查询数据库的隔离级别：`select @@tx_isolation;`\n\n* 设置数据库的隔离级别：`set global transaction isolation level 级别字符串;`\n\n  * 级别字符串：上述4种隔离级别的枚举。\n\n## JDBC\n\n### 概念\n\n**JDBC是 Java DataBase Connectivity （ Java数据库连接）的简写，即利用Java操作数据库**\n\n![image-20210802002118624](/images/image-20210802002118624.png)\n\n* JDBC的本质：官方(SUN公司)定义的一套操作所有关系型数据库的规则，即接口。各个数据库厂商去实现了这些接口，提供数据库驱动jar包，我们可以使用这套接口（JDBC）编程，真正执行的是驱动jar包中的实现类\n\n### 快速入门\n\n* 步骤：\n\n  1. 导入驱动jar包\n\n     * 把驱动jar包复制到项目里\n     * 添加为库![image-20210802012636684](/images/image-20210802012636684.png)\n\n  2. 注册驱动：`Class.forName(\"com.mysql.jdbc.Driver\");`\n\n  3. 获取数据库连接对象`Connection getConnection(String url,String user,String password)`\n\n     ```java\n     Connection conn = DriverManager.getConnection(\"jdbc:mysql://localhost:3306/db3\", \"root\", \"199988\"); //指定URL，用户名，密码\n     ```\n\n  4. 定义sql：`String sql = \"update account set money = 200 where id = 2\";`\n\n     * IDEA连接数据库可能需要指定时区：![image-20210802012048708](/images/image-20210802012048708.png)\n\n       在URL后面加上`?serverTimezone=GMT%2B8`\n\n  5. 获取执行sql语句的`Statement`对象：`Statement createStatement()`\n\n     ```Java\n     Statement statement = conn.createStatement();\n     ```\n\n  6. 执行sql，接收返回的结果\n\n     ```java\n     int count = statement.executeUpdate(sql); //返回执行的语句的行数\n     ```\n\n  7. 处理结果\n\n  8. 释放资源：关闭`Statement`对象和`Connection`对象\n\n#### 详解各个对象\n\n##### DriverManager驱动管理对象\n\n* 功能：\n\n  1. 注册驱动：告诉程序该使用哪一个驱动jar包\n\n     ```java\n     static synchronized void registerDriver(java.sql.Driver driver)\n     ```\n\n     注册给定的驱动程序，写代码使用：`Class.forName(\"com.mysql.jdbc.Driver\");`\n    \n     实际上`com.mysql.jdbc.Driver`里含有一个静态代码块：\n    \n     ```java\n     static {\n             try {\n                 DriverManager.registerDriver(new Driver());\n             } catch (SQLException var1) {\n                 throw new RuntimeException(\"Can't register driver!\");\n             }\n         }\n     ```\n    \n     在MySQL5之后的驱动jar包的`META-INF/services`含有一个`java.sql.Driver`文件，会自动注册驱动\n\n  2. 获取数据库连接：\n\n     ```java\n     public static Connection getConnection(String url,String user, String password)\n     ```\n\n     * 参数：\n\n       * `url`：指定连接的路径（jdbc:mysql://ip:端口/数据库名称）\n\n         例如：`jdbc:mysql://ip地址(域名):端口号/数据库名称`\n\n         例如：`jdbc:mysql://localhost:3306/db3`\n\n         如果连接的是本机的mysql，并且默认端口是3306，那么可以不写ip:端口\n\n         例如`jdbc:mysql:///db3`\n\n       * `user`：用户名\n\n       * `password`：密码\n\n##### Connection数据库连接对象\n\n* 功能：\n  1. 获取执行sql的对象\n     * `Statement createStatement()`：获取一个可以执行SQL的Statement对象\n     * `PreparedStatement prepareStatement(String sql)`\n  2. 管理事务\n     * 开启事务：`void setAutoCommit(boolean autoCommit)`设置为`false`则开启事务\n     * 提交事务：`void commit()`\n     * 回滚事务：`void rollback()`\n\n##### Statement执行SQL的对象\n\n* 功能：用于执行**静态** SQL 语句并返回它产生的结果的对象。\n\n  1. `boolean execute(String sql)`执行给定的 SQL 语句，该语句可能返回多个结果。 在某些（不常见）情况下，单个 SQL 语句可能返回多个结果集和/或更新计数。\n\n     返回值：如果第一个结果是ResultSet对象，则为true ； 如果是更新计数或没有结果，则为false\n\n  2. `int executeUpdate(String sql)`执行给定的 SQL 语句，它可以是INSERT 、 UPDATE或DELETE语句或不返回任何内容的DDL 语句。\n\n     返回值：(1) SQL 数据操作语言 (DML) 语句受影响的行数   (2) 对于不返回任何内容的 SQL 语句返回0\n\n  3. `ResultSet executeQuery(String sql)`执行给定的 SQL 语句，该语句返回单个ResultSet对象\n\n     返回值：一个ResultSet对象，包含给定查询生成的数据； 从不为null\n\n##### ResultSET结果集对象\n\n* 功能：封装了查询的结果\n\n  1. `boolean next()`将光标从当前位置向前移动一行。 ResultSet游标最初位于第一行之前； 第一次调用next方法使第一行成为当前行； 第二次调用使第二行成为当前行，依此类推。\n\n     返回值：如果新的当前行有效，则为true ； 如果没有更多行，则为false\n\n  2. `xxx getXxx(参数)`获取数据，其中Xxx是数据类型，例如`getInt()`、`getString()`\n\n     参数：\n\n     * `int columnIndex`：根据列的编号获取，从1开始\n     * `String columnLabel`：根据列名称获取，是AS子句指定的列名。 如果未指定 ，则标签是列的名称\n\n* 使用步骤：\n\n  1. 游标向下移动一行\n  2. 判断是否有数据\n  3. 获取数据\n\n* 抽取JDBC工具类\n\n  * 目的：简化书写\n  * 分析：\n    1. 注册驱动\n    2. 抽取一个方法获取连接对象：不传递参数，还要保证工具类的通用性，通过配置文件解决\n    3. 抽取一个方法释放资源\n\n##### PreparedStatement\n\n* 功能：表示预编译 SQL 语句的对象。SQL 语句被预编译并存储在PreparedStatement对象中。 然后可以使用此对象多次有效地执行此语句。\n\n* **解决SQL注入问题**：在拼接SQL语句时，有一些SQL特殊关键词参与字符串的拼接会造成**安全性问题**\n\n  对于：<img src=\"/images/image-20210802170207621.png\" alt=\"image-20210802170207621\" style=\"zoom: 67%;\" />\n\n  如果`password`值为![image-20210802170250282](/images/image-20210802170250282.png)\n\n  则整条SQL语句为：`select * from user where username = 'ur' AND password = 'a' or 'a' = 'a'`\n\n  此时最后一个表达式为 `false or true`，结果为`true`，造成了SQL注入\n\n* 使用步骤：\n\n  1. 导入驱动jar包\n\n  2. 注册驱动\n\n  3. 获取数据库连接对象Connection\n\n  4. **定义SQL**\n\n     * 此时SQL语句的参数使用`?`作为占位符\n\n       例如：`select * from user where username = ? AND password = ?;`\n\n  5. 获取执行SQL的对象`PreparedStatement prepareStatement(String sql)`\n\n  6. 给`?`赋值\n\n     * 方法：`void setXxx(int parameterIndex,Xxx x)`\n\n       参数：\n\n       1. `parameterIndex`：第几个`?`问好\n       2. `x`：Xxx类型的值\n\n  7. 执行SQL，不需要再次传递`SQL`语句，直接调用`executeQuery()`\n\n  8. 处理结果\n\n  9. 释放资源\n\n* 注意：建议都使用`PreparedStatement`来完成CRUD操作，好处：\n\n  * 可以防止SQL注入\n  * 效率更高\n\n### JDBC控制事务\n\n* 事务：一个包含多个步骤的业务操作，如果这个业务操作被事务管理，那么这些步骤要么同时成功，要么同时失败。\n* 操作：\n  * 开启事务：`void setAutoCommit(boolean autoCommit)`设置为`false`则开启事务\n  * 提交事务：`void commit()`\n  * 回滚事务：`void rollback()`\n* 使用Connection对象管理事务\n  1. 在执行SQL之前开启事务\n  2. 在所有SQL执行完毕后提交事务\n  3. 如果产生异常，就在异常处理程序中回滚事务\n\n### 数据库连接池\n\n* 概念：一个存放数据库连接的容器（集合），当系统初始化好之后，容器被创建，容器中会申请一些连接对象，当用户申请访问数据库时，从容器中获取连接对象，当用户访问完之后，把连接归还给连接池。\n\n* 好处：\n\n  1. 节约资源\n  2. 访问高效\n\n* 实现：\n\n  1. 标准接口：![image-20210802183748055](/images/image-20210802183748055.png)![image-20210802184006077](/images/image-20210802184006077.png)\n\n     * 方法：\n       1. 获取连接：`Connection getConnection()`\n       2. 归还连接：如果连接对象Connection是从连接池获取的，那么调用Connection.close()方法，不会再关闭连接，而是归还连接\n\n  2. DataSource接口由驱动程序供应商实现，一般有：\n\n#### `C3P0`：数据库连接池技术\n\n* 步骤：\n\n  1. 导入jar包：`c3p0-0.9.5.2.jar`和`mchange-commons-java-0.2.12.jar`。前提是要导入`mysql`驱动包\n\n  2. 定义配置文件：\n\n     * 名称：`c3p0.properties`或`c3p0-config.xml`\n\n       在xml配置文件中可以定义多套配置：\n\n       1. 默认配置在`<default-config>`中\n       2. 其他自定义配置在`<named-config name=\"配置名称\">`中\n\n     * 路径：直接将文件放在`src`目录下即可\n\n  3. 创建核心对象：数据库连接池对象`ComboPooledDataSource`\n\n     无参构造会使用配置文件的`<default-config>`：<img src=\"/images/image-20210802195312357.png\" alt=\"image-20210802195312357\" style=\"zoom:80%;\" /> \n\n     需要使用指定`<named-config name=\"配置名称\">`利用重载的`ComboPooledDataSource(String conf)`\n\n   4. 获取连接对象：`Connection getConnection()`\n\n#### `Druid`：数据库连接池实现技术，由阿里巴巴提供\n\n* 步骤：\n\n  1. 导入jar包：`druid-1.1.21.jar`\n\n  2. 定义配置文件：\n\n     * properties形式的\n     * 可以叫任意名称，可以放在任意目录下\n\n  3. 加载配置文件：利用`Properties`对象的`load()`\n\n  4. 获取数据库连接池对象：通过工厂类：`DruidDataSourceFactory`来获取\n\n     方法：`DataSource createDataSource(Properties properties)`传入`Properties`对象\n\n  5. 获取连接对象：`Connection getConnection()`\n\n* 定义工具类：\n\n  * 定义一个`DruidUtils`\n  * 提供静态代码块加载配置文件并且初始化连接池对象\n  * 提供方法\n    1. 获取连接的方法：提高数据库连接池获取连接\n    2. 释放资源\n    3. 获取连接池的方法\n\n### Spring JDBC：JDBC Template\n\n* Spring框架对JDBC的简单封装。提供了一个JDBCTemplate对象来简化JDBC的开发\n\n#### 基本步骤\n\n1. 导入jar包\n\n2. 创建`JdbcTemplate`对象：`public JdbcTemplate(DataSource ds)`该构造方法需要一个数据源对象\n   ```java\n   JdbcTemplate template = new JdbcTemplate(ds);\n   ```\n\n3. 调用JdbcTemplate对象的方法来完成CRUD的操作\n\n   * `update()`：执行DML语句：增、删、改\n\n   * `queryForMap()`：查询结果，将结果集封装为Map集合，返回`Map<String,Object>`，只能查询一行\n\n   * `queryForList()`：查询结果，将结果集封装为List集合，返回`List<Map<String, Object>>`\n\n   * `query()`：查询结果，将结果集封装为JavaBean对象\n\n     注意：`query()`方法的参数比较特殊，\n\n     1. 一般来说第一个参数为`String sql`的SQL语句\n\n     2. 第二个参数为`RowMapper<T> rowMapper`一个`RowMapper<T>`接口，我们可以：\n\n        * 传递一个接口的匿名类，直接：\n\n          ![image-20210802231214849](/images/image-20210802231214849.png) \n\n        * 使用lambda表达式简化上面的代码：\n\n           ![image-20210802231301364](/images/image-20210802231301364.png)\n\n        * 传递一个`RowMapper<T>`接口的实现类：\n\n          ![image-20210802231719762](/images/image-20210802231719762.png)\n\n          例如：`new BeanPropertyRowMapper<T>(Class<T> mappedClass)`\n\n          构造方法参数为查询的`JavaBean`的`Class`对象，即`T.class`\n\n   * `queryForObject()`：查询结果，将结果封装为对象，一般用于聚合函数的查询\n\n     方法：`T queryForObject(String sql,Class<T> requiredType)`\n\n     参数：\n\n     1. `String sql`：SQL语句，一般是聚合函数查询\n     2. `Class<T> requiredType`：查询出来的类型的`Class`对象，一般为`Long.class`\n\n#### 练习：\n\n* 需求：操作db3数据库的emp表\n  1. 修改1号记录的salary为8888\n  2. 添加一条记录\n  3. 删除刚才添加的记录\n  4. 查询id为1的记录，将其封装为Map集合\n  5. 查询所有的记录，将其封装为List集合\n  6. 查询所有的记录，将其封装为Emp对象的List集合\n  7. 查询总的记录数\n\n## Redis\n\n### 概念\n\n* Redis是一款高性能的**NOSQL**系列的**非关系型**数据库\n\n  * 它是用C语言开发的一个高性能键值对（`key-value`）数据库，目前Redis支持的键值数据类型如下：\n    * 字符串类型：string\n    * 哈西类型：hash\n    * 列表类型：list\n    * 集合类型：set\n    * 有序集合类型：sortedset\n  * Redis的应用场景：\n    * 缓存（数据查询，短连接，新闻内容，商品内容等）\n    * 聊天室的在线好友列表\n    * 任务队列（秒杀，抢购，12306等）\n    * 应用排行榜\n    * 网站访问统计\n    * 数据过期处理\n    * 分布式集群架构中的session分离\n\n  \n\n![image-20210810125041138](/images/image-20210810125041138.png)\n\n* **NOSQL**：\n\n  NOSQL是Not Only SQL（不仅仅是SQL）的缩写，是一项全新的数据库理念，泛指非关系型数据库。NOSQL数据库的产生是为了解决大规模数据集合多重数据种类带来的挑战，尤其是大数据应用难题。\n\n  * **NOSQL和关系型数据库的比较：**\n    * 优点：\n      1. 成本：NOSQL数据库简单易部署，基本都是开源软件，不需要像使用Oracle那样花费大量成本购买使用，相比关系型数据库价格便宜。\n      2. 查询速度：NOSQL数据库将数据存储在缓存之中，关系型数据库将数据存储在硬盘之中，NOSQL查询速度远高于关系型数据库。\n      3. 存储数据的格式：NOSQL的存储格式是`key,value`的形式、文档形式、图片形式等，所以可以存储基本数据类型以及对象或者集合等多种格式，而关系型数据库只支持基础类型。\n      4. 扩展性：关系型数据库有类似于join这样的多表查询机制的限制导致扩展很艰难。\n    * 缺点：\n      1. 维护的工具和资料有限，因为NOSQL是属于新的技术，不如关系型数据库的技术成熟\n      2. 不提供对SQL的支持，没有SQL这样的工业标准，将会导致一定的学习和使用成本\n      3. 不提供关系型数据库对事务的处理\n  * **NOSQL数据库的优势：**\n    * 性能NOSQL是基于键值对的，可以想象成表中的主键和值的对应关系，而且不需要经过SQL层的解析，所以性能非常高\n    * 可扩展性同样也是基于键值对，数据之间没有耦合性，所以非常容易水平扩展\n  * **关系型数据库的优势：**\n    * 复杂查询可以用SQL语句方便的在一个表和多个表之间做非常复杂的数据查询\n    * 事务支持使得对于安全性能很高的数据访问要求得以实现。\n  * **主流的NOSQL产品：**\n    1. 键值对（`key,value`）存储数据库：\n       * 相关产品：Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB\n       * 典型应用：内容缓存，主要用于处理大量数据的高负载访问\n       * 数据模型：一系列键值对\n       * 优势：快速查询\n       * 劣势：存储的数据缺少结构化\n    2. 列存储数据库\n       * 相关产品：Cassandra、HBase、Riak\n       * 典型应用：分布式的文件系统\n       * 数据模型：以列簇式的存储，将同一列数据存储在一起\n       * 优势：查找速度快，可扩展性强，更容易进行分布式扩展\n       * 劣势：功能相对局限\n    3. 文档型数据库\n       * 相关产品：CouchDB、MongoDB\n       * 典型应用：Web应用（与`key,value`类似，value是结构化的）\n       * 数据模型：一系列键值对\n       * 优势：数据结构要求不严格\n       * 劣势：查询性能不高，而且缺乏统一的查询语法\n    4. 图形（Graph）数据库\n       * 相关产品：Neo4J、InfoGrid、Infinite Graph\n       * 典型应用：社交网络\n       * 数据模型：图结构\n       * 优势：利用图结构相关算法\n       * 劣势：需要对整个图做计算才能得出结果，不容易做分布式的集群方案\n\n### 下载安装\n\n1. 官网：`https://redis.io`\n2. 中文网：`https://www.redis.net.cn`\n3. 下载解压之后可以直接使用：\n   * `redis.windows.conf`：配置文件\n   * `redis-cli.exe`：客户端，显示中文乱码时指定`--raw`方式打开：`redis-cli.exe --raw`\n   * `redis-server.exe`：服务器端\n\n### Redis的数据结构\n\n* Redis存储的是：`key,value`格式的数据，其中key都是字符串，**而value有5种**：\n  * 字符串类型：string，最基本的类型，是二进制安全的，string可以存储任意类型数据，每个最大512M\n  * 哈西类型：hash，类似于Map，一个键值对集合，每个hash可以存储2<sup>32</sup>-1个键值对，大约42亿\n  * 列表类型：list，类似于linkedlist，允许重复的字符串列表，每个list最多可包含2<sup>32</sup>-1个元素，大约42亿\n  * 集合类型：set，类似于HashSet，不允许重复的无序字符串集合，通过hash表实现，最大成员数2<sup>32</sup>-1个\n  * 有序集合类型：sortedset，不允许重复的有序字符串集合，通过hash表实现，最大成员数2<sup>32</sup>-1个\n\n![image-20210810140033474](/images/image-20210810140033474.png)\n\n### 命令操作\n\n#### 操作字符串string类型\n\n1. 存储：`set key value`，存储一个`value`到`key`里\n2. 获取：`get key`，通过此`key`获取对应的`value`\n3. 删除：`del key`，删除此`key`对应的键值对\n\n#### 操作哈西hash类型\n\n1. 存储：`hset key field value`，存储一个`field,value`的Map到对应的`key`里\n\n2. 获取：`hget key field`，通过`key`获取对应的`field`的`value`\n\n   * 获取`key`中所有的键和值：`hgetall key`\n\n3. 删除：`hdel key field`，删除此`key`对应的`field`的键值对\n\n   * 删除此hash容器：`del key`，删除此`key`对应的hash容器\n\n   **小知识**：`HSET`只能设置单个键值对，同时设置多个请使用`HMSET`，但是`Redis4.0.0`开始`HMSET`已弃用\n\n#### 操作列表list类型\n\n* 添加：`lpush key value`，添加一个`key`对应的`value`到列表左边\n  * `rpush key value`，添加一个`key`对应的`value`到列表右边\n* 获取：`lrange key start end`，获取索引下标`start`到`end`的在`key`对应的元素，从左到右\n  * `start`表示起始位置下标\n  * `end`表示结束位置下标\n  * 下标可以为正数（从左到右以0开始的下标），也可以为负数（从右到左以-1表示最后一位）\n  * `end`下标必须在下标逻辑上大于`start`下标，否则会导致返回`empty list or set`\n* 删除：`lpop key`，删除`key`对应的列表最左边的一个元素并返回被删除的元素\n  * `rpop key`，删除`key`对应的列表最右边的一个元素并返回被删除的元素\n\n#### 操作集合set类型\n\n* 存储：`sadd key value`，添加一个`value`到`key`对应的set集合中\n* 获取：`smembers key`，获取set集合中所有的元素\n* 删除：`srem key value`，删除`key`对应的set集合中的`value`值\n\n#### 操作有序集合zset类型\n\n* 存储：`zadd key score value`，添加一个`value`以及其排序依据`score`到`key`对应的集合中\n* 获取：`zrange key start end`，获取索引下标为`start`到`end`的`key`对应的zset集合中的元素\n  * 可选的`withscores`：在获取元素的同时展示这些元素的`score`\n* 删除：`zrem key value`，删除`key`对应的zset集合中的`value`元素\n\n#### 通用命令：\n\n* 获取所有的键：`keys *`\n  * `*`可以为正则表达式\n* 获取此`key`对应的`value`的类型：`type key`\n* 删除此`key`对应的`key-value`键值对：`del key`\n* :warning:清空Redis服务器数据：`flushall`\n* :warning:清空当前库的所有键值对：`flushdb`\n\n**一个坑**：\n\n* 描述：windows控制台默认GBK编码，通过Jedis写入的数据如果是UTF-8可能导致控制台查询乱码\n* 解决方法：除了`redis-cli.exe --raw`以中文显示汉字之外，还需要先**设置控制台编码**：`chcp 65001`\n\n### 持久化操作\n\n* Redis是一个内存数据库，当Redis服务器重启后，或者电脑重启后，内存数据会丢失，所以需要将Redis的数据持久化保存到硬盘的文件中\n\n* Redis持久化机制：\n\n  * RDB：默认方式，不需要进行配置，在一定的间隔时间中，检测key的变化，然后持久化保存数据\n    * 设置间隔时间和`key`的变化检测：\n      1. 编辑配置文件：`redis.windows.conf`，设置`save 秒数 变化数`\n      2. 重新启动redis服务器，并且指定配置文件名称：`redis-server.exe redis.windows.conf`\n\n  * AOF：日志记录的方式，可以记录每一条命令的操作，在每一次命令操作后持久化数据\n    * 指定是否在每次操作后进行日志记录：`appendonly`\n      1. 编辑配置文件：`redis.windows.conf`，设置`appendonly`为`yes`开启日志记录，默认为`no`\n    * 指定更新日志的条件：`appendfsync`\n      1. 编辑配置文件：`redis.windows.conf`，设置`appendfsync`\n         * `no`：等操作系统进行数据缓存同步到磁盘（快）\n         * `always`：每次更新操作后手动调用`fsync()`将数据写到磁盘（慢，安全）\n         * `everysec`：每秒同步一次（折中，**默认值**）\n\n## Jedis\n\n### 概念：\n\n* 一款Java操作Redis数据库的工具\n\n### 使用步骤：\n\n1. 下载Jedis的jar包：`jedis-2.7.0.jar`和Apache的`commons-pool2-2.3.jar`\n\n2. 使用\n\n   * 创建一个`Jedis`对象：`Jedis jedis = new Jedis()`\n\n     对于此构造方法，重载形式的`Jedis(String host,int port)`参数传递主机名和端口号，本机可省略\n\n   * 调用相应的方法，与命令行操作的方法名一致\n\n   * 使用完成后释放资源：`jedis.close()`\n\n### Jedis连接池\n\n* 使用：\n  1. 创建`JedisPool`连接池对象\n  2. 调用方法`getResource()`获取一个`Jedis`连接\n  3. 调用`close()`方法归还连接到连接池\n\n### 案例\n\n* 使用Redis优化此案例：\n\n  * 提供一个index页面，页面中有一个省份的下拉列表\n\n  * 当页面加载完成，自动发送AJAX请求到服务器查询数据库中所有的省份列表\n\n    **注意：如果查询的数据经常变化，要记得更新缓存**\n\n    * **例如在增删改数据库时删除掉Redis缓存，这样下次再次查询时就会发现没有此`key`对应的缓存数据，这时查询数据更新Redis缓存，可以保证缓存是更新之后的数据**\n\n优化前：![image-20210810202656627](/images/image-20210810202656627.png)\n\n优化后：![image-20210810202711265](/images/image-20210810202711265.png)","tags":["数据库","MySQL","SQL"],"categories":["数据库"]},{"title":"Junit、反射、注解","url":"/2021/08/10/Junit反射注解/","content":"\n# Junit单元测试\n\n## 测试分类：\n\n* 黑盒测试：不需要写代码，测试程序对输入的值时候能输出期望的值\n\n* 白盒测试：需要写代码，关注程序的具体流程（Junit属于这一种）\n\n## Junit使用\n\n### 步骤：\n\n1. 定义一个测试类（测试用例）\n\n   **建议**\n\n   * 测试类名为：被测试的类+Test，例如CalculatorTest\n   * 包名：xxx.xxx.xx.test，例如cn.taoyyz.test\n\n2. 定义测试方法（可以独立运行）\n\n   **建议**\n\n   * 方法名：test+测试的方法名，例如testAdd()\n   * 返回值：void\n   * 参数列表：空参\n   \n3. 给方法加注解：@Test，需要解决报错把依赖添加到路径\n\n4. 判定结果：一般使用*断言* Assert类来处理结果\n\n   ![image-20210719114838150](/images/image-20210719114838150.png)  绿色代表成功，否则失败\n\n   <img src=\"/images/image-20210719115133805.png\" alt=\"image-20210719115133805\" style=\"zoom:80%;\" /> \n\n5. 可以通过多个@Test注解进行多个测试\n\n**补充**\n* @Before：修饰的方法会在@Test测试的方法之前执行\n* @After：修饰的方法会在@Test测试的方法之后执行\n\n# 反射\n\n**反射是框架设计的灵魂**\n\n* 框架：半成品软件。可以在框架的基础上进行软件开发，简化编码。\n\n## 概念\n将类的各个组成部分封装为其他对象，这就是反射机制。\n\n![image-20210719134708584](/images/image-20210719134708584.png)\n\n* 好处：\n  1. 可以在程序运行过程中，操作这些对象。\n  2. 可以解耦，提高程序的可扩展性。\n  \n## 获取Class对象的方式：\n  1.`Class.forName(\"全类名\")`：将字节码文件加载进内存，返回Class对象\n    多用于配置文件,将类名定义在配置文件中.读取文件,加载类\n  2.`类名.class`：通过类名的属性class获取\n    多用于参数的传递\n  3.`对象.getClass()`：继承于Object类\n    多用于对象的获取字节码的方式\n\n  **同一个.class字节码文件在一次程序运行过程中只被加载一次，不论通过哪种方式获取的对象都是同一个**\n\n## Class对象的功能\n### 获取功能:\n#### 1.获取成员变量们\n* Field getField(String name) 通过指定的成员变量名获取public字段\n\n* Field[] getFields() 返回此Class对象表示的类或接口的所有可访问public字段\n\n* Field[] getDeclaredFields() 返回由此Class对象表示的类或接口声明的所有字段的Filed数组\n\n* Field getDeclaredField( String name) 返回此Class对象表示的类或接口的指定声明字段。\n\n  **操作Field：成员变量**\n\n  1.设置值\n\n  `void set(Object obj,Object value)`\n\n  2.获取值\n\n  `Object get(Object obj)`\n\n  3.忽略访问权限修饰符的安全检查(暴力反射)\n\n  `setAccessible(boolean flag)`\n#### 2.获取构造方法们\n* Constructor<T> getConstructor( Class<?>... parameterTypes)返回此Class对象表示的类的指定公共构造方法\n\n* Constructor<?>[] getConstructors() 返回此Class对象表示的类的所有公共构造函数\n\n* Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回此Class对象表示的类或接口的指定构造函数。\n\n* Constructor<?>[] getDeclaredConstructors() 返回此Class对象表示的类声明的所有构造函数。\n\n  **操作Constructor：构造器**\n\n  1.利用构造器创建对象\n\n  `T newInstance(Object... initargs)`\n\n  2.利用空参构造创建对象时，无需获取Class对象的构造器再调用newInstance方法，可以直接利用Class对象自带的newInstance方法。\n#### 3.获取成员方法们\n\n* Method getMethod(String name,Class<?>... parameterTypes) 此Class对象表示的类或接口的指定公共成员方法。\n\n* Method[] getMethods() 此Class对象表示的类或接口的所有公共方法，包括由类或接口声明的方法以及从超类和超接口继承的方法。\n\n* Method getDeclaredMethod(String name,Class<?>... parameterTypes) 返回此Class对象表示的类或接口的指定声明方法。\n\n* Method[] getDeclaredMethods() 返回此Class对象表示的类或接口的所有声明方法，包括公共、受保护、默认（包）访问和私有方法，但不包括继承的方法。\n\n  **操作Method：成员方法**\n\n  1.执行方法\n\n  `Object invoke(Object obj,Object... args)`如果要执行的方法没有参数列表，则第二个参数可以省略\n\n  2.获取方法名称\n\n  `String getName()`返回方法的名称\n\n#### 4.获取类名\n\n* String getName() 返回该实体（类，接口，数组类，基本类型，或void）的全类名（包括包路径名）\n\n* String getSimpleName() 返回简单类名（不包括包路径）\n\n##　案例需求\n**写一个\"框架\"，不能改变任何代码的情况下，可以创建任意对象，执行任意的方法**\n\n### 实现：\n1.配置文件\n2.反射\n\n### 步骤：\n1.将需要创建的对象的全类名和需要执行的方法定义在配置文件中\n2.在程序中加载读取配置文件\n3.使用反射技术来加载类文件进内存\n4.创建对象\n5.执行方法\n\n\n\n# 注解\n\n## 概念\n  *区别于\"注释\"：*\n  1.注释是用来说明程序的一些文字，给程序员看的\n  2.注解是用来说明程序的，给计算机看的\n**注解：从JDK5开始,Java增加对注解的支持，也就是注解，注解与注释是有一定区别的，可以把注解理解为代码里的特殊标记，这些标记可以在编译，类加载，运行时被读取，并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。**\n\n### 概念描述\n\n* JDK1.5之后的新特性\n* 用来说明程序\n* 使用注解：@注解名称\n### 作用分类\n* **编写文档**：通过代码里标识的注解生成文档【生成javadoc文档】\n\n  ​\t例如在javadoc文档注释中可以利用一些注解例如：@author指定作者，利用@since指定自从...\n\n* **代码分析**：通过代码里标识的注解对代码进行分析【使用反射】\n\n* **编译检查**：通过代码里标识的注解让编译器能够实现基本的编译检查【Override】\n\n### JDK中预定义的一些注解\n\n* @Override：检测被该注解标注的方法时候是继承于父类(接口)的\n* @Deprecated：将该注解标注的内容表示为“已过时”\n* @SuppressWarnings：压制警告，需要传递参数String[] value，一般传递\"all\"\n\n### 自定义注解\n\n#### 格式\n\n  public @interface 注解名称 {\n​\t\t\t属性列表...;\n  }\n\n* **元注解：用于描述注解的注解**\n\n  * @Target：描述直接能够作用的位置\n\n    * 参数ElementType[] value()掌握3个取值：\n\n       1.ElementType.TYPE 表示能作用于类上\n\n       2.ElementType.METHOD 表示能作用于方法上 \n\n       3.ElementType.FIELD 表示能作用于成员变量上\n\n  * @Retention：描述注解被保留的阶段\n\n    * 参数RetentionPolicy value() 一般使用RetentionPolicy.RUNTIME 表示当前注解会保留到class字节码文件中，并被JVM读取到\n\n  * @Documented：描述注解是否能被抽取到API文档中\n\n  * @Inherited：描述注解是否被子类继承\n\n**以上的“元注解”是用于修饰Annotation的，也就是打在注解上的注解**\n\n#### 本质\n\n* public interface MyAnnotation extends java.lang.annotation.Annotation { }\n\n* 属性：接口中可以定义的成员方法，可以看做抽象方法\n\n  **要求：**\n\n  1.属性的返回值类型：\n\n     * 基本数据类型\n\n     * String\n\n     * 枚举\n\n     * 注解\n\n     * 以上类型的数组\n\n   2.使用了含有属性的注解时必须给属性赋值\n  \n  * 定义的属性如果指定了default值，那就可以不赋值\n  * 如果只有一个属性需要赋值，并且属性的名称为value，那么可以把(value=12)简写为(12)\n  * 枚举类型赋值为color = Color.red\n  * 注解类型赋值为anno = @MyAnno\n  * 数组类型赋值为strArr = {\"a\",\"b\",\"c\"}，当数组只有一个值时，可以省略{大括号}\n\n### 在程序中使用(解析)注解\n\n  1.获取该类的字节码对象：类名.class 返回一个Class<T>\n\n  2.获取指定的注解：调用该字节码对象的getAnnotation(Class<A> annotationClass) 返回一个<A extends annotation.Annotation>注解对象\n\n* 打在一个class类上的注解可以通过getAnnotation()得到。同样的，打在一个方法上的注解也可以通过getAnnotation得到，所以对于一个Method对象，也可以调用getAnnotation()方法获取注解\n* 对于获取到的注解对象，可以直接调用它里面属性(或者是说抽象方法)得到打注解时指定的值\n\n3.也可以调用getAnnotations()返回所有的注解到一个Annotation[ ]数组里。","tags":["Java"],"categories":["Java"]},{"title":"开发、构建、部署","url":"/2021/08/05/开发工具/","content":"\n# Maven\n\n## 概念\n\n* Maven 翻译为\"专家\"、\"内行\"，是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型（缩写：POM）概念，Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。\n\n* Maven 是一个**项目管理工具**，可以对 Java 项目进行构建、依赖管理。\n\n* Maven 也可被用于构建和管理各种项目，例如 C#，Ruby，Scala 和其他语言编写的项目。\n\n  <img src=\"/images/image-20210831142805921.png\" alt=\"image-20210831142805921\" style=\"zoom:67%;\" />\n\n## 作用\n\n1. 依赖管理：jar包放置在maven仓库中\n2. 一键构建：体现在Maven的生命周期，每一个构建项目的命令都对应了底层的一个Maven插件\n3. 热编译、热部署\n\n## 下载和安装\n\n![image-20210810214007199](/images/image-20210810214007199.png) \n\n### 下载Maven：\n\n* 访问：`http://maven.apache.org/download.cgi`\n* 下载对应版本：![image-20210810215303581](/images/image-20210810215303581.png)\n\n### 安装Maven：\n\n* 解压下载好的Maven安装包\n* 添加一个系统环境变量`Path`为Maven目录的`bin`目录，即配置一个`MAVEN_HOME`并作为`Path`\n* 利用`mvn -v`测试版本信息\n\n**注意：Maven依赖JAVA_HOME，需要先确保电脑正确配置了JAVA_HOME**\n\n## Maven仓库\n\n### 分类：\n\n1. **本地（Local）**\n\n   * Maven 的本地仓库，在安装 Maven 后并不会创建，它是在第一次执行 maven 命令的时候才被创建。\n\n     **默认的本地Maven仓库在`${user.home}/.m2/repository`里**\n\n   * 运行 Maven 的时候，Maven 所需要的任何构件都是直接从本地仓库获取的。如果本地仓库没有，它会首先尝试从远程仓库下载构件至本地仓库，然后再使用本地仓库的构件。\n     \n   * 自定义本地仓库位置：修改Maven安装目录下的`conf`文件夹内的`settings.xml`文件：\n\n     ![image-20210810220335309](/images/image-20210810220335309.png)\n\n2. **中央（Central）**\n\n   * Maven 中央仓库是由 Maven 社区提供的仓库，其中包含了大量常用的库。\n   * 中央仓库包含了绝大多数流行的开源Java构件，以及源码、作者信息、SCM、信息、许可证信息等。一般来说，简单的Java项目依赖的构件都可以在这里下载到。\n   * 中央仓库的关键概念：\n     * 这个仓库由 Maven 社区管理。\n     * 不需要配置。\n     * 需要通过网络访问\n\n3. **远程（Remote）**\n\n   * 如果 Maven 在中央仓库中也找不到依赖的文件，它会停止构建过程并输出错误信息到控制台。为避免这种情况，Maven 提供了远程仓库的概念，它是开发人员自己定制仓库，包含了所需要的代码库或者其他工程中用到的 jar 文件。\n   * 在中央仓库获取不到的`pom.xml`中声明的依赖文件会在远程仓库中下载。\n\n### Maven依赖搜索顺序\n\n* **步骤 1** － 在本地仓库中搜索，如果找不到，执行步骤 2，如果找到了则执行其他操作。\n* **步骤 2** － 在中央仓库中搜索，如果找不到，并且有一个或多个远程仓库已经设置，则执行步骤 4，如果找到了则下载到本地仓库中以备将来引用。\n* **步骤 3** － 如果远程仓库没有被设置，Maven 将简单的停滞处理并抛出错误（无法找到依赖的文件）。\n* **步骤 4** － 在一个或多个远程仓库中搜索依赖的文件，如果找到则下载到本地仓库以备将来引用，否则 Maven 将停止处理并抛出错误（无法找到依赖的文件）。\n\n## Maven坐标\n\n### 概念\n\n* **坐标**用于描述仓库中资源的位置\n\n### 主要组成\n\n* groupId：定义当前Maven项目隶属的组织名称（通常是域名反写）\n* artifactId：定义当前Maven项目名称（通常是模块名称）\n* version：定义当前项目的版本号\n* packaging：定义该项目的打包方式\n\n## Maven项目标准目录结构\n\n* `src/main/java`：核心Java代码部分\n\n* `src/main/resources`：配置文件部分\n\n* `src/test/java`：测试代码部分\n\n* `src/test/resources`：测试配置文件部分\n\n* `src/main/webapp`：页面资源，包括js、css、图片等等，Web项目包含此目录\n\n## Maven常用命令\n\n* `mvn clean`：清除target目录\n* `mvn compile`：编译`src/main/java`下的源代码生成到target目录\n* `mvn test`：编译`src/test/java`与`src/main/java`下的源代码生成到target目录（会执行`compile`）\n* `mvn package`：打包项目到target目录（会执行`compile`和`test`）\n  * 打包的类型由pom.xml内的`<project>`标签内的`<packaging>`标签指定（默认为`jar`）\n* `mvn install`：将项目打包并放到本地仓库中（会执行`compile`和`test`和`package`）\n* `mvn deploy`：将项目发布到远程仓库（会执行`compile`和`test`和`package`和`install`）\n\n## Maven生命周期\n\n![image-20210810231244337](/images/image-20210810231244337.png)\n\n## Maven概念模型\n\n![image-20210810232814775](/images/image-20210810232814775.png)\n\n## IDEA集成Maven\n\n![image-20210810232956154](/images/image-20210810232956154.png)\n\n<img src=\"/images/image-20210810233129562.png\" alt=\"image-20210810233129562\"  />\n\n**这种方式修改的Maven配置只对当前项目生效，要更改所有新项目配置，在IDEA左上角的`File`选择新项目设置**\n\n### 使用骨架（archetype）创建Maven的Java项目\n\n![image-20210810233451734](/images/image-20210810233451734.png)\n\n**这种方式创建的Maven目录结构缺少`src/main`下的`resources`目录**\n\n### 不使用骨架（archetype）创建Maven的Java项目\n\n创建之后的目录结构：![image-20210810235628712](/images/image-20210810235628712.png)\n\n### 创建Maven的Web项目\n\n![image-20210810235849030](/images/image-20210810235849030.png)\n\n创建之后的目录结构：![image-20210810235953017](/images/image-20210810235953017.png)\n\n### 手动指定Maven的Web项目的资源包\n\n如果想在`src/main/java`下也能放置web资源文件，需要添加此目录为模块的**Web资源目录**：\n\n![image-20210811000431096](/images/image-20210811000431096.png)\n\n### 在pom.xml中指定依赖\n\n* 每一个`<dependency>`标签的依赖都写在`<dependencies>`标签里\n\n* 每一个`<dependency>`依赖至少包含3个部分：\n\n  * `groupId`：库来源的公司名称\n  * `artifactId`：库来源的项目名称\n  * `version`：版本号\n\n  选填的：\n\n  * `scope`：作用域\n\n例如，引入Servlet的依赖：\n\n ![image-20210811011751584](/images/image-20210811011751584.png)\n\n### 在pom.xml中指定插件\n\n* 每一个`<plugin>`标签的插件都写在`<plugins>`标签里，而`<plugins>`在`<build>`标签里\n* `<pluginManagement>`标签的`<plugins>`内的插件只是声明，Maven不会加载\n\n例如，引入tomcat7的插件：\n\n![image-20210811012022982](/images/image-20210811012022982.png) \n\n或者，指定maven的compiler编译器插件：\n\n![image-20210811012432823](/images/image-20210811012432823.png) \n\n### 可能遇到的问题：\n\n#### 1.IDEA无法创建Servlet，没有Servlet提示\n\n* 原因：没有导入javax.servlet的包\n* 解决方法：在`pom.xml`里导入以下依赖后重新加载Maven\n\n```xml\n<dependency>\n    <groupId>javax.servlet</groupId>\n    <artifactId>javax.servlet-api</artifactId>\n    <version>4.0.0</version>\n    <scope>provided</scope>\n    <!--provided只保证写代码时的jar包依赖，解决了部署到tomcat造成的冲突-->\n</dependency>\n    <dependency>\n    <groupId>javax.servlet.jsp</groupId>\n    <artifactId>jsp-api</artifactId>\n    <version>2.2</version>\n    <scope>provided</scope>\n    <!--provided只保证写代码时的jar包依赖，解决了部署到tomcat造成的冲突-->\n</dependency>\n```\n\n#### 2.无法使用Maven自带的Tomcat启动项目（mvn tomcat:run命令无法使用）\n\n* 原因：Maven本地仓库没有Tomcat的插件\n* 解决方法：在`pom.xml`里导入Tomcat插件后重新加载Maven\n\n```xml\n        <plugin>\n                <groupId>org.apache.tomcat.maven</groupId>\n                <artifactId>tomcat7-maven-plugin</artifactId>\n                <version>2.1</version>\n                <configuration>\n                    <port>80</port>\n                    <path>/mvn</path>\n                    <uriEncoding>UTF-8</uriEncoding>\n                    <finalName>mvn</finalName>\n                    <server>tomcat7</server>\n                </configuration>\n            </plugin>\n```\n\n* 补充：IDEA右侧的Maven侧边工具栏中，还是无法显示tomcat插件，需要去掉pom.xml的`pluginManagement`标签重新加载Maven即可\n\n#### 3.Servlet依赖冲突\n\n* 原因：`pom.xml`中导入了Servlet的依赖jar包，在运行时Tomcat也自带有依赖jar包导致冲突\n\n* 解决方法：为pom.xml的Servlet依赖指定`scope`标签：`<scope>provided</scope>`只保留在写代码阶段\n\n  **具体解释来源于[CSDN](https://blog.csdn.net/u012501054/article/details/80360906):**\n\n  * 对于scope=compile的情况（默认scope),也就是说这个项目在编译，测试，运行阶段都需要这个artifact对应的jar包在classpath中。\n\n  * 而对于scope=provided的情况，则可以认为这个provided是目标容器已经provide这个artifact。换句话说，它只影响到编译，测试阶段。在编译测试阶段，我们需要这个artifact对应的jar包在classpath中，而在运行阶段，假定目标的容器（比如我们这里的liferay容器）已经提供了这个jar包，所以无需我们这个artifact对应的jar包了。\n\n  <img src=\"/images/image-20210811012929374.png\" alt=\"image-20210811012929374\" style=\"zoom: 67%;\" />\n\n#### 4.IDEA控制台乱码\n\n* 原因：Maven的tomcat编码问题\n* 解决方法：修改IDEA的Maven设置：修改Runner项设置`VM Options`：`-Dfile.encoding=gb2312`\n\n## Maven高级\n\n### 分模块开发与设计\n\n* **ssm_pojo**拆分：\n  * 新建模块\n  * 复制原始项目中的pojo层内容到ssm_pojo模块中\n    * 复制实体类到对应的实体包\n    * 不需要配置文件到resources目录\n* **ssm_dao**拆分\n  * 新建模块\n  * 复制原始项目中的dao层对应内容到ssm_dao模块中\n    * 数据dao层的接口\n    * 配置文件：保留与dao层相关的内容，例如分页插件等\n  * pom.xml中引入坐标，删除springMVC坐标\n    * spring\n    * mybatis\n    * spring整合mybatis\n    * mysql\n    * druid\n    * pagehelper\n    * pojo层的模块依赖（需要先把pojo层的maven模块执行`install`安装到仓库作为依赖）、\n* **ssm_service**拆分\n  * 新建模块\n  * 复制原始项目中的service层对应内容到ssm_service模块中\n    * 业务层接口与实现类（service包以及service层的impl实现包）\n    * 配置文件：保留与service层相关的配置文件\n    * pom.xml中保留与service相关坐标即可，删除springMVC坐标\n      * spring\n      * junit\n      * spring整合junit\n      * 直接依赖ssm_dao模块\n      * 间接依赖ssm_pojo模块（由于依赖传递，已经被ssm_dao模块依赖）\n    * 修改service模块spring核心配置文件名为applicationContext-service.xml\n    * 修改dao模块spring核心配置文件名为applicationContext-dao.xml\n    * 修改单元测试`@ContextConfiguration`注解引入的配置文件名称，由单个文件修改为多个文件\n* **ssm_controller**拆分\n  * 新建模块\n  * 复制原始项目中的controller层对应内容到ssm_controller模块中\n    * 表现层控制器类与相关设置类（Controller与异常类）\n    * 配置文件：保留与表现层springMVC相关配置文件、服务器相关配置文件\n    * pom.xml引入表现层相关坐标即可，删除springMVC相关坐标\n      * spring\n      * springMVC\n      * jackson\n      * servlet\n      * tomcat服务器插件\n      * 直接依赖ssm_service\n      * 间接依赖ssm_dao、ssm_pojo\n    * 修改web.xml配置文件中加载spring核心配置文件的名称，使用`applicationContext-*.xml`通配\n\n**分模块开发总结：**\n\n* 模块中仅包含档期啊你模块对应的功能类与配置文件\n* spring核心配置根据模块的功能独立制作，分别命名区分\n* 当前模块所依赖的模块可以通过导入坐标的形式导入依赖，并且需要位于maven仓库（先`install`安装）\n* web.xml需要加载所有的spring核心配置文件（通过通配符加载多个核心配置文件）\n\n### 聚合\n\n作用：用于快速构建Maven工程，一次性构建多个项目/模块\n\n制作方式：\n\n* 创建一个空模块，pom.xml中指定打包类型`packaging`为`pom`\n\n  ```xml\n  <!--定义此模块用于构建管理-->\n  <packaging>pom</packaging>\n  ```\n\n* 定义当前模块进行构建操作时关联的其他模块名称\n\n  ```xml\n  <!--管理的工程列表-->\n  <modules>\n      <!--具体的模块名称-->\n      <module>01module1</module>\n      <module>02module2</module>\n      <module>03module3</module>\n  </modules>\n  ```\n\n### 继承\n\n作用：通过继承可以实现在子工程中沿用父工程中的配置\n\n制作方式：\n\n* 在子工程中声明其父工程坐标与对应的位置\n\n  ```xml\n  <parent>\n      <artifactId>SpringDemo</artifactId>\n      <groupId>com.taoyyz</groupId>\n      <version>1.0-SNAPSHOT</version>\n  </parent>\n  ```\n\n* 在父工程中定义依赖管理\n\n  ```xml\n  <!--声明此处进行依赖管理-->\n  <dependencyManagement>\n      <!--具体的一些依赖-->\n      <dependencies>\n          <!--具体依赖项-->\n          <dependency>\n              <groupId>mysql</groupId>\n              <artifactId>mysql-connector-java</artifactId>\n              <version>5.1.38</version>\n          </dependency>\n      </dependencies>\n  </dependencyManagement>\n  ```\n\n* 在子工程中定义依赖关系，无需声明依赖版本，版本会参照父工程中的依赖版本\n\n  ```xml\n  <dependencies>\n      <dependency>\n          <groupId>mysql</groupId>\n          <artifactId>mysql-connector-java</artifactId>\n      </dependency>\n  </dependencies>\n  ```\n\n<img src=\"/images/image-20210901010702519.png\" alt=\"image-20210901010702519\" style=\"zoom:67%;\" />\n\n### 属性\n\n#### 自定义属性\n\n* 自定义属性：\n\n  ```xml\n  <!--定义自定义属性-->\n  <properties>\n  \t<spring.version>5.0.5.RELEASE</spring.version>\n  </properties>\n  ```\n\n* 使用此自定义属性：`${属性名}`\n\n  ```xml\n  <dependency>\n      <groupId>org.springframework</groupId>\n      <artifactId>spring-context</artifactId>\n      <version>${spring.version}</version>\n  </dependency>\n  ```\n  \n\n#### 内置属性：\n\n* 可以利用`${内置属性名}`直接使用pom.xml中内置属性的内容\n\n  例如：\n\n  * `${project.version}`简写为`${version}`\n  * `${project.basedir}`简写为`${basedir}`\n\n#### Setting属性\n\n* 利用`${settings.属性名}`使用settings.xml配置文件中的属性\n\n  例如：\n\n  * `${settings.localRepository}`\n  * `${settings.interactiveMode}`\n\n#### Java系统属性\n\n* 利用`${user.属性名}`使用Java系统属性\n\n  例如：\n\n  * `${user.home}`\n  * `${user.dir}`\n\n* 系统属性查询方式：`mvn help:system`\n\n#### 环境变量属性\n\n* 利用`${env.环境变量属性名}`获取环境变量属性\n\n  例如：\n\n  * `${env.JAVA_HOME}`\n  * `${env.MAVEN_HOME}`\n\n* 环境变量查询方式：`mvn help:system`\n\n### 版本管理\n\n#### 分类：\n\n* SNAPSHOT（快照版本）\n* RELEASE（发布版本）\n\n#### 工程版本号约定\n\n* 约定规范：\n  * <主版本>.<次版本>.<增量版本>.<里程碑版本>\n  * 主版本：表示项目重大架构变更\n  * 次版本：表示有较大的功能增加和变化，或者全面系统地修复漏洞\n  * 增量版本：表示有重大漏洞的修复\n  * 里程碑版本：表名一个版本的里程碑（内部版本），这样的版本同下一个正式版本相比有待测试\n* 范例：`5.1.9.RELEASE`\n\n### 资源配置\n\n配置文件可以引用pom.xml中定义的**属性**\n\n* 作用：在任意配置文件中加载pom文件中的属性\n\n* 调用格式：`${属性名}`\n\n* 开启配置文件加载pom属性：\n\n  ```xml\n  <!--配置资源文件的信息，从Maven的属性加载值-->\n  <build>\n      <!--配置资源文件的信息-->\n      <resources>\n          <resource>\n              <directory>${basedir}/18_SSM_integration/src/main/resources</directory>\n              <filtering>true</filtering>\n          </resource>\n      </resources>\n      <!--配置测试目录的资源文件的信息-->\n      <testResources>\n          <testResource>\n              <directory>${basedir}/18_SSM_integration/src/test/resources</directory>\n              <filtering>true</filtering>\n          </testResource>\n      </testResources>\n  </build>\n  ```\n\n### 多环境开发配置\n\n* 定义多个环境\n\n  ```xml\n  <!--创建多环境-->\n  <profiles>\n      <!--生产环境-->\n      <profile>\n          <!--定义此环境的唯一名称-->\n          <id>produce_env</id>\n          <!--定义此环境中专用的属性值-->\n          <properties>\n              <jdbc.url>jdbc:mysql:///db1</jdbc.url>\n          </properties>\n      </profile>\n      \n      <!--开发环境-->\n      <profile>\n          <!--定义此环境的唯一名称-->\n          <id>development_env</id>\n          <!--定义此环境中专用的属性值-->\n          <properties>\n              <jdbc.url>jdbc:mysql:///db3</jdbc.url>\n          </properties>\n          <!--设置为默认启动此环境-->\n          <activation>\n              <activeByDefault>true</activeByDefault>\n          </activation>\n      </profile>\n  </profiles>\n  ```\n\n* 使用某个环境：\n\n  * 利用`activation`标签指定默认环境\n  * mvn命令指定加载某环境：`mvn 指令 -P 环境id`，例如`mvn install -P produce_env`\n\n### 跳过测试\n\n* 应用场景：\n\n  * 整体模块功能未开发\n  * 模块中某个功能未开发完毕\n  * 单个功能更新调试导致其他功能失败\n  * 快速打包\n\n* **跳过测试**\n\n  * 方式1： ![image-20210902232007642](/images/image-20210902232007642.png)\n\n  * 方式2：执行命令：`mvn install -D skipTests`\n\n  * 方式3：修改pom.xml中的插件，跳过测试\n\n    ```xml\n    <plugins>\n        <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <version>2.22.1</version>\n            <configuration>\n                <!--跳过全部测试-->\n                <skipTests>true</skipTests>\n            </configuration>\n        </plugin>\n    </plugins>\n    ```\n\n  * 指定或跳过测试类：\n\n    ```xml\n    <plugins>\n        <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <version>2.22.1</version>\n            <configuration>\n                <!--指定测试内容-->\n                <includes>\n                    <include>**/MultiTest.java</include>\n                </includes>\n                <!--指定排除内容-->\n                <excludes>\n                    <exclude>**/MyBatisTest.java</exclude>\n                </excludes>\n            </configuration>\n        </plugin>\n    </plugins>\n    ```\n\n### 私服\n\n* **Nexus**\n\n  * 下载安装\n  * 启动：`nexus.exe /run nexus`\n  * 访问服务器：`http://localhost:8081`\n  * 修改配置：\n    * 基础配置：修改etc目录下的`nexus-default.properties`\n    * 运行配置：修改bin目录下的`nexus.vmoptions`\n\n* **仓库分类**\n\n  * 宿主仓库hosted\n    * 保存无法从中央仓库获取的资源\n      * 自主研发\n      * 第三方非开源项目\n  * 代理仓库proxy\n    * 代理远程仓库，通过nexus访问其他公共仓库，例如中央仓库\n  * 仓库组group\n    * 将若干个仓库组成一个群组，简化配置\n    * 仓库组不能保存资源，属于设计型仓库\n\n* **资源上传**\n\n  * 把宿主仓库加入到`maven-public`群组\n  * 保存的位置（宿主仓库）\n  * 资源文件\n  * 对应坐标\n\n* **IDEA中资源的上传和下载**\n\n  <img src=\"/images/image-20210903000422508.png\" alt=\"image-20210903000422508\" style=\"zoom: 50%;\" />\n\n  *  配置本地Maven仓库访问私服的权限：settings.xml\n\n    * 配置`servers`标签中的多个`server`\n      * 配置此`server`的`id`为nexus仓库的id\n      * 配置此`server`的`username`\n      * 配置此`server`的`password`\n\n  * 配置本地仓库的资源来源\n\n    * 配置`mirrors`标签中的`mirror`\n      * 配置此`mirror`的`id`\n      * 配置此`mirror`的`mirrorOf`为*\n      * 配置此`mirror`的`url`为nexus仓库的`url`\n  \n* **发布资源到私服**\n\n  * 在pom.xml中配置发布管理\n\n    ```xml\n    <distributionManagement>\n        <repository>\n            <!--对应server的id-->\n            <id>repo_tjj</id>\n            <url>地址url</url>\n        </repository>\n        <snapshotRepository>\n            <!--对应server的id-->\n            <id>repo_snap</id>\n            <url>snap的地址url</url>\n        </snapshotRepository>\n    </distributionManagement>\n    ```\n\n  * 发布到私服：`mvn deploy`\n\n# Linux\n\n## 概念\n\n* **Unix**：Unix是一个强大的多用户、多任务的操作系统。于1969年在贝尔实验室开发。Unix的商标权由国际开放标准组织（The Open Group）所拥有。Unix是商业版，需要收费。\n* **Linux**：Linux是基于Unix的，是一种自由和开放源码的操作系统，存在许多的发行版本使用Linux内核。\n\n## 版本\n\n* 内核版本\n* 发行版本\n\n## 主流版本\n\n<img src=\"/images/image-20210811201646925.png\" alt=\"image-20210811201646925\" style=\"zoom:67%;\" /> \n\n## 安装Linux\n\n### 安装到虚拟机\n\n#### 虚拟机的概念\n\n* 软件模拟的虚拟电脑\n\n#### 常用的虚拟机\n\n* **VMware**\n* **VirtualBox**\n\n#### VMware安装CentOS\n\n### 安装到物理机\n\n## Linux目录结构\n\n<img src=\"/images/image-20210811205523078.png\" alt=\"image-20210811205523078\" style=\"zoom: 80%;\" />\n\n* root --- 启动[Linux](http://linux-wiki.cn/wiki/Linux)时使用的一些核心文件。如操作系统[内核](http://linux-wiki.cn/index.php?title=内核&action=edit&redlink=1)、引导程序[Grub](http://linux-wiki.cn/wiki/Category:Grub)等。\n* home --- 存储普通用户的个人文件\n  - ftp --- 用户所有服务\n  - httpd\n  - samba\n  - user1\n  - user2\n* bin --- 系统启动时需要的执行文件（二进制）\n* sbin --- 可执行程序的目录，但大多存放涉及系统管理的命令。只有root权限才能执行\n* proc --- 虚拟，存在linux内核镜像；保存所有内核参数以及系统配置信息\n  - 1 --- 进程编号\n* usr --- 用户目录，存放用户级的文件\n  - bin --- 几乎所有用户所用命令，另外存在与/bin，/usr/local/bin\n  - sbin --- 系统管理员命令，与用户相关，例如，大部分服务器程序\n  - include --- 存放C/C++头文件的目录\n  - lib --- 固定的程序数据\n  - local --- 本地安装软件保存位置\n  - man --- 手工生成的目录\n  - info --- 信息文档\n  - doc --- 不同包文档信息\n  - tmp\n  - X11R6 --- 该目录用于保存运行X-Window所需的所有文件。该目录中还包含用于运行GUI要的配置文件和二进制文件。\n  - X386　--- 功能同X11R6，X11 发行版5 的系统文件\n* boot --- 引导加载器所需文件，系统所需图片保存于此\n* lib ---根文件系统目录下程序和核心模块的公共库\n  - modules --- 可加载模块，系统崩溃后重启所需模块\n* dev --- 设备文件目录\n* etc --- 配置文件\n  - skel --- home目录建立，该目录初始化\n  - sysconfig --- 网络，时间，键盘等配置目录\n* var\n  - file\n  - lib --- 该目录下的文件在系统运行时，会改变\n  - local --- 安装在/usr/local的程序数据，变化的\n  - lock --- 文件使用特定外设或文件，为其上锁，其他文件暂时不能访问\n  - log --- 记录日志\n  - run --- 系统运行合法信息\n  - spool --- 打印机、邮件、代理服务器等假脱机目录\n  - tmp\n  - catman --- 缓存目录\n* mnt --- 临时用于挂载文件系统的地方。一般情况下这个目录是空的，而在我们将要挂载分区时在这个目录下建立目录，再将我们将要访问的设备挂载在这个目录上，这样我们就可访问文件了。\n* tmp --- 临时文件目录，系统启动后的临时文件存放在/var/tmp\n* lost+found --- 在文件系统修复时恢复的文件\n\n## Linux常用命令\n\n### 操作目录\n\n1. 切换目录：`cd`\n2. 列出文件列表：`ls`\n   * `ls -a`：列举出包括**隐藏文件**的所有文件\n   * `ls -l`：列举出详细信息，简写为`ll`\n   * `ls -lh`：以`MB`为单位列举出详细信息\n3. 创建目录/删除目录：`mkdir/rmdir`\n   * 创建多级目录：`mkdir -p parent/son`，`-p`参数表示如果不存在`parent`就同时创建父目录\n   * 删除多级空目录：`rmdir -p parent/son/sonson`，会从sonson内层删除到parent之间的空目录\n\n### 操作文件\n\n1. 查看文件内容：`cat`\n2. 查看文件内容，一次查看一页：`more`，按空格翻页，按`q`退出\n3. 查看文件内容，一次查看一页：`less`，按空格翻页，按`q`退出，按上下键可以方便地查看一行\n4. 倒序查看文件内容：`tail`，默认倒数10行\n   * 指定倒序查看到第几行：`tail -行数 文件名`即可\n   * 倒序查看文件的实时更新内容：`tail -f 文件名`\n5. 复制文件：`cp`\n6. 移动文件：`mv`，可以用来重命名\n7. 删除文件：`rm`，需要输入`n`（no）或`y`（yes）来确认\n   * 删除非空文件夹：`rm -r 文件夹`，删除目录内容及所有子目录下的内容，此命令会询问\n   * 删除非空文件夹，且不询问：`rm -rf 文件夹`，删除目录内容及所有子目录下的内容\n   * 删除超过指定天数7天的zip文件：`find ./ -type f -name \"*.zip\" -mtime +7 -delete`\n\n### 打包或解压\n\n1. `tar -cvf`：打包指定的文件或目录，但不压缩，例如：`tar -cvf xxx.tar ./*`\n   * `c`：创建一个新的tar文件\n   * `v`：显示出运行过程\n   * `f`：指定文件名\n   * `z`：调用gzip进行压缩\n   * `t`：查看压缩文件的内容\n   * `x`：解压缩\n2. `tar -zcvf `：打包指定的文件或目录，且利用gzip压缩，例如：`tar -zcvf xxx.tar.gz ./*`\n3. `tar -xvf`：解压指定的tar包\n4. `tar -xzvf`：解压指定的gzip压缩包，可以利用`-C`重新指定解压位置\n\n### 查找文件或内容\n\n1. `find`：查找文件，例如：`find 目录 -name \"文件名\"`，查找指定目录（包括子目录的文件）\n   * `find 目录 -maxdepth 最大查找深度 -name \"文件名\"`，限制查找深度，比如为1时只查找当前1层\n2. `grep`：查找文件的内容，例如：`grep 要查找的关键字 文件名`，在指定文件名的文件中查找关键字的内容\n   * `grep 关键字 文件名 --color`，查找结果中高亮显示查找的关键字\n   * `grep 关键字 文件名 -A行数`，查找的结果往后（After）显示几行\n   * `grep 关键字 文件名 -B行数`，查找的结果往前（Before）显示几行\n\n### 其他常用命令\n\n1. `pwd`：查看当前目录\n2. `groups`：查看当前属于的组\n3. `whoami`：查看当前用户\n4. `getconf LONG_BIT`：查看系统版本位数\n5. `cat /etc/redhat-release`：查看发行版本\n6. `uname -r`：查看Linux内核版本\n7. `free -h`：查看内存使用情况\n8. `lsof -i:端口号`：查看占用端口的进程\n9. `netstat -tunlp | grep 端口号`：同上\n10. `touch`：创建一个文件\n11. `clear`：清除屏幕，等同于`ctrl+L`\n\n## VI和VIM编辑器\n\n### VI提供了3种模式\n\n1. 命令行\n2. 插入\n3. 底行\n\n### 切换到**命令行模式**：`ESC`\n\n### 切换到**插入模式**：`i`、`o`、`a`\n\n* `i`：在当前位置插入\n* `I`：在当前行首插入\n* `a`：在当前位置后插入\n* `A`：在当前行尾插入\n* `o`：在当前行之后插入\n* `O`：在当前行之前插入\n\n### 切换到替换模式：`R`\n\n* 替换模式下对每一个字符做的输入都会替换掉原来位置的字符\n\n### 切换到可视模式：v或V\n\n* `v`：从当前光标开始可视\n* `V`：从当前行开始按行可视\n\n### 操作VIM文本\n\n* `Home`：定位光标到这一行的开头\n  * `^`：定位光标到这一行开头的第一个非空白字符\n* `End`：定位光标到这一行的结尾\n  * `$`：定位光标到这一行结尾的最后一个非空白字符\n* `PageUp`：向上翻页，相当于`ctrl + b/B`\n  * `ctrl + y`：向上滚动屏幕\n* `PageDown`：向下翻页，相当于`ctrl + f/F`\n  * `ctrl + e`：向下滚动屏幕\n* `h`：向左移动光标，但不会导致换行\n* `l`：向右移动光标，但不会导致换行\n* `enter`：向下移动光标到下一行第一个非空白字符处\n* `H`：移动至顶部第一个非空白字符处\n* `M`：移动至屏幕中间第一个非空白字符处\n* `L`：移动至底部第一个非空白字符处\n* `zz`：移动至当前行作为屏幕中间处\n* `w`：移动到下一个单词字首\n* `W`：移动到下一个单词字首（忽略标点符号）\n* `e`：移动到下一个单词字尾\n* `E`：移动到下一个单词字尾（忽略标点符号）\n* `b`：移动到上一个单词字首\n* `B`：移动到上一个单词字首（忽略标点符号）\n* `)`：移动到下一个句首\n* `(`：移动到上一个句首\n* `}`：移动到下一个段落首\n* `{`：移动到上一个段落首\n* `%`：移动到匹配的另一个`{}`、`[]`、`()`等括号\n* `列数|`：光标移动到第`列数`列\n* `d + 一个改变光标操作`：从光标起始位置删除至改变后的位置\n* `dd`：快速删除整行\n* `x`：删除光标处的字符\n  * `Nx`：删除光标处往后N个字符\n* `X`：删除光标前一个字符\n  * `NX`：删除光标处往前N个字符\n* `D`：删除光标到这行结尾之间的字符\n* `C`：修改光标到这行结尾之间的字符，相当于`D + i`\n* `S`：修改整行的字符，相当于`Home + C`\n* `diw`：删除光标所在的单词，不包括空白字符\n* `daw`：删除光标所在的单词，包括空白字符\n* `dw`：删除光标到下一个单词词首之间的字符\n* `yy`：复制当前行\n  * `P`：粘贴复制的内容到当前光标前\n  * `p`：粘贴复制的内容到当前光标后\n* `ma`：对当前光标处做一个`a`标记\n  * `y'a`：复制新光标处到`a`标记之间的内容\n  * `d'a`：删除新光标处到`a`标记之间的内容\n* `u`：撤销改动（undo）\n* `ctrl + r`：重做改动（redo）\n* `:/关键字`：从头开始向下搜索关键字\n  * 搭配`n`快速定位光标到下一个搜索到的内容\n  * 搭配`N`快速定位光标到上一个搜索到的内容\n* `:?关键字`：从末尾开始向上搜索关键字\n* `zf + 一个改变光标的操作`：折叠文本\n* `zfap`：折叠一个段落的文本\n* `zo`：打开折叠的文本\n* `zc`：关闭折叠\n* `'`：移动到上一次修改的行\n* `''`：移动到上一次光标所在位置\n* `N%`：跳转到文本的百分之`N`的位置\n* `>>`：把当前行向右移动一段距离\n  * `N>>`：把下面`N`行向右移动一段距离\n  * `:N,M>>`：把`N`到`M`行向右移动一段距离\n* `<<`：把当前行向左移动一段距离\n  * `N<<`：把下面`N`行向左移动一段距离\n  * `:N,M<<`：把`N`到`M`行向左移动一段距离\n* `.`：重复上一条命令\n\n### 页面命令\n\n* 显示行号：命令模式下`:`后输入`set number`或`set nu`\n  * `set`指令可简写为`se`\n* 查看总行数和所处位置百分比：`ctrl + g`\n* 字数统计：`g ctrl + g`\n\n* 到第一行：`1G`，相当于`gg`，也相当于命令模式下`:`后输入`1`\n* 到第N行：`行号G`，命令模式下`:`后输入`行号`\n* 到最后一行：`G`\n* 向上半页：`ctrl + u/U`\n* 向下半页：`ctrl + d/D`\n* 暂停vi回到提示符：`ctrl + z`\n* 重新回到vi：`fg`\n\n### 退出VIM\n\n* `:q`退出VI或VIM\n\n* `:q!`放弃所做的更改，强制退出\n* `:wq`保存更改并退出\n\n## 重定向控制台的输出\n\n* `cat 文本文件 > 目标文件名`：把文本文件的内容输出到目标文件中\n* `cat 文本文件 >> 目标文件名`：把文本文件输出并且追加到目标文件中\n\n**注意：任何能在控制台打印的信息都可以重定向，不是只有`cat`**\n\n## 进程管理\n\n* `ps -ef`：查看正在运行的进程\n  * `ps -ef | grep 关键字`：筛选进程列表中含有关键字的进程\n* `kill 进程号`：关闭进程\n* `kill -9 进程号`：强制关闭进程\n\n## 管道：`|`\n\n管道可以将一个命令的输出作为另一个命令的输入，例如上面筛选进程的案例：`ps -ef | grep 关键字`\n\n## Linux权限管理\n\n![image-20210811232306155](/images/image-20210811232306155.png)\n\n**第一个字符**表示文件类型：\n\n* `-`：表示这是一个**文件**\n* `l`：表示这是一个**链接**\n* `d`：表示这是一个**目录**\n* `b`：表示这是一个**块设备**\n* `s`：表示这是一个**套接字**\n\n**第一组字符（rwx）**表示**当前用户**对该文件的权限\n\n**第二组字符（rwx）**表示**当前组内**的用户对该文件的权限\n\n**第三组字符（rwx）**表示**其他组**的用户对该文件的权限\n\n### 权限符号\n\n* `r`表示对于该用户可读，对于文件来说是允许读取内容，对于目录来说是允许读取其中的文件\n* `w`表示对于该用户可写，对于文件来说是允许修改其内容，对于目录来说可以写信息到目录中，即可以创建、删除文件、移动文件等操作\n* `x`表示对于该用户可执行，对于文件来说就是可以执行该文件，对于目录来说则是可以进入目录；可以搜索（能用该目录名称作为路径名去访问它所包含的文件和子目录）\n\n### 修改权限\n\n* `chmod u=rwx[,g=rwx,o=rwx] 文件名`：修改该文件的对应的用户权限，可选\n* `chmod 777 文件名 `：修改该文件名对应的用户权限，其中`r`：4，`w`：2，`x`：1\n* `chmod +x 文件名`：修改该文件名对应的权限，为所有用户增加`x`可执行权限\n\n## Linux网络操作\n\n### 主机名配置\n\n* `hostname`：查看当前主机名\n  * `hostname 新主机名`：临时更改主机名，重启失效\n* `hostnamectl set-hostname 新主机名`：设置主机名\n* 或者修改`/etc/sysconfig/network`文件的`HOSTNAME`\n\n### 关闭防火墙\n\n**CentOS 7** 默认以**firewalld**作为防火墙\n\n* `systemctl stop firewalld`：关闭防火墙\n\n* `systemctl disable firewalld`：禁用防火墙\n\n### IP地址配置\n\n* 修改`/etc/sysconfig/network-scripts/`下的网卡配置，例如`ifcfg-ens33`\n  * 修改`ONBOOT`为`yes`以保证网卡在开机时自启\n  * 修改`IPADDR`为IP地址\n  * 修改`NETMASK`为子网掩码\n  * 修改`GATEWAY`为网关地址\n  * 修改`DNS1`和`DNS2`为DNS\n* 修改完毕后执行`service network restart`重启网络服务\n\n### 域名映射\n\n* 修改`/etc/hosts`文件\n\n![image-20210811235630532](/images/image-20210811235630532.png) \n\n## Linux的软件安装\n\n### 常见的软件安装方式\n\n* 二进制发布包：解压即可安装\n* RPM包：获取RPM包然后用RPM命令安装\n* Yum：获取RPM软件用Yum安装，可以解决一些库依赖问题\n* 源码编译安装：把源码编译打包部署\n\n### 上传与下载工具\n\n* FileZilla\n* lrzsz：支持yum安装：`yum install lrzsz`\n  * 上传到本机：`rz`\n  * 从本机下载：`sz 文件名`\n\n## Linux配置JavaWeb环境\n\n### 安装JDK\n\n* CentOS 7自带Open JDK\n  * 使用`rpm -qa | grep java`，查看所有的`java`包名\n  * 使用`rpm -e --nodeps 包名 `，卸载掉报名对应的包\n\n1. 复制`JDK`安装包\n\n2. 解压缩包\n\n3. 配置环境变量\n\n   * 使用vim修改`/etc/profile`文件\n\n   * 添加JAVA环境变量：\n\n     ```bash\n     export JAVA_HOME=/root/jdk1.8.0_171\n     export PATH=$PATH:$JAVA_HOME/bin\n     ```\n\n   * 使环境变量生效：`source /etc/profile`\n\n### 安装MySQL\n\n1. 复制`MySQL`安装包\n\n2. 解压缩包\n\n   * 得到的rpm文件：\n\n     ![image-20210812003923200](/images/image-20210812003923200.png)\n\n3. 安装**服务器端**：`rpm -ivh MySQL-server-5.6.22-1.el7.x86_64.rpm`\n\n    * 一大堆`mariadb`报错解决方案：\n       1. `rpm -qa | grep mariadb`列举所有的`mariadb`包\n       2. 通过`rpm -e --nodeps 包名`全部卸载\n       3. 重新执行MySQL安装即可\n\n   * 安装完成后的初始密码在`/root/.mysql_secret`下\n\n4. 安装**客户端**：`rpm -ivh MySQL-client-5.6.22-1.el7.x86_64.rpm`\n\n5. 启动mysql：`service mysql start`\n\n    * 第一次使用mysql必须设置密码：`set password = password('密码');`\n\n6. 开放远程访问权限：\n\n    * `grant all privileges on *.* to 'root'@'%' identified by 'root';`授权\n      * 注意这个`identified by`后面是授权给这个用户的密码\n    * `flush privileges;`刷新权限\n\n### 安装Tomcat\n\n1. 复制`Tomcat`安装包\n2. 解压缩包\n3. 解压完即可使用`bin`目录下的`startup.sh`启动\n\n### 安装Redis\n\n* Redis需要手动使用GCC编译，需要先安装GCC：`yum install gcc`\n\n1. 复制`Redis`安装包\n2. 解压缩\n3. 进入Redis安装目录，执行：`make`编译\n4. 编译完成后，执行：`make PREFIX=/usr/local/redis install`进行安装\n5. Redis将被安装到`/usr/local/redis`下\n\n**提示**：\n\n* Redis配置文件在安装包内，可以手动复制到`/usr/local/redis/bin`下\n* 使用配置文件启动服务器：`./redis-server ./redis.conf`\n* 启动客户端：`./redis-cli`\n\n### 安装Nginx\n\n* Nginx是一款高性能的Web服务器/反向代理服务器及电子邮件代理服务器。\n  * Nginx可以作为HTTP代理服务器\n  * 可以在一台服务器虚拟出多个网站\n  * 反向代理，负载均衡：对于多个tomcat服务器集群可以平均负担负载\n\n#### 安装\n\n* Nginx需要手动使用GCC编译，需要先安装GCC环境\n* 需要第三方的开发包：\n  * PCRE：`yum install -y pcre pcre-devel`，一个Perl库，包括Perl兼容的正则表达式库，Nginx的HTTP模块需要使用PCRE来解析正则表达式\n  * zlib：`yum install -y zlib zlib-devel`，zlib提供了很多压缩和解压缩的方式，Nginx使用zlib对HTTP包的内容进行gzip\n  * OpenSSL：`yum install -y openssl openssl-devel`，OpenSSL是一个强大的安全套接字层密码库，囊括主要的密码算法、常用的密钥和证书封装管理管理及SSL协议。Nginx不仅支持HTTP，还支持HTTPS\n\n1. 复制`Nginx`安装包\n\n2. 解压缩\n\n3. 使用`configure`命令生成`makefile`：\n\n   ```bash\n   ./configure \\\n   --prefix=/usr/local/nginx \\\n   --pid-path=/var/run/nginx/nginx.pid \\\n   --lock-path=/var/lock/nginx.lock \\\n   --error-log-path=/var/log/nginx/error.log \\\n   --http-log-path=/var/log/nginx/access.log \\\n   --with-http_gzip_static_module \\\n   --http-client-body-temp-path=/var/temp/nginx/client \\\n   --http-proxy-temp-path=/var/temp/nginx/proxy \\\n   --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \\\n   --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \\\n   --http-scgi-temp-path=/var/temp/nginx/scgi\n   ```\n\n4. 使用`make`命令编译`makefile`文件：`make`\n\n5. 使用`make install`命令安装：`make install`\n\n6. 创建`configure`命令里指定的临时目录：`mkdir /var/temp/nginx/client -p`\n\n7. Nginx将被安装到`/usr/local/nginx`下\n\n* 启动Nginx：执行Nginx安装目录下的`sbin`目录下的`nginx`：`./nginx`\n* 正常停止Nginx：`./nginx -s quit`\n* 强制停止Nginx：`./nginx -s stop`\n* 重新加载配置文件：`./nginx -s reload`\n* Nginx默认端口：80\n\n#### 部署静态资源\n\n##### 第一种方式：\n\n* 放置html资源到Nginx安装目录的html文件夹，Nginx默认部署`html`文件夹的`index.html`\n  * `nginx.conf`指定`server { }`配置的`location / { }`里的站点路径\n\n##### 第二种方式：配置虚拟主机\n\n* 配置Nginx安装目录下的配置文件：可以指定多个`server { }`的**端口**\n  * `nginx.conf`指定`server { }`配置的`listen`后的端口号，通过不同端口访问不同的`server`\n* 配置Nginx安装目录下的配置文件：可以指定多个`server { }`的**域名**\n  * `nginx.conf`指定`server { }`配置的`server_name`后配置域名或IP地址，以此访问不同的`server`\n    * 域名需要通过DNS服务器解析，否则就要手动修改电脑的hosts文件\n\n#### 反向代理与负载均衡\n\n* 正向代理：正向代理，架设在客户机与目标主机之间，只用于代理内部网络对Internet的连接请求，客户机必须指定代理服务器,并将本来要直接发送到Web服务器上的http请求发送到代理服务器中。\n* 反向代理：反向代理服务器架设在服务器端，通过缓冲经常被请求的页面来缓解服务器的工作量，将客户机请求转发给内部网络上的目标服务器；并将从服务器上得到的结果返回给请求连接的客户端，此时代理服务器与目标主机一起对外表现为一个服务器。\n\n##### 反向代理\n\n**修改`nginx.conf`配置：**\n\n1. 在`http { }`内配置一个upstream：\n\n   ```basic\n   upstream 代理的名称 {\n       server 被代理的服务器ip:端口;    \n   }\n   ```\n\n2. 定义一个server：\n\n   ```coffeescript\n   server {\n           listen       80; #用于访问的端口号\n           server_name  localhost; #用于访问的域名或IP地址\n   \n           location / {\n               proxy_pass http://代理的项目名称; #反向代理的访问路径\n               index  index.jsp; #访问的文件名\n           }\n   }\n   ```\n\n3. 重启nginx：`./nginx -s reload`\n\n##### 负载均衡\n\n1. 为每一个被代理的tomcat服务器和端口配置为server：此时访问被代理的项目，3台server被Nginx随机分配\n\n   ```basic\n   upstream 代理的名称 {\n       server 被代理的服务器1ip:端口;\n       server 被代理的服务器2ip:端口;\n       server 被代理的服务器3ip:端口;\n   }\n   ```\n\n2. 为每一个被代理的tomcat服务器和端口配置为server，并且指定**权重**：\n\n   ```basic\n   upstream 代理的名称 {\n       server 被代理的服务器1ip:端口 weight=权重1;\n       server 被代理的服务器2ip:端口 weight=权重2;\n       server 被代理的服务器3ip:端口 weight=权重3;\n   }\n   ```\n\n   **注意：由于每次访问的tomcat服务器不确定，可能导致session问题，建议使用`ip_hash`方式分配负载**\n\n3. 为此upstream代理指定为**按照请求的ip的hash结果分配**：\n\n   ```basic\n   upstream 代理的名称 {\n   \tip_hash;\n       server 被代理的服务器1ip:端口;\n       server 被代理的服务器2ip:端口;\n       server 被代理的服务器3ip:端口;\n   }\n   ```\n\n4. 为此upstream代理指定为**按照访问url的hash结果分配**，使每个url定向到同一个后端服务器，后端服务器为缓存时比较有效：\n\n   ```basic\n   upstream 代理的名称 {\n   \thash $request_uri;\n   \thash_method crc32;\n       server 被代理的服务器1ip:端口;\n       server 被代理的服务器2ip:端口;\n       server 被代理的服务器3ip:端口;\n   }\n   ```\n\n   **注意：此时不能指定weight等其他参数，hash_method后为hash算法**\n\n5. 为此upstream代理的server指定其他参数：\n\n   * `down`：此server暂时不参与负载\n\n   * `backup`：当其他所有的非backup服务器为down或者忙的时候才请求backup服务器\n\n     ```basic\n     upstream 代理的名称 {\n     \tip_hash;\n         server 被代理的服务器1ip:端口 down; #不参与负载\n         server 被代理的服务器2ip:端口 weight=2; #权重为2\n         server 被代理的服务器3ip:端口 backup; #备用服务器\n     }\n     ```\n\n## Linux部署项目案例\n\n* Maven项目进行`package`命令后**生成的包名**过长，可以在`build`标签内指定`finalName`标签**设置名称**\n\n* 指定数据库连接配置的**字符集**：\n  * 修改`druid.properties`：`url=jdbc:mysql://localhost:3306/db1?characterEncoding=utf-8`\n\n* 修改项目中的绝对路径为相对路径\n* 修改无误后利用Maven进行打包，执行Maven的：`package`\n* 复制打包好的`war`文件到服务器上，在运行`tomcat`的情况下直接复制到`webapps`目录下即可自动部署\n\n**可能遇到的问题**：\n\n* Web项目获取不到服务器端的验证码，日志显示服务器500错误：<font color=darkred>`根本原因：java.awt.AWTError:Can't connect to X11 window server using localhost as the value of the DISPLAY variable.`</font>\n\n  * 原因：这种情况是由于用到了java.awt包下的Image等对象导致的问题\n\n  * 解决方法：修改tomcat的`bin`目录下的`catalina.sh`：\n\n    在最上方添加：`JAVA_OPTS=\"$JAVA_OPTS -Djava.awt.headless=true\"`即可解决图片500问题\n\n# Docker\n\n## 使用背景\n\n**项目部署的问题：**\n\n![image-20210925152744642](/images/image-20210925152744642.png)\n\n**Docker解决依赖的兼容问题：**\n\n![image-20210925152950772](/images/image-20210925152950772.png)\n\n**Docker解决不同系统环境问题：**\n\n![image-20210925153350716](/images/image-20210925153350716.png)\n\n**Docker解决大型项目依赖关系复杂、不同组件依赖的兼容性问题：**\n\n* Docker允许开发中将应用、依赖、函数库、配置一起打包，形成可移植镜像\n\n* Docker应用运行在容器中，使用沙箱机制，相互隔离\n\n**Docker解决开发、测试、生产环境的差异问题：**\n\n* Docker镜像中包含完整运行环境，包括系统函数库，仅依赖系统的Linux内核，因此可以在任意的Linux系统上运行\n\n![image-20210925154120533](/images/image-20210925154120533.png)\n\n## 概念\n\n### Docker与虚拟机\n\n![image-20210925154814518](/images/image-20210925154814518.png)\n\n![image-20210925155029677](/images/image-20210925155029677.png)\n\n### 镜像和容器\n\n![image-20210925155316704](/images/image-20210925155316704.png)\n\n### Docker和DockerHub\n\n![image-20210925155421943](/images/image-20210925155421943.png)\n\n### Docker架构\n\n![image-20210925155643573](/images/image-20210925155643573.png)\n\n![image-20210925155741092](/images/image-20210925155741092.png)\n\n## 快速入门\n\n### 安装Docker\n\n> 企业部署一般都是采用Linux操作系统，而其中又数CentOS发行版占比最多，所以安装Docker到CentOS下\n\n1. 卸载Docker（可选）\n\n   ```shell\n   yum remove docker \\\n                     docker-client \\\n                     docker-client-latest \\\n                     docker-common \\\n                     docker-latest \\\n                     docker-latest-logrotate \\\n                     docker-logrotate \\\n                     docker-selinux \\\n                     docker-engine-selinux \\\n                     docker-engine \\\n                     docker-ce\n   ```\n\n2. 安装yum工具\n\n   ```shell\n   yum install -y yum-utils \\\n              device-mapper-persistent-data \\\n              lvm2 --skip-broken\n   ```\n\n3. 更新本地镜像源\n\n   ```shell\n   # 设置docker镜像源\n   yum-config-manager \\\n       --add-repo \\\n       https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo\n       \n   sed -i 's/download.docker.com/mirrors.aliyun.com\\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo\n   \n   yum makecache fast\n   ```\n\n4. 安装Docker-CE\n\n   ```shell\n   yum install -y docker-ce\n   ```\n\n### 启动Docker\n\n1. **启动前要关闭防火墙**\n\n   ```shell\n   # 关闭\n   systemctl stop firewalld\n   # 禁止开机启动防火墙\n   systemctl disable firewalld\n   # 查看防火墙状态\n   systemctl status firewalld\n   ```\n\n2. 启动Docker\n\n   ```shell\n   systemctl start docker  # 启动docker服务\n   \n   systemctl stop docker  # 停止docker服务\n   systemctl restart docker  # 重启docker服务\n   ```\n\n3. 查看状态可以通过：\n\n   * `systemctl status docker`\n   * `docker -v`\n\n### 配置镜像加速\n\n> 参考阿里云容器镜像服务：https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors\n\n配置`/etc/docker/daemon.json`\n\n```shell\nsudo mkdir -p /etc/docker\n\nsudo tee /etc/docker/daemon.json <<-'EOF'\n{\n  \"registry-mirrors\": [\"https://kcok668y.mirror.aliyuncs.com\"]\n}\nEOF\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```\n\n## Docker基本操作\n\n### 镜像名称\n\n* 镜像名称一般分为两部分组成：[repository]:[tag]\n\n* 如果没有指定[tag]，默认为latest最新版本\n\n<img src=\"/images/image-20210925163641758.png\" alt=\"image-20210925163641758\" style=\"zoom: 33%;\" /> \n\n### 镜像操作命令\n\n![image-20210925163901563](/images/image-20210925163901563.png)\n\n#### 拉取`pull`\n\n案例：从DockerHub拉取一个镜像\n\n1. 在DockerHub搜索镜像名\n\n   ![image-20210925172741128](/images/image-20210925172741128.png)\n\n2. 复制拉取命令\n\n   <img src=\"/images/image-20210925172858950.png\" alt=\"image-20210925172858950\" style=\"zoom: 80%;\" /> \n\n3. 执行拉取命令等待`pull`完成\n\n   ![image-20210925173026244](/images/image-20210925173026244.png)\n\n#### 导出`save`和加载`load`\n\n案例：利用`docker save`将镜像导出磁盘，然后再通过`load`加载回来\n\n1. 利用docker 命令 --help查看`docker save`和`docker load`的语法\n\n2. 使用`docker tag`创建新镜像`mynginx1.0`\n\n3. 使用`docker save`导出到磁盘\n\n   ```shell\n   docker save -o mynginx.tar nginx:latest\n   # 导出成-o后面的mynginx.tar，镜像为nginx的最新版本\n   ```\n\n4. 使用`docker load`导入镜像\n\n   ```shell\n   docker load -i mynginx.tar\n   # 导入的来源为-i后的mynginx.tar\n   ```\n\n   <img src=\"/images/image-20210925183121919.png\" alt=\"image-20210925183121919\" style=\"zoom: 50%;\" /> \n\n### 容器命令\n\n![image-20210925222930569](/images/image-20210925222930569.png)\n\n#### 案例1：创建运行Nginx容器\n\n1. 去DockerHub查看Nginx的容器运行命令\n\n   ![image-20210925224318458](/images/image-20210925224318458.png)\n\n2. 在80端口运行nginx容器映射到80端口上，可以通过`ip:80端口`访问此nginx容器\n\n   `docker run --name 容器名 -p 80:80 -d nginx`\n\n3. 查看访问日志：\n\n   * `docker logs 容器名`\n   * 刷新查看日志：`docker logs -f 容器名`\n   \n4. 清理日志\n\n   * 进入容器目录：`cd /var/lib/docker/containers/容器ID`\n   * 查看大小：`ls -lh`\n   * 清空日志：`cat /dev/null > 容器ID-json.log`\n\n![image-20210925225033757](/images/image-20210925225033757.png)\n\n#### 案例2：进入容器修改内容\n\n要求：进入Nginx容器，修改HTML内容，添加一行文本\n\n![image-20210925225825090](/images/image-20210925225825090.png)\n\n![image-20210925230547503](/images/image-20210925230547503.png)\n\n同理：操作redis容器\n\n1. 创建redis容器：\n\n   ```shell\n   docker run --name myredis -p 80:80 -d redis redis-server --appendonly yes\n   ```\n\n2. 进入容器：\n\n   * 进入bash命令进入然后启动redis-cli\n\n     ```shell\n     [root@master docker]# docker exec -it myredis bash\n     root@d5ae50394a6d:/data# redis-cli\n     ```\n\n   * 直接进入redis-cli命令\n\n     ```shell\n     [root@master docker]# docker exec -it myredis redis-cli\n     ```\n\n3. 现在即可使用redis的命令\n\n#### 查看已部署的容器信息\n\n* 查看正在运行的容器：`docker ps`\n* 查看所有容器，包括没有运行的：`docker ps -a`\n* 查看容器信息并且不省略：`docker ps --no-trunc`\n\n## 数据卷\n\n![image-20210925232700504](/images/image-20210925232700504.png)\n\n### 概念\n\n> 数据卷（volume）是一个虚拟目录，指向宿主机文件系统中的某个目录\n\n<img src=\"/images/image-20210925233238823.png\" alt=\"image-20210925233238823\" style=\"zoom: 50%;\" /> \n\n### 基本语法\n\n![image-20210925233205970](/images/image-20210925233205970.png)\n\n![image-20210925233633952](/images/image-20210925233633952.png)\n\n![image-20210925233706310](/images/image-20210925233706310.png)\n\n### 挂载数据卷\n\n![image-20210925233812608](/images/image-20210925233812608.png)\n\n**案例1：**创建一个nginx容器，修改容器内的html目录内的index.html内容\n\n![image-20210925234939623](/images/image-20210925234939623.png)\n\n**执行`docker run`时利用`-v`指定了volume时若不存在，会自动创建此volume**\n\n![image-20210925235114407](/images/image-20210925235114407.png)\n\n**案例2：**创建并运行一个MySQL容器，将宿主机目录挂载到容器\n\n![image-20210926000524650](/images/image-20210926000524650.png)\n\n1. 创建启动容器\n\n   ```shell\n   docker run \\\n           --name mysql \\\n           -e MYSQL_ROOT_PASSWORD=123 \\\n           -p 3306:3306 \\\n           -v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \\\n           -v /tmp/mysql/logs:/var/log/mysql \\\n           -v /tmp/mysql/data:/var/lib/mysql \\\n           -d mysql:5.7.25\n   ```\n\n2. 启动后`/etc/docker/mysql`子目录会同步mysql的数据卷，并且可以通过3307端口登录root用户，密码123\n\n   <font color=red>注意：自定义mysql端口需要给容器配置文件中指定`port=3307`</font>![image-20211122014300152](images/image-20211122014300152.png)\n\n**修改mysql时区：**\n\n* 进入容器：`docker exec -it 容器名 bash`\n* 查看当前系统时区：`date -R`\n* 修改时区：`cp /usr/share/zoneinfo/PRC /etc/localtime`\n* 退出容器后重启容器：`docker restart 容器名`\n\n**redis容器案例**\n\n```sh\ndocker run  \\\n    -p 6379:6379    \\\n    --name redis_tjj    \\\n    -v /etc/redis/redis.conf:/etc/redis/redis.conf  \\\n    -v /etc/redis/data:/data    \\\n    -d redis:latest \\\n    redis-server /etc/redis/redis.conf\n```\n\n![image-20210926002503970](/images/image-20210926002503970.png)\n\n## 自定义镜像\n\n### 镜像结构\n\n![image-20210926005342122](/images/image-20210926005342122.png)\n\n* 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成\n\n![image-20210926005407454](/images/image-20210926005407454.png)\n\n### Dockerfile\n\nDockerfile就是一个文本文件，其中包含一个个的**指令（Instruction）**，用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer\n\n![image-20210926005710948](/images/image-20210926005710948.png)\n\n**案例：**基于Ubuntu镜像构建一个新镜像，运行一个java项目\n\n![image-20210926011218197](/images/image-20210926011218197.png)\n\n**简化**：基于java:8-alpine镜像，将一个Java项目构建为镜像\n\n<img src=\"/images/image-20210926011547599.png\" alt=\"image-20210926011547599\" style=\"zoom:50%;\" /> \n\n![image-20210926011929772](/images/image-20210926011929772.png)\n\n## DockerCompose\n\n* Docker Compose可以基于Compose文件帮我们快速的部署分布式应用，而无需手动一个个创建和运行容器\n* Compose文件是一个文本文件，通过指令定义集群中的每个容器如何运行\n\n![image-20210926012924275](/images/image-20210926012924275.png)\n\n### 安装Docker Compose\n\n1. 下载安装Docker Compose\n\n   ```shell\n   # 安装\n   curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose\n   ```\n\n   或者通过上传本地的docker-compose到`/user/local/bin`目录\n\n2. 修改文件权限：添加可执行权限\n\n   ```shell\n   chmod +x docker-compose\n   ```\n\n3. Bash自动补全命令\n\n   ```shell\n   # 补全命令\n   curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose\n   ```\n\n   * 可能需要修改hosts：`echo \"199.232.68.133 raw.githubusercontent.com\" >> /etc/hosts`\n\n![image-20210926014150151](/images/image-20210926014150151.png)\n\n### 案例：部署微服务集群\n\n要求：将cloud-demo的order、user微服务项目利用Docker Compose部署\n\n![image-20210926125314530](/images/image-20210926125314530.png)\n\n## Docker镜像仓库\n\n### 常见镜像仓库服务\n\n镜像仓库（Docker Registry）有公共的和私有的两种形式：\n\n* **公共仓库：**例如Docker官方的DockerHub，国内也有一些云服务商提供类似于DockerHub的公开服务，例如网易云镜像服务、DaoCloud镜像服务、阿里云镜像服务等\n* **私有仓库：**用户可以在本地搭建私有Dcoker Registry。\n\n### 搭建镜像仓库\n\n#### 简化版镜像仓库\n\n> Docker官方的Docker Registry是一个基础版本的Docker镜像仓库，具备仓库管理的完整功能，但是没有图形化界面。搭建方式比较简单，命令如下：\n\n```shell\ndocker run -d \\\n    --restart=always \\\n    --name registry\t\\\n    -p 5000:5000 \\\n    -v registry-data:/var/lib/registry \\\n    registry\n```\n\n命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录，这是私有镜像库存放数据的目录。\n\n访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像\n\n#### 带有图形化界面版本\n\n使用DockerCompose部署带有图形界面的DockerRegistry，命令如下：\n\n```shell\nversion: '3.0'\nservices:\n  registry:\n    image: registry\n    volumes:\n      - ./registry-data:/var/lib/registry\n  ui:\n    image: joxit/docker-registry-ui:static\n    ports:\n      - 8080:80\n    environment:\n      - REGISTRY_TITLE=这是私有仓库名字\n      - REGISTRY_URL=http://registry:5000\n    depends_on:\n      - registry\n```\n\n由于私服采用的是http协议，默认不被Docker信任，所以需要做一个配置：\n\n```shell\n# 打开要修改的文件\nvi /etc/docker/daemon.json\n# 添加内容：\n\"insecure-registries\":[\"http://192.168.150.101:8080\"]\n# 重加载\nsystemctl daemon-reload\n# 重启docker\nsystemctl restart docker\n```\n\n部署成功后访问：`ip:8080`即可访问`Docker Registry UI`\n\n**在私有镜像仓库推送或拉取镜像：**\n\n![image-20210929005648444](/images/image-20210929005648444.png)\n\n![image-20210929005733712](/images/image-20210929005733712.png)\n\n# Kubernetes\n\n## 介绍\n\n### 应用程序部署方式的演变\n\n* 物理机部署：应用程序直接部署在物理机上\n  * 缺点：很难合理分配资源，应用程序之间容易产生影响\n* 虚拟机部署：在一台物理机上运行多个虚拟机，每个虚拟机都是独立的一个环境\n  * 应用程序之间独立，但浪费了系统资源\n* 容器化部署：与虚拟机类似，但是共享了操作系统\n  * 优点：\n    * 可以保证每个容器拥有自己的文件系统、CPU、内存、进程空间等\n    * 运行应用程序所需要的资源都被容器包装，并和底层基础架构解耦\n    * 容器化的应用程序可以跨云服务商、跨Linux操作系统版本进行部署\n\n**容器化部署方式的问题：**\n\n* 一个容器故障停机，怎么样让另一个容器立刻启动去替补停机的容器\n* 当并发访问量变大的时候，怎么做到横向扩展容器数量\n\n这些容器管理的问题统称为**容器编排**问题，为了解决这些容器编排问题，产生了一些容器编排的软件：\n\n* **Swarm**：Docker自己的容器编排根据\n* **Mesos**：Apache的一个资源统一管控的工具，需要和Marathon结合使用\n* **Kubernetes**：Google开源的容器编排工具\n\n### Kubernetes简介\n\nKubernetes是一个全新的基于容器技术的分布式架构领先方案，是谷歌严格保密十几年的秘密武器——Borg系统的一个开源版本，于2014年9月发布第一个版本，2015年7月发布第一个正式版本\n\nKubernetes的本质是**一组服务器集群**，它可以在集群的每个节点上运行特定的程序，来对节点中的容器进行管理。它的目的是实现资源管理的自动化，主要提供了以下功能：\n\n* **自我修复**：一旦某个容器崩溃，能够在1秒左右迅速启动新的容器\n* **弹性伸缩**：可以根据需要，自动对集群中正在运行的容器数量进行调整\n* **服务发现**：服务可以通过自动发现的形式找到它所依赖的服务\n* **负载均衡**：如果一个服务启动了多个容器，能够自动实现请求的负载均衡\n* **版本回退**：如果发现新发布的程序版本有问题，可以立即回退到原来的版本\n* **存储编排**：可以根据容器自身的需求自动创建存储卷\n\n### Kubernetes组件\n\n一个Kubernetes集群主要是由**控制节点(`master`)**、**工作节点(`node`)**构成。每个节点上都会安装不同的组件。\n\n* **master：集群的控制平面，负责集群的决策**\n  * **ApiServer**：资源操作的唯一入口，接收用户输入的命令，提供认证、授权、API注册和发现等机制\n  * **Scheduler**：负责集群资源调度，按照预订的调度策略将Pod调度到相应的node节点上\n  * **ControllerManager**：负责维护集群的状态，比如程序部署安排、故障检测、自动扩展、滚动更新等\n  * **Etcd**：负责存储集群中各种资源对象的信息\n* **node：集群的数据平面，负责为容器提供运行环境**\n  * **Kubelet**：负责维护容器的生命周期，即通过控制docker来创建、更新、销毁容器\n  * **KubeProxy**：负责提供集群内部的服务发现和负载均衡\n  * **Docker**：负责节点上容器的各种操作\n\n![image-20211210234316472](images/image-20211210234316472.png)","tags":["运维","服务器","构建工具"],"categories":["运维"]},{"title":"JavaWeb","url":"/2021/08/03/JavaWeb/","content":"\n# JavaWeb服务器端学习\n\n## Web相关概念回顾\n\n### 软件架构\n\n* C/S：客户端/服务器端\n* B/S：浏览器/服务器端\n\n### 资源分类\n\n* 静态资源，如html，css，JavaScript\n\n* 动态资源，如servlet/jsp，asp，php\n\n  <img src=\"/images/image-20210731161144760.png\" alt=\"image-20210731161144760\" style=\"zoom: 50%;\" />\n\n### 网络通信三要素\n\n* IP：电子设备（计算机）在网络中的唯一标识\n* 端口：应用程序在计算机上的唯一标识（0~65535）\n* 传输协议：规定了数据传输的规则\n    * TCP：三次握手的安全协议\n    * UDP：面向无连接的不安全协议\n\n## Tomcat服务器软件\n\n* **服务器**：安装了服务器软件的计算机\n\n* **服务器软件**：用于接受用户的请求、处理请求、作出响应\n\n* **Web服务器软件（Web容器）**：在Web服务器软件中，可以部署Web项目，让用户可以通过服务器来访问这些项目\n\n* 常见的Java相关的Web服务器软件：\n\n    * WebLogic：Oracle公司的大型JavaEE服务器，收费的，支持所有的JavaEE规范\n\n    * WebSphere：IBM公司的大型JavaEE服务器，收费的，支持所有的JavaEE规范\n\n    * JBoss：JBoss公司的大型JavaEE服务器，收费的，支持所有的JavaEE规范\n\n    * Tomcat：Apache基金组织的中小型JavaEE服务器，仅支持少量JavaEE规范，开源、免费\n\n      *JavaEE：Java语言在企业级开发中使用的技术规范的总和，一共规定了13项大的规范*\n\n### Tomcat\n\n#### 下载：\n\n* 访问`https://tomcat.apache.org`下载对应的版本\n\n#### 安装：\n\n* 直接解压压缩包即可，安装目录不要有中文和空格\n* 目录结构：<img src=\"/images/image-20210731163425498.png\" alt=\"image-20210731163425498\" style=\"zoom:67%;\" />\n\n#### 卸载：\n\n* 直接删除安装目录即可\n\n#### 启动：\n\n* 在安装目录下的`bin`目录执行`startup.bat`批处理文件\n    * 如果出现控制台乱码，修改安装目录下的`conf`目录下的`logging.properties`文件的`encoding`为`GBK`\n    * 如果控制台一闪而过：配置`JAVA_HOME`和`JRE_HOME`的环境变量\n    * 启动报错：在安装目录下的`logs`目录查看日志文件\n        * 端口占用错误：\n            1. 找到占用端口的程序，杀死该进程：\n                * cmd输入`netstat -ano`查看占用8080端口的进程PID\n                * 任务管理器杀死该PID的进程\n            2. 修改自身的端口号：\n                * 编辑安装目录下的`conf`目录下的`server.xml`\n                * 修改`<Connerctor port=端口号/>`中相应的端口号，使得不冲突\n        * HTTP协议默认端口号默认为80，访问时可以省略端口号\n\n#### 访问：\n\n* 本机访问：浏览器访问`http://127.0.0.1:8080`或`http://localhost:8080`\n* 访问其他：`http://服务器ip:8080`\n\n#### 关闭：\n\n* 正常关闭：执行安装目录下的`bin`目录的`shutdown.bat`批处理文件，也可以在窗口直接`Ctrl+C`\n* 强制关闭：直接关掉启动`startup.bat`的窗口\n\n#### 配置：\n\n##### 部署项目的方式：\n\n###### 1.直接将项目放到安装目录下的`webapps`目录下即可\n\n1. 在`webapps`目录下放置项目文件夹\n2. 将`war`包放置到`webapps`目录下，这种方式在Tomcat服务器启动时会自动解包成项目文件夹\n\n###### 2.配置`conf`目录下的`server.xml`文件，重启服务器生效\n\n​ 在`<Host></Host>`标签体中配置`<Context docBase=\"项目路径\" path=\"/虚拟访问路径\"/>`\n\n###### 3.在`conf/Catalina/localhost`目录下创建任意的xml文件\n\n​ 在创建的xml文件中配置`<Context docBase=\"项目路径\" />`，此时的虚拟访问路径就是创建的xml文件名称\n\n##### 静态项目和动态项目\n\n* 目录结构：\n\n    * Java动态项目的目录结构：\n\n      ——`项目根目录`\n\n      ————`WEB-INF目录`\n\n      ——————`web.xml` ：web项目的核心配置文件\n\n      ——————`classes目录`：放置字节码文件的目录\n\n      ——————`lib目录`：放置依赖的jar包\n\n#### Tomcat与IDEA集成\n\n##### IDEA集成Tomcat\n\n* 在IDEA的导航栏选择![image-20210731181519317](/images/image-20210731181519317.png)\n* 在`运行`二级菜单选择![image-20210731181608049](/images/image-20210731181608049.png)\n* 在`编辑配置`弹出的窗口中选择`+`号添加新配置![image-20210731181655875](/images/image-20210731181655875.png)\n* 配置新服务器的来源![image-20210731181756724](/images/image-20210731181756724.png)\n* 保存即可完成配置\n\n##### IDEA启动Tomcat\n\n* 创建JavaEE项目或模块，勾选Web Application![image-20210731182403416](/images/image-20210731182403416.png)\n  并勾选`创建web.xml`![image-20210731182428481](/images/image-20210731182428481.png)\n\n* 启动创建的JavaWeb项目：点击导航栏的![image-20210731182541089](/images/image-20210731182541089.png)\n  或右上角的![image-20210731182624541](/images/image-20210731182624541.png)\n\n* 浏览器访问ip:端口以及Tomcat部署的war包，访问路径为Tomcat配置窗口的`部署`菜单下\n\n  **注意**：\n\n    1. **新版IDEA**下的`部署`菜单的最下面，找到`Application Context`即为**访问路径\n       **![image-20210731183046584](/images/image-20210731183046584.png)\n    2. **新版IDEA**的JavaEE项目创建稍有不同，如果想要原来的**JavaEE项目创建界面**，修改IDEA的软件注册表：\n        * `Ctrl Shift Alt /`：打开`维护界面`\n          ，选择`注册表`：<img src=\"/images/image-20210731183455885.png\" alt=\"image-20210731183455885\" style=\"zoom:80%;\" />\n        * 搜索并勾选`javaee.legacy.project.wizard`：![image-20210731183551065](/images/image-20210731183551065.png)\n\n##### IDEA的Tomcat项目配置\n\n1. IDEA会为每一个Tomcat部署的项目单独建立一份配置文件：在启动项目时打印在Server控制台\n\n   ![image-20210801015440946](/images/image-20210801015440946.png)\n\n   其中`CATALINA_BASE`后面的路径就是项目配置文件的路径\n\n2. IDEA项目的工作空间和Tomcat部署的web项目不是同一个概念\n\n    *\n   Tomcat真正访问的是Tomcat部署的web项目，对应着IDEA项目工作空间的![image-20210801020007942](/images/image-20210801020007942.png)\n   目录下的所有资源\n    * WEB-INF目录下的资源不能被浏览器直接访问\n3. 断点调试：通过![image-20210801020356707](/images/image-20210801020356707.png)以debug方式启动项目\n\n## Servlet入门\n\n`Servlet = Server + Applet`\n\n* 概念：运行在服务器端的小程序，Servlet是一个接口，定义了Java类被浏览器访问到(Tomcat识别)的规则\n\n### 快速入门：\n\n1. 创建JavaEE项目\n\n2. 定义一个类，实现Servlet接口![image-20210731211508413](/images/image-20210731211508413.png)\n\n3. 实现接口中的抽象方法\n\n4. **配置Servlet**：在`web.xml`文件中配置Servlet\n\n   <img src=\"/images/image-20210731210732207.png\" alt=\"image-20210731210732207\"  />\n\n### Servlet执行过程：\n\n1. 当服务器接收到浏览器的请求后，会解析请求的URL路径，获取访问的Servlet的资源路径\n2. 查找web.xml文件，是否有对应的`<url-pattern>`标签体内容\n3. 如果有，就通过`<servlet-name>`标签找到对应的`<servlet-class>`全类名\n4. Tomcat会将字节码文件加载进内存，并且创建其对象\n5. 调用其方法\n\n### Servlet生命周期\n\n* 被创建：执行`init()`方法，只执行一次\n\n    * Servlet什么时候被创建？\n\n        * 默认情况下，第一次被访问时，Servlet被创建\n\n        * 可以配置指定Servlet的创建时机：\n\n          在web.xml配置文件中，对每一个`<servlet>`标签配置`<load-on-startup>`标签的值\n\n            1. 此标签值默认为-1，指定为负数会在第一次被访问时创建，指定为0或正数会在启动服务器时创建\n            2. `<load-on-startup>`较小值的 servlet 在以较大值的 servlet 之前加载\n\n    * 由于Servlet的`init()`方法只执行一次，说明一个Servlet在内存中只存在一个对象，Servlet是单例的，所以多个用户同时访问时，可能会存在安全问题，所以尽量不要再Servlet中定义成员变量\n\n* 提供服务：执行`service()`方法，会执行多次\n\n    * 每次访问Servlet时都会调用一次`service()`方法\n\n* 被销毁：执行`destroy()`方法，只执行一次\n\n    * 在服务器关闭之前执行\n\n  *对于每一个Servlet都会执行上述方法（初始化、销毁）*\n\n### Servlet3.0注解开发\n\n* 好处：简化了`web.xml`的配置\n\n* 步骤：\n\n    * 创建JavaEE项目，选择Servlet的版本为3.0以上，可以不勾选`web.xml`了\n\n      注意：选择的`JavaEE`版本为`JavaEE6`开始才支持`3.0`，`JavaEE7`为`3.1`，`JavaEE8`为`4.0`\n\n    * 定义一个类，实现Servlet接口\n\n    * 重写抽象方法\n\n    * 在类上使用`@WebServlet`注解，指定映射的虚拟访问路径，例如：\n\n    * 通过`urlPatterns`指定：![image-20210801014531501](/images/image-20210801014531501.png)\n\n    * 或直接给`value`赋值（可省略）：![image-20210801014651107](/images/image-20210801014651107.png)\n\n### Servlet体系结构\n\n![image-20210801134635415](/images/image-20210801134635415.png)\n\n* `GenericServlet`对`Servlet`接口中其他的方法做了默认空实现，只将`service()`方法作为抽象方法\n* `HttpServlet`对`HTTP`协议进行了封装，在`service()`方法中对每种请求方式（`doXxx()`）做了分发\n\n### Servlet相关配置\n\n1. `urlPattern`：Servlet的访问路径，这是一个`String[]`数组\n    * 可以为一个Servlet定义多个访问路径\n    * 路径定义规则：\n        1. `/xxx`\n        2. `/xxx/xxx` 多层路径的目录型结构\n        3. `/xxx/*` 匹配/xxx开头，后面内容随意的Servlet\n        4. `/*` 匹配所有可能的虚拟请求路径\n        5. `*.do` 匹配前面任意，后面以.do结尾的Servlet，注意前面没有`/`\n\n## HTTP\n\n* 概念：`Hyper Text Transfer Protocol` 超文本传输协议\n\n    * 定义和客户端和服务器端的通信时发送数据的格式\n    * 特点：\n        1. 基于TCP/IP的高级协议\n        2. 默认端口号：80，例如访问`http://www.baidu.com`相当于`http://ip地址:80`\n        3. 基于请求/响应模型，一次请求对应一次响应\n        4. 无状态的：每次请求之间相互独立，不能通信数据\n    * 历史版本：\n        * 1.0 ：每次请求/响应都会建立新的连接\n        * 1.1：可以复用连接\n\n### 请求消息数据格式\n\n![image-20210801173507355](/images/image-20210801173507355.png)\n\n* 请求行\n\n    1. 格式：`请求方式 请求url 请求协议/版本`    ![image-20210801170942567](/images/image-20210801170942567.png)\n\n    2. 请求方式：HTTP协议有7种请求方式，**常用的有2种**：\n        * GET：\n            1. 请求参数在请求行中，在url后    ![image-20210801172350702](/images/image-20210801172350702.png)\n            2. 请求的url长度有限制\n            3. 不太安全\n        * POST：\n            1. 请求参数在请求体中\n            2. 请求的url长度没有限制\n            3. 相对安全\n\n* 请求头：包含若干个属性，格式为 `请求头属性名：请求头属性值`\n\n  常见的请求头:\n\n    * `Host`：请求的主机名\n    * `User-Agent`：浏览器告诉服务器，访问服务器的浏览器的版本信息\n    * `Accept`：可接收的格式\n    * `Accept-Language`：可接收的语言\n    * `Referer`：告诉服务器这个请求是哪个URL过来的，可以用于防盗链和统计工作\n    * `Connection`：连接方式\n\n* 请求空行：最后一个请求头之后是一个空行，包括回车符和换行符，分隔POST请求头和体\n\n* 请求体（正文）：在POST方法中含有，封装了POST请求消息的请求参数\n\n### Request\n\n*Request对象和Response对象都是由服务器创建的，我们来使用它们，Request对象用于获取请求消息，Response对象用于设置响应消息*\n\n#### Request对象继承体系\n\n![image-20210801184932632](/images/image-20210801184932632.png)\n\n#### Request功能\n\n##### Request对象获取请求消息\n\n1. 获取**请求行**数据\n    * `public String getMethod()`获取**请求方式**，例如 GET、POST 或 PUT。\n    * `public String getContextPath()`<font color=orange>获取**请求上下文**(虚拟目录)</font>，该路径以`/`\n      字符开头但不以`/`字符结束，对于默认上下文（即`/`根），此方法返回`\"\"`空字符串\n    * `public String getServletPath()`获取**请求调用Servlet的URL部分**，此路径以`/`字符开头，包括 servlet 名称或到 servlet\n      的路径\n    * `public String queryString()`获取**以GET请求查询的参数**，如果没有参数则返回`null`\n    * `public String getRequestURI()`<font color=orange>获取**请求的URL的一部分**</font>，相当于拼接ContextPath和ServletPath\n        * `URI`：统一资源标识符\n    * `public String getRequestURL()`获取**请求的URL**，返回的 URL 包含一个协议、服务器名称、端口号、服务器路径，但是*\n      *不包含`queryString()`的内容**\n        * `URL`：统一资源定位符\n    * `public String getProtocol()`继承于`ServletRequest`接口，返回请求使用的协议的名称和版本\n    * `public String getRemoteAddr()`继承于`ServletRequest`接口，获取客户机的IP地址\n\n2. 获取**请求头**数据\n\n    * `String getHeader(String name)`返回指定的请求头的值，如果该请求不包含指定`name`的头，则此方法返回 `null`\n      。如果有多个具有相同名称的头，则此方法返回请求中的第一个头。**头名称不区分大小写**\n    * `Enumeration<E> getHearderNames() `返回此请求包含的所有头名称的枚举。如果该请求没有头，则此方法返回一个空枚举\n\n3. 获取**请求体**数据\n\n    * 只有POST方式才有请求体，在请求体中封装了POST请求的请求参数\n\n    * 步骤：\n\n        1. 获取流对象\n\n           `BufferedReader getReader()`<font color=orange>获取**字符输入流**</font>，只能操作字符数据，适合操作文本\n\n           `ServletInputStream getInputStream()`获取字节输入流，可操作所有数据，适合文件上传场景\n\n        2. 从流对象中拿数据\n\n##### 其他功能\n\n1. **获取请求参数的通用方式**：不论GET还是POST方式都可以使用下列的方法\n\n    * `String getParameter(String name)`根据参数名`name`获取参数值，如果该参数不存在则返回`null`\n    * `String[] getParameterValues(String name)`根据参数名`name`\n      获取参数值的数组，多用于复选框，如果该参数不存在则返回`null`\n    * `Enumeration<String> getParameterNames()`获取所有请求的参数名称，没有参数则返回空枚举\n    * `Map<String,String[]> getParameterMap()`获取所有请求的参数的集合\n\n   **<font color=red>中文乱码问题：</font>**\n\n   :white_check_mark:GET方式在Tomcat 8下的乱码问题被解决了\n\n   :x:POST方式会乱码，**解决方式**：`request.setCharacterEncoding(\"utf-8\");`设置字符编码方式\n\n2. **请求转发**：一种在服务器内部的资源跳转方式\n\n   ![image-20210801232407311](/images/image-20210801232407311.png)\n\n    * 步骤：\n        1. 通过request对象获取*请求转发器*对象：`RequestDispatcher getRequestDispatcher(String s)`\n        2. 使用*请求转发器*对象进行转发：`void forward(ServletRequest var1, ServletResponse var2)`\n    * 特点：\n        1. 转发后浏览器地址栏路径没有发生变化\n        2. 只能转发到**当前服务器的内部资源**\n        3. 转发是一次请求\n\n3. **共享数据**\n\n    * 域对象：一个有作用范围的对象，可以在范围内共享数据\n    * request域：代表一次请求的范围，一般用于请求转发的多个资源中共享数据\n    * 方法：继承于`ServletRequest`接口\n        1. `void setAttribute(String s,Object o)`：存储数据\n        2. `Object getAttribute(String s)`：通过键获取值\n        3. `void removeAttribute(String s)`：通过键移除键值对\n\n4. **获取ServletContext**\n\n   ![image-20210801235001351](/images/image-20210801235001351.png)\n\n    * `ServletContext getServletContext()`：获取ServletContext对象\n\n#### 案例\n\n* 需求：\n    1. 编写login.html登录页面。需要具有username和password两个输入框\n\n    2. 使用Druid数据库连接池技术，操作mysql的day14数据库中的user表\n\n       **注意：使用这些框架时，要把jar包放到WEB-INF目录下才能被打包进项目！**\n\n    3. 使用jdbcTemplate技术封装JDBC\n\n    4. 登录成功跳转到SuccessServlet展示：登录成功！[用户名]，欢迎您\n\n       **注意：页面乱码要设置request的`setCharacterEncoding()`和response的`setContentType()`**\n\n    5. 登录失败跳转到FailServlet展示：登录失败，用户名或密码错误\n\n#### BeanUtils\n\n* 用于封装`JavaBean`\n\n    * `JavaBean`：标准的Java类\n        * `标准的Java类`：\n            1. 类必须被`public`修饰\n            2. 必须提供空参构造器\n            3. 成员变量必须用`private`修饰\n            4. 提供公共`setter`和`getter`方法\n    * 功能：封装数据\n\n* 概念：\n\n    * 成员变量\n\n    * 属性：`setter`和`getter`方法截取替换后的产物\n\n      例如：对于方法`getUsername()`，截取之后：`Username`，首字母替换为小写：`username`\n\n    * 方法：\n\n        1. `void setProperty(Object bean,String name,Object value)`\n\n           此方法为`bean`的成员变量**属性**：`name`赋值为`value`\n\n        2. `String getProperty(Object bean,String name)`\n\n           此方法返回`bean`的成员变量**属性**：`name`的值，总会返回`String`字符串形式\n\n        3. `void populate(Object bean,Map properties)`：根据`properties`的属性封装到`bean`里\n\n           建议配合`request.getParameterMap()`返回所有的请求参数的`Map`键值对集合使用\n\n### 响应消息数据格式\n\n![image-20210803154816147](/images/image-20210803154816147.png)\n\n* 响应行\n\n    1. 格式：`协议/版本 响应状态码 状态码描述`\n\n    2. 响应状态码：服务器告诉浏览器本次请求和响应的状态\n\n       分类：状态码都是3位数字\n\n        * `1xx`：服务器接收客户端消息但没有接收完成，等待一段时间后发送`1xx`状态码。这一类型的状态码，代表请求已被接受，需要继续处理。\n\n        * `2xx`：请求已成功，出现此状态码是表示正常状态。例如：`200 OK`\n\n        * `3xx`：重定向，后续的请求地址（重定向目标）在本次响应的 `Location`域中指明。\n\n          例如：`302：`：请求的资源临时从不同的 URI响应请求。由于这样的重定向是临时的，客户端应当继续向原有地址发送以后的请求。\n\n          ​            `304`：客户端发送了一个带条件的 GET 请求且该请求已被允许，而文档的内容(\n          自上次访问以来或根据请求的条件)并没有改变则返回此状态码。响应禁止包含消息体，因此以消息头后空行结尾。\n\n        * `4xx`：客户端请求错误\n\n          例如：`404`：代表请求的路径在服务器上没有对应的资源`Not Found`\n\n          ​            `405`：请求方式没有对应的*方法*（GET、POST等）\n\n        * `5xx`：服务器错误，例如`500`：服务器内部错误，一般是服务器后台源代码错误\n\n* 响应头\n\n  常见的响应头：\n\n    * `Content-Type`：服务器告诉客户端本次响应体的数据格式以及编码格式。\n\n      浏览器会根据`Content-Type`自动适应编码格式\n\n    * `Content-disposition`：服务器告诉客户端以什么格式打开响应体数据\n\n        * `in-line`：在浏览器内打开，默认值\n        * `attachment`：以附件形式打开响应体，需要指定`filename=xxx`，常用于文件下载\n\n* 响应空行：最后一个响应头之后是一个空行，包括回车符和换行符，分隔响应头和体\n\n* 响应体：服务器传送给客户端的数据\n\n### Response\n\n#### Response对象继承体系\n\n![image-20210803163341005](/images/image-20210803163341005.png)\n\n#### Response功能\n\n##### Response对象设置响应消息\n\n1. 设置响应行\n\n   格式：`HTTP/1.1 200 OK`\n\n    * `void setStatus(int sc)`设置此响应的**状态码**。此方法用于设置没有错误时的返回状态代码（例如状态代码 SC_OK 或\n      SC_MOVED_TEMPORARILY）。如果有错误，并且调用者希望调用 Web 应用程序中定义的错误页面，则应改用  `sendError` 方法。\n\n2. 设置响应头：\n\n    * `void setHeader(String name, String value)`设置**响应头**，如果已经设置了头，则新值将重写以前的值。`containsHeader`\n      方法可用于测试在设置其值之前头是否存在。\n\n3. 设置响应体\n\n   使用步骤：\n\n    1. 获取输出流\n        * `PrintWriter getWriter()`获取**字符**输出流\n        * `ServletOutputStream getOutputStream()`获取**字节**输出流\n    2. 使用输出流将数据输出到客户端浏览器\n\n#### 案例\n\n##### 完成重定向\n\n![image-20210803164915947](/images/image-20210803164915947.png)\n\n###### 步骤：\n\n在设置重定向时，往往分为两个步骤：\n\n1. 利用`setStatus(302)`告知浏览器需要重定向\n2. 利用`setHeader(\"location\",\"重定向到哪个虚拟访问路径\")`设置`location`域\n\n可以把上述两部简化为一步，调用`void sendRedirect(String s)`方法指定<font color=red>重定向</font>的虚拟访问路径`s`即可\n\n<font color=GoldEnrod>**转发的特点：**</font>\n\n1. 转发后浏览器**地址栏路径没有发生变化**\n2. 只能转发到**当前服务器的内部资源**\n3. 转发是**一次请求**\n4. `forward()`可以使用request域对象共享数据\n\n###### <font color=Orchid>**重定向的特点**：</font>\n\n1. 重定向后浏览器的**地址栏路径发生变化**\n2. 重定向可以访问**其他站点（服务器）的资源**\n3. 重定向是**两次请求**，\n4. `sendRedirect()`不能使用request域对象共享数据\n\n###### 路径写法：\n\n分类：\n\n1. 相对路径：通过相对路径不可以确定唯一的资源，不以`/`开头\n\n   规则：找到当前的资源和目标资源之间的相对位置关系\n\n    * `./`：当前目录，不写也默认\n    * `../`：上一级目录\n\n2. 绝对路径：通过绝对路径可以确定唯一的资源，以`/`开头\n\n   例如：`http://localhost/day15/responseDemo2`，可以简写为`/day15/responseDemo2`\n\n   规则：判断定义的路径是给谁用的（客户端/服务器）？判断请求是从哪里来的\n\n    * 给客户端浏览器使用：需要加虚拟目录（项目的访问路径），建议使用`getContextPath()`来动态获取\n    * 给服务器使用：不需要加虚拟目录\n\n##### 服务器输出字符数据到浏览器\n\n###### 步骤：\n\n1. 获取字符输出流：`PrintWriter getWriter()`，调用`ServletResponse`的此方法返回字符输出流对象，\n\n   但获取到的流默认编码是`ISO-8859-1`（1个字节编码），如果要输出中文（2到3个字节）需要解决<font color=DarkOrange>\n   乱码问题</font>\n\n2. 输出数据\n\n###### 注意：<font color=DarkOrange>乱码问题</font>\n\n由于浏览器默认解码方式与平台有关，比如`windows`下默认`GBK`，所以需要在<font color=Crimson>\n获取流之前指定`ContentType`</font>\n\n1. `response.setHeader(\"content-type\",\"text/html;charset=utf-8\")`，这种方式太麻烦\n2. `response.setContentType(\"text/html;charset=utf-8\")`，这个方法直接设置`ContentType`\n\n##### 服务器输出字节数据到浏览器\n\n###### 步骤\n\n1. 获取字节输出流：`ServletOutputStream getOutputStream()`，调用此方法返回字节输出流对象\n2. 输出数据\n\n###### 注意\n\n1. 输出字节数据时，如果使用`byte[] getBytes()`方法，则按照平台的默认字符集（`windows`下为`GBK`）编码\n2. 需要按照指定的编码获得字节数组，则利用重载形式`byte[] getBytes(String charsetName)`设置编码集\n\n##### 验证码案例——Response\n\n###### 产生验证码图像\n\n1. 利用`BufferedImage`类的构造方法`BufferedImage(int width,int height,int imageType)`产生一个指定`width`、`height`\n   和图像类型`imageType`的初始图像，`imageType`可以为`BufferedImage.TYPE_INT_RGB`\n\n2. 获取此`BufferedImage`图像的`Graphics`画笔对象，用于对图像进行绘制\n\n3. 绘制：\n\n    * 利用`setColor(Color c)`设置画笔的渲染颜色\n    * 利用`fillRect(int x,int y,int width,int height)`填充矩形从`(x,y)`到`(x+width,y+height)`\n    * 利用`drawRect(int x,int y,int width,int height)`绘制边框，注意边框有`1px`的宽度\n    * 利用` drawLine(int x1,int y1,int x2,int y2)`绘制线条，线条从`(x1,y1)`到`(x2,y2)`\n\n4. 产生随机验证码字符\n\n    * 利用`drawString(String str,int x,int y)`绘制文本，`str`将会被绘制到`(x,y)`位置\n    * 文本可以由随机数`Random`类产生，再由`String valueOf(Object obj)`包装为`String`对象\n\n5. 将图片输出到页面上\n\n    * `ImageIO`的静态方法`write(RenderedImage image,String formatName,OutputStream output)`可以把实现了`RenderedImage`\n      接口的指定`image`对象以`formatName`格式输出到`output`输出流\n\n      在本案例中把上面绘制好的`image`以`jpg`格式输出到`ServletResponse`对象的字节输出流\n\n      ```java\n      ImageIO.write(image, \"jpg\", response.getOutputStream());\n      ```\n\n### ServletContext\n\n* 概念：代表整个WEB应用，可以和程序的容器（服务器）来通信\n\n  ![image-20210801235001351](/images/image-20210801235001351.png)\n\n* 获取：\n\n    1. 通过`ServletRequest`对象获取：`ServletContext getServletContext()`\n    2. 通过`HttpServlet`对象获取：`this.getServletContext()`\n\n* 功能：\n\n    1. 获取MIME类型：`String getMimeType(String s)`返回文件名为`s`的MIME类型\n\n        * MIME：在互联网通信过程中定义的一种文件数据类型\n\n          格式： `大类型/小类型`，例如`text/html`或 `image/jpeg`\n\n    2. 域对象：共享数据\n\n        * 方法\n            1. `void setAttribute(String s,Object o)`设置域对象`s`的值为`o`\n            2. `Object getAttribute(String s)`返回域对象`s`的值为`Object`类型\n            3. `void removeAttribute(String s)`删除域对象中指定的`s`对应的键值对\n        * 作用范围：所有用户所有请求的数据，生命周期很长（伴随服务器）\n\n    3. 获取文件的真实（服务器）路径\n\n        * 方法：`String getRealPath(String s)`返回`s`资源的真实路径\n\n          <font color = red>**注意：真实路径：**</font>\n\n            1. 对于项目<font color = Crimson>\n               web目录下的文件</font>![image-20210804031015234](/images/image-20210804031015234.png)，均被打包到部署的真实根路径下\n\n               **真实根路径：**`Tomcat`服务器此项目配置文件`conf/Catalina/localhost/上下文名称.xml`里\n\n               可以在Tomcat启动时控制台打印的`Using CATALINA_BASE:此项目配置文件路径`找到\n\n               在`xml`配置文件里的`<Context />`标签里的`docBase=\"项目路径\"`即为项目真实路径\n\n            2. 对于项目<font color = Crimson>web目录下的`WEB-INF`文件夹</font>，均被打包到部署的真实根路径下的`WEB-INF`\n               文件夹\n\n               `WEB-INF`文件夹还包含了编译之后的`class`字节码文件（存放于`classes`文件夹）\n\n            3. 对于项目<font color = Crimson>`src`目录下的文件</font>，均被打包到部署的真实根路径下的`WEB-INF`\n               下的`classes`文件夹\n\n               相比之下`ClassLoader`类加载器对象的`getResource(String name)`方法只能获取`src`的文件\n\n#### 文件下载案例\n\n* 需求：\n\n    1. 页面显示超链接\n    2. 点击超链接后弹出下载提示框\n    3. 完成图片文件下载\n\n* 分析：\n\n    * 超链接指向的资源如果能被浏览器解析，则在浏览器中展示（例如图片），如果不能解析，则会弹出下载提示框（例如视频）\n\n    * 任何资源都需要弹出下载提示框\n\n    * 使用**响应头**可以指定资源的打开方式：\n\n        * `Content-disposition`：服务器告诉客户端以什么格式打开响应体数据\n\n            1. `in-line`：在浏览器内打开，默认值\n\n            2. `attachment`：以**附件形式打开**响应体，需要指定`filename=xxx`，常用于文件下载\n\n* 步骤：\n\n    1. 定义页面，编辑超链接`href`指向`Servlet`，可以利用**请求参数携带需要下载的文件名**\n\n       ![image-20210804130306959](/images/image-20210804130306959.png)\n\n    2. 定义Servlet\n\n        * 获取文件名称：位于请求参数\n        * 使用字节输入流加载文件到内存\n            * 加载文件进内存需要获取到文件的**真实路径**\n        * 指定response的响应头：`Content-disposition`为以附件形式打开：`attachment`\n        * 将数据输出到`response`的输出流\n\n##### 中文文件名问题\n\n* 解决思路：\n    1. 获取客户端使用的浏览器版本信息\n    2. 根据不同的版本信息，设置不同的`filename`编码方式（利用编码工具类）\n\n### 会话技术\n\n* 会话：一次会话中包含多次请求和响应\n    * 一次会话：浏览器第一次给服务器资源发送请求，会话**建立**，直到有一方断开为止，会话**结束**\n* 功能：在**一次会话的范围内的多次请求间<font color=OrangeRed>共享数据</font>**\n* 方式：\n    1. 客户端会话技术：Cookie\n    2. 服务器端会话技术：Session\n\n#### Cookie\n\n##### 概念\n\n* 客户端会话技术，将数据保存到客户端\n\n##### 快速入门\n\n* 使用步骤：\n\n    1. 创建Cookie对象，绑定数据\n        * `Cookie(String name, String value)`，构造一个带指定名称和值的`Cookie`对象\n    2. 发送Cookie对象，随着`HttpServletResponse`对象\n        * `void addCookie(Cookie cookie) `将指定`Cookie`添加到`response`，可多次调用设置多个`Cookie`\n    3. 获取Cookie，拿到数据，通过`HttpServletRequest`对象\n        * `Cookie[] getCookies()`返回包含客户端随此请求一起发送的所有 `Cookie` 对象的数组。如果没有发送任何`Cookie`\n          ，则此方法返回  `null`\n\n* 实现原理：\n\n    * 基于响应头：`Set-Cookie`和请求头`Cookie`实现\n\n* Cookie细节：\n\n    1. 一次发送多个Cookie？\n\n       **可以**创建多个Cookie并多次调用`addCookie(Cookie cookie)`发送\n\n    2. Cookie在浏览器中保存的时间？\n\n        * 默认情况下，浏览器关闭后，Cookie被销毁\n\n        * 持久化存储Cookie：`setMaxAge(int seconds)`\n\n          参数`seconds`的取值情况：\n\n            1. 正数：将Cookie数据写到硬盘的文件中，持久化存储，存活时间为`seconds`秒，写入即开始计时\n            2. 零：删除Cookie信息\n            3. 负数：默认值，浏览器关闭后Cookie被销毁\n\n    3. Cookie保存中文？\n\n        * `Tomcat8`**之前**，Cookie**不能**直接存储中文数据\n\n          需要将中文数据转码保存，一般采用URL编码（由%XX组成）\n\n        * `Tomcat8`**之后**，Cookie**可以**直接存储中文数据，但是对于特殊字符（例如空格、`:`等）还是不支持，建议使用URL编码\n\n    4. Cookie共享的范围？\n\n        1. 假设在<font color=OrangeRed>同一个Tomcat服务器中部署了多个web项目</font>，能否共享Cookie？\n\n            * 默认情况下，Cookie不能共享\n\n            * 可以通过`setPath(String path)`设置Cookie的共享范围，**默认为当前的虚拟上下文目录**\n\n              例如默认相当于`setPath(\"/虚拟上下文目录\")`\n\n              如果要共享到Tomcat服务器多个项目，可以设置为服务器根目录`setPath(\"/\")`\n\n        2. 假设在<font color=OrangeRed>不同的Tomcat服务器中部署了多个web项目</font>，能否共享Cookie？\n\n            * `setDomain(String path)`如果设置的一级域名相同，那么多个服务器之间Cookie可以共享\n\n              例如`setDomain(\".baidu.com\")`，那么`tieba.baidu.com`和`news.baidu.com`可以共享cookie\n\n##### Cookie的特点\n\n1. Cookie存储数据在客户端浏览器\n2. 浏览器对于单个Cookie的大小有限制(一般4KB)，以及对于同一个域名下的总Cookie数量也有限制(一般20个)\n\n##### Cookie的作用\n\n1. Cookie一般用于存储少量的不太敏感的数据\n2. 在不登陆的情况下完成服务器对客户端的身份识别\n\n##### 案例\n\n* 记住上一次访问的时间\n    * 需求：\n        1. 访问一个Servlet，如果是第一次访问，则提示：您好，欢迎您首次登录\n        2. 如果不是第一次访问，则提示：欢迎回来，上次您登录的时间是：[时间字符串]\n\n    * 分析：\n\n        * 可以采用Cookie来完成\n\n        * 在Servlet中判断是否有一个名为`lastTime`的Cookie\n\n          **有**？说明不是第一次访问\n\n            1. 响应数据：欢迎回来，您上次访问的事件是：[lastTime的时间]\n            2. 写回Cookie：lastTime=[当前时间]\n\n          **没有**？说明是第一次访问\n\n            1. 响应数据：您好，欢迎您首次登录\n            2. 写回Cookie：lastTime=[当前时间]\n\n#### Session\n\n##### 概念\n\n* 服务器端会话技术，在一次会话的多次请求间共享数据，将数据保存在服务器端的对象中：`HttpSession`类\n\n##### 快速入门\n\n获取`HttpSession`对象：`HttpServletRequest`实现类调用`HttpSession getSession()`返回此对象\n\n使用`HttpSession`对象：3个方法：\n\n1. `Object getAttribute(Stirng name)`返回与此会话中的指定`name`绑定在一起的对象，没有则返回`null`\n2. `void setAttribute(String name,Object value)`使用指定`name`将`value`对象绑定到此会话，有则替换\n3. `void removeAttribute(String name)` 从此会话中移除与指定`name`绑定在一起的对象\n\n##### 原理\n\n* Session的实现依赖于Cookie，其实就是一个Cookie请求头/响应头字段：`JSESSIONID`\n\n![image-20210805021115545](/images/image-20210805021115545.png)\n\n##### Session的细节\n\n1. 客户端关闭后，服务器不关闭，两次获取的Session是同一个吗？\n\n    * **默认情况下**不是同一个，客户端关闭后Session就失效了\n    * 可以通过手动指定Cookie的字段：`JSESSIONID`为`session.getId()`然后设置Cookie的存活时间\n\n2. 客户端不关闭，服务器关闭后，两次获取的Session是同一个吗？\n\n    * 不是同一个，但是要确保数据不丢失：\n\n      **Tomcat会自动序列化/反序列化，但IDEA活化会删除work目录导致活化失败，建议部署到Tomcat**\n\n        * session的钝化：在服务器正常关闭之前，把session对象序列化到硬盘上\n        * session的活化：在服务器启动后，将序列化的session文件反序列化到内存，还原`JSESSIONID`\n\n3. Session的失效时间？\n\n    * 服务器关闭\n    * session对象调用`invalidate()`\n    * session的默认失效时间：30分钟，在Tomcat配置文件`web.xml`中配置`<session-config>`的超时时间\n\n##### Session的特点\n\n1. session用于存储一次会话的多次请求的数据，存储在服务器端\n2. session可以存储任意类型、任意大小的数据\n\n#### Session和Cookie的区别：\n\n1. session存储数据在服务器端，而Cookie在客户端\n2. session存储数据没有大小限制，而Cookie有\n3. session数据安全，Cookie相对不安全\n\n#### 验证码案例——Session\n\n* 需求：\n\n    1. 访问带有验证码的登录界面login.jsp\n    2. 用户输入用户名、密码以及验证码\n        * 如果用户名和密码输入有误，则跳转到登录页面，提示：用户名或密码错误\n        * 如果验证码输入有误，则跳转到登录页面，提示：验证码错误\n        * 如果全部输入正确，则跳转到success.jsp，显示：[用户名]，欢迎您\n\n* 分析：\n\n  ![image-20210805024528927](/images/image-20210805024528927.png)\n\n### JSP\n\n**JSP是 Java Server Pages （Java服务器端页面）的缩写**\n\n#### 概念\n\n* 可以理解为一个特殊的页面，其中既可以直接定义HTML标签，又可以定义Java代码\n* 可以用于简化书写\n\n#### 原理：\n\n**JSP本质上就是一个`Servlet`**\n\n* <img src=\"/images/image-20210805000518670.png\" alt=\"image-20210805000518670\"  /> \n  * JSP编译之后的类继承于`HttpJspBase`，而`HttpJspBase`继承于`HttpServlet`\n\n    * JSP的脚本：JSP定义Java代码的方式\n\n        1. `<% Java代码 %>`：定义的Java代码在`service()`方法中，可以定义`service()`方法中的所有内容\n        2. `<%! Java代码 %>`：只能定义成员变量和成员方法\n        3. `<%= Java代码%>`：定义的Java代码会输出到页面上，相当于调用`out.write(Java代码)`\n\n#### JSP的内置对象\n\n* 概念：在JSP页面中不需要创建，可以直接使用的对象\n\n* JSP一共有9个内置对象，都可以通过`pageContext`对象来获取：![image-20210805144208603](/images/image-20210805144208603.png)\n\n  ![image-20210805144128175](/images/image-20210805144128175.png)\n\n    1. `request`：![image-20210805143123203](/images/image-20210805143123203.png)\n\n    2. `response`：![image-20210805143143363](/images/image-20210805143143363.png)\n\n    3. `out`：一个`JspWriter`\n       字符输出流对象，可以将数据输出到页面上![image-20210805002755326](/images/image-20210805002755326.png)\n\n       `JspWriter`的`out`对象和`response.getWriter()`对象的**区别**：\n\n        * `JspWriter`的`out`对象定义在什么位置就输出内容到什么位置\n\n        * `response.getWriter()`是一个`PrintWriter`对象（继承于`Writer`），它的输出总会在页面最前面\n\n          **原因：**`Tomcat`服务器作出响应之前会先找`response.getWriter()`的缓冲区数据，再找`out`\n          对象的缓冲区数据，所以导致`response.getWriter()`输出的内容永远在`out`对象之前\n\n          **总结：**在JSP中，编写的普通的文本内容相当于调用了`out.write(\"文本内容\")`\n          ，这些文本内容会被写入到页面相应的位置，也可以被夹在JSP的两段`<%Java代码%>`之间\n\n    4. `pageContext`：![image-20210805143232859](/images/image-20210805143232859.png)一个`PageContext`对象，只在当前页面有效\n\n    5. `application`：![image-20210805143342462](/images/image-20210805143342462.png)一个`ServletContext`实现类对象\n\n    6. `page`：一个![image-20210805143357398](/images/image-20210805143357398.png)对象\n\n    7. `config`：一个![image-20210805143419831](/images/image-20210805143419831.png)实现类对象\n\n    8. `session`：一个![image-20210805143450080](/images/image-20210805143450080.png)实现类对象\n\n    9. `exception`：![image-20210805143018353](/images/image-20210805143018353.png)\n\n        * 只有当JSP页面声明了`isErrorPage=\"true\"`才有此内置对象\n\n#### JSP指令\n\n* 作用：用于配置JSP页面，导入资源文件\n\n* 格式：`<%@指令名称 属性名1=属性值1 属性名2=属性值2...%>`\n\n* 分类：\n\n    * `page`：用于配置JSP页面\n\n        * `contentType`：等同于`response.setContentType()`。IDEA会自动读取JSP的此属性更改文件编码\n            1. 设置响应体的MIME类型以及字符集\n            2. 设置当前JSP页面的编码，IDEA会根据此属性自动修改文件编码，否则需手动指定`pageEncoding`\n        * `pageEncoding`：指定当前JSP文件的编码，建议与`contentType`的`charset`统一\n        * `import`：导入包，和Java中的`import`效果一样\n        * `errorPage`：当前页面发生异常后，会自动跳转到指定的错误页面\n        * `isErrorPage`：标识当前页面是否是错误页面\n            1. 如果为true，则可以调用`exception`对象输出错误信息\n            2. 如果为false，则不可以调用`exception`对象（默认值）\n\n    * `include`：用于导入页面的资源文件\n\n        * 例如在一个JSP页面利用`@include`引入另一个页面，在访问此JSP页面时会先载入引入的页面在上方\n\n    * `taglib`：用于导入资源，例如导入`JSTL`：\n\n      ![image-20210805141755489](/images/image-20210805141755489.png)\n\n#### JSP注释\n\n* HTML注释：`<!-- -->`只能注释HTML代码\n\n* JSP注释：`<%-- --%>`可以注释HTML代码也可以注释JSP代码，推荐使用\n\n  区别：\n\n    * 使用HTML注释，会把注释也响应给浏览器，只不过浏览器不显示出来\n    * 使用JSP注释，在编译时就会忽略JSP注释及其内容\n\n#### MVC开发模式\n\n##### JSP演变历史：\n\n* 早期只有Servlet，只能使用response输出标签数据，非常麻烦\n* 后来有了JSP，简化了Servlet的开发，但是在JSP中既写Java代码，又写HTML标签等造成了难以维护\n* 再后来Java的Web开发借鉴了MVC开发模式，使得程序的设计更加合理\n\n##### MVC的概念：\n\n* `M`：Model，模型：`JavaBean`\n    * 完成具体的业务操作，如：查询数据库、封装对象\n* `V`：View，视图：`JSP`\n    * 展示数据\n* `C`：Controller，控制器：`Servlet`\n    * 获取用户的输入\n    * 调用Model\n    * 将Model返回的数据交给View进行展示\n\n优缺点：\n\n* 优点\n    1. 耦合性低，方便维护，利于分工协作\n    2. 重用性高\n* 缺点：\n    1. 使得项目架构变得复杂，对开发人员要求高\n\n#### EL表达式\n\n**EL 是 Expression Language 表达式语言的缩写**\n\n* 作用：替换和简化JSP页面中Java代码的编写\n\n* 语法：`${表达式}`\n\n* 注意：\n\n    * JSP默认支持EL表达式\n        * **忽略EL表达式**：\n            1. 设置JSP指令`@page`的属性`isELIgnored=\"true\"`，忽略当前JSP页面所有的EL表达式\n            2. `\\${表达式}`，忽略`\\`之后的EL表达式\n\n* 使用：\n\n    * 运算\n        * 运算符：\n            1. 算术运算符：`+`、`-`、`*`、`/(div)`、`%(mod)`\n            2. 比较运算符：`>`、`<`、`>=`、`<=`、`==`、`!=`\n            3. 逻辑运算符：`&&(and)`、`||(or)`、`!(not)`\n            4. 空运算符：`empty`\n                * 空运算符用于判断字符串、集合、数组对象是否为null或长度是否为0\n                * 可以搭配逻辑运算符使用：\n                    1. `not empty 对象`，对`empty`的判断结果取反\n                    2. `empty 对象1 and empty 对象2`，对两个`empty`判断结果进行逻辑与运算\n\n    * **获取值**\n\n        * EL表达式只能从域对象中获取值\n\n        * 语法：\n\n            1. `${域名称.键名}`：从指定的域名称中获取指定键的值\n\n               **域名称（相当于Map）：**\n\n                * pageScope --> pageContext\n\n                * requestScope --> request\n\n                * sessionScope -->session\n\n                * applicantScope -->application（ServletContext）\n\n                  例如在request域中存储了`name=Jerry`\n\n                  使用`${requestScope.name}`即可获取`Jerry`，这相当于`${requestScope.get(\"name\")}`\n\n                  值得注意的是，如果获取的数据不存在域中，不会导致显示`null`，而是不显示\n\n            2. `${键名}`：依次从*最小的域*中查找是否有该键对应的值，直到查找到为止\n\n               **最小的域**：按照上方**域名称**从上往下的顺序即从小到大\n\n            3. 获取对象：\n\n                * `${域名称.键名}`，返回键名对应的对象的`toString()`形式\n\n                * `${域名称.键名.属性名}`，相当于调用`get属性名()`\n\n                  例如`${域名称.user.birthday}`相当于`${域名称.user.getBirthDay()}`\n\n                  也可以调用自定义的`getXxx()`方法：`${域名称.user.getXxx()}相当于${域名称.user.xxx}`\n\n            4. 获取List集合：\n\n                * `${域名称.键名}`，返回键名对应的List集合，相当于`${域名称.get(\"键名\")}`\n                * `${域名称.键名[索引]}`，相当于调用键名对应的List集合对象的`get(索引)`，此方法不会越界\n\n            5. 获取Map集合：\n\n                * `${域名称.键名}`返回键名对应的Map集合，相当于`${域名称.get(\"键名\")}`\n\n                * `${域名称.键名.Map的键名}`，相当于调用键名对应的Map集合对象的`get(\"Map的键名\")`\n\n                  还可以写成`${域名称.键名[\"Map的键名\"]}`,此方法相当于Map集合对象的`get(\"Map的键名\")`\n\n* 隐式对象\n\n    * EL表达式中有11个隐式对象\n\n        * `pageContext`：可以用于获取其他8个内置对象\n\n          用法：`${pageContext.request}`：获取`request`内置对象\n\n          案例：可以用于**动态获取虚拟目录**：`${pageContext.request.contextPath}`\n\n#### JSTL标签\n\n**JSTL 是 JavaServer Pages Standard Tag Library （JSP标准标签库）的缩写**\n\n* 概念：是由Apache组织提供的开源免费JSP标签\n\n* 作用：用于简化和替换JSP页面上的Java代码\n\n* 使用步骤：\n\n    1. 导入jar包：`taglibs-standard-impl-1.2.5.jar`和`taglibs-standard-spec-1.2.5.jar`\n    2. 引入标签库：`<%@ tablib prefix=\"前缀名\" uri=\"资源路径\"%>`\n    3. 使用标签\n\n* 常用的JSTL标签\n\n    * `if`，相当于Java代码的if语句\n\n        * 格式：`<前缀名:if test=\"boolean表达式的值\"></前缀名:if>`\n\n        * 用法：必须指定`test`属性，如果为`true`，则显示`if`标签体的内容，否则不显示`if`标签体的内容\n\n          通常在`test`属性搭配EL表达式使用\n\n    * `choose`，相当于Java代码的switch语句\n\n        * 格式：\n\n          ```jsp\n          <c:choose>\n                      <c:when test=\"${requestScope.number == 1}\">\n                          <h3>星期一</h3>\n                      </c:when>\n                      <c:when test=\"${requestScope.number == 2}\">\n                          <h3>星期二</h3>\n                      </c:when>\n                      <c:otherwise>\n                          <h3>其他</h3>\n                      </c:otherwise>\n                  </c:choose>\n          ```\n\n        * 用法：在`choose`标签内的每一个`when`标签相当于`switch`语句的每一个`case`，`otherwise`相当于`switch`\n          语句的`default`\n\n          `when`标签：条件在`test`属性中指出，通常搭配EL语句\n\n    * `foreach`，相当于Java代码的for语句\n\n        * 格式：\n        * 属性：\n            1. `begin`：开始值，包括\n            2. `end`：结束值，包括\n            3. `var`：临时变量名\n            4. `step`：步长\n            5. `varStatus`：循环的状态对象\n                * `index`：元素在容器中的索引，从0开始\n                * `count`：循环计数器，从1开始\n\n* JSTL案例：\n\n    * 需求：在request域中有一个存放User对象的List集合，使用JSTL和EL将List集合内容展示到JSP的表格中\n\n### 三层架构\n\n1. 界面层（表示层）：用户看得到的界面，用户可以通过界面上的组件和服务器交互\n2. 业务逻辑层：处理业务逻辑\n3. 数据访问层：操作数据存储文件\n\n![image-20210806001159331](/images/image-20210806001159331.png)\n\n##### 案例：用户信息的列表展示，代码详见day17_case\n\n* 需求：用户信息的增删改查操作\n\n* 设计：\n\n    * 技术选型：Servlet + JSP + MySQL + JDBCTemplate + Druid + BeanUtils + Tomcat\n\n    * 数据库设计：user表，包括用户的编号、姓名、性别、年龄、籍贯、QQ、邮箱\n\n      ```sql\n      create table user(\n          id int primary key auto_increment,\n          name varchar(20) not null,\n          gender varchar(5),\n          age int,\n          address varchar(32),\n          qq varchar(20),\n          email varchar(50)\n      )\n      ```\n\n* 开发：\n\n    * 环境搭建：\n        1. 创建数据库环境\n        2. 创建项目，导入需要的jar包\n    * coding：\n\n![image-20210806011642699](/images/image-20210806011642699.png)\n\n![image-20210806155853142](/images/image-20210806155853142.png)\n\n![image-20210806162531202](/images/image-20210806162531202.png)\n\n![image-20210806165132525](/images/image-20210806165132525.png)\n\n![image-20210806184821807](/images/image-20210806184821807.png)\n\n![image-20210806211454118](/images/image-20210806211454118.png)\n\n![image-20210806213921964](/images/image-20210806213921964.png)\n\n* 测试\n* 部署运维\n\n###### 基础功能：\n\n1. 列表查询\n2. 登录\n3. 添加\n4. 删除\n5. 修改\n\n###### 复杂功能：\n\n1. 删除选中\n2. 分页查询\n3. 复杂条件查询\n\n### Filter：过滤器\n\n#### web中的过滤器：\n\n* 当访问服务器资源时，过滤器可以将请求拦截下来，完成一些特殊的功能\n\n#### 过滤器的作用：\n\n* 一般用于完成通用的操作，如：登录验证、统一编码处理、敏感字符过滤\n\n#### 快速入门：\n\n##### 步骤：\n\n1. 定义一个类，实现接口`Filter`\n2. 重写接口的方法\n    * `init()`\n    * `doFilter()`\n        * 放行请求：`filterChain.doFilter(ServletRequest servletRequest,ServletResponse servletResponse)`\n    * `destroy()`\n3. 配置拦截路径\n\n##### 过滤器细节：\n\n###### web.xml配置\n\n类似于Servlet的配置，指定某个`filter-name`的`url-pattern`\n\n![image-20210807133551501](/images/image-20210807133551501.png)\n\n###### 过滤器执行流程\n\n1. 执行过滤器\n2. 执行放行后的资源：`chain.doFilter(request,response)`\n3. 执行放行之后的代码\n\n###### 过滤器生命周期方法\n\n* `init()`：服务器开启后即执行，不需要访问都会执行，一般用于加载资源\n* `doFilter()`：**每一次**请求**被拦截的资源时**会执行\n* `destroy()`：服务器正常关闭后销毁执行，一般用于释放资源\n\n###### 过滤器配置详解\n\n* 拦截路径设置：\n\n    1. 具体资源的拦截：`/index.jsp`，只有访问`index.jsp`资源时，过滤器才会被执行\n    2. 具体目录拦截：`/user/*`，访问/user下的所有资源时都会被过滤器执行拦截\n    3. 后缀名拦截：`*.jsp`，访问所有JSP资源时都会被过滤器执行拦截\n    4. 拦截所有：`/*`，访问所有资源都会被过滤器执行拦截\n\n* 拦截方式设置：资源被访问的方式\n\n    1. 注解配置：设定`dispatcherTypes`属性的值\n\n       可选的*属性值*：\n\n        * `REQUEST`：浏览器**直接请求**资源时才会执行此过滤器，默认值\n        * `FORWARD`：**转发**访问资源时才会执行此过滤器\n        * `INCLUDE`：包含访问资源时才会执行此过滤器\n        * `ERROR`：错误跳转资源时才会执行此过滤器\n        * `ASYNC`：异步访问资源时才会执行此过滤器\n\n    2. `web.xml`配置：在`filter-mapping`标签里设定`dispatcher`标签的内容为可选的*属性值*，*属性值*与注解方式取值一样\n\n###### 过滤器链(配置多个过滤器)\n\n* **拦截顺序**：对于两个过滤器A和B，被拦截和返回是对称的\n\n    1. <font color=SandyBrown>A执行了</font>\n    2. <font color=Tomato>B执行了</font>\n    3. <font color=DarkCyan>资源被访问到了</font>\n    4. <font color=Tomato>B回来了</font>\n    5. <font color=SandyBrown>A回来了</font>\n\n* **先后顺序**：\n\n    * 注解配置：按照**类名**的字符串进行比较，值小的先执行\n\n      例如：`AFilter`和`BFilter`，显然`AFilter`小于`BFilter`，所以`AFilter`先执行；\n\n      同样的：`Filter12`小于`Filter2`，所以`Filter12`先执行\n\n    * `web.xml`配置：看`<filter-mapping>`的位置，定义在`web.xml`越靠前的越先执行\n\n    * 如果注解配置和`web.xml`同时配置了过滤器，那么会优先执行`web.xml`中配置的过滤器\n\n###### 案例1：登录验证（权限控制）\n\n![image-20210807174933312](/images/image-20210807174933312.png)\n\n* 需求：\n    1. 访问day17_case案例的资源，验证其是否登录\n        * 验证用户是否登录，可以通过Session域中存放的User对象判断，如果有直接放行，如果没有就跳转到登录页面\n        * 排除登录页面本身，只关心是否是登录相关的资源才走过滤器，否则直接放行\n    2. 如果登录了，则直接放行\n    3. 如果没有登录，则跳转到登录页面，提示\"您尚未登录，请先登录\"\n\n###### 案例2：敏感词汇过滤\n\n![image-20210807174853954](/images/image-20210807174853954.png)\n\n* 需求：\n    1. 对day17_case案例录入的数据进行敏感词汇过滤\n    2. 敏感词汇参考`敏感词汇.txt`\n    3. 如果是敏感词汇，替换为`***`\n\n* 分析：\n\n    * 需要对request对象进行增强\n\n* 增强对象的功能：利用**设计模式**\n\n    * `Decorator`装饰模式\n\n    * 代理模式\n\n        * 概念：\n            1. 真实对象：被代理的对象\n            2. 代理对象：\n            3. 代理模式：代理对象去代理真实对象，达到增强真实对象功能的目的\n        * 实现方式：\n\n            1. 静态代理：在一个类文件描述代理模式\n            2. 动态代理：在内存中形成代理类\n        * 实现步骤：\n            1. 代理对象和真实对象实现相同的接口\n            2. 利用`java.lang.reflect.Proxy`\n               类的静态方法：`newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)`\n               获取一个代理对象，其中参数：\n                * `loader`是真实对象的`ClassLoader`类加载器\n                * `interfaces`是真实对象实现的接口的数组，提供`ClassLoader`的`getInterfaces()`获取\n                * `h`是代理对象调用任意方法时都会执行的函数式接口，重写`invoke(Object proxy,Method method,Object[] args)`\n                  方法即可，此方法中`proxy`是代理对象，`method`是代理对象的所有方法，`args`是执行`method`所需的参数列表。\n            3. 使用代理对象调用方法\n            4. 增强方法，增强的方式有：\n                * 增强参数列表\n                * 增强返回值\n                * 增强方法体的执行逻辑\n\n### Listener：监听器\n\n#### web中的监听器：\n\n* web的三大组件之一\n\n#### 事件监听机制：\n\n* 事件：一件事情\n* 事件源：事件发生的地方\n* 监听器：一个对象\n* 注册监听：将事件、事件源、监听器绑定在一起。当事件源上发生某个事件之后，执行监听器代码\n\n#### 监听器对象：\n\n* `ServletContextListener`：监听`ServletContext`对象的创建和销毁。此接口的实现接收有关其所属 Web\n  应用程序的`ServletContext`上下文更改的通知。要接收通知事件，必须在 Web 应用程序的部署描述符中配置实现类。\n\n    * 方法：\n\n        1. `public void contextInitialized(ServletContextEvent sce) `\n\n           ServletContext对象被创建后会调用此方法，在初始化 Web 应用程序中的所有过滤器或 servlet 之前，应该通知所有\n           ServletContextListener 关于上下文初始化的信息。\n\n        2. `public void contextDestroyed(ServletContextEvent sce) `\n\n           ServletContext对象被销毁之前会调用此方法，通知即将关闭ServletContext，在通知所有 ServletContextListener\n           上下文销毁之前，所有 servlet 和过滤器都已销毁。\n\n#### 快速入门：\n\n##### 步骤：\n\n1. 定义一个类，实现`ServletContextListener`接口\n\n2. 重写接口的方法\n\n3. 配置：\n\n    * `web.xml`配置\n\n      ![image-20210807215817460](/images/image-20210807215817460.png)\n\n      可以用于读取`<context-param>`标签内的初始化参数，通过`<param-name>`来获取`<param-value>`\n      标签内的值，调用ServletContext对象的`String getInitParameter(String s)`方法传入`param-name`即可\n\n    * 注解配置：<img src=\"/images/image-20210807215853195.png\" alt=\"image-20210807215853195\" style=\"zoom:80%;\" />\n      被此注解标记的类是一个监听器类，等同于`web.xml`的`<listener-class>`\n\n## 综合案例：黑马旅游网\n\n### 需求：\n\n* 技术选型：\n    1. Web层：\n        * Servlet：前端控制器\n        * HTML：视图层\n        * Filter：过滤器\n        * BeanUtils：数据封装成JavaBean\n        * Jackson：序列化JSON\n    2. Service层\n        * JavaMail：发送邮件的工具\n        * Redis：NOSQL内存数据库做缓存\n        * Jedis：操作Redis\n    3. DAO层\n        * MySQL：数据库持久层\n        * Druid：数据库连接池\n        * JDBCTemplate：操作数据库的模板工具\n\n* 数据库：\n\n![image-20210812213208092](/images/image-20210812213208092.png)\n\n### 实现：\n\n#### 1.用户注册：\n\n![image-20210812225952027](/images/image-20210812225952027.png)\n\n* 前端表单提交之前在浏览器利用正则表达式验证：\n\n    * 1.用户名：6到20位的单词字符\n    * 2.密码：6到20位的单词字符\n    * 3.email：例如123@qq.com\n    * 4.姓名：非空\n    * 5.手机号：手机号正则\n    * 6.出生日期：非空\n    * 7.验证码：非空\n* 所有表单项验证通过后提交表单：使用AJAX异步提交到registerUserServlet，参数可以调用表单的serialize()\n    * 原因：前端使用的是HTML，不像JSP可以直接从Servlet获取值，只能通过AJAX响应数据\n    * <font color=red>踩坑1</font>：点击提交按钮提交表单数据，但表单提交**会导致页面刷新**，在submit事件绑定的回调函数内部做逻辑处理时就要\n      **防止表单提交刷新**页面，利用AJAX发送请求完毕后，要**使submit回调函数返回false**\n    * <font color=red>踩坑2</font>：AJAX返回的数据默认是字符串，要么在AJAX发送请求指定参数为'json'，要么在后端指定响应头\n    * <font color=red>**踩坑踩大坑！！疯狂乱码，跟过滤器设置响应头有关，响应头全部当做text/html处理会乱码**</font>\n* 邮件激活：\n    * 发送邮件\n        * 开启发件邮箱的POP3/SMTP服务，发送一条带有激活链接的邮件，链接到activeUserServlet\n            * <font color=red>踩坑</font>：MailUtil工具类默认是qq邮箱作为发件箱，需要自己灵活修改\n        * 激活邮件中为了区分是哪个用户点击的，需要给激活链接一个唯一校验码，用UuidUtil工具类生成\n        * 激活链接需要把UuidUtil工具类生成的校验码设置为激活链接的请求参数，以供服务器比对数据库\n    * 用户点击邮件激活\n        * 用户点击激活链接之后发送请求到activeCodeServlet进行校验，请求参数为code校验码\n            * 如果存在此校验码的用户，则修改`tab_user`表的`status`字段为`Y`，并返回激活成功提示\n            * 如果不存在此校验码的用户，则返回一个错误提示\n\n#### 2.用户登录\n\n![image-20210813145133785](/images/image-20210813145133785.png)\n\n* 前端：\n    * 给表单绑定submit事件，点击登录按钮发送AJAX请求到loginServlet，根据返回的JSON判断是否成功登录\n* 后端：\n    * 接收前端的请求参数，封装为User对象，传递给service层查询\n    * 根据查询的结果，判断是否存在此User对象在数据库中\n        * 如果存在此User，判断User对象的status是否激活\n            * 如果没有激活，则封装ResultInfo一个错误提示\n            * 如果已经激活，则封装ResultInfo一个成功信息，并且把登录的User对象存入Session中\n        * 如果不存在此User，封装ResultInfo对象一个错误提示\n    * 把ResultInfo对象转为JSON字符串并且返回给前端，记得设置响应头为JSON格式\n\n**登录成功之后的用户名显示：**\n\n* 登录成功之后页面加载完成就发送AJAX请求，取出Seesion中的User对象，显示到前端对应的文本中\n\n#### 3.用户退出登录\n\n* 前端：\n    * 点击退出链接访问exitServlet\n* 后端：\n    * 销毁存放了User对象的Session，可以`removeAttribute()`也可以`invalidate()`使所有的session失效\n    * 跳转到登录页面\n\n#### <font color=red>优化Servlet</font>\n\n* 背景原因：对于前端请求的每一个功能都需要对应的Servlet进行接收和响应，造成大量Servlet的编写\n* 优化方式：把对同一个数据库的操作的Servlet抽象为一个Servlet，前端请求的每一个功能封装为每一个方法\n\n<img src=\"/images/image-20210813173209744.png\" alt=\"image-20210813173209744\" style=\"zoom:50%;\" />\n\n* 在封装的BaseServlet中获取request请求对象的URI，提取出访问的具体方法`method`\n* 获取调用此`service()`方法的具体的BaseServlet**子类对象**，调用this.getClass()利用反射执行这个`method`\n    * 因为\n\n#### 4.旅游分类信息导航栏\n\n![image-20210813230628575](/images/image-20210813230628575.png)\n\n* 前端：\n    * 发送AJAX请求，加载分类导航栏的数据\n    * 请求到后端的所有分类导航栏JSON数据后，遍历数据显示到页面上\n* 后端\n    * 查询数据库中分类导航栏tab_category表中所有的数据，可以利用`order by`子句按照`cid`排序查询结果\n    * 封装为List集合，转为JSON返回给前端\n\n#### 5.对分类信息导航栏的查询进行缓存优化\n\n* 原因：由于分类信息导航栏不会经常产生变化，每一次`header.html`\n  加载完毕都会加载分类信息导航栏，大量查询数据库，适合使用`Redis`进行缓存\n* 实现：在Servlet调用的ServiceImpl实现类中对`findAll()`方法进行优化\n    * 判断分类信息导航栏集合是否为空\n        * 如果为空，说明是第一次查询，则查询数据库，然后写入`Redis`缓存\n        * 如果不为空，说明不是第一次查询，直接返回Redis的结果\n* <font color=red>踩坑</font>：\n    * Jedis死活连接不上，配置文件也没有问题，结果是忘了开启Redis服务器端了...\n    * UTF-8编码存入Redis的内容，在windows控制台下默认GBK查看会乱码，先利用`chcp 65001`切换编码\n\n#### 6.旅游线路的分类展示\n\n* 首先要保证前端可以区分每一个分类导航栏中的按钮链接：也就是每一个分类按钮指向的链接的请求参数可以为这个分类的`cid`\n  ，在第`4`步拼接这些分类标签时的循环中就要处理好每一个a标签的`href`中的参数`cid`\n\n* **怎么让每一个分类的HTML静态页面能发送自己的cid到后端查询内容？**\n    * 由于点击不同分类的链接，跳转到统一的`route_list.html`页面，唯一能区分的就是请求参数，所以：\n        * 通过此`route_list.html`页面的<font color=purple>`location`对象的`search`属性</font>返回此页面URL`?`及之后的请求参数\n        * 然后AJAX发送这个`?`之后的请求参数（也就是此页面的`cid`）查询数据\n\n* 前端：\n    * AJAX发送请求，携带参数：\n        * 要查询的路线`cid`：`cid`的值\n        * 要查询的分页的页码`currentPage`\n        * 每一页显示的条数`pageSize`\n* 后端：\n    * 接收前端请求的参数：`cid`、当前页码`currentPage`、每一页的条数`pageSize`\n    * 调用service层查询此`cid`对应的数据，以及按照当前页码和每一页的条数`limit`查询\n    * 查询的结果封装为`PageBean<T>`对象，序列化为JSON返回给前端\n\n#### 7.旅游线路分类的分页\n\n* 前端：根据JSON的数据**动态拼接HTML标签**，主要是`ul`标签中的`li`标签数量和内容要根据查询出的此路线分类的**分页数量**动态拼接\n    * 拼接标签要注意，为了能保证标签中的链接也是异步请求，不能直接`href`发送请求，要把`href`\n      的超链接设置为`javascript:function`的形式\n        * 例如`<a href=\"javascript:load(要请求的cid,要请求的页码)\"></a>`\n          ，这里的cid就是此页面的分类，请求的页码就是此标签要链接到的请求的`currentPage`，此`load(cid,currentPage)`是异步的\n    * 可以把`load(cid,currentPage)`方法抽取出来，方便不同的链接传入参数异步请求不同的分类及其分页\n* 后端：接收前端的请求参数：`cid`和`currentPage`（和`pageSize`），查询数据库并返回结果\n    * 包装成PageBean对象，并序列化为JSON返回\n\n#### 8.分页按钮优化显示\n\n* 由于一个分类可能包含若干个分页，那么导致分页按钮过多，需要根据目前所在的页码动态调整分页按钮数量\n\n* 前端：分页按钮的起始索引和结束索引都保持他么之间最多相差10个按钮\n\n    * 定义两个变量当做索引：起始的`begin`和一个结束的`end`\n\n    * 如果此分类的总分页数量不超过10页，那么就直接显示这些分页按钮：`begin=1`，`end=10`\n\n    * 如果此分类的总分页数量超过10页，那么就按照前面5个按钮，后面4个按钮，当前分页按钮位于第6个\n\n        * `begin = currentPage -5`，`end = currentPage + 4`\n\n        * 处理最前面和最后面的情况：\n\n            * 如果`currentPage < 5`就显示前10页的结果：`begin =1`，`end =10`\n\n            * 如果`currentPage > 总分页数量 - 4`就显示最后10页的结果：\n\n              `begin = 总分页数量 - 9`，`end = 总分页数量`\n\n    * 经过以上处理就可以保证当前页面按钮在第6个的位置，除非总记录数不足10条或者：\n\n        * 当前页码不足6，则只显示前10个分页\n        * 当前页码在最后4条以内，则只显示最后10个分页\n\n    * 另外，如果每一次分页之后都想让页面**回到顶端**，可以在此函数内执行：`window.scrollTo(0,0)`\n\n#### 9.旅游线路模糊搜索\n\n* 前端：\n\n    * 为搜索按钮绑定单击事件，如果单击就获取搜索输入框的值作为参数跳转到相应的搜索结果页面\n        * 跳转：修改`location`对象的`href`属性：`location.href = \"要跳转的url?传递的参数列表\"`\n    * 搜索结果页面也就是`route_list.html`，只不过这次查询的参数除了`cid`之外还要限制为`rname`\n        * 在这个页面接收`location`地址栏的参数，获取`cid`和`rname`\n    * 前端对`load(cid,currentPage)`方法添加一个参数：`load(cid,currentPage,rname)`\n    * 获取当前页面实际的`rname`，利用`window.decodeURI`解码，然后修改所有按钮的`load`\n      方法为新的load方法，即传入`cid`、`currentPage`、`rname`\n    *\n\n* 后端：接收前端传递的参数`cid`和`rname`，编写SQL进行模糊查询，并返回结果\n\n    * 注意判断`rname`是否为空或是否为`\"\"`空字符串\n\n        * <font color=red>踩坑1</font>：\n\n            1. `rname`还有可能为\"`null`\"这个字符串，因为JavaScript中：这个拼接操作会导致\n\n          <img src=\"/images/image-20210814213435643.png\" alt=\"image-20210814213435643\" style=\"zoom:80%;\" /> \n\n          被解码的`currentRname`**可能为`null`对象的情况**，被第二条语句和`''`拼接就成了字符串的`'null'`\n\n        * <font color=red>踩坑2</font>：`cid`也有可能为`\"null\"`这个字符串的值，没有选择分类时直接搜索就会导致`cid`\n          为`\"null`\"\n\n    * 可以在dao层利用条件查询的`where 1 = 1`拼接`and 字段名 = 字段值`\n      来拼接SQL，然后把参数按实际是否存在的情况装入集合调用`toArray()`进行JDBCTemplate的查询\n\n    * 最后把查询到的结果封装为`PageBean`对象，序列化为JSON返回给前端\n\n#### 10.旅游路线的查看详情\n\n![image-20210815013209415](/images/image-20210815013209415.png)\n\n![image-20210815013246676](/images/image-20210815013246676.png)\n\n* 前端：点击每一个路线的`<a>查看详情</a>`标签，会被链接到此旅游路线的详情页面：`route_detail.html`\n    * 区分点击的是哪个详情页面：为链接的`URL`传递参数：`rid=每条路线的rid`\n    * 在`route_detail.html`页面加载完成后发送AJAX请求，参数为`rid`，查询此旅游路线的数据\n    * 接收后端返回的此旅游路线的JSON数据，完成展示：\n        * 遍历JSON数据，拼接相应的数据为HTML标签，填入页面\n        * 对于小图预览图，由于已经有写好的JavaScript控制样式，只需要注意超过4张小图就隐藏后面的\n            * 隐藏：即设置此图片所在的`<a></a>`标签的`style`为`display:none`\n* 后端：接收前端发送的`rid`，查询数据库中此路线的数据\n    * 首先要查出请求的`rid`旅游路线在`tab_route`中的路线详情，封装为`Route`对象\n    * 再从`tab_img`表查询出此`rid`对应的图片数据，封装为`List<RouteImg>`集合，设置为`Route`对象的成员\n    * 最后从`tab_seller`表查询出此`rid`的路线对应的商家详情，封装为`Seller`对象，同样的设置为成员\n    * 可以再查询出此`Route`对象所属的旅游路线分类，但是此页面不要求做展示，没有必要\n    * 最后把封装好的`Route`对象序列化为JSON返回给前端\n\n#### 11.点击收藏按钮或取消收藏按钮的判断\n\n![image-20210815160048822](/images/image-20210815160048822.png)\n\n* 前端：\n    * 发送AJAX请求，参数传递此旅游路线的rid\n        * 发送AJAX的时机要等到这个页面加载完毕并且此商品的详细信息加载完毕，否则修改按钮样式可能还没有经过第[10]\n          步的查看详情遍历显示完，自然也就获取不到标签对象修改不了值了\n        * 所以要编写一个AJAX方法，调用方法的时机在`loadImg()`方法里的最后，也就是上一步加载完毕\n    * 接收后端返回的结果，根据结果显示按钮 `点击收藏` 或 `取消收藏`\n        * 返回的结果为`true`就给`$(#favorite)`标签添加已收藏样式和不可点击，否则`false`显示默认样式\n* 后端：\n    * 接收前端的旅游路线rid参数，并且获取session中的User用户对象，去`tab_favorite`表中查询路线与用户是否存在此对应记录判断是否收藏，返回一个是否收藏的结果的布尔值\n\n#### 12.收藏次数\n\n* 前端：不需要做调整，只需要对返回的Route旅游路线对象的`count`属性进行展示就行了\n*\n\n后端：在查询路线详情service层封装Route对象的时候额外再去调用dao层利用聚合函数统计一下此旅游路线被收藏的数量，然后设置Route对象的`count`\n属性就行了\n\n#### 13.点击收藏功能的实现\n\n![image-20210815224049640](/images/image-20210815224049640.png)\n\n* 前端：发送AJAX请求，判断用户是否登录\n    * 已经登录：\n        * 发送AJAX请求，传递要收藏的旅游路线的rid即可，User对象不需要传递，因为session对象里有\n        * 收藏写入数据库后不需要返回值，但是前端此时应该刷新一下页面改变\"点击收藏\"为\"取消收藏\"\n    * 没有登录：弹框提示用户尚未登录，然后跳转到登录页面\n* 后端：\n    * 接收前端的参数，要收藏的路线的rid、当前登录的用户的uid\n    * 调用service层添加收藏记录","tags":["Java","Servlet"],"categories":["Java"]},{"title":"Java复习——从入土到入门","url":"/2021/05/14/JavaReview/","content":"# Java知识点概览：\n## Java的概念和特点：\n### 概念\n&emsp;&emsp;Java是一门面向对象编程语言，不仅吸收了C++语言的各种优点， 还摒弃了C++里难以理解的多继承、指针等概念，因此Java语言具有功能强大和简单易用两个特征。\n&emsp;&emsp;由上述介绍可知：\n&emsp;&emsp;&emsp;&emsp;1. Java是一门面向对象的语言\n&emsp;&emsp;&emsp;&emsp;2. Java是单继承的\n&emsp;&emsp;&emsp;&emsp;3. Java语法没有指针概念\n### 特点\n&emsp;&emsp;Java还有几个重要特点：\n&emsp;&emsp;&emsp;&emsp;1. Java是类型安全的\n&emsp;&emsp;&emsp;&emsp;2. Java是跨平台的\n### Java的JVM、JRE、JDK\n&emsp;&emsp;Java的<font color=orange><b>跨平台性</b></font>是区别于其他语言的显著特点：\n&emsp;&emsp;&emsp;&emsp;Java程序运行在<font color=red>JVM</font>(Java虚拟机)上，我们写的Java源代码通过javac命令编译成.class字节码文件，这个字节码文件可以在各个平台的JVM环境上运行。\n&emsp;&emsp;&emsp;&emsp;Java运行环境(<font color=red>JRE</font>)：光有JVM是不够的，Java还需要依赖一些类库，这些类库存在于JRE中，所以JRE包括两部分：JVM和类库\n&emsp;&emsp;&emsp;&emsp;Java开发工具包(<font color=red>JDK</font>)：作为开发人员，我们需要编译、调试Java程序，例如javac就存在于JDK中。\n&emsp;&emsp;&emsp;&emsp;所以，想要运行Java程序，借助JRE这个运行环境即可，而开发Java程序，需要JDK这个开发工具包。\n## Java基本语法\n### 数据类型\n&emsp;&emsp;Java的数据类型大体上可以分为2种：\n&emsp;&emsp;&emsp;&emsp;1. <font color=red>基本</font>数据类型\n&emsp;&emsp;&emsp;&emsp;2. <font color=red>引用</font>数据类型\n#### 基本数据类型：\n&emsp;&emsp;和C语言类似：Java也有整型、浮点型之分，除此之外，Java还多了一种更小的整型：byte型，还有一种表达真假的、仅有两个值的数据类型：boolean\n&emsp;&emsp;<font color=red>基本数据类型</font>一共有4类，8种：\n&emsp;&emsp;&emsp;&emsp;1. 整型：byte、short、int、long\n&emsp;&emsp;&emsp;&emsp;2. 浮点型：float、double\n&emsp;&emsp;&emsp;&emsp;3. 字符型：char\n&emsp;&emsp;&emsp;&emsp;4. 布尔型：boolean\n#### 引用数据类型：\n&emsp;&emsp;除了基本数据类型外，Java中的各种对象称为引用数据类型\n&emsp;&emsp;<font color=red>引用数据类型</font>主要有：\n&emsp;&emsp;&emsp;&emsp;1. 数组\n&emsp;&emsp;&emsp;&emsp;2. 类：Class\n&emsp;&emsp;&emsp;&emsp;3. 接口：Interface\n&emsp;&emsp;&emsp;&emsp;4. 注解：Annotation(一种特殊的接口)\n&emsp;&emsp;我们常用的引用类型有数组(int[]整型数组，char[]字符型数组，甚至Person[]对象数组)、类(例如上面提到的String字符串类)和接口(内含抽象方法)。\n### 程序结构\n#### 分支\n&emsp;&emsp;分支结构主要有：\n&emsp;&emsp;&emsp;&emsp;1. if\n&emsp;&emsp;&emsp;&emsp;2. else\n&emsp;&emsp;&emsp;&emsp;3. else if\n&emsp;&emsp;&emsp;&emsp;4. switch...case\n&emsp;&emsp;值得注意的是：<font color=red>else总是与最近的if配对</font>\n#### 循环\n&emsp;&emsp;循环结构主要有：\n&emsp;&emsp;&emsp;&emsp;1. while循环\n&emsp;&emsp;&emsp;&emsp;2. do...while循环\n&emsp;&emsp;&emsp;&emsp;3. for循环\n&emsp;&emsp;三种循环均可以互相转换，<font color=red>do...while循环至少会执行一次</font>\n&emsp;&emsp;结束本次循环，继续下次循环<font color=red>continue关键字</font>，直接结束掉循环体<font color=red>break关键字</font>\n### 数据的输入与输出\n#### 数据输入\n&emsp;&emsp;<font color=red>利用Scanner类</font>，或利用文件操作相关的类\n&emsp;&emsp;常用写法：<code>Scanner sc = new Scanner(System.in)</code>\n#### 数据输出\n&emsp;&emsp;<font color=red>利用System类的输出流静态变量out下的多种print()方法</font>，或利用文件操作相关的类\n&emsp;&emsp;常用写法：<code>System.out.println()</code>\n\n\n## Java的面向对象\n### 简介\n&emsp;&emsp;所谓的面向对象，是相对于面向过程(C语言)而言的。\n&emsp;&emsp;例如：我们需要把一个字符串\"china\"转化为大写字母:\n&emsp;&emsp;<font color=red>面向过程</font>的思想是利用循环，分别对每一个字符的ASCII码进行处理(因为小写字母和大写字母的ASCII码存在特定的联系)\n&emsp;&emsp;而<font color=red>面向对象</font>的思想中，我们不关心具体怎么处理，而是利用一个能够处理<b>小写转为大写</b>的<font color=red>对象</font>来处理：\n&emsp;&emsp;例如String类的<code>toUpperCase()</code>方法，就能把该字符串对象转换为大写字母\n&emsp;&emsp;上面提到的String类是JDK的Java大神写好的类，类中有很多有用的<font color=red>方法</font>，除了大小写转换外，还有<code>split()</code>分割字符串、<code>substring()</code>截取字符串、<code>charAt()</code>返回某个字符的下标位置等等\n&emsp;&emsp;另外，除了JDK的大神写好的类外，我们也可以自己定义一些类，一旦定义了一个类，就能通过new关键字不断创建这个类的实例，也叫对象。可以说，<font color=green>类是对一个对象共同特征的一种抽取，它只是一个模板，是抽象的；而对象才是类的一个实例，我们的操作一般是通过实例化出来的对象来操作。</font>这一点放在第三章讲。\n### 面向对象思想\n#### 封装\n&emsp;&emsp;封装就是把具有一定共性的属性抽取出来，形成一个模板，我们称之为类(Class)，同时，我们对外尽量隐藏内部实现细节，而只提供一个公共接口(非特指interface)访问。这样可以使得程序更容易维护，也加强了安全性。\n#### 继承\n&emsp;&emsp;类(Class)与类之间是可以继承的，但要注意，Java是<font color=red>单继承</font>的，即一个类只有一个直接父类。继承对于子类来说是对父类的扩充，而父类是对子类的抽象。所以子类不但含有父类的内容，也可以新增自己的内容。\n&emsp;&emsp;例如<code>class Student extends Person</code>\n#### 多态\n&emsp;&emsp;多态是同一个行为具有多个不同表现形式或形态的能力。Java中，可以把一个<font color=red>子类赋值给一个父类。</font>\n&emsp;&emsp;例如<code>Person stu1 = new Student()</code>\n&emsp;&emsp;多态是以继承为前提的，并且多态的子类在赋值给父类时，相当于把子类当作父类使用，即此时的子类特有属性和方法被隐藏起来了。我们可以利用强制类型转换把一个多态写法中当成父类使用的子类对象重新转换为子类。\n&emsp;&emsp;例如<code>(Student)stu1</code>把Person类型的对象stu1重新转为子类Student，转为子类后的stu1对象被屏蔽掉的特有属性和方法可以使用了。\n&emsp;&emsp;但是要注意，不能把一个本来就不是子类的对象转为子类\n&emsp;&emsp;例如：\n\n```c\nPerson stu2 = new Person();\n(Student)stu2;\n//此时会引发类型转换异常\n```\n### 类中的成员\n#### 成员变量\n&emsp;&emsp;成员变量在类中仅作声明，实际上在类实例化成一个对象时才会初始化和分配空间。\n&emsp;&emsp;定义成员变量：\n```c\nclass Person{\n    int age;    //年龄\n    String name;//姓名\n}\n```\n&emsp;&emsp;成员变量可用权限修饰符修饰：public、protected、不写(default)、private\n#### 成员方法\n&emsp;&emsp;成员方法类似于C语言的函数(function)，在Java中，它叫方法(method)，用来实现某种操作。\n&emsp;&emsp;定义成员方法：\n```c\nclass Person{\n    void speak(){\n        System.out.println(\"Person类的成员方法speak()\");\n    }\n}\n```\n&emsp;&emsp;同样的，成员方法也可用权限修饰符修饰：public、protected、不写(default)、private\n#### 静态变量\n&emsp;&emsp;又叫类变量，静态变量是用static修饰的变量，此时这个变量不属于某个具体对象，而是属于类，直接利用<font color=red>类名.静态变量名</font>使用。对它的修改会影响所有用到静态变量的地方。因为它在内存中只保存了一份，任何用到它的地方都指向唯一的地址(存在于静态方法区中)。\n#### 静态方法\n&emsp;&emsp;同样的，静态方法又叫类方法，是用static修饰的方法，此时这个方法也不属于某个具体对象，而是属于类，直接利用<font color=red>类名.静态方法名</font>调用方法。\n&emsp;&emsp;<font color=green>值得注意的是，在静态方法中不能调用非静态变量(包括this关键字)，因为静态方法会<font color=red>先于</font>对象创建，此时静态方法内的非静态变量(实例变量)还没有被初始化，因为对象创建时才会初始化那些实例变量。</font>\n\n#### 静态代码块\n&emsp;&emsp;形如：\n```c\nstatic {\n    System.out.println(\"类中的静态代码块\");\n}\n```\n&emsp;&emsp;这样的代码块叫做静态代码块，它会<font color=red>在类初始化之前执行一次，且只会执行一次。</font>如果一个类中有多个静态代码块，那么会从上到下依次执行一次，且只会执行一次。\n### 构造方法和对象的初始化\n#### 构造方法\n&emsp;&emsp;构造方法是类中特殊的方法，用于初始化一个类，在创建对象(实例化一个类为对象)时调用。\n&emsp;&emsp;特点是：\n&emsp;&emsp;&emsp;&emsp;1. <font color=red>没有返回值，void也不能写。</font>\n&emsp;&emsp;&emsp;&emsp;2. <font color=red>名称与类名相同</font>\n&emsp;&emsp;&emsp;&emsp;3. <font color=red>如果不写，类中会默认隐式声明一个无参构造。一旦定义了一个构造方法，将不会隐式声明其他构造方法。</font>\n&emsp;&emsp;&emsp;&emsp;4. 可以有多种重载形式\n&emsp;&emsp;&emsp;&emsp;5. 在继承关系中，子类会默认调用父类的无参构造，形如：`super()`，并且只能写在方法的第一行。\n&emsp;&emsp;&emsp;&emsp;6. 可以利用<code>this()</code>调用其他重载的构造方法，但是不能形成递归，否则会造成栈溢出异常(StackOverflowException)。并且<code>this()</code>也只能写在方法的第一行。可见，<code>this()</code>和<code>super()</code>不可能在同一个构造方法中同时出现(因为这两句话都要求写在第一行，显然这是不可能的)。\n\n#### 对象的初始化\n&emsp;&emsp;静态代码块会在第一次使用到该类时执行，静态变量也会随之初始化。\n&emsp;&emsp;在使用new 构造方法()初始化一个对象时，会执行该类的构造方法来实例化类为对象。\n&emsp;&emsp;类中的成员变量会被初始化为<font color=red>默认值</font>。具体默认值参考：\n```c\n//默认值：\n                整型：       0\n                浮点型：     0.0\n                字符型：     '\\u0000'\n                布尔型：     false\n                引用类型：    null\n```\n&emsp;&emsp;<font color=red>new出来的对象在堆中，直到被垃圾回收。而执行方法的语句和这个方法的局部变量在栈中，当执行这个语句时会把变量压入栈，一旦方法执行完毕，方法中的数据会被立马弹出栈。</font>\n## Java常用类\n### 字符串类\n#### String\n&emsp;&emsp;最常用的字符串，底层是用final修饰的数组，所以值不可改变。\n&emsp;&emsp;可以利用\"+\"号连接字符串，但不能直接用于数值计算。值得注意的是：\n&emsp;&emsp;当利用`String str = \"abc\";`定义一个String对象时，\"abc\"这个字符串会被放入<font color=red>字符串常量池</font>，其他的字符串对象会共用这个\"abc\"字符串内容。\n&emsp;&emsp;而利用`String str = new String(\"abc\");`定义时，这个str<font color=red>指向堆中的对象</font>。\n&emsp;&emsp;我们知道，\"==\"对于基本数据类型是比较数值，而对于引用类型是比较地址值，显然，第二种方式创建的对象在堆中的地址是不同的，所以会返回false，而常量池中的对象地址相同，会返回true\n&emsp;&emsp;<font color=red>高频常用方法：</font>\n&emsp;&emsp;&emsp;&emsp;1.<code> equals()</code>：由于String类重写了Object类的equals()方法返回一个boolean值，建议在比较字符串时使用equals()方法而不是\"==\"\n&emsp;&emsp;&emsp;&emsp;2.<code> compareTo()</code>：由于String类实现了Comparable接口并重写了compareTo()方法，当两个字符串相同时，会返回0\n\n#### StringBuilder\n&emsp;&emsp;可变长度的字符串，初始长度为16加上字符串参数的长度。\n&emsp;&emsp;例如：`StringBuilder s = new StringBuilder(\"123\");`的长度为16+3=>19\n&emsp;&emsp;不能使用\"+\"号连接，可利用<code>append()</code>方法追加StringBuilder对象。\n&emsp;&emsp;可以使用<code>toString()</code>方法返回此StringBuilder对象的String类形式。\n\n#### StringBuffer\n&emsp;&emsp;相当于<font color=green>线程安全的StringBuilder</font>，由于做了同步操作，速度略慢于StringBuilder\n### 日期类\n#### Date类\n&emsp;&emsp;Java中时间的计算以1970年1月1日00:00:00为起点，计算至今的毫秒值。\n&emsp;&emsp;创建一个当前系统时间的Date对象的方法：`Date date = new Date();`\n&emsp;&emsp;Date类常用方法：\n&emsp;&emsp;&emsp;&emsp;1. <code>getTime()</code>：返回Date类对象的毫秒值\n&emsp;&emsp;&emsp;&emsp;2. <code>setTime()</code>：参数传入一个long类型的毫秒值，以设置Date对象的日期\n&emsp;&emsp;&emsp;&emsp;3. <code>getYear()</code>：返回Date对象的年份\n&emsp;&emsp;&emsp;&emsp;4. <code>getMonth()</code>：返回Date对象的月份(注意：此月份比实际月份少1，因为范围为0-11)\n&emsp;&emsp;&emsp;&emsp;5. <code>getDate()</code>：返回Date对象在月份中的天数\n&emsp;&emsp;&emsp;&emsp;6. <code>getDay()</code>：返回Date对象在一周中的星期几(注意：星期日为0)\n\n#### Calendar类\n&emsp;&emsp;Date类中的大部分方法已经由Calendar类代替。\n&emsp;&emsp;注意创建Calendar对象的方法：`Calendar cal = Calendar.getInstance();`\n&emsp;&emsp;创建的Calendar对象包含很多字段(Field)，例如一年中的第几天、一年中的第几周、年份、月份、天数等等...\n&emsp;&emsp;Calender类常用方法类似于Date类，获取字段的方法集中在get()方法中：\n&emsp;&emsp;&emsp;&emsp;1. <code>getTime()</code>：返回Calendar类对象的毫秒值\n&emsp;&emsp;&emsp;&emsp;2.<code> get()</code>：参数可以为YEAR、MONTH、DAY_OF_YEAR等等...\n&emsp;&emsp;<font color=red>同样的，注意Date类和Calendar类的月份都比实际月份少1，因为范围为0-11</font>\n\n### 集合类\n#### List\\<E>接口的集合\n&emsp;&emsp;List\\<E>接口的集合<font color=red>类似于数组</font>，它有3个特点：\n&emsp;&emsp;&emsp;&emsp;1. 有序的\n&emsp;&emsp;&emsp;&emsp;2. 有索引\n&emsp;&emsp;&emsp;&emsp;3. 允许存储重复元素\n##### ArrayList\\<E>\n&emsp;&emsp;底层是数组，查询快，增删慢\n##### LinkedList\\<E>\n&emsp;&emsp;底层是双向链表，查询慢、增删快(区别于Vector，LinkedList不同步)\n&emsp;&emsp;有特有方法：\n&emsp;&emsp;&emsp;&emsp;1. <code>addFirst(E e)</code>：插入e到开头\n&emsp;&emsp;&emsp;&emsp;2.<code> addLast(E e)</code>：插入e到结尾\n&emsp;&emsp;&emsp;&emsp;3. <code>getFirst(E e)</code>：返回第一个元素\n&emsp;&emsp;&emsp;&emsp;4. <code>getLast(E e)</code>：返回最后一个元素\n##### Vector\\<E>\n&emsp;&emsp;底层是数组，查询快，增删慢(区别于LinkedList，Vector是同步的)\n&emsp;&emsp;特有方法：<code>addElement(E e)</code>：将e添加到此集合末尾，并使大小+1\n\n#### Set\\<E>接口的集合\n&emsp;&emsp;Set\\<E>接口的集合重要特点是<font color=red>不允许重复元素</font>，特点为：\n&emsp;&emsp;&emsp;&emsp;1. 不包含重复元素\n&emsp;&emsp;&emsp;&emsp;2. 无索引(所以也就不能用普通for循环遍历，可以使用迭代器或增强for循环)\n\n##### HashSet\\<E>\n&emsp;&emsp;特点：\n&emsp;&emsp;&emsp;&emsp;1. 底层是哈希表，查询速度极快\n&emsp;&emsp;&emsp;&emsp;2. 无序的\n\n##### LinkedHashSet\\<E>\n&emsp;&emsp;&emsp;&emsp;1. 底层是哈希表，查询速度极快\n&emsp;&emsp;&emsp;&emsp;2. 有序的(多了一个链表记录顺序)\n\n#### Map<K,V>接口的集合\n&emsp;&emsp;Map集合是一个双列集合，由键K和值V组成，<font color=red>其中键K不允许重复</font>\n&emsp;&emsp;特点：\n&emsp;&emsp;&emsp;&emsp;1. K不能重复\n&emsp;&emsp;&emsp;&emsp;2. K和V的类型任意\n&emsp;&emsp;&emsp;&emsp;3. 每一个K只对应一个V\n&emsp;&emsp;常用方法：\n&emsp;&emsp;&emsp;&emsp;1. <code>put(K k,V v)</code>：把一个元素放入集合，如果K已经存在则替换原来的V为新V\n&emsp;&emsp;&emsp;&emsp;2. <code>get(Object k)</code>：取出K对应的V，如果不存在则返回null\n&emsp;&emsp;&emsp;&emsp;3. <code>remove(Object k)</code>：删除K指定的键值对\n&emsp;&emsp;&emsp;&emsp;4. <code>clear()</code>：清空这个Map集合\n## Swing图形化\n### Java的图形化\n&emsp;&emsp;我们主要学习Swing和AWT，但是AWT是早期SUN公司提供的GUI开发工具包，它与Swing最显著的特点就是AWT是利用了操作系统提供的图形库，其缺点很明显：显然每个操作系统的图形库是不同的，这导致AWT的跨平台GUI软件界面不统一。\n&emsp;&emsp;而Swing是Java基础类的一部分，它提供了比AWT更好的显示元素，而且是用纯Java写的，但显然，运行速度肯定是直接调用操作系统图形库的AWT更快，所以AWT叫做重量级组件，而Swing叫做轻量级组件(不用怀疑，没有写反)。\n&emsp;&emsp;我们可以简单的判断一个组件是否是Swing组件：<font color=red>Swing组件以'J'开头</font>，例如JLabel、JButton。\n### Swing组件和容器\n&emsp;&emsp;组件不能单独启动，要放在容器中。\n&emsp;&emsp;容器(Container)也是组件(Component)的一种。\n&emsp;&emsp;我们最常用的<code>JFrame</code>容器就是<code>Container</code>的一个子类，显然，也是<code>Component</code>的一个子类，见下方继承图。\n&emsp;&emsp;其他诸如<code>JLabel</code>就是<code>JComponent</code>的一个子类，这里的<code>JComponent</code>其实也是<code>Container</code>的一个子类，当然，也就是<code>Component</code>的一个子类了(子类什么的看下方继承图!)。区别就在于'J'开头的组件存在于<font color=red>javax.swing</font>包下，而普通组件在<font color=red>java.awt</font>包下。\n&emsp;&emsp;来看看我们<font color=red>最常用的独立容器：JFrame</font>的继承图：\n![JFrameExtends](JFrameExtends.png)\n&emsp;&emsp;显然，JFrame继承自Frame，同样的，也继承自更高层的父类Window、Container以至于Component，别忘了，Java中最最高级的根父类是Object，那么它也间接继承自Object类，并实现了一些接口罢了。\n&emsp;&emsp;看看容器有哪些：\n<img src=\"./Container.png\" width = \"500\" height = \"260\" alt=\"容器\" align=center />\n\n### 常用Swing组件\n#### JFrame\n&emsp;&emsp;用于创建一个窗口\n&emsp;&emsp;首先导入包：\n&emsp;&emsp;&emsp;&emsp;1. <code>import javax.swing.*</code>;\n&emsp;&emsp;&emsp;&emsp;2. <code>import java.awt.*</code>;\n&emsp;&emsp;构造方法：\n&emsp;&emsp;&emsp;&emsp;<code>Frame fm = new Frame()</code>创建一个不可见的窗口\n&emsp;&emsp;&emsp;&emsp;<code>Frame fm = new Frame(String title)</code>创建一个带标题的不可见的窗口\n&emsp;&emsp;发现了吗？创建的窗口都<font color=red>不可见</font>，因为创建的窗口默认高度宽度都为0，而且可见性为false。那么想要<font color=red>显示窗口</font>，就需要两步：\n&emsp;&emsp;&emsp;&emsp;1. <code>setSize(int width,int height)</code> ：设置一个宽度和高度\n&emsp;&emsp;&emsp;&emsp;2. <code>setVisible(boolean b) </code>：设置可见性为true\n&emsp;&emsp;值得注意的是JFrame的<font color=red>默认布局</font>为BorderLayout，这可以使你轻松创建一个具有上、下、左、右、中，一共5个区域的布局，但在BorderLayout中，方向以东南西北中表示。\n#### JPanel\n&emsp;&emsp;是一个中间容器，<font color=red>必须放置在其他容器中</font>，采用了双缓冲，减少窗口闪烁。\n&emsp;&emsp;经常用于把其他组件加入到JPanel中，再把JPanel一次性加入到其他容器中。\n#### JLabel\n&emsp;&emsp;用于显示文本或图片。默认文本靠左对齐，上下居中对齐。\n&emsp;&emsp;常用方法：<code>setText(String text)</code>设置这个JLabel的文本\n#### JTextArea\n&emsp;&emsp;一个用来显示文本的多行区域。\n&emsp;&emsp;使用技巧：如果想要这个多行文本区域具有<font color=red>滚动条</font>，可以把这个JTextArea加入到JScrollPane组件中。\n#### JButton\n&emsp;&emsp;一个按钮，可以实现ActionListener动作监听器来响应<font color=red>点击事件</font>。这个点击事件的处理在<code>actionPerformed()</code>方法中。\n&emsp;&emsp;你需要把这个按钮调用<code>addActionListener(ActionLisenter l)</code>并传入一个动作监听器，当你在本类实现了ActionLisenter接口中的方法，那么可以直接传入<code>this</code>\n#### JComboBox\n&emsp;&emsp;一个下拉列表，可以设置单选、多选等模式。\n&emsp;&emsp;常用方法：\n&emsp;&emsp;&emsp;&emsp;1. <code>getSelectedIndex()</code>：获取当前选择项的索引\n&emsp;&emsp;&emsp;&emsp;2. <code>getSelectedItem()</code>：获取当前选择项(Object类型)\n&emsp;&emsp;&emsp;&emsp;3. <code>addItem(E item)</code>：把一个泛型元素加入到下拉列表\n&emsp;&emsp;&emsp;&emsp;4. <code>removeItemAt(int index)</code>：删除指定索引的元素\n&emsp;&emsp;&emsp;&emsp;3. <code>removeAllItems()</code>：删除列表所有的元素\n### 常用布局\n&emsp;&emsp;设置布局调用<code>setLayout()</code>方法传入一个实现了LayoutManager接口的布局对象，例如FlowLayout、GridLayout等...\n&emsp;&emsp;就像这样：<code>this.setLayout(new FlowLayout);</code>\n&emsp;&emsp;或者针对某个面板设置它的布局：\n```c\n    JPanel pn = new JPanel();\t//创建JPanel对象\n    pn.setLayout(new GridLayout); //设置JPanel对象的布局\n```\n&emsp;&emsp;Swing相比AWT新增了BoxLayout布局，然而我们<font color=blue>主要学习以下三种布局：</font>\n#### FlowLayout\n&emsp;&emsp;<font color=red>流式布局</font>，元素按照从左到右依次排列，元素<font color=red>大小不变，位置会变</font>，默认水平垂直间距均为5\n#### GridLayout\n&emsp;&emsp;<font color=red>网格布局</font>，构造方法中指定一个行数和列数，元素<font color=red>大小会变，相对位置不变</font>，默认为1行0列，水平垂直间距均为0；\n#### BorderLayout\n&emsp;&emsp;<font color=red>边框布局</font>，构造方法中指定水平间距和垂直间距，<font color=red>CENTER部分大小会变，四周大小不变</font>，默认水平垂直间距均为0；\n![Layout](Layout.png)\n&emsp;&emsp;<font color=blue>在上面的GUI软件中，很明显主体采用<font color=red>BorderLayout</font>，这种边框布局在放入组件时需要指定放入到东南西北中的哪个位置，否则默认为CENTER，中间的留言内容区域为<font color=red>CENTER</font>区域，而下方的表情选择、输入框都在<font color=red>SOUTH</font>区域，右侧的6个按钮在<font color=red>EAST</font>区域，还有\"留言板\"的小标题在<font color=red>NORTH</font>区域，<font color=red>WEST</font>区域没放东西。\n&emsp;&emsp;而SOUTH区域明显有JLabel标签、JComboBox下拉列表、以及JTextField单行输入框和一个JButton提交按钮，这些组件都是从左到右依次排列的，那么就很显然是FlowLayout流式布局了。\n&emsp;&emsp;右侧EAST区域的6个按钮排列整齐，可以利用一个8行，但只有1列的GridLayout网格布局放置这些按钮。\n&emsp;&emsp;顶部NORTH区域就不用说了，只有一个组件，随便什么布局都可以，但也建议使用FlowLayout流式布局。\n</font>\n### 保姆级案例\n#### 创建一个窗口\n&emsp;&emsp;就像在1.5.2中说的那样，<font color=red>创建一个窗口</font>要导入包、设定构造方法、设置大小和可见性。具体来说就是首先继承JFrame类，然后定义构造方法...\n![JFrameBasic](JFrameBasic.png)\n```c\nimport javax.swing.*;\nimport java.awt.*;\n\npublic class MyFrame extends JFrame {\n    public MyFrame(String title) throws HeadlessException {\n        super(title);\n        setSize(400,300);\n        setVisible(true);\n    }\n\n    public static void main(String[] args) {\n        new MyFrame(\"这是标题\");\n    }\n}\n```\n#### 往窗体里添加一些东西\n![JFrameBasic2](JFrameBasic2.png)\n&emsp;&emsp;我们添加了一个文本标签JLabel到JFrame中一共<font color=red>花了4步</font>，而且由于JFrame默认是BorderLayout布局，所以在第4步调用<code>add()</code>方法时，可以指定添加到东南西北中哪个方位，这里我们就加入到了南方(也就是下方)。\n&emsp;&emsp;显然，我们如果想再加入一个文本在最上方(也就是NORTH北方)，就像这样：\n![JFrameBasic3](JFrameBasic3.png)\n&emsp;&emsp;发现什么区别了吗，我们这次<font color=red>只花了2步</font>，相对于上次并没有new什么JPanel，而是直接把new出来的JLabel文本标签直接add到NORTH北方区域，文本靠左对齐了，就像在1.5.3.3中讲的那样(你不会才知道右侧有目录吧？！)，其实就是这两步：\n![JFrameBasic4](JFrameBasic4.png)\n&emsp;&emsp;但是上面的JLabel文本标签<font color=red>没有居中对齐</font>，逼死强迫症？只需要调用<code>setHorizontalAlignment()</code>方法传入一个int数值就可以，居中CENTER其实相当于写0：\n![JFrameBasic5](JFrameBasic5.png)\n&emsp;&emsp;现在假设我们需要一个<font color=red>文本区域</font>，可以供我们输入很多文字，而且这个文本区域最好在窗口中间...那么很明显定义一个JTextArea，然后放入JFrame的CENTER区域就行了：\n![JFrameBasic6](JFrameBasic6.png)\n&emsp;&emsp;<font color=red>结合上面几个案例，不难看出，一般情况下添加一个组件到JFrame窗口，两步就够了：</font>\n&emsp;&emsp;&emsp;&emsp;1. new一个你想要的组件，比如JLabel、JTextArea等等\n&emsp;&emsp;&emsp;&emsp;2. 调用`add()`方法并指定添加到东南西北中哪个方位\n#### 关闭按钮\n&emsp;&emsp;然而实际上自己运行一下会发现，我们做的窗口点击关闭按钮后，窗口确实消失了，但是你的IDE(比如eclipse、IDEA)并没有显示这个进程退出了，也就是说你的窗口程序还在运行。\n&emsp;&emsp;所以想让窗口的关闭按钮正确结束掉你的窗口程序，常用的有以下<font color=red>两种方法：</font>\n&emsp;&emsp;&emsp;&emsp;1. 调用<code>addWindowListener()</code>传入一个WindowAdapter对象并重写<code>windowClosing()</code>方法，方法内执行<code>System.exit(0);</code>\n![JFrameBasic7](JFrameBasic7.png)\n&emsp;&emsp;&emsp;&emsp;2. 调用<code>setDefaultCloseOperation()</code>并传入一个int值，例如3相当于<code>EXIT_ON_CLOSE</code>\n![JFrameBasic8](JFrameBasic8.png)\n&emsp;&emsp;<font color=red>反正我是喜欢第二种关闭方法，一句话搞定。</font>\n#### JPanel(面板)的作用\n&emsp;&emsp;通过以上案例，你已经能写出一个可以随心所欲设置放置到哪个位置的窗口程序，并且关闭按钮也可以正常结束掉程序。但是有没有注意到，我们第一次添加那个JLabel文本标签的时候，它是被放入到一个JPanel面板中的，而且它竟然居中了，这是由于JPanel默认为FlowLayout流式布局，会从左到右依次排列组件并且居中。\n&emsp;&emsp;也就是说，<font color=red>我们可以把一些乱七八糟的组件都加入到一个JPanel面板中，最后一次性把JPanel加入到JFrame的某个位置就行了。</font>\n![JFrameBasic9](JFrameBasic9.png)\n&emsp;&emsp;运用同样的方法，你可以把一些乱七八糟的组件都加到另一个面板中，然后一次性把面板加入到其他位置，比如SOUTH南方放置上面案例中的那些组件，而EAST东方可以放置一些按钮，这些按钮都放在一个面板中，NORTH北方也可以放一个面板，面板里面可以放置一下乱七八糟的组件...","tags":["Java"],"categories":["JavaSE知识总览"]},{"title":"Java-swing图形化——小小留言板","url":"/2021/05/01/Java-Swing/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n\n<font size=6><center>实验四 图形界面及应用系统的设计</center></font>\n\n# 要求：\n\n&emsp;&emsp;制作如图一个留言板的界面，并按要求加入所需控件，并能按要求进行窗口控件的布局。并按要求为按钮、文本框、窗口添加事件，使之实现提交显示留言，清屏，留言至顶和至尾。要求文本框能自动产生滚动条，界面美观。\n\n<font size=2> \n<b>注意：</b></br>\n1、通过两个文本文件存储表情和留言内容。</br>\n&emsp;&emsp;1）Expression.dat：用于存储表情，如：微笑、大哭、流泪等；每个表情占一行。</br>\n&emsp;&emsp;2）Msg.dat：用于存储留言信息，留言信息格式为：\n `[2019-10-04 12:35] 你微笑地说：今天下午去图书馆吗？`\n 其中：时间是提交留言的时间，每条留言一行。</br>\n2、窗口启动的时候（或点击“查看”按钮时），从 Msg.dat 文件中读出所有留言\n记录，显示在文本框中；从 Expression.dat 文件中读出所有表情记录，显示在\n表情下拉列表中。将 Msg.dat 中所有留言记录，倒序显示在文本域中。 </br>\n3、点击提交按钮，将表单内容存入文件，同时刷新文本框的留言内容。</br>\n4、“清屏”代表清除留言框内容，“至顶”和“至尾”功能是当留言内容过多时，\n将滚动条滚动到最上面或最下面，实现过程为移动文本区域里面的光标，使\n其指向第一个位置和最后一个位置。（设置光标位置函数为：setCaretPosition（int 位置））</br>\n5、（选作）增加“表情维护”按钮，在新窗口修改表情文件的内容</br>\n6、（选作）增加“删除留言”按钮，在新窗口通过选择数字或全部删除相关的留言内容</br>\n7、时间安排：前 3 学时，做界面和基本的显示效果操作事件，后 3 学时将文件操作引入相关的事件中。\n</font>\n\n# 思路：\n\n定义3个类，分别为留言板主界面类MessageBoard，表情维护类ExpressionManager，删除留言类DeleteMsg</br><b>重要方法：</b></br>&emsp;&emsp;1.在多个方法之间产生交互的组件最好定义为全局成员变量</br>&emsp;&emsp;2.显示窗口的2个必不可少的步骤：setSize()和setVisible(true)</br>&emsp;&emsp;3.根据需要选择合适的Layout，最外层窗口选择BorderLayout最合适</br>&emsp;&emsp;4.可以利用方法的可变参数列表来一次性初始化多个组件，很方便！</br>&emsp;&emsp;5.新增、删除、修改留言之后要再次调用相应的load方法重新载入达到刷新效果</br>&emsp;&emsp;6.其他类需要调用主类的方法时，可以把主类方法设置为静态static的</br>&emsp;&emsp;7.JList类配合Collection集合类进行增删改</br>&emsp;&emsp;8.把集合内容作为参数作为JList的数据，调用setListData()方法</br>\n# 代码：\n\n```c MessageBoard类(含main方法)：\npackage com.experiment.exp4;\n/*\n    小小留言板类，此类实现了一个简易的留言板，包括留言/展示、选择表情、清屏、滚动到顶部/底部、删除留言等功能\n    继承于JFrame类，为按钮增加了ActionListener监听器，利用ComponentListener监听窗口大小改变以实现标题居中\n        1.整体采用BorderLayout布局：\n            1.1 CENTER区域显示留言内容，并可以随时更新\n            1.2 EAST区域放置常用功能，例如清屏、至顶/尾、查看、表情维护、删除留言功能\n            1.3 SOUTH区域可以输入留言，并选择表情\n        2.在构造方法中指定窗口的标题、设置窗口大小、并初始化组件(加入窗口组件监听、限制窗口大小、设置窗口居中屏幕以及标题居中)\n        3.标题居中：通过FontMetrics类的stringWidth()方法得到当前字体所占用的宽度，来计算出需要在标题前补多少个空格\n        4.CENTER区域的JTextArea设置自动换行，并放入JScrollPane中，设置垂直滚动条\n        5.EAST区域的6个按钮放置在8行1列的GridLayout布局中，方便排列\n        6.SOUTH区域采用FlowLayout布局，并利用setPreferredSize设置面板大小\n        7.在actionPerformed()方法中分别对各个按钮的事件进行动作响应：\n            7.1 留言按钮除了会把输入框的内容追加到JTextArea中，还会调用saveMsg()方法保存到本地文本文件中\n            7.2 清屏按钮仅对JTextArea区域清空，不会影响本地文本文件中的内容\n            7.3 查看功能会调用loadMsg()再次载入本地文本文件中的留言内容，loadMsg()在WinInit()过程中会被初次调用\n            7.4 至顶：即调用setCaretPosition()并传入参数为0，把光标移至文本开头\n            7.5 至尾：即调用setCaretPosition()并传入参数为JTextArea实例的字符串长度，相当于把光标移至末尾\n            7.6 表情维护：调用manageExp()产生ExpressionManager对象，此对象会生成一个JFrame窗口，用于管理表情\n            7.7 删除留言：调用deleteMsg()产生DeleteMsg对象，此对象会生成一个JFrame窗口，用于删除留言\n        8.有个坑：双显示器场景下，窗口在副屏选中了JComboBox的某个内容，点击按钮(表情维护、删除留言)产生新JFrame对象时会导致那个按钮所处的面板下方花屏，猜测是面板没有刷新导致。仅在双显示器窗口位于副屏下重现。解决方法：调用repaint()方法重绘面板组件。\n */\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.awt.event.ComponentEvent;\nimport java.awt.event.ComponentListener;\nimport java.io.BufferedReader;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/5/1 11:07\n * @Version 1.0\n */\npublic class MessageBoard extends JFrame implements ComponentListener, ActionListener {\n    private static JTextArea textArea;\n    private static JComboBox<String> expressions;\n    private JTextField inputText;\n    private JButton submit;\n    private JButton clear;\n    private JButton toTop;\n    private JButton toTail;\n    private JButton viewAll;\n    private JButton expressionManager;\n    private JButton delete;\n    private final String title;\n    public static final String filePath = \"C:\\\\Users\\\\SBTTG\\\\Desktop\\\\\" +\n            \"IDEA\\\\Demo\\\\src\\\\main\\\\java\\\\com\\\\experiment\\\\exp4\";\n\n    public MessageBoard(String title) throws HeadlessException {\n        super(title);\n        this.title = title;\n        setSize(640, 480);\n        WinInit(title);\n    }\n\n    public static void main(String[] args) {\n        MessageBoard mb = new MessageBoard(\"小小留言板\");\n    }\n\n    private void WinInit(String title) {\n        setLayout(new BorderLayout()); //设置整体JFrame布局为BorderLayout\n        setTitleCenter(title); //设置标题居中\n        addComponentListener(this); //加入组件监听器，主要是为了检测窗口大小变动\n        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //设置关闭按钮功能\n        setLocationRelativeTo(null); //设置窗口位置默认为屏幕中间\n        setMinimumSize(new Dimension(560, 420)); //设置窗口最小尺寸\n        //实例化组件\n        initTopPanel();\n        initRightPanel();\n        initBelowPanel();\n        initCenterPanel();\n        //下面的语句利用可变参数列表一次性设置按钮样式和监听器，我也太J8机智了\n        setButtonStyleAndListener(clear, toTop, toTail, viewAll, submit, expressionManager, delete);\n        setVisible(true); //设置可见性-->必须放在最后面，否则此语句后面的语句不生效，需要产生事件刷新屏幕才会生效\n    }\n\n    private void initCenterPanel() {\n        textArea = new JTextArea();\n        textArea.setText(\"留言内容：\\n\");\n        loadMsg();\n        textArea.setEditable(false);\n        textArea.setWrapStyleWord(true);\n        textArea.setLineWrap(true);\n        textArea.setFont(new Font(\"微软雅黑\", Font.PLAIN, 14));\n//        textArea.append(\"呵呵哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈\\n\");\n        JScrollPane centerScrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,\n                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);\n        add(centerScrollPane, BorderLayout.CENTER);\n    }\n\n    private void initBelowPanel() {\n        JPanel below = new JPanel();\n        below.setLayout(new FlowLayout());\n        below.setPreferredSize(new Dimension(540, 40)); //BorderLayout下只能用这种方式修改面板大小\n        below.add(new JLabel(\"你\"));\n        expressions = new JComboBox<>();\n//        expressions.addItem(\"微笑\");\n//        expressions.addItem(\"开心\");\n        loadExp();\n        below.add(expressions);\n        below.add(new JLabel(\"地说：\"));\n        submit = new JButton(\"提交\");\n        inputText = new JTextField(20);\n        below.add(inputText);\n        below.add(submit);\n        add(below, BorderLayout.SOUTH);\n    }\n\n    private void initTopPanel() {\n        JPanel top = new JPanel();\n        JLabel title = new JLabel(\"留言板\");\n        title.setVerticalAlignment(SwingConstants.CENTER);\n        top.add(title);\n        add(top, BorderLayout.NORTH);\n    }\n\n    private void initRightPanel() {\n        JPanel right = new JPanel();\n        right.setLayout(new GridLayout(8, 1, 0, 25));\n        clear = new JButton(\"清屏\");\n        toTop = new JButton(\"至顶\");\n        toTail = new JButton(\"至尾\");\n        viewAll = new JButton(\"查看\");\n        expressionManager = new JButton(\"表情维护\");\n        delete = new JButton(\"删除留言\");\n        right.add(clear);\n        right.add(toTop);\n        right.add(toTail);\n        right.add(viewAll);\n        right.add(expressionManager);\n        right.add(delete);\n        add(right, BorderLayout.EAST);\n    }\n\n    private void setButtonStyleAndListener(JButton... buttons) {\n        for (JButton btn : buttons) {\n            btn.addActionListener(this); //加入按钮动作监听器\n            btn.setFocusPainted(false); //取消掉按钮点击后的虚线框\n            btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); //设置光标悬停样式为小手\n        }\n    }\n\n    private void setTitleCenter(String title) {\n        setFont(new Font(\"System\", Font.PLAIN, 14));\n        FontMetrics fm = this.getFontMetrics(this.getFont());\n        String spaces = String.format(\"%\" + ((this.getWidth() - title.length()) / 2\n                / fm.stringWidth(\" \") - fm.stringWidth(title) / 4) + \"s\", \"\");\n        setTitle(spaces + title);\n    }\n\n    public static void loadMsg() {\n        textArea.setText(\"留言内容：\");\n        try {\n            BufferedReader reader = new BufferedReader(new FileReader(filePath + \"\\\\Msg.dat\"));\n            String msg;\n            while ((msg = reader.readLine()) != null) {\n                textArea.insert(\"\\n\" + msg, \"留言内容：\".length());\n            }\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n    }\n\n    public static void loadExp() {\n        expressions.removeAllItems();\n        try {\n            BufferedReader reader = new BufferedReader(new FileReader(filePath + \"\\\\Expression.dat\"));\n            String exp;\n            while ((exp = reader.readLine()) != null) {\n                expressions.addItem(exp);\n            }\n            reader.close();\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n    }\n\n    private void saveMsg(String msg) {\n        try {\n            //注意这里的FileWriter构造方法应该调用带是否追加的，即第二个参数设定为true，否则默认覆盖\n            FileWriter fw = new FileWriter(MessageBoard.filePath + \"\\\\Msg.dat\", true);\n            fw.append(msg).append(\"\\n\");\n            fw.flush();\n            fw.close();\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (e.getSource() == submit) {\n            if (!(inputText.getText().equals(\"\"))) {\n                SimpleDateFormat sf = new SimpleDateFormat(\"yyyy-MM-dd hh:mm\");\n                String msg = \"[\" + sf.format(new Date()) + \"]\" + \" 你\"\n                        + expressions.getSelectedItem() + \"地说：\" + inputText.getText();\n                textArea.insert(\"\\n\" + msg, \"留言内容：\".length());\n                saveMsg(msg);\n                inputText.setText(null);\n            }\n        } else if (e.getSource() == clear) {\n            textArea.setText(\"留言内容：\\n\");\n        } else if (e.getSource() == toTop) {\n            textArea.setCaretPosition(0);\n        } else if (e.getSource() == toTail) {\n            System.out.println(\"文本长度：\" + textArea.getText().length());\n            textArea.setCaretPosition(textArea.getText().length());\n        } else if (e.getSource() == viewAll) {\n            System.out.println(\"查看\");\n            loadMsg();\n        } else if (e.getSource() == expressionManager) {\n            manageExp();\n            right.repaint(); //我擦？竟然有残影，利用repaint()刷新面板解决\n        } else if (e.getSource() == delete) {\n            deleteMsg();\n            right.repaint(); //同理\n        }\n    }\n\n    @Override\n    public void componentResized(ComponentEvent e) {\n        setTitleCenter(title);\n    }\n\n    @Override\n    public void componentMoved(ComponentEvent e) {\n\n    }\n\n    @Override\n    public void componentShown(ComponentEvent e) {\n\n    }\n\n    @Override\n    public void componentHidden(ComponentEvent e) {\n\n    }\n\n    private void manageExp() {\n        ExpressionManager em = new ExpressionManager(\"表情\");\n        System.gc();\n    }\n\n    private void deleteMsg() {\n        DeleteMsg dm = new DeleteMsg(\"删除\");\n        System.gc();\n    }\n}\n\n```\n\n```c ExpressionManager类：\npackage com.experiment.exp4;\n/*\n    表情维护 继承于JFrame：\n        1.采用BorderLayout布局\n            1.1 CENTER部分放置一个可编辑的JTextArea(加入滚动条)，用于操作修改表情文本\n            1.2 SOUTH部分放置2个按钮，分别用于确定和取消\n        2.首先在构造方法中设置窗口大小，调用初始化方法来初始化组件，设置可见性为可见\n        3.对于初始化方法，可以利用可变参数列表来适应参数个数，达到一次性设置按钮样式以及监听器\n        4.把CENTER区域的JTextArea放入JScrollPane中，设置产生垂直滚动条\n        5.在按钮动作监听事件中分别处理2个按钮的行为：\n            4.1 确认按钮：在按下此按钮之前，可以任意修改JTextArea区域的表情文本\n                        当按下此按钮，调用saveExp()方法保存表情\n                        要注意保存完毕后刷新面板，即调用loadExp()方法重新载入表情\n            4.2 取消按钮：直接调用dispose()方法关闭此Window，不影响其他JFrame\n */\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.io.BufferedReader;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/5/1 16:01\n * @Version 1.0\n */\npublic class ExpressionManager extends JFrame implements ActionListener {\n    static JButton confirm;\n    static JButton cancel;\n    JTextArea textArea;\n    JPanel btnPanel;\n\n    public ExpressionManager(String title) throws HeadlessException {\n        super(title);\n        setSize(200, 250);\n        WinInit();\n        setVisible(true);\n    }\n\n    private void WinInit() {\n        setLayout(new BorderLayout());\n        setLocationRelativeTo(null);\n        setMinimumSize(new Dimension(230, 300));\n        initContent();\n        initBtnPanel();\n        setButtonAction(confirm, cancel);\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n    }\n\n    private void setButtonAction(JButton... buttons) {\n        for (JButton button : buttons) {\n            button.addActionListener(this);\n        }\n    }\n\n    private void initContent() {\n        textArea = new JTextArea();\n        textArea.setFont(new Font(\"微软雅黑\", Font.PLAIN, 16));\n        try {\n            BufferedReader reader = new BufferedReader(new FileReader(MessageBoard.filePath\n                    + \"\\\\Expression.dat\"));\n            String exp;\n            while ((exp = reader.readLine()) != null) {\n                textArea.append(exp + \"\\n\");\n            }\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n        JScrollPane centerScrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,\n                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);\n        add(centerScrollPane, BorderLayout.CENTER);\n    }\n\n    private void initBtnPanel() {\n        btnPanel = new JPanel();\n        btnPanel.setPreferredSize(new Dimension(180, 40));\n        confirm = new JButton(\"确认\");\n        cancel = new JButton(\"取消\");\n        btnPanel.add(confirm);\n        btnPanel.add(cancel);\n        add(btnPanel, BorderLayout.SOUTH);\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (e.getSource() == confirm) {\n            System.out.println(\"确定\");\n            saveExp();\n            MessageBoard.loadExp(); //记得刷新主面板的表情列表\n            dispose();\n        } else if (e.getSource() == cancel) {\n            System.out.println(\"取消\");\n            dispose();\n        }\n    }\n\n    private void saveExp() {\n        try {\n            FileWriter fw = new FileWriter(MessageBoard.filePath + \"\\\\Expression.dat\");\n            fw.write(\"\");\n            fw.write(textArea.getText());\n            fw.flush();\n            fw.close();\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n    }\n}\n\n```\n```c DeleteMsg类：\npackage com.experiment.exp4;\n/*\n    删除留言 继承于JFrame：\n        1.采用BorderLayout布局：\n            1.1 CENTER部分放置一个JList，用于显示所有留言\n            1.2 SOUTH部分放置3个JButton，分别用于删除单项、删除全部、取消\n        2.首先在构造方法中设置窗口大小，调用初始化方法来初始化组件，设置可见性为可见\n        3.对于初始化方法，可以利用可变参数列表来适应参数个数，达到一次性设置按钮样式以及监听器\n        4.在按钮动作监听事件中分别处理3个按钮的行为：\n            4.1 取消按钮直接调用dispose()释放此Window，不影响其他JFrame，相当于关闭此窗口\n            4.2 删除按钮：如果选中了一个JList中的内容，那么调用delete()方法移除它\n                4.2.1 发现一个问题，直接调用JList实例的remove()方法并不会正确移除\n                      但可以通过其他手段处理列表元素(例如通过集合中的remove和add等方法)\n                      然后通过处理过后的集合作为参数，调用setListData(E[] listData)\n                也就是说，操作JList数据实际上是通过另一个Collection集合来进行增删改\n                把修改完成之后的Collection集合内容作为参数重新产生JList的数据\n            4.3 删除全部：同样的，也是通过Collection集合的clear()方法清空集合\n                        然后把这个空集合作为参数，调用setListData()达到清空JList效果\n */\n\nimport javax.swing.*;\nimport java.awt.*;\nimport java.awt.event.ActionEvent;\nimport java.awt.event.ActionListener;\nimport java.io.BufferedReader;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.util.ArrayList;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/5/1 17:16\n * @Version 1.0\n */\npublic class DeleteMsg extends JFrame implements ActionListener {\n    JButton delete;\n    JButton deleteAll;\n    JButton cancel;\n    JPanel btnPanel;\n    JList<String> msgList;\n    ArrayList<String> messages;\n\n    public DeleteMsg(String title) throws HeadlessException {\n        super(title);\n        setSize(400, 300);\n        WinInit();\n        setVisible(true);\n    }\n\n    private void WinInit() {\n        setLocationRelativeTo(null);\n        setLayout(new BorderLayout());\n        msgList = new JList<>();\n        msgList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\n        loadList();\n        add(msgList, BorderLayout.CENTER);\n        btnPanel = new JPanel();\n        delete = new JButton(\"删除\");\n        deleteAll = new JButton(\"删除全部\");\n        cancel = new JButton(\"取消\");\n        btnPanel.add(delete);\n        btnPanel.add(deleteAll);\n        btnPanel.add(cancel);\n        setButtonStyleAndListener(delete, deleteAll, cancel);\n        add(btnPanel, BorderLayout.SOUTH);\n        setMinimumSize(new Dimension(400, 300));\n        setDefaultCloseOperation(DISPOSE_ON_CLOSE);\n    }\n\n    private void setButtonStyleAndListener(JButton... buttons) {\n        for (JButton btn : buttons) {\n            btn.addActionListener(this); //加入按钮动作监听器\n            btn.setFocusPainted(false); //取消掉按钮点击后的虚线框\n            btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); //设置光标悬停样式为小手\n        }\n    }\n\n    @Override\n    public void actionPerformed(ActionEvent e) {\n        if (e.getSource() == cancel) {\n            System.out.println(\"取消\");\n            dispose();\n        } else if (e.getSource() == delete) {\n            System.out.println(\"删除\");\n            delete();\n            loadList();\n        } else if (e.getSource() == deleteAll) {\n            System.out.println(\"删除全部\");\n            deleteAll();\n            loadList();\n        }\n    }\n\n    private void deleteAll() {\n        messages.clear();\n        updateMsgList();\n    }\n\n    private void delete() {\n        if (msgList.getSelectedIndex() != -1) {\n            messages.remove(msgList.getSelectedIndex());\n            updateMsgList();\n        }\n    }\n\n    private void updateMsgList() {\n        try {\n            FileWriter fw = new FileWriter(MessageBoard.filePath + \"\\\\Msg.dat\");\n            fw.write(\"\");\n            for (String message : messages) {\n                fw.write(message + \"\\n\");\n            }\n            fw.flush();\n            fw.close();\n            MessageBoard.loadMsg();\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n    }\n\n    private void loadList() {\n        try {\n            BufferedReader reader = new BufferedReader(new FileReader(MessageBoard.filePath + \"\\\\Msg.dat\"));\n            messages = new ArrayList<>();\n            String msg;\n            while ((msg = reader.readLine()) != null) {\n                messages.add(msg);\n            }\n            String[] msgs = new String[messages.size()];\n            for (int i = 0; i < messages.size(); i++) {\n                msgs[i] = messages.get(i);\n            }\n            msgList.setListData(msgs);\n        } catch (IOException e) {\n            System.out.println(e.getMessage());\n        }\n    }\n}\n\n```\n\n# 运行截图：\n\n![main](main.png)![expression](expression.png)![delete](delete.png)![compare](compare.png)\n\n\n# 心得：\n\n<font size=3>\n&emsp;&emsp;五一第一天，室友凌晨1点就在学习疯狂卷！睡到中午11点赶紧开始今天一天的学习，总的来说这次实验掌握了swing的很多方法，也踩了好几个坑，例如<code>setVisible()</code>位置不对，导致后初始化的组件显示不出来，这时候拖动窗口缩放才会显示，困扰了半天，原来只需要把<code>setVisible()</code>放在组件初始化语句后面就可以正常显示了。<br>\n&emsp;&emsp;在JList的使用上也有很多坑，JList自带的<code>remove()</code>方法和<code>removeAll()</code>方法居然不能正常移除列表中的元素，而且还会报下标越界异常，真的🐔8坑。好在解决方法是：定义一个Collection集合作为JList的操作辅助，因为Collection集合刚好也带有<code>remove()、clear()、add()</code>这些方法，在Collection集合中操作内容之后，把每一个元素全部存入一个对象数组中，作为参数传入JList的<code>setListData()</code>方法重新设置JList内容就可以变相操作JList。（妈的为什么不能直接用JList的<code>remove()</code>等等方法啊，但是<code>getSelectedIndex()</code>又可以正常获取到点击的下标，害！）<br>\n&emsp;&emsp;swing现在用的好像并不多，虽然是所谓的跨平台窗口库，但是没什么卵用。<br>\n&emsp;&emsp;JavaWeb才是永远滴神，有浏览器就能用，为什么不学JavaWeb呢？</br>&emsp;&emsp;耗时一天完成了这个swing小程序，终于脱离Console枯燥的文字了，图形化编程还是挺有成就感的🤩！</font>","tags":["Java","Swing"],"categories":["Java实训题"]},{"title":"C++第一次实验","url":"/2021/04/26/C-FirstExp/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n<font size=6><center>实验一 C++运行环境及基础语法</center></font>\n\n# 要求：\n&emsp;&emsp;1.熟悉C++的开发运行环境\n&emsp;&emsp;2.掌握C++的基本语法\n&emsp;&emsp;3.熟悉结构化程序设计\n\n# 思路：\n&emsp;&emsp;`全是有手就行的题`\n\n# 代码：\n## 最简单的C++程序运行调试\n```c\n#include <iostream>\n\nvoid main()\n{\n\tstd::cout<<\"my first cpp program\"<<std::endl;\n}\n```\n## 结构化程序设计基础\n用long long存放结果：\n```c\n#include<iostream>\nusing namespace std;\nint main1(){\n\tlong long n,fact=1;\n\tcout<<\"input a num:\";   //这条语句有错，应怎么改？\n\tcin>>n;\n\tint i=1;\n\tfor(;i<=n;i++)\n\t{\n\t\tfact=fact*i;\n\t}\n\tcout<<n<<\"!=\"<<fact<<endl;\n\tcout<<i<<endl;\n\treturn 0;\n}\n```\n用数组存放结果：\n```c\n#include <iostream>\nusing namespace std; \nint main()\n{\n    int n;\n    int arr[100];\n    int num = 1; \n    int temp;\n    int i, j, c;\n    cout << \"输入一个n：\";\n    cin >> n;\n    arr[0] = 1;\n    for (i = 2; i <= n; i++)\n    {\n        for (j = 1, c = 0; j <= num; j++)\n        {\n            temp = arr[j - 1] * i + c;\n            arr[j - 1] = temp % 10;\n            c = temp / 10;\n        }\n        while (c)\n        {\n            arr[++num - 1] = c % 10;\n            c = c / 10;\n        }\n    }\n    cout << n << \"的阶乘为：\";\n    for (j = num; j >= 1; j--)\n    {\n        cout << arr[j - 1];\n    }\n    cout << endl;\n    return 0;\n}\n```\n## 引用和函数\n```c\n#include<iostream>\nusing namespace std;\nvoid swap(int &a, int &b){\n\tint t;\n\tt=a;  a=b;  b=t;\n}\nint main(){\n\tint a=1,b=6;\n\tcout<<\"before swap:\\n\"<<\"a is:\"<<a\n\t\t<<\"  b is:\"<<b<<endl;\n\tswap(a,b);\n\tcout<<\"after  swap:\\n\"<<\"a is:\"<<a\n\t\t<<\"  b is:\"<<b<<endl;\n\treturn 0;\n}\n```\n## 函数的重载和默认参数\n```c\n#include <iostream>\nusing namespace std;\nvoid print(float y,char sex)\n{\n\tcout << (int)(y + 0.5) << \" years old,\" << (sex == 'M' ? \"male\" : \"female\") << endl;\n}\n\nvoid print(char sex,float y)\n{\n\tcout << (int)(y + 0.5) << \" years old,\" << (sex == 'M' ? \"male\" : \"female\") << endl;\n}\n\nvoid print()\n{\n\tcout<<\"0 years old,male\"<<endl;\n}\n\nint main(){\n\tfloat year;  char sex;\n\t//输入\n\tcin>>year>>sex;\n\tprint(year,sex);\n\tprint(sex,year);\n\tprint();           //这里输出0 years old， male，即默认是0岁，male\n\treturn 0;\n}\n```\n## 自己编写程序\n### 从键盘上读入两数，比较两个数的大小，并按从小到大的次序输出。（要求使用变量引用） \n```c\n#include <iostream>\n//（1）、从键盘上读入两数，比较两个数的大小，并按从小到大的次序输出。\n//（要求使用变量引用）\nusing namespace std;\n\nvoid input(int& x, int& y);\nvoid compare(int& x, int& y);\n\nint main()\n{\n\tint x, y;//利用变量引用输入x和y\n\tinput(x, y);//按照从小到大次序输出\n\tcompare(x, y);\n\treturn 0;\n}\n\nvoid input(int& x, int& y)\n{\n\tcout << \"输入x以及y：\";\n\tcin >> x >> y;\n}\n\nvoid compare(int& x, int& y)\n{\n\tx < y ? cout << x << \",\" << y << endl : cout << y << \",\" << x << endl;\n}\n\n```\n### 输出1到1000的素数，一行5个。\n```c\n#include <iostream>\n#include <cmath>\n#include <iomanip>\n//（2）、输出1到1000的素数，一行5个。\nusing namespace std;\nint main()\n{\n\tint count = 0;\n\tbool isPrime;\n\tfor (int i = 2; i < 1000; i++) {\n\t\tisPrime = true; \n\t\t//对于每一个i(i>=2)先假定为素数\n\t\tfor (int inside = 2; \n\t\t\tinside <= sqrt(i); \n\t\t\tinside++) {\n\t\t\tif (i % inside == 0) { \n\t\t\t\tisPrime = false; \n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (isPrime) {\n\t\t\tcout<<setw(4)<<i;\n\t\t\tif (++count % 5 == 0) {\n\t\t\t\t//每5个数换一行\n\t\t\t\tcout << endl;\n\t\t\t}\n\t\t}\n\t}\n}\n```\n### 编写一个C++风格的程序，解决百元问题：将一元人民币兑换成1、2、5分的硬币，有多少种换法？\n```c\n#include <iostream>\n//（3）、编写一个C++风格的程序，解决百元问题，有多少种换法？\nusing namespace std;\nint main()\n{\n\tint x, y, z;\t//1分、2分、5分的硬币数目分别用x、y、z表示\n\tint sum = 0;\t//换法计数器\n\tfor (z = 0; z <= 20; z++)\n\t\tfor (y = 0; y <= 50; y++)\n\t\t{\n\t\t\tfor (x = 0; x <= 100 - y - z; x++)\n\t\t\t{\n\t\t\t\tif (x + 2 * y + 5 * z == 100)\n\t\t\t\t{\t//cout << x << \",\" << y << \",\" << z << endl;\n\t\t\t\t\tsum++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcout << \"有\" << sum << \"种换法\" << endl;\n}\n```\n### 编写一个程序，建立一个上sroot（）的函数，返回其参数的二次方根。要求使用重载函数，重载3次，让其返回整数、长整数与双精度数的二次方根。\n```c\n#include <iostream>\n//（4）、编写一个程序，建立一个上sroot（）的函数，返回其参数的二次方根。\n//\t\t要求使用重载函数，重载3次，让其返回整数、长整数与双精度数的二次方根。\nusing namespace std;\ndouble sroot(int num);\ndouble sroot(long num);\ndouble sroot(double num);\n\nint main()\n{\n\tcout << sroot(10) << endl;\n\tcout << sroot(20L) << endl;\n\tcout << sroot(30.0) << endl;\n}\n\ndouble sroot(int num)\n{\n\treturn sqrt(num);\n}\ndouble sroot(long num)\n{\n\treturn sqrt(num);\n}\ndouble sroot(double num)\n{\n\treturn sqrt(num);\n}\n```\n### 写C++风格的程序，用二分法求解f（x）=0的根。\n```c\n#include <iostream>\n#include<cmath>\n//（5）、写C++风格的程序，用二分法求解f（x）=0的根。\nusing namespace std;\nint main()\n{\n    float x0, x1, x2, func, fx1, fx2;\n    do\n    {\n        cout << \"输入范围：\" << endl;\n        cin >> x1 >> x2;\n        fx1 = sin(x1);\n        fx2 = sin(x2);\n    } while (fx1 * fx2 > 0);\n    do\n    {\n        x0 = (x1 + x2) / 2;\n        func = sin(x0);\n        if ((func * fx1) < 0)\n        {\n            x2 = x0;\n            fx2 = func;\n        }\n        else\n        {\n            x1 = x0;\n            fx1 = func;\n        }\n    } while (fabs(func) >= 1e-5);\n    cout << \"x = \" << x0 << endl;\n    return 0;\n}\n```\n### 编写一个程序，用动态分配空间的方法计算 Fibonacci数列的前20项并存储到动态分配的空间中。注：使用malloc和free来实现\n```c\n#include<iostream>\n#include <iomanip>\n//（6）、编写一个程序，用动态分配空间的方法计算\n//     Fibonacci数列的前20项并存储到动态分配的空间中。\n//\t\t注：使用malloc和free来实现\nusing namespace std;\n\nint main() {\n\tint* p = (int*)malloc(sizeof(int) * 20);\n\t*p = 0; //第一个数为0\n\t*(p + 1) = 1; //第二个数为1\n\tfor (int i = 2; i < 20; ++i) \n\t{\n\t\t*(p + i) = *(p + i - 1) + *(p + i - 2);\n\t}\n\tfor (int i = 0; i < 20; ++i) \n\t{\n\t\tcout << \"第\" <<setw(2)<< i + 1 << \"项：\";\n\t\tcout <<setw(4)<< *(p + i)<<\"\\t\\t\";\n\t\tif ((i + 1) % 2 == 0)\n\t\t{\n\t\t\tcout << endl;\n\t\t}\n\t}\n\tcout << endl;\n\tfree(p);\n\treturn 0;\n}\n```\n\n# 运行截图：\n\n![C-FirstExp](1.png)![C-FirstExp](2.png)\n\n# 心得：\n\n<font size=3>&emsp;`main()`函数的返回值用于判断程序是否正常结束，通常，返回0表示程序正常结束。</br>&emsp;在Visual Studio 2019环境下，`main()`返回值可定义为void类型，但根据C99标准，`main()`的标准定义格式为：\n &emsp;`int main(void) `\n &emsp;或：\n&emsp;`int main(int argc,char *argv[]) `\n&emsp;`main()`函数也可以省略return 0，编译器会自动调用`exit(0)`来析构栈变量。\n&emsp;`using namespace std`也可以不写，但是这时想要使用`cout`或者`endl`等位于`std`命名空间下的，如果不引入`std`命名空间，那么需要使用作用域运算符`::`来说明命名空间来源，例如：`std::cout`或者`std::endl`</br>&emsp;在老的C89编译器之前使用C语言的for循环，不能再for循环的`表达式1`位置定义变量，只能写在for循环之前，而新标准之后的C++中，可以在for循环里定义变量，此时的变量作用域在for循环内部。\n</font>\n\n<center><font size=4>感谢阅读💗</font></center>","tags":["C++"],"categories":["C++实验"]},{"title":"Java身份证验证CardID类","url":"/2021/03/17/Java-CardID/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n\n<font size=6><center>实验三 身份证号码验证及个人信息输出。</center></font>\n\n# 要求：\n输入身份证号码，进行三重验证，并且输出对应信息。\n<font size=3> 1、身份证验证主要包含：</br>&emsp;&emsp;1. 位数错误：正确应该为 18 位。</br>&emsp;&emsp;2. 字符错误：前面 17 个应该是数字，最后一位可以为‘X’或‘x’;</br>&emsp;&emsp;3. 校验码错误：\n   &emsp;&emsp;&emsp;&emsp;身份证的最后一位为校验码，校验码是用于验证前面的数字是否有错误，校\n   验码运算规则：\n   &emsp;&emsp;&emsp;&emsp;（1）先将最后一位设定为 0。\n   &emsp;&emsp;&emsp;&emsp;（2）效验码的计算公式为：(Σ (ai×wi))%11\n    &emsp;&emsp;其中：i 是从右向左的包括效验码在内的序号（1－18）\n    &emsp;&emsp;ai 是第 i 位的数字\n    &emsp;&emsp;wi 是第 i 位上的加权因子，其数值依据公式 wi=(2(i-1))%11，（计算 ab：函数为：java.lang.Math.pow(a,b)） i，ai，wi 的对应关系如下：\n\n```\n身份证号码： 5  1  0  1  0  1  1  9  8  8 0 8 0 8 0 1 2 0\n    i    ：18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1\n    wi   ： 用公式计算 2^(i-1)%11 …… 9 10 5 8 4 2 1\n```\n&emsp;&emsp;&emsp;&emsp;（3）求出(Σ (ai×wi))%11 之后依据下表得到效验码：\n```\n(Σ (ai×wi))%11 :0 1 2 3 4 5 6 7 8 9 10\n        校验码值:1 0 X 9 8  7 6 5 4 3 2\n```\n&emsp;&emsp;&emsp;&emsp;（4）计算 ab：函数为：java.lang.Math.pow(a,b)\n2、个人信息输出主要包含：\n&emsp;&emsp;1: 生日：如 1985 年 5 月 9 日。\n&emsp;&emsp;2: 年龄：如 20 岁，应当精确到日；\n&emsp;&emsp;3: 性别：顺序码的计数为，男性以奇数计数，女性以偶数计数</font>\n<font size=2>&emsp;&emsp;&emsp;&emsp;（1）编写一个身份证类，包含一个属性：身份证号；三个验证函数；一个信息输出函数。\n&emsp;&emsp;&emsp;&emsp;（2）编写一个测试类，可从键盘上输入一个身份证号码，产生一个身份证对象，然后调用其相关方法对身份证号码进行验证，没有通过验证的话，提示错\n误信息并让用户重新输入，如果通过验证，则输出该人的信息。\n代码框架如下：\n\n```\nclass CardID{\n    private String cardNum; \n    public void setCardNum(String cardNum){//换一个号码\n        this. cardNum= cardNum; }\n    public boolean lengthVerify(){○1 位数错误验证\n    }\n    public boolean charVerify(){○2 字符错误验证\n    }\n    public boolean checkcodeVerify(){○3 校验码错误验证\n    //测试时请将每一轮的 i,ai,wi,sum 输出，避免出错，成功后不再输出\n    }\n    public void output(){○4 信息输出\n    //按要求输出个人信息\n    }\n}\nclass CardIDDemo{\n    public static void main(String []args){\n    //用死循环反复测试输入、验证、输出\n    //注意第一步验证失败不能进入第二步，第二步失败不能进入第三步，所有验\n    证通过才能进行输出。\n    }\n}\n```\n</font>\n\n# 思路：\n\n需要两个类：Card类和CardID类\n其中:1.在Card类中定义String类型的cardNum变量\n&emsp;&emsp;2.在Card类中定义Getter/Setter方法\n&emsp;&emsp;3.在Card类中定义3种验证方法：\n&emsp;&emsp;&emsp;&emsp;3.1对于位数验证，直接判断输入的cardNum长度是否为18\n&emsp;&emsp;&emsp;&emsp;3.2对于字符验证，前17位为数字，最后一位还可以为大小写的'x'\n&emsp;&emsp;&emsp;&emsp;3.3对于校验码，需要取出每一位，转化为整形进行算术运算\n&emsp;&emsp;4.在output()输出方法中，输出生日、年龄、性别\n&emsp;&emsp;5.以上3种输出需求均在身份证号码中截取获得\n&emsp;&emsp;6.利用Calendar类不是必须的，但是逻辑清晰，有利于日期计算\n&emsp;&emsp;7.年龄判断不是简单的相减，需要考虑到月份和天数不够减情况下的处理\n# 代码：\n\n```c CardID类：\npackage com.experiment.exp3;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Objects;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/3 15:25\n * @Version 1.0\n */\npublic class CardID {\n    private String cardNum;\n\n    public void setCardNum(String cardNum) {\n        this.cardNum = cardNum;\n    }\n\n    //位数错误验证 （是否为18位）\n    public boolean lengthVerify() {\n        if (this.cardNum.length() == 18) {\n            return true;\n        } else {\n            System.out.print(\"位数验证错误！\");\n            return false;\n        }\n    }\n\n    //字符错误验证（前17位为数字，最后一位为'X'或'x'）\n    public boolean charVerify() {\n        char[] chars = this.cardNum.toCharArray();\n        for (int i = 0; i < chars.length - 1; i++) {\n            if (!('0' <= chars[i] && chars[i] <= '9')) {\n                System.out.print(\"字符验证错误！\");\n                return false;\n            }\n        }\n        if (('0' <= chars[17] && chars[17] <= '9') || chars[17] == 'x' || chars[17] == 'X') {\n            return true;\n        } else {\n            System.out.print(\"字符验证错误！\");\n            return false;\n        }\n    }\n\n    //校验码错误验证 （最后一位）\n    public boolean checkCodeVerify() {\n        //先把输入的字符串类型身份证号转为字符串数组\n        String[] cardNumStr = numStringToArray();\n        //定义整型数组ai，以存放身份证号每一位的整型值，方便算术运算\n        int[] ai = new int[18];\n        //先把最后一位设定为0\n        ai[ai.length - 1] = 0;\n        //把身份证号除最后一位外，依次存入整型数组ai中（因为最后一位可能为字符'x'或'X'）\n        for (int i = 0; i < cardNumStr.length - 1; i++) {\n            ai[i] = Integer.parseInt(cardNumStr[i]);\n        }\n        int[] wi = new int[18];\n        //计算wi[]数组每一位\n        for (int i = ai.length; i > 0; i--) {\n            wi[18 - i] = (int) Math.pow(2, (i - 1)) % 11;\n        }\n        //计算校验码 verifyCode = (∑(ai x wi))%11\n        int result = 0;\n        for (int i = 0; i < ai.length; i++) {\n            result += ai[i] * wi[i];\n        }\n        int verifyCode = result % 11;\n        //定义一个字符串数组，存储每一位校验码应该对应的校验码值\n        String[] verifyCodeList = {\"1\", \"0\", \"X\", \"9\", \"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\"};\n        //判断校验码是否正确\n        String lastChar = verifyCodeList[verifyCode];\n        System.out.println(\"身份证最后一位应该是\" + \"'\" + lastChar + \"'\");\n        //注意在判断时，校验码数组只有大写的'X'，用户输入的可能为小写，会导致误判。应该都转为小写/大写再判断\n        if (Objects.equals(cardNumStr[cardNumStr.length - 1].toLowerCase(), lastChar.toLowerCase())) {\n            return true;\n        } else {\n            System.out.println(\"校验码验证错误！\");\n            return false;\n        }\n    }\n\n    //信息输出\n    public void output() {\n        /*\n            生日 第7位到第10位年 第11位到第12位月 第13位到第14位日    yyyy年M月d日\n            年龄 今天 - 生日    精确到日\n            性别 第15到17位    奇数男 偶数女\n        */\n        /*调用相应的方法计算生日、年龄、性别*/\n//        生日\n        //格式化Calendar类的生日为指定格式\n        Calendar birthDate = calcBirth();\n        SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy年M月d日\");   //格式化时间成字符串\n        String birth = sdf.format(birthDate.getTime());\n//        年龄\n        String age = calcAge(birthDate);\n//        性别\n        String sex = calcSex();\n\n        //输出计算出的生日、年龄、性别\n        System.out.println(\"生日：\" + birth);\n        System.out.println(\"年龄：\" + age);\n        System.out.println(\"性别：\" + sex);\n        System.out.println(\"===========\");\n    }\n\n    //计算性别\n    private String calcSex() {\n\n        int numOfSex = Integer.parseInt(this.cardNum.substring(14, 17));\n        return numOfSex % 2 != 0 ? \"男\" : \"女\";\n    }\n\n    //计算年龄\n    private String calcAge(Calendar birthDateString) {\n        //解析生日到单独的变量中\n        int yearOfBirth = birthDateString.get(Calendar.YEAR);\n        int monthOfBirth = birthDateString.get(Calendar.MONTH) + 1;\n        int dayOfBirth = birthDateString.get(Calendar.DATE);\n        //获取今天的日期到单独变量中\n        Calendar nowDate = Calendar.getInstance();\n        int yearOfNow = nowDate.get(Calendar.YEAR);\n        int monthOfNow = nowDate.get(Calendar.MONTH) + 1;\n        int dayOfNow = nowDate.get(Calendar.DATE);\n        //计算年份差\n        int ageOfYear = yearOfNow - yearOfBirth;\n        //计算月份差\n        int ageOfMonth = monthOfNow - monthOfBirth;\n        if (ageOfMonth < 0) {    //今年的月份比出生月份小\n            ageOfYear--;         //年龄 - 1\n            ageOfMonth += 12;    //月份补12个月\n        }\n        //计算天数差\n        int ageOfDay = dayOfNow - dayOfBirth;\n        if (ageOfDay < 0) {     //天数比生日天数小\n            ageOfMonth--;       //月份 - 1\n            ageOfDay += nowDate.getActualMaximum(Calendar.DAY_OF_MONTH) + 1; //天数补齐一个月\n            /*这里有个小争议：如果活了某个月的最大天数，也可以算作活了这个月的下个月的第0天\n            对于某人的生日来说，+1 操作相当于把今天看做为新的一岁的第0天，而不是活了11月30天(因为活了12个月相当于就是下一年了)\n            加不加1就看怎么理解今天作为生日的话，到底是把今天算作这一岁的最后一天还是下一岁新的一天*/\n        }\n        //再次修正月份以及年份\n        if (ageOfMonth < 0) {   //上一步计算天数时，可能导致原本0月自减1变成-1\n            ageOfYear--;        //修正岁数 - 1\n            ageOfMonth += 12;   //月份补12个月\n        }\n        return ageOfYear + \"岁\" + ageOfMonth + \"月\" + ageOfDay + \"天\";\n    }\n\n    //计算生日\n    private Calendar calcBirth() {\n        //从输入的身份证号获取相应位置的字符串，转换成整数型\n        int year = Integer.parseInt(this.cardNum.substring(6, 10));   //注意substring()下标[begin,end)\n        int month = Integer.parseInt(this.cardNum.substring(10, 12));\n        int day = Integer.parseInt(this.cardNum.substring(12, 14));\n        Calendar birthDate = Calendar.getInstance();\n        //把Calendar类的birthDate设置为获取到的整数型的年、月、日\n        birthDate.set(Calendar.YEAR, year);\n        birthDate.set(Calendar.MONTH, month - 1);   //Calendar类月份从0到11月，实际上的8月只是MONTH的7月\n        birthDate.set(Calendar.DATE, day);\n        //把计算出来的birthDate作为Calendar类返回值带回\n        return birthDate;\n    }\n\n    //把输入的身份证字符串转化为字符串数组\n    private String[] numStringToArray() {\n        char[] chars = this.cardNum.toCharArray();\n        String[] numArray = new String[cardNum.length()];\n        for (int i = 0; i < cardNum.length(); i++) {\n            numArray[i] = String.valueOf(chars[i]);   //身份证号每一位的字符顺序存入int数组\n        }\n        return numArray;\n    }\n}\n\n```\n\n```c CardIDDemo类(含main方法)：\npackage com.experiment.exp3;\n/*\n    （1）编写一个身份证类，包含一个属性：身份证号；三个验证函数；一个\n    信息输出函数。\n    （2）编写一个测试类，可从键盘上输入一个身份证号码，产生一个身份证\n    对象，然后调用其相关方法对身份证号码进行验证，没有通过验证的话，提示错\n    误信息并让用户重新输入，如果通过验证，则输出该人的信息。\n    1。用死循环反复测试输入、验证、输出\n    2。注意第一步验证失败不能进入第二步，第二步失败不能进入第三步，所有验证通过才能进行输出。\n */\n\nimport java.util.Scanner;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/3 15:31\n * @Version 1.0\n */\npublic class CardIDDemo {\n    public static void main(String[] args) {\n        Scanner sc = new Scanner(System.in);\n        while (true) {\n            System.out.println(\"请输入一个身份证号：\");\n            String cardId = sc.next();      //键盘输入一个身份证号码\n            CardID cardID = new CardID();   //产生一个身份证对象\n            //开始验证\n            cardID.setCardNum(cardId);\n            if (cardID.lengthVerify()) {    //位数错误验证\n                if (cardID.charVerify()) {  //字符错误验证\n                    if (cardID.checkCodeVerify()) { //校验码错误验证\n                        System.out.println(\"验证通过\");\n                        cardID.output();\n                    } else {\n                        error();\n                    }\n                } else {\n                    error();\n                }\n            } else {\n                error();\n            }\n        }\n    }\n\n    public static void error() {\n        System.out.println(\"验证错误，请重新输入\");\n        System.out.println(\"-----------\");\n    }\n}\n\n```\n\n# 运行截图：\n\n![CardID01](CardID01.png)![CardID02](CardID02.png)\n\n\n# 心得：\n\n<font size=3>&emsp;&emsp;这个CardID类考验面向对象的综合能力，特别是成员方法，如何进行三重验证。其次就是多次用到字符串截取：String类的<code>substring()</code>方法</br>&emsp;&emsp;在年龄计算那一块，精确到年很简单，属于有手就行。精确到月也只是多一步判断，就是当前月份-出生月份为负数时，说明当前月份比出生月份小，那么年龄就要-1，然后修正月份，也就是月份+=12。\n&emsp;&emsp;天数的处理跟上一步类似。要注意天数相减为负数时导致月份-1，这时候还需要再次判断月份是否为负。否则会导致原本0月，经过天数不够减之后，月份成为负1月。\n\n</font>\n\n<center><font size=4>感谢阅读💗</font></center>","tags":["Java"],"categories":["Java实训题"]},{"title":"Java复数Complex类","url":"/2021/03/15/Java-Complex/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n\n<font size=6><center>实验二 定义一个复数类，并实现以下复数类的方法</center></font>\n\n# 要求：\n&emsp;&emsp;定义一个复数类，并实现以下复数类的方法：构造方法、得到实部、得到虚部。\n<font size=3> 1、复数类 Complex 必须满足如下要求：\n(1) 复数类 Complex 的属性有：\nrealPart : double 型 ，私有属性，代表复数的实数部分\nimaginPart : double 型 ，私有属性，代表复数的虚数部分11\n\n(2) 复数类 Complex 的构造方法有：\nComplex( ) : 构造函数，将复数的实部和虚部都置 0。\nComplex( double r , double i ) : 构造函数，形参 r 为实部的初值，i 为虚部的初值。\n\n(3) 复数类 Complex 的公有方法有：\nvoid setReal(): 设置复数对象的实部值；\nvoid setImagin (): 设置复数对象的虚部值；\ndouble getReal(): 获得复数对象的实部数值；\ndouble getImagin (): 获得复数对象的虚部数值；\n\nComplex complexAdd(Complex a) : 将当前复数对象与形参复数对象相加，所得的结果仍是一个复数值，返回给此方法的调用者。\n说明：（a+bi）+(c+di)= (a+c)+(b+d)i\n\nComplex complexSub(Complex a) : 将当前复数对象与形参复数对象相减，所得的结果仍是一个复数值，返回给此方法的调用者。\n\nComplex complexMulti(Complex a) : 将当前复数对象与形参复数对象相乘，所得的结果仍是一个复数值，返回给此方法的调用者。\n说明：（a+bi）(c+di)=ac+bci+adi+bdi2=(ac-bd)+(bc+ad)i\n\nComplex complexDiv(Complex a) : 将当前复数对象与形参复数对象相除，所得的结果仍是一个复数值，返回给此方法的调用者。\n说明：复数相除其实采用的是分子分母同时乘以分母的共轭复数，用以将分母的虚部消除掉，除法可调用乘法进行计算更简便。\n\nboolean equals(Complex a) : 将当前复数对象与形参复数对象进行比较，判断是否相等，返回一个布尔值。\n\nString toString( ) : 把当前复数对象的实部、虚部组合成 a+bi 的字符串形式，其中 a 和 b 分别为实部和虚部的数据，注意特殊数值的情况，如实部为 0、虚部为负，等等情况的表现方法。\n\n常见复数的写法有：3+2i，3-2i，4+i，4-i，1，0，-2i，i，-i 等(应当编写一个数组，存入以上9个复数，然后循环一次性全部输出，便于检查)。 \n\n2、定义个 ComplexDemo 类：\n    该类是程序的入口，要求能在主方法中创建至少 3 个复数（两个做操作数，通过键盘输入实部和虚部创建，一个做结果，不需实部和虚部），然后调用上述方法进行运算，并打印相应结果进行观察。</font>\n\n# 思路：\n\n\t两个类：Complex和ComplexDemo\n\t其中：1.在Complex类中定义复数的实部和虚部，double型。\n\t\t 2.定义Getter/Setter方法\n\t\t 3.定义复数的加减乘除方法，返回值是Complex类，返回给此方法的调用者。\n\t\t //对于3.也就是说谁调用的 加减乘除，结果返回给这个调用者，改变其值\n\t\t 4.定义equals()方法和toString()方法\n\t\t //对于equals()方法，比较两个Complex类的实部和虚部是否相同\n\t\t //对于toString()方法，遇到特殊情况(例如实部/虚部)为0，简化输出\n\t\t //对于toString()方法，当虚部为1/-1时，简化掉1，保留i以及符号\n\t\t 5.在主方法中创建3个复数，其中2个通过键盘输入实部和虚部，进行运算\n\t\t 6.toString()方法的检验可以通过存入多种极端情况测试简化效果\n\n# 代码：\n\n```c Complex类：\npackage com.experiment.exp2;\n\nimport java.text.DecimalFormat;\nimport java.util.Scanner;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/3 1:07\n * @Version 1.0\n */\npublic class Complex {\n    private double realPart;\n    private double imaginPart;\n\n    public Complex() {\n    }\n\n    public Complex(double r, double i) {\n        this.realPart = r;\n        this.imaginPart = i;\n    }\n\n/*    public Complex(String string) {       //随意以字符串输入复数，例如3+4i，但是实现起来太复杂了暂时没时间写了\n        boolean isRealPart=true;\n        char[] chars = string.toCharArray();\n        for (int i = 0; i < chars.length; i++) {\n            if (chars[i]=='+'||chars[i]=='-'){\n                continue;\n            } else {\n                if (isRealPart) {\n                    this.realPart=chars[i];\n                }\n            }\n        }\n    }*/\n\n    public double getReal() {\n        return realPart;\n    }\n\n    public void setReal(double realPart) {\n        this.realPart = realPart;\n    }\n\n    public double getImagin() {\n        return imaginPart;\n    }\n\n    public void setImagin(double imaginPart) {\n        this.imaginPart = imaginPart;\n    }\n\n    public Complex complexAdd(Complex a) {      //复数的加法\n        this.realPart += a.realPart;\n        this.imaginPart += a.imaginPart;\n        return this;\n    }\n\n    public Complex complexSub(Complex a) {      //复数的减法\n        this.realPart -= a.realPart;\n        this.imaginPart -= a.imaginPart;\n        return this;\n    }\n\n    public Complex complexMulti(Complex a) {    //复数的乘法\n        double r, i;\n        r = (this.realPart * a.realPart - this.imaginPart * a.imaginPart);\n        i = (this.imaginPart * a.realPart + this.realPart * a.imaginPart);\n        return new Complex(r, i);\n    }\n\n    public Complex complexDiv(Complex a) {  //复数的除法\n        //注意除数的实部和虚部不能同时为0，否则会导致除法运算时分母为0出错\n        while (a.realPart == 0 && a.imaginPart == 0) {\n            System.out.println(\"除数的实部和虚部不能同时为0，请重新输入\");\n            System.out.println(\"重新输入除数的实部：\");\n            a.realPart = new Scanner(System.in).nextDouble();\n            System.out.println(\"重新输入除数的虚部：\");\n            a.imaginPart = new Scanner(System.in).nextDouble();\n        }\n        double r, i;\n        r = (this.realPart * a.realPart + this.imaginPart * a.imaginPart) /\n                (Math.pow(a.realPart, 2) + Math.pow(a.imaginPart, 2));\n        i = (this.imaginPart * a.realPart - this.realPart * a.imaginPart) /\n                (Math.pow(a.realPart, 2) + Math.pow(a.imaginPart, 2));\n        return new Complex(r, i);\n    }\n\n    public Complex ConjugateComNum() { //返回共轭复数\n        this.imaginPart *= -1;\n        return this;\n    }\n\n    public boolean equals(Complex a) {\n        if (this == a) return true;\n        if (a == null || getClass() != a.getClass()) return false;\n        return this.realPart == a.realPart && this.imaginPart == a.imaginPart;\n    }\n\n    @Override\n    public String toString() {\n        DecimalFormat df = new DecimalFormat(\"#.##\");\n        String rp = df.format(this.realPart);\n        String ip = df.format(this.imaginPart);\n        String result = \"\";\n        if (this.realPart != 0) {\n            if (this.imaginPart == 0)\n                result = rp;\n            else if (this.imaginPart == 1)\n                result = rp + \"+i\";\n            else if (this.imaginPart == -1)\n                result = rp + \"-i\";\n            else if (this.imaginPart > 0)\n                result = rp + \"+\" + ip + \"i\";\n            else\n                result = rp + ip + \"i\";\n        } else {\n            if (this.imaginPart == 0)\n                result = \"0\";\n            else if (this.imaginPart == 1)\n                result = \"i\";\n            else if (this.imaginPart == -1)\n                result = \"-i\";\n            else\n                result = ip + \"i\";\n        }\n        return result;\n    }\n\n    /*@Override\n    public String toString() {\n        //保留两位小数\n        DecimalFormat df = new DecimalFormat(\"#.##\");\n        String formatRealPart = df.format(this.realPart);\n        String formatImaginPart = df.format(this.imaginPart);\n        //处理虚部符号 正负\n        String symbol;\n        if (imaginPart >= 0) {\n            symbol = \"+\";\n        } else {\n            symbol = \"\";\n        }\n        //实部为0\n        if (realPart == 0) {\n            //虚部也为0\n            if (imaginPart == 0) {\n                return \"0\";\n            }\n            //实部为0，虚部不为0且为整数\n            if (imaginPart % 1 == 0) {\n                if (imaginPart == 1) {\n                    return \"i\";\n                }\n                if (imaginPart == -1) {\n                    return \"-i\";\n                }\n                return symbol + (int) imaginPart + \"i\";\n            }\n            //虚部不为整数时\n            return symbol + formatImaginPart + \"i\";\n        }\n        //否则实部不为0\n        //虚部为0，实部不为0\n        if (imaginPart == 0) {\n            //实部为整数\n            if (realPart % 1 == 0) {\n                return String.valueOf((int) realPart);\n            }\n            //实部不为整数\n            return formatRealPart;\n        }\n\n        //实部和虚部都不为0\n        //实部为整数\n        if (realPart % 1 == 0) {\n            //虚部也为整数\n            if (imaginPart % 1 == 0) {                                      //实部和虚部都为整数\n                if (imaginPart == 1) {\n                    return (int) realPart + symbol + \"i\";                   //虚部为1\n                }\n                if (imaginPart == -1) {                                     //虚部为-1\n                    return (int) realPart + \"-i\";\n                }\n                return (int) realPart + symbol + (int) imaginPart + \"i\";    //实部和虚部都为整数\n            } else {\n                return (int) realPart + symbol + formatImaginPart + \"i\";    //实部为整数，虚部不为整数\n            }\n            //实部不为整数\n        } else if (imaginPart % 1 == 0) {\n            return formatRealPart + symbol + (int) imaginPart + \"i\";         //只有虚部为整数\n        } else {\n            return formatRealPart + symbol + formatImaginPart + \"i\";         //实部和虚部都不为整数\n        }\n    }*/\n}\n\n```\n```c ComplexDemo类(含main方法)：\npackage com.experiment.exp2;\n\n/*\n    定义个 ComplexDemo 类：\n    该类是程序的入口，要求能在主方法中创建至少 3 个复数\n    （两个做操作数，通过键盘输入实部和虚部创建，一个做结果，不需实部和虚部）\n    ，然后调用上述方法进行运算，并打印相应结果进行观察。\n */\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Scanner;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/3 1:16\n * @Version 1.0\n */\npublic class ComplexDemo {\n    public static void main(String[] args) {\n        Complex result = new Complex(); //创建一个复数对象 作为结果 不需要实部和虚部\n        printComplexArray();\n        while (true) {\n            System.out.println(\"  1. add()\");\n            System.out.println(\"  2. sub()\");\n            System.out.println(\"  3. multi()\");\n            System.out.println(\"  4. div()\");\n            System.out.println(\"  5. 退出程序\");\n            System.out.print(\"选择操作：\");\n            int i = new Scanner(System.in).nextInt();\n            switch (i) {\n                case 1:\n                    add();\n                    break;\n                case 2:\n                    sub();\n                    break;\n                case 3:\n                    multi();\n                    break;\n                case 4:\n                    div();\n                    break;\n                case 5:\n                    return;\n            }\n            System.out.println(\"===================\");\n        }\n\n//        add();\n//        sub();\n//        multi();\n//        div();\n    }\n\n    public static ArrayList<Complex> input() {\n        //创建集合方便存放多个复数对象\n        ArrayList<Complex> arrayList = new ArrayList<>();\n        //通过键盘输入两个复数\n        for (int i = 0; i < 2; i++) {\n            arrayList.add(inputComplex(i + 1));\n        }\n        return arrayList;\n    }\n\n    public static void add() {\n        ArrayList<Complex> arrayList = input();\n        //加法运算  仅适用于2个复数相加\n        System.out.println(arrayList.get(0) + \" + \" + arrayList.get(1) + \" = \"\n                + arrayList.get(0).complexAdd(arrayList.get(1)));\n    }\n\n    public static void sub() {\n        ArrayList<Complex> arrayList = input();\n        //减法运算  仅适用于2个复数相减\n        System.out.println(arrayList.get(0) + \" - \" + arrayList.get(1) + \" = \"\n                + arrayList.get(0).complexSub(arrayList.get(1)));\n    }\n\n    public static void multi() {\n        ArrayList<Complex> arrayList = input();\n        //乘法运算  仅适用于2个复数相乘\n        System.out.println(arrayList.get(0) + \" * \" + arrayList.get(1) + \" = \"\n                + arrayList.get(0).complexMulti(arrayList.get(1)));\n    }\n\n    public static void div() {\n        ArrayList<Complex> arrayList = input();\n        //除法运算  仅适用于2个复数相除\n        System.out.println(arrayList.get(0) + \" ÷ \" + arrayList.get(1) + \" = \"\n                + arrayList.get(0).complexDiv(arrayList.get(1)));\n    }\n\n    public static Complex inputComplex(int seq) {\n        //输入复数的实部和虚部，产生一个复数并返回\n        Scanner scanner = new Scanner(System.in);\n        System.out.print(\"请输入第\" + seq + \"个复数的实部：\");\n        double realPart = scanner.nextDouble();\n        System.out.print(\"请输入第\" + seq + \"个复数的虚部：\");\n        double imaginPart = scanner.nextDouble();\n        System.out.println(\"----------\");\n        return new Complex(realPart, imaginPart);\n    }\n\n    public static void printComplexArray() {\n        //存入9个复数对象 方便检查\n        ArrayList<Complex> complexes = new ArrayList<>(Arrays.asList(new Complex(3, 2), new Complex(3, -2),\n                new Complex(4, 1), new Complex(4, -1), new Complex(1, 0), new Complex(0, 0),\n                new Complex(0, -2), new Complex(0, 1), new Complex(0, -1)));\n        for (Complex complex : complexes) {\n            System.out.print(complex + \"  \");\n        }\n        System.out.println();\n    }\n}\n\n```\n\n# 运行截图：\n\n![Complex01](Complex01.png)![Complex02](Complex02.png)![Complex03](Complex03.png)\n\n\n# 心得：\n\n<font size=3>&emsp;&emsp;手动实现一个Complex类，想要做到严谨无Bug还是有一定的难度。对于强迫症患者来说，toString()方法就有够麻烦的了，而加减乘除方法可以通过百度查资料获得算术方法。</br>&emsp;&emsp;注意在乘除法处理时，上一条语句改变了realPart的值，在下一句又用到realPart值时会造成计算错误，所以这个时候需要一个中间变量来避免这种情况。</br>&emsp;&emsp;一般情况下对复数对象本身进行运算，并且返回给调用者时，可以直接`return this;`</font>\n<font size=3>&emsp;&emsp;CardID身份证类即将于下一期上线💗</font>","tags":["Java"],"categories":["Java实训题"]},{"title":"Java输出数字金字塔","url":"/2021/03/11/Java-DrawTriangle/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n<font size=6><center>实验一 输出如下图形，行数通过键盘输入的数据确定</center></font>\n# 要求：\n&emsp;&emsp;用死循环反复测试，即把绘制函数放入`while()`循环中\n&emsp;&emsp;其实就是金字塔形，不过跟传统的那种输出*堆砌的金字塔不一样，这个奇数行是1，偶数行是2\n\n# 思路：\n\n个人分析：\n&emsp;&emsp;第一行1个元素              共计1个字符\n&emsp;&emsp;&emsp;&emsp;第二行2个元素+1个空格       共计3个字符\n&emsp;&emsp;&emsp;&emsp;第二行3个元素+2个空格       共计5个字符\n&emsp;&emsp;&emsp;&emsp;...\n&emsp;&emsp;&emsp;&emsp;第n行n个元素+(n-1)个空格    共计n+(n-1)个字符，即2n-1个字符\n\t\n&emsp;&emsp;对齐图形，只需要在第1行前面补(2n-1)/2个空格字符\n&emsp;&emsp;&emsp;&emsp;第2行前面补(2n-1)/2-1个空格字符\n&emsp;&emsp;&emsp;&emsp;第i行前面补(2n-1)/2-(i-1)个空格字符\n为了输出效果跟老师的一模一样，注意数字之间也要有空格分隔。\n\n# 代码：\n\n```c\npackage com.experiment.exp1.DrawTriangle;\n\nimport java.util.Scanner;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/2 21:37\n * @Version 1.0\n */\n\n/*\n    输出如下图形，行数通过键盘输入的数据确定：（用死循环反复测试）\n\n个人分析：\n    第一行1个元素              共计1个字符\n    第二行2个元素+1个空格       共计3个字符\n    第二行3个元素+2个空格       共计5个字符\n    ...\n    第n行n个元素+(n-1)个空格    共计n+(n-1)个字符，即2n-1个字符\n\n    对齐图形，只需要在第1行前面补(2n-1)/2个空格字符\n            第2行前面补(2n-1)/2-1个空格字符\n            第i行前面补(2n-1)/2-(i-1)个空格字符\n */\n\npublic class DrawTriangle {\n    public static void main(String[] args) {\n//        draw(); //调用即可输出一次\n        //题目要求死循环反复测试\n        while (true) {\n            draw();\n        }\n    }\n\n    //draw函数 接收一个金字塔的行数并输出\n    public static void draw() {\n        boolean isOddNum = true;\n        System.out.print(\"请输入金字塔层数：\");\n        int num = new Scanner(System.in).nextInt();         //接收金字塔层数\n        for (int i = 1; i <= num; i++) {                    //打印num层\n            for (int j = 0; j < (2 * num - 1) / 2 - (i - 1); j++) {\n                System.out.print(\" \");                      //对齐金字塔，前面补相应个空格\n            }\n            //单数层\n            if (isOddNum) {                                 //单数层输出1\n                System.out.print(1);\n                for (int printNum = 1; printNum < i; printNum++) {\n                    System.out.print(\" \" + 1);\n                }\n                isOddNum = false;                           //下次是双(复)数层\n                //双数层\n            } else {                                        //否则双(复)数层输出2\n                System.out.print(2);\n                for (int printNum = 1; printNum < i; printNum++) {\n                    System.out.print(\" \" + 2);\n                }\n                isOddNum = true;                            //下次是单数层\n            }\n            System.out.println();   //换行\n        }\n    }\n}\n\n```\n有个精简版，这里只展示`draw()`方法：\n```c\npublic static void draw() {\n        System.out.print(\"请输入金字塔层数：\");\n        int num = new Scanner(System.in).nextInt();   //接收金字塔层数\n        for (int i = 1; i <= num; i++) {              //打印num层\n            for (int j = i; j < num; j++) {\n                System.out.print(\" \");                //对齐金字塔，前面补相应个空格\n            }\n            for (int j = 1; j <= i; j++) {\n                System.out.print(i % 2 == 1 ? \"1 \" : \"2 \");\n            }\n            System.out.println();\n        }\n    }\n```\n# 运行截图：\n\n![JavaDrawTriangle01](JavaDrawTriangle01.png)\n\n\n# 心得：\n\n<font size=3>这种输出金字塔、菱形、正方形(最简单)、矩形等等的题，考虑嵌套循环。这道题需要考虑奇数层/偶数层 输出的数字是不同的。顺带一提，在输出 数字+\" \" 这种带空格的需要多次连接字符串，非常浪费性能，其实可以直接输出 \"数字 \" 这样一个字符串就解决了。但是现在编译器一般会自动优化成第二种格式。\n\n特别是对于菱形这种对称图形，在给定的\" * \"个数以内输出的话，可以先输出对称的其中一半，然后每次判断输出下一层还够不够。\n\n</font>\n<font size=3><center>💖💖💖这道题想做好就要动点脑子惹💖💖💖</center></font>\n\n<center><font size=4>感谢阅读💗</font></center>","tags":["Java"],"categories":["Java实训题"]},{"title":"Java输出素数","url":"/2021/03/11/Java-Prime/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n<font size=6><center>实验一 输出 1,000,000 之内的所有素数</center></font>\n\n# 要求：\n&emsp;&emsp;程序简单，程序运行速度较快，行列对齐美观。\n# 思路：\n&emsp;&emsp;1. 只需判定2到根号i之间有无数可以让n被整除，能被2到sqrt(i)之间的数整除说明是合数，否则为素数\n&emsp;&emsp;2. 如果不注意上述这点，每次都循环2到n的话，当数字非常大的时候时间复杂度指数上升很可怕。\n&emsp;&emsp;3. 关于对齐：利用String类的format()方法格式化，并且每打印10个换一行\n\n# 代码：\n```c\npackage com.experiment.exp1;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/2 23:55\n * @Version 1.0\n */\n\npublic class PrimeNum {\n    public static void main(String[] args) {\n        printPrimeNum(1_000_000);\n    }\n\n    public static void printPrimeNum(int range) {\n        long start = System.currentTimeMillis();\n        boolean isPrime = false; //是否素数标志\n        int count = 0;  // 素数计数器\n\n        for (int i = 2; i < range; i++) {\n            if (2 > (int) Math.sqrt(i)) {\n                System.out.print(String.format(\"%8d\", i)); //利用String类的format()方法格式化字符串，固定宽度7，右对齐\n                count++;\n            }\n            //处理4到i的数\n            int num = (int) Math.sqrt(i);\n            //查看i是否能被2到num之间的某个数整除\n            for (int inside = 2; inside <= num; inside++) { //检查是否是素数\n                if (i % inside == 0) {  //能被整除说明不是素数\n                    isPrime = false;\n                    break;\n                } else {\n                    isPrime = true; //不能被整除就暂且判定是素数然后inside++，看能不能被下一位整数整除\n                }\n            }\n            //循环结束仍然是true，就打印这个素数\n            if (isPrime) {\n                System.out.print(String.format(\"%8d\", i));\n                if (++count % 8 == 0) {     //每8个数换一行\n                    System.out.println();\n                }\n            }\n        }\n        long end = System.currentTimeMillis();\n        System.out.println(\"\\n素数个数：\" + count + \"消耗时间：\" + (end - start) + \"毫秒\");\n        //打印出素数结果  R7 4800H用时   IDEA≈600ms  cmd≈4s\n        //只统计素数个数  R7 4800H用时   IDEA≈300ms  cmd≈230ms\n    }\n}\n```\n# 运行截图：\n ![JavaPrime01](JavaPrime01.png)\n<center> 此处省略7万个素数</center>\n\n ![JavaPrime02](JavaPrime02.png)\n\n\n# 心得：\n&emsp;&emsp;<font size=3>可见，通过String类的`format()`方法进行格式化，配合每隔几个数就换行，整体排版还是比较整齐。并且由于内层只循环平方根次，所以即使是100万个数，仅耗时600ms，很🉑，所以一定要记得把O(n²)优化为O(n·logn)🍻</font>\n<center><font size=4>感谢阅读💗</font></center>","tags":["Java"],"categories":["Java实训题"]},{"title":"Java找成绩最大值最小值平均值","url":"/2021/03/10/Java-Count/","content":"\n<big><b><center><font color='red'>=====taoyyz小陶©版权所有=====</font></center></b></big>\n<font size=6><center>实验一 输入一系列成绩，输出最高分、最低分、平均分...</center></font>\n# 要求：\n&emsp;&emsp;输入输出格式正确，一重循环解决全部问题。\n&emsp;&emsp;输入一系列成绩，输出最高分、最低分、平均分，并统计存在多少个并列最高分，要求输入输出格式正确，一重循环解决全部问题。\n&emsp;&emsp;&emsp;&emsp;输入：`65 87 95 86 72 58 56 75 95 94 78…`\n&emsp;&emsp;&emsp;&emsp;输出：`最高分：95，最低分：56，平均分：72.36，同时存在 2 个并列最高分。`\n&emsp;&emsp;个人觉得题目里面的平均分是有问题的，计算器算出来也是78.27\n\n# 思路：\n\t一重循环，有手就行\n不过要注意输入格式，老师给的这个要求貌似是连续输入一串数字，以空格分开，那就考虑使用String类的split()方法来分割输入的字符串，返回值是个String[]字符数组。一会儿比较大小的时候利用包装类Integer的parseInt()方法转换成整型再比较。\n\n# 代码：\n\n```c\npackage com.experiment.exp1;\n\nimport java.util.Scanner;\n\n/**\n * @Author taoyyz(陶俊杰)\n * @Date 2021/3/2 16:12\n * @Version 1.0\n */\n/*\n    输入一系列成绩，输出最高分、最低分、平均分，并统计存在多少个并列最高分，要求输入输出格式正确，一重循环解决全部问题。\n    输入：65 87 95 86 72 58 56 75 95 94 78…\n    输出：最高分：95，最低分：56，平均分：72.36，同时存在 2 个并列最高分。\n    个人觉得题目里面的平均分是有问题的，计算器算出来也是78.27\n */\npublic class Count {\n    public static void main(String[] args) {\n        count();\n    }\n\n    private static void count() {\n        System.out.println(\"请输入一串成绩：\");\n        //输入一串数字,带空格用nextLine()读到回车结束\n        String[] scoreStr = new Scanner(System.in).nextLine().split(\" \");\n        //定义一个整型数组，用于把输入的数字字符串转为整型后存入\n        int[] scores = new int[scoreStr.length];\n        for (int i = 0; i < scoreStr.length; i++) {\n            scores[i] = Integer.parseInt(scoreStr[i]);\n        }\n        //定义最大值、最小值、平均数、并列最高分的计数器\n        int max = scores[0], min = scores[0], maxCount = 0;\n        double avg = 0;\n        //一重循环找出最大值、最小值、平均数、并列最高分出现的次数\n        for (int score : scores) {\n            if (max < score) {\n                max = score;\n                maxCount = 1;\n            } else if (max == score) {\n                maxCount++;\n            }\n            if (min > score) {\n                min = score;\n            }\n            avg += (double) score / scores.length;\n        }\n        //输出结果\n        System.out.println(\"最高分：\" + max + \",最低分：\" + min + \",平均分：\"\n                + String.format(\"%.2f\", avg) + \",同时存在\" + maxCount + \"个并列最高分\");\n    }\n}\n\n```\n\n# 运行截图：\n\n![JavaCount01](JavaCount01.png)\n\n\n# 心得：\n\n<font size=3>这种题是属于比较简单的循环问题。只需事先定义一个max、min、avg的值以及一个maxCount用于统计最大值出现了几次（这一点有点坑，要注意最大值变化之后计数器要重新计算）。</font>\n<font size=3>说白了有手就行，这都不会建议还是重修吧😅</font>\n\n<center><font size=4>感谢阅读💗</font></center>","tags":["Java"],"categories":["Java实训题"]}]