为了让更多同学学习到前后端分离管理系统的搭建过程,这里我写了详细的开发过程的文档,使用的是springsecurity + jwt + vue的技术栈组合,如果有帮助,别忘了点个赞和关注我的公众号哈!
首发公众号:MarkerHub
作者:吕一明
项目源码:关注公众号 MarkerHub 回复【 234 】获取
项目视频:www.bilibili.com/video/BV1af…
转载请保留此声明,感谢!
1. 前言
从零开始搭建一个项目骨架,最好选择合适熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以Springboot作为我们的框架基础,这是离不开的了。
然后数据层,我们常用的是Mybatis,易上手,方便维护。但是单表操作比较困难,特别是添加字段或减少字段的时候,比较繁琐,所以这里我推荐使用Mybatis Plus(mp.baomidou.com/),为简化开发而生,只… CRUD 操作,从而节省大量时间。
作为一个项目骨架,权限也是我们不能忽略的,上一个项目vueblog我们使用了shiro,但是有些同学想学学SpringSecurity,所以这一期我们使用security作为我们的权限控制和会话控制的框架。
考虑到项目可能需要部署多台,一些需要共享的信息就保存在中间件中,Redis是现在主流的缓存中间件,也适合我们的项目。
然后因为前后端分离,所以我们使用jwt作为我们用户身份凭证,并且session我们会禁用,这样以前传统项目使用的方式我们可能就不再适合使用,这点需要注意了。
ok,我们现在就开始搭建我们的项目脚手架!
技术栈:
- SpringBoot
- mybatis plus
- spring security
- lombok
- redis
- hibernate validatior
- jwt
2. 新建springboot项目,注意版本
这里,我们使用IDEA来开发我们项目,新建步骤比较简单,我们就不截图了。
开发工具与环境:
- idea
- mysql
- jdk 8
- maven3.3.9
新建好的项目结构如下,SpringBoot版本使用的目前最新的2.4.0版本
pom的jar包导入如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/>
</parent>
<groupId>com.markerhub</groupId>
<artifactId>vueadmin-java</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vueadmin-java</name>
<description>公众号:MarkerHub</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
复制代码
- devtools:项目的热加载重启插件
- lombok:简化代码的工具
3. 整合mybatis plus,生成代码
接下来,我们来整合mybatis plus,让项目能完成基本的增删改查操作。步骤很简单:可以去官网看看:mp.baomidou.com/guide/
第一步:导入jar包
pom中导入mybatis plus的jar包,因为后面会涉及到代码生成,所以我们还需要导入页面模板引擎,这里我们用的是freemarker。
<!--整合mybatis plus https://baomidou.com/-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--mp代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
复制代码
第二步:然后去写配置文件
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: admin
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
复制代码
上面除了配置数据库的信息,还配置了myabtis plus的mapper的xml文件的扫描路径,这一步不要忘记了。然后因为前段默认是8080端口了,所以后端我们设置为8081端口,防止端口冲突。
第三步:开启mapper接口扫描,添加分页、防全表更新插件
新建一个包:通过@mapperScan注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。
- com.markerhub.config.MybatisPlusConfig
@Configuration
@MapperScan("com.markerhub.mapper")
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 防止全表更新和删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
复制代码
上面代码中,我们给Mybatis plus添加了2个拦截器,这是根据mp官网配置的:
- PaginationInnerInterceptor:新的分页插件
- BlockAttackInnerInterceptor:防止全表更新和删除
第四步:创建数据库和表
因为是后台管理系统的权限模块,所以我们需要考虑的表主要就几个:用户表、角色表、菜单权限表、以及关联的用户角色中间表、菜单角色中间表。就5个表,至于什么字段其实都听随意的,用户表里面除了用户名、密码字段必要,其他其实都听随意,然后角色和菜单我们可以参考一下其他的系统、或者自己在做项目的过程中需要的时候在添加也行,反正重新生成代码也是非常简便的事情,综合考虑,数据库名称为vueadmin,我们建表语句如下:
- vueadmin.sql
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50717
Source Host : localhost:3306
Source Database : vueadmin
Target Server Type : MYSQL
Target Server Version : 50717
File Encoding : 65001
Date: 2021-01-23 09:41:50
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` varchar(64) NOT NULL,
`path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
`component` varchar(255) DEFAULT NULL,
`type` int(5) NOT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
`orderNum` int(11) DEFAULT NULL COMMENT '排序',
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`code` varchar(64) NOT NULL,
`remark` varchar(64) DEFAULT NULL COMMENT '备注',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE,
UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL,
`menu_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL,
`email` varchar(64) DEFAULT NULL,
`city` varchar(64) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`last_login` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;
复制代码
第五步:代码生成
- 获取项目数据库所对应表和字段的信息
- 新建一个freemarker的页面模板 - SysUser.java.ftl - ${baseEntity}
- 提供相关需要进行渲染的动态数据 - BaseEntity、表字段、注释、baseEntity=SuperEntity
- 使用freemarker模板引擎进行渲染! - SysUser.java
# 获取表
SELECT
*
FROM
information_schema. TABLES
WHERE
TABLE_SCHEMA = (SELECT DATABASE());
# 获取字段
SELECT
*
FROM
information_schema. COLUMNS
WHERE
TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = "sys_user";
复制代码
有了数据库之后,那么现在就已经可以使用mybatis plus了,官方给我们提供了一个代码生成器,然后我写上自己的参数之后,就可以直接根据数据库表信息生成entity、service、mapper等接口和实现类。 因为代码比较长,就不贴出来了,说明一下重点:
- com.markerhub.CodeGenerator
上面代码生成的过程中,我默认所有的实体类都继承BaseEntity,控制器都继承BaseController,所以在代码生成之前,最好先编写这两个基类:
- com.markerhub.entity.BaseEntity
@Data
public class BaseEntity implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private LocalDateTime created;
private LocalDateTime updated;
private Integer statu;
}
复制代码
- com.markerhub.controller.BaseController
public class BaseController {
@Autowired
HttpServletRequest req;
}
复制代码
然后我们单独运行CodeGenerator的main方法,注意调整CodeGenerator的数据库连接、账号密码啥的,然后我们输入表名称,通过逗号隔开:sys_menu,sys_role,sys_role_menu,sys_user,sys_user_role 执行结果成功:
然后我们生成了一些代码如下:
这里有点需要注意,因为关联的用户角色中间表、菜单角色中间表我们是没有created等几个公共字段的,所以我们把这两个实体继承BaseEntity去掉:
最后这样子的:
@Data
public class SysRoleMenu {
...
}
复制代码
简洁!方便!经过上面的步骤,基本上我们已经把mybatis plus框架集成到项目中了,并且也生成了基本的代码,省了好多功夫。然后我们做个简单测试:
- com.markerhub.controller.TestController
@RestController
public class TestController {
@Autowired
SysUserService userService;
@GetMapping("/test")
public Object test() {
return userService.list();
}
}
复制代码
然后sys_user随意添加几条数据,结果如下:
ok,毛什么问题,大家不用在意密码是怎么生成的,后面我们会说到,你现在随意填写就好了。对了,好多人问我的浏览器的json数据怎么显示这么好看,这是因为我用了JSONView这个插件:
4. 结果封装
因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。
这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的
- 是否成功,可用code表示(如200表示成功,400表示异常)
- 结果消息
- 结果数据
所以可得到封装如下:
- com.markerhub.common.lang.Result
@Data
public class Result implements Serializable {
private int code; // 200是正常,非200表示异常
private String msg;
private Object data;
public static Result succ(Object data) {
return succ(200, "操作成功", data);
}
public static Result succ(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
public static Result fail(String msg, Object data) {
return fail(400, msg, data);
}
public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
}
复制代码
另外出了在结果封装类上的code可以提现数据是否正常,我们还可以通过http的状态码来提现访问是否遇到了异常,比如401表示五权限拒绝访问等,注意灵活使用。
5. 全局异常处理
有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。
处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。
步骤二、定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。
- com.markerhub.common.exception.GlobalExceptionHandler
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(value = AccessDeniedException.class)
public Result handler(AccessDeniedException e) {
log.info("security权限不足:----------------{}", e.getMessage());
return Result.fail("权限不足");
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) {
log.info("实体校验异常:----------------{}", e.getMessage());
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
return Result.fail(objectError.getDefaultMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:----------------{}", e.getMessage());
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:----------------{}", e);
return Result.fail(e.getMessage());
}
}
复制代码
上面我们捕捉了几个异常:
- ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
- IllegalArgumentException:处理Assert的异常
- MethodArgumentNotValidException:处理实体校验的异常
- RuntimeException:捕捉其他异常
6. 整合Spring Security
很多人不懂spring security,觉得这个框架比shiro要难,的确,security更加复杂一点,同时功能也更加强大,我们首先来看一下security的原理....
后续:请进入主页查看下篇
也可以关注公众号MarkerHub,回复234获取更详细的开发笔记和源码、视频等。