Spring Cloud基础介绍
- 微服务的基本概念、设计与拆分原则
- 微服务和Spring Cloud的关系
- 微服务常见的组件和功能
- 课程查询案例基本介绍、系统架构设计和接口设计
- 分模块构建Spring Cloud项目
- 完成课程列表、课程价格服务开发
- 课程服务整合,服务注册与发现
- 整合Feign实现服务间调用
- 网关的集成与开发,并接入服务
- 引入服务的熔断与降级,并进行实操演练
微服务基础
- 什么是微服务?
- 微服务的特点
- 微服务优缺点
- 微服务的两大门派 [Spring Cloud 和 Dubbo]
- 微服务拆分
- 微服务扩展
- 微服务重要模块
微服务热度
单体应用的痛点
什么是服务化
- 把传统的单机应用中的本地方法调用,改造成通过RPC、HTTP产生的远程方法调用
- 把模块从单体应用中拆分出来,独立成一个服务部署
什么是微服务
一系列、一部分
是一种架构风格
开发单体应用作为一系列小型服务的套件,其中每个服务都运行再自己的进程中,并且通过轻量级的机制实现彼此间的通信,这通常是HTTP资源API
这些服务是围绕着业务功能构建的,并且可以通过完全自动化的部署机制进行独立部署
这些服务的集中式管理做到了最小化(例如docker相关技术),每一种服务都可以通过不同的编程语言进行编写,并且可以使用不同的数据存储技术
微服务的特点
微服务优缺点
- 服务简单、便于学习和上手,相对易于维护
- 独立部署,灵活扩展
- 技术栈丰富
微服务缺点
- 运维成本过高 [磁盘满 CPU高]
- 接口可能不匹配
- 代码可能重复 [要非常明确的定义每个API]
- 架构复杂度提高
微服务两大门派
- Spring Cloud:众多子项目【最大供应者:Netflix内容付费知识】
- dubbo:高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
- dubbo提供的能力只是SpringCloud的一部分子集
[Dubbo虽然有很多不提供的组件 但是可以和其他供应商合作完成提供任务]
核心组件 | Dubbo | Spring Cloud |
---|---|---|
服务注册中心 | Zookeeper | Spring Cloud Netflix Eureka |
服务调用方式 | RPC | REST API |
服务网关 | 无 | Spring Cloud Netflix Zuul |
断路器 | 不完善 | Spring Cloud Netflix Hystrix |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总线 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
通信协议对比
- RPC vs REST
- 服务提供方与调用方接口依赖方式太强
- 服务对平台敏感,难以简单复用
文档质量对比
- Dubbo的文档可以说再国内开源框架中算是一流的,提供了中文与英文两种版本
- Spring Cloud文档体量大,更多的是偏向整合,更深的使用方法还是需要查看整合组件的详细文档
两大门派选型建议
- Dubbo => 组装电脑 【中文文档多】
- Spring Cloud => 品牌机【稳定可靠】
微服务拆分
什么时候进行服务化拆分
- 第一阶段的主要目标是快速开发和验证想法
- 进一步增加更多的新特性来吸引更多的目标用户
- 同时进行开发的人员超过10人,这个时候就该考虑到服务化拆分了
不适合拆分的情况
- 小团队,技术基础较薄弱
- 流量不高,压力小,业务变化也不大
- 对延迟很敏感的低延迟高并发系统
服务化拆分的两种方式
- 纵向拆分[上方区别图]
- 横向拆分
- 结合业务综合分析
服务扩展
维度
自动按需扩展
- 根据CPU负载程度、特定时间(比如周末)、消息中间件的队列长度、业务具体规则、预测等来决定是否扩展
- 自动分配一个新的服务实例,提高可用性
- 提高了可伸缩性(双11之后,自动减少服务器)
- 具有最佳使用率,节约成本
微服务重要模块
- 服务描述
- 注册中心
- 服务框架
- 负载均衡
- 熔断和降级
- 网关
Spring Cloud课程查询
- Spring Cloud简介
- 项目整体设计
- 课程列表模块开发
- 课程价格模块开发 [模块间互相调用]
- 服务注册与发现Eureka
- 服务间调用Feign
- 负载均衡Ribbon
- 熔断器Hystrix [兜底界面 默认返回]
- 网关Zuul
- 整体测试
Spring Cloud简介
- 成熟的微服务框架,定位为开发人员提供工具,以快速构建分布式系统
核心组件
核心组件 | Spring Cloud |
---|---|
服务注册中心 | Spring Cloud Netflix Eureka |
服务调用方式 | REST API、Feign、Ribbon |
服务网关 | Spring Cloud Netflix Zuul |
熔断器 | Spring Cloud Netflix Hystrix |
项目整体设计
系统数据流向
【课程列表数据】→ 【课程列表服务】
↓
【课程价格数据】→ 【课程价格服务】→ 【整体列表+价格】
新建多模块项目
创建一个 spring-cloud-course-pracice 并删除src文件夹
新建New → module → Maven → course-service → 删除src
新建New → module → New Module → Maven → course-list → Parent:course-service
新建New → module → New Module → Maven → course-price → Parent:course-service
D:\Java+4399\阶段5:Java分布与微服务实战\第32周 Spring Cloud基础\第2节 Spring Cloud开发课程查询功能\辅助材料\SpringCloud课程查询源码【优质it资源微信it-wangke18】.zip\SpringCloud课程查询源码\课程价格模块开发后\spring-cloud-course-practice
course-service.course-list
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>course-service</artifactId>
<groupId>com.imooc</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>course-list</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
com/imooc/course/controller/CourseListController.java
package com.imooc.course.controller;
import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* CourseListController课程列表Controller
*/
@RestController
public class CourseListController {
//提供课程列表服务
@Autowired //引入课程服务
CourseListService courseListService;
@GetMapping("/courses")
public List<Course> courseList() {
//return → service
return courseListService.getCourseList();
}
}
com/imooc/course/service/CourseListService.java
package com.imooc.course.service;
import com.imooc.course.entity.Course;
import java.util.List;
/**
* 课程列表服务
*/
public interface CourseListService {
List<Course> getCourseList();
}
com/imooc/course/service/impl/CourseListServiceImpl.java
package com.imooc.course.service.impl;
import com.imooc.course.dao.CourseMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 课程服务实现类
*/
@Service
public class CourseListServiceImpl implements CourseListService {
@Autowired
CourseMapper courseMapper;
@Override
public List<Course> getCourseList() {
return courseMapper.findValidCourses();
}
}
com/imooc/course/entity/Course.java
package com.imooc.course.entity;
import java.io.Serializable;
/**
* Course的实体类
*/
public class Course implements Serializable {
Integer id;
Integer courseId;
String courseName;
Integer valid;
@Override
public String toString() {
return "Course{" +
"id=" + id +
", courseId=" + courseId +
", courseName='" + courseName + '\'' +
", valid=" + valid +
'}';
}
Getter+Setter
}
com/imooc/course/dao/CourseMapper.java
package com.imooc.course.dao;
import com.imooc.course.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 描述课程的Mapper类
*/
@Mapper
@Repository
public interface CourseMapper {
@Select("SELECT * FROM course WHERE valid = 1")
List<Course> findValidCourses();
}
application.properties
server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity的实体类与数据库名进行驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-list
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CoursePriceApplication {
public static void main(String[] args) {
SpringApplication.run(CoursePriceApplication.class, args);
}
}
=====================================================================
数据库名:course_practice
表名:course
字段:id[自增] course_id course_name valid
课程列表模块开发-总结 [注意点]
- 多模块开发
- 实体类实现Serializable接口、set方法
- MyBatis的驼峰配置
course-list.course-price
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>course-service</artifactId>
<groupId>com.imooc</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>course-price</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 课程价格控制器
*/
@RestController
public class CoursePriceController {
@Autowired
CoursePriceService coursePriceService;
@GetMapping("/price")
public Integer getCoursePrice(Integer courseId){
CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
return coursePrice.getPrice();
}
}
com/imooc/course/service/CoursePriceService.java
package com.imooc.course.service;
import com.imooc.course.entity.CoursePrice;
import java.util.List;
/**
* 课程价格服务
*/
public interface CoursePriceService {
CoursePrice getCoursePrice(Integer courseId);
}
com/imooc/course/service/impl/CoursepriceServiceImpl.java
package com.imooc.course.service.impl;
import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 课程价格的服务实现类
*/
@Service
public class CoursepriceServiceImpl implements CoursePriceService {
@Autowired
CoursePriceMapper coursePriceMapper;
@Override
public CoursePrice getCoursePrice(Integer courseId) {
return coursePriceMapper.findCoursePrice(courseId);
}
}
com/imooc/course/entity/CoursePrice.java
package com.imooc.course.entity;
import java.io.Serializable;
/**
* CoursePrice的实体类
*/
public class CoursePrice implements Serializable {
Integer id;
Integer courseId;
Integer price;
@Override
public String toString() {
return "CoursePrice{" +
"id=" + id +
", courseId=" + courseId +
", price=" + price +
'}';
}
Getter+Setter
}
com/imooc/course/dao/CoursePriceMapper.java
package com.imooc.course.dao;
import com.imooc.course.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
/**
* 课程价格Mapper类
*/
@Mapper
@Repository
public interface CoursePriceMapper {
@Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
CoursePrice findCoursePrice(Integer courseId);
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CoursePriceApplication {
public static void main(String[] args) {
SpringApplication.run(CoursePriceApplication.class, args);
}
}
==========================================================
http://127.0.0.1:8082/price?courseId=409
数据库名:course_practice
表名:course_price
字段:id[自增] course_id price
Eureka的作用和架构
Eureka
- 用于定位服务,直接找到组件中的各个服务地址
- 114[各种服务的提供者]、物业[维护各个住户的信息(注册中心)]
为什么需要服务注册与发现 [移除不影响 但会有很多麻烦]
- IP变化
- 难以维护
- 改进
- 节点变化[服务的提供者和消费者] [消费者需要调用提供者的API来获得服务] 若提供者修改了ip 此时应该上传到注册中心,消费者此时需要得到IP和API 无需直接找提供者 直接去注册中心调用
Eureka架构
- EureKa Server 和 EureKa Client
- 集群 [只要能获得一个Eureka Server 就能获得整个信息]
引入Eureka
pom.xml(eureka-server)
<?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>spring-cloud-course-practice</artifactId>
<groupId>com.imooc</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>course-eureka-server</name>
<description>Spring Cloud Eureka</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
pom.xml(Spring-cloud-course-practice)
<?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>
<packaging>pom</packaging>
<modules>
<module>course-service</module>
<module>eureka-server</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.imooc</groupId>
<artifactId>spring-cloud-course-practice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-course-practice</name>
<description>course project for Spring Cloud</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 表示Spring Cloud的版本-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties(eureka-server)
spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#fetch-registry???????????????????
eureka.client.fetch-registry=false
#register-with-eureka??????????Eureka Server????true?
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
application.properties(course-list)
server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity?????????????????
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-list
#??????? eureka-server??application.properties?defaultZone????????????????
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
application.properties(course-price)
server.port=8082
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-price
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/EurekaServerApplication.java
package com.imooc.course;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* Eureka的服务端
*/
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
========================================================
http://127.0.0.1:8000/
利用Feign实现服务间调用
Feign
- 声明式、模板化的HTTP客户端,方便的调用远程的HTTP请求 [基于接口实现]
集成Feign
- 引入依赖
- 配置文件
- 注解
pom.xml (course-price)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
application.properties
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;
import com.imooc.course.client.CourseListClient;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import java.util.List;
import javax.xml.ws.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 描述: 课程价格控制器
*/
@RestController
public class CoursePriceController {
@Autowired
CoursePriceService coursePriceService;
@Autowired
CourseListClient courseListClient;
@GetMapping("/price")
public Integer getCoursePrice(Integer courseId) {
CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
return coursePrice.getPrice();
}
@GetMapping("/coursesInPrice")
public List<Course> getCourseListInPrice(Integer courseId) {
List<Course> courses = courseListClient.courseList();
return courses;
}
}
com/imooc/course/client/CourseListClient.java[接口]
package com.imooc.course.client;
import com.imooc.course.entity.Course;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 描述: 课程列表的Feign客户端
*/
@FeignClient("course-list")
public interface CourseListClient {
@GetMapping("/courses")
List<Course> courseList();
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class CoursePriceApplication {
public static void main(String[] args) {
SpringApplication.run(CoursePriceApplication.class, args);
}
}
=============================================================
http://127.0.0.1:8082/coursesInPrice
[
{
"id": 1,
"courseId": 362,
"courseName": "SpringCloud自学",
"valid": 1
},
{
"id": 2,
"courseId": 409,
"courseName": "玩转Java并发工具",
"valid": 1
}
]
负载均衡的两种类型
- 客户端负载均衡(Ribbon)
- 服务端负载均衡(Nginx)
负载均衡策略
- RandomRule 表示随机策略
- RoundRobinRule 表示轮询策略
- ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
配置不同的负载均衡方式
- Ribbon.NFLoadBalancerRuleClassName
application.properties(course-price)
course-list.ribbon.NFLoadBanlancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
为什么要断路器
Hystrix
pom.xml(course-price)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.properties(course-price)
feign.hystrix.enabled=true
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class CoursePriceApplication {
public static void main(String[] args) {
SpringApplication.run(CoursePriceApplication.class, args);
}
}
================================================================
所有服务都打开时 http://127.0.0.1:8082/coursesInPrice
[
{
"id": 1,
"courseId": 362,
"courseName": "SpringCloud白学",
"valid": 1
},
{
"id": 2,
"courseId": 409,
"courseName": "玩转Java并发工具",
"valid": 1
}
]
当把CouresListApplication服务关闭的时候
[
{
"id": 1,
"courseId": 1,
"courseName": "默认课程",
"valid": 1
}
]
整合两个服务
com/imooc/course/controller/CoursePriceController.java
@GetMapping("/coursesAndPrice")
public List<CourseAndPrice> getCoursesAndPrice(){
List<CourseAndPrice> courseAndPrices = coursePriceService.getCourseAndPrice();
return courseAndPrices;
}
=====================================================
http://127.0.0.1:8082/coursesAndPrice
[
{
"id": 1,
"courseId": 362,
"name": "SpringCloud自学",
"price": 348
},
{
"id": 2,
"courseId": 409,
"name": "玩转Java并发工具",
"price": 399
}
]
com/imooc/course/service/CoursePriceService.java
public interface CoursePriceService {
CoursePrice getCoursePrice(Integer courseId);
List<CourseAndPrice> getCourseAndPrice();
}
com/imooc/course/service/impl/CoursePriceServiceImpl.java
package com.imooc.course.service.impl;
import com.imooc.course.client.CourseListClient;
import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CourseAndPrice;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 描述: 课程价格的服务实现类
*/
@Service
public class CoursePriceServiceImpl implements CoursePriceService {
@Autowired
CoursePriceMapper coursePriceMapper;
@Autowired
CourseListClient courseListClient;
@Override
public CoursePrice getCoursePrice(Integer courseId) {
return coursePriceMapper.findCoursePrice(courseId);
}
@Override
public List<CourseAndPrice> getCourseAndPrice() {
List<CourseAndPrice> courseAndPrices = new ArrayList<>();
List<Course> courses = courseListClient.courseList();
for (int i = 0; i < courses.size(); i++) {
Course course = courses.get(i);
//inn
if (course != null) {
CoursePrice coursePrice = getCoursePrice(course.getCourseId());
CourseAndPrice courseAndPrice = new CourseAndPrice();
courseAndPrice.setPrice(coursePrice.getPrice());
courseAndPrice.setName(course.getCourseName());
courseAndPrice.setId(coursePrice.getId());
courseAndPrice.setCourseId(course.getCourseId());
courseAndPrices.add(courseAndPrice);
}
}
return courseAndPrices;
}
}
com/imooc/course/entity/CourseAndPrice.java
package com.imooc.course.entity;
/**
* 课程与价格的融合类
*/
public class CourseAndPrice {
Integer id;
Integer courseId;
String name;
Integer price;
}
网关Zuul
- 为什么需要网关
- 签名校验、登录校验冗余问题
- Spring Cloud Zuul 与 Spring Cloud
- API网关允许您将API请求(内部或外部)路由到正确的位置
集成Zuul [统一修改访问url地址]
- 把自己注册到Eureka这个注册中心
- 引入依赖
- 配置路由地址
pom.xml(course-zuul)
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
com/imooc/course/ZuulGatewayApplication.java
package com.imooc.course;
import org.springframework.boot.SpringApplication;
/**
* 网关启动类
*/
@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class, args);
}
}
===============================================================
http://127.0.0.1:9000/imooc/price/coursesInPrice
[
{
"id": 1,
"courseId": 362,
"courseName": "SpringCloud自学",
"valid": 1
},
{
"id": 2,
"courseId": 409,
"courseName": "玩转Java并发工具",
"valid": 1
}
]
application.properties
spring.application.name=course-gateway
server.port=9000
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
zuul.prefix=/imooc
zuul.routes.course-list.path=/list/**
zuul.routes.course-list.service-id=course-list
zuul.routes.course-price.path=/price/**
zuul.routes.course-price.service-id=course-price
利用网关实现过滤器
- pre 过滤器在路由请求之前运行
- route 过滤器可以处理请求的实际路由
- post 路由请求后运行过滤器
- error 如果在处理请求的过程中发生错误,则过滤器将运行
com/imooc/course/filter/PreRequestFilter.java
package com.imooc.course.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/**
* 记录请求时间
*/
@Component
public class PreRequestFilter extends ZuulFilter {
@Override
public String filterType() {
//过滤器的类型
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
//是否启用过滤器
return true;
}
@Override
public Object run() throws ZuulException {
//通过时间戳 获取上下文
RequestContext currentContext = RequestContext.getCurrentContext();
currentContext.set("startTime",System.currentTimeMillis());
System.out.println("过滤器已经记录时间");
return null;
}
}
com/imooc/course/filter/PostRequestFilter.java
package com.imooc.course.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/**
* 请求处理后的过滤器
*/
@Component //有了才能被spring捕捉到
public class PostRequestFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() { //在其之前-1 先运行
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
Long startTime = (Long) currentContext.get("startTime");
long duration = System.currentTimeMillis() - startTime;
String requestURI = currentContext.getRequest().getRequestURI();
System.out.println("uri:" + requestURI + ",处理时长:" + duration);
return null;
}
}
Spring Cloud电商实践
- 服务拆分过程分析和经验分享
- 完成用户、商品、购物车和订单等服务开发
- 通用common模块的拆分和应用
- Eureka server注册中心开发,用Feign完成服务之间的调用
- 共享Session的处理方案
- 统一网关的集成与开发
Spring Cloud电商项目
- 项目介绍
- 在Spring Boot的基础上升级为Spring Cloud
- 从0到1 (Spring Boot)
- 从1到多、微服务
- 模块拆分
- Eureka-server开发
- 用户模块开发
- 公共模块开发
- 网关模块开发
- 商品分类和商品模块开发
- 购物车和订单模块开发
- 总结
模块拆分
粒度:过粗、适中、过细
人员的角度
业务的角度:相关、独立
Eureka-server模块
网关模块
公共模块 (md5、统一API返回、常量、异常、异常枚举、二维码、工具类模块……)
用户模块 (相对来说比较独立,登录的时候无需获得商品信息)
商品分类和商品模块
购物车和订单模块
功能模块介绍
项目功能:
前台 {用户、商品分类、商品信息、购物车、订单}
- 用户模块{注册、登录、更新签名、身份认证、登出}
- 商品分类模块{多级目录、递归查询、缓存}
- 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
- 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
- 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
后台 {用户、商品分类、商品信息、订单}
- 管理员模块{登录登出、身份认证、安全限制}
- 商品分类模块{分类列表、增加分类、修改分类、删除分类}
- 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
- 订单模块{订单列表、地址信息、发货、订单完结}
``
项目初始化
Eureka-server模块开发
- 引入依赖
- 配置文件
- 启动注解
由于是多模块开发,创建了maven项目后把cloud-mall-practice的src删除
pom.xml(cloud-mall-practice)
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.imooc</groupId>
<artifactId>cloud-mall-practice</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>cloud-mall-eureka-server</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
<configuration>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
</plugin>
</plugins>
</build>
</project>
cloud-mall-eureka-server/com/imooc/cloud/mall/practice/eureka/EurekaServerApplication.java
package com.imooc.cloud.mall.practice.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* 1. Eureka Server的启动类,提供服务注册与发现
* 2写其pom和resources的配置文件
*
*/
@EnableEurekaServer //对外提供服务
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
pom.xml(cloud-mall-eureka-server)
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.imooc</groupId>
<artifactId>cloud-mall-practice</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-mall-eureka-server</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<name>
cloud-mall-eureka-server
</name>
<description>Spring cloud Eureka Server</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#是否同步其他节点的信息
eureka.client.fetch-registry=false
#是否把自己作为服务注册在服务上
eureka.client.register-with-eureka=false
#eureka-server所在的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
用户模块知识点
登录、注册[加盐md5]、重名校验[注册就不可注册]、密码加密存储、Session的使用、越权校验[不可编辑别人的签名]
用户模块
- 表设计
- 开发
- 测试
idea中如何将包名折叠或者或如何将折叠的包名展开_idea包名折叠-CSDN博客
用户模块初始化
公共模块
- 常量、异常、工具类
- 自身不是Spring Boot项目
进行模块各层级的重构 + 用户模块的测试
一定要记得如果引用其他muder的时候 要在xml中假如其项目文件的依赖才可以跨项目引用
比如我这个项目是【一定要引用噢!! 不然在非其项目的时候找不到import导包】
pom.xml(cloud-mall-zuul)
<dependency>
<groupId>com.imooc</groupId>
<artifactId>cloud-mall-user</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
网关模块开发
pom.xml(cloud-mall-zuul)
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.imooc</groupId>
<artifactId>cloud-mall-practice</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-mall-zuul</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties(cloud-mall-zuul)
server.port=8083
spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath*:mappers/*.xml
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
spring.application.name=cloud-mall-zuul
zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
com/imooc/cloud/mall/practice/zuul/filter/UserFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;
import com.imooc.cloud.mall.practice.common.common.Constant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 用户鉴权过滤器
*/
@Component
public class UserFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = request.getRequestURI();
//不经过过滤器
if (requestURI.contains("images") || requestURI.contains("pay")) {
return false;
}
if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
return true;
}
return false;
}
@Override
//用户过滤器
public Object run() throws ZuulException {//返回true的时候执行的
RequestContext currentContext = RequestContext.getCurrentContext();//获取request
//获取session 因为user对象保存在里面 session.getAttribute取出session对象
HttpServletRequest request = currentContext.getRequest();
HttpSession session = request.getSession();
//拿出User对象
User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
if (currentUser == null) {
//无需通过网关再去发送
currentContext.setSendZuulResponse(false);
//返回给前端的对象
currentContext.setResponseBody("{\n"
+ " \"status\": 10007,\n"
+ " \"msg\": \"NEED_LOGIN\",\n"
+ " \"data\": null\n"
+ "}");
currentContext.setResponseStatusCode(200);
}
return null;
}
}
com/imooc/cloud/mall/practice/zuul/filter/AdminFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;
import com.imooc.cloud.mall.practice.common.common.Constant;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import com.imooc.cloud.mall.practice.zuul.feign.UserFeignClient;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 管理员鉴权过滤器
*/
@Component
public class AdminFilter extends ZuulFilter {
@Autowired
UserFeignClient userFeignClient;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = request.getRequestURI();
//不经过过滤器
if (requestURI.contains("adminLogin")){
return false;
}
if (requestURI.contains("admin")){
return true;
}
if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
return true;
}
return false;
}
@Override
//用户过滤器
public Object run() throws ZuulException {//返回true的时候执行的
RequestContext currentContext = RequestContext.getCurrentContext();//获取request
//获取session 因为user对象保存在里面 session.getAttribute取出session对象
HttpServletRequest request = currentContext.getRequest();
HttpSession session = request.getSession();
//拿出User对象
User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
if (currentUser == null) {
//无需通过网关再去发送
currentContext.setSendZuulResponse(false);
//返回给前端的对象
currentContext.setResponseBody("{\n"
+ " \"status\": 10010,\n"
+ " \"msg\": \"NEED_LOGIN\",\n"
+ " \"data\": null\n"
+ "}");
currentContext.setResponseStatusCode(200);
return null; //程序可以停止了
}
//进一步判断是否是管理员
Boolean adminRole = userFeignClient.checkAdminRole(currentUser);
if (!adminRole){
currentContext.setSendZuulResponse(false);
//返回给前端的对象
currentContext.setResponseBody("{\n"
+ " \"status\": 10011,\n"
+ " \"msg\": \"NEED_ADMIN\",\n"
+ " \"data\": null\n"
+ "}");
currentContext.setResponseStatusCode(200);
}
return null;
}
}
com/imooc/cloud/mall/practice/zuul/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.zuul.feign;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* UserFeignClient
*/
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
@PostMapping("/checkAdminRole")
public Boolean checkAdminRole(@RequestBody User user);
}
com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
网关模块加上SpringBoot启动类
package com.imooc.cloud.mall.practice.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 网关启动类
*/
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class, args);
}
}
=================================================================
127.0.0.1:8083/user/login?userName=mumu9&password=lihao123
{
"status": 10000,
"msg": "SUCCESS",
"data": {
"id": 21,
"username": "mumu9",
"password": null,
"personalizedSignature": "乘风破浪的姐姐",
"role": 2,
"createTime": "2024-04-17T18:08:15.000+0000",
"updateTime": "2024-04-17T18:10:49.000+0000"
}
}
POSTMAN中cloud-mall-practice的注册新用户、用户登录、管理员登录、登出模块都通过网关共用地址的调用,但是更新个性签名不可以,提示需要登录,因为更新签名需要提前登录。但是问题的最主要的点就是没有拿到session所以并没有把用户信息传到个性签名中 这是就需要Session共享机制
Session共享机制
登录功能分析
- 登录状态需要保持
- session的实现方案:登陆后,会保存用户信息到session
- 之后的访问,先从session中获取用户信息,然后再执行业务逻辑
目前遇到的障碍 [记得去电脑端启动Redis]
- session被网关过滤
- 共享session [现在是多模块项目 其他项目保存了session 另一个项目无法获得]
- EnableRedisHttpSession[需要通过中介Redis去调取]
在cloud-mall-user/application.properties中
server.port=8081
spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath*:mappers/*.xml
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
spring.application.name=cloud-mall-user
spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
在cloud-mall-zuul/application.properties中
server.port=8083
spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath*:mappers/*.xml
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
spring.application.name=cloud-mall-zuul
spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
#没有敏感的需要过滤
zuul.sensitive-headers=
zuul.host.connect-timeout-millis=15000
zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
之后在UserApplication和ZuulGatewayApplication前面加上@EnableRedisHttpSession
com/imooc/cloud/mall/practice/user/UserApplication.java
package com.imooc.cloud.mall.practice.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* 启动类
*/
@SpringBootApplication
@EnableSwagger2
@MapperScan(basePackages = "com.imooc.cloud.mall.practice.user.model.dao")
@EnableRedisHttpSession
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
-------------------------------------------------------------------------------------
com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
package com.imooc.cloud.mall.practice.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* 网关启动类
*/
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
@EnableRedisHttpSession
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class, args);
}
}
商品分类与商品模块初始化
什么是商品分类
- 条理清楚,层次分明
- 方便用户进行筛选和辨别
- 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
- 在商品分类上需要继续做归类操作
- 分类设置成三级
- 层级太深的弊端:
- 一是对用户不太友好,不利于寻找
- 二是对于后台管理人员不友好,不方便管理
[cloud-mall-category-product]创造出来,将controller,model(dao,pojo),service,impi 里转入 Category来搞 因为在网关项目里写了关于管理员校验的方法 所以在此次处CategoryController把session里的校验删除
要让商品和商品目录用到User类 一定要去pom文件里添加依赖 不然idea找不到
pom.xml(cloud-mall-category-product)
<dependency>
<groupId>com.imooc</groupId>
<artifactId>cloud-mall-user</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
com/imooc/cloud/mall/practice/categoryproduct/config/CachingConfig.java
创建处理缓存的配置类 configpackage com.imooc.cloud.mall.practice.categoryproduct.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import java.time.Duration;
*/**
* ** 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口
* CategoryVO implements Serializable
* **/
*@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter
.lockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
cacheConfiguration);
return redisCacheManager;
}
}
在网关中增加path和service-id
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
查端口 => netstat -ano | findstr :8083 [pid为7812]
杀死端口PID => taskkill /PID 7812 /f
商品模块
更新和新增商品
- 合并写法不可取
- 业务逻辑清晰、独立
批量上下架
- MyBatis遍历List
- where语句拼接
商品列表:搜索功能
入参判空 → 加%通配符 → like关键字
对于查询目录的in处理
- 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
- 这里要拿到某一个目录Id下的所有子目录id的List
前台:商品列表
- 排序功能
- Mybatis PageHelper
- 枚举:order by
把所有product的java都移动过去 controller/service/impl/modal.dao/modal.pojo/query.ProductListQuery/request.AddProductReq、ProductListReq、UpdateProductReq/resourcs.mappers.ProductMapper.xml
将Constant里的@Value("${file.upload.dir}")的文件上传目录重构到category-product项目中
然后在其项目中的resources的application.properties中写一下路径
文件地址映射:config/imoocMallWebMvcConfig
图片端口的特殊处理
application.properties(cloud-mall-category-product)
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/imooc-mall-prepare-static/
不能再通过获取getHost来配置了 根据实际情况去配置 没有办法获取到网关对外暴露的真正端口号
private URI getHost(URI uri){
URI effectiveURI;
try {
effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
} catch (URISyntaxException e) {
effectiveURI = null; //如果新建失败 就返回回去
}
return effectiveURI;
}
要改为com/imooc/cloud/mall/practice/categoryproduct/controller/ProductAdminController.java
/**
* 58.后台商品管理Controller pojo的product复制一份到request变成AddProductReq 59.需要ProductService.java
*/
@RestController
public class ProductAdminController {
@Autowired
ProductService productService;
@Value("${file.upload.ip}")
String ip;
@Value("${file.upload.port}")
Integer port;
@PostMapping("/admin/product/add")
public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq) {
productService.add(addProductReq);
return ApiRestResponse.success();
}
@PostMapping("/admin/upload/file")
public ApiRestResponse upload(HttpServletRequest httpServletRequest,
@RequestParam("file") MultipartFile file) {
String fileName = file.getOriginalFilename();
String suffixName = fileName.substring(fileName.lastIndexOf("."));
//生成文件名称UUID
UUID uuid = UUID.randomUUID();
String newFileName = uuid.toString() + suffixName;
//创建文件
File fileDirectory = new File(ProductConstant.FILE_UPLOAD_DIR);
File destFile = new File(ProductConstant.FILE_UPLOAD_DIR + newFileName);
if (!fileDirectory.exists()) {
if (!fileDirectory.mkdir()) {
throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
}
}
try {
file.transferTo(destFile);
} catch (IOException e) {
e.printStackTrace();
}
try {
return ApiRestResponse
.success(getHost(new URI(httpServletRequest.getRequestURL() + "")) + "/category-product/images/"
+ newFileName);
} catch (URISyntaxException e) {
return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
}
}
private URI getHost(URI uri) {
URI effectiveURI;
try {
effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), ip, port,
null, null, null);
} catch (URISyntaxException e) {
effectiveURI = null;
}
return effectiveURI;
}
阶段性重难点和常见错误
阶段总结
- 重难点:模块拆分设计、公共模块、Zuul(网关)过滤器、Session处理、Feign调用
Session在微服务的情况下就不容易处理到了可以共享Session,把其放在Redis中实现共享
Feign模块之间的接口调用[HTTP是手动调用] - 常见错误:模块粒度不合适、无公共模块、各接口独立校验、session无法共享、HTTP手动调用
模块拆分
- 粒度:过粗、适中、过细
- 人员的角度
- 业务的角度:相关、独立
购物车与订单模块
购物车模块
添加商品到购物车 → 商品是否在售、是否有库存
→[否] 提示用户
→[是] 该商品之前就在购物车里
- →[否] 添加新商品
- →[是] 原有基础上添加数量
创建一个module 加入关于cart的controller/model.dao.pojo.vo/service.impl
其中ProductMapper productMapper会爆红是理所应当的 这样证明耦合不是很严重
利用远程调用Feign进行调用
思路:通过调查发现productMapper只用于挑选ID
Product product = productMapper.selectByPrimaryKey(productId);
所以可以去重构一下代码 回到商品模块的地方cloud-mall-category-product
其中controller中的ProductController
//com/imooc/cloud/mall/practice/categoryproduct/controller/ProductController.java
//这个是服务与服务之间的内部调用 不需要层层包装 只需要返回就好
@GetMapping("product/detailForFeign")
public Product detailForFeign(@RequestParam Integer id){
Product product = productService.detail(id);
return product;
}
//千万不要直接引用另一个项目的mapper 因为耦合有点严重 只要对面发生变化 就完蛋了
//com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;
import com.imooc.cloud.mall.practice.categoryproduct.model.pojo.Product;
import org.springframework.web.bind.annotation.RequestParam;
public interface ProductFeignClient {
Product detailForFeign(@RequestParam Integer id);
}
用户模块提供获取当前用户接口
com/imooc/cloud/mall/practice/user/controller/UserController.java
@GetMapping("/getUser")
@ResponseBody
public User getUser(HttpSession session){
User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
return currentUser;
} /**
* 获取当前登录的User对象 为了避免暴露用户信息,可以再filter中对getUser进行拦截
* @param session
* @return
*/
@GetMapping("/getUser")
@ResponseBody
public User getUser(HttpSession session){
User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
return currentUser;
}
com/imooc/cloud/mall/practice/cartorder/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* UserFeign客户端
*/
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
/**
* 获取当前登录的user对象
*/
@GetMapping("/getUser")
User getUser();
}
修改CaetMapper.xml的路径名
com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;
import com.imooc.cloud.mall.practice.cartorder.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 描述: 商品FeignClient
*/
@FeignClient(value = "cloud-mall-category-product")
public interface ProductFeignClient {
//不经过网关 只是内部调用
@GetMapping("product/detailForFeign")
Product detailForFeign(@RequestParam Integer id);
@PostMapping("product/updateStock")
void updateStock(@RequestParam Integer productId, @RequestParam Integer stock);
}
将新增模块加入到网关[前缀地址] zuul
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
zuul.routes.cloud-mall-cart-order.path=/cart-order/**
zuul.routes.cloud-mall-cart-order.service-id=cloud-mall-cart-order
让Feign携带Session信息
错误新消息:500 Internal Server Error
在购物车中去调用User的getUser方法 是经过Feign
但是Feign的调用不经过网关 它是一个HTTP的调用
需要携带网关的Session信息
- FeignRequestInterceptor [对每一个发出的Feign进行拦截]
把网关的所有信息都复制到Feign的请求上 就不会遗漏相关信息
com/imooc/cloud/mall/practice/cartorder/filter/FeignRequestInterceptor.java
package com.imooc.cloud.mall.practice.cartorder.filter;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 描述: Feign请求拦截器
*/
@EnableFeignClients
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
//通过RequestContextHolder获取到请求 拿到requestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return;
}//类型转换 拿到Request
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
Enumeration<String> headerNames = request.getHeaderNames(); //拿到所有Header名字
if (headerNames != null) {
while (headerNames.hasMoreElements()) { //如果还有新的元素 先获取
String name = headerNames.nextElement(); //获取相关的值
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {//获取到当前元素
String value = values.nextElement();
requestTemplate.header(name, value);
}
}
}
}
}
订单模块
"订单编号的名字也要改 不能是以前的自增id 要变成order_no"
order_no是每一种商品的id
否则黑客早上下一单 晚上下一单 相减 就可以估计客流量
分成了很详细的字段名
product_id/name/img 都是之前购买过的订单产生的数据
生成订单–用户下单
- 入参
- 从购物车中查询已经勾选的商品
- 判断商品是否正在售卖中
- 判断库存,保证不超卖
- 调用商品服务扣库存 [及时更新库存]
- 删除购物车为中对应的商品
- 生成订单
- 订单号生成规则
- 循环保存每一个商品到order_item表
图片路径配置
com/imooc/cloud/mall/practice/cartorder/config/ImoocMallWebMvcConfig.java
package com.imooc.cloud.mall.practice.cartorder.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 描述: 地址映射配置
*/
@Configuration
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
@Value("${file.upload.dir}")
String FILE_UPLOAD_DIR;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**").addResourceLocations("file:" + FILE_UPLOAD_DIR);
}
购物车和订单模块-重难点
- 订单表[多个关联(订单编号+状态)]、订单状态设计
- 购物车流程
- 下单流程
- Feign调用的处理
购物车和订单模块-常见错误
- Feign调用取不到User对象
- URL错误拦截(图片url可以不拦截[订单+购物车])
- 路由配置错误(二维码/图片不显示)