前端技术
HTML
CSS
Javascript
前端框架和UI库
VUE
ReactJS
AngularJS
JQuery
NodeJS
JSON
Element-UI
Bootstrap
Material UI
服务端和客户端
Java
Python
PHP
Golang
Scala
Kotlin
Groovy
Ruby
Lua
.net
c#
c++
后端WEB和工程框架
SpringBoot
SpringCloud
Struts2
MyBatis
Hibernate
Tornado
Beego
Go-Spring
Go Gin
Go Iris
Dubbo
HessianRPC
Maven
Gradle
数据库
MySQL
Oracle
Mongo
中间件与web容器
Redis
MemCache
Etcd
Cassandra
Kafka
RabbitMQ
RocketMQ
ActiveMQ
Nacos
Consul
Tomcat
Nginx
Netty
大数据技术
Hive
Impala
ClickHouse
DorisDB
Greenplum
PostgreSQL
HBase
Kylin
Hadoop
Apache Pig
ZooKeeper
SeaTunnel
Sqoop
Datax
Flink
Spark
Mahout
数据搜索与日志
ElasticSearch
Apache Lucene
Apache Solr
Kibana
Logstash
数据可视化与OLAP
Apache Atlas
Superset
Saiku
Tesseract
系统与容器
Linux
Shell
Docker
Kubernetes
[方法调用]的搜索结果
这里是文章列表。热门标签的颜色随机变换,标签颜色没有特殊含义。
点击某个标签可搜索标签相关的文章。
点击某个标签可搜索标签相关的文章。
SpringCloud
...tion这个类里面的方法时,实际上它玩的是代理对象的小把戏,就是在调用代理对象的方法呢。 在这个过程中,Spring做了两件事情: 1. 保存原始类的引用 在创建代理对象的时候,Spring会保存原始类的引用,以便在需要的时候能够恢复到原始类。这是因为代理对象就像是原始类的一个分身小弟,它代替原始类执行任务。但如果我们让它完全取代了原始类这位“大哥”,那我们可就摸不着头脑了,没法再去调用原始类那些特有的方法和属性了。 2. 添加拦截器 在创建代理对象的时候,Spring还会添加一些拦截器。这些拦截器会在代理对象执行方法之前和之后做一些额外的操作。比如说,我们可以插一个拦截器,就像一个小秘书那样,专门记录下每次方法被调用的具体时间。这样一来,我们就能像看手表一样,实时掌握系统的运行效率和性能状况了。 这就是@Configuration类被代理的基本原理。下面我们来看一个具体的例子。 四、实战演示 假设我们有一个@Service类,它里面有一些业务逻辑。现在呢,我们想要实时地盯着这些业务逻辑的运行状况,就像有个小雷达一样随时监测。所以,咱们琢磨了一下,决定动手用Spring的那个强大的AOP功能,来帮我们达成这个小心愿。不过,在配置的过程中,我们碰到了个不大不小的难题,那就是咱们还没搞清楚到底该在哪些环节巧妙地插入AOP的切面。这时,我们就需要用到@Configuration类了。 在@Configuration类中,我们可以添加一个@Bean注解来声明一个Bean。而在@Bean注解后面,我们可以添加一个方法来返回这个Bean。那么,如果我们想要给这个Bean添加一个切面,我们应该怎么做呢? 这时,我们就需要用到Spring的AOP功能了。我们可以用@Aspect这个小家伙来标记一个切面,接着再通过@Pointcut这个小帮手来确定我们要切入的具体位置。就像是在编程的世界里画了个“切割符号”,先声明“我要处理哪一类事情”(切面),再具体指定“在哪儿动手做”(切点)。最后,我来给你说个有趣的事情,我们可以用一个叫@Around的神奇小标签,给它定义一个“通知员”的角色。每当找到符合条件的方法要开始执行或者已经执行完毕时,这位“通知员”就会自动出场,前后忙活起来。 然后,我们将这个切面注入到Spring的ApplicationContext中,这样就可以在运行的时候使用这个切面了。 五、总结 @Configuration类被代理是Spring的一种重要特性,它为我们提供了一种方便的方式来管理和配置Bean。了解了@Configuration类被代理的原理后,咱们就能更深入地掌握Spring的AOP功能,而且能够随心所欲地运用@Configuration类来满足咱们的各种需求,让编程变得更加游刃有余。
2023-10-23 20:18:43
128
海阔天空_t
SpringCloud
...- 网络延迟:服务间调用时,由于网络环境不稳定或拥塞,请求可能无法在设定的时间内到达目标服务。 - 服务处理耗时过长:被调用的服务端逻辑复杂、资源消耗大,导致无法在预设的响应时间内完成处理并返回结果。 - 线程池不足:服务端处理请求的线程池大小设置不当,导致请求堆积,无法及时处理。 3. SpringCloud中的超时配置及优化策略 (1) Hystrix超时设置 Hystrix是SpringCloud中用于实现服务容错和隔离的重要组件。我们可以通过调整hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds属性来设定命令执行的超时时间: java // application.yml hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 5000 设置超时时间为5秒 (2) Ribbon客户端超时配置 Ribbon是SpringCloud中的客户端负载均衡器,它允许我们为HTTP请求设置连接超时(ConnectTimeout)和读取超时(ReadTimeout): java @Configuration public class RibbonConfiguration { @Bean publicribbon: ReadTimeout: 2000 设置读取超时时间为2秒 ConnectTimeout: 1000 设置连接超时时间为1秒 } } (3) 服务端性能优化 对于服务处理耗时过长的问题,我们需要对服务进行性能优化,如数据库查询优化、缓存使用、异步处理等。例如,我们可以利用@Async注解实现异步方法调用: java @Service public class SomeService { @Async public Future timeConsumingTask() { // 这是一个耗时的操作... return new AsyncResult<>("Task result"); } } 4. 系统设计层面的思考与探讨 除了上述具体配置和优化措施外,我们也需要从系统设计角度去预防和应对超时问题。比如,咱们可以像安排乐高积木一样,把各个服务间的调用关系巧妙地搭建起来,别让它变得太绕太复杂。同时呢,咱也要像精打细算的管家,充分揣摩每个服务的“饭量”(QPS和TPS)大小,然后据此给线程池调整合适的“碗筷”数量,再定个合理的“用餐时间”(超时阈值)。再者,就像在电路中装上保险丝、开关控制电流那样,我们可以运用熔断、降级、限流这些小妙招,确保整个系统的平稳运行,随时都能稳定可靠地为大家服务。 5. 结语 总之,面对SpringCloud应用中的“超时”问题,我们应根据实际情况,采取针对性的技术手段和策略,从配置、优化和服务设计等多个维度去解决问题。这个过程啊,可以说是挑战满满,但这也恰恰是技术最吸引人的地方——就是要不断去摸索、持续改进,才能打造出一套既高效又稳定的微服务体系。就像是盖房子一样,只有不断研究和优化设计,才能最终建成一座稳固又实用的大厦。而这一切的努力,最终都会化作用户满意的微笑和体验。
2023-04-25 12:09:08
39
桃李春风一杯酒
Scala
...cala允许我们通过方法定义的方式,将自定义类的特定行为与已有的运算符关联起来,这就是运算符重载。下面,让我们以轻松愉快、充满探索精神的方式一步步揭开Scala运算符重载的神秘面纱,并通过一系列实例展示其具体应用。 2. Scala中的运算符本质 在Scala中,你可能已经注意到许多看起来像运算符的东西实际上就是方法调用。例如,+通常用于加法,但在字符串间则是连接操作。这是因为Scala将这些符号视为方法名的一部分,如a + b实际上是调用了a.+(b)。这就意味着,只要你愿意,你完全可以在自定义的类里面创建一个叫+的方法,这样一来,这个运算符就被我们赋予了新的含义和功能,实现了重载,让它能按照我们的想法去工作。就像是给数学里的加号换了个个性化的“面具”,让它在特定场合下执行特殊任务一样。 3. 运算符重载示例一 自定义向量类的加法 首先,假设我们创建了一个简单的二维向量类: scala class Vector2D(x: Double, y: Double) { def +(that: Vector2D): Vector2D = new Vector2D(this.x + that.x, this.y + that.y) } 上述代码中,我们为Vector2D类定义了一个+方法,它接受另一个Vector2D对象作为参数,并返回一个新的Vector2D对象,代表两个向量相加的结果。这样一来,当我们写v1 + v2时,实际上是在调用v1.+(v2),实现了对加法运算符的重载。 4. 运算符重载示例二 自定义复杂度比较 接下来,我们看一个更复杂的例子,比如我们想在自定义的“任务”类中,用 < 符号来表示任务的优先级比较: scala class Task(val priority: Int, val description: String) { def <(that: Task): Boolean = this.priority < that.priority } val task1 = new Task(3, "Do laundry") val task2 = new Task(1, "Feed the cat") if (task1 < task2) println(s"${task1.description} has higher priority!") 在这个例子中,我们定义了一个<方法,用于比较两个任务的优先级。所以,在条件判断的时候,task1 < task2已经不是老套的字节码或者整数之间的较量了,而是按照我们自定义的方式来决定谁该排前面,谁该让位。这就像是我们在玩一场游戏,规则由我们自己定,哪个任务优先级更高,不再是由它们本身的数字大小说了算,而是看我们怎么给它们排座次。 5. 小结与思考 通过以上两个实例,我们可以看到Scala的运算符重载是如何让我们能够根据实际需求重新定义运算符的行为。这个特点让代码变得更加简单易懂,就像咱们人类一瞧就明白的那样,而且还给代码表达力来了个大升级,让它更能“说”出程序员的心声。 但值得注意的是,虽然运算符重载能极大提高代码的可读性和编写效率,但也可能导致潜在的混淆。所以,在我们设计和实现的时候,得悠着点儿选择什么时候、怎么去搞运算符重载这事儿。重点是,咱得保证这个重载后的运算符行为跟原本那个运算符的基本含义保持逻辑上的一致性,这样一来,其他开发者瞅见了也能秒懂,方便他们后续的维护工作。 总结一下,Scala中重载运算符的过程其实就是在自定义类中定义相应名称的方法,通过这种方式,我们可以使运算符服务于特定场景,进一步提升代码的灵活性和表现力。希望这篇讲得既透彻又易懂的文章,能实实在在地在你未来的Scala编程冒险中,助你更溜地运用运算符重载这个超级给力的工具,让编程变得更轻松有趣。
2023-04-15 13:42:55
137
繁华落尽
Javascript
...undefined去调用一个方法或访问一个属性时,JavaScript引擎会抛出上述错误。哎呀,你知道吗?在JavaScript的世界里,null和undefined就像是一些空空如也的盒子。你不能指望从这些盒子里拿出什么东西来用,对吧?比如说,你打算用它们做点什么运算或者访问某个属性,但JavaScript可不知道该拿这些空盒子怎么办。所以,当它尝试去处理这些空空如也的东西时,就会出现错误或者奇怪的行为。这就是为什么我们说null和undefined表示“无值”的原因了。它们就像是编程中的空白页,需要我们用实际的数据来填充。 2. 理解null和undefined - null:通常用于表示变量已经被赋值为“空”或“没有值”。它是一个特殊的值,用于明确表示某个变量或引用的对象不存在。 - undefined:当一个变量未被初始化时,其默认值就是undefined。此外,函数的参数在调用函数之前也是undefined。 3. 代码示例 理解错误原因 假设我们有一个函数getInfo,用于获取用户信息: javascript function getInfo(userId) { return users[userId]; } const users = {}; console.log(getInfo(1)); // undefined, 因为users中没有id为1的用户 这里,由于users对象中不存在userId对应的键,因此getInfo返回的是undefined。如果我们在使用这个函数时直接使用getInfo()(即传入null或undefined),会发生什么呢? javascript console.log(getInfo(null)); // TypeError: Cannot read properties of null (reading 'userId') 4. 避免错误的策略 4.1 使用条件判断 在调用可能返回null或undefined的方法前,先检查是否为null或undefined: javascript function safeGetInfo(userId) { if (userId !== null && userId !== undefined) { return users[userId]; } else { console.log("User ID not found."); return null; // 或者抛出异常,取决于你的应用需求 } } console.log(safeGetInfo(1)); // 正常返回用户信息 console.log(safeGetInfo(null)); // 输出警告信息并返回null 4.2 使用默认值 在访问属性时,可以使用?.操作符(三元点)或.()(括号访问)来避免错误: javascript const user = users[1] ?? "User not found"; // 使用三元点操作符 // 或者 const user = users[1] || "User not found"; // 使用逻辑或运算符 // 或者使用括号访问 const user = users[(userId === null || userId === undefined) ? "User not found" : userId]; 4.3 使用try...catch块 对于更复杂的逻辑,可以使用try...catch结构来捕获并处理错误: javascript try { const user = users[userId]; } catch (error) { console.error("An error occurred:", error); } 5. 结语 面对“TypeError: null 或 undefined 不能作为对象使用”这样的错误,关键在于理解null和undefined的本质以及它们在JavaScript中的作用。嘿,兄弟!要想避免那些烦人的错误,咱们就得在代码上下点功夫了。比如说,咱们可以用条件判断来分清楚啥时候该做啥,啥时候不该动。再比如,设置个默认值,让程序知道如果啥都没给,就用这个值顶替,免得因为参数没填出问题。还有,咱们别忘了加个错误处理机制,万一程序遇到啥意外,咱就能及时捕捉到,不让它胡乱操作,把事儿搞砸了。这样,咱们的代码就更稳健,更不容易出岔子了!嘿,兄弟!每次你碰到点小错误,那可不就是一次大大的学习机会嘛!就像是在玩游戏时不小心踩了个坑,结果发现了一个新宝藏!你得动手实践,多想想为什么会这样,下次怎么避免。就像你做菜时,多试几次,找到那个完美的味道一样。这样一步步走来,你编程的路就会越走越稳,越来越自信!
2024-07-27 15:32:00
299
醉卧沙场
HessianRPC
...一种轻量级的远程过程调用协议被广泛应用。然而,在实际动手开发的时候,我们免不了会撞上一个常见的小插曲:“IllegalArgumentException”这个家伙,它跑出来告诉你:喂喂,你传的参数不达标,比如说,方法签名跟我期待的样子对不上号。这篇东西会手把手地,用详尽的步骤解析和实实在在的例子演示,让大家都能更接地气地理解,更能轻松上手解决这个问题。 1. HessianRPC简介 首先,让我们回顾一下HessianRPC的基本概念。Hessian是一种高效、紧凑的二进制RPC协议,由Caucho公司开发,特别适合于互联网传输。这个东西超级实用,它能够让Java和其他一些好兄弟语言(比如.NET、Python这些)毫无障碍地远程互相调用对方的方法,就跟在本地调用一样方便。你只需要稍微捣鼓一下配置,写点简单的代码,就能轻松实现服务端和客户端的顺畅交流啦! 2. 方法签名的重要性 在HessianRPC中,每个远程方法都有其独特的“方法签名”,它包括方法名以及参数类型列表。当客户端调用服务器端的方法时,Hessian会根据这个签名来匹配和校验参数。如果客户端传过来的参数“不按套路出牌”,跟服务器端方法要求的参数类型或数量对不上号,那可就得闹脾气了,会直接抛出一个“IllegalArgumentException”异常。 java // 服务器端接口示例 public interface MyService { String process(String input, int num); } // 客户端错误调用示例 MyService service = (MyService) hessianProxyFactory.create(MyService.class, serverUrl); String result = service.process("Hello", "World"); // 这里第二个参数应该是int类型,而非String类型,会导致IllegalArgumentException 3. “IllegalArgumentException:传入参数不合法”问题解析 上述代码中的客户端尝试以一个字符串参数代替整型参数去调用process方法,这就导致了"IllegalArgumentException"。在进行序列化和反序列化的时候,Hessian这家伙发现传过来的参数类型跟预先给定的方法签名对不上号儿,于是它就毫不客气地抛出了一个异常。 4. 解决方案及预防措施 面对这种问题,我们需要从以下几个方面着手: 4.1 检查并确保参数类型正确 在编写客户端调用代码时,应仔细核对每个参数是否符合服务端方法签名的要求。比如上例中,我们需要将第二个参数修改为整型数值: java String result = service.process("Hello", 123); // 正确的调用方式 4.2 强化代码审查与测试 在项目开发过程中,建议采用自动化测试工具和单元测试,覆盖所有RPC方法调用,确保参数类型的准确无误。同时,代码审查也是防止此类问题的有效手段。 4.3 提供清晰的API文档 对于对外提供的服务接口,应该编写详尽且易于理解的API文档,明确指出每个方法的签名,包括方法名、参数类型和返回值类型,以便开发者在调用时有据可依。 4.4 利用IDE的智能提示 现代集成开发环境(IDE)如IntelliJ IDEA或Eclipse都具有强大的智能提示功能,能自动识别和匹配方法签名,利用好这些特性也能有效避免参数类型不匹配的问题。 总结起来,遭遇HessianRPC的“IllegalArgumentException:传入参数不合法”异常,本质上是对方法签名的理解和使用不到位的结果。在编程实战中,只要我们足够细心、步步为营,像侦探破案那样运用各种工具和策略,完全可以把这些潜在问题扼杀在摇篮里,让系统的运行稳如磐石。记住了啊,解决任何技术难题都得像咱们看侦探小说那样,得瞪大眼睛仔仔细细地观察,用脑子冷静地分析推理,动手实践去验证猜想,最后才能拨开层层迷雾,看到那片晴朗的蓝天。
2024-01-16 09:18:32
542
风轻云淡
Lua
...receive()方法调用后,都会检查返回的错误信息是否为"closed",如果是,则表明网络连接已经被关闭,此时我们会打印出相应的提示信息,并采取相应措施(如关闭连接)。 4. 理解与探讨 在实际项目开发中,应对ClosedNetworkConnectionError的策略往往更加复杂多样。比如,我们能给程序装个“回马枪”功能,一旦发现连接断了,它就自动尝试再连上;甚至还能让它变得更聪明些,比如说在网络抽风的时候先把要发的数据存起来,等网络恢复了,再把这些数据顺顺当当地发送出去。 这就涉及到开发者对网络通信原理的理解深度以及业务需求的细致把控,同时也要求我们具备良好的异常处理习惯和鲁棒性编程思维。记住了啊,真正厉害的程序员,可不只是会写能跑起来的代码那么简单。他们更明白,在编程的世界里,就像生活一样,总会有些意想不到的状况和稀奇古怪的异常情况冒出来,而他们就有那个本事,把这些麻烦事儿处理得既漂亮又从容,这才是高手风范! 总的来说,面对Lua编程中的ClosedNetworkConnectionError,我们需要保持敏锐的洞察力,合理运用Lua及其扩展库的功能特性,结合具体应用场景,灵活制定和实施有效的错误处理策略,才能确保我们的应用程序在网络世界中稳定、可靠地运行。
2023-11-24 17:48:02
132
月影清风
Tomcat
...情况、CPU使用率、方法调用次数和时间等信息。文章中提到的Profiler工具,如VisualVM和JProfiler,可以帮助开发者检测出哪些对象占用了大量内存,以及这些对象是如何生成的,从而帮助定位和解决内存泄漏问题。 异步处理 , 异步处理是一种编程模型,允许程序在等待某些耗时操作(如网络请求、文件读写、数据库查询等)完成时,继续执行其他任务。这种方式可以避免程序在等待过程中阻塞,提高程序的响应速度和吞吐量。文章中提到的异步处理,通过创建新的线程来执行耗时操作,使得主线程可以继续执行其他任务,从而减少线程阻塞,提升系统性能。
2025-01-07 16:14:31
34
草原牧歌
Scala
...Scala程序能直接调用Java的各种库,反过来也一样。这就像是两个好朋友可以随时互相串门聊天一样方便!此外,Scala语法设计上借鉴了许多Java元素,例如类定义和方法调用等。这些相似之处让开发者在从Java转到Scala时感觉更轻松,甚至可以在同一个项目里同时用这两种语言,完全没有问题。 代码示例: scala // 在Scala中调用Java静态方法 import java.lang.Math._ val result = sqrt(25) println(s"Square root of 25 is $result") // 输出:Square root of 25 is 5.0 2. 面向对象编程中的兼容性挑战 尽管Scala支持面向对象编程,但它对类的继承和接口的实现方式与Java有所不同。这可能导致一些开发者在初次尝试将Java代码转换为Scala时遇到困难。 代码示例: java // Java接口定义 public interface Animal { void makeSound(); } // Java类实现接口 public class Dog implements Animal { @Override public void makeSound() { System.out.println("Woof!"); } } 转换到Scala: scala // Scala trait定义(类似于Java的接口) trait Animal { def makeSound(): Unit } // Scala类实现trait class Dog extends Animal { override def makeSound(): Unit = println("Woof!") } 3. 函数式编程带来的新问题 Scala的一大特色是其强大的函数式编程支持,包括高阶函数、模式匹配等功能。然而,这些功能在Java中要么不存在,要么难以实现。所以嘛,当你搞那些复杂的函数式编程时,Scala和Java混着用就会变得有点儿头大。 代码示例: scala // Scala高阶函数示例 def applyFunction(f: Int => Int, x: Int): Int = f(x) val square = (x: Int) => x x println(applyFunction(square, 5)) // 输出:25 相比之下,Java的函数式编程支持则需要借助Lambda表达式或方法引用: java import java.util.function.Function; public class Main { public static void main(String[] args) { Function square = x -> x x; System.out.println(applyFunction(square, 5)); // 输出:25 } public static int applyFunction(Function f, int x) { return f.apply(x); } } 4. 解决方案与最佳实践 为了克服上述兼容性挑战,我们可以采取以下几种策略: - 谨慎选择API:优先使用那些具有良好跨语言支持的库。 - 逐步迁移:对于大型项目,可以考虑逐步将Java代码迁移到Scala,而不是一次性全部替换。 - 利用工具辅助:有些工具和框架可以帮助简化两种语言之间的交互,如Akka,它允许开发者使用Scala或Java编写Actor模型的应用程序。 结语:兼容性是桥梁,而非障碍 虽然Scala与Java之间存在一定的兼容性挑战,但正是这些挑战促使开发者不断学习和创新。搞清楚这两种语言的异同,然后用点巧劲儿,咱们就能扬长避短,打造出既灵活又高效的程序来。希望能帮到你,在遇到Scala和Java兼容性问题时,找到自己的解决办法。 --- 希望这篇文章符合您的要求,如果有任何特定的需求或想进一步探讨的部分,请随时告诉我!
2024-11-25 16:06:22
113
月下独酌
Netty
...消息并打印出来,然后调用 ctx.fireChannelRead(msg) 将消息传递给管道中的下一个处理器。 - JIT编译器可以针对这种频繁调用的方法进行优化,通过预测调用路径减少分支预测错误,进而提升整体性能。 3. ByteBuf 内存管理的艺术 接下来,我们来看看ByteBuf,这是Netty用来替代传统的byte[]数组的一个高性能类。ByteBuf提供了自动内存管理和池化功能,能够显著减少垃圾回收的压力。 java ByteBuf buffer = Unpooled.buffer(16); buffer.writeBytes(new byte[]{1, 2, 3, 4}); System.out.println(buffer.readByte()); buffer.release(); 探讨性话术: - 在这个例子中,我们创建了一个容量为16字节的缓冲区,并写入了一些字节。之后读取第一个字节并释放缓冲区。这里的关键在于JIT编译器如何识别和优化这些内存操作。 - 比如,JIT可能会预热并缓存一些常见的方法调用路径,如writeBytes() 和 readByte(),从而在实际运行时提供更快的访问速度。 4. 内联与逃逸分析 JIT优化的利器 说到JIT编译器的优化策略,不得不提的就是内联和逃逸分析。内联就像是把函数的小身段直接塞进调用的地方,这样就省去了函数调用时的那些繁文缛节;而逃逸分析呢,就像是个聪明的侦探,帮JIT(即时编译器)搞清楚对象到底能不能在栈上安家,这样就能避免在堆上分配对象时产生的额外花销。 java public int sum(int a, int b) { return a + b; } // 调用sum方法 int result = sum(10, 20); 思考过程: - 这段代码展示了简单的内联优化。比如说,如果那个sum()方法老是被反复调用,聪明的JIT编译器可能就会直接把它变成简单的加法运算,这样就省去了每次调用函数时的那些麻烦和开销。 - 同样,如果JIT发现某个对象只在方法内部使用且不逃逸到外部,它可能决定将该对象分配到栈上,这样就无需进行垃圾回收。 5. 结语 拥抱优化,追求极致 总之,Netty框架通过精心设计和利用JIT编译器的各种优化策略,实现了卓越的性能表现。作为开发者,咱们得好好搞懂这些机制,然后在自己的项目里巧妙地用上。说真的,性能优化就像一场永无止境的马拉松,每次哪怕只有一点点进步,也都值得我们去琢磨和尝试。 希望这篇文章能给你带来一些启发,让我们一起在编程的道路上不断前行吧! --- 以上就是我对Netty中JIT编译优化的理解和探讨。如果你有任何问题或者想法,欢迎随时留言交流!
2025-01-21 16:24:42
55
风中飘零_
HessianRPC
...发中,高效的远程过程调用(RPC)框架是构建高性能、高扩展性服务的关键一环。HessianRPC,这可真是个轻巧灵活的RPC框架小能手。它巧妙地借助了Hessian协议的大招,玩转序列化和反序列化的游戏,让Java和其他各种编程语言能够无缝对接、高效沟通,就像一个随叫随到、传递消息的小信使一样。然而,在实际操作时,我们可能时不时会遇到个头疼的问题——“HessianURLException:在捣鼓或者构建URL时出了岔子。”嘿,老铁们,这次咱要聊的这个主题可有点意思了。这篇东西呢,就是专门针对这种“诡异现象”,打算手把手地带大家伙儿通过一些实实在在的代码实例,抽丝剥茧地探寻这异常背后的秘密原因,并且一起琢磨琢磨怎么才能把它给妥妥地解决掉。 2. HessianRPC基础与工作原理 HessianRPC的核心在于对HTTP协议的运用以及Hessian二进制序列化机制。开发者只需要这么干,先定义一个接口,然后在这接口上,客户端和服务端两边各自整上实现,这样一来,远程方法调用就轻松搞定了。就像是你在家画好一张购物清单,然后分别让家人和超市那边按照清单准备东西,最后就能完成“远程”的物资调配啦。例如: java // 定义服务接口 public interface HelloService { String sayHello(String name); } // 服务端实现 @Service("helloService") public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return "Hello, " + name; } } // 客户端调用示例 HessianProxyFactory factory = new HessianProxyFactory(); HelloService service = (HelloService) factory.create(HelloService.class, "http://localhost:8080/hello"); String greeting = service.sayHello("World"); 3. HessianURLException详解 当我们在使用HessianRPC进行远程调用时,如果出现"HessianURLException: 创建或处理URL时发生错误。"异常,这通常意味着在创建或解析目标服务的URL地址时出现了问题。比如URL格式不正确、网络不可达或者其他相关的I/O异常。 java try { // 错误的URL格式导致HessianURLException HelloService wrongService = (HelloService) factory.create(HelloService.class, "localhost:8080/hello"); } catch (MalformedURLException e) { System.out.println("HessianURLException: 创建或处理URL时发生错误。"); // 抛出异常 } 在这个例子中,由于我们没有提供完整的URL(缺少协议部分"http://"),所以HessianRPC无法正确解析并创建到服务端的连接,从而抛出了HessianURLException。 4. 解决方案与预防措施 面对HessianURLException,我们需要从以下几个方面着手解决问题: 4.1 检查URL格式 确保提供的URL是完整且有效的,包括协议(如"http://"或"https://")、主机名、端口号及资源路径等必要组成部分。 java // 正确的URL格式 HelloService correctService = (HelloService) factory.create(HelloService.class, "http://localhost:8080/hello"); 4.2 确保网络可达性 检查客户端和服务端之间的网络连接是否畅通无阻。如果服务端未启动或者防火墙阻止了连接请求,也可能引发此异常。 4.3 异常捕获与处理 在代码中合理地处理此类异常,给用户提供明确的错误信息提示。 java try { HelloService service = (HelloService) factory.create(HelloService.class, "http://localhost:8080/hello"); } catch (HessianConnectionException | MalformedURLException e) { System.err.println("无法连接到远程服务,请检查URL和网络状况:" + e.getMessage()); } 5. 总结 在我们的编程旅程中,理解并妥善处理像"HessianURLException: 创建或处理URL时发生错误"这样的异常,有助于提升系统的稳定性和健壮性。对于HessianRPC来说,每一个细节都可能影响到远程调用的成功与否。所以呢,真要解决这类问题,归根结底就俩大法宝:一个是牢牢掌握的基础知识,那叫一个扎实;另一个就是严谨到家的编码习惯了,这两样可真是缺一不可的关键所在啊!伙计们,让我们一起瞪大眼睛,鼓起勇气,把HessianRPC变成我们手里的神兵利器,让它在开发分布式应用时,帮我们飞速提升效率,让开发过程更轻松、更给力!
2023-10-16 10:44:02
531
柳暗花明又一村
Groovy
Groovy中的方法参数传递方式有哪些? 1. 为什么我们要关心方法参数传递? 作为一个开发者,特别是当你在用Groovy这样的语言时,你可能会问自己:“这玩意儿到底怎么工作的?方法里的参数到底是怎么传进来的?”Groovy作为一种脚本语言,它在Java的基础上进行了很多扩展,比如动态类型、闭包支持等等。哎呀,说到方法参数传递嘛,Groovy这小子可真是个“有样学样”的家伙,把Java的那一套全盘接收了过来,但又不是简单照搬,它还自己搞了些小创意,就像在菜里加了点独家调料,味道更特别了! 比如说,你知道Groovy的方法参数可以是可变数量的吗?这在处理不确定数量的输入参数时特别有用。再比如,Groovy支持默认参数值,这意味着你可以给方法参数设置一个默认值,这样调用方就可以选择性地传入参数或者直接使用默认值。 今天我们就来聊聊Groovy中方法参数传递的方式,我保证会用一些例子让你明白这些概念。 --- 2. 参数传递的基础 按值传递 vs 按引用传递 首先,让我们来谈谈最基本的参数传递方式——按值传递和按引用传递。在Groovy里啊,情况其实挺简单的:基本数据类型,像int、double之类的,都是直接“按值传递”的,也就是说,传过去的是它们的具体值,改了也不会影响原来的变量。但要是你传的是对象,那就不一样了,传的是引用,相当于给了个“地址”,所以如果你在方法里对这个对象做了修改,外面的那个对象也会跟着变。简单来说,基本类型自己玩自己的,对象嘛,大家资源共享! 2.1 按值传递的例子 groovy def addNumbers(a, b) { a = a + 10 b = b + 20 return a + b } def x = 5 def y = 10 def result = addNumbers(x, y) println "Result: $result" // 输出: Result: 35 println "x: $x, y: $y" // 输出: x: 5, y: 10 在这个例子中,x和y的原始值并没有被改变,因为它们是基本数据类型,传递到方法中时是按值传递的。方法内部对它们的修改不会影响外部的变量。 2.2 按引用传递的例子 groovy class Person { String name } def modifyPerson(person) { person.name = "Alice" } def p = new Person(name: "Bob") modifyPerson(p) println "Name: ${p.name}" // 输出: Name: Alice 这里我们看到,Person对象是按引用传递的。当我们在modifyPerson方法中修改person对象的属性时,这个修改会影响到外部的p对象。 --- 3. 可变参数 处理不确定数量的输入 有时候,你可能不知道你的方法需要接收多少个参数。Groovy允许你定义可变参数的方法,这非常方便。 3.1 使用可变参数 groovy def sum(numbers) { def total = 0 numbers.each { num -> total += num } return total } println sum(1, 2, 3, 4) // 输出: 10 println sum(5, 10, 15) // 输出: 30 在这个例子中,numbers是一个数组,它可以接收任意数量的参数。通过遍历这个数组,我们可以轻松地计算出所有参数的总和。 --- 4. 默认参数值 简化调用 Groovy还支持为方法参数设置默认值。这使得方法调用更加灵活,尤其是当你不想每次都传入所有的参数时。 4.1 使用默认参数值 groovy def greet(name, greeting = "Hello") { println "$greeting, $name!" } greet("Alice") // 输出: Hello, Alice! greet("Bob", "Hi") // 输出: Hi, Bob! 在这个例子中,第二个参数greeting有一个默认值"Hello"。如果调用方没有提供这个参数,方法就会使用默认值。这不仅减少了代码量,也提高了灵活性。 --- 5. 总结与个人感悟 通过今天的讨论,我们了解了Groovy中方法参数传递的几种主要方式:按值传递、按引用传递、可变参数以及默认参数值。其实啊,每种方法都有自己的拿手好戏,就像不同的工具适合干不同的活儿一样。要是咱们能搞明白这些,就能写出既顺溜又聪明的代码啦! 说实话,当我第一次接触到Groovy的这些特性时,我感到非常兴奋。它让我意识到编程不仅仅是遵循规则,更是一种艺术。通过合理运用这些技巧,我们可以让代码变得更加简洁、优雅。 如果你还在纠结如何选择合适的参数传递方式,不妨多尝试几个例子,看看哪种方式最适合你的项目需求。记住,编程是一个不断学习和实践的过程,每一次尝试都是一次成长的机会!
2025-03-15 15:57:01
101
林中小径
转载文章
...它,进入 main 方法,执行完所有的方法,也就会退出了 我们写的代码就是通过 Handler 驱动起来的,我们 Activity 的 onCreate、onResume、onStop 等等这些生命周期方法,包括我们的 UI 绘制的信号,这些UI绘制的事件都是通过 Handler Looper 循环内部发起的,来调用回调我们的各个 Activity,各个 Fragment 等等这样的一些组件里面的各个生命周期方法,我们的代码就是在循环里面执行的,所以不会阻塞 简述 Handler 的实现原理 Android 应用是通过消息驱动运行的,在 Android 中一切皆消息,包括触摸事件,视图的绘制、显示和刷新等等都是消息 Handler 是消息机制的上层接口,平时开发中我们只会接触到 Handler 和 Message,内部还有 MessageQueue 和 Looper 两大助手共同实现消息循环系统。 延迟消息是怎么实现的? 无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的 when 字段进程赋值 在 MessageQueue 中找到合适的位置(安排 when 小到大排列),并将消息插入到 MessageQueue 中;这样, MessageQueue 就是一个按照消息时间排列的一个链表结构 为什么 Handler 会报内存泄漏? 因为是内部类持有外部类的对象, sendMessage 的时候会调用到 Handler 的 enqueueMessage 方法,msg.target = this; Message 会持有 handler,而 handler 持有调用 handler 的对象,所以 gc 不能回收 Binder 篇 Binder 的定向制导,如何找到目标 Binder,唤起进程或者线程呢? Binder 实体服务其实有两种: 一是通过 addService 注册到 ServiceManager 中的服务,比如 ActivityManagerService、PackageManagerService、PowerManagerService 等,一般都是系统服务; 还有一种是通过 bindService 拉起的一些服务,一般是开发者自己实现的服务 这里先看通过 addService 添加的被 ServiceManager 所管理的服务 ServiceManager 是比较特殊的服务,所有应用都能直接使用,因为 ServiceManager 对于 Client 端来说 Handle 句柄是固定的,都是 0,所以 ServiceManager 服务并不需要查询,可以直接使用 Binder 为什么会有两棵 binder_ref 红黑树? Binder_proc 中存在两棵 binder_ref 红黑树,其实两棵红黑树中的节点是复用的,只是查询方式不同,一个通过 Handle 句柄,一个通过 node 节点查找 refs_by_node 红黑树主要是为了 Binder驱动往用户空间写数据所使用的,而 refs_by_desc 是用户空间向 Binder 驱动写数据使用的,只是方向问题 比如在服务 addService 的时候,binder 驱动会在在 ServiceManager 进程的 binder_proc 中查找 binder_ref 结构体 Binder 是如何做到一次拷贝的 用户空间的虚拟内存地址是映射到物理内存中的 对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射 这个内存映射过程是通过系统调用 mmap() 来实现的 Binder借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝 Binder机制是如何跨进程的 在内核空间创建一块接收缓存区, 实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区 发送进程通过系统调用(copy_from_user)将数据发送到内核缓存区;由于内核缓存区和接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信 就举例这么多了,面试题也不是几个就能全部覆盖的,毕竟面试官不是吃素的,他会换着花样问你;有想跳槽拿高薪的 Android 开发的朋友,我这里分享一份 Handler、Binder 精选面试 PDF 文档;私信发送 “面试” 直达获取;想拿高薪的人很多,就看你肯不肯努力了 面试题 PDF 文档内容展示: Handler 机制之 Thread Handler 机制之 ThreadLocal Handler 机制之 SystemClock 类 Handler 机制之 Looper 与 Handler 简介 Android 跨进程通信 IPC 之 Binder 之 Framewor k层 C++ 篇 Android 跨进程通信 IPC 之 Binder 之 Framework 层 Java 篇 Android 跨进程通信 IPC 之 Binder 的补充 Android 跨进程通信 IPC 之 Binder 总结 小伙伴们如果有需要以上这些资料:私信发送 “面试” 直达获取,承诺100%免费! 本篇文章为转载内容。原文链接:https://blog.csdn.net/m0_62167422/article/details/127129133。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-11-15 10:35:50
217
转载
Dubbo
...理、以及透明化的远程方法调用等功能,广泛应用于分布式系统架构中,实现服务治理和服务间的高效解耦。 环境变量 , 在计算机操作系统中,环境变量是一种特殊的变量,用于存储与操作系统运行环境相关的信息,如JAVA_HOME。在本文语境中,环境变量未正确设置可能导致Dubbo无法找到Java安装路径,进而影响其正常启动和运行。因此,为保证Dubbo能顺利运行,需要确保相关的环境变量已按照要求正确配置。 日志配置文件(如logback.xml) , 日志配置文件是应用程序用来指定日志输出格式、目的地(如控制台、文件、数据库等)、过滤规则以及日志级别等信息的配置文件。在Dubbo框架中,若日志配置文件内容有误,则可能造成日志输出异常,使得开发者无法通过日志获取到有效信息,以了解系统的运行状态和排查问题。例如,在文章中提到的logback.xml即为基于Logback的日志框架所使用的配置文件,其中的错误配置会直接影响到Dubbo应用的日志记录功能。
2023-06-21 10:00:14
435
春暖花开-t
转载文章
...些课程,简单的来实现调用学习的方法,然后在一个Test类之中输入学习的内容。但是我暂时只学java和web,但是可能我后面还要学习Spring,SpringMVC… 1.面向实现编程 public class Ggzx {public void stduyJava(){System.out.println("学习了java课程");}public void studyWeb(){System.out.println("学习了Web课程");} } public class Test {public static void main(String[] args) {Ggzx ggzx=new Ggzx();ggzx.studyJava();ggzx.studyPython();ggzx.studyGo();} } 分析: 上面使用的面向实现编程,但是Test作为我们控制的"应用层",也就是高层,而Ggzx作为低层,其实这样在比较简单的例子中,其实是没问题的,因为假如不需要扩展,仅仅是实现两个很简单的功能,并没有必要去面向接口开发,但是一般在开发中通常有很复杂的开发环境和开发需求。 现在如果想添加新的功能,学习其他的课程,怎么办??? 继续使用面向实现编程,直接在 Ggzx 类中直接添加新的方法,可以完成这个功能需求。 用上面的方法实现有没有缺点??? 学习的课程和 Ggzx 类耦合比较严重。是学习的课程只能通过Ggzx 才能得到 。并且是想要学习新的课程也要在 Ggzx 类中不断添加和修改 —>高耦合 Ggzx 作为当前 demo 的底层,经常的被改动,高层Test依赖于低层 Ggzx 的实现 ---->对应依赖倒置原则中的:高层过度依赖低层了 2.面向接口编程(简单版) 为了解决上面出现的问题,我们可以考虑把学习的课程抽出来成为一个类。到现在,类和类之间的耦合其实就已经降低很多了。然后将其当做参数传入Ggzx里面,然后调用课程里面的学习方法 //web课程类public class WebCourse {public void studyCourse() {System.out.println("学习了Web课程");} } //这里是Java课程类public class JavaCourse {public void studyCourse() {System.out.println("学习Java课程");} } 当我们写出来这两个类,想要对Ggzx里面的学习方法进行编写的时候,有没有发现其实有一些小问题呢???? Ggzx里面接收这些类的参数是什么?? 难道要这样? //以下是Ggzx类中的内容public void studyJava(JavaCourse javaCourse){}public void studyWeb(WebCourse webCourse){} nonono,如果这样做,虽然当前已经把课程类和 Ggzx 用户剥离一点点了,但是是还是形同虚设,课程类虽然分离开了,但是还是像狗皮膏药一样贴在 Ggzx 类中,但是看着还是很难受,高层 Test 调用方法还是得依赖 Ggzx 里面有什么方法 每次加入新课程,都需要修改底层功能 如何修改??? 接口是个好东西,课程类之间是不是都包含同样一个方法,被学习的方法( studyCourse ),那么我们可以将所有课程类都实现一个ICourse课程! 对应上面的问题,我们该传入什么参数能解决问题??可以传入一个接口 改编后的 UML 图解展示(Ggzx 被废弃,用新的 NewGgzx 代替):(如果没了解过UML类图,或者是纯小白,只需要知道一个大框是一个类,虚线表示实现了箭头方向的接口,小m是方法 即可) 观察上面的UML图 WebCourse 和 JavaCourse 实现自同一个接口 ICourse,每个课程都有自己的 studyXxx 方法。 这样好在什么地方? - 课程类和Ggzx类是解耦的,无论你增加多少个课程类,只要实现了ICourse接口,都能直接传入Ggzx的studyMyCourse()方法中 public interface ICourse {void studyCourse();} public class WebCourse implements ICourse{@Overridepublic void studyCourse() {System.out.println("学习了Web课程");} } public class NewGgzx {public void studyMyCourse(ICourse iCourse){iCourse.studyCourse();} } 上面就是案例的面向接口编程,我们可以看到,在 NewGgzx 类中,我们可以传入一个实现 ICourse 接口的课程类,我们在Test类中调用的时候,只需要传入一个课程类即可调用学习方法,这样当想扩展新的内容,只需要创建一个新的课程类实现 ICourse 即可 Test使用 NewGgzx newGgzx =new NewGgzx();newGgzx.studyMoocCourse(new WebCourse());newGgzx.studyMoocCourse(new com.ggzx.design.priciple.dependenceiversion.JavaCourse()); 从面向实现到面向接口,我们处理问题的方法改变了: 开始时,我们需要考虑在Test类中调用Ggzx里面的哪一种学习方法,即注重调用什么方法能够实现特定的课程 到面向接口编程,我们考虑传入什么课程即可实现学习 当业务需求拓展时,拓展方法也改变了: 面向实现:需要改变底层的代码来协调我们需要使用的功能,用上面的例子来解释就是:当你想要学习一个课程,你就需要改变你底层的实现,增加新的代码 面向接口:想学习什么课程,不会对其他课程造成影响,也不会影响到低层的Ggzx 。实际操作就是增加一门新的课程即可,实现接口之后,传入这个类到Ggzx的方法中就可以学习这一门课了 相对于细节的多变性,抽象的东西更稳定,以抽象为基础搭建的架构比以细节搭建的架构更加稳定 本篇文章为转载内容。原文链接:https://blog.csdn.net/m0_52410356/article/details/122828154。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-08-26 15:35:43
633
转载
HessianRPC
...C是一个轻量级的远程调用框架,主要用于Java应用程序之间的通信。它支持多种协议,比如HTTP、TCP等,非常适合构建分布式系统。不过,HessianRPC本身并没有内置的服务降级功能,所以我们需要手动去实现。 刚开始接触HessianRPC的时候,我觉得它的API还挺简洁的。比如,我们可以定义一个接口: java public interface HelloService { String sayHello(String name); } 然后通过代理类来调用这个接口的方法: java HessianProxyFactory factory = new HessianProxyFactory(); HelloService helloService = (HelloService) factory.create(HelloService.class, "http://localhost:8080/hello"); String result = helloService.sayHello("World"); System.out.println(result); 看到这段代码的时候,我心里想着:“嗯,看起来挺简单的嘛!”但是,当我尝试在高负载情况下运行它时,才发现事情并没有那么简单。 3. 服务降级的重要性与实践 服务降级的核心思想就是在系统资源紧张时,优先保证核心业务的正常运转,而暂时关闭一些非关键的功能。对于HessianRPC来说,我们可以通过异常捕获的方式来实现这一点。 假设我们现在有一个UserService,其中包含了一个getUserInfo()方法。要是咱们直接用这个方法,后端服务要是挂了,程序立马就“崩”了,那用户的体验肯定惨不忍睹啊!所以,我们需要对这个方法进行改造,加入降级逻辑。 java public class UserServiceFallback implements UserService { @Override public UserInfo getUserInfo(int userId) { // 返回默认值 return new UserInfo(-1, "Default User", "No Data Available"); } } 接着,在主逻辑中使用装饰器模式来包裹原始的服务: java public class UserServiceDecorator implements UserService { private final UserService userService; private final UserService fallback; public UserServiceDecorator(UserService userService, UserService fallback) { this.userService = userService; this.fallback = fallback; } @Override public UserInfo getUserInfo(int userId) { try { return userService.getUserInfo(userId); } catch (Exception e) { System.err.println("Service unavailable, falling back..."); return fallback.getUserInfo(userId); } } } 通过这种方式,即使后端服务出现问题,我们也能够提供一个友好的备用方案,不至于让用户感到困惑。 4. 面临挑战与解决方案 当然,实际开发过程中总会遇到各种意想不到的问题。比如说,当多个服务同时发生故障时,我们应该如何合理分配降级策略?另外,频繁触发降级会不会影响性能? 为了解决这些问题,我们可以引入熔断器模式(Circuit Breaker Pattern)。简单讲啊,就好比给系统装了个“自动切换”的小开关。要是某个服务老是连不上,失败个好几次之后,这个开关就会自动启动,直接给用户返回个备用的数据,省得一直傻乎乎地去重试那个挂掉的服务,多浪费时间啊! 下面是一个基于HessianRPC的熔断器实现: java public class CircuitBreaker { private final T delegate; private boolean open = false; private int failureCount = 0; public CircuitBreaker(T delegate) { this.delegate = delegate; } public T getDelegate() { if (open && failureCount > 5) { return null; // 返回null表示断路器处于打开状态 } return delegate; } public void recordFailure() { failureCount++; if (failureCount >= 5) { open = true; } } } 将熔断器集成到之前的装饰器中: java public class CircuitBreakingUserServiceDecorator implements UserService { private final CircuitBreaker circuitBreaker; public CircuitBreakingUserServiceDecorator(CircuitBreaker circuitBreaker) { this.circuitBreaker = circuitBreaker; } @Override public UserInfo getUserInfo(int userId) { UserService userService = circuitBreaker.getDelegate(); if (userService == null) { return new UserInfo(-1, "Circuit Opened", "Service Unavailable"); } try { return userService.getUserInfo(userId); } catch (Exception e) { circuitBreaker.recordFailure(); return new UserInfo(-1, "Fallback User", "Service Unavailable"); } } } 这样,我们就能够在一定程度上缓解高负载带来的压力,并且确保系统的稳定性。 5. 总结与展望 回顾这次经历,我深刻体会到服务降级并不是一件轻松的事情。这事儿吧,不光得靠技术硬功夫,还得会提前打算,脑子转得也得快,不然真容易手忙脚乱。虽然HessianRPC没有提供现成的服务降级工具,但通过灵活运用设计模式,我们完全可以打造出适合自己项目的解决方案。 未来,我希望能够在更多场景下探索HessianRPC的应用潜力,同时也期待社区能够推出更加完善的降级框架,让开发者们少走弯路。毕竟,谁不想写出既高效又优雅的代码呢?如果你也有类似的经历或想法,欢迎随时交流讨论!
2025-05-01 15:44:28
17
半夏微凉
转载文章
...te__ 这三个特殊方法。 详解描述符 说说 Property 在上文,我们给出了 Propery 实现代码,现在让我们来详细说说这个classPerson(object): """""" ---------------------------------------------------------------------- def__init__(self, first_name, last_name): """Constructor""" self.first_name = first_name self.last_name = last_name ---------------------------------------------------------------------- @Property deffull_name(self): """ Return the full name """ return"%s %s"% (self.first_name, self.last_name) if__name__=="__main__": person = Person("Mike","Driscoll") print(person.full_name) 'Mike Driscoll' print(person.first_name) 'Mike' 首先,如果你对装饰器不了解的话,你可能要去看看这篇文章,简而言之,在我们正式运行代码之前,我们的解释器就会对我们的代码进行一次扫描,对涉及装饰器的部分进行替换。类装饰器同理。在上文中,这段代码@Property deffull_name(self): """ Return the full name """ return"%s %s"% (self.first_name, self.last_name) 会触发这样一个过程,即 full_name=Property(full_name) 。然后在我们后面所实例化对象之后我们调用 person.full_name 这样一个过程其实等价于 person.full_name.__get__(person) 然后进而触发 __get__() 方法里所写的 return self.fget(obj) 即原本上我们所编写的 def full_name 内的执行代码。 这个时候,同志们可以去思考下 getter() , setter() ,以及 deleter() 的具体运行机制了=。=如果还是有问题,欢迎在评论里进行讨论。 关于描述符 还记得之前我们所提到的一个定义么: Descriptors 是一种特殊的对象,这种对象实现了 __get__ , __set__ , __delete__ 这三个特殊方法 。然后在 Python 官方文档的说明中,为了体现描述符的重要性,有这样一段话:“They are the mechanism behind properties, methods, static methods, class methods, and super(). They are used throughout Python itself to implement the new style classes introduced in version 2.2. ” 简而言之就是 先有描述符后有天,秒天秒地秒空气 。恩,在新式类中,属性,方法调用,静态方法,类方法等都是基于描述符的特定使用。 OK,你可能想问,为什么描述符是这么重要呢?别急,我们接着看 使用描述符 首先请看下一段代码 classA(object):注:在 Python 3.x 版本中,对于 new class 的使用不需要显式的指定从 object 类进行继承,如果在 Python 2.X(x>2)的版本中则需要defa(self): pass if__name__=="__main__": a=A() a.a() 大家都注意到了我们存在着这样一个语句 a.a() ,好的,现在请大家思考下,我们在调用这个方法的时候发生了什么? OK?想出来了么?没有?好的我们继续 首先我们调用一个属性的时候,不管是成员还是方法,我们都会触发这样一个方法用于调用属性 __getattribute__() ,在我们的 __getattribute__() 方法中,如果我们尝试调用的属性实现了我们的描述符协议,那么会产生这样一个调用过程 type(a).__dict__['a'].__get__(b,type(b)) 。好的这里我们又要给出一个结论了:“在这样一个调用过程中,有这样一个优先级顺序,如果我们所尝试调用属性是一个 data descriptors ,那么不管这个属性是否存在我们的实例的 __dict__ 字典中,优先调用我们描述符里的 __get__ 方法,如果我们所尝试调用属性是一个 non data descriptors ,那么我们优先调用我们实例里的 __dict__ 里的存在的属性,如果不存在,则依照相应原则往上查找我们类,父类中的 __dict__ 中所包含的属性,一旦属性存在,则调用 __get__ 方法,如果不存在则调用 __getattr__() 方法”。理解起来有点抽象?没事,我们马上会讲,不过在这里,我们先要解释下 data descriptors 与 non data descriptors ,再来看一个例子。什么是 data descriptors 与 non data descriptors 呢?其实很简单,在描述符中同时实现了 __get__ 与 __set__ 协议的描述符是 data descriptors ,如果只实现了 __get__ 协议的则是 non data descriptors 。好了我们现在来看个例子:importmath classlazyproperty: def__init__(self, func): self.func = func def__get__(self, instance, owner): ifinstanceisNone: returnself else: value = self.func(instance) setattr(instance, self.func.__name__, value) returnvalue classCircle: def__init__(self, radius): self.radius = radius pass @lazyproperty defarea(self): print("Com") returnmath.pi self.radius 2 deftest(self): pass if__name__=='__main__': c=Circle(4) print(c.area) 好的,让我们仔细来看看这段代码,首先类描述符 @lazyproperty 的替换过程,前面已经说了,我们不在重复。接着,在我们第一次调用 c.area 的时候,我们首先查询实例 c 的 __dict__ 中是否存在着 area 描述符,然后发现在 c 中既不存在描述符,也不存在这样一个属性,接着我们向上查询 Circle 中的 __dict__ ,然后查找到名为 area 的属性,同时这是一个 non data descriptors ,由于我们的实例字典内并不存在 area 属性,那么我们便调用类字典中的 area 的 __get__ 方法,并在 __get__ 方法中通过调用 setattr 方法为实例字典注册属性 area 。紧接着,我们在后续调用 c.area 的时候,我们能在实例字典中找到 area 属性的存在,且类字典中的 area 是一个 non data descriptors ,于是我们不会触发代码里所实现的 __get__ 方法,而是直接从实例的字典中直接获取属性值。 描述符的使用 描述符的使用面很广,不过其主要的目的在于让我们的调用过程变得可控。因此我们在一些需要对我们调用过程实行精细控制的时候,使用描述符,比如我们之前提到的这个例子classlazyproperty: def__init__(self, func): self.func = func def__get__(self, instance, owner): ifinstanceisNone: returnself else: value = self.func(instance) setattr(instance, self.func.__name__, value) returnvalue def__set__(self, instance, value=0): pass importmath classCircle: def__init__(self, radius): self.radius = radius pass @lazyproperty defarea(self, value=0): print("Com") ifvalue ==0andself.radius ==0: raiseTypeError("Something went wring") returnmath.pi value 2ifvalue !=0elsemath.pi self.radius 2 deftest(self): pass 利用描述符的特性实现懒加载,再比如,我们可以控制属性赋值的值classProperty(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def__init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel ifdocisNoneandfgetisnotNone: doc = fget.__doc__ self.__doc__ = doc def__get__(self, obj, objtype=None): ifobjisNone: returnself ifself.fgetisNone: raiseAttributeError("unreadable attribute") returnself.fget(obj) def__set__(self, obj, value=None): ifvalueisNone: raiseTypeError("You cant to set value as None") ifself.fsetisNone: raiseAttributeError("can't set attribute") self.fset(obj, value) def__delete__(self, obj): ifself.fdelisNone: raiseAttributeError("can't delete attribute") self.fdel(obj) defgetter(self, fget): returntype(self)(fget, self.fset, self.fdel, self.__doc__) defsetter(self, fset): returntype(self)(self.fget, fset, self.fdel, self.__doc__) defdeleter(self, fdel): returntype(self)(self.fget, self.fset, fdel, self.__doc__) classtest(): def__init__(self, value): self.value = value @Property defValue(self): returnself.value @Value.setter deftest(self, x): self.value = x 如上面的例子所描述的一样,我们可以判断所传入的值是否有效等等。 以上就是Python 描述符(Descriptor)入门,更多相关文章请关注PHP中文网(www.gxlcms.com)! 本条技术文章来源于互联网,如果无意侵犯您的权益请点击此处反馈版权投诉 本文系统来源:php中文网 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_39736934/article/details/112888600。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-05-07 19:03:49
94
转载
转载文章
...收到这个信号时最终会调用到doFrame进行帧的绘制操作。 Choreographer在系统中流程: 如何通过Choreographer计算掉帧情况:原理就是: 通过给Choreographer设置FrameCallback,在每次绘制前后看时间差是16.6ms的多少倍,即为前后掉帧率。 使用方式如下: //Application.javapublic void onCreate() {super.onCreate();//在Application中使用postFrameCallbackChoreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));}public class FPSFrameCallback implements Choreographer.FrameCallback {private static final String TAG = "FPS_TEST";private long mLastFrameTimeNanos = 0;private long mFrameIntervalNanos;public FPSFrameCallback(long lastFrameTimeNanos) {mLastFrameTimeNanos = lastFrameTimeNanos;mFrameIntervalNanos = (long)(1000000000 / 60.0);}@Overridepublic void doFrame(long frameTimeNanos) {//初始化时间if (mLastFrameTimeNanos == 0) {mLastFrameTimeNanos = frameTimeNanos;}final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;if (jitterNanos >= mFrameIntervalNanos) {final long skippedFrames = jitterNanos / mFrameIntervalNanos;if(skippedFrames>30){//丢帧30以上打印日志Log.i(TAG, "Skipped " + skippedFrames + " frames! "+ "The application may be doing too much work on its main thread.");} }mLastFrameTimeNanos=frameTimeNanos;//注册下一帧回调Choreographer.getInstance().postFrameCallback(this);} } UI绘制全路径分析: 有了前面几个概念,这里我们让SurfaceFlinger结合View的绘制流程用一张图来表达整个绘制流程: 生产者:APP方构建Surface的过程。 消费者:SurfaceFlinger UI绘制全路径分析卡顿原因: 接下来,我们逐个分析,看看都会有哪些原因可能造成卡顿: 1.渲染流程 1.Vsync 调度:这个是起始点,但是调度的过程会经过线程切换以及一些委派的逻辑,有可能造成卡顿,但是一般可能性比较小,我们也基本无法介入; 2.消息调度:主要是 doframe Message 的调度,这就是一个普通的 Handler 调度,如果这个调度被其他的 Message 阻塞产生了时延,会直接导致后续的所有流程不会被触发 3.input 处理:input 是一次 Vsync 调度最先执行的逻辑,主要处理 input 事件。如果有大量的事件堆积或者在事件分发逻辑中加入大量耗时业务逻辑,会造成当前帧的时长被拉大,造成卡顿,可以尝试通过事件采样的方案,减少 event 的处理 4.动画处理:主要是 animator 动画的更新,同理,动画数量过多,或者动画的更新中有比较耗时的逻辑,也会造成当前帧的渲染卡顿。对动画的降帧和降复杂度其实解决的就是这个问题; 5.view 处理:主要是接下来的三大流程,过度绘制、频繁刷新、复杂的视图效果都是此处造成卡顿的主要原因。比如我们平时所说的降低页面层级,主要解决的就是这个问题; 6.measure/layout/draw:view 渲染的三大流程,因为涉及到遍历和高频执行,所以这里涉及到的耗时问题均会被放大,比如我们会降不能在 draw 里面调用耗时函数,不能 new 对象等等; 7.DisplayList 的更新:这里主要是 canvas 和 displaylist 的映射,一般不会存在卡顿问题,反而可能存在映射失败导致的显示问题; 8.OpenGL 指令转换:这里主要是将 canvas 的命令转换为 OpenGL 的指令,一般不存在问题 9.buffer 交换:这里主要指 OpenGL 指令集交换给 GPU,这个一般和指令的复杂度有关 10.GPU 处理:顾名思义,这里是 GPU 对数据的处理,耗时主要和任务量和纹理复杂度有关。这也就是我们降低 GPU 负载有助于降低卡顿的原因; 11.layer 合成:Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。 可以选择降低Surface层级来优化卡顿。 12.光栅化/Display:这里暂时忽略,底层系统行为; Buffer 切换:主要是屏幕的显示,这里 buffer 的数量也会影响帧的整体延迟,不过是系统行为,不能干预。 2.系统负载 内存:内存的吃紧会直接导致 GC 的增加甚至 ANR,是造成卡顿的一个不可忽视的因素; CPU:CPU 对卡顿的影响主要在于线程调度慢、任务执行的慢和资源竞争,比如 1.降频会直接导致应用卡顿; 2.后台活动进程太多导致系统繁忙,cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 ,这种情况比较常见,可以使用dumpsys cpuinfo查看当前设备的cpu使用情况: 3.主线程调度不到 , 处于 Runnable 状态,这种情况比较少见 4.System 锁:system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿 GPU:GPU 的影响见渲染流程,但是其实还会间接影响到功耗和发热; 功耗/发热:功耗和发热一般是不分家的,高功耗会引起高发热,进而会引起系统保护,比如降频、热缓解等,间接的导致卡顿。 如何监控卡顿 线下监控: 我们知道卡顿问题的原因错综复杂,但最终都可以反馈到CPU使用率上来 1.使用dumpsys cpuinfo命令 这个命令可以获取当时设备cpu使用情况,我们可以在线下通过重度使用应用来检测可能存在的卡顿点 A8S:/ $ dumpsys cpuinfoLoad: 1.12 / 1.12 / 1.09CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 14:53:30.866):2% 1053/scanserver: 0.2% user + 1.7% kernel0.6% 934/system_server: 0.4% user + 0.1% kernel / faults: 563 minor0.4% 564/signserver: 0% user + 0.4% kernel0.2% 256/ueventd: 0.1% user + 0% kernel / faults: 320 minor0.2% 474/surfaceflinger: 0.1% user + 0.1% kernel0.1% 576/vendor.sprd.hardware.gnss@2.0-service: 0.1% user + 0% kernel / faults: 54 minor0.1% 286/logd: 0% user + 0% kernel / faults: 10 minor0.1% 2821/com.allinpay.appstore: 0.1% user + 0% kernel / faults: 1312 minor0.1% 447/android.hardware.health@2.0-service: 0% user + 0% kernel / faults: 1175 minor0% 1855/com.smartpos.dataacqservice: 0% user + 0% kernel / faults: 755 minor0% 2875/com.allinpay.appstore:pushcore: 0% user + 0% kernel / faults: 744 minor0% 1191/com.android.systemui: 0% user + 0% kernel / faults: 70 minor0% 1774/com.android.nfc: 0% user + 0% kernel0% 172/kworker/1:2: 0% user + 0% kernel0% 145/irq/24-70900000: 0% user + 0% kernel0% 575/thermald: 0% user + 0% kernel / faults: 300 minor... 2.CPU Profiler 这个工具是AS自带的CPU性能检测工具,可以在PC上实时查看我们CPU使用情况。 AS提供了四种Profiling Model配置: 1.Sample Java Methods:在应用程序基于Java的代码执行过程中,频繁捕获应用程序的调用堆栈 获取有关应用程序基于Java的代码执行的时间和资源使用情况信息。 2.Trace java methods:在运行时对应用程序进行检测,以在每个方法调用的开始和结束时记录时间戳。收集时间戳并进行比较以生成方法跟踪数据,包括时序信息和CPU使用率。 请注意与检测每种方法相关的开销会影响运行时性能,并可能影响性能分析数据。对于生命周期相对较短的方法,这一点甚至更为明显。此外,如果您的应用在短时间内执行大量方法,则探查器可能会很快超过其文件大小限制,并且可能无法记录任何进一步的跟踪数据。 3.Sample C/C++ Functions:捕获应用程序本机线程的示例跟踪。要使用此配置,您必须将应用程序部署到运行Android 8.0(API级别26)或更高版本的设备。 4.Trace System Calls:捕获细粒度的详细信息,使您可以检查应用程序与系统资源的交互方式 您可以检查线程状态的确切时间和持续时间,可视化CPU瓶颈在所有内核中的位置,并添加自定义跟踪事件进行分析。在对性能问题进行故障排除时,此类信息可能至关重要。要使用此配置,您必须将应用程序部署到运行Android 7.0(API级别24)或更高版本的设备。 使用方式: Debug.startMethodTracing("");// 需要检测的代码片段...Debug.stopMethodTracing(); 优点:有比较全面的调用栈以及图像化方法时间显示,包含所有线程的情况 缺点:本身也会带来一点的性能开销,可能会带偏优化方向 火焰图:可以显示当前应用的方法堆栈: 3.Systrace Systrace在前面一篇分析启动优化的文章讲解过 这里我们简单来复习下: Systrace用来记录当前应用的系统以及应用(使用Trace类打点)的各阶段耗时信息包括绘制信息以及CPU信息等。 使用方式: Trace.beginSection("MyApp.onCreate_1");alt(200);Trace.endSection(); 在命令行中: python systrace.py -t 5 sched gfx view wm am app webview -a "com.chinaebipay.thirdcall" -o D:\trac1.html 记录的方法以及CPU中的耗时情况: 优点: 1.轻量级,开销小,CPU使用率可以直观反映 2.右侧的Alerts能够根据我们应用的问题给出具体的建议,比如说,它会告诉我们App界面的绘制比较慢或者GC比较频繁。 4.StrictModel StrictModel是Android提供的一种运行时检测机制,用来帮助开发者自动检测代码中不规范的地方。 主要和两部分相关: 1.线程相关 2.虚拟机相关 基础代码: private void initStrictMode() {// 1、设置Debug标志位,仅仅在线下环境才使用StrictModeif (DEV_MODE) {// 2、设置线程策略StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode.detectDiskReads().detectDiskWrites().detectNetwork() // or .detectAll() for all detectable problems.penaltyLog() //在Logcat 中打印违规异常信息// .penaltyDialog() //也可以直接跳出警报dialog// .penaltyDeath() //或者直接崩溃.build());// 3、设置虚拟机策略StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()// 给NewsItem对象的实例数量限制为1.setClassInstanceLimit(NewsItem.class, 1).detectLeakedClosableObjects() //API等级11.penaltyLog().build());} } 线上监控: 线上需要自动化的卡顿检测方案来定位卡顿,它能记录卡顿发生时的场景。 自动化监控原理: 采用拦截消息调度流程,在消息执行前埋点计时,当耗时超过阈值时,则认为是一次卡顿,会进行堆栈抓取和上报工作 首先,我们看下Looper用于执行消息循环的loop()方法,关键代码如下所示: / Run the message queue in this thread. Be sure to call {@link quit()} to end the loop./public static void loop() {...for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;// This must be in a local variable, in case a UI event sets the loggerfinal Printer logging = me.mLogging;if (logging != null) {// 1logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}...try {// 2 msg.target.dispatchMessage(msg);dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} finally {if (traceTag != 0) {Trace.traceEnd(traceTag);} }...if (logging != null) {// 3logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);} 在Looper的loop()方法中,在其执行每一个消息(注释2处)的前后都由logging进行了一次打印输出。可以看到,在执行消息前是输出的">>>>> Dispatching to “,在执行消息后是输出的”<<<<< Finished to ",它们打印的日志是不一样的,我们就可以由此来判断消息执行的前后时间点。 具体的实现可以归纳为如下步骤: 1、首先,我们需要使用Looper.getMainLooper().setMessageLogging()去设置我们自己的Printer实现类去打印输出logging。这样,在每个message执行的之前和之后都会调用我们设置的这个Printer实现类。 2、如果我们匹配到">>>>> Dispatching to "之后,我们就可以执行一行代码:也就是在指定的时间阈值之后,我们在子线程去执行一个任务,这个任务就是去获取当前主线程的堆栈信息以及当前的一些场景信息,比如:内存大小、电脑、网络状态等。 3、如果在指定的阈值之内匹配到了"<<<<< Finished to ",那么说明message就被执行完成了,则表明此时没有产生我们认为的卡顿效果,那我们就可以将这个子线程任务取消掉。 这里我们使用blockcanary来做测试: BlockCanary APM是一个非侵入式的性能监控组件,可以通过通知的形式弹出卡顿信息。它的原理就是我们刚刚讲述到的卡顿监控的实现原理。 使用方式: 1.导入依赖 implementation 'com.github.markzhai:blockcanary-android:1.5.0' Application的onCreate方法中开启卡顿监控 // 注意在主进程初始化调用BlockCanary.install(this, new AppBlockCanaryContext()).start(); 3.继承BlockCanaryContext类去实现自己的监控配置上下文类 public class AppBlockCanaryContext extends BlockCanaryContext {....../ 指定判定为卡顿的阈值threshold (in millis), 你可以根据不同设备的性能去指定不同的阈值 @return threshold in mills/public int provideBlockThreshold() {return 1000;}....} 4.在Activity的onCreate方法中执行一个耗时操作 try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();} 5.结果: 可以看到一个和LeakCanary一样效果的阻塞可视化堆栈图 那有了BlockCanary的方法耗时监控方式是不是就可以解百愁了呢,呵呵。有那么容易就好了 根据原理:我们拿到的是msg执行前后的时间和堆栈信息,如果msg中有几百上千个方法,就无法确认到底是哪个方法导致的耗时,也有可能是多个方法堆积导致。 这就导致我们无法准确定位哪个方法是最耗时的。如图中:堆栈信息是T2的,而发生耗时的方法可能是T1到T2中任何一个方法甚至是堆积导致。 那如何优化这块? 这里我们采用字节跳动给我们提供的一个方案:基于 Sliver trace 的卡顿监控体系 Sliver trace 整体流程图: 主要包含两个方面: 检测方案: 在监控卡顿时,首先需要打开 Sliver 的 trace 记录能力,Sliver 采样记录 trace 执行信息,对抓取到的堆栈进行 diff 聚合和缓存。 同时基于我们的需要设置相应的卡顿阈值,以 Message 的执行耗时为衡量。对主线程消息调度流程进行拦截,在消息开始分发执行时埋点,在消息执行结束时计算消息执行耗时,当消息执行耗时超过阈值,则认为产生了一次卡顿。 堆栈聚合策略: 当卡顿发生时,我们需要为此次卡顿准备数据,这部分工作是在端上子线程中完成的,主要是 dump trace 到文件以及过滤聚合要上报的堆栈。分为以下几步: 1.拿到缓存的主线程 trace 信息并 dump 到文件中。 2.然后从文件中读取 trace 信息,按照数据格式,从最近的方法栈向上追溯,找到当前 Message 包含的全部 trace 信息,并将当前 Message 的完整 trace 写入到待上传的 trace 文件中,删除其余 trace 信息。 3.遍历当前 Message trace,按照(Method 执行耗时 > Method 耗时阈值 & Method 耗时为该层堆栈中最耗时)为条件过滤出每一层函数调用堆栈的最长耗时函数,构成最后要上报的堆栈链路,这样特征堆栈中的每一步都是最耗时的,且最底层 Method 为最后的耗时大于阈值的 Method。 之后,将 trace 文件和堆栈一同上报,这样的特征堆栈提取策略保证了堆栈聚合的可靠性和准确性,保证了上报到平台后堆栈的正确合理聚合,同时提供了进一步分析问题的 trace 文件。 可以看到字节给的是一整套监控方案,和前面BlockCanary不同之处就在于,其是定时存储堆栈,缓存,然后使用diff去重的方式,并上传到服务器,可以最大限度的监控到可能发生比较耗时的方法。 开发中哪些习惯会影响卡顿的发生 1.布局太乱,层级太深。 1.1:通过减少冗余或者嵌套布局来降低视图层次结构。比如使用约束布局代替线性布局和相对布局。 1.2:用 ViewStub 替代在启动过程中不需要显示的 UI 控件。 1.3:使用自定义 View 替代复杂的 View 叠加。 2.主线程耗时操作 2.1:主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完成。 2.2:sharepreference尽量使用apply,少使用commit,可以使用MMKV框架来代替sharepreference。 2.3:网络请求回来的数据解析尽量放在子线程中,不要在主线程中进行复制的数据解析操作。 2.4:不要在activity的onResume和onCreate中进行耗时操作,比如大量的计算等。 2.5:不要在 draw 里面调用耗时函数,不能 new 对象 3.过度绘制 过度绘制是同一个像素点上被多次绘制,减少过度绘制一般减少布局背景叠加等方式,如下图所示右边是过度绘制的图片。 4.列表 RecyclerView使用优化,使用DiffUtil和notifyItemDataSetChanged进行局部更新等。 5.对象分配和回收优化 自从Android引入 ART 并且在Android 5.0上成为默认的运行时之后,对象分配和垃圾回收(GC)造成的卡顿已经显著降低了,但是由于对象分配和GC有额外的开销,它依然又可能使线程负载过重。 在一个调用不频繁的地方(比如按钮点击)分配对象是没有问题的,但如果在在一个被频繁调用的紧密的循环里,就需要避免对象分配来降低GC的压力。 减少小对象的频繁分配和回收操作。 好了,关于卡顿优化的问题就讲到这里,下篇文章会对卡顿中的ANR情况的处理,这里做个铺垫。 如果喜欢我的文章,欢迎关注我的公众号。 点击这看原文链接: 参考 Android卡顿检测及优化 一文读懂直播卡顿优化那些事儿 “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解! 深入探索Android卡顿优化(上) 西瓜卡顿 & ANR 优化治理及监控体系建设 5376)] 参考 Android卡顿检测及优化 一文读懂直播卡顿优化那些事儿 “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解! 深入探索Android卡顿优化(上) 西瓜卡顿 & ANR 优化治理及监控体系建设 本篇文章为转载内容。原文链接:https://blog.csdn.net/yuhaibing111/article/details/127682399。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-03-26 08:05:57
214
转载
转载文章
...的委托和事件、委托中方法异常和超时的处理、委托与异步编程、委托和事件对Observer 设计模式的意义,对它们的编译代码也做了讨论。 1.1 理解委托 1.1.1 将方法作为方法的参数 我们先不管这个标题如何的绕口,也不管委托究竟是个什么东西,来看下面这两个最简单的方法,它们不过是在屏幕上输出一句问候的话语: public void GreetPeople(string name){EnglishGreeting(name);}public void EnglishGreeting(string name){Console.WriteLine("Good Morning, " + name);} 暂且不管这两个方法有没有什么实际意义。GreetPeople 用于向某人问好,当我们传递代表某人姓名的 name 参数,比如说“Liker”进去的时候,在这个方法中,将调用 EnglishGreeting 方法,再次传递 name 参数,EnglishGreeting 则用于向屏幕输出 “Good Morning, Liker”。 现在假设这个程序需要进行全球化,哎呀,不好了,我是中国人,我不明白“Good Morning”是什么意思,怎么办呢?好吧,我们再加个中文版的问候方法: public void ChineseGreeting(string name){Console.WriteLine("早上好, " + name);} 这时候,GreetPeople 也需要改一改了,不然如何判断到底用哪个版本的 Greeting 问候方法合适呢?在进行这个之前,我们最好再定义一个枚举作为判断的依据: public enum Language{English, Chinese}public void GreetPeople(string name, Language lang){switch (lang){case Language.English:EnglishGreeting(name);break;case Language.Chinese:ChineseGreeting(name);break;} } OK,尽管这样解决了问题,但我不说大家也很容易想到,这个解决方案的可扩展性很差,如果日后我们需要再添加韩文版、日文版,就不得不反复修改枚举和GreetPeople() 方法,以适应新的需求。 在考虑新的解决方案之前,我们先看看 GreetPeople 的方法签名: public void GreetPeople(string name, Language lang); 我们仅看 string name,在这里,string 是参数类型,name 是参数变量,当我们赋给 name 字符串“Liker”时,它就代表“Liker”这个值;当我们赋给它“李志中”时,它又代表着“李志中”这个值。然后,我们可以在方法体内对这个 name 进行其他操作。哎,这简直是废话么,刚学程序就知道了。 如果你再仔细想想,假如 GreetPeople() 方法可以接受一个参数变量,这个变量可以代表另一个方法,当我们给这个变量赋值 EnglishGreeting 的时候,它代表着 EnglsihGreeting() 这个方法;当我们给它赋值ChineseGreeting 的时候,它又代表着 ChineseGreeting() 法。我们将这个参数变量命名为 MakeGreeting,那么不是可以如同给 name 赋值时一样,在调用 GreetPeople()方法的时候,给这个MakeGreeting 参数也赋上值么(ChineseGreeting 或者EnglsihGreeting 等)?然后,我们在方法体内,也可以像使用别的参数一样使用MakeGreeting。但是,由于 MakeGreeting 代表着一个方法,它的使用方式应该和它被赋的方法(比如ChineseGreeting)是一样的,比如:MakeGreeting(name); 好了,有了思路了,我们现在就来改改GreetPeople()方法,那么它应该是这个样子了: public void GreetPeople(string name, MakeGreeting) { MakeGreeting(name); } 注意到 ,这个位置通常放置的应该是参数的类型,但到目前为止,我们仅仅是想到应该有个可以代表方法的参数,并按这个思路去改写 GreetPeople 方法,现在就出现了一个大问题:这个代表着方法的 MakeGreeting 参数应该是什么类型的? 说明:这里已不再需要枚举了,因为在给MakeGreeting 赋值的时候动态地决定使用哪个方法,是 ChineseGreeting 还是 EnglishGreeting,而在这个两个方法内部,已经对使用“Good Morning”还是“早上好”作了区分。 聪明的你应该已经想到了,现在是委托该出场的时候了,但讲述委托之前,我们再看看MakeGreeting 参数所能代表的 ChineseGreeting()和EnglishGreeting()方法的签名: public void EnglishGreeting(string name) public void ChineseGreeting(string name) 如同 name 可以接受 String 类型的“true”和“1”,但不能接受bool 类型的true 和int 类型的1 一样。MakeGreeting 的参数类型定义应该能够确定 MakeGreeting 可以代表的方法种类,再进一步讲,就是 MakeGreeting 可以代表的方法的参数类型和返回类型。 于是,委托出现了:它定义了 MakeGreeting 参数所能代表的方法的种类,也就是 MakeGreeting 参数的类型。 本例中委托的定义: public delegate void GreetingDelegate(string name); 与上面 EnglishGreeting() 方法的签名对比一下,除了加入了delegate 关键字以外,其余的是不是完全一样?现在,让我们再次改动GreetPeople()方法,如下所示: public delegate void GreetingDelegate(string name);public void GreetPeople(string name, GreetingDelegate MakeGreeting){MakeGreeting(name);} 如你所见,委托 GreetingDelegate 出现的位置与 string 相同,string 是一个类型,那么 GreetingDelegate 应该也是一个类型,或者叫类(Class)。但是委托的声明方式和类却完全不同,这是怎么一回事?实际上,委托在编译的时候确实会编译成类。因为 Delegate 是一个类,所以在任何可以声明类的地方都可以声明委托。更多的内容将在下面讲述,现在,请看看这个范例的完整代码: public delegate void GreetingDelegate(string name);class Program{private static void EnglishGreeting(string name){Console.WriteLine("Good Morning, " + name);}private static void ChineseGreeting(string name){Console.WriteLine("早上好, " + name);}private static void GreetPeople(string name, GreetingDelegate MakeGreeting){MakeGreeting(name);}static void Main(string[] args){GreetPeople("Liker", EnglishGreeting);GreetPeople("李志中", ChineseGreeting);Console.ReadLine();} } 我们现在对委托做一个总结:委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If … Else(Switch)语句,同时使得程序具有更好的可扩展性。 1.1.2 将方法绑定到委托 看到这里,是不是有那么点如梦初醒的感觉?于是,你是不是在想:在上面的例子中,我不一定要直接在 GreetPeople() 方法中给 name 参数赋值,我可以像这样使用变量: static void Main(string[] args){GreetPeople("Liker", EnglishGreeting);GreetPeople("李志中", ChineseGreeting);Console.ReadLine();} 而既然委托 GreetingDelegate 和类型 string 的地位一样,都是定义了一种参数类型,那么,我是不是也可以这么使用委托? static void Main(string[] args){GreetingDelegate delegate1, delegate2;delegate1 = EnglishGreeting;delegate2 = ChineseGreeting;GreetPeople("Liker", delegate1);GreetPeople("李志中", delegate2);Console.ReadLine();} 如你所料,这样是没有问题的,程序一如预料的那样输出。这里,我想说的是委托不同于 string 的一个特性:可以将多个方法赋给同一个委托,或者叫将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其所绑定的方法。在这个例子中,语法如下: static void Main(string[] args){GreetingDelegate delegate1;delegate1 = EnglishGreeting; delegate1 += ChineseGreeting;GreetPeople("Liker", delegate1);Console.ReadLine();} 实际上,我们可以也可以绕过GreetPeople 方法,通过委托来直接调用EnglishGreeting 和ChineseGreeting: static void Main(string[] args){GreetingDelegate delegate1;delegate1 = EnglishGreeting;delegate1 += ChineseGreeting; delegate1("Liker");Console.ReadLine();} 说明:这在本例中是没有问题的,但回头看下上面 GreetPeople() 的定义,在它之中可以做一些对于 EnglshihGreeting 和 ChineseGreeting 来说都需要进行的工作,为了简便我做了省略。 注意这里,第一次用的“=”,是赋值的语法;第二次,用的是“+=”,是绑定的语法。如果第一次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。我们也可以使用下面的代码来这样简化这一过程: GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);delegate1 += ChineseGreeting; 既然给委托可以绑定一个方法,那么也应该有办法取消对方法的绑定,很容易想到,这个语法是“-=”: static void Main(string[] args){GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);delegate1 += ChineseGreeting;GreetPeople("Liker", delegate1);Console.WriteLine();delegate1 -= EnglishGreeting;GreetPeople("李志中", delegate1);Console.ReadLine();} 让我们再次对委托作个总结: 使用委托可以将多个方法绑定到同一个委托变量,当调用此变量时(这里用“调用”这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。 1.2 事件的由来 1.2.1 更好的封装性 我们继续思考上面的程序:上面的三个方法都定义在 Programe 类中,这样做是为了理解的方便,实际应用中,通常都是 GreetPeople 在一个类中,ChineseGreeting 和 EnglishGreeting 在另外的类中。现在你已经对委托有了初步了解,是时候对上面的例子做个改进了。假设我们将 GreetingPeople() 放在一个叫 GreetingManager 的类中,那么新程序应该是这个样子的: namespace Delegate{public delegate void GreetingDelegate(string name);public class GreetingManager{public void GreetPeople(string name, GreetingDelegate MakeGreeting){MakeGreeting(name);} }class Program{private static void EnglishGreeting(string name){Console.WriteLine("Good Morning, " + name);}private static void ChineseGreeting(string name){Console.WriteLine("早上好, " + name);}static void Main(string[] args){GreetingManager gm = new GreetingManager();gm.GreetPeople("Liker", EnglishGreeting);gm.GreetPeople("李志中", ChineseGreeting);} }} 我们运行这段代码,嗯,没有任何问题。程序一如预料地那样输出了: // Good Morning, Liker 早上好, 李志中 // 现在,假设我们需要使用上一节学到的知识,将多个方法绑定到同一个委托变量,该如何做呢?让我们再次改写代码: static void Main(string[] args){GreetingManager gm = new GreetingManager();GreetingDelegate delegate1;delegate1 = EnglishGreeting;delegate1 += ChineseGreeting;gm.GreetPeople("Liker", delegate1);} 输出: Good Morning, Liker 早上好, Liker 到了这里,我们不禁想到:面向对象设计,讲究的是对象的封装,既然可以声明委托类型的变量(在上例中是delegate1),我们何不将这个变量封装到 GreetManager 类中?在这个类的客户端中使用不是更方便么?于是,我们改写GreetManager 类,像这样: public class GreetingManager{/// <summary>/// 在 GreetingManager 类的内部声明 delegate1 变量/// </summary>public GreetingDelegate delegate1;public void GreetPeople(string name, GreetingDelegate MakeGreeting){MakeGreeting(name);} } 现在,我们可以这样使用这个委托变量: static void Main(string[] args){GreetingManager gm = new GreetingManager();gm.delegate1 = EnglishGreeting;gm.delegate1 += ChineseGreeting;gm.GreetPeople("Liker", gm.delegate1);} 输出为: Good Morning, Liker 早上好, Liker 尽管这样做没有任何问题,但我们发现这条语句很奇怪。在调用gm.GreetPeople 方法的时候,再次传递了gm 的delegate1 字段, 既然如此,我们何不修改 GreetingManager 类成这样: public class GreetingManager{/// <summary>/// 在 GreetingManager 类的内部声明 delegate1 变量/// </summary>public GreetingDelegate delegate1;public void GreetPeople(string name){if (delegate1 != null) // 如果有方法注册委托变量{ delegate1(name); // 通过委托调用方法} }} 在客户端,调用看上去更简洁一些: static void Main(string[] args){GreetingManager gm = new GreetingManager();gm.delegate1 = EnglishGreeting;gm.delegate1 += ChineseGreeting;gm.GreetPeople("Liker"); //注意,这次不需要再传递 delegate1 变量} 尽管这样达到了我们要的效果,但是还是存在着问题:在这里,delegate1 和我们平时用的string 类型的变量没有什么分别,而我们知道,并不是所有的字段都应该声明成public,合适的做法是应该public 的时候public,应该private 的时候private。 我们先看看如果把 delegate1 声明为 private 会怎样?结果就是:这简直就是在搞笑。因为声明委托的目的就是为了把它暴露在类的客户端进行方法的注册,你把它声明为 private 了,客户端对它根本就不可见,那它还有什么用? 再看看把delegate1 声明为 public 会怎样?结果就是:在客户端可以对它进行随意的赋值等操作,严重破坏对象的封装性。 最后,第一个方法注册用“=”,是赋值语法,因为要进行实例化,第二个方法注册则用的是“+=”。但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时先后顺序不同,再没有任何的分别,这样不是让人觉得很别扭么? 现在我们想想,如果delegate1 不是一个委托类型,而是一个string 类型,你会怎么做?答案是使用属性对字段进行封装。 于是,Event 出场了,它封装了委托类型的变量,使得:在类的内部,不管你声明它是public还是protected,它总是private 的。在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。我们改写GreetingManager 类,它变成了这个样子: public class GreetingManager{//这一次我们在这里声明一个事件public event GreetingDelegate MakeGreet;public void GreetPeople(string name){MakeGreet(name);} } 很容易注意到:MakeGreet 事件的声明与之前委托变量 delegate1 的声明唯一的区别是多了一个 event 关键字。看到这里,在结合上面的讲解,你应该明白到:事件其实没什么不好理解的,声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。 为了证明上面的推论,如果我们像下面这样改写Main 方法: static void Main(string[] args){GreetingManager gm = new GreetingManager();gm.MakeGreet = EnglishGreeting; // 编译错误1gm.MakeGreet += ChineseGreeting;gm.GreetPeople("Liker");} 会得到编译错误: 1.2.2 限制类型能力 使用事件不仅能获得比委托更好的封装性以外,还能限制含有事件的类型的能力。这是什么意思呢?它的意思是说:事件应该由事件发布者触发,而不应该由事件的客户端(客户程序)来触发。请看下面的范例: using System;class Program{static void Main(string[] args){Publishser pub = new Publishser();Subscriber sub = new Subscriber();pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);pub.DoSomething(); // 应该通过DoSomething()来触发事件pub.NumberChanged(100); // 但可以被这样直接调用,对委托变量的不恰当使用} }/// <summary>/// 定义委托/// </summary>/// <param name="count"></param>public delegate void NumberChangedEventHandler(int count);/// <summary>/// 定义事件发布者/// </summary>public class Publishser{private int count;public NumberChangedEventHandler NumberChanged; // 声明委托变量//public event NumberChangedEventHandler NumberChanged; // 声明一个事件public void DoSomething(){// 在这里完成一些工作 ...if (NumberChanged != null) // 触发事件{ count++;NumberChanged(count);} }}/// <summary>/// 定义事件订阅者/// </summary>public class Subscriber{public void OnNumberChanged(int count){Console.WriteLine("Subscriber notified: count = {0}", count);} } 上面代码定义了一个NumberChangedEventHandler 委托,然后我们创建了事件的发布者Publisher 和订阅者Subscriber。当使用委托变量时,客户端可以直接通过委托变量触发事件,也就是直接调用pub.NumberChanged(100),这将会影响到所有注册了该委托的订阅者。而事件的本意应该为在事件发布者在其本身的某个行为中触发,比如说在方法DoSomething()中满足某个条件后触发。通过添加event 关键字来发布事件,事件发布者的封装性会更好,事件仅仅是供其他类型订阅,而客户端不能直接触发事件(语句pub.NumberChanged(100)无法通过编译),事件只能在事件发布者Publisher 类的内部触发(比如在方法pub.DoSomething()中),换言之,就是NumberChanged(100)语句只能在Publisher 内部被调用。大家可以尝试一下,将委托变量的声明那行代码注释掉,然后取消下面事件声明的注释。此时程序是无法编译的,当你使用了event 关键字之后,直接在客户端触发事件这种行为,也就是直接调用pub.NumberChanged(100),是被禁止的。事件只能通过调用DoSomething() 来触发。这样才是事件的本意,事件发布者的封装才会更好。 就好像如果我们要定义一个数字类型,我们会使用int 而不是使用object 一样,给予对象过多的能力并不见得是一件好事,应该是越合适越好。尽管直接使用委托变量通常不会有什么问题,但它给了客户端不应具有的能力,而使用事件,可以限制这一能力,更精确地对类型进行封装。 说 明:这里还有一个约定俗称的规定,就是订阅事件的方法的命名,通常为“On 事件名”,比如这里的OnNumberChanged。 1.3 委托的编译代码 这时候,我们注释掉编译错误的行,然后重新进行编译,再借助 Reflactor 来对 event 的声明语句做一探究,看看为什么会发生这样的错误: 可以看到,实际上尽管我们在GreetingManager 里将 MakeGreet 声明为public,但是,实际上MakeGreet 会被编译成私有字段,难怪会发生上面的编译错误了,因为它根本就不允许在GreetingManager 类的外面以赋值的方式访问,从而验证了我们上面所做的推论。 我们再进一步看下MakeGreet 所产生的代码: // private GreetingDelegate MakeGreet; //对事件的声明实际是声明一个私有的委托变量 [MethodImpl(MethodImplOptions.Synchronized)] public void add_MakeGreet(GreetingDelegate value) { this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value); } [MethodImpl(MethodImplOptions.Synchronized)] public void remove_MakeGreet(GreetingDelegate value) { this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value); } // 现在已经很明确了:MakeGreet 事件确实是一个GreetingDelegate 类型的委托,只不过不管是不是声明为public,它总是被声明为private。另外,它还有两个方法,分别是add_MakeGreet和remove_MakeGreet,这两个方法分别用于注册委托类型的方法和取消注册。实际上也就是:“+= ”对应 add_MakeGreet,“-=”对应remove_MakeGreet。而这两个方法的访问限制取决于声明事件时的访问限制符。 在add_MakeGreet()方法内部,实际上调用了System.Delegate 的Combine()静态方法,这个方法用于将当前的变量添加到委托链表中。 我们前面提到过两次,说委托实际上是一个类,在我们定义委托的时候: // public delegate void GreetingDelegate(string name); // 当编译器遇到这段代码的时候,会生成下面这样一个完整的类: // public class GreetingDelegate:System.MulticastDelegate { public GreetingDelegate(object @object, IntPtr method); public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object); public virtual void EndInvoke(IAsyncResult result); public virtual void Invoke(string name); } // 1.4 .NET 框架中的委托和事件 1.4.1 范例说明 上面的例子已不足以再进行下面的讲解了,我们来看一个新的范例,因为之前已经介绍了很多的内容,所以本节的进度会稍微快一些! 假设我们有个高档的热水器,我们给它通上电,当水温超过95 度的时候:1、扬声器会开始发出语音,告诉你水的温度;2、液晶屏也会改变水温的显示,来提示水已经快烧开了。 现在我们需要写个程序来模拟这个烧水的过程,我们将定义一个类来代表热水器,我们管它叫:Heater,它有代表水温的字段,叫做 temperature;当然,还有必不可少的给水加热方法 BoilWater(),一个发出语音警报的方法 MakeAlert(),一个显示水温的方法,ShowMsg()。 namespace Delegate{/// <summary>/// 热水器/// </summary>public class Heater{/// <summary>/// 水温/// </summary>private int temperature;/// <summary>/// 烧水/// </summary>public void BoilWater(){for (int i = 0; i <= 100; i++){temperature = i;if (temperature > 95){MakeAlert(temperature);ShowMsg(temperature);} }}/// <summary>/// 发出语音警报/// </summary>/// <param name="param"></param>private void MakeAlert(int param){Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);}/// <summary>/// 显示水温/// </summary>/// <param name="param"></param>private void ShowMsg(int param){Console.WriteLine("Display:水快开了,当前温度:{0}度。", param);} }class Program{static void Main(){Heater ht = new Heater();ht.BoilWater();} }} 1.4.2 Observer 设计模式简介 上面的例子显然能完成我们之前描述的工作,但是却并不够好。现在假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。 这时候,上面的例子就应该变成这个样子: /// <summary>/// 热水器/// </summary>public class Heater{private int temperature; private void BoilWater(){for (int i = 0; i <= 100; i++){temperature = i;} }}/// <summary>/// 警报器/// </summary>public class Alarm{private void MakeAlert(int param){Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);} }/// <summary>/// 显示器/// </summary>public class Display{private void ShowMsg(int param){Console.WriteLine("Display:水已烧开,当前温度:{0}度。", param);} } 这里就出现了一个问题:如何在水烧开的时候通知报警器和显示器? 在继续进行之前,我们先了解一下Observer 设计模式,Observer 设计模式中主要包括如下两类对象: Subject:监视对象,它往往包含着其他对象所感兴趣的内容。在本范例中,热水器就是一个监视对象,它包含的其他对象所感兴趣的内容,就是 temprature 字段,当这个字段的值快到100 时,会不断把数据发给监视它的对象。 Observer:监视者,它监视Subject,当 Subject 中的某件事发生的时候,会告知Observer,而Observer 则会采取相应的行动。在本范例中,Observer 有警报器和显示器,它们采取的行动分别是发出警报和显示水温。 在本例中,事情发生的顺序应该是这样的: 1. 警报器和显示器告诉热水器,它对它的温度比较感兴趣(注册)。 2. 热水器知道后保留对警报器和显示器的引用。 3. 热水器进行烧水这一动作,当水温超过 95 度时,通过对警报器和显示器的引用,自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。 类似这样的例子是很多的,GOF 对它进行了抽象,称为 Observer 设计模式:Observer 设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。Observer 模式是一种松耦合的设计模式。 1.4.3 实现范例的Observer 设计模式 我们之前已经对委托和事件介绍很多了,现在写代码应该很容易了,现在在这里直接给出代码,并在注释中加以说明。 namespace Delegate{public class Heater{private int temperature;public delegate void BoilHandler(int param);public event BoilHandler BoilEvent;public void BoilWater(){for (int i = 0; i <= 100; i++){temperature = i;if (temperature > 95){if (BoilEvent != null){ BoilEvent(temperature); // 调用所有注册对象的方法} }} }}public class Alarm{public void MakeAlert(int param){Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);} }public class Display{public static void ShowMsg(int param) // 静态方法{ Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", param);} }class Program{static void Main(){Heater heater = new Heater();Alarm alarm = new Alarm();heater.BoilEvent += alarm.MakeAlert; // 注册方法heater.BoilEvent += (new Alarm()).MakeAlert; // 给匿名对象注册方法heater.BoilEvent += Display.ShowMsg; // 注册静态方法heater.BoilWater(); // 烧水,会自动调用注册过对象的方法} }} 输出为: // Alarm:嘀嘀嘀,水已经 96 度了: Alarm:嘀嘀嘀,水已经 96 度了: Display:水快烧开了,当前温度:96 度。 // 省略... // 1.4.4 .NET 框架中的委托与事件 尽管上面的范例很好地完成了我们想要完成的工作,但是我们不仅疑惑:为什么.NET Framework 中的事件模型和上面的不同?为什么有很多的EventArgs 参数? 在回答上面的问题之前,我们先搞懂 .NET Framework 的编码规范: 1. 委托类型的名称都应该以 EventHandler 结束。 2. 委托的原型定义:有一个void 返回值,并接受两个输入参数:一个Object 类型,一个EventArgs 类型(或继承自EventArgs)。 3. 事件的命名为委托去掉 EventHandler 之后剩余的部分。 4. 继承自 EventArgs 的类型应该以EventArgs 结尾。 再做一下说明: 1. 委托声明原型中的Object 类型的参数代表了Subject,也就是监视对象,在本例中是Heater(热水器)。回调函数(比如Alarm 的MakeAlert)可以通过它访问触发事件的对象(Heater)。 2. EventArgs 对象包含了Observer 所感兴趣的数据,在本例中是temperature。 上面这些其实不仅仅是为了编码规范而已,这样也使得程序有更大的灵活性。比如说,如果我们不光想获得热水器的温度,还想在Observer 端(警报器或者显示器)方法中获得它的生产日期、型号、价格,那么委托和方法的声明都会变得很麻烦,而如果我们将热水器的引用传给警报器的方法,就可以在方法中直接访问热水器了。 现在我们改写之前的范例,让它符合.NET Framework的规范: using System;using System.Collections.Generic;using System.Text;namespace Delegate{public class Heater{private int temperature;public string type = "RealFire 001"; // 添加型号作为演示public string area = "China Xian"; // 添加产地作为演示public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);public event BoiledEventHandler Boiled; // 声明事件// 定义 BoiledEventArgs 类,传递给 Observer 所感兴趣的信息public class BoiledEventArgs : EventArgs{public readonly int temperature;public BoiledEventArgs(int temperature){this.temperature = temperature;} }// 可以供继承自 Heater 的类重写,以便继承类拒绝其他对象对它的监视protected virtual void OnBoiled(BoiledEventArgs e){if (Boiled != null){Boiled(this, e); // 调用所有注册对象的方法} }public void BoilWater(){for (int i = 0; i <= 100; i++){temperature = i;if (temperature > 95){// 建立BoiledEventArgs 对象。BoiledEventArgs e = new BoiledEventArgs(temperature);OnBoiled(e); // 调用 OnBolied 方法} }}public class Alarm{public void MakeAlert(Object sender, Heater.BoiledEventArgs e){Heater heater = (Heater)sender; // 这里是不是很熟悉呢?// 访问 sender 中的公共字段Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);Console.WriteLine("Alarm: 嘀嘀嘀,水已经 {0} 度了:", e.temperature);Console.WriteLine();} }public class Display{public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) // 静态方法{Heater heater = (Heater)sender;Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", e.temperature);Console.WriteLine();} }class Program{static void Main(){Heater heater = new Heater();Alarm alarm = new Alarm();heater.Boiled += alarm.MakeAlert; //注册方法heater.Boiled += (new Alarm()).MakeAlert; //给匿名对象注册方法heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert); //也可以这么注册heater.Boiled += Display.ShowMsg; //注册静态方法heater.BoilWater(); //烧水,会自动调用注册过对象的方法} }} } 输出为: Alarm:China Xian - RealFire 001: Alarm: 嘀嘀嘀,水已经 96 度了: Alarm:China Xian - RealFire 001: Alarm: 嘀嘀嘀,水已经 96 度了: Alarm:China Xian - RealFire 001: Alarm: 嘀嘀嘀,水已经 96 度了: Display:China Xian - RealFire 001: Display:水快烧开了,当前温度:96 度。 // 省略 ... 1.5 委托进阶 1.5.1 为什么委托定义的返回值通常都为 void ? 尽管并非必需,但是我们发现很多的委托定义返回值都为 void,为什么呢?这是因为委托变量可以供多个订阅者注册,如果定义了返回值,那么多个订阅者的方法都会向发布者返回数值,结果就是后面一个返回的方法值将前面的返回值覆盖掉了,因此,实际上只能获得最后一个方法调用的返回值。可以运行下面的代码测试一下。除此以外,发布者和订阅者是松耦合的,发布者根本不关心谁订阅了它的事件、为什么要订阅,更别说订阅者的返回值了,所以返回订阅者的方法返回值大多数情况下根本没有必要。 1.5.2 如何让事件只允许一个客户订阅? 少数情况下,比如像上面,为了避免发生“值覆盖”的情况(更多是在异步调用方法时,后面会讨论),我们可能想限制只允许一个客户端注册。此时怎么做呢?我们可以向下面这样,将事件声明为private 的,然后提供两个方法来进行注册和取消注册: public class Publishser{private event GeneralEventHandler NumberChanged; // 声明一个私有事件// 注册事件public void Register(GeneralEventHandler method){NumberChanged = method;}// 取消注册public void UnRegister(GeneralEventHandler method){NumberChanged -= method;}public void DoSomething(){// 做某些其余的事情if (NumberChanged != null){ // 触发事件string rtn = NumberChanged();Console.WriteLine("Return: {0}", rtn); // 打印返回的字符串,输出为Subscriber3} }} 注意上面,在UnRegister()中,没有进行任何判断就使用了NumberChanged -= method 语句。这是因为即使method 方法没有进行过注册,此行语句也不会有任何问题,不会抛出异常,仅仅是不会产生任何效果而已。 注意在Register()方法中,我们使用了赋值操作符“=”,而非“+=”,通过这种方式就避免了多个方法注册。 1.7 委托和方法的异步调用 通常情况下,如果需要异步执行一个耗时的操作,我们会新起一个线程,然后让这个线程去执行代码。但是对于每一个异步调用都通过创建线程来进行操作显然会对性能产生一定的影响,同时操作也相对繁琐一些。.NET 中可以通过委托进行方法的异步调用,就是说客户端在异步调用方法时,本身并不会因为方法的调用而中断,而是从线程池中抓取一个线程去执行该方法,自身线程(主线程)在完成抓取线程这一过程之后,继续执行下面的代码,这样就实现了代码的并行执行。使用线程池的好处就是避免了频繁进行异步调用时创建、销毁线程的开销。当我们在委托对象上调用BeginInvoke()时,便进行了一个异步的方法调用。 事件发布者和订阅者之间往往是松耦合的,发布者通常不需要获得订阅者方法执行的情况;而当使用异步调用时,更多情况下是为了提升系统的性能,而并非专用于事件的发布和订阅这一编程模型。而在这种情况下使用异步编程时,就需要进行更多的控制,比如当异步执行方法的方法结束时通知客户端、返回异步执行方法的返回值等。本节就对 BeginInvoke() 方法、EndInvoke() 方法和其相关的 IAysncResult 做一个简单的介绍。 我们先看这样一段代码,它演示了不使用异步调用的通常情况: class Program7{static void Main(string[] args){Console.WriteLine("Client application started!\n");Thread.CurrentThread.Name = "Main Thread";Calculator cal = new Calculator();int result = cal.Add(2, 5);Console.WriteLine("Result: {0}\n", result);// 做某些其它的事情,模拟需要执行3 秒钟for (int i = 1; i <= 3; i++){Thread.Sleep(TimeSpan.FromSeconds(i));Console.WriteLine("{0}: Client executed {1} second(s).", Thread.CurrentThread.Name, i);}Console.WriteLine("\nPress any key to exit...");Console.ReadLine();} }public class Calculator{public int Add(int x, int y){if (Thread.CurrentThread.IsThreadPoolThread){Thread.CurrentThread.Name = "Pool Thread";}Console.WriteLine("Method invoked!");// 执行某些事情,模拟需要执行2 秒钟for (int i = 1; i <= 2; i++){Thread.Sleep(TimeSpan.FromSeconds(i));Console.WriteLine("{0}: Add executed {1} second(s).", Thread.CurrentThread.Name, i);}Console.WriteLine("Method complete!");return x + y;} } 上面代码有几个关于对于线程的操作,如果不了解可以看一下下面的说明,如果你已经了解可以直接跳过: 1. Thread.Sleep(),它会让执行当前代码的线程暂停一段时间(如果你对线程的概念比较陌生,可以理解为使程序的执行暂停一段时间),以毫秒为单位,比如Thread.Sleep(1000),将会使线程暂停1 秒钟。在上面我使用了它的重载方法,个人觉得使用TimeSpan.FromSeconds(1),可读性更好一些。 2. Thread.CurrentThread.Name,通过这个属性可以设置、获取执行当前代码的线程的名称,值得注意的是这个属性只可以设置一次,如果设置两次,会抛出异常。 3. Thread.IsThreadPoolThread,可以判断执行当前代码的线程是否为线程池中的线程。 通过这几个方法和属性,有助于我们更好地调试异步调用方法。上面代码中除了加入了一些对线程的操作以外再没有什么特别之处。我们建了一个Calculator 类,它只有一个Add 方法,我们模拟了这个方法需要执行2 秒钟时间,并且每隔一秒进行一次输出。而在客户端程序中,我们使用result 变量保存了方法的返回值并进行了打印。随后,我们再次模拟了客户端程序接下来的操作需要执行2 秒钟时间。运行这段程序,会产生下面的输出: // Client application started! Method invoked! Main Thread: Add executed 1 second(s). Main Thread: Add executed 2 second(s). Method complete! Result: 7 Main Thread: Client executed 1 second(s). Main Thread: Client executed 2 second(s). Main Thread: Client executed 3 second(s). Press any key to exit... // 如果你确实执行了这段代码,会看到这些输出并不是一瞬间输出的,而是执行了大概5 秒钟的时间,因为线程是串行执行的,所以在执行完 Add() 方法之后才会继续客户端剩下的代码。 接下来我们定义一个AddDelegate 委托,并使用BeginInvoke()方法来异步地调用它。在上面已经介绍过,BeginInvoke()除了最后两个参数为AsyncCallback 类型和Object 类型以外,前面的参数类型和个数与委托定义相同。另外BeginInvoke()方法返回了一个实现了IAsyncResult 接口的对象(实际上就是一个AsyncResult 类型实例,注意这里IAsyncResult 和AysncResult 是不同的,它们均包含在.NET Framework 中)。 AsyncResult 的用途有这么几个:传递参数,它包含了对调用了BeginInvoke()的委托的引用;它还包含了BeginInvoke()的最后一个Object 类型的参数;它可以鉴别出是哪个方法的哪一次调用,因为通过同一个委托变量可以对同一个方法调用多次。 EndInvoke()方法接受IAsyncResult 类型的对象(以及ref 和out 类型参数,这里不讨论了,对它们的处理和返回值类似),所以在调用BeginInvoke()之后,我们需要保留IAsyncResult,以便在调用EndInvoke()时进行传递。这里最重要的就是EndInvoke()方法的返回值,它就是方法的返回值。除此以外,当客户端调用EndInvoke()时,如果异步调用的方法没有执行完毕,则会中断当前线程而去等待该方法,只有当异步方法执行完毕后才会继续执行后面的代码。所以在调用完BeginInvoke()后立即执行EndInvoke()是没有任何意义的。我们通常在尽可能早的时候调用BeginInvoke(),然后在需要方法的返回值的时候再去调用EndInvoke(),或者是根据情况在晚些时候调用。说了这么多,我们现在看一下使用异步调用改写后上面的代码吧: using System.Threading;using System;public delegate int AddDelegate(int x, int y);class Program8{static void Main(string[] args){Console.WriteLine("Client application started!\n");Thread.CurrentThread.Name = "Main Thread";Calculator cal = new Calculator();AddDelegate del = new AddDelegate(cal.Add);IAsyncResult asyncResult = del.BeginInvoke(2, 5, null, null); // 异步调用方法// 做某些其它的事情,模拟需要执行3 秒钟for (int i = 1; i <= 3; i++){Thread.Sleep(TimeSpan.FromSeconds(i));Console.WriteLine("{0}: Client executed {1} second(s).", Thread.CurrentThread.Name, i);}int rtn = del.EndInvoke(asyncResult);Console.WriteLine("Result: {0}\n", rtn);Console.WriteLine("\nPress any key to exit...");Console.ReadLine();} }public class Calculator{public int Add(int x, int y){if (Thread.CurrentThread.IsThreadPoolThread){Thread.CurrentThread.Name = "Pool Thread";}Console.WriteLine("Method invoked!");// 执行某些事情,模拟需要执行2 秒钟for (int i = 1; i <= 2; i++){Thread.Sleep(TimeSpan.FromSeconds(i));Console.WriteLine("{0}: Add executed {1} second(s).", Thread.CurrentThread.Name, i);}Console.WriteLine("Method complete!");return x + y;} } 此时的输出为: // Client application started! Method invoked! Main Thread: Client executed 1 second(s). Pool Thread: Add executed 1 second(s). Main Thread: Client executed 2 second(s). Pool Thread: Add executed 2 second(s). Method complete! Main Thread: Client executed 3 second(s). Result: 7 Press any key to exit... // 现在执行完这段代码只需要3 秒钟时间,两个for 循环所产生的输出交替进行,这也说明了这两段代码并行执行的情况。可以看到Add() 方法是由线程池中的线程在执行, 因为Thread.CurrentThread.IsThreadPoolThread 返回了True,同时我们对该线程命名为了Pool Thread。另外我们可以看到通过EndInvoke()方法得到了返回值。有时候,我们可能会将获得返回值的操作放到另一段代码或者客户端去执行,而不是向上面那样直接写在BeginInvoke()的后面。比如说我们在Program 中新建一个方法GetReturn(),此时可以通过AsyncResult 的AsyncDelegate 获得del 委托对象,然后再在其上调用EndInvoke()方法,这也说明了AsyncResult 可以唯一的获取到与它相关的调用了的方法(或者也可以理解成委托对象)。所以上面获取返回值的代码也可以改写成这样: private static int GetReturn(IAsyncResult asyncResult){AsyncResult result = (AsyncResult)asyncResult;AddDelegate del = (AddDelegate)result.AsyncDelegate;int rtn = del.EndInvoke(asyncResult);return rtn;} 然后再将int rtn = del.EndInvoke(asyncResult);语句改为int rtn = GetReturn(asyncResult);。注意上面IAsyncResult 要转换为实际的类型AsyncResult 才能访问AsyncDelegate 属性,因为它没有包含在IAsyncResult 接口的定义中。 BeginInvoke 的另外两个参数分别是AsyncCallback 和Object 类型,其中AsyncCallback 是一个委托类型,它用于方法的回调,即是说当异步方法执行完毕时自动进行调用的方法。它的定义为: // public delegate void AsyncCallback(IAsyncResult ar); // Object 类型用于传递任何你想要的数值,它可以通过IAsyncResult 的AsyncState 属性获得。下面我们将获取方法返回值、打印返回值的操作放到了OnAddComplete()回调方法中: using System.Threading;using System;using System.Runtime.Remoting.Messaging;public delegate int AddDelegate(int x, int y);class Program9{static void Main(string[] args){Console.WriteLine("Client application started!\n");Thread.CurrentThread.Name = "Main Thread";Calculator cal = new Calculator();AddDelegate del = new AddDelegate(cal.Add);string data = "Any data you want to pass.";AsyncCallback callBack = new AsyncCallback(OnAddComplete);del.BeginInvoke(2, 5, callBack, data); // 异步调用方法// 做某些其它的事情,模拟需要执行3 秒钟for (int i = 1; i <= 3; i++){Thread.Sleep(TimeSpan.FromSeconds(i));Console.WriteLine("{0}: Client executed {1} second(s).", Thread.CurrentThread.Name, i);}Console.WriteLine("\nPress any key to exit...");Console.ReadLine();}static void OnAddComplete(IAsyncResult asyncResult){AsyncResult result = (AsyncResult)asyncResult;AddDelegate del = (AddDelegate)result.AsyncDelegate;string data = (string)asyncResult.AsyncState;int rtn = del.EndInvoke(asyncResult);Console.WriteLine("{0}: Result, {1}; Data: {2}\n", Thread.CurrentThread.Name, rtn, data);} }public class Calculator{public int Add(int x, int y){if (Thread.CurrentThread.IsThreadPoolThread){Thread.CurrentThread.Name = "Pool Thread";}Console.WriteLine("Method invoked!");// 执行某些事情,模拟需要执行2 秒钟for (int i = 1; i <= 2; i++){Thread.Sleep(TimeSpan.FromSeconds(i));Console.WriteLine("{0}: Add executed {1} second(s).", Thread.CurrentThread.Name, i);}Console.WriteLine("Method complete!");return x + y;} } 它产生的输出为: Client application started! Method invoked! Main Thread: Client executed 1 second(s). Pool Thread: Add executed 1 second(s). Main Thread: Client executed 2 second(s). Pool Thread: Add executed 2 second(s). Method complete! Pool Thread: Result, 7; Data: Any data you want to pass. Main Thread: Client executed 3 second(s). Press any key to exit... 这里有几个值得注意的地方: 1、我们在调用BeginInvoke()后不再需要保存IAysncResult 了,因为AysncCallback 委托将该对象定义在了回调方法的参数列表中; 2、我们在OnAddComplete()方法中获得了调用BeginInvoke()时最后一个参数传递的值,字符串“Any data you want to pass”; 3、执行回调方法的线程并非客户端线程Main Thread,而是来自线程池中的线程Pool Thread。另外如前面所说,在调用EndInvoke()时有可能会抛出异常,所以在应该将它放到try/catch 块中,这里就不再示范了。 1.8 总结 我们详细地讨论了C中的委托和事件,包括什么是委托、为什么要使用委托、事件的由来、.NET Framework 中的委托和事件、委托中方法异常和超时的处理、委托与异步编程、委托和事件对Observer 设计模式的意义。拥有了本章的知识,相信你以后遇到委托和事件时,将不会再有所畏惧。 本篇文章为转载内容。原文链接:https://blog.csdn.net/beyonddeg/article/details/53528482。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-10-05 16:02:19
80
转载
转载文章
...,供学习服务进行远程调用。 在学习服务中远程调用 课程计划媒资信息查询接口,获取该课程计划的视频播放的 m3u8 url地址,并返回给前端,前端使用该 url 进行视频的在线播放。 在线学习完整的测试流程:媒资信息的上传、选择、发布到前端门户、搜索门户测试,在线学习的播放视频。 目录 内容会比较多,小伙伴门可以根据目录进行按需查阅。 文章目录 😎 知识点概览 目录 一、学习页面:查询课程计划 0x01 需求分析 0x02 Api接口 0x03 服务端开发 Controller Service 测试 0x04 前端开发 配置NGINX虚拟主机 前端 API 方法 前端 API 方法调用 测试 二、学习页面:获取视频播放地址 0x01 需求分析 0x02 课程发布:储存媒资信息 需求分析 数据模型 Dao Service 测试 0x03 Logstash:扫描课程计划媒资 创建索引 创建模板文件 配置 mysql.conf 启动 logstash.bat Logstash多实例运行 0x04 搜素服务:查询课程媒资接口 需求分析 Api接口定义 Service Controller 测试 三、在线学习:接口开发 0x01 需求分析 0x02 搭建开发环境 0x03 Api接口 0x04 服务端开发 需求分析 搜索服务注册Eureka 搜索服务客户端 自定义错误代码 Service Controller 测试 0x05 前端开发 需求分析 api方法 配置代理 视频播放页面 简单的测试 完整的测试 1、上传文件 一些问题 ~~方案1:删除本地分块文件重新尝试上传~~ 方案2:检查前端提交的MD5值是否正确 2、为课程计划选择媒资信息 3、前端门户测试 四、待完善的一些功能 😁 认识作者 一、学习页面:查询课程计划 0x01 需求分析 到目前为止,我们已可以编辑课程计划信息并上传课程视频,下一步我们要实现在线学习页面动态读取章节对应的视频并进行播放。在线学习页面所需要的信息有两类: 课程计划信息 课程学习信息(视频地址、学习进度等) 如下图: 在线学习集成媒资管理的需求如下: 1、在线学习页面显示课程计划 2、点击课程计划播放该课程计划对应的视频 本章节实现学习页面动态显示课程计划,进入不同课程的学习页面右侧动态显示当前课程的课程计划。 0x02 Api接口 课程计划信息从哪里获取? 在课程发布完成后会自动发布到一个 course_pub 的表中,logstash 会自动将课程发布后的信息自动采集到 ES 索引库中,这些信息也包含课程计划信息。 所以考虑性能要求,课程发布后对课程的查询统一从 ES 索引库中查询。 前端通过请求 搜索服务 获取课程信息,需要单独在 搜索服务 中定义课程信息查询接口。 本接口接收课程id,查询课程所有信息返回给前端。 我们在搜素服务 API 下添加以下方法 @ApiOperation("根据id搜索课程发布信息")public Map<String,CoursePub> getdetail(String id); 返回的课程信息为 json 结构:key 为课程id,value 为课程内容。 0x03 服务端开发 在搜索服务中开发查询课程信息接口。 Controller 在搜素服务下添加以下方法 / 根据id搜索课程发布信息 @param id 课程id @return JSON数据/@Override@GetMapping("/getdetail/{id}")public Map<String, CoursePub> getdetail(@PathVariable("id")String id) {return esCourseService.getdetail(id);} Service / 根据id搜索课程发布信息 @param id 课程id @return JSON数据/public Map<String, CoursePub> getdetail(String id) {//设置索引SearchRequest searchRequest = new SearchRequest(es_index);//设置类型searchRequest.types(es_type);//创建搜索源对象SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();//设置查询条件,根据id进行查询searchSourceBuilder.query(QueryBuilders.termQuery("id",id));//这里不使用source的原字段过滤,查询所有字段// searchSourceBuilder.fetchSource(new String[]{"name", "grade", "charge","pic"}, newString[]{});//设置搜索源对象searchRequest.source(searchSourceBuilder);//执行搜索SearchResponse searchResponse = null;try {searchResponse = restHighLevelClient.search(searchRequest);} catch (IOException e) {e.printStackTrace();}//获取搜索结果SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits(); //获取最优结果Map<String,CoursePub> map = new HashMap<>();for (SearchHit hit: searchHits) {//从搜索结果中取值并添加到coursePub对象Map<String, Object> sourceAsMap = hit.getSourceAsMap();String courseId = (String) sourceAsMap.get("id");String name = (String) sourceAsMap.get("name");String grade = (String) sourceAsMap.get("grade");String charge = (String) sourceAsMap.get("charge");String pic = (String) sourceAsMap.get("pic");String description = (String) sourceAsMap.get("description");String teachplan = (String) sourceAsMap.get("teachplan");CoursePub coursePub = new CoursePub();coursePub.setId(courseId);coursePub.setName(name);coursePub.setPic(pic);coursePub.setGrade(grade);coursePub.setTeachplan(teachplan);coursePub.setDescription(description);//设置map对象map.put(courseId,coursePub);}return map;} 测试 使用 swagger-ui 或 postman 测试查询课程信息接口。 0x04 前端开发 配置NGINX虚拟主机 学习中心的二级域名为 ucenter.xuecheng.com ,我们在 nginx 中配置 ucenter 虚拟主机。 学成网用户中心server {listen 80;server_name ucenter.xuecheng.com;个人中心location / {proxy_pass http://ucenter_server_pool;} } 前端ucenterupstream ucenter_server_pool{server 127.0.0.1:7081 weight=10;server 127.0.0.1:13000 weight=10;} 在学习中心要调用搜索的 API,使用 Nginx 解决代理,如下图: 在 ucenter 虚拟主机下配置搜索 Api 代理路径 后台搜索(公开api)upstream search_server_pool{server 127.0.0.1:40100 weight=10;} 学成网用户中心server {listen 80;server_name ucenter.xuecheng.com;个人中心location / {proxy_pass http://ucenter_server_pool;}后端搜索服务location /openapi/search/ {proxy_pass http://search_server_pool/search/;} } 前端 API 方法 在学习中心 xc-ui-pc-leanring 对课程信息的查询属于基础常用功能,所以我们将课程查询的 api 方法定义在base 模块下,如下图: 在system.js 中定义课程查询方法: import http from './public'export const course_view = id => {return http.requestGet('/openapi/search/course/getdetail/'+id);} 前端 API 方法调用 在 learning_video.vue 页面中调用课程信息查询接口得到课程计划,将课程计划json 串转成对象。 xc-ui-pc-leanring/src/module/course/page/learning_video.vue 1、定义视图 课程计划 <!--课程计划部分代码--><div class="navCont"><div class="course-weeklist"><div class="nav nav-stacked" v-for="(teachplan_first, index) in teachplanList"><div class="tit nav-justified text-center"><i class="pull-left glyphicon glyphicon-th-list"></i>{ {teachplan_first.pname} }<i class="pull-right"></i></div><li v-if="teachplan_first.children!=null" v-for="(teachplan_second, index) in teachplan_first.children"><i class="glyphicon glyphicon-check"></i><a :href="url" @click="study(teachplan_second.id)">{ {teachplan_second.pname} }</a></li><!-- <div class="tit nav-justified text-center"><i class="pull-left glyphicon glyphicon-th-list"></i>第一章<i class="pull-right"></i></div><li ><i class="glyphicon glyphicon-check"></i><a :href="url" >第一节</a></li>--><!--<li><i class="glyphicon glyphicon-unchecked"></i>为什么分为A、B、C部分</li>--></div></div></div> 课程名称 <div class="top text-center">{ {coursename} }</div> 定义数据对象 data() {return {url:'',//当前urlcourseId:'',//课程idchapter:'',//章节Idcoursename:'',//课程名称coursepic:'',//课程图片teachplanList:[],//课程计划playerOptions: {//播放参数autoplay: false,controls: true,sources: [{type: "application/x-mpegURL",src: ''}]},} } 在 created 钩子方法中获取课程信息 created(){//当前请求的urlthis.url = window.location//课程idthis.courseId = this.$route.params.courseId//章节idthis.chapter = this.$route.params.chapter//查询课程信息systemApi.course_view(this.courseId).then((view_course)=>{if(!view_course || !view_course[this.courseId]){this.$message.error("获取课程信息失败,请重新进入此页面!")return ;} let courseInfo = view_course[this.courseId]console.log(courseInfo)this.coursename = courseInfo.nameif(courseInfo.teachplan){let teachplan = JSON.parse(courseInfo.teachplan);this.teachplanList = teachplan.children;} })}, 测试 在浏览器请求:http://ucenter.xuecheng.com//learning/4028e581617f945f01617f9dabc40000/0 4028e581617f945f01617f9dabc40000:第一个参数为课程 id,测试时从 ES索引库找一个课程 id 0:第二个参数为课程计划 id,此参数用于点击课程计划播放视频。 如果出现跨域问题,但是确定已经配置了跨域,请尝试结束所以 nginx.exe 的进程 和 清空浏览器缓存。 如果还没有解决?重启电脑试试。 二、学习页面:获取视频播放地址 0x01 需求分析 用户进入在线学习页面,点击课程计划将播放该课程计划对应的教学视频。 业务流程如下: 业务流程说明: 1、用户进入在线学习页面,页面请求搜索服务获取课程信息(包括课程计划信息)并且在页面展示。 2、在线学习请求学习服务获取视频播放地址。 3、学习服务校验当前用户是否有权限学习,如果没有权限学习则提示用户。 4、学习服务校验通过,请求搜索服务获取课程媒资信息。 5、搜索服务请求ElasticSearch获取课程媒资信息。 为什么要请求 ElasticSearch 查询课程媒资信息? 出于性能的考虑,公开查询课程信息从搜索服务查询,分摊 mysql 数据库的访问压力。 什么时候将课程媒资信息存储到 ElasticSearch 中? 课程媒资信息是在课程发布的时候存入 ElasticSearch,因为课程发布后课程信息将基本不再修改。 0x02 课程发布:储存媒资信息 需求分析 课程媒资信息是在课程发布的时候存入 ElasticSearch 索引库,因为课程发布后课程信息将基本不再修改,具体的业务流程如下。 1、课程发布,向课程媒资信息表写入数据。 1)根据课程 id 删除 teachplanMediaPub 中的数据 2)根据课程 id 查询 teachplanMedia 数据 3)将查询到的 teachplanMedia 数据插入到 teachplanMediaPub 中 2、Logstash 定时扫描课程媒资信息表,并将课程媒资信息写入索引库。 数据模型 在 xc_course 数据库创建课程计划媒资发布表: CREATE TABLE teachplan_media_pub (teachplan_id varchar(32) NOT NULL COMMENT '课程计划id',media_id varchar(32) NOT NULL COMMENT '媒资文件id',media_fileoriginalname varchar(128) NOT NULL COMMENT '媒资文件的原始名称',media_url varchar(256) NOT NULL COMMENT '媒资文件访问地址',courseid varchar(32) NOT NULL COMMENT '课程Id',timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'logstash使用',PRIMARY KEY (teachplan_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 数据模型类如下: package com.xuecheng.framework.domain.course;import lombok.Data;import lombok.ToString;import org.hibernate.annotations.GenericGenerator;import javax.persistence.;import java.io.Serializable;import java.util.Date;@Data@ToString@Entity@Table(name="teachplan_media_pub")@GenericGenerator(name = "jpa-assigned", strategy = "assigned")public class TeachplanMediaPub implements Serializable {private static final long serialVersionUID = -916357110051689485L;@Id@GeneratedValue(generator = "jpa-assigned")@Column(name="teachplan_id")private String teachplanId;@Column(name="media_id")private String mediaId;@Column(name="media_fileoriginalname")private String mediaFileOriginalName;@Column(name="media_url")private String mediaUrl;@Column(name="courseid")private String courseId;@Column(name="timestamp")private Date timestamp;//时间戳} Dao 创建 TeachplanMediaPub 表的 Dao,向 TeachplanMediaPub 存储信息采用先删除该课程的媒资信息,再添加该课程的媒资信息,所以这里定义根据课程 id 删除课程计划媒资方法: public interface TeachplanMediaPubRepository extends JpaRepository<TeachplanMediaPub, String> {//根据课程id删除课程计划媒资信息long deleteByCourseId(String courseId);} 从TeachplanMedia查询课程计划媒资信息 //从TeachplanMedia查询课程计划媒资信息public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {List<TeachplanMedia> findByCourseId(String courseId);} Service 编写保存课程计划媒资信息方法,并在课程发布时调用此方法。 1、保存课程计划媒资信息方法 本方法采用先删除该课程的媒资信息,再添加该课程的媒资信息,在 CourseService 下定义该方法 //保存课程计划媒资信息private void saveTeachplanMediaPub(String courseId){//查询课程媒资信息List<TeachplanMedia> byCourseId = teachplanMediaRepository.findByCourseId(courseId);if(byCourseId == null) return; //没有查询到媒资数据则直接结束该方法//将课程计划媒资信息储存到待索引表//删除原有的索引信息teachplanMediaPubRepository.deleteByCourseId(courseId);//一个课程可能会有多个媒资信息,遍历并使用list进行储存List<TeachplanMediaPub> teachplanMediaPubList = new ArrayList<>();for (TeachplanMedia teachplanMedia: byCourseId) {TeachplanMediaPub teachplanMediaPub = new TeachplanMediaPub();BeanUtils.copyProperties(teachplanMedia, teachplanMediaPub);teachplanMediaPubList.add(teachplanMediaPub);}//保存所有信息teachplanMediaPubRepository.saveAll(teachplanMediaPubList);} 2、课程发布时调用此方法 修改课程发布的 coursePublish 方法: ....//保存课程计划媒资信息到待索引表saveTeachplanMediaPub(courseId);//页面urlString pageUrl = cmsPostPageResult.getPageUrl();return new CoursePublishResult(CommonCode.SUCCESS,pageUrl);..... 测试 测试课程发布后是否成功将课程媒资信息存储到 teachplan_media_pub 中,测试流程如下: 1、指定一个课程 2、为课程计划添加课程媒资 3、执行课程发布 4、观察课程计划媒资信息是否存储至 teachplan_media_pub 中 注意:由于此测试仅用于测试发布课程计划媒资信息的功能,可暂时将 cms页面发布的功能暂时屏蔽,提高测试效率。 测试结果如下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vrzs5589-1595567273126)(https://qnoss.codeyee.com/20200704_15/image7)] 0x03 Logstash:扫描课程计划媒资 Logstash 定时扫描课程媒资信息表,并将课程媒资信息写入索引库。 创建索引 1、创建 xc_course_media 索引 2、并向此索引创建如下映射 POST: http://localhost:9200/xc_course_media/doc/_mapping {"properties" : {"courseid" : {"type" : "keyword"},"teachplan_id" : {"type" : "keyword"},"media_id" : {"type" : "keyword"},"media_url" : {"index" : false,"type" : "text"},"media_fileoriginalname" : {"index" : false,"type" : "text"} }} 索引创建成功 创建模板文件 在 logstach 的 config 目录文件 xc_course_media_template.json 文件路径为 %ES_ROOT_DIR%/logstash6.8.8/config/xc_course_media_template.json %ES_ROOT_DIR% 为 ElasticSearch 和 logstash 的安装目录 内容如下: {"mappings" : {"doc" : {"properties" : {"courseid" : {"type" : "keyword"},"teachplan_id" : {"type" : "keyword"},"media_id" : {"type" : "keyword"},"media_url" : {"index" : false,"type" : "text"},"media_fileoriginalname" : {"index" : false,"type" : "text"} }},"template" : "xc_course_media"} } 配置 mysql.conf 在logstash的 config 目录下配置 mysql_course_media.conf 文件供 logstash 使用,logstash 会根据 mysql_course_media.conf 文件的配置的地址从 MySQL 中读取数据向 ES 中写入索引。 参考https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html 配置输入数据源和输出数据源。 input {stdin {} jdbc {jdbc_connection_string => "jdbc:mysql://localhost:3306/xc_course?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC" 数据库信息jdbc_user => "root"jdbc_password => "123123" MYSQL 驱动地址,修改为maven仓库对应的位置jdbc_driver_library => "D:/soft/apache-maven-3.5.4/repository/mysql/mysql-connector-java/5.1.40/mysql-connector-java-5.1.40.jar" the name of the driver class for mysqljdbc_driver_class => "com.mysql.jdbc.Driver"jdbc_paging_enabled => "true"jdbc_page_size => "50000"要执行的sql文件statement_filepath => "/conf/course.sql"statement => "select from teachplan_media_pub where timestamp > date_add(:sql_last_value,INTERVAL 8 HOUR)"定时配置schedule => " "record_last_run => truelast_run_metadata_path => "D:/soft/elasticsearch/logstash-6.8.8/config/xc_course_media_metadata"} } output {elasticsearch {ES的ip地址和端口hosts => "localhost:9200"hosts => ["localhost:9200","localhost:9202","localhost:9203"]ES索引库名称index => "xc_course_media"document_id => "%{teachplan_id}"document_type => "doc"template => "D:/soft/elasticsearch/logstash-6.8.8/config/xc_course_media_template.json"template_name =>"xc_course_media"template_overwrite =>"true"} stdout {日志输出codec => json_lines} } 启动 logstash.bat 启动 logstash.bat 采集 teachplan_media_pub 中的数据,向 ES 写入索引。 logstash.bat -f ../config/mysql_course_media.conf 课程发布成功后,Logstash 会自动参加 teachplan_media_pub 表中新增的数据,效果如下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILPBxfXi-1595567273134)(https://qnoss.codeyee.com/20200704_15/image10)] Logstash多实例运行 由于之前我们还启动了一个 Logstash 对课程的发布信息进行采集,所以如果想两个 logstash 实例同时运行,因为每个实例都有一个.lock文件,所以不能使用同一个目录来存放数据,所以我们需要使用 --path.data= 为每个实例指定单独的数据目录,具体的代码如下: 该配置是在windows下进行的 课程发布实例 logstash_start_course_pub.bat @title logstash in course_publogstash.bat -f ..\config\mysql.conf --path.data=../data/course_pub 课程计划媒体发布实例 logstash_start_teachplan_media.bat @title logstash i n teachplan_media_publogstash.bat -f ../config/mysql_course_media.conf --path.data=../data/teachplan_media/ 同时运行效果如下 0x04 搜素服务:查询课程媒资接口 需求分析 搜索服务 提供查询课程媒资接口,此接口供学习服务调用。 Api接口定义 @ApiOperation("根据课程计划查询媒资信息")public TeachplanMediaPub getmedia(String teachplanId); Service 1、配置课程计划媒资索引库等信息 在 application.yml 中配置 xuecheng:elasticsearch:hostlist: ${eshostlist:127.0.0.1:9200} 多个结点中间用逗号分隔course:index: xc_coursetype: docsource_field: id,name,grade,mt,st,charge,valid,pic,qq,price,price_old,status,studymodel,teachmode,expires,pub_time,start_time,end_timemedia:index: xc_course_mediatype: docsource_field: courseid,media_id,media_url,teachplan_id,media_fileoriginalname 2、service 方法开发 在 课程搜索服务 中定义课程媒资查询接口,为了适应后续需求,service 参数定义为数组,可一次查询多个课程计划的媒资信息。 / 根据一个或者多个课程计划id查询媒资信息 @param teachplanIds 课程id @return QueryResponseResult/public QueryResponseResult<TeachplanMediaPub> getmedia(String [] teachplanIds){//设置索引SearchRequest searchRequest = new SearchRequest(media_index);//设置类型searchRequest.types(media_type);//创建搜索源对象SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();//源字段过滤String[] media_index_arr = media_field.split(",");searchSourceBuilder.fetchSource(media_index_arr, new String[]{});//查询条件,根据课程计划id查询(可以传入多个课程计划id)searchSourceBuilder.query(QueryBuilders.termsQuery("teachplan_id", teachplanIds));searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = null;try {searchResponse = restHighLevelClient.search(searchRequest);} catch (IOException e) {e.printStackTrace();}//获取结果SearchHits hits = searchResponse.getHits();long totalHits = hits.getTotalHits();SearchHit[] searchHits = hits.getHits();//数据列表List<TeachplanMediaPub> teachplanMediaPubList = new ArrayList<>();for(SearchHit hit:searchHits){TeachplanMediaPub teachplanMediaPub =new TeachplanMediaPub();Map<String, Object> sourceAsMap = hit.getSourceAsMap();//取出课程计划媒资信息String courseid = (String) sourceAsMap.get("courseid");String media_id = (String) sourceAsMap.get("media_id");String media_url = (String) sourceAsMap.get("media_url");String teachplan_id = (String) sourceAsMap.get("teachplan_id");String media_fileoriginalname = (String) sourceAsMap.get("media_fileoriginalname");teachplanMediaPub.setCourseId(courseid);teachplanMediaPub.setMediaUrl(media_url);teachplanMediaPub.setMediaFileOriginalName(media_fileoriginalname);teachplanMediaPub.setMediaId(media_id);teachplanMediaPub.setTeachplanId(teachplan_id);//将对象加入到列表中teachplanMediaPubList.add(teachplanMediaPub);}//构建返回课程媒资信息对象QueryResult<TeachplanMediaPub> queryResult = new QueryResult<>();queryResult.setList(teachplanMediaPubList);queryResult.setTotal(totalHits);return new QueryResponseResult<TeachplanMediaPub>(CommonCode.SUCCESS,queryResult);} Controller / 根据课程计划id搜索发布后的媒资信息 @param teachplanId @return/@GetMapping(value="/getmedia/{teachplanId}")@Overridepublic TeachplanMediaPub getmedia(@PathVariable("teachplanId") String teachplanId) {//为了service的拓展性,所以我们service接收的是数组作为参数,以便后续开发查询多个ID的接口String[] teachplanIds = new String[]{teachplanId};//通过service查询ES获取课程媒资信息QueryResponseResult<TeachplanMediaPub> mediaPubQueryResponseResult = esCourseService.getmedia(teachplanIds);QueryResult<TeachplanMediaPub> queryResult = mediaPubQueryResponseResult.getQueryResult();if(queryResult!=null&& queryResult.getList()!=null&& queryResult.getList().size()>0){//返回课程计划对应课程媒资return queryResult.getList().get(0);} return new TeachplanMediaPub();} 测试 使用 swagger-ui 和 postman 测试课程媒资查询接口。 三、在线学习:接口开发 0x01 需求分析 根据下边的业务流程,本章节完成前端学习页面请求学习服务获取课程视频地址,并自动播放视频。 0x02 搭建开发环境 1、创建数据库 创建 xc_learning 数据库,学习数据库将记录学生的选课信息、学习信息。 导入:资料/xc_learning.sql 2、创建学习服务工程 参考课程管理服务工程结构,创建学习服务工程: 导入:资料/xc-service-learning.zip 项目工程结构如下 0x03 Api接口 此 api 接口是课程学习页面请求学习服务获取课程学习地址。 定义返回值类型: package com.xuecheng.framework.domain.learning.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@NoArgsConstructorpublic class GetMediaResult extends ResponseResult {public GetMediaResult(ResultCode resultCode, String fileUrl) {super(resultCode);this.fileUrl = fileUrl;}//媒资文件播放地址private String fileUrl;} 定义接口,学习服务根据传入课程 ID、章节 Id(课程计划 ID)来取学习地址。 @Api(value = "录播课程学习管理",description = "录播课程学习管理")public interface CourseLearningControllerApi {@ApiOperation("获取课程学习地址")public GetMediaResult getMediaPlayUrl(String courseId,String teachplanId);} 0x04 服务端开发 需求分析 学习服务根据传入课程ID、章节Id(课程计划ID)请求搜索服务获取学习地址。 搜索服务注册Eureka 学习服务要调用搜索服务查询课程媒资信息,所以需要将搜索服务注册到 eureka 中。 1、查看服务名称是否为 xc-service-search 注意修改application.xml中的服务名称:spring:application:name: xc‐service‐search 2、配置搜索服务的配置文件 application.yml,加入 Eureka 配置 如下: 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} 指定实例idribbon:MaxAutoRetries: 2 最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试,如果eureka中找不到服务则直接走断路器MaxAutoRetriesNextServer: 3 切换实例的重试次数OkToRetryOnAllOperations: false 对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为falseConnectTimeout: 5000 请求连接的超时时间ReadTimeout: 6000 请求处理的超时时间 3、添加 eureka 依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring‐cloud‐starter‐netflix‐eureka‐client</artifactId></dependency> 4、修改启动类,在class上添加如下注解: @EnableDiscoveryClient 搜索服务客户端 在 学习服务 创建搜索服务的客户端接口,此接口会生成代理对象,调用搜索服务: package com.xuecheng.learning.client;import com.xuecheng.framework.domain.course.TeachplanMediaPub;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;@FeignClient(value = "xc‐service‐search")public interface CourseSearchClient {@GetMapping(value="/getmedia/{teachplanId}")public TeachplanMediaPub getmedia(@PathVariable("teachplanId") String teachplanId);} 自定义错误代码 我们在 com.xuecheng.framework.domain.learning.response 包下自定义一个错误消息模型 package com.xuecheng.framework.domain.learning.response;import com.xuecheng.framework.model.response.ResultCode;import lombok.ToString;@ToStringpublic enum LearningCode implements ResultCode {LEARNING_GET_MEDIA_ERROR(false,23001,"学习中心获取媒资信息错误!");//操作代码boolean success;//操作代码int code;//提示信息String message;private LearningCode(boolean success, int code, String message){this.success = success;this.code = code;this.message = message;}@Overridepublic boolean success() {return success;}@Overridepublic int code() {return code;}@Overridepublic String message() {return message;} } 该消息模型基于 ResultCode 来实现,代码如下 package com.xuecheng.framework.model.response;/ Created by mrt on 2018/3/5. 10000-- 通用错误代码 22000-- 媒资错误代码 23000-- 用户中心错误代码 24000-- cms错误代码 25000-- 文件系统/public interface ResultCode {//操作是否成功,true为成功,false操作失败boolean success();//操作代码int code();//提示信息String message(); 从 ResultCode 中我们可以看出,我们约定了用户中心的错误代码使用 23000,所以我们定义的一些错误信息的代码就从 23000 开始计数。 Service 在学习服务中定义 service 方法,此方法远程请求课程管理服务、媒资管理服务获取课程学习地址。 package com.xuecheng.learning.service.impl;import com.netflix.discovery.converters.Auto;import com.xuecheng.framework.domain.course.TeachplanMediaPub;import com.xuecheng.framework.domain.learning.response.GetMediaResult;import com.xuecheng.framework.exception.ExceptionCast;import com.xuecheng.framework.model.response.CommonCode;import com.xuecheng.learning.client.CourseSearchClient;import com.xuecheng.learning.service.LearningService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class LearningServiceImpl implements LearningService {@AutowiredCourseSearchClient courseSearchClient;/ 远程调用搜索服务获取已发布媒体信息中的url @param courseId 课程id @param teachplanId 媒体信息id @return/@Overridepublic GetMediaResult getMediaPlayUrl(String courseId, String teachplanId) {//校验学生权限,是否已付费等//远程调用搜索服务进行查询媒体信息TeachplanMediaPub mediaPub = courseSearchClient.getmedia(teachplanId);if(mediaPub == null) ExceptionCast.cast(CommonCode.FAIL);return new GetMediaResult(CommonCode.SUCCESS, mediaPub.getMediaUrl());} } Controller 调用 service 根据课程计划 id 查询视频播放地址: @RestController@RequestMapping("/learning/course")public class CourseLearningController implements CourseLearningControllerApi {@AutowiredLearningService learningService;@Override@GetMapping("/getmedia/{courseId}/{teachplanId}")public GetMediaResult getMediaPlayUrl(@PathVariable String courseId, @PathVariable String teachplanId) {//获取课程学习地址return learningService.getMedia(courseId, teachplanId);} } 测试 使用 swagger-ui 或postman 测试学习服务查询课程视频地址接口。 0x05 前端开发 需求分析 需要在学习中心前端页面需要完成如下功能: 1、进入课程学习页面需要带上 课程 Id参数及课程计划Id的参数,其中 课程 Id 参数必带,课程计划 Id 可以为空。 2、进入页面根据 课程 Id 取出该课程的课程计划显示在右侧。 3、进入页面后判断如果请求参数中有课程计划 Id 则播放该章节的视频。 4、进入页面后判断如果 课程计划id 为0则需要取出本课程第一个 课程计划的Id,并播放第一个课程计划的视频。 进入到模块 xc-ui-pc-leanring/src/module/course api方法 let sysConfig = require('@/../config/sysConfig')let apiUrl = sysConfig.xcApiUrlPre;/获取播放地址/export const get_media = (courseId,chapter) => {return http.requestGet(apiUrl+'/api/learning/course/getmedia/'+courseId+'/'+chapter);} 配置代理 在 Nginx 中的 ucenter.xuecheng.com 虚拟主机中配置 /api/learning/ 的路径转发,此url 请转发到学习服务。 学习服务upstream learning_server_pool{server 127.0.0.1:40600 weight=10;}学成网用户中心server {listen 80;server_name ucenter.xuecheng.com;个人中心location / {proxy_pass http://ucenter_server_pool;}后端搜索服务location /openapi/search/ {proxy_pass http://search_server_pool/search/; }学习服务location ^~ /api/learning/ {proxy_pass http://learning_server_pool/learning/;} } 视频播放页面 1、如果传入的课程计划id为0则取出第一个课程计划id 在 created 钩子方法中完成 created(){//当前请求的urlthis.url = window.location//课程idthis.courseId = this.$route.params.courseId//章节idthis.chapter = this.$route.params.chapter//查询课程信息systemApi.course_view(this.courseId).then((view_course)=>{if(!view_course || !view_course[this.courseId]){this.$message.error("获取课程信息失败,请重新进入此页面!")return ;}let courseInfo = view_course[this.courseId]console.log(courseInfo)this.coursename = courseInfo.nameif(courseInfo.teachplan){console.log("准备开始播放视频")let teachplan = JSON.parse(courseInfo.teachplan);this.teachplanList = teachplan.children;//开始学习if(this.chapter == "0" || !this.chapter){//取出第一个教学计划this.chapter = this.getFirstTeachplan();console.log("第一个教学计划id为 ",this.chapter);this.study(this.chapter);}else{this.study(this.chapter);} }})}, 取出第一个章节 id,用户未输入课程计划 id 或者输入为 0 时,播放第一个。 //取出第一个章节getFirstTeachplan(){for(var i=0;i<this.teachplanList.length;i++){let firstTeachplan = this.teachplanList[i];//如果当前children存在,则取出第一个返回if(firstTeachplan.children && firstTeachplan.children.length>0){let secondTeachplan = firstTeachplan.children[0];return secondTeachplan.id;} }return ;}, 开始学习: //开始学习study(chapter){// 获取播放地址courseApi.get_media(this.courseId,chapter).then((res)=>{if(res.success){let fileUrl = sysConfig.videoUrl + res.fileUrl//播放视频this.playvideo(fileUrl)}else if(res.message){this.$message.error(res.message)}else{this.$message.error("播放视频失败,请刷新页面重试")} }).catch(res=>{this.$message.error("播放视频失败,请刷新页面重试")});}, 2、点击右侧课程章节切换播放 在原有代码基础上添加 click 事件,点击调用开始学习方法(study)。 <li v‐if="teachplan_first.children!=null" v‐for="(teachplan_second, index) inteachplan_first.children"><i class="glyphicon glyphicon‐check"></i><a :href="url" @click="study(teachplan_second.id)">{ {teachplan_second.pname} }</a></li> 3、地址栏路由url变更 这里需要注意一个问题,在用户点击课程章节切换播放时,地址栏的 url 也应该同步改变为当前所选择的课程计划 id 4、在线学习按钮 将 learnstatus 默认更改为 1,这样就能显示出马上学习的按钮,方便我们后续的集成测试。 文件路径为 xc-ui-pc-static-portal/include/course_detail_dynamic.html 部分代码块如下 <script>var body= new Vue({ //创建一个Vue的实例el: "body", //挂载点是id="app"的地方data: {editLoading: false,title:'测试',courseId:'',charge:'',//203001免费,203002收费learnstatus: 1 ,//课程状态,1:马上学习,2:立即报名、3:立即购买course:{},companyId:'template',company_stat:[],course_stat:{"s601001":"","s601002":"","s601003":""} }, 简单的测试 访问在线学习页面:http://ucenter.xuecheng.com//learning/课程id/课程计划id 通过 url 传入两个参数:课程id 和 课程计划id 如果没有课程计划则传入0 测试项目如下: 1、传入正确的课程id、课程计划id,自动播放本章节的视频 2、传入正确的课程id、课程计划id传入0,自动播放第一个视频 3、传入错误的课程id 或 课程计划id,提示错误信息。 4、通过右侧章节目录切换章节及播放视频。 访问: http://ucenter.xuecheng.com//learning/4028e58161bcf7f40161bcf8b77c0000/4028e58161bd18ea0161bd1f73190008 传入正确的课程id、课程计划id,自动播放本章节的视频 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ef0xxym7-1595567273153)(https://qnoss.codeyee.com/20200704_15/image17)] 传入正确的课程id、课程计划id传入0,自动播放第一个视频 访问 http://ucenter.xuecheng.com//learning/4028e58161bcf7f40161bcf8b77c0000/0 识别出第一个课程计划的 id 需要注意的是这里的 chapter 参数是我自己在 study 函数里加上去的,可以忽略。 传入错误的课程id或课程计划id,提示错误信息。 通过右侧章节目录切换章节及播放视频。 点击章节即可播放,但是点击制定章节后 url 没有发生改变,这个问题暂时还没有解决,关注笔记后面的内容。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TOGdxwb4-1595567273158)(https://qnoss.codeyee.com/20200704_15/image20)] 完整的测试 准备工作 启动 RabbitMQ,启动 Logstash、ElasticSearch 建议把所有后端服务都开起来 启动 前端静态门户、启动 nginx 、启动课程管理前端 我们整理一下测试的流程 上传两个媒资视频文件,用于测试 进入到课程管理,为课程计划选择媒资信息 发布课程,等待 logstash 将数据采集到 ElasticSearch 的索引库中 进入学成网主页,点击课程,进入到搜索门户页面 搜索课程,进入到课程详情页面 点击开始学习,进入到课程学习页面,选择课程计划中的一个章节进行学习。 1、上传文件 首先我们使用之前开发的媒资管理模块,上传两个视频文件用于测试。 第一个文件上传成功 一些问题 在上传第二个文件时,发生了错误,我们来检查一下问题出在了哪里 在媒体服务的控制台中可以看到,在 mergeChunks 方法在校验文件 md5 时候抛出了异常 我们在 MD5 校验这里打个断点,重新上传文件,分析一下问题所在。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OpEMZGI8-1595567273166)(https://qnoss.codeyee.com/20200704_15/image23)] 单步调试后发现,合并文件后的MD5值与用户上传的源文件值不相等 方案1:删除本地分块文件重新尝试上传 考虑到可能是在用户上传完 视频的分块文件时发生了一些问题,导致合并文件后与源文件的大小不等,导致MD5也不相同,这里我们把这个视频上传到本地的文件全部删除,在媒资上传页面重新上传文件。 对比所有分块文件的字节大小和本地源文件的大小,完全是相等的 删除所有文件后重新上传,md5值还是不等,考虑从调试一下文件合并的代码。 方案2:检查前端提交的MD5值是否正确 在查阅是否有其他的MD5值获取方案时,发现了一个使用 windows 本地命令获取文件MD5值的方法 certutil -hashfile .\19-在线学习接口-集成测试.avi md5 惊奇的发现,TM的原来是前端那边转换的MD5值不正确,后端这边是没有问题的。 从前面的图可以看出,本地和后端转换的都是以一个 f6f0 开头的MD5值 那么问题就出现在前端了,还需要花一些时间去分析一下,这里暂时就先告一段落,因为上传了几个文件测试中只有这一个文件出现了问题。 2、为课程计划选择媒资信息 进入到一个课程的管理页面 http://localhost:12000//course/manage/baseinfo/4028e58161bcf7f40161bcf8b77c0000 将刚才我们上传的媒资文件的信息和课程计划绑定 选择效果如下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-epKaqzCD-1595567273178)(https://qnoss.codeyee.com/20200704_15/image29)] 2、发布课程,等待 logstash 从 course_pub 以及 teachplan_media_pub 表中采集数据到 ElasticSearch 当中 发布成功后,我们可以从 teachplan_media_pub 表中看到刚才我们发布的媒资信息 再观察 Logstash 的控制台,发现两个 Logstash 的实例都对更新的课程发布信息进行了采集 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hTUve2ik-1595567273183)(https://qnoss.codeyee.com/20200704_15/image32)] 3、前端门户测试 打开我们的门户主站 http://www.xuecheng.com/ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wZe9R84-1595567273185)(https://qnoss.codeyee.com/20200704_15/image33)] 点击导航栏的课程,进入到我们的搜索门户页面 如果无法进入到搜索门户,请检查你的 xc-ui-pc-portal 前端工程是否已经启动 进入到搜索门户后,可以看到一些初始化时搜索的课程数据,默认是搜索第一页的数据,每页2个课程。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJ1AKoJb-1595567273187)(https://qnoss.codeyee.com/20200704_15/image34)] 我们可以测试搜索一下前面我们选择媒资信息时所用的课程 点击课程,进入到课程详情页面,然后再点击开始学习。 点击马上学习后,会进入到该课程的在线学习页面,默认自动播放我们第一个课程计划中的视频。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tcuLWnf2-1595567273193)(https://qnoss.codeyee.com/20200704_15/image37)] 我们可以在右侧的目录中选择第二个课程计划,会自动播放所选的课程计划所对应的媒资视频播放地址,该 播放地址正是我们刚才通过 Logstash 自动采集到 ElasticSearch 的索引信息,效果图如下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cvi9Dr0Y-1595567273195)(https://qnoss.codeyee.com/20200704_15/image38)] 四、待完善的一些功能 课程发布前,校验课程计划里面是否包含二级课程计划 课程发布前,校验课程计划信息里面是否全部包含媒资信息 删除媒资信息,并且同步删除ES中的索引 在获取该课程的播放地址时校验用户的合法、 在线学习页面,点击右侧目录中的课程计划同时改变url中的课程计划地址 视频文件 19-在线学习接口-集成测试.avi 前端上传时提交的MD5值不正确 😁 认识作者 作者:👦 LCyee ,全干型代码🐕 自建博客:https://www.codeyee.com 记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。 CSDN 博客:https://blog.csdn.net/codeyee 记录和分享一些开发过程中遇到的问题以及解决的思路。 欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。 本篇文章为转载内容。原文链接:https://blog.csdn.net/codeyee/article/details/107558901。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-12-16 12:41:01
73
转载
JQuery插件下载
...mate插件文件,并调用相应的API方法,即可快速实现这一功能,让复杂的表单布局变得生动而易用。 点我下载 文件大小:38.29 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2023-04-11 09:17:57
117
本站
JQuery插件下载
...跳转的生硬感。用户在调用相关方法时,无需依赖jQuery等大型库,只需引入SmoothScroll.js文件,即可在网页中启用平滑滚动功能。无论是点击锚点链接自动滚动到目标位置,还是响应鼠标滚轮事件进行动态平滑滚动,都能轻松实现,从而极大地提升了网站的交互性和用户的舒适度。这款插件具有良好的兼容性和易用性,是现代网页开发优化滚动行为的理想选择之一。 点我下载 文件大小:15.23 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2023-07-08 10:30:22
346
本站
JQuery插件下载
...配置和扩展。只需简单调用相关方法和选项,即可在Web页面上轻松创建出功能齐全、外观仿真的桌面窗口界面。 点我下载 文件大小:195.99 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2024-01-06 23:30:14
35
本站
站内搜索
用于搜索本网站内部文章,支持栏目切换。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
xz -d file.txt.xz
- 解压xz格式的压缩文件。
推荐内容
推荐本栏目内的其它文章,看看还有哪些文章让你感兴趣。
2023-04-28
2023-08-09
2023-06-18
2023-04-14
2023-02-18
2023-04-17
2024-01-11
2023-10-03
2023-09-09
2023-06-13
2023-08-07
2023-03-11
历史内容
快速导航到对应月份的历史文章列表。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"