精华内容
下载资源
问答
  • 2019-11-09 16:35:30

    环境

    一台笔记本电脑,Windows操作系统,安装了VirtualBox,Vagrant,Github。

    目标

    操作系统Oracle Linux 7,运行容器数据库,数据库企业版,RAC,版本为19.3.0,实例名为ORCLCDB,带一个可插拔数据库orclpdb1。两个RAC节点均运行于同一主机。

    创建Linux操作系统

    克隆项目以获得Linux Vagrant Box:

    PS D:\DB> git clone https://github.com/oracle/vagrant-boxes.git
    

    安装磁盘扩展插件:

    vagrant plugin install vagrant-disksize
    

    在Vagrantfile中将内存由默认的2048改为8192,然后修改根盘的大小为80G。如下:

    ...
    config.vm.box = "ol7-latest"
      config.disksize.size = "80GB"
      config.vm.box_url = "https://yum.oracle.com/boxes/oraclelinux/latest/ol7-latest.box"
      config.vm.define NAME
      
      config.vm.box_check_update = false
      
      # change memory size
      config.vm.provider "virtualbox" do |v|
        v.memory = 8192
        v.name = NAME
      end
    ...
    

    然后创建虚机(Oracle Linux 7)。耗时7分42秒,我的环境一般在7分钟左右。

    PS E:\DB\vagrant-boxes\OracleLinux\7> vagrant up
    

    启动VM后,磁盘是64G,但根分区仍是32G:

    $ df -h
    Filesystem                   Size  Used Avail Use% Mounted on
    devtmpfs                     3.8G     0  3.8G   0% /dev
    tmpfs                        3.8G     0  3.8G   0% /dev/shm
    tmpfs                        3.8G  8.5M  3.8G   1% /run
    tmpfs                        3.8G     0  3.8G   0% /sys/fs/cgroup
    /dev/mapper/vg_main-lv_root   32G  1.7G   31G   6% /
    /dev/sda1                    497M  125M  373M  26% /boot
    vagrant                      1.9T  1.1T  753G  60% /vagrant
    tmpfs                        771M     0  771M   0% /run/user/1000
    
    $ lsblk
    NAME                MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
    sdb                   8:16   0 15.6G  0 disk
    sda                   8:0    0   80G  0 disk
    ├─sda2                8:2    0   36G  0 part
    │ ├─vg_main-lv_swap 252:1    0    4G  0 lvm  [SWAP]
    │ └─vg_main-lv_root 252:0    0   32G  0 lvm  /
    └─sda1                8:1    0  500M  0 part /boot
    
    

    因此需要扩展分区,大致过程如下:

    fdisk /dev/sda (n, p, <Enter>, <Enter>, w) -> 产生新分区/dev/sda3
    partprobe
    pvcreate /dev/sda
    vgextend vg_main /dev/sda3
    lvextend /dev/vg_main/lv_root /dev/sda3
    xfs_growfs /
    

    扩容后的空间:

    # df -h
    Filesystem                   Size  Used Avail Use% Mounted on
    devtmpfs                     3.8G     0  3.8G   0% /dev
    tmpfs                        3.8G     0  3.8G   0% /dev/shm
    tmpfs                        3.8G  8.5M  3.8G   1% /run
    tmpfs                        3.8G     0  3.8G   0% /sys/fs/cgroup
    /dev/mapper/vg_main-lv_root   76G  1.7G   74G   3% /
    /dev/sda1                    497M  125M  373M  26% /boot
    vagrant                      1.9T  1.1T  753G  60% /vagrant
    tmpfs                        771M     0  771M   0% /run/user/1000
    
    

    以下操作均登入Linux中运行。

    安装Docker

    安装Docker,耗时0m49.161s:

    sudo yum install -y yum-utils
    sudo yum-config-manager --enable ol7_addons
    sudo yum install -y docker-engine
    sudo systemctl start docker
    sudo systemctl enable docker
    sudo usermod -aG docker vagrant
    

    确认docker安装成功:

    $ docker version
    Client: Docker Engine - Community
     Version:           18.09.8-ol
     API version:       1.39
     Go version:        go1.10.8
     Git commit:        76804b7
     Built:             Fri Sep 27 21:00:18 2019
     OS/Arch:           linux/amd64
     Experimental:      false
    
    Server: Docker Engine - Community
     Engine:
      Version:          18.09.8-ol
      API version:      1.39 (minimum version 1.12)
      Go version:       go1.10.8
      Git commit:       76804b7
      Built:            Fri Sep 27 20:54:00 2019
      OS/Arch:          linux/amd64
      Experimental:     false
      Default Registry: docker.io
    
    

    克隆Github项目

    耗时0m31.599s:

    sudo yum install -y git
    git clone https://github.com/oracle/docker-images.git
    

    设置内核参数

    docker会从Host OS继承参数,因此需在文件/etc/sysctl.conf中设置以下参数:

    fs.file-max = 6815744
    net.core.rmem_max = 4194304
    net.core.rmem_default = 262144
    net.core.wmem_max = 1048576
    net.core.wmem_default = 262144
    net.core.rmem_default = 262144
    

    使其生效:

    sudo sysctl -a
    sudo sysctl -p
    

    创建虚拟网络

    docker network create --driver=bridge --subnet=172.16.1.0/24 rac_pub1_nw
    docker network create --driver=bridge --subnet=192.168.17.0/24 rac_priv1_nw
    

    查看状态:

    $ docker network ls
    NETWORK ID          NAME                DRIVER              SCOPE
    aac5636fe8fc        bridge              bridge              local
    051a1439f036        host                host                local
    1a9007862a18        none                null                local
    fe35e54e1aa0        rac_priv1_nw        bridge              local
    0c6bdebeab78        rac_pub1_nw         bridge              local
    
    

    配置实时模式

    RAC的某些进程需要运行在实时模式,因此需要在文件/etc/sysconfig/docker中添加以下:

    OPTIONS='--selinux-enabled --cpu-rt-runtime=950000'
    

    使其生效:

    sudo systemctl daemon-reload
    sudo systemctl stop docker
    sudo systemctl start docker
    

    SELINUX 配置为 permissive模式(/etc/selinux/config),过程略。
    然后重启实例使得SELINUX生效。

    此时的空间状态:

    $ df -h
    Filesystem                   Size  Used Avail Use% Mounted on
    devtmpfs                     3.8G     0  3.8G   0% /dev
    tmpfs                        3.8G     0  3.8G   0% /dev/shm
    tmpfs                        3.8G  8.5M  3.8G   1% /run
    tmpfs                        3.8G     0  3.8G   0% /sys/fs/cgroup
    /dev/mapper/vg_main-lv_root   76G  2.1G   74G   3% /
    /dev/sda1                    497M  125M  373M  26% /boot
    vagrant                      1.9T  1.1T  753G  60% /vagrant
    tmpfs                        771M     0  771M   0% /run/user/1000
    

    将安装文件拷贝到目录

    耗时真的看运气,有时7分钟,最近一次1分半:

    cd docker-images/OracleDatabase/RAC/OracleRealApplicationClusters/dockerfiles/19.3.0
    cp /vagrant/LINUX.X64_193000_db_home.zip .
    cp /vagrant/LINUX.X64_193000_grid_home.zip .
    

    此时的空间状态:

    $ df -h
    Filesystem                   Size  Used Avail Use% Mounted on
    devtmpfs                     3.8G     0  3.8G   0% /dev
    tmpfs                        3.8G     0  3.8G   0% /dev/shm
    tmpfs                        3.8G  8.5M  3.8G   1% /run
    tmpfs                        3.8G     0  3.8G   0% /sys/fs/cgroup
    /dev/mapper/vg_main-lv_root   76G  7.6G   68G  11% /
    /dev/sda1                    497M  125M  373M  26% /boot
    vagrant                      1.9T  1.1T  745G  61% /vagrant
    tmpfs                        771M     0  771M   0% /run/user/1000
    

    构建Docker Install Image

    这一步最重要的任务就是拷贝介质和配置脚本,还有从网络下载OS更新。然后安装GI和数据库。
    执行以下命令开始构建:

    $ cd docker-images/OracleDatabase/RAC/OracleRealApplicationClusters/dockerfiles
    $ ls
    12.2.0.1  18.3.0  19.3.0  buildDockerImage.sh
    $ time ./buildDockerImage.sh -v 19.3.0
    

    如果空间不够,会报错:

    ...
    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    checkSpace.sh: ERROR - There is not enough space available in the docker container.
    checkSpace.sh: The container needs at least 35 GB , but only 14 available.
    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    ...
    There was an error building the image.
    

    以下是成功时的完整日志,整个过程耗时58分钟:

    $ time ./buildDockerImage.sh -v 19.3.0
    Checking if required packages are present and valid...
    LINUX.X64_193000_grid_home.zip: OK
    LINUX.X64_193000_db_home.zip: OK
    ==========================
    DOCKER info:
    Containers: 0
     Running: 0
     Paused: 0
     Stopped: 0
    Images: 0
    Server Version: 18.09.8-ol
    Storage Driver: overlay2
     Backing Filesystem: xfs
     Supports d_type: true
     Native Overlay Diff: false
    Logging Driver: json-file
    Cgroup Driver: cgroupfs
    Plugins:
     Volume: local
     Network: bridge host macvlan null overlay
     Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
    Swarm: inactive
    Runtimes: runc
    Default Runtime: runc
    Init Binary: docker-init
    containerd version: c4446665cb9c30056f4998ed953e6d4ff22c7c39
    runc version: 4bb1fe4ace1a32d3676bb98f5d3b6a4e32bf6c58
    init version: fec3683
    Security Options:
     seccomp
      Profile: default
     selinux
    Kernel Version: 4.14.35-1902.6.6.el7uek.x86_64
    Operating System: Oracle Linux Server 7.7
    OSType: linux
    Architecture: x86_64
    CPUs: 2
    Total Memory: 7.528GiB
    Name: ol7-vagrant-rac
    ID: MS7Y:32TG:TGTF:C3QP:DR4Q:IDG4:RHHS:SQVW:5QWY:U45Z:ZCXK:BDCP
    Docker Root Dir: /var/lib/docker
    Debug Mode (client): false
    Debug Mode (server): false
    Registry: https://index.docker.io/v1/
    Labels:
    Experimental: false
    Insecure Registries:
     127.0.0.0/8
    Live Restore Enabled: false
    Product License: Community Engine
    
    Registries: docker.io (secure)
    ==========================
    Building image 'oracle/database-rac:19.3.0' ...
    Sending build context to Docker daemon  5.949GB
    Step 1/11 : FROM oraclelinux:7-slim
    Trying to pull repository docker.io/library/oraclelinux ...
    7-slim: Pulling from docker.io/library/oraclelinux
    a316717fc6ee: Pull complete
    Digest: sha256:c5f3baff726ffd97c7e9574e803ad0e8a1e5c7de236325eed9e87f853a746e90
    Status: Downloaded newer image for oraclelinux:7-slim
     ---> 874477adb545
    Step 2/11 : MAINTAINER Paramdeep Saini <paramdeep.saini@oracle.com>
     ---> Running in a1b3685f3111
    Removing intermediate container a1b3685f3111
     ---> 8187c15c17ab
    Step 3/11 : ENV SETUP_LINUX_FILE="setupLinuxEnv.sh"     INSTALL_DIR=/opt/scripts     GRID_BASE=/u01/app/grid     GRID_HOME=/u01/app/19.3.0/grid     INSTALL_FILE_1="LINUX.X64_193000_grid_home.zip"     GRID_INSTALL_RSP="gridsetup_19c.rsp"     GRID_SW_INSTALL_RSP="grid_sw_install_19c.rsp"     GRID_SETUP_FILE="setupGrid.sh"     FIXUP_PREQ_FILE="fixupPreq.sh"     INSTALL_GRID_BINARIES_FILE="installGridBinaries.sh"     INSTALL_GRID_PATCH="applyGridPatch.sh"     INVENTORY=/u01/app/oraInventory     CONFIGGRID="configGrid.sh"      ADDNODE="AddNode.sh"       DELNODE="DelNode.sh"     ADDNODE_RSP="grid_addnode.rsp"      SETUPSSH="setupSSH.expect"      DOCKERORACLEINIT="dockeroracleinit"     GRID_USER_HOME="/home/grid"     SETUPGRIDENV="setupGridEnv.sh"     ASM_DISCOVERY_DIR="/dev"     RESET_OS_PASSWORD="resetOSPassword.sh"     MULTI_NODE_INSTALL="MultiNodeInstall.py"     DB_BASE=/u01/app/oracle     DB_HOME=/u01/app/oracle/product/19.3.0/dbhome_1     INSTALL_FILE_2="LINUX.X64_193000_db_home.zip"     DB_INSTALL_RSP="db_sw_install_19c.rsp"     DBCA_RSP="dbca_19c.rsp"     DB_SETUP_FILE="setupDB.sh"     PWD_FILE="setPassword.sh"     RUN_FILE="runOracle.sh"     STOP_FILE="stopOracle.sh"     ENABLE_RAC_FILE="enableRAC.sh"     CHECK_DB_FILE="checkDBStatus.sh"     USER_SCRIPTS_FILE="runUserScripts.sh"     REMOTE_LISTENER_FILE="remoteListener.sh"     INSTALL_DB_BINARIES_FILE="installDBBinaries.sh"     GRID_HOME_CLEANUP="GridHomeCleanup.sh"     ORACLE_HOME_CLEANUP="OracleHomeCleanup.sh"     DB_USER="oracle"     GRID_USER="grid"    FUNCTIONS="functions.sh"    COMMON_SCRIPTS="/common_scripts"    CHECK_SPACE_FILE="checkSpace.sh"    RESET_FAILED_UNITS="resetFailedUnits.sh"    SET_CRONTAB="setCrontab.sh"    CRONTAB_ENTRY="crontabEntry"    EXPECT="/usr/bin/expect"    BIN="/usr/sbin"    container="true"
     ---> Running in 01dfaa3cc133
    Removing intermediate container 01dfaa3cc133
     ---> be15b2094e53
    Step 4/11 : ENV  INSTALL_SCRIPTS=$INSTALL_DIR/install      PATH=/bin:/usr/bin:/sbin:/usr/sbin:$PATH      SCRIPT_DIR=$INSTALL_DIR/startup      GRID_PATH=$GRID_HOME/bin:$GRID_HOME/OPatch/:/usr/sbin:$PATH       DB_PATH=$DB_HOME/bin:$DB_HOME/OPatch/:/usr/sbin:$PATH      GRID_LD_LIBRARY_PATH=$GRID_HOME/lib:/usr/lib:/lib      DB_LD_LIBRARY_PATH=$DB_HOME/lib:/usr/lib:/lib
     ---> Running in 7c6a76bc6baf
    Removing intermediate container 7c6a76bc6baf
     ---> 1666646716e1
    Step 5/11 : COPY $GRID_SW_INSTALL_RSP  $INSTALL_GRID_PATCH $SETUP_LINUX_FILE $GRID_SETUP_FILE $INSTALL_GRID_BINARIES_FILE $FIXUP_PREQ_FILE $DB_SETUP_FILE $CHECK_SPACE_FILE $DB_INSTALL_RSP $INSTALL_DB_BINARIES_FILE $ENABLE_RAC_FILE $GRID_HOME_CLEANUP $ORACLE_HOME_CLEANUP $INSTALL_FILE_1 $INSTALL_FILE_2 $INSTALL_SCRIPTS/
     ---> aeded06d0a00
    Step 6/11 : COPY $RUN_FILE $ADDNODE $ADDNODE_RSP $SETUPSSH $FUNCTIONS $CONFIGGRID $GRID_INSTALL_RSP $DBCA_RSP $PWD_FILE $CHECK_DB_FILE $USER_SCRIPTS_FILE $STOP_FILE $CHECK_DB_FILE $REMOTE_LISTENER_FILE $SETUPGRIDENV $DELNODE $RESET_OS_PASSWORD $MULTI_NODE_INSTALL  $SCRIPT_DIR/
     ---> b9b139ebda70
    Step 7/11 : RUN chmod 755 $INSTALL_SCRIPTS/*.sh  &&     sync &&     $INSTALL_DIR/install/$CHECK_SPACE_FILE &&     $INSTALL_DIR/install/$SETUP_LINUX_FILE &&     $INSTALL_DIR/install/$GRID_SETUP_FILE &&     $INSTALL_DIR/install/$DB_SETUP_FILE &&     sed -e '/hard *memlock/s/^/#/g' -i /etc/security/limits.d/oracle-database-preinstall-19c.conf &&     su  $GRID_USER -c "$INSTALL_DIR/install/$INSTALL_GRID_BINARIES_FILE EE $PATCH_NUMBER" &&     $INVENTORY/orainstRoot.sh &&     $GRID_HOME/root.sh &&     su  $DB_USER  -c  "$INSTALL_DIR/install/$INSTALL_DB_BINARIES_FILE EE" &&     su  $DB_USER  -c  "$INSTALL_DIR/install/$ENABLE_RAC_FILE" &&     $INVENTORY/orainstRoot.sh &&     $DB_HOME/root.sh &&     su  $GRID_USER -c "$INSTALL_SCRIPTS/$GRID_HOME_CLEANUP" &&     su  $DB_USER -c "$INSTALL_SCRIPTS/$ORACLE_HOME_CLEANUP" &&     $INSTALL_DIR/install/$FIXUP_PREQ_FILE &&     rm -rf $INSTALL_DIR/install &&     rm -rf $INSTALL_DIR/install &&     sync &&     chmod 755 $SCRIPT_DIR/*.sh &&     chmod 755 $SCRIPT_DIR/*.expect &&     chmod 666 $SCRIPT_DIR/*.rsp &&     echo "nohup $SCRIPT_DIR/runOracle.sh &" >> /etc/rc.local &&     rm -f /etc/rc.d/init.d/oracle-database-preinstall-19c-firstboot &&     mkdir -p $GRID_HOME/dockerinit &&     cp $GRID_HOME/bin/$DOCKERORACLEINIT $GRID_HOME/dockerinit/ &&     chown $GRID_USER:oinstall $GRID_HOME/dockerinit &&     chown root:oinstall $GRID_HOME/dockerinit/$DOCKERORACLEINIT &&     chmod 4755 $GRID_HOME/dockerinit/$DOCKERORACLEINIT &&     ln -s $GRID_HOME/dockerinit/$DOCKERORACLEINIT /usr/sbin/oracleinit &&     chmod +x /etc/rc.d/rc.local  &&     rm -f /etc/sysctl.d/99-oracle-database-preinstall-19c-sysctl.conf &&     rm -f /etc/sysctl.d/99-sysctl.conf &&     sync
     ---> Running in c40a0e8ea8ed
    Loaded plugins: ovl
    No package openssh-client available.
    Resolving Dependencies
    --> Running transaction check
    ---> Package e2fsprogs.x86_64 0:1.42.9-16.el7 will be installed
    ...
    Transaction Summary
    ================================================================================
    Install  14 Packages (+109 Dependent packages)
    Upgrade              (   9 Dependent packages)
    
    Total download size: 70 M
    Downloading packages:
    Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
    ...
    Complete!
    Loaded plugins: ovl
    Cleaning repos: ol7_UEKR5 ol7_developer_EPEL ol7_latest
    ...
    /opt/scripts/install/installGridBinaries.sh: line 57:  : command not found
    Launching Oracle Grid Infrastructure Setup Wizard...
    
    [WARNING] [INS-13014] Target environment does not meet some optional requirements.
       CAUSE: Some of the optional prerequisites are not met. See logs for details. gridSetupActions2019-11-11_03-39-25AM.log
       ACTION: Identify the list of failed prerequisite checks from the log: gridSetupActions2019-11-11_03-39-25AM.log. Then either from the log file or from installation manual find the appropriate configuration to meet the prerequisites and fix it manually.
    The response file for this session can be found at:
     /u01/app/19.3.0/grid/install/response/grid_2019-11-11_03-39-25AM.rsp
    
    You can find the log of this install session at:
     /tmp/GridSetupActions2019-11-11_03-39-25AM/gridSetupActions2019-11-11_03-39-25AM.log
    
    As a root user, execute the following script(s):
            1. /u01/app/oraInventory/orainstRoot.sh
            2. /u01/app/19.3.0/grid/root.sh
    
    Execute /u01/app/oraInventory/orainstRoot.sh on the following nodes:
    [c40a0e8ea8ed]
    Execute /u01/app/19.3.0/grid/root.sh on the following nodes:
    [c40a0e8ea8ed]
    
    
    Successfully Setup Software with warning(s).
    Moved the install session logs to:
     /u01/app/oraInventory/logs/GridSetupActions2019-11-11_03-39-25AM
    Changing permissions of /u01/app/oraInventory.
    Adding read,write permissions for group.
    Removing read,write,execute permissions for world.
    
    Changing groupname of /u01/app/oraInventory to oinstall.
    The execution of the script is complete.
    Check /u01/app/19.3.0/grid/install/root_c40a0e8ea8ed_2019-11-11_03-41-46-398462346.log for the output of root script
    
    
    Launching Oracle Database Setup Wizard...
    
    [WARNING] [INS-13014] Target environment does not meet some optional requirements.
       CAUSE: Some of the optional prerequisites are not met. See logs for details. /u01/app/oraInventory/logs/InstallActions2019-11-11_03-46-31AM/installActions2019-11-11_03-46-31AM.log
       ACTION: Identify the list of failed prerequisite checks from the log: /u01/app/oraInventory/logs/InstallActions2019-11-11_03-46-31AM/installActions2019-11-11_03-46-31AM.log. Then either from the log file or from installation manual find the appropriate configuration to meet the prerequisites and fix it manually.
    The response file for this session can be found at:
     /u01/app/oracle/product/19.3.0/dbhome_1/install/response/db_2019-11-11_03-46-31AM.rsp
    
    You can find the log of this install session at:
     /u01/app/oraInventory/logs/InstallActions2019-11-11_03-46-31AM/installActions2019-11-11_03-46-31AM.log
    
    As a root user, execute the following script(s):
            1. /u01/app/oracle/product/19.3.0/dbhome_1/root.sh
    
    Execute /u01/app/oracle/product/19.3.0/dbhome_1/root.sh on the following nodes:
    [c40a0e8ea8ed]
    
    
    Successfully Setup Software with warning(s).
    (if /u01/app/oracle/product/19.3.0/dbhome_1/bin/skgxpinfo | grep rds;\
    then \
    make -f  /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/ins_rdbms.mk ipc_rds; \
    else \
    make -f  /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/ins_rdbms.mk ipc_g; \
    fi)
    make[1]: Entering directory `/'
    rm -f /u01/app/oracle/product/19.3.0/dbhome_1/lib/libskgxp19.so
    cp /u01/app/oracle/product/19.3.0/dbhome_1/lib//libskgxpg.so /u01/app/oracle/product/19.3.0/dbhome_1/lib/libskgxp19.so
    make[1]: Leaving directory `/'
     - Use stub SKGXN library
    cp /u01/app/oracle/product/19.3.0/dbhome_1/lib/libskgxns.so /u01/app/oracle/product/19.3.0/dbhome_1/lib/libskgxn2.so
    /usr/bin/ar d /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/libknlopt.a ksnkcs.o
    /usr/bin/ar cr /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/libknlopt.a /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/kcsm.o
    chmod 755 /u01/app/oracle/product/19.3.0/dbhome_1/bin
    
     - Linking Oracle
    rm -f /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/oracle
    /u01/app/oracle/product/19.3.0/dbhome_1/bin/orald  -o /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/oracle -m64 -z noexecstack -Wl,--disable-new-dtags -L/u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/ -L/u01/app/oracle/product/19.3.0/dbhome_1/lib/ -L/u01/app/oracle/product/19.3.0/dbhome_1/lib/stubs/   -Wl,-E /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/opimai.o /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/ssoraed.o /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/ttcsoi.o -Wl,--whole-archive -lperfsrv19 -Wl,--no-whole-archive /u01/app/oracle/product/19.3.0/dbhome_1/lib/nautab.o /u01/app/oracle/product/19.3.0/dbhome_1/lib/naeet.o /u01/app/oracle/product/19.3.0/dbhome_1/lib/naect.o /u01/app/oracle/product/19.3.0/dbhome_1/lib/naedhs.o /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/config.o  -ldmext -lserver19 -lodm19 -lofs -lcell19 -lnnet19 -lskgxp19 -lsnls19 -lnls19  -lcore19 -lsnls19 -lnls19 -lcore19 -lsnls19 -lnls19 -lxml19 -lcore19 -lunls19 -lsnls19 -lnls19 -lcore19 -lnls19 -lclient19  -lvsnst19 -lcommon19 -lgeneric19 -lknlopt -loraolap19 -lskjcx19 -lslax19 -lpls19  -lrt -lplp19 -ldmext -lserver19 -lclient19  -lvsnst19 -lcommon19 -lgeneric19 `if [ -f /u01/app/oracle/product/19.3.0/dbhome_1/lib/libavserver19.a ] ; then echo "-lavserver19" ; else echo "-lavstub19"; fi` `if [ -f /u01/app/oracle/product/19.3.0/dbhome_1/lib/libavclient19.a ] ; then echo "-lavclient19" ; fi` -lknlopt -lslax19 -lpls19  -lrt -lplp19 -ljavavm19 -lserver19  -lwwg  `cat /u01/app/oracle/product/19.3.0/dbhome_1/lib/ldflags`    -lncrypt19 -lnsgr19 -lnzjs19 -ln19 -lnl19 -lngsmshd19 -lnro19 `cat /u01/app/oracle/product/19.3.0/dbhome_1/lib/ldflags`    -lncrypt19 -lnsgr19 -lnzjs19 -ln19 -lnl19 -lngsmshd19 -lnnzst19 -lzt19 -lztkg19 -lmm -lsnls19 -lnls19  -lcore19 -lsnls19 -lnls19 -lcore19 -lsnls19 -lnls19 -lxml19 -lcore19 -lunls19 -lsnls19 -lnls19 -lcore19 -lnls19 -lztkg19 `cat /u01/app/oracle/product/19.3.0/dbhome_1/lib/ldflags`    -lncrypt19 -lnsgr19 -lnzjs19 -ln19 -lnl19 -lngsmshd19 -lnro19 `cat /u01/app/oracle/product/19.3.0/dbhome_1/lib/ldflags`    -lncrypt19 -lnsgr19 -lnzjs19 -ln19 -lnl19 -lngsmshd19 -lnnzst19 -lzt19 -lztkg19   -lsnls19 -lnls19  -lcore19 -lsnls19 -lnls19 -lcore19 -lsnls19 -lnls19 -lxml19 -lcore19 -lunls19 -lsnls19 -lnls19 -lcore19 -lnls19 `if /usr/bin/ar tv /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/libknlopt.a | grep "kxmnsd.o" > /dev/null 2>&1 ; then echo " " ; else echo "-lordsdo19 -lserver19"; fi` -L/u01/app/oracle/product/19.3.0/dbhome_1/ctx/lib/ -lctxc19 -lctx19 -lzx19 -lgx19 -lctx19 -lzx19 -lgx19 -lclscest19 -loevm -lclsra19 -ldbcfg19 -lhasgen19 -lskgxn2 -lnnzst19 -lzt19 -lxml19 -lgeneric19 -locr19 -locrb19 -locrutl19 -lhasgen19 -lskgxn2 -lnnzst19 -lzt19 -lxml19 -lgeneric19  -lgeneric19 -lorazip -loraz -llzopro5 -lorabz2 -lorazstd -loralz4 -lipp_z -lipp_bz2 -lippdc -lipps -lippcore  -lippcp -lsnls19 -lnls19  -lcore19 -lsnls19 -lnls19 -lcore19 -lsnls19 -lnls19 -lxml19 -lcore19 -lunls19 -lsnls19 -lnls19 -lcore19 -lnls19 -lsnls19 -lunls19  -lsnls19 -lnls19  -lcore19 -lsnls19 -lnls19 -lcore19 -lsnls19 -lnls19 -lxml19 -lcore19 -lunls19 -lsnls19 -lnls19 -lcore19 -lnls19 -lasmclnt19 -lcommon19 -lcore19  -ledtn19 -laio -lons  -lmql1 -lipc1 -lfthread19    `cat /u01/app/oracle/product/19.3.0/dbhome_1/lib/sysliblist` -Wl,-rpath,/u01/app/oracle/product/19.3.0/dbhome_1/lib -lm    `cat /u01/app/oracle/product/19.3.0/dbhome_1/lib/sysliblist` -ldl -lm   -L/u01/app/oracle/product/19.3.0/dbhome_1/lib `test -x /usr/bin/hugeedit -a -r /usr/lib64/libhugetlbfs.so && test -r /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/shugetlbfs.o && echo -Wl,-zcommon-page-size=2097152 -Wl,-zmax-page-size=2097152 -lhugetlbfs`
    rm -f /u01/app/oracle/product/19.3.0/dbhome_1/bin/oracle
    mv /u01/app/oracle/product/19.3.0/dbhome_1/rdbms/lib/oracle /u01/app/oracle/product/19.3.0/dbhome_1/bin/oracle
    chmod 6751 /u01/app/oracle/product/19.3.0/dbhome_1/bin/oracle
    (if [ ! -f /u01/app/oracle/product/19.3.0/dbhome_1/bin/crsd.bin ]; then \
        getcrshome="/u01/app/oracle/product/19.3.0/dbhome_1/srvm/admin/getcrshome" ; \
        if [ -f "$getcrshome" ]; then \
            crshome="`$getcrshome`"; \
            if [ -n "$crshome" ]; then \
                if [ $crshome != /u01/app/oracle/product/19.3.0/dbhome_1 ]; then \
                    oracle="/u01/app/oracle/product/19.3.0/dbhome_1/bin/oracle"; \
                    $crshome/bin/setasmgidwrap oracle_binary_path=$oracle; \
                fi \
            fi \
        fi \
    fi\
    );
    Changing permissions of /u01/app/oraInventory.
    Adding read,write permissions for group.
    Removing read,write,execute permissions for world.
    
    Changing groupname of /u01/app/oraInventory to oinstall.
    The execution of the script is complete.
    Check /u01/app/oracle/product/19.3.0/dbhome_1/install/root_c40a0e8ea8ed_2019-11-11_03-51-20-097025139.log for the output of root script
    Preparing...                          ########################################
    Updating / installing...
    cvuqdisk-1.0.10-1                     ########################################
    Removing intermediate container c40a0e8ea8ed
     ---> 947c42f51105
    Step 8/11 : USER grid
     ---> Running in f12659d9d383
    Removing intermediate container f12659d9d383
     ---> 06b8b4dcd15e
    Step 9/11 : WORKDIR /home/grid
     ---> Running in 0106cd633ef9
    Removing intermediate container 0106cd633ef9
     ---> c2ad15635695
    Step 10/11 : VOLUME ["/common_scripts"]
     ---> Running in d817b9de8b29
    Removing intermediate container d817b9de8b29
     ---> c0465d5925d5
    Step 11/11 : CMD ["/usr/sbin/oracleinit"]
     ---> Running in 499d3e1cf5d9
    Removing intermediate container 499d3e1cf5d9
     ---> 049f87053beb
    Successfully built 049f87053beb
    Successfully tagged oracle/database-rac:19.3.0
    
      Oracle Database Docker Image for Real Application Clusters (RAC) version 19.3.0 is ready to be extended:
    
        --> oracle/database-rac:19.3.0
    
      Build completed in 3463 seconds.
    
    
    real    57m55.524s
    user    0m16.831s
    sys     0m20.288s
    
    
    

    至此,数据库的RAC docker image就绪。linux image使用的瘦身版。

    $ docker images
    REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
    oracle/database-rac   19.3.0              049f87053beb        About an hour ago   20.6GB
    oraclelinux           7-slim              874477adb545        3 months ago        118MB
    

    此时的空间状态:

    $ df -h
    Filesystem                   Size  Used Avail Use% Mounted on
    devtmpfs                     3.8G     0  3.8G   0% /dev
    tmpfs                        3.8G     0  3.8G   0% /dev/shm
    tmpfs                        3.8G  8.5M  3.8G   1% /run
    tmpfs                        3.8G     0  3.8G   0% /sys/fs/cgroup
    /dev/mapper/vg_main-lv_root   76G   27G   49G  36% /
    /dev/sda1                    497M  125M  373M  26% /boot
    vagrant                      1.9T  1.2T  698G  63% /vagrant
    tmpfs                        771M     0  771M   0% /run/user/1000
    
    

    实际上,此时可以删除数据库和GI的安装介质了。

    创建共享主机解析文件

    sudo mkdir /opt/containers
    sudo touch /opt/containers/rac_host_file
    

    准备共享磁盘(块设备)

    停止虚机。vagrant halt
    挂接50G磁盘。VBoxManage createmedium diskVBoxManage storageattach
    启动虚机。vagrant up
    确认可看到新盘,本例为sdc:

    $ lsblk
    NAME                MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
    sdb                   8:16   0 15.6G  0 disk
    sdc                   8:32   0   50G  0 disk
    sda                   8:0    0   80G  0 disk
    ├─sda2                8:2    0   36G  0 part
    │ ├─vg_main-lv_swap 252:1    0    4G  0 lvm  [SWAP]
    │ └─vg_main-lv_root 252:0    0 75.5G  0 lvm  /
    ├─sda3                8:3    0 43.5G  0 part
    │ └─vg_main-lv_root 252:0    0 75.5G  0 lvm  /
    └─sda1                8:1    0  500M  0 part /boot
    
    

    初始化磁盘以确保其上无文件系统:

    $ sudo dd if=/dev/zero of=/dev/sdc  bs=8k count=10000
    

    口令管理

    以下设置的口令为oracle和grid操作系统用户以及数据库共同使用。

    mkdir /opt/.secrets/
    openssl rand -hex 64 -out /opt/.secrets/pwd.key
    -- 将口令明码写入临时文件
    echo Oracle.123# >/opt/.secrets/common_os_pwdfile
    -- 加密后存储
    openssl enc -aes-256-cbc -salt -in /opt/.secrets/common_os_pwdfile -out /opt/.secrets/common_os_pwdfile.enc -pass file:/opt/.secrets/pwd.key
    -- 删除临时文件
    rm -f /opt/.secrets/common_os_pwdfile
    

    创建第一个RAC节点:racnode1容器

    先创建容器:

    docker create -t -i \
      --hostname racnode1 \
      --volume /boot:/boot:ro \
      --volume /dev/shm \
      --tmpfs /dev/shm:rw,exec,size=4G \
      --volume /opt/containers/rac_host_file:/etc/hosts  \
      --volume /opt/.secrets:/run/secrets \
      --dns-search=example.com \
      --device=/dev/sdc:/dev/asm_disk1  \
      --privileged=false  \
      --cap-add=SYS_NICE \
      --cap-add=SYS_RESOURCE \
      --cap-add=NET_ADMIN \
      -e NODE_VIP=172.16.1.160 \
      -e VIP_HOSTNAME=racnode1-vip  \
      -e PRIV_IP=192.168.17.150 \
      -e PRIV_HOSTNAME=racnode1-priv \
      -e PUBLIC_IP=172.16.1.150 \
      -e PUBLIC_HOSTNAME=racnode1  \
      -e SCAN_NAME=racnode-scan \
      -e SCAN_IP=172.16.1.70  \
      -e OP_TYPE=INSTALL \
      -e DOMAIN=example.com \
      -e ASM_DEVICE_LIST=/dev/asm_disk1 \
      -e ASM_DISCOVERY_DIR=/dev \
      -e COMMON_OS_PWD_FILE=common_os_pwdfile.enc \
      -e PWD_KEY=pwd.key \
      --restart=always --tmpfs=/run -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
      --cpu-rt-runtime=95000 --ulimit rtprio=99  \
      --name racnode1 \
      oracle/database-rac:19.3.0
    

    查看状态:

    $ docker ps -a
    CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS               NAMES
    aa88f55d68cd        oracle/database-rac:19.3.0   "/usr/sbin/oracleinit"   5 seconds ago       Created                                 racnode1
    
    

    配置racnode1的网络:

    docker network disconnect bridge racnode1
    docker network connect rac_pub1_nw --ip 172.16.1.150 racnode1
    docker network connect rac_priv1_nw --ip 192.168.17.150  racnode1
    

    启动第一个容器:

    docker start racnode1
    

    查看日志:

    docker logs -f racnode1
    

    在容器内部或在宿主机都可以查看到dbca 进程,这个比较神奇。
    以下命令可登录到容器内部:

    docker exec -it racnode1 bash
    

    /u01/app/oracle/cfgtoollogs/dbca/ORCLCDB中可以查看到dbca的日志。

    以下为成功执行时的完整日志,可知首先内存还是配小了,另外整个过程耗时1小时50分:

    $ docker logs -f racnode1
    PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOSTNAME=racnode1
    TERM=xterm
    NODE_VIP=172.16.1.160
    VIP_HOSTNAME=racnode1-vip
    PRIV_IP=192.168.17.150
    PRIV_HOSTNAME=racnode1-priv
    PUBLIC_IP=172.16.1.150
    PUBLIC_HOSTNAME=racnode1
    SCAN_NAME=racnode-scan
    SCAN_IP=172.16.1.70
    OP_TYPE=INSTALL
    DOMAIN=example.com
    ASM_DEVICE_LIST=/dev/asm_disk1
    ASM_DISCOVERY_DIR=/dev
    COMMON_OS_PWD_FILE=common_os_pwdfile.enc
    PWD_KEY=pwd.key
    SETUP_LINUX_FILE=setupLinuxEnv.sh
    INSTALL_DIR=/opt/scripts
    GRID_BASE=/u01/app/grid
    GRID_HOME=/u01/app/19.3.0/grid
    INSTALL_FILE_1=LINUX.X64_193000_grid_home.zip
    GRID_INSTALL_RSP=gridsetup_19c.rsp
    GRID_SW_INSTALL_RSP=grid_sw_install_19c.rsp
    GRID_SETUP_FILE=setupGrid.sh
    FIXUP_PREQ_FILE=fixupPreq.sh
    INSTALL_GRID_BINARIES_FILE=installGridBinaries.sh
    INSTALL_GRID_PATCH=applyGridPatch.sh
    INVENTORY=/u01/app/oraInventory
    CONFIGGRID=configGrid.sh
    ADDNODE=AddNode.sh
    DELNODE=DelNode.sh
    ADDNODE_RSP=grid_addnode.rsp
    SETUPSSH=setupSSH.expect
    DOCKERORACLEINIT=dockeroracleinit
    GRID_USER_HOME=/home/grid
    SETUPGRIDENV=setupGridEnv.sh
    RESET_OS_PASSWORD=resetOSPassword.sh
    MULTI_NODE_INSTALL=MultiNodeInstall.py
    DB_BASE=/u01/app/oracle
    DB_HOME=/u01/app/oracle/product/19.3.0/dbhome_1
    INSTALL_FILE_2=LINUX.X64_193000_db_home.zip
    DB_INSTALL_RSP=db_sw_install_19c.rsp
    DBCA_RSP=dbca_19c.rsp
    DB_SETUP_FILE=setupDB.sh
    PWD_FILE=setPassword.sh
    RUN_FILE=runOracle.sh
    STOP_FILE=stopOracle.sh
    ENABLE_RAC_FILE=enableRAC.sh
    CHECK_DB_FILE=checkDBStatus.sh
    USER_SCRIPTS_FILE=runUserScripts.sh
    REMOTE_LISTENER_FILE=remoteListener.sh
    INSTALL_DB_BINARIES_FILE=installDBBinaries.sh
    GRID_HOME_CLEANUP=GridHomeCleanup.sh
    ORACLE_HOME_CLEANUP=OracleHomeCleanup.sh
    DB_USER=oracle
    GRID_USER=grid
    FUNCTIONS=functions.sh
    COMMON_SCRIPTS=/common_scripts
    CHECK_SPACE_FILE=checkSpace.sh
    RESET_FAILED_UNITS=resetFailedUnits.sh
    SET_CRONTAB=setCrontab.sh
    CRONTAB_ENTRY=crontabEntry
    EXPECT=/usr/bin/expect
    BIN=/usr/sbin
    container=true
    INSTALL_SCRIPTS=/opt/scripts/install
    SCRIPT_DIR=/opt/scripts/startup
    GRID_PATH=/u01/app/19.3.0/grid/bin:/u01/app/19.3.0/grid/OPatch/:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    DB_PATH=/u01/app/oracle/product/19.3.0/dbhome_1/bin:/u01/app/oracle/product/19.3.0/dbhome_1/OPatch/:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    GRID_LD_LIBRARY_PATH=/u01/app/19.3.0/grid/lib:/usr/lib:/lib
    DB_LD_LIBRARY_PATH=/u01/app/oracle/product/19.3.0/dbhome_1/lib:/usr/lib:/lib
    HOME=/home/grid
    Failed to parse kernel command line, ignoring: No such file or directory
    systemd 219 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN)
    Detected virtualization other.
    Detected architecture x86-64.
    
    Welcome to Oracle Linux Server 7.6!
    
    Set hostname to <racnode1>.
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directoryFailed to parse kernel command line, ignoring: No such file or directory
    
    /usr/lib/systemd/system-generators/systemd-fstab-generator failed with error code 1.
    Binding to IPv6 address not available since kernel does not support IPv6.
    Binding to IPv6 address not available since kernel does not support IPv6.
    Cannot add dependency job for unit display-manager.service, ignoring: Unit not found.
    [  OK  ] Reached target Swap.
    [  OK  ] Started Dispatch Password Requests to Console Directory Watch.
    [  OK  ] Started Forward Password Requests to Wall Directory Watch.
    [  OK  ] Created slice Root Slice.
    [  OK  ] Listening on /dev/initctl Compatibility Named Pipe.
    [  OK  ] Created slice System Slice.
    [  OK  ] Created slice User and Session Slice.
    [  OK  ] Reached target Slices.
    [  OK  ] Listening on Journal Socket.
             Starting Read and set NIS domainname from /etc/sysconfig/network...
             Starting Configure read-only root support...
             Starting Journal Service...
             Starting Rebuild Hardware Database...
    Couldn't determine result for ConditionKernelCommandLine=|rd.modules-load for systemd-modules-load.service, assuming failed: No such file or directory
    Couldn't determine result for ConditionKernelCommandLine=|modules-load for systemd-modules-load.service, assuming failed: No such file or directory
    [  OK  ] Created slice system-getty.slice.
    [  OK  ] Listening on Delayed Shutdown Socket.
    [  OK  ] Reached target Local Encrypted Volumes.
    [  OK  ] Reached target Local File Systems (Pre).
    [  OK  ] Reached target RPC Port Mapper.
    [  OK  ] Started Journal Service.
    [  OK  ] Started Read and set NIS domainname from /etc/sysconfig/network.
             Starting Flush Journal to Persistent Storage...
    [  OK  ] Started Flush Journal to Persistent Storage.
    [  OK  ] Started Configure read-only root support.
             Starting Load/Save Random Seed...
    [  OK  ] Reached target Local File Systems.
             Starting Mark the need to relabel after reboot...
             Starting Preprocess NFS configuration...
             Starting Rebuild Journal Catalog...
             Starting Create Volatile Files and Directories...
    [  OK  ] Started Load/Save Random Seed.
    [  OK  ] Started Mark the need to relabel after reboot.
    [  OK  ] Started Preprocess NFS configuration.
    [  OK  ] Started Rebuild Journal Catalog.
    [  OK  ] Started Create Volatile Files and Directories.
             Starting Update UTMP about System Boot/Shutdown...
             Mounting RPC Pipe File System...
    [FAILED] Failed to mount RPC Pipe File System.
    See 'systemctl status var-lib-nfs-rpc_pipefs.mount' for details.
    [DEPEND] Dependency failed for rpc_pipefs.target.
    [DEPEND] Dependency failed for RPC security service for NFS client and server.
    [  OK  ] Started Update UTMP about System Boot/Shutdown.
    [  OK  ] Started Rebuild Hardware Database.
             Starting Update is Completed...
    [  OK  ] Started Update is Completed.
    [  OK  ] Reached target System Initialization.
    [  OK  ] Started Flexible branding.
    [  OK  ] Reached target Paths.
    [  OK  ] Started Daily Cleanup of Temporary Directories.
    [  OK  ] Reached target Timers.
    [  OK  ] Listening on D-Bus System Message Bus Socket.
    [  OK  ] Listening on RPCbind Server Activation Socket.
             Starting RPC bind service...
    [  OK  ] Reached target Sockets.
    [  OK  ] Reached target Basic System.
             Starting Self Monitoring and Reporting Technology (SMART) Daemon...
             Starting OpenSSH Server Key Generation...
    [  OK  ] Started D-Bus System Message Bus.
             Starting Login Service...
             Starting GSSAPI Proxy Daemon...
             Starting Resets System Activity Logs...
             Starting LSB: Bring up/down networking...
    [  OK  ] Started RPC bind service.
             Starting Cleanup of Temporary Directories...
    [  OK  ] Started Resets System Activity Logs.
    [  OK  ] Started Login Service.
    [  OK  ] Started GSSAPI Proxy Daemon.
    [  OK  ] Reached target NFS client services.
    [  OK  ] Reached target Remote File Systems (Pre).
    [  OK  ] Reached target Remote File Systems.
             Starting Permit User Sessions...
    [  OK  ] Started Cleanup of Temporary Directories.
    [  OK  ] Started Permit User Sessions.
    [  OK  ] Started Command Scheduler.
    [  OK  ] Started OpenSSH Server Key Generation.
    [  OK  ] Started LSB: Bring up/down networking.
    [  OK  ] Reached target Network.
             Starting OpenSSH server daemon...
             Starting /etc/rc.d/rc.local Compatibility...
    [  OK  ] Reached target Network is Online.
             Starting Notify NFS peers of a restart...
    [  OK  ] Started Notify NFS peers of a restart.
    [  OK  ] Started /etc/rc.d/rc.local Compatibility.
    [  OK  ] Started Console Getty.
    [  OK  ] Reached target Login Prompts.
    [  OK  ] Started OpenSSH server daemon.
    11-11-2019 06:55:07 UTC :  : Process id of the program :
    11-11-2019 06:55:07 UTC :  : #################################################
    11-11-2019 06:55:07 UTC :  :  Starting Grid Installation
    11-11-2019 06:55:07 UTC :  : #################################################
    11-11-2019 06:55:07 UTC :  : Pre-Grid Setup steps are in process
    11-11-2019 06:55:07 UTC :  : Process id of the program :
    11-11-2019 06:55:07 UTC :  : Disable failed service var-lib-nfs-rpc_pipefs.mount
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    11-11-2019 06:55:07 UTC :  : Resetting Failed Services
    11-11-2019 06:55:07 UTC :  : Sleeping for 60 seconds
    [  OK  ] Started Self Monitoring and Reporting Technology (SMART) Daemon.
    [  OK  ] Reached target Multi-User System.
    [  OK  ] Reached target Graphical Interface.
             Starting Update UTMP about System Runlevel Changes...
    [  OK  ] Started Update UTMP about System Runlevel Changes.
    
    Oracle Linux Server 7.6
    Kernel 4.14.35-1902.6.6.el7uek.x86_64 on an x86_64
    
    racnode1 login: 11-11-2019 06:56:07 UTC :  : Systemctl state is running!
    11-11-2019 06:56:07 UTC :  : Setting correct permissions for /bin/ping
    11-11-2019 06:56:08 UTC :  : Public IP is set to 172.16.1.150
    11-11-2019 06:56:08 UTC :  : RAC Node PUBLIC Hostname is set to racnode1
    11-11-2019 06:56:08 UTC :  : racnode1 already exists : 172.16.1.150     racnode1.example.com    racnode1
    192.168.17.150  racnode1-priv.example.com       racnode1-priv
    172.16.1.160    racnode1-vip.example.com        racnode1-vip, no  update required
    11-11-2019 06:56:08 UTC :  : racnode1-priv already exists : 192.168.17.150      racnode1-priv.example.com       racnode1-priv, no  update required
    11-11-2019 06:56:08 UTC :  : racnode1-vip already exists : 172.16.1.160 racnode1-vip.example.com        racnode1-vip, no  update required
    11-11-2019 06:56:08 UTC :  : racnode-scan already exists : 172.16.1.70  racnode-scan.example.com        racnode-scan, no  update required
    11-11-2019 06:56:08 UTC :  : Preapring Device list
    11-11-2019 06:56:08 UTC :  : Changing Disk permission and ownership /dev/asm_disk1
    11-11-2019 06:56:08 UTC :  : DNS_SERVERS is set to empty. /etc/resolv.conf will use default dns docker embedded server.
    11-11-2019 06:56:08 UTC :  : #####################################################################
    11-11-2019 06:56:08 UTC :  :  RAC setup will begin in 2 minutes
    11-11-2019 06:56:08 UTC :  : ####################################################################
    11-11-2019 06:56:10 UTC :  : ###################################################
    11-11-2019 06:56:10 UTC :  : Pre-Grid Setup steps completed
    11-11-2019 06:56:10 UTC :  : ###################################################
    11-11-2019 06:56:10 UTC :  : Checking if grid is already configured
    11-11-2019 06:56:10 UTC :  : Process id of the program :
    11-11-2019 06:56:10 UTC :  : Public IP is set to 172.16.1.150
    11-11-2019 06:56:10 UTC :  : RAC Node PUBLIC Hostname is set to racnode1
    11-11-2019 06:56:10 UTC :  : Domain is defined to example.com
    11-11-2019 06:56:10 UTC :  : Default setting of AUTO GNS VIP set to false. If you want to use AUTO GNS VIP, please pass DHCP_CONF as an env parameter set to true
    11-11-2019 06:56:10 UTC :  : RAC VIP set to 172.16.1.160
    11-11-2019 06:56:10 UTC :  : RAC Node VIP hostname is set to racnode1-vip
    11-11-2019 06:56:10 UTC :  : SCAN_NAME name is racnode-scan
    11-11-2019 06:56:10 UTC :  : SCAN PORT is set to empty string. Setting it to 1521 port.
    11-11-2019 06:56:10 UTC :  : 172.16.1.70
    11-11-2019 06:56:10 UTC :  : SCAN Name resolving to IP. Check Passed!
    11-11-2019 06:56:11 UTC :  : SCAN_IP name is 172.16.1.70
    11-11-2019 06:56:11 UTC :  : RAC Node PRIV IP is set to 192.168.17.150
    11-11-2019 06:56:11 UTC :  : RAC Node private hostname is set to racnode1-priv
    11-11-2019 06:56:11 UTC :  : CMAN_NAME set to the empty string
    11-11-2019 06:56:11 UTC :  : CMAN_IP set to the empty string
    11-11-2019 06:56:11 UTC :  : Cluster Name is not defined
    11-11-2019 06:56:11 UTC :  : Cluster name is set to 'racnode-c'
    11-11-2019 06:56:11 UTC :  : Password file generated
    11-11-2019 06:56:11 UTC :  : Common OS Password string is set for Grid user
    11-11-2019 06:56:11 UTC :  : Common OS Password string is set for  Oracle user
    11-11-2019 06:56:11 UTC :  : Common OS Password string is set for Oracle Database
    11-11-2019 06:56:11 UTC :  : Setting CONFIGURE_GNS to false
    11-11-2019 06:56:11 UTC :  : GRID_RESPONSE_FILE env variable set to empty. configGrid.sh will use standard cluster responsefile
    11-11-2019 06:56:11 UTC :  : Location for User script SCRIPT_ROOT set to /common_scripts
    11-11-2019 06:56:11 UTC :  : IGNORE_CVU_CHECKS is set to true
    11-11-2019 06:56:11 UTC :  : Oracle SID is set to ORCLCDB
    11-11-2019 06:56:11 UTC :  : Oracle PDB name is set to ORCLPDB
    11-11-2019 06:56:11 UTC :  : Check passed for network card eth1 for public IP 172.16.1.150
    11-11-2019 06:56:11 UTC :  : Public Netmask : 255.255.255.0
    11-11-2019 06:56:11 UTC :  : Check passed for network card eth0 for private IP 192.168.17.150
    11-11-2019 06:56:11 UTC :  : Building NETWORK_STRING to set  networkInterfaceList in Grid Response File
    11-11-2019 06:56:11 UTC :  : Network InterfaceList  set to eth1:172.16.1.0:1,eth0:192.168.17.0:5
    11-11-2019 06:56:11 UTC :  : Setting random password for grid user
    11-11-2019 06:56:12 UTC :  : Setting random password for oracle user
    11-11-2019 06:56:13 UTC :  : Calling setupSSH function
    11-11-2019 06:56:13 UTC :  : SSh will be setup among racnode1 nodes
    11-11-2019 06:56:13 UTC :  : Running SSH setup for grid user between nodes racnode1
    11-11-2019 06:56:52 UTC :  : Running SSH setup for oracle user between nodes racnode1
    11-11-2019 06:57:00 UTC :  : SSH check fine for the racnode1
    11-11-2019 06:57:01 UTC :  : SSH check fine for the oracle@racnode1
    11-11-2019 06:57:01 UTC :  : Preapring Device list
    11-11-2019 06:57:01 UTC :  : Changing Disk permission and ownership
    11-11-2019 06:57:01 UTC :  : ASM Disk size : 0
    11-11-2019 06:57:01 UTC :  : ASM Device list will be with failure groups /dev/asm_disk1,
    11-11-2019 06:57:01 UTC :  : ASM Device list will be groups /dev/asm_disk1
    11-11-2019 06:57:01 UTC :  : CLUSTER_TYPE env variable is set to STANDALONE, will not process GIMR DEVICE list as default Diskgroup is set to DATA. GIMR DEVICE List will be processed when CLUSTER_TYPE is set to DOMAIN for DSC
    11-11-2019 06:57:01 UTC :  : Nodes in the cluster racnode1
    11-11-2019 06:57:01 UTC :  : Setting Device permissions for RAC Install  on racnode1
    11-11-2019 06:57:01 UTC :  : Preapring ASM Device list
    11-11-2019 06:57:01 UTC :  : Changing Disk permission and ownership
    11-11-2019 06:57:01 UTC :  : Command : su - $GRID_USER -c "ssh $node sudo chown $GRID_USER:asmadmin $device" execute on racnode1
    11-11-2019 06:57:02 UTC :  : Command : su - $GRID_USER -c "ssh $node sudo chmod 660 $device" execute on racnode1
    11-11-2019 06:57:02 UTC :  : Populate Rac Env Vars on Remote Hosts
    11-11-2019 06:57:02 UTC :  : Command : su - $GRID_USER -c "ssh $node sudo echo \"export ASM_DEVICE_LIST=${ASM_DEVICE_LIST}\" >> /etc/rac_env_vars" execute on racnode1
    11-11-2019 06:57:02 UTC :  : Generating Reponsefile
    11-11-2019 06:57:02 UTC :  : Running cluvfy Checks
    11-11-2019 06:57:02 UTC :  : Performing Cluvfy Checks
    11-11-2019 06:58:26 UTC :  : Checking /tmp/cluvfy_check.txt if there is any failed check.
    
    ERROR:
    PRVG-10467 : The default Oracle Inventory group could not be determined.
    
    Verifying Physical Memory ...FAILED (PRVF-7530)
    Verifying Available Physical Memory ...PASSED
    Verifying Swap Size ...FAILED (PRVF-7573)
    Verifying Free Space: racnode1:/usr,racnode1:/var,racnode1:/etc,racnode1:/sbin,racnode1:/tmp ...PASSED
    Verifying User Existence: grid ...
      Verifying Users With Same UID: 54332 ...PASSED
    Verifying User Existence: grid ...PASSED
    Verifying Group Existence: asmadmin ...PASSED
    Verifying Group Existence: asmdba ...PASSED
    Verifying Group Membership: asmdba ...PASSED
    Verifying Group Membership: asmadmin ...PASSED
    Verifying Run Level ...PASSED
    Verifying Hard Limit: maximum open file descriptors ...PASSED
    Verifying Soft Limit: maximum open file descriptors ...PASSED
    Verifying Hard Limit: maximum user processes ...PASSED
    Verifying Soft Limit: maximum user processes ...PASSED
    Verifying Soft Limit: maximum stack size ...PASSED
    Verifying Architecture ...PASSED
    Verifying OS Kernel Version ...PASSED
    Verifying OS Kernel Parameter: semmsl ...PASSED
    Verifying OS Kernel Parameter: semmns ...PASSED
    Verifying OS Kernel Parameter: semopm ...PASSED
    Verifying OS Kernel Parameter: semmni ...PASSED
    Verifying OS Kernel Parameter: shmmax ...PASSED
    Verifying OS Kernel Parameter: shmmni ...PASSED
    Verifying OS Kernel Parameter: shmall ...FAILED (PRVG-1201)
    Verifying OS Kernel Parameter: file-max ...PASSED
    Verifying OS Kernel Parameter: aio-max-nr ...FAILED (PRVG-1205)
    Verifying OS Kernel Parameter: panic_on_oops ...PASSED
    Verifying Package: kmod-20-21 (x86_64) ...PASSED
    Verifying Package: kmod-libs-20-21 (x86_64) ...PASSED
    Verifying Package: binutils-2.23.52.0.1 ...PASSED
    Verifying Package: compat-libcap1-1.10 ...PASSED
    Verifying Package: libgcc-4.8.2 (x86_64) ...PASSED
    Verifying Package: libstdc++-4.8.2 (x86_64) ...PASSED
    Verifying Package: libstdc++-devel-4.8.2 (x86_64) ...PASSED
    Verifying Package: sysstat-10.1.5 ...PASSED
    Verifying Package: ksh ...PASSED
    Verifying Package: make-3.82 ...PASSED
    Verifying Package: glibc-2.17 (x86_64) ...PASSED
    Verifying Package: glibc-devel-2.17 (x86_64) ...PASSED
    Verifying Package: libaio-0.3.109 (x86_64) ...PASSED
    Verifying Package: libaio-devel-0.3.109 (x86_64) ...PASSED
    Verifying Package: nfs-utils-1.2.3-15 ...PASSED
    Verifying Package: smartmontools-6.2-4 ...PASSED
    Verifying Package: net-tools-2.0-0.17 ...PASSED
    Verifying Port Availability for component "Oracle Remote Method Invocation (ORMI)" ...PASSED
    Verifying Port Availability for component "Oracle Notification Service (ONS)" ...PASSED
    Verifying Port Availability for component "Oracle Cluster Synchronization Services (CSSD)" ...PASSED
    Verifying Port Availability for component "Oracle Notification Service (ONS) Enterprise Manager support" ...PASSED
    Verifying Port Availability for component "Oracle Database Listener" ...PASSED
    Verifying Users With Same UID: 0 ...PASSED
    Verifying Current Group ID ...PASSED
    Verifying Root user consistency ...PASSED
    Verifying Host name ...PASSED
    Verifying Node Connectivity ...
      Verifying Hosts File ...PASSED
      Verifying Check that maximum (MTU) size packet goes through subnet ...PASSED
    Verifying Node Connectivity ...PASSED
    Verifying Multicast or broadcast check ...PASSED
    Verifying ASM Integrity ...PASSED
    Verifying Device Checks for ASM ...
      Verifying Access Control List check ...PASSED
    Verifying Device Checks for ASM ...PASSED
    Verifying Network Time Protocol (NTP) ...
      Verifying '/etc/ntp.conf' ...PASSED
    Verifying Network Time Protocol (NTP) ...FAILED (PRVG-1017)
    Verifying Same core file name pattern ...PASSED
    Verifying User Mask ...PASSED
    Verifying User Not In Group "root": grid ...PASSED
    Verifying Time zone consistency ...PASSED
    Verifying VIP Subnet configuration check ...PASSED
    Verifying resolv.conf Integrity ...FAILED (PRVG-10048)
    Verifying DNS/NIS name service ...
      Verifying Name Service Switch Configuration File Integrity ...PASSED
    Verifying DNS/NIS name service ...FAILED (PRVG-1101)
    Verifying Single Client Access Name (SCAN) ...WARNING (PRVG-11368)
    Verifying Domain Sockets ...PASSED
    Verifying /boot mount ...PASSED
    Verifying Daemon "avahi-daemon" not configured and running ...PASSED
    Verifying Daemon "proxyt" not configured and running ...PASSED
    Verifying loopback network interface address ...PASSED
    Verifying Oracle base: /u01/app/grid ...
      Verifying '/u01/app/grid' ...PASSED
    Verifying Oracle base: /u01/app/grid ...PASSED
    Verifying User Equivalence ...PASSED
    Verifying RPM Package Manager database ...INFORMATION (PRVG-11250)
    Verifying Network interface bonding status of private interconnect network interfaces ...PASSED
    Verifying /dev/shm mounted as temporary file system ...PASSED
    Verifying File system mount options for path /var ...PASSED
    Verifying DefaultTasksMax parameter ...PASSED
    Verifying zeroconf check ...PASSED
    Verifying ASM Filter Driver configuration ...PASSED
    Verifying Systemd login manager IPC parameter ...PASSED
    Verifying Access control attributes for cluster manifest file ...PASSED
    
    Pre-check for cluster services setup was unsuccessful on all the nodes.
    
    
    Failures were encountered during execution of CVU verification request "stage -pre crsinst".
    
    Verifying Physical Memory ...FAILED
    racnode1: PRVF-7530 : Sufficient physical memory is not available on node
              "racnode1" [Required physical memory = 8GB (8388608.0KB)]
    
    Verifying Swap Size ...FAILED
    racnode1: PRVF-7573 : Sufficient swap size is not available on node "racnode1"
              [Required = 7.5283GB (7893968.0KB) ; Found = 4GB (4194300.0KB)]
    
    Verifying OS Kernel Parameter: shmall ...FAILED
    racnode1: PRVG-1201 : OS kernel parameter "shmall" does not have expected
              configured value on node "racnode1" [Expected = "2251799813685247" ;
              Current = "18446744073692774000"; Configured = "1073741824"].
    
    Verifying OS Kernel Parameter: aio-max-nr ...FAILED
    racnode1: PRVG-1205 : OS kernel parameter "aio-max-nr" does not have expected
              current value on node "racnode1" [Expected = "1048576" ; Current =
              "65536"; Configured = "1048576"].
    
    Verifying Network Time Protocol (NTP) ...FAILED
    racnode1: PRVG-1017 : NTP configuration file "/etc/ntp.conf" is present on
              nodes "racnode1" on which NTP daemon or service was not running
    
    Verifying resolv.conf Integrity ...FAILED
    racnode1: PRVG-10048 : Name "racnode1" was not resolved to an address of the
              specified type by name servers "127.0.0.11".
    
    Verifying DNS/NIS name service ...FAILED
    PRVG-1101 : SCAN name "racnode-scan" failed to resolve
    
    Verifying Single Client Access Name (SCAN) ...WARNING
    racnode1: PRVG-11368 : A SCAN is recommended to resolve to "3" or more IP
              addresses, but SCAN "racnode-scan" resolves to only "172.16.1.70"
    
    Verifying RPM Package Manager database ...INFORMATION
    PRVG-11250 : The check "RPM Package Manager database" was not performed because
    it needs 'root' user privileges.
    
    
    CVU operation performed:      stage -pre crsinst
    Date:                         Nov 11, 2019 6:57:15 AM
    CVU home:                     /u01/app/19.3.0/grid/
    User:                         grid
    11-11-2019 06:58:27 UTC :  : CVU Checks are ignored as IGNORE_CVU_CHECKS set to true. It is recommended to set IGNORE_CVU_CHECKS to false and meet all the cvu checks requirement. RAC installation might fail, if there are failed cvu checks.
    11-11-2019 06:58:27 UTC :  : Running Grid Installation
    11-11-2019 07:00:07 UTC :  : Running root.sh
    11-11-2019 07:00:07 UTC :  : Nodes in the cluster racnode1
    11-11-2019 07:00:07 UTC :  : Running root.sh on racnode1
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directoryFailed to parse kernel command line, ignoring: No such file or directoryFailed to parse kernel command line, ignoring: No such file or directory
    
    
    Failed to parse kernel command line, ignoring: No such file or directoryFailed to parse kernel command line, ignoring: No such file or directory
    
    Failed to parse kernel command line, ignoring: No such file or directory
    11-11-2019 07:27:11 UTC :  : Running post root.sh steps
    11-11-2019 07:27:12 UTC :  : Running post root.sh steps to setup Grid env
    11-11-2019 07:33:02 UTC :  : Checking Cluster Status
    11-11-2019 07:33:02 UTC :  : Nodes in the cluster
    11-11-2019 07:33:02 UTC :  : Removing /tmp/cluvfy_check.txt as cluster check has passed
    11-11-2019 07:33:02 UTC :  : Running User Script for grid user
    11-11-2019 07:33:04 UTC :  : Generating DB Responsefile Running DB creation
    11-11-2019 07:33:04 UTC :  : Running DB creation
    11-11-2019 08:39:01 UTC :  : Checking DB status
    11-11-2019 08:39:10 UTC :  : #################################################################
    11-11-2019 08:39:10 UTC :  :  Oracle Database ORCLCDB is up and running on racnode1
    11-11-2019 08:39:10 UTC :  : #################################################################
    11-11-2019 08:39:10 UTC :  : Running User Script oracle user
    11-11-2019 08:39:13 UTC :  : Setting Remote Listener
    11-11-2019 08:39:20 UTC :  : ####################################
    11-11-2019 08:39:20 UTC :  : ORACLE RAC DATABASE IS READY TO USE!
    11-11-2019 08:39:20 UTC :  : ####################################
    

    注意最后3行,即表示已成功完成:
    登录到容器内部确认GI和数据库均正常:

    [grid@racnode1 ~]$ crsctl check cluster
    CRS-4537: Cluster Ready Services is online
    CRS-4529: Cluster Synchronization Services is online
    CRS-4533: Event Manager is online
    
    [grid@racnode1 ~]$ asmcmd lsdg
    State    Type    Rebal  Sector  Logical_Sector  Block       AU  Total_MB  Free_MB  Req_mir_free_MB  Usable_file_MB  Offline_disks  Voting_files  Name
    MOUNTED  EXTERN  N         512             512   4096  4194304     51200    47000                0           47000              0             Y  DATA/
    
    [grid@racnode1 ~]$ export ORACLE_HOME=/u01/app/19.3.0/grid
    [grid@racnode1 ~]$ lsnrctl status
    
    LSNRCTL for Linux: Version 19.0.0.0.0 - Production on 12-NOV-2019 12:10:31
    
    Copyright (c) 1991, 2019, Oracle.  All rights reserved.
    
    Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=IPC)(KEY=LISTENER)))
    STATUS of the LISTENER
    ------------------------
    Alias                     LISTENER
    Version                   TNSLSNR for Linux: Version 19.0.0.0.0 - Production
    Start Date                12-NOV-2019 12:04:52
    Uptime                    0 days 0 hr. 5 min. 38 sec
    Trace Level               off
    Security                  ON: Local OS Authentication
    SNMP                      OFF
    Listener Parameter File   /u01/app/19.3.0/grid/network/admin/listener.ora
    Listener Log File         /u01/app/grid/diag/tnslsnr/racnode1/listener/alert/log.xml
    Listening Endpoints Summary...
      (DESCRIPTION=(ADDRESS=(PROTOCOL=ipc)(KEY=LISTENER)))
      (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=172.16.1.150)(PORT=1521)))
      (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=172.16.1.160)(PORT=1521)))
      (DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(HOST=racnode1.example.com)(PORT=5500))(Security=(my_wallet_directory=/u01/app/oracle/product/19.3.0/dbhome_1/admin/ORCLCDB/xdb_wallet))(Presentation=HTTP)(Session=RAW))
    Services Summary...
    Service "+ASM" has 1 instance(s).
      Instance "+ASM1", status READY, has 1 handler(s) for this service...
    Service "+ASM_DATA" has 1 instance(s).
      Instance "+ASM1", status READY, has 1 handler(s) for this service...
    Service "970f09422a5234d2e053960110ac7965" has 1 instance(s).
      Instance "ORCLCDB1", status READY, has 1 handler(s) for this service...
    Service "ORCLCDB" has 1 instance(s).
      Instance "ORCLCDB1", status READY, has 1 handler(s) for this service...
    Service "ORCLCDBXDB" has 1 instance(s).
      Instance "ORCLCDB1", status READY, has 1 handler(s) for this service...
    Service "orclpdb" has 1 instance(s).
      Instance "ORCLCDB1", status READY, has 1 handler(s) for this service...
    The command completed successfully
    
    [grid@racnode1 ~]$ sudo -s
    bash-4.2# su - oracle
    
    [oracle@racnode1 ~]$ export ORACLE_HOME=/u01/app/oracle/product/19.3.0/dbhome_1/
    [oracle@racnode1 admin]$ cat $ORACLE_HOME/network/admin/tnsnames.ora
    # tnsnames.ora Network Configuration File: /u01/app/oracle/product/19.3.0/dbhome_1/network/admin/tnsnames.ora
    # Generated by Oracle configuration tools.
    
    ORCLCDB =
      (DESCRIPTION =
        (ADDRESS = (PROTOCOL = TCP)(HOST = racnode-scan)(PORT = 1521))
        (CONNECT_DATA =
          (SERVER = DEDICATED)
          (SERVICE_NAME = ORCLCDB)
        )
      )
    
    [oracle@racnode1 admin]$ sqlplus sys/Oracle.123#@ORCLCDB as sysdba
    
    SQL*Plus: Release 19.0.0.0.0 - Production on Tue Nov 12 12:13:30 2019
    Version 19.3.0.0.0
    
    Copyright (c) 1982, 2019, Oracle.  All rights reserved.
    
    
    Connected to:
    Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
    Version 19.3.0.0.0
    
    SQL> select instance_name from v$instance;
    
    INSTANCE_NAME
    ----------------
    ORCLCDB1
    
    SQL> select name from v$database;
    
    NAME
    ---------
    ORCLCDB
    
    SQL> exit
    Disconnected from Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
    Version 19.3.0.0.0
    
    

    添加第二个RAC节点

    首先创建容器racnode2:

    docker create -t -i \
      --hostname racnode2 \
      --volume /dev/shm \
      --tmpfs /dev/shm:rw,exec,size=4G  \
      --volume /boot:/boot:ro \
      --dns-search=example.com  \
      --volume /opt/containers/rac_host_file:/etc/hosts \
      --volume /opt/.secrets:/run/secrets \
      --device=/dev/sdc:/dev/asm_disk1 \
      --privileged=false \
      --cap-add=SYS_NICE \
      --cap-add=SYS_RESOURCE \
      --cap-add=NET_ADMIN \
      -e EXISTING_CLS_NODES=racnode1 \
      -e NODE_VIP=172.16.1.161  \
      -e VIP_HOSTNAME=racnode2-vip  \
      -e PRIV_IP=192.168.17.151  \
      -e PRIV_HOSTNAME=racnode2-priv \
      -e PUBLIC_IP=172.16.1.151  \
      -e PUBLIC_HOSTNAME=racnode2  \
      -e DOMAIN=example.com \
      -e SCAN_NAME=racnode-scan \
      -e SCAN_IP=172.16.1.70 \
      -e ASM_DISCOVERY_DIR=/dev \
      -e ASM_DEVICE_LIST=/dev/asm_disk1 \
      -e ORACLE_SID=ORCLCDB \
      -e OP_TYPE=ADDNODE \
      -e COMMON_OS_PWD_FILE=common_os_pwdfile.enc \
      -e PWD_KEY=pwd.key \
      --tmpfs=/run -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
      --cpu-rt-runtime=95000 \
      --ulimit rtprio=99  \
      --restart=always \
      --name racnode2 \
      oracle/database-rac:19.3.0
    

    为第二个容器分配网络:

    docker network disconnect bridge racnode2
    docker network connect rac_pub1_nw --ip 172.16.1.151 racnode2
    docker network connect rac_priv1_nw --ip 192.168.17.151 racnode2
    

    启动第二个容器:

    docker start racnode2
    

    查看日志:

    docker logs -f racnode2
    

    日志如下:

    [vagrant@ol7-vagrant-rac ~]$ docker logs -f racnode2
    PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOSTNAME=racnode2
    TERM=xterm
    EXISTING_CLS_NODES=racnode1
    NODE_VIP=172.16.1.161
    VIP_HOSTNAME=racnode2-vip
    PRIV_IP=192.168.17.151
    PRIV_HOSTNAME=racnode2-priv
    PUBLIC_IP=172.16.1.151
    PUBLIC_HOSTNAME=racnode2
    DOMAIN=example.com
    SCAN_NAME=racnode-scan
    SCAN_IP=172.16.1.70
    ASM_DISCOVERY_DIR=/dev
    ASM_DEVICE_LIST=/dev/asm_disk1
    ORACLE_SID=ORCLCDB
    OP_TYPE=ADDNODE
    COMMON_OS_PWD_FILE=common_os_pwdfile.enc
    PWD_KEY=pwd.key
    SETUP_LINUX_FILE=setupLinuxEnv.sh
    INSTALL_DIR=/opt/scripts
    GRID_BASE=/u01/app/grid
    GRID_HOME=/u01/app/19.3.0/grid
    INSTALL_FILE_1=LINUX.X64_193000_grid_home.zip
    GRID_INSTALL_RSP=gridsetup_19c.rsp
    GRID_SW_INSTALL_RSP=grid_sw_install_19c.rsp
    GRID_SETUP_FILE=setupGrid.sh
    FIXUP_PREQ_FILE=fixupPreq.sh
    INSTALL_GRID_BINARIES_FILE=installGridBinaries.sh
    INSTALL_GRID_PATCH=applyGridPatch.sh
    INVENTORY=/u01/app/oraInventory
    CONFIGGRID=configGrid.sh
    ADDNODE=AddNode.sh
    DELNODE=DelNode.sh
    ADDNODE_RSP=grid_addnode.rsp
    SETUPSSH=setupSSH.expect
    DOCKERORACLEINIT=dockeroracleinit
    GRID_USER_HOME=/home/grid
    SETUPGRIDENV=setupGridEnv.sh
    RESET_OS_PASSWORD=resetOSPassword.sh
    MULTI_NODE_INSTALL=MultiNodeInstall.py
    DB_BASE=/u01/app/oracle
    DB_HOME=/u01/app/oracle/product/19.3.0/dbhome_1
    INSTALL_FILE_2=LINUX.X64_193000_db_home.zip
    DB_INSTALL_RSP=db_sw_install_19c.rsp
    DBCA_RSP=dbca_19c.rsp
    DB_SETUP_FILE=setupDB.sh
    PWD_FILE=setPassword.sh
    RUN_FILE=runOracle.sh
    STOP_FILE=stopOracle.sh
    ENABLE_RAC_FILE=enableRAC.sh
    CHECK_DB_FILE=checkDBStatus.sh
    USER_SCRIPTS_FILE=runUserScripts.sh
    REMOTE_LISTENER_FILE=remoteListener.sh
    INSTALL_DB_BINARIES_FILE=installDBBinaries.sh
    GRID_HOME_CLEANUP=GridHomeCleanup.sh
    ORACLE_HOME_CLEANUP=OracleHomeCleanup.sh
    DB_USER=oracle
    GRID_USER=grid
    FUNCTIONS=functions.sh
    COMMON_SCRIPTS=/common_scripts
    CHECK_SPACE_FILE=checkSpace.sh
    RESET_FAILED_UNITS=resetFailedUnits.sh
    SET_CRONTAB=setCrontab.sh
    CRONTAB_ENTRY=crontabEntry
    EXPECT=/usr/bin/expect
    BIN=/usr/sbin
    container=true
    INSTALL_SCRIPTS=/opt/scripts/install
    SCRIPT_DIR=/opt/scripts/startup
    GRID_PATH=/u01/app/19.3.0/grid/bin:/u01/app/19.3.0/grid/OPatch/:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    DB_PATH=/u01/app/oracle/product/19.3.0/dbhome_1/bin:/u01/app/oracle/product/19.3.0/dbhome_1/OPatch/:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    GRID_LD_LIBRARY_PATH=/u01/app/19.3.0/grid/lib:/usr/lib:/lib
    DB_LD_LIBRARY_PATH=/u01/app/oracle/product/19.3.0/dbhome_1/lib:/usr/lib:/lib
    HOME=/home/grid
    Failed to parse kernel command line, ignoring: No such file or directory
    systemd 219 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN)
    Detected virtualization other.
    Detected architecture x86-64.
    
    Welcome to Oracle Linux Server 7.6!
    
    Set hostname to <racnode2>.
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    /usr/lib/systemd/system-generators/systemd-fstab-generator failed with error code 1.
    Failed to parse kernel command line, ignoring: No such file or directory
    Binding to IPv6 address not available since kernel does not support IPv6.
    Binding to IPv6 address not available since kernel does not support IPv6.
    Cannot add dependency job for unit display-manager.service, ignoring: Unit not found.
    [  OK  ] Created slice Root Slice.
    [  OK  ] Listening on Journal Socket.
    [  OK  ] Reached target Swap.
    [  OK  ] Created slice System Slice.
             Starting Journal Service...
             Starting Read and set NIS domainname from /etc/sysconfig/network...
    [  OK  ] Listening on Delayed Shutdown Socket.
    [  OK  ] Reached target Local Encrypted Volumes.
    [  OK  ] Reached target RPC Port Mapper.
    [  OK  ] Started Forward Password Requests to Wall Directory Watch.
    [  OK  ] Created slice system-getty.slice.
    [  OK  ] Created slice User and Session Slice.
    [  OK  ] Reached target Slices.
    [  OK  ] Started Dispatch Password Requests to Console Directory Watch.
    [  OK  ] Listening on /dev/initctl Compatibility Named Pipe.
             Starting Configure read-only root support...
             Starting Rebuild Hardware Database...
    [  OK  ] Reached target Local File Systems (Pre).
    Couldn't determine result for ConditionKernelCommandLine=|rd.modules-load for systemd-modules-load.service, assuming failed: No such file or directory
    Couldn't determine result for ConditionKernelCommandLine=|modules-load for systemd-modules-load.service, assuming failed: No such file or directory
    [  OK  ] Started Journal Service.
    [  OK  ] Started Read and set NIS domainname from /etc/sysconfig/network.
             Starting Flush Journal to Persistent Storage...
    [  OK  ] Started Configure read-only root support.
    [  OK  ] Reached target Local File Systems.
             Starting Rebuild Journal Catalog...
             Starting Preprocess NFS configuration...
             Starting Mark the need to relabel after reboot...
             Starting Load/Save Random Seed...
    [  OK  ] Started Mark the need to relabel after reboot.
    [  OK  ] Started Preprocess NFS configuration.
    [  OK  ] Started Flush Journal to Persistent Storage.
             Starting Create Volatile Files and Directories...
    [  OK  ] Started Create Volatile Files and Directories.
             Mounting RPC Pipe File System...
             Starting Update UTMP about System Boot/Shutdown...
    [FAILED] Failed to mount RPC Pipe File System.
    See 'systemctl status var-lib-nfs-rpc_pipefs.mount' for details.
    [DEPEND] Dependency failed for rpc_pipefs.target.
    [DEPEND] Dependency failed for RPC security service for NFS client and server.
    [  OK  ] Started Update UTMP about System Boot/Shutdown.
    [  OK  ] Started Load/Save Random Seed.
    [  OK  ] Started Rebuild Journal Catalog.
    [  OK  ] Started Rebuild Hardware Database.
             Starting Update is Completed...
    [  OK  ] Started Update is Completed.
    [  OK  ] Reached target System Initialization.
    [  OK  ] Listening on D-Bus System Message Bus Socket.
    [  OK  ] Started Flexible branding.
    [  OK  ] Reached target Paths.
    [  OK  ] Started Daily Cleanup of Temporary Directories.
    [  OK  ] Reached target Timers.
    [  OK  ] Listening on RPCbind Server Activation Socket.
    [  OK  ] Reached target Sockets.
             Starting RPC bind service...
    [  OK  ] Reached target Basic System.
             Starting OpenSSH Server Key Generation...
             Starting LSB: Bring up/down networking...
             Starting GSSAPI Proxy Daemon...
    [  OK  ] Started D-Bus System Message Bus.
             Starting Resets System Activity Logs...
             Starting Self Monitoring and Reporting Technology (SMART) Daemon...
             Starting Login Service...
    [  OK  ] Started RPC bind service.
             Starting Cleanup of Temporary Directories...
    [  OK  ] Started GSSAPI Proxy Daemon.
    [  OK  ] Reached target NFS client services.
    [  OK  ] Reached target Remote File Systems (Pre).
    [  OK  ] Reached target Remote File Systems.
             Starting Permit User Sessions...
    [  OK  ] Started Permit User Sessions.
    [  OK  ] Started Command Scheduler.
    [  OK  ] Started Resets System Activity Logs.
    [  OK  ] Started Login Service.
    [  OK  ] Started Cleanup of Temporary Directories.
    [  OK  ] Started OpenSSH Server Key Generation.
    [  OK  ] Started Self Monitoring and Reporting Technology (SMART) Daemon.
    [  OK  ] Started LSB: Bring up/down networking.
    [  OK  ] Reached target Network.
             Starting /etc/rc.d/rc.local Compatibility...
             Starting OpenSSH server daemon...
    [  OK  ] Reached target Network is Online.
             Starting Notify NFS peers of a restart...
    [  OK  ] Started /etc/rc.d/rc.local Compatibility.
    [  OK  ] Started Console Getty.
    [  OK  ] Reached target Login Prompts.
    [  OK  ] Started Notify NFS peers of a restart.
    [  OK  ] Started OpenSSH server daemon.
    [  OK  ] Reached target Multi-User System.
    [  OK  ] Reached target Graphical Interface.
             Starting Update UTMP about System Runlevel Changes...
    [  OK  ] Started Update UTMP about System Runlevel Changes.
    11-12-2019 12:21:28 UTC :  : Process id of the program :
    11-12-2019 12:21:28 UTC :  : #################################################
    11-12-2019 12:21:28 UTC :  :  Starting Grid Installation
    11-12-2019 12:21:28 UTC :  : #################################################
    11-12-2019 12:21:28 UTC :  : Pre-Grid Setup steps are in process
    11-12-2019 12:21:28 UTC :  : Process id of the program :
    11-12-2019 12:21:28 UTC :  : Disable failed service var-lib-nfs-rpc_pipefs.mount
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    11-12-2019 12:21:28 UTC :  : Resetting Failed Services
    11-12-2019 12:21:28 UTC :  : Sleeping for 60 seconds
    
    Oracle Linux Server 7.6
    Kernel 4.14.35-1902.6.6.el7uek.x86_64 on an x86_64
    
    racnode2 login: 11-12-2019 12:22:28 UTC :  : Systemctl state is running!
    11-12-2019 12:22:28 UTC :  : Setting correct permissions for /bin/ping
    11-12-2019 12:22:28 UTC :  : Public IP is set to 172.16.1.151
    11-12-2019 12:22:28 UTC :  : RAC Node PUBLIC Hostname is set to racnode2
    11-12-2019 12:22:28 UTC :  : Preparing host line for racnode2
    11-12-2019 12:22:28 UTC :  : Adding \n172.16.1.151\tracnode2.example.com\tracnode2 to /etc/hosts
    11-12-2019 12:22:28 UTC :  : Preparing host line for racnode2-priv
    11-12-2019 12:22:28 UTC :  : Adding \n192.168.17.151\tracnode2-priv.example.com\tracnode2-priv to /etc/hosts
    11-12-2019 12:22:28 UTC :  : Preparing host line for racnode2-vip
    11-12-2019 12:22:28 UTC :  : Adding \n172.16.1.161\tracnode2-vip.example.com\tracnode2-vip to /etc/hosts
    11-12-2019 12:22:28 UTC :  : racnode-scan already exists : 172.16.1.70  racnode-scan.example.com        racnode-scan, no  update required
    11-12-2019 12:22:28 UTC :  : Preapring Device list
    11-12-2019 12:22:28 UTC :  : Changing Disk permission and ownership /dev/asm_disk1
    11-12-2019 12:22:28 UTC :  : DNS_SERVERS is set to empty. /etc/resolv.conf will use default dns docker embedded server.
    11-12-2019 12:22:28 UTC :  : #####################################################################
    11-12-2019 12:22:28 UTC :  :  RAC setup will begin in 2 minutes
    11-12-2019 12:22:28 UTC :  : ####################################################################
    11-12-2019 12:22:30 UTC :  : ###################################################
    11-12-2019 12:22:30 UTC :  : Pre-Grid Setup steps completed
    11-12-2019 12:22:30 UTC :  : ###################################################
    11-12-2019 12:22:30 UTC :  : Checking if grid is already configured
    11-12-2019 12:22:31 UTC :  : Public IP is set to 172.16.1.151
    11-12-2019 12:22:31 UTC :  : RAC Node PUBLIC Hostname is set to racnode2
    11-12-2019 12:22:31 UTC :  : Domain is defined to example.com
    11-12-2019 12:22:31 UTC :  : Setting Existing Cluster Node for node addition operation. This will be retrieved from racnode1
    11-12-2019 12:22:31 UTC :  : Existing Node Name of the cluster is set to racnode1
    11-12-2019 12:22:31 UTC :  : 172.16.1.150
    11-12-2019 12:22:31 UTC :  : Existing Cluster node resolved to IP. Check passed
    11-12-2019 12:22:31 UTC :  : Default setting of AUTO GNS VIP set to false. If you want to use AUTO GNS VIP, please pass DHCP_CONF as an env parameter set to true
    11-12-2019 12:22:31 UTC :  : RAC VIP set to 172.16.1.161
    11-12-2019 12:22:31 UTC :  : RAC Node VIP hostname is set to racnode2-vip
    11-12-2019 12:22:31 UTC :  : SCAN_NAME name is racnode-scan
    11-12-2019 12:22:31 UTC :  : 172.16.1.70
    11-12-2019 12:22:31 UTC :  : SCAN Name resolving to IP. Check Passed!
    11-12-2019 12:22:31 UTC :  : SCAN_IP name is 172.16.1.70
    11-12-2019 12:22:31 UTC :  : RAC Node PRIV IP is set to 192.168.17.151
    11-12-2019 12:22:31 UTC :  : RAC Node private hostname is set to racnode2-priv
    11-12-2019 12:22:31 UTC :  : CMAN_NAME set to the empty string
    11-12-2019 12:22:31 UTC :  : CMAN_IP set to the empty string
    11-12-2019 12:22:31 UTC :  : Password file generated
    11-12-2019 12:22:31 UTC :  : Common OS Password string is set for Grid user
    11-12-2019 12:22:31 UTC :  : Common OS Password string is set for  Oracle user
    11-12-2019 12:22:31 UTC :  : GRID_RESPONSE_FILE env variable set to empty. AddNode.sh will use standard cluster responsefile
    11-12-2019 12:22:31 UTC :  : Location for User script SCRIPT_ROOT set to /common_scripts
    11-12-2019 12:22:31 UTC :  : ORACLE_SID is set to ORCLCDB
    11-12-2019 12:22:31 UTC :  : Setting random password for root/grid/oracle user
    11-12-2019 12:22:31 UTC :  : Setting random password for grid user
    11-12-2019 12:22:32 UTC :  : Setting random password for oracle user
    11-12-2019 12:22:32 UTC :  : Setting random password for root user
    11-12-2019 12:22:32 UTC :  : Cluster Nodes are racnode1 racnode2
    11-12-2019 12:22:32 UTC :  : Running SSH setup for grid user between nodes racnode1 racnode2
    11-12-2019 12:22:45 UTC :  : Running SSH setup for oracle user between nodes racnode1 racnode2
    11-12-2019 12:23:00 UTC :  : SSH check fine for the racnode1
    11-12-2019 12:23:00 UTC :  : SSH check fine for the racnode2
    11-12-2019 12:23:00 UTC :  : SSH check fine for the racnode2
    11-12-2019 12:23:00 UTC :  : SSH check fine for the oracle@racnode1
    11-12-2019 12:23:01 UTC :  : SSH check fine for the oracle@racnode2
    11-12-2019 12:23:01 UTC :  : SSH check fine for the oracle@racnode2
    11-12-2019 12:23:01 UTC :  : Setting Device permission to grid and asmadmin on all the cluster nodes
    11-12-2019 12:23:01 UTC :  : Nodes in the cluster racnode2
    11-12-2019 12:23:01 UTC :  : Setting Device permissions for RAC Install  on racnode2
    11-12-2019 12:23:01 UTC :  : Preapring ASM Device list
    11-12-2019 12:23:01 UTC :  : Changing Disk permission and ownership
    11-12-2019 12:23:01 UTC :  : Command : su - $GRID_USER -c "ssh $node sudo chown $GRID_USER:asmadmin $device" execute on racnode2
    11-12-2019 12:23:01 UTC :  : Command : su - $GRID_USER -c "ssh $node sudo chmod 660 $device" execute on racnode2
    11-12-2019 12:23:01 UTC :  : Populate Rac Env Vars on Remote Hosts
    11-12-2019 12:23:01 UTC :  : Command : su - $GRID_USER -c "ssh $node sudo echo \"export ASM_DEVICE_LIST=${ASM_DEVICE_LIST}\" >> /etc/rac_env_vars" execute on racnode2
    11-12-2019 12:23:02 UTC :  : Checking Cluster Status on racnode1
    11-12-2019 12:23:02 UTC :  : Checking Cluster
    11-12-2019 12:23:02 UTC :  : Cluster Check on remote node passed
    11-12-2019 12:23:02 UTC :  : Cluster Check went fine
    11-12-2019 12:23:03 UTC :  : CRSD Check went fine
    11-12-2019 12:23:03 UTC :  : CSSD Check went fine
    11-12-2019 12:23:04 UTC :  : EVMD Check went fine
    11-12-2019 12:23:04 UTC :  : Generating Responsefile for node addition
    11-12-2019 12:23:04 UTC :  : Clustered Nodes are set to racnode2:racnode2-vip:HUB
    11-12-2019 12:23:04 UTC :  : Running Cluster verification utility for new node racnode2 on racnode1
    11-12-2019 12:23:04 UTC :  : Nodes in the cluster racnode2
    11-12-2019 12:23:04 UTC :  : ssh to the node racnode1 and executing cvu checks on racnode2
    11-12-2019 12:24:33 UTC :  : Checking /tmp/cluvfy_check.txt if there is any failed check.
    
    Verifying Physical Memory ...PASSED
    Verifying Available Physical Memory ...PASSED
    Verifying Swap Size ...FAILED (PRVF-7573)
    Verifying Free Space: racnode2:/usr,racnode2:/var,racnode2:/etc,racnode2:/u01/app/19.3.0/grid,racnode2:/sbin,racnode2:/tmp ...PASSED
    Verifying Free Space: racnode1:/usr,racnode1:/var,racnode1:/etc,racnode1:/u01/app/19.3.0/grid,racnode1:/sbin,racnode1:/tmp ...PASSED
    Verifying User Existence: oracle ...
      Verifying Users With Same UID: 54321 ...PASSED
    Verifying User Existence: oracle ...PASSED
    Verifying User Existence: grid ...
      Verifying Users With Same UID: 54332 ...PASSED
    Verifying User Existence: grid ...PASSED
    Verifying User Existence: root ...
      Verifying Users With Same UID: 0 ...PASSED
    Verifying User Existence: root ...PASSED
    Verifying Group Existence: asmadmin ...PASSED
    Verifying Group Existence: asmoper ...PASSED
    Verifying Group Existence: asmdba ...PASSED
    Verifying Group Existence: oinstall ...PASSED
    Verifying Group Membership: oinstall ...PASSED
    Verifying Group Membership: asmdba ...PASSED
    Verifying Group Membership: asmadmin ...PASSED
    Verifying Group Membership: asmoper ...PASSED
    Verifying Run Level ...PASSED
    Verifying Hard Limit: maximum open file descriptors ...PASSED
    Verifying Soft Limit: maximum open file descriptors ...PASSED
    Verifying Hard Limit: maximum user processes ...PASSED
    Verifying Soft Limit: maximum user processes ...PASSED
    Verifying Soft Limit: maximum stack size ...PASSED
    Verifying Architecture ...PASSED
    Verifying OS Kernel Version ...PASSED
    Verifying OS Kernel Parameter: semmsl ...PASSED
    Verifying OS Kernel Parameter: semmns ...PASSED
    Verifying OS Kernel Parameter: semopm ...PASSED
    Verifying OS Kernel Parameter: semmni ...PASSED
    Verifying OS Kernel Parameter: shmmax ...PASSED
    Verifying OS Kernel Parameter: shmmni ...PASSED
    Verifying OS Kernel Parameter: shmall ...FAILED (PRVG-1201)
    Verifying OS Kernel Parameter: file-max ...PASSED
    Verifying OS Kernel Parameter: aio-max-nr ...FAILED (PRVG-1205)
    Verifying OS Kernel Parameter: panic_on_oops ...PASSED
    Verifying Package: kmod-20-21 (x86_64) ...PASSED
    Verifying Package: kmod-libs-20-21 (x86_64) ...PASSED
    Verifying Package: binutils-2.23.52.0.1 ...PASSED
    Verifying Package: compat-libcap1-1.10 ...PASSED
    Verifying Package: libgcc-4.8.2 (x86_64) ...PASSED
    Verifying Package: libstdc++-4.8.2 (x86_64) ...PASSED
    Verifying Package: libstdc++-devel-4.8.2 (x86_64) ...PASSED
    Verifying Package: sysstat-10.1.5 ...PASSED
    Verifying Package: ksh ...PASSED
    Verifying Package: make-3.82 ...PASSED
    Verifying Package: glibc-2.17 (x86_64) ...PASSED
    Verifying Package: glibc-devel-2.17 (x86_64) ...PASSED
    Verifying Package: libaio-0.3.109 (x86_64) ...PASSED
    Verifying Package: libaio-devel-0.3.109 (x86_64) ...PASSED
    Verifying Package: nfs-utils-1.2.3-15 ...PASSED
    Verifying Package: smartmontools-6.2-4 ...PASSED
    Verifying Package: net-tools-2.0-0.17 ...PASSED
    Verifying Users With Same UID: 0 ...PASSED
    Verifying Current Group ID ...PASSED
    Verifying Root user consistency ...PASSED
    Verifying Node Addition ...
      Verifying CRS Integrity ...PASSED
      Verifying Clusterware Version Consistency ...PASSED
      Verifying '/u01/app/19.3.0/grid' ...PASSED
    Verifying Node Addition ...PASSED
    Verifying Host name ...PASSED
    Verifying Node Connectivity ...
      Verifying Hosts File ...PASSED
      Verifying Check that maximum (MTU) size packet goes through subnet ...PASSED
      Verifying subnet mask consistency for subnet "172.16.1.0" ...PASSED
      Verifying subnet mask consistency for subnet "192.168.17.0" ...PASSED
    Verifying Node Connectivity ...PASSED
    Verifying Multicast or broadcast check ...PASSED
    Verifying ASM Integrity ...PASSED
    Verifying Device Checks for ASM ...
      Verifying Access Control List check ...PASSED
    Verifying Device Checks for ASM ...PASSED
    Verifying Database home availability ...PASSED
    Verifying OCR Integrity ...PASSED
    Verifying Time zone consistency ...PASSED
    Verifying Network Time Protocol (NTP) ...
      Verifying '/etc/ntp.conf' ...PASSED
      Verifying '/var/run/ntpd.pid' ...PASSED
      Verifying '/var/run/chronyd.pid' ...PASSED
    Verifying Network Time Protocol (NTP) ...FAILED (PRVG-1017)
    Verifying User Not In Group "root": grid ...PASSED
    Verifying Time offset between nodes ...PASSED
    Verifying resolv.conf Integrity ...FAILED (PRVG-10048)
    Verifying DNS/NIS name service ...PASSED
    Verifying User Equivalence ...PASSED
    Verifying /dev/shm mounted as temporary file system ...PASSED
    Verifying /boot mount ...PASSED
    Verifying zeroconf check ...PASSED
    
    Pre-check for node addition was unsuccessful on all the nodes.
    
    
    Failures were encountered during execution of CVU verification request "stage -pre nodeadd".
    
    Verifying Swap Size ...FAILED
    racnode2: PRVF-7573 : Sufficient swap size is not available on node "racnode2"
              [Required = 9.497GB (9958344.0KB) ; Found = 4GB (4194300.0KB)]
    
    racnode1: PRVF-7573 : Sufficient swap size is not available on node "racnode1"
              [Required = 9.497GB (9958344.0KB) ; Found = 4GB (4194300.0KB)]
    
    Verifying OS Kernel Parameter: shmall ...FAILED
    racnode2: PRVG-1201 : OS kernel parameter "shmall" does not have expected
              configured value on node "racnode2" [Expected = "2251799813685247" ;
              Current = "18446744073692774000"; Configured = "1073741824"].
    
    racnode1: PRVG-1201 : OS kernel parameter "shmall" does not have expected
              configured value on node "racnode1" [Expected = "2251799813685247" ;
              Current = "18446744073692774000"; Configured = "1073741824"].
    
    Verifying OS Kernel Parameter: aio-max-nr ...FAILED
    racnode2: PRVG-1205 : OS kernel parameter "aio-max-nr" does not have expected
              current value on node "racnode2" [Expected = "1048576" ; Current =
              "65536"; Configured = "1048576"].
    
    racnode1: PRVG-1205 : OS kernel parameter "aio-max-nr" does not have expected
              current value on node "racnode1" [Expected = "1048576" ; Current =
              "65536"; Configured = "1048576"].
    
    Verifying Network Time Protocol (NTP) ...FAILED
    racnode2: PRVG-1017 : NTP configuration file "/etc/ntp.conf" is present on
              nodes "racnode2,racnode1" on which NTP daemon or service was not
              running
    
    racnode1: PRVG-1017 : NTP configuration file "/etc/ntp.conf" is present on
              nodes "racnode2,racnode1" on which NTP daemon or service was not
              running
    
    Verifying resolv.conf Integrity ...FAILED
    racnode2: PRVG-10048 : Name "racnode2" was not resolved to an address of the
              specified type by name servers "127.0.0.11".
    
    racnode1: PRVG-10048 : Name "racnode1" was not resolved to an address of the
              specified type by name servers "127.0.0.11".
    
    
    CVU operation performed:      stage -pre nodeadd
    Date:                         Nov 12, 2019 12:23:09 PM
    CVU home:                     /u01/app/19.3.0/grid/
    User:                         grid
    11-12-2019 12:24:33 UTC :  : CVU Checks are ignored as IGNORE_CVU_CHECKS set to true. It is recommended to set IGNORE_CVU_CHECKS to false and meet all the cvu checks requirement. RAC installation might fail, if there are failed cvu checks.
    11-12-2019 12:24:33 UTC :  : Running Node Addition and cluvfy test for node racnode2
    11-12-2019 12:24:33 UTC :  : Copying /tmp/grid_addnode.rsp on remote node racnode1
    11-12-2019 12:24:33 UTC :  : Running GridSetup.sh on racnode1 to add the node to existing cluster
    11-12-2019 12:26:08 UTC :  : Node Addition performed. removing Responsefile
    11-12-2019 12:26:08 UTC :  : Running root.sh on node racnode2
    11-12-2019 12:26:08 UTC :  : Nodes in the cluster racnode2
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    Failed to parse kernel command line, ignoring: No such file or directory
    11-12-2019 12:36:37 UTC :  : Checking Cluster
    11-12-2019 12:36:38 UTC :  : Cluster Check passed
    11-12-2019 12:36:38 UTC :  : Cluster Check went fine
    11-12-2019 12:36:39 UTC :  : CRSD Check went fine
    11-12-2019 12:36:39 UTC :  : CSSD Check went fine
    11-12-2019 12:36:39 UTC :  : EVMD Check went fine
    11-12-2019 12:36:39 UTC :  : Removing /tmp/cluvfy_check.txt as cluster check has passed
    11-12-2019 12:36:39 UTC :  : Checking Cluster Class
    11-12-2019 12:36:39 UTC :  : Checking Cluster Class
    11-12-2019 12:36:40 UTC :  : Cluster class is CRS-41008: Cluster class is 'Standalone Cluster'
    11-12-2019 12:36:40 UTC :  : Running User Script for grid user
    11-12-2019 12:36:40 UTC :  : Performing DB Node addition
    11-12-2019 12:38:05 UTC :  : Node Addition went fine for racnode2
    11-12-2019 12:38:05 UTC :  : Running root.sh
    11-12-2019 12:38:05 UTC :  : Nodes in the cluster racnode2
    11-12-2019 12:38:09 UTC :  : Adding DB Instance
    11-12-2019 12:38:09 UTC :  : Adding DB Instance on racnode1
    11-12-2019 12:45:49 UTC :  : Checking DB status
    11-12-2019 12:46:51 UTC : : ORCLCDB is not up and running on racnode2
    11-12-2019 12:46:52 UTC : : Error has occurred in Grid Setup, Please verify!
    
    

    从日志中可知,数据库启动没有成功,也许是资源的原因。
    重启racnode2容器,然后就正常了:

    docker stop racnode2
    docker start racnode2
    

    查看日志,以下为最后部分日志:

    11-12-2019 13:06:54 UTC :  : Setting correct permissions for /bin/ping
    11-12-2019 13:06:54 UTC :  : Public IP is set to 172.16.1.151
    11-12-2019 13:06:54 UTC :  : RAC Node PUBLIC Hostname is set to racnode2
    11-12-2019 13:06:54 UTC :  : racnode2 already exists : 172.16.1.151     racnode2.example.com    racnode2
    192.168.17.151  racnode2-priv.example.com       racnode2-priv
    172.16.1.161    racnode2-vip.example.com        racnode2-vip, no  update required
    11-12-2019 13:06:54 UTC :  : racnode2-priv already exists : 192.168.17.151      racnode2-priv.example.com       racnode2-priv, no  update required
    11-12-2019 13:06:54 UTC :  : racnode2-vip already exists : 172.16.1.161 racnode2-vip.example.com        racnode2-vip, no  update required
    11-12-2019 13:06:54 UTC :  : racnode-scan already exists : 172.16.1.70  racnode-scan.example.com        racnode-scan, no  update required
    11-12-2019 13:06:54 UTC :  : Preapring Device list
    11-12-2019 13:06:54 UTC :  : Changing Disk permission and ownership /dev/asm_disk1
    11-12-2019 13:06:54 UTC :  : DNS_SERVERS is set to empty. /etc/resolv.conf will use default dns docker embedded server.
    11-12-2019 13:06:54 UTC :  : #####################################################################
    11-12-2019 13:06:54 UTC :  :  RAC setup will begin in 2 minutes
    11-12-2019 13:06:54 UTC :  : ####################################################################
    11-12-2019 13:06:56 UTC :  : ###################################################
    11-12-2019 13:06:56 UTC :  : Pre-Grid Setup steps completed
    11-12-2019 13:06:56 UTC :  : ###################################################
    11-12-2019 13:06:56 UTC :  : Checking if grid is already configured
    11-12-2019 13:06:56 UTC :  : Grid is installed on racnode2. runOracle.sh will start the Grid service
    11-12-2019 13:06:56 UTC :  : Setting up Grid Env for Grid Start
    11-12-2019 13:06:56 UTC :  : ##########################################################################################
    11-12-2019 13:06:56 UTC :  : Grid is already installed on this container! Grid will be started by default ohasd scripts
    11-12-2019 13:06:56 UTC :  : ############################################################################################
    
    

    检查,可以看到所有资源正常:

    [grid@racnode2 ~]$ crsctl status resource
    NAME=ora.ASMNET1LSNR_ASM.lsnr(ora.asmgroup)
    TYPE=ora.asm_listener.type
    TARGET=ONLINE            , ONLINE            , ONLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2, OFFLINE
    
    NAME=ora.DATA.dg(ora.asmgroup)
    TYPE=ora.diskgroup.type
    TARGET=ONLINE            , ONLINE            , OFFLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2, OFFLINE
    
    NAME=ora.LISTENER.lsnr
    TYPE=ora.listener.type
    TARGET=ONLINE            , ONLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2
    
    NAME=ora.LISTENER_SCAN1.lsnr
    TYPE=ora.scan_listener.type
    TARGET=ONLINE
    STATE=ONLINE on racnode1
    
    NAME=ora.asm(ora.asmgroup)
    TYPE=ora.asm.type
    TARGET=ONLINE            , ONLINE            , OFFLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2, OFFLINE
    
    NAME=ora.asmnet1.asmnetwork(ora.asmgroup)
    TYPE=ora.asm_network.type
    TARGET=ONLINE            , ONLINE            , OFFLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2, OFFLINE
    
    NAME=ora.chad
    TYPE=ora.chad.type
    TARGET=ONLINE            , ONLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2
    
    NAME=ora.cvu
    TYPE=ora.cvu.type
    TARGET=ONLINE
    STATE=ONLINE on racnode1
    
    NAME=ora.net1.network
    TYPE=ora.network.type
    TARGET=ONLINE            , ONLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2
    
    NAME=ora.ons
    TYPE=ora.ons.type
    TARGET=ONLINE            , ONLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2
    
    NAME=ora.orclcdb.db
    TYPE=ora.database.type
    TARGET=ONLINE            , ONLINE
    STATE=ONLINE on racnode1, ONLINE on racnode2
    
    NAME=ora.qosmserver
    TYPE=ora.qosmserver.type
    TARGET=ONLINE
    STATE=ONLINE on racnode1
    
    NAME=ora.racnode1.vip
    TYPE=ora.cluster_vip_net1.type
    TARGET=ONLINE
    STATE=ONLINE on racnode1
    
    NAME=ora.racnode2.vip
    TYPE=ora.cluster_vip_net1.type
    TARGET=ONLINE
    STATE=ONLINE on racnode2
    
    NAME=ora.scan1.vip
    TYPE=ora.scan_vip.type
    TARGET=ONLINE
    STATE=ONLINE on racnode1
    
    

    此时的空间状态:

    [vagrant@ol7-vagrant-rac ~]$ df -h
    Filesystem                   Size  Used Avail Use% Mounted on
    devtmpfs                     4.8G     0  4.8G   0% /dev
    tmpfs                        4.8G     0  4.8G   0% /dev/shm
    tmpfs                        4.8G  8.6M  4.8G   1% /run
    tmpfs                        4.8G     0  4.8G   0% /sys/fs/cgroup
    /dev/mapper/vg_main-lv_root   76G   36G   41G  47% /
    /dev/sda1                    497M  125M  373M  26% /boot
    vagrant                      1.9T  1.2T  687G  64% /vagrant
    tmpfs                        973M     0  973M   0% /run/user/1000
    
    

    此时的内存情况:

    [vagrant@ol7-vagrant-rac ~]$ free
                  total        used        free      shared  buff/cache   available
    Mem:        9958344     4261636      428572     4124668     5268136     1040228
    Swap:       4194300     2250268     1944032
    
    

    参考

    1. https://github.com/oracle/docker-images/blob/master/OracleDatabase/RAC/OracleRealApplicationClusters/README.md
    2. https://stackoverflow.com/questions/49822594/vagrant-how-to-specify-the-disk-size
    3. https://asanga-pradeep.blogspot.com/2018/07/rac-on-docker-single-host-setup.html
    4. https://marcbrandner.com/blog/increasing-disk-space-of-a-linux-based-vagrant-box-on-provisioning/
    5. https://stackoverflow.com/questions/27380641/see-full-command-of-running-stopped-container-in-docker
    更多相关内容
  • 《SpringBoot+Spring Cloud+Vue+Element项目实战》权限管理系统后端部分

    文章目录


    前后端源码以及markdown笔记及转化的pdf文件均已上传网盘:
    链接:https://pan.baidu.com/s/1BVXTBAJfAYGS01n8fP2GvA 提取码:ziyi

    关键技术

    1. Spring Boot

      官方教程:https://spring.io/projects/spring-boot/

    2. Spring Cloud

      官方教程:https://spring.io/projects/spring-cloud

      博客教程:https://www.cnblogs.com/xifengxiaoma/

    3. Spring Security

      官方教程:https://spring.io/projects/spring-security

      博客教程:https://www.cnblogs.com/xifengxiaoma/

    4. MaBatis

      官方教程:http://www.mybatis.org/mybatis-3/zh/

      博客教程:https://www.w3cschool.cn/mybatis/

    5. Vue.js

      官方教程:https://cn.vuejs.org/v2/guide/

      博客教程:https://www.runoob.com/vue2/vue-tutorial.html

    6. Element

      官网教程:http://element.eleme.io/#/zh-CN

    第一篇 后端实现篇

    1. 搭建开发环境

    1. 登录spring initializer生成spring boot项目模板,保存到本地,网站地址为https://start.spring.io/

    2. 将maven项目导入到eclipse,修改application配置文件为yml后缀,清理掉不需要的mvnw、mvnw.cmd和test目录下的测试文件。

    3. 编译打包运行,右击pom.xml,选择run as→maven install。

    4. 启动应用,右击DemoApplication,选择run as→Java application。

    5. 修改启动端口。(默认为8080),在application.yml可修改启动端口:(下例修改为8001,注意port前不能使用tab,否则会报错)

      server:
        port: 8001
      
    6. 自定义Banner

      spring boot启动后会在控制台输出banner信息,默认是显示spring字样,如下:

    在这里插入图片描述

    如果要定制自己的Banner只需要在resources下放置一个banner.txt文件,输入自己的banner字符即可。

    Banner字符可通过类似以下网站生成:

    在这里插入图片描述

    复制字符到banner.txt并附上应用和版本信息,重启应用,如下:

    在这里插入图片描述

    1. 接口测试

      新建一个controller包,并在其下创建一个hellocontroller类,添加一个hello接口:

    在这里插入图片描述

    在浏览器中访问,如下:

    在这里插入图片描述

    2. 集成Swagger文档

    使用Swagger集成文档有以下几个优势:

    • 功能丰富:支持多种注解,自动生成接口文档界面,支持在界面中测试API接口功能。
    • 及时更新:开发过程中花一点写注释的时间,就可以及时更新API文档、
    • 整合简单:通过添加pom依赖和简单配置,内嵌于应用中就可以同时发布API接口文档界面,不需要部署独立服务。

    官网:https://swagger.io/

    官方文档:https://swagger.io/docs/

    1. 添加依赖

      <!--swagger-->
      		<dependency>
      			<groupId>io.springfox</groupId>
      			<artifactId>springfox-swagger2</artifactId>
      			<version>2.9.2</version>
      		</dependency>
      		<dependency>
      			<groupId>io.springfox</groupId>
      			<artifactId>springfox-swagger-ui</artifactId>
      			<version>2.9.2</version>
      		</dependency>
      
    2. 配置类

      添加config包,并在其下添加Swagger配置类SwaggerConfig.java:

      @Configuration
      @EnableSwagger2
      public class SwaggerConfig {
      	
      	@Bean
      	public Docket createRestApi(){
              return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
              		.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
          }
      	
      	private ApiInfo apiInfo(){
              return new ApiInfoBuilder().build();
          }
      }
      
    3. 页面测试

      启动应用,在浏览器中访问http://localhost:8001/swagger-ui.html#/,就可以看到Swagger的接口文档页面了:

    在这里插入图片描述

    还可以选择接口进行测试,单击右侧的try it out→execute,发现接口成功返回“hello mango!”:

    在这里插入图片描述

    3. 集成MyBatis框架

    MyBatis是一款优秀的持久层框架,支持定制化SQL、存储过程以及高级映射。MyBatis可以使用简单的XML或注解来配置和映射原生信息,并将接口和Java的POJOs映射成数据库中的记录。

    中文官网:http://www.mybatis.cn/

    参考教程:https://www.w3cschool.cn/mybatis/

    1. 添加依赖

      <!-- mybatis -->
      		<dependency>
      		    <groupId>org.mybatis.spring.boot</groupId>
      		    <artifactId>mybatis-spring-boot-starter</artifactId>
      		    <version>1.3.2</version>
      		</dependency>
      		<!-- mysql -->
      		<dependency>
      		    <groupId>mysql</groupId>
      		    <artifactId>mysql-connector-java</artifactId>
      		</dependency>
      
    2. 添加配置

      1. 添加MyBatis配置

        添加MyBatis配置类,配置相关扫描路径,包括DAO、Model、XML映射文件的扫描,在config包下新建一个MyBatis配置类MybatisConfig.java:

        @Configuration
        @MapperScan("com.louis.mango.**.dao")    // 扫描DAO
        public class MybatisConfig {
          @Autowired
          private DataSource dataSource;
        
          @Bean
          public SqlSessionFactory sqlSessionFactory() throws Exception {
            SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(dataSource);
            sessionFactory.setTypeAliasesPackage("com.louis.mango.**.model");    // 扫描Model
            
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            sessionFactory.setMapperLocations(resolver.getResources("classpath*:**/sqlmap/*.xml"));    // 扫描映射文件
            
            return sessionFactory.getObject();
          }
        }
        
      2. 添加数据源配置

        打开应用配置文件,添加MySQL数据源连接信息。

        application.yml:

        spring:
          datasource:
            driverClassName: com.mysql.jdbc.Driver
            url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8
            username: root
            password: admin123
        
      3. 修改启动类

        给启动类的@SpringBootApplication注解配置包扫描,表示在应用启动时自动扫描com.louis.mango包下的内容,当然Spring Boot默认会扫描启动类包及子包的组件,所以如果启动类就是放在com.louis.mango下,那么默认配置其实就是com.louis.mango了。

        @SpringBootApplication(scanBasePackages={"com.louis.mango"})
        public class MangoApplication {
        	public static void main(String[] args) {
        		SpringApplication.run(MangoApplication.class, args);
        	}
        }
        
    3. 生成MyBatis模块

      手动编写MyBatis的Model、DAO、XML映射文件比较繁琐,通常都会通过一些生成工具来生成。MyBatis官方也提供了生成工具(MyBatis Generator),另外还有一些基于官方基础改进的第三方工具,比如MyBatis Plus就是国内提供的一款非常优秀的开源工具。

      生成好代码之后,分别将Domain、DAO、XML映射文件复制到相应的包里。

      打开生成的Mapper,我们可以看到默认生成的一些增删改查方法。

    4. 编写服务接口

      在Mapper中新增一个findAll方法,用于查询所有的用户信息:

      SysUserMapper.java:

      public interface SysUserMapper {
          int deleteByPrimaryKey(Long id);
      
          int insert(SysUser record);
      
          int insertSelective(SysUser record);
      
          SysUser selectByPrimaryKey(Long id);
      
          int updateByPrimaryKeySelective(SysUser record);
      
          int updateByPrimaryKey(SysUser record);
          
          /**
           * 查询全部
           * @return
           */
          List<SysUser> findAll();
      }
      

      在映射文件中添加一个查询方法,编写findAll的查询语句:

      SysUserMapper.xml:

      <select id="findAll" resultMap="BaseResultMap">
          select 
          <include refid="Base_Column_List" />
          from sys_user
        </select>
      

      然后编写用户管理接口,包含一个findAll方法:

      SysUserService.java

      public interface SysUserService {
      
          /**
           * 查找所有用户
           * @return
           */
          List<SysUser> findAll();
      
      }
      

      接着编写用户管理实现类,调用SysUserMapper方法完成查询操作:

      SysUserServiceImpl.java:

      @Service
      public class SysUserServiceImpl implements SysUserService {
          
          @Autowired
          private SysUserMapper sysUserMapper;
      
          @Override
          public List<SysUser> findAll() {
              return sysUserMapper.findAll();
          }
      }
      

      然后编写用户管理RESTful接口,返回JSON数据格式,提供外部调用。被@RestController注解的接口控制器默认使用JSON格式交互,返回JSON结果:

      SysUserController.java:

      @RestController
      @RequestMapping("user")
      public class SysUserController {
      
          @Autowired
          private SysUserService sysUserService;
          
          @GetMapping(value="/findAll")
          public Object findAll() {
              return sysUserService.findAll();
          }
      }
      
    5. 配置打包资源

      虽然代码编写已经完成,但是此时启动运行还是会有问题的,因为在编译打包的时候,我们的XML映射文件是不在默认打包范围内的,所以需要修改打包资源配置。

      修改pom.xml,在build标签加入形式如下的resource标签的打包配置,这样打包时就会把MyBatis映射文件也复制过去了:

      <build>
              <plugins>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                  </plugin>
              </plugins>
              <!-- 打包时拷贝MyBatis的映射文件 -->
              <resources>
                  <resource>
                      <directory>src/main/java</directory>
                      <includes>
                          <include>**/sqlmap/*.xml</include>
                      </includes>
                      <filtering>false</filtering>
                  </resource>
                  <resource>  
                      <directory>src/main/resources</directory>  
                          <includes> 
                              <include>**/*.*</include>  
                          </includes> 
                          <filtering>true</filtering>  
                  </resource> 
              </resources>
          </build>
      
    6. 编译运行测试

      编译并启动应用,访问http://localhost:8001/user/findAll,可以看到查询接口成功返回了所有的用户信息,如下:

    在这里插入图片描述

    也可以用Swagger进行测试:

    在这里插入图片描述

    4. 集成Druid数据源

    数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新再建立一个,释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。通过数据库连接池能明显提高对数据库操作的性能。

    1. Druid介绍

      Druid是阿里开源的一个JDBC应用组件,其主要包括三个部分:

      • DruidDriver:代理Driver,能够提供基于Filter-Chain模式的插件体系。
      • DruidDataSource:高效可管理的数据库连接池。
      • SQLParser:实用的SQL语法分析。

      通过Druid连接池中间件,我们可以实现:

      • 监控数据库访问性能。Druid内置了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,对于线上分析数据库访问性能有所帮助。
      • 替换传统的DBCP和C3P0连接池中间件。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
      • 数据库密码加密。直接把数据库密码写在配置文件中,容易导致安全问题。DruidDriver和DruidDataSource都支持PasswordCallback。
      • SQL执行日志。Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,可以按需选取相应的LogFilter,监控你应用的数据库访问情况。
      • 扩展JDBC。如果对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制很方便地编写JDBC层的扩展插件。

      更多详细信息可参考官方文档,https://github.com/alibaba/druid/wiki

    2. 添加依赖

      在pom文件中添加Druid相关的maven依赖:

      <!-- druid -->
      		<dependency>
      		   <groupId>com.alibaba</groupId>
      		   <artifactId>druid-spring-boot-starter</artifactId>
      		   <version>1.1.10</version>
      		</dependency>
      

      druid-spring-boot-starter是阿里官方提供的Spring Boot插件,用于帮助在Spring Boot项目中轻松集成Druid数据库连接池和监控。

      更多资料可以参考:

    3. 添加配置

      修改配置文件,把原有的数据源配置替换成Druid数据源并配置数据源相关参数。

      application.yml:

      spring:
        datasource:
          name: druidDataSource
          type: com.alibaba.druid.pool.DruidDataSource
          druid:
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8
            username: root
            password: admin123
            filters: stat,wall,log4j,config
            max-active: 100
            initial-size: 1
            max-wait: 60000
            min-idle: 1
            time-between-eviction-runs-millis: 60000
            min-evictable-idle-time-millis: 300000
            validation-query: select 'x'
            test-while-idle: true
            test-on-borrow: false
            test-on-return: false
            pool-prepared-statements: true
            max-open-prepared-statements: 50
            max-pool-prepared-statement-per-connection-size: 20
      

      参数说明:

      • max-active:最大连接数。
      • initial-size:初始化大小。
      • min-idle:最小连接数。
      • max-wait:获取连接等待超时时间。
      • time-between-eviction-runs-millis:间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒。
      • min-evictable-idle-time-millis:一个连接在池中最小生存的时间,单位是毫秒。
      • filters: stat,wall,log4j,config:配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,wall用于防火墙。

      Druid Spring Starter简化了很多配置,如果默认配置不能满足需求,可以自定义配置,更多配置参考如下:

      Druid Spring Starter:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

    4. 配置Servlet和Filter

      在config包下新建一个DruidConfig配置类,主要是注入属性和配置连接池相关的配置,如黑白名单、监控管理后台登录账户密码等,内容如下:

      DruidConfig.java:

      @Configuration
      @EnableConfigurationProperties({DruidDataSourceProperties.class})
      public class DruidConfig {
          @Autowired
          private DruidDataSourceProperties properties;
      
          @Bean
          @ConditionalOnMissingBean
          public DataSource druidDataSource() {
              DruidDataSource druidDataSource = new DruidDataSource();
              druidDataSource.setDriverClassName(properties.getDriverClassName());
              druidDataSource.setUrl(properties.getUrl());
              druidDataSource.setUsername(properties.getUsername());
              druidDataSource.setPassword(properties.getPassword());
              druidDataSource.setInitialSize(properties.getInitialSize());
              druidDataSource.setMinIdle(properties.getMinIdle());
              druidDataSource.setMaxActive(properties.getMaxActive());
              druidDataSource.setMaxWait(properties.getMaxWait());
              druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
              druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
              druidDataSource.setValidationQuery(properties.getValidationQuery());
              druidDataSource.setTestWhileIdle(properties.isTestWhileIdle());
              druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
              druidDataSource.setTestOnReturn(properties.isTestOnReturn());
              druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
              druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(properties.getMaxPoolPreparedStatementPerConnectionSize());
      
              try {
                  druidDataSource.setFilters(properties.getFilters());
                  druidDataSource.init();
              } catch (SQLException e) {
                  e.printStackTrace();
              }
      
              return druidDataSource;
          }
      
          /**
           * 注册Servlet信息, 配置监控视图
           *
           * @return
           */
          @Bean
          @ConditionalOnMissingBean
          public ServletRegistrationBean<Servlet> druidServlet() {
              ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<Servlet>(new StatViewServlet(), "/druid/*");
      
              //白名单:
      //        servletRegistrationBean.addInitParameter("allow","127.0.0.1,139.196.87.48");
              //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page.
              servletRegistrationBean.addInitParameter("deny","192.168.1.119");
              //登录查看信息的账号密码, 用于登录Druid监控后台
              servletRegistrationBean.addInitParameter("loginUsername", "admin");
              servletRegistrationBean.addInitParameter("loginPassword", "admin");
              //是否能够重置数据.
              servletRegistrationBean.addInitParameter("resetEnable", "true");
              return servletRegistrationBean;
      
          }
      
          /**
           * 注册Filter信息, 监控拦截器
           *
           * @return
           */
          @Bean
          @ConditionalOnMissingBean
          public FilterRegistrationBean<Filter> filterRegistrationBean() {
              FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
              filterRegistrationBean.setFilter(new WebStatFilter());
              filterRegistrationBean.addUrlPatterns("/*");
              filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
              return filterRegistrationBean;
          }
      }
      

      代码说明:

      • @EnableConfigurationProperties注解用于导入上一步Druid的配置信息。
      • public ServletRegistrationBean druidServlet()相当于Web Servlet配置。
      • public FilterRegistrationBean filterRegistrationBean()相当于Web Filter配置。
    5. 编译运行

      添加log4j依赖:

      <!-- log4j -->
      		<dependency>
      		    <groupId>log4j</groupId>
      		    <artifactId>log4j</artifactId>
      		    <version>1.2.17</version>
      		</dependency>
      

      添加log4j配置:在resources目录下,新建一个log4j参数配置文件:

      log4j.properties:

      ### set log levels ###    
      log4j.rootLogger = INFO,DEBUG, console, infoFile, errorFile ,debugfile,mail 
      LocationInfo=true    
      
      log4j.appender.console = org.apache.log4j.ConsoleAppender  
      log4j.appender.console.Target = System.out  
      log4j.appender.console.layout = org.apache.log4j.PatternLayout 
      
      log4j.appender.console.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m   %x %n 
      
      log4j.appender.infoFile = org.apache.log4j.DailyRollingFileAppender  
      log4j.appender.infoFile.Threshold = INFO  
      log4j.appender.infoFile.File = C:/logs/log
      log4j.appender.infoFile.DatePattern = '.'yyyy-MM-dd'.log'  
      log4j.appender.infoFile.Append=true
      log4j.appender.infoFile.layout = org.apache.log4j.PatternLayout  
      log4j.appender.infoFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m  %x %n 
      
      log4j.appender.errorFile = org.apache.log4j.DailyRollingFileAppender  
      log4j.appender.errorFile.Threshold = ERROR  
      log4j.appender.errorFile.File = C:/logs/error  
      log4j.appender.errorFile.DatePattern = '.'yyyy-MM-dd'.log'  
      log4j.appender.errorFile.Append=true  
      log4j.appender.errorFile.layout = org.apache.log4j.PatternLayout  
      log4j.appender.errorFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m  %x %n
      
      log4j.appender.debugfile = org.apache.log4j.DailyRollingFileAppender  
      log4j.appender.debugfile.Threshold = DEBUG  
      log4j.appender.debugfile.File = C:/logs/debug  
      log4j.appender.debugfile.DatePattern = '.'yyyy-MM-dd'.log'  
      log4j.appender.debugfile.Append=true  
      log4j.appender.debugfile.layout = org.apache.log4j.PatternLayout  
      log4j.appender.debugfile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m  %x %n
      

      配置完成后,编译启动。

    6. 查看监控

      启动应用,访问http://localhost:8001/druid/login.html,进入Druid监控后台页面:

    在这里插入图片描述

    登录之后,Druid后台的管理首页如下所示:

    在这里插入图片描述

    数据源页显示连接数据源的相关信息:

    在这里插入图片描述

    访问http://localhost:8001/user/findAll。接口调用成功之后可以看到SQL监控的执行记录,可以查看和分析执行的SQL性能,方便进行数据库性能优化:

    在这里插入图片描述

    5. 跨域解决方案

    如果一个请求地址里面的协议、域名和端口号都相同,就属于同源。

    依据浏览器同源策略,非同源脚本不可操作其他源下面的对象,想要操作其他源下的对象就需要跨域。在同源策略的限制下,非同源的网站之间不能发生AJAX请求。如果需要,可通过降域或其他技术实现。

    1. CORS技术

      CORS可以在不破坏既有规则的基础下,通过后端服务器实现CORS接口,从而实现跨域通信。CORS将请求分为两类:简单请求和非简单请求,分别对跨域通信提供了支持。

      • 简单请求

        在CORS出现前,发生HTTP请求时在头信息中不能包含任何自定义字段,且HTTP信息不超过以下几个字段:

        • Accept
        • Accept-Language
        • Content-Language
        • Last-Event-ID
        • Content-Type。

        一个简单请求的例子:

        GET /test HTTP/1.1
        Accept: */*
        Accept-Encoding: gzip, deflate, sdch, br
        Origin: http://www.test.com
        Host: www.test.com
        

        对于简单请求,CORS的策略是请求时在请求头中增加一个Origin字段,服务器收到请求后根据该字段判断是否允许该请求访问。如果允许,就在HTTP头信息中增加Access-Control-Allow-Origin字段,并返回正确的结果。如果不允许,就不在HTTP头信息中添加Access-Control-Allow-Origin字段。

        除了上面提到的Access-Control-Allow-Origin,还有几个字段用于描述CORS返回结果。Access-Control-Allow-Credentials:可选,用户是否可以发送、处理cookie。Access-Control-Expose-Headers:可选,可以让用户拿到的字段。有几个字段无论设置与否都可以拿到的,包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。

      • 非简单请求

        对于非简单请求的跨源请求,浏览器会在真实请求发出前增加一次OPTION请求,称为预检请求(preflight request)。预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到HTTP头信息字段中,询问服务器是否允许这样的操作。

        例如一个GET请求:

        OPTIONS /test HTTP/1.1
        Origin: http://www.test.com
        Access-Control-Request-Method: GET
        Access-Control-Request-Headers: X-Custom-Header
        Host: www.test.com
        

        与CORS相关的字段有:请求使用的HTTP方法Access-Control-Request-Method;请求中包含的自定义头字段Access-Control-Request-Headers。

        服务器收到请求时,需要分别对Origin、Access-Control-Request-Method、Access-Control-Request-Headers进行验证,验证通过后会在返回HTTP头信息中添加:

        Access-Control-Allow-Origin: http://www.test.com
        Access-Control-Allow-Methods: GET, POST, PUT, DELETE
        Access-Control-Allow-Headers: X-Custom-Header
        Access-Control-Allow-Credentials: true
        Access-Control-Max-Age: 1728000
        

        它们的含义分别是:Access-Control-Allow-Methods(真实请求允许的方法)、Access-Control-Allow-Headers(服务器允许使用的字段)、Access-Control-Allow-Credentials(是否允许用户发送、处理cookie)、Access-Control-Max-Age(预检请求的有效期,单位为秒。有效期内,不会重复发送预检请求)。

        当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。

    2. CORS实现

      CORS的代码实现比较简单,主要是要理解CORS实现跨域的原理和方式。在config包下新建一个CORS配置类,实现WebMvcConfigurer接口。

      CorsConfig.java:

      @Configuration
      public class CorsConfig implements WebMvcConfigurer {
      
          @Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/**")	// 允许跨域访问的路径
              .allowedOrigins("*")	// 允许跨域访问的源
              .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")	// 允许请求方法
              .maxAge(168000)	// 预检间隔时间
              .allowedHeaders("*")  // 允许头部设置
              .allowCredentials(true);	// 是否发送cookie
          }
      }
      

      这样每当客户端发送请求的时候,都会在头部附上跨域信息,就可以支持跨域访问了。

    6. 业务功能实现

    这一章节开始实现各个业务功能接口,包括用户管理、机构管理、角色管理、菜单管理、字典管理、系统配置、操作日志、登录日志等业务功能的实现。

    6.1 工程结构规划

    我们要采用的是微服务架构,虽然我们现在只有一个工程,但随着项目越来越大,代码的可重用性和可维护性就会变得越来越难,所以尽早对工程结构进行合理的规划,对项目于的前期开发、后期的扩展和维护都是非常重要的。

    经过重新规划,我们的工程结构如下:

    • mango-common:公共代码模块,主要放置一些工具类。
    • mango-core:核心业务代码模块,主要封装公共业务模块。
    • mango-admin:后台管理模块,包含用户、角色、菜单管理等。
    • mango-pom:聚合模块,仅为简化打包,一键执行打包所有模块。

    6.1.1 mango-admin

    将原先mango工程更名为mango-admin,遵循以下步骤进行工程重构:

    1. 关闭应用,在Eclipse上选择删除mango工程,注意不要勾选删除磁盘。
    2. 找到工程所在位置,修改mango工程名为mango-admin。
    3. 编辑pom.xml,将需要替换的mango字符替换为mango-admin。
    4. 右击Eclipse导航栏,选择import→exist maven project重新导入maven工程。
    5. 重构包结构,将基础包重构为com.louis.mango.admin,以区分不同工程的包。
    6. 由于重构了包路径,但是XML映射文件的内容无法同步更新,要将所有的MyBatis的XML映射文件出现的Mapper和Model的包路径修改为正确的路径。可以通过将"com.louis.mango"全部替换为"com.louis.mango.admin"进行统一修改。
    7. 将MangoApplication改名为MangoAdminApplication,编译启动应用,服务访问正常就启动成功了。

    6.1.2 mango-common

    新建一个空的maven工程,除了一个pom.xml没有其他内容,后续放置一些工具方法和常量。

    6.1.3 mango-core

    新建一个空的maven工程,后续放置一些公共核心业务代码封装,如HTTP交互格式封装、业务基类封装和分页工具封装等。

    6.1.4 mango-pom

    为了方便统一打包,新建一个mango-pom工程。这个工程依赖所有模块,负责统一进行打包(不然编译的时候需要逐个编译,工程一多很是麻烦),但因我们采用的是微服务架构,每个工程模块使用的依赖版本可能都是不一样的,所以这里的mango-pom与所有模块不存在实质性的父子模块关系,也不由mango-pom进行统一版本和依赖管理,只是为了便利打包。

    6.1.5 打包测试

    下面进行统一打包测试,我们最终所要的效果是:只要在mango-pom下的pom.xml运行打包就可以编译打包所有模块。现在各个子模块都没有编译过,模块间的依赖也还没有加,所以第一次还需要遵循以下步骤进行操作:

    • 右击mango-common下的pom.xml,执行Maven Install编译打包。

    • 在mango-core下的pom.xml内添加mango-common为dependency依赖,然后执行编译打包命令。

      <dependency>
      			<groupId>com.louis</groupId>
      			<artifactId>mango-common</artifactId>
      			<version>1.0.0</version>
      		</dependency>
      
    • 在mango-admin下的pom.xml内添加,mango-core为dependency依赖,然后执行编译打包命令。

      <dependency>
      			<groupId>com.louis</groupId>
      			<artifactId>mango-core</artifactId>
      			<version>1.0.0</version>
      		</dependency>
      
    • 在mango-pom下的pom.xml内添加以上所有模块的modules依赖,然后执行编译打包命令。

      <modules>
      		<module>../mango-admin</module>
      		<module>../mango-common</module>
      		<module>../mango-core</module>
      	</modules>
      

    注:如果工程出现红叉,可以尝试右击工程Maven→Update Project进行解决。

    以后只要对mango-pom下的pom.xml执行命令,就可以统一打包所有模块了。如果控制台输出信息如下所示的打包信息,就表示打包成功了。

    在这里插入图片描述

    6.2 业务代码封装

    为了统一业务代码接口、保持代码整洁、提升代码性能,这里对一些通用的代码进行了统一封装,封装内容如下所示:

    在这里插入图片描述

    6.2.1 通用CURD接口

    CurdService是对通用增删改查接口的封装,统一定义了包含保存、删除、批量删除、根据ID查询和分页查询方法,一般的业务Service接口会继承此接口,提供基础增删改查服务,这几个接口能满足大部分基础CRUD业务的需求,封装详情参见代码注释。

    CurdService.java:

    public interface CurdService<T> {
    	
    	/**
    	 * 保存操作
    	 * @param record
    	 * @return
    	 */
    	int save(T record);
    	
    	/**
    	 * 删除操作
    	 * @param record
    	 * @return
    	 */
    	int delete(T record);
    	
    	/**
    	 * 批量删除操作
    	 * @param entities
    	 */
    	int delete(List<T> records);
    	
    	/**
    	 * 根据ID查询
    	 * @param id
    	 * @return
    	 */
    	T findById(Long id);
    	
        /**
         * 分页查询
    	 * 这里统一封装了分页请求和结果,避免直接引入具体框架的分页对象, 如MyBatis或JPA的分页对象
    	 * 从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会
    	 * 影响服务层以上的分页接口,起到了解耦的作用
    	 * @param pageRequest 自定义,统一分页查询请求
    	 * @return PageResult 自定义,统一分页查询结果
         */
    	PageResult findPage(PageRequest pageRequest);
    	
    }
    

    6.2.2 分页请求封装

    对分页请求的参数做了统一封装,传入分页查询的页码和数量即可。

    PageRequest.java:

    public class PageRequest {
    	/**
    	 * 当前页码
    	 */
    	private int pageNum = 1;
    	/**
    	 * 每页数量
    	 */
    	private int pageSize = 10;
    	/**
    	 * 查询参数
    	 */
    	private Map<String, Object> params = new HashMap<>();
    	
    	public int getPageNum() {
    		return pageNum;
    	}
    	public void setPageNum(int pageNum) {
    		this.pageNum = pageNum;
    	}
    	public int getPageSize() {
    		return pageSize;
    	}
    	public void setPageSize(int pageSize) {
    		this.pageSize = pageSize;
    	}
    	public Map<String, Object> getParams() {
    		return params;
    	}
    	public void setParams(Map<String, Object> params) {
    		this.params = params;
    	}
    	public Object getParam(String key) {
    		return getParams().get(key);
    	}	
    }
    

    6.2.3 分页结果封装

    对分页查询的结果进行了统一封装,结果返回业务数据和分页数据。

    PageResult.java:

    public class PageResult {
    	/**
    	 * 当前页码
    	 */
    	private int pageNum;
    	/**
    	 * 每页数量
    	 */
    	private int pageSize;
    	/**
    	 * 记录总数
    	 */
    	private long totalSize;
    	/**
    	 * 页码总数
    	 */
    	private int totalPages;
    	/**
    	 * 分页数据
    	 */
    	private List<?> content;
    	public int getPageNum() {
    		return pageNum;
    	}
    	public void setPageNum(int pageNum) {
    		this.pageNum = pageNum;
    	}
    	public int getPageSize() {
    		return pageSize;
    	}
    	public void setPageSize(int pageSize) {
    		this.pageSize = pageSize;
    	}
    	public long getTotalSize() {
    		return totalSize;
    	}
    	public void setTotalSize(long totalSize) {
    		this.totalSize = totalSize;
    	}
    	public int getTotalPages() {
    		return totalPages;
    	}
    	public void setTotalPages(int totalPages) {
    		this.totalPages = totalPages;
    	}
    	public List<?> getContent() {
    		return content;
    	}
    	public void setContent(List<?> content) {
    		this.content = content;
    	}
    }
    

    6.2.4 分页助手封装

    对MyBatis的分页查询业务代码进行统一的封装,通过分页助手可以极大简化Service查询业务的编写。

    MybatisPageHelper.java:

    public class MybatisPageHelper {
    
    	public static final String findPage = "findPage";
    	
    	/**
    	 * 分页查询, 约定查询方法名为 “findPage” 
    	 * @param pageRequest 分页请求
    	 * @param mapper Dao对象,MyBatis的 Mapper	
    	 * @param args 方法参数
    	 * @return
    	 */
    	public static PageResult findPage(PageRequest pageRequest, Object mapper) {
    		return findPage(pageRequest, mapper, findPage);
    	}
    	
    	/**
    	 * 调用分页插件进行分页查询
    	 * @param pageRequest 分页请求
    	 * @param mapper Dao对象,MyBatis的 Mapper	
    	 * @param queryMethodName 要分页的查询方法名
    	 * @param args 方法参数
    	 * @return
    	 */
    	@SuppressWarnings({ "unchecked", "rawtypes" })
    	public static PageResult findPage(PageRequest pageRequest, Object mapper, String queryMethodName, Object... args) {
    		// 设置分页参数
    		int pageNum = pageRequest.getPageNum();
    		int pageSize = pageRequest.getPageSize();
    		PageHelper.startPage(pageNum, pageSize);
    		// 利用反射调用查询方法
    		Object result = ReflectionUtils.invoke(mapper, queryMethodName, args);
    		return getPageResult(pageRequest, new PageInfo((List) result));
    	}
    
    	/**
    	 * 将分页信息封装到统一的接口
    	 * @param pageRequest 
    	 * @param page
    	 * @return
    	 */
    	private static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo) {
    		PageResult pageResult = new PageResult();
            pageResult.setPageNum(pageInfo.getPageNum());
            pageResult.setPageSize(pageInfo.getPageSize());
            pageResult.setTotalSize(pageInfo.getTotal());
            pageResult.setTotalPages(pageInfo.getPages());
            pageResult.setContent(pageInfo.getList());
    		return pageResult;
    	}
    }
    

    6.2.5 HTTP结果封装

    对接口调用返回结果进行了统一的封装,方便前端或者移动端对返回结果进行统一的处理。

    HttpResult.java:

    public class HttpResult {
    
    	private int code = 200;
    	private String msg;
    	private Object data;
    	
    	public static HttpResult error() {
    		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
    	}
    	
    	public static HttpResult error(String msg) {
    		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    	}
    	
    	public static HttpResult error(int code, String msg) {
    		HttpResult r = new HttpResult();
    		r.setCode(code);
    		r.setMsg(msg);
    		return r;
    	}
    
    	public static HttpResult ok(String msg) {
    		HttpResult r = new HttpResult();
    		r.setMsg(msg);
    		return r;
    	}
    	
    	public static HttpResult ok(Object data) {
    		HttpResult r = new HttpResult();
    		r.setData(data);
    		return r;
    	}
    	
    	public static HttpResult ok() {
    		return new HttpResult();
    	}
    
    	public int getCode() {
    		return code;
    	}
    
    	public void setCode(int code) {
    		this.code = code;
    	}
    
    	public String getMsg() {
    		return msg;
    	}
    
    	public void setMsg(String msg) {
    		this.msg = msg;
    	}
    
    	public Object getData() {
    		return data;
    	}
    
    	public void setData(Object data) {
    		this.data = data;
    	}
    }
    

    6.3 MyBatis分页查询

    使用MyBatis时,最头疼的就是分页,需要先写一个查询count的select语句,再写一个真正分页查询的语句,当查询条件多了之后,就会发现真不想花双倍的时间写count和select。幸好我们有pagehelper分页插件。pagehelper是一个强大实用的MyBatis分页插件,可以帮助我们快速实现分页功能。

    6.3.1 添加依赖

    在mango-core下的pom.xml文件内添加分页插件依赖包,因为mango-admin依赖mango-core模块,所以mango-admin模块也能获取分页插件依赖。

    <!-- pagehelper -->
    		<dependency>
    		    <groupId>com.github.pagehelper</groupId>
    		    <artifactId>pagehelper-spring-boot-starter</artifactId>
    		    <version>1.2.5</version>
    		</dependency>
    

    6.3.2 添加配置

    在mango-admin配置文件内添加分页插件配置

    application.yml:

    pagehelper:
      helper-dialect: mysql
      reasonable: true
      support-methods-arguments: true
      params: count=countSql
    

    6.3.3 分页代码

    首先我们在DAO层添加一个分页查询方法:

    SysUserMapper.java:

    List<SysUser> findPage();
    

    给SysUserMapper.xml添加查询方法,这是一个普通的查找全部记录的查询语句,并不需要写分页SQL,分页插件会拦截查询请求,并读取前台传来的分页查询参数重新生成分页查询语句。

    SysUserMapper.xml:

    <select id="findPage" resultMap="BaseResultMap">
        select u.*, (select d.name from sys_dept d where d.id = u.dept_id) deptName from sys_user u
      </select>
    

    服务层调用DAO层完成分页查询,让SysUserService继承CurdService接口。

    SysUserService.java:

    /**
    	 * 查找所有用户
    	 * @return
    	 */
    	List<SysUser>findAll();
    

    在实现类中编写分页查询业务类实现,我们可以看到,经过对分页查询业务的封装,普通分页查询非常简单,只需调用MyBatisPageHelper.findPage(pageRequest,sysUserMapper)一行代码即可完成分页查询功能.

    SysUserServiceImpl.java:

    @Service
    public class SysUserServiceImpl  implements SysUserService {
    	@Autowired
    	private SysUserMapper sysUserMapper;
        
        @Override
    	public PageResult findPage(PageRequest pageRequest) {
        	return MyBatisPageHelper.findPage(pageRequest,sysUserMapper)
        }
    }
    

    编写分页查询接口,简单调用Service的查询接口。

    SysUserController.java:

    @RestController
    @RequestMapping("user")
    public class SysUserController {
        @Autowired
    	private SysUserService sysUserService;
        
        @PostMapping(value="/findPage")
    	public HttpResult findPage(@RequestBody PageRequest pageRequest) {
    		return HttpResult.ok(sysUserService.findPage(pageRequest));
    	}
    }
    

    6.3.4 接口测试

    通过swagger接口测试,结果如下:

    在这里插入图片描述
    在这里插入图片描述

    6.4 业务功能开发

    业务功能的开发基本都是各种增删改查业务的编写,大多数是重复性工作,业务编写也没有多少技术要点,这里就拿机构管理的开发作为基础CURD的开发范例。

    首先需要事先规划一下,根据需求设计好需要的接口,比如字典管理除了通用的保存、删除、分页查询接口外,还需要一个根据标签名称查询记录的查询方法。

    6.4.1 编写DAO接口

    打开DAO接口,添加findPage、findPageByLabel和findByLable三个接口:

    SysDictMapper.java:

    public interface SysDictMapper {
         List<SysDict> findPage();
        
        List<SysDict> findPageByLabel(@Param(value="label") String label);
    
        List<SysDict> findByLable(@Param(value="label") String label);
    }
    

    6.4.2 编写映射文件

    打开映射文件,编写三个查询方法:findPage、findPageByLabel和findByLable。

    SysDictMapper.xml:

    <select id="findPage" resultMap="BaseResultMap">
        select 
        <include refid="Base_Column_List" />
        from sys_dict
      </select>
      <select id="findPageByLabel" parameterType="java.lang.String" resultMap="BaseResultMap">
      	<bind name="pattern" value="'%' + _parameter.label + '%'" />
      	select 
        <include refid="Base_Column_List" />
        from sys_dict
        where label like #{pattern}
      </select>
      <select id="findByLable" parameterType="java.lang.String" resultMap="BaseResultMap">
        select 
        <include refid="Base_Column_List" />
        from sys_dict
        where label = #{label,jdbcType=VARCHAR}
      </select>
    

    6.4.3 编写服务接口

    新建一个字典接口并继承通用业务接口CurdService,额外添加一个findByLable接口。

    SysDictService.java:

    public interface SysDictService extends CurdService<SysDict> {
    
    	/**
    	 * 根据名称查询
    	 * @param lable
    	 * @return
    	 */
    	List<SysDict> findByLable(String lable);
    }
    

    6.4.4 编写服务实现

    新建一个实现类并实现SysDictService,调用DAO实现相应的业务功能。

    SysDictServiceImpl.java:

    @Service
    public class SysDictServiceImpl  implements SysDictService {
    
    	@Autowired
    	private SysDictMapper sysDictMapper;
    
    	@Override
    	public int save(SysDict record) {
    		if(record.getId() == null || record.getId() == 0) {
    			return sysDictMapper.insertSelective(record);
    		}
    		return sysDictMapper.updateByPrimaryKeySelective(record);
    	}
    
    	@Override
    	public int delete(SysDict record) {
    		return sysDictMapper.deleteByPrimaryKey(record.getId());
    	}
    
    	@Override
    	public int delete(List<SysDict> records) {
    		for(SysDict record:records) {
    			delete(record);
    		}
    		return 1;
    	}
    
    	@Override
    	public SysDict findById(Long id) {
    		return sysDictMapper.selectByPrimaryKey(id);
    	}
    
    	@Override
    	public PageResult findPage(PageRequest pageRequest) {
    		Object label = pageRequest.getParam("label");
    		if(label != null) {
    			return MybatisPageHelper.findPage(pageRequest, sysDictMapper, "findPageByLabel", label);
    		}
    		return MybatisPageHelper.findPage(pageRequest, sysDictMapper);
    	}
    
    	@Override
    	public List<SysDict> findByLable(String lable) {
    		return sysDictMapper.findByLable(lable);
    	}
    
    }
    

    6.4.5 编写控制器

    新建一个字典管理控制器,注入Service并调用Service方法实现接口:

    SysDictController.java:

    @RestController
    @RequestMapping("dict")
    public class SysDictController {
    
    	@Autowired
    	private SysDictService sysDictService;
    	
    	@PostMapping(value="/save")
    	public HttpResult save(@RequestBody SysDict record) {
    		return HttpResult.ok(sysDictService.save(record));
    	}
    
    	@PostMapping(value="/delete")
    	public HttpResult delete(@RequestBody List<SysDict> records) {
    		return HttpResult.ok(sysDictService.delete(records));
    	}
    
    	@PostMapping(value="/findPage")
    	public HttpResult findPage(@RequestBody PageRequest pageRequest) {
    		return HttpResult.ok(sysDictService.findPage(pageRequest));
    	}
    	
    	@GetMapping(value="/findByLable")
    	public HttpResult findByLable(@RequestParam String lable) {
    		return HttpResult.ok(sysDictService.findByLable(lable));
    	}
    }
    

    其他业务功能还有诸如用户管理、角色管理、机构管理、菜单管理、系统日志等业务同理。

    6.5 业务接口汇总

    略。看源码吧,太多了。

    6.6 导出Excel报表

    在实际项目中,报表导出是非常普遍的需求,特别是Excel报表,对数据的汇总和传递都是非常便利,Apache POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读写的功能。这里我们将使用POI实现用户信息的Excel报表作为范例进行讲解。

    官网地址:http://poi.apache.org/

    相关教程:https://www.yiibai.com/apache_poi/

    6.6.1 添加依赖

    在mango0common下的pom文件中添加POI的相关依赖包:

    <!-- poi -->
    		<dependency>
    			<groupId>org.apache.poi</groupId>
    			<artifactId>poi-ooxml</artifactId>
    			<version>4.0.1</version>
    		</dependency>
    

    6.6.2 编写服务接口

    在用户管理接口中添加一个导出用户信息Excel报表的方法,采用分页查询的方式,可以传入要导出数据的范围,如需导出全部,把页数调至很大即可,同时因为调用的是分页查询方法查询数据,所以同样支持传入过滤字段进行数据过滤。

    SysUserService.java:

    /**
    	 * 生成用户信息Excel文件
    	 * @param pageRequest 要导出的分页查询参数
    	 * @return
    	 */
    	File createUserExcelFile(PageRequest pageRequest);
    

    6.6.3 编写服务实现

    在用户管理服务实现类中编写实现代码,生成Excel文件。

    SysUserServiceImpl.java:

    @Override
    	public File createUserExcelFile(PageRequest pageRequest) {
    		PageResult pageResult = findPage(pageRequest);
    		return createUserExcelFile(pageResult.getContent());
    	}
    	
    	public static File createUserExcelFile(List<?> records) {
    		if (records == null) {
    			records = new ArrayList<>();
    		}
    		Workbook workbook = new XSSFWorkbook();
    		Sheet sheet = workbook.createSheet();
    		Row row0 = sheet.createRow(0);
    		int columnIndex = 0;
    		row0.createCell(columnIndex).setCellValue("No");
    		row0.createCell(++columnIndex).setCellValue("ID");
    		row0.createCell(++columnIndex).setCellValue("用户名");
    		row0.createCell(++columnIndex).setCellValue("昵称");
    		row0.createCell(++columnIndex).setCellValue("机构");
    		row0.createCell(++columnIndex).setCellValue("角色");
    		row0.createCell(++columnIndex).setCellValue("邮箱");
    		row0.createCell(++columnIndex).setCellValue("手机号");
    		row0.createCell(++columnIndex).setCellValue("状态");
    		row0.createCell(++columnIndex).setCellValue("头像");
    		row0.createCell(++columnIndex).setCellValue("创建人");
    		row0.createCell(++columnIndex).setCellValue("创建时间");
    		row0.createCell(++columnIndex).setCellValue("最后更新人");
    		row0.createCell(++columnIndex).setCellValue("最后更新时间");
    		for (int i = 0; i < records.size(); i++) {
    			SysUser user = (SysUser) records.get(i);
    			Row row = sheet.createRow(i + 1);
    			for (int j = 0; j < columnIndex + 1; j++) {
    				row.createCell(j);
    			}
    			columnIndex = 0;
    			row.getCell(columnIndex).setCellValue(i + 1);
    			row.getCell(++columnIndex).setCellValue(user.getId());
    			row.getCell(++columnIndex).setCellValue(user.getName());
    			row.getCell(++columnIndex).setCellValue(user.getNickName());
    			row.getCell(++columnIndex).setCellValue(user.getDeptName());
    			row.getCell(++columnIndex).setCellValue(user.getRoleNames());
    			row.getCell(++columnIndex).setCellValue(user.getEmail());
    			row.getCell(++columnIndex).setCellValue(user.getStatus());
    			row.getCell(++columnIndex).setCellValue(user.getAvatar());
    			row.getCell(++columnIndex).setCellValue(user.getCreateBy());
    			row.getCell(++columnIndex).setCellValue(DateTimeUtils.getDateTime(user.getCreateTime()));
    			row.getCell(++columnIndex).setCellValue(user.getLastUpdateBy());
    			row.getCell(++columnIndex).setCellValue(DateTimeUtils.getDateTime(user.getLastUpdateTime()));
    		}
    		return PoiUtils.createExcelFile(workbook, "download_user");
    	}
    

    6.6.4 编写控制器

    在用户管理控制器类中添加一个接口,并调用Service获取File,最终通过文件操作工具类将File下载到本地。

    SysUserController.java:

    @PostMapping(value="/exportExcelUser")
    	public void exportExcelUser(@RequestBody PageRequest pageRequest, HttpServletResponse res) {
    		File file = sysUserService.createUserExcelFile(pageRequest);
    		FileUtils.downloadFile(res, file, file.getName());
    	}
    

    6.6.5 工具类代码

    为了简化代码,前面代码的实现封装了一些工具类。

    1. PoiUtils

      在编写服务实现的时候我们通过PoiUtils中的createUserExcelFile方法生成Excel文件:

      public class PoiUtils {
      
      	/**
      	 * 生成Excel文件
      	 * @param workbook
      	 * @param fileName
      	 * @return
      	 */
      	public static File createExcelFile(Workbook workbook, String fileName) {
      		OutputStream stream = null;
      		File file = null;
      		try {
      			file = File.createTempFile(fileName, ".xlsx");
      			stream = new FileOutputStream(file.getAbsoluteFile());
      			workbook.write(stream);
      		} catch (FileNotFoundException e) {
      			e.printStackTrace();
      		} catch (IOException e) {
      			e.printStackTrace();
      		} finally {
      			IOUtils.closeQuietly(workbook);
      			IOUtils.closeQuietly(stream);
      		}
      		return file;
      	}
      }
      
    2. FileUtils

      在编写导出接口时我们通过FileUtils中的downloadFile将Excel文件下载到本地:

      public class FileUtils {
      
      	/**
      	 * 下载文件
      	 * @param response
      	 * @param file
      	 * @param newFileName
      	 */
      	public static void downloadFile(HttpServletResponse response, File file, String newFileName) {
      		try {
      			response.setHeader("Content-Disposition", "attachment; filename=" + new String(newFileName.getBytes("ISO-8859-1"), "UTF-8"));
      			BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
      			InputStream is = new FileInputStream(file.getAbsolutePath());
      			BufferedInputStream bis = new BufferedInputStream(is);
      			int length = 0;
      			byte[] temp = new byte[1 * 1024 * 10];
      			while ((length = bis.read(temp)) != -1) {
      				bos.write(temp, 0, length);
      			}
      			bos.flush();
      			bis.close();
      			bos.close();
      			is.close();
      		} catch (Exception e) {
      			e.printStackTrace();
      		}
      	}
      }
      

    6.6.6 接口测试

    编译启动应用,访问http://localhost:8001/swagger-ui.html#/,进入Swagger接口测试页。

    输入分页查询信息,指定要导出的用户数据范围,单击Execute按钮发送请求。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    7. 登录流程实现

    用户登录流程是后台管理系统必备的功能,接下来我们将实现用户登录流程。在这个过程中我们将利用kaptcha实现登录验证码,利用Spring Security进行安全控制。

    7.1 登录验证码

    1. 添加依赖

      在mango-admin下的pom文件添加kaptcha依赖包:

      <!-- kaptcha -->
      		<dependency>
      			<groupId>com.github.axet</groupId>
      			<artifactId>kaptcha</artifactId>
      			<version>0.0.9</version>
      		</dependency>
      
    2. 添加配置

      在config包下创建一个kaptcha配置类,配置验证码的一些生成属性:

      KaptchaConfig.java:

      /**
       * 验证码配置
       */
      @Configuration
      public class KaptchaConfig {
      
          @Bean
          public DefaultKaptcha producer() {
              Properties properties = new Properties();
              properties.put("kaptcha.border", "no");
              properties.put("kaptcha.textproducer.font.color", "black");
              properties.put("kaptcha.textproducer.char.space", "5");
              Config config = new Config(properties);
              DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
              defaultKaptcha.setConfig(config);
              return defaultKaptcha;
          }
      }
      
    3. 生成代码

      新建一个控制器,提供系统登录相关的API,在其中添加验证码生成接口。

      SysLoginController.java:

      @RestController
      public class SysLoginController {
      
      	@Autowired
      	private Producer producer;
      	@Autowired
      	private SysUserService sysUserService;
      	@Autowired
      	private AuthenticationManager authenticationManager;
      
      	@GetMapping("captcha.jpg")
      	public void captcha(HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException {
      		response.setHeader("Cache-Control", "no-store, no-cache");
      		response.setContentType("image/jpeg");
      
      		// 生成文字验证码
      		String text = producer.createText();
      		// 生成图片验证码
      		BufferedImage image = producer.createImage(text);
      		// 保存到验证码到 session
      		request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text);
      
      		ServletOutputStream out = response.getOutputStream();
      		ImageIO.write(image, "jpg", out);	
      		IOUtils.closeQuietly(out);
      	}
      
      	/**
      	 * 登录接口
      	 */
      	@PostMapping(value = "/login")
      	public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
      		String username = loginBean.getAccount();
      		String password = loginBean.getPassword();
      		String captcha = loginBean.getCaptcha();
      		// 从session中获取之前保存的验证码跟前台传来的验证码进行匹配
      		Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
      		if(kaptcha == null){
      			return HttpResult.error("验证码已失效");
      		}
      		if(!captcha.equals(kaptcha)){
      			return HttpResult.error("验证码不正确");
      		}
      		// 用户信息
      		SysUser user = sysUserService.findByName(username);
      		// 账号不存在、密码错误
      		if (user == null) {
      			return HttpResult.error("账号不存在");
      		}
      		if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
      			return HttpResult.error("密码不正确");
      		}
      		// 账号锁定
      		if (user.getStatus() == 0) {
      			return HttpResult.error("账号已被锁定,请联系管理员");
      		}
      		// 系统登录认证
      		JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
      		return HttpResult.ok(token);
      	}
      
      }
      
    4. 接口测试

      编译启动应用在Swagger测试页进行测试:

    在这里插入图片描述

    7.2 Spring Security

    在Web应用开发中,安全一直是非常重要的一个方面。Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。JWT(JSON Web Token)是当前比较主流的Token令牌生成方案,非常适合作为登录和授权认证的凭证。这里我们使用Spring Security并结合JWT实现用户认证(Authentication)和用户授权(Authorization)两个主要部分的安全内容。

    7.2.1 添加依赖

    在mango-admin下的pom文件中添加Spring Security和JWT依赖包。

    <!-- spring security -->
    		<dependency>
    		    <groupId>org.springframework.boot</groupId>
    		    <artifactId>spring-boot-starter-security</artifactId>
    		</dependency>
    		<!-- jwt -->
    		<dependency>
    		    <groupId>io.jsonwebtoken</groupId>
    		    <artifactId>jjwt</artifactId>
    		    <version>0.9.1</version>
    		</dependency>
    

    7.2.2 添加配置

    在config包下新建一个Spring Security的配置类WebSecurityConfig,主要是进行一些安全相关的配置,比如权限URL匹配策略、认证过滤器配置、定制身份验证组件、开启权限认证注解等。

    WebSecurityConfig.java:

    @Configuration
    @EnableWebSecurity	// 开启Spring Security 
    @EnableGlobalMethodSecurity(prePostEnabled = true)	// 开启权限注解,如:@PreAuthorize注解
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
        
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 使用自定义身份验证组件
            auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
            http.cors().and().csrf().disable()
        		.authorizeRequests()
        		// 跨域预检请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // web jars
                .antMatchers("/webjars/**").permitAll()
                // 查看SQL监控(druid)
                .antMatchers("/druid/**").permitAll()
                // 首页和登录页面
                .antMatchers("/").permitAll()
                .antMatchers("/login").permitAll()
                // swagger
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
                // 验证码
                .antMatchers("/captcha.jpg**").permitAll()
                // 服务监控
                .antMatchers("/actuator/**").permitAll()
                // 其他所有请求需要身份认证
                .anyRequest().authenticated();
            // 退出登录处理器
            http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
            // token验证过滤器
            http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManager() throws Exception {
        	return super.authenticationManager();
        }
        
    }
    

    7.2.3 登录认证过滤器

    登录认证过滤器负责登录认证时检查并生产令牌保存到上下文,接口权限认证过程时,系统从上下文获取令牌校验接口访问权限,新建一个security包,在其下创建JwtAuthenticationFilter并继承BasicAuthenticationFilter,覆写其中的doFilterInternal方法进行Token校验。

    JwtAuthenticationFilter.java:

    public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    
    	
    	@Autowired
        public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        	// 获取token, 并检查登录状态
            SecurityUtils.checkAuthentication(request);
            chain.doFilter(request, response);
        }
        
    }
    

    这里我们把验证逻辑抽取到了SecurityUtils的checkAuthentication方法中,checkAuthentication通过JwtTokenUtils的方法获取认证信息并保存到Spring Security上下文。

    SecurityUtils.java:

    /**
    	 * 获取令牌进行认证
    	 * @param request
    	 */
    	public static void checkAuthentication(HttpServletRequest request) {
    		// 获取令牌并根据令牌获取登录认证信息
    		Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
    		// 设置登录认证信息到上下文
    		SecurityContextHolder.getContext().setAuthentication(authentication);
    	}
    

    JwtTokenUtils的getAuthenticationeFromToken方法获取并校验请求携带的令牌。

    JwtTokenUtils.java:

    /**
    	 * 根据请求令牌获取登录认证信息
    	 * @param token 令牌
    	 * @return 用户名
    	 */
    	public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
    		Authentication authentication = null;
    		// 获取请求携带的令牌
    		String token = JwtTokenUtils.getToken(request);
    		if(token != null) {
    			// 请求令牌不能为空
    			if(SecurityUtils.getAuthentication() == null) {
    				// 上下文中Authentication为空
    				Claims claims = getClaimsFromToken(token);
    				if(claims == null) {
    					return null;
    				}
    				String username = claims.getSubject();
    				if(username == null) {
    					return null;
    				}
    				if(isTokenExpired(token)) {
    					return null;
    				}
    				Object authors = claims.get(AUTHORITIES);
    				List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    				if (authors != null && authors instanceof List) {
    					for (Object object : (List) authors) {
    						authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
    					}
    				}
    				authentication = new JwtAuthenticatioToken(username, null, authorities, token);
    			} else {
    				if(validateToken(token, SecurityUtils.getUsername())) {
    					// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
    					authentication = SecurityUtils.getAuthentication();
    				}
    			}
    		}
    		return authentication;
    	}
    

    JwtTokenUtils的getToken尝试从请求头中获取请求携带的令牌,默认从请求头中的"Authorization"参数以"Bearer"开头的信息为令牌信息,若为空则尝试从"Token"参数获取。

    JwtTokenUtils.java:

    /**
         * 获取请求token
         * @param request
         * @return
         */
        public static String getToken(HttpServletRequest request) {
        	String token = request.getHeader("Authorization");
            String tokenHead = "Bearer ";
            if(token == null) {
            	token = request.getHeader("token");
            } else if(token.contains(tokenHead)){
            	token = token.substring(tokenHead.length());
            } 
            if("".equals(token)) {
            	token = null;
            }
            return token;
        }
    

    7.2.4 身份验证组件

    Spring Security的登录验证是由ProviderManager负责的,ProviderManager在实际验证的适合又会通过调用AuthenticationProvider的authenticate方法进行认证。数据库类型的默认实现方案是DaoAuthenticationProvider。我们这里通过继承DaoAuthenticationProvider定制默认的登录认证逻辑,在Security包下新建验证器JwtAuthenticationProvider并继承DaoAuthenticationProvider,覆盖实现additionalAuthenticationChecks方法进行密码匹配,我们这里没有使用默认的密码认证器(我们使用盐salt来对密码进行加密,默认密码验证其没有加盐),所以在这里定制了自己的密码校验逻辑。也可以直接覆写authenticate方法来完成更大范围的登录认证需求定制。

    JwtAuthenticationProvider.java:

    public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
    
        public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
            setUserDetailsService(userDetailsService);
        }
    
        @Override
    	protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
    			throws AuthenticationException {
    		if (authentication.getCredentials() == null) {
    			logger.debug("Authentication failed: no credentials provided");
    			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    		}
    
    		String presentedPassword = authentication.getCredentials().toString();
    		String salt = ((JwtUserDetails) userDetails).getSalt();
    		// 覆写密码验证逻辑
    		if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
    			logger.debug("Authentication failed: password does not match stored value");
    			throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    		}
    	}
    
    }
    

    7.2.5 认证信息查询

    我们上面提到的登录认证默认是通过DaoAuthenticationProvider来完成登录认证的,而我们知道登录验证器在进行时肯定要从数据库获取用户信息进行匹配的,而这个获取用户信息的任务是通过Spring Security的UserDetailsService组件完成的。

    在security包下新建一个UserDetailsServiceImpl并实现UserDetailsService接口,覆写其中的方法loadUserByUsername,查询用户的密码信息和权限信息并封装在UserDetails的实现类对象,作为结果JwtUserDetails返回给DaoAuthenticationProvider做进一步处理。

    UserDetailsServiceImpl.java:

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private SysUserService sysUserService;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            SysUser user = sysUserService.findByName(username);
            if (user == null) {
                throw new UsernameNotFoundException("该用户不存在");
            }
            // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
            Set<String> permissions = sysUserService.findPermissions(user.getName());
            List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
            return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
        }
    }
    

    JwtUserDetails是对认证信息的封装,实现Spring Security,提供UserDetails接口,主要包含用户名、密码、加密盐和权限等信息。

    JwtUserDetails.java:

    public class JwtUserDetails implements UserDetails {
    
    	private static final long serialVersionUID = 1L;
    	
    	private String username;
        private String password;
        private String salt;
        private Collection<? extends GrantedAuthority> authorities;
    
        JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
            this.username = username;
            this.password = password;
            this.salt = salt;
            this.authorities = authorities;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @JsonIgnore
        @Override
        public String getPassword() {
            return password;
        }
    
        public String getSalt() {
    		return salt;
    	}
        
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @JsonIgnore
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @JsonIgnore
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @JsonIgnore
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @JsonIgnore
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    

    GrantedAuthorityImpl实现Spring Security的GrantedAuthority接口,是对权限的封装,内部包含一个字符串类型的权限标识authority,对应菜单表的perms字段的权限字符串,比如用户管理的增删改查权限标志sys:user:view、sys:user:add、sys:user:edit、sys:user:delete。

    GrantedAuthorityImpl.java:

    public class GrantedAuthorityImpl implements GrantedAuthority {
    	
    	private static final long serialVersionUID = 1L;
    
    	private String authority;
    
        public GrantedAuthorityImpl(String authority) {
            this.authority = authority;
        }
    
        public void setAuthority(String authority) {
            this.authority = authority;
        }
    
        @Override
        public String getAuthority() {
            return this.authority;
        }
    }
    

    7.2.6 添加权限注解

    在用户拥有某个后台访问权限才能访问,这叫做接口保护。我们这里通过Spring Security提供的权限注解来保护后台接口免受非法访问,这里以字典管理模块为例。

    在SysDictController的接口上添加类似@PreAuthorize(“hasAuthority(‘sys:dict:view’)”)的注解,表示只有当前登录用户拥有sys:dict:view权限标识才能访问此接口,这里的权限标识需对应菜单表中的perms权限信息,所以可通过配置菜单表的权限来灵活控制接口的访问权限。

    SysDictController.java:

    @RestController
    @RequestMapping("dict")
    public class SysDictController {
    
    	@Autowired
    	private SysDictService sysDictService;
    	
    	@PreAuthorize("hasAuthority('sys:dict:add') AND hasAuthority('sys:dict:edit')")
    	@PostMapping(value="/save")
    	public HttpResult save(@RequestBody SysDict record) {
    		return HttpResult.ok(sysDictService.save(record));
    	}
    
    	@PreAuthorize("hasAuthority('sys:dict:delete')")
    	@PostMapping(value="/delete")
    	public HttpResult delete(@RequestBody List<SysDict> records) {
    		return HttpResult.ok(sysDictService.delete(records));
    	}
    
    	@PreAuthorize("hasAuthority('sys:dict:view')")
    	@PostMapping(value="/findPage")
    	public HttpResult findPage(@RequestBody PageRequest pageRequest) {
    		return HttpResult.ok(sysDictService.findPage(pageRequest));
    	}
    	
    	@PreAuthorize("hasAuthority('sys:dict:view')")
    	@GetMapping(value="/findByLable")
    	public HttpResult findByLable(@RequestParam String lable) {
    		return HttpResult.ok(sysDictService.findByLable(lable));
    	}
    }
    

    7.2.7 Swagger添加令牌函数

    由于我们引入Spring Security安全框架,接口受到保护,需要携带合法的token令牌(一般是登录成功之后由后台返回)才能正常访问,但是Swagger本身的接口测试页面默认没有提供传送token参数的地方,因此需要简单定制一下,修改SwaggerConfig配置类即可。

    SwaggerConfig.java:

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
    
        @Bean
        public Docket createRestApi(){
        	// 添加请求参数,我们这里把token作为请求头部参数传入后端
    		ParameterBuilder parameterBuilder = new ParameterBuilder();  
    		List<Parameter> parameters = new ArrayList<Parameter>();  
    		parameterBuilder.name("token").description("令牌")
    			.modelRef(new ModelRef("string")).parameterType("header").required(false).build();  
    		parameters.add(parameterBuilder.build());  
    		return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
    				.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
    				.build().globalOperationParameters(parameters);
    //        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
    //        		.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
        }
    
        private ApiInfo apiInfo(){
            return new ApiInfoBuilder().build();
        }
    
    }
    

    配置之后重新启动就可以进行传参了,测试接口时先将登录接口返回的token复制到此处即可.

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    7.3 登录接口实现

    在登录控制器中添加一个登录接口login,在其中验证验证码、用户名、密码信息。匹配成功之后,执行Spring Security的登录认证机制。登录成功之后返回Token令牌凭证。

    SysLoginController.java:

    /**
    	 * 登录接口
    	 */
    	@PostMapping(value = "/login")
    	public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
    		String username = loginBean.getAccount();
    		String password = loginBean.getPassword();
    		String captcha = loginBean.getCaptcha();
    		// 从session中获取之前保存的验证码跟前台传来的验证码进行匹配
    		Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
    		if(kaptcha == null){
    			return HttpResult.error("验证码已失效");
    		}
    		if(!captcha.equals(kaptcha)){
    			return HttpResult.error("验证码不正确");
    		}
    		// 用户信息
    		SysUser user = sysUserService.findByName(username);
    		// 账号不存在、密码错误
    		if (user == null) {
    			return HttpResult.error("账号不存在");
    		}
    		if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
    			return HttpResult.error("密码不正确");
    		}
    		// 账号锁定
    		if (user.getStatus() == 0) {
    			return HttpResult.error("账号已被锁定,请联系管理员");
    		}
    		// 系统登录认证
    		JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
    		return HttpResult.ok(token);
    	}
    

    这里将Spring Security的登录认证逻辑封装到了工具类SecurityUtils的login方法中,认证流程大致分为以下四步:

    • 将用户名密码的认证信息封装到JwtAuthenticatioToken对象。
    • 通过调用authenticationManager.authenticate(token)执行认证流程。
    • 通过SecurityContextHolder将认证信息保存到上下文。
    • 通过JwtTokenUtils.generateToken(authentication)生成token并返回。

    SecurityUtils.java:

    /**
    	 * 系统登录认证
    	 * @param request
    	 * @param username
    	 * @param password
    	 * @param authenticationManager
    	 * @return
    	 */
    	public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
    		JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
    		token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    		// 执行登录认证过程
    	    Authentication authentication = authenticationManager.authenticate(token);
    	    // 认证成功存储认证信息到上下文
    	    SecurityContextHolder.getContext().setAuthentication(authentication);
    		// 生成令牌并返回给客户端
    	    token.setToken(JwtTokenUtils.generateToken(authentication));
    		return token;
    	}
    

    关于JwtTokenUtils如何生成Token的逻辑参见以下两个方法:

    /**
    	 * 生成令牌
    	 *
    	 * @param userDetails 用户
    	 * @return 令牌
    	 */
    	public static String generateToken(Authentication authentication) {
    	    Map<String, Object> claims = new HashMap<>(3);
    	    claims.put(USERNAME, SecurityUtils.getUsername(authentication));
    	    claims.put(CREATED, new Date());
    	    claims.put(AUTHORITIES, authentication.getAuthorities());
    	    return generateToken(claims);
    	}
    
    	/**
         * 从数据声明生成令牌
         *
         * @param claims 数据声明
         * @return 令牌
         */
        private static String generateToken(Map<String, Object> claims) {
            Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
        }
    

    LoginBean是对登录认证信息的简单封装,包含账号密码和验证码信息:

    LoginBean.java:

    public class LoginBean {
    
    	private String account;
    	private String password;
    	private String captcha;
    	
    	public String getAccount() {
    		return account;
    	}
    	public void setAccount(String account) {
    		this.account = account;
    	}
    	public String getPassword() {
    		return password;
    	}
    	public void setPassword(String password) {
    		this.password = password;
    	}
    	public String getCaptcha() {
    		return captcha;
    	}
    	public void setCaptcha(String captcha) {
    		this.captcha = captcha;
    	}
    	
    }
    

    JwtAuthenticatioToken继承UsernamePasswordAuthenticationToken,是对令牌信息的简单封装,用来作为认证和授权的信任凭证,其中的token信息由JWT负责生成:

    JwtAuthenticatioToken.java:

    public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {
    
    	private static final long serialVersionUID = 1L;
    	
    	private String token;
    
        public JwtAuthenticatioToken(Object principal, Object credentials){
            super(principal, credentials);
        }
        
        public JwtAuthenticatioToken(Object principal, Object credentials, String token){
        	super(principal, credentials);
        	this.token = token;
        }
    
        public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        	super(principal, credentials, authorities);
        	this.token = token;
        }
        
    	public String getToken() {
    		return token;
    	}
    
    	public void setToken(String token) {
    		this.token = token;
    	}
    
    	public static long getSerialversionuid() {
    		return serialVersionUID;
    	}
    
    }
    

    接口测试见上节图片。

    7.4 Spring Security执行流程剖析

    Spring Security功能强大,使用也稍显复杂,因为涉及的内容比较多,所以入门门槛比较高,很多从业人员深受其烦,不少人就算跟着网上的教程会使用项目案例了,但是遇到问题还是摸不着头脑,这都是对其执行流程不熟悉造成的。(焯!你再骂!)

    作者大大是个好人,为我这种菜鸡附上了他博客的一篇剖析文章,通过追踪与解读源码的方式为读者详细剖析Spring Security的执行流程。在熟悉整个流程之后,许多问题就迎刃而解了。https://www.cnblogs.com/xifengxiaoma/p/10020960.html。(哼!在我收藏夹吃灰去吧)

    8. 数据备份还原

    很多时候,我们需要对系统数据进行备份和还原。当然,实际生产环境的数据备份和还原通常是由专业数据库维护人员在数据库端通过命令执行的。这里提供的是通过代码进行数据备份,主要是方便一些日常的数据恢复,比如说想把数据恢复到某一时间节点的数据。这一章节讲解如何通过代码调用MySQL的备份还原命令实现系统备份还原的功能。

    8.1 新建工程

    新建mango-backup工程,这是一个独立运行于admin的服务模块,可以分开独立部署。

    8.2 添加依赖

    在pom.xml中添加以下相关依赖,主要包含Web、Swagger和common依赖包:

    <dependencies>
    		<!-- spring boot -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<!-- swagger -->
    		<dependency>
    		    <groupId>io.springfox</groupId>
    		    <artifactId>springfox-swagger2</artifactId>
    		    <version>2.9.2</version>
    		</dependency>
    		<dependency>
    		    <groupId>io.springfox</groupId>
    		    <artifactId>springfox-swagger-ui</artifactId>
    		    <version>2.9.2</version>
    		</dependency>
    		<dependency>
    			<groupId>com.louis</groupId>
    			<artifactId>mango-common</artifactId>
    			<version>1.0.0</version>
    		</dependency>
    	</dependencies>
    

    8.3 添加配置

    在配置文件中添加以下配置,定义启动端口为8002,应用名称是mango-backup,定义系统数据备份还原的数据库连接信息:

    # tomcat
    server:
      port: 8002
    spring:
      application:
        name: mango-backup
    # backup datasource
    mango:
      backup:
        datasource:
          host: localhost
          userName: root
          password: admin123
          database: mango
    

    8.4 自定义Banner

    8.5 启动类

    修改启动类MangoBackupApplication,指定包扫描路径为com.louis.mango。

    MangoBackupApplication.java:

    @SpringBootApplication(scanBasePackages={"com.louis.mango"})
    public class MangoBackupApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(MangoBackupApplication.class, args);
    	}
    }
    

    8.6 跨域配置

    在config包下添加跨域配置类。

    CorsConfig.java:

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")	// 允许跨域访问的路径
            .allowedOrigins("*")	// 允许跨域访问的源
            .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")	// 允许请求方法
            .maxAge(168000)	// 预检间隔时间
            .allowedHeaders("*")  // 允许头部设置
            .allowCredentials(true);	// 是否发送cookie
        }
    }
    

    8.7 Swagger配置

    在config包下添加Swagger配置类。

    SwaggerConfig.java:

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
    	
    	@Bean
    	public Docket createRestApi() {
    		return new Docket(DocumentationType.SWAGGER_2).select()
    				.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
    	}
    	
    }
    

    8.8 数据源属性

    添加一个数据源属性配置类,配置@ConfigurationProperties(prefix = “mango.backup.datasource”) 注解,这样就可以通过注入BackupDataSourceProperties读取数据源属性了。

    BackupDataSourceProperties.java:

    @Component  
    @ConfigurationProperties(prefix = "mango.backup.datasource")  
    public class BackupDataSourceProperties {
    	
    	private String host;
    	private String userName;
    	private String password;
    	private String database;
    	public String getHost() {
    		return host;
    	}
    	public void setHost(String host) {
    		this.host = host;
    	}
    	public String getUserName() {
    		return userName;
    	}
    	public void setUserName(String userName) {
    		this.userName = userName;
    	}
    	public String getPassword() {
    		return password;
    	}
    	public void setPassword(String password) {
    		this.password = password;
    	}
    	public String getDatabase() {
    		return database;
    	}
    	public void setDatabase(String database) {
    		this.database = database;
    	}
    }  
    

    遇到springboot configuration annotation precessor not configured情况时在pom.xml中添加依赖:

    <dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-configuration-processor</artifactId>
    			<optional>true</optional>
    		</dependency>
    

    8.9 备份还原接口

    在service包下添加一个MysqlBackupService接口,包含backup和restore两个接口,分别对应备份和还原两个接口。

    MysqlBackupService.java:

    public interface MysqlBackupService {
    
    	/**
    	 * 备份数据库
    	 * @param host host地址,可以是本机也可以是远程
    	 * @param userName 数据库的用户名
    	 * @param password 数据库的密码
    	 * @param savePath 备份的路径
    	 * @param fileName 备份的文件名
    	 * @param databaseName 需要备份的数据库的名称
    	 * @return
    	 * @throws IOException 
    	 */
    	boolean backup(String host, String userName, String password, String backupFolderPath, String fileName, String database) throws Exception;
    
        /**
         * 恢复数据库
         * @param restoreFilePath 数据库备份的脚本路径
         * @param host IP地址
         * @param database 数据库名称
         * @param userName 用户名
         * @param password 密码
         * @return
         */
    	boolean restore(String restoreFilePath, String host, String userName, String password, String database) throws Exception;
    
    }
    

    8.10 备份还原实现

    在service.impl下添加MysqlBackupServiceImpl,实现backup和restore两个接口。

    MysqlBackupServiceImpl.java:

    @Service
    public class MysqlBackupServiceImpl implements MysqlBackupService {
    
    	@Override
    	public boolean backup(String host, String userName, String password, String backupFolderPath, String fileName,
    			String database) throws Exception {
    		return MySqlBackupRestoreUtils.backup(host, userName, password, backupFolderPath, fileName, database);
    	}
    
    	@Override
    	public boolean restore(String restoreFilePath, String host, String userName, String password, String database)
    			throws Exception {
    		return MySqlBackupRestoreUtils.restore(restoreFilePath, host, userName, password, database);
    	}
    }
    

    8.11 备份还原逻辑

    为了方便复用,我们将系统数据备份和还原逻辑封装到了MySqlBackupRestoreUtils工具类,主要是通过代码调用MySQL的数据库备份和还原命令实现。

    MySqlBackupRestoreUtils.java:

    package com.louis.mango.backup.util;
    
    import java.io.File;
    import java.io.IOException;
    
    /**
     * MySQL备份还原工具类
     * @author Louis
     * @date Jan 15, 2019
     */
    public class MySqlBackupRestoreUtils {
    
    	/**
    	 * 备份数据库
    	 * @param host host地址,可以是本机也可以是远程
    	 * @param userName 数据库的用户名
    	 * @param password 数据库的密码
    	 * @param savePath 备份的路径
    	 * @param fileName 备份的文件名
    	 * @param databaseName 需要备份的数据库的名称
    	 * @return
    	 * @throws IOException 
    	 */
    	public static boolean backup(String host, String userName, String password, String backupFolderPath, String fileName,
    			String database) throws Exception {
    		File backupFolderFile = new File(backupFolderPath);
    		if (!backupFolderFile.exists()) {
    			// 如果目录不存在则创建
    			backupFolderFile.mkdirs();
    		}
    		if (!backupFolderPath.endsWith(File.separator) && !backupFolderPath.endsWith("/")) {
    			backupFolderPath = backupFolderPath + File.separator;
    		}
    		// 拼接命令行的命令
    		String backupFilePath = backupFolderPath + fileName;
    		StringBuilder stringBuilder = new StringBuilder();
    		stringBuilder.append("mysqldump --opt ").append(" --add-drop-database ").append(" --add-drop-table ");
    		stringBuilder.append(" -h").append(host).append(" -u").append(userName).append(" -p").append(password);
    		stringBuilder.append(" --result-file=").append(backupFilePath).append(" --default-character-set=utf8 ").append(database);
    		// 调用外部执行 exe 文件的 Java API
    		Process process = Runtime.getRuntime().exec(getCommand(stringBuilder.toString()));
    		if (process.waitFor() == 0) {
    			// 0 表示线程正常终止
    			System.out.println("数据已经备份到 " + backupFilePath + " 文件中");
    			return true;
    		}
    		return false;
    	}
    
        /**
         * 还原数据库
         * @param restoreFilePath 数据库备份的脚本路径
         * @param host IP地址
         * @param database 数据库名称
         * @param userName 用户名
         * @param password 密码
         * @return
         */
    	public static boolean restore(String restoreFilePath, String host, String userName, String password, String database)
    			throws Exception {
    		File restoreFile = new File(restoreFilePath);
    		if (restoreFile.isDirectory()) {
    			for (File file : restoreFile.listFiles()) {
    				if (file.exists() && file.getPath().endsWith(".sql")) {
    					restoreFilePath = file.getAbsolutePath();
    					break;
    				}
    			}
    		}
    		StringBuilder stringBuilder = new StringBuilder();
    		stringBuilder.append("mysql -h").append(host).append(" -u").append(userName).append(" -p").append(password);
    		stringBuilder.append(" ").append(database).append(" < ").append(restoreFilePath);
    		try {
    			Process process = Runtime.getRuntime().exec(getCommand(stringBuilder.toString()));
    			if (process.waitFor() == 0) {
    				System.out.println("数据已从 " + restoreFilePath + " 导入到数据库中");
    			}
    		} catch (IOException e) {
    			e.printStackTrace();
    			return false;
    		}
    		return true;
    	}
    
    	private static String[] getCommand(String command) {
    		String os = System.getProperty("os.name");  
    		String shell = "/bin/bash";
    		String c = "-c";
    		if(os.toLowerCase().startsWith("win")){  
    			shell = "cmd";
    			c = "/c";
    		}  
    		String[] cmd = { shell, c, command };
    		return cmd;
    	}
    
    	public static void main(String[] args) throws Exception {
    		String host = "localhost";
    		String userName = "root";
    		String password = "admin123";
    		String database = "mango";
    		
    		System.out.println("开始备份");
    		String backupFolderPath = "c:/dev/";
    		String fileName = "mango.sql";
    		backup(host, userName, password, backupFolderPath, fileName, database);
    		System.out.println("备份成功");
    		
    		System.out.println("开始还原");
    		String restoreFilePath = "c:/dev/mango.sql";
    		restore(restoreFilePath, host, userName, password, database);
    		System.out.println("还原成功");
    
    	}
    
    }
    

    8.12 备份还原控制器

    在controller包下新建一个控制器,备份还原控制器对外提供数据备份还原的服务。包含以下几个接口。

    8.12.1 数据备份接口

    backup是数据备份接口,读取数据源信息及备份信息生成数据备份。

    @GetMapping("/backup")
    public HttpResult backup() {
       String backupFodlerName = BackupConstants.DEFAULT_BACKUP_NAME + "_" + (new SimpleDateFormat(BackupConstants.DATE_FORMAT)).format(new Date());
       return backup(backupFodlerName);
    }
    
    private HttpResult backup(String backupFodlerName) {
       String host = properties.getHost();
       String userName = properties.getUserName();
       String password = properties.getPassword();
       String database = properties.getDatabase();
       String backupFolderPath = BackupConstants.BACKUP_FOLDER + backupFodlerName + File.separator;
       String fileName = BackupConstants.BACKUP_FILE_NAME;
       try {
          boolean success = mysqlBackupService.backup(host, userName, password, backupFolderPath, fileName, database);
          if(!success) {
             HttpResult.error("数据备份失败");
          }
       } catch (Exception e) {
          return HttpResult.error(500, e.getMessage());
       }
       return HttpResult.ok();
    }
    

    8.12.2 数据还原接口

    restore是数据还原接口,读取数据源信息和还原版本进行数据还原。

    @GetMapping("/restore")
    public HttpResult restore(@RequestParam String name) throws IOException {
       String host = properties.getHost();
       String userName = properties.getUserName();
       String password = properties.getPassword();
       String database = properties.getDatabase();
       String restoreFilePath = BackupConstants.RESTORE_FOLDER + name;
       try {
          mysqlBackupService.restore(restoreFilePath, host, userName, password, database);
       } catch (Exception e) {
          return HttpResult.error(500, e.getMessage());
       }
       return HttpResult.ok();
    }
    

    8.12.3 查找备份接口

    findRecords是备份查找接口,用于查找和向用户展示数据备份版本。

    @GetMapping("/findRecords")
    public HttpResult findBackupRecords() {
       if(!new File(BackupConstants.DEFAULT_RESTORE_FILE).exists()) {
          // 初始默认备份文件
          backup(BackupConstants.DEFAULT_BACKUP_NAME);
       }
       List<Map<String, String>> backupRecords = new ArrayList<>();
       File restoreFolderFile = new File(BackupConstants.RESTORE_FOLDER);
       if(restoreFolderFile.exists()) {
          for(File file:restoreFolderFile.listFiles()) {
             Map<String, String> backup = new HashMap<>();
             backup.put("name", file.getName());
             backup.put("title", file.getName());
             if(BackupConstants.DEFAULT_BACKUP_NAME.equalsIgnoreCase(file.getName())) {
                backup.put("title", "系统默认备份");
             }
             backupRecords.add(backup);
          }
       }
       // 排序,默认备份最前,然后按时间戳排序,新备份在前面
       backupRecords.sort((o1, o2) -> BackupConstants.DEFAULT_BACKUP_NAME.equalsIgnoreCase(o1.get("name")) ? -1
             : BackupConstants.DEFAULT_BACKUP_NAME.equalsIgnoreCase(o2.get("name")) ? 1 : o2.get("name").compareTo(o1.get("name")));
       return HttpResult.ok(backupRecords);
    }
    

    8.12.4 删除备份接口

    delete是备份删除接口,通过备份还原管理界面删除数据备份版本。

    @GetMapping("/delete")
    public HttpResult deleteBackupRecord(@RequestParam String name) {
       if(BackupConstants.DEFAULT_BACKUP_NAME.equals(name)) {     
          return HttpResult.error("系统默认备份无法删除!");
       }
       String restoreFilePath = BackupConstants.BACKUP_FOLDER + name;
       try {
          FileUtils.deleteFile(new File(restoreFilePath));
       } catch (Exception e) {
          return HttpResult.error(500, e.getMessage());
       }
       return HttpResult.ok();
    }
    

    9. 系统服务监控

    Spring Boot Admin是一个管理和监控Spring Boot应用程序的开源监控软件,针对spring-boot的actuator接口进行UI美化并封装,可以在管理界面中浏览所有被监控的spring-boot项目的基本信息,详细的Health信息、内存信息、JVM信息、垃圾回收信息、各种配置信息(比如数据源、缓存列表和命中率)等,还可以直接修改logger的level,Spring Boot Admin提供的丰富详细的监控信息给Spring Boot应用的监控、维护和优化都带来了极大的便利。

    9.1 新建工程

    新建一个mango-monitor项目,作为服务监控服务端。

    9.2 添加依赖

    在pom.xml中添加依赖包,主要是Spring Boot和Spring Boot Admin依赖。

    <dependencies>
    		<!-- spring boot -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter</artifactId>
    		</dependency>
            <!--spring-boot-admin-->
            <dependency>
    		    <groupId>de.codecentric</groupId>
    		    <artifactId>spring-boot-admin-server</artifactId>
    		    <version>2.1.2</version>
    		</dependency>
    		<dependency>
    		    <groupId>de.codecentric</groupId>
    		    <artifactId>spring-boot-admin-server-ui</artifactId>
    		    <version>2.1.2</version>
    		</dependency>
    	</dependencies>
    

    9.3 添加配置

    在配置文件中添加配置,指定启动端口为8000、应用名称为mango-monitor。

    server:
      port: 8000
    spring:
      application:
        name: mango-monitor
    

    9.4 自定义Banner

    9.5 启动类

    修改启动类MangoMonitorApplication并在头部添加EnableAdminServer注解,开启监控服务。

    @EnableAdminServer
    @SpringBootApplication
    public class MangoMonitorApplication {
    
       public static void main(String[] args) {
          SpringApplication.run(MangoMonitorApplication.class, args);
       }
    }
    

    9.6 监控客户端

    把后台服务mango-admin和数据备份还原服务mango-backup注册到监控服务。

    分别在mango-admin和mango-backup的pom文件中添加监控客户端依赖:

    <!--spring-boot-admin-client-->
    <dependency>
       <groupId>de.codecentric</groupId>
       <artifactId>spring-boot-admin-starter-client</artifactId>
       <version>2.1.2</version>
    </dependency> 
    

    分别在mango-admin和mango-backup的配置文件中配置服务监控信息。主要是指定监控的服务器地址,另外endpoints是开放监控接口,因为处于安全的考虑,Spring Boot默认是没有开放健康检查接口的,可以通过endpoints设置开放特定的接口," * "表示全部开放。

    server:
      port: 8001
    spring:
      application:
        name: mango-admin
      boot:
        admin:
          client:
            url: "http://localhost:8000"
      datasource:
        name: druidDataSource
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8
          username: root
          password: admin123
          filters: stat,wall,log4j,config
          max-active: 100
          initial-size: 1
          max-wait: 60000
          min-idle: 1
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: select 'x'
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-open-prepared-statements: 50
          max-pool-prepared-statement-per-connection-size: 20
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    

    9.7 启动服务端

    编译启动MangoMonitorApplication,访问http://localhost:8000/#/applications,进入应用监控界面,如下:

    在这里插入图片描述

    10. 注册中心(Consul)

    10.1 什么是Consul

    Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案相比,Consul的方案更"一站式",内置了服务注册与发现框架、分布一致性协议实现、健康检查,Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等)。使用起来也较为简单。Consul使用Go语言编写,因此具有天然可移植性(支持Linux、Windows和Max OS X);安装包仅包含一个可执行文件,方便部署,与Docker等轻量级容器可无缝配合。

    10.2 Consul安装

    访问Consul官网,根据操作系统类型,选择下载Consul最新版本。下载下是一个zip压缩包,解压之后是一个exe可执行文件。打开CMD终端进入consul.exe所在目录,执行如下命令启动Consul服务:

    consul agent -dev
    

    启动过程信息如下:

    在这里插入图片描述

    启动成功后,访问http://localhost:8500,可以看到如下所示的Consul服务管理界面。

    在这里插入图片描述

    10.3 monitor改造

    改造mango-monitor工程,作为服务注册到注册中心。

    10.3.1 添加依赖

    在pom.xml中添加Spring Cloud和Consul注册中心依赖。

    注意:Spring Boot2.1后的版本会出现Consul服务注册上的问题,可能是因为配置变更或者支持方式改变,由于版本太新,网上也没有找到相关解决方案,所以这里把Spring Boot版本调整为2.0.4,Spring Cloud版本使用最新的稳定发布版。(解决方案参考https://blog.csdn.net/weixin_46041797/article/details/119006411)

    <!--consul-->
    		<dependency>
    		    <groupId>org.springframework.cloud</groupId>
    		    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    		</dependency>
    
    <!--srping cloud-->
    <dependencyManagement>
       <dependencies>
          <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>Finchley.RELEASE</version>
             <type>pom</type>
             <scope>import</scope>
          </dependency>
       </dependencies>
    </dependencyManagement>
    

    10.3.2 配置文件

    修改配置文件,添加服务注册配置。

    application.yml:

    server:
      port: 8000
    spring:
      application:
        name: mango-monitor
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}	# 注册到consul的服务名称
    

    10.3.3 启动类

    修改启动类,添加@EnableDiscoveryClient注解,开启服务发现支持.

    10.3.4 测试效果

    启动服务监控服务器,访问http://localhost:8500,发现服务已经成功注册到注册中心,如下:

    在这里插入图片描述

    访问http://localhost:8000,查看服务监控管理界面,看到如下所示界面就没问题了:

    在这里插入图片描述

    10.4 backup改造

    改造mango-backup工程,作为服务注册到注册中心。

    10.4.1 添加依赖

    同上。

    10.4.2 配置文件

    修改配置文件,添加服务注册配置。

    application.yml:

    # tomcat
    server:
      port: 8002
    spring:
      application:
        name: mango-backup
      boot:
        admin:
          client:
            url: "http://localhost:8000"
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
    # 开放健康检查接口
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: ALWAYS
    # backup datasource
    mango:
      backup:
        datasource:
          host: localhost
          userName: root
          password: admin123
          database: mango
    

    10.4.3 启动类

    修改启动类,添加@EnableDiscoveryClient注解,开启服务发现支持。

    10.4.4 测试效果

    测试效果同上差不多。

    10.5 admin改造

    改造mango-admin工程,作为服务注册到服务中心。

    10.5.1 添加依赖

    同上。

    10.5.2 配置文件

    修改配置文件,添加服务注册配置。

    application.yml:

    server:
      port: 8001
    spring:
      application:
        name: mango-admin
      boot:
        admin:
          client:
            url: "http://localhost:8000"
      datasource:
        name: druidDataSource
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8
          username: root
          password: admin123
          filters: stat,wall,log4j,config
          max-active: 100
          initial-size: 1
          max-wait: 60000
          min-idle: 1
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: select 'x'
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-open-prepared-statements: 50
          max-pool-prepared-statement-per-connection-size: 20
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
    # 开放健康检查接口
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: ALWAYS
    

    10.5.3 启动类

    同上。

    10.5.4 测试效果

    同上差不多。

    11. 服务消费(Ribbon、Feign)

    11.1 技术背景

    在上一章中,我们利用Consul注册中心实现了服务的注册和发现功能。在单体应用中,代码可以直接依赖,在代码中直接调用即可;但是微服务架构(分布式架构)中服务都运行在各自的进程之中,甚至部署在不同的主机和不同的地区,就需要相关的远程调用技术了。

    Spring Cloud体系里应用比较广泛的服务调用方式有两种:

    • 使用RestTemplate进行服务调用,可以通过Ribbon注解RestTemplate模板,使其拥有负载均衡的功能。
    • 使用Feign进行声明式服务调用,声明之后就像调用本地方法一样,Feign默认使用Ribbon实现负载均衡。

    两种方式都可以实现服务之间的调用,可根据情况选择使用,下面我们分别用实现案例进行详解。

    11.2 服务提供者

    11.2.1 新建项目

    新建一个mango-producer,添加以下依赖:

    • Swagger:API文档
    • Consul:注册中心
    • Spring Boot Admin:服务监控
    <dependencies>
    		<!-- web -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<!-- swagger -->
    		<dependency>
    			<groupId>io.springfox</groupId>
    			<artifactId>springfox-swagger2</artifactId>
    			<version>${swagger.version}</version>
    		</dependency>
    		<dependency>
    		    <groupId>io.springfox</groupId>
    		    <artifactId>springfox-swagger-ui</artifactId>
    		    <version>${swagger.version}</version>
    		</dependency>
            <!--spring-boot-admin-->
           	<dependency>
    		    <groupId>de.codecentric</groupId>
    		    <artifactId>spring-boot-admin-starter-client</artifactId>
    		    <version>${spring.boot.admin.version}</version>
    		</dependency>
    		<!--consul-->
    	    <dependency>
    	        <groupId>org.springframework.cloud</groupId>
    	        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    	    </dependency>
    	    <!--test-->
    	    <dependency>
    	        <groupId>org.springframework.boot</groupId>
    	        <artifactId>spring-boot-starter-test</artifactId>
    	        <scope>test</scope>
    	    </dependency>
    	</dependencies>
    

    11.2.2 配置文件

    在配置文件添加内容如下,将服务注册到注册中心并添加服务监控相关配置。

    application.yml:

    server:
      port: 8003
    spring:
      application:
        name: mango-producer
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
      boot:
        admin:
          client:
            url: "http://localhost:8000"
    # 开放健康检查接口
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: ALWAYS
    

    11.2.3 启动类

    修改启动器类,添加@EnableDiscoveryClient注解,开启服务发现支持。

    11.2.4 自定义Banner

    同之前。

    11.2.5 添加控制器

    新建一个控制器,提供一个hello接口,返回字符串信息。

    HelloController.java:

    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String hello() {
            return "hello Mango !";
        }
    }
    

    为了模拟均衡负载,复制一份上面的项目,重命名为mango-producer2,修改对应的端口为8004,修改hello方法的返回值为"hello Mango 2!"

    依次启动注册中心、服务监控和两个服务提供者,启动成功之后刷新Consul管理界面,发现我们注册的mango-producer服务以及有两个节点实例。

    访问http://localhost:8500,查看两个服务提供者已经注册到注册中心,如下:

    在这里插入图片描述

    访问http://localhost:8000,查看两个服务提供者已经成功显示在监控列表中,如下:

    在这里插入图片描述

    访问http://localhost:8003/hello,输出"hello Mango !"。

    访问http://localhost:8004/hello,输出"hello Mango 2!"。

    在这里插入图片描述
    在这里插入图片描述

    11.3 服务消费者

    11.3.1 新建项目

    新建一个项目mango-consumer,添加以下依赖:

    • Swagger
    • Consul
    • Spring Boot Admin

    代码同上

    11.3.2 添加配置

    修改配置如下:

    server:
      port: 8005
    spring:
      application:
        name: mango-consumer
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
      boot:
        admin:
          client:
            url: "http://localhost:8000"
    # 开放健康检查接口
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: ALWAYS
    

    11.3.3 启动类、

    修改启动类,添加@EnableDiscoveryClient注解,开启服务发现支持

    11.3.4 自定义Banner

    同上。

    11.3.5 服务消费

    添加消费服务测试类,添加两个接口,一个查询我们注册的服务,另一个从我们注册的服务中选取一个服务,采用轮询的方式。

    ServiceController.java:

    @RestController
    public class ServiceController {
    
        @Autowired
        private LoadBalancerClient loadBalancerClient;
        @Autowired
        private DiscoveryClient discoveryClient;
    
       /**
         * 获取所有服务
         */
        @RequestMapping("/services")
        public Object services() {
            return discoveryClient.getInstances("mango-producer");
        }
    
        /**
         * 从所有服务中选择一个服务(轮询)
         */
        @RequestMapping("/discover")
        public Object discover() {
            return loadBalancerClient.choose("mango-producer").getUri().toString();
        }
    }
    

    添加完成之后启动项目,访问http://localhost:8500/,服务消费者已经成功注册到注册中心:

    在这里插入图片描述

    访问http://localhost:8000/,服务消费者已经成功显示到监控列表中,如下:

    在这里插入图片描述

    反复访问http://localhost:8005/discover,结果交替返回服务8003和8004,因为默认负载均衡器采用轮询方式,如下所示。8003和8004两个服务交替出现,从而实现了获取服务端地址的均衡负载:

    在这里插入图片描述
    在这里插入图片描述

    大多数情况下我们希望使用均衡负载的形式去获取服务端提供的服务,因此使用第二种方法来模拟调用服务端提供的hello方法,创建一个控制器CallHelloController。

    CallHelloController.java:

    @RestController
    public class CallHelloController {
    
        @Autowired
        private LoadBalancerClient loadBalancer;
    
        @RequestMapping("/call")
        public String call() {
            ServiceInstance serviceInstance = loadBalancer.choose("mango-producer");
            System.out.println("服务地址:" + serviceInstance.getUri());
            System.out.println("服务名称:" + serviceInstance.getServiceId());
    
            String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);
            System.out.println(callServiceResult);
            return callServiceResult;
        }
    
    }
    

    使用RestTemplate进行远程调用。添加完之后重启项目。在浏览器访问http://localhost:8005/call。依次往复返回的结果如下所示:

    在这里插入图片描述
    在这里插入图片描述

    11.3.6 负载均衡器(Ribbon)

    上面教程中,我们是这样调用服务的,先通过LoadBalancerClient选取出对应的服务,然后使用RestTemplate进行远程调用。

    LoadBalancerClient就是负载均衡器,RibbonLoadBalancerClient是Ribbon默认使用的负载均衡器,采用的负载均衡策略是轮询。

    1. 查找服务,通过LoadBalancer查询服务

      ServiceInstance serviceInstance = loadBalancer.choose("mango-producer");
      
    2. 调用服务,通过RestTemplate远程调用服务

      String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);
      

    这样就完成了一个简单的服务调用和负载均衡。接下来说说Ribbon。

    Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法自动帮助服务消费者去请求。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。当然我们也可为Ribbon实现自定义的负载均衡算法。

    Ribbon内置负载均衡策略可参考https://blog.csdn.net/weixin_30408309/article/details/94870464

    11.3.7 修改启动类

    我们修改一个启动器类,注入RestTemplate,并添加@LoadBalanced注解(用于拦截请求),以使用ribbon来进行负载均衡。

    @EnableFeignClients 
    @EnableDiscoveryClient
    @SpringBootApplication
    public class MangoConsumerApplication {
       
        public static void main(String[] args) {
            SpringApplication.run(MangoConsumerApplication.class, args);
        }
        
        @Bean
        @LoadBalanced
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    

    11.3.8 添加服务

    新建一个控制器类注入RestTemplate并调用服务提供者的hello服务。

    @RestController
    public class RibbonHelloController {
    
        @Autowired
        private RestTemplate restTemplate;
        
        @RequestMapping("/ribbon/call")
        public String call() {
            // 调用服务, service-producer为注册的服务名称,LoadBalancerInterceptor会拦截调用并根据服务名找到对应的服务
            String callServiceResult = restTemplate.getForObject("http://mango-producer/hello", String.class);
            return callServiceResult;
        }
    }
    

    11.3.9 页面测试

    启动消费者服务,访问http://localhost:8005/ribbon/call,依次返回的结果同11.3.6.

    说明ribbon的负载均衡已经成功启动了。

    11.3.10 负载策略

    修改负载均衡策略很简单,只需要在配置文件指定对应的负载均衡器即可。

    11.4 服务消费(Feign)

    Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端,使编写Web服务客户端变得更加简单。我们只需要通过创建接口并用注解来配置它即可完成对Web服务接口的绑定。它具有可插拔的注解支持,包括Feign注解、JAX-RS注解。它也支持可插拔的编码器和解码器。Spring Cloud Feign还扩展了对Spring MVC注解的支持,同时还整合了Ribbon来提供负载均衡的HTTP客户端实现。

    11.4.1 添加依赖

    修改mango-consumer的pom文件,添加feign依赖。

    <!--feign -->
    <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    

    11.4.2 启动类

    修改启动器类,添加@EnableFeignClients 注解,开启扫描Spring Cloud Feign客户端的功能。

    11.4.3 添加Feign接口

    添加MangoProducerService接口,在类头添加注解@FeignClient(name = “mango-producer”),mango-producer是调用的服务名。

    @FeignClient(name = "mango-producer")
    public interface MangoProducerService {
    
        @RequestMapping("/hello")
        public String hello();
        
    }
    

    11.4.4 添加控制器

    添加FeignHelloController控制器,注入MangoProducerService,就可以像使用本地方法一样进行调用了。

    @RestController
    public class FeignHelloController {
    
        @Autowired
        private MangoProducerService mangoProducerService;
        
        @RequestMapping("/feign/call")
        public String call() {
            // 像调用本地服务一样
            return mangoProducerService.hello();
        }
        
    }
    

    11.4.5 页面测试

    启动成功之后访问http://localhost:8005/feign/call,发现调用成功,且依次往复返回的结果同上。

    Feign是声明式调用,会产生一些相关的Feign定义接口,所以建议将Feign定义的接口都统一放置管理,以区别内部服务。

    12. 服务熔断(Hystrix、Turbine)

    12.1 雪崩效应

    在微服务架构中,服务众多,通常会涉及多个服务层级的调用,一旦基础服务发生故障,很可能会导致级联故障,进而造成整个系统不可用,这种现象被称为服务雪崩效应。服务雪崩效应是一种因"服务提供者"的不可用导致"服务消费者"的不可用并将这种不可用逐渐放大的过程。

    比如在一个系统中,A是服务提供者,B是A的服务消费者,C和D又是B的服务消费者。如果此时A发生故障,则会引起B的不可用,而B的不可用又将导致C和D的不可用,当这种不可用像滚雪球一样逐渐放大的时候,雪崩效应就形成了。(感觉和数据库的多米诺效应一个道理)

    12.2 熔断器(CircuitBreaker)

    熔断器的原理非常简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,就会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。熔断器模式就像是那些容易导致错误操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。熔断器是保护服务高可用的最后一道防线。

    12.3 Hystrix特性

    12.3.1 断路器机制%

    断路器很好理解,当Hystrix Command请求后端服务失败数量超过一定比例(默认为50%)。断路器会切换到开路状态(open)。这时所有请求会直接失败而不会发送到后端服务。断路器保持在开路状态一段时间后(默认为5s),自动切换到半开路状态(HALF-OPEN).这时会判断下一次请求的返回情况,如果请求成功,断路器切回闭路状态(CLOSED),否则重新切换为开路状态(OPEN)。Hystrix的断路器就像我们家庭电路中的保险丝,一旦后端服务不可用,断路器就会直接切断请求链,避免发送大量无效请求,从而影响系统吞吐量,并且断路器有自我检测并恢复的能力。

    12.3.2 fallback

    fallback相当于降级操作。对于查询操作,我们可以实现一个fallback方法,当请求后端服务出现异常的时候,可以使用fallback方法返回的值。fallback返回的值一般是设置的默认值或来自缓存。

    12.3.3 资源隔离

    在Hystrix中,主要通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如,调用产品服务的Command放入A线程池,调用账户服务的Command放入B线程池。这样做的优点是运行环境被隔离开了。这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽,也不会对系统的其他服务造成影响,但是带来的代价是维护多个线程池会对系统带来额外的性能开销。如果是对性能有严格要求且确信自己调用服务的客户端代码不会出问题,就可以使用Hystrix的信号模式(Semaphores)来隔离资源。

    12.4 Feign Hystrix

    因为Feign中已经依赖了Hystrix,所以在maven配置上不用做任何改动就可以使用了,我们可以在mango-consumer项目中直接改造。

    12.4.1 修改配置

    在配置文件中添加配置,开启Hystrix熔断器。

    application.yml:

    #开启熔断器
    feign:
      hystrix:
        enabled: true
    

    12.4.2 创建回调类

    创建一个回调类MangoProducerHystrix,实现MangoProducerService接口,并实现对应的方法,返回调用失败后的信息。

    MangoProducerHystrix.java:

    @Component
    public class MangoProducerHystrix implements MangoProducerService {
    
        @RequestMapping("/hello")
        public String hello() {
           return "sorry, hello service call failed.";
        }
    }
    

    添加fallback属性。修改MangoProducerService,在@FeignClient注解中加入fallback属性,绑定我们创建的失败回调处理类。

    MangoProducerService.java:

    @FeignClient(name = "mango-producer", fallback = MangoProducerHystrix.class)
    public interface MangoProducerService {
    
        @RequestMapping("/hello")
        public String hello();
        
    }
    

    到此,所有改动代码就完成了。

    12.4.3 页面测试

    启动成功之后,多次访问http://localhost:8005/feign/call,结果如同之前一样交替返回"hello mango!“和"hello mango 2!”,说明熔断器的启动不会影响正常服务访问。

    把mango-producer服务停掉,再次访问,返回我们提供的熔断回调信息,熔断成功,mango-producer2服务正常.

    在这里插入图片描述
    在这里插入图片描述

    重启mango-producer服务,再次访问发现服务又可以访问了,说明熔断器有自我诊断修复的功能。

    注:在重启成功之后可能需要一些时间,等待熔断器进行自我诊断和修复完成之后,方可正常提供服务。

    12.5 Hystrix Dashboard

    Hystrix-Dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix Dashboard我们可以直观地看到各Hystrix Command的请求响应时间、请求成功率等数据。

    12.5.1 添加依赖

    新建一个mango-hystrix工程,修改pom文件,添加相关依赖。

    pom.xml:

     <!-- spring boot -->
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
       </dependency>
       <!--consul-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!--actuator-->
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-actuator</artifactId>
       </dependency>
       <!--spring-boot-admin-->
           <dependency>
           <groupId>de.codecentric</groupId>
           <artifactId>spring-boot-admin-starter-client</artifactId>
           <version>${spring.boot.admin.version}</version>
       </dependency>
       <!--hystrix-dashboard-->
       <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
       </dependency>
    

    Spring Cloud依赖:

    <!--srping cloud-->
    <dependencyManagement>
       <dependencies>
          <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>${spring-cloud.version}</version>
             <type>pom</type>
             <scope>import</scope>
          </dependency>
       </dependencies>
    </dependencyManagement>
    

    12.5.2 启动类

    在启动类中添加注解@EnableHystrixDashboard,开启熔断监控支持。

    12.5.3 自定义Banner

    同上。

    12.5.4 配置文件

    修改配置文件,把服务注册到注册中心。

    application.yml:

    server:
      port: 8501
    spring:
      application:
        name: mango-hystrix
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
    turbine:
      instanceUrlSuffix: hystrix.stream    # 指定收集路径
      appConfig: kitty-consumer    # 指定了需要收集监控信息的服务名,多个以“,”进行区分
      clusterNameExpression: "'default'"    # 指定集群名称,若为default则为默认集群,多个集群则通过此配置区分
      combine-host-port: true    # 此配置默认为false,则服务是以host进行区分,若设置为true则以host+port进行区分
    

    12.5.5 配置监控路径

    注意,如果使用的是2.x等版本,就需要在Hystrix的消费端配置监控路径。打开消费端mango-consumer工程添加依赖。

    pom.xml:

    <!--actuator-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--hystrix-dashboard-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
    

    修改启动类,添加服务监控路径配置。

    MangoConsumerApplication.java:

    // 此配置是为了服务监控而配置,与服务容错本身无关,
    // ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
    // 只要在自己的项目里配置上下面的servlet就可以了
    @Bean
    public ServletRegistrationBean getServlet() {
       HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
       ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
       registrationBean.setLoadOnStartup(1);
       registrationBean.addUrlMappings("/hystrix.stream");
       registrationBean.setName("HystrixMetricsStreamServlet");
       return registrationBean;
    }
    

    12.5.6 页面测试

    先后启动monitor、producer、consumer、hystrix等服务,访问http://localhost:8501/hystrix会看到如下界面:

    在这里插入图片描述

    此时没有任何具体的监控信息,需要输入要监控的消费者地址及监控信息的轮询时间和标题。

    Hystrix Dashboard共支持以下三种不同的监控方式:

    • 单体Hystrix消费者:通过URL http://hystrix-app:port/hystrix.stream开启,实现对具体某个服务实例的监控。
    • 默认集群监控:通过URL http://turbine-hostname:port/turbine.stream开启,实现对默认集群的监控。
    • 自定集群监控:通过URL http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。

    这里先介绍对单体Hystrix消费者的监控,后面整个Turbine集群的时候再说明后两种监控方式。

    首先访问http://localhost:8005/feign/call,查看要监控的服务是否可以正常访问。

    确认服务可以正常访问之后,在监控地址内输入http://localhost:8005/hystrix.stream,然后单击Monitor Stream开始监控,如下所示:

    在这里插入图片描述

    刚进去页面先显示loading…信息,在多次间断访问http://localhost:8005/hystrix.stream之后统计图表信息如下:

    在这里插入图片描述

    12.6 Spring Cloud Turbine

    上面我们集成了Hystrix Dashboard,使用Hystrix Dashboard可以看到单个应用内的服务信息。显然这是不够的,我们还需要一个工具能让我们汇总系统内多个服务的数据并显示到Hystrix Dashboard上,这个工具就是Turbine。

    12.6.1 添加依赖

    修改mango-hystrix的pom文件,添加turbine依赖包(因为我们使用的注册中心是consul,所以需要排除默认的euraka包,不然会出现冲突,导致启动过程出错)

    pom.xml:

    <!--turbine-->
    <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
       <exclusions>  
           <exclusion>     
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
           </exclusion>  
       </exclusions> 
    </dependency>
    

    12.6.2 启动类

    为启动类添加@EnableTurbine注解,开启Turbine支持。

    12.6.3 配置文件

    修改配置文件,添加Turbine的配置信息。

    application.yml:

    server:
      port: 8501
    spring:
      application:
        name: mango-hystrix
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
    turbine:
      instanceUrlSuffix: hystrix.stream    # 指定收集路径
      appConfig: mango-consumer    # 指定了需要收集监控信息的服务名,多个以“,”进行区分
      clusterNameExpression: "'default'"    # 指定集群名称,若为default则为默认集群,多个集群则通过此配置区分
      combine-host-port: true    # 此配置默认为false,则服务是以host进行区分,若设置为true则以host+port进行区分
    

    12.6.4 测试效果

    依次启动monitor、producer、consumer、hystrix等服务,确认服务启动无误后访问http://localhost:8501/hystrix/,输入http://localhost:8501/turbine.stream,查看熔断监控图表信息,下图就是利用Turbine集合多个Hystrix消费者的熔断监控信息结果:

    在这里插入图片描述

    13. 服务网关(Zuul)

    13.1 技术背景

    前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢?

    因为是REST API接口,所以外部客户端直接调用各个微服务是没有问题的,但是出于种种原因,这并不是一个好的选择。

    让客户端直接与各个微服务通信,会有以下几个问题:

    • 客户端会多次请求不同的微服务,增加客户端的复杂性。
    • 存在跨域请求,在一定场景下处理会变得相对比较复杂。
    • 实现认证复杂,每个微服务都需要独立认证。
    • 难以重构,项目迭代可能导致微服务重新划分。如果客户端直接与微服务通信,那么重构将会很难实施。
    • 如果某些微服务使用的防火墙/浏览器不友好的协议,直接访问会有一定困难。
    • 面对类似上面的问题,解决方案就是服务网关。

    使用服务网关有以下几个优点:

    • 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
    • 易于认证。可在服务网关上进行认证,然后转发请求到微服务,无须在每个微服务中进行认证。
    • 客户端只和服务网关打交道,减少了客户端与各个微服务之间的交互次数。
    • 多渠道支持。可根据不同客户端(Web端、移动端、桌面端)提供不同的API服务网关。

    13.2 Spring Cloud Zuul

    服务网关是微服务架构中一个不可或缺的部分。在通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。

    Spring Cloud Netflix中的Zuul就担任了这一角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主题能够具备更高的可复用性和可测试性。

    在Spring Cloud体系中,Spring Cloud Zuul封装了Zuul组件,作为一个API网关,负责提供负载均衡、反向代理和权限认证。

    13.3 Zuul工作机制

    13.3.1 过滤器机制

    Zuul的核心是一系列的filters,其作用类似Servlet框架中的Filter,Zuul把客户端请求路由到业务处理逻辑的过程中,这些filter在路由的特定时期参与了一些过滤处理,比如实现鉴权、流量转发、请求统计等功能。Zuul的整个运行机制如下:

    在这里插入图片描述

    13.3.2 过滤器的生命周期

    Filter的生命周期有四个,分别是"PRE" “ROUTING” “POST” “ERROR”,整个声明周期可以用下图形容:

    在这里插入图片描述

    基于Zuul的这些过滤器可以实现各种丰富的功能,而这些过滤器类型则对应于请求的典型生命周期。

    • PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
    • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netflix Ribbon请求微服务。
    • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
    • ERROR:在其他阶段发送错误时执行该过滤器。

    除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

    Zuul默认实现了很多的Filter,如下表:

    类型顺序过滤器功能
    pre-3ServletDetectionFilter标记处理Servlet的类型
    pre-2Servlet30WrapperFilter包装HttpServletRequest请求
    pre-1FormBodyWrapperFilter包装请求体
    route1DebugFilter标记调试标志
    route5PreDecorationFilter处理请求上下文供后续使用
    route10RibbonRoutingFilterserviceId请求转发
    route100SimpleHostRoutingFilterurl请求转发
    route500SendForwardFilterforward请求转发
    post0SendErrorFilter处理有错误的请求响应
    post1000SendResponseFilter处理正常的请求响应

    13.3.3 禁用指定的Filter

    可以在application.yml中配置需要禁用的filter,格式为 zuul.< SimpleClassName >.< filterType >.disable=true。比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,进行如下设置即可:

    zuul:
    	SendResponseFilter:
    		post:
    			disable: true
    

    自定义filter。实现自定义过滤器需要继承ZuulFilter,并实现ZuulFilter中的抽象方法:

    public class MyFilter extends ZuulFilter {
    
        @Override
        public String filterType() {
            return "pre"; // 定义filter的类型,有pre、route、post、error四种
        }
    
        @Override
        public int filterOrder() {
            return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
        }
    
        @Override
        public boolean shouldFilter() {
            return true;	 // 表示是否需要执行该filter,true表示执行,false表示不执行
        }
    
        @Override
        public Object run() throws ZuulException {
            return null;	//filter需要执行的具体操作
        }
    }
    

    13.4 实现案例

    13.4.1 新建工程

    新建一个项目mango-zuul作为服务网关

    13.4.2 添加依赖

    添加consul、zuul相关依赖。

    pom.xml:

    <dependencies>
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
           </dependency>
           <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
    </dependencies>
    
    <dependencyManagement>
       <dependencies>
          <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>${spring-cloud.version}</version>
             <type>pom</type>
             <scope>import</scope>
          </dependency>
       </dependencies>
    </dependencyManagement>
    

    13.4.3 启动类

    为启动类添加@EnableZuulProxy注解,开启服务网关支持。

    13.4.4 配置文件

    配置启动端口为8010,注册服务到注册中心,配置Zuul转发规则。这里配置在返回http://localhost:8010/feign/callhttp://localhost:8010/ribbon/call时调用消费者相关接口。

    application.yml:

    server:
      port: 8010
    spring:
      application:
        name: mango-zuul
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            serviceName: ${spring.application.name}    # 注册到consul的服务名称
    zuul:
      routes:
        ribbon:
          path: /ribbon/**
          serviceId: mango-consumer  # 转发到消费者 /ribbon/
        feign:
          path: /feign/**
          serviceId: mango-consumer  # 转发到消费者 /feign/
    

    13.4.5 页面测试

    依次启动注册中心、监控、服务提供者、服务消费者、服务网关等项目。

    访问http://localhost:8010/feign/callhttp://localhost:8010/ribbon/call,效果如下:

    在这里插入图片描述
    在这里插入图片描述

    说明Zuul已经成功转发请求,并成功调用后端服务。

    13.4.6 配置接口前缀

    如果想给每个服务的API接口加上一个前缀,可使用zuul.prefix进行配置。例如http://localhost:8010/v1/feign/call,即在所有的API接口上加一个v1作为版本号。

    zuul:
      prefix: /v1
      routes:
        ribbon:
          path: /ribbon/**
          serviceId: mango-consumer  # 转发到消费者 /ribbon/
        feign:
          path: /feign/**
          serviceId: mango-consumer  # 转发到消费者 /feign/
    

    13.4.7 默认路由规则

    上面我们通过添加路由配置进行请求转发。

    但是如果后端服务非常多,每一个都这样配置挺麻烦的。Spring Cloud Zuul已经帮我们做了默认配置。默认情况下,Zuul会代理所有注册到注册中心的微服务,并且Zuul的默认路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务注册中心的serviceId/**会被转发到serviceId对应的微服务。如果遵循默认路由规则,基本上就没什么配置了。

    比如我们直接通过serviceId/feign/call的方式访问也是可以正常访问的。访问http://localhost:8010/mango-consumer/feign/call,结果也是一样的。

    13.4.8 路由熔断

    Zuul作为Netflix的组件,可以与Ribbon、Eureka和Hystrix等组件相结合,实现负载均衡、熔断器的功能。默认情况下Zuul和Ribbon相结合,实现了负载均衡。实现熔断器功能需求实现FallbackProvider接口。实现该接口有两个方法,一个是getRoute(),用于指定熔断器功能应用于哪些路由的服务;另一个方法是fallbackResponse(),为进入熔断器功能时执行的逻辑。

    创建MyFallbackProvider类,getRoute()方法返回"mango-consumer",只针对consumer服务进行熔断。如果需要所有的路由服务都加熔断功能,需要在getRoute()上返回"*“匹配符。getBody()方法返回发送熔断时的反馈信息,这里在发送熔断时返回信息"Sorry, the service is unavailable now.”。

    @Component
    public class MyFallbackProvider implements FallbackProvider {
        @Override
        public String getRoute() {
            return "mango-consumer";
        }
    
        @Override
        public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
            System.out.println("route:"+route);
            System.out.println("exception:"+cause.getMessage());
            return new ClientHttpResponse() {
                @Override
                public HttpStatus getStatusCode() throws IOException {
                    return HttpStatus.OK;
                }
    
                @Override
                public int getRawStatusCode() throws IOException {
                    return 200;
                }
    
                @Override
                public String getStatusText() throws IOException {
                    return "ok";
                }
    
                @Override
                public void close() {
    
                }
    
                @Override
                public InputStream getBody() throws IOException {
                    return new ByteArrayInputStream("Sorry, the service is unavailable now.".getBytes());
                }
    
                @Override
                public HttpHeaders getHeaders() {
                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON);
                    return headers;
                }
            };
        }
    }
    

    重新启动,访问http://localhost:8010/mango-consumer/feign/call,可以正常访问。停掉mango-consumer服务,f昂问http://localhost:8010/mango-consumer/feign/call,返回效果如下:

    在这里插入图片描述

    结果返回了我们自定义的信息,说明我们自定义的熔断器起作用了。

    13.4.9 自定义Filter

    创建一个MyFilter,继承ZuulFilter类,覆写run()逻辑,在转发请求之前进行token认证,如果请求没有携带token,返回"there is no request token"提示。

    MyFilter.java:

    @Component
    public class MyFilter extends ZuulFilter {
    
        private static Logger log=LoggerFactory.getLogger(MyFilter.class);
    
        @Override
        public String filterType() {
            return "pre"; // 定义filter的类型,有pre、route、post、error四种
        }
    
        @Override
        public int filterOrder() {
            return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
        }
    
        @Override
        public boolean shouldFilter() {
            return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
        }
    
        @Override
        public Object run() throws ZuulException {
            // filter需要执行的具体操作
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String token = request.getParameter("token");
            System.out.println(token);
            if(token==null){
                log.warn("there is no request token");
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(401);
                try {
                    ctx.getResponse().getWriter().write("there is no request token");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            log.info("ok");
            return null;
        }
    }
    

    这样,Zuul就会自动加载Filter执行过滤了。重新启动Zuul项目,访问http://localhost:8010/mango-consumer/feign/call,结果如下所示:

    在这里插入图片描述

    请求时带上token,访问http://localhost:8010/mango-consumer/feign/call?token=115,接口返回正确结果,如下所示:

    在这里插入图片描述

    Zuul作为API服务网关,不同的客户端使用不同的负载将请求统一分发到后端的Zuul,再由Zuul转发到后端服务。为了保证Zuul的高可用性,前端可以同时开启多个Zuul实例进行负载均衡。另外,在Zuul的前端还可以使用Nginx或者F5再进进行负载转发,从而保证Zuul的高可用性。

    14. 链路追踪(Sleuth、Zipkin)

    14.1 技术背景

    在微服务架构中,随着业务发展,系统拆分导致系统调用链路愈发复杂,一个看似简单的前端请求可能最终需要调用很多次后端服务才能完成,那么当整个请求出现问题时,我们很难得知到底是哪个服务出了问题导致的,这时就需要解决一个问题,即如何快速定位服务故障点,分布式系统调用链追踪技术就此诞生了。

    14.2 ZipKin

    ZipKin是一个由Twitter公司提供并开放源代码分布式的跟踪系统。,它可以帮助收集服务的时间数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。

    每个服务向ZipKin报告定时数据,ZipKin会根据调用关系通过ZipKin UI生成依赖关系图,展示多少追踪请求经过了哪些服务,该系统让开发者可通过一个Web前端轻轻松松收集和分析数据,例如用户每次请求服务的处理时间等,可非常方便地监测系统中存在的瓶颈。

    ZipKin提供了可插拔数据存储方式:In-Memory、MySQL、Cassandra、以及Elasticsearch。我们可以根据需求选择不同的存储方式,生成环境一般都需要持久化。我们这里采用Elasticsearch作为ZipKin的数据存储器。

    14.3 Spring Cloud Sleuth

    一般而言,一个分布式服务追踪系统,主要由三部分组成:数据收集、数据存储和数据展示。

    Spring Cloud Sleuth为服务之间的调用提供链路追踪,通过Sleuth可以很清楚地了解到一个服务请求经过了哪些服务,每个服务处理花费了多少时间,从而让我们可以很方便地理清各微服务之间的调用关系,此外,Sleuth还可以帮助我们:

    • 耗时分析:通过Sleuth可以很方便地了解到每个采样请求的耗时,从而分析出哪些服务调用比较耗时。
    • 可视化错误:对于程序未捕捉的异常,可以通过集成ZipKin服务在界面上看到。
    • 链路优化:对于调用比较频繁的服务,可以针对这些服务实施一些优化措施。

    Spring Cloud Sleuth可以结合ZipKin,将信息发送到ZipKin,利用ZipKin的存储来存储信息,利用ZipKin UI来展示数据。

    14.4 实现案例

    在早前的Spring Cloud版本里是需要自建ZipKin服务端的,但是从Spring Cloud 2.0以后,官方已经不支持自建Server了,改成提供编译好的jar包供用户使用。这里我们使用Docker方式部署ZipKin服务,并采用Elasticsearch作为ZipKin的数据存储器。

    14.4.1 下载镜像

    此前先安装好Dockers环境(安装环境用了小半天,一直出问题一直出问题)。

    (我不李姐,为什么,现在看好像就这几步就行的事,我怎么搞了那么久,甚至创建了虚拟机想在Linux系统上试试,结果试试就逝世,白浪费了时间。都是题外话了)

    使用以下命令分别拉取ZipKin和Elasticsearch镜像。

    docker pull openzipkin/zipkin
    docker pull docker.elastic.co/elasticsearch/elasticsearch:6.3.0
    

    拉取镜像的过程及其十分非常缓慢,甚至还可能报错,再下载一次应该可能大概就行吧。

    在这里插入图片描述

    通过docker images查看下载镜像,如下所示:

    在这里插入图片描述

    14.4.2 编写启动文件

    到这里就失败了,多次尝试未果,遂放弃。

    14参考

    对于链路追踪参考https://blog.csdn.net/qq_40587263/article/details/117338097

    经测试发现可行。

    15. 配置中心(Config、Bus)

    15.1 技术背景

    如今微服务架构盛行,在分布式系统中,项目日益庞大,子项目日益增多,每个项目都散落着各种配置文件,且随着服务的增加而不断增多。此时,往往某一个基础服务信息变更都会导致一系列服务的更新与重启,运维也是苦不堪言,而且很容易出错。配置中心便由此应运而生。

    目前市面上开源的配置中心很多,像Spring家族的Spring Cloud Config、Apache的Apache Commons Configuration、淘宝的diamond、百度的disconf、360的QConf等,都是为了解决这类问题。

    15.2 Spring Cloud Config

    Spring Cloud Config 是一套为分布式系统中的基础设施和微服务应用提供集中化配置的管理方案,分为服务端和客户端两个部分。服务端也称为分布式配置中心,是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息。客户端是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理服务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。

    Spring Cloud Config对服务端和客户端中的环境变量和属性配置实现了抽象映射,所以除了适用于Spring应用,也是可以在任何其他语言应用中使用的。Spring Cloud Config实现的配置中心默认采用Git来存储配置信息,所以使用Spring Cloud Config构建的配置服务器天然就支持对微服务应用配置信息的版本管理,并且可以通过Git客户端工具非常方便地管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如SVN仓库、本地化文件系统等。

    15.3 实现案例

    15.3.1 准备配置文件

    首先在Git下新建一个config-repo目录,用来存放配置文件,如下所示,这里分别模拟了三个环境的配置文件,分别编辑三个文件,配置hello属性的值为"consumer.hello=hello, xx configurations"。

    在这里插入图片描述
    在这里插入图片描述

    15.3.2 服务端实现

    1. 新建工程

      新建mango-config工程,作为配置中心的服务端,负责把git仓库的配置文件发布为RESTFul接口。

    2. 添加依赖

      除了Spring Cloud依赖外,另需添加配置中心依赖包。

      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-config-server</artifactId>
      </dependency>
      
    3. 启动类

      启动类添加注解@EnableConfigServer。

    4. 配置文件

      修改配置文件,添加如下内容。如果是私有仓库,需要填写用户名、密码,如果是公有仓库可以不配置密码。

      server:
        port: 8020
      spring:
        application:
          name: mango-config
        cloud:
          consul:
            host: localhost
            port: 8500
            discovery:
              serviceName: ${spring.application.name}    # 注册到consul的服务名称
          config:
            label: master  # git仓库分支
            server:
              git:
                uri: https://gitee.com/lhtyw/config-repo.git  # 配置git仓库的地址
                search-paths: src/config-repo  # git仓库地址下的相对地址,可以配置多个,用,分割。
      management:
        endpoints:
          web:
            exposure:
              include: "*"
      

      Spring Cloud Config也提供本地存储配置方式,只需设置属性spring.profiles.active=native,Config Server会默认从应用的src/main/resource目录下检索配置文件。另外也可以通过spring.cloud.config.server.native.searchLocations=file:D:/properties/属性来指定配置文件的位置。虽然Spring Cloud Config提供了这样的功能,但是为了更好的支持内容管理和版本控制,还是推荐使用GIT的方式。

    5. 页面测试

      启动注册中心,配置中心服务,访问http://localhost:8020/consumer/dev,返回dev配置文件的信息:

    在这里插入图片描述

    访问http://localhost:8020/consumer/pro,返回pro配置文件的信息:

    在这里插入图片描述

    上述的返回信息包含了配置文件的位置、版本、配置文件的名称一以及配置文件的具体内容,说明server端已经成功获取了GIT仓库的配置信息。

    访问http://localhost:8020/consumer-dev.properties返回结果如下所示:

    在这里插入图片描述

    将dev配置文件中的内容修改为"hello=hello, dev configurations2."重新访问http://localhost:8020/consumer-dev.properties,发现读取的是修改后提交的东西,说明服务端是会自动读取最新提交的数据。

    仓库中的配置文件会被转换成相应的Web接口,访问可参照以下规则:

    • /{application}/{profile}[/{label}]
    • /{application}-{profile}.yml
    • /{label}/{application}-{profile}.yml
    • /{application}-{profile}.properties
    • /{label}/{application}-{profile}.properties

    以consumer-dev.properties为例,它的application是consumer、profile是dev。客户端会根据填写的参数来选择读取对应的配置。

    15.3.3 客户端实现

    1. 添加依赖

      打开mango-consumer工程,添加相关依赖。

      <!-- spring-cloud-config -->
      <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-config</artifactId>
      </dependency>
      
    2. 配置文件

      添加一个叫bookstrap.yml的配置文件,添加配置中心,并把注册中心的配置移到这里,因为在通过配置中心查找配置时需要通过注册中心的发现服务。

      bookstrap.yml:

      spring:
        cloud:
          consul:
            host: localhost
            port: 8500
            discovery:
              serviceName: ${spring.application.name}    # 注册到consul的服务名称
          config:
            discovery:
              enabled: true  # 开启服务发现
              serviceId: mango-config # 配置中心服务名称
            name: consumer  # 对应{application}部分
            profile: dev  # 对应{profile}部分
            label: master  # 对应git的分支,如果配置中心使用的是本地存储,则该参数无用
      

      配置说明:

      • spring.cloud.config.uri:配置中心的具体地址。
      • spring.cloud.config.name:对应{application}部分。
      • spring.cloud.config.profile:对应{profile}部分。
      • spring.cloud.config.label:对应git的分支。如果配置中心使用的是本地存储,则该参数无用。
      • spring.cloud.config.discovery.serviceId:指定配置中心的service-id,便于扩展为高可用的配置集群。

      (上面这些与spring cloud有关的属性必须配置在bookstrap.yml中,这样config部分内容才能被正确加载,因为config的相关配置会先于application.yml,而bookstrap.yml的加载也是先于application.yml的)

      application.yml:

      server:
        port: 8005
      spring:
        application:
          name: mango-consumer
        boot:
          admin:
            client:
              instance:
                instance.service-base-url: ${spring.application.name}
        zipkin:
          base-url: http://localhost:9411/
        sleuth:
          sampler:
            probability: 1 #样本采集量,默认为0.1,为了测试这里修改为1,正式环境一般使用默认值
      # 开放健康检查接口
      management:
        endpoints:
          web:
            exposure:
              include: "*"
        endpoint:
          health:
            show-details: ALWAYS
      #开启熔断器
      feign:
        hystrix:
          enabled: true
      
    3. 控制器

      添加一个SpringConfigController控制器,添加注解@Value("${hello}"),声明hello属性从配置文件中读取。

      @RestController
      class SpringConfigController {
          
          @Value("${hello}")
          private String hello;
      
          @RequestMapping("/hello")
          public String from() {
              return this.hello;
          }
      }
      
    4. 页面测试

      启动注册中心、配置中心和服务消费者,访问http://localhost:8005/hello,返回结果如下图:

    在这里插入图片描述

    说明客户端已经成功从服务端获取了配置信息。

    手动修改一下仓库配置文件的内容,修改完成并提交。再次访问http://localhost:8005/hello发现返回结果并没有读取最新提交的内容,这是因为springboot项目只有在启动的时候才会获取配置文件的内容,虽然GIT配置信息被修改了,但是客户端并未重新去获取,所以导致读取的信息仍然是旧配置。

    15.3.4 Refresh机制

    Refresh机制是Spring Cloud Config提供的一种刷新机制,它允许客户端通过POST方法触发各自的/refresh,只要依赖spring-boot-starter-actuator包就拥有了/refresh的功能。下面我们为客户端添加刷新功能,以支持更新配置的获取。

    1. 添加依赖

      添加actuator依赖。actuator是健康检查依赖包,依赖包里携带了/refresh功能。

      <!--actuator-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      

      在使用配置属性的类型中加上@RefreshScopezh蟹,这样在客户端执行/refresh的时候就会刷新此类下面的配置属性了。

    2. 修改配置

      健康检查接口开放需要在配置文件添加如下内容,开放Refresh的相关接口,因为之前已经配置过,所以无需添加。

      # 开放健康检查接口
      management:
        endpoints:
          web:
            exposure:
              include: "*"
        endpoint:
          health:
            show-details: ALWAYS
      

      通过上面的接口开放配置,以后以post请求的方式访问http://localhost:8005/actuator/refresh时就会更新修改后的配置文件了。

      这里存在版本大坑,1.x和2.x的配置不太一样,我们用的是2.0+版本,务必注意:
      	安全配置变更:新版本为management.endpoints.web.exposure.include="*"
      	访问地址变更:新版本为http://localhost:8005/actuator/refresh,老版本为http://localhost:8005/refresh
      

      这里解释一下上面这个配置起到什么作用。actuator是一个健康检查包,提供了一些健康检查数据接口。Refresh功能是其中的一个接口,为了安全起见,默认只开放了health和info接口.而上面的配置就是设置要开放哪些接口,我们设置成"*"是开放所有接口。也可以指定开放几个,比如health、info、refresh,而这里因为我们需要用的是Refresh功能,所以需要把Refresh接口开放出来。

    3. 页面测试

      重新启动服务,访问http://localhost:8005/hello,返回结果同上。

      修改仓库配置内容再次访问结果也没有更新。因为我们没有调用refresh方法,通过工具或自写代码发生post请求http://localhost:8005/actuator/refresh,刷新配置。
      在这里插入图片描述

      刷新后再次访问http://localhost:8005/hello返回结果更新:

      在这里插入图片描述

      查看返回结果,刷新之后已经可以获取最新提交的配置内容,但是每次都需要手动刷新客户端还是很麻烦,如果客户端数量一多就简直难以忍受,一个好的解决方法就是Spring Cloud Bus。

    15.3.5 Spring Cloud Bus

    Spring Cloud Bus称为消息总线,通过轻量级的消息代理来连接各个分布的节点,可以利用像消息队列的广播机制在分布式系统中进行消息传播。通过消息总线可以实现很多业务功能,其中对于配置中心客户端刷新就是一个非常典型的使用场景。

    消息总线作用流程如下:(图源:https://www.cnblogs.com/xifengxiaoma/p/9857110.html).

    在这里插入图片描述

    Spring Cloud Bus进行配置更新的步骤如下:

    • 提交代码触发post请求给/actuator/bus-refresh。
    • Server端接收到请求并发送给Spring Cloud Bus。
    • Spring Cloud Bus接到消息并通知给其他客户端。
    • 其他客户端接收到通知,请求Server端获取最新配置。
    • 全部客户端均获取到最新的配置。
    1. 安装RabbitMQ

      参考https://developer.aliyun.com/article/769883 and https://blog.csdn.net/weixin_40822435/article/details/105738375

      用系统提供的默认账号登录后管理界面如下:(用户名密码皆为guest)

    在这里插入图片描述

    1. 客户端实现

      添加依赖,打开客户端mango-consumer添加消息总线相关依赖:

      <!-- bus-amqp -->
            <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-bus-amqp</artifactId>
      </dependency>
      

      修改配置,添加RabbitMQ的相关配置,这样客户端代码就改造完成了。

      bookstrap.yml:

      spring:
        cloud:
          consul:
            host: localhost
            port: 8500
            discovery:
              serviceName: ${spring.application.name}    # 注册到consul的服务名称
          config:
            discovery:
              enabled: true  # 开启服务发现
              serviceId: mango-config # 配置中心服务名称
            name: consumer  # 对应{application}部分
            profile: dev  # 对应{profile}部分
            label: master  # 对应git的分支,如果配置中心使用的是本地存储,则该参数无用
        rabbitmq:
          host: localhost
          port: 5672
          username: guest
          password: guest
      
    2. 服务端实现

      添加依赖,修改mango-config,添加相关依赖:

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-bus-amqp</artifactId>
         </dependency>
      

      修改配置,添加RabbitMQ和接口开放相关的配置,这样服务端代码就改造完成了。

      application.yml:

      server:
        port: 8020
      spring:
        application:
          name: mango-config
        cloud:
          consul:
            host: localhost
            port: 8500
            discovery:
              serviceName: ${spring.application.name}    # 注册到consul的服务名称
          config:
            label: master  # git仓库分支
            server:
              git:
                uri: https://gitee.com/lhtyw/config-repo.git  # 配置git仓库的地址
                search-paths: src/config-repo  # git仓库地址下的相对地址,可以配置多个,用,分割。
        rabbitmq:
          host: localhost
          port: 5672
          username: guest
          password: guest
      management:
        endpoints:
          web:
            exposure:
              include: "*"
      
    3. 页面测试

      启动服务端,成功集成消息总线后,启动信息可以看到如下所示信息:

    在这里插入图片描述

    启动客户端。会报错,启动追踪代码发现下图中对应位置找不到相应的Bean.
    在这里插入图片描述

    在刷新的时候缺少一个拦截器,可以自己设置一个。加一个配置类,并在resources下新建一个META-INF目录和一个spring.factories文件,如下:

    在这里插入图片描述

    RetryConfiguration.java:

    public class RetryConfiguration {
       
       @Bean
       @ConditionalOnMissingBean(name = "configServerRetryInterceptor")
       public RetryOperationsInterceptor configServerRetryInterceptor() {
          return RetryInterceptorBuilder.stateless().backOffOptions(1000, 1.2, 5000).maxAttempts(10).build();
       }
       
    }
    

    spring.factories:

    org.springframework.cloud.bootstrap.BootstrapConfiguration=com.louis.mango.consumer.RetryConfiguration
    

    指定新建的拦截器,这样系统初始化时就会加载这个Bean。然后重新启动就没有报错了。

    http://localhost:8005/hello,界面打印"hello, dev configurations."。

    修改仓库配置文件,修改完成提交。

    在这里插入图片描述

    再次访问发现还是旧信息。

    用工具发送post请求:http://localhost:8020/actuator/bus-refresh.

    这次是向注册中心服务端发送请求,发送成功之后服务端会通过消息总线通知所有的客户端进行刷新。另外,开启消息总线后的请求地址是/actuator/bus-refresh,不再是refresh了。

    在这里插入图片描述

    给服务端发送刷新请求后,再次访问http://localhost:8005/hello,结果如下:

    在这里插入图片描述

    最终,我们愉快地发现客户端已经能够通过消息总线获取最新配置了。

    展开全文
  • 考试管理系统【软件工程实践课设报告】

    万次阅读 多人点赞 2021-02-01 14:55:43
    培养学生项目管理与团队协调沟通的能力; 掌握数据库模式设计与实现; 掌握软件工程项目的测试流程; 熟练掌握UML建模、开发、数据库设计和测试工具的使用。 二、软件工程实践需求分析能力培养 1.实践目的 ....

    若本文对你有用,请点赞、关注我哟!

    软件工程专业大四上学期课设之一,随便乱写的,报告评分不高,仅供参考。

    一、软件工程实践课程任务目标

    1. 掌握软件工程问题调研、问题分析和原型设计;
    2. 掌握从技术、经济、社会等方面对系统软件的评估;
    3. 掌握运用软件工程原理、方法与技术进行软件系统设计实践能力;
    4. 培养学生项目管理与团队协调沟通的能力;
    5. 掌握数据库模式设计与实现;
    6. 掌握软件工程项目的测试流程;
    7. 熟练掌握UML建模、开发、数据库设计和测试工具的使用。

    二、软件工程实践需求分析能力培养

    1.实践目的

           掌握UML软件建模工具的使用,掌握问题的分析、可行性研究和相关技术约束评估,熟练绘制数据流图;学习快速原型工具的使用和需求分析文档撰写。

    2.实践基本能力要求

    (1)针对XXX系统进行文献查阅和调研,问题细化,团队协调和沟通,需求分析进行详细设计;

    (2)安装 VISIO 2008 以上版本软件,熟练应用VISIO绘制DFD图,绘制XXX系统业务流图,完成系统的软件逻辑模型;

    (3)安装 Axure RP Pro 或者 Balsamiq Mockups 原型设计软件,学习绘制软件原型,完成XXX系统的软件原型设计。

    3.实践方式

           需求文档撰写和项目组成员讨论;

    4.实践报告格式与内容

    (1)XXX系统任务分析;

    (2)XXX系统需求分析;

    (3)XXX 系统的软件逻辑模型;

    (4)XXX 系统的软件原型设计;

    (4)提交XXX系统需求分析设计书。

    三、软件工程实践系统功能设计

    1.实践目的

           系统功能的详细设计和概要设计,学习UML绘制类图、功能图、活动图,掌握面向对象分析与设计方法,协调团队之间的合作与分工以及项目的管理。

    2.实践基本能力要求

    (1)具备面向对象分析与设计能力;

    (2)安装Star UML和Rational Rose软件,熟练使用UML软件工具,对目标系统进行设计;完成类图、序列图、状态图、活动图等软件建模图形的绘制;

    (3)学习 UML 模型和源代码的双向工程。

    3.实践方式

           实验上机和项目开发;

    4.实验报告格式与内容

    (1)基于UML对目标系统进行概要和详细设计,绘制软件模型;

    (2)描述UML模型和源代码的逆向工程、正向工程;

    (3)提交绘制的图形和设计文档。

    四、软件工程实践编码规范约束设计

    1.实践目的

           学习PHP、.Net、JAVA 等集成语言环境,熟悉各语言的编码规范,进行系统编码规范约束设计。

    2.实践基本要求

    (1)熟悉各种语言的编程环境;

    (2)学习各语言的编码技巧和调试方法;

    (3)设计系统编码规范与约束。

    3.实验方式

           上机编程与文档设计;

    4.实验报告格式与内容

           选择PHP、.Net、JAVA 三种语言和集成编程环境其中之一,介绍环境的基本使用方法,调试技术。

    五、软件工程实践项目版本控制

    1.实践目的

           掌握SVN源代码版本管理工具,会部署Tortoise SVN服务端、客户端,实现不同IDE环境下的集成。例如:Ankh SVN(VS2008 插件) + Visual SVN Server 在Visual Studio环境下进行代码版本管理。

    2.项目版本控制的基本要求

    (1)安装SVN服务器和客户端软件,建立代码仓库,创建用户及授权;

    (2)学习代码的同步、管理和协同开发方法;

    (3)在Visual Studio环境下对所选系统的源代码进行版本管理;

    (4)在Eclipse环境下对所选系统的源代码进行版本管理。

    3.实践方式

           上机实践和项目开发协同管理;

    4.实践报告格式与内容

    (1)Visual Studio环境下源代码版本管理;

    (2)Eclipse环境源代码版本管理。

    六、软件工程实践项目测试

    1.实践目的

           熟练掌握IBM Rational Robot、IBM Purify、Win Runner、NUnit、JUnit、CPPUnit、Webstress等各类软件测试工具的使用;编写项目测试计划、设计测试用例;掌握软件自动化测试方法,完成对系统的功能测试和性能测试。

    2.软件工程实践项目测试的基本要求

    (1)下载、安装IBM Rational Robot、IBM Purify、Win Runner、NUnit、JUnit、CPPUnit、Webstress、QTP、Load Runner和Test Manager等测试工具,熟练掌握其使用;

    (2)掌握IBM Rational Robot中测试脚本录制、测试代码编写方法,针对实验代码进行自动测试;

    (3)了解程序运行错误分析软件IBM Purify的工作原理,使用IBM Purify 进行代码错误检测,分析检测结果;

    (4)采用Win Runner等黑盒测试工具进行所选系统的黑盒测试;

    (5)运用NUnit、JUnit、CPPUnit工具软件进行白盒测试用例设计及自动测试;

    (6)运用Webstress工具软件进行性能自动测试;

    (7)了解Test Manager测试管理工具的使用方法;

    (8)学习.Net、JAVA等集成开发工具中的软件测试、调试。

    3.实践方式

           上机实践和测试文档的撰写;

    4.实践报告格式与内容

    (1)说明测试脚本录制、测试代码编写、针对实验代码进行自动测试的步骤、方法和实现情况;

    (2).Net 或者JAVA等集成开发工具中的软件测试、调试方法;

    (3)针对目标系统,使用各种测试工具的测试过程。

    七、软件工程实践系统数据库设计

    1.实践目的

           掌握数据库分析和设计并熟练掌握数据库的安装、操作和使用;完成对表的设计、实体关系图设计;学习使用数据库软件(例如:Power Designer)的使用。

    2.实践基本要求

           使用数据库软件(Power Designer)设计数据库物理模型,并创建数据库。

    (1)安装数据库软件Power Designer;

    (2)设计目标系统的CDM;

    (3)设计目标系统的PDM。

    3.实验方式

           上机实践和数据库设计文档;

    4.实验报告格式与内容

    (1)数据库实体关系设计;

    (2)数据库表的设计;

    (3)使用数据库软件(Power Designer)构建目标系统数据库模型。

    参考题目(仅供参考,也可自选题目由教师审核)

    1. 工资管理系统
    2. 宾馆客房管理系统       
    3. 人事管理系统      
    4. 学生学籍管理系统
    5. 员工档案管理系统
    6. 银行储蓄管理系统           
    7. 考试管理系统    
    8. 民航(铁路)订票系统
    9. 商场销售管理系统       
    10. 水电管理系统       
    11. 证券交易分析系统

    需求说明书

    一、给出完成系统的主要业务流程

                                                                              图1 系统主要业务活动图

    二、给出完成系统的主要用例

                                                                              图2 系统用例图

    三、根据每个用例给出相应的时序图

                                                                              图3 注册时序图

                                                                              图4 登录时序图

                                                                              图5 学生在线考试时序图

                                                                              图6 学生浏览历史试卷时序图

                                                                              图7 学生浏览历史错题时序图

                                                                              图8 考试管理时序图

                                                                              图9 管理员设计考试时序图

                                                                              图10 统计分析时序图

    软件工程实践报告

    1. 实践题目及实现功能

           实践题目:考试管理系统

           本系统的用户可分为管理员和普通用户两类。

           共分为两个界面:一个界面用于管理员登录,主要负责进行基本资料、题库、试卷、成绩的管理以及查询等;另外一个界面用于普通用户登录,主要负责在线考试、查询以往考试成绩等。

           从总体上考虑,系统应实现下列功能:

           对管理员来说,包括试卷管理、题库管理、成绩管理、学生管理、考试安排。

           试卷管理:管理员可以从课程,各种题型的数量等方面对某份试卷提出一定的要求生成试卷规则。同时,管理员还可以对库中已有的试卷进行修改和删除,添加新试卷等。

           题库管理:管理员可以对题库中的试题进行三种基本操作:添加新的考题、删除旧的考题、修改原有考题、其中试题类型包括判断题、填空题、选择题;对于每种类型的试题,教师可以设置题干、答案等属性。

           成绩管理:管理员可以查看考生的考试成绩,并针对不同的课程进行成绩统计,包括考试人数、最高分、最低分、平均分以及各分数段得分人数等。

           学生管理:管理员可以对用户的资料进行查询和删除。

           考试安排:管理员可以发布指定时期的考试任务,学生只能在规定的时间内完成考试。

           对普通用户来说,包括在线考试、查询以往考试成绩、错题浏览等。

           在线考试:学生可以任选时间进行在线测试,考试结束后,系统会根据已有的标准答案进行在线判卷,考生可以立刻知道考试成绩。

           查询以往考试成绩:学生可以在线询问以前参加过的每门课程的考试相关信息。 

           错题浏览:系统自动汇总学生以往所有错题,方便学生后期复习。

    2. 软件需求分析

    2.1考试管理系统任务分析

           传统的考试方式一般要经过人工出卷、考生考试、人工阅卷等过程。对于一些课程来说,随着考生数量的增加,教师出卷阅卷的工作量将会越来越大,并且其工作十分繁琐和非常容易出错。在线考试系统课题产生的背景是当今教育信息化的趋势及我国高校教育信息化系统的建设,目的是充分利用学校现有的计算机软、硬件和网络资源实现无纸化考试以避免传统手工考试的不足。与传统考试模式相比,网上考试渗入了更多的技术环节,对实现安全性的途径、方法也提出了更高的技术要求。

    2.2考试管理系统的软件逻辑模型

           在需求分析过程中,通过用例建立系统模型,展示系统外部角色对系统的功能需求。

           1、识别参与者与用例,在本系统中,把教师(出题者)、学员(考试人员)、考试管理员、系统管理员等确定为参与者;系统主要用例:考纲管理、考点管理、题库管理、专题测试管理、试卷管理、试卷生成、考试管理、成绩管理、公告管理、统计分析、学生登录、学生考试、模拟考试、考试信息、考试过程管理、注册、基本信息管理、班级管理、角色管理。

           2、建立用例图,利用 Microsoft Office Visio 建模环境,创建系统的考生用例图、教师与考试管理员用例图、系统管理员用例图、客户端脚本对象用例图如图1~3所示,其中图3为一级用例图。

                                                                              图1 系统管理员和客户端浏览器用例

                                                                              图2 考生用例

                                                                              图3 教师与考试管理员用例

           3、用例文档,以试卷生成的为例:用例编号:001。用例名:试卷生成。用例描述:出题人根据自身权限定义试卷后,由计算机自动生成试卷。参与者:计算机或用户。前置条件:用户已登录,并进入后台管理系统,在系统里事前定义好了试卷。后置条件:向管理员发送审核通知。事件路径:①用户选择定义试卷,并保存;②系统提示是否成生试卷;③按试卷配置生成试卷,3a试卷不符合考纲覆盖要求和试卷难度系统,3b循环执行生成试卷;④循环结束判断试卷生成结果,成功保存试卷至数据库;⑤系统显示操作成功。

    2.3考试管理系统的软件原型

    ①前台首页模块设计

           考生通过“考生登录”模块的验证后,可以登录到网络在线考试的前台首页,如图4所示。前台首页主要用于实现前台功能导航,在该页面中只包括在线考试、成绩查询、修改个人资料和退出4个导航链接。

           由于本系统的前台首页主要用于进行系统导航,所以在实现时,采用了为图像设置热点的方法,这样可以增加页面的灵活度,使页面不至于太枯燥。下面将对如何设置图像的热点进行详细介绍。为图像设置热点,也可以称作图像映射,是指一幅图像可以建立多个超链接,即在图像上定义多个区域,每个区域链接到不同的地址,这样的区域称为热点。 

           图像映射有服务器端映射(Server-side-Image Map)和客户端映射(Client-side-Image Map)两种。目前使用最多的是客户端映射,因为客户端映射使图像上对应的坐标以及超链接的URL地址都在浏览器读入,省去和服务器之间互传坐标和URL的时间。

                                                                              图4 前台首页图

    ②考生信息模块设计

           考生信息模块主要包括考生注册、考生登录、修改个人资料以及找回密码等四个功能。考生首先要注册成为网站用户,然后才能被授权登录网站进行一系列操作的权限;登录后考生还可以修改个人的注册资料。如果考生忘记了登录密码,还可以通过网站提供的找回密码功能快速找回密码。考生信息注册模块的系统如图5所示:

                                                                              图5 考生信息注册图

           考生信息模块的Action实现类Student继承了Action类。在该类中,首先需要在该类的构造方法中分别实例化考生信息模块的StudentDAO类。Action实现类的主要方法是execute(),该方法会被自动执行,这个方法本身没有具体的事务,它是根据HttpServletRequest的getParameter()方法获取的action参数值执行相应方法的。

    ③在线考试模块设计

           在线考试模块的主要功能是允许考生在网站上针对指定的课程进行考试。在该模块中,考生首先需要阅读考试规则,在同意所列出的考试规则后,才能选择考试,在选择考试课程后,系统将随机抽取试题,然后进入考试页面进行答题,当考生提交试卷或者到达考试结束时间时,系统将自动对考生提交的试卷进行评分,并给出最终考试成绩。在线考试模块的系统流程如图6所示:

                                                                              图6 在线考试流程图

           考生登录到网络在线考试的前台首页后,单击“在线考试”超链接,将进入到考试规则页面,在该页面中单击“同意”按钮,即可进入到选择考试课程页面,在该页面中将以下拉列表框的形式显示需要参加考试的课程.在该页面中,单击“开始考试”按钮,将关闭当前窗口,并打开新的窗口显示试题,如图7所示:

                                                                              图7 考试试题图

    ④考试题目管理模块设计

           网络在线考试系统的后台首页是管理员对网站信息进行管理的首页面。在该页面中,管理员可以清楚地了解网站后台管理系统包含的基本操作。

           a)管理员信息管理:主要包括管理员信息列表、添加管理员、修改管理员和删除管理员。

           b)考生信息管理:主要包括查看注册考生信息列表和删除已注册的考生信息。

           c)考生成绩查询:主要用于根据准考证号、考试课程或考试时间模糊查询考生成绩。

           d)课程信息管理:主要包括查看课程列表、添加课程信息和删除课程信息。

           e)套题信息管理:主要包括查看套题信息列表、添加套题信息、修改套题信息和删除套题信息。

           f)考试题目管理:主要包括查看考试题目列表、添加考试题目、修改考试题目和删除考试题目。

           g)退出管理:主要用于退出后台管理系统。

           为了方便管理员管理,在网络在线考试系统的后台首页中显示考生成绩查询页面,其运行结果如图8所示:

                                                                              图8 后台首页图

           管理员登录系统后,单击“考试题目管理”超链接,进入到查看考试题目列表页面,在该页面中单击“添加考试题目”超链接,进入到添加考试题目页面。在该页面的“属性课程”下拉列表框中选择“计算机专业英语”,在“所属套题”下拉列表框中将显示该课程对应的套题名称。添加考试题目页面的运行结果如图9所示:

                                                                              图9 添加考试题目图

    3. 面向对象的软件分析与设计

    3.1系统类图

           以用例模型为输人,对用例模型进行分析,把系统分解为相互协作的分析类时,以MVC模式识别系统的类、对类进行划分、建立初步模型、确定类的属性和操作、确定类之间的联系,最后建立类图。系统的类图如图4所示,实现时,类主要体现在视图所对应的后置文件中,除了数据库操作、加密算法等公共业务类以外。

    3.2时序图

           在线考试系统中,用顺序图更能表示系统服务流程,通过图4可以识别出系统里的对象,考试服务子系统的各对象执行的顺序如图5所示,先定义考纲,再定义试卷。考试终端子系统的工作时序为:考生登录,如有考试自动切换进入考试界面,否则由考生选择专题进行测试。一旦进入考试,客户端脚本对象开始倒计时,考生可以自由选择考题类型和考题进行作答。与此同时,考试数据将实时备份至客户端,并在设置时间隔内异步备份至服务端,如图6所示。专题测试在服务端的定义与试卷定义及生成执行时序类似,而专题测试的考试终端仅获取考题,考试结東后给出评价分数。

    3.3状态图

           状态图主要用于描述一个对象在其生存期间的动态行为,表现为一个对象所经历的状态序列,引起状态转移的事件,以及因状态转移而伴随的动作。考试与测试状态图如图7所示,考试分为测试和期末考试,测试包含专题测试与期末模拟考试。考试试卷的状态如图8a所示,其中考试备份为服务器端对考生考试过程进行备份的状态。一般数据对象状态如图8b所示,一般数据对象包含题库考题、考纲考点等,所有数据信息的添加、编辑均需要审核通过后方可使用,为此将信息类型分为活动状态和非活动状态。

                                                                              图10 在线考试系统服务端类图

                                                                              图11 考试管理子系统的顺序图

                                                                              图12 考试终端子系统顺序图

                                                                              图13 考试与测试的状态图

                                                                              图14 一般数据对象的状态图

    3.4描述UML模型和源代码的逆向工程、正向工程

                                                                              图15 UML模型的正向工程图

                                                                              图16 源代码的逆向工程图

    4. 集成编程环境、编码规范及程序清单

    4.1环境的基本使用方法,调试技术

           选择MySql作为后台的数据库,选择MyEclipse作为应用程序开发工具,应用JAVA、JSP、JavaScript、Html、Tomcat服务器技术,整个系统完全基于B/S (Browser/Server)模式进行设计,采用strus框架进行架构。

           在开发网络在线考试系统时,需要具备下面的软件环境:

    1. 操作系统:Windows10。 
    2. Web服务器:Tomcat8.5。 
    3. Java开发包:JDK1.8。 
    4. 开发工具:MyEclipse2019。
    5. 数据库:MySQL及其图形化管理工具SQLyog。 
    6. 浏览器:火狐、IE、Chrome游览器。 

    4.2程序清单

                                                                              图17 目录详情

    1、主函数

    package com.songlea.springboot.demo;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.builder.SpringApplicationBuilder;
    import org.springframework.boot.web.servlet.ServletComponentScan;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    // 相关于使用@Configuration,@EnableAutoConfiguration与@ComponentScan的默认属性
    @SpringBootApplication
    // @EnableAutoConfiguration:尝试根据你添加的jar依赖自动配置你的Spring应用
    // 扫描WebServlet,WebFilter,WebListener注解,只有使用servlet容器时作用
    @ServletComponentScan(value = {"com.songlea.springboot.demo.servlet"})
    // 启注解事务管理,等同于xml配置方式的<tx:annotation-driven />,可以使用@Transactional注解
    @EnableTransactionManagement
    @MapperScan(basePackages = "com.songlea.springboot.demo.mapper")
    public class DemoApplication  /* extends SpringBootServletInitializer */ {
    	public static void main(String[] args) {
    		SpringApplication.run(DemoApplication.class, args);
    	}
    }

    2、.properties配置文件

    # spring boot配置文件:部分配置属性与说明
    # 是否开启压缩,默认为false
    server.compression.enabled=true
    # 设定http监听端口
    server.port=8080
    # datasource config (use druid)
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    # 数据库url,用户名与密码(界面有表格故使用前需要导入ci_host_compare表结构与数据)
    spring.datasource.url=jdbc:mysql://localhost:3306/student_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.driverClassName=com.mysql.jdbc.Driver
    spring.datasource.initialSize=2
    spring.datasource.minIdle=2
    spring.datasource.maxActive=50
    spring.datasource.maxWait=60000
    spring.datasource.timeBetweenEvictionRunsMillis=60000
    spring.datasource.minEvictableIdleTimeMillis=300000
    spring.datasource.validationQuery=SELECT 1 FROM DUAL
    spring.datasource.testWhileIdle=true
    spring.datasource.testOnBorrow=false
    spring.datasource.testOnReturn=false
    spring.datasource.poolPreparedStatements=true
    spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
    spring.datasource.filters=stat,wall,log4j
    spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    
    mybatis.type-aliases-package=com.songlea.springboot.demo.pojo
    mybatis.mapper-locations=classpath:mapper/*.xml
    # 驼峰命名映射
    mybatis.configuration.map-underscore-to-camel-case=true
    mybatis.configuration.mapUnderscoreToCamelCase= true
    #pagehelper.helper-dialect=oracle
    # 启用合理化时如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页;禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据,默认为false
    pagehelper.reasonable=true
    # 支持通过Mapper接口参数来传递分页参数
    pagehelper.support-methods-arguments=true
    # 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值
    # 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值.不理解该含义的前提下,不要随便复制该配置
    # pagehelper.params=count=countSql
    # 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用,默认false
    pagehelper.offset-as-page-num=true
    # 设置为true时使用RowBounds分页会进行count查询,默认为false
    pagehelper.row-bounds-with-count=true
    # 设置为true时,如果pageSize=0或者RowBounds.limit=0就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是Page类型)
    pagehelper.page-size-zero=true
    
    #org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties配置类
    # 是否开启模板缓存,默认true
    spring.thymeleaf.cache=true
    # 是否检查模板路径是否存在,默认true
    spring.thymeleaf.check-template-location=true
    # 指定Content-Type,默认为:text/html
    spring.thymeleaf.content-type=text/html
    # 是否允许MVC使用Thymeleaf,默认为: true
    spring.thymeleaf.enabled=true
    # 指定模板的编码,默认为: UTF-8
    spring.thymeleaf.encoding=UTF-8
    # spring.thymeleaf.excluded-view-names: 指定不使用模板的视图名称,多个以逗号分隔.
    # 指定模板的模式,具体查看StandardTemplateModeHandlers,默认为:HTML5
    spring.thymeleaf.mode=HTML5
    # 指定模板的前缀,默认为:classpath:/templates/
    spring.thymeleaf.prefix=classpath:/templates/
    # 指定模板的后缀,默认为:.html
    spring.thymeleaf.suffix=.html
    # 指定模板的解析顺序,默认为第一个
    spring.thymeleaf.template-resolver-order=1
    # spring.thymeleaf.view-names:指定使用模板的视图名,多个以逗号分隔.

    3、用户接口

    package com.songlea.springboot.demo.common;
    
    public interface UserRole {
        public static final String STUDENT = "student";
        public static final String TEACHER = "teacher";
        public static final String USER = "user";
    }

    4、用户常态接口

    package com.songlea.springboot.demo.common;
    public interface Constant {
        public static final String PAPERID = "paperId";
        public static final String EXAMROOMID = "examroomId";
        public static final String PROBLEMSLIST = "problemsList";
        public static final String EXAMROOMLIST = "examroomList";
        public static final String  USERLIST= "userList";
        public static final String ERRORMESSAGE = "errorMessage";
        public static final String CHOICE = "选择题";
        public static final String CHOICELIST = "choicelist";
        public static final String SHORTANS = "简答题";
        public static final String SHORTANSLIST = "shortanslist";
        public static final String HASVIEWPAPER = "yes";
        public static final String NOTVIEWPAPER = "no";
        public static final String SUCCESS = "success";
        public static final String FAIL = "fail";
        public static final String MESSAGE = "message";
    }

    5、pom.xml配置

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.songlea.springboot</groupId>
        <artifactId>examroom</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <name>examination</name>
        <description>Demo project for Spring Boot</description>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.3.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
            <thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
            <thymeleaf-layout-dialect.version>2.0.5</thymeleaf-layout-dialect.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    
            <!--mybatis支持与分页插件-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.0</version>
            </dependency>
            <!--page helper-->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.1.1</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!--数据库连接池与驱动jar包-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.0.29</version>
            </dependency>
    
            <!--使用mysql数据库时需要-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
    
            <!--热部署-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <optional>true</optional>
                <scope>true</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
    
                <!--spring boot 热部署-->
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <fork>true</fork>
                    </configuration>
                </plugin>
    
                <!--mybatis-generator-->
                <plugin>
                    <groupId>org.mybatis.generator</groupId>
                    <artifactId>mybatis-generator-maven-plugin</artifactId>
                    <version>1.3.5</version>
                    <dependencies>
                        <dependency>
                            <groupId>mysql</groupId>
                            <artifactId>mysql-connector-java</artifactId>
                            <version>5.1.39</version>
                        </dependency>
                        <dependency>
                            <groupId>org.mybatis.generator</groupId>
                            <artifactId>mybatis-generator-core</artifactId>
                            <version>1.3.5</version>
                        </dependency>
                    </dependencies>
                    <executions>
                        <execution>
                            <id>Generate MyBatis Artifacts</id>
                            <phase>package</phase>
                            <goals>
                                <goal>generate</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <!--允许移动生成的文件 -->
                        <verbose>true</verbose>
                        <!-- 是否覆盖 -->
                        <overwrite>true</overwrite>
                        <!-- 自动生成的配置 -->
                        <configurationFile>
                            src/main/resources/generatorConfig.xml
                        </configurationFile>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>

    5. 源代码版本管理

    1、进入Github首页,点击New repository新建一个项目。

                                                                              图18 上传至GitHub

    2、填写相应信息后点击create即可。

            Repository name: 仓库名称

            Description(可选): 仓库描述介绍

            Public,Private: 仓库权限(公开共享,私有或指定合作者)

            Initialize this repository with a README: 添加一个README.md

            gitignore: 不需要进行版本管理的仓库类型,对应生成文件.gitignore

            license: 证书类型,对应生成文件LICENSE

                                                                              图19 获取SHH密钥

    3、点击copy这个地址备用。

                                                                              图20 复制SHH密钥

    4、接下来就到本地操作了,首先右键项目,我之前已安装git,故右键会出现两个新选项,分别为Git Gui Here,Git Bash Here,选择Git Bash Here,进入如下界面,springboot即为我的项目名。

                                                                              图21 git bash进入项目目录

    5、接下来输入如下代码(关键步骤),把github上面的仓库克隆到本地。

                                                                              图22 克隆到本地

    6、本地项目文件夹下面就会多出个文件夹,该文件夹名即为我github上面的项目名,如图我多出了个ExamManage文件夹,把本地项目文件夹下的所有文件(除了新多出的那个文件夹不用),其余都复制到那个新多出的文件夹下。

                                                                              图23 克隆后的本地情况

    7、接着依次输入以下代码即可完成其他剩余操作:

            cd ExamManage,进入Test文件夹

            git add .(把ExamManage文件夹下面的文件都添加进来)

            git commit -m "提交信息"

            git push -u origin master(把本地仓库push到github上面,此步骤需要输入帐号和密码)

                                                                              图24 弹出输入账户和密码的任务框

                                                                              图25 git bash上的执行过程

                                                                              图26 最终上传结果

    6. 自动软件测试

    6.1软件开发技术概述 

            Ajax技术是Asynchronous JavaScript and XML的缩写,意思是异步的JavaScript和XML。Ajax并不是一门新的语言或技术,它是JavaScript、XML、CSS、DOM等多种已有技术的组合,它可以实现客户端的异步请求操作。这样可以实现在不需要刷新页面的情况下与服务器进行通信的效果,从而减少了用户的等待时间。 

    6.2通过Ajax技术实现计时与显示剩余时间 

            在通过Ajax技术实现计时与显示剩余时间,首先需要创建一个封装Ajax必须实现的功能的对象AjaxRequest,并将其代码保存为AjaxRequest.js,然后在开始考试页面中包含该文件,具体代码如下:<script language =“javascript” src=“..//JS/AjaxRequest.js”></script>由于通过Ajax技术实现计时与显示剩余时间表的方法类似,下面以实现自动计时为例进行介绍。编写调用AjaxRequest对象的函数、错误处理函数和返回值处理函数。

            计时方法showStartTime()中,首先需要获取保存在Session中的考试开始时间,并将其转化为对应的毫秒数,然后获取当前时间的毫秒数;再应用这两个时间生成两位的小时数、分钟数和秒数,并组合为新的时间;最后将其保存到showStartTime参数中,并转到输出计时时间的页面。

    7. 设计数据库表

    7.1描述使用Power Designer设计目标系统数据库模型的过程

    7.1.1概念模型

            实体-联系图(Entity Relationship Diagram),E-R模型即实体联系数据模型,它属于概念数据模型。其中,构成E-R图的基本要素主要包括实体型、属性和联系3个部分。本系统的数据需求局部模型如所图9示,为了简化E-R图,每个实体的属性省略,具体的实体属性表示如下。

            班级(C-C):班级编号、班级名称、班级成员、创建者;

            教师(A)(系统、试卷管理员):用户名、密码、角色、数据权限;

            考纲(O):编号、课程编号、考纲名称、创建者、创建时间、审核状态、审人、审核时间;

            考点(P):编号、课程编号、考点名称、最小分数、最大分数;

            考题(Q):编号、课程编号、考点编号、课程章节、题干、参考答案、解题分析、难度系数,参考答案是集合,用特殊符号区分,答案所存储的图文表以HIML标记及文件资源构成,依此处理题干所包含选择题的选项;

            课程(C):编号、课程名称、所属单位、创建者、创建时间、关键字、课程描述等;

            试卷(T):试卷编号、考试名称、测试类型、班级编号、课程编号、试卷总分、考试时间、考试地址、考纲编号、考试时长、考卷难度、考点覆盖范围、试卷状态、创建者、创建日期、审核状态、审核人、审核日期,试卷的试题详表记录,试题详表(T-D):试卷编号、题目类型、每个类型题目数量、每个类型题目分数、每个类型题目的单个试题分数;

            考生(S):考号、帐号、姓名、密码、班级編号、登录标志;

            考试信息(T-I):课程编号、创建者、标题、关键字、描述、简介、内容。

    7.1.2逻辑数据模型

            关系数据库的逻辑结构由一组关系模式组成,从概念结构到关系数据库的逻辑结构的转换就是从E-R图转换为关系模式,其转换过程和规则为:实体和实体属性的转换方法为一个实体转换为一个关系模式;实体之间的联系和联系属性的转换视具体情况进行,图9中角色和权限经优化后不再另外转换为关系模式。联系为1:n的转换方法:在n端的子表中增加父表的关键字列2.于是系统的逻辑数据模式为:

            C-C(classId,className,classMember,creator)

            A (userId,userName,userPwd,userRole,userDataAuthority)

            O(outlineld,courseId,outlineName,creator,addTime,checkState,checker,checkTime)

            P (testPointId,courseId,testPointName,minimumGrade,maximumGrade)

            Q(questionId,courseId,testpointId,courseChapterName,question,anwser,anwserAnalysis,questionDifficultyDegree)

            C(courseId,courseName,subordinateUnit,creator,addTime,courseKeyword,courseDescription)

            T(paperld,testName,testType,classId,courseId,totalGrade,testDate,testAddress,outlineld,durationTime,testDifficulty,testRange,testState,creator,addTime,chechState,checkUser, checkDate)

            T-D(paperId,questionType,eachTypeQuantity,eachTypeGrade,eachTypeQuestionGrade)

            S(testId, userAccount, userName, userPwd, classId, isLogin)

            T-I (courseId,author,title,keyword,description,introduction,content)

            成绩(G):考号、科目编号、试卷编号、答案、每个题目的得分情况、平时成绩、在线学习成绩、总成绩,每种威绩的加权系统由任课教师设定。其关系模式表示为:

            G(testId,courseld,testNumber,single-optionGrade,judgeGrade,blankGrade,multipleChoiceGrade,appQuestionGrade,processGrade,e-learningGrade,totalGrade)

            考生答题记录表,该表应该记录着考生的在线考试近3分钟前的答题情况,当考试过程发生故障而又不能从客户端不能进行考试恢复时,从服务器端恢复备份。考生答题记录(B):试卷编号、考号试卷编号、试题编号、试题解答。其关系模式表示为:

            B(paperld, testid, courseld, questionid, solutionresult)

            系统的物理模型是给定的逻辑数据模型选取一个合适应用要求的物理结构,包括数据库的存储记录格式、存取方法等,且数据库的物理模型依赖给定硬件环境和数据库产品。

                                                                              图26 数据需求局部模型

    7.2数据库设计表

    ①tb_manager(管理员信息表) 

            管理员信息表用来保存管理员信息,该表的结构如表1所示:

    ②tb_Student(考生信息表) 

            考生信息表用来保存考生信息,该表的结构如表2所示:

    ③tb_stuResult(考生成绩信息表) 

            考生成绩信息表用来保存考生成绩,该表中的所属课程字段whichLesson与tb_Lesson表中的Name字段相关联,并且设置为级联更新。考生成绩信息表的结构如表3所示:

    ④tb_TaoTi(套题信息表) 

            套题信息表用来保存套题信息,该表中保存着所属套题ID,套题名称,套题所属课程以及套题的添加时间信息。该表的结构如表4所示:

    ⑤tb_Lesson(课程信息表) 

            课程信息表用来保存课程信息,该表中保存着所属课程的ID,课程名以及课程的添加时间信息。该表的结构如表5所示:

    ⑥tb_Questions(考试题目信息表) 

            考试题目信息表用来保存考试题目信息。考试题目信息表的结构如表6所示:

    7.3数据表关系设计

            本系统设计了如图10所示的数据表之间的关系,该关系实际上也反映了系统中各个实体之间的关系。

                                                                              图27 数据表之间的关系图

    8. 实践工作总结

    8.1在设计及编程过程中遇到的问题及解决的方法

                                                                              图28 样式加载不出来

            问题的点在于引用的jquery以及各种css样式都是静态的,所以解析出现错误,把css样式和jQuery的js文件都放在静态资源下就可以解决这个问题,看最后的静态目录,其中需要注意,静态资源的文件夹的名称必须要写成static,不然不能识别,然后在静态资源下放各种js和css文件以及html和图片。

    8.2对软件工程与工匠精神的理解

            “臣之所好者道也,进乎技矣。”庖丁之语,解答了自己解牛何以神乎其技,道出了一个工匠追求技艺的价值所在。从历史的维度来看,工匠是现代社会之前的一个群体,他们的工作和劳动,主要依靠手工完成。工业革命之后,机器化大生产代替了手工作坊的生产,工匠逐渐受到了冷落。然而,在机器化大生产的时代,更注重产品的精度和品质,更需要工匠精神。当前,中国正在由“制造大国”向“制造强国”迈进,培育工匠精神迫在眉睫。学校要大力加强实践教育,让“心灵手巧”成为衡量人才的标准,尤其在职业教育与应用技术教育中要予以体现。企业要有奖励工匠的体制机制,技艺精湛的工匠,应该在企业内部受到应有的重视,多注重从政策上对工匠倾斜。工匠和简单从事体力劳动的群体不同,他们的劳动中闪耀着智慧的灵光,具有创造性和开拓性,社会各方对他们理应给予足够的人文关怀。

            软件在当今的信息社会中占有重要的地位,软件产业是信息社会的支柱产业之一。随着软件应用日益广泛、软件规模日益扩大,人们开发、使用、维护软件不得不采用工程的方法,以求经济有效地解决软件问题。对于软件工程的学习需要借助于计算机科学技术、数学、管理科学与工程诸多学科,如今的软件工程已由最初的一个学科方向发展成为以计算机科学技术为基础的一个新兴交叉学科。

            软件工程是计算机学科中一个年轻且充满活力的研究领域。现代科学技术将人类带入了信息社会,计算机软件扮演着十分重要的角色,软件工程已成为信息社会高技术竞争的关键领域之一。

            从软件工程专业的种种特性来看,软件工程与工匠精神密不可分,软件工程是一门构建优质、高效软件、知识涉略广泛、逻辑严谨的技术性学科,一丝的差错可能导致严重的后果,故当我们在开发软件时一定要时刻保持着工匠精神!

    8.3对《软件工程师职业道德规范和实践要求 5.2版》的理解

            飞速发展的技术使得这个世界对人类的要求越来越高,她越发希望甚至迫使我们要自觉跟踪技术发展动态,积极参与各种技术交流、技术培训和继续教育活动,不断改进和提高自己的技能,自觉参与项目管理和软件过程改进活动。要能注意对个人软件过程活动的监控和管理,积累工程数据,研究和不断改进自己的软件生产效率和质量,并积极参与发展高效的团队软件过程管理,使各项软件产出,都能达到国际和国家标准与规范。

            社会上的大多数职业,倘若不能主动学习,不能自我进步,那么至少你将失去对于你来讲最有价值的手段,用来获得高薪机会的手段。对软件工程师来讲,这一点尤其明显。一方面是技术的不断普及,一方面是公司对经验人员的迫切需求。在开发领域甚至在任何其他领域一个默认的共识是才华横溢绝对不是你想象的那么重要,从才华横溢到事业成功,要付出艰辛的劳动。你再去问问资深软件工程师,成为资深工程师所需要的不是天份,而是1万小时定律支配下的时间和你的肝功能。软件业是一个不断变化和不断创新的行业,面对层出不穷的新技术,软件人才的求知欲和进取心就显得尤为重要,它是在这个激烈竞争的行业中立足的基本条件。软件工程师应具有较强的学习总结能力、需求理解能力和对IT新技术比较敏感,同时,掌握最新的IT实用技术。

            一个合格的软件工程师,是真正理解了软件产品的本质及软件产品研发的思想精髓的人(个人观点、欢迎探讨)。掌握软件开发语言、应用语言工具解决工作中的具体问题、完成目标任务是软件工程师的主要工作,但从软件工程师这个角度来看,这只是外在的东西,并非重要的、本质的工作。学习、掌握软件产品开发理论知识、软件开发方法论,并在实践中理解、应用软件产品的分析、设计、实现思想来解决具体的软件产品研发问题,才是真正的软件工程师的工作。站在成熟理论与可靠方法论的高度思考、分析、解决问题,并在具体实践中验证和修正这些思想与方式,最终形成自己的理论体系和实用方法论。程序员是个很容易被淘汰,很容易落伍的职业,因为一种技术可能仅仅在三两年内具有领先性,程序员如果想安身立命,就必须不断跟进新的技术,学习新的技能。善于学习,对于任何职业而言,都是前进所必需的动力,对于程序员,这种要求就更加高了。但学习也要找准目标,不能三心二意、盲目泛读,技术不是学得越多越好,而是即使你只掌握一门技术,精益求精、不断钻研,终有一天你会成为这个领域的佼佼者。

    质量目标指标样例及属性详细说明

    分类

    指标

    指标内容

    详细说明

    功能性

    适合性

    行业适合性

    软件支持业务(或领域应用)的必要功能必须存在,这些功能应当与其应用的业务(或领域)场景相适宜,是软件最根本的质量属性。

    准确性

    需求的吻合程度

    应100%覆盖用户的需求。

    互操作性

    如系统进行互联和互通等

    如本系统的某些信息能够与民政网站上的某些信息相对应,进行信息共享。

    依从性

    是否遵守规范

    指系统所依循的书目和文档等参考资料。

    安全性

     

    指软件产品所具备防止未经授权访问程序或数据的能力,以免造成意外的损失。

    可靠性

     

    成熟性

    可用度

    用功能度量即可,能够满足客户一般使用即可。

    初期故障率

    指系统完成初期,在一定时间内,发生系统不能正常使用(如系统崩溃)或影响客户正常工作的问题数。

    偶然故障率

    系统运行一定时间内,发生系统不能正常使用(如系统崩溃)或影响客户正常工作的问题数。

    容错性

    平均失效前时间(MTTF)

    指软件在失效前正常工作的平均统计时间。其中失效指造成系统崩溃的错误,时间为验收后,系统能够正式稳定运行。

    平均失效间隔时间(MTBF)

    指软件在相继两次失效之间正常工作的平均统计时间。其中失效指造成系统崩溃的错误,时间为验收后,系统能够正式稳定运行。

    缺陷密度(FD)

    指缺陷数/代码行数。

    易恢复性

    平均失效恢复时间(MTTR)

    指系统失效后重新启动的平均时间。

    易用性

    易理解性

     

    指用户认识软件的结构、功能、逻辑、概念、应用范围、接口等难易程度的软件属性。

    易学习性

     

    指用户学习软件应用(运行控制、输入、输出)难易程度的软件属性。

    易操作性

     

    指用户操作和运行控制软件的难易程度的软件属性。

    效率

    时间特性

    性能

    输出结果更新周期(页面反应时间)

    不能超过用户所能忍受的限度。

    并发

    指系统允许多少个相同的动作在同一时间进行操作。

    压力

    系统所能承受的最大数据。

    可维护性

    易分析性

     

    指代码名称等是否符合规范,是否完整,

    数据库表格命名等是否符合规范。

    可扩展性

     

    指能与其他系统相连,扩展之后的程序修改不大。

    可修改性

     

    从系统的架构、层次、文档、代码注释等方面考虑。

    可移植性

    适应性

     

    指在不同浏览器、数据库和操作系统上都可运行使用。

    易安装性

     

    指用户使用系统所用的安装方式。

    易替换性

     

    指更新版本后系统是否容易更新。

    交付产品清单

    展开全文
  • 日常开发,数据库连接池是个必不可少的配置,使用优秀的数据库连接池,可以有效的提高数据库访问效率,降低连接异常等,本篇就来学习一下Spirngboot自带连接池和阿里Druid两个最常见的连接池 什么是HikariCP ...

    前言

    日常开发中,数据库连接池是个必不可少的配置,使用优秀的数据库连接池,可以有效的提高数据库访问效率,降低连接异常等,本篇就来学习一下Spirngboot自带连接池和阿里Druid两个最常见的连接池

    什么是HikariCP

    HikariCP是由日本程序员开源的一个数据库连接池组件,代码非常轻量,并且速度非常的快。根据官方提供的数据,在i7,开启32个线程32个连接的情况下,进行随机数据库读写操作,HikariCP的速度是现在常用的C3P0数据库连接池的数百倍。在SpringBoot2.0中,官方默认也是使用的HikariCP作为数据库连接池,可见HikariCP连接池的目的就是为了极致的数据库连接性能体验,下面附上一张HikariCP和其他连接池的比较图:
    HikariCP连接池性能图

    从图中的结果可以看出来,HikariCP从性能来说的确一骑绝尘,那么HikariCP是如何做到这么极致的性能呢?主要依托于HikariCP自身所做的优化机制

    HikariCP优化机制

    字节码精简

    HikariCP优化了代码,尽量减少了生成的字节码,使得cpu可以加载更多程序代码

    优化了拦截和代理机制

    HikariCP对拦截器机制和代理机制进行了代码优化处理,例如Statement proxy只有100行代码,大大减少了代码量,只有其他连接池例如BoneCP的十分之一

    自定义数组

    HikariCP针对数组操作进行了自定义数组–FastStatementList,用来替代jdk的ArrayList,优化了get、remove等方法,避免了每次调用get的时候进行范围检查,也避免了每次remove操作的时候会将数据从头到尾进行扫描的性能问题

    自定义集合

    同样的,针对jdk自带的集合类,HikariCP专门封装了无锁的集合类型 ,用来提高在使用中的读写并发的效率,减少并发造成的资源竞争问题

    CPU时间片算法优化

    HikariCP也对cpu时间片分配算法进行了优化,尽可能使得一个时间片内完成相关的操作

    使用HikariCP

    了解了HikariCP以后,我们开始使用吧,首先找到HikariCP的坐标:

    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>2.7.6</version>
    </dependency>
    

    然后配置HikariCP对应的配置文件,用来读取/加载连接池配置:

    /**
     * HikariCP连接池配置
     */
    @Configuration
    public class DataSourceConfig {
    
        @Value("${spring.datasource.url}")
        private String dataSourceUrl;
    
        @Value("${spring.datasource.username}")
        private String user;
    
        @Value("${spring.datasource.password}")
        private String password;
    
        @Bean
        public DataSource primaryDataSource() {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(dataSourceUrl); //数据源url
            config.setUsername(user); //用户名
            config.setPassword(password); //密码
            config.addDataSourceProperty("cachePrepStmts", "true"); //是否自定义配置,为true时下面两个参数才生效
            config.addDataSourceProperty("prepStmtCacheSize", "250"); //连接池大小默认25,官方推荐250-500
            config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); //单条语句最大长度默认256,官方推荐2048
            config.addDataSourceProperty("useServerPrepStmts", "true"); //新版本MySQL支持服务器端准备,开启能够得到显著性能提升
            config.addDataSourceProperty("useLocalSessionState", "true");
            config.addDataSourceProperty("useLocalTransactionState", "true");
            config.addDataSourceProperty("rewriteBatchedStatements", "true");
            config.addDataSourceProperty("cacheResultSetMetadata", "true");
            config.addDataSourceProperty("cacheServerConfiguration", "true");
            config.addDataSourceProperty("elideSetAutoCommits", "true");
            config.addDataSourceProperty("maintainTimeStats", "false");
    
            HikariDataSource ds = new HikariDataSource(config);
            return ds;
        }
    }
    

    接着我们在SpringBoot的application.properties 文件中进行配置:

    server.port=8090
    
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.driverClassName=com.mysql.jdbc.Driver
    
    spring.datasource.max-idle=10
    spring.datasource.max-active=15
    spring.datasource.max-lifetime=86430000
    spring.datasource.log-abandoned=true
    spring.datasource.remove-abandoned=true
    spring.datasource.remove-abandoned-timeout=60
    spring.datasource.initialize=false
    spring.datasource.sqlScriptEncoding=UTF-8
    

    配置完毕,此时我们启动工程,即可看到控制台已经将我们配置的HikariCP数据库连接池信息打印出来了

    阿里巴巴Druid

    提到大名鼎鼎的Druid连接池,相信很多人都不陌生,因为该连接池是阿里开源的优秀的连接池,几乎已经成为现在使用最多的连接池之一。我们先打开Druid的官方github:

    https://github.com/alibaba/druid 
    

    可以看到此项目已经有19.4k的star数,并且是2019最受欢迎的开源之一,经历过真实线上双十一的考验,可以说是个很成熟的开源连接池,而Druid连接池专为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Logging能诊断Hack应用行为等。

    快速使用

    Druid 0.1.18 之后版本都发布到maven中央仓库中,所以你只需要在项目的pom.xml中加上dependency就可以了,中央仓库坐标如下:

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid-version}</version>
    </dependency>
    

    这里我们使用了springBoot,由于默认支持的数据连接池只有四种:dbcp,dbcp2, tomcat, hikariCP,并不包含druid,所以我们这里也可以选择直接使用阿里官方编写的druid-spring-boot-starter,并且我们添加对应的mybatis和pageHelper的依赖:

    <!-- 数据库驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- Mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.0</version>
    </dependency>
    <!--Mybatis 分页插件 pagehelper -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!-- Druid连接池包 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.1</version>
    </dependency>
    

    在application.properties中 进行数据源配置:

    # 数据库访问配置
    # 主数据源,默认的
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    spring.datasource.username=root
    spring.datasource.password=123456
    
    # 下面为连接池的补充设置,应用到上面所有数据源中
    # 初始化大小,最小,最大
    spring.datasource.initialSize=5
    spring.datasource.minIdle=5
    spring.datasource.maxActive=20
    # 配置获取连接等待超时的时间
    spring.datasource.maxWait=60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 
    spring.datasource.timeBetweenEvictionRunsMillis=60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒 
    spring.datasource.minEvictableIdleTimeMillis=300000
    spring.datasource.validationQuery=SELECT 1 FROM DUAL
    spring.datasource.testWhileIdle=true
    spring.datasource.testOnBorrow=false
    spring.datasource.testOnReturn=false
    # 打开PSCache,并且指定每个连接上PSCache的大小 
    spring.datasource.poolPreparedStatements=true
    spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 
    spring.datasource.filters=stat,wall,log4j
    # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
    spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    # 合并多个DruidDataSource的监控数据
    #spring.datasource.useGlobalDataSourceStat=true
    

    配置完毕以后,由于这里我们使用了mybatis,所以我们还要在application.properties中配置一下mybatis相关:

    #mybatis
    #entity扫描的包名
    mybatis.type-aliases-package=com.xiaolyuh.domain.model
    #Mapper.xml所在的位置
    mybatis.mapper-locations=classpath*:/mybaits/*Mapper.xml
    #开启MyBatis的二级缓存
    mybatis.configuration.cache-enabled=true
    
    #pagehelper
    pagehelper.helperDialect=mysql
    pagehelper.reasonable=true
    pagehelper.supportMethodsArguments=true
    pagehelper.params=count=countSql
    

    准备就绪后,我们来编写一个测试类:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class DataSourceTests {
    
        @Autowired
        ApplicationContext applicationContext;
    
        @Test
        public void testDataSource() throws Exception {
            // 获取配置的数据源
            DataSource dataSource = applicationContext.getBean(DataSource.class);
    System.out.println(dataSource.getClass().getName());
        }
    }
    

    将测试方法运行起来,即可在控制台中看到对应的数据源的输出信息

    Druid开启监控统计功能

    druid最强大的功能就是自身提供了对sql的数据监控功能,并且内置了很多详细的拦截器,可以实现多个角度的拦截处理,那么如何开启监控?在Druid中内置提供了一个StatViewServlet用于展示Druid的统计信息,这个StatViewServlet的用途包括:

    • 提供监控信息展示的html页面
    • 提供监控信息的JSON API

    如果是ssm工程,则可以在web.xml中配置StatViewServlet,如下:

    <servlet>
          <servlet-name>DruidStatView</servlet-name>
          <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
      </servlet>
      <servlet-mapping>
          <servlet-name>DruidStatView</servlet-name>
          <url-pattern>/druid/*</url-pattern>
      </servlet-mapping>
    

    配置完毕以后,启动工程则可以按照配置的监控地址访问监控信息,默认为:http://ip:port/project-name/druid/index.html

    同样的,在StatViewServlet中我们可以添加访问密码的设置,只需要配置Servlet的 loginUsername 和 loginPassword这两个初始参数 即可,例如:

    <!-- 配置 Druid 监控信息显示页面 -->  
    <servlet>  
        <servlet-name>DruidStatView</servlet-name>  
        <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>  
        <init-param>  
    	<!-- 允许清空统计数据 -->  
    	<param-name>resetEnable</param-name>  
    	<param-value>true</param-value>  
        </init-param>  
        <init-param>  
    	<!-- 用户名 -->  
    	<param-name>loginUsername</param-name>  
    	<param-value>druid</param-value>  
        </init-param>  
        <init-param>  
    	<!-- 密码 -->  
    	<param-name>loginPassword</param-name>  
    	<param-value>druid</param-value>  
        </init-param>  
    </servlet>  
    <servlet-mapping>  
        <servlet-name>DruidStatView</servlet-name>  
        <url-pattern>/druid/*</url-pattern>  
    </servlet-mapping> 
    

    此时访问监控页面的时候就需要输入我们设置的用户名和密码了,如果还想针对用户有敏感信息配置和访问权限控制,我们还可以配置allowdeny参数,例如:

    <servlet>
          <servlet-name>DruidStatView</servlet-name>
          <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
      	<init-param>
      		<param-name>allow</param-name>
      		<param-value>128.242.127.1/24,128.242.128.1</param-value>
      	</init-param>
      	<init-param>
      		<param-name>deny</param-name>
      		<param-value>128.242.127.4</param-value>
      	</init-param>
      </servlet>
    

    这里的访问规则为:

    1.deny配置优先于allow,即deny为优先拒绝,即使在allow中配置了白名单,但是只要存在于deny中,一样也会被拒绝访问

    2.如果allow没有配置,或者配置为空,默认为全部都可以访问,不进行白名单限制

    使用SpringBoot配置监控

    由于我们这里使用的是SpringBoot,所以我们仅需要在application.properties 添加配置统计拦截的filters:

    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    spring.datasource.druid.filters=stat,wall,log4j
    

    这里的配置是通过别名方式配置扩展支持的插件,如下:

    • 监控统计用的filter:stat
    • 日志用的filter:log4j
    • 防御sql注入的filter:wall

    接着我们需要在application.properties继续添加WebStatFilterStatViewServlet的配置项:

    # WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
    #启动项目后访问 http://127.0.0.1:8080/druid
    #是否启用StatFilter默认值true
    spring.datasource.druid.web-stat-filter.enabled=true
    spring.datasource.druid.web-stat-filter.url-pattern=/*
    spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
    #缺省sessionStatMaxCount是1000个
    spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
    #关闭session统计功能
    spring.datasource.druid.web-stat-filter.session-stat-enable=false
    #配置principalSessionName,使得druid能够知道当前的session的用户是谁
    #如果你session中保存的是非string类型的对象,需要重载toString方法
    spring.datasource.druid.web-stat-filter.principalSessionName=xxx.user
    #如果user信息保存在cookie中,你可以配置principalCookieName,使得druid知道当前的user是谁
    spring.datasource.druid.web-stat-filter.principalCookieName=xxx.user
    #druid 0.2.7版本开始支持profile,配置profileEnable能够监控单个url调用的sql列表。
    spring.datasource.druid.web-stat-filter.profile-enable=false
    
    # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
    #启动项目后访问 http://127.0.0.1:8080/druid
    #是否启用StatViewServlet默认值true
    spring.datasource.druid.stat-view-servlet.enabled=true
    spring.datasource.druid.stat-view-servlet.urlPattern=/druid/*
    #禁用HTML页面上的“Reset All”功能
    spring.datasource.druid.stat-view-servlet.resetEnable=false
    #用户名
    spring.datasource.druid.stat-view-servlet.loginUsername=admin
    #密码
    spring.datasource.druid.stat-view-servlet.loginPassword=admin
    #IP白名单(没有配置或者为空,则允许所有访问)
    spring.datasource.druid.stat-view-servlet.allow=127.0.0.1,192.168.163.1
    #IP黑名单 (存在共同时,deny优先于allow)
    spring.datasource.druid.stat-view-servlet.deny=192.168.1.73
    

    接着我们启动工程,访问http://localhost/druid ,输入配置的用户名:admin以及密码:admin,即可看到druid的监控页面:

    druid监控页面

    慢sql日志打印

    在开发过程中,往往会遇到sql时间过长问题,为了定位慢sql,我们往往会定义固定时长作为慢sql的时长,而Druid支持慢sql查询,在Druid中内置提供了一个StatFilter,用于统计监控信息 ,我们可以利用这个StatFilter来统计慢sql:

    StatFilter的别名是stat,这个别名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties
    

    我们需要在Spring中加入以下配置:

     <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
      	<property name="filters" value="stat" />
      </bean>
    

    当然如果需要我们可以同时开启多个Filter进行组合使用,中间用,隔开即可,如下:

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
      	<property name="filters" value="stat,log4j" />
      </bean>
    

    而如果开启慢sql的记录,我们需要先定义slowSqlMillis 来配置sql慢查询的标准,如下:

    <bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
    	<property name="slowSqlMillis" value="10000" />
    	<property name="logSlowSql" value="true" />
    </bean>
    

    配置完毕以后,所有的超过10s的sql都会在监控页面的慢sql模块记录,可以查看具体的sql以及执行时间等,快速定位开发过程中的慢sql

    DruidDataSource配置

    如果我们根据业务的不同,需要更改不同的配置,这个时候我们就需要参考DriudDataSource的配置,通用的配置项如下:

    Druid常见配置与问题

    除了我们已经了解的druid常见知识以外,开发中经常还会遇到很多其他常见需求,如开启druid的防sql注入功能、记录每次执行的sql、数据库加密、log输出执行的sql等常见需求,这个时候我们就可以在官方的github的文档中查找,官方已经给我们整理好了一些开发常见的问题,地址如下:

    https://github.com/alibaba/druid/wiki/常见问题
    

    总结

    在实际开发过程中,我们往往会根据自身需求或者项目本身来选择最适合的连接池,这里我们将常见的数据连接池从可扩展性、可靠稳定性、性能、可运维性以及自身功能几个方向进行了比较,可供参考:
    连接池比较图

    Hi~ o(* ̄▽ ̄ *)ブ

    疫情无情,人有情!

    ####小编宅家期间整理了约 300G 关于BATJ大厂的面试资料

    需要的小伙伴都可以转发+私信回复“1” 免费领取!

    本次面试答案,以及收集到的大厂必问面试题分享:

    字节跳动超高难度三面java程序员面经,大厂的面试都这么变态吗?

    靠稳定性、性能、可运维性以及自身功能几个方向进行了比较,可供参考:
    [外链图片转存中…(img-Mispi8n6-1623727839659)]

    Hi~ o(* ̄▽ ̄ *)ブ

    疫情无情,人有情!

    ####小编宅家期间整理了约 300G 关于BATJ大厂的面试资料

    需要的小伙伴都可以转发+私信回复“1” 免费领取!

    [外链图片转存中…(img-R0NHs2OF-1623727839660)]

    [外链图片转存中…(img-6JRxhDbF-1623727839662)]

    [外链图片转存中…(img-207dNg2A-1623727839662)]

    [外链图片转存中…(img-ouyNNLuJ-1623727839664)]

    本次面试答案,以及收集到的大厂必问面试题分享:

    [外链图片转存中…(img-sMKOizTi-1623727839665)]

    资料领取方式:戳这里即可免费下载

    展开全文
  • 在数据仓库中,最好的方法是使用所谓的包含不同值得维表 (Dimension Table),将主要的参数存储为引用维表整数键: In [ 15 ]: values = pd.Series([ 0 , 1 , 0 , 0 ] * 2 ) In [ 16 ]: dim = pd.Series([ '...
  • SIZE[0] or first[1] [1] > GAME_SIZE[1]: return True # 头与墙壁 for xy in wall_list: wall_rect = (xy[0]-WALL_WIDTH/2,xy[1]-WALL_WIDTH/2,WALL_WIDTH,WALL_HEIGHT) if rect_cover(snake_head_rect,wall_rect):...
  • 然后配置HikariCP对应的配置文件,用来读取/加载连接池... 快速使用 Druid 0.1.18 之后版本都发布到maven中央仓库中,所以你只需要在项目的pom.xml加上dependency就可以了,中央仓库坐标如下: <dependency> <groupId>...
  • 点击关注公众号,实用技术文章及时了解来源:juejin.cn/post/70625069231945810291 基本概念Druid 是Java语言最好的数据库连接池。虽然 Hikari...
  • 为了使生产恢复正常,Oats更正了分配给.asset的配置文件类型并重建了GI,确保文件已作为二进制文件提交,因为它们已作为文本文件存在于软件仓库中。 处理新的太阳姿势 (Handling new sun positions) Another issue ...
  • 启动速度是用户对我们应用的第一体验,用户只有启动我们的应用才能使用我们应用的功能。 就算我们应用内部设计得再精美,其他性能优化地再好,如果打开速度很慢的话,用户对我们应用的第一印象还是很差。 你可以...
  • } //arg = argument ,参数 //argc = argument count ,调用main函数时传递agrv值得个数 //argv = argument value , #define typdef /* typdef常用来定义一些标识符和关键字的别名,它是语言编译过程的一部分,但不...
  • Try to count how many commands were executed in this terminal: Shell提示应为彩色。 不同意? 尝试计算在此终端执行了多少命令: And now with color: 现在带有颜色: Shell prompt should display a current...
  • [INFO] [INFO] Using the MultiThreadedBuilder implementation with a thread count of 5 [INFO] [INFO] ---------------------< org.apereo.cas:cas-overlay >--------------------- [INFO] Building cas-overlay...
  • fwrapv -Wall-Wstrict-prototypes -fPIC -std=c99 -O3 - 2.fomit-frame-pointer -Isrc/ -I/usr/include/python2.7 -c src/MD2.c -o build/temp.linux-x86_64- 3.2.7/src/MD2.o 4.src/MD2.c:31:20: fatal error: ...
  • iOS开发的小技巧

    2016-11-18 11:47:40
    iOS开发的小技巧: UITableView的Group样式下顶部空白处理 //分组列表头部空白处理 UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0.1)]; self.tableView.tableHeaderView = view; ...
  • 2020羊城杯CTF随缘Writeup

    千次阅读 2021-01-09 16:18:58
    至于给的 pom.xml 有什么用,除了提示 JDBC 反序列化,其次就是说明引进了 commons-collections 依赖,在 maven 仓库中查询 serialkiller,就会发现它引进了 commons-collections。 拿到flag:GWHT{5e97245bd9c98...