上海高端网站建设高端网站建设厦门百度开户
后端基础框架搭建
父子项目搭建
- 新建springboot项目
- 选择springboot版本,并勾选所需依赖
这里选择的是springboot 2.7.6,jdk为1.8版本!!!
- 删除src目录
- 新建 cy-blogs-admin 子模块作为项目主入口
- 在 cy-blogs-admin 子模块新建 MainApplication 启动类
注:路径需与创建项目时的软件包名称保持一致,不知道软件包路径可到父模块的pom.xml找<mainClass>
- 修改父项目 pom.xml 中,主类入口配置(改成自己启动类的名称即可)
-
新建 cy-blogs-business 子模块用于编写网站业务代码
-
在 cy-blogs-business 子模块下新建controller目录,并新建 TextController 类用于测试
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/test")
public class TestController {@GetMapping("/test")public String test(){return "test";}
}
目录结构如下
- 修改 cy-blogs-admin 子模块的 pom.xml 文件,引入业务模块
<dependencies><dependency><!-- 业务模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-business</artifactId><version>0.0.1-SNAPSHOT</version></dependency>
</dependencies>
更新依赖后,运行启动类,可正常访问接口
父子模块版本同步
如果有想法正式上线项目,那么就需要考虑项目版本的更新,在父子模块中,父模块和各个子模块的 pom.xml 中都有各自的版本信息<version>
如果每次版本变动,我们都一个一个去改的话,会很麻烦,或者出现漏改的情况。所以通常常用脚本一键设置所有模块的版本号
- 在父模块的pom.xml中添加如下代码
<build><plugins><!--多模块Maven项目统一修改版本号--><plugin><groupId>org.codehaus.mojo</groupId><artifactId>versions-maven-plugin</artifactId><version>2.18.0</version><configuration><generateBackupPoms>false</generateBackupPoms></configuration></plugin></plugins>
</build>
执行插件 -> versions -> versions:set 命令,输入新的版本号即可
所有模块版本都会同步修改
依赖管理
当前项目的主要依赖是在父模块的pom.xml中引入,虽然也能正常运行项目,但各个模块都会重复导入父模块中的所有依赖,即使某个子模块可能不需要。比如父模块没有任何代码,并不需要导入任意依赖,而admin模块并不需要用到Lombok
一般而言,父模块只负责依赖版本管理,然后在子模块中导入各自需要的依赖。所以做如下调整
- 父模块 pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.cyfy</groupId><artifactId>cy-blogs-backend</artifactId><packaging>pom</packaging><version>1.0.1-SNAPSHOT</version><modules><module>cy-blogs-business</module><module>cy-blogs-admin</module></modules><name>cy-blogs-backend</name><description>父模块,只负责依赖版本管理,不引入任何业务模块</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.7.6</spring-boot.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><!--多模块Maven项目统一修改版本号--><plugin><groupId>org.codehaus.mojo</groupId><artifactId>versions-maven-plugin</artifactId><version>2.18.0</version><configuration><generateBackupPoms>false</generateBackupPoms></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.cyfy.cyblogsbackend.MainApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build>
</project>
- admin模块
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-admin</artifactId><description> web服务入口 </description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><!-- 业务模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-business</artifactId><version>1.0.1-SNAPSHOT</version></dependency></dependencies>
</project>
- business模块
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-business</artifactId><description> 业务模块 </description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies></project>
- 刷新 Maven 依赖后,依赖结构如下
因为admin 引入了business 模块,所以能使用business模块的方法和能传递的依赖,这使得当前项目虽然依赖结构发生了变化,但与之前的效果一样
这里子模块导入依赖,虽然可以凭借依赖的传递性,但最好还是确保依赖能在需要的模块层中引入。
如模块A引入模块B,模块B引入模块C的链式中,如果只有模块A需要依赖a,模块B需要依赖b,模块C需要依赖c,那么应该在模块A中引入依赖a,模块B中引入依赖b,模块C中引入依赖c,而不是依赖a、b、c都在模块C中引入。这样模块A虽然会有依赖b和依赖c,但模块B和模块C不会有依赖a
如果想要依赖b只被模块B引入,依赖c只被模块C引入,可以在依赖项中使用<optional>true</optional>
,如项目中的Lombok就有该标签。这使得Lombok只在business中被引入,而admin没有
移除该标签,可以很明显的看到admin模块会新增Lombok依赖
注意:请注意避免循环引入模块,即模块A引入模块B,模块B引入模块C,模块C又引入模块A,形成闭环
核心模块
上面我们将项目分成admin和business两个模块,admin模块作为项目主入口模块,只负责启动项目,而business模块负责项目业务代码,但如果我们项目很大很大,那么如果都在一个business模块中编写,该模块内容就会非常非常多,可能有几十个目录,几千个文件,这样我们后续要修改某个文件时,就会很吃力。
所以就需要再新增新的业务模块,分别去做不同业务的事。比如business-blogs模块专门写博客相关业务,business-shop模块专门写商城相关业务等等,专门的模块写专门的业务逻辑,这样我们要改博客相关的业务代码就可以去business-blogs模块改。
那么如果业务分成多个模块了,所需依赖该在哪引入呢?因为都是业务模块,使用的依赖可能大径相同。我们自然可以在每个业务模块中重复导入相同依赖,但这样并不是一件好事,就不说项目导入多少依赖,就说如果我们需要更新某个依赖项时,那么我们是不是就要一个一个模块的去改。当然我们也可以让其他业务模块都直接或间接引入其中一个业务模块,这样也能实现依赖传递,但倘若这个业务恰好是我们不需要的业务模块呢?是否又要去找一个新的业务模块作为主要依赖模块?
所以更好的解决方案是,新增一个专门管理这些依赖的模块,然后所有业务模块都去引入它即可
- 新建 framework 模块
- 修改 framework 模块的pom.xml,将business模块的依赖移到这里
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-framework</artifactId><description> 核心模块 </description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies></project>
- 修改business模块的pom.xml,引入framework模块
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-business</artifactId><description> 业务模块 </description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><!-- 核心模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-framework</artifactId><version>1.0.1-SNAPSHOT</version></dependency></dependencies>
</project>
这里有两个注意事项
- framework模块中引入Lombok时,要移除
<optional>true</optional>
标签,使Lombok依赖能够传递使用 spring-boot-starter-test
测试依赖只针对当前模块,所以应该只在所需模块中引入
当前项目依赖结构
运行项目,无任何异常
公共模块
项目开发中,会出现一下重复的代码,我们都可以封装成一个通用类去使用,比如异常处理类、通用响应类等,这些类可能贯彻我们整个项目,所以我们可以再新建这么一个模块,专门去编写这些贯彻整个项目的方法类
因为当前项目模块中, framework 模块是最底层的,所以在framework 模块中引入common模块,就能让所有模块都具有common模块的方法和依赖
- framework 模块pom.xml
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-framework</artifactId><description> 核心模块 </description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><!-- 公共模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-common</artifactId><version>1.0.1-SNAPSHOT</version></dependency></dependencies></project>
- common模块pom.xml
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-common</artifactId><description> 公共模块 </description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies></project>
这里把spring-boot-starter-web
和lombok
都放在common模块中,是考虑到后续common模块中的类也需要使用到这两个依赖
当前项目依赖结构
项目预期结构
运行项目,无任何异常
小优化
移除所有子模块中引入其它模块的版本信息
统一在父模块中管理版本信息
这样,版本更替时,只需要在父模块中修改即可
整合Knife4j接口文档
Knife4j是基于Swagger接口文档的增强工具,提供了更加友好的Api文档界面和功能扩展,例如动态参数调试、分组文档等
- 在admin模块中添加依赖
<!-- knife4j 接口文档 -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
</dependency>
- 父模块中添加版本管理
<!-- knife4j 接口文档 -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.4.0</version>
</dependency>
后面添加依赖不再分开展示,自行在所需模块中引入,并在父模块中管理版本
- 在admin模块中的resources目录下新建application.yml配置文件,配置如下
# 开发环境配置
server:# 服务器的HTTP端口,默认为 8080port: 8080
# 接口文档配置
knife4j:enable: trueopenApi:title: "接口文档"version: "1.0.0"group:default:api-rule: packageapi-rule-resources:# 项目控制层路径- com.cyfy.cyblogsbackend.controller
当前项目结构如下
运行项目,访问http://localhost:8080/doc.html,可正常访问接口文档,
- Knife4j接口文档整合完成
整合MyBatis Plus 进行数据操作
在 framework 模块中导入依赖
<!--引入mybatisPlus 包含了 jdbc-->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.7</version>
</dependency>
<!--mysql-->
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
在admin模块中的application.yml中配置数据库和mybatis-plus 的相关配置
# 数据库连接配置,记得新建一个数据库
spring:datasource:url: jdbc:mysql://localhost:3306/cy_blogs?useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456# Mybatis-Plus配置
mybatis-plus:configuration:# 将数据库的字段的下划线的命名映射成驼峰的方式,设为false则不进行转换map-underscore-to-camel-case: true# 仅在开发环境开启日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: isDelete # 全局逻辑删除的实体字段logic-delete-value: 1 # 逻辑已删除值(默认为1)logic-not-delete-value: 0 # 逻辑未删除值(默认为0)
新建对应数据库,并使用IDEA进行连接
填写数据库信息,点击测试无异常后,点击应用并确定
此时IDEA已连接上了数据库
设计用户表,因为这里只是做整合测试,所以随便创建个表即可,不用在意是否符合库表设计,后续再根据实际需求修改库表结构
# 创建用户表
create table if not exists user_info (user_id bigint auto_increment primary key comment '用户表主键',user_account varchar(256) not null comment '用户账号',user_password varchar(256) not null comment '用户密码',user_email varchar(256) not null comment '用户邮件',user_avatar varchar(256) not null comment '用户头像',create_time timestamp not null default current_timestamp comment '创建时间',update_time timestamp not null default current_timestamp on update current_timestamp comment '更新时间',is_delete tinyint not null default 0 comment '是否删除',UNIQUE KEY `uk_user_account` (`user_account`)
)comment '用户信息表' collate = utf8mb4_unicode_ci;
- UNIQUE KEY:唯一约束,避免用户创建相同账号
使用MyBatisX快速生成MyBatis Plus的基础curd代码(MyBatisX直接在设置 -> 插件中安装)
右键数据库栏中的user_info表,选择MybatisX-Generator
选择生成代码路径
根据所需选择生成Mybatis-Plus3代码
注意取消Actual Column的勾选,否则生成的实体类不为驼峰
点击Finish后,我们可在business模块中看到生成的代码
将generator目录下的文件全部移动到自己项目路径下,最后删除generator目录
修改resources/mapper/UserInfoMapper.xml文件如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cyfy.cyblogsbackend.mapper.UserInfoMapper"><resultMap id="BaseResultMap" type="com.cyfy.cyblogsbackend.domain.UserInfo"><id property="userId" column="user_id" jdbcType="BIGINT"/><result property="userAccount" column="user_account" jdbcType="VARCHAR"/><result property="userPassword" column="user_password" jdbcType="VARCHAR"/><result property="userEmail" column="user_email" jdbcType="VARCHAR"/><result property="userAvatar" column="user_avatar" jdbcType="VARCHAR"/><result property="createTime" column="create_time" jdbcType="TIMESTAMP"/><result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/><result property="isDelete" column="is_delete" jdbcType="TINYINT"/></resultMap><sql id="Base_Column_List">user_id,user_account,user_password,user_email,user_avatar,create_time,update_time,is_delete</sql>
</mapper>
测试:controller目录新建UserController,编写测试接口
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate UserInfoService userInfoService;@PostMapping("/add")public String add(@RequestBody UserInfo userInfo) {userInfoService.save(userInfo);return "success";}@PostMapping("/get")public UserInfo get(long id) {return userInfoService.getById(id);}@PostMapping("/update")public String update(@RequestBody UserInfo userInfo) {userInfoService.updateById(userInfo);return "success";}@PostMapping("/delete")public String delete(long id) {userInfoService.removeById(id);return "success";}
}
在启动类中增加@MapperScan
注解扫描mapper包
@SpringBootApplication
@MapperScan("com.cyfy.cyblogsbackend.mapper")
public class MainApplication {public static void main(String[] args) {SpringApplication.run(MainApplication.class, args);}
}
运行项目,在接口文档中进行测试,看是否可正常进行增删改查操作
注意:删除操作是逻辑删除,需要查看是否为逻辑删除而不是真删
- MyBatis Plus数据操作整合完成
小优化
回看framework模块中引入的依赖mysql-connector-j
,其scope
属性为runtime
,表示该依赖仅在运行时需要。作用是使项目能够在运行时连接和操作MySQL数据库。
这一类依赖,与Knife4j依赖相同,就是我们后续其他模块编写代码时,都不会需要该依赖的方法,没有模块真实需要它们,所以这些依赖可以直接丢到admin模块中,可减少其它模块引入无关依赖的频率
- admin模块依赖
<dependencies><dependency><!-- 业务模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-business</artifactId></dependency><!-- knife4j 接口文档 --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId></dependency><!--mysql--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency>
</dependencies>
- framework模块依赖
<dependencies><dependency><!-- 公共模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-common</artifactId></dependency><!--引入mybatisPlus 包含了 jdbc--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency>
</dependencies>
这里为了更好区分哪个类在哪个模块下,所以除admin模块外,其他模块在路径上都新增了一层模块名目录
自定义异常类
在common模块中添加exception目录,用于存放我们自定义的异常类
- ErrorCode:状态码枚举类
/*** 状态码枚举类*/
@Getter
public enum ErrorCode {SUCCESS(0,"ok"),PARAMS_ERROR(40000,"请求参数错误"),NOT_LOGIN_ERROR(40100,"未登录"),NO_AUTH_ERROR(40101,"无权限"),NOT_FOUND_ERROR(40400,"请求数据不存在"),FORBIDDEN_ERROR(40300,"禁止访问"),SYSTEM_ERROR(50000,"系统内部异常"),OPERATION_ERROR(50001,"操作失败");private final int code;private final String message;ErrorCode(int code, String message) {this.code = code;this.message = message;}
}
- BusinessException:自定义异常类
/*** 自定义异常类*/
@Data
public class BusinessException extends RuntimeException{// 错误码private final int code;public BusinessException(int code, String message){super(message);this.code = code;}public BusinessException(ErrorCode errorCode){super(errorCode.getMessage());this.code = errorCode.getCode();}public BusinessException(ErrorCode errorCode, String message){super(message);this.code = errorCode.getCode();}
}
- ThrowUtils:自定义异常抛出工具
/*** 自定义异常抛出工具*/
public class ThrowUtils {/*** 条件成立,则抛出异常* @param condition 条件* @param runtimeException 异常*/public static void throwIf(boolean condition, RuntimeException runtimeException){if (condition){throw runtimeException;}}/*** 条件成立,则抛出异常* @param condition 条件* @param errorCode 错误码*/public static void throwIf(boolean condition, ErrorCode errorCode){throwIf(condition,new BusinessException(errorCode));}/*** 条件成立,则抛出异常* @param condition 条件* @param errorCode 错误码* @param message 错误信息*/public static void throwIf(boolean condition, ErrorCode errorCode,String message){throwIf(condition,new BusinessException(errorCode,message));}
}
自定义响应类
在common模块中添加 result 目录,用于存放我们自定义的响应类,即自定义返回给前端的数据格式
- BaseResponse:通用响应类,即返回给前端的格式
/*** 通用响应类* @param <T>*/
@Data
public class BaseResponse<T> implements Serializable {private int code;private T data;private String message;/*** 返回状态码、数据、消息* @param code 状态码* @param data 数据* @param message 消息*/public BaseResponse(int code, T data, String message){this.code = code;this.data = data;this.message = message;}/*** 只返回数据和状态码* @param code 状态码* @param data 数据*/public BaseResponse(int code, T data){this(code,data,"");}/*** 只返回状态码和消息* @param errorCode 错误状态信息*/public BaseResponse(ErrorCode errorCode){this(errorCode.getCode(),null,errorCode.getMessage());}
}
- ResultUtils:响应工具类,可使用该类方法返回数据
/*** 响应工具类*/
public class ResultUtils {/**** 成功* @param data 数据* @param <T> 数据类型* @return 响应*/public static <T> BaseResponse<T> success(T data){return new BaseResponse<>(ErrorCode.SUCCESS.getCode(),data,ErrorCode.SUCCESS.getMessage());}/*** 失败* @param errorCode 错误码* @return 响应*/public static BaseResponse<?> error(ErrorCode errorCode){return new BaseResponse<>(errorCode);}/**** 失败* @param code 错误码* @param message 错误信息* @return 响应*/public static BaseResponse<?> error(int code, String message){return new BaseResponse<>(code,null,message);}/**** 失败* @param errorCode 错误码* @param message 错误信息* @return 响应*/public static BaseResponse<?> error(ErrorCode errorCode,String message){return new BaseResponse<>(errorCode.getCode(),null,message);}
}
全局异常处理器
exception目录下,新建GlobalExceptionHandler,用于处理捕获到的异常
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(BusinessException.class)public BaseResponse<?> businessExceptionHandler(BusinessException e) {log.error("BusinessException", e);return ResultUtils.error(e.getCode(), e.getMessage());}@ExceptionHandler(RuntimeException.class)public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {log.error("RuntimeException", e);return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");}
}
通用请求类
在common模块中添加 request 目录,用于存放通用的请求体或请求包装类
- PageRequest:分页请求包装类
/*** 分页请求包装类*/
@Data
public class PageRequest {/*** 当前页码*/private int current = 1;/*** 每页显示条数*/private int pageSize = 10;/*** 排序字段*/private String sortField;/*** 排序方式,默认降序*/private String sortOrder = "descend";
}
- DeleteRequest:删除请求包装类
/*** 删除请求包装类*/
@Data
public class DeleteRequest implements Serializable {private Long id;private static final long serialVersionUID = 1L;
}
公共配置类
在common模块中添加 config 目录,用于存放公共配置类
CorsConfig:全局跨域配置
/*** 全局跨域配置*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 覆盖所有请求registry.addMapping("/**").allowCredentials(true) // 允许发送cookie.allowedOriginPatterns("*") // 放行哪些域名(必须用patterns,否则*会和allowCredentials冲突).allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").maxAge(3600).allowedHeaders("*").exposedHeaders("*");}
}
- MybatisPlusConfig:MybatisPlus配置
@Configuration
@MapperScan("com.cyfy.cyblogsbackend.business.mapper")
public class MybatisPlusConfig {/*** 添加分页插件*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加return interceptor;}
}
需要将mybatis-plus依赖移到common模块中,因为该模块使用到了该依赖
- common 模块依赖
<dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency>
</dependencies>
- framework 模块依赖
<dependencies><dependency><!-- 公共模块 --><groupId>com.cyfy</groupId><artifactId>cy-blogs-common</artifactId></dependency>
</dependencies>
当前 common 模块目录结构
整合 jBCrypt 对用户加密
在common模块下引入依赖
<!-- jBCrypt 加密 -->
<dependency><groupId>org.mindrot</groupId><artifactId>jbcrypt</artifactId><version>0.4</version>
</dependency>
在common模块下新建tools目录,用于存放通用工具类
- EncipherUtils
public class EncipherUtils {// 盐值,用于加强密码private static final String SALT_PRE = "cyfy";private static final String SALT_SUFF = "SUff";// 密码最小长度,最大长度private static final int PWD_MIN_LENGTH = 6;private static final int PWD_MAX_LENGTH = 25;public static String hashPsd(String psd){if (psd == null || psd.length() < PWD_MIN_LENGTH || psd.length() > PWD_MAX_LENGTH) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}// 增加密码长度,提高加密强度String password = SALT_PRE + psd + SALT_SUFF;return BCrypt.hashpw(password,BCrypt.gensalt());}public static boolean checkPsd(String psd,String hashPsd){// 增加密码长度,提高加密强度String password = SALT_PRE + psd + SALT_SUFF;return BCrypt.checkpw(password,hashPsd);}
}
修改UserContorller,测试加密
@PostMapping("/add")public BaseResponse<String> add(@RequestBody UserInfo userInfo) {// 获取用户密码String password = userInfo.getUserPassword();// 对密码进行加密userInfo.setUserPassword(EncipherUtils.hashPsd(password));userInfoService.save(userInfo);return ResultUtils.success();}// 登录@PostMapping("/login")public BaseResponse<String> login(@RequestBody UserInfo userInfo) {// 获取用户密码String password = userInfo.getUserPassword();// 查询用户是否存在UserInfo user = userInfoService.getById(userInfo.getUserId());if (user == null){throw new BusinessException(ErrorCode.ACCOUNT_NOT_EXIST);}// 对比密码是否一致if (!EncipherUtils.checkPsd(password, user.getUserPassword())) {throw new BusinessException(ErrorCode.PASSWORD_ERROR);}return ResultUtils.success();}
- 新增状态码
ACCOUNT_NOT_EXIST(40011, "账号不存在"),
PASSWORD_ERROR(40012, "密码错误"),
运行结果,添加新用户后,新用户密码加密
- jBCrypt 对用户加密整合完成
小工具添加
common模块添加依赖
<!-- hutool 工具,提供便捷的常用工具类 -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version>
</dependency>
<!-- fastjson JSON字符串处理工具 -->
<dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.7</version>
</dependency>
修改前面代码中的if判断部分,改为使用hutool工具类判断(也可不改,看个人习惯)
// if(user == null )
if (ObjUtil.isEmpty(user)) {throw new BusinessException(ErrorCode.ACCOUNT_NOT_EXIST);
}
整合 JWT
framework 模块导入依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
framework模块下新建 jwt 目录,用于存放jwt相关代码
新建 JwtUtil 工具类,用于创建和解密令牌
@Component
public class JwtUtil {// 令牌秘钥private static final String JWT_KEY = "cyfy";// 令牌有效期private static final long JWT_EXPIRE = 30 * 60 * 1000;/*** 根据传入数据生成令牌* @param data 令牌数据* @return*/public String createToken(Object data){// 获取当前时间long currentTime = System.currentTimeMillis();// 过期时间long expireTime = currentTime + JWT_EXPIRE;// 构造令牌JwtBuilder builder = Jwts.builder().setId(UUID.randomUUID() + "").setSubject(JSON.toJSONString(data)).setIssuer("blogs").setIssuedAt(new Date(currentTime)).signWith(SignatureAlgorithm.HS256,encodeSecret(JWT_KEY)).setExpiration(new Date(expireTime));return builder.compact();}/*** 加密密钥* @param key 令牌秘钥字符串* @return*/private SecretKey encodeSecret(String key){// 将输入字符串转换为字节数组并进行Base64编码。byte[] encode = Base64.getEncoder().encode(key.getBytes());// 创建一个SecretKeySpec对象,使用AES算法对输入的字节数组进行加密。SecretKeySpec aes = new SecretKeySpec(encode, 0, encode.length, "AES");return aes;}/*** 解密,获取token数据* @param token 令牌* @return*/public Claims parseToken(String token){Claims body = Jwts.parser().setSigningKey(encodeSecret(JWT_KEY)).parseClaimsJws(token).getBody();return body;}/*** 解密,并转换为对应实体类* @param token 令牌* @param clazz 实体类* @param <T>* @return*/public <T> T parseToken(String token, Class<T> clazz){Claims body = parseToken(token);return JSON.parseObject(body.getSubject(), clazz);}
}
测试:放在admin模块中测试
@SpringBootTest
class JwtUtilTest {@Autowiredprivate JwtUtil jwtUtil;@Testvoid createToken() {UserInfo userInfo = new UserInfo();userInfo.setUserId(1L);userInfo.setUserAccount("admin");userInfo.setUserEmail("154545484");userInfo.setUserAvatar("头像");userInfo.setCreateTime(new Date());userInfo.setUpdateTime(new Date());// 加密,生成令牌String token = jwtUtil.createToken(userInfo);System.out.println(token);}@Testvoid parseToken() {String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxZGU3MzJlMy01NTYwLTRmY2UtYjdiMS1mN2YwMjQ4YmRlNDIiLCJzdWIiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDI1LTAyLTIzVDAxOjMyOjA1LjE1NCswODowMFwiLFwidXBkYXRlVGltZVwiOlwiMjAyNS0wMi0yM1QwMTozMjowNS4xNTQrMDg6MDBcIixcInVzZXJBY2NvdW50XCI6XCJhZG1pblwiLFwidXNlckF2YXRhclwiOlwi5aS05YOPXCIsXCJ1c2VyRW1haWxcIjpcIjE1NDU0NTQ4NFwiLFwidXNlcklkXCI6MX0iLCJpc3MiOiJibG9ncyIsImlhdCI6MTc0MDI0NTUyNSwiZXhwIjoxNzQwMjQ3MzI1fQ.vzBCA1eAvpHH8GK9vv-ZLPv8KavTgGk4dbymwDB38ps";UserInfo userInfo = jwtUtil.parseToken(token, UserInfo.class);System.out.println(userInfo.getUserId());System.out.println(userInfo.getUserAccount());System.out.println(userInfo.getUserAvatar());System.out.println(token.length());}
}
多模块打包测试
这里直接打包,会在各个子模块的 target 目录中生成其对应的jar包,且不能直接运行,需要修改父模块的pom文件和主入口模块文件
- 父模块 pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.cyfy</groupId><artifactId>cy-blogs-backend</artifactId><!-- 父模块必须为pom --><packaging>pom</packaging><version>1.0.1-SNAPSHOT</version>...... <dependencyManagement>......</dependencyManagement><build><plugins><!--多模块Maven项目统一修改版本号--><plugin><groupId>org.codehaus.mojo</groupId><artifactId>versions-maven-plugin</artifactId><version>2.18.0</version><configuration><generateBackupPoms>false</generateBackupPoms></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><!-- 移动了spring-boot-maven-plugin插件 --></plugins></build>
</project>
- admin模块:有启动类的模块
<?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>cy-blogs-backend</artifactId><groupId>com.cyfy</groupId><version>1.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cy-blogs-admin</artifactId><!-- admin模块显式声明为 jar --><packaging>jar</packaging><description>web服务入口</description>......<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><!-- 这里必须显式声明版本,不要乱设,与自己springboot版本一致即可 --><version>2.7.6</version><configuration><!-- 你的主类全路径 --><mainClass>com.cyfy.cyblogsbackend.MainApplication</mainClass><includeSystemScope>true</includeSystemScope></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins><finalName>${project.artifactId}</finalName></build>
</project>
打包步骤:先使用 clean 再使用 package 打包,或者控制台在当前项目根目录上使用 mvn clean package
命令
打包后,到 admin 模块下的target文件夹下,打开cmd控制台,执行 java -jar cy-blogs-admin.jar
命令
能正常跑起来,不会中断,且可正常访问接口文档,并且测试接口无问题
- 测试与其它 jar 包是否存在依赖性
因为打包后,每个模块都有生成各自的jar包,担心如果我们以后部署项目时,会不会不能只上传一个admin模块的 jar 包。
所以这里做两个测试
1.删除其他模块的jar包,并把 admin 模块的jar包移动到桌面,看是否还能正常运行(移动 admin 模块的 jar 包后,执行clean就能把其他模块的jar包干掉)
2.将包丢到远程服务器中,查看是否能正常运行
前端基础框架搭建
*环境准备
- node:20.12.2
- npm:10.5.0
想使用其它版本,请先确认自己是否有排错能力,版本不同可能会出现奇奇怪怪的问题
创建项目
cmd控制台使用 npm create vue@3.12.1
命令,根据提示创建vue项目(使用vue@3.12.1指定脚手架版本创建而不是最新版本)
执行 npm install
初始化项目,再执行 npm run dev
运行项目,然后查看是否能正常访问网站
引入Ant design vue组件库
官网:https://antdv.com/docs/vue/getting-started-cn
安装
npm i --save ant-design-vue@4.x
修改 main.ts,全局注册组件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'const app = createApp(App)app.use(createPinia())
app.use(router)
app.use(Antd)app.mount('#app')
基础布局页面
新建layouts目录,用于存放网页布局页面
新建BasicLayout.vue,编写网站主要布局界面
<template><div id="basicLayout"><a-layout style="min-height: 100vh"><a-layout-header class="header"><!-- 导航栏组件 --><GlobalHeader /></a-layout-header><a-layout-content class="content"><!-- 页面主内容,通过 router-view 渲染 --><router-view /></a-layout-content><a-layout-footer class="footer"><!-- 页脚组件 -->我的博客 @残炎锋羽</a-layout-footer></a-layout></div>
</template><script setup lang="ts">
import GlobalHeader from '@/components/GlobalHeader.vue';</script><style scoped>
#basicLayout {
}
#basicLayout .header{padding-inline: 20px;margin-bottom: 16px;color: unset;background: white;
}
#basicLayout footer{background: #efefef;padding: 16px;position: fixed;bottom: 0;left: 0;right: 0;text-align: center;
}
</style>
在components目录下新建GlobalHeader.vue,编写导航栏组件
<template><div class="globalHeager"><a-row :wrap="false"><a-col flex="200px"><RouterLink to="/"><div class="title-bar"><img class="logo" src="../assets/logo.jpeg" alt="logo" /><div class="title">我的博客</div></div></RouterLink></a-col><a-col flex="auto"><a-menuv-model:selectedKeys="current"mode="horizontal":items="items"@click="doMenuClick"/></a-col><a-col flex="120px"><div class="user-login-status"><a-button type="primary" href="/user/login">登录</a-button></div></a-col></a-row></div>
</template><script lang="ts" setup>
import { h, ref } from 'vue';
import { HomeOutlined } from '@ant-design/icons-vue';
import type { MenuProps } from 'ant-design-vue';
import { useRouter } from 'vue-router';const router = useRouter();
// 菜单点击事件,跳转指定路由
const doMenuClick = ({key} : { key: string }) =>{router.push({path: key});
}// 当前选中菜单
const current = ref<string[]>(['/']);
// 监听路由变化,更新当前选中菜单
router.afterEach((to) => {current.value = [to.path];
});// 菜单项
const items = ref<MenuProps['items']>([{key: '/',icon: () => h(HomeOutlined),label: '主页',title: '主页',},{key: '/about',label: '关于',title: '关于',},
]);
</script><style scoped>
.title-bar {display: flex;align-items: center;
}.title {color: black;font-size: 18px;margin-left: 16px;
}.logo {height: 48px;
}
</style>
App.vue中引入 BasicLayout 布局组件
<template><div id="app"><BasicLayout /></div>
</template><script setup lang="ts">
import BasicLayout from '@/layouts/BasicLayout.vue'
</script><style scoped>
</style>
运行效果
整合 Axios 实现请求发送
官网:https://axios-http.com/docs/intro
安装Axios
npm install axios
src目录下新建request.ts文件,编写请求配置文件
import axios from 'axios'
import { message } from 'ant-design-vue'// 创建 Axios 实例
const myAxios = axios.create({baseURL: 'http://localhost:8081', // 后端请求URLtimeout: 5 * 60 * 1000, // 超时时间withCredentials: true, // 发送请求时携带cookie信息
})// 全局配置拦截器
myAxios.interceptors.request.use((config) => {// 在发送请求之前做些什么return config},(error) => {// 对请求错误做些什么return Promise.reject(error)},
)// 全局响应拦截器
myAxios.interceptors.response.use((response) => {// 对响应数据做点什么const { data } = response// 40100: 未登录if (data.code === 40100) {// 不是获取用户信息的请求,并且用户目前不是已经在用户登录页面,则跳转到登录界面if (!response.request.responseURL.includes('user/get/login') &&!window.location.pathname.includes('/user/login')) {message.warning('请先登录')// 重定向到登录页面window.location.href = `/user/login?redirect=${window.location.href}`}}return response},(error) => {// 对响应错误做点什么if (error.response) {// 请求已发出,但服务器响应的状态码不在 2xx 范围内message.error(`${error.response.status} ${error.response.data}`)}},
)export default myAxios
安装 OpenAPI 工具,实现代码自动生成
npm i --save-dev @umijs/openapi
项目根目录新建openapi.config.js
文件,根据配置定制生成的代码
import { generateService } from '@umijs/openapi'generateService({requestLibPath: "import request from '@/request'",schemaPath: 'http://localhost:8081/v2/api-docs',serversPath: './src',
})
- schemaPath为接口文档的 host + 分组URL
到package.json文件中,增加执行脚本命令
"api": "node openapi.config.js"
需要更新后端接口请求请求文件时,执行 npm run api
命令即可根据后端提供的接口生成对应的请求文件
测试:
以访问 /test/test
为例,找到对应的 testController.ts
文件,找到要使用的方法,在随便一个能访问的页面中使用它
运行项目,打开控制台可见其请求响应
小优化:通过代理转发请求
当前项目发送请求,是直接请求后端接口,直接在控制台网络中暴露了后端url
修改vite.config.ts文件
export default defineConfig({server: {// 代理转发proxy: {'/api': {target: 'http://localhost:8081',},},},plugins: [vue(), vueDevTools()],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url)),},},
})
修改request.ts文件,移除或注释后端请求URL
// 创建 Axios 实例
const myAxios = axios.create({baseURL: '', // 后端请求URLtimeout: 5 * 60 * 1000, // 超时时间withCredentials: true, // 发送请求时携带cookie信息
})
修改后,我们看到的请求地址是前端的而不是后端的,隐藏了后端地址
整合Pinia 实现全局状态管理
创建项目时已引入pinia,直接使用即可
在stores目录下新建useLoginUserStore.ts文件,用于保存登录用户信息状态
import { defineStore } from 'pinia'
import { ref } from 'vue'export const useLoginUserStore = defineStore('loginUser', () => {// 登录用户的初始值const loginUser = ref<any>({userName: '未登录',})async function fetchLoginUser() {// 模拟用户登录,后续对接后端接口实现setTimeout(() => {loginUser.value = {userName: 'admin',id: 1,}}, 3000)}// 设置登录用户function setLoginUser(newLoginUser: any) {loginUser.value = newLoginUser}return { loginUser, setLoginUser, fetchLoginUser }
})
修改App.vue文件,调用fetchLoginUser获取用户信息
<script setup lang="ts">
import BasicLayout from '@/layouts/BasicLayout.vue'
import { useLoginUserStore } from '@/stores/useLoginUserStore'const loginUserStore = useLoginUserStore()
loginUserStore.fetchLoginUser()
</script>
修改导航栏组件GlobalHeader.vue,修改登录部分
<template><div class="globalHeager">.......<a-col flex="120px"><div class="user-login-status"><div v-if="loginUserStore.loginUser.id">{{ loginUserStore.loginUser.userName ?? '无名' }}</div><div v-else><a-button type="primary" href="/user/login">登录</a-button></div></div></a-col>......</div>
</template>
<script lang="ts" setup>
......
import { useLoginUserStore } from '@/stores/useLoginUserStore'const loginUserStore = useLoginUserStore()
......
</script>
效果:3s后,右上方导航栏用户信息模块由登录按钮变为用户名
小优化
- 设置网站标题和图标
找到根目录的index.html文件,该文件可配置我们网站标题和网站icon图标等信息
- 分离路由配置文件
在 router 目录下新建 routes.ts 文件,将index.ts中的路由配置移到该文件中
import HomeView from '../views/HomeView.vue'
const routes = [{path: '/',name: 'home',component: HomeView,},{path: '/about',name: 'about',component: () => import('../views/AboutView.vue'),},
]export default routes
index.ts文件导入routes,后续增加修改路由直接在routes.ts文件中修改即可
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes
})
export default router
- views目录结构优化
分别新建home、about、blogs目录,用于存放主页、关于页、博客页的视图文件,并把原些的视图文件改为index.vue(可不改,保持之前的所有视图文件都放在views目录下)
改动后,同步修改routes.ts文件
import HomeView from '../views/home/index.vue'
const routes = [{path: '/',name: 'home',component: HomeView,},{path: '/about',name: 'about',component: () => import('@/views/about/index.vue'),},{path: '/404',name: '404',component: () => import('../views/404.vue'),},
]export default routes
这样的好处在于,后续编写代码时,不用将单个页面的所有内容全部推在一个vue文件中编写,而是拆分成多个组件文件,然后在 index 引入,如果是某个组件部分除了问题,去找对应文件即可。
页面组件化,会导致需要频繁用到父子组件的数据交互操作,这也加大了开发难度,需要对父子组件的数据交互有较高理解,所以这个纯看个人喜好,我不喜欢所有代码都放在一个文件,搞得一个文件上千行代码
动态路由
暂不考虑,因为可能会用若依再做个后台项目进行博客管理;但后续会考虑整合