# 基于SCN的原型开发 **Repository Path**: cllyl/prototype-scn ## Basic Information - **Project Name**: 基于SCN的原型开发 - **Description**: 基于Spring Cloud Netflix的第一代产品进行原型的开发 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-11-08 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 微服务架构下原型开发 ## 技术栈 #### 注册中心 Zookeeper:主从节点 Eureak:对等节点 Nacos:主从节点 #### 远程调用 Feign、Dubbo #### 网关 GateWay #### 熔断器 Hystrix、Sentinel #### 负载均衡 Ribbon、Sentinel #### 流量监控 Sentinel #### 限流 GateWay、Sentinel #### 配置中心 Config + Bus、Nacos #### 三方认证 OAuth、JWT #### 消息队列 Stream #### 链路追踪 Sleuth、Zipkin ## 基于Spring Cloud第一代产品开发原型技术选型 GateWay(统一网关、限流等) + Eureka(服务注册中心) + Feign(面向接口远程调用) + Hystrix(服务熔断) + Ribbon(负载均衡) + Config和Bus(配置中心) + Stream(消息队列推送) + mybatis(持久层框架) + druid(数据库连接池) + Guava(Google的开源工具类,用来限流使用) ## 开发 #### 模块设计 prototype-common:通用工具类、数据传输对象等 prototype-api:持久层接口+Mapper映射文件、实体类、服务层接口 prototype-user:用户模块 prototype-captcha:验证码模块 prototype-email:邮件模块 #### 开发过程 ###### 1.创建仓库 1. gitee仓库地址:https://gitee.com/cllyl/prototype-scn 2. 关联仓库、创建基础配置说明文件 README.md说明文件 .gitignore版本控制文件 ```.gitignore .idea *.iml target ``` ###### 2.创建项目 1. 创建项目 > 项目pom.xml文件如下 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.6.RELEASE com.cll.prototype prototype-scn 1.0-SNAPSHOT pom 基于Spring Cloud的第一代产品SCN进行原型的开发 UTF-8 UTF-8 1.8 1.8 org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-test org.projectlombok lombok provided org.springframework.boot spring-boot-starter-actuator org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 utf-8 org.springframework.boot spring-boot-maven-plugin ``` 2. 创建通用模块 > 通用模块pom.xml文件如下 ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 prototype-common 项目通用模块开发,包含通用的工具类、数据传输对象等。自己开发的业务逻辑模块都要依赖于当前模块 org.projectlombok lombok org.apache.commons commons-lang3 org.apache.commons commons-collections4 ${commons-collections4.version} com.alibaba fastjson ${fastjson.version} ``` > 返回数据对象封装信息如下 ```java package com.cll.prototype.common.dto; import com.cll.prototype.common.dictionary.AjaxResultEnum; import lombok.*; import java.io.Serializable; /** * 描述信息: * 前端ajax请求通用返回结果 * @author CLL * @version 1.0 * @date 2020/10/26 16:58 */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class AjaxResult implements Serializable { /** * 返回状态 * 1: * 2:正常 * 3:重定向,转发等 * 4:客户端错误 * 5:服务器错误 */ private Integer code; private String message; private Object data; /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success() { return success(AjaxResultEnum.SUCCESS.getCode(), AjaxResultEnum.SUCCESS.getInfo(), null); } /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success(AjaxResultEnum ajaxResultEnum) { return success(ajaxResultEnum.getCode(), ajaxResultEnum.getInfo(), null); } /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success(String info) { return success(AjaxResultEnum.SUCCESS.getCode(), info, null); } /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success(Integer code, String info) { return success(code, info, null); } /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success(Object data) { return success(AjaxResultEnum.SUCCESS.getCode(), AjaxResultEnum.SUCCESS.getInfo(), data); } /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success(String info, Object data) { return success(AjaxResultEnum.SUCCESS.getCode(), info, data); } /** * 无数据返回的正常返回结果 * @return 正常返回 */ public static AjaxResult success(Integer code, String info, Object data) { return new AjaxResult(code, info, data); } /** * 无数据返回的错误的返回结果 * @return 错误返回 */ public static AjaxResult error() { return error(AjaxResultEnum.ERROR.getCode(), AjaxResultEnum.ERROR.getInfo()); } /** * 无数据返回的错误的返回结果 * @return 错误返回 */ public static AjaxResult error(String info) { return error(AjaxResultEnum.ERROR.getCode(), info); } /** * 无数据返回的错误的返回结果 * @return 错误返回 */ public static AjaxResult error(Integer code, String info) { return new AjaxResult(code, info, null); } } ``` > 枚举类型数据常量信息如下 ```java package com.cll.prototype.common.dictionary; import lombok.*; /** * 描述信息: * JSON返回结果数据字典 * @author CLL * @version 1.0 * @date 2020/10/26 17:01 */ @Getter @RequiredArgsConstructor public enum AjaxResultEnum { /** * 请求成功 */ SUCCESS(20, "请求成功"), /** * 内部错误 */ ERROR(50, "内部错误"); @NonNull private int code; @NonNull private String info; } ``` > 转换器工具类如下 ```java package com.cll.prototype.common.util; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.time.LocalDateTime; import java.time.ZoneOffset; /** * 描述信息: * 将LocalDateTime类型数据转换成时间戳形式 * @author CLL * @version 1.0 * @date 2020/10/27 17:01 */ public class LocalDateTimeConverter extends JsonSerializer { @Override public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeNumber(localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli()); } } ``` 3. 创建API模块 > api模块pom.xml如下 ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 prototype-api 项目的API模块 持久层API、实体类、持久层Mapper映射文件 服务层API com.cll.prototype prototype-common 1.0-SNAPSHOT mysql mysql-connector-java runtime org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis-spring-boot-starter.version} com.alibaba druid-spring-boot-starter ${druid-spring-boot-starter.version} ``` 4. 创建服务注册中心 1. 修改hosts文件完成本机域名解析 ``` # windows下:C:\Windows\System32\drivers\etc\hosts # unix下:/etc/hosts 127.0.0.1 www.cllyl.com 127.0.0.1 eurekaServerA 127.0.0.1 eurekaServerB ``` 2. 开发eureka服务端 > Eureka对等服务节点1开发 > pom文件如下 ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 spring-cloud-eureka-server-8760 自定义实现的eureka的服务端。 eureka和zookeeper不同,必须自己实现服务端。 当前为eureka服务端的一个节点 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` > 配置文件 ```yml # eureka server服务端口 server: port: 8760 spring: application: # 应用名称会在eureka中作为服务名称 name: eureka-server # eureka配置信息。当前eureka项目是eureka服务端,但是所有的eureka服务相对于eureka来说也都是一个client eureka: instance: # 当前eureka实例的主机名 hostname: eurekaServerA # 使用IP显示 prefer-ip-address: false # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ client: # 配置当前eureka 服务交互的eureka server地址 service-url: # 集群模式下,应该写成其它副本的地址。如果多个副本使用英文逗号隔开 defaultZone: http://eurekaServerB:8761/eureka # 是否将当前eureka服务注册到eureka服务端,因为当前eureka服务本身就是服务端,所以不需要进行注册(单机版)。集群版需要注册 register-with-eureka: true # 从eureka server查询注册的服务信息,单节点不需要,集群部署需要 fetch-registry: true ``` > 启动类开发 ```java package com.cll.prototype.eureka; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * 描述信息: * eureka注册中心服务端节点1.@EnableEurekaServer注解标记当前应用为eureka服务端 * @author CLL * @version 1.0 * @date 2020/10/26 15:09 */ @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication8760 { private static final Logger logger = LoggerFactory.getLogger(EurekaServerApplication8760.class); public static void main(String[] args) { logger.info("===>>> eureka server 8760 is starting..."); SpringApplication.run(EurekaServerApplication8760.class, args); logger.info("===>>> eureka server 8760 is started success!"); } } ``` > 对等服务节点2 > pom文件 ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 spring-cloud-eureka-server-8761 自定义实现的eureka的服务端。 eureka和zookeeper不同,必须自己实现服务端。 当前为eureka服务端的一个节点 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` > 配置文件 ```yml # eureka server服务端口 server: port: 8761 spring: application: # 应用名称会在eureka中作为服务名称 name: eureka-server # eureka配置信息。当前eureka项目是eureka服务端,但是所有的eureka服务相对于eureka来说也都是一个client eureka: instance: # 当前eureka实例的主机名 hostname: eurekaServerB # 使用IP显示 prefer-ip-address: false # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ client: # 配置当前eureka 服务交互的eureka server地址 service-url: # 集群模式下,应该写成其它副本的地址。如果多个副本使用英文逗号隔开 defaultZone: http://eurekaServerA:8760/eureka # 是否将当前eureka服务注册到eureka服务端,因为当前eureka服务本身就是服务端,所以不需要进行注册 register-with-eureka: true # 从eureka server查询注册的服务信息,因为当前 fetch-registry: true ``` > 启动类 ```java package com.cll.prototype.eureka; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/11/8 16:36 */ @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication8761 { private static final Logger logger = LoggerFactory.getLogger(EurekaServerApplication8761.class); public static void main(String[] args) { logger.info("===>>> eureka server 8761 is starting..."); SpringApplication.run(EurekaServerApplication8761.class, args); logger.info("===>>> eureka server 8761 is started success!"); } } ``` 5. 创建用户模块 > API模块添加持久层接口、实体类、mapper映射配置文件 > 登录令牌实体类 ```java package com.cll.prototype.api.user.entity; import java.io.Serializable; import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; /** *

* 用户登录成功之后的令牌(token)表 *

* * @author CLL * @since 2020-10-26 */ @Data public class LoginToken implements Serializable { private static final long serialVersionUID = 1L; /** * 自增主键 */ private Integer id; /** * 用户邮箱 */ private String email; /** * 登录成功之后的令牌 */ private String token; /** * 创建时间 * 注解@JsonFormat:标识返回数据的时候将已json格式返回 * 注解@DateTimeFormat:标识该对象接收数据的时候,将以指定格式接收 */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime gmtCreate; } ``` > 登录令牌持久层接口 ```java package com.cll.prototype.api.user.mapper; import com.cll.prototype.api.user.entity.LoginToken; import org.apache.ibatis.annotations.Param; /** *

* 用户登录成功之后的令牌(token)表 Mapper 接口 *

* * @author CLL * @since 2020-10-26 */ public interface LoginTokenMapper { /** * 新增用户登录成功之后的令牌信息 * @param loginToken 登录成功的令牌 * @return 新增记录数 */ int add(@Param("loginToken") LoginToken loginToken); /** * 根据令牌获取用户信息 * @param token 令牌 * @return 用户信息 */ LoginToken getByToken(@Param("token") String token); } ``` > 登录令牌mapper映射配置文件 ```xml insert into login_token(email, token, gmt_create) values(#{loginToken.email},#{loginToken.token},#{loginToken.gmtCreate}) ``` > 登录令牌服务层接口 ```java package com.cll.prototype.api.user.service; import com.cll.prototype.common.dto.AjaxResult; /** *

* 用户登录成功之后的令牌(token)表 服务类 *

* * @author CLL * @since 2020-10-26 */ public interface ILoginTokenService { /** * 生成登录成功用户的token * @param email 用户邮箱 * @return 生成结果 */ AjaxResult generateLoginToken(String email); /** * 根据token获取用户信息 * @param token 登录之后的token * @return 用户名 */ AjaxResult getEmailByToken(String token); } ``` > 令牌持久层接口测试类 ```java package com.cll.prototype.user.mapper; import com.cll.prototype.api.user.entity.LoginToken; import com.cll.prototype.api.user.entity.User; import com.cll.prototype.api.user.mapper.LoginTokenMapper; import com.cll.prototype.api.user.mapper.UserMapper; import com.cll.prototype.common.util.UUIDUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.time.LocalDateTime; /** * 描述信息: * 用户持久层接口测试 * @author CLL * @version 1.0 * @date 2020/11/8 15:17 */ @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) public class LoginTokenMapperTest { private static final Logger logger = LoggerFactory.getLogger(LoginTokenMapperTest.class); @Autowired private LoginTokenMapper loginTokenMapper; @Test public void testAdd() throws Exception{ LoginToken loginToken = new LoginToken(); loginToken.setEmail("2203815100@qq.com"); loginToken.setToken(UUIDUtils.getInstance()); loginToken.setGmtCreate(LocalDateTime.now()); int insertRecord = loginTokenMapper.add(loginToken); logger.info("===>>> result = {}", insertRecord); } @Test public void testGetByToken() throws Exception{ LoginToken loginToken = loginTokenMapper.getByToken("f2b60f3f6fe1424e987c94bbd4fc154d"); logger.info("===>>> result = {}", loginToken); } } ``` > 用户实体类 ```java package com.cll.prototype.api.user.entity; import com.cll.prototype.common.util.LocalDateTimeConverter; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** *

* 用户服务的表 *

* * @author CLL * @since 2020-10-26 */ @Data public class User implements Serializable { private static final long serialVersionUID = 1L; /** * 自增主键 */ private Integer id; /** * 邮箱 */ private String email; /** * 密码 */ private String password; /** * 创建时间 * 注解@JsonSerialize指定序列化方式返回给前台 */ @JsonSerialize(using = LocalDateTimeConverter.class) private LocalDateTime gmtCreate; } ``` > 用户持久层接口 ```java package com.cll.prototype.api.user.mapper; import com.cll.prototype.api.user.entity.User; import org.apache.ibatis.annotations.Param; /** *

* 用户服务的表 Mapper 接口 *

* * @author CLL * @since 2020-10-26 */ public interface UserMapper { /** * 根据用户主键查询用户实体 * @param id 主键 * @return 查询结果 */ User getById(@Param("id") Integer id); /** * 新增用户 * @param user 用户实体 * @return 新增记录数 */ int add(@Param("user") User user); /** * 根据邮箱名查询用户 * @param email 邮箱 * @return 用户实体 */ User getByEmail(@Param("email") String email); } ``` > 用户mapper映射配置文件 ```xml insert into user(email, password, gmt_create) values(#{user.email}, #{user.password}, #{user.gmtCreate}) ``` > 用户服务层接口 ```java package com.cll.prototype.api.user.service; import com.cll.prototype.api.user.entity.User; import com.cll.prototype.common.dto.AjaxResult; /** *

* 用户服务的表 服务类 *

* * @author CLL * @since 2020-10-26 */ public interface IUserService { /** * 添加新用户 * @param email 邮箱号码 * @param password 密码 * @param confirmPassword 确认密码 * @param captcha 验证码 * @return 添加结果 */ AjaxResult add(String email, String password, String confirmPassword, String captcha); /** * 添加新用户 * @param user 用户实体 * @return 添加结果 */ AjaxResult add(User user); /** * 检测邮箱是否注册 * @param email 邮箱号码 * @return 检测结果.data:0表示邮箱没有注册;data:1:表示邮箱已经注册 */ AjaxResult isRegistry(String email); /** * 用户登录 * @param email 邮箱号 * @param password 密码 * @return 登录结果 */ AjaxResult login(String email, String password); } ``` > 用户持久层接口测试类 ```java package com.cll.prototype.user.mapper; import com.cll.prototype.api.user.entity.User; import com.cll.prototype.api.user.mapper.UserMapper; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.time.LocalDateTime; /** * 描述信息: * 用户持久层接口测试 * @author CLL * @version 1.0 * @date 2020/11/8 15:17 */ @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) public class UserMapperTest { private static final Logger logger = LoggerFactory.getLogger(UserMapperTest.class); @Autowired private UserMapper userMapper; @Test public void testGetById() throws Exception{ User user = userMapper.getById(1); logger.info("===>>> result = {}", user); } @Test public void testAdd() throws Exception{ User user = new User(); user.setEmail("794685750@qq.com"); user.setGmtCreate(LocalDateTime.now()); user.setPassword("123456"); int insertRecord = userMapper.add(user); logger.info("===>>> result = {}", insertRecord); } @Test public void testGetByEmail() throws Exception{ User user = userMapper.getByEmail("794685750@qq.com"); logger.info("===>>> result = {}", user); } } ``` > 用户模块添加服务层实现类和控制成 > jar包引入 ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 prototype-service-user-8080 com.cll.prototype prototype-api ${project.version} org.springframework.boot spring-boot-starter-web ``` > application.yml配置文件创建 ```yml spring: profiles: active: dev application: name: service-user server: port: 8080 mybatis: configuration: # 自动开启驼峰命名 map-underscore-to-camel-case: true # mapper映射配置文件路径 mapper-locations: classpath*:mapper/user/*.xml # 借助springboot暴漏健康检查等端点接口 management: # 暴漏web应用的所有 endpoints: web: exposure: include: "*" # 暴漏检查接口的细节 endpoint: health: show-details: always ``` ```yml spring: datasource: # 使用druid数据源 type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 756123 url: jdbc:mysql://127.0.0.1:3306/prototype?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong # 数据库连接池配置信息 # 初始化连接池中连接数量 initial-size: 5 # 最小连接数量 min-idle: 5 # 最大的活跃的连接数 max-active: 20 # 获取连接的最大超时时间 max-wait: 60000 # 多久检测一次需要释放的连接 time-between-eviction-runs-millis: 60000 # Mysql:SELECT 1; # SQLSERVER:SELECT 1; # ORACLE:SELECT 'x' FROM DUAL; # PostGresql:SELECT 'x'; validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开pst缓存,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # druid监控连接的过滤器,# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=3000 web-stat-filter: enabled: true url-pattern: "/" exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*" stat-view-servlet: enabled: true url-pattern: "/druid/*" reset-enable: false login-username: druidroot login-password: druidroot # 访问的IP白名单,""表示所有都可以访问 allow: "" deny: "192.168.1.100" # 合并多个DruidDataSource的监控数据 use-global-data-source-stat: true ``` > 项目启动类创建 ```java package com.cll.prototype.user; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/11/8 14:31 */ @MapperScan(basePackages = "com.cll.prototype.api.user.mapper") @SpringBootApplication public class UserApplication8080 { private static final Logger logger = LoggerFactory.getLogger(UserApplication8080.class); public static void main(String[] args) { logger.info("===>>> user module is starting..."); SpringApplication.run(UserApplication8080.class, args); logger.info("===>>> user module is started success!"); } } ``` > 用户服务层实现类 ```java package com.cll.prototype.user.service.impl; import com.cll.prototype.api.user.entity.User; import com.cll.prototype.api.user.mapper.UserMapper; import com.cll.prototype.api.user.service.ILoginTokenService; import com.cll.prototype.api.user.service.IUserService; import com.cll.prototype.common.dictionary.AjaxResultEnum; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.common.util.EncryptUtils; import com.cll.prototype.common.util.ValidateUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/11/8 15:13 */ @Service public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Autowired private ILoginTokenService loginTokenService; @Override public AjaxResult add(String email, String password, String confirmPassword, String captcha) { if (StringUtils.isAnyBlank(email, password, confirmPassword)) { return AjaxResult.error(41, "数据不合法"); } if (!ValidateUtils.checkEmailAddress(email)) { return AjaxResult.error(43, "邮箱地址不合法"); } if (!password.equals(confirmPassword)) { return AjaxResult.error(42, "两次密码不一致"); } // 检测邮箱是否已经注册 AjaxResult registryResult = isRegistry(email); if (1 == (Integer) registryResult.getData()) { return AjaxResult.error(43, "当前邮箱已经注册"); } // 检测用户输入的邮箱验证码是否正确 TODO 未实现远程调用验证码微服务 // AjaxResult ajaxResult = authCaptchaControllerFeignClient.checkCaptcha(email, captcha); AjaxResult ajaxResult = AjaxResult.success(1); boolean captchaFlag = null != ajaxResult && AjaxResultEnum.SUCCESS.getCode() == ajaxResult.getCode() && null != ajaxResult.getData() && (1 == (Integer) ajaxResult.getData()); if (captchaFlag) { String encodePassword = EncryptUtils.encryptUseMD5(password); if (StringUtils.isBlank(encodePassword)) { return AjaxResult.error("加密密码失败"); } User user = new User(); user.setEmail(email); user.setGmtCreate(LocalDateTime.now()); user.setPassword(encodePassword); AjaxResult addResult = add(user); if (AjaxResultEnum.SUCCESS.getCode() == addResult.getCode()) { return loginTokenService.generateLoginToken(email); } return addResult; } else { return ajaxResult; } } @Override public AjaxResult add(User user) { int insertRecord = userMapper.add(user); if (1 == insertRecord) { return AjaxResult.success(20, "新增用户成功"); } return AjaxResult.error(50, "新增用户失败"); } @Override public AjaxResult isRegistry(String email) { if (!ValidateUtils.checkEmailAddress(email)) { return AjaxResult.error(43, "邮箱地址不合法"); } User user = userMapper.getByEmail(email); return null == user ? AjaxResult.success(0) : AjaxResult.success(1); } @Override public AjaxResult login(String email, String password) { if (StringUtils.isAnyBlank(email, password)) { return AjaxResult.error(41, "数据不合法"); } if (!ValidateUtils.checkEmailAddress(email)) { return AjaxResult.error(43, "邮箱地址不合法"); } User user = userMapper.getByEmail(email); if (null == user) { return AjaxResult.error(42, "用户不存在"); } String encodePassword = EncryptUtils.encryptUseMD5(password); if (StringUtils.isBlank(encodePassword)) { return AjaxResult.error(43, "加密密码失败"); } String sourcePassword = user.getPassword(); if (!encodePassword.equals(sourcePassword)) { return AjaxResult.error(44, "密码错误"); } return loginTokenService.generateLoginToken(email); } } ``` > 用户控制层 ```java package com.cll.prototype.user.controller; import com.cll.prototype.common.dictionary.AjaxResultEnum; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.api.user.service.IUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; /** *

* 用户服务的表 前端控制器 *

* * @author CLL * @since 2020-10-26 */ @RestController @RequestMapping("/user") public class UserController { private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); @Autowired private IUserService userService; /** * 用户注册 * @param email 邮箱号 * @param password 密码 * @param confirmPassword 确认密码 * @param captcha 验证码 * @return 注册结果 */ @PostMapping(value = "/registry") public AjaxResult registry(@RequestParam("email") String email, @RequestParam("password") String password, @RequestParam("confirmPassword") String confirmPassword, @RequestParam("captcha") String captcha, HttpServletResponse response){ try { AjaxResult registryResult = userService.add(email, password, confirmPassword, captcha); if (null != registryResult && AjaxResultEnum.SUCCESS.getCode() == registryResult.getCode() && null != registryResult.getData()) { Cookie cookie = new Cookie("token", registryResult.getData().toString()); response.addCookie(cookie); } return registryResult; } catch (Exception e) { LOGGER.error("******用户注册抛出异常" + e.getMessage(), e); return AjaxResult.error(50, "注册失败"); } } /** * 检测邮箱是否注册 * @param email 邮箱号码 * @return 检测结果 */ @PostMapping(value = "/isRegistry") public AjaxResult isRegistry(@RequestParam("email") String email){ LOGGER.info("===>>> 检测邮箱是否已经注册。email = {}", email); try { return userService.isRegistry(email); } catch (Exception e) { LOGGER.error("****** 检测邮箱是否注册抛出异常" + e.getMessage(), e); return AjaxResult.error(52, "检测失败"); } } } ``` > 登录令牌服务层实现类 ```java package com.cll.prototype.user.service.impl; import com.cll.prototype.api.user.entity.LoginToken; import com.cll.prototype.api.user.mapper.LoginTokenMapper; import com.cll.prototype.api.user.service.ILoginTokenService; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.common.util.UUIDUtils; import com.cll.prototype.common.util.ValidateUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; /** * 描述信息: * 登录成功令牌服务层实现类 * @author CLL * @version 1.0 * @date 2020/11/8 15:57 */ @Service public class LoginTokenServiceImpl implements ILoginTokenService { @Autowired private LoginTokenMapper loginTokenMapper; @Override public AjaxResult generateLoginToken(String email) { if (StringUtils.isBlank(email)) { return AjaxResult.error(42, "参数不合法"); } if (!ValidateUtils.checkEmailAddress(email)) { return AjaxResult.error(46, "邮箱地址不合法"); } String token = UUIDUtils.getInstance(); LoginToken loginToken = new LoginToken(); loginToken.setEmail(email); loginToken.setToken(token); loginToken.setGmtCreate(LocalDateTime.now()); int addRecord = loginTokenMapper.add(loginToken); return 1 == addRecord ? AjaxResult.success(20, "登录成功", token) : AjaxResult.error("系统错误"); } @Override public AjaxResult getEmailByToken(String token) { LoginToken loginToken = loginTokenMapper.getByToken(token); if (null == loginToken) { return AjaxResult.error(43, "token不合法"); } return AjaxResult.success(20, "获取成功", loginToken.getEmail()); } } ``` > 登录令牌控制层 ```java package com.cll.prototype.user.controller; import com.cll.prototype.common.dictionary.AjaxResultEnum; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.api.user.service.ILoginTokenService; import com.cll.prototype.api.user.service.IUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; /** *

* 用户登录成功之后的令牌(token)表 前端控制器 *

* * @author CLL * @since 2020-10-26 */ @RestController @RequestMapping("/login_token") public class LoginTokenController { private static final Logger logger = LoggerFactory.getLogger(LoginTokenController.class); @Autowired private IUserService userService; @Autowired private ILoginTokenService loginTokenService; /** * 用户登录 * @param email 邮箱 * @param password 密码 * @return 登录结果.登录成功之后,生成token,存放到数据库中,并写入cookie中 */ @PostMapping(value = "/login") public AjaxResult login(@RequestParam("email") String email, @RequestParam("password") String password, HttpServletResponse response){ try { AjaxResult loginResult = userService.login(email, password); if (null != loginResult && AjaxResultEnum.SUCCESS.getCode() == loginResult.getCode() && null != loginResult.getData()) { Cookie cookie = new Cookie("token", loginResult.getData().toString()); response.addCookie(cookie); } return loginResult; } catch (Exception e) { logger.error("****** 用户登录抛出异常" + e.getMessage(), e); return AjaxResult.error(55, "用户登录抛出异常"); } } /** * 根据令牌获取用户邮箱号 * @param token 令牌 * @return 获取结果 */ @PostMapping(value = "/getEmailByToken") public AjaxResult getEmailByToken(@RequestParam("token") String token){ try { return loginTokenService.getEmailByToken(token); } catch (Exception e) { logger.error("****** 根据token获取用户信息抛出异常" + e.getMessage(), e); return AjaxResult.error(51, "获取用户信息失败"); } } } ``` > 将服务注册到Eureka注册中心上 > jar包引入 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` > 配置文件修改 ```yml eureka: client: service-url: # 当前eureka服务连接的eureka服务信息 defaultZone: http://eurekaServerA:8760/eureka,http://eurekaServerB:8761/eureka instance: # 使用IP显示 prefer-ip-address: true # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ # 自定义元数据信息,注册到eureka server中 metadata-map: c-author: cll c-wife: yl ``` > 启动类修改 ```java package com.cll.prototype.user; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/11/8 14:31 */ @MapperScan(basePackages = "com.cll.prototype.api.user.mapper") @SpringBootApplication @EnableDiscoveryClient public class UserApplication8080 { private static final Logger logger = LoggerFactory.getLogger(UserApplication8080.class); public static void main(String[] args) { logger.info("===>>> user module is starting..."); SpringApplication.run(UserApplication8080.class, args); logger.info("===>>> user module is started success!"); } } ``` 6. 创建验证码模块 > API模块添加 > > > 验证码实体类 > > ```java > package com.cll.prototype.api.captcha.entity; > > import java.time.LocalDateTime; > import java.io.Serializable; > import lombok.Data; > > /** > *

> * 验证码服务的表 > *

> * > * @author CLL > * @since 2020-10-26 > */ > @Data > public class AuthCaptcha implements Serializable { > > private static final long serialVersionUID = 1L; > > /** > * 自增主键 > */ > private Integer id; > > /** > * 接收验证码主体 > * 可以是邮箱号、手机号等 > */ > private String subject; > > /** > * 接收验证码主体类型 > * 1:邮箱 > * 2:手机号码 > */ > private Integer subjectType; > > /** > * 验证码 > */ > private String captcha; > > /** > * 验证码类型。1:注册验证码 > */ > private Integer type; > > /** > * 创建时间 > */ > private LocalDateTime gmtCreate; > > /** > * 验证码过期时间 > */ > private LocalDateTime gmtExpire; > > > } > ``` > > > 验证码持久层接口 > > ```java > package com.cll.prototype.api.captcha.mapper; > > import com.cll.prototype.api.captcha.entity.AuthCaptcha; > import org.apache.ibatis.annotations.Param; > > /** > *

> * 验证码服务的表 Mapper 接口 > *

> * > * @author CLL > * @since 2020-10-26 > */ > public interface AuthCaptchaMapper { > > /** > * 获取邮箱最后一次发送的验证码 > * @param email 邮箱号 > * @return 最后一次记录 > */ > AuthCaptcha getLastByEmail(@Param("email") String email); > > /** > * 新增验证码记录 > * @param authCaptcha 验证码实体 > * @return 新增记录数 > */ > int add(@Param("authCaptcha") AuthCaptcha authCaptcha); > } > ``` > > > 验证码mapper映射配置文件 > > ```xml > > > > > > > > insert into auth_captcha(subject_type, subject, captcha, type, gmt_create, gmt_expire) > VALUES > (#{authCaptcha.subjectType},#{authCaptcha.subject},#{authCaptcha.captcha}, > #{authCaptcha.type},#{authCaptcha.gmtCreate},#{authCaptcha.gmtExpire}) > > > ``` > > > 验证码服务层接口 > > ```java > package com.cll.prototype.api.captcha.service; > > import com.cll.prototype.common.dto.AjaxResult; > > /** > *

> * 验证码服务的表 服务类 > *

> * > * @author CLL > * @since 2020-10-26 > */ > public interface IAuthCaptchaService { > > /** > * 生成验证码 > * 存储数据库 > * 发送邮件 > * @param email 邮箱 > * @return 执行结果 > */ > AjaxResult createRegisterCaptchaAndSendEmail(String email); > > /** > * 校验验证码是否有效 > * @param email 邮箱号码 > * @param captcha 验证码 > * @return 校验结果 > */ > AjaxResult checkCaptcha(String email, String captcha); > > } > ``` > > > 验证码持久层接口测试类 > > ```java > package com.cll.prototype.captcha.mapper; > > import com.cll.prototype.api.captcha.entity.AuthCaptcha; > import com.cll.prototype.api.captcha.mapper.AuthCaptchaMapper; > import com.cll.prototype.common.util.RandomUtils; > import org.junit.Test; > import org.junit.runner.RunWith; > import org.slf4j.Logger; > import org.slf4j.LoggerFactory; > import org.springframework.beans.factory.annotation.Autowired; > import org.springframework.boot.test.context.SpringBootTest; > import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; > > import java.time.LocalDateTime; > > /** > * 描述信息: > * > * @author CLL > * @version 1.0 > * @date 2020/11/8 17:30 > */ > @SpringBootTest > @RunWith(SpringJUnit4ClassRunner.class) > public class AuthCaptchaMapperTest { > > private static final Logger logger = LoggerFactory.getLogger(AuthCaptchaMapperTest.class); > > @Autowired > private AuthCaptchaMapper authCaptchaMapper; > > @Test > public void testAdd() throws Exception { > AuthCaptcha authCaptcha = new AuthCaptcha(); > authCaptcha.setCaptcha(RandomUtils.getRandomStr2(6)); > authCaptcha.setSubject("3302815100@qq.com"); > authCaptcha.setSubjectType(1); > authCaptcha.setType(1); > LocalDateTime now = LocalDateTime.now(); > authCaptcha.setGmtCreate(now); > authCaptcha.setGmtExpire(now.plusMinutes(10)); > int insertRecord = authCaptchaMapper.add(authCaptcha); > logger.info("===>>> result = {}", insertRecord); > } > > @Test > public void testGetLastByEmail() throws Exception { > AuthCaptcha last = authCaptchaMapper.getLastByEmail("3302815100@qq.com"); > logger.info("===>>> result = {}", last); > } > } > ``` > 创建项目 ```xml prototype-service-captcha-8070 验证码模块 ``` > 添加依赖 ```xml com.cll.prototype prototype-api ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` > application.yml文件配置 ```yml spring: profiles: active: dev application: name: service-captcha server: port: 8070 mybatis: configuration: # 自动开启驼峰命名 map-underscore-to-camel-case: true # mapper映射配置文件路径 mapper-locations: classpath*:mapper/captcha/*.xml # 借助springboot暴漏健康检查等端点接口 management: # 暴漏web应用的所有 endpoints: web: exposure: include: "*" # 暴漏检查接口的细节 endpoint: health: show-details: always ``` ```yml spring: datasource: # 使用druid数据源 type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 756123 url: jdbc:mysql://127.0.0.1:3306/prototype?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong # 数据库连接池配置信息 # 初始化连接池中连接数量 initial-size: 5 # 最小连接数量 min-idle: 5 # 最大的活跃的连接数 max-active: 20 # 获取连接的最大超时时间 max-wait: 60000 # 多久检测一次需要释放的连接 time-between-eviction-runs-millis: 60000 # Mysql:SELECT 1; # SQLSERVER:SELECT 1; # ORACLE:SELECT 'x' FROM DUAL; # PostGresql:SELECT 'x'; validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开pst缓存,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # druid监控连接的过滤器,# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=3000 web-stat-filter: enabled: true url-pattern: "/" exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*" stat-view-servlet: enabled: true url-pattern: "/druid/*" reset-enable: false login-username: druidroot login-password: druidroot # 访问的IP白名单,""表示所有都可以访问 allow: "" deny: "192.168.1.100" # 合并多个DruidDataSource的监控数据 use-global-data-source-stat: true eureka: client: service-url: # 当前eureka服务连接的eureka服务信息 defaultZone: http://eurekaServerA:8760/eureka,http://eurekaServerB:8761/eureka instance: # 使用IP显示 prefer-ip-address: true # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ # 自定义元数据信息,注册到eureka server中 metadata-map: c-author: cll c-wife: yl ``` > 启动类开发 ```java package com.cll.prototype.captcha; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 描述信息: * 验证码模块启动类 * @author CLL * @version 1.0 * @date 2020/11/8 17:12 */ @SpringBootApplication @MapperScan(basePackages = "com.cll.prototype.api.captcha.mapper") public class CaptchaApplication8070 { private static final Logger logger = LoggerFactory.getLogger(CaptchaApplication8070.class); public static void main(String[] args) { logger.info("===>>> captcha module 8070 is starting...."); SpringApplication.run(CaptchaApplication8070.class, args); logger.info("===>>> captcha module 8070 is started success!"); } } ``` > 服务层实现类开发 ```java package com.cll.prototype.captcha.service.impl; import com.cll.prototype.api.captcha.entity.AuthCaptcha; import com.cll.prototype.api.captcha.mapper.AuthCaptchaMapper; import com.cll.prototype.api.captcha.service.IAuthCaptchaService; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.common.util.RandomUtils; import com.cll.prototype.common.util.ValidateUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; /** *

* 验证码服务的表 服务实现类 *

* * @author CLL * @since 2020-10-26 */ @Service public class AuthCaptchaServiceImpl implements IAuthCaptchaService { private static final Logger logger = LoggerFactory.getLogger(AuthCaptchaServiceImpl.class); @Autowired private AuthCaptchaMapper authCaptchaMapper; @Override public AjaxResult createRegisterCaptchaAndSendEmail(String email) { logger.info("===>>> 验证码服务开始生成验证码"); // 1.检验邮箱合法性 boolean checkEmailAddress = ValidateUtils.checkEmailAddress(email); if (!checkEmailAddress){ return AjaxResult.error(45, "邮箱不合法"); } // 2.检测验证码获取频率。必须间隔大于1分钟 AuthCaptcha last = authCaptchaMapper.getLastByEmail(email); if (null != last && LocalDateTime.now().isBefore(last.getGmtCreate().plusMinutes(1))) { return AjaxResult.error(46, "一分钟只能获取一次注册验证码"); } // 3.生成验证码 String captcha = RandomUtils.getRandomStr2(6); AuthCaptcha authCaptcha = new AuthCaptcha(); authCaptcha.setCaptcha(captcha); authCaptcha.setSubject(email); authCaptcha.setSubjectType(1); authCaptcha.setType(1); LocalDateTime now = LocalDateTime.now(); authCaptcha.setGmtCreate(now); authCaptcha.setGmtExpire(now.plusMinutes(10)); // 4.存储数据库 int insertRecord = authCaptchaMapper.add(authCaptcha); if (1 == insertRecord) { // 5.发送邮件 TODO 未实现远程调用 // return sendEmailLogControllerFeignClient.sendEmailCaptcha(email, captcha); return AjaxResult.error(); } return AjaxResult.error(51, "生成注册验证码失败"); } @Override public AjaxResult checkCaptcha(String email, String captcha) { if (StringUtils.isAnyBlank(email, captcha)) { return AjaxResult.error(41, "参数不合法"); } // 1.检测邮箱号是否合法 boolean emailAddress = ValidateUtils.checkEmailAddress(email); if (emailAddress) { AuthCaptcha last = authCaptchaMapper.getLastByEmail(email); if (null == last) { return AjaxResult.error(44, "请先获取验证码"); } if (LocalDateTime.now().isAfter(last.getGmtExpire())) { return AjaxResult.error(43, "验证码已过有效期,请重新获取"); } if (!captcha.equals(last.getCaptcha())) { return AjaxResult.error(45, "验证码不正确"); } return AjaxResult.success(20, "校验通过", 1); } return AjaxResult.error(42, "邮箱号码不合法"); } } ``` > 控制层开发 ```java package com.cll.prototype.captcha.controller; import com.cll.prototype.api.captcha.service.IAuthCaptchaService; import com.cll.prototype.common.dto.AjaxResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** *

* 验证码服务的表 前端控制器 *

* * @author CLL * @since 2020-10-26 */ @RestController @RequestMapping("/auth_captcha") public class AuthCaptchaController { private static final Logger logger = LoggerFactory.getLogger(AuthCaptchaController.class); @Autowired private IAuthCaptchaService authCaptchaService; /** * 1. 生成验证码 * 2. 存储到数据库 * 3. 调用扇出邮箱微服务发送 * @param email 邮箱 * @return 发送结果 */ @PostMapping(value = "/createRegisterCaptcha") public AjaxResult createRegisterCaptcha(@RequestParam("email") String email){ logger.info("===>>> 验证码微服务接收到请求"); try { return authCaptchaService.createRegisterCaptchaAndSendEmail(email); } catch (Exception e) { logger.error("****** 获取注册验证码抛出异常" + e.getMessage(), e); return AjaxResult.error(51, "获取注册验证码异常"); } } /** * 校验验证码 * @param email 邮箱号码 * @param captcha 验证码 * @return 检测结果 */ @PostMapping(value = "/checkCaptcha") public AjaxResult checkCaptcha(@RequestParam("email") String email, @RequestParam("captcha") String captcha){ return authCaptchaService.checkCaptcha(email, captcha); } } ``` 7. 创建邮箱模块 > API模块填充 > 实体类 ```java package com.cll.prototype.api.email.entity; import java.time.LocalDateTime; import java.io.Serializable; import lombok.Data; /** *

* *

* * @author CLL * @since 2020-10-27 */ @Data public class SendEmailLog implements Serializable { private static final long serialVersionUID = 1L; private Integer id; /** * 发件人 */ private String sender; /** * 收件人 */ private String receiver; /** * 邮件标题 */ private String subject; /** * 发送内容 */ private String content; /** * 1.文本;2.html;3.图片;4.附件;5.模板 */ private Integer type; /** * 发送时间 */ private LocalDateTime gmtCreate; /** * 发送结果 */ private Integer result; } ``` > 持久层接口 ```java package com.cll.prototype.api.email.mapper; import com.cll.prototype.api.email.entity.SendEmailLog; import org.apache.ibatis.annotations.Param; /** *

* Mapper 接口 *

* * @author CLL * @since 2020-10-27 */ public interface SendEmailLogMapper { /** * 添加邮件发送日志 * @param sendEmailLog 日志 * @return 新增记录数 */ int add(@Param("sendEmailLog") SendEmailLog sendEmailLog); } ``` > 持久层mapper映射文件 ```xml insert into send_email_log(sender, receiver, subject, content, type, gmt_create, result) VALUES ( #{sendEmailLog.sender},#{sendEmailLog.receiver},#{sendEmailLog.subject},#{sendEmailLog.content}, #{sendEmailLog.type},#{sendEmailLog.gmtCreate},#{sendEmailLog.result} ) ``` > 服务层接口 ```java package com.cll.prototype.api.email.service; import com.cll.prototype.common.dto.AjaxResult; /** *

* 服务类 *

* * @author CLL * @since 2020-10-27 */ public interface ISendEmailLogService { /** * 发送邮箱验证码并记录发送日志 * @param to 收件人 * @param subject 邮件主题 * @param captcha 验证码 * @return 发送结果 */ AjaxResult sendAndRecord(String to, String subject, String captcha); } ``` > 邮件模块 > JAR包引入 ```xml com.cll.prototype prototype-api ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-mail org.springframework.boot spring-boot-starter-thymeleaf org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` > 编写启动类 ```java package com.cll.prototype.email; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * 描述信息: * 邮件模块启动类 * @author CLL * @version 1.0 * @date 2020/11/8 18:13 */ @SpringBootApplication @MapperScan(basePackages = "com.cll.prototype.api.email.mapper") @EnableDiscoveryClient public class EmailApplication8060 { private static final Logger logger = LoggerFactory.getLogger(EmailApplication8060.class); public static void main(String[] args) { logger.info("===>>> email module 8060 is starting..."); SpringApplication.run(EmailApplication8060.class, args); logger.info("===>>> email module 8060 is started success!"); } } ``` > 编写配置文件 ```yml spring: profiles: active: dev application: name: service-email server: port: 8060 mybatis: configuration: # 自动开启驼峰命名 map-underscore-to-camel-case: true # mapper映射配置文件路径 mapper-locations: classpath*:mapper/email/*.xml # 借助springboot暴漏健康检查等端点接口 management: # 暴漏web应用的所有 endpoints: web: exposure: include: "*" # 暴漏检查接口的细节 endpoint: health: show-details: always ``` ```yml spring: # QQ邮箱配置 mail: # 发送邮件服务器 host: smtp.qq.com # 发送邮件的邮箱地址 username: 3302815100@qq.com # 客户端授权码,不是邮箱密码,这个在qq邮箱设置里面自动生成的 password: svhpfodrpzwvcjgd # properties.mail.smtp.port: 465 #端口号465或587 # properties.mail.smtp.starttls.enable: true # properties.mail.smtp.starttls.required: true # properties.mail.smtp.ssl.enable: true default-encoding: UTF-8 datasource: # 使用druid数据源 type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 756123 url: jdbc:mysql://127.0.0.1:3306/prototype?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong # 数据库连接池配置信息 # 初始化连接池中连接数量 initial-size: 5 # 最小连接数量 min-idle: 5 # 最大的活跃的连接数 max-active: 20 # 获取连接的最大超时时间 max-wait: 60000 # 多久检测一次需要释放的连接 time-between-eviction-runs-millis: 60000 # Mysql:SELECT 1; # SQLSERVER:SELECT 1; # ORACLE:SELECT 'x' FROM DUAL; # PostGresql:SELECT 'x'; validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开pst缓存,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # druid监控连接的过滤器,# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=3000 web-stat-filter: enabled: true url-pattern: "/" exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*" stat-view-servlet: enabled: true url-pattern: "/druid/*" reset-enable: false login-username: druidroot login-password: druidroot # 访问的IP白名单,""表示所有都可以访问 allow: "" deny: "192.168.1.100" # 合并多个DruidDataSource的监控数据 use-global-data-source-stat: true eureka: client: service-url: # 当前eureka服务连接的eureka服务信息 defaultZone: http://eurekaServerA:8760/eureka,http://eurekaServerB:8761/eureka instance: # 使用IP显示 prefer-ip-address: true # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ # 自定义元数据信息,注册到eureka server中 metadata-map: c-author: cll c-wife: yl ``` > 服务层接口实现类 ```java package com.cll.prototype.email.service.impl; import com.cll.prototype.api.email.mapper.SendEmailLogMapper; import com.cll.prototype.common.dictionary.EmailTypeEnum; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.api.email.entity.SendEmailLog; import com.cll.prototype.api.email.service.ISendEmailLogService; import com.cll.prototype.email.dto.TemplateEmailDto; import com.cll.prototype.email.service.IEmailService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.HashMap; /** *

* 服务实现类 *

* * @author CLL * @since 2020-10-27 */ @Service public class SendEmailLogServiceImpl implements ISendEmailLogService { private static final Logger logger = LoggerFactory.getLogger(SendEmailLogServiceImpl.class); @Value("${spring.mail.username}") private String from; @Autowired private SendEmailLogMapper sendEmailLogMapper; @Autowired private IEmailService emailService; @Override public AjaxResult sendAndRecord(String to, String subject, String captcha) { logger.info("===>>> 发送邮件并记录发送日志"); HashMap param = new HashMap<>(1); param.put("code", captcha); TemplateEmailDto templateEmailDto = new TemplateEmailDto(EmailTypeEnum.REGISTER.getName(), param); AjaxResult ajaxResult = emailService.sendTemplateEmail(null, to, subject, templateEmailDto); SendEmailLog sendEmailLog = new SendEmailLog(); sendEmailLog.setSender(from); sendEmailLog.setReceiver(to); sendEmailLog.setSubject(subject); sendEmailLog.setContent("注册模板"); sendEmailLog.setType(5); sendEmailLog.setResult(ajaxResult.getCode()); sendEmailLog.setGmtCreate(LocalDateTime.now()); int insertRecord = sendEmailLogMapper.add(sendEmailLog); return 1 == insertRecord ? AjaxResult.success(20, "发送成功", 1) : AjaxResult.error(50, "发送失败"); } } ``` > 邮件服务发送接口和实现类 ```java package com.cll.prototype.email.service; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.email.dto.TemplateEmailDto; import java.util.Map; /** * 描述信息: * 短信发送服务层 * 1. 发送简单文本邮件 * 2. 发送简单HTML邮件 * 3. 发送带有附件的邮件 * 4. 发送带有图片的邮件 * 5. 根据模板发送邮件 * @author CLL * @version 1.0 * @date 2020/10/27 17:36 */ public interface IEmailService { /** * 发送邮件 * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param content 发送内容 * @return 发送结果 */ AjaxResult sendEmail(String from, String to, String subject, String content); /** * 发送简单文本邮件 * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param content 发送内容 * @return 发送结果 */ AjaxResult sendTextEmail(String from, String to, String subject, String content); /** * 发送邮件 * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param content 发送内容 * @return 发送结果 */ AjaxResult sendHtmlEmail(String from, String to, String subject, String content); /** * 发送带有附件的邮件 * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param content 发送内容 * @param filePath 文件路径(磁盘存储路径) * @return 发送结果 */ AjaxResult sendAttachmentEmail(String from, String to, String subject, String content, String filePath); /** * 发送带有附件的邮件 * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param content 发送内容 * @param inlineId 文件路径(磁盘存储路径) * @param inlinePath 文件路径(磁盘存储路径) * @return 发送结果 */ AjaxResult sendInlineEmail(String from, String to, String subject, String content, String inlineId, String inlinePath); /** * 发送模板邮件(使用thymeleaf模板引擎) * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param templateName 模板名称 * @param templateParams 模板里面需要的参数 * @return 发送结果 */ AjaxResult sendTemplateEmail(String from, String to, String subject, String templateName, Map templateParams); /** * 发送模板邮件(使用thymeleaf模板引擎) * @param from 发送人 * @param to 接收人 * @param subject 邮件主体 * @param templateEmailDto 模板参数 * @return 发送结果 */ AjaxResult sendTemplateEmail(String from, String to, String subject, TemplateEmailDto templateEmailDto); } ``` ```java package com.cll.prototype.email.service.impl; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.email.dto.TemplateEmailDto; import com.cll.prototype.email.service.IEmailService; import lombok.NonNull; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.Date; import java.util.Map; /** * 描述信息: * 短信发送服务 * @author CLL * @version 1.0 * @date 2020/10/27 17:41 */ @Service public class EmailServiceImpl implements IEmailService { private static final Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class); private static final String START_HTML = ""; private static final String END_HTML = ""; @Value("${spring.mail.username}") private String defaultSendEmailFrom; @Autowired private JavaMailSender javaMailSender; @Autowired private TemplateEngine templateEngine; @Override public AjaxResult sendEmail(String from, String to, String subject, String content){ if (StringUtils.isAnyBlank(to, subject, content)) { return AjaxResult.error(41, "参数不合法"); } if (StringUtils.isBlank(from)) { from = defaultSendEmailFrom; } if (content.contains(START_HTML) && content.contains(END_HTML)) { return sendHtmlEmail(from, to, subject, content); } else { return sendTextEmail(from, to, subject, content); } } @Override public AjaxResult sendTextEmail(String from, String to, String subject, String content) { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(from); message.setTo(to); message.setSubject(subject); message.setText(content); javaMailSender.send(message); return AjaxResult.success(20, "发送邮件成功"); } @Override public AjaxResult sendHtmlEmail(String from, String to, String subject, String content) { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); mimeMessageHelper.setFrom(from); mimeMessageHelper.setTo(to); mimeMessageHelper.setSubject(subject); mimeMessageHelper.setSentDate(new Date()); mimeMessageHelper.setText(content, true); javaMailSender.send(mimeMessage); } catch (MessagingException me) { logger.error("****** 邮件发送异常" + me.getMessage(), me); return AjaxResult.error(50, "邮件发送异常"); } return AjaxResult.success(20, "发送成功"); } @Override public AjaxResult sendAttachmentEmail(String from, String to, String subject, String content, String filePath) { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); mimeMessageHelper.setFrom(from); mimeMessageHelper.setTo(to); mimeMessageHelper.setSubject(subject); mimeMessageHelper.setSentDate(new Date()); mimeMessageHelper.setText(content, true); FileSystemResource fileSystemResource = new FileSystemResource(filePath); String filename = fileSystemResource.getFilename(); if (StringUtils.isBlank(filename)) { filename = "默认附件"; } mimeMessageHelper.addAttachment(filename, fileSystemResource); javaMailSender.send(mimeMessage); } catch (MessagingException me) { logger.error("****** 邮件发送异常" + me.getMessage(), me); return AjaxResult.error(50, "邮件发送异常"); } return AjaxResult.success(20, "发送成功"); } @Override public AjaxResult sendInlineEmail(String from, String to, String subject, String content, String inlineId, String inlinePath) { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); mimeMessageHelper.setFrom(from); mimeMessageHelper.setTo(to); mimeMessageHelper.setSubject(subject); mimeMessageHelper.setSentDate(new Date()); mimeMessageHelper.setText(content, true); FileSystemResource inlineFile = new FileSystemResource(inlinePath); mimeMessageHelper.addInline(inlineId, inlineFile); javaMailSender.send(mimeMessage); } catch (MessagingException me) { logger.error("****** 邮件发送异常" + me.getMessage(), me); return AjaxResult.error(50, "邮件发送异常"); } return AjaxResult.success(20, "发送成功"); } @Override public AjaxResult sendTemplateEmail(String from, String to, String subject, String templateName, Map templateParams) { Context context = new Context(); for (Map.Entry entry: templateParams.entrySet()) { context.setVariable(entry.getKey(), entry.getValue()); } String emailContent = templateEngine.process(templateName, context); MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); mimeMessageHelper.setFrom(from); mimeMessageHelper.setTo(to); mimeMessageHelper.setSubject(subject); mimeMessageHelper.setSentDate(new Date()); mimeMessageHelper.setText(emailContent, true); javaMailSender.send(mimeMessage); } catch (MessagingException me) { logger.error("****** 邮件发送异常" + me.getMessage(), me); return AjaxResult.error(50, "邮件发送异常"); } return AjaxResult.success(20, "发送成功"); } @Override public AjaxResult sendTemplateEmail(String from, String to, String subject, TemplateEmailDto templateEmailDto) { if (StringUtils.isBlank(from)) { from = defaultSendEmailFrom; } Context context = new Context(); @NonNull String templateName = templateEmailDto.getTemplateName(); @NonNull Map templateParams = templateEmailDto.getParams(); for (Map.Entry entry: templateParams.entrySet()) { context.setVariable(entry.getKey(), entry.getValue()); } String emailContent = templateEngine.process(templateName, context); MimeMessage mimeMessage = javaMailSender.createMimeMessage(); try { MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); mimeMessageHelper.setFrom(from); mimeMessageHelper.setTo(to); mimeMessageHelper.setSubject(subject); mimeMessageHelper.setSentDate(new Date()); mimeMessageHelper.setText(emailContent, true); javaMailSender.send(mimeMessage); } catch (MessagingException me) { logger.error("****** 邮件发送异常" + me.getMessage(), me); return AjaxResult.error(50, "邮件发送异常"); } return AjaxResult.success(20, "发送成功"); } } ``` > 静态枚举常量 ```java package com.cll.prototype.email.dto; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; /** * 描述信息: * 发送图片邮件的数据传输对象 * @author CLL * @version 1.0 * @date 2020/10/28 14:37 */ @Getter @RequiredArgsConstructor public class InlineEmailDto { /** * 标识 */ @NonNull private String id; /** * 图片在磁盘的存储路径 */ @NonNull private String path; } ``` ```java package com.cll.prototype.email.dto; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.util.Map; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/10/28 15:50 */ @Getter @RequiredArgsConstructor public class TemplateEmailDto { @NonNull private String templateName; @NonNull private Map params; } ``` > 邮件发送模板 ```html 注册邮件模板 您好,欢迎注册原型平台,您的验证码是[[${code}]] ,如果非本人操作请忽略! ``` > 控制层 ```java package com.cll.prototype.email.controller; import com.cll.prototype.common.dto.AjaxResult; import com.cll.prototype.api.email.service.ISendEmailLogService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** *

* 前端控制器 *

* 邮件发送日志 * @author CLL * @since 2020-10-27 */ @RestController @RequestMapping("/send_email_log") public class SendEmailLogController { private static final Logger logger = LoggerFactory.getLogger(SendEmailLogController.class); @Autowired private ISendEmailLogService sendEmailLogService; @PostMapping(value = "/sendEmailCaptcha") public AjaxResult sendEmailCaptcha(@RequestParam("email") String email, @RequestParam("captcha") String captcha){ logger.info("===>>> 邮件服务发送注册验证码邮件"); return sendEmailLogService.sendAndRecord(email, "注册验证码", captcha); } } ``` > 持久层接口单元测试 ```java package com.cll.prototype.email.mapper; import com.cll.prototype.api.email.entity.SendEmailLog; import com.cll.prototype.api.email.mapper.SendEmailLogMapper; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.time.LocalDateTime; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/11/8 19:34 */ @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) public class SendEmailLogMapperTest { private static final Logger logger = LoggerFactory.getLogger(SendEmailLogMapperTest.class); @Autowired private SendEmailLogMapper sendEmailLogMapper; @Test public void testAdd() throws Exception { SendEmailLog sendEmailLog = new SendEmailLog(); sendEmailLog.setSender("3302815100@qq.com"); sendEmailLog.setReceiver("794685750@qq.com"); sendEmailLog.setSubject("注册验证码"); sendEmailLog.setContent("注册模板"); sendEmailLog.setType(5); sendEmailLog.setResult(20); sendEmailLog.setGmtCreate(LocalDateTime.now()); int insertRecord = sendEmailLogMapper.add(sendEmailLog); logger.info("===>>> result = {}", insertRecord); } } ``` 8. 用户模块添加Feign的远程调用 > 添加依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` > 添加配置 ```yml # 开启hystrix支持 feign: hystrix: enabled: true # Hystrix超时时间设置 hystrix: command: default: execution: isolation: thread: # Hystrix的超时时长设置 timeoutInMilliseconds: 1000 ribbon: # 请求连接超时时间 ConnectTimeout: 2000 # 请求处理超时时间 # Feign超时时长设置 ReadTimeout: 1000 #对所有操作都进行重试 OkToRetryOnAllOperations: false ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置), ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不行,返回失败信息。 # 对当前选中实例重试次数,不包括第一次调用 MaxAutoRetries: 0 # 切换实例的重试次数 MaxAutoRetriesNextServer: 0 # 负载策略调整 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule logging: level: # Feign日志只会对日志级别为debug的做出响应 com.cll.prototype.user.controller.service.captcha.AuthCaptchaControllerFeignClient: debug ``` > 修改启动类 ``` 启动类添加注解 @EnableFeignClients ``` > 添加Feign的日志 ```java package com.cll.prototype.user.config.feign; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述信息: * Feign日志级别 * @author CLL * @version 1.0 * @date 2020/10/30 14:59 */ @Configuration public class FeignLogConfiguration { @Bean Logger.Level feignLogLevel() { return Logger.Level.FULL; } } ``` > 添加Feign的远程调用接口及其熔断降级方法 ```java package com.cll.prototype.user.controller.service.captcha; import com.cll.prototype.common.dto.AjaxResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 描述信息: * 远程调用验证码微服务下的控制层接口 * @author CLL * @version 1.0 * @date 2020/10/29 9:34 */ @FeignClient(name = "service-captcha", path = "/auth_captcha", qualifier = "authCaptchaControllerFeignClient", fallback = AuthCaptchaControllerFeignClientFallback.class ) public interface AuthCaptchaControllerFeignClient { /** * 1. 生成验证码 * 2. 存储到数据库 * 3. 调用扇出邮箱微服务发送 * @param email 邮箱 * @return 发送结果 */ @PostMapping(value = "/createRegisterCaptcha") public AjaxResult createRegisterCaptcha(@RequestParam("email") String email); /** * 校验验证码 * @param email 邮箱号码 * @param captcha 验证码 * @return 检测结果 */ @PostMapping(value = "/checkCaptcha") public AjaxResult checkCaptcha(@RequestParam("email") String email, @RequestParam("captcha") String captcha); } ``` ```java package com.cll.prototype.user.controller.service.captcha; import com.cll.prototype.common.dto.AjaxResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; /** * 描述信息: * 调用验证码微服务的服务降级方法 * @author CLL * @version 1.0 * @date 2020/10/30 17:32 */ @Component @Qualifier("authCaptchaControllerFeignClientFallback") public class AuthCaptchaControllerFeignClientFallback implements AuthCaptchaControllerFeignClient { private static final Logger logger = LoggerFactory.getLogger(AuthCaptchaControllerFeignClientFallback.class); @Override public AjaxResult createRegisterCaptcha(String email) { logger.error("****** 调用验证码微服务获取注册验证码失败,进行服务降级"); return AjaxResult.success(24, "获取注册验证码服务降级", -6); } @Override public AjaxResult checkCaptcha(String email, String captcha) { logger.error("****** 校验注册验证码是否正确失败,进行服务降级"); return AjaxResult.success(24, "校验验证码服务降级", -6); } } ``` 9. 验证码模块添加Feign的远程调用 > 引入依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` > 添加配置 ```yml # 开启hystrix支持 feign: hystrix: enabled: true # Hystrix超时时间设置 hystrix: command: default: execution: isolation: thread: # Hystrix的超时时长设置 timeoutInMilliseconds: 1000 ribbon: # 请求连接超时时间 ConnectTimeout: 2000 # 请求处理超时时间 # Feign超时时长设置 ReadTimeout: 1000 # 对所有操作都进行重试 OkToRetryOnAllOperations: false ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置), ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不行,返回失败信息。 #对当前选中实例重试次数,不包括第一次调用 MaxAutoRetries: 0 #切换实例的重试次数 MaxAutoRetriesNextServer: 0 #负载策略调整 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule logging: level: # 配置项目日志级别为DEBUG root: debug # Feign日志只会对日志级别为debug的做出响应 com.cll.prototype.captcha.controller.service.email.SendEmailLogControllerFeignClient: debug ``` > 修改启动类 ```` 开启Feign @EnableFeignClients ```` > 调整Feign日志级别 ```java package com.cll.prototype.captcha.config.feign; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述信息: * Feign日志级别 * @author CLL * @version 1.0 * @date 2020/10/30 14:59 */ @Configuration public class FeignLogConfiguration { @Bean Logger.Level feignLogLevel() { return Logger.Level.FULL; } } ``` > 创建远程调用接口及其熔断降级方法 ```java package com.cll.prototype.captcha.controller.service.email; import com.cll.prototype.common.dto.AjaxResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 描述信息: * 通过openfeign封装了对email微服务的调用 * 更加符合面向接口的开发 * @author CLL * @version 1.0 * @date 2020/10/29 9:18 */ @FeignClient(name = "service-email", qualifier = "sendEmailLogControllerFeignClient", path = "/send_email_log", fallback = SendEmailLogControllerFeignClientFallback.class) public interface SendEmailLogControllerFeignClient { /** * 邮件微服务下发送注册验证码邮件接口 * @param email 邮箱 * @param captcha 验证码 * @return 发送结果 */ @PostMapping(value = "/sendEmailCaptcha") public AjaxResult sendEmailCaptcha(@RequestParam("email") String email, @RequestParam("captcha") String captcha); } ``` ```java package com.cll.prototype.captcha.controller.service.email; import com.cll.prototype.common.dto.AjaxResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * 描述信息: * 自定义feignClient中远程调用异常时候的服务降级方法 * @author CLL * @version 1.0 * @date 2020/10/29 10:49 */ @Component("sendEmailLogControllerFeignClientFallback") public class SendEmailLogControllerFeignClientFallback implements SendEmailLogControllerFeignClient { private static final Logger logger = LoggerFactory.getLogger(SendEmailLogControllerFeignClientFallback.class); @Override public AjaxResult sendEmailCaptcha(String email, String captcha) { logger.error("****** 调用发送邮件服务进行降级"); return AjaxResult.success(24, "邮件服务降级", -1); } } ``` 10. 编写Hystrix DashBoard监控 > 引入JAR ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 spring-cloud-hystrix-dashboard-8775 hystrix熔断器监控仪表盘 org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard ``` > 编写配置 ```yml server: port: 8775 spring: application: name: hystrix-dashboard eureka: client: service-url: # 当前eureka服务连接的eureka服务信息 defaultZone: http://eurekaServerA:8760/eureka,http://eurekaServerB:8761/eureka instance: # 使用IP显示 prefer-ip-address: true # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ # 自定义元数据信息,注册到eureka server中 metadata-map: c-author: cll c-wife: yl ``` > 创建启动类 ```yml package com.cll.prototype.hystrix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; /** * 描述信息: * Hystrix熔断器监控的仪表盘 * @author CLL * @version 1.0 * @date 2020/10/29 9:47 */ @SpringBootApplication @EnableDiscoveryClient @EnableHystrixDashboard public class HystrixDashboardApplication8775 { private static final Logger logger = LoggerFactory.getLogger(HystrixDashboardApplication8775.class); public static void main(String[] args) { logger.info("===>>> Hystrix仪表盘服务开始启动"); SpringApplication.run(HystrixDashboardApplication8775.class, args); logger.info("===>>> Hystrix仪表盘服务启动成功"); } } ``` 11. 编写Hystrix Turbine的聚合 > 引入JAR ```xml prototype-scn com.cll.prototype 1.0-SNAPSHOT 4.0.0 spring-cloud-hystrix-turbine-8770 熔断微服务。对项目中服务的熔断情况进行聚合,最后还需要通过hystrix-dashboard来查看聚合监控结果 org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-turbine ``` > 编写配置 ```yml server: port: 8770 spring: application: name: hystrix-turbine eureka: client: service-url: # 当前eureka服务连接的eureka服务信息 defaultZone: http://eurekaServerA:8760/eureka,http://eurekaServerB:8761/eureka instance: # 使用IP显示 prefer-ip-address: true # 自定义实例的ID instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ # 自定义元数据信息,注册到eureka server中 metadata-map: c-author: cll c-wife: yl # 聚合服务配置 turbine: cluster-name-expression: "'default'" app-config: service-user,service-captcha ``` > 创建启动类 ```java package com.cll.prototype.hystrix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.turbine.EnableTurbine; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/10/29 10:30 */ @SpringBootApplication @EnableDiscoveryClient @EnableTurbine public class HystrixTurbineApplication8770 { private static final Logger logger = LoggerFactory.getLogger(HystrixTurbineApplication8770.class); public static void main(String[] args) { logger.info("===>>> hystrix turbine module 8770 is starting..."); SpringApplication.run(HystrixTurbineApplication8770.class, args); logger.info("===>>> hystrix turbine module 8770 is started success!"); } } ``` 12. 用户模块开启Hystrix的服务的监控 > 引入JAR ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` > 添加Servlet的监控 ```java package com.cll.prototype.user.config.hystrix; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/10/28 20:51 */ @Configuration public class HystrixConfiguration { /** * 在被监控的微服务中注册一个servlet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的 * 前提:被监控的微服务需要引入springboot的actuator功能 * @return 注册的servlet的bean对象 */ @Bean public ServletRegistrationBean getServlet(){ HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } } ``` 13. 验证码模块开启Hystrix的服务的监控 > 引入JAR ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` > 暴漏监控数据流 ```java package com.cll.prototype.captcha.config.hystrix; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述信息: * * @author CLL * @version 1.0 * @date 2020/10/28 20:51 */ @Configuration public class HystrixConfiguration { /** * 在被监控的微服务中注册一个servlet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的 * 前提:被监控的微服务需要引入springboot的actuator功能 * @return 注册的servlet的bean对象 */ @Bean public ServletRegistrationBean getServlet(){ HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return rn registrationBean; } } ``` ###### GateWay网关服务开发 ###### Config + Bus配置中心服务开发