点击去看上一篇
一、创建用户 model
1.创建用户数据库库 leadnews_user
核心表 ap_user
建库建表语句
这里一定要使用 navicat,执行SQL 文件,以防止 cmd 中的编码问题
先将 SQL 语句,保存在电脑中,再使用 navicat 打开
CREATE DATABASE IF NOT EXISTS leadnews_user DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE leadnews_user;
SET NAMES utf8;
/*
Navicat MySQL Data TransferSource Server : localhost
Source Server Version : 50721
Source Host : localhost:3306
Source Database : leadnews_userTarget Server Type : MYSQL
Target Server Version : 50721
File Encoding : 65001Date: 2021-04-12 13:58:42
*/SET FOREIGN_KEY_CHECKS=0;-- ----------------------------
-- Table structure for ap_user
-- ----------------------------
DROP TABLE IF EXISTS `ap_user`;
CREATE TABLE `ap_user` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`salt` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码、通信等加密盐',`name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',`password` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码,md5加密',`phone` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',`image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',`sex` tinyint(1) unsigned DEFAULT NULL COMMENT '0 男\r\n 1 女\r\n 2 未知',`is_certification` tinyint(1) unsigned DEFAULT NULL COMMENT '0 未\r\n 1 是',`is_identity_authentication` tinyint(1) DEFAULT NULL COMMENT '是否身份认证',`status` tinyint(1) unsigned DEFAULT NULL COMMENT '0正常\r\n 1锁定',`flag` tinyint(1) unsigned DEFAULT NULL COMMENT '0 普通用户\r\n 1 自媒体人\r\n 2 大V',`created_time` datetime DEFAULT NULL COMMENT '注册时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户信息表';-- ----------------------------
-- Records of ap_user
-- ----------------------------
INSERT INTO `ap_user` VALUES ('1', 'abc', 'zhangsan', 'abc', '13511223453', null, '1', null, null, '1', '1', '2020-03-19 23:22:07');
INSERT INTO `ap_user` VALUES ('2', 'abc', 'lisi', 'abc', '13511223454', '', '1', null, null, '1', '1', '2020-03-19 23:22:07');
INSERT INTO `ap_user` VALUES ('3', 'sdsa', 'wangwu', 'wangwu', '13511223455', null, null, null, null, null, '1', null);
INSERT INTO `ap_user` VALUES ('4', '123abc', 'admin', '81e158e10201b6d7aee6e35eaf744796', '13511223456', null, '1', null, null, '1', '1', '2020-03-30 16:36:32');
INSERT INTO `ap_user` VALUES ('5', '123', 'suwukong', 'suwukong', '13511223458', null, '1', null, null, '1', '1', '2020-08-01 11:09:57');
INSERT INTO `ap_user` VALUES ('6', null, null, null, null, null, null, null, null, null, null, null);-- ----------------------------
-- Table structure for ap_user_fan
-- ----------------------------
DROP TABLE IF EXISTS `ap_user_fan`;
CREATE TABLE `ap_user_fan` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',`fans_id` int(11) unsigned DEFAULT NULL COMMENT '粉丝ID',`fans_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '粉丝昵称',`level` tinyint(1) unsigned DEFAULT NULL COMMENT '粉丝忠实度\r\n 0 正常\r\n 1 潜力股\r\n 2 勇士\r\n 3 铁杆\r\n 4 老铁',`created_time` datetime DEFAULT NULL COMMENT '创建时间',`is_display` tinyint(1) unsigned DEFAULT NULL COMMENT '是否可见我动态',`is_shield_letter` tinyint(1) unsigned DEFAULT NULL COMMENT '是否屏蔽私信',`is_shield_comment` tinyint(1) unsigned DEFAULT NULL COMMENT '是否屏蔽评论',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户粉丝信息表';-- ----------------------------
-- Records of ap_user_fan
-- ------------------------------ ----------------------------
-- Table structure for ap_user_follow
-- ----------------------------
DROP TABLE IF EXISTS `ap_user_follow`;
CREATE TABLE `ap_user_follow` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',`follow_id` int(11) unsigned DEFAULT NULL COMMENT '关注作者ID',`follow_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '粉丝昵称',`level` tinyint(1) unsigned DEFAULT NULL COMMENT '关注度\r\n 0 偶尔感兴趣\r\n 1 一般\r\n 2 经常\r\n 3 高度',`is_notice` tinyint(1) unsigned DEFAULT NULL COMMENT '是否动态通知',`created_time` datetime DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户关注信息表';-- ----------------------------
-- Records of ap_user_follow
-- ------------------------------ ----------------------------
-- Table structure for ap_user_realname
-- ----------------------------
DROP TABLE IF EXISTS `ap_user_realname`;
CREATE TABLE `ap_user_realname` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` int(11) unsigned DEFAULT NULL COMMENT '账号ID',`name` varchar(20) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户名称',`idno` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '资源名称',`font_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '正面照片',`back_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '背面照片',`hold_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手持照片',`live_image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '活体照片',`status` tinyint(1) unsigned DEFAULT NULL COMMENT '状态\r\n 0 创建中\r\n 1 待审核\r\n 2 审核失败\r\n 9 审核通过',`reason` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '拒绝原因',`created_time` datetime DEFAULT NULL COMMENT '创建时间',`submited_time` datetime DEFAULT NULL COMMENT '提交时间',`updated_time` datetime DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP实名认证信息表';-- ----------------------------
-- Records of ap_user_realname
-- ----------------------------
INSERT INTO `ap_user_realname` VALUES ('1', '1', 'zhangsan', '512335455602781278', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '9', '', '2019-07-30 14:34:28', '2019-07-30 14:34:30', '2019-07-12 06:48:04');
INSERT INTO `ap_user_realname` VALUES ('2', '2', 'lisi', '512335455602781279', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '1', '', '2019-07-11 17:21:18', '2019-07-11 17:21:20', '2019-07-12 06:48:04');
INSERT INTO `ap_user_realname` VALUES ('3', '3', 'wangwu6666', '512335455602781276', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '9', '', '2019-07-11 17:21:18', '2019-07-11 17:21:20', '2019-07-12 06:48:04');
INSERT INTO `ap_user_realname` VALUES ('5', '5', 'suwukong', '512335455602781279', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '1', '', '2020-08-01 11:10:31', '2020-08-01 11:10:34', '2020-08-01 11:10:36');
2.在 heima-leadnews-model 模块底下创建 com.heima.model.user 包
3.在 com.heima.model.user 包底下创建 pojos 包
dtos 包的创建暂时忽略,不过这里也会讲解 dtos 包的大概作用(存放 DTO 类型)
POJO
POJO 的全程有几种,如 pure old java object 、plain ordinary java object 等。但意思都差不多,可以理解为就是简单的 Java 对象,符合没有被继承类、没有实现接口、属性私有、拥有无参构造方法等规则的 Java 对象
POJO 可划分为 VO、DTO、BO、DO 等
POJO | 全称 | 作用 |
VO | View Object | 前端会将 VO 内的属性渲染到页面上 |
DTO | Data Transfer Object | 前后端通过 DTO 传输数据、后端的服务与服务之间也可以通过 DTO 传输数据 |
BO | Business Object | BO 内会封装多个 DO,即业务直接处理 BO,间接处理 DO。如简历相关业务,简历就是 BO,专业技能、实习经历、项目经历、个人信息等为 DO |
DO | Data Object | DO 中的属性与数据库表中的属性一一对应,即实体类 |
这里黑马头条中的包划分可能不够严谨,也可能有其他考量,不是重点,暂时不关心
在阿里开发规范中,规定类名中的 DO/BO/DTO/VO/AO/PO/UID 需要大写
4.在 pojos 包中创建 ApUser 类
package com.heima.model.user.pojos;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.io.Serializable;
import java.util.Date;/*** <p>* APP用户信息表* </p>** @author itheima*/
@Data
@TableName("ap_user")
public class ApUser implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 密码、通信等加密盐*/@TableField("salt")private String salt;/*** 用户名*/@TableField("name")private String name;/*** 密码,md5加密*/@TableField("password")private String password;/*** 手机号*/@TableField("phone")private String phone;/*** 头像*/@TableField("image")private String image;/*** 0 男1 女2 未知*/@TableField("sex")private Boolean sex;/*** 0 未1 是*/@TableField("is_certification")private Boolean certification;/*** 是否身份认证*/@TableField("is_identity_authentication")private Boolean identityAuthentication;/*** 0正常1锁定*/@TableField("status")private Boolean status;/*** 0 普通用户1 自媒体人2 大V*/@TableField("flag")private Short flag;/*** 注册时间*/@TableField("created_time")private Date createdTime;}
@MyBatisPlus 注解
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
ApUser 类使用了 MybatisPlus 中的 4 个注解
注解 | 作用 |
@TableName | 让 MybatisPlus 识别当前类对应的表,ApUser > ap_user |
@TableId | 让 MybatisPlus 识别当前属性为表的主键,id > id |
@IdType | 让 MybatisPlus 识别当前主键的类型,IdType.AUTO > 自增类型 |
@TableField | 让 MybatisPlus 识别当前属性对应的表字段,name > name |
添加注解是为了防止数据库字段或 Java 代码属性的命名不规范、不统一
蛇形驼峰转化
如果满足以下三个条件,即可开启蛇形驼峰转化,可以不用上面的 MybatisPlus 注解来进行识别:
- Java 严格按照驼峰命名(名字中单词首字母大写,如属性名 helloWorld、类名 HelloWorld)
- 数据库严格按照蛇形命名(名字中的单词之间用下划线分割,如 hello_world)
- 名字都一一对应完全相同(名字都是 hello world 只是命名形式不同)
在 yml 中开启蛇形驼峰转化:
黑马提供的初始项目的 model 模块下并没有 resources 目录,需要自己创建。然后在 resources 目录下创建 application.yml 文件来进行配置(黑马并没有使用 MybatisPlus 的自动命名转化配置,为了不给后面埋雷,不建议大家做这个配置)
mybatis-plus:configuration:map-underscore-to-camel-case: true
MyBatisPlus POM 依赖
在 heima-leadnews-model 模块的 pom.xml 中添加依赖
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId>
</dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
实际上 mybatis-plus-boot-starter 中包含了 mybatis,但是可能和导入的 mybatis 相比版本什么的有点不同,建议按照黑马的来
@Lombok 注解
import lombok.Data;
注解 | 作用 |
@Data | 为当前类的所有属性添加 getter & setter 方法 |
Lombok POM 依赖
该依赖已经在父工程 pom.xml 中的 dependencies 标签下被引入,在该标签下被引入的依赖会在所有子模块中生效,因此无需再次引入
<!-- lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><scope>provided</scope>
</dependency>
二、用户 service 微服务搭建
微服务搭建口诀:建 module、改 pom、写 yml、主启动、业务类(不绝对,仅提供参考)
1.在heima-leadnews-service 模块底下的 pom.xml 中添加所有微服务公用的依赖
黑马提供的初始工程中已经添加好了,不必再次添加
引入其他子模块
<!-- 引入依赖模块 -->
<dependency><groupId>com.heima</groupId><artifactId>heima-leadnews-model</artifactId>
</dependency><dependency><groupId>com.heima</groupId><artifactId>heima-leadnews-common</artifactId>
</dependency><dependency><groupId>com.heima</groupId><artifactId>heima-leadnews-feign-api</artifactId>
</dependency>
依赖 | 作用 |
heima-leadnews-model | 导入实体类等,如 ApUser |
heima-leadnews-common | 导入全局配置、全局功能等,如全局异常捕获 |
heima-leadnews-feign-api | 导入该模块中统一管理的 feign 远程调用接口(一般微服务之间的调用,都通过 feign 来调用) |
引入开发和测试相关依赖
<!-- Spring boot starter -->
<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>
依赖 | 作用 |
spring-boot-starter-web | 包含 @Controller、@Service、@RequestMapping 等 web 相关注解,实现相关 web 功能 |
spring-boot-starter-test | 包含 @SpringBootTest、@Test 等测试相关注解,实现相关测试功能 |
SpringBoot 把许多场景抽象为了启动器 starter。当前我们需要一个搭建 web 服务的应用场景,因此使用 starter-web 启动器,即可自动完成 web 服务的所有相关配置(使用默认值),如将使用了 @Bean 注解的类注册进 spring 容器、将内置的 tomcat 配置好等等。与 Spring 相比方便了许多,减少了大量配置文件、配置操作
引入 nacos 服务注册、配置中心相关依赖
<!-- Spring Cloud Alibaba Nacos-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
依赖 | 作用 |
spring-cloud-starter-alibaba-nacos-discovery | 实现 nacos 相关服务发现、服务注册等功能 |
spring-cloud-starter-alibaba-nacos-config | 实现 nacos 相关配置拉取功能 |
注册中心
在微服务架构中,会有某些服务需要由许多机器/容器来提供,而每个机器/容器的 url 都是不一样的。因此我们需要通过服务注册中心将不同的 url 注册到同一个服务名底下,之后再调用该服务名的时候,会从注册中心将服务注册表(包含着 url 与服务名之间映射关系)拉取到本地,然后 nacos 内置的 Ribbon 会将服务名转化为具体的 url(默认轮询,这次是第一个 url,下次就轮到另一个 url),再进行服务调用
配置中心
由于在微服务架构下有许许多多的服务,因此也会产生许许多多的配置文件,每个配置文件也会有许许多多的配置项。当有需求变更时,修改起配置来十分头疼,管理起来也很混乱,很容易改错配置,开发环境直接成为修罗炼狱。而配置中心可以让项目中的配置全部统一管理,并且还可以实现动态修改、运行环境区分(开发、测试、生产等)、配置回滚等功能,很好的解决上述问题
2.在 heima-leadnews-service 模块底下创建 heima-leadnews-user 子模块
黑马提供初始项目中已经建好了的,不必再重建
3.修改 heima-leadnews-user 底下的 pom.xml
由于我们已经在 heima-leadnews-user 的父工程 heima-leadnews-service 中引入了必要的依赖,因此这里暂时不需要添加依赖
<?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>heima-leadnews-service</artifactId><groupId>com.heima</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>heima-leadnews-user</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties></project>
4.编写 heima-leadnews-user 的 yml 配置
在 heima-leadnews-user 底下的 resources 目录中新建 bootstrap.yml 文件
SpringBoot 核心配置文件有两种:bootstrap 和 application
bootstrap
bootstrap 是应用程序的父上下文,由父 ApplicationContext 加载,优先级更高。一般用来做系统层级的参数配置
application
application 是当前应用程序的上下文,优先级较低。一般用来做应用层级的参数配置
为什么用 bootstrap
由于 Spring 根据优先级会先加载 bootstrap 中 nacos 服务的 url(没有就先使用 nacos 默认的),并进行连接尝试,如果你的 nacos 不在默认的 url 上,那么就会报错。因此一定要在 bootstrap 上做好配置,避免报错,才能启动成功
编写 bootstarp 文件的内容
server:port: 51801
spring:application:name: leadnews-usercloud:nacos:discovery:server-addr: localhost:8848config:server-addr: localhost:8848file-extension: ymlprofiles:active: dev
注意使用自己的 nacos 服务地址
nacos 下载
Releases · alibaba/nacos · GitHub
通过 GitHub 下载,可以自行选择版本(我当时下载的是 1.1.4),点击版本下的 Assets 中的 nacos-server-1.1.4.zip(不论 tar.gz/zip 在 linux 和 windows 都能使用,应该)
https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.zip
nacos 启动
解压后双击 bin/startup.cmd 启动 nacos(默认端口为 8848)
在 nocos 配置中心添加 heima-leadnews-user 的服务配置
左边导航栏选择配置列表,点击列表右上方的加号,添加配置
Data ID = spring.application.name (即 leadnews-user) + "-" + spring.profiles.active (即 dev) + "." + spring.cloud.nacos.config.file-extension (即 yml) = leadnews-user-dev.yml
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: 159357
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:mapper-locations: classpath*:mapper/*.xml# 设置别名包扫描路径,通过该属性可以给包中的类注册别名type-aliases-package: com.heima.model.user.pojos
5.完善 heima-leadnews-user 的主启动类 UserApplication
创建 com.heima.user.UserApplication 主启动类(黑马提供的初始工程也建好了)
package com.heima.user;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.user.mapper")
public class UserApplication {public static void main(String[] args) {SpringApplication.run(UserApplication.class,args);}
}
@SpringBootApplication 注解
用来标记当前类为 SpringBoot 的主配置类,SpringBoot 应用由当前类的 main 方法启动,当 mian 方法执行下面这条代码的时候,
SpringApplication.run(UserApplication.class,args);
SpringBoot 将会把当前包底下的 @Bean 方法返回的对象以及 @Component 类的实例化对象自动存放入 Spring 容器中,并且将所有的配置项以默认值自动装配(约定大于配置)
@EnableDiscoveryClient 注解
该注解是由 SpringBoot 提供的,拥有服务注册与订阅的相关功能。使用该注解后,SpringBoot 应用会自动向配置的服务注册中心注册自己,并定时发送心跳包告知注册中心自己的服务状态
@MapperScan 注解
由 heima-leadnews-model 中的 MyBatis 依赖所提供的注解,可以自动扫描指定包下的类,将扫描到的接口作为 Mapper 并将其实现。有了这个注解可以不必再 XXXMapper 接口上再写 @Mapper 注解了。如果当前服务有许多的 Mapper 接口,那每一个接口上都要写 @Mapper 注解,就会有许多的 @Mapper 注解。如果使用 @MapperScan 注解,即可用一个 @MapperScan 注解代替大量 @Mapper 注解
6.构建 heima-leadnews-user 的微服务目录
config、controller、service、mapper
config
存放配置相关类
controller
为前端提供访问接口,不关心具体业务逻辑,只负责接收参数和返回结果
service
向 contrller 提供业务逻辑接口,serviceImpl 负责将接口中的方法实现
mapper
向 service 提供操作持久层数据的接口,mapper.xml 负责实现接口
为什么这样设计
这种设计方式体现了一种思想 MVC(即 model + view + controller)
分层 | 作用 |
view | 从 controller 获取数据,并展示给用户 |
controller | 接收 view 的请求,让 model 进行处理,并返回处理结果给 view |
model | 操作数据完成业务,将结果返回给 controller |
view 对应项目中的 前端,controller 对应项目中的 controller,model 对应项目中的 service + controller。controller 就如一个中介者,封装 view 和 model 之间的交互,松散耦合,集中控制交互。而 service 与 serviceIpml 分开,mapper 与 mapper.xml 分开,体现了依赖倒转,使调用方不依赖于实现,而是依赖于抽象接口
7.开发 heima-leadnews-user 登录功能相关的 controller 层代码
在 controller 下新建 v1 包,表示第一个版本
在 v1 包底下新建 ApUserLoginController 类
登录接口
接口路径 | /api/v1/login/login_auth |
请求方式 | POST |
参数 | LoginDto |
响应结果 | ResponseResult |
LoginDto
在 heima-leadnews-model 模块下的 com.heima.model.user.dtos 包下创建 LoginDto
@Data
public class LoginDto {/*** 手机号*/private String phone;/*** 密码*/private String password;
}
ResponseResult
在 heima-leadnews-model 中的 com.heima.model.common.dtos 包底下创建 ResponseResult 类
/*** 通用的结果返回类* @param <T>*/
public class ResponseResult<T> implements Serializable {private String host;private Integer code;private String errorMessage;private T data;public ResponseResult() {this.code = 200;}public ResponseResult(Integer code, T data) {this.code = code;this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.errorMessage = msg;this.data = data;}public ResponseResult(Integer code, String msg) {this.code = code;this.errorMessage = msg;}public static ResponseResult errorResult(int code, String msg) {ResponseResult result = new ResponseResult();return result.error(code, msg);}public static ResponseResult okResult(int code, String msg) {ResponseResult result = new ResponseResult();return result.ok(code, null, msg);}public static ResponseResult okResult(Object data) {ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());if(data!=null) {result.setData(data);}return result;}public static ResponseResult errorResult(AppHttpCodeEnum enums){return setAppHttpCodeEnum(enums,enums.getErrorMessage());}public static ResponseResult errorResult(AppHttpCodeEnum enums, String errorMessage){return setAppHttpCodeEnum(enums,errorMessage);}public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){return okResult(enums.getCode(),enums.getErrorMessage());}private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String errorMessage){return okResult(enums.getCode(),errorMessage);}public ResponseResult<?> error(Integer code, String msg) {this.code = code;this.errorMessage = msg;return this;}public ResponseResult<?> ok(Integer code, T data) {this.code = code;this.data = data;return this;}public ResponseResult<?> ok(Integer code, T data, String msg) {this.code = code;this.data = data;this.errorMessage = msg;return this;}public ResponseResult<?> ok(T data) {this.data = data;return this;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getErrorMessage() {return errorMessage;}public void setErrorMessage(String errorMessage) {this.errorMessage = errorMessage;}public T getData() {return data;}public void setData(T data) {this.data = data;}public String getHost() {return host;}public void setHost(String host) {this.host = host;}}
AppHttpCodeEnum
在 heima-leadnews-model 模块中的 com.heima.model.common.enums 包下新建 AppHttpCodeEnum 类
public enum AppHttpCodeEnum {// 成功段固定为200SUCCESS(200,"操作成功"),// 登录段1~50NEED_LOGIN(1,"需要登录后操作"),LOGIN_PASSWORD_ERROR(2,"密码错误"),// TOKEN50~100TOKEN_INVALID(50,"无效的TOKEN"),TOKEN_EXPIRE(51,"TOKEN已过期"),TOKEN_REQUIRE(52,"TOKEN是必须的"),// SIGN验签 100~120SIGN_INVALID(100,"无效的SIGN"),SIG_TIMEOUT(101,"SIGN已过期"),// 参数错误 500~1000PARAM_REQUIRE(500,"缺少参数"),PARAM_INVALID(501,"无效参数"),PARAM_IMAGE_FORMAT_ERROR(502,"图片格式有误"),SERVER_ERROR(503,"服务器内部错误"),// 数据错误 1000~2000DATA_EXIST(1000,"数据已经存在"),AP_USER_DATA_NOT_EXIST(1001,"ApUser数据不存在"),DATA_NOT_EXIST(1002,"数据不存在"),// 数据错误 3000~3500NO_OPERATOR_AUTH(3000,"无权限操作"),NEED_ADMIND(3001,"需要管理员权限");int code;String errorMessage;AppHttpCodeEnum(int code, String errorMessage){this.code = code;this.errorMessage = errorMessage;}public int getCode() {return code;}public String getErrorMessage() {return errorMessage;}
}
ApUserLoginController 代码
@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {@Autowiredprivate ApUserService apUserService;@PostMapping("/login_auth")public ResponseResult login(@RequestBody LoginDto dto){return apUserService.login(dto);}
}
@RestController 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {@AliasFor(annotation = Controller.class)String value() default "";
}
本质是个复合注解,即 @RestController = @Controller + @ResponseBody
注解 | 作用 |
@Target | 规定注解的使用范围( ElementType.TYPE 类注解, ElementType.FIELD 属性注解, ElementType.METHOD 方法注解... ) |
@Retention | 规定注释的有效时期( RetentionPolicy.SOUTCE 代码注解, RetenionPolicy.CLASS 字节码注解, RetentionPolicy.RUNTIME 运行注解... ) |
@Documented | 将该注解写入 JAVADOC 中 |
@Inherited | 使用该注解的类的子类,可以继承该注解 |
注解 | 作用 |
@Controller | 包含 @Component 注解,可以将使用该注解的类的实例化对象注册到 Spring IOC 容器中 |
@ResponseBody | 将方法的返回值直接写入 HTTP Response 的 body 中 |
@RequestMapping 注解
当请求路径与 @RequestMapping 的 value 值一致时,对请求进行路由,路由到当前类或方法
@AutoWired 注解
自动装载,将注册进容器中的 ApUserService 类的实例化对象取出并注入到当前的 ApUserController 类中。为 ApUserController 提供业务方法(ApUserService 还没建好,可以先不注入)
@RequestBody 与 @RequestParam 的区别
@RequestBody 发送
@RequestBody 接收
@RestController
public class TestController {@RequestMapping(value = "/test")public void test(@RequestBody String body) {System.out.println(body);}
}
@RequestParam 发送
第一种 query params
第二种 body form-data
@RequestParam 接收
@RestController
public class TestController {@RequestMapping(value = "/test")public void test(@RequestParam String param) {System.out.println(param);}
}
8.开发 heima-leadnews-user 登录功能相关的 service 层代码
在 service 下新建 apUserService 接口
public interface ApUserService extends IService<ApUser> {/*** app端登录功能* @param dto* @return*/public ResponseResult login(LoginDto dto);
}
IService 是由 MyBatis Plus 提供的一个接口,MyBatis 也提供了对应的实现类 ServiceImpl
ServiceImpl 类注入了 baseMapper 接口,而 MyBatis Plus 也实现了该接口
因此我们可以使用 ServiceImpl 中的丰富的基础数据处理方法间接调用 mapper 来进行数据库操作
在 service 下新建 impl 包并创建 ApUserServiceImpl 类实现 ApUserService 接口
@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUserMapper, ApUser> implements ApUserService {/*** app端登录功能* @param dto* @return*/@Overridepublic ResponseResult login(LoginDto dto) {//1.正常登录 用户名和密码if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())){//1.1 根据手机号查询用户信息ApUser dbUser = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));if(dbUser == null){return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");}//1.2 比对密码String salt = dbUser.getSalt();String password = dto.getPassword();String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());if(!pswd.equals(dbUser.getPassword())){return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);}//1.3 返回数据 jwt userString token = AppJwtUtil.getToken(dbUser.getId().longValue());Map<String,Object> map = new HashMap<>();map.put("token",token);dbUser.setSalt("");dbUser.setPassword("");map.put("user",dbUser);return ResponseResult.okResult(map);}else {//2.游客登录Map<String,Object> map = new HashMap<>();map.put("token",AppJwtUtil.getToken(0L));return ResponseResult.okResult(map);}}
}
判断用户名和密码是否为空
使用 StringUtils 的 isNotBlank 方法判断字符串是否为空、是否有长度、是否含有空白字符
if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())) {// 1.正常登录
} else {// 2.游客登录Map<String,Object> map = new HashMap<>();map.put("token",AppJwtUtil.getToken(0L));return ResponseResult.okResult(map);
}
游客登陆返回
{"host": "","code": 200, // AppHttpCodeEnum.SUCCESS"errorMessage": "操作成功","data": {"token": // 使用 AppJwtUtil.getToken(0L) 方法生成游客 token,具体实现下面会讲}
}
判断该用户是否存在
使用 ServiceImpl 中的 getOne 方法,用 Wrapper 构造查询条件( dbUser.getPhone() 等与 dto.getPhone)
//1.1 根据手机号查询用户信息
ApUser dbUser = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));
if(dbUser == null){return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
}
不存在返回
{"host": "","code": 1002, // AppHttpCodeEnum.DATA_NOT_EXIST"errorMessage": "用户信息不存在","data": {}
}
判断密码是否正确
数据库中的密码加密是通过先加盐再MD5的方式完成的
MD5 为一种单向加密算法,只能加密不能解密。因此很适合用来加密密码,这样连数据库所有者都无法知道用户的真实密码,大大提高安全性。但是既然这么安全为什么还要加盐呢?因为MD5的广泛使用,黑客认为有利可图,于是使用暴力学习,将许多密码进行 MD5 加密,并将映射关系存起来,这样就能用映射表,将 MD5 加密后的密码转为真实的密码。所以,我们通过加盐,也就是给原有的密码加上其他的字符/字符串再进行 MD5 加密,使得暴力解密后的密码也不是真正的密码
使用 org.springframework.util 中的 DigestUtils 类里的 md5DigestAsHex 方法对 用户输入的密码 + 数据库中获取的盐值 进行加密,然后将加密后的输入密码与数据库中的比较
//1.2 比对密码
String salt = dbUser.getSalt();
String password = dto.getPassword();
String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
if(!pswd.equals(dbUser.getPassword())){return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
密码错误返回
{"host": "","code": 2, // AppHttpCodeEnum.LOGIN_PASSWORD_ERROR"errorMessage": "密码错误","data": {"token": // 使用 AppJwtUtil.getToken(0L) 方法生成游客 token,具体实现下面会讲}
}
生成 JWT
JWT,即 Json Web Token。它由三部分组成
header JWT头 | 一个 JSON 对象用来描述 JWT 的签名算法、令牌类型 |
payload JWT载荷 | 一个 JSON 对象用来传递数据 |
signature JWT签名 | 使用 base64 加密后的 header + base64 加密后的 payload + 服务器生成的密钥,通过指定算法生成签名 |
形如 headerheaderheader.payloadpayloadpayload.signaturesignaturesignature
即 header 与 payload 与 signature 之间使用 '.' 点号隔开
eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQrDMAzA_uJzA_biuKa_cWnKUigEnMLG2N_nHnaTEPrAMRosUFhUmCmZUEmsImnN-kgZ90q2Z1EWmKDZgIVmRBRS1gn8WuP2t4963t099FnbaWF2bWHWe3B99f9Z-D5bNPz-AKKQJrqAAAAA.0ZNpu7dGgNtoF-UBGM3LvFZxEsltZFHazfByij_IW5KIv82oNxV4VremFE4zAx_lFAxE2XuOZ53LGb1u2mY7Lg
由于 base64 可以解密,因此不要在 payload 中存放重要的隐私信息
为什么需要 JWT
JWT 用来存储客户端与服务器之间的身份识别信息,相当于是存会话信息。既然是会话,为什么不用 session 呢?因为 session 依赖于 cookie,session 中会存放用户信息,浏览器的 cookie 中会存 session 的 id,每次浏览器发送请求的时候就会携带 cookie,这样也能将 cookie 中的 sessionID 发送给服务器,这样服务器就能识别出这个请求时哪个用户发出的。可是有些客户端并没有 cookie,因此也无法存储 sessionID,于是我们就使用 JWT,客户端发送请求的时候带上 JWT,而 JWT 中又有用户的信息,这样服务器也能识别出请求的来源。抛弃掉 session cookie,实现前后端分离
在 heima-leadnews-utils 模块下创建 com.heima.utils.common.AppJwtUtil 类
public class AppJwtUtil {// TOKEN的有效期一天(S)private static final int TOKEN_TIME_OUT = 3_600;// 加密KEYprivate static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";// 最小刷新间隔(S)private static final int REFRESH_TIME = 300;// 生产IDpublic static String getToken(Long id){Map<String, Object> claimMaps = new HashMap<>();claimMaps.put("id",id);long currentTime = System.currentTimeMillis();return Jwts.builder().setId(UUID.randomUUID().toString()).setIssuedAt(new Date(currentTime)) //签发时间.setSubject("system") //说明.setIssuer("heima") //签发者信息.setAudience("app") //接收用户.compressWith(CompressionCodecs.GZIP) //数据压缩方式.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳.addClaims(claimMaps) //cla信息.compact();}/*** 获取token中的claims信息** @param token* @return*/private static Jws<Claims> getJws(String token) {return Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(token);}/*** 获取payload body信息** @param token* @return*/public static Claims getClaimsBody(String token) {try {return getJws(token).getBody();}catch (ExpiredJwtException e){return null;}}/*** 获取hearder body信息** @param token* @return*/public static JwsHeader getHeaderBody(String token) {return getJws(token).getHeader();}/*** 是否过期** @param claims* @return -1:有效,0:有效,1:过期,2:过期*/public static int verifyToken(Claims claims) {if(claims==null){return 1;}try {claims.getExpiration().before(new Date());// 需要自动刷新TOKENif((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){return -1;}else {return 0;}} catch (ExpiredJwtException ex) {return 1;}catch (Exception e){return 2;}}/*** 由字符串生成加密key** @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}}
生成过程
//1.3 返回数据 jwt user
String token = AppJwtUtil.getToken(dbUser.getId().longValue());
Map<String,Object> map = new HashMap<>();
map.put("token",token);
dbUser.setSalt("");
dbUser.setPassword("");
map.put("user",dbUser);return ResponseResult.okResult(map);
public static String getToken(Long id){Map<String, Object> claimMaps = new HashMap<>();claimMaps.put("id",id);long currentTime = System.currentTimeMillis();return Jwts.builder().setId(UUID.randomUUID().toString()).setIssuedAt(new Date(currentTime)) //签发时间.setSubject("system") //说明.setIssuer("heima") //签发者信息.setAudience("app") //接收用户.compressWith(CompressionCodecs.GZIP) //数据压缩方式.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳.addClaims(claimMaps) //cla信息.compact();
}
使用 jjwt 依赖提供的 jsonwebtoken 包中的 Jwts.builder 方法构建 jwt,并将用户 id 存放在 jwt 的 payload 中,从而让服务器能够识别请求的发送者
三、测试接口
{"host": null,"code": 200,"errorMessage": "操作成功","data": {"user": {"id": 4,"salt": "","name": "admin","password": "","phone": "13511223456","image": null,"sex": true,"certification": null,"identityAuthentication": null,"status": true,"flag": 1,"createdTime": "2020-03-30T08:36:32.000+00:00"},"token": "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWL0QrDIAwA_yXPFZLqau3fpBqZg4IQCxuj_770YW93HPeF12iwAfqEiWRxNefiAu-rYx-z48dMNZKUtSaYoPGAjSIizcHjMoGeu9360SHH3VVNn9IONuOzmHHvxvLu_zOm-2zWwvUD7al0KoAAAAA.5GEoiqO9MH7WCZeBWM95XAlAtlrkHvrbzUqZOO0HktuJjcCCJ20MVprJjXXa-DuNY9qdHIy0yt7Z1ziaflTHUw"}
}
基础用户登录功能完成