新建后台项目
1、新建项目
File——>New——>Project——>Maven——>Groupld(公司名称com.tx)、ArtifactId(项目名称project1)、Version(版本号1)
2、新建子项目Product
右键点击刚刚新建的父项目——>New——>Module——>Maven——>ArtifactId(子项目名称product)
3、新建启动类
在java下新建com.tx包,再在这个包下面新建启动类ProductApp
4、新建控制类
在com.tx包下新建controller包再在这个包下新建TestController
@RestController //这个注解等于@Controller + @ResponseBody
5、新建配置文件
在resources下新建application.yml
6、浏览器验证是否成功
然后把ProductApp启动在浏览器输入本地地址加上端口号8081即可访问到刚刚在控制器中返回的"abcdefg"
7、新建实体子项目pojo
需要引入包
<!--Project Lombok用来解决pojo实体类过长的问题,也就是生成get、set、toString-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
pojo是什么?
pojo、domain、entity、javabean、model讲的其实差不多其实是同一个东西就是实体类。实体类这一层,与数据库中的属性值基本保持一致。有的开发写成pojo,有的写成model,也有domain,也有dto(这里做参数验证,比如password不能为空等),实体类如果你不懂什么东西的话,那你就想成是范围。所以pojo也要单独提出来为一个项目模块
8、引入mybatis-plus
引入mybatis-plus后就不用自己去建controller、service、pojo的包了,里面的初始代码也不用自己建,在通过mybatis-plus的模板生成后就都有了,包括增删改查这些功能也都不用写了,十分方便,。
需要引入包
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--generator的版本必须得和上面的boot-starter的版本一致-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--代码生成器模板引擎-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
新建generator子项目,新建CodingApp类
CodingApp类中的代码
package com.tangxin.util;
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.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.tangxin.pojo.BasePojo;
import java.util.HashMap;
import java.util.Map;
public class CodingApp {
public static void main(String[] args) {
//创建生成器对象
AutoGenerator generator = new AutoGenerator();
//配置代码生成规则
//全局配置
GlobalConfig globalConfig = new GlobalConfig();
//获取根目录的地址 user.dir当前项目project2101的路径
//System.getProperty("user.dir");获取所有环境变量
String projectPath = System.getProperty("user.dir");
//配置所有者
globalConfig.setAuthor("tangxin");
//生成的目录
globalConfig.setOutputDir(projectPath);
//将全局配置加载到生成器
generator.setGlobalConfig(globalConfig);
//包配置
PackageConfig pc = new PackageConfig();
//配置父包的名字路径
pc.setParent("com.tangxin");
//配置子包的名字路径
//配置实体,因为父路径已经配置了所以可以省略pc.setEntity("com.tangxin.pojo");
pc.setEntity("pojo");
//配置控制包
pc.setController("controller");
//配置服务包
pc.setService("service");
//配置服务实体包
pc.setServiceImpl("service.impl");
//配置mapper包
pc.setMapper("mapper");
//设置生成路径
//模板名称
String moduleName = "product";
//pojo所在路径
String pojoPath = projectPath + "/pojo/src/main/java/com/tangxin/pojo";
//其他模块路径
String otherPath = projectPath + "/"+moduleName+"/src/main/java/com/tangxin";
Map<String,String> pathInfo = new HashMap<>();
pathInfo.put("entity_path",pojoPath);
pathInfo.put("mapper_path",otherPath+"/mapper");
pathInfo.put("xml_path",projectPath+"/"+moduleName+"/src/main/resources/com/tangxin/mapper");
pathInfo.put("service_path",otherPath+"/service");
pathInfo.put("service_impl_path",otherPath+"/service/impl");
pathInfo.put("controller_path",otherPath+"/controller");
pc.setPathInfo(pathInfo);
//将包配置加载到生成器
generator.setPackageInfo(pc);
//数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://192.168.149.131:3306/shop?useUnicode=true&useSSL=false&characterEncoding=utf8");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
generator.setDataSource(dsc);
//策略配置
StrategyConfig strategy = new StrategyConfig();
//命名的策略,当他发现你的命名有下划线会自动变成驼峰命名
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//你自己的父类实体,没有就不用设置!一般设置一些各个表中都有的字段如id
strategy.setSuperEntityClass(BasePojo.class);
//要不要加Lombok,好像是用来生成get和set的
strategy.setEntityLombokModel(true);
//生成的Controller要不要加RestController注解,RestController注解专门处理前后分离
strategy.setRestControllerStyle(true);
//写于父类中的公共字段,然后就不会在各个类中再自动生成了,由父类生成
strategy.setSuperEntityColumns("id");
//设置具体生成的表 scanner("表名,多个英文逗号分割").split(",")
strategy.setInclude("pms_attr_value");
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
generator.setStrategy(strategy);
//设置模板为Freemarker模板
generator.setTemplateEngine(new FreemarkerTemplateEngine());
//执行代码生成
generator.execute();
}
}
通过generator生成的代码中,service接口都继承了IService功能强大
9、新建公共子项目bus,封装统一异常
需要引入的包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<!-- 要启动一个项目作为web项目,就得需要web容器
因为内置的tomcat容器性能不是特别好
所以要把内置的tomcat容器的依赖去掉 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 再用一个新的web容器也是spring-boot内置的所以直接引入就好 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- 一个工具依赖包 -->
<dependency>
<groupId>org.duracloud</groupId>
<artifactId>common</artifactId>
</dependency>
<dependency>
<groupId>com.tangxin</groupId>
<artifactId>pojo</artifactId>
</dependency>
</dependencies>
新建bus公共项目。一些公共的依赖例如web这些可以都写在这儿,其他子项目引入bus包,再通过依赖传递将引入的包传递过去就可以获得bus项目下的所有方法,还有一些公共的方法也可以方在这儿比如之前写的跨域请求的过滤
当然还有封装的统一异常也可以放在这,在前端拿到后台返回的数据过程中,可能会出现运行错误,因此后台需要封装统一异常然后返回给前端,最后由前端显示给用户看
如果只是返回Sting类型的错误信息,前端不知道这是正确的返回还是错误的返回,因此我们要建立返回数据的规范来判断他是正确的返回,还是错误的返回,可以新建一个返回JSON类来判断
为了确保code只能是哪儿几个值,还得新建一个ResultCode类,用枚举来规定只有哪儿几个值有效
最后把这ResultJson返回给前端让前端判断显示
10、分页查询
当数据过多的时候就需要用到分页查询,当前端把pageNo当前页,size每页显示条数传后台时,后台需要处理返回页面数据。
首先需要去mybatis-plus的官网找到分页插件(用的是旧版的,就是往IOC容器里面加一个Bean),然后复制到之前建的ProductConfig类中
接着就可以在service的PmsBrandServiceImpl实现类中调用page方法,通过源码可得知page方法中参数的含义,这样可以准确的把前端的值传入
先是新建page接口
接着在PmsBrandServiceImpl实现类中重写page方法,完成对数据库数据的查询和返回
当然要想实现分页查询还得在设置中加上一个分页拦截器
最后经过控制器的调用和返回数据给前端,前端拿到数据后即可显示在页面上
11、条件查询
和分页查询共用一个方法page,通过判断前端传递过来的name是否为空来决定是否查询,如果为空就查询所有信息,如果不为空就按照条件查询
StringUtils.isNoneBlank(name),就是专门用来判断是否为空的,这也就是之前在bus下引入的工具包的函数
12、nacos注册中心
在分布式项目中,有多个微服务,就需要一个注册中心进行统一管理,在此之前要了解一下CAP原则
C —致性所有节点必须是统—的 (例如zookeeper,修改一个节点需要其他节点同步完成后才会告诉你修改完了)
A 高可用性所有的请求都会得到响应 (例如eureka 用注册中心AP比CP更靠谱点)
P 分区容错性 如果有少量节点挂了,应该还是可以正常访问的(P是必须满足的,nacos可以在AP和CP间互相切换)
在P满足的前提下,C和A只能满足一个,
构建虚拟化容器nacos,到docker Hub上搜索并拿到nacos快速开始的代码
//网址
https://registry.hub.docker.com/
//拿到的运行代码
docker run --name nacos-quick -e MODE=standalone -p 8849:8848 -d nacos/nacos-server:2.0.2
在linux的虚拟化容器docker中运行这段代码后,可以在浏览器输入
//账号密码都是nacos
http://192.168.149.131:8848/nacos
来访问nacos的后台,如果访问不成功可以换个浏览器继续访问试试,在nacos后台能成功访问后,我们需要把微服务注册到nacos中,需要用到spingcloud下的nacos组件,老规矩先导包
①服务的发现与注册
<!--版本号-->
<properties>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
</properties>
<!--父包中引入,springcloud的版本必须和springboot的版本对应-->
<!--导入springcloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--因为用的都是阿里的组件,所以还需要导入spring-cloud-alibaba-dependencies-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--子包中引入-->
<!--在父项目中引入了spring-cloud-alibaba-dependencies后
子包中的spring-cloud-starter-alibaba-nacos-discovery
spring-cloud-starter-alibaba-nacos-config等等就都不需要再写版本号了由父包直接控制-->
<!--nacos注册中心发现依赖 管的是服务的发现与注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置的包的作用是将服务注册到注册中心,配置文件是配的是你注册到哪儿个注册中心
然后去启动类配置注解
同理配置其他子项目,最后就你能在nacos的服务列表中看到注册的服务了
②统一配置中心
在nacos后台管理找到配置管理,然后点击右边的加号新增配置文件
文件的名字需要有统一的前缀和后缀(前缀名字中间也必须要有-),这样方便日后管理
如何读到nacos的配置文件呢?
第一步还是导包,需要加上spring-cloud-starter-alibaba-nacos-config,nacos注册中心配置依赖,管的是配置中心。
<!--版本号-->
<properties>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
</properties>
<!--父包中引入,springcloud的版本必须和springboot的版本对应-->
<!--导入springcloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--因为用的都是阿里的组件,所以还需要导入spring-cloud-alibaba-dependencies-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--子包中引入-->
<!--在父项目中引入了spring-cloud-alibaba-dependencies后
子包中的spring-cloud-starter-alibaba-nacos-discovery
spring-cloud-starter-alibaba-nacos-config等等就都不需要再写版本号了由父包直接控制-->
<!--nacos注册中心发现依赖 管的是服务的发现与注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos注册中心配置依赖 管的是配置中心-->
<!--如果使用配置中心,那么项目中就不允许再有application.yml配置文件了-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
新建bootstrap.yml,如果使用配置中心,那么项目中就不允许再有application.yml配置文件,需要将其他的配置文件存放在nacos配置中心中,项目启动的时候就通过bootstrap.yml自动去nacos中读配置文件
建好后服务的发现与注册,统一配置管理就都已经完成了
13、Brand页面的增删改查功能
①查询
详情见之前的分页查询和条件查询
②新增
在前端将form表单的数据都传递到后台时,由于传递的是formData对象,所以用PmsBrand对象接收,最后调用save方法,进行保存新增即可
③删除
删除比较简单,一般都是逻辑删除,把数据库中的active从1变成0即可,查询的时候之要查active为1的数据,那么自然也就实现了如同删除的操作
如果点击“恢复数据”按钮也会将active重新变成1
④修改
修改过程中前端会传入需要修改的id,后台通过id查询到单个数据返回即可
在前端修改完数据后,需要通过后台进行数据更新,后台拿到品牌对象可以通过品牌对象的id进行更新操作
14、MinIo图片服务器(图片存取)
首先我们要了解,当图片上传后它的存储方式是本地+云端的存储方式,本地数据库只存图片的所在地址(路径),而图片本身是存在云端的 (因为存储图片是以二进制的方式进行存储的如果直接存在数据库里,就会导致数据库体积特别大)
当前端将数据和图片文件传入后台时,后台需要用MultipartFile来接收,名字必须和前端传入的文件参数名一致file
在将文件用Java代码上传到云端MinIo之前,需要通过docker构建MinIo的容器
docker run -p 9000:9000 --name minio1 \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
minio/minio server /data
构建完成后,通过http://192.168.149.131:9000/minio进行网页访问,然后新建桶Create bucket,其实也就新建文件夹意思
点击images桶旁边的三个小点,选择Edit policy,然后修改权限变成可读可写就行
在这之后,想在Java中完成文件上传到MinIo,需要先导包,可以在MinIo开发文档或者Maven仓库中拿到
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.0.2</version>
</dependency>
Java中将文件传入到MinIo的代码
完善加注释后的
但是在将文件上传到MinIo中又出现了一个新的问题,那就是上传的名字是用户文件本身的名字,如果有人再次上传文件和之前的文件有同名的就会进行覆盖,所以为了不被覆盖需要进行文件的重命名再上传,利用服务器的当前时间来+6位随机数确保文件不会出现重复名称
解决了重复的问题后,发现还可以继续优化,如果俩个人上次的文件是一模一样的,那么第二次就不用上传了,服务器在经过判断之后如果确定一样就直接将之前的服务器有的文件路径返回即可,如果两个文件一样那么就表示它们的字节流一样,但是又不能直接存储字节流不然不就存储原文件又绕回去了嘛,因此我们要存储的是MD5码,MD5码是由文件字节流计算而来的,为了保险起见我们还要对比来个文件的大小和后缀名(毕竟来个文件的MD5码相同的可能还是有的,但是再加上大小和后缀名相同就几乎不可能了),为了保存这个文件的信息就需要再建一张表来存放
建完表后用generator生成代码,只需要修改moduleName和表名就好
将上传文件的方法抽离出来单独封装成一个类,在product下新建工具类MinioUtil,首先就先将MinIo的url、账号、密码给抽离出来放到Nacos的配置文件中
然后我们要判断文件是否已经上传过,如果上传过,直接返回url地址即可,如果没有那么再将文件上传,返回新上传后的地址
下面是getImage方法
15、配置网关(Gateway)
由于微服务的增多,那么在前端配置的baseUrl中的地址明显是不能是某一个微服务,就比如我Product是8081,admin是8082,如果固定死那么自然就会有其他的访问不到,因此我们要新建一个网关服务让所有的请求先到网关Gatewy的8090中,再由网关来根据不同的地址,访问不同的微服务
新建Gateway项目
老规矩依旧是导包、配置、注解
导包
<!--geteway项目 不能导入spring-boot-starter-web的包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos注册中心发现依赖 管的是服务的发现与注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos注册中心发现依赖 管的是配置中心-->
<!--如果使用配置中心,那么项目中就不允许再有application.yml配置文件了-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
配置
在配置过程中需要配置跨域过滤器,然后就需要将之前写bus公共项目下的单独的跨域过滤器注释掉,以前是由前端自己访问过滤后的后端地址,现在是由前端访问Gateway,然后由Gateway根据地址去访问各个项目
spring:
servlet:
#设置文件上传大小
multipart:
max-file-size: 512MB
max-request-size: 512MB
enabled: true
file-size-threshold: 0
cloud:
gateway:
discovery:
locator:
#开启gateway
enabled: true
#能将模块的名字转为小写
lower-case-service-id: true
#routes路由不止一个因此要加s
routes:
#实际上它是个list,因此要下方要加上-号
- id: admin
#uri访问地址 必须用lb协议从注册中心中去访问admin服务
uri: lb://admin
#配置地址
predicates:
#以/ums-user开头的链接全部跳转到admin微服务上
- Path=/ums-user/**
#实际上它是给list,因此要下方要加上-号
- id: product
#uri访问地址 必须用lb协议从注册中心中去访问product服务
uri: lb://product
#配置地址
predicates:
#以/pms-brand和/pms-category开头的链接全部跳转到product微服务上
- Path=/pms-brand/**,/pms-category/**,/pms-attr/**,/pms-sku/**,/pms-product/**,/pms-images/**
#配置跨域过滤器 然后就需要将单独的跨域过滤器注释掉
globalcors:
cors-configurations:
#过滤所有地址 都允许访问
'[/**]':
#允许什么样的域名
allowedOrigins: "*"
#允许什么样的域名
allowedMethods: "*"
#允许什么样的请求头
allowedHeaders: "*"
注解
新建启动类Gateway
16、user页面的增删改查功能
①新增(feign调用其他服务上传文件)
在前端把数据提交到后台后,后台接收到数据,进行上传文件重命名处理,最后再将其保存
但是问题来了,这个upload上传文件的方法是在另外一个Product项目中,在当前项目中调用,就相当于是跨项目调用了,此时就需要用到feign组件,这个就是用来解决分布式项目之间互相调用的问题
如果你的项目中的方法要给别的项目使用,需要先对外发布,也就是对其他微服务暴露功能
要想使用feign组件依旧是导包、配置、注解
导包
<!--2、导入feign依赖包,可以使不同微服务之间进行功能的相互调用-->
<!--然后到启动类中开启FeignClient AdminApp-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
配置
如果要使用product微服务下的功能需要自己写个service接口
注解
最后再在Controller层进行调用
接着把整个文件进行保存新增返回给前端即可
这时候又会有一个问题,我们新增后随机了密码,但是我们不能就这么直接将密码返回给前端,我们应该用邮箱或者短信将密码发给用户,但是如果直接写在add添加方法之后,或者抽离出来让其进行调用,都有点强耦合的意思了,就好比一个日志功能有很多个方法都需要,我们如果在每个方法后面都写上就不符合程序思想了,当然确实能抽离出来作为一个方法类然后进行调用,但是这样如果有一天这个日志方法不需要了,我们还得挨个去改,如果是几个还好要成百上千个想想都绝望,因此这时候就要引入我们的切面编程思想AOP,这个小问题用AOP后置通知就轻松解决了,下面是我看到一个写的不错的博主发布的,可以看看
AOP详解
通过后置增强通知,在add方法运行结束之后,如果没有异常就执行ater方法,将你的密码通过邮件发送出去
②查询、修改、删除
这三个和品牌模块一模一样也就不多写了
17、新增发送邮件子项目(消息中间件rabbitmq)
邮件的调用失败与成功都不应该影响到主项目的运行,那么此时用FeignClient进行远程服务调用显然就不合适了,因为用FeignClient远程调用的话,邮件调用如果失败了,那么主项目也就失败了,主项目应该是那种只是通知你一下要给谁发个什么样的邮件,至于你发送成功了没有,什么时候发送都不关心,所以用消息中间件比较合适
消息中间件也就是消息队列, 把一个消息作为一个独立的应用部署到服务器上,这样的东西我们叫消息队列,我们可以把消息放到消息对列中,再让需要的方法自己去拿
如果没有消息中间件,会出现邮件系统操作完后要主系统发邮件,短信系统也要主系统发短信,日志系统也要,就会大幅度增加主系统的负担
而用了消息中间件主系统就能解放了,在主系统A操作成功后,将操作成功的消息发送到消息队列中,由邮件系统、短信系统等等,接收到消息后自己去发邮件和短信
应用场景一
还有一种情况是高并发,同一时间有很多人发起同一个操作,服务器会承受不住,虽然可以用同步锁来解决问题,但是在面对多个服务器有负载均衡的情况下,操作发送到负载均衡器,由负载均衡器负责去想各个服务转发,这时候锁只能同步一个服务器中的操作,影响不到另外一个服务器,还是会出现两个操作同步进行的情况
应用场景二
所有的用户请求,都往消息中间件这里面放,服务器不停的往消息队列中拿消息就好
在用消息队列的情况下,即使很多人同时发起同一操作,也都只是发送到消息队列中而已,最后再由服务器一条一条去拿过来处理就好了
回归正题要想发送邮件不影响到主程序的运行,那么消息中间件就不可缺少,要想使用rabbitmq我们就得在docker中新增虚拟化容器rabbitmq
//密码必须是8位以上
//rabbitmq中有两个端口,一个是通信端口5672,一个是控制台端口15672
docker run -d --hostname my-rabbit --name some-rabbit -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 5672:5672 -p 15672:15672 rabbitmq:management
在docker上添加好后开始运行,可以上控制台尝试添加一个新的队列email
//后台地址
http://192.168.149.131:15672
要想在Java后台使用rabbitmq依旧是导包、配置、注解,当然rabbitmq的导包特别一点,导入的是amqp的包,这其实也就是rabbitmq的协议。AMQP协议(Advanced Message Queuing Protocol,高级消息队列协议)
在user类中新增adminAop,往消息队列中发送消息
导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置
注解
植入RabbitTemplate,就可以通过调用convertAndSend方法,将数据发送到队列email中了
要想拿到消息队列中的消息,就要新建一个email子项目,负责在收到消息后将邮件发送出去
导包
发送邮件要导入spring-boot-starter-mail包
<dependency>
<groupId>com.tangxin</groupId>
<artifactId>bus</artifactId>
</dependency>
<dependency>
<groupId>com.tangxin</groupId>
<artifactId>pojo</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--nacos注册中心发现依赖 管的是配置中心-->
<!--如果使用配置中心,那么项目中就不允许再有application.yml配置文件了-->
<!--不需要写入注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--导入发送邮件的依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
配置
同样也是要拿到rabbitmq的配置文件
除了需要配置rabbitmq以外,还需要配置mail,因为发邮件还需要一个邮件服务器,自己搭建不好搭建,可以用第三方的
例如:以QQ邮箱为例,点击设置,再点击其中的账户
下拉看到POP3等服务,其中POP3是接邮件的服务,SMTP是发邮件的服务,这两个服务必须先开启
发邮件的服务地址(点击“如何使用 Foxmail 等软件收发邮件?”查看),这也就是为什么配置文件中host主机的地址要配置为smtp.qq.com了
username是你的邮箱,password是你点击生成授权码后获得的
发送短信之后,授权码才是你要的密码
注解
当然最主要的是要写一个监听队列的监听器,这里写的是监听Uesr项目的
通过导入的mail包拿到JavaMailSender属性,再通过其下的send方法将邮件发出
18、新商品分类子项目
新建商品分类表
然后就用generator生成pms-category分类模块的代码,修改模块名称和表名称为当前项目对应的即可,记得要把contoller层的@requestMapping中的//去掉一个
再去nacos中修改一下Gateway配置文件的代码,让/pms-category开头的链接也跳转到product微服务上
①查询
接到前端发送的请求,查询所有层级下的数据
通过parentId不断递归直到把所有层级下的数据查出来
因为返回的数据中必须要有children字段,也就是存放子集的字段,这样前端的表格在用row-key指定了唯一标识id作为分层对象后,才能看到它更好的分层显示
所以要在实体类中加上children属性,由于它并不是数据库中的字段,只是一个临时字段,所以要加@TableField注解
②删除
当然这里面的删除允许真删,所以前端只用传入一个id就好,还需要注意的是如果当前层级下有子集在是不允许删除的所以还要加个判断
通过父级id查有多少个子集呗,如果有子集就不让删
删除完返回给前端,告诉前端删除成功了,前端调用查询函数刷新一下页面就好
③新增、拿到单条数据、修改
除了查询外,其他的都是些常规的操作也就不细写了
④attr属性列表
需要新建数据库表文件pms_attr
然后就用generator生成pms_attr商品属性模块的代码,修改模块名称和表名称(可以添加逗号一次性生成多个)为当前项目对应的即可,记得要把contoller层的@requestMapping中的//去掉一个
要想前端访问到还得去nacos中配置网关
①查询
因为属性列表的数据是根据分类页面下的id来的,所以在拿到前端传入的分类id后,数据名会和后台@GetMapping注解的方法的参数进行匹配,如果对应的上就将值转到后台并赋值(后台只需要接自己想要的参数就好),我们需要通过这个分类id去查到,属性列表的数据
通过传入的分类id去拿到属性列表数据
拿到后返回给前端,前端再根据数据进行显示
②新增、删除、修改
都是简单的数据调用也就不多写了
⑤SKU列表
需要新建数据库表文件pms_sku
然后就用generator生成pms_sku商品SKU属性模块的代码,修改模块名称和表名称(可以添加逗号一次性生成多个)为当前项目对应的即可,记得要把contoller层的@requestMapping中的//去掉一个
要想前端访问到还得去nacos中配置网关
②查询、新增、删除、修改
查询、新增、删除、修改全部都跟attr属性列表的后台操作一致
19、商品模块设计
新建商品表pms_product
然后就用generator生成pms_product商品模块的代码,修改模块名称和表名称(可以添加逗号一次性生成多个)为当前项目对应的即可,记得要把contoller层的@requestMapping中的//去掉一个
要想前端访问到还得去nacos中配置网关
商品模块的增删改查
①查询
前端发送"/list"请求,查询分页信息
如果有传入name就查询单条,如果没有name就查询所有数据
最后将分页信息返回给前端
②新增
因为新增的属性过多,前端是分成了五段来分段输入的,这其中就包括“基础资料 商品图片 商品属性 商品SKU SKU库存 商品详情”,但是后端存却是一次存完的,只是这期间有很多类似于select选择这样的需要数据进行选择
基础资料
关联品牌下的关联选择器需要品牌列表,通过get方法发送"/getData"请求到后端
后端通过iPmsBrandService下的getAll方法获得brandList,只需要返回active=1的有效数据就好
商品分类下的Cascader 级联选择器需要分类列表,通过get方法发送"/getData"请求到后端,因为是和品牌列表的查询一起返回的所以添加个map来存放数据
后端通过iPmsCategoryService下的getByParentId方法获得categoryList,需要返回从顶级0开始所有的子集数据,子集数据赋值给children,最后将数据放入map中再返回给前端
商品图片
最后通过保存save方法,统一将数据传入后台进行新增
商品属性
在商品分类被选择时,触发前端changeCategory方法将数组categoryIds传入后台,当然是被qs处理过后的数组参数,后端通过一个Long数组接收参数
通过类别id数组去查询对应的数据,,因为是数组是多个数据,所以要用in,最后用list集合返回
最后将skus和attrs两个list放入一个map集合,返回给前端
商品SKU
详情见上方商品属性
SKU库存
主要修改的也就是form.skus属性,最后将这个属性通过save方法发生到后台新增就好
商品详情
商品详情中用的是一个富文本编辑器,因为富文本编辑器在传入图片的时候会直接将图片转换为字节流,导致往数据库中存的时候太大了,所以加了个useCustomImageHandler属性,用我们自己的上传方法,添加了一个添加图片的事件@image-added=“handleImageAdded”,在handleImageAdded函数中,通过post的方法将url为’/pms-images/upload’的参数传到了后端的upload方法中
该方法会调用MinioUtil中的upload方法,最后返回个Sting类型的图片url路径给前端,当然前端想要访问后台的方法需要在nacos的gateway网关配置文件中新增转发地址
保存数据
最后就是前端点击保存按钮将上述的所有新增的属性this.form传入后台进行新增,因为file、picfiles、skus、attrs这四个属性在pmsProduct实体中没有,所以要单独接收
因为skus和attrs的数据太多,还需要重新建两个数据表来存放两个属性中的数据
新建pms_sku_store表,存放sku的数据
新建pms_attr_value表,存放attr的数据
然后去generator中生成代码,修改module模块名和setInclude表名就可以了
因为要保存三表,所以要开启事务管理
还需要去启动类中加上开启事务管理的注解@EnableTransactionManagement
先是将file文件处理好,就file用upload上传后将返回的数据,设置到pmsProduct中的Img属性中
接着处理picfiles文件数组,需要先新建一个字符串数组,好用来存放通过upload上传后的文件,通过for循环将picfiles挨个上传然后存放到picArray中,然后再将picArray设置到pmsProduct中的Pics属性中就好,处理完pmsProduct就将pmsProduct保存到数据库中,也必须先将pmsProduct保存到数据库中,因为后面需要拿到pmsProduct的Id进行使用
然后是skus,同样也是要新建一个skulist来存放skuStore的数据,在将sku反序列化为skuStore对象后,还需要把当前的商品Id设置到skuStore中,最后才是把这一条数据添加到skulist中,然后再通过saveBatch方法进行数据保存
反序列化就是将原本,这样的数据
序列化为这样的对象数据
attrs的数据处理方法同上方skus,在往三张表分别存入数据后,把他们的返回值进行与操作,只有全部为true才算新增成功
③删除
当前端发送的url为"/del"时,后端就通过传入的id和active属性进行逻辑上的删除
④修改
修改最开始得拿到各个tabs要显示的数据,this.getOne()、this.getData()、this.getAttrsAndSkus()
this.getOne()
根据productId把produc的属性都查出来并返回给前端
this.getData()
这个是之前就有的,专门用来查brandList和categoryList的
一个将所有的品牌信息查出来,用map返回brandList
一个将所有的分类信息包括子集的查出来,用map返回categoryList
this.getAttrsAndSkus()
将productId对应的attrs和skus的属性值查出来并返回
查到attrs的值数据并通过map返回attrs给前端
查到skus的值数据并通过map返回skus给前端
基础资料
基础资料修改完后点击保存,触发点击事件updateBase往后台传递baseform数据,后台在接收到updateBase的发送请求后,通过productId来进行商品数据的更新
商品图片
商品图片修改完后点击保存,触发点击事件updateImg往后台传递file、商品相册老的图片的names、新的图片的files、id等数据,后台在接收到updateImg的发送请求后,通过productId来进行商品图片的更新
商品属性
后台在接到前端通过post将url为this.url.updateAttr,参数为attrs的请求发送的数据时,会挨个把attr的属性取出,将JSON反序列化为PmsAttrValue对象,再加入list数组,最后通过productId进行attrs的数据更新
SKU库存
后台在接到前端通过post将url为this.url.updateSku,参数为skus的请求发送的数据时,会挨个把skus的属性取出,将JSON反序列化为PmsSkuStore对象,再加入list数组,最后通过productId进行skus的数据更新
商品详情
后台在接到前端通过post将url为this.url.saveDetail,参数为detailHtml和id的请求发送的数据时,通过productId将detailHtml数据更新
⑤是否上架、是否热买的修改
根据前端传入的id和publishStatus值进行数据更新,更新完后返回给前端,前端重新调用查询方法即可刷新列表看到更新后的数据
20、新增JWT工具类
要想使用JWT第一步还是引入jar包,去maven中搜索JWT
找了个用的人最多的(也可以使用jjwt来进行token加密)
导包
在project2101.xml中加入下列代码
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
然后在pojo中新增一个JwtUtil类,将后端要返回的数据用这里的方法进行加密和解密,再返回给前端
21、新增login登陆子项目
接收到前端传来的post请求,url为/ums-user/login,参数为username,password
需要先通过username拿到匹配的数据,如果是登录名查不到就返回”登陆名不存在“,如果查到的user用户active=0那么就返回”用户已经失效,无法登陆“,然后就将拿到的加密密码和传入的明文密码用BCryptPasswordEncoder中的matches方法进行匹配,如果匹配失败就返回“密码错误”,
①单用JWT的方式实现登陆
用户账号密码校验成功后就将用户的登录名传入JWT进行加密(详情见20、新增JWT工具类),再传入map,最好后将这个map返回就好
然后前端每次请求都会在请求头带上JWT加密的token信息,我们在gateway中新增过滤器进行过滤拦截,详情见附14、token认证后端
②token+redis的方式实现登陆
首先要在docker中创建redis的docker容器(附15、dock安装redis)
导包
安装好后在后端引入redis的jar包(客户端依赖)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
配置
到naco中新建一个tx-redis.yml文件,配置redis需要的参数
测试以级使用,新建测试类,有意思的说测试这一环节我测了一下午,因为问题好像有亿点点多(附16、redis引入及测试问题),最后测试成功,可以看到通过opsForValue.set()方法是可以将key-value键值对放入redis的,我们也能通过opsForValue.get()方法传入key的值拿到value
我们需要在登陆成功验证成功后,将token数据存入redis数据库,并将其用JWT加密的token返回给前端
然后前端每次请求都会在请求头带上JWT加密的token信息,我们需要修改gateway中过滤器方法,加上redis判断校验,要判断存在redis中数据是否过时,如果没过时就和前端传入的token进行比较一样就放行,不通过就返回错误提示
附1、引包规则
父包对子包jar的版本号控制,在用了dependencyManagement后,父项目如果用dependencies去引包的话,子项目会跟着引,在父项目中使用版本号放在properties中,version中用${}来引入
附2、依赖排除
exclusion排除包中的某一项引入
附3、前后端交互流程
前端发送请求就接上面的"axios异步请求(显示数据)"
后台接收到请求命令,在Controller层中对比请求
在找到请求后,运行代码发现接着就是运行到了了service层的方法
在service层接口中找到对应方法,然后通过接口找到对应的实现类方法
在实现类方法中发现,最后是运行到了系统的page方法中完成了数据的增删改查
附4、SpringCloud的引入包必须和SpringBoot的引入包对应
图中2.3.x(Satarting with SR5)表示如果你springboot的版本是2.3以上那么你springcloud的版本就必须得是Hoxton的SR5以上
附5、SpringCloud组件三部曲
导包、配置、注解
附6、数据库新增字段处理方法以级不是数据库字段怎么办?
虽然我们的后台代码架构都是由generator构建出来的,但是在数据库的表中新增一个字段后也不需要这么麻烦
只需要在pojo实例中找到你表对应的类添加一个属性即可
如果有数据需要用到但是又不需要存入数据库该怎么办?就比如密码这种敏感的东西,如果存在数据库,别人拿到你的数据库就能拿到你所有用户的用户名账号和密码是很恐怖的事,所以这种字段我们有需要从前端接收到对它进行处理,又不希望它存入数据库中,那么就要在对应的实例类中的属性上加上@TableField(exist = false) 这个注解,表示当前字段与数据库中的字段没有关系
附7、加密密码的方法
就如之前说的一样,我们是不能直接将明文密码直接存入数据库,必须得加密把加密后的密码存入数据库
导包
<!--密码加密-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
配置
新建AdminConfig类,将刚刚导包的security下的BCryptPasswordEncoder加载的IOC容器中,然后就可以通过resource接口注解在其他地方使用了
注解
使用BCryptPasswordEncoder下的encode方法就可以将密码进行加密
附8、有关于AfterReturning通知中的JoinPoint异议与不解
通过代码可以知道我这里通过joinPoint.getArgs()[0];是获得了UmsUserController下add方法中的第一个参数
UmsUserController中add方法代码
然而这两个参数最后输出时明显不一样,通过joinPoint.getArgs()[0];获得的参数是已经在add方法中执行完,各种属性添加完之后的对象
附9、SpringCloud电商后台项目部署
我们需要对我们的项目进行打包,首先是对gateway进行打包,我们需要使用springboot的插件来进行打包
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!--配置 因为我们打包文件是<packaging>jar</packaging> jar文件-->
<!--jar文件是运行不了的,需要告诉它运行哪儿个类,要运行的是具有main方法的启动类-->
<configuration>
<mainClass>com.tangxin.AdminApp</mainClass>
</configuration>
<!--还要加上repackage,不然打包只会把gateway下面我们写的代码给打包了,
至于其他的依赖文件,配置文件就都没打包了-->
<executions>
<execution>
<goals>
<goal>
repackage
</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- 设置版本号 -->
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
插件引入好后就可以进行打包操作了,要先clean再package
打好的包可以通过右键target文件,然后选择show Explorer找到(也就是打开target所在的文件夹而已)
然后我们在Linux上新建个app文件夹
将刚刚打包好的gateway-1.0-SNAPSHOT通过Xshell传到Linux中的app文件中
然后我们要在Linux上构建一个有JDK的容器,因为我们上传的jar包,只要我们的容器中能运行jar包就好,如果是web就要构建有tomcat的容器
docker search jdk1.8 搜索一下jdk1.8的镜像文件
docker pull majiajue/jdk1.8 这是下载JDK1.8
然后将docker容器运行起来
docker run --name tx-gateway -p 8082:8082 -v /app/gateway-1.0-SNAPSHOT.jar:/app/gateway-1.0-SNAPSHOT.jar -d majiajue/jdk1.8 java -jar /app/gateway-1.0-SNAPSHOT.jar
//--name 自己取的名字
//-p 端口号要和你原来项目配置文件中的端口号对上
//-v 映射目录或者映射文件,将Linux主机中app下的gateway-1.0-SNAPSHOT映射到docker容器的app下
//-d 后台运行,后面可以跟上镜像运行起来后你要执行的命令
//java -jar /app/gateway-1.0-SNAPSHOT.jar 运行的是docker容器中app下的gateway-1.0-SNAPSHOT.jar文件
运行完后可以通过docker ps查看它的运行状态
通过docker logs tx-gateway拿到它的运行日志
确认在Linux上运行起来后,可以到nacos的服务列表中看到自己运行起来的服务(因为本机也有运行gateway所以有两个实例,上面那个是本机的,下面那个是Linux上运行的)
然后再通过上述的方法同样打包product和admin,就可以在nacos的服务列表中看到三个是Linux上运行的服务了
这时候可以尝试通过浏览器去访问这些服务,只需要修改前端访问后台的地址即可
正常显示表示部署成功
附10、打包出错解决方法
如果在打包过程中出现pojo找不到符号等等小问题,很可能就是因为当前项目依赖的类似于pojo这样的内容没有打包导致的
我们只需要将总的项目进行一次打包即可,需要先clean再进行package打包操作
附11、session原理
session是一种保持会话的机制,它的目的就是我们在一个页面登陆后它下一次还知道是我们在操作,因为http协议是一种无状态协议,你浏览器往服务器发请求,发了两次,即使是连着发的,服务器也不知道二次请求都是你发的。
我们来举个cookie的例子,比如咖啡厅的服务员是一个极度脸盲的人,顾客他都是记不住对方点了什么的,但是好在政府规定大家出门必须带手机(发请求一定要用服务器一样),那么顾客只需要对服务员出示自己的手机点了什么就好了,因为店里有一个寄杯服务就是在你买完一个咖啡后,下次再来可以不花钱再喝一杯,所以你每次买完咖啡服务员都会在你的手机上记录咖啡的信息+1,结果月底一算账,卖了10w杯,送出去了15w杯,很明显是有人自己修改了自己手机上的咖啡信息。这个存在自己手机上的咖啡信息就好比浏览器的cookie,可以由自己随意更改,而且别人拿到手机也能进行更改就很不好。所以这说明了一个道理cookie可以篡改和伪造非常不安全。
session是什么呢?就好比咖啡店经过一个月亏损后吸取了教训,在咖啡厅中加了一个账本,记录00001 张三 拿铁 剩余1,然后只往顾客的手机上记录编号00001这样,那么session就是咖啡厅手中拿到账本代表全局是存在服务器上的,手机上记录的00001就是cookie只记录个编号存在浏览器上。但是问题又来了顾客掌握了规律把00001改成了00003,给服务员看,服务员就会把00003的东西给他,所以这样也不行,顾客的手机上应该保存加密的数据并且最好有签让用户不容易伪造和篡改。
在session还有个问题就是不支持集群,什么意思呢?就比如咖啡厅开了个分店,那么原来咖啡厅的账本上记录的续杯信息,新店显然是没办法通用的,所以也就是说httpsession是不支持集群的,换了服务器就不行那么肯定不适合分布式项目,为了解决这个问题有两个方案(没有最好的方案,只有最适合的),第一个是token+redis缓存服务器,将账本也就是session信息直接存入redis中(那redis缓存当作一个服务器去用),然后咖啡厅(服务器)往redis中统一拿数据;第二个是JWT,JWT就是之前说的我们把存入顾客手机上的信息进行加密(把放入session中的数据进行加密,然后把加密的字符串放到Session Storage中(因为Session Storage不会被修改),每次请求会把这个加密的字符串带上,然后服务器再进行解析,解析成我们想要的就好)
附12、JWT和token+redis缓存服务器的优缺点
JWT的优缺点,优点:是减少成本他不依赖于其他服务器,他就是一个字符串的加密解密;缺点:这个JWT是存在客户端浏览器上的,一旦发生泄露,你也就只能看着它以别人的身份进来也无能为力
token+redis缓存服务器的优缺点,优点:如果出现安全问题他可以通过后端来控制,毕竟信息都存在后端了;缺点:增加了一个服务器,增大了成本
附13、SSDB和Redis的优缺点
SSDB的优缺点,优点:SSDB是将数据存在硬盘上的,意味着他更节约成本(同存储大小下硬盘比内存便宜40倍),还有个优点是SSDB支持redis的客户端访问,也就是说redis在使用时引入什么包,SSDB也就引入什么包就可以了(他们的底层内核是一样的);缺点:也就是应为它是硬盘存储的,所以他的性能相比Redis肯定下降了一些(10%左右),但是这和数据库比起来依旧是快了很多
Redis的优缺点,优点:redis是内存型服务器,也就代表着redis的性能特别好,读写特别快;缺点:也就是因为redis的性能太好了,只要有点功能就想往这儿上面靠,然后就导致存储的数据增多,然而redis的数据又是存储在内存上的也就导致内存越来越大,中小型的企业就得更换更大的云服务器,变相的增加了成本,当然大型企业随意还是redis香
附14、token认证后端
前端只能判断的存在全局域中的token有没有,为了验证每次请求时都会带上的token的合法性,就只能到后端的gateway(毕竟无论要转发到哪儿个微服务都要先经过gateway)再新建一个过滤器进行认证了
新建GatewayFilter
过滤器方法
如果token是正确的会直接通过过滤器,只有不正确的要对外输出错误信息,将对外输出做成了一个writerMessage方法,传入参数后返回给前端
对外输出方法
过滤器排序方法
附15、dock安装redis
拉取redis镜像,不选择版本号,默认拉取最新版本的redis
docker pull redis
创建实例并启动
docker run -di --name tx-redis -p 6379:6379 redis
进入容器
docker exec -it tx-redis /bin/bash
连接redis服务端,并进行测试
redis-cli
附16、redis引入及测试问题
①错误1 RedisConnectionFailureException
首先是redis的配置文件在配置时要记得在,redis上层再加个spring层,因为忘记了导致我被折磨了半个多小时
如果不加的话,错误信息
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379
②错误2 NullPointerException
在测试时会出现NullPointerException错误,原因就是stringRedisTemplate报空指针错误,原因是注入的时候stringRedisTemplate就是null
查了一下后面发现是容器没有启动,单纯的测试是无法注入的,要在测试类上加上
@RunWith(SpringRunner.class)
@SpringBootTest
然后再启动就好了
③错误3 BeanNotOfRequiredTypeException
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'redisTemplate' is expected to be of type 'org.springframework.data.redis.core.StringRedisTemplate' but was actually of type 'org.springframework.data.redis.core.RedisTemplate'
这段意思大概就是,没有找到对应的bean,注入依赖资源项失败
BeanNotOfRequiredTypeException:名为“redisTemplate”的Bean应为“org.springframework.data.redis.core.StringRedisTemplate”类型,但实际为“org.springframework.data.redis.core.redisTemplate”类型。这个就很奇怪,我并没有用到redisTemplate的依赖,也不可能注入出错
后面才发现是这个Bean的名称问题,将名称改成stringRedisTemplate就可以了,原因是@Resource是默认取字段名进行按照名称注入,具体可以看@Autowired与@Resource区别