微服务改造
- 服务划分原则
- 狼行天下服务拆分
- 服务人员配置
- 1、基础服务
- 2、用户注册
服务划分原则
- 基于业务逻辑
将系统中的业务按照职责范围进行识别,职责相同的划分为一个单独的服务。 - 基于稳定性
将系统中的业务模块按照稳定性进行排序。稳定的、不经常修改的划分一块;将不稳定的,经常修改的划分为一个独立服务。比如日志服务、监控服务都是相对稳定的服务,可以归到一起。 - 基于可靠性
同样,将系统中的业务模块按照可靠性进行排序。对可靠性要求比较高的核心模块归在一起,对可靠性要求不高的非核心模块归在一块。
这种拆分的高明可以很好的规避因为一颗老鼠屎坏了一锅粥的单体弊端,同时将来要做高可用方案也能很好的节省机器或带宽的成本。 - 基于高性能
同上,将系统中的业务模块按照对性能的要求进行优先级排序。把对性能要求较高的模块独立成一个服务,对性能要求不高的放在一起。比如全文搜索,商品查询和分类,秒杀就属于高性能的核心模块。
狼行天下服务拆分
- 文章服务
- 目的地管理
- 旅游攻略
- 旅游日记
- 评论服务
- 旅游功能评论
- 旅游日记评论
- 景点评论
- 旅行社评论
- 用户服务
- 用户个人中心
- 用户积分相关
- 黑名单/白名单
- 粉丝关注
- 消息服务
- 短信通知
- 邮件通知
- 站内信
- 搜索服务
- 攻略搜索
- 游记搜索
- 用户搜索
- 景点搜索
服务人员配置
-
需求
假如我们按照业务来划分,根据粒度大小,可能存在以下两种:
- 第一种分为商品、交易、用户3个服务
- 第二种分为商品、订单、支付、物流、买家、卖家6个服务 -
分配
如果你的团队只有9个人,那么分成3个是合理的,如果有18个人,那么6个服务是合理的。这里引入团队成员进行协助拆分
在拆分遇到争议的时候,一般情况下我们增加一项拆分条件,虽然不是充要条件,但至少我们的答案会更加接近真理
除了业务可能存在争议,其他的划分也会有争议,比如一个独立的服务到底需要多少人员的配置? -
三个人分配一个服务?
为什么说是三个人分配一个服务(当然,成员主要是后端人员)?
- 假设是1个人,请个假、生个病都不行。一个人会遇到单点的问题,所以不合理
- 假设是2个人,终于有备份了,但是抽离一个后,剩下1个压力还是很大,不合理
- 假设是3个人,抽离一个还有2个在。而且数字3是个稳定而神奇数字,用得好事半功倍。特别是遇到技术讨论,3个人相对周全,如果是2个可能会各持己见,带有自我的偏见和盲区那么这个3是不是就是稳定的数量呢?
假设你做的是边开飞机边换引擎的重写工作,那么前期3个人都可能捉襟见肘。但是到了服务后期,你可能1个就够了
1、基础服务
项目结构图
1 基础模块搭建
1、parent模块
管理统一依赖
新建父项目-wolf2w-cloud
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2、注册中心Euraka
提供服务的注册和发现功能
新建子项目-eureka-server
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
#默认是true,真实开发中不可以关;现在关闭自我保护机制,保证不可用服务被删除
enable-self-preservation: false
启动类 EurekaServer
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
测试
http://localhost:8761/
3、配置中心Config-Server
提供统一的配置中心,支持读取本地配置和远程Git服务器配置
新建子项目-config-server
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 9100
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/wangdafei/wolf2w_cloud46
username: wangyifei@wolfcode.cn
password: dafei666
label: master
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
启动类ConfigServer
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
测试
码云上创建 test-server.yml :
server:
port: 9999
访问:
http://localhost:9100/master/test-server.yml
4、网关Zuul
可以统一解决:认证,授权,安全,流量管控,熔断,监控报警等功能
新建子项目-zuul-server
pom.xml
<dependencies>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--netflix-zuul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--config-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
</dependencies>
项目中的配置文件 bootstrap.yml
spring:
application:
name: zuul-server
cloud:
config:
label: master #分支名称
name: zuul #配置文件名称
profile: server #读取后缀名称:
uri: http://localhost:9100
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
码云上的配置文件 zuul-server.yml
server:
port: 9000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
zuul:
sensitiveHeaders:
forceOriginalQueryStringEncoding: true #强制采用原始请求的编码格式,即不对Get请求参数做编解码
#忽略匹配,既: order-server这种格式请求路径忽略掉
ignoredPatterns: /*-server/**
启动类ZuulServer
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulServer {
public static void main(String[] args) {
SpringApplication.run(ZuulServer.class, args);
}
}
5、公共模块common
存放整个项目中的公共的代码.
新建子项目-common
pom.xml
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
6、服务接口父模块provider-api
7、服务父模块provider-server
2 前端项目
2种方案
1:将原先的静态项目(trip-website)改为 springclout 项目,加eureka, config 进行统一管理
2:维持之前单纯的静态项目,使用外部 tomcat 管理
新建子项目-website-frontend
注意 新建的是静态页面项目,然后将之前的静态文件拷贝进去
注意并列排,目的为了好看
将之前的静态文件拷贝进去
部署到外部tomcat中并启动-端口8088
3 前端聚合服务website-server
概念
什么是聚合服务,简单理解是接收前端分服务,调用远程调用其他服务,充当服务协调,撮合的角色
项目中 trip-website-api 本质就是一个聚合服务,
负责接收请求,
调用服务处理请求(聚合各类服务的功能)
请求转发
新建孙子项目-在provider-server中创建website-server
1、修改provider-server项目的pom.xml,添加公共server依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
</dependencies>
2、website-server项目添加配置文件
项目中的配置文件 bootstrap.yml
spring:
application:
name: website-server
cloud:
config:
label: master #分支名称
name: website #配置文件名称
profile: server #读取后缀名称:
uri: http://localhost:9100
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
码云上的配置文件 website-server.yml
server:
port: 8080
3、码云上修改zuul-server.yml文件加上website-server路由
zuul:
routes:
website-server-route:
path: /website/**
service-id: website-server
4、启动类WebsiteServer
@SpringBootApplication
@EnableEurekaClient
public class WebsiteServer {
public static void main(String[] args) {
SpringApplication.run(WebsiteServer.class, args);
}
}
测试
创建-UserController
@RestController
@RequestMapping("users")
public class UserController {
@GetMapping("/info")
public Object info(){
return "ok";
}
}
启动服务器访问
- 用上zuul网关
http://localhost:9000/website/users/info
- 不用zuul网关
http://localhost:8080/users/info
2、用户注册
用户服务搭建
处理用户相关的服务
1、对外服务接口-member-api
新建子项目-在provider-api中创建member-api
member-api中的pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<!---依赖不传递-->
<optional>true</optional>
</dependency>
</dependencies>
修改provider-api中pom.xml
common项目中有lombok的依赖
添加 common的依赖
建议手敲使用idea 提示引入
<dependencies>
<dependency>
<groupId>com</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
新建一个数据库-member
实体类UserInfo
@Setter
@Getter
@Document("userInfo")@ToString
public class UserInfo implements Serializable {
public static final int GENDER_SECRET = 0; //保密
public static final int GENDER_MALE = 1; //男
public static final int GENDER_FEMALE = 2; //女
public static final int STATE_NORMAL = 0; //正常
public static final int STATE_DISABLE = 1; //冻结
@Id
protected String id;
private String nickname; //昵称
private String phone; //手机
private String email; //邮箱
private String password; //密码
private int gender = GENDER_SECRET; //性别
private int level = 0; //用户级别
private String city; //所在城市
private String headImgUrl; //头像
private String info; //个性签名
private int state = STATE_NORMAL; //状态
}
2、用户服务-member-server
新建子项目-在provider-server中创建member-server
member-server中的pom.xml
<dependencies>
<dependency>
<groupId>com</groupId>
<artifactId>member-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
项目中的配置文件 bootstrap.yml
spring:
application:
name: member-server
cloud:
config:
label: master #分支名称
name: member #配置文件名称
profile: server #读取后缀名称:
uri: http://localhost:9100
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
码云上的配置文件 member-server.yml
server:
port: 8081
spring:
data:
mongodb:
uri: mongodb://localhost:27017/member
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
hystrix:
enabled: true
UserInfoRepository
public interface UserInfoRepository extends MongoRepository<UserInfo, String> {
}
IUserInfoService
UserInfoServiceImpl
@Service
public class UserInfoServiceImpl implements IUserInfoService {
@Autowired
private UserInfoRepository repository;
@Override
public void save(UserInfo userInfo) {
repository.save(userInfo);
}
@Override
public void update(UserInfo userInfo) {
repository.save(userInfo);
}
@Override
public void delete(String id) {
repository.deleteById(id);
}
@Override
public UserInfo get(String id) {
return repository.findById(id).orElse(null);
}
@Override
public List<UserInfo> list() {
return repository.findAll();
}
}
启动类-MemberServer
@SpringBootApplication
public class MemberServer {
public static void main(String[] args) {
SpringApplication.run(MemberServer.class, args);
}
}
3、对外提供调用-FeginApi
修改provider-api的pom.xml文件
添加feign依赖
<dependency>
<groupId>com</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在member-api添加UserInfoFeignApi
@FeignClient(name = "member-server")
public interface UserInfoFeignApi {
@GetMapping("/users/get")
UserInfo get(@RequestParam("id") String id);
//若参数为对象,使用
//UserInfo get(@RequestBody UserInfo user);
}
在member-server添加UserInfoFeignApiClient
@RestController
public class UserInfoFeignClient implements UserInfoFeignApi {
@Autowired
private IUserInfoService userInfoService;
@Override
public UserInfo get(String id) {
return userInfoService.get(id);
}
}
4、FeginApi接口降级实现
在member-api添加UserInfoFeignHystrix
@Component
public class UserInfoFeignHystrix implements UserInfoFeignApi {
@Override
public UserInfo get(String id) {
System.out.println("出问题啦,走降级方法啦..UserInfoFeignHystrix...");
return new UserInfo();
}
}
在member-api修改UserInfoFeignApi
@FeignClient(name = "member-server", fallback = UserInfoFeignHystrix.class)
public interface UserInfoFeignApi {
}
测试
修改website-server项目pom.xml文件
<dependencies>
<dependency>
<groupId>com</groupId>
<artifactId>member-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
在website-server的UserInfoController添加get方法
因为所有请求都是通过 前端/后端聚合服务 ,然后访问内部的各种服务
get 方法中 调用了用户服务
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private UserInfoFeignApi userInfoFeignApi;
@GetMapping("/get")
public Object get(String id){
return userInfoFeignApi.get(id);
}
}
码云上修改member-server.yml开启feign/hystix调用权限
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
hystrix:
enabled: true
修改website-server的启动类
一个细节: 在 website-server 的pom.xml文件中添加上 feign 的依赖, 按道理不需要加的(因为provider-api里面加了)
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class WebsiteServer {
public static void main(String[] args) {
SpringApplication.run(WebsiteServer.class, args);
}
}
启动member-server跟website-server
访问users/get方法
http://localhost:9000/website/users/get?id=5e295f01a00e265228f963ea
手机号码校验
1、修改website-frontend中的common.js
因为之前前端的请求路径是发送到 8080 端口,现在使用了网关,应该直接发送到网关服务
将 domainURL 改为:
http://localhost:9000/website
2、zuul网关跨域操作
因为 website-frontend 端口号是 8088 ,网关是 9000 。需要解决跨域问题
(因为 website-frontend 是 Ajax 异步请求发起的跨域)
修改 zuul-server 项目中的启动类
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulServer {
//跨域访问
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
//重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//放行哪些原始域
.allowedOrigins("*")
//是否发送Cookie信息
.allowCredentials(true)
//放行哪些原始域(请求方式)
.allowedMethods("GET", "POST", "PUT", "DELETE","OPTIONS")
//放行哪些原始域(头部信息)
.allowedHeaders("*")
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Header1", "Header2");
}
};
}
public static void main(String[] args) {
SpringApplication.run(ZuulServer.class, args);
}
}
3、实现手机校验逻辑
/**
* @return true表示不存在 false表示手机号码存在
*/
@GetMapping("/checkPhone")
public Object checkPhone(String phone){
return userInfoFeignApi.checkPhone(phone);
}
统一结果返回对象
功能:
1:规范各个服务调用的返回值,统一规范,避免各种五花八门的返回值尴尬场景出现
(五花八门的返回值,会导致聚合服务没有统一结果处理规则)
2:规范并统一异常处理机制【暂时不理】
在 common 项目中,添加 工具类 JsonResult
@Setter
@Getter
@NoArgsConstructor
public class JsonResult<T> {
public static final int CODE_SUCCESS = 200;
public static final String MSG_SUCCESS = "操作成功";
public static final int CODE_NOLOGIN = 401;
public static final String MSG_NOLOGIN = "请先登录";
public static final int CODE_ERROR = 500;
public static final String MSG_ERROR = "系统异常,请联系管理员";
public static final int CODE_ERROR_PARAM = 501; //参数异常
private int code; //区分不同结果, 而不再是true或者false
private String msg;
private T data; //除了操作结果之后, 还行携带数据返回
public JsonResult(int code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> JsonResult success(T data){
return new JsonResult(CODE_SUCCESS, MSG_SUCCESS, data);
}
public static JsonResult success(){
return new JsonResult(CODE_SUCCESS, MSG_SUCCESS, null);
}
public static <T> JsonResult error(int code, String msg, T data){
return new JsonResult(code, msg, data);
}
public static JsonResult defaultError(){
return new JsonResult(CODE_ERROR, MSG_ERROR, null);
}
public static JsonResult noLogin() {
return new JsonResult(CODE_NOLOGIN, MSG_NOLOGIN, null);
}
}
则修改 UserInfoFeignClient 的返回结果类型
@RestController
public class UserInfoFeignClient implements UserInfoFeignApi {
@Autowired
private IUserInfoService userInfoService;
@Override
public JsonResult get(String id) {
return JsonResult.success(userInfoService.get(id));
}
@Override
public JsonResult checkPhone(String phone) {
return JsonResult.success(userInfoService.checkPhone(phone));
}
}
修改 controller
@GetMapping("/checkPhone")
public JsonResult checkPhone(String phone){
JsonResult result = userInfoFeignApi.checkPhone(phone);
//约定表示降级方法执行的结果是result为null,即如果result为null,说明是降级方法
if (result == null){
return JsonResult.defaultError();
}
return result;
}
创建短信服务
1、对外服务接口-msg-api
新建子项目-在provider-api中创建msg-api
暂时无需引入任何依赖
2、短信服务-msg-server
新建子项目-在provider-server中创建msg-server
msg-server中的pom.xml
需要引入 redis
<dependency>
<groupId>com</groupId>
<artifactId>msg-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
项目中的配置文件 bootstrap.yml
spring:
application:
name: msg-server
cloud:
config:
label: master #分支名称
name: msg #配置文件名称
profile: server #读取后缀名称:
uri: http://localhost:9100
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
码云上的配置文件 msg-server.yml
server:
port: 8082
spring:
redis:
host: 127.0.0.1
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
hystrix:
enabled: true
ISmsRedisService
此处实现参照项目二
public interface ISmsRedisService {
/**
* 将code添加到redis中
* @param phone
* @param code
*/
void setVerifyCode(String phone, String code);
/**
* 获取验证码
* @param phone
*/
String getVerifyCode(String phone);
}
启动类-MsgServer
@SpringBootApplication
public class MsgServer {
public static void main(String[] args) {
SpringApplication.run(MsgServer.class, args);
}
}
3、对外提供调用-FeginApi
在msg-api添加SmsFeignApi
@FeignClient(name = "msg-server", fallback = SmsFeignHystrix.class)
public interface SmsFeignApi {
@GetMapping("/sms/checkPhone")
JsonResult sendVerifyCode(@RequestParam("phone") String phone);
}
在msg-server添加SmsFeignApiClient
@RestController
public class SmsFeignApiClient implements SmsFeignApi {
@Autowired
private ISmsRedisService smsRedisService;
@Override
public JsonResult sendVerifyCode(String phone) {
//短信发送
//创建验证码
String code = UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 4);
//创建短信
StringBuilder sb = new StringBuilder(80);
sb.append("您注册的短信验证码是:").append(code).append(",请在")
.append(Consts.VERIFY_CODE_VAI_TIME)
.append("分钟内使用");
//假装短信已发送
System.out.println(sb);
//将短信验证码缓存redis
smsRedisService.setVerifyCode(phone,code);
return JsonResult.success();
}
}
4、FeginApi接口降级实现
在msg-api添加SmsFeignHystrix
@Component
public class SmsFeignHystrix implements SmsFeignApi {
@Override
public JsonResult sendVerifyCode(String phone) {
System.out.println("出问题啦,走降级方法啦..SmsFeignHystrix...");
return null;
}
}
调用
修改website-server项目pom.xml文件
<dependency>
<groupId>com</groupId>
<artifactId>msg-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在website-server中新建类SmsController
@RestController
@RequestMapping("sms")
public class SmsController {
@Autowired
private SmsFeignApi smsFeignApi;
@GetMapping("/sendVerifyCode")
public JsonResult sendVerifyCode(String phone){
//调用 msg-server 服务接口进行短信发送
JsonResult result = smsFeignApi.sendVerifyCode(phone);
//约定表示降级方法执行的结果是result为null,即如果result为null,说明是降级方法
if (result == null){
return JsonResult.defaultError();
}
return result;
}
}
用户注册
注意其中验证验证码是否正确应该属于短信服务 msg-api
SmsFeignApiClient
@Override
public JsonResult checkVerifyCode(String phone, String verifyCode) {
//校验短信验证码是否正确
String code = smsRedisService.getVerifyCode(phone);
if (code == null || !verifyCode.equalsIgnoreCase(code)){
throw new RuntimeException("验证码失效或错误");
}
return JsonResult.success(true);
}
UserInfoFeignClient
@Override
public JsonResult regist(String phone, String nickname, String password, String rpassword) {
//校验参数是否为空
AssertUtils.hasLength(phone , "手机号不可为空");
AssertUtils.hasLength(nickname , "昵称不可为空");
AssertUtils.hasLength(password , "密码不可为空");
AssertUtils.hasLength(rpassword , "确认密码不可为空");
//校验两次密码是否相等
AssertUtils.isEquals(password,rpassword,"两次输入的密码不一致");
//校验手机号码是否正确 @Todo java的正则表达式
//校验手机号是否唯一
if (!userInfoService.checkPhone(phone)){
throw new RuntimeException("该手机号码已经被注册");
}
//注册
UserInfo userInfo = new UserInfo();
userInfo.setNickname(nickname);
userInfo.setPhone(phone);
userInfo.setEmail("");
userInfo.setPassword(password); //假装加密
userInfo.setGender(UserInfo.GENDER_SECRET);
userInfo.setLevel(1);
userInfo.setCity("");
userInfo.setHeadImgUrl("/images/default.jpg");
userInfo.setInfo("");
//核心属性必须自己控制,就算实体类里面有默认值,为了防止意外最好自己添加
userInfo.setState(UserInfo.STATE_NORMAL);
//userInfo.setId(null);
userInfoService.save(userInfo);
return JsonResult.success();
}
UserController
@PostMapping("/regist")
public JsonResult regist(String phone,String nickname,String password,String rpassword,String verifyCode){
//先调用msg-spi进行短信验证
JsonResult verifyCodeResult = smsFeignApi.checkVerifyCode(phone, verifyCode);
//再调用member-api实现用户注册
JsonResult result = userInfoFeignApi.regist(phone,nickname,password,rpassword);
if (verifyCodeResult == null || result == null){
return JsonResult.defaultError();
}
return result;
}
自定义异常与统一异常处理
之前项目二,自定义异常是为了区分 系统异常、给用户提示的异常。且对于异常是在 异常处理类 中统一处理
现在使用了 Feign 远程调用,对于出现异常,会执行 降级方法。且在 controller 里面是统一返回 JsonResult.defaultError()
所以现在考虑 针对 UserInfoFeignClient 做统一异常处理,如果是自定义异常,使用统一异常处理,如果是系统异常,走降级方法处理
在 common 项目加入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
异常状态码优化
约定不同的服务出错,状态码不同,报错的时候方便找错误
member-server中定义异常状态码类
public class MemberCodeMsg {
public static final MemberCodeMsg PHONE_ERROR =
new MemberCodeMsg(500100,"手机号不可为空");
public static final MemberCodeMsg NICKNAME_ERROR =
new MemberCodeMsg(500101,"昵称不可为空");
public static final MemberCodeMsg PASSWORD_ERROR =
new MemberCodeMsg(500102,"密码不可为空");
public static final MemberCodeMsg REPASSWORD_ERROR =
new MemberCodeMsg(500103,"确认密码不可为空");
public static final MemberCodeMsg PASSWORD_EQUAL_ERROR =
new MemberCodeMsg(500104,"两次输入的密码不一致");
public static final MemberCodeMsg HAS_REGIST_ERROR =
new MemberCodeMsg(500105,"该手机号码已经被注册");
private int code;
private String msg;
public MemberCodeMsg(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
common 项目加入状态码的父类
为了能在 common 项目中使用状态码类,新定义一个父类
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class CodeMsg {
private int code;
private String msg;
}
然后让 MemberCodeMsg 继承
public class MemberCodeMsg extends CodeMsg{
//......
}
common 项目加入自定义异常类
@Setter
@Getter
public class BusinessException extends RuntimeException {
/*private int code;
private String msg;
public BusinessException(int code, String msg){
this.code = code;
this.msg = msg;
}*/
//code:1开头是用户服务出错 2开头是信息服务出错
private CodeMsg codeMsg;
public BusinessException(CodeMsg codeMsg){
this.codeMsg = codeMsg;
}
}
统一异常处理类
common 项目中定义异常处理类统一的父类,因为有多个服务
//统一的父类
public class CommExceptionAdvice {
@ExceptionHandler(BusinessException.class)
@ResponseBody
public JsonResult handler(BusinessException e){
e.printStackTrace();
return new JsonResult(e.getCode(), e.getMsg(), null);
}
}
在各自的项目服务中定义异常处理类,继承上面的 CommExceptionAdvice
member-server中定义异常处理类
//针对member-server服务统一异常处理
@ControllerAdvice
public class MemberCommExceptionAdvice extends CommExceptionAdvice {
//针对member-server服务所有异常做统一异常处理
}
msg-server中定义异常处理类
修改其他代码
修改出现用户服务相关的异常位置
//校验参数是否为空
AssertUtils.hasLength(phone , MemberCodeMsg.PHONE_ERROR);
AssertUtils.hasLength(nickname , MemberCodeMsg.NICKNAME_ERROR);
AssertUtils.hasLength(password , MemberCodeMsg.PASSWORD_ERROR);
AssertUtils.hasLength(rpassword , MemberCodeMsg.REPASSWORD_ERROR);
//校验两次密码是否相等
AssertUtils.isEquals(password,rpassword,MemberCodeMsg.PASSWORD_EQUAL_ERROR);
//校验手机号是否唯一
if (!userInfoService.checkPhone(phone)){
throw new BusinessException(MemberCodeMsg.HAS_REGIST_ERROR);
}
修改 AssertUtils 工具类
public static void hasLength(String v, CodeMsg msg) {
if(v==null | "".equals(v)){
throw new BusinessException(msg);
}
}
修改统一异常处理类的父类
//统一的父类
public class CommExceptionAdvice {
@ExceptionHandler(BusinessException.class)
@ResponseBody
public JsonResult handler(BusinessException e){
e.printStackTrace();
return new JsonResult(e.getCodeMsg().getCode(),
e.getCodeMsg().getMsg(), null);
}
}
问题
测试异常处理功能的时候,发现未填入信息的时候报以下错误:
feign.FeignException: status 400 reading UserInfoFeignApi#regist(String,String,String,String)
说明该参数是必须存在的。修改如下:required = false
@GetMapping("/users/regist")
JsonResult regist(@RequestParam(value="phone",required = false) String phone,
@RequestParam(value="nickname",required = false) String nickname,
@RequestParam(value="password",required = false) String password,
@RequestParam(value="rpassword",required = false)String rpassword);