Skip to main content

SpringBoot/Vue3 - 电子书网站

项目介绍

  • 基于SprintBoot/MyBatis/Vue3的电子书管理网站,功能包括身份认证系统 (单点登录),用户管理,电子书管理等.
  • 前端是使用 Vue3/AntDesign 开发的单页应用,使用 vue-router 路由与 axios 调用后端请求.
  • 使用了Redis + Token来实现简化版的单点登录.

数据库

表结构

  • category表 (id, parent, name, sort) → 电子书分类的数据和层级
  • user表 (id, login_name, name, password)
  • doc表 (id, ebook_id, parent, name, sort, view..) → 某电子书拥有的相关文档数据
  • ebook表 (id, name, category, 描述...) → 电子书数据

数据库连接

IDEA集成Database 使用IDEA集成的Database功能进行连接.

连接Localhost

数据库账号密码 输入Localhost的数据库的账号密码,即可完成连接.

连接云数据库

RDS外网地址

登录开发

登录

  1. 前端输入用户名密码
  2. 校验用户名密码
  3. 登录成功后,生成token (注意 topken的生成ID,要保证唯一)
  4. 后端保存 token (redis)={token1: 用户信息1, token2: 用户信息2 ....}
// 雪花算法生成token id
// long or string type 都可以
long token = snowFlake.nextId();

// 保存时效 = 3600s * 24hrs = 1day
redisTemplate.opsForValue().set(token.toString(), userLoginResp, 3600 * 24, TimeUnit.SECONDS);
  1. 前端保存token,使用vuex + sessionStorage来保存登录信息

校验

  1. 前端请求时,带上token(放在header)
  2. 登录拦截器(or AOP),校验token (从redis中获取token,如果命中则有效)
LoginInterceptor

// 从请求的header中获取token
String token = request.getHeader('token');
if (token == null || token.isEmpty()) // 说明token为空

// 从redis中查找token
// redis查找的get()方法对类型有要求,必须一致
Object object = redisTemplate.opsForValue().get(token);
if (object == null) return false; // 没有命中
else return true; // 找到了
  1. 校验成功则继续后面的业务
  2. 校验失败则回到登录页面

Token 与 JSON Web Token

Token + redis: token本身无意义,只需要保持唯一性。可以用md5字符串. JWT: token是有意义的,加密的,包含业务信息与用户信息。不需要用到JWT.

e.g. JWT由3部分组成 = header : {’alg(加密算法)’: ‘’HS256, ‘typ’: ‘JWT’}, data: {...}, signature: {签名}

Q&A

2.1 SpringBoot有哪些优点?

  • 快速创建独立运行的Spring项目以及与主流框架集成
  • 使用嵌入式的Servlet容器,应用无需打成WAR包
  • starters自动依赖与版本控制
  • 大量的自动配置,简化开发,也可修改默认值
  • 无需配置XML,无代码生成,开箱即用
  • 准生产环境的运行时应用监控
  • 与云计算的天然集成

2.2 SpringBoot为什么能直接运行?

SpringBoot内置嵌入式的Servlet容器,且有大量的自动配置简化开发,能做到开箱即用,所以可以直接运行

2.3 列一下常见的git命令(你都用过哪些命令)?或具体到某个命令的用法,如:怎么从远程仓库把代码取下来?

git branch 查看本地所有分支

git status 查看当前状态

git commit 提交

git branch -a 查看所有的分支

git branch -r 查看远程所有分支

git commit -am "init" 提交并且加注释

git remote add origin git@192.168.1.119:ndshow

git push origin master 将文件给推到服务器上

git remote show origin 显示远程库origin里的资源

git push origin master:hb-dev 将本地库与服务器上的库进行关联

git checkout --track origin/dev 切换到远程dev分支

git branch -D master develop 删除本地库develop

git checkout -b dev 建立一个新的本地分支dev

git merge origin/dev 将分支dev与当前分支进行合并

git checkout dev 切换到本地dev分支

git remote show 查看远程库

2.4 SVN和Git用起来有什么区别,你推荐哪一个?

 1. GIT是分布式的,SVN不是
2. GIT把内容按元数据方式存储,而SVN是按文件
3. GIT分支和SVN的分支不同
4. GIT没有一个全局的版本号,而SVN有
5. GIT的内容完整性要优于SVN
6. 综上所述,我更推荐使用Git来管理

2.5 logback日志级别都有哪些?

默认由低到高 trace < debug < info < warn < error,可以调整输出的日志级别,日志就只会在这个级别和更高级别生效。SpringBoot默认给我们使用的是info级别的,没有指定级别就使用默认级别的。

2.6 slf4j, logback, log4j是什么关系?

SpringBoot底层使用slf4j+logback的方式进行日志记录,如果使用其他的日志,SpringBoot会把其他的日志实现底层都替换为slf4j。log4j和logback是同一个作者写的日志,不过logback实现了slf4j,slf4j本身是一个日志门面,并不包含具体的实现

2.7 列举出常见的http请求方式

GET、POST、DELETE、PUT等等

2.8 怎么读取自定义的配置项?用什么注解?怎么设置默认值?

 1. 如果我们需要读取单个自定义配置项,可以使用@Value注解 。例@Value("${test.hello}"),@Value注解支持SpEL。@Value("${test.hello:TEST}"),加上冒号可以设置默认值
2. 如果我们是写了一个JavaBean来和配置文件进行映射,这时候就需要使用到@ConfigurationProperties注解来绑定配置项,它支持绑定多个属性配置项,支持松散绑定,还支持复杂类型的封装

2.9 bootstrap和application配置有什么区别?

Spring Cloud 构建于 Spring Boot 之上,在 Spring Boot 中有两种上下文,一种是 bootstrap, 另外一种是 application, bootstrap 是应用程序的父上下文,也就是说 bootstrap 加载优先于 applicaton。bootstrap 主要用于从额外的资源来加载配置信息,还可以在本地外部配置文件中解密属性。这两个上下文共用一个环境,它是任何Spring应用程序的外部属性的来源。bootstrap 里面的属性会优先加载,它们默认也不能被本地相同配置覆盖。

2.10 为什么引入SpringBoot内置依赖不需要加版本号?

点开pom.xml中的spring-boot-starter-parent父项目就会发现,里面就指定了内置依赖的版本号,所以我们在导入SpringBoot内置依赖时不需要指定版本号,因为父项目已经帮我们指定了版本号。

3.1 常见的持久层框架有哪些?

主要有 Mybatis, Hibernate 两种持久层框架,前者为半自动,后者为全自动

3.2 什么是半自动?什么是全自动?

半自动的持久层框架有时需要程序员手写一些SQL语句,灵活性高,比如 Mybatis。

全自动的持久层框架不需要程序员手写SQL语句,几乎所有的事情都可以交给框架来做,非常省心省事

3.3 Mybatis和Hibernate有什么区别?

Mybatis:需要程序员手写SQL语句,可以严格控制sql执行性能,灵活度高。但是数据无关性差,如果是多种数据库的话,每种数据库都要编写专门的SQL语句,非常麻烦。

Hibernate:不需要程序员手写SQL语句,数据无关性好,可以适应多数据库类型的项目,但是比起Mybatis执行性能会差一些。

3.4 泛型和Object有什么区别?

泛型和Object在使用上区别不大,但是泛型在使用时不需要做强制类型转换,编译时更安全。如果使用Object类的话,你没法保证返回的类型一定是需要的类型,也许是其它类型。这时你就会在运行时得到一个类型转换异常(ClassCastException)

3.5 你做过的项目中,有没有用过泛型,怎么使用的?

CommonResp<List> resp = new CommonResp<>()

泛型可以在使用时才指定具体的数据类型,非常方便

5.1 Vue怎么调用后端接口?你一般用什么组件?

使用Axios,npm install axios --save安装,

//发送get请求
axios.get("http://localhost:8880/ebook/list?name=Vue").then((response) => {
console.log(response);
});

5.2 Vue3的setup方法起什么作用?

初始化方法,组件加载完后初始执行的方法。setup执行的时候界面没有渲染好

5.3 简单谈一谈你对跨域的理解?

来自一个IP端口的页面(vue项目8080端口),要访问另一个IP端口的资源(springboot请求端口8880),就会产生跨域访问。解决方法,跨域配置:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //映射请求地址,针对所有接口
.allowedOrigins("*") //允许来源所有
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true) //允许携带凭证sessionId,cookie
.maxAge(3600);//1小时内不需要再预检(发OPTIONS请求)
}
}

5.4 Vue3实现数据绑定有几种方法

ref():const ebooks = ref(); ebooks.value = response.data.content; reactive里面放一个对象,自定义属性:

const ebooks1 = reactive({books: []});
ebooks1.books = response.data.content;
...
return {
ebooks,
ebooks2: toRef(ebooks1, "books")
}

5.5 说几个Vue组件生命周期函数

生命周期:组件或界面从加载到销毁的过程 onCreate:渲染前 onMounted:组件加载完毕,界面渲染完毕后执行

5.6 双向数据绑定是什么意思?

当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化

5.7 Vue怎么配置多环境?

增加开发和生产配置文件,放在web根目录下:.env.dev和 .env.prod 自定义参数:VUE_APP_XXX 修改package.json中的编译和启动命令:

  "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"
},

5.8 axios拦截器用过吗?能用来做什么?

使用axios拦截器打印请求日志和返回参数 main.ts

/**
* 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);
});

5.9 过滤器用过吗?有什么用?

配置过滤器打印接口耗时。 过滤器Filter为J2EE Servlet组件,使用时实现Filter接口,重写其中的doFilter方法,对请求和响应进行统一拦截,

5.10 过滤器能注入类吗?

可以

5.11 过滤器和拦截器有什么区别?

拦截器是Spring框架特有的,用于登录校验,权限校验,请求日志打印。分为preHandle和postHandle,preHandle返回true才会向后执行,还需要增加一个全局配置类 而过滤器只在doFilter方法编写逻辑。 执行顺序是过滤器先于拦截器。过滤器的范围更大,因为它在tomcat容器内,然后再进入到容器内的springboot应用中

5.12 拦截器能注入类吗?

可以,@Component注解将拦截器注成一个 bean。然后使用@Autowired注解将类注入到拦截器

5.13 是否用过AOP?一般用来做什么?

AOP是面向切面编程,采用横向抽取机制,取代传统纵向继承机制,将业务无关的代码解耦,适用于性能监视,操作日志的记录,权限校验等。横向抽取即通过代理向目标方法织入增强方法。

5.14 AOP的切点、切面是什么意思?

Pointcut切点:我们所要对哪些连接点进行拦截的定义,使用execution函数定义切点的方法切入 Advice通知,拦截到方法以后要做的事情。分为前置通知,后置通知、环绕通知等。 织入:动词,将增强应用到目标对象来创建新的代理对象的过程 Aspect切面:切点+通知

5.15 AOP有哪些通知?

前置通知@Before,后置通知@After、环绕通知@Around等。

5.16 AOP能注入类吗?

可以

5.17 过滤器、拦截器、AOP有什么区别?

见上文

6.1 什么是物理分页,什么是逻辑分页?

物理分页(后端分页):每次只从数据库查出当前页的数据,并查出总条数,前端显示页码和数据

逻辑分页(前端分页):数据一次性查询到前端,由前端根据总数据,来设置分页页码和当前页数据

适用场景: 物理分页适用于数据量大、更新频繁的场景 逻辑分页适用于数据量少、更新不频繁的场景

6.2 Mysql的分页关键字是什么?

LIMIT关键字 集成PageHelper插件后,在service层可直接使用

PageHelper.startPage(page, size)

page从1开始 获取分页信息

PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
pageInfo.getTotal();
pageInfo.getPages();

PageHelper原理:Mybatis拦截器,拦截到SQL后,增加limit关键字 只对第一个遇到的SQL起作用

6.3 数据库ID有哪些设计方法,都有什么优缺点?

自增长ID8

  • 特点: 数值类型,值递增,由数据库内部生成
  • 优点: 是最简单的方式,开发简单,性能优秀
  • 缺点: 不适合分表分库场景,会出现主键冲突,ID重复,这是硬伤 会有N+1次查询问题,Java代码想获取ID,需要再查询一次

UUID

  • 特点: 字符串类型,值没什么规律
  • 优点: 适用于分表分库场景 UUID一般由Java代码生成,Java不需要查询就能知道ID
  • 缺点: 性能不如自增

雪花算法

  • 特点: 数值类型,由Twitter提供的分布式ID算法,递增
  • 优点: 适用于分表分库场景,就是为这场景而生的 由Java代码生成,Java不需要查询就能知道ID 所有需要生成唯一ID的都可用雪花算法,比如登录token、日志编号等
  • 缺点: 性能稍稍不如自增。生成的值较长,传递到前端number类型容易出现精度丢失,可以转成字符串解决

总结 如果是简单的小项目,可以考虑用自增ID 如果是中大型项目,推荐用雪花ID 至于UUID,因为有雪花算法的存在,所以可以放弃UUID 但是,小项目我也建议用雪花算法。以后做的项目多了,就不至于一会是自增ID,一会是雪花ID,不至于思维跳跃

6.4 什么是物理删除,什么是逻辑删除?

数据库的逻辑删除:通常使用一个is_deleted字段标示行记录是不是被删除(或者使用一个status字段代表所谓的“删除”状态),在逻辑上是数据是被删除的,但数据本身是依然存在的。

物理删除一定程度上删除了暂时“无用”的数据,降低了表的数据量,对性能肯定是有好处的;但是如果没有备份的话,数据很难恢复。

逻辑删除恢复的话只要修改is_deleted等类似的状态标示字段就可以了,但是表的数据量肯定会比物理删除增加了,并且查询时经常要考虑到is_deleted字段,对索引都会有影响。

6.5 什么是前端校验?什么是后端校验?

前端校验就是表单验证,后端校验就是接口参数校验。使用validation组件对接口请求的实体类通过NotNull等注解实现检验。

借助浏览器开发者工具,可以修改界面元素属性,比如将不可编辑变为可编辑,将不可点击变为可点击。 或者找到按钮提交的接口和请求参数,再利用接口测试工具,直接访问后端接口,完美绕过界面校验。

只做前端校验:会有安全问题 只做后端校验:会有服务器压力问题 正解:前端校验+后端校验

6.6 你们项目是如何处理异常的?

编写 统一异常处理类ControllerAdvice注解,ExceptionHandler注解对某种异常进行统一处理

7.1 常见的树型表结构设计有哪些?

设计1:邻接表,即本项目中采取的设计方式,表中包含parentId字段,这样是最常见的设计,能正确的表达菜单的树状结构且没有冗余数据,但在跨层级查询需要递归处理。不使用递归情况下无法查询某节点所有父级,所有子集

设计2:路径枚举 在设计1基础上新增一个parentIds字段,用来存储所有父集,多个以固定分隔符分隔,比如逗号。 优点:方便查询所有的子集 ,可以因此通过比较字符串parentIds长度获取当前节点层级 ; 缺点:新增节点时需要将parentIds字段值处理好 。parentIds字段的长度很难确定,无论长度设为多大,都存在不能够无限扩展的情况 。节点移动复杂,需要同时变更所有子集中的parentIds字段值 ;

设计3:闭包表 需要额外创建了一张TreePaths表,它记录了树中所有节点间的关系 ; 包含两列,祖先列与后代列,即使这两个节点之间不是直接的父子关系;同时增加一行指向节点自己 ; 参考文章

7.2 递归算法

/**
* 递归算法套路
* 在if里调用自己,或者在else里调用自己都可以
* 下面是以在if里调用自己为例
*/
public static func () {
if (...) {
// 触发条件时,自己调用自己
func()
} else {
// 不再调用自己
}
return ;
}
使用递归方法将数组转为树形结构

/**
* 使用递归将数组转为树形结构
* 父ID属性为parent
*/
public static array2Tree (array: any, parentId: number) {
if (Tool.isEmpty(array)) {
return [];
}

const result = [];
for (let i = 0; i < array.length; i++) {
const c = array[i];
// console.log(Number(c.parent), Number(parentId));
if (Number(c.parent) === Number(parentId)) {
result.push(c);

// 递归查看当前节点对应的子节点
const children = Tool.array2Tree(array, c.id);
if (Tool.isNotEmpty(children)) {
c.children = children;
}
}
}
return result;
}

9.1 你们项目是否用过自定义异常,怎么用的?

自定一个BusinessException异常,继承的是运行时异常RuntimeException,封装自定义属性参数(枚举类实现),在捕获异常时返回自定义的参数和描述

9.2 你们项目的自定义异常是继承Exception还是RuntimeException?

继承的是运行时异常RuntimeException Exception :受检查的异常,这种异常是强制我们catch或throw的异常。 RuntimeException:运行时异常,这种异常我们不需要处理,完全由虚拟机接管。

9.3 常用的加密算法有哪些?

对称加密算法(AES、DES、3DES),加密和解密采用相同的密钥,是可逆的 非对称加密算法(RSA、DSA、ECC)指加密和解密采用不同的密钥(公钥和私钥),因此非对称加密也叫公钥加密,是可逆的 MD5:信息摘要算法5,单向的算法不可逆(被MD5加密的数据不能被解密)。MD5加密后的数据长度要比加密数据小的多,且长度固定,且加密后的串是唯一的。常用在不可还原的密码存储、信息完整性校验等。

9.4 盐值有什么作用?

用户注册时,系统用来和用户密码进行组合而生成的随机数值,称作salt值,通称为加盐值。为用户密码添加Salt值,使得加密的得到的密文更加冷僻,不宜查询。

9.5 如何保证用户信息安全,比如数据库信息泄露了?

两层加密处理:前端对用户输入的密码进行MD5+盐值加密,后端对结果再次进行MD5加密,

9.6 简单的说说单点登录的方案

单点登录的意思是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 单点登录系统,所有需要登录的地方都调用该系统。一种方案是提供登录界面,登录完成后再跳转回原系统;另一种为只提供登录接口,不提供页面。

9.7 token和JWT是什么?

token+redis:token是无意义的随机字符串,保证唯一性就可以; JWT JSON Web Token 是加密的有意义的数据(用户信息)

9.8 全局变量存储使用vuex和sessionStorage区别?

vuex存储数据刷新页面之后就清空,sessionStorage是H5内置,键值对形式存储数据,保存在浏览器会话窗口上,在关闭窗口或标签页之后将会删除这些数据

9.9 什么是vuex?(一句话说明vuex是做什么的)?

vuex就是一个全局的响应式的变量,方便其他组件从中获取信息