前言
- 从下午到晚上, 构建了springboot的后台然后将后台和前端关联起来, 前端发送请求后端接收, 完成简单的登录逻辑判断. 由于是新手花费了很多时间. 感觉后端分层并不是很清晰.同时很多技术问题,仅仅是复制黏贴, 先追求跑通代码, 没有理解运行细节, 如果错误希望大佬不吝赐教.
前端请求
疑问1: axios请求
- 前端发表单数据post请求,应该采用什么格式? form-data/form-urlencoded/json
// 表单信息
loginForm: {
username: 'admin',
password: '123456'
}
// 发出axios请求
await this.$http.post('login', Qs.stringify(this.loginForm))
后端搭建
- 前端只要能发出请求包含用户信息就行了, 先不管这么多了, 开始springboot吧. 今天的目标是完成一个输入用户名密码验证跳转.
后端技术
-
mysql 8.0.20
-
mybatis plus
-
druid
-
springboot 2.x
创建Springboot项目
- 引入依赖
- druid 连接池直接用的starter
- mybatis plus
- jackson 用于处理json数据
- lombok 用于偷懒
- spring-boot-devtools 热部署工具
- mysql-connector-java 连接mysql
基本就是这些了
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.alex</groupId>
<artifactId>alex-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>alex-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>14</java.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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</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>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
</dependencies>
</project>
连接数据库
1. 查看数据库表单
这是直接前端项目拿的配套数据库sql脚本, 有点繁琐其实…
其中mg_name, mg_pwd为用户名,密码, 其他是用户信息.
CREATE TABLE `sp_manager` (
`mg_id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`mg_name` varchar(32) NOT NULL COMMENT '名称',
`mg_pwd` char(64) NOT NULL COMMENT '密码',
`mg_time` int unsigned NOT NULL COMMENT '注册时间',
`role_id` tinyint NOT NULL DEFAULT '0' COMMENT '角色id',
`mg_mobile` varchar(32) DEFAULT NULL,
`mg_email` varchar(64) DEFAULT NULL,
`mg_state` tinyint DEFAULT '1' COMMENT '1:表示启用 0:表示禁用',
PRIMARY KEY (`mg_id`)
) ENGINE=InnoDB AUTO_INCREMENT=511 DEFAULT CHARSET=utf8 COMMENT='管理员表';
2. 建立pojo类
根据mybatis plus 官网教程走, 建立一个和数据库表名一致的实体类
pojo/MgRole
@Data
@TableName("sp_manager")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MgRole {
@TableId
@JsonProperty("id")
private int mg_id;
private String mg_name;
@JsonIgnore
private String mg_pwd;
private int mg_time;
private int role_id;
private String mg_mobile;
private String mg_email;
private int mg_state;
}
3. 建立Mapper接口
- 这里@Repository标记 才不会有注入时的错误提示. 但是不加也无所谓(小坑)
- 其次mybatis plus 不用重写任何方法
mappers/RoleMapper
@Repository
public interface RoleMapper extends BaseMapper<MgRole> {
}
4. 配置数据库连接信息
- 这是基本格式了, 如果复制记得要改掉(数据库表名, 用户名, 密码) 三个信息.
- 犯了点小错, 实践的时候直接加上了druid连接池, 导致报错处理了蛮久, 应该先确认基本数据库能够使用, 再使用连接池.
resources/application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/vuedb?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: xxxxxxx
5. 测试失败
- 细节直接在RoleMapper里, 用快捷键command + N 生成测试类, 保持包名一致.
- 但是结果一直返回null, 查询不到数据, 应该是表名, 字段出了些问题.
6. 更简单的测试类
- 这里我自己建了了一个2行的数据表来测试, 通过即可下一步
- 注意@SpringBootTest和junit test的引用, 和我一致(小坑)
- 其次表名上面可以加上@TableName(“test”) 保持表名和数据库表名一致
测试Mapper
@Repository
public interface TestMapper extends BaseMapper<ForTest> {
}
测试实体类
@TableName("test")
@Data
public class ForTest {
private int id;
private String name;
}
测试用例
import cn.alex.pojo.ForTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class MapperTest {
@Autowired
private TestMapper mapper;
@Test
public void test() {
List<ForTest> list= mapper.selectList(null);
ForTest t = mapper.selectById(1);
System.out.println(t);
list.forEach(System.out::println);
}
}
7.排查
经过一番搜索, 问题应该在于字段名映射出问题了, 可以打开mybatis plus的log日志, 同时数据库这张表实在是太难受了, 字段名mg_name下划线, 但是前端需要的字段是username, 有点烦恼, 不过没事 先把后端做好.
mybatis-plus:
configuration:
# mybatis plus日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 类 mg_name 字段 mg_name 应该关闭
map-underscore-to-camel-case: false
加上了log日志 能直接看到mybatis生成的查询语句, map-underscore-to-camel-case意思应该是不把下划线映射到驼峰. 因为我这里都是用下划线形式.改为false.
加入连接池
- 凭印象感觉, mybatis-plus应该是一个简化jdbc操作的东西, 而连接池可以为我们省去手动创建数据库连接.看了网上很多资料,都还在写配置类, 现在都有druid starter了 直接用了, 不需要写任何配置类了
只需要在application.yml这样写即可
server:
servlet:
# 后台地址
context-path: /api
# 端口号
port: 8080
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/vuedb?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: xxxxx
druid:
#初始化大小
initialSize: 5
#最小值
minIdle: 5
#最大值
maxActive: 20
#最大等待时间,配置获取连接等待超时,时间单位都是毫秒ms
maxWait: 60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接
timeBetweenEvictionRunsMillis: 60000
#配置一个连接在池中最小生存的时间
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,
#'wall'用于防火墙,SpringBoot中没有log4j,我改成了log4j2
filters: stat,wall,log4j2
#最大PSCache连接
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 配置StatFilter
web-stat-filter:
#默认为false,设置为true启动
enabled: true
url-pattern: "/*"
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
#配置StatViewServlet
stat-view-servlet:
url-pattern: "/druid/*"
#允许那些ip
allow: 127.0.0.1
login-username: admin
login-password: 123456
#禁止那些ip
deny: 192.168.1.102
#是否可以重置
reset-enable: true
#启用
enabled: true
- 因为我配置了后端服务器挂载到localhost:8080/api下
浏览器输入localhost:8080/api/druid/stat 便可访问druid管理页面, 这样连接池就构建好了, 不需要再有任何操作了.
Controller
- 配置如何接收请求
这里的细节在于, 前端发出了一个post请求我该如何接收, 暂时用的是@RequestParam(“username”)这种方式, 如果有这个字段的映射就能拿到数据. 目前是成功的. 但应该不是最好的方式, 对象可能比较合适, 是否要建一个新的对象呢? 等我下次试试
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public Result login(@RequestParam("username") String username, @RequestParam("password") String password) {
return loginService.validate(username, password);
}
}
Service层
-
目前的理解是用于查询数据库, 同时判断用户名密码是否符合, 最后返回封装好的对象. 原本是dao层来处理sql的, 但现在用了mybatis plus, mapper是个接口了. 所以数据库语句直接写在service层了(没有考虑很多)
-
目前只做了基本判断
@Service
public class LoginService {
@Autowired
private RoleMapper roleMapper;
public Result validate(String username, String password) {
Result res = null;
// username = "admin";
// password = "123456";
MgRole mgRole = roleMapper.selectOne(new QueryWrapper<MgRole>()
.select("mg_id", "role_id", "mg_name","mg_mobile","mg_email")
.eq("mg_name", username)
.eq("mg_pwd", password));
System.out.println(username + " " + password);
if (mgRole == null) {
System.out.println(mgRole);
res = ResultFactory.buildFailResult("登录失败");
}
res = ResultFactory.buildSuccessResult(mgRole);
return res;
}
}
查询语句
- 我只是照葫芦画瓢写了一下, 感觉应该还有更好的方式.
MgRole mgRole = roleMapper.selectOne(new QueryWrapper<MgRole>()
.select("mg_id", "role_id", "mg_name","mg_mobile","mg_email")
.eq("mg_name", username)
.eq("mg_pwd", password));
JSON对象
- Controller层是能够直接返回对象作为响应体的, Springboot会进行自动类型转换, 转成json给前端. 但是!!!
前端要求的数据是, 并不仅仅是实体类的某些属性, 还要加上meta对象 包括字符串信息和状态码
{
"data": {
"id": 500,
"rid": 0,
"username": "admin",
"mobile": "123",
"email": "123@qq.com",
}
"meta": {
"msg": "登录成功",
"status": 200
}
}
因此又写了一个返回对象Result类
- @JsonProperty(“data”) 用来指定返回json时的属性名称(很好用!)
package cn.alex.result;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
@JsonProperty("data")
private Object result;
@JsonProperty("meta")
private Meta meta;
Result(int code, String message, Object data) {
this.meta = new Meta(code, message);
this.result = data;
}
@Data
private class Meta {
private int code;
private String message;
public Meta(int code, String message) {
this.message = message;
this.code = code;
}
}
}
状态码 枚举类
public enum ResultCode {
SUCCESS(200),
FAIL(400),
UNAUTHORIZED(401),
NOT_FOUND(404),
INTERNAL_SERVER_ERROR(500);
public int code;
ResultCode(int code) {
this.code = code;
}
}
Result对象工厂类
public class ResultFactory {
public static Result buildSuccessResult(Object data) {
return buildResult(ResultCode.SUCCESS, "成功", data);
}
public static Result buildFailResult(String message) {
return buildResult(ResultCode.FAIL, message, null);
}
public static Result buildResult(ResultCode resultCode, String message, Object data) {
return buildResult(resultCode.code, message, data);
}
public static Result buildResult(int resultCode, String message, Object data) {
return new Result(resultCode, message, data);
}
}
测试: 跨域问题
一顿操作以后, 后端算是写好了, 在前端发出请求的时候,
前端响应 状态码:403
请求方法: options
mode: cors 跨域请求 因为我的前后端端口号不一致, 并且post请求不是一个"简单请求"
- 经过查阅只需要加一个配置类即可
config/MyWebMvcConfiguer
@Configuration
public class MyWebConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//所有请求都允许跨域,使用这种配置方法就不能在 interceptor 中再配置 header 了
registry.addMapping("/**")
.allowCredentials(true)
.allowedOrigins("http://localhost:8081")//改成前端地址
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
解决啦! 最后前端就可以通过表单提交,得到验证信息通过登录测试了.
遗留问题(xiaciyiding)
- JWT token 没弄
- 前后端数据对接问题, 前端应该发json后端接收json比较好吧, 有必要把前端发送的数据封装成一个新的对象吗
- 跨域问题? 有什么意义吗?
- 是否该加一个dao层
- 如果仅仅想返回对象的部分数据 还要封装成json 该怎么做(目前做法是用@JsonInclude(JsonInclude.Include.NON_NULL 直接排除了null的元素, 应该有一些场景数据就是要返回null的吧这样应该不太合适)
- mybatis-plus更高级的用法…