Spring Cloud + Vue前后端分离-第5章 单表管理功能前后端开发

2023-12-14 09:42:09

Spring Cloud + Vue前后端分离-第5章 单表管理功能前后端开发

完成单表的增删改查

控台单表增删改查的前后端开发,重点学习前后端数据交互,vue ajax库axios的使用等

通用组件开发:分页、确认框、提示框、等待框等

常用的公共组件:确认框、提示框、等待框,统一日志拦截器等。使用vue自定义组件制作分页组件,mybatis分页插件pagehelper的使用等

5-1 大章列表查询功能开发1

增加maven子项目business

1.增加business模块,并增加初始启动代码

Shift+F6重命名。重命名也是一种重构,会将所有引用到的地方都一起改名,甚至是注释掉的代码也会一起改掉

application.properties

spring.application.name=business
server.servlet.context-path=/business
server.port=9002
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

大章表设计及持久层代码生成

将sql脚本和代码放一起的好处是,可以通用git提交记录来查看sql的变更记录,方便追溯

一般的表结构设计,都会有一个ID字段,作为主键,与业务无关

1.增加大章chapter表sql,生成持久层代码

小技巧:可以将常用的文件放入收藏夹,方便查找

注:每次要生成新表代码时,旧的表不要删除,但要注释掉。(同时生产多个表也可以,但没必要)

自动生成的代码


完成后端列表查询接口

同样,在business里的controller层也是一样的创建方法?

启动注册中心,再启动business服务

1.增加dto层,用于controller和service层?

DTO : Data Transfer Object 数据传输对象,用于数据传输

又是一个约定: domain内的实体,是mybatis generator自动生成的,不允许手动修改。一旦修改,再次生成实体类时, 所做的修改会被覆盖

domain作用于service和mapper;dto作用于controller和service

Ctrl+Alt+V为表达式生成一个变量

拓展:编写自己的for语句代码

for(int $INDEX$ = 0, l = $LIST$.size(); $INDEX$ < l; $INDEX$++) {
? ? $END$
}

1. 点击按钮弹出变量设置窗口

2.设置这两个变量

BeanUtils是Spring提供的一个工具类,用于实体间的复制。后续我们会对BeanUtils做封闭,简化使用,提高开发效率

2.增加ChapterDto

是chapter复制的
3.修改ChapterService,将返回Chapter改成返回ChapterDto

5-2 大章列表查询功能开发2

从这个地方开始,我换mac了,嘿嘿

前端页面开发

row col-xs-12都是bootstrap栅格系统的内置样式,用于响应式页面的布局,需熟练掌握

选中全部,Shift+Tab,反向缩进

点击sidebar菜单实现页面跳转

二级菜单要显示成激活状态,只需要添加active样式

接下来完成功能:点击左侧菜单,该菜单变成激活状态,并跳到相应的路由页面

siblings,jquery的方法,获取所有兄弟节点

约定:id 的命名要和路由相关。后续我们会用到这个约定。

<router-link to="">,类似于<a href="">,用于链接跳转

为每一个路由都加上一个name属性,后续做通用的sidebar激活样式方法时会用到

?

通用的sidebar 点击激活样式方法

通用的功能,要尽量做个通用的方法,要学会“懒”。

1.通用的sidebar点击激活样式方法,使用watch 监听路由变化

vue 内置的watch,用来监测vue 实例上的数据变动,$route 也是一个变量。

通过name 属性值,得到菜单id 的值。前面有约定:id 的命名要和路由相关。程序开发中有一项设计范式叫:约定大于配置(按约定编程)。

此时如果从login页面点击登录跳到welcome页面,welcome并不会有激活样式。这里的watch,只在admin下面的子组件互相跳转时有效

js中有this 关键字,代表当前执行方法的对象。养成习惯,在方法开头,声明本地变量_this 代替this。后面会介绍直接用this的坑。

5-3 大章列表查询功能开发3

集成axios 完成前后端交互

vue也支持使用jquery ajax 来请求后端借口,推荐使用vue axios

注意:要先进到vue cli 项目,再安装插件

--save:在package.json添加依赖。(不加-- save的话,只是去下载插件,项目中并没有依赖插件)

1.安装axios

npm install axios --save

2.以vue属性的方式使用axios

修改main.js

import axios from 'axios'

Vue.prototype.$ajax = axios;

Vue.prototype.xxx,可以理解为Vue组件的全局变量。可以在任意Vue组件中,使用this.xxx 来获取这个值。$ 是代表Vue 全局属性的一个约定

3.chapter.vue 中使用$ajax

list() {
  let _this = this;
  _this.$ajax.get("http://127.0.0.1:9002/business/admin/chapter/list").then((response) => {
    console.log("查询大章列表结果", response);
  })
}

/admin 用于控台类的接口,/web 用于网站类的接口。接口设计中,用不同的请求前缀代表不同的入口,做接口隔离,方便做鉴权、统计、监控等

启动serve、注册中心EurekaApplication、BusinessApplication

CORS,Cross-Origin?Resource Sharing 跨站点资源分享,属于跨域问题。同个IP的不同端口间访问也属于跨域。前后端分离必然有跨域问题

4.解决跨域的问题

1.集成axios 完成前后端交互

2.增加CorsConfig,解决前后端跨域的问题

增加CorsConfig.java

package com.course.server.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
    }

}

页面改造显示真实数据

1.大章页面显示真实数据

Ctrl+Shift+减号:收起所有节点,包括所有的子节点。

Ctrl+Shift+加号:展开所有的层级。

使用data定义组件内的变量,可用于做双向数据绑定,双向数据绑定是vue 的核心功能之一。

使用this.xxx来访问组件内的变量

使用gateway 路由转发

1.使用gateway 路由转发,vue页面只访问gateway的端口

spring.cloud.gateway.routes[1].id=business
spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args[0]=/business/**

这里的请求地址目前是写死在代码中的,后续我们会做优化,对请求地址做多环境的配置。

?扩展:1.解决gateway 跨域问题

gateway跨域配置

在gateway 启动类里增加

/**
     * 配置跨域
     * @return
     */
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(Boolean.TRUE);
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }

把CorsConfig.java注释掉

把服务重新启动

是否直接访问gateway就不需要跨域配置里呢?需要再验证一下

单个SpringBoot 应用使用CorsConfig 解决跨域问题。使用SpringCloud Gateway的,使用CorsWebFilter解决跨域问题。

扩展:2.使用lb://+注册中心名称作路由转发

lb意思是loadbalance? 负载均衡

问题:如果配置的是IP端口,那发布到生产时就可能会访问不到,就算配置了maven多环境,也需要提前知道上线后的IP和端口,提前配好。

#spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].uri=lb://business

5-4 分页功能开发

集成分页插件pagehelper

1.集成分页插件pagehelper,注意页码从1开始

mybatis-generator 生成的代码是不带分页功能的,使用pagehelper插件来扩展分页功能。

父包

<!-- mybatis分页插件pagehelper -->
  <dependency>
     <groupId>com.github.pagehelper</groupId>
     <artifactId>pagehelper-spring-boot-starter</artifactId>
     <version>1.2.10</version>
  </dependency>

server子包

<!-- mybatis分页插件pagehelper -->
  <dependency>
     <groupId>com.github.pagehelper</groupId>
     <artifactId>pagehelper-spring-boot-starter</artifactId>
  </dependency>

ChapterService.java

PageHelper.startPage(1,1);

PageHelper 的分页参数:pageNum是从1开始的?

分页功能的关键字:limit。从日志可以看出,该sql 执行的是limit 1,相当于limit 0,1 ,即从第0行开始,查1条。

插件分页语句规则:调用startPage 方法之后,执行的第一个select 语句会进行分页。

limit 1,1? :从行号1(行号是从0开始)开始,查1条

分页查询功能需要两条sql ,一条是查总记录数(通过每页条数计算出总共有多少页),一条是查当前页的记录。

分页参数前后端交互

1.分页参数前后端交互,axios 的post 请求默认是以流的方式传递参数,所以controller 里的参数要加@RequestBody 注解

泛型需要熟练掌握,在写一些通用类,工具类时很好用。

扩展:使用泛型的地方都可以用Object 代替,但是泛型可以在编译期就发现问题,并且避免了代码中写强制类型转换。

PageDto 即用来接收入参,也用来返回结果。

?当传入的分页参数不合法时,比如0,0 ,程序不会报错,而是查全部记录,分页不生效。

经验分享:在开发完代码后,需要进行测试,特别要针对一些边界值做测试。

?

接口请求参数传递,尽量使用post。使用get 请求在url 里拼参数的话,会使url 变得很长,有些浏览器或服务器会对url 长度做限制,导致请求失败。

private static final Logger LOG = LoggerFactory.getLogger($CLASSNAME$.class);

日志输出时,变量使用点位符,比如LOG.info("输出:id={},姓名={}",id,name),而不是LOG.info("输出:id=“+ id +”,姓名=" + name)

post请求有多种参数传递,通过header里的Content-Type来标识,常见的有两种,一种是表单的方式,另一种是json(流)的方式。

jquery 默认是以表单的方式,vue angular 默认是用json 的方式。

5-5 前端分页组件的使用

增加刷新功能

注意:<template>标签只能有一个子标签

?fa 样式是 fontawesome 图标,可以百度搜“fontawesome图标”查看所有的图标样式

前端分页组件的使用

1.增加分页组件pagination.vue

2.大章管理页面使用分页组件,可自定义初始每页10条,最多显示8个按钮

问题:当数据量很大的时候,分页页码很多,这时把所有页码都显示出来,会占用页面的大部分空间,影响体验。所以需要设置显示页码数量

v-bind:list="list",前面的list,是分页组件暴露出来的一个回调方法,后面的list,是chapter组件的list方法

props,定义父组件向子组件传递的参数,可以是一个函数或数据。本组件中暴露了两个参数list 和 itemCount 给外部。

pagination.vue

<template>
  <div class="pagination" role="group" aria-label="分页">
    <button type="button" class="btn btn-default btn-white btn-round"
            v-bind:disabled="page === 1"
            v-on:click="selectPage(1)">
      1
    </button>
    <button type="button" class="btn btn-default btn-white btn-round"
            v-bind:disabled="page === 1"
            v-on:click="selectPage(page - 1)">
      上一页
    </button>
    <button v-for="p in pages" v-bind:id="'page-' + p"
            type="button" class="btn btn-default btn-white btn-round"
            v-bind:class="{'btn-primary active':page == p}"
            v-on:click="selectPage(p)">
      {{p}}
    </button>
    <button type="button" class="btn btn-default btn-white btn-round"
            v-bind:disabled="page === pageTotal"
            v-on:click="selectPage(page + 1)">
      下一页
    </button>
    <button type="button" class="btn btn-default btn-white btn-round"
            v-bind:disabled="page === pageTotal"
            v-on:click="selectPage(pageTotal)">
      {{pageTotal||1}}
    </button>
    &nbsp;
    <span class="m--padding-10">
        每页
        <select v-model="size">
            <option value="1">1</option>
            <option value="5">5</option>
            <option value="10">10</option>
            <option value="20">20</option>
            <option value="50">50</option>
            <option value="100">100</option>
        </select>
        条,共【{{total}}】条
    </span>
  </div>
</template>

<script>
export default {
  name: 'pagination',
  //props,定义父组件向子组件传递的参数,可以是一个函数或数据。本组件中暴露了两个参数list 和 itemCount 给外部。
  props: {
    list: {
      type: Function,
      default: null
    },
    itemCount: Number // 显示的页码数,比如总共有100页,只显示10页,其它用省略号表示
  },
  data: function () {
    return {
      total: 0, // 总行数
      size: 10, // 每页条数
      page: 0, // 当前页码
      pageTotal: 0, // 总页数
      pages: [], // 显示的页码数组
    }
  },
  methods: {
    /**
     * 渲染分页组件
     * @param page
     * @param total
     */
    render(page, total) {
      let _this = this;
      _this.page = page;
      _this.total = total;
      _this.pageTotal = Math.ceil(total / _this.size);
      _this.pages = _this.getPageItems(_this.pageTotal, page, _this.itemCount || 5);
    },

    /**
     * 查询某一页
     * @param page
     */
    selectPage(page) {
      let _this = this;
      if (page < 1) {
        page = 1;
      }
      if (page > _this.pageTotal) {
        page = _this.pageTotal;
      }
      if (this.page !== page) {
        _this.page = page;
        if (_this.list) {
          _this.list(page);
        }
      }
    },

    /**
     * 当前要显示在页面上的页码
     * @param total
     * @param current
     * @param length
     * @returns {Array}
     */
    getPageItems(total, current, length) {
      let items = [];
      if (length >= total) {
        for (let i = 1; i <= total; i++) {
          items.push(i);
        }
      } else {
        let base = 0;
        // 前移
        if (current - 0 > Math.floor((length - 1) / 2)) {
          // 后移
          base = Math.min(total, current - 0 + Math.ceil((length - 1) / 2)) - length;
        }
        for (let i = 1; i <= length; i++) {
          items.push(base + i);
        }
      }
      return items;
    }
  }
}
</script>

<style scoped>
.pagination {
  vertical-align: middle !important;
  font-size: 16px;
  margin-top: 0;
  margin-bottom: 10px;
}

.pagination button {
  margin-right: 5px;
}

.btn-primary.active {
  background-color: #2f7bba !important;
  border-color: #27689d !important;
  color: white !important;
  font-weight: 600;
}

/*.pagination select {*/
/*vertical-align: middle !important;*/
/*font-size: 16px;*/
/*margin-top: 0;*/
/*}*/
</style>

5-6 增加新增大章功能

页面设计与前端代码开发

1.增加新增大章功能,前端代码开发

Bootstrap v3 中文文档 · Bootstrap 是最受欢迎的 HTML、CSS 和 JavaScript 框架,用于开发响应式布局、移动设备优先的 WEB 项目。 | Bootstrap 中文网

新增功能的页面如何设计,需要平时心里有些储备,可以平时浏览bootstrap 文档,看看都有哪些组件,用的时候心里有数。

模态框主要分为三大块:

modal-header 是标题;

modal-body 是主体内容,大章的表单内容就放在这里;

modal-footer 是底部按钮。

小技巧:

1.选中开头,一小部分代码

2.滚轮滚动到结尾鼠标拖动滚动条到结尾

3.按住shift并鼠标点击结尾

这种操作特别适合选中大段文本。

$(".modal")里的modal 是 css 的选择器,模态框代码里有class="modal" 样式;modal() 里的modal 是内置的方法,用于弹出或关闭模态框

启动admin、eureka、gateway、business

可以使用$(".modal").modal({backdrop:"static"});

禁止点空白的地方关闭,某些场景需求会用到这个功能。

&nbsp;

vuecli 会将我们写的html js css 代码编译压缩,空格和换行都会被压缩掉,导致按钮间的间隔没有了

html 有很多转义字符,比如你想在界面显示文本"<text>",但是浏览器会认为<text>是一个标签,这时可以在html中用转义字符:&lt;text&gt;

<label for="id">有个场景会经常用到:点击复选框checkbox时选中,使用lable for 后,点击label 的文字,也能选中复制框

模态框弹出和关闭,可以用js代码,也可以用button属性:data-dismiss="css选择器"关闭;

data-toggle="css选择器"打开

短ID设计与后端代码开发

1.增加新增大章功能,后端代码开发,完成前后端联调,保存成功

面试:为什么不用自增ID?自增ID至少有三个问题:

1.id 是连续,容易被探测;

2.需要+1次查询才能得到id 的值;

3.分布式存储中,id 会出现重复

uuid 是根据机器、时间等多个维度生成的32位16进制数,有生之年不会重复。我在uuid 的基础上,封装了8位短uuid。

短ID 是根据将32位ID,转为62进制8位ID,减少存储空间。

原理是将uuid 转为10进制,再对62取余。也可以再添加两个符号,转为64进制。

xxxx.sout 用到了postfix

目前使用BeanUtil.copyProperties,需要多行代码,后续会对其做封装优化。

chapter变量用于绑定form 表单的数据。

将绑定好数据的chapter 作为前后端交互传参

增加复制工具类CopyUtil

1.增加复制工具类CopyUtil,封装BeanUtils.copyProperties,简化单实体复制和列表复制的代码

该工具类封装了BeanUtils.copyProperties,利用反射,牺牲一点性能(可忽略不计),换取开发效率。

统一返回参数ResponseDto

纯接口应用,一般会规范固定的请求参数,如版本号、请求流水等;再规范固定的返回参数,如返回码、返回描述等。方便调用方统一处理。

1.增加统一返回实体类ResponseDto,前后端代码针对ResponseDto 做修改

2.chapter 保存成功后关闭表单,并刷新列表

3.为modal增加id 属性

ResponseDto.java

ChapterController.java?

response.data 就相当于responseDto

列表查询业务上一般都是成功的(查不到数据也是成功的,所以不需要判断success。保存有可能失败,所以需要判断success)

验证功能:

1.列表查询没问题;

2.保存功能没问题;

3.保存成功后关闭modal,并刷新列表。

css 选择器,可以通过id、class、标签等选择页面元素

问题:同一个页面有多个modal时,用class选择时,会出现重复,所以需要给每个modal增加id属性

需要测试modal相关的操作,点击新增,点击关闭,点击取消,点击保存,点击空白

5-7 修改删除大章功能

增加大章修改功能

1.增加修改大章功能,新增和修改用同一个保存功能,通过传入的参数id 有没有值来判断

新增和编辑功能弹出来的模态框是同一个。vue、controller、service 调用的都是同一个方法,只是到service层再根据id 是否有值来判断是新增还是删除

hidden-md:中等屏幕隐藏,其它可见;
hidden-lg:大屏幕隐藏,其它可见。
相反的有visible-xx,具体可参考https://v3.bootcss.com/css/#responsive-utilities-classes

在响应式页面中,同一个页面在大屏和小屏里显示的内容不太一样,大屏显示的内容更多,hidden-xx和visible-xx会经常用到

 <div class="hidden-md hidden-lg">
            <div class="inline pos-rel">
              <button class="btn btn-minier btn-primary dropdown-toggle" data-toggle="dropdown" data-position="auto">
                <i class="ace-icon fa fa-cog icon-only bigger-110"></i>
              </button>

              <ul class="dropdown-menu dropdown-only-icon dropdown-yellow dropdown-menu-right dropdown-caret dropdown-close">
                <li>
                  <a href="#" class="tooltip-info" data-rel="tooltip" title="View">
                                  <span class="blue">
                                    <i class="ace-icon fa fa-search-plus bigger-120"></i>
                                  </span>
                  </a>
                </li>

                <li>
                  <a href="#" class="tooltip-success" data-rel="tooltip" title="Edit">
                                  <span class="green">
                                    <i class="ace-icon fa fa-pencil-square-o bigger-120"></i>
                                  </span>
                  </a>
                </li>

                <li>
                  <a href="#" class="tooltip-error" data-rel="tooltip" title="Delete">
                                  <span class="red">
                                    <i class="ace-icon fa fa-trash-o bigger-120"></i>
                                  </span>
                  </a>
                </li>
              </ul>
            </div>
          </div>

1.将表格每一行数据传递到edit中做处理

2.将传递过来的一行数据chapter,赋给vue变量_this.chapter

vue变量_this.chapter会通过v-model属性和form表单做数据绑定

数据显示:将表格行数据显示到表单。反过来,数据修改:修改表单影响表格行数据。

_this.chapter = $.extend({},chapter);

发现问题:对文本框编辑后,点新增弹出文本框,会带出上一次编辑过的值。

_this.chapter = {};

增加大章删除功能

1.增加删除大章功能

delete 是js 的关键字,vue 方法里不能使用js 关键字

restful 是一种请求风格。简单的理解:通过看url 就能知道这个请求是要对什么资源做什么操作

后端的代码还没写,所以你报错404,需要熟记常用的返回码,如:200,301,400,401,403,404,500,503等

5-8 集成前端通用组件

集成sweetalert 用于界面消息确认框

1.集成sweetalert2,删除时弹出确认框

删除是一个有风险的操作,需要有确认的动作。

SweetAlert2 - a beautiful, responsive, customizable and accessible (WAI-ARIA) replacement for JavaScript's popup boxes

?

制作消息提示框

1.制作toast组件,内部用sweetalert2实现

通过修改timer可以设置弹出的时长,设置icon可以设置成成功、错误、警告等。

养成一种思维,将通用的代码做成组件

如果组件包含html代码,可以用vue组件;如果组件只有js代码,可以用原生的js

toast 是js 全局变量,可以在其它js 文件中使用,也可以在vue 组件中直接使用。

集成blockUI 用于界面等待框

1.制作Loading组件,内部用jquery blockui插件实现

等待框的作用:

1.让用户知道,后端正在处理,耐心等待;

2.防止用户恶意重复点击。

malsup.com/jquery/block/

BootCDN - Bootstrap 中文网开源项目免费 CDN 加速服务

本身loading功能不复杂,jquery blockUI 插件已经多年没更新了,也说明很稳定了。

一般使用压缩过的

1.修改Toastr 组件的显示效果,更大气
2.制作Confirm 组件

组件化的好处:只需要修改组件代码,就可以改变组件的样式,使用的地方完全不用动

简单理解 js 回调函数:将一个函数以参数的形式传递到另一个函数里去执行。在自定义组件中经常用到回调函数。

将变化的代码(组件无关的代码)作为回调函数传递进来

原来的代码先注释掉

5-9 代码优化

前端代码校验

1.增加工具类tool.js和校验类validator.js

2.大章保存非空和长度增加校验

validator.js

tool.js

后端代码校验

1.增加后端校验工具类ValidatorUtil

2.增加统一异常处理,ControllerExceptionHandler,关键字:@ControllerAdvice

新增什么都不填写,依旧保存成功

这种数据是不对的

前后端分离的项目,后端接口需要增加和前端一样的校验,防止被绕过前端界面,利用第三方工具如postman,直接访问后端接口

自定义异常可以继承RuntimeException 或?Exception。一般项目内部的业务异常,可以用RuntimeException,不需要try catch。如果是开发一些框架或工具类,明确告诉外部需要做异常处理的,可以用Exception。另外还需要考虑事务中的异常处理,后续介绍

测试一下

刷新

没有新增

说明我们校验生效了

现象:后端出异常,导致前后收不到结果,vue中的.then方法没有执行,等待框没有关闭,导致不能继续任何操作,只能刷新页面

选中代码,ctrl+alt+T,选择try/catch

但是这么做,如果有多个地方都用到,依然比较复杂

@ControllerAdvice 是Controller增强其,可以对Controller 做统一的处理,如异常处理,数据处理等。

前端也需要增加一下

测试

但还有一个安全问题

有时候我们的接口原本是不对外的,或者只跟特定的第三方应用做对接,这时为了内部安全,不应该把参数的校验规则暴露出去,所以需要模糊返回信息。类似登录接口应该返回“用户名或密码错误”,而不是“用户名不存在”,或“密码错误”(容易被探测)

如果开发过程中提示“请求参数异常”,说明后端有校验拦截,前端没有,此时应该把前端校验加上

使用AOP制作统一日志输出

1.增加日志AOP,统一日志输出

2.logback 增加打印日志跟踪号

问题:从打印的日志内容,看不出业务信息。日志不仅开发时有用,生产运维时查看业务日志也很重要,所以需要把日志加上业务信息。

统一日志处理,可以用AOP,也可以用Spring拦截器

package com.course.server.config;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.course.server.util.UuidUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
@Aspect
@Component
public class LogAspect {
    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    /** 定义一个切点 */
    @Pointcut("execution(public * com.course.*.controller..*Controller.*(..))")
    public void controllerPointcut() {}

    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 日志编号
        MDC.put("UUID", UuidUtil.getShortUuid());

        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();

        // 打印业务操作
        String nameCn = "";
        if (name.contains("list") || name.contains("query")) {
            nameCn = "查询";
        } else if (name.contains("save")) {
            nameCn = "保存";
        } else if (name.contains("delete")) {
            nameCn = "删除";
        } else {
            nameCn = "操作";
        }

        // 使用反射,获取业务名称
        Class clazz = signature.getDeclaringType();
        Field field;
        String businessName = "";
        try {
            field = clazz.getField("BUSINESS_NAME");
            if (!StringUtils.isEmpty(field)) {
                businessName = (String) field.get(clazz);
            }
        } catch (NoSuchFieldException e) {
            LOG.error("未获取到业务名称");
        } catch (SecurityException e) {
            LOG.error("获取业务名称失败", e);
        }

        // 打印请求信息
        LOG.info("------------- 【{}】{}开始 -------------", businessName, nameCn);
        LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
        LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
        LOG.info("远程地址: {}", request.getRemoteAddr());

        // 打印请求参数
        Object[] args = joinPoint.getArgs();
        Object[] arguments  = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"shard"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter)); // 为空的会不打印,但是像图片等长字段也会打印
    }

    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "shard"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
        LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
        return result;
    }

}

约定优于配置。又一个约定:查询类接口以list或query开头,保存用save开头,删除用delete开头

敏感字段时不能明文打印或存储,比如身份证,手机号等。

后续会介绍图片上传,图片会转为base64 文本,太长,没有打印的必要,且占用空间,可以不打印。

一个日志跟踪号用来标识一次请求。生产环境中,往往同时会打印多个请求的日志,通过“grep 日志跟踪号” 可以查找出一次请求的所有日志。

1.前端增加统一日志输出

2.加上注释?

添加了一些注释

?删除了输出的日志

对ChapterController.java也是进行了注释和删除

ChapterService.java也是进行了注释和删除

文章来源:https://blog.csdn.net/qq_52963857/article/details/134844674
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。