该系列将记录一份完整的实战项目的完成过程,该篇属于优化篇第三天,主要负责完成前后端分离问题
案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容
该篇我们将完成以下内容:
我们在这一小节将主要介绍前后端分离的注意事项以及前端的部分知识
在我们之前的工程中由于只有我们一人开发,所以程序大多数都堆积在一起进行开发:
但是这种开发模式会导致很多的不利影响:
所以我们提出了前后端分离开发的概念:
同时我们的项目分离后,打包部署方式也发生了变化:
在目前,我们的前后端分离开发人员需要一个固定的流程来完成协商与开发:
我们将流程分为以上四步:
其中定义规范是最为重要的一项,只有规范定义合适,前后代码才会开发完善,我们给出一个规范定义的模型:
我们可以注意到里面包含了这几项重要内容:
我们在后续会逐渐介绍如何定义规范以及采用合适的工具加快规范以及接口的定义形式
我们在这次项目中主要负责后端的任务,但是我们也需要适当了解前端的技术,这里简单介绍一下前端技术栈:
开发类型 | 开发名称 | 开发用途 |
---|---|---|
开发工具 | Visual Studio Code | 前端开发软件 |
开发工具 | hbuilder | 前端开发软件 |
开发框架 | nodejs | 基本框架,相当于后端的JDK |
开发框架 | VUE | 静态资源框架,用于布局静态资源H5,CSS3等 |
开发框架 | ElementUI | 静态资源框架,方便美化静态资源的部署 |
开发框架 | mock | 前端测试工具,模拟响应数据在前端的表现形式 |
开发框架 | webpack | 打包工具,前端有专门的打包类型,如js等 |
我们在这一小节将主要介绍一个API的网页管理平台
我们首先来简单介绍一下YApi平台:
我们可以直接在官网注册登录YApi并使用其产品
官网链接展示:YApi Pro-高效、易用、功能强大的可视化接口管理平台
下面我们来介绍YApi的具体使用细节:
我们在这一小节将主要介绍一个SwaggerAPI自动生成的IDEA插件
我们首先来简单介绍一下Swagger插件:
下面我们来详细介绍Swagger的使用:
<!--knife4j(Swagger)坐标-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
// 我们直接在WebMvcConfig中编写配置类即可
// 1.首先我们需要添加两个注解@EnableSwagger2,@EnableKnife4j表示开启Swagger以及knife4j
// 2.其次我们需要添加两个方法用分别来负责扫描接口并返回文档信息,以及编写文档基本信息
package com.qiuluo.reggie.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.qiuluo.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
// 新导入两个注解@EnableSwagger2,@EnableKnife4j
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Bean
public Docket createRestApi() {
// 文档类型
// (返回一个文档类型Docket,下面是返回文档的类型,基本为固定形式,除了basePackage,书写你的Controller包的位置)
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.qiuluo.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
// 描述文档内容
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
// 我们Swagger插件会自动生成一个doc.html网页,我们通过查询该网页来查看接口,我们需要将这个静态资源放开
package com.qiuluo.reggie.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.qiuluo.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态映射");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
registry.addResourceHandler("classpath:/static/*.html");
registry.addResourceHandler("/static/**");
// 系统自动帮忙生成这doc.html页面用于展示我们的接口信息,我们需要将他们放行
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
// 我们在之前的项目中设置了登录需求,我们在这里将该文档排除,否则我们需要先登录再访问该页面
package com.qiuluo.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.qiuluo.reggie.common.BaseContext;
import com.qiuluo.reggie.common.Result;
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;
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
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;
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求路径("/doc.html","/webjars/**","/swagger-resources","/v2/api-docs")
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/login",
"/user/sendMsg",
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
};
boolean check = check(urls, requestURI);
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
log.info("登录中...");
log.info("线程id" + Thread.currentThread().getId());
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
if(request.getSession().getAttribute("user") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
log.info("登录中...");
log.info("线程id" + Thread.currentThread().getId());
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(Result.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;
}
}
下面我们来简单介绍一个doc.html网页都具备什么功能:
Swagger为我们提供了相关注解来帮助书写doc文档:
注解 | 说明 |
---|---|
@Api | 用于请求的类上,表示对类的说明(Controller) |
@ApiModel | 用于类上,通常是实体类,表示一个返回数据的信息(domain,Result) |
@ApiModelProperty | 用于属性上,描述相应类的属性(name) |
@ApiOperation | 用于请求的方法上,说明方法的用途,作用 |
@ApiImplicitParams | 用于请求的方法上,表示一组参数说明 |
@ApiImplicitParam | 用于请求的方法上,表示单个参数说明 |
我们先给出实体类的常用注解示例:
package com.qiuluo.reggie.common;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
// @ApiModel(value = "返回类型")表示解释实体类
@Data
@ApiModel(value = "返回类型")
public class Result<T> implements Serializable {
// @ApiModelProperty(value = "状态码")表示解释实体类属性
@ApiModelProperty(value = "状态码")
private Integer code; //编码:1成功,0和其它数字为失败
@ApiModelProperty(value = "错误信息")
private String msg; //错误信息
@ApiModelProperty(value = "数据信息")
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> Result<T> success(T object) {
Result<T> res = new Result<T>();
res.data = object;
res.code = 1;
return res;
}
public static <T> Result<T> error(String msg) {
Result res = new Result();
res.msg = msg;
res.code = 0;
return res;
}
public Result<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
我们再给出服务层的常用注解案例:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
// @Api(tags = "套餐接口数据")表示服务层名称
@Slf4j
@RestController
@RequestMapping("/setmeal")
@Api(tags = "套餐接口数据")
public class SetmealController {
@Autowired
private DishServiceImpl dishService;
@Autowired
private SetmealServiceImpl setmealService;
@Autowired
private SetmealDishServiceImpl setmealDishService;
@Autowired
private CategoryServiceImpl categoryService;
// @ApiOperation(value = "新增接口")表示服务层接口名称
/*
@ApiImplicitParams(
{
@ApiImplicitParam(name = "page",value = "页码",required = true),
@ApiImplicitParam(name = "pageSize",value = "每页大小",required = true)
}
)
用于解释内部参数信息
@ApiImplicitParams作为整体框架,内部存在多个参数时,需要采用{}包括
@ApiImplicitParam作为内部信息,name表示参数名,value表示文档名,required表示是否必须
*/
/**
* 新增
* @param setmealDto
* @return
*/
@PostMapping
@ApiOperation(value = "新增接口")
@CacheEvict(value = "setmealCache",allEntries = true)
public Result<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
log.info("套餐新增成功");
return Result.success("新创套餐成功");
}
/**
* 分页
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("page")
@ApiOperation(value = "分页接口")
@ApiImplicitParams(
{
@ApiImplicitParam(name = "page",value = "页码",required = true),
@ApiImplicitParam(name = "pageSize",value = "每页大小",required = true),
@ApiImplicitParam(name = "name",value = "查找值",required = false)
}
)
public Result<Page> page(int page, int pageSize, String name){
// 构造分页器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();
// 构造条件
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null,Setmeal::getName,name);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
// 查询
setmealService.page(pageInfo);
// 赋值
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item,setmealDto);
// 将CategoryName复制进去
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
// 返回结果
return Result.success(setmealDtoPage);
}
/**
* 删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation(value = "删除接口")
@CacheEvict(value = "setmealCache",allEntries = true)
public Result<String> delete(@RequestParam List<Long> ids){
setmealService.removeWithDish(ids);
return Result.success("删除成功");
}
/**
* 修改状态
* @param ids
* @return
*/
@PostMapping("/status/0")
@ApiOperation(value = "修改状态接口")
@CacheEvict(value = "setmealCache",allEntries = true)
public Result<String> closeStatus(@RequestParam List<Long> ids){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
List<Setmeal> setmeals = setmealService.list(queryWrapper);
for (Setmeal setmeal:setmeals
) {
setmeal.setStatus(0);
setmealService.updateById(setmeal);
}
return Result.success("修改成功");
}
/**
* 修改状态
* @param ids
* @return
*/
@PostMapping("/status/1")
@ApiOperation(value = "修改状态接口")
@CacheEvict(value = "setmealCache",allEntries = true)
public Result<String> openStatus(@RequestParam List<Long> ids){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
List<Setmeal> setmeals = setmealService.list(queryWrapper);
for (Setmeal setmeal:setmeals
) {
setmeal.setStatus(1);
setmealService.updateById(setmeal);
}
return Result.success("修改成功");
}
/**
* 查询
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation(value = "查询接口")
public Result<SetmealDto> getById(@PathVariable Long id){
SetmealDto setmealDto = new SetmealDto();
// 将普通数据传入
Setmeal setmeal = setmealService.getById(id);
BeanUtils.copyProperties(setmeal,setmealDto);
// 将菜品信息传递进去
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,id);
List<SetmealDish> list = setmealDishService.list(queryWrapper);
setmealDto.setSetmealDishes(list);
return Result.success(setmealDto);
}
/**
* 修改
* @param setmealDto
* @return
*/
@PutMapping
@ApiOperation(value = "修改接口")
@CacheEvict(value = "setmealCache",allEntries = true)
public Result<String> update(@RequestBody SetmealDto setmealDto){
setmealService.updateById(setmealDto);
return Result.success("修改成功");
}
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
@ApiOperation(value = "条件查询接口")
@Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
public Result<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return Result.success(list);
}
/**
* 移动端点击套餐图片查看套餐具体内容
* 这里返回的是dto 对象,因为前端需要copies这个属性
* 前端主要要展示的信息是:套餐中菜品的基本信息,图片,菜品描述,以及菜品的份数
* @param SetmealId
* @return
*/
@GetMapping("/dish/{id}")
@ApiOperation(value = "点击查看接口")
public Result<List<DishDto>> dish(@PathVariable("id") Long SetmealId) {
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId, SetmealId);
//获取套餐里面的所有菜品 这个就是SetmealDish表里面的数据
List<SetmealDish> list = setmealDishService.list(queryWrapper);
List<DishDto> dishDtos = list.stream().map((setmealDish) -> {
DishDto dishDto = new DishDto();
//其实这个BeanUtils的拷贝是浅拷贝,这里要注意一下
BeanUtils.copyProperties(setmealDish, dishDto);
//这里是为了把套餐中的菜品的基本信息填充到dto中,比如菜品描述,菜品图片等菜品的基本信息
Long dishId = setmealDish.getDishId();
Dish dish = dishService.getById(dishId);
BeanUtils.copyProperties(dish, dishDto);
return dishDto;
}).collect(Collectors.toList());
return Result.success(dishDtos);
}
}
在进行部分注解修饰后,我们的doc网页会发生部分变化,我们来简单看一下:
在前面我们已经完成了项目并且掌握了项目部署的根本需求,下面我们来完成项目部署
我们首先给出部署架构图:
我们可以看到:
我们给出整个部署所需要的环境:
我们首先来完成前端项目的部署:
到这里我们的前端部署就结束了
我们来简单解释一下以上操作:
首先是页面展示问题:
然后是请求跳转问题:
我们再来完成后端项目的部署:
# 进入到/usr/local/javaapp目录下
cd /usr/local/javaapp
# git命令复制(可以到我的git仓库下载V1.2的完成版:https://gitee.com/QiuLuoYuWeiLiang/qiu-luo-reggie)
git clone SSH地址
# 上传直接采用FinalShell的上传机制,这里不做解释了
# 上传后我们可以通过chmod设置权限
chmod 777 reggieStart.sh
# 下面我们给出reggieStart.sh文件做简单讲解
#!/bin/sh
echo =================================
echo 自动化部署脚本启动
echo =================================
echo 停止原来运行中的工程
APP_NAME=qiu-luo-reggie # 这里的名字是你的项目名称(记得修改)
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
echo 'Stop Process...'
kill -15 $tpid
fi
sleep 2
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
echo 'Kill Process!'
kill -9 $tpid
else
echo 'Stop Success!'
fi
echo 准备从Git仓库拉取最新代码
cd /usr/local/javaapp/qiu-luo-reggie # 这里改为你的目录(记得修改)
echo 开始从Git仓库拉取最新代码
git pull
echo 代码拉取完成
echo 开始打包
output=`mvn clean package -Dmaven.test.skip=true`
cd target
echo 启动项目
nohup java -jar reggie_take_out-1.0-SNAPSHOT.jar &> reggie_take_out.log &
echo 项目启动完成
# 执行sh文件(第一次需要下载大量依赖,时间较长)
./reggieStart.sh
# 查看log日志
vim reggie/target/reggie_take_out.log
# 可以实时查看
tail -f reggie/target/reggie_take_out.log
整个项目到这里就结束了,希望能为你带来帮助~
该文章属于学习内容,具体参考B站黑马程序员的Java项目实战《瑞吉外卖》
这里附上视频链接:项目优化Day3-01-本章内容介绍_哔哩哔哩_bilibili