学成在线(第9天)课程预览 Eureka Feign
Eureka 注册中心
需求分析
在前后端分离架构中,服务层被拆分成了很多的微服务,微服务的信息如何管理?Spring Cloud中提供服务注册中
心来管理微服务信息。
为什么 要用注册中心?
1、微服务数量众多,要进行远程调用就需要知道服务端的ip地址和端口,注册中心帮助我们管理这些服务的ip和
端口。
2、微服务会实时上报自己的状态,注册中心统一管理这些微服务的状态,将存在问题的服务踢出服务列表,客户
端获取到可用的服务进行调用。
Eureka介绍
Spring Cloud Eureka 是对Netflix公司的Eureka的二次封装,它实现了服务治理的功能,Spring Cloud Eureka提
供服务端与客户端,服务端即是Eureka服务注册中心,客户端完成微服务向Eureka服务的注册与发现。服务端和
客户端均采用Java语言编写。下图显示了Eureka Server与Eureka Client的关系:
1、Eureka Server是服务端,负责管理各各微服务结点的信息和状态。
2 、在微服务上部署Eureka Client程序,远程访问Eureka Server将自己注册在Eureka Server。
3、微服务需要调用另一个微服务时从Eureka Server中获取服务调用地址,进行远程调用。
Eureka Server搭建
高可用环境搭建
Eureka Server 高可用环境需要部署两个Eureka server,它们互相向对方注册。如果在本机启动两个Eureka需要
注意两个Eureka Server的端口要设置不一样,这里我们部署一个Eureka Server工程,将端口可配置,制作两个
Eureka Server启动脚本,启动不同的端口,如下图:
- 在实际使用时Eureka Server至少部署两台服务器,实现高可用。
- 两台Eureka Server互相注册。
- 微服务需要连接两台Eureka Server注册,当其中一台Eureka死掉也不会影响服务的注册与发现。
- 微服务会定时向Eureka server发送心跳,报告自己的状态。
- 微服务从注册中心获取服务地址以RESTful方式发起远程调用。
1、创建xc-govern-center工程:
包结构:com.xuecheng.govern.center
2、添加依赖
在Eureka Server工程添加:
<dependencies> <!‐‐ 导入Eureka服务的依赖 ‐‐> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐netflix‐eureka‐server</artifactId> </dependency> </dependencies>
3、启动类
@EnableEurekaServer//标识这是一个Eureka服务,开启服务 @SpringBootApplication public class GovernCenterApplication { public static void main(String[] args) { SpringApplication.run(GovernCenterApplication.class, args); } }
4、@EnableEurekaServer
需要在启动类上用@EnableEurekaServer标识此服务为Eureka服务
5、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://eureka02:50102/eureka/} server: enable‐self‐preservation: false #是否开启自我保护模式 eviction‐interval‐timer‐in‐ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000) instance: hostname: ${EUREKA_DOMAIN:eureka01}
6、在IDEA中制作启动脚本
启动1:
启动2:
运行两个启动脚本,分别浏览:
http://localhost:50101/
http://localhost:50102/
服务注册
将cms注册到Eureka Server
1、在cms服务中添加依赖
<!‐‐ 导入Eureka客户端的依赖 ‐‐> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐netflix‐eureka‐client</artifactId> </dependency>
2、在application.yml配置
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
3、在启动类上添加注解
在启动类上添加注解 @EnableDiscoveryClient ,表示它是一个Eureka的客户端
4、刷新Eureka Server查看注册情况
Feign 远程调用
在前后端分离架构中,服务层被拆分成了很多的微服务,服务与服务之间难免发生交互,比如:课程发布需要调用
CMS服务生成课程静态化页面,本节研究微服务远程调用所使用的技术。
下图是课程管理服务远程调用CMS服务的流程图:
工作流程如下:
1 、cms服务将自己注册到注册中心。
2、课程管理服务从注册中心获取cms服务的地址。
3、课程管理服务远程调用cms服务。
Ribbon
Ribbon是Netflix公司开源的一个负载均衡的项目(https://github.com/Netflix/ribbon),它是一个基于 HTTP、
TCP的客户端负载均衡器。
1、什么是负载均衡?
负载均衡是微服务架构中必须使用的技术,通过负载均衡来实现系统的高可用、集群扩容等功能。负载均衡可通过
硬件设备及软件来实现,硬件比如:F5、Array等,软件比如:LVS、Nginx等。
如下图是负载均衡的架构图:
2、什么是客户端负载均衡?
上图是服务端负载均衡,客户端负载均衡与服务端负载均衡的区别在于客户端要维护一份服务列表,Ribbon从
Eureka Server获取服务列表,Ribbon根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务。
如下图是Ribbon负载均衡的流程图:
1、在消费微服务中使用Ribbon实现负载均衡,Ribbon先从EurekaServer中获取服务列表。
2、Ribbon根据负载均衡的算法去调用微服务。
Ribbon测试
1、在客户端添加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>
2、配置Ribbon参数
这里在课程管理服务的application.yml中配置ribbon参数
ribbon: MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试 MaxAutoRetriesNextServer: 3 #切换实例的重试次数 OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作 没有实现幂等的情况下是很危险的,所以设置为false ConnectTimeout: 5000 #请求连接的超时时间 ReadTimeout: 6000 #请求处理的超时时间
3、负载均衡测试
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
Feign是Netflix公司开源的轻量级rest客户端,使用Feign可以非常方便的实现Http 客户端。Spring Cloud引入
Feign并且集成了Ribbon实现客户端负载均衡调用。
Feign测试
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>
2、定义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); }
3、启动类添加@EnableFeignClients注解
4、测试
@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 工作原理如下:
1、 启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理
对象
2、 @FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS)即指定了cms的服务名称,Feign会从注册中
心获取cms服务列表,并通过负载均衡算法进行服务调用。
3、在接口方法 中使用注解@GetMapping("/cms/page/get/{id}"),指定调用的url,Feign将根据url进行远程调
用。
课程详情页面静态化
静态页面测试
页面内容组成
我们在编写一个页面时需要知道哪些信息是静态信息,哪些信息为动态信息,下图是页面的设计图:
打开静态页面,观察每部分的内容。
红色表示动态信息,红色以外表示静态信息。
红色动态信息:表示一个按钮,根据用户的登录状态、课程的购买状态显示按钮的名称及按钮的事件。
包括以下信息内容:
1、课程信息
课程标题、价格、课程等级、授课模式、课程图片、课程介绍、课程目录。
2、课程统计信息
课程时长、评分、收藏人数
3、教育机构信息
公司名称、公司简介
4、教育机构统计信息
好评数、课程数、学生人数
5、教师信息
老师名称、老师介绍
静态页面测试
静态资源虚拟主机
1、配置静态资源虚拟主机
静态资源虚拟主机负责处理课程详情、公司信息、老师信息、统计信息等页面的请求:
将课程资料中的“静态页面目录”中的目录拷贝到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测试课程详情页面模板是否可以正
常浏览。
课程数据模型查询接口
接口定义
1、响应结果类型
@Data @ToString @NoArgsConstructor public class CourseView implements Serializable { CourseBase courseBase;//基础信息 CourseMarket courseMarket;//课程营销 CoursePic coursePic;//课程图片 TeachplanNode TeachplanNode;//教学计划 }
2、请求类型
String :课程id
3、接口定义如下
@ApiOperation("课程视图查询") public CourseView courseview(String id);
Dao
需要对course_base、course_market、course_pic、teachplan等信息进行查询,
新建课程营销的dao,其它dao已经存在不用再建。
public interface CourseMarketRepository extends JpaRepository<CourseMarket,String> { }
Service
//课程视图查询 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; }
Controller
@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"; }
模板保存
模板编写并测试通过后要在数据库保存:
1、模板信息保存在xc_cms数据库(mongodb)的cms_template表
2、模板文件保存在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系统提供的页面预览功能,业务流程如下:
1、用户进入课程管理页面,点击课程预览,请求到课程管理服务
2、课程管理服务远程调用cms添加页面接口向cms添加课程详情页面
3、课程管理服务得到cms返回课程详情页面id,并拼接生成课程预览Url
4、课程管理服务将课程预览Url给前端返回
5、用户在前端页面请求课程预览Url,打开新窗口显示课程详情内容
CMS 页面预览测试
CMS已经提供了页面预览功能,课程预览功能要使用CMS页面预览接口实现,下边通过cms页面预览接口测试课
程预览的效果。
1、向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" }
2、课程详细页面 使用ssi注意
由于 Nginx先请求cms的课程预览功能得到html页面,再解析页面中的ssi标签,这里必须保证cms页面预览返回的
页面的Content-Type为text/html;charset=utf-8
在cms页面预览的controller方法中添加:
response.setHeader("Content‐type","text/html;charset=utf‐8");
3、测试
请求:http://www.xuecheng.com/cms/preview/5b3469f794db44269cb2bff1传入页面 Id,测试效果如下:
CMS 添加页面接口
cms服务对外提供添加页面接口,实现:如果不存在页面则添加,否则就更新页面信息。
此接口由课程管理服务在课程预览时调用。
Api接口
@ApiOperation(" 保存页面") public CmsPageResult save(CmsPage cmsPage);
Service
// 添加页面,如果已存在则更新页面 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); } }
Controller
@Override @PostMapping("/save") public CmsPageResult save(@RequestBody CmsPage cmsPage) { return pageService.save(cmsPage); }
课程预览服务端
Api定义
此Api是课程管理前端请求服务端进行课程预览的Api
请求:课程Id
响应:课程预览Url
1、定义响应类型
@Data @ToString @NoArgsConstructor public class CoursePublishResult extends ResponseResult { String previewUrl; public CoursePublishResult(ResultCode resultCode,String previewUrl) { super(resultCode); this.previewUrl = previewUrl; } }
2、接口定义如下
@ApiOperation("预览课程") public CoursePublishResult preview(String id);
创建 Feign Client
在课程管理工程创建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); }
Service
1、配置添加页面参数信息
在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/
2、代码如下:
@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); }
Controller
@Override @PostMapping("/preview/{id}") public CoursePublishResult preview(@PathVariable("id") String id) { return courseService.preview(id); }
页面预览swagger测试
页面预览前端测试