前言
跟着视频学了那么多技术,有没有自己尝试过做一个开源项目呢?
下面让我们一步一步分析这个最火的前后端分离项目
项目地址:
https://github.com/YuyanCai/mall
从0开始一个开源项目
- 看简介,知道项目是做什么的
- 看代码更新频率,几年没更新的最好别用
- 看README.md了解项目是否符合自己的技术栈
- 运行项目
- 本地拉取代码
- 看项目从整体到局部,先看项目架构 =》 看POM文件 =》看YML配置文件 =》看目录结构
项目架构
mall-admin-web
mall-admin-web是一个电商后台管理系统的前端项目,基于Vue+Element实现。主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等功能。
[外链图片转存中…(img-kDx7BIEi-1656677294008)]
mall
mall项目(50k+star)是一套电商系统,使用现阶段主流技术实现。涵盖了SpringBoot 2.3.0、MyBatis 3.4.6、Elasticsearch 7.6.2、RabbitMQ 3.7.15、Redis 5.0、MongoDB 4.2.5、Mysql5.7等技术,采用Docker容器化部署。
[外链图片转存中…(img-ufkfKlgB-1656677294011)]
从上面可以看出,这是一个前后端分离的项目
前端项目为mall-admin-web
后端项目为mall
所用技术栈也比较符合我们Java工程师
其中Mybatis不想用的话也可以用MP来代替
ES和Mongodb没接触过的话,不用从头去学,了解下怎么使用即可~
Github1s
一个开源项目,能够直接在github页面通过vscode查看项目代码
使用方法就是在项目地址中的github关键字后加上1s回车即可查看
[外链图片转存中…(img-ZpMsKSo5-1656677294012)]
前端代码架构
前端看视频学的谷粒学院很相似,
所以前端没啥大问题。
[外链图片转存中…(img-bAG9VC0r-1656677294013)]
后端代码架构
mall
├── mall-common – 工具类及通用代码
├── mall-mbg – MyBatisGenerator生成的数据库操作代码
├── mall-security – SpringSecurity封装公用模块
├── mall-admin – 后台商城管理系统接口
├── mall-search – 基于Elasticsearch的商品搜索系统
├── mall-portal – 前台商城系统接口
└── mall-demo – 框架搭建时的测试代码
[外链图片转存中…(img-2GTDsRam-1656677294015)]
简单了解下商品模块的功能
这个商品列表,也就是CRUD中的查询,只不过人家查询的条目很多
[外链图片转存中…(img-q1sJnoWk-1656677294016)]
添加商品呢,也就是CRUD中的增
[外链图片转存中…(img-a5MgcDkN-1656677294017)]
商品回收站,也就是CRUD中的删
[外链图片转存中…(img-xzOpv49T-1656677294017)]
商品的配置就属于是优化部分了,比如打开一级菜单的时候显示属于一级菜单的二级菜单
[外链图片转存中…(img-jMPN5B8j-1656677294018)]
最后是一些附加项,如商品的品牌管理,库存管理,图片管理,分类管理等等的一些CRUD操作
[外链图片转存中…(img-YdLrpeww-1656677294019)]
总结
实现功能如下,简单说就是针对商品的各种管理。如商品,类型,分类,品牌,订单…
[外链图片转存中…(img-fqSHCpz2-1656677294022)]
整体看下来,技术点是不难的。难点是细节比较多,业务逻辑可能稍微复杂,但是越难我们越要上!要对自己很有自信,一个一个的去攻破难点,这样我们才能不断的变强,与诸君共勉!
后台架构
mall
├── mall-common – 工具类及通用代码
├── mall-mbg – MyBatisGenerator生成的数据库操作代码
├── mall-security – SpringSecurity封装公用模块
├── mall-admin – 后台商城管理系统接口
├── mall-search – 基于Elasticsearch的商品搜索系统
├── mall-portal – 前台商城系统接口
└── mall-demo – 框架搭建时的测试代码
通过前面的学习我们知道了开发接口的套路
- 建表写sql
- 定义实体类
- dao与mapper
- service
- controller
maven工程分析
此项目采用maven来做包的依赖管理
下面简单分析下maven的结构
聚合
项目采用maven的聚合和继承
聚合是为了更快的构建项目,是表示项目与子项目之间的关系
继承则是消除不同模块同种依赖节省了不必要配置
mall这个项目,有管理商品项目、mbg项目,权限管理项目。这个时候在maven中表达这种归属关系,就可以用maven的聚合来表示,如下:
<modules> <module>mall-common</module> <module>mall-admin</module> <module>mall-mbg</module> </modules>
一般情况把子模块放到父模块下面,也可以在同一模块,只需要改变module的值即可
<modules> <module>../mall-common</module> <module>../mall-admin</module> <module>../mall-mbg</module> </modules>
继承
spring-boot-starter-actuator可以用于检测系统的健康情况、当前的Beans、系统的缓存等
spring-boot-starter-aop Spring Boot使用AOP
项目的 dependencies 元素中声明该依赖,就会自动继承到子模块中
其中spring-boot-starter-actuator、spring-boot-starter-aop…都可以自动继承到子模块
[外链图片转存中…(img-aLxaODtX-1656677294023)]
common模块
此模块定义多个微服务模块公用的工具类,异常处理类,统一返回类等公共部分
为了更好的理解,有一些前置知识需要在回顾一下:
一、枚举
Java 枚举是一个特殊的类,一般表示一组常量,它是线程安全的,所以定义固定的常量一般把他们定义在枚举类里
创建一个枚举类,经过编译后实际上会生成一个对应的抽象类,这个类继承了Java API中的java.lang.Enum类
还为我们生成了两个静态方法,分别是values()和 valueOf()
图中所举例子TEST1将会变成public static final R TEST1;
[外链图片转存中…(img-y0ju3K0R-1656677294024)]
项目中用到的是枚举的高级用法,向enum类添加方法与自定义属性和构造函数
public enum ResultCode implements IErrorCode { SUCCESS(200, "操作成功"), FAILED(500, "操作失败"), VALIDATE_FAILED(404, "参数检验失败"), UNAUTHORIZED(401, "暂未登录或token已经过期"), FORBIDDEN(403, "没有相关权限"); private long code; private String message; private ResultCode(long code, String message) { this.code = code; this.message = message; } public long getCode() { return code; } public String getMessage() { return message; } }
二、泛型
1.什么是泛型?
泛型是程序语言的一种特性,指类型参数化
2.为什么要有泛型?
为了使代码更灵活,因为java是强类型语言(强类型语言是一种强制类型定义的语言,即一旦某一个变量被定义类型,如果不经强制转换,那么它永远就死该数据类型。),引入泛型后可以让部分代码可变,这部分代码在使用前必须声明。还有就是减少强制类型转换
3.泛型方法
泛型方法就是方法的返回值不是确定的类型,通过一个通配符来占位,等真正用到此方法的时候在指定返回值类型。
4.泛型类
同泛型方法在详细可看我之前发布的文章泛型篇
强哥说Java–Java的泛型_小蜗牛耶的博客-CSDN博客_强哥说java
pom
这里redis先注释,等整合的时候再用
<?xml version="1.0" encoding="UTF-8"?> <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"> <parent> <artifactId>mall-study</artifactId> <groupId>com.caq.mall</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>mall-common</artifactId> <dependencies> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-data-redis</artifactId>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> </dependency> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> </project>
统一返回类
将后端处理好的数据以同一的格式返回给前端
格式固定,可以根据项目需求更改
这个项目写的这套返回类是很标准的,工作了我们也可以拿这个来写
/** * 通用返回对象 */ public class CommonResult<T> { /** * 状态码 */ private long code; /** * 提示信息 */ private String message; /** * 数据封装 */ private T data; protected CommonResult() { } protected CommonResult(long code, String message, T data) { this.code = code; this.message = message; this.data = data; } /** * 成功返回结果 * * @param data 获取的数据 */ public static <T> CommonResult<T> success(T data) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } /** * 成功返回结果 * * @param data 获取的数据 * @param message 提示信息 */ public static <T> CommonResult<T> success(T data, String message) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data); } /** * 失败返回结果 * @param errorCode 错误码 */ public static <T> CommonResult<T> failed(IErrorCode errorCode) { return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null); } /** * 失败返回结果 * @param errorCode 错误码 * @param message 错误信息 */ public static <T> CommonResult<T> failed(IErrorCode errorCode,String message) { return new CommonResult<T>(errorCode.getCode(), message, null); } /** * 失败返回结果 * @param message 提示信息 */ public static <T> CommonResult<T> failed(String message) { return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null); } /** * 失败返回结果 */ public static <T> CommonResult<T> failed() { return failed(ResultCode.FAILED); } /** * 参数验证失败返回结果 */ public static <T> CommonResult<T> validateFailed() { return failed(ResultCode.VALIDATE_FAILED); } /** * 参数验证失败返回结果 * @param message 提示信息 */ public static <T> CommonResult<T> validateFailed(String message) { return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null); } /** * 未登录返回结果 */ public static <T> CommonResult<T> unauthorized(T data) { return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); } /** * 未授权返回结果 */ public static <T> CommonResult<T> forbidden(T data) { return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); } public long getCode() { return code; } public void setCode(long code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
分页数据封装类
package com.caq.mall.common.api; import com.github.pagehelper.PageInfo; import org.springframework.data.domain.Page; import java.util.List; /** * 分页数据封装类 */ public class CommonPage<T> { /** * 当前页码 */ private Integer pageNum; /** * 每页数量 */ private Integer pageSize; /** * 总页数 */ private Integer totalPage; /** * 总条数 */ private Long total; /** * 分页数据 */ private List<T> list; /** * 将PageHelper分页后的list转为分页信息 */ public static <T> CommonPage<T> restPage(List<T> list) { CommonPage<T> result = new CommonPage<T>(); PageInfo<T> pageInfo = new PageInfo<T>(list); result.setTotalPage(pageInfo.getPages()); result.setPageNum(pageInfo.getPageNum()); result.setPageSize(pageInfo.getPageSize()); result.setTotal(pageInfo.getTotal()); result.setList(pageInfo.getList()); return result; } /** * 将SpringData分页后的list转为分页信息 */ public static <T> CommonPage<T> restPage(Page<T> pageInfo) { CommonPage<T> result = new CommonPage<T>(); result.setTotalPage(pageInfo.getTotalPages()); result.setPageNum(pageInfo.getNumber()); result.setPageSize(pageInfo.getSize()); result.setTotal(pageInfo.getTotalElements()); result.setList(pageInfo.getContent()); return result; } public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } public Integer getTotalPage() { return totalPage; } public void setTotalPage(Integer totalPage) { this.totalPage = totalPage; } public List<T> getList() { return list; } public void setList(List<T> list) { this.list = list; } public Long getTotal() { return total; } public void setTotal(Long total) { this.total = total; } }
异常处理
分别定义自定义异常、全局异常、断言
断言的作用是简化方法入参检测的代码
不使用断言的情况下我们要这样写:
public InputStream getData(String file) { if (file == null || file.length() == 0|| file.replaceAll("\\s", "").length() == 0) { throw new IllegalArgumentException("file入参不是有效的文件地址"); }
在应用 Assert 断言类后,其代码可以简化为以下的形式:
public InputStream getData(String file){ Assert.hasText(file,"file入参不是有效的文件地址");
自定义异常类:ApiException
package com.macro.mall.common.exception; import com.macro.mall.common.api.IErrorCode; /** * 自定义API异常 */ public class ApiException extends RuntimeException { private IErrorCode errorCode; public ApiException(IErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } public ApiException(String message) { super(message); } public ApiException(Throwable cause) { super(cause); } public ApiException(String message, Throwable cause) { super(message, cause); } public IErrorCode getErrorCode() { return errorCode; } }
断言类:Asserts
/** * 断言处理类,用于抛出各种API异常 */ public class Asserts { public static void fail(String message) { throw new ApiException(message); } public static void fail(IErrorCode errorCode) { throw new ApiException(errorCode); } }
全局异常处理类:GlobalExceptionHandler
/** * 全局异常处理 */ @ControllerAdvice public class GlobalExceptionHandler { @ResponseBody @ExceptionHandler(value = ApiException.class) public CommonResult handle(ApiException e) { if (e.getErrorCode() != null) { return CommonResult.failed(e.getErrorCode()); } return CommonResult.failed(e.getMessage()); } @ResponseBody @ExceptionHandler(value = MethodArgumentNotValidException.class) public CommonResult handleValidException(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField()+fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } @ResponseBody @ExceptionHandler(value = BindException.class) public CommonResult handleValidException(BindException e) { BindingResult bindingResult = e.getBindingResult(); String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField()+fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } }
Swagger
以下是固定写法
/** * Swagger基础配置 */ public abstract class BaseSwaggerConfig { @Bean public Docket createRestApi() { SwaggerProperties swaggerProperties = swaggerProperties(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo(swaggerProperties)) .select() .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getApiBasePackage())) .paths(PathSelectors.any()) .build(); // if (swaggerProperties.isEnableSecurity()) { // docket.securitySchemes(securitySchemes()).securityContexts(securityContexts()); // } return docket; } private ApiInfo apiInfo(SwaggerProperties swaggerProperties) { return new ApiInfoBuilder() .title(swaggerProperties.getTitle()) .description(swaggerProperties.getDescription()) .contact(new Contact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(), swaggerProperties.getContactEmail())) .version(swaggerProperties.getVersion()) .build(); } }
Swagger自定义配置
/** * Swagger自定义配置 */ @Data @EqualsAndHashCode(callSuper = false) @Builder public class SwaggerProperties { /** * API文档生成基础路径 */ private String apiBasePackage; /** * 是否要启用登录认证 */ private boolean enableSecurity; /** * 文档标题 */ private String title; /** * 文档描述 */ private String description; /** * 文档版本 */ private String version; /** * 文档联系人姓名 */ private String contactName; /** * 文档联系人网址 */ private String contactUrl; /** * 文档联系人邮箱 */ private String contactEmail; }