前后端交互整合

集成HTTP库axios

集成axios

1
npm install axios@0.21.0 --save

Vue3新增了setup初始化方法

跨域
可以这样理解,来自一个IP端口的页面(vue项目),要访问另一个IP端口的资源(springboot请求接口),会产生跨域访问。

  • 增加CorsConfig配置类,解决跨域问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.javami.wiki.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("/**")
.allowedOriginPatterns("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
}

}

Vue3数据绑定显示列表数据

Vue2代码结构,目前实际工作中还是以Vue2为主

Vue3生命周期函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured } from 'vue'

export default {
setup() {
onBeforeMount(() => {
// ...
})
onMounted(() => {
// ...
})
onBeforeUpdate(() => {
// ...
})
onUpdated(() => {
// ...
})
onBeforeUnmount(() => {
// ...
})
onUnmounted(() => {
// ...
})
onActivated(() => {
// ...
})
onDeactivated(() => {
// ...
})
onErrorCaptured(() => {
// ...
})
}
}

使用Vue3 ref实现数据绑定

使用Vue3 reactive实现数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script lang="ts">
import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
import axios from 'axios';

export default defineComponent({
name: 'Home',
setup() {
console.log("setup");
const ebooks = ref();
const ebooks1 = reactive({books: []});

onMounted(() => {
console.log("onMounted");
axios.get("http://127.0.0.1:8880/ebook/list?name=Spring").then((response) => {
const data = response.data;
ebooks.value = data.content;
ebooks1.books = data.content;
console.log(response);
});
});

return {
ebooks,
ebooks2: toRef(ebooks1, "books")
}
}
});
</script>

电子书列表界面展示

  • any类型是一把双刃剑

集成图标库

1
npm install @ant-design/icons-vue@5.1.8 --save
1
2
3
4
5
6
7
import * as Icons from '@ant-design/icons-vue';

// 全局使用图标
const icons: any = Icons;
for (const i in icons) {
app.component(i, icons[i]);
}

Example可以实现动态SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public List<EbookResp> list(EbookReq req) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getName())) {
criteria.andNameLike("%" + req.getName() + "%");
}
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);

// List<EbookResp> respList = new ArrayList<>();
// for (Ebook ebook : ebookList) {
// // EbookResp ebookResp = new EbookResp();
// // BeanUtils.copyProperties(ebook, ebookResp);
// // 对象复制
// EbookResp ebookResp = CopyUtil.copy(ebook, EbookResp.class);
//
// respList.add(ebookResp);
// }

// 列表复制
List<EbookResp> list = CopyUtil.copyList(ebookList, EbookResp.class);

return list;
}
  • 样式调整可以先在浏览器开发者工具里面调试,再加到页面上

Vue CLI多环境配置

  • 多环境配置文件要放在web根目录下
  • .env.xxx,后缀xxx和package.json里的指令的–mode xxx对应
  • 增加–port参数来修改启动端口自定义变量必须以VUE_APP_开头

web/.env.dev

1
2
NODE_ENV=development
VUE_APP_SERVER=http://127.0.0.1:8880

web/package.json

1
2
3
4
5
6
7
"scripts": {
"serve-dev": "vue-cli-service serve --mode dev",
"serve-prod": "vue-cli-service serve --mode prod",
"build-dev": "vue-cli-service build --mode dev",
"build-prod": "vue-cli-service build --mode prod",
"lint": "vue-cli-service lint"
},

web/src/main.ts

1
2
3

console.log('环境:', process.env.NODE_ENV);
console.log('服务端:', process.env.VUE_APP_SERVER);
  • 通过设置axios.defaults.baseURL,来统一设置后端的IP端口或域名

使用axios拦截器打印前端日志

配置axios拦截器打印请求参数和返回参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* axios拦截器
*/
axios.interceptors.request.use(function (config) {
console.log('请求参数:', config);
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('返回结果:', response);
return response;
}, error => {
console.log('返回错误:', error);
return Promise.reject(error);
});

SpringBoot过滤器的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.javami.wiki.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
public class LogFilter implements Filter {

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

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 打印请求信息
HttpServletRequest request = (HttpServletRequest) servletRequest;
LOG.info("------------- LogFilter 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());

long startTime = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}

SpringBoot拦截器的使用

拦截器的使用

  • 返回true会往后执行
  • 返回false会结束,可以利用这点来做权限拦截
    addPathPatterns(),要拦截请求
    excludePathPatterns(),排除请求,不拦截

拦截器和过滤器的相同与不同

  • 都可以用来统一处理请求,比如:打印日志、权限控制

  • 过滤器依赖于servlet容器,拦截器依赖Spring框架

  • 过滤器不用注入其它类,拦截器可注入其它类,基于这一点,建议能用拦截器的都用拦截器过滤器

  • 拦截器作用范围图解

src/main/java/com/jiawa/wiki/interceptor/LogInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.javami.wiki.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
*/
@Component
public class LogInterceptor implements HandlerInterceptor {

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LogInterceptor 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());

long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}

src/main/java/com/jiawa/wiki/config/SpringMvcConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.javami.wiki.config;

import com.javami.wiki.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

@Resource
LogInterceptor logInterceptor;

public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**").excludePathPatterns("/login");
}
}

SpringBootAOP的使用

AOP的使用

  • 需要加入AOP依赖

重要概念

  • 切点

  • 切面

  • 前置通知

  • 后置通知

  • 环绕通知

  • 打印请求参数和返回参数很重要,用于开发调试和生产运维,特别是和第三方接口之间的交互。

  • 安全规范:敏感字段不能明文打印内容太长的字段不要打印,或截取固定长度打印文件、富文本

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>

src/main/java/com/jiawa/wiki/aspect/LogAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.javami.wiki.aspect;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
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;

@Aspect
@Component
public class LogAspect {

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

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

@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {

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

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

// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));

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 = {"password", "file"};
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", "file"};
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;
}

}