SSM-项目实战
SSM-项目实战
开发环境搭建
-
创建数据库
1
create database rjwm character set utf8mb4;
-
导入数据
1
source db_reggie.sql
-
创建
springboot
项目 -
创建
vue2
项目 1
2
3
4
5
6
7
8
9
10
11// 创建 vue 项目
vue create vue-rjwm
// 添加 element-ui
vue add element
// 添加 axios
yarn add axios
// 添加vue-router
yarn add vue-router@3
// 添加 less less-loader
yarn add less@3
yarn add lesss-loader@3
跨域解决方案
@CrossOrigin
注解的使用 nginx
搭建 api
- 微服务网关搭建解决跨域
vue
脚手架 devServer
配置代理
SpringBoot
-
创建
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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<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>org.example</groupId>
<artifactId>rjwm</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project> -
配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 8081
spring:
application:
name: rjwm
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/rjwm?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
# 在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
-
springboot
启动 端口配置 测试 -
vue
启动测试 接口测试
-
统一返回处理
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
47package com.coderitl.rjwm.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
public class R<T> {
private Integer code; //编码:1 成功,0 和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> success(T object, String msg) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
r.msg = msg;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
-
模块的导入导出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 默认导出 只能有一个在一个模块中
export default xxx
// 导出
export function foo(){}
// 导出变量
exoport let foo = 10;
// 导入default
import xx from "defaultExport"
import {xxx} from "xx"
// 统一导入
import * as asNewName from "xx"
asNewName.xxx -
封装
axios
1
2
3
4
5
6
7
8
9
10
11
12import axios from "axios"
export function request(config) {
const instance = axios.create({
baseURL: 'http://localhost:8081',
timeout: 5000
})
// 拦截器
// 发送真正的网络请求(Promise)
return instance(config)
}-
模块化
1
2
3
4
5
6
7
8
9
10
11
12// 员工模块
import {request} from "@/api/index";
// 登录
export function loginApi(data) {
return request({
url: '/employee/login',
method: 'post',
data
})
}
... -
使用
1
2
3
4
5// 后期容易维护
import {loginApi} from "@/api/employee";
let res = await loginApi(this.loginForm)
-
后台系统登录功能
-
流程分析与实现
流程分析 实现 -
数据获取
表单数据传递
-
路由处理
-
前端路由拦截处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 挂载路由导航守卫
router.beforeEach((to, from, next) => {
// to 将要访问的路径
if (to.path === '/login') {
return next();
}
// from 代表从那个路径跳转而来
const tokenStr = window.sessionStorage.getItem('userInfo')
if (!tokenStr) {
console.log(tokenStr);
return next('/login');
}
// next 放行 next('/home')强制跳转
next();
}); -
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
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
78package com.coderitl.rjwm.filter;
import com.alibaba.fastjson.JSON;
import com.coderitl.rjwm.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 检查用户是否已经登录
*/
public class checkLogin implements Filter {
// 路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 日志记录
log.info("登录拦截器拦截: {}", request.getRequestURI());
// 1. 获取本次请求的`uri`
String requestURI = request.getRequestURI();
// 定义不需要处理的请求路径
String[] urls = new String[]{
// 登录请求不处理
"/employee/login",
// 退出请求不处理
"/employee/logout"
};
// 2. 判断本次请求是否需要处理
boolean notInUrls = check(urls, requestURI);
// 3. 如果不需要处理,则直接放行
if (notInUrls) {
// 直接放行
filterChain.doFilter(request, response);
return;
}
// 4. 判断登录状态,如果已经登录, 则直接放行
if (request.getSession().getAttribute("employee") != null) {
// 直接放行
filterChain.doFilter(request, response);
return;
}
// 5. 如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次去请求是否需要放行
*
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
// 路径是否匹配
boolean match = PATH_MATCHER.match(url, requestURI);
// 如果匹配,返回 true
if (match) {
return true;
}
}
// 不匹配
return false;
}
}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// 前端拦截器 axios
// 拦截器
instance.interceptors.response.use(
res => {
console.log('---响应拦截器---', res);
console.log(res.data.code)
if (res.data.code === 0 && res.data.msg === 'NOTLOGIN') {
// 返回登录页面
window.top.location.href = '/login';
} else {
console.log("login ok...")
sessionStorage.setItem("userInfo",JSON.stringify(res))
return res.data;
}
},
error => {
let { message } = error;
if (message == 'Network Error') {
message = '后端接口连接异常';
} else if (message.includes('timeout')) {
message = '系统接口请求超时';
} else if (message.includes('Request failed with status code')) {
message =
'系统接口' + message.substr(message.length - 3) + '异常';
}
window.vant.Notify({
message: message,
type: 'warning',
duration: 5 * 1000,
});
//window.top.location.href = '/front/page/no-wify.html'
return Promise.reject(error);
}
);1
2// 登陆的 处理事件处
data 解构时需要注意 -
在启动类上加入注解
@ServletComponentScan
-
完善过滤器的处理逻辑
-
具体实现
- 获取本次请求的
uri
- 判断本次请求是否需要处理
- 如果不需要处理,
则直接放行 - 判断登录状态,
如果已经登录, 则直接放行 - 如果未登录则返回未登录结果
- 获取本次请求的
-
流程图分析
过滤器实现
-
-
-
登录按钮事件处理
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// 登录按钮点击事件
handleLogin() {
// 登录预验证
this.$refs.loginFormRef.validate(async valid => {
console.log(valid);
// 预验证未通过时终止
if (!valid) return;
// 预验证通过发起ajax 请求 将 data 解构重命名为 res
const { data: res } = await loginApi(this.loginForm);
console.log(res);
// 1: 前后端约定 "1" 为登录成功
if (String(res.code) === '1') {
this.$message.success(res.msg);
// 存储在 sessionStorage 中鉴权
sessionStorage.setItem(
'userInfo',
JSON.stringify(res.data)
);
// 跳转 home
this.$router.push('/home');
} else {
// 响应错误信息
this.$message.error(res.msg);
}
});
},
}
-
登录
登录
前端路由
-
路由实现
1
2
3
4
5
6
7
8
9
10
11{
path: '/home',
redirect: '/employeeinfo',
component: () => import('@/components/Home.vue'),
children: [
{
path: '/employeeinfo',
component: () => import('@/pages/employee/EmployeeInfo.vue'),
},
],
}
-
路由分析
子路由
-
需求
1
/home/employee
新增员工
-
原型图
添加员工信息
-
数据模型
新增员工,
其实就是将我们新增页面录入的员工数据插入到 employee
表中, 字段 username
加入了 唯一约束
,因为 username
是员工的登录账号, 必须是唯一的 -
代码开发
- 执行过程
- 页面发送
ajax
请求, 将新增的员工页面中输入的数据以 json
的形式提交到服务端 - 服务端
Controller
接受页面提交的数据并调用 Service
将数据进行保存 Service
调用 Mapper | dao
操作数据库, 保存数据
- 页面发送
- 执行过程
-
实体分析
-
username
唯一约束 (实现用户名唯一约束) -
status
禁用与正常
-
-
请求与参数问题
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// 添加员工
export function saveEmployee(params) {
return request({
url: '/employee/saveemployee',
method: 'post',
// json数据
data: { ...params }
})
}
// 保存与保存继续添加
submitForm() {
// 组织数据为 json
const params = {
...this.ruleForm,
// 改变存储
sex: this.ruleForm.sex === '男' ? '0' : '1'
}
// 预验证
this.$refs.ruleForm.validate((valid) => {
// 如果预验证失败,终止
if (!valid) return this.$message.error('信息错误')
if (valid) {
// 发起 axios 请求 params 就是 json 格式数据
saveEmployee(params).then((res) => {
console.log(res)
// 再次发起请求 pageApi(this.queryInfo)
})
}
})
}, -
控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 添加员工信息
public R<String> save(HttpServletRequest request, { Employee employee)
log.info("添加员工信息: {}", employee.toString());
// 设置初始密码 并使用 md5 加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
// 设置创建时间
employee.setCreateTime(LocalDateTime.now());
// 设置更新时间
employee.setUpdateTime(LocalDateTime.now());
// 获取当前登录用户的 id
log.info("empId: {}", request.getServletContext().getAttribute("employee"));
if (request.getServletContext().getAttribute("employee") != null) {
Long empId = (Long) request.getServletContext().getAttribute("employee");
// 设置创建的用户
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
log.info("组织完毕: {}", employee);
// 执行添加
employeeService.save(employee);
}
return R.success("新增员工成功");
} -
sessionID
存在生命周期 直接使用
request.setAttribute("","")
存储时, 对于前后端分离项目 session
是有生命周期的, 在不同的接口方法中 session
无法获取 request
存取的 session
状态 -
解决方案
1
2
3
4
5
6// 提示 session 的作用域
(登录时)
request.getServletContext().setAttribute("employee", emp.getId());
// 需要获取的地方
request.getServletContext().getAttribute("employee")
-
-
异常处理
-
异常产生原因
字段
username
添加了 unique
约束 日志输出 -
解决
-
在
Controller
方法中加入 try..catch
进行异常捕获 -
使用异常处理器进行全局异常捕获
( 推荐
)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
32package com.coderitl.rjwm.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
* annotations = {RestController.class, Controller.class} 扫描带有如下注解的控制器进行处理
*/
// 返回 JSON 数据
// 日志
public class GlobalExceptionHandler {
// 处理 username 重复异常
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
if (ex.getMessage().contains("Duplicate entry")) {
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "用户已存在!";
return R.error(msg);
}
return R.error("未知错误");
}
}
-
-
分页查询 (模糊查询)
-
演示
分页实现
-
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package com.coderitl.rjwm.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// mybatis-plus 配置拦截器
public class MybatisPlusPageConfig {
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 接口实现
/**
* 搜索: 分页 模糊查询
*
* @param page
* @param pagesize
* @param username
* @return
*/
public R<Page> page(Integer page, Integer pagesize, { String username)
log.info("pagg: {}, pagesize: {} username: {}", page, pagesize, username);
// 构造分页构造器
Page pageInfo = new Page(page, pagesize);
// 构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// // 不等于空 SQL 才会出现
queryWrapper.like(StringUtils.isNotEmpty(username), Employee::getUsername, username);
// // 执行查询
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo, "成功获取数据所有数据!");
}
-
使用
element-组件
1
2
3
4
5
6
7
8
9<el-pagination
@size-change='handleSizeChange'
@current-change='handleCurrentChange'
:current-page='queryInfo.page'
:page-sizes='[5, 10, 25, 50]'
:page-size='queryInfo.pagesize'
layout='total, sizes, prev, pager, next, jumper'
:total='total'
></el-pagination>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// 分页
export function pageApi(params) {
return request({
url: '/employee/page',
method: 'get',
params
})
}
// 发送请求
// 控制 pageSize 改变
async handleSizeChange(val) {
// 控制 pageSize
this.queryInfo.pagesize = val
const parmas = {
...this.queryInfo
}
const { data: res } = await pageApi(parmas)
this.tableData = res.data.records
},
// 控制 page 改变
async handleCurrentChange(val) {
this.queryInfo.page = val
const parmas = {
...this.queryInfo
}
// 再次发起请求
const { data: res } = await pageApi(parmas)
this.tableData = res.data.records
},
公共字段自动填充
-
实现步骤
-
在实体类的属性上加入
@TableField
注解, 指定自动填充的策略 1
2
3
4
5
6
7
8
9
10
11
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Long createUser;
private Long updateUser; -
安装框架要求编写元数据对象处理器,
在此类中统一为公共字段赋值, 此类需要实现 MetaObjectHandler
接口
-
ThreadLocal
-
什么是
ThreadLocal
ThreadLocal
并不是一个 Thread
,而是Thread
的局部变量, 当使用 ThreadLocal
维护变量时, ThreadLocal
为每个使用该变量的线程提供独立的变量副本, 所以每一个线程都可以独立的改变自己的副本,而不是影响其他线程所对应的副本, ThreadLocal
为每个线程提供单独一份存储空间, 具有线程隔离的效果, 只有在线程内才能获取到对应的值,线程外则不能访问 -
常用方法
-
set(T value)
1
public void set(T value) // 设置当前线程的线程局部变量的值
-
get()
1
publiv T get() // 返回当前线程所对应的线程局部变量的值
我们可以在
filter
的 doFilter
方法中获取对应当前登录用户 id
,并调用ThreadLocal
的 set
方法来设置当前线程的局部变量的值 ( 用户
),id 然后再 MyMetaObjectHandler
的 updateFill
方法中调用 ThreadLocal
的 get
方法来获取当前所对应的线程局部变量的值 ( 用户
)id -