在先后端分离架构中,服务层被拆分红了不少的微服务,微服务的信息如何管理?Spring Cloud中提供服务注册中
心来管理微服务信息。
为何 要用注册中心?
一、微服务数量众多,要进行远程调用就须要知道服务端的ip地址和端口,注册中心帮助咱们管理这些服务的ip和
端口。
二、微服务会实时上报本身的状态,注册中心统一管理这些微服务的状态,将存在问题的服务踢出服务列表,客户
端获取到可用的服务进行调用。css
Spring Cloud Eureka 是对Netflix公司的Eureka的二次封装,它实现了服务治理的功能,Spring Cloud Eureka提
供服务端与客户端,服务端便是Eureka服务注册中心,客户端完成微服务向Eureka服务的注册与发现。服务端和
客户端均采用Java语言编写。下图显示了Eureka Server与Eureka Client的关系:html
一、Eureka Server是服务端,负责管理各各微服务结点的信息和状态。前端
2 、在微服务上部署Eureka Client程序,远程访问Eureka Server将本身注册在Eureka Server。
三、微服务须要调用另外一个微服务时从Eureka Server中获取服务调用地址,进行远程调用。nginx
Eureka Server 高可用环境须要部署两个Eureka server,它们互相向对方注册。若是在本机启动两个Eureka须要
注意两个Eureka Server的端口要设置不同,这里咱们部署一个Eureka Server工程,将端口可配置,制做两个
Eureka Server启动脚本,启动不一样的端口,以下图:git
包结构:com.xuecheng.govern.centergithub
在Eureka Server工程添加:web
<dependencies> <!‐‐ 导入Eureka服务的依赖 ‐‐> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐netflix‐eureka‐server</artifactId> </dependency> </dependencies>
@EnableEurekaServer//标识这是一个Eureka服务,开启服务 @SpringBootApplication public class GovernCenterApplication { public static void main(String[] args) { SpringApplication.run(GovernCenterApplication.class, args); } }
须要在启动类上用@EnableEurekaServer标识此服务为Eureka服务算法
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://eureka02:50102/eureka/} server: enable‐self‐preservation: false #是否开启自我保护模式 eviction‐interval‐timer‐in‐ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000) instance: hostname: ${EUREKA_DOMAIN:eureka01}
启动1:spring
启动2:mongodb
运行两个启动脚本,分别浏览:
http://localhost:50101/
http://localhost:50102/
<!‐‐ 导入Eureka客户端的依赖 ‐‐> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐netflix‐eureka‐client</artifactId> </dependency>
eureka: client: registerWithEureka: true #服务注册开关 fetchRegistry: true #服务发现开关 serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔 defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/} instance: prefer‐ip‐address: true #将本身的ip地址注册到Eureka服务中 ip‐address: ${IP_ADDRESS:127.0.0.1} instance‐id: ${spring.application.name}:${server.port} #指定实例id
在启动类上添加注解 @EnableDiscoveryClient ,表示它是一个Eureka的客户端
在先后端分离架构中,服务层被拆分红了不少的微服务,服务与服务之间不免发生交互,好比:课程发布须要调用
CMS服务生成课程静态化页面,本节研究微服务远程调用所使用的技术。
下图是课程管理服务远程调用CMS服务的流程图:
工做流程以下:
1 、cms服务将本身注册到注册中心。
二、课程管理服务从注册中心获取cms服务的地址。
三、课程管理服务远程调用cms服务。
Ribbon是Netflix公司开源的一个负载均衡的项目(https://github.com/Netflix/ribbon),它是一个基于 HTTP、
TCP的客户端负载均衡器。
负载均衡是微服务架构中必须使用的技术,经过负载均衡来实现系统的高可用、集群扩容等功能。负载均衡可经过
硬件设备及软件来实现,硬件好比:F五、Array等,软件好比:LVS、Nginx等。
以下图是负载均衡的架构图:
上图是服务端负载均衡,客户端负载均衡与服务端负载均衡的区别在于客户端要维护一份服务列表,Ribbon从
Eureka Server获取服务列表,Ribbon根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务。
以下图是Ribbon负载均衡的流程图:
一、在消费微服务中使用Ribbon实现负载均衡,Ribbon先从EurekaServer中获取服务列表。
二、Ribbon根据负载均衡的算法去调用微服务。
一、在客户端添加Ribbon依赖:
这里在课程管理服务配置ribbon依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐ribbon</artifactId> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency>
二、配置Ribbon参数
这里在课程管理服务的application.yml中配置ribbon参数
ribbon: MaxAutoRetries: 2 #最大重试次数,当Eureka中能够找到服务,可是服务连不上时将会重试 MaxAutoRetriesNextServer: 3 #切换实例的重试次数 OkToRetryOnAllOperations: false #对全部操做请求都进行重试,若是是get则能够,若是是post,put等操做 没有实现幂等的状况下是很危险的,因此设置为false ConnectTimeout: 5000 #请求链接的超时时间 ReadTimeout: 6000 #请求处理的超时时间
三、负载均衡测试
1)启动两个cms服务,注意端口要不一致
2)定义RestTemplate,使用@LoadBalanced注解
在课程管理服务的启动类中定义RestTemplate
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); }
3 )测试代码
在课程管理服务工程建立单元测试代码,远程调用cms的查询页面接口:
//负载均衡调用 @Test public void testRibbon() { //服务id String serviceId = "XC‐SERVICE‐MANAGE‐CMS"; for(int i=0;i<10;i++){ //经过服务id调用 ResponseEntity<CmsPage> forEntity = restTemplate.getForEntity("http://" + serviceId + "/cms/page/get/5a754adf6abb500ad05688d9", CmsPage.class); CmsPage cmsPage = forEntity.getBody(); System.out.println(cmsPage); } }
4)负载均衡测试
添加@LoadBalanced注解后,restTemplate会走LoadBalancerInterceptor拦截器,此拦截器中会经过
RibbonLoadBalancerClient查询服务地址,能够在此类打断点观察每次调用的服务地址和端口,两个cms服务会轮
流被调用。
Feign是Netflix公司开源的轻量级rest客户端,使用Feign能够很是方便的实现Http 客户端。Spring Cloud引入
Feign而且集成了Ribbon实现客户端负载均衡调用。
1 、在客户端添加依赖
在课程管理服务添加下边的依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐openfeign</artifactId> </dependency> <dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign‐okhttp</artifactId> </dependency>
二、定义FeignClient接口
参考Swagger文档定义FeignClient,注意接口的Url、请求参数类型、返回值类型与Swagger接口一致。
在课程管理服务中建立client包,定义查询cms页面的客户端该用接口,
@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS) public interface CmsPageClient { @GetMapping("/cms/page/get/{id}") public CmsPage findById(@PathVariable("id") String id); }
三、启动类添加@EnableFeignClients注解
四、测试
@RunWith(SpringRunner.class) @SpringBootTest public class FeignTest { @Autowired CmsPageClient cmsPageClient; @Test public void testFeign() { //经过服务id调用cms的查询页面接口 CmsPage cmsPage = cmsPageClient.findById("5a754adf6abb500ad05688d9"); System.out.println(cmsPage); } }
Feign 工做原理以下:
一、 启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理
对象
二、 @FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS)即指定了cms的服务名称,Feign会从注册中
心获取cms服务列表,并经过负载均衡算法进行服务调用。
三、在接口方法 中使用注解@GetMapping("/cms/page/get/{id}"),指定调用的url,Feign将根据url进行远程调
用。
咱们在编写一个页面时须要知道哪些信息是静态信息,哪些信息为动态信息,下图是页面的设计图:
打开静态页面,观察每部分的内容。
红色表示动态信息,红色之外表示静态信息。
红色动态信息:表示一个按钮,根据用户的登陆状态、课程的购买状态显示按钮的名称及按钮的事件。
包括如下信息内容:
一、课程信息
课程标题、价格、课程等级、授课模式、课程图片、课程介绍、课程目录。
二、课程统计信息
课程时长、评分、收藏人数
三、教育机构信息
公司名称、公司简介
四、教育机构统计信息
好评数、课程数、学生人数
五、教师信息
老师名称、老师介绍
一、配置静态资源虚拟主机
静态资源虚拟主机负责处理课程详情、公司信息、老师信息、统计信息等页面的请求:
将课程资料中的“静态页面目录”中的目录拷贝到F:/develop/xuecheng/static下
在nginx中配置静态虚拟主机以下:
学成网静态资源 server { listen 91; server_name localhost; #公司信息 location /static/company/ { alias F:/develop/xuecheng/static/company/; } #老师信息 location /static/teacher/ { alias F:/develop/xuecheng/static/teacher/; } #统计信息 location /static/stat/ { alias F:/develop/xuecheng/static/stat/; } location /course/detail/ { alias F:/develop/xuecheng/static/course/detail/; } }
2 、经过www.xuecheng.com虚拟主机转发到静态资源
因为课程页面须要经过SSI加载页头和页尾因此须要经过www.xuecheng.com虚拟主机转发到静态资源
在www.xuecheng.com虚拟主机加入以下配置:
location /static/company/ { proxy_pass http://static_server_pool; } location /static/teacher/ { proxy_pass http://static_server_pool; } location /static/stat/ { proxy_pass http://static_server_pool; } location /course/detail/ { proxy_pass http://static_server_pool; }
配置upstream实现请求转发到资源服务虚拟主机:
#静态资源服务 upstream static_server_pool{ server 127.0.0.1:91 weight=10; }
门户中的一些图片、样式等静态资源统一经过/static路径对外提供服务,在www.xuecheng.com虚拟主机中配置如
下:
#静态资源,包括系统所须要的图片,js、css等静态资源 location /static/img/ { alias F:/develop/xc_portal_static/img/; } location /static/css/ { alias F:/develop/xc_portal_static/css/; } location /static/js/ { alias F:/develop/xc_portal_static/js/; } location /static/plugins/ { alias F:/develop/xc_portal_static/plugins/; add_header Access‐Control‐Allow‐Origin http://ucenter.xuecheng.com; add_header Access‐Control‐Allow‐Credentials true; add_header Access‐Control‐Allow‐Methods GET; }
cors 跨域参数:
Access-Control-Allow-Origin:容许跨域访问的外域地址
若是容许任何站点跨域访问则设置为*,一般这是不建议的。
Access-Control-Allow-Credentials: 容许客户端携带证书访问
Access-Control-Allow-Methods:容许客户端跨域访问的方法
请求:http://www.xuecheng.com/course/detail/course_main_template.html测试课程详情页面模板是否能够正
常浏览。
一、响应结果类型
@Data @ToString @NoArgsConstructor public class CourseView implements Serializable { CourseBase courseBase;//基础信息 CourseMarket courseMarket;//课程营销 CoursePic coursePic;//课程图片 TeachplanNode TeachplanNode;//教学计划 }
二、请求类型
String :课程id
三、接口定义以下
@ApiOperation("课程视图查询") public CourseView courseview(String id);
须要对course_base、course_market、course_pic、teachplan等信息进行查询,
新建课程营销的dao,其它dao已经存在不用再建。
public interface CourseMarketRepository extends JpaRepository<CourseMarket,String> { }
//课程视图查询 public CourseView getCoruseView(String id) { CourseView courseView = new CourseView(); //查询课程基本信息 Optional<CourseBase> optional = courseBaseRepository.findById(id); if(optional.isPresent()){ CourseBase courseBase = optional.get(); courseView.setCourseBase(courseBase); } //查询课程营销信息 Optional<CourseMarket> courseMarketOptional = courseMarketRepository.findById(id); if(courseMarketOptional.isPresent()){ CourseMarket courseMarket = courseMarketOptional.get(); courseView.setCourseMarket(courseMarket); } //查询课程图片信息 Optional<CoursePic> picOptional = coursePicRepository.findById(id); if(picOptional.isPresent()){ CoursePic coursePic = picOptional.get(); courseView.setCoursePic(picOptional.get()); } //查询课程计划信息 TeachplanNode teachplanNode = teachplanMapper.selectList(id); courseView.setTeachplanNode(teachplanNode); return courseView; }
@Override @GetMapping("/courseview/{id}") public CourseView courseview(@PathVariable("id") String id) { return courseService.getCoruseView(id); }
使用test-freemarker工程测试模板
编写模板过程采用test-freemarker工程测试模板。
将course.ftl拷贝到test-freemarker工程的resources/templates下,并在test-freemarker工程的controller中添加
测试方法
//课程详情页面测试 @RequestMapping("/course") public String course(Map<String,Object> map){ ResponseEntity<Map> forEntity = restTemplate.getForEntity("http://localhost:31200/course/courseview/4028e581617f945f01617f9dabc4 0000", Map.class); Map body = forEntity.getBody(); map.put("model",body); return "course"; }
模板编写并测试经过后要在数据库保存:
一、模板信息保存在xc_cms数据库(mongodb)的cms_template表
二、模板文件保存在mongodb的GridFS中。
第一步:将模板文件上传到GridFS中
因为本教学项目中模板管理模块没有开发,因此咱们使用Junit代码向GridFS中保存:
// 文件存储2 @Test public void testStore2() throws FileNotFoundException { File file = new File("C:\\Users\\admin\\Desktop\\course.ftl"); FileInputStream inputStream = new FileInputStream(file); //保存模版文件内容 GridFSFile gridFSFile = gridFsTemplate.store(inputStream, "课程详情模板文件",""); String fileId = gridFSFile.getId().toString(); System.out.println(fileId); }
保存成功须要记录模板文件的id,即上边代码中的fileId。
第二步:向cms_template表添加模板记录(请不要重复添加)
使用Studio 3T链接mongodb,向cms_template添加记录:
{ "_class" : "com.xuecheng.framework.domain.cms.CmsTemplate", "siteId" : "5a751fab6abb5044e0d19ea1", "templateName" : "课程详情页面正式模板", "templateFileId" : "这里填写上边代码返回的模板文件id" }
课程预览功能将使用cms系统提供的页面预览功能,业务流程以下:
一、用户进入课程管理页面,点击课程预览,请求到课程管理服务
二、课程管理服务远程调用cms添加页面接口向cms添加课程详情页面
三、课程管理服务获得cms返回课程详情页面id,并拼接生成课程预览Url
四、课程管理服务将课程预览Url给前端返回
五、用户在前端页面请求课程预览Url,打开新窗口显示课程详情内容
CMS已经提供了页面预览功能,课程预览功能要使用CMS页面预览接口实现,下边经过cms页面预览接口测试课
程预览的效果。
一、向cms_page表插入一条页面记录或者从cms_page找一个页面进行测试。
注意:页面配置必定要正确,需设置正确的模板id和dataUrl。
以下,是一条页面的记录。
{ "_id" : ObjectId("5b3469f794db44269cb2bff1"), "_class" : "com.xuecheng.framework.domain.cms.CmsPage", "siteId" : "5a751fab6abb5044e0d19ea1", "pageName" : "4028e581617f945f01617f9dabc40000.html", "pageAliase" : "课程详情页面测试01", "pageWebPath" : "/course/detail/", "pagePhysicalPath" : "/course/detail/", "pageType" : "1", "pageCreateTime" : ISODate("2018‐02‐25T01:37:25.974+0000"), "templateId" : "5b345a6b94db44269cb2bfec", "dataUrl" : "http://localhost:31200/course/courseview/4028e581617f945f01617f9dabc40000" }
二、课程详细页面 使用ssi注意
因为 Nginx先请求cms的课程预览功能获得html页面,再解析页面中的ssi标签,这里必须保证cms页面预览返回的
页面的Content-Type为text/html;charset=utf-8
在cms页面预览的controller方法中添加:
response.setHeader("Content‐type","text/html;charset=utf‐8");
三、测试
请求:http://www.xuecheng.com/cms/preview/5b3469f794db44269cb2bff1传入页面 Id,测试效果以下:
cms服务对外提供添加页面接口,实现:若是不存在页面则添加,不然就更新页面信息。
此接口由课程管理服务在课程预览时调用。
@ApiOperation(" 保存页面") public CmsPageResult save(CmsPage cmsPage);
// 添加页面,若是已存在则更新页面 public CmsPageResult save(CmsPage cmsPage){ //校验页面是否存在,根据页面名称、站点Id、页面webpath查询 CmsPage cmsPage1 = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath()); if(cmsPage1 !=null){ //更新 return this.update(cmsPage1.getPageId(),cmsPage); }else{ //添加 return this.add(cmsPage); } }
@Override @PostMapping("/save") public CmsPageResult save(@RequestBody CmsPage cmsPage) { return pageService.save(cmsPage); }
此Api是课程管理前端请求服务端进行课程预览的Api
请求:课程Id
响应:课程预览Url
一、定义响应类型
@Data @ToString @NoArgsConstructor public class CoursePublishResult extends ResponseResult { String previewUrl; public CoursePublishResult(ResultCode resultCode,String previewUrl) { super(resultCode); this.previewUrl = previewUrl; } }
二、接口定义以下
@ApiOperation("预览课程") public CoursePublishResult preview(String id);
在课程管理工程建立CMS服务的Feign Client,经过此Client远程请求cms添加页面。
@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS) public interface CmsPageClient{ //保存页面 @PostMapping("/cms/page/save") public CmsPageResult save(@RequestBody CmsPage cmsPage); }
一、配置添加页面参数信息
在application.yml中配置:
course‐publish: siteId: 5b30cba5f58b4411fc6cb1e5 templateId: 5b345a6b94db44269cb2bfec previewUrl: http://www.xuecheng.com/cms/preview/ pageWebPath: /course/detail/ pagePhysicalPath: /course/detail/ dataUrlPre: http://localhost:31200/course/courseview/
二、代码以下:
@Value("${course‐publish.dataUrlPre}") private String publish_dataUrlPre; @Value("${course‐publish.pagePhysicalPath}") private String publish_page_physicalpath; @Value("${course‐publish.pageWebPath}") private String publish_page_webpath; @Value("${course‐publish.siteId}") private String publish_siteId; @Value("${course‐publish.templateId}") private String publish_templateId; @Value("${course‐publish.previewUrl}") private String previewUrl; //根据id查询课程基本信息 public CourseBase findCourseBaseById(String courseId){ Optional<CourseBase> baseOptional = courseBaseRepository.findById(courseId); if(baseOptional.isPresent()){ CourseBase courseBase = baseOptional.get(); return courseBase; } ExceptionCast.cast(CourseCode.COURSE_GET_NOTEXISTS); return null; } //课程预览 public CoursePublishResult preview(String courseId){ CourseBase one = this.findCourseBaseById(courseId); //发布课程预览页面 CmsPage cmsPage = new CmsPage(); //站点 cmsPage.setSiteId(publish_siteId);//课程预览站点 //模板 cmsPage.setTemplateId(publish_templateId); //页面名称 cmsPage.setPageName(courseId+".html"); //页面别名 cmsPage.setPageAliase(one.getName()); //页面访问路径 cmsPage.setPageWebPath(publish_page_webpath); //页面存储路径 cmsPage.setPagePhysicalPath(publish_page_physicalpath); //数据url cmsPage.setDataUrl(publish_dataUrlPre+courseId); //远程请求cms保存页面信息 CmsPageResult cmsPageResult = cmsPageClient.save(cmsPage); if(!cmsPageResult.isSuccess()){ return new CoursePublishResult(CommonCode.FAIL,null); } //页面id String pageId = cmsPageResult.getCmsPage().getPageId(); //页面url String pageUrl = previewUrl+pageId; return new CoursePublishResult(CommonCode.SUCCESS,pageUrl); }
@Override @PostMapping("/preview/{id}") public CoursePublishResult preview(@PathVariable("id") String id) { return courseService.preview(id); }
页面预览swagger测试
页面预览前端测试