瑞吉外卖


软件开发流程

开发流程

  • 需求分析
    • 产品原型、需求规格说明书
  • 设计
    • 产品文档、UI 界面设计、概要设计、详情设计、数据库设计
  • 编码
    • 项目代码、单元测试
  • 测试
    • 测试用例、测试报告
  • 上线运维
    • 软件环境安装、配置

角色分工

  • 项目经理
    • 对整个项目负责,任务分配、把控进度
  • 产品经理
    • 进行需求调研,输出需求调研文档、产品原型等
  • UI 设计师
    • 根据产品原型输出界面效果图
  • 架构师
    • 项目整体架构设计、技术选型等
  • 开发工程师
    • 代码实现
  • 测试工程师
    • 编写测试用例,输出测试报告
  • 运维工程师
    • 软件环境搭建、项目上线

软件环境

  • 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
  • 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
  • 生产环境(production):即线上环境,正式提供对外服务的环境

瑞吉外卖项目介绍

项目介绍

本项目共分 3 期进行开发:

第一期主要实现基本需求,其中移动端应用通过 H5 实现,用户可以通过手机浏览器访问。

第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。

第三期主要针对系统进行优化升级,提供系统的访问性能。

产品原型展示

产品原型,就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展示出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。

注意事项:产品原型主要用于展示项目的功能,并不是最终的页面效果。

技术选型

用户层

H5、Vue.js、ElementUI、微信小程序

网关层

Nginx

应用层

Spring Boot、Spring MVC、Spring Session、Spring、Swagger、lombok

数据层

Mysql、Mybatis、Mybatis Plus、Redis

工具

git、maven、junit

功能架构

移动端前台(H5、微信小程序)

手机号、微信登陆、地址管理、历史订单、菜品规格、购物车、下单、菜品浏览

系统管理后台

分类管理、菜品管理、套餐管理、菜品口味管理、员工登陆、员工退出、员工管理、订单管理

角色

  • 后台系统管理员:登陆后台管理系统,拥有后台系统中的所有操作权限
  • 后台系统普通员工:登陆后台管理系统,对菜品、套餐、订单等进行管理
  • C 端用户:登陆移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等

具体实现

设置静态资源映射

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
package com.itheima.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

/*
* 设置静态资源映射
* @param registry
* */

@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始映射");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");

}
}

登录拦截器

实现步骤:

  • 1、创建自定义过滤器 LoginCheckFilter
  • 2、在启动类上加入注解 @ServletComponentScan(开启组件扫描,这样才会扫描过滤器)
  • 3、完善过滤器的处理逻辑

代码实现

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
package com.itheima.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
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;

/*
* 检查用户是否已经完成登录
* */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1、获取本次请求的 URI
String requestURI = request.getRequestURI();

log.info("拦截到请求:{}", requestURI);

// 放行的资源
String[] urls = new String[] {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};

//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);

//3、如果不需要处理,则直接放行
if(check) {
log.info("本次请求{}不需要处理", requestURI);
filterChain.doFilter(request, response);
return;
}

//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户 id 为:{}", request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);
return;
}

//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}

/*
* 路径匹配,检查本次请求是否需要放行
* */
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match) {
return true;
}
}
return false;
}
}

新增员工

具体步骤

  • 1、页面发送 ajax 请求,将新增员工页面中输入的数据以 json 的形式提交到服务端
  • 2、服务端 Controller 接受页面提交的数据并调用 Service 将数据进行保存
  • 3、Service 调用 Mapper 操作数据库,保存数据

设置员工状态

员工 id 解决方法

员工 id 过长,前端 js 只能读取 16 位。

具体实现步骤:
  • 1.提供对象转换器 JacksonObjectMapper,基于 Jackson 进行 Java 对象到 json 数据的转换
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
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
  • 2.在 WebMvcConfig 配置类中拓展 SpringMvc 的消息转换器,在此消息转换器中使用提供的对象转换器进行 Java 对象到 json 数据的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// common/WebMvcConfig


/*
* 拓展 mvc 框架的消息转换器
* */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("拓展消息转换器...");
// 创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
// 设置对象转换器,底层使用 Jackson 将 Java 对象转为 json
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将上面的消息转换器对象追加到 mvc 框架的转换器容器集合中
converters.add(0, messageConverter);
}
}

编辑员工信息

代码开发

  • 1、点击编辑按钮时,页面跳转到 add.html,并在 url 中携带参数
  • 2、在 add.html 页面获取 url 中的参数 [员工 id]
  • 3、发送 axios 请求,请求服务端,同时提交员工 id 参数
  • 4、服务端接收请求,根据员工 id 查询员工信息,将员工信息以 json 形式响应给页面
  • 5、页面接收服务端响应的 json数据,通过 Vue 的数据绑定进行员工信息回显
  • 6、点击保存按钮,发送 axios 请求,将页面中的员工信息以 json 方式提交给服务端
  • 7、服务端接收员工信息,并进行处理,完成后给页面响应
  • 8、页面接收到服务端响应信息后进行相应处理

注意:add.html 页面为公共页面,新增员工和编辑员工都是在此页面进行操作

菜品分类管理

公共字段自动填充

问题分析

前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:

  • create_time
  • update_time
  • create_user
  • update_user

能不能对于这些公共字段在某个地方统一处理来简化开发呢?

答案就是使用 MyBatis Plus 提供的公共字段自动填充功能。

代码实现

Mybatis Plus 公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

实现步骤
  • 1、在实体类的属性上加入 @TableField 注解,指定自动填充的策略
  • 2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现 MetaObjectHandler 接口
实现
  • 第一步,添加 @TableField 注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Employee 类
@Data
public class Employee implements Serializable {

@TableField(fill = FieldFill.INSERT) // 插入时填充字段
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时填充字段
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT)
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

}
  • 2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现 MetaObjectHandler 接口
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
// common/MetaObjectHandler.java
/*
* 自定义元数据对象处理器
* */

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

/*
* 插入操作自动填充
* */
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", new Long(1));
metaObject.setValue("updateUser", new Long(1));
}

/*
* 更新操作自动填充
* */
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", new Long(1));
}
}

注意:当我们设置 createUser 和 updateUser 为固定值,后面我们需要进行改造,改为动态获得当前登录用户的 id

功能测试

将 EmployeeController 中的新增和修改方法手动设置公共字段的代码注释掉,测试这些公共字段是否完成了自动填充

功能完善

前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充 createUser 和 updateUser 时设置的用户 id 是固定值,现在我们需要改造成动态获取当前登录用户的 id。

注意,我们在 MyMetaObjectHandler 类中是不能获得 HttpSession 对象的,所以我们需要通过其他方式来获取登录用户 id。

可以使用 ThreadLocal 来解决此问题,它就是一个 JDK 中提供的一个类。


在学习 ThreadLocal 之前,我们需要先确定一个事情,就是客户端发送的每次 http 请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

  • 1、LoginCheckFilter 的 doFilter 方法
  • 2、EmployeeController 的 update 方法
  • 3、MyMetaObjectHandler 的 updateFill 方法

可以在上面的三个方法中分别加入下面代码(获取当前线程 id):

1
2
long id = Thread.currentThread().getId();
log.info("线程id:{}", id);

执行编辑员工功能验证,通过观察控制台输出可以发现,一次请求对应的线程 id 是相同的。


什么是ThreadLocal?

ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才会获取到对应的值,线程外不能访问。

ThreadLocal 常用方法:

  • public void set(T value):设置当前线程的线程局部变量的值
  • public T get():返回当前线程所对应的线程局部变量的值

我们可以在 LoginCheckFilter 的 doFilter 方法中获取当前登录用户 id,并调用 ThreadLocal 的 set 方法来设置当前线程的线程局部变量的值(用户 id),然后在 MyMeteObjectHandler 的 updateFill 方法中调用 ThreadLocal 的 get 方法来获取当前线程所对应的线程局部变量的值(用户 id)

实现步骤
  • 1、编写 BaseContext 工具类,基于 ThreadLocal 封装的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 基于 ThreadLocal 封装,用户保存和获取当前登录用户 id
* */
public class BaseContext {

private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

public static void setCurrentId(Long id) {
threadLocal.set(id);
}

public static Long getCurrentId() {
return threadLocal.get();
}

}
  • 2、在 LoginChechFilter 的 doFilter 方法中调用 BaseContext 来设置当前登录用户的 id
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
/*
* 检查用户是否已经完成登录
* */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1、获取本次请求的 URI
String requestURI = request.getRequestURI();

log.info("拦截到请求:{}", requestURI);

// 放行的资源
String[] urls = new String[] {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};

//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);

//3、如果不需要处理,则直接放行
if(check) {
log.info("本次请求{}不需要处理", requestURI);
filterChain.doFilter(request, response);
return;
}

//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户 id 为:{}", request.getSession().getAttribute("employee"));

Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

filterChain.doFilter(request, response);
return;
}

//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}

}
  • 3、在 MyMetaObjectHandler 的方法中调用 BaseContext 获取登录用户的 id
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
/*
* 自定义元数据对象处理器
* */

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

/*
* 插入操作自动填充
* */
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}

/*
* 更新操作自动填充
* */
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}

删除分类

功能完善

前面我们已经实现了根据 id 删除分类的功能,但是并没有检查删除的分类是否关联了菜品或套餐,所以我们需要进行功能完善。

要完善分类删除功能,需要先准备基础的类和接口:

  • 1、实体类 Dish 和 Setmeal
  • 2、Mapper 接口和 DishMapper 和 SetmealMapper
  • 3、Service 接口 DishService 和 SetmealService
  • 4、Service 实现类 DishServiceImpl 和 SetmealServiceImpl

  • 1、在 CategoryService 中新增 remove 方法,并在 CategoryServiceImpl 中实现
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
// CategoryServiceImpl.java
@Service
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

@Autowired
private DishService dishService;

@Autowired
private SetmealService setmealService;

/*
* 根据 id 删除分类,删除之前需要进行判断
* */
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据分类 id 进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
int count1 = dishService.count(dishLambdaQueryWrapper);

// 查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if(count1 > 0) {
// 已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类项关联了菜品,不能删除");

}
// 查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据分类 id 进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2 > 0) {
// 已经关联套餐,抛出一个业务异常
throw new CustomException("当前分类项关联了套餐,不能删除");

}
// 正常删除分类
super.removeById(id);
}
}
  • 2、自定义异常
1
2
3
4
5
6
7
8
9
10
11
12
13
// common/CustomException
package com.itheima.reggie.common;

/*
* 自定义业务异常类
* */
public class CustomException extends RuntimeException{

public CustomException(String message) {
super(message);
}

}
  • 3、在 CategoryController 中调用 remove 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* 根据 id 删除分类
* */
@DeleteMapping
public R<String> delete(Long ids) {
log.info("删除分类,id为:{}", ids);

// categoryService.removeById(ids);

categoryService.remove(ids);

return R.success("分类信息删除成功");
}

文件上传下载

文件上传介绍

文件上传,也就是 upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。


文件上传时,对页面的 form 表单有如下要求:

  • method=”post” 采用 post 方式提交数据
  • enctype=”multipart/form-data” 采用 multipart 格式上传文件
  • type=”file” 使用 input 的 file 控件上传

举例:

1
2
3
4
<form method="post" action="/common/upload" enctype="multipart/form-data">
<input name="myFile" type="file" />
<input type="submit" value="提交" />
</form>

服务端要接收客户端页面上传的文件,通常会使用 Apache 的两个组件:

  • commons-fileupload
  • commons-io

Spring 框架在 spring-web 包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在 Controller 的方法中声明一个 MultipartFile 类型的参数即可接收上传的文件,例如:

1
2
3
4
5
6
7
8
/*
* 文件上传
*/
@PostMapping(value="/upload")
public R<String> upload(MultipartFile file) {
System.out.println(file);
return null;
}
文件上传实现
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
// commonController
/*
* 文件上传和下载
* */
@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {

@Value("${reggie.path}")// 来自 application.yml 文件配置
private String basePath;

/*
* 文件上传
* */
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
// file 是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());

// 原始文件名
String originalFilename = file.getOriginalFilename();// xxx.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

// 使用 UUID 重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;

// 创建一个目录对象
File dir = new File(basePath);
// 判断当前目录是否存在
if(!dir.exists()) {
// 目录不存在,需要创建
dir.mkdirs();
}

try {
// 将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);

}
}

文件下载介绍

文件下载,也称为 download,是指将文件从服务器传输到本地计算机的过程。

通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式回写到浏览器的过程。


文件下载,页面端可以使用 标签展示下载的图片

1
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
代码实现
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
// CommonController 类
/*
* 文件上传和下载
* */
@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {

@Value("${reggie.path}")
private String basePath;
/*
* 文件下载
* */
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
try {
// 输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

// 输出流,通过输出流将文件写回浏览器,在浏览器回显展示图片
ServletOutputStream outputStream = response.getOutputStream();

response.setContentType("image/jepg");

int len = 0;
byte[] bytes = new byte[1024];
while((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}

// 关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}

}
}

新增菜品

需求分析

后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。

数据模型

新增菜品,其实就是将新增页面录入的信息插入到 dish 表,如果添加了口味做法,还需要向 dish_flavor 表插入数据。所以在新增菜品时,涉及到两个表:

  • dish 菜品表
  • dish_flavor 菜品口味表

代码开发

梳理交互过程

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

  • 1、页面(backend/page/food/add.html)发送 ajax 请求,请求服务端获取菜品分类数据并展示到下拉框中
  • 2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
  • 3、页面发送请求进行图片下载,将上传的图片进行回显
  • 4、点击保存按钮,发送 ajax 请求,将菜品相关数据以 json 形式提交到服务端

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这 4 次请求即可。

导入 DTO

导入 DishDto 类,用于封装页面提交的数据

注意事项:DTO,全称为 Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。

功能测试

套餐管理

新增套餐

需求分析

新增套餐,其实就是将新增页面录入的套餐信息插入到 setmeal 表,还需要向 setmeal_dish 表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:

  • setmeal 套餐表
  • setmeal_dish 套餐菜品关系表

代码实现

准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好

  • 实体类 SetmealDish
  • DTO类 SetmealDto
  • Mapper 接口 SetmealDishMapper
  • 业务层接口 SetmealDishService
  • 业务层实现类 SetmealDishServiceImpl
  • 控制层 SetmealController
梳理交互过程
  • 1、页面(backend/page/combo/add.html)发送 ajax 请求,请求服务端获取套餐分类数据并展示到下拉框中
  • 2、页面发送 ajax 请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
  • 3、页面发送 ajax 请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
  • 4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
  • 5、页面发送请求进行图片下载,将上传的图片进行回显
  • 6、点击保存按钮,发送 ajax 请求,将套餐相关数据以 json 形式提交到服务端

难点部分
1
2
3
4
5
6
7
8
// SetmealService 类
public interface SetmealService extends IService<Setmeal> {

/*
* 新增套餐,同时需要保存套餐和菜品的关联关系
* */
public void saveWithDish(SetmealDto setmealDto);
}
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
// SetmealServiceImpl 类
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {

@Autowired
private SetmealDishService setmealDishService;
/*
* 新增套餐,同时需要保存套餐和菜品的关联关系
* */
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
// 保存套餐的基本信息,操作 setmeal,执行 insert 操作
this.save(setmealDto);

List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)-> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
// 保存套餐和菜品的关联信息,操作 setmeal_dish,执行 insert 操作
setmealDishService.saveBatch(setmealDishes);
}
}

删除套餐

需求分析

在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

短信发送

短信服务介绍

目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信对接)我们只需要注册称为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。

常用短信服务

  • 阿里云
  • 华为云
  • 腾讯云
  • 京东
  • 梦网
  • 乐信

阿里云短信服务

阿里云短信服务(Short Message Service :sms)

应用场景:

  • 验证码
  • 短信通知
  • 推广短信

代码开发

具体开发步骤

  • 1、导入 maven 坐标
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
  • 2、调用 API

手机验证码登录

需求分析

为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。

手机验证码登录的优点:

  • 方便快捷,无需注册,直接登录
  • 使用短信验证码作为登录凭证,无需记忆密码
  • 安全

登录流程:

输入手机号>获取验证码>输入验证码>点击登录>登录成功

注意:通过手机验证码登录,手机号是区分不同用户的标识

代码开发

梳理交互过程

  • 1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送 ajax 请求,在服务端调用短信服务 API 给指定手机号发送验证码短信
  • 2、在登录页面输入验证码,点击【登录】按钮,发送 ajax 请求,在服务端处理登录请求。

准备工作

在开发业务前,先将需要用到的类和接口基本结构创建好

  • 实体类 User
  • Mapper 接口 UserMapper
  • 业务层接口 UserService
  • 业务层实现类 UseServiceImpl
  • 控制层 UserController
  • 工具类 SMSUtis、ValidateCodeUtils

菜品展示、购物车、下单

  • 导入用户地址簿相关功能代码
  • 菜品展示
  • 购物车
  • 下单

导入用户地址簿相关功能代码

  • 需求分析

  • 数据模型

  • 导入功能代码

  • 功能测试

需求分析

地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。

数据模型

用户的地址信息会存储在 address_book 表,即地址表中

导入功能代码

功能代码清单:

  • 实体类 AddressBook
  • Mapper 接口 AddressBookMapper
  • 业务层接口 AddressBookService
  • 业务层实现类 AddressBookServiceImpl
  • 控制层 AddressBookController

git 常用命令

git 全局设置

安装 git 后首先要做的事是设置用户名称和 email 地址。因为每次 git 提交都会使用该用户信息


在 git 命令行中执行下面命令:

  • 设置用户信息

git config –global user.name “demo”

git config –global user.email “hello@qq.com

  • 查看配置信息

git config –list

注意:上面的 user.name 和 user.email 并不是我们在注册码云或其他托管账号时使用的用户名和邮箱,此处可任意处理。

获取 git 仓库

要使用 git 对我们的代码进行版本控制,首先需要获得 git 仓库


获取 git 仓库通常有两种方式:

  • 在本地初始化一个 git 仓库(不常用)
  • 从远程仓库克隆(常用)

获取 git 仓库-在本地初始化 git 仓库

执行步骤如下:

1、在任意目录下创建一个空目录(例如 repo1)作为我们的本地 git 仓库

2、进入这个目录中,点击右键打开 git bash 窗口

3、执行命令 git init

如果在当前目录中看到 .git 文件夹(此文件夹为隐藏文件夹)则说明 git 仓库创建成功

获取 git 仓库-从远程仓库克隆

可以通过 git 提供的命令从远程仓库进行克隆,将远程仓库克隆到本地

命令形式:git clone [远程 git 仓库地址]

工作区、缓存区、版本库概念

版本库:前面看到的.git 隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等

工作区:包含 .git 文件夹的目录就是工作区,也称为工作目录,主要用于存放开发的代码

暂存区:.git 中有很多文件,其中有一个 index 文件就是暂缓区,也可以叫做 stage。暂存区是一个临时保存修改文件的地方

git 工作区中文件的状态

git 工作区中的文件存在两种状态:

  • untracked 未跟踪(未被纳入版本控制)
  • tracked 已跟踪(被纳入版本控制)
    • 1)Unmodified 未修改状态
    • 2)Modified 已修改状态
    • 3)Staged 已暂存状态

注意:这些文件的状态会随着我们执行 git 的命令发生变化

本地仓库操作

本地仓库常用命令如下:

  • git status 查看文件状态
  • git add 将文件的修改加入暂存区
  • git reset 将暂存区的文件取消暂存或者是切换到指定版本
  • git commit 将暂存区的文件修改提交到版本库
  • git log 查看日志

远程仓库操作

前面执行的命令操作都是针对的本地仓库,远程仓库操作如下:

  • git remote 查看远程仓库
  • git remote add 添加远程仓库
  • git clone 从远程仓库克隆
  • git pull 从远程仓库拉取
  • git push 推送到远程仓库

查看远程仓库

如果想查看已经配置的远程仓库服务器,可以运行 git remote 命令,它会列出每一个远程服务器的简写。如果已经克隆了远程仓库,那么至少应该能看到 origin,这是 git 克隆的仓库服务器的默认名字。


git remote -v 查看详细的远程仓库

添加远程仓库

添加远程仓库,运行 git remote add [short-name] [url] 添加一个新的远程仓库,同时指定一个可以引用的简写。

克隆远程仓库到本地

如果你想获得一份已经存在了的 git 远程仓库的拷贝,这时就要用到 git clone 命令。git 克隆的是该 git 仓库服务器上的几乎所有数据(包括日志信息、历史记录等),而不仅仅是复制工作所需要的文件。

克隆仓库的命令格式是:git clone [url]

推送至远程仓库

将本地仓库内容推送到远程仓库,可以使用命令:git push [remote-name] [branch-name]

在使用 git push 命令将本地文件推送至码云远程仓库时,需要进行身份认证,认证通过才可以推送

从远程仓库拉取

git pull 命令的作用是从远程仓库获取最新版本并合并到本地仓库,命令格式:git pull [short-name] [branch-name]

注意:如果当前本地仓库不是从远程仓库克隆,而是本地创建的仓库,并且仓库中存在文件,此时再从远程仓库拉取文件的时候会报错(fatal: refusing to merge unrelated histories)

解决此问题可以在 git pull 命令后加入参数 –allow-unrelated-histories(从远程仓库获取最新的版本,并且合并到本地仓库)

分支操作

分支是 git 使用过程中非常重要的概念。使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。


通过 git init 命令创建本地仓库时默认会创建一个 master 分支

相关具体命令如下:

  • git branch 查看分支
    • git branch -r 列出所有本地分支
    • git branch -a 列出所有本地分支和远程分支
    • git branch -d [name] 删除指定分支
  • git branch [name] 创建分支
  • git checkout [name] 切换分支
  • git push [short-name] [name] 推送至远程仓库分支
  • git merge [name] 合并分支

解决合并时冲突

手动修改再重新 git add

如果提交时报错:cannot do a partial commit during a merge

则添加一个参数 -i (ignore)

标签操作

git 中的标签,指的是某个分支某个特定时间点的状态。通过标签,可以很方便的切换到标记时的状态。比较有代表性的是人们会使用这个功能来标记发布结点(v1.0、v1.2等)。具体可参考 mybatis-plus 的标签。

相关命令:

  • git tag 列出已有标签
  • git tag [name] 创建标签
  • git push [short-name] [name] 将标签推送至远程仓库
  • git checkout -b [branch] [name] 检出标签

打好标签号,分支状态不会再变化。应用于版本更新与新版本发行

在 IDEA 中使用 git

在 IDEA 中使用 git,本质上还是使用的本地安装的 git,所以需要在 IDEA 中配置 git。

获取 git 仓库

在 IDEA 中使用 git 获取仓库有两种方式:

  • 本地初始化仓库
  • 从远程仓库克隆

本地仓库操作

  • 将文件加入暂存区
  • 将暂存区的文件提交到版本库
  • 查看日志

远程仓库操作

  • 查看远程仓库
  • 添加远程仓库
  • 推送至远程仓库
  • 从远程仓库拉取

Linux

环境搭建(Linux 安装远程连接)

常用命令(文件、目录拷贝、移动打包、压缩文件编辑)

安装软件(文件上传、jdk、tomcat、mysql)

项目部署(Java 应用、Python应用、日志查看、系统管理、用户权限)

Linux 简介

不同应用领域的主流操作系统

  • 桌面操作系统
    • Windows(用户最多)
    • Mac OS(操作体验好,办公人士首选)
    • Linux(用户最少)
  • 服务器操作系统
    • UNIX(安全、稳定、付费)
    • Linux(安全、稳定、免费、占有率高)
    • Windows Server(付费、占有率低)
  • 移动设备操作系统
    • Android(基于 Linux、开源,主要用于智能手机、平板电脑和智能电视)
    • IOS(苹果公司开发、不开源,用于苹果公司的产品,例如:iPhone、iPad)
  • 嵌入式操作系统
    • Linux(机顶盒、路由器、交换机)

Linux 系统版本

Linux 系统分为内核版和发行版

  • 内核版
    • 由 Linux Torvalds 及其团队开发、维护
    • 免费、开源
    • 负责控制硬件
  • 发行版
    • 基于 Linux 内核版进行拓展
    • 由各个 Linux 厂商开发、维护
    • 有收费版本和免费版本
Linux 系统发行版
  • Ubuntu:以桌面应用为主
  • RedHat:应用最广泛、收费
  • CentOS:RedHat 的社区版、免费
  • openSUSE:对个人完全免费、图形界面华丽
  • Fedora:功能完备、快速更新、免费
  • 红旗 Linux:北京中科红旗软件技术有限公司开发

Linux 安装

安装方式

Linux 系统的安装方式

  • 物理机安装:直接将操作系统安装到服务器硬件上
  • 虚拟机安装:通过虚拟机软件安装

虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能、运行在完全隔离环境中的完整计算机系统。

常用虚拟机软件

  • VMWare
  • VirtualBox
  • VMLite WorkStation
  • Qemu
  • HopeddotVOS

网卡设置

由于启动服务器时未加载网卡,导致 IP 地址初始化失败

修改网络初始化配置,设定网卡在系统启动时初始化

cd / 进入根目录

cd etc 进入 etc 目录

cd sysconfig 进入 sysconfig 目录

cd network-scripts 进入 network-scripts

vi ifcfg-ens33 编辑 ifcfg-ens33 文件

i 进入编辑状态

-> :wq 保存退出

安装 SSH 连接工具

SSH(Secure Shell),建立在应用层基础上的安全协议

常用的 SSH 连接工具

  • putty
  • secureCRT
  • xshell
  • finalshell

通过 SSH 连接工具就可以实现从本地连接到远程的 Linux 服务器

Linux 和 Windows 目录结构对比

Linux 系统中的目录

  • / 是所有目录的顶点
  • 目录结构像一颗倒挂的树

Linux 目录介绍

  • bin 存放二进制可执行文件
  • boot 存放系统引导时使用的各种文件
  • dev 存放设备文件
  • etc 存放系统配置文件
  • home 存放系统用户的文件
  • lib 存放程序运行所需的共享库和内核模块
  • opt 额外安装的可选应用程序包所放置的位置
  • root 超级用户目录
  • sbin 存放二进制可执行文件,只有 root 用户才能访问
  • tmp 存放临时文件
  • usr 存放系统应用程序
  • var 存放运行时需要改变数据的文件,例如日志文件

Linux 常用命令

文件目录操作命令 ls

作用:显示指定目录下的内容

语法:ls [-al] [dir]

说明:

  • -a 显示所有文件及目录(.开头的隐藏文件也会列出)
  • -l 除文件名称为,同时将文件型态(d 表示目录,- 表示文件)、权限、拥有者、文件大小等信息详细列出。

注意:由于我们使用 ls 命令时经常需要加入 -l 选项,所以 Linux 为 ls -l 命令提供了一种简写方式,即 ll

文件目录操作命令 cd

作用:用于切换当前目录,即进入指定目录

语法:cd [dir-name]

特殊说明:

  • ~ 表示用户的 home 目录
  • . 表示目前所在的目录
  • .. 表示目前目录位置的上级目录

文件目录操作命令 cat

作用:用于显示文件内容

语法:cat [-n] file-name

说明:

  • -n:由 1 开始对所有输出的行数编号

文件目录操作命令 more

作用:以分页的形式显示文件内容

语法:more file-name

操作说明:

  • 回车键:向下滚动一行
  • 空格键:向下滚动一屏
  • b:返回上一屏
  • q 或者 ctrl + c:退出 more

文件目录操作命令 tail

作用:查看文件末尾的内容

语法:tail [-f] file-name

说明:

  • -f:动态读取文件末尾内容并显示,通常用于日志文件的内容输出

文件目录操作命令 mkdir

作用:创建目录

语法:mkdir [-p] dir-name

说明:

  • -p:确保目录名称存在,不存在的就创建一个。通过此选项,可以实现多层目录同时创建

文件目录操作命令 rmdir

作用:删除空目录

语法:rmdir [-p] dir-name

说明:

  • -p:当子目录被删除后使父目录为空目录的话,则一并删除

文件目录操作命令 rm

作用:删除文件或者目录

语法:rm [-rf] name

说明:

  • -r:将目录及目录中所有文件(目录)逐一删除,即递归删除
  • -f:无需确认,直接删除

拷贝移动命令 cp

作用:用于复制文件或目录

语法:cp [-r] source dest

说明:

  • -r:如果复制的是目录需要使用此选项,此时将复制该目录下所有的子目录和文件

拷贝移动命令 mv

作用:为文件或目录改名、或将文件或目录移动到其他位置

语法:mv source dest

打包压缩命令 tar

作用:对文件进行打包、解包、压缩、解压

语法:tar [-zcxvf] file-name [files]

包文件后缀为 .tar 表示只是完成了打包,并没有压缩

包文件后缀为 .tar.gz 表示打包的同时还进行了压缩

说明:

  • -z:z 代表的是 gzip,通过 gzip 命令处理文件,gzip 可以对文件压缩或解压
  • -c:c 代表的是 create,即创建新的包文件
  • -x:x 代表的是 extract,实现从包文件中还原文件
  • -v:v 代表的是 verbose,显示命令的执行过程
  • -f:f 代表的是 file,用于指定包文件的名称

举例:

打包

  • tar -cvf hello.tar ./* 将当前目录下所有文件打包,打包后的文件名为 hello.tar
  • tar -zcvf hello.tar.gz ./* 将当前目录下所有文件打包并压缩,打包后的文件名为 hello.tar.gz

解包

  • tar -xvf hello.tar 将 hello.tar 文件进行解包,并将解包后的文件放在当前目录
  • tar -zxvf hello.tar.gz 将 hello.tar.gz 文件进行解压,并将解压后的文件放在当前目录
  • tar -zxvf hello.tar.gz -C /user/local 将 hello.tar.gz 文件进行解压,并将解压后的文件放在 /usr/local

文本编辑命令 vi/vim

作用:vi 命令是 Linux 系统提供的一个文本编辑工具,可以对文件内容进行编辑,类似于 Windows 中的记事本

语法:vi/vim file-name

说明:

1、vim 是从 vi 发展来的一个功能更强大的文本编辑工具,在编辑文件时可以对文本内容进行着色,方便我们对文件进行编辑处理,所以实际工作中 vim 更加常用

2、要使用 vim 命令,需要我们自己完成安装。可以使用下面的命令来完成安装

yum install vim

3、在使用 vim 命令编辑文件时,如果指定的文件存在则直接打开此文件。如果指定的文件不存在则新建文件。

4、vim 在进行文本编辑时工分为三种模式,分别是命令模式(Command mode),插入模式(Insert mode)和底形模式(Last line mode)。这三种模式之间可以互相切换。我们在使用 vim 时一定要注意我们当前所处的是哪种模式。


针对 vim 中的三种模式说明如下:

1、命令模式

  • 命令模式下可以查看文件内容、移动光标(上下左右箭头、gg、G)
  • 通过 vim 命令打开文件后,默认进入命令模式
  • 另外两种模式需要首先进入命令模式,才能进入彼此

2、插入模式

  • 插入模式下可以对文件内容进行编辑
  • 在命令模式下按下 [i,a,o] 任意一个,可以进入插入模式。进入模式后下方会出现 insert 字样
  • 在插入模式下按下 ESC 键,回到命令模式

3、底行模式

  • 底行模式下可以通过命令对文件内容进行查找、显示行号、退出等操作
  • 在命令模式下按下 [:,/] 任意一个,可以对文件内容进行查找
  • 通过 / 方式进入底行模式后,可以对文件内容进行查找
  • 通过 : 方式进入底行模式后,可以输入 wq(保存并退出)、q!(不保存退出)、set nu(显示行号)

查找命令 find

作用:在指定目录下查找文件

语法:find dir-name -option file-name

查找命令 grep

作用:从指定文件中查找指定的文本内容

语法:grep word file-name

说明:

  • -r:如果复制的是目录需要使用此选项,此时将复制该目录下所有的子目录和文件

软件安装

  • 二进制发布包安装
    • 软件已经针对具体平台编译打包发布,只要解压,修改配置即可
  • rpm 安装
    • 软件已经按照 redhat 的包管理规范进行打包,使用 rpm 命令进行安装,不能自行解决库依赖问题
  • yum 安装
    • 一种在线软件安装方式,本质上还是 rpm 安装,自动下载安装包并安装,安装过程中自动解决库依赖问题
  • 源码编译安装
    • 软件以源码工程的形式发布,需要自己编译打包

XShell 安装方法

1、打开xshell 连接虚拟机

2、yum 安装 lrzsz:yum install lrzsz -y

3、输入:rpm -qa |grep lrzsz 检查是否能连接服务器

4、直接把文件拖进 xshell 窗口即可上传文件

安装 jdk

操作步骤:

1、使用 FinalShell 自带的上传工具将 jdk 的二进制发布包上传到 Linux

2、解压安装包,命令为 tar -zxvf jdk-8u171-linux-x64.tar.gz -C /usr/local

3、配置环境变量,使用 vim 命令修改 /etc/profile 文件,在文件末尾位置加入如下配置:

JAVA_HOME=/usr/local/jdk1.8.0_171

PATH=$JAVA_HOME/bin:$PATH

4、重新加载 profile 文件,使更改的配置立即生效,命令为 source /etc/profile

5、检查安装是否成功,命令为 java -version

安装 Tomcat

操作步骤:

1、使用 FinalShell 自带的上传工具将 Tomcat 的二进制发布包上传到 Linux

2、解压安装包,命令为 tar -zxvf apache-tomcat-7.0.57.tar.gz -C /usr/local

3、进入 tomcat 的 bin 目录启动服务,命令为 sh startup.sh 或者 ./startup/sh


验证 Tomcat 启动是否成功,有多种方式:

  • 查看启动日志
    • more /usr/local/apache-tomcat-7.0.57/logs/catalina.out
    • tail -50 /usr/local/apache-tomcat-7.0.57/logs/catalina.out
  • 查看进程 ps -ef | grep tomcat

注意:

  • ps 命令是 linux 下非常强大的进程查看命令,通过 ps -ef 可以查看当前运行的所有进程的详细信息
  • “|” 在 Linux 中称为管道符,可以将前一个命令的结果输出给后一个命令作为输入
  • 使用 ps 查看进程时,经常配合管道符和查找命令 grep 一起使用,来查看特定进程
防火墙操作
  • 查看防火墙状态(systemctl status firewalld、firewall-cmd –state)
  • 暂时关闭防火墙(systemctl stop firewalld)
  • 永久关闭防火墙(systemctl disable firewalld)
  • 开启防火墙(systemctl start firewalld)
  • 开放指定端口(firewall-cmd –zone=public –add-port=8080/tcp –permanent)
  • 关闭指定端口(firewall-cmd –zone=public –remove-port=8080/tcp –permanent)
  • 立即生效(firewall-cmd –reload)
  • 查看开放的窗口(firewall-cmd –zone=public –list-ports)

注意:

1、systemctl 是管理 Linux 中服务的命令,可以对服务进行启动、停止、重启、查看状态等操作

2、firewall-cmd 是 Linux 中专门用于控制防火墙的命令

3、为了保证系统安全,服务器的防火墙不建议关闭

停止 Tomcat 服务的方式
  • 运行 Tomcat 的 bin 目录中提供的停止服务的脚本文件:shutdown.sh
    • sh shutdown.sh
    • ./shutdown.sh
  • 结束 Tomcat 进程
    • 查看 Tomcat 进程,获取进程 id
    • 执行命令结束进程 kill -9 [id]

注意:kill 命令是 Linux 提供的用于结束进程的命令,-9 表示强制结束

安装 Mysql

1、检测当前系统中是否安装 Mysql 数据库

rpm -qa 查询当前系统中安装的所有软件

rpm -qa | grep mysql 查询当前系统中安装的名称带 mysql 的软件

rpm -qa | grep mariadb 查询当前系统中安装的名称带 mariadb 的软件

RPM(Red-Hat Package Manager)RPM 软件包管理器,是红帽 Linux 用于管理和安装软件的工具

注意事项:如果当前系统中已经安装有 mysql 数据库,安装将失败。Centos 自带 mariadb,与 mysql 数据库冲突

2、卸载已经安装的冲突软件

rpm -e –nodeps 软件名称

rpm -e –nodeps mariadb-libs-5.5.60-1.el7_5.x86_64

3、将资料中提供的 mysql 安装包上传到 Linux 并解压

mkdir /usr/local/mysql

tar -zxvf mysql-5.7.25-1.el7.x86_64.rpm-bundle.tar.gz -C /usr/local/mysql

说明:解压后得到 6 个 rpm 的安装文件

4、按照顺序安装 rpm 软件包

rpm -ivh mysql-community-common-5.7.25-1.el7.x86_64.rpm

rpm -ivh mysql-community-libs-5.7.25-1.el7.x86_64.rpm

rpm -ivh mysql-community-devel-5.7.25-1.el7.x86_64.rpm

rpm -ivh mysql-community-libs-compat-5.7.25-1.el7.x86_64.rpm

rpm -ivh mysql-community-client-5.7.25-1.el7.x86_64.rpm

yum install net-tools

rpm -ivh mysql-community-server-5.7.25-1.el7.x86_64.rpm

说明1:安装过程中提示缺少 net-tools 依赖,使用 yum 安装

说明2:可以通过指令升级现有软件及系统内核:yum update

5、启动 mysql

systemctl status mysqld 查看 mysql 服务状态

systemctl start mysqld 启动 mysql 服务

说明:可以设置 开机时启动 mysql 服务,避免每次开机启动 mysql

systemctl enable mysqld 开机启动 mysql 服务

netstat -tunlp 查看已经启动的服务

netstat -tunlp | grep mysql

ps -ef | grep mysql 查看 mysql 进程

6、登录 mysql 数据库,查阅临时密码

cat /var/log/mysqld.log 查看文件内容

cat /var/log/mysqld.log | grep password 查看文件内容中包含 password 的行信息

grep ‘temporary password’ /var/log/mysqld.log

注意:冒号后面的是密码,注意空格

7、登录 mysql,修改密码,开放访问权限

mysql -uroot -p 登录 mysql(使用临时密码登录)

修改密码

  • set global validate_password_length=4; 设置密码长度最低位数
  • set global validate_password_policy=LOW; 设置密码安全等级低,便于密码可以修改成 root
  • set password = password(‘root’); 设置密码为 root

开启访问权限

  • grant all on *.* to ‘root‘@’%’ identified by ‘root’;
  • flush privileges;

yum

Yum(全称为 Yellow dog Updater,Modified)是一个在 Fedora 和 RedHat 以及 CentOS 中的 Shell 前端软件包管理器。基于 RPM 包管理,能够从指定的服务器自动下载 RPM 包并安装,可以自动处理依赖关系,并且一次安装所有依赖的软件包,无需繁琐地一次次下载、安装。

期间遇到的问题

阿里云安装mysql后查看不到初始密码的解决办法

在阿里云安装mysql后用grep ‘A temporary password’ /var/log/mysqld.log命令查看MySQL初始密码,毛线都没有看到,然后直接到/var/log/mysqld.log查看mysqld.log文件发现文件是一片空白,一脸懵逼。

解决办法如下:

修改mysql的配置文件,使之可以跳过密码直接用root进入数据库

(1)*首先找到mysql的配置文件my.cnf 一般在 /etc/my.cnf*

*(2)**cd进 /etc 直接 vim my.cnf 编辑他***

*(3)**在【mysqld】标签下或者文件最下面添加一句skip-grant-tables 即可***

***(4) **esc退出编辑,然后输入冒号:再输入wq保存退出 ,**然后重启服务器 service mysqld restart 让修改生效*********

*****(5)**mysql -u root 就可以直接进入数据库了*********

******(6)**********然后是修改密码,最好设置密码复杂点,建议:*大写字母+小写字母+符号。*否则后面mysql会报错让你改到符合mysql的安全策略,或者也可以修改mysql安全策略

mysql> USE mysql;

​ *mysql> update mysql.user set authentication_string=password(‘*新密码*‘) where user=’root’;*

mysql> flush privileges ;

mysql> quit

*(7)*改完密码,再将配置文件改回来,就把添加的那句删掉就好了,然后再次重启服务器**********service mysqld restart************

**********(8)**然后就可以使用 mysql -uroot -p输入你的新密码进入啦**************

项目部署

手工部署项目

  • 1、在 IDEA 中开发 SpringBoot 项目并打成 jar 包

  • 2、将 jar 包上传到 Linux 服务器

    • mkdir /usr/local/app 创建项目,将 jar 包放到此目录
  • 3、启动 SpringBoot 程序

    • java -jar jar包
  • 4、检查防火墙,确保 8080 端口对外开放,访问 springboot 项目

  • 5、改为后台运行 SpringBoot 程序,并将日志输出到日志文件

    • 目前程序运行的问题

      • 线上程序不会采用控制台霸屏的形式运行程序,而是将程序在后台运行
      • 线上程序不会将日志输出到控制台,而是输出到日志文件,方便运维查阅信息
    • nohup 命令:全程 no hang up(不挂起),用于不挂断地运行指定命令,退出终端不会影响程序的运行

      • 语法格式 nohup command [Arg…] [&]

      • 参数说明:

        • Command:要执行的命令
        • Arg:一些参数,可指定输出文件
        • &:让命令在后台运行
      • 举例

        • nohup java -jar boot工程.jar &> hello.log &
        • 后台运行 java -jar 命令,并将日志输出到 hello.log 文件

通过 Shell 脚本自动部署项目

操作步骤
1、在 Linux 中安装 git
  • yum list git 列出 git 安装包
  • yum install git 在线安装 git
2、使用 git 克隆代码
  • cd /usr/local/
  • git clone [url]
3、在 Linux 中安装 maven
  • tar -zxvf apache-maven-3.5.4-bin.tar.gz -C /usr/local
  • vim /etc/profile 修改配置文件,加入如下内容
    • export MAVEN_HOME=/usr/local/apache-maven-3.5.4
    • export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH
  • source /etc/profile
  • mvn -version
  • vim /usr/local/apache-maven-3.5.4/conf/settings.xml 修改配置文件如下
    • <localRepository>/usr/local/repo</localRepository>
4、编写 Shell 脚本(拉取代码、编译、打包、启动)
  • Shell 脚本(shell script),是一种 Linux 系统中的脚本程序。使用 Shell 脚本编程跟 JavaScript、Java 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。
5、为用户授予执行 Shell 脚本的权限
  • chmod(英文全拼:change mode)命令是控制用户对文件的权限的命令
  • Linux 中的权限分为:读(r)、写(w)、执行(x)三种权限
  • Linux 的文件调用权限分为三级:文件所有者(Owner)、用户组(Group)、其他用户(Other Users)
  • 只有文件的所有者和超级用户可以修改文件或目录的权限
  • 要执行 Shell 脚本需要有对此脚本文件的执行权限,如果没有则不执行

举例

只执行 1、只读 4、只写 2

  • chmod 777 bootStart.sh 为所有用户授予读、写、执行权限
  • chmod 755 bootStart.sh 为文件拥有者授予读、写、执行权限,同组用户和其他用户授予读、执行权限
  • chmod 210 bootStart.sh 为文件拥有者授予写权限,同组用户授予执行权限,其他用户没有任何权限

注意

  • 第1位表示文件拥有者的权限

  • 第2位表示同组用户的权限

  • 第3位表示其他用户的权限

6、执行 Shell 脚本
7、通过 Shell 脚本自动部署项目

修改文件 /etc/sysconfig/net-work-scripts/ifcfg-ens33,内容如下:

BOOTPROTO=”static” #使用静态 IP 地址,默认为 dhcp

IPADDR=”192.168.138.100” # 设置的静态 IP 地址

NETMASK=”255.255.255.0” # 子网掩码

GATEWAY=”192.168.138.2” # 网关地址

DNS1=”192.168.138.2” # DNS 服务器

8、重启网络服务

systemctl restart network

注意:重启完网络服务后 ip 地址已经发生了改变,此时 FianlShell已经连接不上 Linux 系统,需要创建一个新连接才能连接到 Linux

Redis

Redis 是一个基于内存的 key-value 结构数据库

  • 基于内存存储,读写性能高
  • 适合存储热点数据(热点商品、资讯、新闻)
  • 企业应用广泛

Redis 应用场景

  • 缓存
  • 任务队列
  • 消息队列
  • 分布式锁

Redis 下载与安装

在 Linux 安装 Redis 步骤:

1、将 Redis 安装包上传到 Linux

2、解压安装包,命令:tar -zxvf redis-4.0.0.tar.gz -C /usr/local

3、安装 Redis 依赖环境 gcc,命令:yum install gcc-c++

4、进入 /usr/local/redis-4.0.0,进行编译,命令:make

5、进入 redis 的 src 目录,进行安装,命令:make install

在 Java 中操作 Redis

Redis 的 java 客户端很多,官方推荐的有三种:

  • jedis
  • lettuce
  • Redisson

Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在 Spring Boot 项目中还提供了对应的 starter,即 spring-boot-starter-data-redis

Jedis

Jedis 的 maven 坐标

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.0</version>
</dependency>

使用 Jedis 操作 Redis 的步骤

  • 1、获取连接
  • 2、执行操作
  • 3、关闭连接
Spring Data Redis

在 Spring Boot 项目中,可以使用 Spring Data Redis 来简化 Redis 操作,maven 坐标:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Spring Data Redis 中提供了一个高度封装的类:RedisTemplate,针对 jedis 客户端中大量 api 进行了归类封装,将同一类型操作封装为 operation 接口,具体分类如下:

  • ValueOperations:简单 K-V 操作
  • SetOperations:set 类型数据操作
  • ZSetOperations:zset 类型数据操作
  • HashOperations:针对 map 类型的数据操作
  • ListOperations:针对 list 类型的数据操作

缓存优化

问题说明

用户数量多,系统访问量大,频繁访问数据库,系统性能下降,用户体验差

环境搭建

maven 坐标

在项目的 pom.xml 文件中导入 spring data redis 的 maven 坐标:

配置文件

在项目的 application.yml 中加入 redis 相关配置:

1
2
3
4
5
6
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0

配置类

在项目中加入配置类 RedisConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itheima.reggie.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 默认的 key 序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

缓存短信验证码

前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存再 HttpSession 中的。

1、在服务端 UserController 中注入 RedisTemplate 对象,用于操作 Redis

2、在服务端 UserController 的 sendMsg 方法中,将随机生成的验证码缓存

3、在服务端 UserController 的 login 方法中,从 Redis 中获取缓存的验证码,如果登录成功则删除 Redis 中的验证码

缓存菜品数据

实现思路

高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。

具体的实现思路如下:

1、改造 DishController 的 list 方法,先从 Redis 中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入 Redis

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
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
List<DishDto> dishDtoList = null;

String key = "dish" + dish.getCategoryId() + "_" + dish.getStatus(); //

// 先从 redis 中获取缓存数据
dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);

if(dishDtoList != null) {
// 如果存在,直接返回,无需查询数据库
return R.success(dishDtoList);
}

// 构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
// 添加条件,查询状态为 1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus, 1);

// 添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);

dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();

BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();// 分类 id
// 根据 id 查询分类对象
Category category = categoryService.getById(categoryId);

if(category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}

// 查询口味数据
// 当前菜品 id
Long id = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId, id);
// SQL: select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());

// 如果不存在,需要查询数据库,将查询到的菜品数据缓存到 Redis
redisTemplate.opsForValue().set(key, dishDtoList, 60, TimeUnit.MINUTES);

return R.success(dishDtoList);
}

2、改造 DishController 的 save 和 update 方法,加入清空缓存的逻辑

注意:在使用缓存的过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。

Spring Cache

Spring Cache 介绍

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同地 cache 实现。具体就是通过 CacheManager 接口来统一不同的缓存技术。

CacheManager 是 Spring 提供的各种缓存技术抽象接口。

针对不同的缓存技术需要实现不同的 CacheManager:

CacheManger 描述
EhCacheCacheManager 使用 EhCache 作为缓存技术
GuavaCacheManager 使用 Google 的 GuavaCache 作为缓存技术
RedisCacheManager 使用 Redis 作为缓存技术

Spring Cache 常用注解

注解 说明
@EnableCaching 开启缓存注解功能
@Cacheable 在方法执行前 spring 先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

在 spring boot 项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用 @EnableCaching 开启缓存支持即可。

例如,使用 Redis 作为缓存技术,只需要导入 Spring data Redis 的 maven 坐标即可。

Spring Cache 使用方式

在 Spring Boot 项目中使用 Spring Cache 的操作步骤(使用 redis 缓存技术):

1、导入 maven 坐标

  • spring-boot-starter-data-redis、spring-boot-starter-cache

2、配置 application.yml

1
2
3
4
spring: 
cache:
redis:
time-to-live: 1800000 #设置缓存有效期

3、在启动类上加入 @EnableCaching 注解,开启缓存注解功能

4、在 Controller 的方法上加入 @Cacheable、@CacheEvict 等注解,进行缓存操作

读写分离

读和写所有压力都由一台数据库承担,压力大数据库服务器磁盘损坏则数据丢失,单点故障

Mysql 主从复制

介绍

Mysql 主从复制是一个异步的复制过程,底层是基于 Mysql 数据库自带的二进制日志功能。就是一台或多台 Mysql 数据库(slave,即从库)从另一台 Mysql 数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。Mysql 主从复制是 Mysql 数据库自带功能,无需借助第三方工具。


Mysql 复制过程分为三步:

  • master 将改变记录到二进制日志(binary log)
  • slave 将 master 的 binary log 拷贝到它的中继日志(relay log)
  • slave 重做中继日志中的事件,将改变应用到自己的数据库中

配置-前置条件

提前准备好两台服务器,分别安装 Mysql 并启动服务成功

  • 主库 Master:Master 192.168.138.100
  • 从库 Slave:192.168.138.101

配置-主库 Master

1、第一步:修改 Mysql 数据库的配置文件 /etc/my.cnf

[mysqld]

log-bin=mysql-bin #[必须]启用二进制日志

server-id=100 #[必须]服务器唯一 ID

2、第二步:重启 Mysql 服务

systemctl restart mysqld

3、第三步:登录 Mysql 数据库,执行下面 sql

GRANT REPLICATION SLAVE ON *.* to ‘xiaoming‘@’%’ identified by ‘Root@123456’;

注:上面 SQL 的作用是创建一个用户 xiaoming,密码是 Root@123456,并且给 xiaoming 用户授予 REPLICATION SLAVE 权限。常用于建立复制时所需要用到的用户权限,也就是 slave 必须被 master 授予具有该权限的用户,才能通过该用户复制。

4、第四步:登录 Mysql 数据库,执行下面 sql,记录下结果中 File 和 Position 的值

show master status;

注:上面 sql 的作用是查看 Master 的状态,执行完此 sql 后不要再执行任何操作

配置-从库 Slave

1、第一步:修改 mysql 数据库的配置文件 /etc.my.cnf

[mysqld]

server-id=101 #[必须]服务器唯一ID

2、第二步:重启 mysql 服务

systemctl restart mysqld

3、第三步:登陆 mysql 数据库,执行下面 sql

change master to master_host=’121.40.94.156’,master_user=’xiaoming’,master_password=‘Root@123456’,master_log_file=’mysql-bin.000001’,master_log_pos=439;

start slave;

注:439是在主库执行 show master status 获取的 position

注意:可能会遇到报错 ERROR 3021 (HY000): This operation cannot be performed with a running slave io thread; run STOP SLAVE IO_THREAD FOR CHANNEL ‘’ first.

将之前的从库关闭即可,执行 stop slave;

4、第四步:登录 mysql 数据库,执行下面 sql,查看从数据库的状态

show slave status;

或者

show slave status\G;

读写分离案例

Sharding-JDBC 介绍

Sharding-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供额外服务。它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和 各种 ORM 框架。使用 Sharding-JDBC 可以在程序中轻松的实现数据库读写分离。

  • 适用于基于 JDBC 的 ORM 框架,如:JPA、Hibernate、Mybatis、Spring JDBC Template 或直接使用 JDBC。
  • 支持任何第三方的数据库连接池,如:DBCP、C3P0、BoneCP、Druid、HikariCP等。
  • 支持任意实现 JDBC 规范的数据库。目前支持 Mysql、Oracle、SQLServer、PostgreSQL 以及任何遵循 SQL92标准的数据库
1
2
3
4
5
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>

入门案例

1、导入 maven 坐标

2、在配置文件中配置读写分离规则

3、在配置文件中配置允许 bean 定义覆盖配置项

项目实现读写分离

数据库环境准备(主从复制)

直接使用我们前面在虚拟机中搭建的主从复制的数据库环境即可。

在主库中创建瑞吉外卖项目的业务数据库 reggie 并导入相关表结构和数据

代码改造

在项目中加入 Sharding-JDBC 实现读写分离步骤:

1、导入 maven 坐标

2、在配置文件中配置读写分离规则

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
shardingsphere:
datasource:
names:
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.138.100/reggie?characterEncoding=utf-8
username: root
password: root
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.138.101/reggie?characterEncoding=utf-8
username: root
password: root
masterslave:
# 读写分离配置
load-balance-algorithm-type: round_robin # 轮询
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启 SQL 显示,默认 false

3、在配置文件中配置允许 bean 定义覆盖配置项

1
allow-bean-definition-overriding: true

Nginx

Nginx 介绍

Nginx 是一款轻量级的 Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上 nginx 的并发能力在同类型的网页服务器中表现较好,中国大陆使用 nginx 的网站有:百度、京东、新浪、网易、腾讯、淘宝等。


Nginx 是由伊戈尔·赛索耶夫俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

官网:https://nginx.org/

Nginx 下载和安装

可以到 Nginx 官方网站下载 Nginx 的安装包,地址为:https://nginx.org/en/download.hmtl


安装过程:

1、安装依赖包:yum -y install gcc pcre-devel zlib-devel openssl openssl-devel

2、下载 Nginx 安装包 wget https://nginx.org/download/nginx-1.16.1.tar.gz

3、解压 tar -zxvf nginx-1.16.1.tar.gz

4、cd nginx-1.16.1

5、./configure –prefix=/usr/local/nginx (记得先创建目录)

6、make && make install

Nginx 目录结构

重点目录/文件:

  • conf/nginx.cnf nginx 配置文件
  • html 存放静态文件(html、css、js等)
  • logs 日志目录,存放日志文件
  • sbin/nginx 二进制文件,用于启动、停止 Nginx 服务

Nginx 命令

查看版本

查看 Nginx 版本可以使用命令:

在 sbin 目录下运行改命令

./nginx -V

检查配置文件正确性

在启动 Nginx 服务之前,可以先检查一下 conf/nginx.conf 文件配置的是否有错误,命令如下:

./nginx -t

启动和停止

启动 Nginx 服务使用如下命令:

./nginx

停止 Nginx 服务使用如下命令:

./nginx -s stop

启动完成后可以查看 Nginx 进程:

ps -ef | grep nginx

重新加载配置文件

当修改 Nginx 配置文件后,需要重新加载才能生效,可以使用下面命令重新加载配置文件:

./nginx -s reload

注意:nginx 配置环境变量:vim /etc/profile

在 PATH 追加/usr/local/nginx/sbin:

在执行 source /etc/profile 重新加载文件

Nginx 配置文件结构

整体结构介绍

Nginx 配置文件(conf/nginx.conf)整体分为三部分:

  • 全局块:和 Nginx 运行相关的全局配置
  • events 块:和网络连接相关的配置
  • http 块:代理、缓存、日志记录、虚拟主机配置
    • http 全局块
    • server 块
      • server 全局块
      • location 块

注意:http 块中可以配置多个 server 块,每个 server 块中可以配置多个 location 块。

Nginx 具体应用

部署静态资源

Nginx可以作为静态 web 服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的 html 页面、css 文件、js 文件、图片、视频等资源。

相对于 Tomcat,Nginx 处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到 Nginx 中,将静态资源部署到 Nginx 非常简单,只需要将文件复制到 Nginx 安装目录下的 html 目录中即可。

1
2
3
4
5
6
7
8
server {
listen: 80; # 监听端口
server_name: localhost; # 服务器名称
location { # 匹配客户端请求 url
root hmtl; # 指定静态资源根目录
index index.hmtl; # 指定默认首页
}
}

反向代理

  • 正向代理

是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。

正向代理的典型用途是为在防火墙内的局域网客户端提供访问 Internet 的途径。

正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。

  • 反向代理

反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。

用户不需要知道目标服务器的地址,也无需在用户端做任何设定。

  • 配置反向代理
1
2
3
4
5
6
7
8
9
10
11
12
13
server {

listen 82;

server_name localhost;

location {

proxy_pass http://192.168.138.101:8080; # 反向代理,将请求转发到指定服务

}

}

负载均衡

早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。

  • 应用集群:将同意应用部署到多台机器上,组成应用集群,接收负载均衡器发布的请求,进行业务处理并返回响应数据
  • 负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理

配置负载均衡
1
2
3
4
5
6
7
8
9
10
11
upstream targetserver { # upstream 指令可以定义一组服务器
server 192.168.138.101:8080;
server 192.168.138.101:8081;
}
server {
listen 8080;
server_name localhost;
location {
proxy_pass http://targetserver
}
}
负载均衡策略
名称 说明
轮询 默认方式
weight 权重方式
ip_hash 根据 ip 分配方式
least_conn 根据最少连接方式
url_hash 根据 url 分配方式
fair 根据响应时间方式

前后端分离

问题说明

  • 开发人员同时负责前端和后端代码开发,分工不明确
  • 开发效率低
  • 前后端代码混合在一个工程中,不便于管理
  • 对开发人员要求高,人员招聘困难

前后端分离开发

介绍

前后端分离开发,就是在项目开发过程中,对于前端代码的开发由专门的前端开发人员负责,后端代码则由后端开发人员负责,这样可以做到分工明确、各司其职,提高开发效率,前后端代码并行开发,可以加快项目开发进度。

目前,前后端分离开发方式已经被越来越多的公司所采用,称为当前项目开发的主流开发方式。

前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个 maven 工程中,而是分为前端工程和后端工程。


后端工程打包部署到 Tomcat上、前端工程打包部署到 Nginx 上

开发流程

前后端分离开发后,面临一个问题,就是前端开发人员和后端开发人员如何进行配合来共同开发一个项目?

可按照如下流程进行:

image-20221014203622238

接口(API 接口)就是一个 http 的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容。

前端技术栈

开发工具
  • Visual Studio Code
  • hbuilder
技术框架
  • nodejs
  • VUE
  • ElementUI
  • mock
  • webpack

YApi

介绍

YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需要利用平台提供的接口数据写 YApi 让接口开发更简单高效,让接口的管理更具可读性,可维护性,让团队协作更合理。

源码地址:https://github.com/YMEE/yapi

要使用 YApi,需要自己进行部署

使用方式

使用 YApi,可以执行下面操作:

  • 添加项目
  • 添加分类
  • 添加接口
  • 编辑接口
  • 查看接口

Swagger

介绍

使用 Swagger 你只需要按照它的规范去定义接口及接口相关的信息,再通过 Swagger 衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。

官网:https://swagger.io/


knife4j 是为 Java MVC 框架继承 Swagger 生成 Api 文档的增强解决方案

1
2
3
4
5
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>

使用方式

操作步骤:

1、导入 knife4j 的 maven 坐标

2、导入 knife4j 相关配置类(WebMvcConfig)

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
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {

/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}

/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}

@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}

3、设置静态资源,否则接口文档页面无法访问

WebMvcConfig 类中的 addResourceHandlers 方法,添加

1
2
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

4、在 LoginCheckFilter 中设置不需要处理的请求路径

1
2
3
4
5
6
7
// 定义不需要处理的请求路径
String[] urls = new String[] {
"/doc.html",
"webjars/**",
"swagger-resources",
"/v2/api-docs"
}

常用注解

注解 说明
@Api 用在请求的类上,例如 Controller,表示对类的说明
@ApiModel 用在类上,通常是实体类,表示一个返回响应数据的信息
@ApiModelProperty 用在属性上,描述响应类的属性
@ApiOperation 用在请求的方法上,说明方法的用途、作用
@ApiImplicitParams 用在请求的方法上,表示一组参数说明
@ApiImplicitParam 用在@ApiImplicitParams 注解中,指定一个请求参数的各个方面

项目部署

部署架构

image-20221014212850609

部署环境说明

服务器:

  • 192.168.138.100(服务器A)
    • Nginx:部署前端项目、配置反向代理
    • Mysql:主从复制结构中的主库
  • 192.168.138.101(服务器B)
    • jdk:运行 Java 项目
    • git:版本控制工具
    • maven:项目构建工具
    • jar:Spring Boot 项目打包成 jar 包基于内置 Tomcat 运行
    • Mysql:主从复制结构中的从库
  • 172.17.2.94(服务器C)
    • Redis:缓存中间件

部署前端项目

第一步:在服务器 A 中安装 Nginx,将 dist 目录中上传到 Nginx 的 html 目录下

第二步:修改 Nginx 配置文件 nginx.conf

部署后端项目

第一步:在服务器 B 中安装 jdk、git、maven、Mysql,使用 git clone 命令将 git 远程仓库的代码克隆下来

第二步:将 reggieStart.sh 文件上传到服务器 B,通过 chmod 命令设置执行权限


文章作者: Water monster
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Water monster !
评论
  目录