1.前后端分离为了什么
1.1前言
本文作为系列文章的第一篇,是铁柱在工作之余对自我学习的总结,以下内容是搭建基础的前后端分离的Demo来展开的,需要对MySQL,Spring,SpringMVC,Mybatis,Vue,Node.js有一定的前置掌握,也可以先行进行简单的学习,下面也会贴出以上几种技术推荐的学习教程供小伙伴们参考,后续还会有更多干货文章,JUC,多线程,集合,Netty,Redis,中间件技术等,全部结合实际展开,感兴趣的小伙伴可以关注点赞走起。
本文全部代码已上传到Gitee仓库: https://gitee.com/lu-tiezhu/separationVue-boot.git
喜欢的小伙伴可以一键start,感谢支持。
1.2前后端分离的前因后果
前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。
核心思想是前端html页面通过http请求调用后端的restuful 风格接口并使用json数据进行交互。以前的JavaWeb项目大多数都是java程序员又当爹又当妈,又搞前端,又搞后端。
随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只管前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会,那么他毕竟什么都不精。
对于后端java工程师:
把精力放在java基础,设计模式,jvm原理,spring+springmvc原理及源码,linux,mysql事务隔离与锁机制,mongodb,http/tcp,多线程,分布式架构,弹性计算架构,微服务架构,java性能优化,以及相关的项目管理等等。
后端追求的是:三高(高并发,高可用,高性能),安全,存储,业务等等。
对于前端工程师:
把精力放在html5,css3,jquery,angularjs,bootstrap,reactjs,vuejs,webpack,less/sass,gulp,nodejs,Google V8引擎,javascript多线程,模块化,面向切面编程,设计模式,浏览器兼容性,性能优化等等。
前端追求的是:页面表现,速度流畅,兼容性,用户体验等等。
1.3 使用SpringBoot框架开发前后端分离的原因
来看看百度百科关于SpringBoot的介绍:
SpringBoot框架中还有两个非常重要的策略:开箱即用和约定优于配置。开箱即用,Outofbox,是指在开发过程中,通过在MAVEN项目的pom文件中添加相关依赖包,然后使用对应注解来代替繁琐的XML配置文件以管理对象的生命周期。这个特点使得开发人员摆脱了复杂的配置工作以及依赖的管理工作,更加专注于业务逻辑。约定优于配置,Convention over configuration,是一种由SpringBoot本身来配置目标结构,由开发者在结构中添加信息的软件设计范式。这一特点虽降低了部分灵活性,增加了BUG定位的复杂性,但减少了开发人员需要做出决定的数量,同时减少了大量的XML配置,并且可以将代码编译、测试和打包等工作自动化。
SpringBoot应用系统开发模板的基本架构设计从前端到后台进行说明:前端常使用模板引擎,主要有FreeMarker和Thymeleaf,它们都是用Java语言编写的,渲染模板并输出相应文本,使得界面的设计与应用的逻辑分离,同时前端开发还会使用到Bootstrap、AngularJS、JQuery等;在浏览器的数据传输格式上采用Json,非xml,同时提供RESTfulAPI;SpringMVC框架用于数据到达服务器后处理请求;到数据访问层主要有Hibernate、MyBatis、JPA等持久层框架;数据库常用MySQL;开发工具推荐IntelliJIDEA。
用一句话来说:SpringBoot可以快速搭建后端来实现一个前后端分离的项目,避免大量繁琐的配置,且由于天然良好的支持SpringMVC,可以非常敏捷的开发接口
2.搭建后端基础环境
2.1 技术选型
Java企业级开发常见技术栈:
开发工具:IDEA 2020,VS Code
Java version “1.8.0_301”
Java™ SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot™ 64-Bit Server VM (build 25.301-b09, mixed mode)
后端框架:
- Spring 5.x
- SpringMVC 5.x
- MySQL 5.7
- SpringBoot 2.2.11.RELEASE
- Redis 5.5
- MybatisPlus 3.4.0
- spring-boot-devtools
- hutool
- swagger-ui
- Git
- …
前端:
-
Node.js
-
Vue2
-
ElementUI
-
Axios
-
…
后续随着文章的进度,将会整合更多技术,争取一个项目吃透Java开发常用技术
2.2搭建数据表
先创建数据库 separation_boot,再创建数据表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for db_scores
-- ----------------------------
DROP TABLE IF EXISTS `db_scores`;
CREATE TABLE `db_scores` (
`id` bigint(20) NOT NULL COMMENT '主键id',
`subjects` varchar(50) COMMENT '考试科目',
`stu_number` varchar(100) COMMENT '学号',
`score` int(11) NULL DEFAULT NULL COMMENT '分数',
`describe` varchar(255) COMMENT '考试说明',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB;
-- ----------------------------
-- Records of db_scores
-- ----------------------------
INSERT INTO `db_scores` VALUES (3458451354, '数学', '413026200510101213', 135, '高三年级十月月中考试', '2022-10-10 21:38:37');
INSERT INTO `db_scores` VALUES (3458451351236, '数学', '413026200510101216', 120, '高三年级十月月中考试', '2022-10-10 21:38:37');
INSERT INTO `db_scores` VALUES (3458451354112, '数学', '413026200510101214', 95, '高三年级十月月中考试', '2022-10-10 21:38:37');
INSERT INTO `db_scores` VALUES (3458451389471, '数学', '413026200510101212', 115, '高三年级十月月中考试', '2022-10-10 21:38:37');
-- ----------------------------
-- Table structure for db_students
-- ----------------------------
DROP TABLE IF EXISTS `db_students`;
CREATE TABLE `db_students` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`stu_name` varchar(50) COMMENT '姓名',
`stu_
number` varchar(100) COMMENT '学号',
`sex` varchar(10) COMMENT '性别',
`birth` datetime(0) NULL DEFAULT NULL COMMENT '出生日期',
`address` varchar(255) COMMENT '地址',
`stu_class` varchar(50) COMMENT '班级',
`stu_grade` varchar(50) COMMENT '年级',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB;
-- ----------------------------
-- Records of db_students
-- ----------------------------
INSERT INTO `db_students` VALUES (357453451, '刘玉兰', '413026200510101213', '女', '2005-10-10 21:33:57', '河南省郑州市高新区', '02班', '高中三年级', '2022-10-10 21:35:15');
INSERT INTO `db_students` VALUES (357453454, '李辉', '413026200510101214', '男', '2005-10-10 21:33:57', '河南省郑州市高新区', '01班', '高中三年级', '2022-10-10 21:35:15');
INSERT INTO `db_students` VALUES (357453455, '张梦怡', '413026200510101212', '女', '2005-10-10 21:33:57', '河南省郑州市高新区', '01班', '高中三年级', '2022-10-10 21:35:15');
INSERT INTO `db_students` VALUES (357453457, '王明明', '413026200510101216', '男', '2005-10-10 21:33:57', '河南省郑州市高新区', '04班', '高中三年级', '2022-10-10 21:35:15');
SET FOREIGN_KEY_CHECKS = 1;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for db_dictionary
-- ----------------------------
DROP TABLE IF EXISTS `db_dictionary`;
CREATE TABLE `db_dictionary` (
`id` int(11) NOT NULL COMMENT '主键',
`code_name` varchar(100) COMMENT '字典名称',
`fid` int(11) NULL DEFAULT NULL COMMENT '父id',
`describe` varchar(255) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB ;
-- ----------------------------
-- Records of db_dictionary
-- ----------------------------
INSERT INTO `db_dictionary` VALUES (101, '年级', 1, NULL);
INSERT INTO `db_dictionary` VALUES (102, '班级', 1, NULL);
INSERT INTO `db_dictionary` VALUES (103, '学科', 1, NULL);
INSERT INTO `db_dictionary` VALUES (10101, '高中一年级', 101, NULL);
INSERT INTO `db_dictionary` VALUES (10102, '高中二年级', 101, NULL);
INSERT INTO `db_dictionary` VALUES (10103, '高中三年级', 101, NULL);
INSERT INTO `db_dictionary` VALUES (10104, '初中一年级', 101, NULL);
INSERT INTO `db_dictionary` VALUES (10105, '初中二年级', 101, NULL);
INSERT INTO `db_dictionary` VALUES (10106, '初中三年级', 101, NULL);
INSERT INTO `db_dictionary` VALUES (10201, '01班', 102, NULL);
INSERT INTO `db_dictionary` VALUES (10202, '02班', 102, NULL);
INSERT INTO `db_dictionary` VALUES (10203, '03班', 102, NULL);
INSERT INTO `db_dictionary` VALUES (10204, '04班', 102, NULL);
INSERT INTO `db_dictionary` VALUES (10205, '05班', 102, NULL);
INSERT INTO `db_dictionary` VALUES (10301, '语文', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10302, '数学', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10303, '英语', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10304, '政治', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10305, '历史', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10306, '地理', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10307, '物理', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10308, '化学', 103, NULL);
INSERT INTO `db_dictionary` VALUES (10309, '生物', 103, NULL);
INSERT INTO `db_dictionary` VALUES (103010, '体育', 103, NULL);
SET FOREIGN_KEY_CHECKS = 1;
2.3 添加maven依赖和配置文件
IDEA创建项目
废话不多说,直接上POM文件内容
<groupId>com.tiezhu</groupId>
<artifactId>separationVue-boot</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.11.RELEASE</version>
<relativePath/>
</parent>
<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.2.11.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.4</version>
</dependency>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
<!--swagger接口注解插件-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<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>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes> <include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
下面是application.yml文件的内容
# 应用服务 WEB 访问端口
server:
port: 9090
# 应用名称
spring:
application:
name: separationVue-boot
devtools:
restart:
enabled: false
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/separation_boot?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.tiezhu.separation.pojo
mapper-locations: classpath:com/tiezhu/separation/mapper/xml/*.xml
2.4 构建基础目录
目录如下图:
2.5 整合MybatisPlus和代码生成器
对MybatisPlus不熟悉的小伙伴可以阅读我的这篇入门教程:
Mybatis-Plus入门(一)_道上叫我卢铁柱的博客-CSDN博客
MybatisPlus的相关配置参数已经放在yml文件当中:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 配置打印sql执行日志
type-aliases-package: com.tiezhu.separation.pojo 实体类路径
mapper-locations: classpath:com/tiezhu/separation/mapper/xml/*.xml 各个类的Mybatis实现文件
代码生成器,名如其字(也可以说顾名思义),这玩意是根据数据库的表生成相关增删改查代码的,这样避免了繁琐的重复代码编写,前面提到我们使用的是MybatisPlus框架,它是Mybatis的超集,就是说我们可以同时使用Mybatis,在代码生成器的加持上,我们可以减少编写基础的Mybatis代码,相关依赖已经贴在maven中,废话不多说,直接上代码
package com.tiezhu.separation;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;
/**
* @author tiezhu
* @since 2021/8/7
*/
public class CodeGenerator {
@Test
public void run() {
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("tiezhu");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ID_WORKER); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/separation_boot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("separation"); //模块名
pc.setParent("com.tiezhu");
pc.setController("controller");
pc.setEntity("pojo");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("db_students");//此处为表名,将需要生成的数据表放在这里即可
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setTablePrefix("db_"); //生成实体时去掉表前缀
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}
2.6 整合Redis
Redis作为一个快速的缓存数据库,使用键值对结构可以完成很多操作,如分布式锁,秒杀,页面缓存,消息传输等功能,Redis也是将在本项目发挥很多种可能。
在配置文件中添加
redis:
host: 127.0.0.1
port: 6379
lettuce:
shutdown-timeout: 2000
timeout: 2000
jedis:
pool:
max-wait: -1
在config文件夹下面添加redis的优化配置类:
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
为了一步到位我们直接整上Redisson的配置类,在config文件夹下面添加:
package com.tiezhu.separation.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfiguration {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
// 可以用"rediss://"来启用SSL连接
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
在项目和日常使用中我们可以通过两种方法来注入使用redis
@Autowired
private RedissonClient redissonClient;
//需要配置类RedissonConfiguration
@Autowired
private RedissonClient redissonClient;
2.7配置SwaggerUI
先来讲讲Swagger是什么:
使用Swagger根据在代码中使用自定义的注解来生成接口文档,这个在前后端分离的项目中很重要。这样做的好处是 在开发接口时可以通过swagger 将接口文档定义好,同时也方便以后的维护。
- 号称时最流行的 API 框架
- 接口文档在线生成,避免同步的麻烦
- 可以支持在线对接口执行测试
- 支持多语言
同样的在config文件里添加Swagger配置类:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket adminApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("indexApi")
.apiInfo(webApiInfo())
.select()
.paths(Predicates.and(PathSelectors.regex("/index/.*")))
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("separation-SpringBoot前后端分离学习项目API文档")
.description("本文描述了separationVue-boot接口的定义")
.version("1.0")
.contact(new Contact("tiezhu", "http://127.0.0.1", "1766419110@qq.com"))
.build();
}
}
最终启动起来是这样的效果(后面的代码还没有写,所以现在还是启动不了的,后续跟上就ok):
3.实现业务代码
基本的东西料理妥当大概这样:
3.1 实体类和工具类
实体类由代码生成器生成之后可不用修改,在createTime上加入如下代码:
@TableField(fill = FieldFill.INSERT_UPDATE)
Result类是实现前后端分离的核心关键代码,后端所查询的数据均通过它渲染成JSON传输到前端。
@Data
public class Result {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "状态码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String, Object> data=new HashMap<>();
private final static int SUCCESS=200;
private final static int ERROR=404;
private Result(){}
public static Result ok(){
Result r= new Result();
r.setSuccess(true);
r.setCode(SUCCESS);
r.setMessage("success");
return r;
}
public static Result error(){
Result r = new Result();
r.setSuccess(false);
r.setCode(ERROR);
r.setMessage("error");
return r;
}
public Result success(Boolean success){
this.setSuccess(success);
return this;
}
public Result message(String message){
this.setMessage(message);
return this;
}
public Result code(Integer code){
this.setCode(code);
return this;
}
public Result data(String key, Object value){
this.data.put(key, value);
return this;
}
public Result data(Map<String, Object> map){
this.setData(map);
return this;
}
}
3.2 业务层和数据层
业务层和数据层使用代码生成器生成的即可,可以直接在Controller层调用简单的增删改查操作,阅读本文的小伙伴们也可以尝试使用Mybatis去实现一些自己的想实现的功能。
3.3 SpringMVC和前后端分离的关系
学过SpringMVC的小伙伴都知道,它是一个实现MVC模式的Web开发框架,通过DispatchServlet实现对用户请求的解析,调用业务层实现数据的增删改查,将返回的数据渲染的前端,在传统的前后端不分离的项目中,Controller层一般使用的注解为 @Controller ,前端一般使用模板引擎来渲染数据。
当项目来到前后端分离的模式后,我们就需要在controller层加入更多的注解,如 @CrossOrigin @RestController @RequestMapping,其中RestController 注解尤其重要,配合restful风格的接口,将数据转化为JSON或者其他格式返回给前端,这些结果的如何渲染到页面上去就是前端的事情了,SpringMVC完美的支持了Http的几种请求方式,POST,GET,DELETE,PUT等,这些配合JQuery或者Axios等前端请求技术,构建起一个完美的前后端开发技术栈。
3.4 基础CRUD接口开发
对于我们已经建的三个数据库且已经有了基础的代码,剩下的就是开发一些简单的接口,足以本篇文章使用即可,有创造力的小伙伴也可以着急实现更多复杂的功能。
/**
* <p>
* 前端控制器
* </p>
*
* @author tiezhu
* @since 2022-10-10
*/
@Slf4j
@Api(description = "成绩数据操作接口")
@CrossOrigin
@RestController
@RequestMapping("/index/score")
public class ScoresController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ScoresService scoresService;
@Autowired
private DictionaryService dictionaryService;
@ApiOperation("查询成绩信息列表")
@GetMapping("/list")
public Result selectStudentList(){
List<Scores> list = scoresService.list();
return Result.ok().data("list",list);
}
@ApiOperation("根据学号查询学生成绩")
@GetMapping("/get/{key}")
public Result selectByIdOrName(@ApiParam(name = "key", value = "查询关键字", required = true) @PathVariable String key){
QueryWrapper<Scores> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("stu_number",key);
List<Scores> list = scoresService.list(queryWrapper);
if (list==null||list.size()==0){
return Result.error().message("您输入的数据有误,请重新输入");
}else {
return Result.ok().data("scores",list);
}
}
@ApiOperation("添加学生成绩信息")
@PostMapping("/insert")
public Result saveStudentInfo(@ApiParam(value = "插入数据") Scores score ){
if (score!=null){
boolean save = this.scoresService.save(score);
if (save){
return Result.ok();
}else {
return Result.error();
}
}else {
return Result.error().message("插入失败");
}
}
@ApiOperation("修改学生成绩信息")
@PostMapping("/update")
public Result editStudentInfo(@ApiParam(value = "插入数据") Scores sc ){
if (sc!=null){
UpdateWrapper<Scores> updateWrapper=new UpdateWrapper<>();
updateWrapper.eq("id",sc.getId());
boolean flag = this.scoresService.update(sc,updateWrapper);
if (flag){
return Result.ok();
}else {
return Result.error();
}
}else {
return Result.error().message("修改失败");
}
}
@ApiOperation("删除学生成绩信息")
@DeleteMapping("/delete/{number}")
public Result delScores(@PathVariable Long number ) {
if (this.scoresService.removeById(number)){
return Result.ok();
}else {
return Result.error();
}
}
@GetMapping("/dic/get/{mode}")
public Result selectDictionaryList(
@ApiParam(name = "mode", value = "查询关键字", required = true)
@PathVariable Integer mode
){
//传入0或者null就是查询字典里所有内容,101 年级 102班级 103 学科
QueryWrapper<Dictionary> queryWrapper=new QueryWrapper<>();
if (mode==null||mode==0){
queryWrapper.eq("fid",101);
List<Dictionary> list0 = this.dictionaryService.list(queryWrapper);
queryWrapper.eq("fid",102);
List<Dictionary> list1 = this.dictionaryService.list(queryWrapper);
queryWrapper.eq("fid",103);
List<Dictionary> list2 = this.dictionaryService.list(queryWrapper);
Map<String, Object> maps=new HashMap<>();
maps.put("grade",list0);
maps.put("class",list1);
maps.put("subject",list2);
return Result.ok().data(maps);
}else{
queryWrapper.eq("fid",mode);
List<Dictionary> list0 = this.dictionaryService.list(queryWrapper);
if (mode==101){
return Result.ok().data("grade",list0);
}else if (mode==102){
return Result.ok().data("class",list0);
}else if (mode==102){
return Result.ok().data("subject",list0);
}
}
return Result.error().message("输入的查询条件有误!");
}
}
@Slf4j
@Api(description = "学生数据操作接口")
@CrossOrigin
@RestController
@RequestMapping("/index/stu")
public class StudentsController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StudentsService studentsService;
@ApiOperation("查询学生信息列表")
@GetMapping("/list")
public Result selectStudentList(){
List<Students> list = studentsService.list();
return Result.ok().data("list",list);
}
@ApiOperation("根据条件查询学生信息")
@GetMapping("/get/{key}")
public Result selectByIdOrName(@ApiParam(name = "key", value = "查询关键字", required = true) @PathVariable String key){
Students stu = studentsService.getByNumberOrName(key);
if (stu==null){
return Result.error().message("您输入的数据有误,请重新输入");
}else {
return Result.ok().data("stu",stu);
}
}
@ApiOperation("添加学生信息")
@PostMapping("/insert")
public Result saveStudentInfo(@ApiParam(value = "插入数据") Students students ){
if (students!=null){
boolean save = this.studentsService.save(students);
if (save){
return Result.ok();
}else {
return Result.error();
}
}else {
return Result.error().message("插入失败");
}
}
@ApiOperation("修改学生信息")
@PostMapping("/update")
public Result editStudentInfo(@ApiParam(value = "插入数据") Students students ){
if (students!=null){
UpdateWrapper<Students> updateWrapper=new UpdateWrapper<>();
updateWrapper.eq("id",students.getId());
boolean flag = this.studentsService.update(students,updateWrapper);
if (flag){
return Result.ok();
}else {
return Result.error();
}
}else {
return Result.error().message("修改失败");
}
}
@ApiOperation("删除学生信息")
@DeleteMapping("/delete/{stuId}")
public Result delStudents(@PathVariable Long stuId ) {
if (this.studentsService.removeById(stuId)){
return Result.ok();
}else {
return Result.error();
}
}
}
对SeparationVueApplication类中代码做一些补充:
@Slf4j
@MapperScan(basePackages = {"com.tiezhu.separation.mapper"})
@SpringBootApplication
public class SeparationVueApplication {
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(SeparationVueApplication.class,args);
Environment env = application.getEnvironment();
String ip = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
String contextPath = env.getProperty("server.servlet.context-path");
if (contextPath == null) {
contextPath = "";
}
log.info("\n----------------------------------------------------------\n\t" +
"Application is running! Access URLs:\n\t" +
"Local: \t\thttp://127.0.0.1:" + port + "/swagger-ui.html" + "\n\t" +
"External: \thttp://" + ip + ':' + port + contextPath + '\n' +
"----------------------------------------------------------");
}
}
启动项目:
点击debug启动,下图为启动成功的控制台:
3.5 接口测试,使用SwaggerUI
打开浏览器访问:Swagger UI http://127.0.0.1:9090/swagger-ui.html 即可看到可以操作的接口的网页:
代码已经编写的有十一个接口,本节只会拿部分接口测试一下效果,其他接口都一样的测试方法。
查询全部学生信息:
4.搭建前端基础框架代码
4.1 电脑需要具备的前端环境
前端开发软件方面常用的都是VS code,大家可以去官网下载免费的。
前端基础和前端进阶方面推荐看黑马程序员或者尚硅谷的,直接在哔哩哔哩可以搜到很多。
https://www.bilibili.com/video/BV1YW411T7GX
电脑需要安装好node环境,没有按照过的可以先进按照菜鸟教程上的步骤操作,非常简单。
Node.js 安装配置 | 菜鸟教程 (runoob.com)
安装完成之后在CMD命令框输入node -v 即可查看版本
node环境安装完成之后安装pnpm也是在cmd中执行一条命令即可:
npm i pnpm -g
安装完成后可以看到一些信息,也可以查看pnpm的镜像源。
4.2 导入前端项目和安装依赖
separationVue-boot的前端直接采用现成的前端脚手架不从零搭建,感兴趣的小伙伴可以学习一下Vue和ElementUI,电脑如果没有Git环境可以通过项目的Git链接下载代码。
vue-admin-template这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
https://gitee.com/panjiachen/vue-admin-template.git
也可以选择直接解压本项目代码下载后note目录里面的separationVue-web.zip文件,该文件为已开发的前端代码。
下载后的代码通过vs code打开代码文件夹。
# 克隆项目
git clone https://github.com/PanJiaChen/vue-admin-template.git
# 进入项目目录
cd vue-admin-template
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
运行成功后效果如下:
4.3 整合Vue编写简易页面
在前端基础框架的代码运行成功后,就可以开始写前端的页面代码了,先找到前端代码目录下 src/router/index.js,修改其中一部分内容。
{
path: ‘/login’,
component: () => import(‘@/views/login/index’),
hidden: true
},
{
path: ‘/404’,
component: () => import(‘@/views/404’),
hidden: true
},
{
path: ‘/’,
component: Layout,
redirect: ‘/dashboard’,
children: [{
path: ‘dashboard’,
name: ‘Dashboard’,
component: () => import(‘@/views/dashboard/index’),
meta: { title: ‘Dashboard’, icon: ‘dashboard’ }
}]
},
{
path: ‘/student’,
component: Layout,
redirect: ‘/student/list’,
name: ‘Student’,
meta: { title: ‘学生信息管理’, icon: ‘el-icon-s-help’ },
children: [
{
path: ‘/student/list’,
name: ‘StuList’,
component: () => import(‘@/views/stu/list’),
meta: { title: ‘学生信息列表’, icon: ‘table’ }
},
{
path: ‘/student/add’,
name: ‘StuAdd’,
component: () => import(‘@/views/stu/form’),
meta: { title: ‘添加学生’, icon: ‘form’ }
},
{
path: ‘/student/edit/:id’,
name: ‘StuEdit’,
component: () => import(‘@/views/stu/form’),
meta: { title: ‘编辑学生’ },
hidden: true
},
{
path: ‘/score/list’,
name: ‘ScoreList’,
component: () => import(‘@/views/scores/list’),
meta: { title: ‘学生成绩信息列表’, icon: ‘nested’ }
},
{
path: ‘/score/look/:id’,
name: ‘ScoreList’,
component: () => import(‘@/views/scores/list’),
meta: { title: ‘查看学生成绩’, icon: ‘nested’ },
hidden: true
},
{
path: ‘/score/add/:id’,
name: ‘ScoreAdd’,
component: () => import(‘@/views/scores/form’),
meta: { title: ‘添加学生成绩’, icon: ‘nested’ },
hidden: true
}
]
},
此部分修改是为前端的路由添加内容,修改后的效果如下:
在src/view/ 路径下添加两个文件夹,stu和scores文件夹。
stu下为学生的相关页面,在该文件夹下添加 form.vue 和list.vue 两个vue页面文件。
## list.vue页面代码
<template>
<div class="app-container">
<el-table
v-loading="listLoading"
:data="list"
stripe
element-loading-text="Loading"
border
highlight-current-row
>
<el-table-column align="center" label="ID" width="95">
<template slot-scope="scope">
{{ scope.$index+2-1 }}
</template>
</el-table-column>
<el-table-column label="姓名" width="110">
<template slot-scope="scope">
{{ scope.row.stuName }}
</template>
</el-table-column>
<el-table-column label="学号" width="180" align="center">
<template slot-scope="scope">
<span>{{ scope.row.stuNumber }}</span>
</template>
</el-table-column>
<el-table-column label="性别" width="95" align="center">
<template slot-scope="scope">
{{ scope.row.sex }}
</template>
</el-table-column>
<el-table-column align="center" label="出生日期" width="250">
<template slot-scope="scope">
<i class="el-icon-time" />
<span>{{ scope.row.birth }}</span>
</template>
</el-table-column>
<el-table-column label="地址" width="200" align="center">
<template slot-scope="scope">
{{ scope.row.address }}
</template>
</el-table-column>
<el-table-column label="班级" width="150" align="center">
<template slot-scope="scope">
{{ scope.row.stuGrade }}{{ scope.row.stuClass }}
</template>
</el-table-column>
<el-table-column label="操作" width="350" align="center">
<template slot-scope="scope">
<router-link :to="'/score/look/'+scope.row.stuNumber">
<el-button type="primary" size="small">查看成绩</el-button>
</router-link>
<router-link :to="'/score/add/'+scope.row.stuNumber">
<el-button type="success" size="small">录入成绩</el-button>
</router-link>
<router-link :to="'/student/edit/'+scope.row.id">
<el-button type="primary" size="small">修改</el-button>
</router-link>
<el-button type="warning" size="small" @click="deleteStu(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import stuApi from '@/api/students'
export default {
filters: {
statusFilter(status) {
const statusMap = {
published: 'success',
draft: 'gray',
deleted: 'danger'
}
return statusMap[status]
}
},
data() {
return {
list: null,
listLoading: true
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
this.listLoading = true
stuApi.getList().then(response => {
console.log(response.data.list)
this.list = response.data.list
this.listLoading = false
})
},
deleteStu(stuId) {
this.$confirm('此操作将永久删除该学生信息, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
stuApi.deleteStuent(stuId).then(response => {
this.fetchData()
this.$message.success(response.message)
})
}).then(response => {
this.fetchData()
this.$message.success(response.message)
})
}
}
}
</script>
## form.vue页面代码
<template>
<div class="app-container">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="学生姓名">
<el-input v-model="form.stuName" />
</el-form-item>
<el-form-item label="学号">
<el-input v-model="form.stuNumber" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option key="男" label="男" value="男" />
<el-option key="女" label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="出生时间">
<el-date-picker v-model="form.birth" value-format="yyyy-MM-dd" />
</el-form-item>
<el-form-item label="所属年级">
<el-select v-model="form.stuGrade">
<el-option
v-for="item in optionsGrade"
:key="item.codeName"
:label="item.codeName"
:value="item.codeName"
/>
</el-select>
</el-form-item>
<el-form-item label="所属班级">
<el-select v-model="form.stuClass">
<el-option
v-for="item in optionsClass"
:key="item.codeName"
:label="item.codeName"
:value="item.codeName"
/>
</el-select>
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveOrUpdate()">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import stuApi from '@/api/students'
import ScoreApi from '@/api/scores'
export default {
data() {
return {
form: { },
optionsClass: [],
optionsGrade: [],
saveBtnDisabled: false // 保存按钮是否禁用,防止表单重复提交
}
},
// 页面渲染成功
created() {
if (this.$route.params.id) {
this.fetchDataById(this.$route.params.id)
}
ScoreApi.getByDictionary(0).then(response => {
this.optionsGrade = response.data.grade
this.optionsClass = response.data.class
})
},
methods: {
fetchDataById(id) {
stuApi.getById(id).then(response => {
this.form = response.data.stu
})
},
saveOrUpdate() {
// 禁用保存按钮
this.saveBtnDisabled = true
if (!this.form.id) {
this.saveData(this.form)
} else {
this.updateData(this.form)
}
},
saveData() {
stuApi.save(this.form).then(response => {
this.$message({
type: 'success',
message: response.message
})
this.$router.push({ path: '/student/list' })
})
},
// 根据id更新记录
updateData() {
stuApi.updateById(this.form).then(response => {
this.$message({
type: 'success',
message: response.message
})
this.$router.push({ path: '/student/list' })
})
}
}
}
</script>
在scores文件夹下,同样新建两个文件 list.vue和form.vue
##list.vue 页面代码:
<template>
<div class="app-container">
<el-table
v-loading="listLoading"
:data="list"
stripe
element-loading-text="Loading"
border
highlight-current-row
>
<el-table-column align="center" label="ID" width="95">
<template slot-scope="scope">
{{ scope.$index+2-1 }}
</template>
</el-table-column>
<el-table-column label="考试科目" width="110" align="center">
<template slot-scope="scope">
{{ scope.row.subjects }}
</template>
</el-table-column>
<el-table-column label="学号" width="180" align="center">
<template slot-scope="scope">
<span>{{ scope.row.stuNumber }}</span>
</template>
</el-table-column>
<el-table-column label="分数" width="95" align="center">
<template slot-scope="scope">
{{ scope.row.score }}
</template>
</el-table-column>
<el-table-column align="center" label="考试说明" width="250">
<template slot-scope="scope">
<span>{{ scope.row.description }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button type="warning" size="small" @click="deleteScore(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import ScoreApi from '@/api/scores'
export default {
data() {
return {
list: null,
listLoading: true
}
}, created() {
this.fetchData()
},
methods: {
fetchData() {
this.listLoading = true
if (this.$route.params.id) {
ScoreApi.getListByStuNumber(this.$route.params.id).then(response => {
this.list = response.data.list
this.listLoading = false
})
} else {
ScoreApi.getList().then(response => {
console.log(response.data.list)
this.list = response.data.list
this.listLoading = false
})
}
},
deleteScore(param) {
this.$confirm('此操作将永久删除该条考试信息, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ScoreApi.deleteScore(param).then(response => {
this.fetchData()
this.$message.success(response.message)
})
}).then(response => {
this.fetchData()
this.$message.success(response.message)
})
}
}
}
</script>
## form.vue 页面代码
<template>
<div class="app-container">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="考试科目">
<el-select v-model="form.subjects">
<el-option
v-for="item in optionsSubject"
:key="item.codeName"
:label="item.codeName"
:value="item.codeName"
/>
</el-select>
</el-form-item>
<el-form-item label="学号">
<el-input v-model="form.stuNumber" readonly="true" />
</el-form-item>
<el-form-item label="分数">
<el-input
v-model="form.score"
type="text"
placeholder="请输入分数"
maxlength="10"
max="150"
show-word-limit
/></el-form-item>
<el-form-item label="考试说明">
<el-input v-model="form.description" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveOrUpdate()">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import ScoreApi from '@/api/scores'
export default {
data() {
return {
form: { },
optionsSubject: [],
saveBtnDisabled: false // 保存按钮是否禁用,防止表单重复提交
}
},
// 页面渲染成功
created() {
if (this.$route.params.id) {
this.fetchDataById(this.$route.params.id)
}
ScoreApi.getByDictionary(103).then(response => {
this.optionsSubject = response.data.subject
})
},
methods: {
fetchDataById(id) {
this.form.stuNumber = this.$route.params.id
},
saveOrUpdate() {
// 禁用保存按钮
this.saveBtnDisabled = true
this.saveData()
},
saveData() {
ScoreApi.save(this.form).then(response => {
this.$message({
type: 'success',
message: response.message
})
this.$router.push({ path: '/student/list' })
})
}
}
}
</script>
一共四个vue文件需要新添加
4.4 使用Axios进行接口请求
vue前端项目的请求一般都写在src/api文件夹下面,可以根据自己的后端接口来自定义请求的js方法。
在src/api文件夹下面新增两个js文件分别为,scores.js 和students.js 文件,两个文件的内容如下:
//scores.js文件内容
import request from '@/utils/request'
const api_name = 'http://127.0.0.1:9090/index/score/'
//请求的接口的基础路径
export default {
getList(params) {
return request({
url: `${api_name}/list`,
method: 'get',
params
})
},
getByDictionary(key) {
return request({
url: `${api_name}/dic/get/${key}`,
method: 'get'
})
},
getListByStuNumber(number) {
return request({
url: `${api_name}/get/${number}`,
method: 'get'
})
},
deleteScore(id) {
return request({
url: `${api_name}/delete/${id}`,
method: 'delete'
})
},
save(info) {
return request({
url: `${api_name}/insert`,
method: `post`,
data: info
})
}
}
//students.js文件内容
import request from '@/utils/request'
const api_name = 'http://127.0.0.1:9090/index/stu/'
export default {
getList(params) {
return request({
url: `${api_name}/list`,
method: 'get',
params
})
},
deleteStuent(stuId) {
return request({
url: `${api_name}/delete/${stuId}`,
method: 'delete'
})
},
getById(id) {
return request({
url: `${api_name}/get/${id}`,
method: 'get'
})
},
save(student) {
return request({
url: `${api_name}/insert`,
method: `post`,
data: student
})
},
updateById(student) {
return request({
url: `${api_name}/update`,
method: `post`,
data: student
})
}
}
以上代码全部完成后,就可以在vs code 的终端中输入 npm run dev 然后回车启动项目(启动之前请先关闭运行,Ctrl+C 关闭)。启动成功及各个页面的效果如下。
到这里我们就实现了一个简单的前后端分离的Demo了,所有的代码均上传Gitee仓库,
https://gitee.com/lu-tiezhu/separationVue-boot.git小伙伴们可以通过直接下载代码或者Git到IDEA里,如果觉得这篇文章尚可,可以收藏点赞转发哦。