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

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也是进行了注释和删除

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/246282.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

时序分解 | Matlab实现DBO-VMD基于蜣螂优化算法优化VMD变分模态分解时间序列信号分解

时序分解 | Matlab实现DBO-VMD基于蜣螂优化算法优化VMD变分模态分解时间序列信号分解 目录 时序分解 | Matlab实现DBO-VMD基于蜣螂优化算法优化VMD变分模态分解时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.利用蜣螂优化算法优化VMD中的参数k、a&…

【PostgreSQL】从零开始:(四)使用PgAdmin4连接数据库,及工具使用

登陆pgAdmin4 连接数据库 填写连接名称 填写连接信息 错误信息如下 解决办法 1.登陆数据库服务器切换到postgres用户 [rootpostgre-sql ~]# su - postgres 上一次登录&#xff1a;三 12月 13 18:10:00 CST 2023pts/0 上 [postgrespostgre-sql ~]$ 2.查看数据库进程 [postgre…

Linux——MySQL备份与恢复

一、数据库备份概述 1、数据备份的重要性 在企业中数据的价值至关重要&#xff0c;数据保障了企业业务的正常运行。因此&#xff0e;数据的安全性及数据的可靠性是运维的重中之重&#xff0c;任何数据的丢失都可能对企业产生严重的后果。通常情况下造成数据丢失的原因有如下几种…

JVM 详解(JVM组成部分、双亲委派机制、垃圾回收算法、回收器、回收类型、了解调优思路)

目录 JVM 详解&#xff08;JVM组成部分、双亲委派机制、垃圾回收算法、回收器、回收类型、了解调优思路&#xff09;1、概念&#xff1a;什么是 JVM ?JVM 的作用&#xff1f; 2、JVM 的主要组成部分&#xff1f;类加载器&#xff08;Class Loader&#xff09;&#xff1a;简单…

ECharts实现数据可视化入门教程

ECharts介绍 Apache ECharts ECharts是一个使用 JavaScript 实现的开源可视化库 入门教程 第一步&#xff1a;下载echarts.js文件 下载地址&#xff1a;下载 - Apache ECharts 点击Dist 点击echarts.min.js并保存 将下载好的.js文件引入到项目的js文件中 第二步&#xff1a;…

[多线程]线程池

目录 1.前言 2. Java中的线程池以及参数介绍 2.1 核心线程数和最大线程数 2.2最大空闲存活时间 2.3任务队列和线程工厂 2.4 拒绝策略(最重要&#xff09; 2.5 线程池的类型 3.线程池的大小如何确定 4.手动写一个线程池 1.前言 我们知道.在开发过程中.为了效率,会引进很…

理解JSX:提高前端开发效率的关键(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

CGAL的最优传输曲线重构

1、介绍 此程序包实现了一种重建和简化二维点集的方法。输入是一组具有质量属性的二维点&#xff0c;可能受到噪声和离群值的干扰。输出是一组线段和孤立点&#xff0c;它们近似于输入点&#xff0c;如下图所示。质量属性与每个点的近似重要性有关。 左&#xff1a;输入点集受到…

SWPU NSS新生赛

&#x1f60b;大家好&#xff0c;我是YAy_17&#xff0c;是一枚爱好网安的小白&#xff0c;正在自学ing。 本人水平有限&#xff0c;欢迎各位大佬指点&#xff0c;一起学习&#x1f497;&#xff0c;一起进步⭐️。 ⭐️此后如竟没有炬火&#xff0c;我便是唯一的光。⭐️ 最近…

网页图标素材免费下载网站

这里是几个可以免费下载网页图标素材的的网站。这些个网站里的图表和素材&#xff0c;应该是都可以免费下载的。&#xff08;至少我下载了几个素材是没有花钱的&#xff09; Flaticon iconArchive freepik 4. iconmonstr 5. Icons and Photos For Everything 如果想下载图片&a…

在项目中,使用drawio创建一个共享协作看板

在项目中&#xff0c;使用drawio创建一个共享协作看板 drawio是一款强大的图表绘制软件&#xff0c;支持在线云端版本以及windows, macOS, linux安装版。 如果想在线直接使用&#xff0c;则直接输入网址draw.io或者使用drawon(桌案), drawon.cn内部完整的集成了drawio的所有功…

【C语言(十一)】

C语言内存函数 一、memcpy使用和模拟实现 void * memcpy ( void * destination, const void * source, size_t num ); • 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。 • 这个函数在遇到 \0 的时候并不会停下来。 • 如果sourc…

【每日一题】【12.14】2132.用邮票贴满网格图

&#x1f525;博客主页&#xff1a; A_SHOWY&#x1f3a5;系列专栏&#xff1a;力扣刷题总结录 数据结构 云计算 数字图像处理 力扣每日一题_ 2132. 用邮票贴满网格图https://leetcode.cn/problems/stamping-the-grid/ 今天的每日一题又是一道恶心的困难题目&#xff0c;花…

【数据结构期末复习】完善中

文章目录 二叉树的三种遍历方式怎么看遍历结果相关题目&#xff1a;已知一颗二叉树的后续遍历序列为&#xff1a;GFEDCBA;中序遍历序列为&#xff1a;FGAEBDC。画出这棵二叉树思路代码版 先序线索树二叉树转树、或森林树转二叉树二叉树转树二叉树转森林森林转二叉树 二叉树的三…

Java之BigDecimal详解

一、BigDecimal概述 Java在java.math包中提供的API类BigDecimal&#xff0c;用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数&#xff0c;但在实际应用中&#xff0c;可能需要对更大或者更小的数进行运算和处理。一般情况下&#xff0c;对…

阿里云人工智能平台PAI多篇论文入选EMNLP 2023

近期&#xff0c;阿里云人工智能平台PAI主导的多篇论文在EMNLP2023上入选。EMNLP是人工智能自然语言处理领域的顶级国际会议&#xff0c;聚焦于自然语言处理技术在各个应用场景的学术研究&#xff0c;尤其重视自然语言处理的实证研究。该会议曾推动了预训练语言模型、文本挖掘、…

【基于卷积神经网络的疲劳检测与预警系统的设计与实现】

基于卷积神经网络的疲劳检测与预警系统的设计与实现 引言数据集介绍技术与工具1. OpenCV2. TensorFlow3. 卷积神经网络&#xff08;CNN&#xff09; 系统功能模块1. 视频采集模块2. 图像预处理模块3. 人脸识别模块4. 疲劳程度判别模块5. 报警模块 系统设计创新点1. 实时监测与预…

js解析.shp文件

效果图 原理与源码 本文采用的是shapefile.js工具 这里是他的npm地址 https://www.npmjs.com/package/shapefile 这是他的unpkg地址&#xff0c;可以点开查看源码 https://unpkg.com/shapefile0.6.6/dist/shapefile.js 这个最关键的核心问题是如何用这个工具&#xff0c;网上…

如何正确使用缓存来提升系统性能

文章目录 引言什么时候适合加缓存&#xff1f;示例1示例2&#xff1a;示例3&#xff1a; 缓存应该怎么配置&#xff1f;数据分布**缓存容量大小&#xff1a;**数据淘汰策略 缓存的副作用总结 引言 在上一篇文章IO密集型服务提升性能的三种方法中&#xff0c;我们提到了三种优化…

如何在iPad Pro上实现SSH远程连接服务器并进行云端编程开发【内网穿透】

文章目录 前言1. 在iPad下载Code APP2.安装cpolar内网穿透2.1 cpolar 安装2.2 创建TCP隧道 3. iPad远程vscode4. 配置固定TCP端口地址4.1 保留固定TCP地址4.2 配置固定的TCP端口地址4.3 使用固定TCP地址远程vscode 前言 本文主要介绍开源iPad应用IDE如何下载安装&#xff0c;并…