Eureka註冊中心
在前後端分離架構中,服務層被拆分成了很多的微服務,微服務的信息如何管理?Spring Cloud中提供服務註冊中心來管理微服務信息。
爲什麼要用註冊中心?
1、微服務數量衆多,要進行遠程調用就需要知道服務端的ip地址和端口,註冊中心幫助我們管理這些服務的ip和端口。
2、微服務會實時上報自己的狀態,註冊中心統一管理這些微服務的狀態,將存在問題的服務踢出服務列表,客戶端獲取到可用的服務進行調用。
註冊中心工程搭建
依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>xc-framework-parent</artifactId>
<groupId>com.xuecheng</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../xc-framework-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xc-govern-center</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
啓動類
package com.xuecheng.govern.center;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class GovernCenterApplication {
public static void main(String[] args) {
SpringApplication.run(GovernCenterApplication.class, args);
}
}
單機版配置-application.yml
server:
port: 50101 #服務端口
spring:
application:
name: xc‐govern‐center #指定服務名
eureka:
client:
registerWithEureka: true #服務註冊,是否將自己註冊到Eureka服務中
fetchRegistry: true #服務發現,是否從Eureka中獲取註冊信息
serviceUrl: #Eureka客戶端與Eureka服務端的交互地址,高可用狀態配置對方的地址,單機狀態配置自己(默認本機8761端口)
defaultZone: http://localhost:50101/eureka/
server:
enable‐self‐preservation: false #是否開啓自我保護模式
eviction‐interval‐timer‐in‐ms: 60000 #服務註冊表清理間隔(單位毫秒,默認是60*1000)
高可用版配置-application.yml
server:
port: ${PORT:50101} #服務端口
spring:
application:
name: xc‐govern‐center #指定服務名
eureka:
client:
registerWithEureka: true #服務註冊,是否將自己註冊到Eureka服務中
fetchRegistry: true #服務發現,是否從Eureka中獲取註冊信息
serviceUrl: #Eureka客戶端與Eureka服務端的交互地址,高可用狀態配置對方的地址,單機狀態配置自己(默認本機8761端口)
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
server:
enable‐self‐preservation: false #是否開啓自我保護模式
eviction‐interval‐timer‐in‐ms: 60000 #服務註冊表清理間隔(單位毫秒,默認是60*1000)
instance:
hostname: ${EUREKA_DOMAIN:eureka01}
通過設置JVM
運行參數,完成多個註冊中心實例的啓動。
服務註冊
依賴
在需要註冊到註冊中心的微服務中添加依賴,如:xc-service-manage-cms
,其他微服務操作相同。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
啓動類上新增註解
在啓動類上新增@EnableDiscoveryClient
註解。
application.yml新增配置
eureka:
client:
registerWithEureka: true #服務註冊開關
fetchRegistry: true #服務發現開關
serviceUrl: #Eureka客戶端與Eureka服務端進行交互的地址,多箇中間用逗號分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/,http://localhost:50102/eureka/}
instance:
prefer‐ip‐address: true #將自己的ip地址註冊到Eureka服務中
ip‐address: ${IP_ADDRESS:127.0.0.1}
instance‐id: ${spring.application.name}:${server.port} #指定實例id
課程詳情靜態化
課程數據查詢
響應結果
package com.xuecheng.framework.domain.course.ext;
import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CourseMarket;
import com.xuecheng.framework.domain.course.CoursePic;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@NoArgsConstructor
public class CourseView {
private CourseBase courseBase;
private CourseMarket courseMarket;
private CoursePic coursePic;
private TeachplanNode teachplanNode;
}
CourseViewControllerApi
package com.xuecheng.api.course;
import com.xuecheng.framework.domain.course.ext.CourseView;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@Api(value = "課程預覽", description = "課程預覽接口,提供課程預覽數據的查詢")
public interface CourseViewControllerApi {
@ApiOperation("課程視圖查詢")
CourseView courseview(String id);
}
CourseViewController
package com.xuecheng.manage_course.controller;
import com.xuecheng.api.course.CourseViewControllerApi;
import com.xuecheng.framework.domain.course.ext.CourseView;
import com.xuecheng.framework.web.BaseController;
import com.xuecheng.manage_course.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("courseview")
public class CourseViewController extends BaseController implements CourseViewControllerApi {
@Autowired
private CourseService courseService;
/**
* 查詢課程預覽所需數據
*
* @param id 課程ID
* @return CourseView
*/
@Override
@GetMapping("{id}")
public CourseView courseview(@PathVariable String id) {
return courseService.getCourseView(id);
}
}
CourseService
package com.xuecheng.manage_course.service;
import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CourseMarket;
import com.xuecheng.framework.domain.course.CoursePic;
import com.xuecheng.framework.domain.course.ext.CourseView;
import com.xuecheng.framework.domain.course.ext.TeachplanNode;
import com.xuecheng.framework.service.BaseService;
import com.xuecheng.manage_course.dao.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Slf4j
@Service
public class CourseService extends BaseService {
@Autowired
private CourseBaseRepository courseBaseRepository;
@Autowired
private CoursePicRepository coursePicRepository;
@Autowired
private CoursePlanMapper coursePlanMapper;
@Autowired
private CoursePlanRepository coursePlanRepository;
@Autowired
private CourseMarketRepository courseMarketRepository;
/**
* 查詢課程預覽所需數據
*
* @param id 課程ID
* @return CourseView
*/
public CourseView getCourseView(String id) {
CourseView result = new CourseView();
// 查詢課程基本信息
Optional<CourseBase> courseBaseOptional = courseBaseRepository.findById(id);
courseBaseOptional.ifPresent(result::setCourseBase);
// 查詢課程圖片
Optional<CoursePic> coursePicOptional = coursePicRepository.findById(id);
coursePicOptional.ifPresent(result::setCoursePic);
// 查詢課程營銷信息
Optional<CourseMarket> courseMarketOptional = courseMarketRepository.findById(id);
courseMarketOptional.ifPresent(result::setCourseMarket);
// 查詢課程計劃信息
TeachplanNode teachplanNode = coursePlanMapper.findList(id);
result.setTeachplanNode(teachplanNode);
return result;
}
}
課程頁面模板
新增課程頁面模板(模板文件在資料裏面提供的有)。
課程預覽實現
需求分析
- 用戶進入課程管理頁面,點擊課程預覽,請求到課程管理服務。
- 課程管理服務遠程調用cms添加頁面接口向cms添加課程詳情頁面。
- 課程管理服務得到cms返回課程詳情頁面id,並拼接生成課程預覽Url。
- 課程管理服務將課程預覽Url給前端返回。
- 用戶在前端頁面請求課程預覽Url,打開新窗口顯示課程詳情內容。
CMS頁面預覽測試
我這裏使用CMS管理頁面手動添加了一個頁面
在頁面預覽Controller中新增代碼,設置響應頭信息
response.setHeader("Content-type","text/html;charset=utf-8");
點擊頁面預覽即可。
CMS添加課程頁面
功能說明:提供API,當課程前端點擊頁面預覽時先添加課程詳情頁面。
CmsPageControllerApi
新增API接口
@ApiOperation("保存頁面")
CmsPageResult save(CmsPage cmsPage);
CmsPageController
新增接口實現
@Override
@PostMapping("save")
public CmsPageResult save(@RequestBody CmsPage cmsPage) {
CmsPage save = cmsPageService.save(cmsPage);
if (save == null) {
ExceptionCast.cast(CommonCode.FAIL);
}
return new CmsPageResult(CommonCode.SUCCESS, save);
}
CmsPageService
新增保存方法
public CmsPage save(CmsPage cmsPage) {
CmsPage _cmsPage = cmsPageRepository
.findBySiteIdAndPageNameAndPageWebPath(cmsPage.getSiteId(), cmsPage.getPageName(), cmsPage.getPageWebPath());
if (_cmsPage == null) {
// 新增
cmsPage = add(cmsPage);
} else {
// 更新
cmsPage.setPageId(_cmsPage.getPageId());
cmsPage = edit(cmsPage);
}
return cmsPage;
}
課程預覽調用
編寫Feign Client
package com.xuecheng.manage_course.client;
import com.xuecheng.framework.client.XcServiceList;
import com.xuecheng.framework.domain.cms.CmsPage;
import com.xuecheng.framework.domain.cms.response.CmsPageResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* CMS PAGE API
*/
@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS)
public interface CmsPageClient {
@PostMapping("cms/page/save")
CmsPageResult save(@RequestBody CmsPage cmsPage);
}
響應結果實體類
package com.xuecheng.framework.domain.course.response;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.model.response.ResultCode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@NoArgsConstructor
public class CoursePublishResult extends ResponseResult {
private String previewUrl;
public CoursePublishResult(ResultCode resultCode, String previewUrl) {
super(resultCode);
this.previewUrl = previewUrl;
}
}
新增配置
主要爲Cms Page相關參數
course‐publish:
siteId: 5d8cd1d35f31573b5c6e4f11
templateId: 5d8ccd635f31573b5c6e4f0e
previewUrl: http://www.xuecheng.com/cms/preview/
pageWebPath: /course/detail/
pagePhysicalPath: F:/xcEdu/xcEdu_ui/static/course/detail/
dataUrlPre: http://localhost:31200/course/courseview/
Cms Page配置類
主要用於讀取Cms Page相關參數
package com.xuecheng.manage_course.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "course-publish")
public class CoursePublishConfig {
private String siteId;
private String templateId;
private String previewUrl;
private String pageWebPath;
private String pagePhysicalPath;
private String dataUrlPre;
}
CourseViewControllerApi
@ApiOperation("課程視圖預覽")
CoursePublishResult coursePreview(String id);
CourseViewController
/**
* 預覽課程
*
* @param id 課程ID
* @return CoursePublishResult
*/
@Override
@PostMapping("courseview/preview/{id}")
public CoursePublishResult coursePreview(@PathVariable String id) {
String preview = courseService.preview(id);
if (StringUtils.isBlank(preview)) {
return new CoursePublishResult(CommonCode.FAIL, null);
}
return new CoursePublishResult(CommonCode.SUCCESS, preview);
}
CourseService
@Autowired
private CoursePublishConfig coursePublishConfig;
/**
* 課程預覽
*
* @param id 課程id
* @return previewUrl
*/
public String preview(String id) {
// 查詢課程基本信息
Optional<CourseBase> courseBaseOptional = courseBaseRepository.findById(id);
if (!courseBaseOptional.isPresent()) {
ExceptionCast.cast(CourseCode.COURSE_NOT_EXIST);
}
CourseBase courseBase = courseBaseOptional.get();
CmsPage cmsPage = new CmsPage();
cmsPage.setSiteId(coursePublishConfig.getSiteId());
cmsPage.setTemplateId(coursePublishConfig.getTemplateId());
cmsPage.setPageAliase(courseBase.getName());
cmsPage.setPageName(courseBase.getId() + ".html");
cmsPage.setPageWebPath(coursePublishConfig.getPageWebPath());
cmsPage.setPagePhysicalPath(coursePublishConfig.getPagePhysicalPath());
cmsPage.setDataUrl(coursePublishConfig.getDataUrlPre() + courseBase.getId());
CmsPageResult save = cmsPageClient.save(cmsPage);
if (save.isSuccess()) {
return coursePublishConfig.getPreviewUrl() + save.getCmsPage().getPageId();
}
return null;
}
前端修改
我這裏的課程預覽API鏈接事:course/courseview/preview/{id}
。
前端調用的確實:course/preview/{id}
所以這裏我需要修改一下前端API接口地址就OK了,其他內容基本上全部是正常的。
排坑
我調用CmsPageClient
時報錯。
Type definition error: [simple type, class com.xuecheng.framework.domain.cms.response.CmsPageResult]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.xuecheng.framework.domain.cms.response.CmsPageResult`, problem: null
at [Source: (PushbackInputStream); line: 1, column: 558]
因爲這裏我之前編寫CmsPageResult
實體類,忘記添加無參構造了,所以這裏Spring反序列化對象的時候出錯了。
解決:
在CmsPageResult
的類上添加@NoArgsConstructor
或者手寫一個無參構造函數即可。
課程發佈實現
課程發佈後將生成正式的課程詳情頁面,課程發佈後用戶即可瀏覽課程詳情頁面,並開始課程的學習。
需求分析
課程發佈生成課程詳情頁面的流程與課程預覽業務流程相同,如下:
- 用戶進入教學管理中心,進入某個課程的管理界面。
- 點擊課程發佈,前端請求到課程管理服務。
- 課程管理服務遠程調用CMS生成課程發佈頁面,CMS將課程詳情頁面發佈到服務器。
- 課程管理服務修改課程發佈狀態爲“已發佈”,並向前端返回發佈成功。
- 用戶在教學管理中心點擊“課程詳情頁面”鏈接,查看課程詳情頁面內容。
CMS課程發佈接口
響應結果實體類
package com.xuecheng.framework.domain.cms.response;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.model.response.ResultCode;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class CmsPostPageResult extends ResponseResult {
private String pageUrl;
public CmsPostPageResult(ResultCode resultCode, String pageUrl) {
super(resultCode);
this.pageUrl = pageUrl;
}
}
CmsPageControllerApi
新增接口定義
@ApiOperation("頁面一鍵發佈")
CmsPostPageResult postPageQuick(CmsPage cmsPage);
CmsPageController
新增接口實現
/**
* cms page頁面一鍵發佈
*
* @param cmsPage 頁面信息
* @return CmsPostPageResult
*/
@Override
@PostMapping("postPageQuick")
public CmsPostPageResult postPageQuick(@RequestBody CmsPage cmsPage) {
String url = cmsPageService.postPageQuick(cmsPage);
if (StringUtils.isBlank(url)) {
ExceptionCast.cast(CommonCode.FAIL);
}
return new CmsPostPageResult(CommonCode.SUCCESS, url);
}
CmsPageService
/**
* 靜態頁面一鍵發佈
*
* @param cmsPage 頁面信息
* @return 頁面路徑url
*/
public String postPageQuick(CmsPage cmsPage) {
// 保存Cms Page數據
CmsPage save = save(cmsPage);
isNullOrEmpty(save, CommonCode.FAIL);
// 靜態化並保存頁面文件
ResponseResult postPage = postPage(save.getPageId());
if (!postPage.isSuccess()) {
ExceptionCast.cast(CommonCode.FAIL);
}
// 生成url
StringBuffer buffer = new StringBuffer();
Optional<CmsSite> cmsSiteOptional = cmsSiteRepository.findById(save.getSiteId());
CmsSite cmsSite = cmsSiteOptional.orElse(null);
isNullOrEmpty(cmsSite, CommonCode.FAIL);
assert cmsSite != null;
String siteDomain = cmsSite.getSiteDomain();
String siteWebPath = cmsSite.getSiteWebPath();
String pageWebPath = cmsPage.getPageWebPath();
String pageName = cmsPage.getPageName();
return buffer
.append(siteDomain)
.append(siteWebPath)
.append(pageWebPath)
.append(pageName)
.toString();
}
課程發佈接口
Feign Client
新增遠程調用的方法定義
/**
* cms page一鍵發佈
*
* @param cmsPage CMS PAGE信息
* @return CmsPostPageResult
*/
@PostMapping("cms/page/postPageQuick")
CmsPostPageResult postPageQuick(@RequestBody CmsPage cmsPage);
CourseViewControllerApi
新增接口定義
@ApiOperation("課程發佈")
CoursePublishResult coursePublish(String id);
CourseViewController
新增接口實現
/**
* 課程發佈
*
* @param id 課程ID
* @return CoursePublishResult
*/
@Override
@PostMapping("publish/{id}")
public CoursePublishResult coursePublish(@PathVariable String id) {
String publishUrl = courseService.publish(id);
isNullOrEmpty(publishUrl, CommonCode.FAIL);
return new CoursePublishResult(CommonCode.SUCCESS, publishUrl);
}
CourseService
新增方法
@Autowired
private CourseBaseService courseBaseService;
/**
* 課程發佈
*
* @param id 課程ID
* @return 課程頁面路徑url
*/
@Transactional
public String publish(String id) {
// 構造cmsPage信息
CmsPage cmsPage = buildCmsPage(id);
// 發佈
CmsPostPageResult cmsPostPageResult = cmsPageClient.postPageQuick(cmsPage);
if (!cmsPostPageResult.isSuccess()) {
ExceptionCast.cast(CourseCode.COURSE_PUBLISH_VIEWERROR);
}
// 更新課程狀態
saveCoursePubState(id, "202002");
return cmsPostPageResult.getPageUrl();
}
/**
* 更新課程狀態
* 狀態值列表:
* 製作中:202001
* 已發佈:202002
* 已下線:202003
*
* @param courseId 課程ID
* @param status 狀態值
* @return CourseBase
*/
private CourseBase saveCoursePubState(String courseId, String status) {
CourseBase courseBase = courseBaseService.findById(courseId);
//更新發布狀態
courseBase.setStatus(status);
return courseBaseRepository.save(courseBase);
}
/**
* 使用課程ID構造Cms Page信息
*
* @param id 課程ID
* @return CmsPage
*/
private CmsPage buildCmsPage(String id) {
// 查詢課程基本信息
Optional<CourseBase> courseBaseOptional = courseBaseRepository.findById(id);
if (!courseBaseOptional.isPresent()) {
ExceptionCast.cast(CourseCode.COURSE_NOT_EXIST);
}
CourseBase courseBase = courseBaseOptional.get();
CmsPage cmsPage = new CmsPage();
cmsPage.setSiteId(coursePublishConfig.getSiteId());
cmsPage.setTemplateId(coursePublishConfig.getTemplateId());
cmsPage.setPageAliase(courseBase.getName());
cmsPage.setPageName(courseBase.getId() + ".html");
cmsPage.setPageWebPath(coursePublishConfig.getPageWebPath());
cmsPage.setPagePhysicalPath(coursePublishConfig.getPagePhysicalPath());
cmsPage.setDataUrl(coursePublishConfig.getDataUrlPre() + courseBase.getId());
return cmsPage;
}
這裏由於預覽和發佈的時候構造Cms Page的內容是一致的,所以我這裏抽取了一下代碼。
前端
修改course_pub.vue
中的publish
方法,完成調用後查詢一下最新數據就OK了。