@TOC前情提要我们上节内容学习了如何创建\注册\读取bean我们发现bean对象操作十分的繁琐!所以我们这个章节,就带大家来了解更加简单的bean操作,通过Spring下的注解来实现!配置spring-config文件我们之前注册bean是通过在xml配置文件中,通过键值对的方式注册bean对象!显然这种方式很麻烦,注册一个对象,就要添加一项!有没有什么好的方式可以让spring直接去注册对象!yes!我们可以直接在配置文件配置好 spring下你要注册对象的包时那个!当spring启动后,spring就会将bean对象自动注册!spring-config配置文件<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:content="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!--在com包下扫描bean注册--> <content:component-scan base-package="com"></content:component-scan> </beans>当然只有一个配置文件显然不够嘛!我们如何知道我们代码中的对象是bean对象捏?这就要引入spring五大注解概念了!我们通过在我们创建好的对象上面添加注解的方式,就是告诉spring这个对象需要注册到容器中!类注解和方法注解类注解:@Controller@Service@Repository@Component@Configuration方法注解:@Bean我们可以通过上述两种注解将对象存储到Spring中!@Controller(控制器存储)使用@Controller注解存储beanpackage com; import org.springframework.stereotype.Controller; @Controller //通过Controller注解存储bean对象 public class UserController { public void sayHi(){ System.out.println("hello Controller注解!");; } }我们通过在UserController类上加上spring类注解,即可完成注册对象!在启动类中读取bean对象即可!//启动类 public class app{ public static void main(String[] args) { //1.获取上下文对象 ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); //读取bean对象! UserController userController = (UserController) context.getBean("userController"); //使用 userController.sayHi(); } }如果我们的需要注册的bean对象不在扫描包下,是否又能注册成功呢?我们在新建一个controller包在其下创建TestController类,并且通过@Controller注册到Spring中!package controller; import org.springframework.stereotype.Controller; @Controller //注册到Spring中! public class TestController { public void sayHi(){ System.out.println("该bean不在扫描的包下"); } } 然后我们通过ApplicationContext上下文对象读取bean可以看到出现异常未找到名为textController的bean对象!结论:只有在扫描包下的类才能被Spring注册@Service(服务存储)注册beanpackage com; import org.springframework.stereotype.Service; @Service // @Service 注解注册对象! public class UserService { public void sayHi(){ System.out.println("Hello Service注解!"); } }读取bean@Configuration(配置存储)package com; import org.springframework.context.annotation.Configuration; @Configuration //Configuration注解注册bean对象 public class UserConfiguration { public void sayHi(){ System.out.println("Hello Configuration注解!"); } } @Repository(仓库存储)package com; import org.springframework.stereotype.Repository; @Repository //@Respository 注解注册对象 public class UserRepository { public void sayHi(){ System.out.println("Hello Respository注解!"); } } @Component(组件存储)package com; import org.springframework.stereotype.Component; @Component //Component注解注册对象! public class UserComponent { public void sayHi(){ System.out.println("Hello Component注解!"); } } 5大类注解联系可以看到这5大类注解使用方式一样,都可以对对象进行注册!而且注册的方式都一样,既然如此为何还需要5个注解呢?我们联系实际生活中的车牌号,我们虽然车牌号的功能都是一样,但是不同地区都有自己的车牌号!我们通过车牌号就可以分辨出这车来自哪里!而这里5大类注解作用也是如此,我们通过类注解,可以知道当前类的用途!例如;@Controller:表示业务逻辑层@Service:服务层@Repository:持久层@Configuration:配置层程序的工程分层,调用流程如下:我们拿去银行办业务做类比:@Controller层就是保安,先要进行检查验证,然后到达Service服务厅询问业务,不同的业务来到Repository,不同的窗口,然后进行相应的工作人员办理业务!类注解之前联系:可以看到其他4个注解都是Component注解的子类!Spring给Bean命名规则我们可以看到我们刚刚读取bean对象时,我们并不知道bean对象注册的id而是直接通过userController读取!难道说Spring注册bean对象id为类名首字母小写,直接就小驼峰?我们查看Spring源码验证!我们顺藤摸瓜下方就是Spring对Bean对象进行命名的方法! public static String decapitalize(String name) { if (name == null || name.length() == 0) { return name; } if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))){ return name; } char chars[] = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); }可以看到我们这里bean对象的id命名规则如下:对象类类名一般采用大驼峰的形式也就是单词第一个字母大小,所以Spring直接bean对象改为小驼峰,`第一个字母分成小写!对象类类名不规范,不是大驼峰,第二个字母和第一个字母都是大小!Spring直接将bean对象命名为类名!我们进行验证:方法注解@Bean我们了解了5大类注解可以进行对象注册,我们使用方法注解进行对象注册!注意: 方法注解要和类注解配合使用!方法注解进行对象注册//User类 public class User { private String name; private int id; public User(String name, int id) { this.name = name; this.id = id; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", id=" + id + '}'; } } //Users类 @Component public class Users { @Bean public User user(){ return new User("java",666); } }可以看到@Bean注解适用于返回值返回对象的方法中!重命名Bean我们既然可以通过五大类注解进行对象注入!那为何还要多此一举在方法上加上@Bean方法注解呢?我们可以通过@Bean方法注解给bean对象重命名,可以直接设置名字!通过 name={"rename1", "rename2"...}可以重命名多个!@Component public class Users { @Bean(name = {"user1"}) public User user(){ return new User("java",666); } }能否通过之前Spring给我们设置的名字访问? 不能@Bean(name={"user1","user2"}) 重命名多个!我们也可以将name省略@Bean({"user1"})获取Bean对象(对象装配)这里可能听了有点迷,啥玩意对象装配,其实就是获取对象!我们将对象注册到Spring容器下,我们要读取将对象取出放入到某个类中,这就是对象装配,也叫对象注入!实现对象装配的3种方法属性注入构造方法注入Setter注入下面我们来演示一下这3种注入方式我们按照实际开发将Service层的类注入到Controller层的类中!属性注入我们通过@Autowired实现属性注入service层类代码@Service public class UserService { public User getUser(){ return new User("Mysql",666); } }controller层类代码通过属性注入将service层代码注入到这@Controller public class UserController { //属性注入 @Autowired private UserService userService; public User getUser(){ return userService.getUser(); } }运行结果:构造方法注入我们还是通过@Autowired注解注入@Controller public class UserController { private UserService userService; //构造方法注入 @Autowired public UserController(UserService userService){ this.userService = userService; } public User getUser(){ return userService.getUser(); } }Setter注入@Controller public class UserController { //Setter注入 private UserService userService; @Autowired public void setUserService(UserService userService){ this.userService = userService; } public User getUser(){ return userService.getUser(); } } 三种注入方式对比属性注入简洁,使用方便! 缺点只能适用于IoC容器,在非IoC容器不适用,并且属性注入只有在是使用的时候才会出现空指针异常(NPE)构造方法注入现在官方推荐注入方式! 缺点 如果注入多个对象,就会使得代码臃肿,不过这就是程序员的问题了,不符合程序设计的单一职责的设计模式,优点通用性强,在使用前一定可以保证注入的类不为空!Setter方式是Spring前期推荐的注入方式,通用性不如构造方法注入,现在已经认准构造方法注入!@Resource注入关键字在进行类注入时,我们还可以通过@Resource注解进行注入!我们只需要将@Autowired注解换成@Resource即可!@Autowired和@Resource区别出身不同: @Autowired注解是Spring提供的,@Resource是来自JDK下的注解使用设置的参数不同:相比@Autowired注解,@Resource注解 支持更多的参数设置 例如name设置,根据name获取对象注入同一类型多个Bean对象我们在Users类中注册了2个相同类型的Bean对象!@Component public class Users { @Bean(name = "user1") public User user1(){ User user = new User("java",666); return user; } @Bean(name = "user2") public User user2(){ User user = new User("MySQL",666); return user; } }当我们直接注入到Controller类中!@Controller public class UserController { @Resource private User user; public User getUser(){ return user; } }因为我们在Spring中注册了2个相同类型的User对象,所以进行对象装配时,也需要通过name属性进行声明你要装配的对象名!@Controller public class UserController { @Resource(name = "user2") private User user; public User getUser(){ return user; } }注意:@Resource注解才提供了name属性,如果用@Autowried需要加上@Qualifier 注解定义名称@Controller public class UserController { // @Resource(name = "user2") @Autowired @Qualifier(value = "user1") private User user; public User getUser(){ return user; } }
基于JVM的开源数据处理语言主要有Kotlin、Scala、SPL,下面对三者进行多方面的横向比较,从中找出开发效率最高的数据处理语言。本文的适用场景设定为项目开发中常见的数据处理和业务逻辑,以结构化数据为主,大数据和高性能不作为重点,也不涉及消息流、科学计算等特殊场景。基本特征适应面Kotlin的设计初衷是开发效率更高的Java,可以适用于任何Java涉及的应用场景,除了常见的信息管理系统,还能用于WebServer、Android项目、游戏开发,通用性比较好。Scala的设计初衷是整合现代编程范式的通用开发语言,实践中主要用于后端大数据处理,其他类型的项目中很少出现,通用性不如Kotlin。SPL的设计初衷是专业的数据处理语言,实践与初衷一致,前后端的数据处理、大小数据处理都很适合,应用场景相对聚焦,通用性不如Kotlin。编程范式Kotlin以面向对象编程为主,也支持函数式编程。Scala两种范式都支持,面向对象编程比Koltin更彻底,函数式编程也比Koltin方便些。SPL可以说不算支持面向对象编程,有对象概念,但没有继承重载这些内容,函数式编程比Kotlin更方便。运行模式Kotlin和Scala是编译型语言,SPL是解释型语言。解释型语言更灵活,但相同代码性能会差一点。不过SPL有丰富且高效的库函数,总体性能并不弱,面对大数据时常常会更有优势。外部类库Kotlin可以使用所有的Java类库,但缺乏专业的数据处理类库。Scala也可以使用所有的Java类库,且内置专业的大数据处理类库(Spark)。SPL内置专业的数据处理函数,提供了大量时间复杂度更低的基本运算,通常不需要外部Java类库,特殊情况可在自定义函数中调用。IDE和调试三者都有图形化IDE和完整的调试功能。SPL的IDE专为数据处理而设计,结构化数据对象呈现为表格形式,观察更加方便,Kotlin和Scala的IDE是通用的,没有为数据处理做优化,无法方便地观察结构化数据对象。学习难度Kotlin的学习难度稍高于Java,精通Java者可轻易学会。Scala的目标是超越Java,学习难度远大于Java。SPL的目标就是简化Java甚至SQL的编码,刻意简化了许多概念,学习难度很低。代码量Kotlin的初衷是提高Java的开发效率,官方宣称综合代码量只有Java的20%,可能是数据处理类库不专业的缘故,这方面的实际代码量降低不多。Scala的语法糖不少,大数据处理类库比较专业,代码量反而比Kotlin低得多。SPL只用于数据处理,专业性最强,再加上解释型语言表达能力强的特点,完成同样任务的代码量远远低于前两者(后面会有对比例子),从另一个侧面也能说明其学习难度更低。语法数据类型原子数据类型:三者都支持,比如Short、Int、Long、Float、Double、Boolean日期时间类型:Kotlin缺乏易用的日期时间类型,一般用Java的。Scala和SPL都有专业且方便的日期时间类型。有特色的数据类型:Kotlin支持非数值的字符Char、可空类型Any?。Scala支持元组(固定长度的泛型集合)、内置BigDecimal。SPL支持高性能多层序号键,内置BigDecimal。集合类型:Kotlin和Scala支持Set、List、Map。SPL支持序列(有序泛型集合,类似List)。结构化数据类型:Kotlin有记录集合List<EntityBean>,但缺乏元数据,不够专业。Scala有专业的结构化数类型,包括Row、RDD、DataSet、DataFrame(本文以此为例进行说明)等。SPL有专业的结构化数据类型,包括record、序表(本文以此为例进行说明)、内表压缩表、外存Lazy游标等。Scala独有隐式转换能力,理论上可以在任意数据类型之间进行转换(包括参数、变量、函数、类),可以方便地改变或增强原有功能。流程处理三者都支持基础的顺序执行、判断分支、循环,理论上可进行任意复杂的流程处理,这方面不多讨论,下面重点比较针对集合数据的循环结构是否方便。以计算比上期为例,Kotlin代码:mData.forEachIndexed{index,it-> if(index>0) it.Mom= it.Amount/mData[index-1].Amount-1 }Kotlin的forEachIndexed函数自带序号变量和成员变量,进行集合循环时比较方便,支持下标取记录,可以方便地进行跨行计算。Kotlin的缺点在于要额外处理数组越界。Scala代码:val w = Window.orderBy(mData("SellerId")) mData.withColumn("Mom", mData ("Amount")/lag(mData ("Amount"),1).over(w)-1)Scala跨行计算不必处理数组越界,这一点比Kotlin方便。但Scala的结构化数据对象不支持下标取记录,只能用lag函数整体移行,这对结构化数据不够方便。lag函数不能用于通用性强的forEach,而要用withColumn之类功能单一的循环函数。为了保持函数式编程风格和SQL风格的底层统一,lag函数还必须配合窗口函数(Python的移行函数就没这种要求),整体代码看上去反而比Kotlin复杂。 SPL代码:mData.(Mom=Amount/Amount[-1]-1)SPL对结构化数据对象的流程控制进行了多项优化,类似forEach这种最通用最常用的循环函数,SPL可以直接用括号表达,简化到极致。SPL也有移行函数,但这里用的是更符合直觉的“[相对位置]"语法,进行跨行计算时比Kotlin的绝对定位强大,比Scala的移行函数方便。上述代码之外,SPL还有更多针对结构化数据的流程处理功能,比如:每轮循环取一批而不是一条记录;某字段值变化时循环一轮。Lambda表达式Lambda表达式是匿名函数的简单实现,目的是简化函数的定义,尤其是变化多样的集合计算类函数。Kotlin支持Lambda表达式,但因为编译型语言的关系,难以将参数表达式方便地指定为值参数或函数参数,只能设计复杂的接口规则进行区分,甚至有所谓高阶函数专用接口,这就导致Kotin的Lambda表达式编写困难,在数据处理方面专业性不足。几个例子:"abcd".substring( 1,2) //值参数 "abcd".sumBy{ it.toInt()} //函数参数 mData.forEachIndexed{ index,it-> if(index>0) it.Mom=…} //函数参数的函数带多个参数Koltin的Lambda表达式专业性不足,还表现在使用字段时必须带上结构化数据对象的变量名(it),而不能像SQL那样单表计算时可以省略表名。同为编译型语言,Scala的Lambda表达式和Kotlin区别不大,同样需要设计复杂的接口规则,同样编写困难,这里就不举例了。计算比上期时,字段前也要带上结构化数据对象变量名或用col函数,形如mData ("Amount")或col("Amount"),虽然可以用语法糖弥补,写成$”Amount”或'Amount,但很多函数不支持这种写法,硬要弥补反而使风格不统一。SPL的Lambda表达式简单易用,比前两者更专业,这与其解释型语言的特性有关。解释型语言可以方便地推断出值参数和函数参数,没有所谓复杂的高阶函数专用接口,所有的函数接口都一样简单。几个例子:mid("abcd",2,1) //值参数 Orders.sum(Amount*Amount) //函数参数 mData.(Mom=Amount/Amount[-1]-1) //函数参数的函数带多个参数SPL可直接使用字段名,无须结构化数据对象变量名,比如:Orders.select(Amount>1000 && Amount<=3000 && like(Client,"*S*"))SPL的大多数循环函数都有默认的成员变量~和序号变量#,可以显著提升代码编写的便利性,特别适合结构化数据计算。比如,取出偶数位置的记录:Students.select(# % 2==0)求各组的前3名:Orders.group(SellerId;~.top(3;Amount))SPL函数选项和层次参数值得一提的是,为了进一步提高开发效率,SPL还提供了独特的函数语法。有大量功能类似的函数时,大部分程序语言只能用不同的名字或者参数进行区分,使用不太方便。而SPL提供了非常独特的函数选项,使功能相似的函数可以共用一个函数名,只用函数选项区分差别。比如,select函数的基本功能是过滤,如果只过滤出符合条件的第1条记录,可使用选项@1:T.select@1(Amount>1000)对有序数据用二分法进行快速过滤,使用@b:T.select@b(Amount>1000)函数选项还可以组合搭配,比如:Orders.select@1b(Amount>1000)有些函数的参数很复杂,可能会分成多层。常规程序语言对此并没有特别的语法方案,只能生成多层结构数据对象再传入,非常麻烦。SQL使用了关键字把参数分隔成多个组,更直观简单,但这会动用很多关键字,使语句结构不统一。而SPL创造性地发明了层次参数简化了复杂参数的表达,通过分号、逗号、冒号自高而低将参数分为三层:join(Orders:o,SellerId ; Employees:e,EId)数据源数据源种类Kotlin原则上可以支持所有的Java数据源,但代码很繁琐,类型转换麻烦,稳定性也差,这是因为Kotlin没有内置的数据源访问接口,更没有针对结构化数据处理做优化(JDBC接口除外)。从这个意义讲,也可以说它不直接支持任何数据源,只能使用Java第三方类库,好在第三方类库的数量足够庞大。Scala支持的数据源种类比较多,且有六种数据源接口是内置的,并针对结构化数据处理做了优化,包括:JDBC、CSV、TXT、JSON、Parquet列存格式、ORC列式存储,其他的数据源接口虽然没有内置,但可以用社区小组开发的第三方类库。Scala提供了数据源接口规范,要求第三方类库输出为结构化数据对象,常见的第三方接口有XML、Cassandra、HBase、MongoDB等。SPL内置了最多的数据源接口,并针对结构化数据处理做了优化,包括:JDBC(即所有的RDB)CSV、TXT、JSON、XML、ExcelHBase、HDFS、Hive、SparkSalesforce、阿里云Restful、WebService、WebcrawlElasticsearch、MongoDB、Kafka、R2dbc、FTPCassandra、DynamoDB、influxDB、Redis、SAP这些数据源都可以直接使用,非常方便。对于其他未列入的数据源,SPL也提供了接口规范,只要按规范输出为SPL的结构化数据对象,就可以进行后续计算。代码比较以规范的CSV文件为例,比较三种语言的解析代码。Kotlin:val file = File("D:\\data\\Orders.txt") data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date) var sdf = SimpleDateFormat("yyyy-MM-dd") var Orders=file.readLines().drop(1).map{ var l=it.split("\t") var r=Order(l[0].toInt(),l[1],l[2].toInt(),l[3].toDouble(),sdf.parse(l[4])) r } var resutl=Orders.filter{ it.Amount>= 1000 && it.Amount < 3000}Koltin专业性不足,通常要硬写代码读取CSV,包括事先定义数据结构,在循环函数中手工解析数据类型,整体代码相当繁琐。也可以用OpenCSV等类库读取,数据类型虽然不用在代码中解析,但要在配置文件中定义,实现过程不见得简单。Scala专业性强,内置解析CSV的接口,代码比Koltin简短得多:val spark = SparkSession.builder().master("local").getOrCreate() val Orders = spark.read.option("header", "true").option("sep","\t").option("inferSchema", "true").csv("D:/data/orders.csv").withColumn("OrderDate", col("OrderDate").cast(DateType)) Orders.filter("Amount>1000 and Amount<=3000")Scala在解析数据类型时麻烦些,其他方面没有明显缺点。SPL更加专业,连解析带计算只要一行:T("D:/data/orders.csv").select(Amount>1000 && Amount<=3000)跨源计算JVM数据处理语言的开放性强,有足够的能力对不同的数据源进行关联、归并、集合运算,但数据处理专业性的差异,导致不同语言的方便程度区别较大。Kotlin不够专业,不仅缺乏内置数据源接口,也缺乏跨源计算函数,只能硬写代码实现。假设已经从不同数据源获得了员工表和订单表,现在把两者关联起来:data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date ) val result = Orders.map { o->var emp=Employees.firstOrNull{ it.EId==o.SellerId } emp?.let{ OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate) } } .filter {o->o!=null}很容易看出Kotlin的缺点,代码只要一长,Lambda表达式就变得难以阅读,还不如普通代码好理解;关联后的数据结构需要事先定义,灵活性差,影响解题流畅性。Scala比Kotlin专业,不仅内置了多种数据源接口,而且提供了跨源计算的函数。同样的计算,Scala代码简单多了:val join=Orders.join(Employees,Orders("SellerId")===Employees("EId"),"Inner")可以看到,Scala不仅具备专用于结构化数据计算的对象和函数,而且可以很好地配合Lambda语言,代码更易理解,也不用事先定义数据结构。SPL更加专业,结构化数据对象更专业,跨源计算函数更方便,代码更简短:join(Orders:o,SellerId;Employees:e,EId)自有存储格式反复使用的中间数据,通常会以某种格式存为本地文件,以此提高取数性能。Kotlin支持多种格式的文件,理论上能够进行中间数据的存储和再计算,但因为在数据处理方面不专业,基本的读写操作都要写大段代码,相当于并没有自有的存储格式。Scala支持多种存储格式,其中parquet文件常用且易用。parquet是开源存储格式,支持列存,可存储大量数据,中间计算结果(DataFrame)可以和parquet文件方便地互转。遗憾的是,parquet的索引尚不成熟。val df = spark.read.parquet("input.parquet") val result=df.groupBy(data("Dept"),data("Gender")).agg(sum("Amount"),count("*")) result.write.parquet("output.parquet")SPL支持btx和ctx两种私有二进制存储格式,btx是简单行存,ctx支持行存、列存、索引,可存储大量数据并进行高性能计算,中间计算结果(序表/游标)可以和这两种文件方便地互转。 A 1 =file("input.ctx").open() 2 =A1.cursor(Dept,Gender,Amount).groups(Dept,Gender;sum(Amount):amt,count(1):cnt) 3 =file("output.ctx").create(#Dept,#Gender,amt,cnt).append(A2.cursor()) 结构化数据计算结构化数据对象数据处理的核心是计算,尤其是结构化数据的计算。结构化数据对象的专业程度,深刻地决定了数据处理的方便程度。Kotlin没有专业的结构化数据对象,常用于结构化数据计算的是List<EntityBean>,其中EntityBean可以用data class简化定义过程。List是有序集合(可重复),凡涉及成员序号和集合的功能,Kotlin支持得都不错。比如按序号访问成员:Orders[3] //按下标取记录,从0开始 Orders.take(3) //前3条记录 Orders.slice(listOf(1,3,5)+IntRange(7,10)) //下标是1、3、5、7-10的记录还可以按倒数序号取成员:Orders.reversed().slice(1,3,5) //倒数第1、3、5条 Orders.take(1)+Orders.takeLast(1) //第1条和最后1条涉及顺序的计算难度都比较大,Kotlin支持有序计集合,进行相关的计算会比较方便。作为集合的一种,List擅长的功能还有集合成员的增删改、交差合、拆分等。但List不是专业的结构化数据对象,一旦涉及字段结构相关的功能,Kotlin就很难实现了。比如,取Orders中的两个字段组成新的结构化数据对象。data class CliAmt(var Client: String, var Amount: Double) var CliAmts=Orders.map{it.let{CliAmt(it.Client,it.Amount) }}上面的功能很常用,相当于简单SQL语句select Client,Amount from Orders,但Kotlin写起来就很繁琐,不仅要事先定义新结构,还要硬编码完成字段的赋值。简单的取字段功能都这么繁琐,高级些的功能就更麻烦了,比如:按字段序号取、按参数取、获得字段名列表、修改字段结构、在字段上定义键和索引、按字段查询计算。Scala也有List,与Kotlin区别不大,但Scala为结构化数据处理设计了更加专业的数据对象DataFrame(以及RDD、DataSet)。DataFrame是有结构的数据流,与数据库结果集有些相似,都是无序集合,因此不支持按下标取数,只能变相实现。比如,第10条记录:Orders.limit(10).tail(1)(0) 可以想象,凡与顺序相关的计算,DataFrame实现起来都比较麻烦,比如区间、移动平均、倒排序等。除了数据无序,DataFrame也不支持修改(immutable特性),如果想改变数据或结构,必须生成新的DataFrame。比如修改字段名,实际上要通过复制记录来实现:Orders.selectExpr("Client as Cli") DataFrame支持常见的集合计算,比如拆分、合并、交差合并,其中并集可通过合集去重实现,但因为要通过复制记录来实现,集合计算的性能普遍不高。虽然有不少缺点,但DataFrame是专业的结构化数据对象,字段访问方面的能力是Kotlin无法企及的。比如,获得元数据/字段名列表:Orders.schema.fields.map(it=>it.name).toList还可以方便地用字段取数,比如,取两个字段形成新dataframe:Orders.select("Client","Amount") //可以只用字段名或用计算列形成新DataFrame:Orders.select(Orders("Client"),Orders("Amount")+1000) //不能只用字段名 遗憾的是,DataFrame只支持用字符串形式的名字来引用字段,不支持用字段序号或默认名字,导致很多场景下不够方便。此外,DataFrame也不支持定义索引,无法进行高性能随机查询,专业性还有缺陷。SPL的结构化数据对象是序表,优点是足够专业,简单易用,表达能力强。按序号访问成员:Orders(3) //按下标取记录,从1开始 Orders.to(3) //前3条记录 Orders.m(1,3,5,7:10) //序号是1、3、5、7-10的记录按倒数序号取记录,独特之处在于支持负号表示倒数,比Kotlin专业且方便:Orders.m(-1,-3,-5) //倒数第1,3,5条 Orders.m(1,-1) //第1条和最后1条作为集合的一种,序表也支持集合成员的增删改、交并差合、拆分等功能。由于序表和List一样都是可变集合(mutable),集合计算时尽可能使用游离记录,而不是复制记录,性能比Scala好得多,内存占用也少。序表是专业的结构化数据对象,除了集合相关功能外,更重要的是可以方便地访问字段。比如,获得字段名列表:Orders.fname() 取两个字段形成新序表:Orders.new(Client,Amount)用计算列形成新序表:Orders.new(Client,Amount*0.2)修改字段名:Orders.alter(;OrderDate) //不复制记录有些场景需要用字段序号或默认名字访问字段,SPL都提供了相应的访问方法:Orders(Client) //按字段名(表达式取) Orders([#2,#3]) //按默认字段名取 Orders.field(“Client”) //按字符串(外部参数) Orders.field(2) //按字段序号取作为专业的结构化数据对象,序表还支持在字段上定义键和索引:Orders.keys@i(OrderID) //定义键,同时建立哈希索引 Orders.find(47) //用索引高速查找计算函数Kotlin支持部分基本计算函数,包括:过滤、排序、去重、集合的交叉合并、各类聚合、分组汇总。但这些函数都是针对普通集合的,如果计算目标改成结构化数据对象,计算函数库就显得非常不足,通常就要辅以硬编码才能实现计算。还有很多基本的集合运算是Kotlin不支持的,只能自行编码实现,包括:关联、窗口函数、排名、行转列、归并、二分查找等。其中,归并和二分查找等属于次序相关的运算,由于Kotlin List是有序集合,自行编码实现这类运算不算太难。总体来讲,面对结构化数据计算,Kotlin的函数库可以说较弱。Scala的计算函数比较丰富,且都是针对结构化数据对象设计的,包括Kotlin不支持的函数:排名、关联、窗口函数、行转列,但基本上还没有超出SQL的框架。也有一些基本的集合运算是Scala不支持的,尤其是与次序相关的,比如归并、二分查找,由于Scala DataFrame沿用了SQL中数据无序的概念,即使自行编码实现此类运算,难度也是非常大的。总的来说,Scala的函数库比Kotlin丰富,但基本运算仍有缺失。SPL的计算函数最丰富,且都是针对结构化数据对象设计的,SPL极大地丰富了结构化数据运算内容,设计了很多超出SQL的内容,当然也是Scala/Kotlin不支持的函数,比如有序计算:归并、二分查找、按区间取记录、符合条件的记录序号;除了常规等值分组,还支持枚举分组、对齐分组、有序分组;将关联类型分成外键和主子;支持主键以约束数据,支持索引以快速查询;对多层结构的数据(多表关联或Json\XML)进行递归查询等。以分组为例,除了常规的等值分组外,SPL还提供了更多的分组方案:枚举分组:分组依据是若干条件表达式,符合相同条件的记录分为一组。对齐分组:分组依据是外部集合,记录的字段值与该集合的成员相等的分为一组,组的顺序与该集合成员的顺序保持一致,允许有空组,可单独分出一组“不属于该集合的记录”。有序分组:分组依据是已经有序的字段,比如字段发生变化或者某个条件成立时分出一个新组,SPL直接提供了这类有序分组,在常规分组函数上加个选项就可以完成,非常简单而且运算性能也更好。其他语言(包括SQL)都没有这种分组,只能费劲地转换为传统的等值分组或者自己硬编码实现。下面我们通过几个常规例子来感受一下这三种语言在计算函数方式的差异。排序按Client顺序,Amount逆序排序。Kotlin:Orders.sortedBy{it.Amount}.sortedByDescending{it.Client}Kotlin代码不长,但仍有不便之处,包括:逆序正序是两个不同的函数,字段名必须带表名,代码写出的字段顺序与实际的排序顺序相反。Scala:Orders.orderBy(Orders("Client"),-Orders("Amount"))Scala简单多了,负号代表逆序,代码写出的字段顺序与排序的顺序相同。遗憾之处在于:字段仍要带表名;编译型语言只能用字符串实现表达式的动态解析,导致代码风格不统一。SPL:Orders.sort(Client,-Amount)SPL代码更简单,字段不必带表名,解释型语言代码风格容易统一。分组汇总Kotlin:data class Grp(var Dept:String,var Gender:String) data class Agg(var sumAmount: Double,var rowCount:Int) var result1=data.groupingBy{Grp(it!!.Dept,it.Gender)} .fold(Agg(0.0,0),{acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)}) .toSortedMap(compareBy<Grp> { it.Dept }.thenBy { it.Gender })Kotlin代码比较繁琐,不仅要用groupingBy和fold函数,还要辅以硬编码才能实现分组汇总。当出现新的数据结构时,必须事先定义才能用,比如分组的双字段结构、汇总的双字段结构,这样不仅灵活性差,而且影响解题流畅性。最后的排序是为了和其他语言的结果顺序保持一致,不是必须的。Scala:val result=data.groupBy(data("Dept"),data("Gender")).agg(sum("Amount"),count("*"))Scala代码简单多了,不仅易于理解,而且不用事先定义数据结构。SPL:data.groups(Dept,Gender;sum(Amount),count(1))SPL代码最简单,表达能力不低于SQL。关联计算两个表有同名字段,对其关联并分组汇总。Kotlin代码:data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date ) val result = Orders.map { o->var emp=Employees.firstOrNull{it.EId==o.EId} emp?.let{ OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)} } .filter {o->o!=null} data class Grp(var Dept:String,var Gender:String) data class Agg(var sumAmount: Double,var rowCount:Int) var result1=data.groupingBy{Grp(it!!.EId.Dept,it.EId.Gender)} .fold(Agg(0.0,0),{acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)}) .toSortedMap(compareBy<Grp> { it.Dept }.thenBy { it.Gender })Kotlin代码很繁琐,很多地方都要定义新数据结构,包括关联结果、分组的双字段结构、汇总的双字段结构。Scalaval join=Orders.as("o").join(Employees.as("e"),Orders("EId")===Employees("EId"),"Inner") val result= join.groupBy(join("e.Dept"), join("e.Gender")).agg(sum("o.Amount"),count("*"))Scala比Kolin简单多了,不用繁琐地定义数据结构,也不必硬编码。SPL更简单:join(Orders:o,SellerId;Employees:e,EId).groups(e.Dept,e.Gender;sum(o.Amount),count(1))综合数据处理对比CSV内容不规范,每三行对应一条记录,其中第二行含三个字段(即集合的集合),将该文件整理成规范的结构化数据对象,并按第3和第4个字段排序.Kotlin:data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date) var Orders=ArrayList<Order>() var sdf = SimpleDateFormat("yyyy-MM-dd") var raw=File("d:\\threelines.txt").readLines() raw.forEachIndexed{index,it-> if(index % 3==0) { var f234=raw[index+1].split("\t") var r=Order(raw[index].toInt(),f234[0],f234[1].toInt(),f234[2].toDouble(), sdf.parse(raw[index+2])) Orders.add(r) } } var result=Orders.sortedByDescending{it.Amount}.sortedBy{it.SellerId}Koltin在数据处理方面专业性不足,大部分功能要硬写代码,包括按位置取字段、从集合的集合取字段。Scala:val raw=spark.read.text("D:/threelines.txt") val rawrn=raw.withColumn("rn", monotonically_increasing_id()) var f1=rawrn.filter("rn % 3==0").withColumnRenamed("value","OrderId") var f5=rawrn.filter("rn % 3==2").withColumnRenamed("value","OrderDate") var f234=rawrn.filter("rn % 3==1") .withColumn("splited",split(col("value"),"\t")) .select(col("splited").getItem(0).as("Client") ,col("splited").getItem(1).as("SellerId") ,col("splited").getItem(2).as("Amount")) f1.withColumn("rn1",monotonically_increasing_id()) f5=f5.withColumn("rn1",monotonically_increasing_id()) f234=f234.withColumn("rn1",monotonically_increasing_id()) var f=f1.join(f234,f1("rn1")===f234("rn1")) .join(f5,f1("rn1")===f5("rn1")) .select("OrderId","Client","SellerId","Amount","OrderDate") val result=f.orderBy(col("SellerId"),-col("Amount"))Scala在数据处理方面更加专业,大量使用结构化计算函数,而不是硬写循环代码。但Scala缺乏有序计算能力,相关的功能通常要添加序号列再处理,导致整体代码冗长。SPL: A 1 =file("D:\\data.csv").import@si() 2 =A1.group((#-1)\3) 3 =A2.new(~(1):OrderID, (line=~(2).array("\t"))(1):Client,line(2):SellerId,line(3):Amount,~(3):OrderDate ) 4 =A3.sort(SellerId,-Amount) SPL在数据处理方面最专业,只用结构化计算函数就可以实现目标。SPL支持有序计算,可以直接按位置分组,按位置取字段,从集合中的集合取字段,虽然实现思路和Scala类似,但代码简短得多。应用结构Java应用集成Kotlin编译后是字节码,和普通的class文件一样,可以方便地被Java调用。比如KotlinFile.kt里的静态方法fun multiLines(): List<Order>,会被Java正确识别,直接调用即可:java.util.List result=KotlinFileKt.multiLines(); result.forEach(e->{System.out.println(e);});Scala编译后也是字节码,同样可以方便地被Java调用。比如ScalaObject对象的静态方法def multiLines():DataFrame,会被Java识别为Dataset类型,稍做修改即可调用:org.apache.spark.sql.Dataset df=ScalaObject.multiLines(); df.show();SPL提供了通用的JDBC接口,简单的SPL代码可以像SQL一样,直接嵌入Java:Class.forName("com.esproc.jdbc.InternalDriver"); Connection connection =DriverManager.getConnection("jdbc:esproc:local://"); Statement statement = connection.createStatement(); String str="=T(\"D:/Orders.xls\").select(Amount>1000 && Amount<=3000 && like(Client,\"*s*\"))"; ResultSet result = statement.executeQuery(str);复杂的SPL代码可以先存为脚本文件,再以存储过程的形式被Java调用,可有效降低计算代码和前端应用的耦合性。Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); CallableStatement statement = conn.prepareCall("{call scriptFileName(?, ?)}"); statement.setObject(1, "2020-01-01"); statement.setObject(2, "2020-01-31"); statement.execute();SPL是解释型语言,修改后不用编译即可直接执行,支持代码热切换,可降低维护工作量,提高系统稳定性。Kotlin和Scala是编译型语言,编译后必须择时重启应用。交互式命令行Kotlin的交互式命令行需要额外下载,使用Kotlinc命令启动。Kotlin命令行理论上可以进行任意复杂的数据处理,但因为代码普遍较长,难以在命令行修改,还是更适合简单的数字计算:>>>Math.sqrt(5.0) 2.236.6797749979Scala的交互式命令行是内置的,使用同名命令启动。Scala命令行理论上可以进行数据处理,但因为代码比较长,更适合简单的数字计算:scala>100*3 rest1: Int=300SPL内置了交互式命令行,使用“esprocx -r -c”命令启动。SPL代码普遍较短,可在命令行进行简单的数据处理。(1): T("d:/Orders.txt").groups(SellerId;sum(Amount):amt).select(amt>2000) (2):^C D:\raqsoft64\esProc\bin>Log level:INFO 1 4263.900000000001 3 7624.599999999999 4 14128.599999999999 5 26942.4通过多方面的比较可知:对于应用开发中常见的数据处理任务,Kotlin因为不够专业,开发效率很低;Scala有一定的专业性,开发效率比Kotlin高,但还比不上SPL;SPL语法更简练,表达效率更高,数据源种类更多,接口更易用,结构化数据对象更专业,函数更丰富且计算能力更强,开发效率远高于Kotlin和Scala。SPL资料SPL下载的,使用同名命令启动。Scala命令行理论上可以进行数据处理,但因为代码比较长,更适合简单的数字计算:scala>100*3 rest1: Int=300SPL内置了交互式命令行,使用“esprocx -r -c”命令启动。SPL代码普遍较短,可在命令行进行简单的数据处理。(1): T("d:/Orders.txt").groups(SellerId;sum(Amount):amt).select(amt>2000) (2):^C D:\raqsoft64\esProc\bin>Log level:INFO 1 4263.900000000001 3 7624.599999999999 4 14128.599999999999 5 26942.4通过多方面的比较可知:对于应用开发中常见的数据处理任务,Kotlin因为不够专业,开发效率很低;Scala有一定的专业性,开发效率比Kotlin高,但还比不上SPL;SPL语法更简练,表达效率更高,数据源种类更多,接口更易用,结构化数据对象更专业,函数更丰富且计算能力更强,开发效率远高于Kotlin和Scala。SPL资料SPL下载SPL源代码
Spring项目的创建前置知识:在java语言中的对象,也叫Bean,这里Spring下的对象都以Bean著称!Spring是IoC容器!我们需要掌握的就是存Bean取BeanSpring项目创建有3步!创建一个Maven 项目添加Spring框架支持(引入Spring-context,Spring-beans依赖)创建移动类创建Maven项目添加Spring框架支持在pom.xml配置文件下引入依赖!引入的框架:Spring-context:spring上下文,Spring-beans:管理对象模块.<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.2.3.RELEASE</version> </dependency> </dependencies>将依赖复制到pom.xml文件中,然后记得刷新项目,保证依赖成功引入!创建启动类在java源文件中创建一个启动类,包含main,项目就创建好了!存储Bean创建Bean将Bean注册到Spring中创建Bean我们知道Bean就是java对象,我们创建一个java对象即可!//Bean public class User { public void sayHi(){ System.out.println("Hello world!"); } }将Bean注册到Spring中首先我们要在项目中添加Spring注册对象的配置文件spring-config.xml将该文件放在resource根目录下即可!spring-config.xml配置文件(内容固定,我们需要用到时复制即可)<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>将Bean注册到Spring中注册操作就是在配置文件中进行,具体在<Beans>中添加配置信息即可!添加这一行就将UserBean注册到了Spring中!<bean id="user" class="User"></bean>id:这里的id就是我们后面取该对象使用的id,这是我们自己设置的,但是命名规范是类名小驼峰!class: 这是我们需要注册的Bean对象的类名(全限定类名)这里的类名是以java源文件为根,然后如果在对应的包下,就要包含包名!例如User在java源文件的com/bug包下,那么class="com.bug.User"获取并使用 Bean 对象获取Spring上下文对象,因为我们的Bean对象已经注册到了Spring下,我们通过Spring上下文对象才能获取Bean对象,也是说,Spring上下文对象是Spring给我们提供的一个获取Bean对象的方式!通过Spring上下文对象,获取到Bean对象使用Bean对象获取Spring上下文对象import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class App { public static void main(String[] args) { //ApplicationContext获取到上下文对象,创建时需要传入spring配置文件名! ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); } }除了ApplicationContext获取上下文对象的方式外,我们可以通过另外一种方式获取到上下文对象!BeanFactory也可以获取到上下文对象!//2.BeanFactory 获取上下文对象 BeanFactory factory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));这两种获取上下文对象的方式都可以,这里的BeanFactory是ApplicationContext`的父类!那么两者有什么区别呢?ApplicationContextVSBeanFactory相同点:都可以实现从容器中获取Bean,提供了getBean方法!不同点继承关系和功能方面上;Spring容器有2个顶级的接口,BeanFactory和ApplicationContextApplicationContext是BeanFactory的子类,所以继承了父类的所有功能属性,并且还拥有自己独特的特性,添加了国际化支持,资源访问支持,以及事件传播等方面支持!性能方面:Application是一次加载好所有Bean对象,(类似于饿汉),而BeanFactory是需要使用时才去加载(懒加载),更加轻量!获取Bean我们通过上下文对象提供的getBean方法获取到相应的Bean!我们通过上下文对象,调用getBean方法,传入我们注册对象时的id获取到对应的Bean对象!当是这里的getBean返回的是Object对象,所以我们需要进行强转!获取到Bean对象,我们就可以使用Bean对象了!//获取Bean User user = (User) context.getBean("user"); //使用Bean 调用该对象下的sayHi方法! user.sayHi();我们还可以通过其他方式获取到Bean对象!getBean方法的3种方式获取到Bean通过 id和类型强转获取Bean//1.通过id + 强转 User user1 = (User) context.getBean("user")通过反射拿到类型获取Bean//2.通过反射拿到类型 User user2 = context.getBean(User.class);通过反射拿到类型+id//3.通过反射拿到类型 + id User user3 = context.getBean("user",User.class);//获取Bean //1.通过id + 强转 User user1 = (User) context.getBean("user"); System.out.println("通过id+强转"); user1.sayHi(); //2.通过反射拿到类型 User user2 = context.getBean(User.class); System.out.println("通过反射拿到类型"); user2.sayHi(); //3.通过反射拿到类型 + id User user3 = context.getBean("user",User.class); System.out.println("类型+id"); user3.sayHi();可以看到上述3种方式都可以获取到响应的Bean对象,并且调用sayHi方法!注意:我们通过类型获取Bean时,要保证该Bean对象只注册过一次,也就是只在spring-config.xml 创建了一个该类的Bean,如果注册了多个Bean那该方法就行不通啦!注册多个类型相同的Bean对象!此时通过类型获取Bean对象就行不通了!其他2个方法不会影响,因为获取Bean时,传入了对应的idSpring操作流程如下:
JVM执行流程我们知道JVM就我们的java虚拟机(Java Virtual Machine)的简称!java执行一个java文件的流程:程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。JVM大致通过4个部分来执行我们的java程序:类加载器运行时数据区执行引擎本地库接口而上述4个部分中我们的java运行时数据区也叫内存布局,我们重点了解一些这块空间的内存是如何布局的即可!内存区域划分为啥要将内存区域进行划分呢?对内存区域进行划分,就让不同的内存空间具有不同的功能,就好比我们的学校不同区域具有不同的功能,教学楼是用来学习的,寝室是用来睡觉的,食堂是用来吃饭的!而我们的JVM中的内存区域也是按照不同区域行使的功能进行了划分!java运行时数据区(内存布局划分如下)主要分为4个重要的区域程序计数器栈堆方法区我们下面来分别介绍这个4个不同区域的功能程序计数器程序计数器这块空间保存了下一条要执行指令的地址,我们通过这个地址空间,就可以让程序顺利执行下去!这里的程序计数器每个线程都有一个,我们知道线程是程序调度的基本单位嘛!而我们的CUP是需要并发执行的,并不能保证某一时刻连贯的就把某一进程执行结束,我们的CPU需要服务于多个线程,所有程序计数器很好的对程序的执行的位置进行了存档,当我们的该线程再次被CPU调用时根据程序计数器中记录的指令就可以找到上次执行的位置继续执行程序!栈这里的栈和我们数据结构中的栈结构类似,不过这里的栈保存的信息是栈帧栈中主要保存2种信息方法调用信息局部变量栈帧(方法调用信息):方法的实参局部变量方法调用的位置方法执行结束的位置我们通过下面代码进一步了解:public class Main { static void fun1(){ fun2(); } static void fun2(){ fun3(); } static void fun3(){ fun4(); } static void fun4(){ System.out.println("fun4"); } public static void main(String[] args) { fun1(); } }当我们需要执行fun1方法,首先我们需要将main栈帧入栈,通过main栈帧中的信息,我们可以知道此时我们需要调用fun1,然后就将fun1栈帧入栈,紧接着fun3入栈,最后调用fun4然后进行打印,当我们的fun4方法调用结束,此时fun4栈帧就会出栈,然后回到fun3调用位置,fun3结束,fun3出栈,回到fun2,fun2出栈,fun1,main栈帧出栈,整个程序就执行结束了!这里就需要注意的是我们的栈空间有限,一般只有几M到十几M大小,虽然可以自行设置,但是当我们递归时如果递归次数过多,或者递归出口没有设置很有可能导致栈溢出StackOverflow堆(线程共享)成员变量new出来的对象class A{ public String name1 = "张三"; public void fun(){ String name2 = new("李四"); System.out.println(name2); } }我们的上述代码中成员变量 name1保存在堆中,fun方法中的name2引用属于局部变量保存在栈中,而其引用指向的本体new("李四")对象本体保存在堆!注意:这里的堆空间是线程共享空间,一个进程只要一个堆空间,所有这里的堆空间大小最大!方法区方法区存放的是类对象啥是类对象呢?我们JVM执行一个.java程序,第一步要先将这个通过javac指令将该文件转成二进制字节码文件.class,而.class文件就会来到内存中,通过JVM加载将其构造成类对象,这里的加载过程就叫做类加载!类对象都有些啥呢?类对象就是描述这个类长啥样!类名,有哪些成员,成员名,成员类型,public/private,方法,方法名,方法中的指令…类对象中还有一个重要东西,静态成员~就是static修饰的成员雷属性,而普通的成员叫做实例属性!上面就是对内存区域划分及其功能的大致介绍,指的注意的是不同版本的JVM可能有不同的划分方式,但是大致都是一样的!类加载过程我们刚刚了解到我们的JVM执行流程,就是将一个.class文件加载到内存中,然后根据.class文件构造一个类对象,当类对象结束使用后,一个类的生命周期也就结束!而我们的类加载过程一共分为3个步骤!加载(Loading)加载过程主要做的,就是先找到对应的.class文件,然后打开并读取.class文件,同时初步生成一个类对象!Loading阶段最关键就是找到对应的.class文件,并且将其解析成类对象,那么如何去找到一个.class文件呢?1)通过一个类的全限定名来获取定义此类的二进制字节流。2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口我们的JVM通过上述格式,就可以将一个.class文件找到,然后通过这个格式进行加载初步构造一个类对象!连接(Linking)连接又分为3个步骤:验证(Verification)这里验证就是通过.class文件中的内容对照JVM中提供的class文件标准格式信息进行对照,如果不符合格式,就会类加载失败,然后抛出异常准备(Preparation)准备阶段就是给静态变量(类变量)分配内存,并且统一初始化置为0解析(Resolution)解析阶段是JVM将常量池内的符号引用替换成直接引用的过程,也就是初始化常量的过程我们的.class文件中的常量是集中放置的.然后每个常量都会对应一个标号引用,而我们的class文件结构体初始只是记录了标号引用,所以我们要根据标号引用拿到对应常量,对常量进行初始化,填充到类对象中!初始化(Initialization)这里才是真正对类进行初始化,尤其是静态变量!初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。经典面试代码题class A { public A(){ System.out.println("A构造方法"); } { System.out.println("A构造代码块"); } static { System.out.println("A静态代码块"); } } class B extends A{ public B(){ System.out.println("B构造方法"); } { System.out.println("B构造代码块"); } static { System.out.println("B静态代码块"); } } public class Main extends B{ public static void main(String[] args) { new B(); new B(); } }输出上述代码的打印结果:上述类型题目抓住3个要点即可攻破!类加载要优先加载类属性(所以这里的静态代码块优先加载),而创建实例之前需要执行类加载过程,并且类加载自进行一次!构造方法在构造代码块前,并且每次实例化都会调用对应的构造方法!父类执行完再子类!双亲委派模型提到类加载机制,不得不提的一个概念就是“双亲委派模型”!我们通过对类加载机制的学习了解了类的一个生命周期!在第一个Loading加载环节!我们需要通过类名去找到对应的类,而双亲委派模型就是JVM使用的找到一个类的机制!我们的JVM对类加载目录扫描进行了分工!通过不同的加载器扫描不同的目录,主要有3个类加载器!启动类加载器(Bootstrap ClassLoader)类启动加载器,就是负责加载JDK中标准库中的类(scanner,String,ArrayList…)扩展类加载器(Extension ClassLoader)负责加载JDK中扩展的类!应用程序类加载器(Application ClassLoader)应用程序类加载器负责加载我们开发人员自己构造的类!我们的JVM如何通过上述类加载器找到对应的类呢?实例一:找到 java.lang.String类!首先第一步来到应用程序类加载器,然后应用程序类加载器会首先检查他的父类加载器是否加载过,如果没有就就调用父类,也就是扩展类加载器,扩展类加载器,也是先检查父类是否加载过,没有就调用父类加载器,启动类加载器,然后检查父类是否加载过,显然他没有父类,就对其下的目录进行扫描,然后在标准库目录找到了该类,也就加载结束!实例二: 找到加载一个 Test类也是和实例一的步骤一样,逐层向上调用加载,到达启动类加载器后扫描后并没有找到,就再次回到扩展类加载器,扫描结束,回到程序类加载器,扫描后在其目录,将其类加载!JVM为啥这样设计呢?安全性: 我们通过这样父类优先类加载的过程,就保证了一个如果我们开发人员写的一个类名和标志库中类一样时,优先加载标志库中的类,也能顺利加载到标志库中的类!避免重复加载类:如果A类和B类都有一个相同的父类C的话,当A启动后,就会将C类加载起来,那么B类启动会无需重复加载C类了!垃圾回收策略(GC)我们知道内存的申请是由我们程序员控制的,当我们创建了一个变量,就是申请了一块内存空间,而当我们这个变量不用时,应该将这块空间释放!这里释放归还的过程就会用到垃圾回收策略garbage collection简称GC!而有的编程语言像C/C++内存的释放还是通过程序员自己释放,但是这样通过程序员自己释放就会降低开发效率!我们程序员有时候很难控制释放的时间,如果释放的早,就会导致申请释放内存的开销,如果释放的晚,或者没有进行释放,就会导致内存泄漏问题!大部分主流语言还是通过一个专门的进程,对内存空间进行释放,也就是我们这里说的垃圾回收!像Python/java/go/PHP等都是如此,不过不同语言的垃圾回收策略会有不同,我们主要学习java中的JVM是如何进行GC的!垃圾回收策略的缺点:1).消化额外的开销(我们在JVM下搞一个需要一个专门进程,这样消化的资源就多了)2).可能影响程序运行的流畅度(垃圾回收会导致STW(Stop The World)问题,就是程序中断,注意这里说的中断并不是我们多线程学的中断,这里指的是GC要对垃圾进行回收,使得我们的业务程序不得不停止!)我们知道了内存区域是如何划分的,而我们划分的空间如果不用了,就需要回收!我们JVM的内存空间是想操作系统申请的!当我们不使用时,就需要将其归还,有借有还再借不难!我们的JVM是如何判断一块空间是不用的,又是如何进行回收的呢?垃圾回收都回收啥内存?我们将JVM的内存空间进行了划分,GC主要回收的内存空间就是堆上的空间,因为这块空间最大,也是我们需要回收的空间,不像栈会根据程序的执行,自己释放!程序计数器的空间大小是固定了也不需要释放,方法区存放的是类对象,而类对象只在类加载时加载一次创建一次,最后该类结束后,进行类卸载,需要释放内存,这里是比较低频的操作!我们GC的关键就是针对堆上的空间进行回收,因为我们代码中大量的内存空间都是在堆上的!上图就是大致我们堆上的内存的使用分布!我们可以知道我们这里的CG基本单位是对象,并不是字节,就好比对象1,有些变量还在使用,有一些不在使用,我们就要保留这块内存空间,自制所有的内存都不在使用,就将其回收!我们主要针对一整个对象回收!如何定位垃圾?当下垃圾回收机制有2个主流的定位垃圾的机制基于引用计数基于可达性分析基于引用计数我们堆上主要的保存的就是我们new的对象和成员变量,我们可以根据一块空间记录指向一个对象的引用个数,然后根据记录的引用数决定是否需要将这块空间回收,如果引用为0说明这个对象已不再使用,就可以进行垃圾回收,这就是引用计数的方式!t1 = new A();//new A()对象的引用加一! t2 = new B();//new B()对象的引用加一!我们的A对象只有有t1引用指向,所以引用数为1,B对象也是如此!当某一时刻,该引用变量释放,对应的对象引用计数也会减1,然后为0时就会将这块内存空间视为垃圾,将其回收!循环引用问题:t1 = new A(); t1.t = new B(); t2 = new B(); t2.t = new A();循环引用就是某一引用变量t1下面的属性也有引用t指向一个对象,当外面的引用变量t1释放后,new A()对象的引用计数是减1了,但是new B()对象的引用计数不会减少,t2也是如此,最后虽然t1和t2引用已经不存在,但是对象A和对象B的引用数还是1,这就尴尬了,虽然没人能拿到这两个对象,但是引用计数还是1,不能释放…引用计数的缺点要消耗额外空间,我们需要一块特点的空间记录引用数!当我们的对象本身空间就不大时,引用计数空间就很费资源!循环引用问题,不能进准定位到垃圾!基于可达性分析我们上述的引用计数是其他语言(PHP/Python)使用的定位垃圾的手段,而我们的java使用的定位垃圾的策略是可达性分析!可达性分析: 就是通过额外的线程,对内存进行扫描,然后可以扫描到的对象就将其标记,这里就需要规定扫描的起始位置GCRoots,通过起始位置,类似于二叉树的深度优先遍历一样,将能够访问到的对象都标记一遍,访问不到的就是是不在使用的内存空间,也就是垃圾了!GCRoots:1).栈上的局部变量!2).常量池中引用指向的对象3).方法区中静态变量指向的对象我们用二叉树遍历来模拟GC可达性分析:我们通过GCRoots起始位置对内存空间进行深度优先扫描,我们可以扫描到的节点对象就是真正使用内存的对象(a,b,d,e,f,g),我们将其标记,而没有扫描到的对象(h,j)也就是垃圾,我们将其空间回收即可!我们通过引用计数和可达性分析可以将垃圾定位,那应该如何对垃圾进行处理呢也就是回收内存?我们通过3种算法机制对垃圾进行回收:标记清楚复制算法标记整理标记清除标记清楚就是对垃圾进行标记,然后将该标记的空间进行清楚回收即可!可以看到这种清除垃圾的算法,将内存空间回收后,会造成大量内存碎片,造成空间浪费!复制算法复制算法就是空间分成2分,一份用于对象使用,一份用于复制!当标记到了垃圾,我们需要对这一半空间中的对象复制到另外一半空间,然后整体回收这一半的空间!虽然这个算法解决了标记清楚的内存碎片问题,但是空间利用率低!有一半的空间未被使用到!标记整理标记整理,就类似数组中删除 某一元素,需要将数组元素进行整理!显然这种算法解决了上述2个算法的缺点,但是效率比较低,每次整理都要耗费大量时间开销!我们GC在进行垃圾回收时会根据不同的情况,使用不同的算法进行回收垃圾!分代回收我们JVM下的垃圾回收,将多种方案进行了结合, 一起使用,叫做分代回收!这里的分代是根据"年龄"进行划分的,这里的年龄并不是传统意义上的年龄,是指经过一轮GC扫描后,如果对象还在,那这个对象就长一岁!根据不同岁数的对象进行了划分!我们将堆空间进行分区,首先2个大类,新生代和老年代,然后新生代下又进行了划分,伊甸区和2个幸存区!新生代伊甸区我们的创建好的对象直接放入到新生代中的伊甸区下!并且伊甸区大部分对象经过一轮GC扫描后就会进行回收,只有少部分还存在!幸存区当伊甸区经过一轮GC扫描后,幸存的对象,就来到辛存区!辛存区的对象也会经过多轮GC扫描,这里的垃圾回收算法采用的是复制算法!然后经过多轮的GC扫描后没有被淘汰的对象就来到了老年代!老年代老年代下的对象,GC扫描的频率会大大减低,并且这里垃圾回收的算法采用的是标记整理算法!这里的GC扫描会经过很长的时间进行扫描,因为一般能来到老年代下的对象,命都比较硬!注意:这里老年代下的对象除了是辛存区下来的,还有就是所占空间比较大的对象,直接就来到老年代,因为大对象,不适合复制算法进行回收,并且大对象一般存活的时间也比较长!我们类比我们找工作的过程来理解分代回收机制:首先投简历直接来到伊甸区,啪的一下,面试官进行一轮简历筛选,大部分简历直接就丢了,然后剩下的人就来到了辛存区,好比我们通过简历后要经过很多轮的笔试和面试,最后才能拿到offer,拿到offer后,我们就来到了老年代,虽然已经进公司了,但也不是就稳定了,如果干的不好,就会将其淘汰!而这里有一些牛逼大大佬,直接就免了笔试和面试,直接就进公司了,就好比大对象一样!垃圾收集器我们上述介绍的回收机制只是思想,如果要具体落地实现,要通过JVM中的垃圾收集器具体进行回收,因为随着JVM版本的更迭,收集器也不断的更新!所以我们就大致了解一下即可!串行收集Serial 针对新生代Serial Old针对老年代在进行垃圾回收时,我们的业务线程需要停止工作,这种方式扫描的慢释放的页慢,产生了严重的STW!并发收集ParNewParallel Scavenge上面2个都是针对新生代Parallel Old:针对老年代并发收集,引入了多线程,但是也是比较低效的收集方式!CMS收集器设计比较巧妙,尽可能减少STW可达性分析1).初始标记,速度很快,会引起短时间的STW,这里的标记只是为了找到Roots2).并发标记,比较慢,但是这是和业务线程并发的,不会产生STW3).重新标记,在并发标记时,并发的业务代码可能会影响标记结果,所以对标记进行微调,速度较快,会引起短时间的STW标记整理4).回收内存,和业务线程并发!-G1收集器把整个内存分成很多个小的区域Region,给这些Region进行标记,然后根据年龄放入不同的分代区域,扫描的时候一次扫描若干个Region,分多次扫描,所以影响业务代码最小,可以使STW减小到1ms
Maven介绍Maven是个啥?Maven就是一个工程管理工具/构建工具!maven是java圈子里的一个知名工具核心功能:管理依赖依赖:就是当我们要进行某项操作之前要先具备某项操作,就例如我们要实现在idea下进行mysql数据库的操控,我们就要先导入jdbc这个依赖,这个jar包!还有我们在编写代码时用到的库都可以看成是依赖!为啥要管理呢?因为当我们的项目要使用到很多依赖,例如很多第三方库时,这时如果我们需要直接管理,就要花费很多时间!而maven就直接帮我们管理好了!不需要我们自己去了解依赖间的关系!构建/编译我们知道我们的java代码最后还是在jdk下编译运行的,而这里的maven也是通过jdk帮助我们进行编译!打包打包就是将我们写的java项目压缩成war包或者jar包,里面有很多.class文件,这就是发布的过程!创建maven项目我们这里的maven虽然是一个工具,但是我们可以不下载,直接使用idea下现成的maven创建项目即可!我们打开idea在创建项目时,选择Maven创建即可!我们只需要将项目名填写,然后选择好路径即可!其他信息自行解读!我们项目创建成功后就可以看到下面目录文件!这里不同的目录下面存放的文件有不同的意义!src/main/java/:存放我们的java代码!src/main/resources/:存放依赖资源src/test/java/存放单元测试的代码!src/pom.xml:这个maven项目的核心配置文件信息!这里就描述了一个maven项目的生命周期!通过点击不同的状态,生成不同状态的maven项目!例如:package就是用来打包成war包的!使用举例那么如何使用maven工具进行开发呢?下载依赖我们通过maven就可以下载引入依赖!例如:引入jdbc!我们通过maven中央管理仓库,找到我们需要下载的jdbc!找到maven配置信息,复制到我们的项目中的pom.xml中即可!我们要将这里的信息复制到<dependencies> </dependencies>我们将所有的第三方依赖信息都放在里面!我们这样就导入了jdbc的依赖信息,maven就会帮我们进行下载对应的资源!我们进行刷新一下即可!这就导入了依赖!我们就可以进行数据库编程了!Servlet项目我们通对学习maven项目的创建已经对maven工具的使用有了一定的了解!我们此时就可以步入今天的正题ServletServlet这是HTTP给我们提供的API,用于java中构建HTTP请求和响应!我们通过对Servlet的学习,就可以直接创建请求和响应,这是我们创建一个web项目的基础!Servlet项目七步走1.创建项目:maven项目2.引入依赖:把servlet依赖引入3.创建目录:webapp/WEB-INF/web.xml4.编写Servlet代码5.打包6.部署7.验证程序我们通过上面7步就可以创建一个Servlet项目!刚刚我们已经解决了上面的第一步!2.引入Servlet依赖4.创建目录:wabapp/WEB-INF/web.xml我们通过刚刚创建的目录不足以编写一个Servlet项目,所以我们需要在src/main下创建wabapp/WEB-INF/web.xml为啥要这样创建呢?嗯…规定!我们来看看Tomcat下的目录这里是多个web项目存放在webapps所以这里要加上s而我们这里只是一个项目,所有用webapp!还有这里web.xml需要写点内容!这里的内容也是规定这样写的,不必深究!<!-- web.xml--> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>将上面的内容复制到web.xml下即可!5.编写Servlet代码import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/Servlet")//ServletPath public class ServletTest extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //req:请求,resp:响应,需要我们直接根据不同的请求编写响应代码! resp.getWriter().write("hello world"); } }HttpServlet:我们通过继承Servlet给我提供的HttpServlet方法,才可以编写以个Servlet代码!这里只有我们正确引入依赖才能继承成功!@webServlet("/Servlet")这里用于关联tomcat服务器下URL的路径,并确该Servlet对象所在位置,因为一个web项目中可能有很多一个网页,或者请求交换,通过这里的路径就可以确定请求或者页面位置!doGet:表示处理一个Get请求!这里是重写父类的方法,需要将调用父类中的该方法这一行注释掉!不注销掉,我们自己编写的方法就不生效!这里可以处理Post等一些列请求!req:(request)是Tomcat服务器接收到的请求,我们不需要处理,只需要根据请求作出响应的响应即可!resp(response):我们只需要将响应写入该响应对象即可,其他操作由我们的tomact处理!服务器工作流程:1.接收并解析请求2.根据请求,计算响应3.构造响应数据返回给客户端这里的1,3都由我们的tomcat服务器完成,我们只需要完成第二步即可!resp.getWriter().write("hello world");getWriter()方法返回一个Writer(字符流)对象,这里的Writer是直接往响应body中写,然后通过writer方法写数据!6 打包部署打包前我们先配置一些pom.xml文件!因为我们默认打包是jar而我们需要打包成war!<packaging>war</packaging> <!--打包类型 war--> <build> <finalName>Test</finalName> <!--指定打包成的war包名!--> </build>我们点击package就完成了打包!我们在这个项目的路径下找到这个war包!然后部署到tomcat服务器上!也就是复制到我们tomcat的webapps目录下即可,会主动帮我们解压缩!验证Content path:这里的Test/一级路径,就是我们的项目路径名(也就是我们打包的war名),定位我们的项目位置,因为服务器下可能有很多项目!Servlet path:这里的Servlet二级目录,就指定了该项目下的请求或者或者网页(html)位置!我们可以看到我们的这个项目部署成功!这里的hello world正是我们返回给服务器的响应数据!我们也能构造动态的页面,用于用户交换!我们通过Fiddler抓包读取一下请求和响应!请求:这里是一个Get请求,服务器的url为127.0.0.1:8080响应:这里的请求成功,响应状态码200 ok!body中的内容就是我们计算的响应数据!smart tomcat我们看到上面的打包部署在我们的服务器上的步骤比较繁琐!我们能不能简化开发?smart tomcat我们通过将smart tomcat插件扩展到idea下,将tomcat集成到idea!就省去了打包部署的步骤,通过这个插件可以做到一键式打包部署!其实就是帮我们把Servlet打包部署到我们本地的tomcat下!smart tomcat安装smart tomcat使用将smart tomcat插件添加到这个项目中!设置Content Path就是我们项目的第一级路径,后面访问要路径一致!一键打包部署可以看到报错了,这里就提示我们端口被占用,因为刚刚我们已经启动了tomcat服务器,需要把刚刚那台服务器关了!这就部署成功了!验证
tomcat介绍tomcat是个啥玩意呀?汤姆猫?这里的tomcatb并不是汤姆猫,这里的tomcat是http的一个服务器!我们之前已经学过了,TCP协议报,http协议报,然后我们知道构造http请求和响应比较繁琐,并不简单!而我们的http是基于tcp协议的,我们通过这里的tomcat服务器就可以更好的构造http请求和响应!我们之前不是一直困扰没有直接的服务器,我们构造的请求没有人响应,然后我们通过这个tomcat服务器,在这个服务器下就可以实现请求和响应的构建,就不用借助其他服务器了!这里的tomcat是跨平台的,是专门为java服务的!我们需要下载到本地!下载安装tomcat官网我们通过官网进行下载!这就是tomcat的官网!tomcat是由一个apache开源组织管理!所以直接找到下载位置下载即可!选择版本号下载对应的版本即可!这里的下载也是有讲究的,并不是版本越新越好,需要下载合适的,因为我们要搭配我们的jdk和MySQL使用!需要版本兼容适配,就是高版本对应高版本,不能跨越太大否者会出现问题!下载解压缩后:我们可以看到这里的tomcat目录下的一些文件!注意:这里的tomcat并不需要真正的进行安装,我们只需要下载下来解压缩后即可!我们就将这个服务器下载到了本地了!使用介绍我们来介绍一下每个目录文件对应的含义!bin这里的bin目录下打开bin目录可以看到一下形如.bat后缀和sh后缀的文件!.bat后缀文件:Windows批处理可执行文件,双击即可运行!.sh后缀文件:Linux下可执行文件!我们刚刚说了这里的tomcat和java一样支持跨平台,所以有不同平台的指令!例如:等下我们就需要通过startup.bat批处理指令打开tomcat服务器!confconf目录下保存了tomcat的一些配置文件!这里就相当于,tomcat的指挥中心,这里都可以配置设置!例如:我们打开server.xml就可以配置我们tomcat服务器的端口号!liblib目录保存了tomcat下的一些jar包,就是一些指令啥的就类似于jdk中的lib一样保存了一些指令!logslogs目录保存了tomcat服务器下的一些日志,就是一些运行细节记录!通过排查这里的日志我们就可以找到在tomcat上部署的项目出现的错误啥的!webappswebapps目录,这个目录用于保存我们在tomcat上附属的项目!后面我们会将项目打包放在该路径下,也就部署到了tomcat服务器!还有就是这个目录,可以自动将war包(压缩包)自动解压缩!啥是war包? 就是我们java项目打包后可以生成一个war报!复制到这个目录会自动解压缩,生成项目文件!启动环境变量配置&端口检查我们通过刚刚的bin目录下的startup.bat就可以启动tomcat服务器了!如果启动成功!这可窗口会出现该字样!失败会闪退!如果闪退了,说明我们tomcat配置环境变量没有配置成功!我们这时可以将startup.bat指令拉到cmd窗口下执行,就可以查看到报错信息!通过这里的报错信息配置好环境变量!这些都是我们需要配置好的环境变量,我们打开计算机属性找到环境变量进行配置!我们的tomcat下载后会尝试帮我们配置好环境变量,当时并不一定都能配置好,所以有时候需要我们自己手动进行配置!如果环境变量啥的都没有问题再次启动服务器!如果还是没用,我们再检查一下端口号是否被占用!我们tomcat的端口号默认是8080和8085如果这两个端口没有其他进程绑定,我们的tomcat才能够其他成功!我们可以通过cmd下输入netstat -ano | findstr 8080通过这个指令就可以查看端口是否被其他进程占用,如果这里不是TCP字样,说明端口被占用,我们可以改变其他进程端口,或者通过conf目录下的配置文件改变tomcat的默认端口!我们知道tomcat是一个服务器,我们启动成功后,那我们就可以通过浏览器进行访问了!我们通过访问本地主机下的tomcat服务器!ip地址就是我们熟悉的主机ip环回ip,127.0.0.1或者localhost,加上端口号8080!如果我们出现上面界面,就说明我们的服务器启动成功,这个界面是tomcat下的欢迎界面!使用我们也可以将自己的项目部署在该服务器下,通过这种方式访问!这个欢迎界面的代码就在webapps目录的ROOT路径下!我们可以将自己的html网页放在该路径下,进行访问!我们只需要在端口号后面跟上我们的路径/test.htm在根目录下的test.html文件!如果我们的项目有很多文件呢?显然刚刚那样直接复制到根路径下并不好,我们可以将我们的项目直接拷贝到webapps路径下即可!然后后面跟上路径即可访问!例如我们的博客系统,我们可以放在webapps下就可以进行访问了!这就访问到了我们的博客系统!
HTTPS我们刚刚学完HTTP,咋有来了一个HTTPS呀,这和HTTP有啥关系么?我们知道HTTP是用户层协议,是由业界大佬编写的协议模板供我们使用,我们来看看 HTTP存在的问题!当我们客户端用户要下载一个天天动听app时,因为我们网络传输数据要经过很多中间设备,其中就肯定要经过运营商设备,毕竟这些网络设施都是由他们搞的!当你的http请求通过他们的设备时,他们就可以拿到你的请求,你本来是想下载天天动听的,他就给你返回了一个QQ浏览器,挣你流量,恰烂钱!我们如何避免这个信息暴露问题呢?我们知道这里最大的问题就是除了服务器和客户端中间网络传输设备都可以读取到里面的数据内容,就好比传纸条!我们可以对数据加密!SSL/TLS我们就将HTTP进行了升级,引入了SSL加密层!这就是我们的HTTPS!加密方式:1.对称加密!2.非对称加密!对称加密如何对数据进行加密呢?明文: 就是我将我们的数据不进行任何处理直接传输!密文: 密文通过某种加密方式(密钥)将明文进行加密后传输!密钥: 通过这里的密钥可以将明文和密文进行转换!我们可以联系实际生活,这里的密钥就相当于钥匙,通过钥匙我们的客户端和服务器都可以对锁进行解锁操作!我们的HTTP也是通过这里的密钥进行加密!对称加密就是通过一个密钥将数据报进行加密操作,然后客户端话服务器通过这个共有的秘钥可以对数据报进行加密和解密!这里通过将密钥,双方共享,服务器和客户端就可以都数据报进行加密解密操作!密钥传输现在又有一个问题就是我们的秘钥也是需要进行网络传输的,也就会被中间有些设备拿到!中间设备拿到后,也可以对数据进行解密操作,所以白加密了!还有就是这里密钥肯定要每一台客户端拥有不一样的密钥,然后将密钥给服务器!如果大家共用一个密钥的话,你想知道密钥开一台客户端就好了!非对称加密对称加密存在的问题就是密钥会被其他设备拿到!我们可不可以对密钥进行加密就解决了这个问题!这时就引入了非对称加密方式!!非对称加密就是套娃!我们再给密钥加上一把锁!这里就引入了私钥和公钥:公钥:服务器和客户端大家共享的密钥,通过这个密钥可以对对称密钥进行加密!私钥:只有自己知道的密钥,通过这个密钥可以对对称密钥进行解密操作!这里我们的公钥,服务器和客户端都会生成一个公钥,然后将公钥发出去,然后大家都拿到了这个公钥,然后私钥留着!然后当服务器和客户端生成一个对称密钥后,会将该对称密钥用这个公钥进行加密后发送,然后我们拿到数据报和加密后的对称密钥后,通过私钥对已经加密后的对称密钥进行解密,拿到对称密钥,然后通过对称密钥对数据报进行解密操作!可能看完有点头大,就类似一个信箱,我们给邮递员一把锁(公钥),然后邮递员将信(对称密钥)放入信箱后,将信箱锁上(通过公钥对对称密钥进行加密),然后用户通过钥匙(私钥)将信拿到!我们一开始将公钥发出去,如果要进行对称密钥传输,就应该事先通过公钥对对称密钥进行加密后再进行传输!而这时中间设备拿到加密后的对称密钥和公钥也无济于事!公钥只能用来加密操作,不能解密!然后我们的另一方拿到加密后的对称密钥后通过自己私有的私钥对对称密钥进行解密!然后就可以对数据报文进行解密操作了!重点就是: 公钥(发出去)只能对对称密钥进行加密,私钥自己留着,用来对对称密钥解密!难道这样就安全了嘛?如果有中间设备自己搞个公钥和私钥冒充会怎么样?上面的过程就是中间人攻击,就是被黑客入侵的网络设备,会自己生成假冒公钥和私钥,黑客收到服务器发来的公钥后保存,将自己的公钥发个客户端,然后客户端将对称密钥用黑客的公钥加密,发个黑客,黑客通过自己的私钥解密,然后通过服务器的公钥进行加密发给服务器,如假包换!!!中间人攻击如何解决呢?这里存在的问题就是我们无法知道公钥到底是冒充的还是真的!只要我们可以确定公钥的真实性,就可以解决这个问题!我们联系实际生活,我们是如何确定一个人的身份呢?我们可以通过这个人的身份证!身份证可以造假,就想公钥可以冒充一样,但是我们可以通过身份证在公安区登记的信息,就可以知道这个身份证的真实性!所以我们这里也是如此,我们引入了第三平台,通过这个第三平台给的证书,就可以保证这里公钥的真实性!我们通过这个公证机构就可以避免中间人攻击了当创建一个服务器后就去这个公证机构登记(类似人一出生就去登记信息),然后将这个公钥数据放在这个证书里发送(类似身份证),客户端拿到证书后去公证机构验明身份!然后再使用加密操作!这里天天访问公证机构有点繁琐,使用在操作系统内部就有这个公证机构的认证方式,就可以进行本地认证!
构造HTTP请求方式我们通过对HTTP请求协议报头格式的学习知道了,我们可以通过3种基本方式去构造HTTP请求!而请求是在客户端构造的也就是属于前端的工作!所以这3种构造请求的方式,大部分是通过前端代码来实现的,不过我们也可以通过java代码基于socket来实现!基于HTML/JS实现构造!基于form表单基于ajax基于java代码实现构造!基于socketform表单HTML中的form表单就可以构造HTTP请求!我们通过下面代码进行学习!<!--form表单的形式构造http请求--> <form action="https://www.csdn.net/" method="get"> <!--action 存放需要请求要提交的服务器url,method请求提交的的方法! --> <input type="text" name="name"> <!--这里通过input标签实现--> <input type="password" name="password"> <!--这里的name是一个特殊的属性,这是的name保存了键值对中的key,专门用于http构造请求的!--> <input type="submit"> <!--提交按钮--> </form>表单标签- <form></form>这个标签允许用户输入一些信息提交到服务器中!其中里面可以存放用户交互的组件描述了要把数据按照什么方式, 提交到哪个页面中.所以我们通过form表单标签就可以编写一个http请求!<form action="https://www.csdn.net/" method="get">这里通过键值对的形式 key action保存的值是我们需要请求服务器的urlmethod保存我们通过那种方法进行请求这里的方法只能是post和get我们知道post请求中要传输的数据保存在body中get方法请求要传输的数据保证在url中的querystring查询字符串中!这里我们通过get方法进行构造,也就是说等下会通过querystring等方式给服务器传输数据!inpt标签我们知道form标签中的组件就可以用于客户端和服务器进行交互!我们通过input标签!type属性,这个属性的取值代表这个input具有不同含义!type=text这是一个普通文本框!type=password这是一个用于提交密码的文本框(输入内容不可见)type=submit提交表单按钮,我们最终要将我们构造的http请求提交到服务器上!<input type="text" name="name"> <input type="password" name="password">这里的name属性保存的就是querystring查询字符串中键值对中的key!我们在输入框中输入的数据就对应了该name属性下的value值我们提交后就会在查询字符串中保存!我们向服务器提交表单后,就构造了一个get方法的http请求!我们发现我们已经跳转到了CSDN的网站上!因为我们提交的服务器url就是C站的首页!我们通过fiddler抓包,看我们构造的请求和服务器给我们返回的响应是怎样的!请求GET https://www.csdn.net/?name=bug%E9%83%AD&password=666666 HTTP/1.1 Host: www.csdn.net User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Connection: keep-alive Cookie: uuid_tt_dd=10_30601611580-1653459529558-297725; log_Id_pv=846; Hm_lvt_6bcd52f51e9b3dce32bec4a3997715ac=1655356530,1655382747,1655518119,1655547417; Hm_up_6bcd52f51e9b3dce32bec4a3997715ac=%7B%22islogin%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isonline%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isvip%22%3A%7B%22value%22%3A%220%22%2C%22scope%22%3A1%7D%2C%22uid_%22%3A%7B%22value%22%3A%22weixin_52345071%22%2C%22scope%22%3A1%7D%7D; Hm_ct_6bcd52f51e9b3dce32bec4a3997715ac=6525*1*10_30601611580-1653459529558-297725!5744*1*weixin_52345071; log_Id_view=1489; __51uvsct__JQTDiOVZ2pRjGa1K=53; __51vcke__JQTDiOVZ2pRjGa1K=1e84abb9-62df-5ecc-828b-e3052ffe5150; __51vuft__JQTDiOVZ2pRjGa1K=1653459532473; log_Id_click=1057; __gads=ID=f159decf46255d5e-22bbe38364d300d7:T=1653459564:RT=1653459564:S=ALNI_Ma5D0b1QWC6YQ8t-on_R6zAurFD0g; __gpi=UID=000005b633162c48:T=1653459564:RT=1655010298:S=ALNI_Mbd0__O7WINiqjmFo0HJe78mx_xOA; ssxmod_itna=eqUxBDcDu0D=34Bake5DkF6diKbMk00fOexGXhIYDZDiqAPGhDC34UeDIYGCYRRrBe+C3jjBRoqH8m4WT4YKC4I0frQ4B3DEx0=PCjeKiinDCeDIDWeDiDGR7D=xGYDj0KGWD4qDOD3qGyS+=Di8t9DdvC7uQDmTNDGup6D7QDIw6g9frVAeDSW7UxKG=DjubD/4xWHeRWH=5DbgeuDeiDtqD9lw=Dbfd3x0pymkU7wGwIbt4US+NDxBtQExDf7kGC4t5D9h6IOfkD0wEY9xpxWiez8AYRgY5NBietW7ePBg+Ci0Dz4+Dd35DuxDG4ka75qiDD==; ssxmod_itna2=eqUxBDcDu0D=34Bake5DkF6diKbMk00f2DA=uxPtD/K3KDFODxDIg8qqFGFB+oQwiXsYI1EvQw7bKMRj1/+6eoNOKGbng2INO81fi6zL/uMizdaYIy8Dg0Z9bnV80FIZXBPsk1hIg=n=R0n=VW7Q5+IQ3RbsCx9C=gvG2m5bN/y=qFOwqKyQG8v4=B7AYSO+lPqpUC3bRn8IazEFbzT6GgctB3SfmO8GDUSh7RSf/OQhrPdAPk8fRzjITkCRxdKw+OHwVQYb/oIrCR+yC6KgAucT6oFi6ueM/6w10qzQbZHZ68tTRmVimrsWGL+aE/ri7Rh/FgIrLm2OQPhfmmDuuDqhW914PEnRcQnKuAEKeuzPnFpYxqg0ATFWQmwx82Ie+oD07cGPD7=DYF=eD===; UserName=weixin_52345071; UserInfo=26ede6cf39e94eab9c6265a032aa59fa; UserToken=26ede6cf39e94eab9c6265a032aa59fa; UserNick=bug+%E9%83%AD; AU=5FF; UN=weixin_52345071; BT=1653462761522; p_uid=U010000; c_dl_prid=1654675272560_674132; c_dl_rid=1655261165060_497667; c_dl_fref=https://so.csdn.net/so/search; c_dl_fpage=/download/weixin_41174502/12247848; c_dl_um=distribute.pc_relevant.none-task-blog-2%7Edefault%7Ebaidujs_utm_term%7Edefault-0-109910474-blog-119543816.pc_relevant_antiscanv2; management_ques=1653738813836; historyList-new=%5B%22pv%E4%BF%A1%E5%8F%B7%E9%87%8F%20%E4%BA%92%E6%96%A5%E5%92%8C%E5%90%8C%E6%AD%A5%22%5D; BS_coupon=20220618; csrfToken=e_t4KDVKgBezzgQgdwsnos36; c_pref=default; c_ref=default; c_first_ref=default; c_first_page=https%3A//www.csdn.net/%3Fname%3Dbug%25E9%2583%25AD%26password%3D6666666; c_segment=13; dc_tos=rdo8dm; Hm_lpvt_6bcd52f51e9b3dce32bec4a3997715ac=1655552363; dc_sid=6f420d07abdfa80dfbad71a91b5f6c41; __vtins__JQTDiOVZ2pRjGa1K=%7B%22sid%22%3A%20%22ded005ac-68ee-549d-989b-5d0fa7f4a0a1%22%2C%20%22vd%22%3A%201%2C%20%22stt%22%3A%200%2C%20%22dr%22%3A%200%2C%20%22expires%22%3A%201655554164935%2C%20%22ct%22%3A%201655552364935%7D; c_hasSub=true; c_utm_source=702048761; logo_advert_close=1; c_page_id=default; dc_session_id=10_1655552360673.249126; c_dsid=11_1655552362461.267119 Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: cross-site Sec-Fetch-User: ?1可以看到这里的请求和我们预想构造的一样!响应HTTP/1.1 200 OK Server: openresty Date: Sat, 18 Jun 2022 11:44:14 GMT Content-Type: text/html; charset=utf-8 Connection: keep-alive Keep-Alive: timeout=20 Vary: Accept-Encoding X-Response-Time: 376 x-xss-protection: 1; mode=block x-content-type-options: nosniff x-download-options: noopen x-readtime: 376 Strict-Transport-Security: max-age=31536000 Content-Length: 520755 <!doctype html><html lang="zh" data-server-rendered="true"><head><title>CSDN - 专业开发者社区</title> <meta name="keywords" content="CSDN博客,CSDN学院,CSDN论坛,CSDN直播"> <meta name="description" content="CSDN是全球知名中文IT技术交流平台,创建于1999年,包含原创博客、精品问答、职业培训、技术论坛、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区."> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <meta name="referrer" content="always"> <!----> <!----> <!----> <script src="https://g.csdnimg.cn/tingyun/tingyun.js"></script> <!----> <!----> <!----> <link rel="shortcut icon" href="https://g.csdnimg.cn/static/logo/favicon32.ico" type="image/x-icon"> <link rel="canonical" href="https://www.csdn.net"> <!----> <meta name="toolbar" content={"type":"0","fixModel":"1"} /> <meta name="report" content={"spm":"1000.2115"} /> <script src="https://g.csdnimg.cn/??lib/jquery/1.12.4/jquery.min.js,user-tooltip/2.2/user-tooltip.js,lib/qrcode/1.0.0/qrcode.min.js"></script> <script src='//g.csdnimg.cn/common/csdn-report/report.js' type='text/javascript'></script> <script src="https://g.csdnimg.cn/user-ordercart/2.0.1/user-ordercart.js?ts=2.0.1"></script> <!----> <script src="https://g.csdnimg.cn/common/csdn-login-box/csdn-login-box.js"></script> <script src="https://g.csdnimg.cn/user-ordertip/3.0.2/user-ordertip.js?t=3.0.2"></script> <!----> <!----> <!----> <!----> <script> window.TINGYUN && window.TINGYUN.init && window.TINGYUN.init(function (ty_rum) { ty_rum.server = { "event_timeout": 60000, "dr_threshold": 4000, "opt_custom_param_rule": [], "cross_page_delay": 3000, "router_enable": true, "fp_threshold": 2000, "token": "568934913a6343de840a781ca5eaba4b", "beacon": "wkbrs1.tingyun.com", "trace_threshold": 7000, "x_server_switch": true, "ignore_err": false, "id": "hWg-u0rE5b8", "key": "Z1Tu5hoKbGw", "fs_threshold": 4000 }; }); </script> <!----> <script src="https://g.csdnimg.cn/common/csdn-toolbar/csdn-toolbar.js"></script> <link rel="stylesheet" href="https://csdnimg.cn/release/cmsfe/public/css/common.ee34ee2e.css"><link rel="stylesheet" href="https://csdnimg.cn/release/cmsfe/public/css/tpl/www-index-new/index.9322d3e1.css"></head> <body><div id="toolbarBox" style="min-height: 48px;"></div> <div id="app"><div><div class="main"><div class="page-container page-component"><div><div class="home_wrap"><div class="content_wrap"><div id="floor-nav_557" floor-index="0"><div comp-data="[object Object]" floor-data="[object Object]" class="blog-nav-tag" data-v-f8e9e086><div class="blog-nav " data-v-f8e9e086><img src="https://img-home.csdnimg.cn/images/20220107105619.png" alt class="blog-nav-down " data-v-f8e9e086> <div class="blog-nav-box" data-v-f8e9e086><ul class="def" data-v-f8e9e086><!----> <!----> <!----> <!----> <li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/back-end" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;back-end&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;back-end&quot;}" data-v-f8e9e086>后端</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/web" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;web&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;web&quot;}" data-v-f8e9e086>前端</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/mobile" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;mobile&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;mobile&quot;}" data-v-f8e9e086>移动开发</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/lang" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;lang&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;lang&quot;}" data-v-f8e9e086>编程语言</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/java" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;java&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;java&quot;}" data-v-f8e9e086>Java</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/python" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;python&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;python&quot;}" data-v-f8e9e086>Python</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/ai" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ai&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ai&quot;}" data-v-f8e9e086>人工智能</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/big-data" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;big-data&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;big-data&quot;}" data-v-f8e9e086>大数据</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/algo" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;algo&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;algo&quot;}" data-v-f8e9e086>数据结构与算法</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/avi" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;avi&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;avi&quot;}" data-v-f8e9e086>音视频</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/cloud-native" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud-native&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud-native&quot;}" data-v-f8e9e086>云原生</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/cloud" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud&quot;}" data-v-f8e9e086>云平台</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/ops" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ops&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ops&quot;}" data-v-f8e9e086>运维</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/server" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;server&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;server&quot;}" data-v-f8e9e086>服务器</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/os" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;os&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;os&quot;}" data-v-f8e9e086>操作系统</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/db-management" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;db-management&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;db-management&quot;}" data-v-f8e9e086>数据库管理</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/ios" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ios&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ios&quot;}" data-v-f8e9e086>iOS</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/android" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;android&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;android&quot;}" data-v-f8e9e086>Android</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/miniprog" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;miniprog&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;miniprog&quot;}" data-v-f8e9e086>小程序</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/hardware" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;hardware&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;hardware&quot;}" data-v-f8e9e086>硬件开发</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/embedded" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;embedded&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;embedded&quot;}" data-v-f8e9e086>嵌入式</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/microsoft" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;microsoft&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;microsoft&quot;}" data-v-f8e9e086>微软技术</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/software-engineering" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;software-engineering&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;software-engineering&quot;}" data-v-f8e9e086>软件工程</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/test" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;test&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;test&quot;}" data-v-f8e9e086>测试</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/sec" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;sec&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;sec&quot;}" data-v-f8e9e086>安全</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/internet" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;internet&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;internet&quot;}" data-v-f8e9e086>网络</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/product-ops" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;product-ops&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;product-ops&quot;}" data-v-f8e9e086>产品/运营</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/design" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;design&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;design&quot;}" data-v-f8e9e086>设计</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/job" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;job&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;job&quot;}" data-v-f8e9e086>职场和发展</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/search" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;search&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;search&quot;}" data-v-f8e9e086>搜索</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/devtools" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;devtools&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;devtools&quot;}" data-v-f8e9e086>开发工具</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/php" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;php&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;php&quot;}" data-v-f8e9e086>PHP</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/game" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;game&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;game&quot;}" data-v-f8e9e086>游戏</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/open" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;open&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;open&quot;}" data-v-f8e9e086>开放平台</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/harmonyos" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;harmonyos&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;harmonyos&quot;}" data-v-f8e9e086>HarmonyOS</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/blockchain" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;blockchain&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;blockchain&quot;}" data-v-f8e9e086>区块链</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/math" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;math&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;math&quot;}" data-v-f8e9e086>数学</a></li> <li class="blog-nav-up" data-v-f8e9e086><img src="https://img-home.csdnimg.cn/images/20220107105622.png" alt data-v-f8e9e086></li></ul></div></div> <!----></div></div><div id="floor-www-index_558" floor-index="1"><div comp-data="[object Object]" pageType="www" class="www-home-top"><div class="wart"></div> <div class="www-home-content"><div id="kp_box_ww9877"><!----></div></div> <div class="www-home-content active"><div floorData="[object Object]" class="headlines"><div class="headlines-left" data-v-e8da5228><div class="top-title" data-v-e8da5228><img src="https://img-home.csdnimg.cn/images/20220107104621.png" alt data-v-e8da5228> <h3 data-v-e8da5228>头条</h3></div> <dl data-v-e8da5228><dt data-v-e8da5228><a target="_blank" data-report-query="spm=1000.2115.3001.5926" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/programmer_editor/article/details/125311214&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/programmer_editor/article/details/125311214&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" href="https://blog.csdn.net/programmer_editor/article/details/125311214" data-v-e8da5228><img src="https://img-home.csdnimg.cn/images/20220616110341.jpg" alt data-v-e8da5228></a></dt> <dd class="desc" data-v-e8da5228><a data-report-query="spm=1000.2115.3001.5926" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/programmer_editor/article/details/125311214&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/programmer_editor/article/details/125311214&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/programmer_editor/article/details/125311214" data-v-e8da5228>90 后 CTO 如何成为国内首位女性 Apache Member?</a></dd> <dd class="desc-text-a" data-v-e8da5228><a data-report-query="spm=1000.2115.3001.5926" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/programmer_editor/article/details/125311214&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/programmer_editor/article/details/125311214" data-v-e8da5228>从DBA,到内核开发者;从一线研发人员,到开源商业公司 CTO。</a></dd> <!----></dl></div> <div class="headlines-right"><div class="headswiper" data-v-0045335f><div class="headswiper-top" data-v-0045335f><div class="top-title" data-v-0045335f><!----> <h3 data-v-0045335f></h3></div> <p data-v-0045335f><img src="https://img-home.csdnimg.cn/images/20220107104919.png" alt data-v-0045335f> <!----> <img src="https://img-home.csdnimg.cn/images/20220107104954.png" alt data-v-0045335f> <!----></p></div> <div class="headswiper-content" data-v-0045335f><div data-v-0045335f><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/ucanuup_/article/details/125328242&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/ucanuup_/article/details/125328242&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/ucanuup_/article/details/125328242" class="title" data-v-0045335f>Apache Doris成为Apache顶级项目</a> <a target="_blank" href="https://blog.csdn.net/ucanuup_/article/details/125328242" data-v-0045335f><p class="name" data-v-0045335f>历时4年</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://csdnnews.blog.csdn.net/article/details/125326095?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://csdnnews.blog.csdn.net/article/details/125326095?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://csdnnews.blog.csdn.net/article/details/125326095?spm=1001.2014.3001.5502" class="title" data-v-0045335f>坐拥755万开发者的中国开源,进度几何?</a> <a target="_blank" href="https://csdnnews.blog.csdn.net/article/details/125326095?spm=1001.2014.3001.5502" data-v-0045335f><p class="name" data-v-0045335f>最大的阻力是什么</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnnews/article/details/125325839&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnnews/article/details/125325839&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnnews/article/details/125325839" class="title" data-v-0045335f>MIT曝光 M1 不可修复的漏洞</a> <a target="_blank" href="https://blog.csdn.net/csdnnews/article/details/125325839" data-v-0045335f><p class="name" data-v-0045335f>网友:时间挺巧,是时候换 M2 了!</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnnews/article/details/125320871&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnnews/article/details/125320871&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnnews/article/details/125320871" class="title" data-v-0045335f>苹果设计团队成员纷纷离职</a> <a target="_blank" href="https://blog.csdn.net/csdnnews/article/details/125320871" data-v-0045335f><p class="name" data-v-0045335f>因危险发言,到手的股权飞了</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnopensource/article/details/125320885?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnopensource/article/details/125320885?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnopensource/article/details/125320885?spm=1001.2014.3001.5502" class="title" data-v-0045335f>微软将闭源VS Code中的C#扩展</a> <a target="_blank" href="https://blog.csdn.net/csdnopensource/article/details/125320885?spm=1001.2014.3001.5502" data-v-0045335f><p class="name" data-v-0045335f>惹开发者怒怼</p></a></div></div></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div></div> <div class="headswiper" data-v-0045335f><div class="headswiper-top" data-v-0045335f><div class="top-title" data-v-0045335f><img src="https://img-home.csdnimg.cn/images/20220107104836.png" alt data-v-0045335f> <h3 data-v-0045335f>热点</h3></div> <p data-v-0045335f><img src="https://img-home.csdnimg.cn/images/20220107104919.png" alt data-v-0045335f> <!----> <img src="https://img-home.csdnimg.cn/images/20220107104954.png" alt data-v-0045335f> <!----></p></div> <div class="headswiper-content" data-v-0045335f><div data-v-0045335f><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Byeweiyang/article/details/125346440&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Byeweiyang/article/details/125346440&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/Byeweiyang/article/details/125346440" class="title" data-v-0045335f>历史上的今天:京东诞生</a> <a target="_blank" href="https://blog.csdn.net/Byeweiyang/article/details/125346440" data-v-0045335f><p class="name" data-v-0045335f>Facebook发布Libra白皮书</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnsevenn/article/details/125326495&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnsevenn/article/details/125326495&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnsevenn/article/details/125326495" class="title" data-v-0045335f>网传一公司成立“专管00后部门”</a> <a target="_blank" href="https://blog.csdn.net/csdnsevenn/article/details/125326495" data-v-0045335f><p class="name" data-v-0045335f>00后整顿职场反被整顿</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdngeeknews/article/details/125327652?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdngeeknews/article/details/125327652?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdngeeknews/article/details/125327652?spm=1001.2014.3001.5502" class="title" data-v-0045335f>京东考虑进入外卖行业</a> <a target="_blank" href="https://blog.csdn.net/csdngeeknews/article/details/125327652?spm=1001.2014.3001.5502" data-v-0045335f><p class="name" data-v-0045335f>特斯拉加速裁员|极客头条</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://bbs.csdn.net/topics/607041278?utm_source=454650209&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://bbs.csdn.net/topics/607041278?utm_source=454650209&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://bbs.csdn.net/topics/607041278?utm_source=454650209" class="title" data-v-0045335f>Ethereum中文社区征文活动</a> <a target="_blank" href="https://bbs.csdn.net/topics/607041278?utm_source=454650209" data-v-0045335f><p class="name" data-v-0045335f>稿酬、证书、周边礼包等你来拿</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnsevenn/article/details/125308744&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnsevenn/article/details/125308744&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnsevenn/article/details/125308744" class="title" data-v-0045335f>让程序员崩溃的微信群消息置顶</a> <a target="_blank" href="https://blog.csdn.net/csdnsevenn/article/details/125308744" data-v-0045335f><p class="name" data-v-0045335f>被吐槽上热搜</p></a></div></div></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div></div></div></div> <div class="www-home-Broadcast"><div class="broadcast" data-v-6ebf2afa><div class="title" data-v-6ebf2afa><div class="top-title" data-v-6ebf2afa><img src="https://img-home.csdnimg.cn/images/20220107105446.png" alt data-v-6ebf2afa> <h3 data-v-6ebf2afa>直播</h3></div> <a target="_blank" href="https://live.csdn.net/?spm=1000.2115.3001.4124" data-v-6ebf2afa>更多<i class="el-icon-arrow-right" data-v-6ebf2afa></i></a></div> <div class="content" data-v-6ebf2afa><div class="www_live_item active" data-v-34c9c026 data-v-6ebf2afa><a href="https://live.csdn.net/room/homeofkernel/Chbe47Ff" data-report-query="utm_medium=distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-Chbe47Ff-null-null.nonecase&amp;depth_1-utm_source=distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-Chbe47Ff-null-null.nonecase&amp;spm=1000.2115.3001.5950" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/homeofkernel/Chbe47Ff&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-Chbe47Ff-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/homeofkernel/Chbe47Ff&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-Chbe47Ff-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-v-34c9c026><div class="www_live_item_top" data-v-34c9c026><div class="img" style="background-image:url(https://live-file.csdnimg.cn/release/live/file/1655392155583.png?x-oss-process=image/resize,l_800);" data-v-34c9c026></div> <img src="https://csdnimg.cn/release/cmsfe/public/img/nowlive.480a0975.gif" alt class="live-type" data-v-34c9c026> <!----> <!----> <span class="live-text" data-v-34c9c026>正在直播</span> <div class="back" data-v-34c9c026></div> <!----> <div class="hover_mask" data-v-34c9c026><img src="https://csdnimg.cn/release/cmsfe/public/img/play.9956ea53.png" alt data-v-34c9c026></div></div> <div class="content-text" data-v-34c9c026><h3 data-v-34c9c026> eBPF 工作原理浅析 </h3> <!----> <span class="text" data-v-34c9c026>228 热度</span></div></a></div><div class="www_live_item active" data-v-34c9c026 data-v-6ebf2afa><a href="https://live.csdn.net/room/hzbooks/KdnWX3bU" data-report-query="utm_medium=distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-2-KdnWX3bU-null-null.nonecase&amp;depth_1-utm_source=distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-2-KdnWX3bU-null-null.nonecase&amp;spm=1000.2115.3001.5950" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/hzbooks/KdnWX3bU&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-2-KdnWX3bU-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/hzbooks/KdnWX3bU&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-2-KdnWX3bU-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-v-34c9c026><div class="www_live_item_top" data-v-34c9c026><div class="img" style="background-image:url(https://live-file.csdnimg.cn/release/live/file/1654685869603.png?x-oss-process=image/resize,l_800);" data-v-34c9c026></div> <img src="https://csdnimg.cn/release/cmsfe/public/img/nowlive.480a0975.gif" alt class="live-type" data-v-34c9c026> <!----> <!----> <span class="live-text" data-v-34c9c026>正在直播</span> <div class="back" data-v-34c9c026></div> <!----> <div class="hover_mask" data-v-34c9c026><img src="https://csdnimg.cn/release/cmsfe/public/img/play.9956ea53.png" alt data-v-34c9c026></div></div> <div class="content-text" data-v-34c9c026><h3 data-v-34c9c026> Java核心技术大会2022——Java工程师个人成长与职场修炼 </h3> <!----> <span class="text" data-v-34c9c026>630 热度</span></div></a></div><div class="www_live_item active" data-v-34c9c026 data-v-6ebf2afa><a href="https://live.csdn.net/room/MicrosoftReactor/IfKAano1" data-report-query="utm_medium=distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-3-IfKAano1-null-null.nonecase&amp;depth_1-utm_source=distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-3-IfKAano1-null-null.nonecase&amp;spm=1000.2115.3001.5950" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/MicrosoftReactor/IfKAano1&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-3-IfKAano1-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/MicrosoftReactor/IfKAano1&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-3-IfKAano1-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-v-34c9c026><div class="www_live_item_top" data-v-34c9c026><div class="img" style="background-image:url(https://live-file.csdnimg.cn/release/live/file/1653380283401.png?x-oss-process=image/resize,l_800);" data-v-34c9c026></div> <!----> <img src="https://csdnimg.cn/release/cmsfe/public/img/livemake.ed2b6426.png" alt class="live-type" data-v-34c9c026> <span class="live-text" data-v-34c9c026>直播预约</span> <!----> <div class="back" data-v-34c9c026></div> <!----> <div class="hover_mask" data-v-34c9c026><img src="https://csdnimg.cn/release/cmsfe/public/img/play.9956ea53.png" alt data-v-34c9c026></div></div> <div class="content-text" data-v-34c9c026><h3 data-v-34c9c026> Build Skills | 使用Power BI 创建和分析报表 </h3> <span class="text" data-v-34c9c026>06/22 19:30</span> <!----></div></a></div><div class="www_live_item active" data-v-34c9c026 data-v-6ebf2afa><a href="https://live.csdn.net/room/MicrosoftReactor/HjnfRbQ6" data-report-query="utm_medium=distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-4-HjnfRbQ6-null-null.nonecase&amp;depth_1-utm_source=distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-4-HjnfRbQ6-null-null.nonecase&amp;spm=1000.2115.3001.5950" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/MicrosoftReactor/HjnfRbQ6&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-4-HjnfRbQ6-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/MicrosoftReactor/HjnfRbQ6&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-4-HjnfRbQ6-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654440_32677\&quot;}&quot;}" data-v-34c9c026><div class="www_live_item_top" data-v-34c9c026><div class="img" style="background-image:url(https://live-file.csdnimg.cn/release/live/file/1653470595908.png?x-oss-process=image/resize,l_800);" data-v-34c9c026></div> <!----> <img src="https://csdnimg.cn/release/cmsfe/public/img/livemake.ed2b6426.png" alt class="live-type" data-v-34c9c026> <span class="live-text" data-v-34c9c026>直播预约</span> <!----> <div class="back" data-v-34c9c026></div> <!----> <div class="hover_mask" data-v-34c9c026><img src="https://csdnimg.cn/release/cmsfe/public/img/play.9956ea53.png" alt data-v-34c9c026></div></div> <div class="content-text" data-v-34c9c026><h3 data-v-34c9c026> CA 训练营|.NET 下的人工智能 </h3> <span class="text" data-v-34c9c026>06/23 19:30</span> <!----></div></a></div></div></div></div> <!----></div> <div class="www-home"><div><div class="www-home-left"><div class="www-content"><ul class="www-content_top"><li style="outline:none"><span><div role="tooltip" id="el-popover-2371" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->您的关注</div><span class="el-popover__reference-wrapper"><p style="outline:none;">关注</p></span></span> <!----></li><li class="active" style="outline:none"><span><div role="tooltip" id="el-popover-5313" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->为您独家定制</div><span class="el-popover__reference-wrapper"><p style="outline:none;">推荐</p></span></span> <!----></li><li style="outline:none"><span><div role="tooltip" id="el-popover-4124" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->前沿 IT 资讯</div><span class="el-popover__reference-wrapper"><p style="outline:none;">资讯</p></span></span> <!----></li><li style="outline:none"><span><div role="tooltip" id="el-popover-3614" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->全站热门内容</div><span class="el-popover__reference-wrapper"><p style="outline:none;">热榜</p></span></span> <!----></li><li style="outline:none"><span><div role="tooltip" id="el-popover-3988" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->专家为您甄选</div><span class="el-popover__reference-wrapper"><p style="outline:none;">专家推荐</p></span></span> <!----></li> <li class="move"><a target="_blank" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5942&quot;}" href="https://blog.csdn.net/rank/list">更多<i class="el-icon-arrow-right"></i></a></li></ul> <div class="Community"><div class="active "><div class="Community-item-active blog"><!----> <!----> <a target="_blank" href="https://blog.csdn.net/qq_62294245/article/details/125288502" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;1&quot;,&quot;style&quot;:&quot;PIC_V2_21&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_62294245/article/details/125288502&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="Community-h-tag"><span class="blog-title" style="text-decoration: none !important">原力计划</span><span class="blog-text">【CSDN云VS腾讯云】要不然怎么说CSDN开发云是打工人和学生党的福音呢?</span></a> <div class="Community-item blog"><div class="content"><a target="_blank" href="https://blog.csdn.net/qq_62294245/article/details/125288502" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;1&quot;,&quot;style&quot;:&quot;PIC_V2_21&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_62294245/article/details/125288502&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" data-report-view="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;1&quot;,&quot;style&quot;:&quot;PIC_V2_21&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_62294245/article/details/125288502&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="blog"><p class="desc">CSDN开发云平台提供更丰富的开源项目及其文档让你快速上手更好、更实用、更优秀的解决方案,依托丰富的基础云服务,助力中小企业、个人开发者和学生群体一键上云</p></a> <div class="operation"><div><div class="operation-b"><p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023128.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023149.png" alt class="img-hover"> <span class="num">63赞</span></p> <p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023256.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023314.png" alt class="img-hover"> <span class="num">踩</span></p></div></div> <div class="operation-c"><a target="_blank" href="https://blog.csdn.net/qq_62294245"><span>小鹏linux</span></a> <span class="border"></span> <span class="color">我的关注</span> <div class="el-dropdown" style="margin-left:16px;"><p class="feedback"><img src="https://img-home.csdnimg.cn/images/20220117023327.png" alt="" class="dian"> <img src="https://img-home.csdnimg.cn/images/20220117023333.png" alt="" class="activeDian"></p> <ul class="el-dropdown-menu el-popper community-floor-popper" style="display:none;"><li tabindex="-1" class="el-dropdown-menu__item"><!---->内容质量差</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不感兴趣</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不喜欢该作者</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->内容重复</li></ul></div></div></div></div> <!----> <div class="right"><a target="_blank" href="https://blog.csdn.net/qq_62294245/article/details/125288502" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;1&quot;,&quot;style&quot;:&quot;PIC_V2_21&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_62294245/article/details/125288502&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}"><img alt="" class="img"></a></div> <!----> <!----></div></div> <!----> <!----> <!----> <!----> <!----></div><div class="active "><div class="Community-item-active blog"><!----> <!----> <a target="_blank" href="https://blog.csdn.net/qq_41250372/article/details/125266741" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;3&quot;,&quot;style&quot;:&quot;PIC_V2_23&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_41250372/article/details/125266741&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="Community-h-tag"><span class="blog-title" style="text-decoration: none !important">原力计划</span><span class="blog-text">【云原生】第五篇--Docker容器化部署企业级应用集群</span></a> <div class="Community-item blog"><div class="content"><a target="_blank" href="https://blog.csdn.net/qq_41250372/article/details/125266741" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;3&quot;,&quot;style&quot;:&quot;PIC_V2_23&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_41250372/article/details/125266741&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" data-report-view="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;3&quot;,&quot;style&quot;:&quot;PIC_V2_23&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_41250372/article/details/125266741&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="blog"><p class="desc">本文主要讲述Docker容器化部署企业级应用,具体包括使用Docker容器实现Nginx部署、Tomcat部署、Tomcat部署、Oracle部署、ElasticSearch+Kibana部署、Redis部署、RabbitMQ部署</p></a> <div class="operation"><div><div class="operation-b"><p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023128.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023149.png" alt class="img-hover"> <span class="num">28赞</span></p> <p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023256.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023314.png" alt class="img-hover"> <span class="num">踩</span></p></div></div> <div class="operation-c"><a target="_blank" href="https://blog.csdn.net/qq_41250372"><span>孙和龚</span></a> <span class="border"></span> <span class="color">我的关注</span> <div class="el-dropdown" style="margin-left:16px;"><p class="feedback"><img src="https://img-home.csdnimg.cn/images/20220117023327.png" alt="" class="dian"> <img src="https://img-home.csdnimg.cn/images/20220117023333.png" alt="" class="activeDian"></p> <ul class="el-dropdown-menu el-popper community-floor-popper" style="display:none;"><li tabindex="-1" class="el-dropdown-menu__item"><!---->内容质量差</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不感兴趣</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不喜欢该作者</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->内容重复</li></ul></div></div></div></div> <!----> <div class="right"><a target="_blank" href="https://blog.csdn.net/qq_41250372/article/details/125266741" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;3&quot;,&quot;style&quot;:&quot;PIC_V2_23&quot;,&quot;strategy&quot;:&quot;user_follow&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/qq_41250372/article/details/125266741&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}"><img alt="" class="img"></a></div> <!----> <!----></div></div> <!----> <!----> <!----> <!----> <!----></div><div class="active "><div class="Community-item-active ask"><!----> <!----> <a target="_blank" href="https://ask.csdn.net/questions/7744635" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;4&quot;,&quot;style&quot;:&quot;TEXT_V2_11&quot;,&quot;strategy&quot;:&quot;ask_personrec_tag&quot;,&quot;dest&quot;:&quot;https://ask.csdn.net/questions/7744635&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="Community-h-tag"><span class="blog-title" style="text-decoration: none !important">等你来答</span><span class="blog-text">liunx系统下vscode终端python版本和xshell终端python版本不对应</span></a> <div class="Community-item ask"><div class="content"><a target="_blank" href="https://ask.csdn.net/questions/7744635" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;4&quot;,&quot;style&quot;:&quot;TEXT_V2_11&quot;,&quot;strategy&quot;:&quot;ask_personrec_tag&quot;,&quot;dest&quot;:&quot;https://ask.csdn.net/questions/7744635&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" data-report-view="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-ask-ask_personrec_tag-4-7744635-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;4&quot;,&quot;style&quot;:&quot;TEXT_V2_11&quot;,&quot;strategy&quot;:&quot;ask_personrec_tag&quot;,&quot;dest&quot;:&quot;https://ask.csdn.net/questions/7744635&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="blog"><!----></a> <div class="operation"><div><div class="operation-b"><p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023128.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023149.png" alt class="img-hover"> <span class="num">0赞</span></p> <p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023256.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023314.png" alt class="img-hover"> <span class="num">踩</span></p></div></div> <div class="operation-c"><a target="_blank" href="https://blog.csdn.net/qq_38767074"><span>肥宅xiaofu</span></a> <!----> <!----> <div class="el-dropdown" style="margin-left:16px;"><p class="feedback"><img src="https://img-home.csdnimg.cn/images/20220117023327.png" alt="" class="dian"> <img src="https://img-home.csdnimg.cn/images/20220117023333.png" alt="" class="activeDian"></p> <ul class="el-dropdown-menu el-popper community-floor-popper" style="display:none;"><li tabindex="-1" class="el-dropdown-menu__item"><!---->内容质量差</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不感兴趣</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不喜欢该作者</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->内容重复</li></ul></div></div></div></div> <!----> <!----> <!----> <!----></div></div> <!----> <!----> <!----> <!----> <!----></div><div class="active "><div class="Community-item-active blog"><!----> <!----> <a target="_blank" href="https://blog.csdn.net/newlw/article/details/125326766" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;5&quot;,&quot;style&quot;:&quot;PIC_V2_13&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/newlw/article/details/125326766&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="Community-h-tag"><!----><span class="blog-text">基于Python实现的迷宫求解游戏设计 课程报告+代码及可执行文件</span></a> <div class="Community-item blog"><div class="content"><a target="_blank" href="https://blog.csdn.net/newlw/article/details/125326766" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;5&quot;,&quot;style&quot;:&quot;PIC_V2_13&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/newlw/article/details/125326766&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" data-report-view="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;5&quot;,&quot;style&quot;:&quot;PIC_V2_13&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/newlw/article/details/125326766&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="blog"><p class="desc">目录任务描述 21、 必做任务一 22、 必做任务二 33、 必做任务三 34、 选做任务一 3问题建模 41、迷宫 42、Q 表模型 43、监督学习模型 54、Q-learning 学习过程 55、加入时间因子 5算法设计和代码 61、迷宫类 62、Q 表模型类 6UI 设计和使用说明 61、已有迷宫界面 62、用户自定义 7总结 7任务描述1、必做任务一使用强化学习算法,对于给定的迷宫,训练老鼠在迷宫中寻找蛋糕。2、必做任务二自行生成不同迷宫(尺寸、地图),</p></a> <div class="operation"><div><div class="operation-b"><p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023128.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023149.png" alt class="img-hover"> <span class="num">2赞</span></p> <p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023256.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023314.png" alt class="img-hover"> <span class="num">踩</span></p></div></div> <div class="operation-c"><a target="_blank" href="https://blog.csdn.net/newlw"><span>biyezuopinvip</span></a> <!----> <!----> <div class="el-dropdown" style="margin-left:16px;"><p class="feedback"><img src="https://img-home.csdnimg.cn/images/20220117023327.png" alt="" class="dian"> <img src="https://img-home.csdnimg.cn/images/20220117023333.png" alt="" class="activeDian"></p> <ul class="el-dropdown-menu el-popper community-floor-popper" style="display:none;"><li tabindex="-1" class="el-dropdown-menu__item"><!---->内容质量差</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不感兴趣</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不喜欢该作者</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->内容重复</li></ul></div></div></div></div> <!----> <div class="right"><a target="_blank" href="https://blog.csdn.net/newlw/article/details/125326766" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-5-125326766-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;5&quot;,&quot;style&quot;:&quot;PIC_V2_13&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/newlw/article/details/125326766&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}"><img alt="" class="img"></a></div> <!----> <!----></div></div> <div class="information-www"><span><div role="tooltip" id="el-popover-2968" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->您的推荐流将在修改后10分钟发生变化</div><span class="el-popover__reference-wrapper"><a target="_blank" href="https://i.csdn.net/#/user-center/profile?floor=interest" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.6425&quot;}" class="information"><span>修改兴趣标签</span></a></span></span> <span class="information-border"></span> <span><div role="tooltip" id="el-popover-7364" aria-hidden="true" class="el-popover el-popper www-content-top-tooltip el-popover--plain" style="width:undefinedpx;display:none;"><!---->您的反馈和建议将有效推动推荐流的优化</div><span class="el-popover__reference-wrapper"><a target="_blank" href="https://bbs.csdn.net/forums/placard?typeId=1100" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.6425&quot;}" class="information"><span>反馈信息流问题</span></a></span></span></div> <!----> <!----> <!----> <!----></div><div class="active "><div class="Community-item-active blog"><!----> <!----> <a target="_blank" href="https://blog.csdn.net/Kenji_Shinji/article/details/125328437" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;6&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Kenji_Shinji/article/details/125328437&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="Community-h-tag"><!----><span class="blog-text">浅谈MindSpore的动态Shape</span></a> <div class="Community-item blog"><div class="content"><a target="_blank" href="https://blog.csdn.net/Kenji_Shinji/article/details/125328437" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;6&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Kenji_Shinji/article/details/125328437&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" data-report-view="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;6&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Kenji_Shinji/article/details/125328437&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="blog"><p class="desc">在MindSpore开发过程中,由于动态Shape算子的开发需求,再加上MindSpore的动态Shape也在持续完善,笔者遇到了框架上的一些问题。通过查看源码和相关文档的方式,获得了初步的解决方案和感悟。这篇博客主要是将当时的见闻加以整理,并给出一点点开发建议。此外,当时笔者还做了组内分享,点击“阅读原文”即可获取原文PPT。由于本人刚入职不久,本博客适合于初学者,高手轻喷。1、动态Shape的定义动态Shape,指的是Tensor的Shape依赖于具体的运算,无法提前通过计算得出。具体来说分两种情况:算</p></a> <div class="operation"><div><div class="operation-b"><p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023128.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023149.png" alt class="img-hover"> <span class="num">0赞</span></p> <p class="operation-b-img operation-b-img-active"><img src="https://img-home.csdnimg.cn/images/20220117023256.png" alt> <img src="https://img-home.csdnimg.cn/images/20220117023314.png" alt class="img-hover"> <span class="num">踩</span></p></div></div> <div class="operation-c"><a target="_blank" href="https://blog.csdn.net/Kenji_Shinji"><span>MSofficial</span></a> <!----> <!----> <div class="el-dropdown" style="margin-left:16px;"><p class="feedback"><img src="https://img-home.csdnimg.cn/images/20220117023327.png" alt="" class="dian"> <img src="https://img-home.csdnimg.cn/images/20220117023333.png" alt="" class="activeDian"></p> <ul class="el-dropdown-menu el-popper community-floor-popper" style="display:none;"><li tabindex="-1" class="el-dropdown-menu__item"><!---->内容质量差</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不感兴趣</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->不喜欢该作者</li> <li tabindex="-1" class="el-dropdown-menu__item"><!---->内容重复</li></ul></div></div></div></div> <!----> <div class="right"><a target="_blank" href="https://blog.csdn.net/Kenji_Shinji/article/details/125328437" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-6-125328437-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;6&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Kenji_Shinji/article/details/125328437&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}"><img alt="" class="img"></a></div> <!----> <!----></div></div> <!----> <!----> <!----> <!----> <!----></div><div class="active "><div class="Community-item-active blog"><!----> <!----> <a target="_blank" href="https://blog.csdn.net/gongdiwudu/article/details/125319593" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;7&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/gongdiwudu/article/details/125319593&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="Community-h-tag"><!----><span class="blog-text">python知识:循环嵌套</span></a> <div class="Community-item blog"><div class="content"><a target="_blank" href="https://blog.csdn.net/gongdiwudu/article/details/125319593" data-report-query="spm=1000.2115.3001.6382&amp;utm_medium=distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec&amp;depth_1-utm_source=distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec" data-report-click="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;7&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/gongdiwudu/article/details/125319593&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" data-report-view="{&quot;mod&quot;:&quot;popu_459&quot;,&quot;extra&quot;:&quot;{\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_v2.none-task-blog-personrec_tag-7-125319593-null-null.pc_personrec\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655552654436_96618\&quot;}&quot;,&quot;dist_request_id&quot;:&quot;1655552654436_96618&quot;,&quot;ab_strategy&quot;:&quot;default&quot;,&quot;index&quot;:&quot;7&quot;,&quot;style&quot;:&quot;PIC_V2_11&quot;,&quot;strategy&quot;:&quot;personrec_tag&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/gongdiwudu/article/details/125319593&quot;,&quot;spm&quot;:&quot;1000.2115.3001.6382&quot;}" class="blog"><p class="desc">循环在 python 中很重要,因为没有它们,我们将不得不\u002Ff08e02d0369cee8cc6a2145f8d96cbd3?utm_source=huodongrili\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-Headlines\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5928"}}],"www-slide-ad":[{"con":"\u003Cdiv id=\"kp_box_592\" data-pid=\"592\"\u003E\u003Ciframe class=\"J_adv\" data-swapimg=\"true\" src=\"https:\u002F\u002Fkunpeng-sc.csdnimg.cn\u002F?timestamp=1645783940\u002F#\u002Fpreview\u002F7681?positionId=592&adId=4444&queryWord=-&spm=3001.5911\" frameborder=\"0\" width= \"338px\" height= \"470px\" scrolling=\"no\" \u003E\u003C\u002Fiframe\u003E\u003Cimg class=\"pre-img-lasy\" data-src=\"https:\u002F\u002Fkunyu.csdn.net\u002F1.png?p=592&adId=4444&a=4444&c=7681&k=-&spm=3001.5911&d=1&t=3&u=d12074520df24bf7b65a929392bd95cf\" style=\"display: block;width: 0px;height: 0px;\"\u003E\u003C\u002Fdiv\u003E","source":1,"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5949\",\"dest\":\"\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-slide-ad\\\",\\\"compDataId\\\":\\\"www-slide-ad\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5949"}}],"www-headhot":[{"itemType":"","description":"历时4年","title":"Apache Doris成为Apache顶级项目","url":"https:\u002F\u002Fblog.csdn.net\u002Fucanuup_\u002Farticle\u002Fdetails\u002F125328242","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fucanuup_\u002Farticle\u002Fdetails\u002F125328242\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"最大的阻力是什么","title":"坐拥755万开发者的中国开源,进度几何?","url":"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125326095?spm=1001.2014.3001.5502","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125326095?spm=1001.2014.3001.5502\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"网友:时间挺巧,是时候换 M2 了!","title":"MIT曝光 M1 不可修复的漏洞","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125325839","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125325839\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"因危险发言,到手的股权飞了","title":"苹果设计团队成员纷纷离职","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125320871","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125320871\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"惹开发者怒怼","title":"微软将闭源VS Code中的C#扩展","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125320885?spm=1001.2014.3001.5502","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125320885?spm=1001.2014.3001.5502\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"七层负载均衡的4大生态","title":"为什么BFE可以取代Nginx","url":"https:\u002F\u002Fblog.csdn.net\u002Fweixin_52406291\u002Farticle\u002Fdetails\u002F125290497","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fweixin_52406291\u002Farticle\u002Fdetails\u002F125290497\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"Ps Web版≠Ps App版","title":"PS 网页版将免费","url":"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125297056?spm=1001.2014.3001.5502","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125297056?spm=1001.2014.3001.5502\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"TensorFlow败给PyTorch","title":"谷歌:未来就靠你了,JAX","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125297978","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125297978\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"深度学习崛起十年","title":"李飞飞:我更像物理学界的科学家,而不是工程师","url":"https:\u002F\u002Fblog.csdn.net\u002FOneFlow_Official\u002Farticle\u002Fdetails\u002F125230405?spm=1001.2014.3001.5502","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002FOneFlow_Official\u002Farticle\u002Fdetails\u002F125230405?spm=1001.2014.3001.5502\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"GitLab:我还在用呢!","title":"Ruby on Rails已死?","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125282817","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125282817\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"程序员自己写 Bug:因拼写错误","title":"PyPI 多个软件包含后门","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125277405","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125277405\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"揭秘软件臃肿的真实原因","title":"业务需求引发软件臃肿","url":"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125270571?spm=1001.2014.3001.5502","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125270571?spm=1001.2014.3001.5502\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"LaMDA:我是一个“人”,不要利用或操纵我","title":"谷歌 AI 被曝已自我觉醒?","url":"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125270429","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fcsdnnews.blog.csdn.net\u002Farticle\u002Fdetails\u002F125270429\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"迟迟不公布OxygenOS 12源码惹怒网友","title":"一加疑违反GPL协议","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125262836","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125262836\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"QUIC 能插遍全球?","title":"HTTP\u002F3标准化,TCP能被替代吗?","url":"https:\u002F\u002Fblog.csdn.net\u002Fdog250\u002Farticle\u002Fdetails\u002F125226727","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fdog250\u002Farticle\u002Fdetails\u002F125226727\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"再次进入 Apache 孵化器","title":"Apache Ambari重启","url":"https:\u002F\u002Fblog.csdn.net\u002Fwypblog\u002Farticle\u002Fdetails\u002F125252718","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fwypblog\u002Farticle\u002Fdetails\u002F125252718\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"Google 用 Debian 服务器给出了答案","title":"π小数点第100万亿数字是多少?","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125220917","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125220917\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"人类可以和AI和谐相处,共创美好未来!","title":"因怀疑对话系统变成人而被带薪休假","url":"https:\u002F\u002Fblog.csdn.net\u002Fqq_35082030\u002Farticle\u002Fdetails\u002F125248029","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fqq_35082030\u002Farticle\u002Fdetails\u002F125248029\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"Java真的老了吗?","title":"6月编程语言排行榜:C++ 即将超越Java","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125217982","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125217982\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"在微软工作 21 年","title":"微软再曝“丑闻” HoloLens之父即将离职!","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125213790","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125213790\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"在挑战中蓬勃发展","title":"2年暴涨180万名开发者,Rust迎来高光时刻","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125208278","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125208278\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"败给 VS Code","title":"GitHub“杀死”Atom","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125203360","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125203360\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"采用开源驱动","title":"Asahi Linux在M1下运行首个三角形渲染","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125189442","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnopensource\u002Farticle\u002Fdetails\u002F125189442\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}},{"itemType":"","description":"全球 77.5% 的网站都在使用PHP","title":"“世界上最好的语言” 27岁生日快乐","url":"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125195769","images":[""],"ext":{},"report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"1000.2115.3001.5927\",\"dest\":\"https:\u002F\u002Fblog.csdn.net\u002Fcsdnnews\u002Farticle\u002Fdetails\u002F125195769\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-swiper\\\",\\\"compDataId\\\":\\\"www-headhot\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":"spm=1000.2115.3001.5927"}}],"www-recomend-community":[{"extend":{"blink_content":null,"vip_img":null,"product_biz_no":null,"avatarurl":"https:\u002F\u002Fprofile.csdnimg.cn\u002F3\u002F4\u002Fd\u002F2_qq_62294245","user_name":"qq_62294245","flowerName":null,"certificate":true,"created_at":"2022-06-15 10:53:33","focus":true,"recommend_type":"ali","digg":"63","recommend":false,"pic":"https:\u002F\u002Fimg-blog.csdnimg.cn\u002F85b5f0c363b74bb6b08e1d0746c2dc1e.jpeg?x-oss-process=image\u002Fresize,m_fixed,h_300,image\u002Fformat,png","show_tag":"原力计划","title":"【CSDN云VS腾讯云】要不然怎么说CSDN开发云是打工人和学生党的福音呢?","certificate_info":"云计算领域新星创作者","report_data":{"eventClick":true,"data":{"mod":"popu_459","extra":"{\"utm_medium\":\"distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec\",\"dist_request_id\":\"1655552654436_96618\"}","dist_request_id":"1655552654436_96618","ab_strategy":"default","index":"1","style":"PIC_V2_21","strategy":"user_follow"},"urlParams":{"utm_medium":"distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec","depth_1-utm_source":"distribute.pc_feed_v2.none-task-blog-user_follow-1-125288502-null-null.pc_personrec"},"eventView":true},"user_info":null,"product_id":"125288502","nickname":"小鹏linux","actionInfo":"540阅读 · 112评论","company":"","beginTime":null,"channelId":0,"durationDesc":null,"views":540,"comments":"112","level":null,"liveMark":null,"recommend_vip_user":{"user_desc":"你未必出类拔萃,但一定与众不同","avatarurl":"https:\u002F\u002Fprofile.csdnimg.cn\u002F3\u002F4\u002Fd\u002F2_qq_62294245","user_info":null,"user_name":"qq_62294245","nickname":"小鹏linux","recommed_desc":"你关注的人发布"},"avatar":"https:\u002F\u002Fprofile.csdnimg.cn\u002F3\u002F4\u002Fd\u002F2_qq_62294245","product_app_id":null,"dateNumber":null,"url":"https:\u002F\u002Fblog.csdn.net\u002Fqq_62294245\u002Farticle\u002Fdetails\u002F125288502","tags":[],"certificate_pic":"https:\u002F\u002Fimg-home.csdnimg.cn\u002Fimages\u002F20210412060958.png","product_type":"blog","userNumberCount":null,"picList":["https:\u002F\u002Fimg-blog.csdnimg.cn\u002F85b5f0c363b74bb6b08e1d0746c2dc1e.jpeg?x-oss-process=image\u002Fresize,m_fixed,h_300"],"share_url":null,"csdnTag":["腾讯云","docker","服务器","linux","云计算"],"reserveAddress":null,"job":"","strategy":"user_follow","tace_code":null,"user_days":"码龄1年","desc":"CSDN开发云平台提供更丰富的开源项目及其文档让你快速上手更好、更实用、更优秀的解决方案,依托丰富的基础云服务,助力中小企业、个人开发者和学生群体一键上云","status":1},"style":"PIC_V2_21","report_data":{"eventClick":true,"eventView":true,"data":"{\"spm\":\"\",\"dest\":\"\",\"extra\":\"{\\\"fId\\\":558,\\\"fName\\\":\\\"floor-www-index\\\",\\\"compName\\\":\\\"www-interaction\\\",\\\"compDataId\\\":\\\"www-recomend-community\\\",\\\"fTitle\\\":\\\"\\\",\\\"pageId\\\":141}\"}","urlParams":""},"appId":"1547458038"},{"extend":{"blink_content":null,"vip_img":null,"product_biz_no":null,"avatarurl":"https:\u002F\u002Fprofile.csdnimg.cn\u002F3\u002Fa\u002F2\u002F2_qq_41250372","user_name":"qq_41250372","flowerName":null,"certificate":false,"created_at":"2022-06-16 17:38:02","focus":true,"recommend_type":"ali","digg":"28","recommend":false,"pic":"https:\u002F\u002Fimg-blog.csdnimg.cn\u002F1dd04b0c58a54ddab4d34187ce68e91c.png?x-oss-process=image\u002Fresize,m_fixed,h_300,image\u002Fformat,png","show_tag":"原力计划","title":"【云原生】第五篇--Docker容器化部署企业级应用集群","certificate_info":"","report_data":{"eventClick":true,"data":{"mod":"popu_459","extra":"{\"utm_medium\":\"distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec\",\"dist_request_id\":\"1655552654436_96618\"}","dist_request_id":"1655552654436_96618","ab_strategy":"default","index":"3","style":"PIC_V2_23","strategy":"user_follow"},"urlParams":{"utm_medium":"distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec","depth_1-utm_source":"distribute.pc_feed_v2.none-task-blog-user_follow-3-125266741-null-null.pc_personrec"},"eventView":true},"user_info":null,"product_id":"125266741","nickname":"孙和龚","actionInfo":"279阅读 · 50评论","company":"","beginTime":null,"channelId":0,"durationDesc":null,"views":279,"comments":"50","level":null,"liveMark":null,"recommend_vip_user":{"user_desc":"喂!起来上课了。90后大数据讲师一枚,热衷于IT教育传播,想让每个学生在课前、课后可以有更多的学习途径,现在开始转战CSDN写课件,欢迎大佬指导,也欢迎小白一起来学习!我写的你能看懂吗?一起来看看吧。","avatarurl":"https:\u002F\u002Fprofile.csdnimg.cn\u002F3\u002Fa\u002F2\u002F2_qq_41250372","user_info":null,"user_name":"qq_41250372","nickname":"孙和龚","recommed_desc":"你关注的人发布"},"avatar":"https:\u002F\u002Fprofile.csdnimg.cn\u002F3\u002Fa\u002F2\u002F2_qq_41250372","product_app_id":null,"dateNumber":null,"url":"https:\u002F\u002Fblog.csdn.net\u002Fqq_41250372\u002Farticle\u002Fdetails\u002F125266741","tags":[],"certificate_pic":"","product_type":"blog","userNumberCount":null,"picList":["https:\u002F\u002Fimg-blog.csdnimg.cn\u002F1dd04b0c58a54ddab4d34187ce68e91c.png?x-oss-process=image\u002Fresize,m_fixed,h_300","https:\u002F\u002Fimg-blog.csdnimg.cn\u002F6e54d3c5a2ce4799a1d261a34bd754e5.png?x-oss-process=image\u002Fresize,m_fixed,h_300","https:\u002F\u002Fimg-blog.csdnimg.cn\u002F81aa0a072b034f1da90af4251654ea49.png?x-oss-process=image\u002Fresize,m_fixed,h_300"],"share_url":null,"csdnTag":["云原生","docker","nginx","java","分布式"],"reserveAddress":null,"job":"","strategy":"user_follow","tace_code":null,"user_days":"码龄5年","desc":"本文主要讲述可以看到这里的响应明明返回的是200 ok但是并没有给我处理这个请求!基于ajaxajax是js提供给我们的一种构造http请求的方式!传统的js下的ajax方式构造方式比较繁琐!我们就通过引入jQuery(js下的框架类似于java中spring框架)实现对ajax方式请求的构造!引入jQuery!jQuery cdn链接随便找个版本复制下来,这里有两种方式!1).直接复制到HTML代码中!2).打开这个链接,然后复制下来,再创建一个js本地文件,然后在HTML中引入这个链接!<!--方式一:复制到本地--> <script src="jQuery.js"></script> <!--方式二:直接复制--> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>构造请求<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ajax方式构造请求</title> </head> <body> <!--复制到本地--> <!-- <script src="jQuery.js"></script> --> <!--直接复制--> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> script <script> $.ajax({ type: "get", url:"https://www.csdn.net/", success:function(body){ console.log("ajax方式构造请求,成功!"+body); }, error:function(){ console.log("请求失败"); } }); </script> </body> </html>$符号在js中可以用于变量名!在jQuery中的很对函数都是通过这个$对象访问!$.ajax();函数这里的ajax函数需要传入参数是一个对象!<script> $.ajax({ type: "get", url:"https://www.csdn.net/", success:function(body){ console.log("ajax方式构造请求,成功!"+body); }, error:function(){ console.log("请求失败"); } }); </script>这里的对象的数据也是用键值对的方式保存信息!type:"post"采用那种方法构造请求这里的构造请求的方法不像form表单只能选择post/get这里所有构造请求的方法都可以选择!~url:"https://www.csdn.next/"请求访问的服务器url域名!success:function(body){ console.log("ajax方式构造请求,成功!"+body); }请求成功,这里采用回调函数的方式,服务器的响应结果信息保存在body中!所以我们查看body的信息就可以获取到从服务器中收到的消息!error:function(){ console.log("请求失败"); }请求失败,这里打印失败结果!通过上方这些键值对就通过ajax构造好了一个http请求!我们看一下我们这个请求最终响应结果!请求失败了,虽然返回的状态码是200 ok这里存在跨域访问!我们看到这里报错了!因为这里我们属于跨站访问了!什么是跨站访问呢?我们这里构造请求的html文件并不在该服务器下!如果我们的请求网页就在https://www.csdn.net/域名下的服务器,那么就不是跨站(跨域)访问以后我们把请求写在自己的服务器下就不存在这个问题!请求:响应基于java的方式这里基于java的方式构造http请求比较繁琐,我们更少使用到!我们一般通过java中的TCPsocket接口,按照请求报文的格式,构造出一个报文匹配的字符串,然后传入socket发给服务器即可!但是在实际开发中我们也不会直接就使用socket构造,我们会向ajax一样借助第三方库!public class HttpClient { private Socket socket; private String ip; private int port; public HttpClient(String ip, int port) throws IOException { this.ip = ip; this.port = port; socket = new Socket(ip, port); } public String get(String url) throws IOException { StringBuilder request = new StringBuilder(); // 构造首行 request.append("GET " + url + " HTTP/1.1\n"); // 构造 header request.append("Host: " + ip + ":" + port + "\n"); // 构造 空行 request.append("\n"); // 发送数据 OutputStream outputStream = socket.getOutputStream(); outputStream.write(request.toString().getBytes()); // 读取响应数据 InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024 * 1024]; int n = inputStream.read(buffer); return new String(buffer, 0, n, "utf-8"); } public String post(String url, String body) throws IOException { StringBuilder request = new StringBuilder(); // 构造首行 request.append("POST " + url + " HTTP/1.1\n"); // 构造 header request.append("Host: " + ip + ":" + port + "\n"); request.append("Content-Length: " + body.getBytes().length + "\n"); request.append("Content-Type: text/plain\n"); // 构造 空行 request.append("\n"); // 构造 body request.append(body); // 发送数据 OutputStream outputStream = socket.getOutputStream(); outputStream.write(request.toString().getBytes()); // 读取响应数据 InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024 * 1024]; int n = inputStream.read(buffer); return new String(buffer, 0, n, "utf-8"); } public static void main(String[] args) throws IOException { HttpClient httpClient = new HttpClient("42.192.83.143", 8080); String getResp = httpClient.get("/AjaxMockServer/info"); System.out.println(getResp); String postResp = httpClient.post("/AjaxMockServer/info", "this is body"); System.out.println(postResp); } }
HTTP响应我们的HTTP响应格式,之前已经学习过了!我们知道每次响应都会在第一行状态行中返回响应的信息!而最重要的信息就是状态码,不同的状态码表示不同的响应状态,对于请求的结果是否失败或者成功或者出现了什么bug都可以通过这个状态码来体现!状态码状态码一般由3位十进制数据构成!不同数字开头的状态码有不同的含义!从1开头到5开头分别代表不同类别的状态码!上面就是状态码的分类及其每个分类含义!我们主要介绍几个常见的状态码信息!200 OK状态码200 OK表示浏览器获取到了响应结果,并且一切顺利!如果我们我们的网络啥的没有问题基本上抓取到都是200 OK这个状态码!301 Moved Permanently在请求的 URL 已被移除时使用。响应的 Location 首部中应该包含 资源现在所处的 URL。除非额外指定,否则这个响应也是可缓存的.就是直接跳转到该URL!302 Found请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。客户端应该使用 Location 首部给出的 URL 来临时定位资源。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。Location记录了重定向后的网页URL!状态码301和302作用有点类似!301重定向是永久跳转到location而302只是暂时重定向!401 Unauthorized表示这个请求需要用户验证!402 Payment Required此响应码保留以便将来使用,创造此响应码的最初目的是用于数字支付系统,然而现在并未使用!403 Forbidden如果你访问的资源没有权限,服务器就会返回403 Forbidden这个状态码! 但我们访问bilibili下的/1.html网页时,因为压根就没有这个网页,资源不存在就会触发Not found 但是bilibili设计的比较卡哇伊,返回了一个动漫给你看!可以看到当我们访问一个不存在的网站时,服务器就会返回一个404 Not Found状态码!405 Method Not Allowed前面我们已经学过了GET和HOST等方法!有些服务器只能允许用GET方法请求访问,如果用POST方法进行请求服务器就会返回这个405 Method Not Allowed响应!这样的状态码比较难抓包,一般在我们自己的服务器构造请求和响应就很常遇到这个响应!也就定位了bug!418 I’m a teapotHTTP文档指出:当客户端给一个茶壶发送泡咖啡的请求时,茶壶就返回一个418错误状态码,表示“我是一个茶壶”。这就是一个菜单,程序员的浪漫!就是可以设置某一时刻你访问的某一个网站,该网站返回的响应和此前的不一样,就有点惊喜,也可能是惊喜,还是少写几行代码舒服!!!500 Internal Server Error服务器出现了重大故障!服务器出bug了!这个状态码也比较少见,在后面自己编写服务器时会出现!504 Gateway Timeout服务器繁忙,它们在 等待另一服务器对其请求进行响应时超时了!总结我们通过状态码的开头就可以知道该响应信息当前状态!100开头是接收的请求正在处理!200开头是请求处理成功!300开头是重定向状态!400开头是客户端的问题!500开头是服务器的问题!
HTTP请求我们上篇博客已经初步了解了HTTP协议的大致内容,已经明白了HTTP协议分请求和响应两部分,而且这两个部分的协议报具有不同到格式,需要我们掌握协议格式里的每个字段代表的信息,能够通过Fiddler抓包工具抓取HTTP协议报从而真正理解学习应用层HTTP协议!而我们本章主要详细了解HTTP协议的请求格式,掌握里面的每个字段对应的信息,从而可以读懂一个HTTP请求,最终到达可以直接编写一个HTTP请求的效果!请求报头(header)我们知道请求头header中的内容采用键值对的形式存储!所以有一些KEY值具有具体的含义,我们来学习一下常见KEY值的的含义!我们抓取一个请求报进行辅助学习!GET https://csdnimg.cn/public/common/libs/jquery/jquery-1.9.1.min.js?1655271269501 HTTP/1.1 Host: csdnimg.cn User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Connection: keep-alive Referer: https://editor.csdn.net/ Sec-Fetch-Dest: script Sec-Fetch-Mode: no-cors Sec-Fetch-Site: cross-site可以看到这里的键值对格式:键和值之间用: 冒号和空格进行分割!一对键值对存在于一行中,进而分割键值对HostHost用于保存这个请求协议的域名!可以看到我们抓取包的域名信息Host: csdnimg.cnContent-Length表示body中的数据长度!显然只有POST方法的请求协议报头中才包含了body数据!所以我们抓取一个POST请求!POST https://incoming.telemetry.mozilla.org/submit/firefox-desktop/baseline/1/176aad6a-352f-4834-bfb9-9fa2575f8863 HTTP/1.1 Host: incoming.telemetry.mozilla.org User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br content-length: 691 content-type: application/json; charset=utf-8 x-telemetry-agent: Glean/44.1.1 (Rust on Windows) date: Wed, 15 Jun 2022 05:46:32 GMT Connection: keep-alive Sec-Fetch-Dest: empty Sec-Fetch-Mode: no-cors Sec-Fetch-Site: none Pragma: no-cache Cache-Control: no-cache {"ping_info":{"seq":4365,"start_time":"2022-06-15T13:36+08:00","end_time":"2022-06-15T13:46+08:00","reason":"inactive"},"client_info":{"telemetry_sdk_build":"44.1.1","first_run_date":"2021-08-09+08:00","app_build":"20220608170832","app_channel":"release","app_display_version":"101.0.1","architecture":"x86_64","os":"Windows","os_version":"10.0","client_id":"0bacdc5f-939a-4115-93bf-a619c5f31b07"},"metrics":{"counter":{"browser.engagement.active_ticks":108,"browser.engagement.uri_count":18},"datetime":{"glean.validation.first_run_hour":"2021-08-09T12+08:00"},"labeled_counter":{"glean.error.invalid_state":{"glean.baseline.duration":1},"glean.validation.pings_submitted":{"baseline":1}}}}我们可以看到POST请求报头中就有Content-Length键值对!content-Length: 691说明这里的POST请求body字段长度是671这里的Content-Length有啥作用呢!我们在传输层中知道TCP要解决粘包问题,就需要在应用层协议动手!而我们知道然后请求报头和body中有空行分割!但是当应用层协议报分用到达接收方的接收队列中如果无法得知一个协议报字段长度大小,就可能出现粘包问题,而这里的Content-Type可以知道body的字段长度也就可以确定一个协议报的尾,从而解决粘包问题!Content-Type表示请求中的body数据字段格式!常见选项格式如下:application/x-www-form-urlencoded:采用form表单提交的数据格式!此时的body的字段格式形如:title=test&content=hello1就是键和值之间用=连接,键值对之间用&分割,类似于我们的querystring查询字符串格式!并且也需要进行urlencode!multipart/form-data: form:表单提交的数据格式(在 form 标签中加上enctyped="multipart/form-data" )这和上面的application/x-www-form-urlencoded区别就是这里的表单一般用于提交图片/文件!body格式形如:Content-Type:multipart/form-data; boundary=---- WebKitFormBoundaryrGKCBY7qhFd3TrwA ------WebKitFormBoundaryrGKCBY7qhFd3TrwA Content-Disposition: form-data; name="text" title ------WebKitFormBoundaryrGKCBY7qhFd3TrwA Content-Disposition: form-data; name="file"; filename="chrome.png" Content-Type: image/png PNG ... content of chrome.png ... ------WebKitFormBoundaryrGKCBY7qhFd3TrwA--application/json:数据为json格式:body格式形如:{"ping_info":{"seq":4365,"start_time":"2022-06-15T13:36+08:00","end_time":"2022-06-15T13:46+08:00","reason":"inactive"},"client_info":{"telemetry_sdk_build":"44.1.1","first_run_date":"2021-08-09+08:00","app_build":"20220608170832","app_channel":"release","app_display_version":"101.0.1","architecture":"x86_64","os":"Windows","os_version":"10.0","client_id":"0bacdc5f-939a-4115-93bf-a619c5f31b07"},"metrics":{"counter":{"browser.engagement.active_ticks":108,"browser.engagement.uri_count":18},"datetime":{"glean.validation.first_run_hour":"2021-08-09T12+08:00"},"labeled_counter":{"glean.error.invalid_state":{"glean.baseline.duration":1},"glean.validation.pings_submitted":{"baseline":1}}}}我们现在的POST请求中的body字段一般采用json格式!我们的POST请求从何获取呢?我们知道GET请求占大部分,抓取一个POST请求好难!我们通过这两种方法的一些特性就可以进准定位到POST请求!首先我们知道GET是通过URL中的查询字符串,进行数据传输!而该URL需要暴露在页面上,如果我们进行登入注册的请求访问,那就很尴尬,直接在页面上暴露了用户信息!这样就显得很不专业!如果使用POST请求就很好的解决了这个问题,我们的POST请求是通过body传输信息的!我们不能直接从页面上看到该信息!所以我们的登入注册页面请求,一般都是使用POST方法!User-Agent(UA)表示当前用户的上网环境!User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0这是博主的User-Agent字段信息!可以看到我们里面还是有一些可以认识的单词信息!Windows NT 10.0 Win64; x64;表示当前我用的设备是PC断,操作系统是Windows 10 64位机器!Gecko/20100101 Firefox/101.0关于使用的浏览器的一些信息,可以看到我使用的Firefox浏览器!UA有啥用呢?通过这个请求报头字段携带给服务器,然后通过该UA信息,程序员就可以返回对应适配的响应!就比如我们的PC端屏幕是宽屏,而手机端是窄屏!这就使得我们返回的需要有兼容性问题!我们通过UA就可以返回对应的响应信息!还有就是之前浏览器不同,代码的适配度也有所不同,就比如有点图片在有的浏览器上就无法查看!使用UA也是解决了这个问题!但是现在的浏览器功能都大差不差!这个字段也失去了意义了!当时返回设备的类型还是意义重大,虽然也有一些其他的解决方案,列如分布式等,但是都不如这个来的高效!Referer表示这个页面从那个页面跳转而来!如果是直接从浏览器输框或者收藏夹点开的请求就没有该字段信息,毕竟这也没跳转呀!GET https://profile.csdnimg.cn/5/F/F/2_weixin_52345071 HTTP/1.1 Host: profile.csdnimg.cn User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0 Accept: image/avif,image/webp,*/* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Connection: keep-alive Referer: https://blog.csdn.net/ Sec-Fetch-Dest: image Sec-Fetch-Mode: no-cors Sec-Fetch-Site: cross-site我们抓取的这个请求报就有Referer字段!Referer: https://blog.csdn.net/通过这个Referer我们就可以知道我们当前域名为profile.csdnimg.cn的页面是从https://blog.csdn.net/跳转过来的!所以Referer有什么用嘛?我们知道这个页面从哪里来有啥意义嘛?我们想象一下一个页面上可能有好多广告,而一个广告主会在多个平台投广告!页面上的有些广告是通过点击计费的,通过这个平台的推广,点击后来到广告主主机的服务器,而广告主通过Referer信息就可以知道这个点击从那个页面而来从而区分不同的平台,更好的进行计费!Cookie实现身份标识的功能!Cookie存储了一个字符串,这个数据可能来自客户端(通过网页中的js写入的),也可能来自服务器(服务器在HTTP响应中的Header中的Set-Cookie字段给浏览器返回的数据)所以说这么多这里的Cookie字段到底有啥作用呢?我们知道当我们在一个网站上进行了一次登入后,后面我们打开这个网页就不需要登入了,浏览器会用户的身份信息保存一段时间!然后每次我们登入这个网站,你都可以获取到之前的信息,因为你之前的信息都在服务器的数据库保存着!而Cookie就是一个身份标识,通过这个Cookie信息访问网站后,服务器通过这个Cookie信息就可以找到该用户的数据信息从而访问,并且也保存在这个Cookie中!可能说的还是不特别明白!我们可以将Cookie想象成一个小房间!因为我们刚刚说了在我们登入一个页面后,这个页面的信息就会在流量器中保留一段时间,这里就报留在本地浏览器的Cookie中!我们登入一个力扣网站!可以看到这个小锁头就保存了网页的Cookie信息,然后我们通过找到LeetCode的Cookie信息就可以查看到这个网页当前缓存在你本地的身份标识,然后这里的Cookie也是采用了键值对的形式存储,每一条信息都是一个键值对!然后如果我们进行删除,只是把本地的Cookie信息给删除了,你重新登入后,你的LeetCode信息还在!还有不同网页具有不同的Cookie,浏览器可以缓存对应网页的信息!浏览器如何放回该用户的数据信息呢?就是通过请求中的Cookie信息,通过这个身份标识,拿到CookieId然后在服务器端中的数据库中找到Cookie信息通过Body返回!接收方通过上方这个Cookie信息,可以返回对应的数据!而放回的Cookie信息保存在body中!我们可以看到请求协议报中的Cookie信息采用键值对的形式:键和值之间用=连接键值对用;分割!Cookie信息就类似一个令牌信息,通过这个令牌进行身份验证!body我们知道我们的Header和body通过空格隔开!然后这里的body数据格式啥的主要由Header中的信息表标识好了!Content-Typebody的格式!Content-Lengthbody字段的长度!
HTTP协议我们网络编程已经了解了很多协议类型了!有传输层中的TCP/UDP,网络层中的IP协议.我们知道身为程序员的我们重点是反正应用层协议的设计上,其他网络层中的协议属于操作系统内核!我们知道我们程序员主要在应用层设计应用层协议!应用层协议由包括两部分,确定传输的数据和协议模板(xml/json/Protobuff)的选择!我们知道,不同水平的程序员设计的应用层协议各有参差!有大佬就设计出了业界比较好的协议,供大家使用!HTTP协议是目前业界使用最多的协议!所以我们重点来学习HTTP协议,通过对HTTP协议的学习,从而自己可以借助HTTP协议在项目中设计应用层协议!我们知道应用层协议在网络传输的关键作用就是,让通信双方可以理解双方的信息,就好比你买一个电子产品,卖家会提供一个使用说明书,这里的应用层协议也就相当于使用说明书!HTTP协议报格式我们通过之前网络协议的学习知道,学习一个协议就是学习这个协议的报头格式!看到这个协议格式发现和我们之前学习的协议报头格式大为不同!主要是因为之前学习的TCP/IP协议都是面向二进制设计的协议类型!而HTTP协议主要是由文本格式的协议,比之前的协议格式利于我们学习和理解!Fiddler抓包我们HTTP协议格式的学习先通过引入一个app来学习!Fiddler抓包工具什么是抓包工具呢?我们知道数据在网络传输中,会经过很多设备,我们的信息虽然通过这些协议进行了分装!当时这些设备还是有这些信息的记录!这里的抓包工具就相当于一个代理,就好比一个传话的,虽然Fiddler并不需要这个信息,当时信息经过了这个抓包工具,就可以将传输中的应用层协议报拿到,也就是拿到了HTTP协议包,这就是抓包!其实其他协议也能通过其他的的抓包工具,进行抓包!我们先通过这个抓包工具的使用,获取几个HTTP数据协议,然后对HTTP协议有所了解,然后再进行协议格式的学习!Fiddler安装我们通过官网下载安装这个Fiddler Classic经典版本!Fiddler下载地址然后安装后就可以使用了!打开界面后就是这个样子!然后我们要先进行设置!我们将这里的HTTPS中的所有选项勾选上,然后就可以进行使用了!啥是HTTPS呢?我们暂且理解为HTTP的升级版,在HTTP的基础上进行了加密,保证了数据传输的安全!详细内容后面我们还会介绍到!这个界面有两栏,左边这栏就是我们抓包抓取的数据!当我们浏览器访问一个页面或者程序中,进行跳转都可能会有HTTP协议数据请求,然后Fiddler抓包工具就可以抓取到数据报!还有就是浏览器中的一次网页请求可能,传输了多个HTTP协议数据包,所以这里的抓取的报一直在增加!当我们双击抓取到的一个HTTP协议数据报后,右边栏就会出现这个协议报的详细内容!这里上方就是这个HTTPS协议数据报的请求信息,下方就是HTTPS的响应信息!我们通过对这里的请求和响应的学习就可以大致了解到HTTPS协议格式!请求:我们选中Raw这一选项就可以查看最原始的HTTP协议请求!然后我们发现这么小的字体咋看啊这是,我们可以通过下方的VIew in Notepad通过记事本查看这里的请求数据!GET https://www.csdn.net/ HTTP/1.1 Host: www.csdn.net User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Connection: keep-alive Cookie: uuid_tt_dd=10_30601611580-1653459529558-297725; log_Id_pv=602; Hm_lvt_6bcd52f51e9b3dce32bec4a3997715ac=1655126066,1655128987,1655170391,1655182762; Hm_up_6bcd52f51e9b3dce32bec4a3997715ac=%7B%22islogin%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isonline%22%3A%7B%22value%22%3A%221%22%2C%22scope%22%3A1%7D%2C%22isvip%22%3A%7B%22value%22%3A%220%22%2C%22scope%22%3A1%7D%2C%22uid_%22%3A%7B%22value%22%3A%22weixin_52345071%22%2C%22scope%22%3A1%7D%7D; Hm_ct_6bcd52f51e9b3dce32bec4a3997715ac=6525*1*10_30601611580-1653459529558-297725!5744*1*weixin_52345071; log_Id_view=1172; __51uvsct__JQTDiOVZ2pRjGa1K=41; __51vcke__JQTDiOVZ2pRjGa1K=1e84abb9-62df-5ecc-828b-e3052ffe5150; __51vuft__JQTDiOVZ2pRjGa1K=1653459532473; log_Id_click=801; __gads=ID=f159decf46255d5e-22bbe38364d300d7:T=1653459564:RT=1653459564:S=ALNI_Ma5D0b1QWC6YQ8t-on_R6zAurFD0g; __gpi=UID=000005b633162c48:T=1653459564:RT=1655010298:S=ALNI_Mbd0__O7WINiqjmFo0HJe78mx_xOA; ssxmod_itna=eqUxBDcDu0D=34Bake5DkF6diKbMk00fOexGXhIYDZDiqAPGhDC34UeDIYGCYRRrBe+C3jjBRoqH8m4WT4YKC4I0frQ4B3DEx0=PCjeKiinDCeDIDWeDiDGR7D=xGYDj0KGWD4qDOD3qGyS+=Di8t9DdvC7uQDmTNDGup6D7QDIw6g9frVAeDSW7UxKG=DjubD/4xWHeRWH=5DbgeuDeiDtqD9lw=Dbfd3x0pymkU7wGwIbt4US+NDxBtQExDf7kGC4t5D9h6IOfkD0wEY9xpxWiez8AYRgY5NBietW7ePBg+Ci0Dz4+Dd35DuxDG4ka75qiDD==; ssxmod_itna2=eqUxBDcDu0D=34Bake5DkF6diKbMk00f2DA=uxPtD/K3KDFODxDIg8qqFGFB+oQwiXsYI1EvQw7bKMRj1/+6eoNOKGbng2INO81fi6zL/uMizdaYIy8Dg0Z9bnV80FIZXBPsk1hIg=n=R0n=VW7Q5+IQ3RbsCx9C=gvG2m5bN/y=qFOwqKyQG8v4=B7AYSO+lPqpUC3bRn8IazEFbzT6GgctB3SfmO8GDUSh7RSf/OQhrPdAPk8fRzjITkCRxdKw+OHwVQYb/oIrCR+yC6KgAucT6oFi6ueM/6w10qzQbZHZ68tTRmVimrsWGL+aE/ri7Rh/FgIrLm2OQPhfmmDuuDqhW914PEnRcQnKuAEKeuzPnFpYxqg0ATFWQmwx82Ie+oD07cGPD7=DYF=eD===; UserName=weixin_52345071; UserInfo=26ede6cf39e94eab9c6265a032aa59fa; UserToken=26ede6cf39e94eab9c6265a032aa59fa; UserNick=bug+%E9%83%AD; AU=5FF; UN=weixin_52345071; BT=1653462761522; p_uid=U010000; c_dl_prid=1653464404138_679702; c_dl_rid=1654675272560_674132; c_dl_fref=https://blog.csdn.net/m0_59140023/article/details/125097573; c_dl_fpage=/download/phx320/10246827; c_dl_um=distribute.pc_search_result.none-task-blog-2%7Eall%7Esobaiduend%7Edefault-3-107703400-null-null.142%5Ev11%5Epc_search_result_control_group%2C157%5Ev13%5Econtrol; management_ques=1653738813836; c_hasSub=true; has-vote-msg=1; c_utm_source=yh-grzx; is_advert=1; historyList-new=%5B%22pv%E4%BF%A1%E5%8F%B7%E9%87%8F%20%E4%BA%92%E6%96%A5%E5%92%8C%E5%90%8C%E6%AD%A5%22%5D; dc_tos=rdgbqv; csrfToken=aUhBDEajAeAqoPzfwJMb1Ltp; c_pref=https%3A//www.csdn.net/; c_ref=https%3A//mp.csdn.net/; c_first_ref=default; c_first_page=https%3A//www.csdn.net/; c_segment=13; Hm_lpvt_6bcd52f51e9b3dce32bec4a3997715ac=1655183480; dc_sid=d52c56b02a4a765ee9dfb3c18ed3278a; __vtins__JQTDiOVZ2pRjGa1K=%7B%22sid%22%3A%20%228e835648-5d35-50bb-af41-f2e30674a4ac%22%2C%20%22vd%22%3A%203%2C%20%22stt%22%3A%20711754%2C%20%22dr%22%3A%20657883%2C%20%22expires%22%3A%201655185276045%2C%20%22ct%22%3A%201655183476045%7D; c_page_id=default; dc_session_id=10_1655185306768.999185 Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1这就是我们抓取的这个报的数据请求!响应:而下方就是对于这个请求,服务器所给出的响应信息!我们响应也是通过Raw选项查看原始的HTTP响应!然后通过记事本打开!HTTP/1.1 200 OK Server: openresty Date: Tue, 14 Jun 2022 05:55:19 GMT Content-Type: text/html; charset=utf-8 Connection: keep-alive Keep-Alive: timeout=20 Vary: Accept-Encoding X-Response-Time: 356 x-xss-protection: 1; mode=block x-content-type-options: nosniff x-download-options: noopen x-readtime: 356 Strict-Transport-Security: max-age=31536000 Content-Length: 529163 <!doctype html><html lang="zh" data-server-rendered="true"><head><title>CSDN - 专业开发者社区</title> <meta name="keywords" content="CSDN博客,CSDN学院,CSDN论坛,CSDN直播"> <meta name="description" content="CSDN是全球知名中文IT技术交流平台,创建于1999年,包含原创博客、精品问答、职业培训、技术论坛、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区."> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <meta name="referrer" content="always"> <!----> <!----> <!----> <script src="https://g.csdnimg.cn/tingyun/tingyun.js"></script> <!----> <!----> <!----> <link rel="shortcut icon" href="https://g.csdnimg.cn/static/logo/favicon32.ico" type="image/x-icon"> <link rel="canonical" href="https://www.csdn.net"> <!----> <meta name="toolbar" content={"type":"0","fixModel":"1"} /> <meta name="report" content={"spm":"1000.2115"} /> <script src="https://g.csdnimg.cn/??lib/jquery/1.12.4/jquery.min.js,user-tooltip/2.2/user-tooltip.js,lib/qrcode/1.0.0/qrcode.min.js"></script> <script src='//g.csdnimg.cn/common/csdn-report/report.js' type='text/javascript'></script> <script src="https://g.csdnimg.cn/user-ordercart/2.0.1/user-ordercart.js?ts=2.0.1"></script> <!----> <script src="https://g.csdnimg.cn/common/csdn-login-box/csdn-login-box.js"></script> <script src="https://g.csdnimg.cn/user-ordertip/3.0.2/user-ordertip.js?t=3.0.2"></script> <!----> <!----> <!----> <!----> <script> window.TINGYUN && window.TINGYUN.init && window.TINGYUN.init(function (ty_rum) { ty_rum.server = { "event_timeout": 60000, "dr_threshold": 4000, "opt_custom_param_rule": [], "cross_page_delay": 3000, "router_enable": true, "fp_threshold": 2000, "token": "568934913a6343de840a781ca5eaba4b", "beacon": "wkbrs1.tingyun.com", "trace_threshold": 7000, "x_server_switch": true, "ignore_err": false, "id": "hWg-u0rE5b8", "key": "Z1Tu5hoKbGw", "fs_threshold": 4000 }; }); </script> <!----> <script src="https://g.csdnimg.cn/common/csdn-toolbar/csdn-toolbar.js"></script> <link rel="stylesheet" href="https://csdnimg.cn/release/cmsfe/public/css/common.db505e6b.css"><link rel="stylesheet" href="https://csdnimg.cn/release/cmsfe/public/css/tpl/www-index-new/index.164507a0.css"></head> <body><div id="toolbarBox" style="min-height: 48px;"></div> <div id="app"><div><div class="main"><div class="page-container page-component"><div><div class="home_wrap"><div class="content_wrap"><div id="floor-nav_557" floor-index="0"><div comp-data="[object Object]" floor-data="[object Object]" class="blog-nav-tag" data-v-f8e9e086><div class="blog-nav " data-v-f8e9e086><img src="https://img-home.csdnimg.cn/images/20220107105619.png" alt class="blog-nav-down " data-v-f8e9e086> <div class="blog-nav-box" data-v-f8e9e086><ul class="def" data-v-f8e9e086><!----> <!----> <!----> <!----> <li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/back-end" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;back-end&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;back-end&quot;}" data-v-f8e9e086>后端</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/web" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;web&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;web&quot;}" data-v-f8e9e086>前端</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/mobile" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;mobile&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;mobile&quot;}" data-v-f8e9e086>移动开发</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/lang" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;lang&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;lang&quot;}" data-v-f8e9e086>编程语言</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/java" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;java&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;java&quot;}" data-v-f8e9e086>Java</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/python" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;python&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;python&quot;}" data-v-f8e9e086>Python</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/ai" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ai&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ai&quot;}" data-v-f8e9e086>人工智能</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/big-data" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;big-data&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;big-data&quot;}" data-v-f8e9e086>大数据</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/algo" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;algo&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;algo&quot;}" data-v-f8e9e086>数据结构与算法</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/avi" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;avi&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;avi&quot;}" data-v-f8e9e086>音视频</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/cloud-native" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud-native&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud-native&quot;}" data-v-f8e9e086>云原生</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/cloud" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;cloud&quot;}" data-v-f8e9e086>云平台</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/ops" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ops&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ops&quot;}" data-v-f8e9e086>运维</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/server" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;server&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;server&quot;}" data-v-f8e9e086>服务器</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/os" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;os&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;os&quot;}" data-v-f8e9e086>操作系统</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/db-management" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;db-management&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;db-management&quot;}" data-v-f8e9e086>数据库管理</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/ios" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ios&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;ios&quot;}" data-v-f8e9e086>iOS</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/android" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;android&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;android&quot;}" data-v-f8e9e086>Android</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/miniprog" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;miniprog&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;miniprog&quot;}" data-v-f8e9e086>小程序</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/hardware" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;hardware&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;hardware&quot;}" data-v-f8e9e086>硬件开发</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/embedded" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;embedded&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;embedded&quot;}" data-v-f8e9e086>嵌入式</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/microsoft" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;microsoft&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;microsoft&quot;}" data-v-f8e9e086>微软技术</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/software-engineering" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;software-engineering&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;software-engineering&quot;}" data-v-f8e9e086>软件工程</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/test" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;test&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;test&quot;}" data-v-f8e9e086>测试</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/sec" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;sec&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;sec&quot;}" data-v-f8e9e086>安全</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/internet" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;internet&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;internet&quot;}" data-v-f8e9e086>网络</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/product-ops" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;product-ops&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;product-ops&quot;}" data-v-f8e9e086>产品/运营</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/design" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;design&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;design&quot;}" data-v-f8e9e086>设计</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/job" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;job&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;job&quot;}" data-v-f8e9e086>职场和发展</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/search" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;search&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;search&quot;}" data-v-f8e9e086>搜索</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/devtools" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;devtools&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;devtools&quot;}" data-v-f8e9e086>开发工具</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/php" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;php&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;php&quot;}" data-v-f8e9e086>PHP</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/game" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;game&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;game&quot;}" data-v-f8e9e086>游戏</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/open" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;open&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;open&quot;}" data-v-f8e9e086>开放平台</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/harmonyos" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;harmonyos&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;harmonyos&quot;}" data-v-f8e9e086>HarmonyOS</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/blockchain" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;blockchain&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;blockchain&quot;}" data-v-f8e9e086>区块链</a></li><li class="navigation-right " data-v-f8e9e086><a href="https://blog.csdn.net/nav/math" data-report-click="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;math&quot;}" data-report-view="{&quot;spm&quot;:&quot;1001.2100.3001.7366&quot;,&quot;extend1&quot;:&quot;math&quot;}" data-v-f8e9e086>数学</a></li> <li class="blog-nav-up" data-v-f8e9e086><img src="https://img-home.csdnimg.cn/images/20220107105622.png" alt data-v-f8e9e086></li></ul></div></div> <!----></div></div><div id="floor-www-index_558" floor-index="1"><div comp-data="[object Object]" pageType="www" class="www-home-top"><div class="wart"></div> <div class="www-home-content"><div id="kp_box_ww9877"><!----></div></div> <div class="www-home-content active"><div floorData="[object Object]" class="headlines"><div class="headlines-left" data-v-e8da5228><div class="top-title" data-v-e8da5228><img src="https://img-home.csdnimg.cn/images/20220107104621.png" alt data-v-e8da5228> <h3 data-v-e8da5228>头条</h3></div> <dl data-v-e8da5228><dt data-v-e8da5228><a target="_blank" data-report-query="spm=1000.2115.3001.5926" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" href="https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502" data-v-e8da5228><img src="https://img-home.csdnimg.cn/images/20220613095441.jpg" alt data-v-e8da5228></a></dt> <dd class="desc" data-v-e8da5228><a data-report-query="spm=1000.2115.3001.5926" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502" data-v-e8da5228>“当你不再是程序员,很多事会脱离掌控”—— 对话全球最大独立开源公司SUSE CTO</a></dd> <dd class="desc-text-a" data-v-e8da5228><a data-report-query="spm=1000.2115.3001.5926" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5926&quot;,&quot;dest&quot;:&quot;https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-headimg\&quot;,\&quot;compDataId\&quot;:\&quot;Headimg\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://newprogrammer.blog.csdn.net/article/details/125198060?spm=1001.2014.3001.5502" data-v-e8da5228>所有行业都将受到云原生冲击</a></dd> <!----></dl></div> <div class="headlines-right"><div class="headswiper" data-v-0045335f><div class="headswiper-top" data-v-0045335f><div class="top-title" data-v-0045335f><!----> <h3 data-v-0045335f></h3></div> <p data-v-0045335f><img src="https://img-home.csdnimg.cn/images/20220107104919.png" alt data-v-0045335f> <!----> <img src="https://img-home.csdnimg.cn/images/20220107104954.png" alt data-v-0045335f> <!----></p></div> <div class="headswiper-content" data-v-0045335f><div data-v-0045335f><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://csdnnews.blog.csdn.net/article/details/125270571?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://csdnnews.blog.csdn.net/article/details/125270571?spm=1001.2014.3001.5502&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://csdnnews.blog.csdn.net/article/details/125270571?spm=1001.2014.3001.5502" class="title" data-v-0045335f>业务需求引发软件臃肿</a> <a target="_blank" href="https://csdnnews.blog.csdn.net/article/details/125270571?spm=1001.2014.3001.5502" data-v-0045335f><p class="name" data-v-0045335f>揭秘软件臃肿的真实原因</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://csdnnews.blog.csdn.net/article/details/125270429&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://csdnnews.blog.csdn.net/article/details/125270429&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://csdnnews.blog.csdn.net/article/details/125270429" class="title" data-v-0045335f>谷歌 AI 被曝已自我觉醒?</a> <a target="_blank" href="https://csdnnews.blog.csdn.net/article/details/125270429" data-v-0045335f><p class="name" data-v-0045335f>LaMDA:我是一个“人”,不要利用或操纵我</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnopensource/article/details/125262836&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnopensource/article/details/125262836&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnopensource/article/details/125262836" class="title" data-v-0045335f>一加疑违反GPL协议</a> <a target="_blank" href="https://blog.csdn.net/csdnopensource/article/details/125262836" data-v-0045335f><p class="name" data-v-0045335f>迟迟不公布OxygenOS 12源码惹怒网友</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/dog250/article/details/125226727&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/dog250/article/details/125226727&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/dog250/article/details/125226727" class="title" data-v-0045335f>HTTP/3标准化,TCP能被替代吗?</a> <a target="_blank" href="https://blog.csdn.net/dog250/article/details/125226727" data-v-0045335f><p class="name" data-v-0045335f>QUIC 能插遍全球?</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5927" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/wypblog/article/details/125252718&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5927&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/wypblog/article/details/125252718&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-headhot\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/wypblog/article/details/125252718" class="title" data-v-0045335f>Apache Ambari重启</a> <a target="_blank" href="https://blog.csdn.net/wypblog/article/details/125252718" data-v-0045335f><p class="name" data-v-0045335f>再次进入 Apache 孵化器</p></a></div></div></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div></div> <div class="headswiper" data-v-0045335f><div class="headswiper-top" data-v-0045335f><div class="top-title" data-v-0045335f><img src="https://img-home.csdnimg.cn/images/20220107104836.png" alt data-v-0045335f> <h3 data-v-0045335f>热点</h3></div> <p data-v-0045335f><img src="https://img-home.csdnimg.cn/images/20220107104919.png" alt data-v-0045335f> <!----> <img src="https://img-home.csdnimg.cn/images/20220107104954.png" alt data-v-0045335f> <!----></p></div> <div class="headswiper-content" data-v-0045335f><div data-v-0045335f><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Byeweiyang/article/details/125272397&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/Byeweiyang/article/details/125272397&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/Byeweiyang/article/details/125272397" class="title" data-v-0045335f>历史上的今天:雅虎收购Overture</a> <a target="_blank" href="https://blog.csdn.net/Byeweiyang/article/details/125272397" data-v-0045335f><p class="name" data-v-0045335f>理论计算机科学的奠基人出生</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnsevenn/article/details/125271267&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdnsevenn/article/details/125271267&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdnsevenn/article/details/125271267" class="title" data-v-0045335f>“真还传”接近尾声,罗永浩再创业</a> <a target="_blank" href="https://blog.csdn.net/csdnsevenn/article/details/125271267" data-v-0045335f><p class="name" data-v-0045335f>在AR时代抢先做出一个像2007年的iPhone + iOS一样的东西</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdngeeknews/article/details/125272310&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdngeeknews/article/details/125272310&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdngeeknews/article/details/125272310" class="title" data-v-0045335f>腾讯下架QQ影音所有版本</a> <a target="_blank" href="https://blog.csdn.net/csdngeeknews/article/details/125272310" data-v-0045335f><p class="name" data-v-0045335f>魅族回应被吉利收购:已签署协议|极客头条</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://fjiang.blog.csdn.net/article/details/125265922&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://fjiang.blog.csdn.net/article/details/125265922&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://fjiang.blog.csdn.net/article/details/125265922" class="title" data-v-0045335f>再议LaMDA,它真的初具思想吗?</a> <a target="_blank" href="https://fjiang.blog.csdn.net/article/details/125265922" data-v-0045335f><p class="name" data-v-0045335f>如果AI初具思想,它应该是什么样的?</p></a></div><div class="headswiper-item" data-v-0045335f><a data-report-query="spm=1000.2115.3001.5928" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdngeeknews/article/details/125254723&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5928&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/csdngeeknews/article/details/125254723&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-swiper\&quot;,\&quot;compDataId\&quot;:\&quot;www-Headlines\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141}&quot;}" target="_blank" href="https://blog.csdn.net/csdngeeknews/article/details/125254723" class="title" data-v-0045335f>谷歌研究员认为AI已具备人格,被罚带薪休假</a> <a target="_blank" href="https://blog.csdn.net/csdngeeknews/article/details/125254723" data-v-0045335f><p class="name" data-v-0045335f>罗永浩宣布退网创业|极客头条</p></a></div></div></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div><div class="headswiper-content" data-v-0045335f><!----></div></div></div></div> <div class="www-home-Broadcast"><div class="broadcast" data-v-6ebf2afa><div class="title" data-v-6ebf2afa><div class="top-title" data-v-6ebf2afa><img src="https://img-home.csdnimg.cn/images/20220107105446.png" alt data-v-6ebf2afa> <h3 data-v-6ebf2afa>直播</h3></div> <a target="_blank" href="https://live.csdn.net/?spm=1000.2115.3001.4124" data-v-6ebf2afa>更多<i class="el-icon-arrow-right" data-v-6ebf2afa></i></a></div> <div class="content" data-v-6ebf2afa><div class="www_live_item active" data-v-34c9c026 data-v-6ebf2afa><a href="https://live.csdn.net/room/m0_46700908/rBYvnVNY" data-report-query="utm_medium=distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-rBYvnVNY-null-null.nonecase&amp;depth_1-utm_source=distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-rBYvnVNY-null-null.nonecase&amp;spm=1000.2115.3001.5950" data-report-click="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/m0_46700908/rBYvnVNY&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-rBYvnVNY-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655186119224_44062\&quot;}&quot;}" data-report-view="{&quot;spm&quot;:&quot;1000.2115.3001.5950&quot;,&quot;dest&quot;:&quot;https://live.csdn.net/room/m0_46700908/rBYvnVNY&quot;,&quot;extra&quot;:&quot;{\&quot;fId\&quot;:558,\&quot;fName\&quot;:\&quot;floor-www-index\&quot;,\&quot;compName\&quot;:\&quot;www-home-silde\&quot;,\&quot;compDataId\&quot;:\&quot;index_live_video_elite_list\&quot;,\&quot;fTitle\&quot;:\&quot;\&quot;,\&quot;pageId\&quot;:141,\&quot;utm_medium\&quot;:\&quot;distribute.pc_feed_video_elite.none-task-liveroom-csdn#pc_feed_video_elite#liveroom-1-rBYvnVNY-null-null.nonecase\&quot;,\&quot;dist_request_id\&quot;:\&quot;1655186119224_44062\&quot;}&quot;}" data-v-34c9c026><div class="www_live_item_top" data-v-34c9c026><div class="img" style="background-image:url(https://live-file.csdnimg.cn/release/live/file/1654672189981.png?x-oss-process=image/resize,l_1000);" data-v-34c9c026></div> <img src="https://csdnimg.cn/release/cmsfe/public/img/nowlive.480a0975.gif" alt class="live-type" data-v-34c9c026> <!----> <!----> <span class="live-text" data-v-34c9c026>正在直播</span> <div class="back" data-v-34c9c026></div> <!----> <div class="hover_mask" data-v-34c9c026><img src="https://csdnimg.cn/release/cmsfe/public/img/play.9956ea53.png" alt data-v-34c9c026></div></div> <div class="content-text" data-v-34c9c026><h3 data-v-34c9c026> CSDN云原生系列在线峰会:Serverless峰会 </h3> <!----> <span class="text" data-v-34c9c026>690 热度</span></div></a></div><div class="www_live_item active" data-v-34c9c026 data-v-6ebf2afa><a href="https://live.csdn.net/room/MicrosoftReactor/fld1I524" data-report-query="utm_medium=distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-2-fld1I524-null-null.nonecase&amp;depth_1-utm_source=distribute.pc_feed_video_elite.none-task-subscribe-liveroom-csdn#pc_feed_video_elite#subscribe-liveroom-2-fld1I524-null-null.nonecase&amp;spm=1000.2115.3001.5950" data-report-这就是我们抓取的CSND首页的响应信息,我们仔细一看这不就是CSND页面的HTML确实如此,你请求内容就是为了访问这个网页,所以自然而然返回一个html网页!请求我们再通过请求协议报格式对刚刚抓取的协议请求进行对比学习!请求行GET https://www.csdn.net/ HTTP/1.1MethodGET这里的Method就是http采取的请求方法!HTTP协议针对不同的请求或者响应可以通过不同的方法进行传输.上表就是HTTP协议数据传输可以采取的方法!每种方法对应不同的应用场景!例如:我们这里抓取的的GET方法通常情况下就是为了获取一个资源,我们就是为了获取CSDN的主页面!但是这里的方法随着时间的推移,都混用,已经没有上表中的限制了,很多方法丧失了他本来的特性现在大部分程序员在大部分场景中都习惯于GET和POST而且经常两个混用!URLhttps://www.csdn.net/URL就是资源唯一标准限定符,就是标志了一个资源具体的网络中的位置!URL基本格式协议方案名这里的HTTPS://就是协议类型方案名!就比如我们之前学的数据库编程中的jdbc:mysql一样!登入信息(认证)这是请求的URL这里一般是用户的登入信息,现在的URL舍去了这部分内容!服务器地址www.csdn.net就是CSDN网站服务器地址服务器端口号这里的服务器端口号就和我们所学的五元组中的端口号一样!这里也一般省略!因为不同的协议类型它有固定的端口号!HTTP协议默认端口号是80HTTPS协议的默认端口号是443但我们加上这个端口号浏览器也能访问!所以直接省略了这个端口号!带层次的文件路径这里的路径就指出了我们请求要拿的资源在主机中的位置!这个路径可能是主机磁盘的真实路径也可能是虚拟的由服务器代码动态构造的路径下的资源!查询字符串(query string)这里的查询字符串一般由键值对的形式构成!https://editor.csdn.net/md?not_checkout=1&articleId=125276146比如我们现在写博客的这个网页的URL这里的查询字符串,就是not_checkout=1&articleId=125276146键值之间用=连接键值对之间用&分割!然后查询字符串和文件路径用?分割!注意:这里的查询字符串完全是由设计该协议的程序员编写的,具体每个键值对代表的内容信息咱也看不懂!片段标识符描述了当前访问的内容是属于当前这个HTML的那个子部分,就例如书签一样,可以直接定位我们之前浏览过的地方!VersionVersion就是HTTP协议的版本号!这里是HTTP/1.1就代表当前的http协议使用的1.1版本!现在的版本号有10/1.1/2.0/3.0而最常用的版本号就是1.1版本!请求报头像上述这些都是请求报头的内容,这里的请求报头是键值对的格式!键和值之间用:分割开!键值对之间用;分割开!空行空行为了分割请求报头和请求正文!请求正文请求正文,有的请求协议报文含有这个正文信息,也可以没有正文信息!这里的Query String有一些需要注意问题urlencode将查询字符串中的值转义!因为我们知道这里的URL有许多字符含有特殊意义,?分割文件路径和查询字符串&分割键值对等等…所以有些字符就需要通过转移,避免URL无法解析!还有这里的查询字符串不能使用中文,使用中文字符也需要通过urlencode转义!urlencode规则我们通过ASCII码进行转义!比如我们需要搜索C++https://www.baidu.com/baidu?tn=monline_4_dg&ie=utf-8&wd=C%2B%2B得到的查询字符串是C%2B%2B这就是查询的C++!因为+在ASCII码表对应的16进制数据就是2B然后再在前面加上%即可完成转义!那我们如果要写一个查询字符串,然后又要对照ASCII码表那得多麻烦!我们可以通过现成的在线网站进行url encode我们可以urlencode也能将已经进行了转义的字符进行urldecode还原!响应状态栏HTTP/1.1 200 OKversion就是这个响应的HTTP版本状态码描述这个请求的响应结果是否请求成功,或者失败!一般由一些数字构成,每个数字有特殊的意义,我们后面进行学习!状态码描述就是告诉我们这个状态码具体描述是什么,请求成功了还是失败!响应报头响应报文也是由键值对构成,描述了该协议的一些重要信息!空行分割响应报头和响应正文响应正文响应正文,就是根据这个请求服务器给出的响应内容,这里是一个HTML网页!HTTP方法我们刚刚了解到HTTP方法有如上几种当时由于某些原因,大部分程序员都不会按照上述方法设计的方式,进行使用!现在都是GET和POST使用的最为广泛,并且两者混用!也是就是说GET能做的POST也可以完成,反正也是如此!我们找两个来对比一下:我们的GET将请求的信息保存在querystring查询字符串中!而POST方法并没有使用querystring,采用的是body去传输一下数据信息!如下所示那GET方法和POST方法有啥区别呢?这两个方法没有本质区别,使用的场景都可以混用!语义上,GET是用来取数据获取资源的,POST是用来传数据,传输资源的通常情况下,GET方法是没有body,要通过querystring向服务器传输信息,POST方法是没有querystring的需要通过body传输数据信息!这里指的是通常情况,你也可以给GET搞个body,POST搞个querystring!GET方法请求一般是幂等的,POST一般是不幂等的(建议这样)幂等就是说每次输入得到的数据是确定的,不幂等就是对结果不确定!GET可以被缓存,POST不能被缓存!因为GET是幂等得到的数据时确定的就可以将结果缓存,如果下次请求直接拿缓存数据就好了!就比如一个网页如果不论谁访问,什么时间访问得到的结果都是一样的那就是可以被缓存!
写在前面这遍博客主要是记录博主期末操作系统速成的笔记!博主平时不听课,全靠期末速成学霸可以自行离开笔记内容来自于B站UP主:夜连三 视频:【操作系统】操作系统期末考试不挂科 大题详解 从0开始的不挂科生活 内容,学渣拯救者!!!40分钟速成,大家可以去看!常考的五大题型缺页问题计算物理地址银行家算法磁盘调度进程调度缺页问题三类算法:先进先出FIFO(First Input First Output)最佳置换算法OPT(Optimal replacement)最近最少使用LRU(Least Recently used)例题:在一个请求分页系统中,假如一个作业的页面走向为:1.2.3.4.5.1.4.1.2.3.4.5 ,当分配给该作业的物理块数为4时,分别采用FIFO,LRU,OPT算法,计算访问过程中发生的缺页次数和缺页率!解题步骤:读题划重点信息 :页面走向,物理块数!画表 页面走向第一行!开局4缺页!因为这4个页面都要放入到cpu中!FIFO算法要点:先进先出!看那个页面最先进就替换!每次替换左边出现次数最多的页面!看谁长,谁长替换谁!缺页次数为10 缺页率:5/6OPT算法要点:最佳替换算法,看已经在物理页中的页面在右边最后使用到的先替换!淘汰的页面将是未来长时间内不再被访问的页面!就比如:开始1234 然后5要替换,后面有 14123显然 3最后使用!所以先替换3!缺页次数为6 缺页率:1/2LRU算法要点:最近最少使用算法!最近未使用!看需要添加页面的左边,页面走向哪个页面理该页面最远就替换该页面!!!直接看页面走向就可以!这里要和FIFO区分开!!!先进先出,看哪个物理页最长!LRU最近最少使用,看页面走向哪个理该页面最远!缺页:9 缺页率:3/4总结:缺页问题,先画图,页面走向数+1为列,物理块+2为行!开局都缺页!FIFO:看(整个表)左边,谁长替换谁!OPT:看(表头)右边,谁最远替换谁!LRU:看(表头)左边,谁最远替换谁!计算物理地址物理地址计算3步曲!求出页号对照页表计算地址地址转换:绝对地址 = 块号*块长 + 块内地址例题:在采用页式存储管理的系统中,某进程的逻辑地址空间为4页(每页2048字节),且已知该进程的页面映像表(页表)如下:计算有效逻辑地址4865所对应的物理地址.解题:读题划重点: 每页多少字节, 页表,有效逻辑地址!3步曲解题!页号:页号 = 逻辑地址/每页字节数d = 4065/2048 = 2对照页表:根据页号找到块号!看页表 ,页号2对应块号6!数地址:绝对地址 = 块号*块长 + 块内地址块内地址= 逻辑地址%每页字节数块内地址: 4065%2048 = 769地址:6*2048 + 769 = 13057银行家算法2种题型判断系统是否"死锁"提供安全系列例题:假定系统中有5个进程{ P0,P1,P2,P3,P4}和三类资源{A,B,C},各种资源的数量分别是10,5,7,在T0时刻的资源分配情况如下图所示:1.判断在T0时刻的安全性?2.P1请求资源:P1发出请求向量Request(1,0,2),系统按照银行家算法进行检查!3.P4请求资源:P4发出请求向量Request(3,3,0),系统按照银行家算法进行检验!MAX:进程所需资源Allocation:系统已经分配给进程的资源数Need:进程还需要的资源数; Need=MAX - AllocationAvailable:系统剩余的资源数;Available=Total-AllAllocation解题:第一问:题目可能会给我们资源分配表,也可能不给我们,我们需要创建该表进行解题!Need是我们需要计算的! MAX - AllocationAvailable: Total(题目给的总资源数) - AllAllocation(全部已经分配的资源数)画一张新表Work:当前所剩资源work+allocation:计算机处理完当前进程后所剩资源!我们根据上面的表得到的Available剩余进程数,看那个进程Need<=Available先执行填入到work中然后一步步将该表完善!1如果最后一行得到的work+allocation与题目所给的总资源数一样说明结果正确!如果都可以完成说明进程安全!p1,p3,p4,p0,p2就是一组线程安全序列(不唯一)第二问:解题步骤:1.先判断请求是否<=所需:Request(1,0,2)<=Need(1,2,2)2.判断请求是否<=系统所剩的:Request(1,0,2)<=Available(3,3,2)3.根据请求资源量进行分配(更改表)4.列表计算根据上表可以看到如果满足Request<=Need&&Request<=Available然后就更改表: Need -=Request Allocation+=Request Available-=RequestRequest(1,0,2)满足!然后通过更改后的表进行第一问的操作! 线程安全序列:P1,P3,P0,P2,P4第三问:解题步骤:和第二问相同,注意这里要在第二问的基础上,如果第二问请求成功的话!1.判断请求是否<=所需 :Request(3,3,0)<=Need(4,3,1) true2.判断请求是否<=系统剩余:Request(3,3,0)<=Available(2,3,0) false线程不安全!不需要进行下面的计算了!磁盘调度4种算法!先来先服务FCFS最短寻道时间优先SSTF扫描算法SCAN循环扫描算法C-SCAN例题:一个磁盘驱动器有150个柱面,考虑一个磁盘队列,它按照到达时间顺序分别是35,52,37,17,80,120,135,104如果读写磁头最初位于柱面90,请使用FCFS,SSTF,SCAN,CSCAN算法求总寻道长度和平均寻道长度. FCFS 先来先服务!到达顺序就是服务顺序!记得磁头一开始位置90!//磁头移动顺序: 90 -> 35 -> 52 -> 37 -> 17 -> 80 -> 120 -> 135 -> 104 //总寻道长度 = (90-35) + (52-35) + (52-37) + ... + (135-120) + (135-104) = 256总寻道长度就是将磁头移动的总距离!平均寻道长度 = 总长/移动次数256/8 = 32SSTF 最短寻道时间优先向画一条数轴!然后根据初始磁头位置90,判断两边距离,往距离更短的一遍走!//磁头移动顺序: 90 -> 80 -> 104 -> 120 -> 135 -> 52 -> 37 -> 35 -> 17 //总寻道长度: //平均寻道长度:SCAN 扫描算法看刚刚的数轴,分成左右两部分!//磁头移动顺序: //移动顺序分两种 //1.向左移动完再往右边最近优先寻道! 90-> 80 -> 52 -> 37 -> 35 -> 17 -> 104 -> 120 -> 135 //2.向右移动完再往左边最近优先寻道! 90 -> 104 -> 120 -> 135 -> 80 -> 52 -> 37 -> 35 -> 17CSCAN 循环扫描算法这里和扫描算法的不同是先左(右)扫描完,直接到最右(左)边开始扫描!//磁头移动顺序: //移动顺序分两种 //1.向左移动完再往右边最远优先寻道! 90-> 80 -> 52 -> 37 -> 35 -> 17 -> 135 -> 120 ->104 //2.向右移动完再往左边最远优先寻道! 90 -> 104 -> 120 -> 135 -> 17 -> 35 -> 37 -> 52 -> 80进程调度3类算法先来先服务FCFS(First Come First Service)短作业优先SJF(Shortest Job First)高响应比优先HRRN(Highest Response Ratio Next)(了解即可)导学到达时间(提交时间):就是进程告诉操作系统要开始处理的时间点,进程进入就绪队列等待等待处理!开始时间:就是操作系统真正开始处理该进程的时间点执行时间(CPU突发时间):就是操作系统处理这个进程的时间例题:从P1到P4有4个进程,每个进程的到达时间和运行时间如下表所示:先来先服务调度算法:按照进程到达先后顺序执行进程:P1,P2,P3,P4方法一:等待时间 = 开始时间 - 到达时间周转时间 = 结束时间 - 到达时间(就是进程从就绪队列等待开始到进程结束所需要的时间)带权周转时间 = 周转时间/执行时间方法二:Gantt图这种方法比较直观!操作快推荐使用!等待时间: 进程开始时间 - 到达时间周转时间 : 用进程的结束时间 - 到达时间短作业优先调度算法SJF非抢占:就是进程从开始执行就一直是该进程执行到结束再执行其他进程按照进程长度,进程越短越先执行: P1,P2,P4,P3抢占 :就是有其他进程抢占执行,当一个进程到达后,发现自己的执行时间比正在执行进程的所需的剩余时间短,就抢占执行该进程!抢占执行使用Gantt图更加直观!这里就不画表格,表格比较麻烦!高响应比优先调度算法:按优先权 = (等待时间+执行时间)/执行时间优先执行等待时间长执行时间短的进程!非抢占:先执行P1计算后面3个进程优先权:P2 = (7+4)/4P3 = (5+5)/5p4 = (6+9)/9这里P2的优先权更大!先执行P2
本章要点学习网络编程的基本原理熟悉网络协议分层掌握网络编程的协议网络发展史独立模式计算机之间的数据相互独立!计算机之间不能进行数据分享和连接网络互联随着时代的发展,越来越需要计算机之间互相通信,共享软件和数据,即以多个计算机协同工作来完成业务,就有了网络互连。网络互连: 将多台计算机连接在一起,完成数据共享。数据共享本质是网络数据传输,即计算机之间通过网络来传输数据,也称为网络通信。根据网络互连的规模不同,可以划分为局域网和广域网。局域网局域网,即 Local Area Network,简称LAN。Local 即标识了局域网是本地,局部组建的一种私有网络。局域网内的主机之间能方便的进行网络通信,又称为内网;局域网和局域网之间在没有连接的情况下,是无法通信的。局域网组建网络的方式有很多种:交换机上的接口都是对等的,一样的!通过交换机,将多个设备组建了一个局域网!基于交换机和路由器组建路由器中有两个接口LAN口和WAN口!其中插在LAN口中的设备在一个局域网里通过WAN口连接到另一个局域网里!所以这里的A局域网是在B局域网中!上述的交换机和路由器指的是传统的!现在的交换机和路由器的功能都大致一样!交换机有的功能路由器也有!没有明确的界限了!通过交换机/路由器组建起来的都是局域网!广域网广域网,即 Wide Area Network,简称WAN。通过路由器,将多个局域网连接起来,在物理上组成很大范围的网络,就形成了广域网。广域网内部的局域网都属于其子网。而广域网和局域网并没有明确的界限!一般认为,范围较大的局域网就可称作广域网!全球最大的广域网internet(因特网)通信基础IP地址描述了网络上一个主机的位置(地址)IP地址本质是一个32位的整数,不方便记忆,所以我们通常将32位的整数将每个字节划分成一组,分为4组,中间用.分隔开.(点分十进制)127.0.0.1特殊的IP环回ip,表示自己的主机!也就是每一组的范围是0-255!一个字节端口号描述了主机上的某个程序(电话号码)本质是16位(2个字节)的无符号整数!范围为0-655353306MySQL默认端口号!每当我们开启一个程序,系统就会给这个程序随机分配一个端口号!而服务器需要默认绑定一个端口号,便于用户访问!认识协议需要进行有效通信,前提就是明确通信协议!协议也就是约定!我们约定好发送的数据是咋样的格式,接收的时候就通过这样的格式解析!网络通信的时候,本质上传输的是光信号和电信号!通过光信号的频率(高频率/低频率),电信号的电平(高电平/低电平)来表示是0和1!学习网络原理就需要研究很多协议!五元组在TCP/IP协议中,用五元组来标识一个网络通信:源IP:标识源主机源端口号:标识源主机中该次通信发送数据的进程目的IP:标识目的主机目的端口号:标识目的主机中该次通信接收数据的进程协议号:标识发送进程和接收进程双方约定的数据格式就如同我们的包裹信息!协议分层对于网络协议来说,往往分成几个层次进行定义。为啥要进行协议分层呢?就比如我们写代码,如果要写一个项目的话!需要多人分工合作!项目不可能一个人就可以独立完成,需要将这个项目拆成多个板块!每个成员负责不同的板块,每个人只需要掌握自己负责的代码就好了,并不需要了解一整个项目,减少成本,更好的封装!而我们的网络协议也是如此,有很多细节,这个协议也十分庞大!我们不可能把所有细节掌握!所有需要通过协议分层(对应了代码封装),将协议分拆成多个小的协议!而我们不同分工的人,也并不需要掌握全部的网络协议!协议分层好处每层协议不需要理解其他层的协议细节(分装)打电话的人并不需要知道电话是如何实现的!实现电话的人也不需要知道打电话的人用啥语言打电话方便将对应层的协议换成其他协议(更好的解耦合)打电话的人可以将汉语协议换成英语协议,并不会影响!网络分层模型OSI七层网络模型我们一般将应用层,表示层,会话层这三层用应用层代替!也就是简化了7层模型!OSI七层网络简化版本为TCP/IP五层(四层) 网络模型!我们程序员有时候又将物理层和数据链路层分为一层,因为偏硬件!所以也能说是四层网络模型!可以看到我们普通程序员只要学会应用层的协议就可以进行日常开发了!传输层和网络层 由编写操作系统的人员实现!而数据链路层和物理层由编写驱动的人员实现就好了!TCP/IP五层(四层)网络模型我们来了解一下具体每层的作用!从细节到宏观! 底层开始!物理层网络通信中的硬件设备!网线/网卡,针对硬件设备的约定,物理层协议负责的范畴,需要保证所有主机和网络设备之间都是匹配的!数据链路层负责完成相邻设备之间网络通信(局部)网络层点到点之间的网络通信(全局),大部分是不相邻,网络层负责规划好这两点之间合适的线路,因为设备之间不止一条线路!传输层负责端到端之间的通信,起点和终点,只是关注结果数据是否能到达,不关注过程(宏观)应用层和应用程序密切相关,你传输的这个数据是干啥用的,不同的应用程序有不同的用途!我来类比我们生活中的网购,便于理解上面五层网络协议!首先应用层就是买家和卖家,买家需要知道买的这个物品有什么用途,才会去购买,卖家也需要他卖的商品的用途,才可以卖出去!传输层就好比,是商家要确认该商品是否能送到你的手上,选择合适的快递公司,就是快递是否能到达你的位置,不然卖个毛线!网络层就好比快递公司,拿到商品好,需要规划合适的线路!然后包裹就到了快递小哥手上!数据链路层这就好比快递小哥送包裹,点到点之间有不同的小哥负责!物理层硬件设备就好比,公路,传输通道!提供传输基础,能顺利到达买家手上!我们对应到相应的设备主机对应了物理层到应用层,包括了这五层传输协议,把这五层都实现了!路由器主要是物理层到网络层(主要实现了,物理层,数据链路层,网络层)交换机主要是物理层到数据链路层(主要实现了物理层,数据链路层)封装和分用封装和分用是网络分层中的一组重要概念!这里的封装并不是像java中的封装一样,指的是不同的网络分层协议之间是如何相互配合的!我们暂且理解为封装就是包快递,分用就是拆快递!封装例如我们的微信程序,A给B发送了一条信息,内容是hello!那么A主机是如何进行传输到达B的主机上的呢?应用层(微信应用程序)根据A输出的内容,就会将数据构造成一个应用层的协议报文(我们知道协议就是约定,协议报文就是遵守这个约定的一组数据)!然后微信就会通过微信的内部代码,根据应用层协议,将协议报文封装成应用层数据报文我们这里需要知道,我们的应用层协议是由程序员编写的不同的应用程序有不同的应用层协议,我们是无法得知里面中的协议内容的,不像其他层(数据链路层,传输层,网络层)已经由操作系统,驱动,硬件编写好了!应用层进行封装后,调用操作系统提供的API (socket api),将数据报文传送给了传输层(操作系统内核)传输层(操作系统内核)数据到了传输层,根据传输层协议(TCP/UDP),将数据报文构造成了一个传输层协议报文!我们的传输层协议有很多种(TCP,UDP…)我们这里以TCP协议举例!TCP报头信息:源端口,目的端口端口号(商家和买家的联系电话)这里的TCP报头+数据载荷, 可以看成是字符串连接,就是将,这里的连接是二级制的连接!然后传输层将TCP协议报文,交给了网络层!网络层(操作系统内核)网络层拿到数据后,根据网络层协议(IP),将传输层协议数据,进一步进行封装,加上了一个IP报文,构造成了一个IP数据包!IP报头信息:源IP和目的IP主机A和主机B的IP地址(商家和买家的地址)网络层进行封装后将数据交给了数据链路层(驱动程序)!数据链路层(驱动程序)数据链路层拿到IP协议报文后,根据数据链路层,将数据进一步封装,构造一个数据链路层的数据报,典型的数据链路层协议是以太网,就会构造成一个"以太网数据帧"!帧头信息:两个相邻设备的地址信息.我们知道IP报头包含的是起始位置和终点位置的地址,而以太网数据帧头包含的是相邻连个节点的地址,根据传输,该地址信息一直在变化!帧尾信息:数据校验和,校验传输的数据是否正确,我们后面在进行介绍!经过数据链路层进行封装后,将数据传输给了物理层(硬件设备)物理层(硬件设备)然后数据就离开了主机A!到了下一个设备,可能是路由器或者/交换机 在到达B主机前要经过一系列的设备,这里我们就不进行研究,我们直接看到达B主机后的操作!分用我们的分用,就是封装的逆过程,将刚刚拿到的包裹,进行拆过程!物理层(硬件设备)我们的B主机网卡感知到了一组高低电平,就会将电平翻译成0/1二进制,这一串二进制,就是以太网数据帧,然后间以太网数据帧,交给了数据链路层!数据链路层(驱动程序)数据链路层,拿到以太网数据帧后,对数据进行解析,去掉帧头和帧尾!取出IP数据报交给了网络层!网络层(操作系统内核)网络层,拿到IP协议报文后,对数据进行了解析,去掉了IP报头,取下TCP报文,交给了传输层传输层(操作系统内核)传输层拿到数据后,对数据进行解析,去掉TCP报头,取下应用层数据!交给应用层处理应用层(微信应用程序)应用层拿到数据,调用操作系统中的socket api读取到操作系统内核中的数据报,解析数据,显示在B对话框中!还有中间的很多连接设备,要进行封装,分用!中间可能有很多的设备,但是无论多复杂,都是对数据进行不停的封装和分用直至传输成功!
现代Java应用架构越来越强调数据存储和处理分离,以获得更好的可维护性、可扩展性以及可移植性,比如火热的微服务就是一种典型。这种架构通常要求业务逻辑要在Java程序中实现,而不是像传统应用架构中放在数据库中。应用中的业务逻辑大都会涉及结构化数据处理。数据库(SQL)中对这类任务有较丰富的支持,可以相对简易地实现业务逻辑。但Java却一直缺乏这类基础支持,导致用Java实现业务逻辑非常繁琐低效。结果,虽然架构上有各种优势,但开发效率却反而大幅下降了。如果我们在Java中也提供有一套完整的结构化数据处理和计算类库,那这个问题就能得到解决:即享受到架构的优势,又不致于降低开发效率。需要什么样的能力?Java下理想的结构化数据处理类库应当具备哪些特征呢?我们可以从SQL来总结:1 集合运算能力结构化数据经常是批量(以集合形式)出现的,为了方便地计算这类数据,有必要提供足够的集合运算能力。如果没有集合运算类库,只有数组(相当于集合)这种基础数据类型,我们要对集合成员做个简单地求和也需要写四五行循环语句才能完成,过滤、分组聚合等运算则要写出数百行代码了。SQL提供有较丰富的集合运算,如 SUM/COUNT 等聚合运算,WHERE 用于过滤、GROUP 用于分组,也支持针对集合的交、并、差等基本运算。这样写出来的代码就会短小很多。2 Lambda语法有了集合运算能力是否就够了呢?假如我们为 Java 开发一批的集合运算类库,是否就可以达到 SQL 的效果呢?没有这么简单!以过滤运算为例。过滤通常需要一个条件,把满足条件的集合成员保留。在 SQL 中这个条件是以一个表达式形式出现的,比如写 WHERE x>0,就表示保留那些使得 x>0 计算结果为真的成员。这个表达式 x>0 并不是在执行这个语句之前先计算好的,而是在遍历时针对每个集合成员计算的。本质上,这个表达式本质上是一个函数,是一个以当前集合成员为参数的函数。对于WHERE运算而言,相当于把一个用表达式定义的函数用作了 WHERE 的参数。这种写法有一个术语叫做 Lambda 语法,或者叫函数式语言。如果没有Lambda语法,我们就要经常临时定义函数,代码会非常繁琐,还容易发生名字冲突。SQL中大量使用了Lambda语法,不在于必须过滤、分组运算中,在计算列等不必须的场景也可以使用,大大简化了代码。3 在 Lambda 语法中直接引用字段结构化数据并非简单的单值,而是带有字段的记录。我们发现,SQL 的表达式参数中引用记录字段时,大多数情况可以直接使用字段名称而不必指明字段所属的记录,只有在多个同名字段时才需要冠以表名(或别名)以区分。新版本的 Java 虽然也开始支持 Lambda 语法了,但只能把当前记录作为参数传入这个用 Lambda 语法定义的函数,然后再写计算式时就总要带上这个记录。比如用单价和数量计算金额时,如果用于表示当前成员的参数名为 x,则需要写成“x. 单价 *x. 数量”这种啰嗦的形式。而在SQL中可以更为直观地写成 " 单价 * 数量”。4 动态数据结构SQL还能很好地支持动态数据结构。结构化数据计算中,返回值经常也是有结构的数据,而结果数据结构和运算相关,没办法在代码编写之前就先准备好。所以需要支持动态的数据结构能力。SQL中任何一个 SELECT 语句都会产生一个新的数据结构,在代码中可以随意添加删除字段,而不必事先定义结构(类)。Java 这类语言则不行,在代码编译阶段就要把用到的结构(类)都定义好,原则上不能在执行过程中动态产生新的结构。5 解释型语言从前面几条的分析,我们已经可以得到结论:Java 本身并不适合用作结构化数据处理的语言。它的 Lambda 机制不支持特征 3,而且作为编译型语言,也不能实现特征 4。其实,前面说到的 Lambda 语法也不太适合采用编译型语言来实现。编译器不能确定这个写到参数位置的表达式是应该当场计算出表达式的值再传递,还是把整个表达式编译成一个函数传递,需要再设计更多的语法符号加以区分。而解释型语言则没有这个问题,作为参数的表达式是先计算还是遍历集合成员时再计算,可以由函数本身来决定。SQL确实是解释型语言。引入 SPLStream是Java8以官方身份推出的结构化数据处理类库,但并不符合上述的要求。它没有专业的结构化数据类型,缺乏很多重要的结构化数据计算函数,不是解释型语言,不支持动态数据类型,Lambda语法的接口复杂。Kotlin属于Java生态系统的一部分,它在Stream的基础上进行了小幅改进,也提供了结构化数据计算类型,但因为结构化数据计算函数不足,不是解释型语言,不支持动态数据类型,Lambda语法的接口复杂,仍然不是理想的结构化数据计算类库。Scala提供了较丰富的结构化数据计算函数,但编译型语言的特点,也使它不能成为理想的结构化数据计算类库。那么,Java生态下还有什么可以用呢?集算器SPL。SPL是由Java解释执行的程序语言,具备丰富的结构化数据计算类库、简单的Lambda语法和方便易用的动态数据结构,是Java下理想的结构化处理类库。丰富的集合运算函数SPL提供了专业的结构化数据类型,即序表。和SQL的数据表一样,序表是批量记录组成的集合,具有结构化数据类型的一般功能,下面举例说明。解析源数据并生成序表:Orders=T("d:/Orders.csv")按列名从原序表生成新的序表:Orders.new(OrderID, Amount, OrderDate)计算列:Orders.new(OrderID, Amount, year(OrderDate))字段改名:Orders.new(OrderID:ID, SellerId, year(OrderDate):y)按序号使用字段:Orders.groups(year(_5),_2; sum(_4))序表改名(左关联)join@1(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))序表支持所有的结构化计算函数,计算结果也同样是序表,而不是Map之类的数据类型。比如对分组汇总的结果,继续进行结构化数据处理:Orders.groups(year(OrderDate):y; sum(Amount):m).new(y:OrderYear, m*0.2:discount)在序表的基础上,SPL提供了丰富的结构化数据计算函数,比如过滤、排序、分组、去重、改名、计算列、关联、子查询、集合计算、有序计算等。这些函数具有强大的计算能力,无须硬编码辅助,就能独立完成计算:组合查询:Orders.select(Amount>1000 && Amount<=3000 && like(Client,"*bro*")) 1排序:Orders.sort(-Client,Amount)分组汇总:Orders.groups(year(OrderDate),Client; sum(Amount))内关联:join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))简洁的Lambda语法SPL支持简单的Lambda语法,无须定义函数名和函数体,可以直接用表达式当作函数的参数,比如过滤:Orders.select(Amount>1000)修改业务逻辑时,也不用重构函数,只须简单修改表达式:Orders.select(Amount>1000 && Amount<2000)SPL是解释型语言,使用参数表达式时不必明确定义参数类型,使Lambda接口更简单。比如计算平方和,想在sum的过程中算平方,可以直观写作:Orders.sum(Amount*Amount)和SQL类似,SPL语法也支持在单表计算时直接使用字段名:Orders.sort(-Client, Amount)动态数据结构SPL是解释型语言,天然支持动态数据结构,可以根据计算结果结构动态生成新序表。特别适合计算列、分组汇总、关联这类计算,比如直接对分组汇总的结果再计算:Orders.groups(Client;sum(Amount):amt).select(amt>1000 && like(Client,"*S*"))或直接对关联计算的结果再计算:join(Orders:o,SellerId ; Employees:e,Eid).groups(e.Dept; sum(o.Amount))较复杂的计算通常都要拆成多个步骤,每个中间结果的数据结构几乎都不同。SPL支持动态数据结构,不必先定义这些中间结果的结构。比如,根据某年的客户回款记录表,计算每个月的回款额都在前10名的客户:Sales2021.group(month(sellDate)).(~.groups(Client;sum(Amount):sumValue)).(~.sort(-sumValue)) .(~.select(#<=10)).(~.(Client)).isect()直接执行SQLSPL中还实现了SQL的解释器,可以直接执行SQL,从基本的WHERE、GROUP到JOIN、甚至WITH都能支持:$select * from d:/Orders.csv where (OrderDate<date('2020-01-01') and Amount<=100)or (OrderDate>=date('2020-12-31') and Amount>100)$select year(OrderDate),Client ,sum(Amount),count(1) from d:/Orders.csv group by year(OrderDate),Client having sum(Amount)<=100$select o.OrderId,o.Client,e.Name e.Dept from d:/Orders.csv o join d:/Employees.csv e on o.SellerId=e.Eid$with t as (select Client ,sum(amount) s from d:/Orders.csv group by Client) select t.Client, t.s, ct.Name, ct.address from t left join ClientTable ct on t.Client=ct.Client更多语言优势作为专业的结构化数据处理语言,SPL不仅覆盖了SQL的所有计算能力,在语言方面,还有更强大的优势:离散性及其支挂下的更彻底的集合化集合化是SQL的基本特性,即支持数据以集合的形式参与运算。但SQL的离散性很不好,所有集合成员必须作为一个整体参于运算,不能游离在集合之外。而Java等高级语言则支持很好的离散性,数组成员可以单独运算。但是,更彻底的集合化需要离散性来支持,集合成员可以游离在集合之外,并与其它数据随意构成新的集合参与运算 。SPL兼具了SQL的集合化和Java的离散性,从而可以实现更彻底的集合化。比如,SPL中很容易表达“集合的集合”,适合分组后计算。比如,找到各科成绩均在前10名的学生:有序计算是离散性和集合化的典型结合产物,成员的次序在集合中才有意义,这要求集合化,有序计算时又要将每个成员与相邻成员区分开,会强调离散性。SPL兼具集合化和离散性,天然支持有序计算。具体来说,SPL可以按绝对位置引用成员,比如,取第3条订单可以写成Orders(3),取第1、3、5条记录可以写成Orders([1,3,5])。SPL也可以按相对位置引用成员,比如,计算每条记录相对于上一条记录的金额增长率:Orders.derive(amount/amount[-1]-1)SPL还可以用#代表当前记录的序号,比如把员工按序号分成两组,奇数序号一组,偶数序号一组:Employees.group(#%2==1)更方便的函数语法大量功能强大的结构化数据计算函数,这本来是一件好事,但这会让相似功能的函数不容易区分。无形中提高了学习难度。SPL提供了特有的函数选项语法,功能相似的函数可以共用一个函数名,只用函数选项区分差别。比如select函数的基本功能是过滤,如果只过滤出符合条件的第1条记录,只须使用选项@1:Orders.select@1(Amount>1000)数据量较大时,用并行计算提高性能,只须改为选项@m:Orders.select@m(Amount>1000)对排序过的数据,用二分法进行快速过滤,可用@b:Orders.select@b(Amount>1000)函数选项还可以组合搭配,比如:Orders.select@1b(Amount>1000)结构化运算函数的参数常常很复杂,比如SQL就需要用各种关键字把一条语句的参数分隔成多个组,但这会动用很多关键字,也使语句结构不统一。SPL支持层次参数,通过分号、逗号、冒号自高而低将参数分为三层,用通用的方式简化复杂参数的表达:join(Orders:o,SellerId ; Employees:e,EId)扩展的Lambda语法普通的Lambda语法不仅要指明表达式(即函数形式的参数),还必须完整地定义表达式本身的参数,否则在数学形式上不够严密,这就让Lambda语法很繁琐。比如用循环函数select过滤集合A,只保留值为偶数的成员,一般形式是:A.select(f(x):{x%2==0} )这里的表达式是x%2==0,表达式的参数是f(x)里的x,x代表集合A里的成员,即循环变量。SPL用固定符号~代表循环变量,当参数是循环变量时就无须再定义参数了。在SPL中,上面的Lambda语法可以简写作:A.select(~ %2==0)普通Lambda语法必须定义表达式用到的每一个参数,除了循环变量外,常用的参数还有循环计数,如果把循环计数也定义到Lambda中,代码就更繁琐了。SPL用固定符号#代表循环计数变量。比如,用函数select过滤集合A,只保留序号是偶数的成员,SPL可以写作:A.select(# %2==0)相对位置经常出现在难度较大的计算中,而且相对位置本身就很难计算,当要使用相对位置时,参数的写法将非常繁琐。SPL用固定形式[序号]代表相对位置:无缝集成、低耦合、热切换作为用Java解释的脚本语言,SPL提供了JDBC驱动,可以无缝集成进Java应用程中。简单语句可以像SQL一样直接执行:… Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); PrepareStatement st = conn.prepareStatement("=T(\"D:/Orders.txt\").select(Amount>1000 && Amount<=3000 && like(Client,\"*S*\"))"); ResultSet result=st.execute(); ...复杂计算可以存成脚本文件,以存储过程方式调用… Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); Statement st = connection.(); CallableStatement st = conn.prepareCall("{call splscript1(?, ?)}"); st.setObject(1, 3000); st.setObject(2, 5000); ResultSet result=st.execute(); ...将脚本外置于Java程序,一方面可以降低代码耦合性,另一方面利用解释执行的特点还可以支持热切换,业务逻辑变动时只要修改脚本即可立即生效,不像使用Java时常常要重启整个应用。这种机制特别适合编写微服务架构中的业务处理逻辑。SPL资料SPL官网SPL下载SPL源代码
认识文件什么是文件呢?我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般.计算机中的文件就和我们现实中的文件相似!我们一般通过硬盘存储文件!所以我们硬盘下存储了好多文件例如:.java文件(我们编写的java代码) .jpg文件(图片也是文件),.txt文件(文本文件)…我们硬盘下的文件还有很多…我们这里就不一一举例了!不同的后缀名,为不同类型的文件!所以我们需要打开查看后缀名(默认是查看不了后缀名的),便于我们知道文件的类型!我们知道存储在硬盘上的就是文件,除了上面这些可以打开的文件,还有目录文件!目录(我们所说的文件夹)也是文件的一种!像这些文件夹都是文件的一种!在操作系统中,网卡也是一种文件,还有很多硬件设备都看作是文件!键盘,鼠标,打印机等等都看做是文件,还有些软件也被当做是文件!这样便于管理,操作同样的文件管理的代码可以操作所有的文件文件分类我们这里的分类站在程序员的角度!我们将文件分为两类:文本文件文本文件存储的是字符,我们知道一个字符,可能占多个字节单位!二进制文件二进制文件储存的是二进制数(0/1),二进制文件中的字节是分开的!我们刚刚还说文件有很多类…为啥现在就2类了,如何区分他们呢?如果用记事本打开不乱码就是文本文件,打开乱码就是二进制文件!可以看到jpg是二进制文件,很多文件都是二进制文件!像word那些也是二进制文件,因为他们有一些格式,属于富文本,也就是二进制! 只有像那些txt文件才是文本文件!目录结构我们硬盘下的文件都是由文件系统管理,就是我们电脑中的文件我们的文件由文件系统管理,就是我们此电脑下的硬盘!文件是由树形结构管理!采用多叉树的形式进行管理!所以通过多叉树的结构管理文件系统!我们的叶子节点就是一个文件!而非叶子节点就是目录文件!路径如果我们要描述一个文件所在的位置,我们可以采用两种描述方式!这两种描述方式是操作系统给我们提供的路径的方式,定位一个文件的位置!绝对路径绝对路径就是电脑我们打开一个文件目录,上方所看到的路径地址!D:\java\2022_1_13\src这里的test.java就是在这个路径里面!简单说就是如果这个路径第一个是盘符(C/D盘等),那么这个路径就是绝对路径,我们在电脑中任意位置都可以精准的找到这个文件!就相当于你身份证上的地址!超详细!相对路径相对路径,我们学过物理都知道,相对就是相对某个参考系而言,地址所在的位置!如果我们已经知道了,你的具体某个现地址,所以只要知道你的乡,村就可以找到你的家庭地址了!也就是我们需要有参考位置!参考这个目录文件,你所在的路径,就是相对路径!假如我们以D:\java\2022_1_13为参考地址也就是基准目录!那么out文件如何表示呢!./out这就表示了基准下的out相对路径了!如果我们要找基准的父文件目录中的文件呢?我们通过../的方式找到基准路径的父目录!../2022_1_12以D:\java\2022_1_13为基准路径表示2022_1_8文件位置!./ 表示当前目录!../表示上一级目录!还有你发现一个小细节没有!为啥路径可以用/也可以用\呢?一般情况下用/反斜杠表示路径!而windows支持用\表示路径!但是也支持/可以看到windows下输出的路径都是\!!!/便于我们编程使用,如果我们用/编译器就以为我们使用了转义字符,就比较繁琐!如果要使用\我们要对\转义操作!java文件操作我们java提供了一个File类,在java.io包下,我们通过这个类就可以完成对文件的操作,首先这个文件对象就描述了一个文件/目录,就可以实现很多功能!File文件和目录路径名的抽象表示。文件系统相关的操作文件系统相关的操作也就是我们看到的文件系统所具有的操作文件的功能,我们通过java代码也能实现!这里可执行的功能,我们通过java代码也都可以实现!例如:打开文件,删除文件,查看文件大小,日期…一系列关于文件的操作我们都可以进行!我们java有一组文件操作的API通过上述的方法就可以对文件进行管理!我们来演示几个常用的方法:package file; import java.io.File; import java.io.IOException; public class File1 { public static void main1(String[] args) { //我们创建一个File对象,传入路径 File file1 = new File("D:/1.txt"); File file2 = new File("D:/test.txt"); //判断该文件是否存在! System.out.println(file1.getName()+"是否存在:"+file1.exists()); System.out.println(file2.getName()+"是否存在:"+file2.exists()); } public static void main(String[] args) { File file = new File("D:/test.txt"); System.out.println("是否可读:"+file.canRead()); System.out.println("是否可写"+file.canWrite()); System.out.println(file.isDirectory()); System.out.println("路径:"+file.getAbsolutePath()); System.out.println("路径:"+file.getPath()); System.out.println("是否为目录文件:"+file.isDirectory()); System.out.println("父目录路径:"+file.getParent()); System.out.println("父目录文件:"+file.getParentFile()); //在D盘下创建 4_22.txt文件! //只是创建了一个文件对象!在硬盘中并没有文件! File newFile = new File("D:/4_22.txt"); try { //在硬盘中创建该文件! newFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } System.out.println("文件:"+newFile.getName()); System.out.println("文件大小:"+newFile.length()); System.out.println("删除文件:"+newFile.delete()); System.out.println("文件是否存在:"+newFile.exists()); } }我们D盘下并没有1.txt文件,所以文件不存在!我们通过文件对象File调用createNewFile()方法,就可以真正的在我们的计算机中创建该文件!//创建目录文件! public static void main(String[] args) { //创建目录! File file = new File("./aaa"); // 创建一级目录 mkdir 方法! file.mkdir(); System.out.println(file.isDirectory()); // 创建多级目录 mkdirs 方法! File file1 = new File("../aaa/bbb/ccc"); file1.mkdirs(); System.out.println(file1.isDirectory()); }
synchronized原理我们总结上面的锁策略,就可以总结出synchronized的一些特性(JDK1.8版本)自适应锁,根据锁竞争激烈程度,开始是乐观锁竞争加剧就变成悲观锁开始是轻量级锁,如果锁冲突加剧,那就变成重量级锁实现轻量级锁是采用自旋锁策略,重量级锁采用挂起等待锁策略是普通的互斥锁可重入锁加锁过程synchronized是如何做到自适应过程的呢?JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。我们用生活中的例子便于理解锁的变化!好比一个男生喜欢上了一个女生(漂亮又对男生超好)但是这个男生比较渣,不想和她纠缠,如果确认了关系,他就要要放弃一片森林了,但是他又想谈恋爱! 所以他选择和那个女生搞暧昧不确立关系(偏向锁)就是说避免了确立关系和分手的纠缠(避免了加锁解锁的开销),过了一段时间有一个其他的男生追这个女生,此时如果这个男生再不确立关系,就有可能失去女生,所以他马上和女生确立关系(自旋锁)…synchronized锁的优化操作锁膨胀/锁升级体现了synchronized锁的自适应能力,根据锁的竞争激励程度自动升级锁锁粗化/锁细化这里的粗细指的是加锁的粒度,换句话说就是加锁代码的范围,范围越大,加锁的粒度越大,锁粗化!编译器会根据你写的代码进行优化策略,在不改变代码逻辑的情况下,使代码效率更高!Thread t1 = new Thread(()->{ Object locker = new Object(); int num = 0; synchronized (locker){ //针对一整个循环加锁,粒度大 for (int i = 0; i < 10; i++) { num++; } } }); Thread t2 = new Thread(()->{ Object locker = new Object(); int num = 0; for (int i = 0; i < 10; i++) { synchronized (locker) {//每次循环加锁,粒度小 num++; } } });锁消除顾名思义,锁消除就是将锁给去掉!有时候加锁操作并没有起到作用,编译器就会将该锁去掉,提供代码效率!比如我们知道Vector和Stringbuffer类的关键方法都进行了加锁操作,如果在单线程代码使用这两个类,编译器就会对代码进行优化,进行锁消除!java中的JUC啥是JUC?java.util.concurrent这个包简化为JUC这个包下有很多java多线程并发编程的接口和类!我们来了解一下其他的一些重要的类和接口!CallableCallable是一个接口,创建线程的一中方法我们就疑惑了,不是已经有Runnable了嘛,Callable实现的对象可以返回结果,而Runnable取不方便!例如我们要实现1到100的相加,Runnable就会比较麻烦,而我们通过Callable就比较方便!//Runnable方式实现 public class Demo2 { static class Result {//辅助类保存结果 public int sum = 0; public Object lock = new Object(); } public static void main(String[] args) throws InterruptedException { Result result = new Result(); Thread t = new Thread() { @Override public void run() { int sum = 0; for (int i = 1; i <= 1000; i++) { sum += i; } synchronized (result.lock) { result.sum = sum; result.lock.notify(); } } }; t.start(); synchronized (result.lock) { while (result.sum == 0) { result.lock.wait(); } System.out.println(result.sum); } } }显然这个代码有点麻烦,还需要借助一个辅助类才能实现!//Callable方式实现 import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Demo3 { public static void main(String[] args) { Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int result = 0; for (int i = 1; i <= 100; i++) { result+=i; } return result; } }; //callable描述了这个任务!(你去点餐) //辅助的类将callable任务标记,便于执行线程!(给了小票,区分谁的食物) FutureTask<Integer> task = new FutureTask<>(callable); Thread t1 = new Thread(task);//执行线程任务!(给你做好了) try { int result = task.get(); //获取到结果(凭小票取餐) System.out.println("计算结果:"+result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }我们通过Callable接口实现时需要注意一些细节,我们要通过FutureTask对象将Callable传入标记,便于后面拿值(task.get())ReentrantLock可重入锁我们知道synchronized也是可重入锁!这个有什么过人之处呢?public class Demo4 { public static void main(String[] args) { //一个参数的构造方法 true 为公平锁 false 非公平锁 默认非公平锁 ReentrantLock lock = new ReentrantLock(true); //加锁 lock.lock(); //解锁 lock.unlock(); } }我们可以看到ReentrantLock将加锁和解锁分开操作!其实分开的做法并不好,有时候可能会忘记解锁(lock,unlock())就会使线程造成阻塞!和synchronized锁的区别synchronized是一个关键字(背后逻辑是JVM,由C++实现),Callable是一个接口(背后逻辑由java代码编写)synchronized不需要手动释放锁操作,出了代码块,锁自动释放,而ReentrantLock必须手动释放锁!synchronized是一个非公平锁,ReentrantLock提供了公平锁和非公平两个版本,供选择!synchronized锁竞争失败就会进行阻塞等待,而ReentrantLock除了阻塞等待外还提供了trylock失败直接返回基于synchronized的等待机制是wait和notify功能相对有限,而ReentrantLock等待机制提供了Condition类功能强大其实在日常开发synchronized功能就够用了!semaphore 信号量一个更广义的锁!锁是信号量的一种为"二元信号量"举个生活中的例子:你去停车场停车:当你到门口你可以看到有个牌子写了当前还剩多少车位!进去一辆车 车位就减一出来一辆车 车位就加一如果当前车位为0 就阻塞等待!这个标识多少车位牌子(描述可用资源的个数)就是信号量每次申请一个资源 计数器就-1(称为p操作)每次释放一个资源 计数器就+1(称为v操作)资源为0阻塞等待!锁是特殊的"二元信号量" 只有0或1标识资源个数!信号量就是把锁推广到一般情况,可用资源更多的时候,如何处理一般很少用到!import java.util.concurrent.Semaphore; public class Demo5 { public static void main(String[] args) throws InterruptedException { //创建一个可用资源个数为3的信号量 Semaphore semaphore = new Semaphore(3); //p操作 申请信号量 semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); //v操作 释放信号量 semaphore.release(); System.out.println("释放成功"); } }我们只有3个资源,而却申请了4次,那么第4次就会进行阻塞等待!CountDownLatch重点线,这里怎么解释呢!就好比一场跑步比赛,裁判要等到所有人越过终点线,比赛才算结束!这样的场景在开放中也很常见!例如多线程下载!比如迅雷等 都是将一个文件分给多个线程同时下载,当所有线程下载完毕,才算下载完成!import java.util.concurrent.CountDownLatch; public class Demo6 { public static void main(String[] args) throws InterruptedException { //5个线程! CountDownLatch latch = new CountDownLatch(5); for (int i = 0; i <5; i++) { Thread t1 = new Thread(()->{ try { Thread.sleep(300); //获取该线程名 System.out.println(Thread.currentThread().getName()); latch.countDown();//该任务执行 } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); } latch.await(); //等待所有线程执行结束 System.out.println("比赛结束"); } }CopyOnWriteArrayList写时拷贝当我们在多线程环境下使用ArrayList时读操作并不会导致线程不安全!但是写操作就可能出现线程不安全问题!当多个线程进行多写操作时,显然就线程不安全,会有脏读问题!我们可以自己加锁操作!我们java提供了多线程环境下使用的ArrayListCopyOnWriteArrayList当多线程去写操作时,我们的ArrayList会创建一个副本进行写操作!当写操作完成后再更新数据! 就不会出现数据修改一般的情况!当ArrayList扩容时,也会慢慢搬运,不会一致性将ArrayList直接拷贝,导致操作卡顿!多线程下使用hash表(常考)HashMap线程不安全!HashTable[不推荐]因为HashTable就是将关键方法进行synchronized加锁!也就相当于直接给HashTable加锁,效率很低!无论进行什么操作都会导致锁竞争!ConcurrentHashMap[推荐]HashTable对象加锁就好比一个公司里的员工需要请假,都要向老板请假才有用!而老板就相当于锁,如果有很多员工请假就会导致锁竞争激烈,线程阻塞!解决方案:老板权利下放!让每个部门的人向部门管理人员请假!我们知道哈希表的结构是由数组,数组元素是链表!并且链表长度相对短! 所以锁冲突就很小!!!ConcurrentHashMap优点针对读操作不进行加锁,只对写操作加锁减少锁冲突,在每个表头加锁广泛使用CAS操作,进一步提高效率(比如维护size操作)进行扩容巧妙的化整为零,进行了优化
本节要点了解常见锁策略了解synchronized使用的锁策略理解CAS实现逻辑了解CAS出现的ABA问题,并解决synchronized锁的原理常见锁策略我们已经知道锁在我们的并发编程十分重要.那我们就需要了解,这些锁实现的策略!都有那些策略,便于我们更加深刻的理解锁!下面介绍的几组锁策略,每一组里面都是相异的,每组策略之间又有相互关联的!乐观锁 vs 悲观锁这是程序员处理锁冲突的态度(原因),通过自己的预期而实现咋样的锁就好比疫情:乐观的人觉得过段时间就好了,就不会囤太多物资悲观的人觉得紧张,就屯好多物资,做了好多工作乐观锁程序员在设计锁的时候,预期锁冲突概率很低做的工作更少,付出的成本低,更高效悲观锁预期锁冲突概率很高做的工作多,付出的成本高,更低效互斥锁vs读写锁互斥锁只有多个线程对同一个对象加锁才会导致互斥互斥锁就是普通的锁,只有加锁和解锁读写锁可以对读操作加锁(不存在互斥关系,可以多线程读)对写操作加锁(只能进行写操作)读写操作加锁(读时不能写,写时不能读)轻量级锁vs重量级锁轻量级锁做的事情更少,开销比较小重量级锁做的事情更多,开销比较大这里的轻量级锁和重量级锁和上面的悲观锁和乐观锁有所重叠一个是设计锁的态度(原因),一个是处理锁冲突的结果!通常情况下一般可以认为乐观锁一般都是轻量级锁,悲观锁都是重量级锁!但是不绝对!!!是如何实现轻量和重量呢?其实我们基于纯用户态实现的锁就是认为是轻量级锁,开销小,程序员可控!如果是基于内核的一些功能实现(比如调用了操作系统内核的mutex接口)的锁就认为是重量级锁(操作系统的锁会在内核做好多事情,比如让线程等待…)自旋锁vs挂起等待锁这是上述轻量级锁和重量级锁的典型实现自旋锁往往通过纯用户态代码实现,较轻挂起等待锁通过内核的一些机制实现,往往较重公平锁vs非公平锁公平锁多个线程等待一把锁时,遵循先来后到原则非公平锁多个线程等待一把锁时,每个线程拿到锁的机会均等!这里就有人有疑惑了,咋的,机会均等还不公平了?但是你换个场景想想,如果你在等待办理业务,先来不就应该先办业务嘛,就好比排队,你先来排在前面!所以这才公平嘛!!!可重入锁vs不可重入锁可重入锁可重入锁就是对一个对象多次加锁时不会造成死锁不可重入锁一个对象多次加锁时会造成死锁!synchronized使用的锁策略我们了解了上述的多组锁策略,我们来分析一下,synchronized用了那些锁策略!自适应锁,即是乐观锁又是悲观锁不是读写锁只是普通的互斥锁既是一个轻量级锁又是重量级锁(根据锁竞争程度自适应)轻量级锁的部分基于自旋锁实现,重量级锁基于挂起等待宿实现非公平锁(锁拿到的机会均等)可重入锁(加锁多次,不会导致死锁)CAS什么是cas?CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 – 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。可能有点抽象,我们看下面案例!我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。比较 A 与 V 是否相等。(比较)如果比较相等,将 B 写入 V。(交换)返回操作是否成功。可能看到这里你还是很懵!boolean CAS(address, expectValue, swapValue) { if (&address == expectedValue) { &address = swapValue; return true; } return false; }我们看这个伪代码!通俗点讲,就是CAS解决了多线程中多条指令进行的赋值问题!我们之前已经了解过当我们需要对一个值进行++在cpu中其实要执行3条指令!先拿到值放在寄存器,将寄存器中的值更改,然后在放回内存!而我们知道在多线程执行写操作时,就会导致线程不安全问题!因为++操作并不是原子性的!而这里的CAS做的就是将多条指令封装成一条指令,达到原子性的效果!避免线程不安全问题!我们的cpu提供了一个单独的指令cas来执行上诉代码!!!CAS使用CAS可以做什么呢?基于CAS能够实现"原子类"java标准库中给我们提供了一组原子类,就是将常用类(int long array …)进行了封装,可以基于CAS进行修改,并且线程安全!//基于CAS多线程对一个数实现自加 import java.util.concurrent.atomic.AtomicInteger; public class Demo1 { public static void main(String[] args) throws InterruptedException { //原子类 AtomicInteger atomicInteger = new AtomicInteger(0); Thread t1 = new Thread(()->{ for (int i = 0; i < 5000 ; i++) { //这个方法相当于 ++num atomicInteger.incrementAndGet(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 5000 ; i++) { //这个方法相当于 ++num atomicInteger.incrementAndGet(); } }); t1.start();//启动线程 t2.start(); t1.join(); t2.join();//等待2个线程执行结束 System.out.println("多线程自加结果:"+atomicInteger.get()); } }可以看到我们基于CAS多线程进行一个数的更改并不用加锁也能保证线程安全!!!我们来学习一下java原子类中的一些方法!原子类在java.util.concurrent.atomic包下!构造方法,可以给初值!实现+=操作自加自减!我们来看一下这里自加的实现逻辑!//伪代码 class AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value; } return oldValue; } }我们分析上述伪代码!基于CAS能够实现"自旋锁"//伪代码 public class SpinLock { private Thread owner = null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner = null; } }可以看到自旋锁的实现和原子类的实现类似!我们来分析一下!如果已经有线程持有了该锁对象,那么while循环就会一直自旋,直到该锁被释放,该线程才可以拿到该锁!if(this.owner==null) 为真,Thread.currentThread当前线程就可以拿到该锁!否者自旋等待锁!CAS的ABA面试问题我们知道CAS的实现是通过对比当前CPU中的值和内存中的值是否相等,如果相等就采取计然后进行交换!我们是否想过另外一种情况,就是该内存中的值改变了多次,又改回了原来那个值,显然这时内存中的值虽然和cpu中值相等,但是该值已经进行了多次改变,并没有保证此次CAS的原子性!举个例子:当有两个线程t1 和t2 这两个对象对同一块内存空间采取CAS修改操作!我们已经知道CAS的原子性,t1和t2都能执行完成!而如果这时有第3个线程在t1的load和t2的CAS之间将该值又更改回去,那么就出现bug了我们假设一种现实场景:某一天你去ATM取款你的余额为1000元然后你取款500元你不小心多点了一次取款,但是你没有察觉到!然后第一次你取款成功了,正常情况你第二次肯定无法取款成功!因为我们知道CAS会比较寄存器和内存中的值!而此时的余额已经不是1000了,该CAS指令就无法成功执行!但是如果在你执行第二个取款操作之前,你的朋友刚好给你转账500元!这样CAS在比较时发现相等就会再次执行取款操作,你取500居然取出了1000,余额还有500,这就是一个BUG!我们用图来描述一下上述情况!我们如何解决这个问题呢?我们可以引入一个版本号记录每次更改内存的次数,如果更改一次,版本号就加1,且版本号只能递增!!!进行比较时只需要比较版本号即可,如果版本号相等就可以进行交换!我们再进行上述的CAS就不会产生bug了!当我们引入版本号时,每次只要比较版本号的值是否相等就可以判断内存中的值是否已经修改过,很好的解决了CAS中的ABA问题!我们也可以用时间戳代替版本号,达到的效果一样,也可解决ABA问题!
本节要点了解一些线程安全的案例学习线程安全的设计模型掌握单例模式,阻塞队列,生产在消费者模型单例模式我们知道多线程编程,因为线程的随机调度会出现很多线程安全问题! 而我们的java有些大佬针对一些多线程安全问题的应用场景,设计了一些对应的解决方法和案例,就是解决这些问题的一些套路,被称为设计模式,供我们学习和使用!单例模式是校招最常考的一个设计模式之一!!!什么是单例模式呢?单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个单例模式的具体实现方法又分为饿汉和懒汉两种!而这里所说的饿和懒并不是贬义词!饿汉指的是在创建一个类的时候就将实例创建好!比较急!懒汉指的是在需要用到实例的时候再去创建实例!比较懒!饿汉模式饿汉模式联系实际生活中例子:就是一个人性子比较急,也许一件事情的期限还有好久,而他却把事情早早干完!因为我们单例模式只能有一个实例那如何去保证一个实例呢?我们会马上想到类中用static修饰的类属性,它只有一份!保证了单例模式的基本条件!显然生活中这样的人很优秀,但是我们的计算机如果这样却不太好!因为cpu和内存的空间有限,如果还不需要用到该实例,却创建了实例,那不就增加了内存开销,显然不科学.但事实问题也不大!class Singleton{ //饿汉模式, static 创建类时,就创建好了类属性的实例! //private 这里的instance实例只有一份!!! private static Singleton instance = new Singleton(); //私有的构造方法!保证该实例不能再创建 private Singleton(){ } //提供一个方法,外界可以获取到该实例! public static Singleton getInstance() { return instance; } }我们可以看到这里饿汉模式,当多个线程并发时,并没有出现线程不安全问题,因为这里的设计模式只是针对了读操作!!! 而单例模式的更改操作,需要看懒汉模式!懒汉模式联系实际中的例子就是.就是这个人比较拖延,有些事情不得不做的时候,他才会去做完!//懒汉模式(线程不安全版本) class Singleton1{ //懒汉模式, static 创建类时,并没有创建实例! //private 保证这里的instance实例只有一份!!! private static Singleton1 instance = null; //私有的构造方法!保证该实例不能再创建 private Singleton1(){ } //提供一个方法,外界可以获取到该实例! public static Singleton1 getInstance() { if(instance==null){//需要时再创建实例! instance = new Singleton1(); } return instance; } }我们分析一下上述代码,该模式,对singleton进行了修改,而我们知道多线程的修改可能会出现线程不安全问题!当我们多个线程同时对该变量进行访问时!我们将该代码的情况分成两种,一种是初始化前要进行读写操作,初始化后只需要进行读操作!instance未初始化化前多个线程同时进入getInstance方法!那就会创建很多次instance实例!联系之前的变量更改内存和cpu的操作:显然很多线程进行了无效操作!!!也会触发内存不可见问题!!!instance初始化后,进行的读操作,就像上面的饿汉模式一样,并没有线程安全问题!我们下面进行多次优化//优化1 class Singleton2{ //懒汉模式, static 创建类时,并没有创建实例! //private 保证这里的instance实例只有一份!!! private static Singleton2 instance = null; //私有的构造方法!保证该实例不能再创建 private Singleton2(){ } //提供一个方法,外界可以获取到该实例! public static Singleton2 getInstance() { synchronized (Singleton.class){ //对读写操作进行加锁! if(instance==null){//需要时再创建实例! instance = new Singleton2(); } return instance; } } }我们将Singleton类对象加锁后,显然避免了刚刚的一些线程安全问题!但是出现了新的问题!instance初始化前在初始化前,我们很好的将读写操作进行了原子封装,并不会造成线程不安全问题!instance初始化后然而初始化后的每次读操作却并不好,当我们多个线程进行多操作时,很多线程就会造成线程阻塞,代码的运行效率极具下降!我们如何保证,线程安全的情况下又保证读操作不会进行加锁,锁竞争呢?我们可以间代码的两种情况分别处理!//优化二 class Singleton2{ //懒汉模式, static 创建类时,并没有创建实例! //private 保证这里的instance实例只有一份!!! private static Singleton2 instance = null; //私有的构造方法!保证该实例不能再创建 private Singleton2(){ } //提供一个方法,外界可以获取到该实例! public static Singleton2 getInstance() { if(instance==null){//如果未初始化就进行加锁操作! synchronized (Singleton.class){ //对读写操作进行加锁! if(instance==null){//需要时再创建实例! instance = new Singleton2(); } } } //已经初始化后直接读!!! return instance; } }我们看到这里可能会有疑惑,咋为啥要套两个if啊,把里面的if删除不行吗!!!我们来看删除后的效果://删除里层if class Singleton2{ //懒汉模式, static 创建类时,并没有创建实例! //private 保证这里的instance实例只有一份!!! private static Singleton2 instance = null; //私有的构造方法!保证该实例不能再创建 private Singleton2(){ } //提供一个方法,外界可以获取到该实例! public static Singleton2 getInstance() { if(instance==null){//如果未初始化就进行加锁操作! synchronized (Singleton.class){ //对读写操作进行加锁! instance = new Singleton2(); } } //已经初始化后直接读!!! return instance; } }在删除里层的if后:我们发现当有多个线程进行了第一个if判断后,进入的线程中有一个线程锁竞争拿到了锁!而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,而这样也就多次创建了instance实例,显然不可!!!所以这里的两个if都有自己的作用缺一不可!第一个if:判断是否要进行加锁初始化第二个if:判断该线程实例是否已经创建!//最终优化版 class Singleton2{ //懒汉模式, static 创建类时,并没有创建实例! //private 保证这里的instance实例只有一份!!! //volatile 保证内存可见!!!避免编译器优化!!! private static volatile Singleton2 instance = null; //私有的构造方法!保证该实例不能再创建 private Singleton2(){ } //提供一个方法,外界可以获取到该实例! public static Singleton2 getInstance() { if(instance==null){//如果未初始化就进行加锁操作! synchronized (Singleton.class){ //对读写操作进行加锁! if(instance==null){ instance = new Singleton2(); } } } //已经初始化后直接读!!! return instance; } }而我们又发现了一个问题,我们的编译器是会对代码进行优化操作的!如果很多线程对第一个if进行判断,那cpu老是在内存中拿instance的值,就很慢,编译器就不开心了,它就优化直接将该值存在寄存器中,而此操作是否危险,如果有一个线程将该实例创建!那就会导致线程安全问题! 而volatile关键字保证了instanse内存可见性!!!总结懒汉模式双if 外层保证未初始化前加锁,创建实例. 里层if保证实例创建唯一一次synchronized加锁,保证读写原子性volatile保证内存可见性,避免编译器优化阻塞队列什么是阻塞队列?顾名思义是队列的一种!也符合先进先出的特点!阻塞队列特点:当队列为空时,读操作阻塞当队列为满时,写操作阻塞阻塞队列一般用在多线程中!并且有很多的应用场景!最典型的一个应用场景就是生产者消费者模型生产者消费者模型我们知道生产者和消费者有着供需关系!而开发中很多场景都会有这样的供需关系!比如有两个服务器A和BA是入口服务器直接接受用户的网络请求B应用服务器对A进行数据提供在通常情况下如果一个网站的访问量不大,那么A和B服务器都能正常使用!而我们知道,很多网站当很多用户进行同时访问时就可能挂!我们知道,A入口服务器和B引用服务器此时耦合度较高!当增加就绪队列后,我们就不用担心A和B的耦合!并且A和B进行更改都不会影响到对方! 甚至将改变服务器,对方也无法察觉!而阻塞队列还保证了,服务器的访问速度,不管用户量多大! 这些数据都会先传入阻塞队列,而阻塞队列如果满,或者空,都会线程阻塞! 也就不存在服务器爆了的问题!!!也就是起到了削峰填谷的作用!不管访问量一时间多大!就绪队列都可以保证服务器的速度!标准库中的就绪队列我们java中提供了一组就绪队列供我们使用!BlockingQueueBlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.put 方法用于阻塞式的入队列,take 用于阻塞式的出队列.BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.//生产着消费者模型 public class Test2 { public static void main(String[] args) throws InterruptedException { //创建一个阻塞队列 BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(); Thread customer = new Thread(() -> {//消费者 while (true) { try { int value = blockingQueue.take(); System.out.println("消费元素: " + value); } catch (InterruptedException e) { e.printStackTrace(); } } }, "消费者"); customer.start(); Thread producer = new Thread(() -> {//生产者 Random random = new Random(); while (true) { try { int num = random.nextInt(1000); System.out.println("生产元素: " + num); blockingQueue.put(num); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "生产者"); producer.start(); customer.join(); producer.join(); } }阻塞队列实现虽然java标准库中提供了阻塞队列,但是我们想自己实现一个阻塞队列!我们就用循环队列实现吧,使用数组!//循环队列 class MyblockingQueue{ //阻塞队列 private int[] data = new int[100]; //队头 private int start = 0; //队尾 private int tail = 0; //元素个数, 用于判断队列满 private int size = 0; public void put(int x){ //入队操作 if(size==data.length){ //队列满 return; } data[tail] = x; tail++;//入队 if(tail==data.length){ //判断是否需要循环回 tail=0; } size++; //入队成功加1 } public Integer take(){ //出队并且获取队头元素 if(tail==start){ //队列为空! return null; } int ret = data[start]; //获取队头元素 start++; //出队 if(start==data.length){ //判断是否要循环回来 start = 0; } // start = start % data.length;//不建议可读性不搞,效率也低 size--;//元素个数减一 return ret; } }我们已经创建好了一个循环队列,目前达不到阻塞的效果!而且当多线程并发时有很多线程不安全问题!而我们知道想要阻塞,那不得加锁,不然哪来的阻塞!//阻塞队列 class MyblockingQueue{ //阻塞队列 private int[] data = new int[100]; //队头 private int start = 0; //队尾 private int tail = 0; //元素个数, 用于判断队列满 private int size = 0; //锁对象 Object locker = new Object(); public void put(int x){ synchronized (locker){//对该操作加锁 //入队操作 if(size==data.length){ //队列满 阻塞等待!!!直到put操作后notify才会继续执行 try { locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } data[tail] = x; tail++;//入队 if(tail==data.length){ //判断是否需要循环回 tail=0; } size++; //入队成功加1 //入队成功后通知take 如果take阻塞 locker.notify();//这个操作线程阻塞并没有副作用! } } public Integer take(){ //出队并且获取队头元素 synchronized (locker){ if(size==0){ //队列为空!阻塞等待 知道队列有元素put就会继续执行该线程 try { locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } int ret = data[start]; //获取队头元素 start++; //出队 if(start==data.length){ //判断是否要循环回来 start = 0; } // start = start % data.length;//不建议可读性不搞,效率也低 size--;//元素个数减一 locker.notify();//通知 put 如果put阻塞! return ret; } } }//测试代码 public class Test3 { public static void main(String[] args) { MyblockingQueue queue = new MyblockingQueue(); Thread customer = new Thread(()->{ int i = 0; while (true){ System.out.println("消费了"+queue.take()); } }); Thread producer = new Thread(()->{ Random random = new Random(); while (true){ int x = random.nextInt(100); System.out.println("生产了"+x); queue.put(x); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }); customer.start(); producer.start(); } }可以看到通过wait和notify的配和,我就实现了阻塞队列!!!定时器定时器是什么定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.也就是说定时器有像join和sleep等待功能,不过他们是基于系统内部的定时器,而我们要学习的是在java给我们提供的定时器包装类,用于到了指定时间就执行代码!并且定时器在我们日常开发中十分常用!java给我们提供了专门一个定时器的封装类Timer在java.util包下!Timer定时器Timer类下有一个schedule方法,用于安排指定的任务和执行时间!也就达到了定时的效果,如果时间到了,就会执行task!schedule 包含两个参数.第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行 (单位为毫秒).//实例 import java.util.Timer; import java.util.TimerTask; public class Demo1 { public static void main(String[] args) { //在java.util.Timer包下 Timer timer = new Timer(); //timer.schedule()方法传入需要执行的任务和定时时间 //Timer内部有专门的线程负责任务的注册,所以不需要start timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello Timer!"); } },3000); //main线程 System.out.println("hello main!"); } }我们可以看到我们只需要创建一个Timer对象,然后调用schedule返回,传入你要执行的任务,和定时时间便可完成!定时器实现我们居然知道java中定时器的使用,那如何自己实现一个定时器呢!我们可以通过Timer中的源码,然后进行操作!Timer内部需要什么东西呢!我们想想Timer的功能!可以定时执行任务!(线程)可以知道任务啥时候执行(时间)可以将多个任务组织起来对比时间执行描述任务也就是schedule方法中传入的TimerTake创建一个专门表示定时器中的任务class MyTask{ //任务具体要干啥 private Runnable runnable; //任务执行时间,时间戳 private long time; ///delay是一个时间间隔 public MyTask(Runnable runnable,long delay){ this.runnable = runnable; time = System.currentTimeMillis()+delay; } public void run(){ //描述任务! runnable.run(); } }组织任务组织任务就是将上述的任务组织起来!我们知道我们的任务需要在多线程的环境下执行,所以就需要有线程安全,阻塞功能的数据结构!并且我们的任务到了时间就需要执行,也就是需要时刻对任务排序!所以我们采用PriorityBlockingQueue优先级队列!阻塞!但是这里我们使用了优先级队列,我们需要指定比较规则,就是让MyTask实现Comparable接口,重写 compareTo方法,指定升序排序,就是小根堆!执行时间到了的任务我们可以创建一个线程,执行时间到了的任务!//执行时间到了的任务! public MyTimer(){ Thread thread = new Thread(()->{ while (true){ try { MyTask task = queue.take();//获取到队首任务 //比较时间是否到了 //获取当前时间戳 long curTime = System.currentTimeMillis(); if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较 //还未到达执行时间 queue.put(task); //将任务放回 }else{//时间到了,执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start();//启动线程! }//定时器完整代码 import java.util.concurrent.PriorityBlockingQueue; class MyTask implements Comparable<MyTask>{ //任务具体要干啥 private Runnable runnable; public long getTime() { return time; } //任务执行时间,时间戳 private long time; ///delay是一个时间间隔 public MyTask(Runnable runnable,long delay){ this.runnable = runnable; time = System.currentTimeMillis()+delay; } public void run(){ //描述任务! runnable.run(); } @Override public int compareTo(MyTask o) { return (int)(this.time - o.time); } } public class MyTimer{ //定时器内部需要存放多个任务 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(); public void schedule(Runnable runnable,long delay){ MyTask task = new MyTask(runnable,delay);//接收一个任务! queue.put(task);//将任务组织起来 } //执行时间到了的任务! public MyTimer(){ Thread thread = new Thread(()->{ while (true){ try { MyTask task = queue.take();//获取到队首任务 //比较时间是否到了 //获取当前时间戳 long curTime = System.currentTimeMillis(); if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较 //还未到达执行时间 queue.put(task); //将任务放回 }else{//时间到了,执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start();//启动线程! } }//测试 public static void main(String[] args) { MyTimer myTimer = new MyTimer(); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("hello Timer"); } }, 3000); System.out.println("hello main"); }我们再来检查一下下面代码存在的问题!//执行时间到了的任务! public MyTimer(){ Thread thread = new Thread(()->{ while (true){ try { MyTask task = queue.take();//获取到队首任务 //比较时间是否到了 //获取当前时间戳 long curTime = System.currentTimeMillis(); if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较 //还未到达执行时间 queue.put(task); //将任务放回 }else{//时间到了,执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start();//启动线程! }我们上述代码还存在一定缺陷就是执行线程到了的代码,我们的while循环一直在处于忙等状态!就好比生活中:你9点要去做核酸,然后你过一会就看时间,一会就看时间,感觉就有啥大病一样!所以我们可以定一个闹钟,到了时间就去,没到时间可以干其他的事情!此处的线程也是如此!我们这里也可以使用wait阻塞! 然后到了时间就唤醒,就解决了忙等问题!我们的wait可以传入指定的时间,到了该时间就唤醒!!!我们再思考另一个问题!如果又加入了新的任务呢?我们此时也需要唤醒一下线程,让线程重新拿到队首元素!//最终定时器代码!!!! import java.util.concurrent.PriorityBlockingQueue; class MyTask implements Comparable<MyTask>{ //任务具体要干啥 private Runnable runnable; public long getTime() { return time; } //任务执行时间,时间戳 private long time; ///delay是一个时间间隔 public MyTask(Runnable runnable,long delay){ this.runnable = runnable; time = System.currentTimeMillis()+delay; } public void run(){ //描述任务! runnable.run(); } @Override public int compareTo(MyTask o) { return (int)(this.time - o.time); } } public class MyTimer{ //定时器内部需要存放多个任务 Object locker = new Object();//锁对象 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(); public void schedule(Runnable runnable,long delay){ MyTask task = new MyTask(runnable,delay);//接收一个任务! queue.put(task);//将任务组织起来 //每次拿到新的任务就需要唤醒线程,重新得到新的队首元素! synchronized (locker){ locker.notify(); } } //执行时间到了的任务! public MyTimer(){ Thread thread = new Thread(()->{ while (true){ try { MyTask task = queue.take();//获取到队首任务 //比较时间是否到了 //获取当前时间戳 long curTime = System.currentTimeMillis(); if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较 //还未到达执行时间 queue.put(task); //将任务放回 //阻塞到该时间唤醒! synchronized (locker){ locker.wait(task.getTime()-curTime); } }else{//时间到了,执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start();//启动线程! } }总结:描述一个任务 runnable + time使用优先级队列组织任务PriorityBlockingQueue实现schedule方法来注册任务到队列创建扫描线程,获取队首元素,判断是否执行注意这里的忙等问题//最后梳理一遍 import java.util.concurrent.PriorityBlockingQueue; /** * Created with IntelliJ IDEA. * Description:定时器 * User: hold on * Date: 2022-04-09 * Time: 16:07 */ //1.描述任务 class Task implements Comparable<Task>{ //任务 private Runnable runnable; //执行时间 private long time; public Task(Runnable runnable,long delay){ this.runnable = runnable;//传入任务 //获取任务需要执行的时间戳 time = System.currentTimeMillis() + delay; } @Override public int compareTo(Task o) {//指定比较方法! return (int) (this.time-o.time); } public long getTime() {//传出任务时间 return time; } public void run(){ runnable.run(); } } //组织任务 class MyTimer1{ private Object locker = new Object();//锁对象 //用于组织任务 private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(); public void schedule(Runnable runnable,long delay){ Task task = new Task(runnable,delay); queue.put(task);//传入队列中 synchronized (locker){ locker.notify();//唤醒线程 } } public MyTimer1(){ //扫描线程获取队首元素,判断执行 Thread thread = new Thread(()->{ while (true){ //获取当前时间戳 long curTimer = System.currentTimeMillis(); try { Task task = queue.take();//队首元素出队 if(curTimer<task.getTime()){ //还未到达执行时间,返回队首元素 queue.put(task); synchronized (locker){ //阻塞等待 locker.wait(task.getTime()-curTimer); } }else { //执行任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start();//启动线程 } } public class Demo2 { public static void main(String[] args) { MyTimer1 myTimer1 = new MyTimer1(); myTimer1.schedule(new Runnable() { @Override public void run() { System.out.println("hello Timer1"); } },1000); System.out.println("hello main"); } }线程池我们之前学过常量池!这里的线程池也大同小异!我们通过创建很多个线程放在一块空间不进行销毁,等到需要的时候就启动线程!避免了创建销毁的时间开销! 提高开发效率!我们之前不是说一个线程创建并不会划分很多时间吗! 但是我们的多线程编程,有时候需要使用到很多很多线程,如果要进行创建,效率就不高,而线程池或者协程(我们后面会介绍)就避免了创建销毁线程! 但我们需要用到线程时,自己从线程池中给出就好!我们创建线程的本质还是要通过内核态(就是我们的操作系统)进行创建,然而内核态创建的时间,我们程序员无法掌控,而通过线程池,我们就可以避免了内核态的操作,直接在用户态,进行线程的调用,也就是应用程序层!使用线程池大大提高了我们的开发效率!我们来学习一下java中给我们提供的线程池类,然后自己实现一个线程池!ThreadPoolExecutor 线程池这个类在java.util.concurrent 并发编程包下,我们用到的很多关于并发编程的类都在!可以看到这个线程池有4个构造方法!我们了解一下参数最多的那个方法!public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 创建一个新的 ThreadPoolExecutor与给定的初始参数。 参数 corePoolSize - 即使空闲时仍保留在池中的线程数,除非设置 allowCoreThreadTimeOut maximumPoolSize - 池中允许的最大线程数 keepAliveTime - 当线程数大于内核时,这是多余的空闲线程在终止前等待新任务的最大时间。 unit - keepAliveTime参数的时间单位 workQueue - 用于在执行任务之前使用的队列。 这个队列将仅保存execute方法提交的Runnable任务。 threadFactory - 执行程序创建新线程时使用的工厂 handler - 执行被阻止时使用的处理程序,因为达到线程限制和队列容量我们这里的线程池类比一个公司,便于我们理解该类int maximumPoolSize,核心线程数(正式员工)maximumPoolSize池中允许的最大线程数(正式员工+临时工)long keepAliveTime,多余的空闲线程的允许等待的最大时间(临时工摸鱼时间)TimeUnit unit,时间单位- BlockingQueue<Runnable> workQueue,任务队列,该类中用一个submit方法,用于将任务注册到线程池,加入到任务队列中!ThreadFactory threadFactory,线程工厂,线程是如何创建的RejectedExecutionHandler handler拒绝策略但任务队列满了后怎么做1.阻塞等待,2.丢弃久任务3.忽略新任务…可以看到java给我们提供的这个线程池类让人头大!但是不必焦虑,我们只需要知道int maximumPoolSize,核心线程数和 maximumPoolSize 池中允许的最大线程数即可!面试问题思考一个问题我们有一个程序需要多线程并发处理一些任务,使用线程池的话,需要设置多大的线程数?这里的话,我们无法准确的给出一个数值,我们要通过性能测试的方式找个一个平衡点!例如我们写一个服务器程序:服务器通过线程池多线程处理机用户请求!如果要确定线程池的线程数的话,就需要通过对该服务器进行性能分析,构造很多很多请求模拟真实环境,根据这里不同的线程数,来观察处理任务的速度和当个线程的cpu占用率!从而找到一个平衡点!如果cpu暂用率过高,就无法应对一些突发情况,服务器容易挂!我们java根据上面的ThreadPoolExecutor类进行封装提供了一个简化版本的线程池!Executors供我们使用!我们通过Executors的使用学习,实现一个线程池!Executorsjava.util.concurrent.Executors下面都是Executor类中创建线程池的一些静态方法创建可以扩容的线程池我们重点学习创建指定大小得到线程池方法!//Executors使用案例 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo3 { public static void main(String[] args) { //创建一个指定线程个数为10的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { int finalI = i; executorService.submit(new Runnable() { @Override public void run() { System.out.println("hello executor!"+ finalI); } }); } } }我们通过ExecutorService类中的submit可以将多个任务注册到线程池中,然后线程池中的线程将任务并发执行,大大提升了编程效率!可以看到,啪的一下,100个任务给10个线程一下就执行结束了!实现线程池我们还是分析一下线程池用什么功能,里面都有些啥!能够描述任务(直接用runnable)需要组织任务(使用BlockingQueue)能够描述工作线程组织线程需要实现往线程池里添加任务//模拟实现线程池 class ThreadPool { //描述任务 直接使用Runnable //组织任务 private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>(); //描述工作线程 static class Worker extends Thread {//继承Thread类 BlockingQueue<Runnable> queue = null; @Override public void run() { while (true){ try { //拿到任务 Runnable runnable = queue.take(); //执行任务 runnable.run(); } catch (InterruptedException e) { e.printStackTrace(); } } } //通过构造方法拿到外面的任务队列! public Worker(BlockingQueue<Runnable> queue) { this.queue = queue; } } //组织多个工作线程 //将多个工作线程放入到workers中! public List<Thread>workers = new LinkedList<>(); public ThreadPool(int n) {//指定放入线程数量 for (int i = 0; i < n; i++) {//创建多个工作线程 Worker worker = new Worker(queue); worker.start();//启动工作线程 workers.add(worker);//放入线程池 } } //创建一个方法供我们放入任务 public void submit(Runnable runnable){ try { queue.put(runnable); } catch (InterruptedException e) { e.printStackTrace(); } } }//测试代码 public class demo5 { public static void main(String[] args) { //线程池线程数量10 ThreadPool pool = new ThreadPool(10); for (int i = 0; i <100 ; i++) {//100个任务 int finalI = i; pool.submit(new Runnable() { @Override public void run() { System.out.println("hello ThreadPool "+ finalI); } }); } } }运行效果案例总结线程安全单例模式阻塞队列->生产着消费者模型定时器MyTask类描述一个任务 Runnable + time带有优先级的阻塞队列扫描线程,不停从队首取出元素,检测时间是否到达,并且执行任务,使用wait解决忙等位问题!实现schedule方法线程池描述一个任务Runnable组织任务,带有优先级的阻塞队列创建一个工作线程work类,从任务队列获取任务,执行任务组织工作线程works数据结构存放work实现一个submit方法将任务放入任务队列中!
认识进程到底啥是进程呢!!!进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。简单讲就是跑起来的应用程序(exe)就是进程!!!我们电脑磁盘上有很多exe文件,我们如果双击这个exe文件,那这个文件只会静静的躺在磁盘上和咸鱼无异!!!只有exe文件(可执行文件)运行了才能称作为进程!!!我们可以打开我们的计算机任务管理,看看此时我们的操作系统上都跑了那些进程!!!一个执行文件(exe)是如何跑起来变成一个进程的呢?一开始在磁盘上的exe文件运行后由操作系统进行资源分配和调度!将文件加载到内存cpu开始执行exe中的一些指令线程我们经常听到的线程又是个啥呢?和进程又有什么关系呢?线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes,但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。我们知道进程是操作系统资源分配和调度的基本单元!!!就比方进程就是我们生活中的一个个工厂!!而线程就是工厂中的车间或者流水线!!!一个工厂可以有一个或多个流水线!一个进程也可以包含一个或多个线程!!!线程是操作系统进行运算调度的最小单位!!!就是说,操作系统需要给进程分配资源和空间,而多个线程公用这块块空间和资源!进程process也称为task任务! 任务管理系统,也就是我们操作系统的进程管理系统!!!进程的属性和管理计算机是如何管理进程的?首先我们需要描述一个进程(明确出进程的一些重要属性)然后组织若多个线程(使用数据结构把多个进程描述信息放在一起用于增删查改)我们的的进程都是由操作系统分配和调度的!而操作系统是由C/C++编写的!我们通过结构体来描述进程的属性然后通过双向链表将若干个进程组织起来,就可以进行增删查改操作!!!进程属性基础属性我们知道我们每个人都有自己的名字和特点!而进程也是如此!每个进程有自己的属性用来区分不同的进程!!!PCB为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。我们通过PCB就可以知道这个进程的信息!pcb 中的一些属性pid进程id对进程进行身份标识!内存指针指明该进程要执行的指令和代码或者资源在内存的具体位置文件描述符表一个进程可能关联多个其他文件,给该进程提供资源,所以我们需要知道该进程要要用到那些文件! 打开一个文件后,文件描述符表就会增加一个信息,文件描述符表可以视为一个数组,元素为结构体!我们一个进程会默认打开三个文件(系统自动打开):标准输入(System.in)标准输出(System.out)标准错误(System.err)实现进程调度的属性并发和并行我们提到进程调度就不得不提并发和并行!我们知道现在的计算机的cpu都是采用多个核心的cpu!一个cpu核心某一时刻只能执行一个进程!!!但是多个cpu核心可以同时执行不同的进程!这就是并行!微观上:并行: 每个cpu核心同一时刻执行不同的任务!并发:一个cpu核心先执行任务1,在进行任务2!!!但是我们知道cpu的执行速度超乎你的想象! 快的一批!所以再宏观上我们无法感受到进程某时刻是并发执行还是并行执行!通常我们将并发和并行通常为并发我们只是在操作系统,区别这两个,其他地方我们都默认叫并发!!!状态这个状态就描述了该进程如何调度就绪状态 : 随时可以上CPU执行阻塞状态/睡眠状态:占时不能上CPU执行还有其他的状态以后在介绍!优先级我们的计算机一般都是多个进程并发执行那么就需要明确一个进程的优先级先给那个进程分配时间,分配多久时间!以及谁分配的时间多少!记账信息统计每个进程分别执行的时间,分别执行了那些指令!等待了多久,给进程调度提供依据!上下文记入上次进程调度出CPU时的执行状态!我们的进程不可能一直执行下去,当我们的CPU需要调度其他进程时,那么该进程就要出cpu,当我们需要再次执行该进程时,上下文属性就可以精准的恢复到上次进程执行的位置!就是在进程调出CPU时,我们吧寄存器中的数据保存在内存中(存档),下次执行该进程时在调回寄存器(读档)我们举一个生活中的例子便于理解上述进程调度属性!假如有一个海王(操作系统)海王同时谈了3个对象!!!一个富婆一个辣妹一个甜妹我们知道该操作明显就是不道德的! 但是只要这3个对象没有见面就问题不大海王只需要安排好在某一时间约一个妹子!(也就是宏观上cpu只执行了一个进程)假如海王比较喜欢辣妹,再是富婆,最后是甜妹(可爱在性感面前不值一提)(这里也就是进程的优先级)星期一二三 和辣妹一起星期四五 和富婆在一起星期六 和甜妹在一起周天休息正常情况下,三个妹子随叫随到(也就是进程就绪)结果辣妹去出差去了一个月回不来!!(此时该进程就是阻塞状态/睡眠状态)海王和甜妹待的时间太少了,甜妹不愿意了(进程的记账信息存档)所以此时海王就要给甜妹安排更多的时间!!!当某一天,富婆说要带海王去讲父母, 辣妹说要带他去见闺蜜,都叫他准备好礼物! 结果海王把给富婆父母的礼物给了,辣妹闺蜜!如果记入了(进程上下文属性)就不会搞错!!!显然海王是不对的!!!令人唾弃!!!进程调度无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。就是cpu如何给进程分配资源!我们知道每个进程都分配了自己的空间!但是cpu,内存资源有限!!!当我们的一个进程出bug因为在不同的空间,并不会影响其他进程!进程的独立性就比如现在的疫情如果一个居民感染了,那么只需要将该栋楼封锁即可!!!(该进程出现bug了)并不会影响其他进程!!!进程间的通信但是显然进程间不可能一直独立,不进行交流,cpu会分配一些公共空间供进程进行通信!!!主要通信方式有两种:文件操作网络操作(socket)后面我们会介绍!!!进程和线程的区别高频面试题进程是系统分配资源的基本单位!线程是系统调度运行的最小单位!一个进程包含一个或多个线程!进程和线程都是为了处理并发编程!进程缺点,创建和销毁效率较低(需要分配资源),而线程弥补了该缺点!(轻量级进程),一个线程执行一段代码!!!进程具有独立性,每个进程会有自己的虚拟空间! 一个进程挂了不会影响另一个进程!但是一个进程有多个线程,一个线程挂了可能影响其他线程,甚至导致进程奔溃!!!
本章目标掌握JDBC的概念和工作原理学会如何使用java中的JDBC编程数据库编程必备条件编程语言,如Java,C、C++、Python等数据库,如Oracle,MySQL,SQL Server等数据库驱动包:不同的数据库,对应不同的编程语言提供了不同的数据库驱动包,如:MySQL提供了Java的驱动包mysql-connector-java,需要基于Java操作MySQL即需要该驱动包。同样的,要基于Java操作Oracle数据库则需要Oracle的数据库驱动包ojdbc。可以看到我们已经具备意思的全部条件了!!!我们学习了编程语言java,数据库 MySQL,此时我们只需要一个数据库驱动包即可完成数据库编程了啥是数据库驱动包呢?其实就是每个数据库提供给编程语言的一个API(函数,接口),让该语言可以通过该驱动包实现对该数据库的基本操作!!!但是每个不同得到数据库都有自己的驱动包!!!就好比你买不同的打印机,需要不同的驱动程序!!!java数据库编程:JDBC但是我们知道java一向比较牛批!! 他的诞生就是为了解决跨平台!!!他一听就不乐意了,咋地,你们这么多数据库,都是不同的驱动包! 这不是违背了嘛所以java想到了一个大一统的方案! 咱出一个JDBC应对不同数据库的驱动包都可以对接!!!JDBC使用JDBC下载! 我们可以官网下载!!但是我们java社区有自己单独的下载工具,就相当于手机的应用商店!!只需要搜索即可下载!!!maven中央仓库使用案例如何使用JDBC在java中编程呢?我们在IDEA中进行操作!首先我们需要创建一个项目!在项目中添加一个文件夹,然后将刚刚下载好的JDBCjar包导入项目中!!!创建lib文件夹,将jar包复制过去Add as Library当我们可以看到这么多文件夹 ,那么我们便导入成功了!!!编写代码JDBC的基本流程!创建DataSource对象,这个对象就是描述了数据库服务器在哪!import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; import javax.sql.DataSource; public class jdbc { public static void main(String[] args) { //创建DAtaSource对象 //导入 javax.sql包 DataSource dataSource = new MysqlDataSource(); //描述数据库服务器在哪里 // 设置数据库所在地址 //jdbc:mysql://127.0.0.1:3306/java_2022?characterEncoding=utf8&useSSL=false ((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/java_2022?characterEncoding=utf8&useSSL=false"); //设置登入数据库用户名 ((MysqlDataSource)dataSource).setUser("root"); //设置数据库登入密码 ((MysqlDataSource)dataSource).setPassword("123456"); } }为啥我们的dataSource对象需要使用强制类型转换呢?因为如果我们直接创建一个MysqlDataSource对象的话,如果我们换一个数据库那么代码的兼容性就较低,需要该的地方较多!而采用类型强制转换,只需要改动部分代码即可!setUrlurl 指的是 uniform/universal resource locator 唯一资源定位地址,就是我们平常说的网站!jdbc:mysql://127.0.0.1:3306/java_2022?characterEncoding=utf8&useSSL=false那这串字符代表什么意思?jdbc:mysql表示mysql jdbc的网址//127.0.0.1 mysql 服务器的主机所在的IP地址 ip地址就描述了网络上一个主机的位置,我们也可以用localhost代替,因为当前我们的数据库服务器和客户端的主机都是同一台3306端口号 访问你的主机上的哪一个程序,3306表示mysql服务器的默认端口号!java_2022 你需要访问的数据库名称characterEncoding=utf8 指定字符集编码! 或者是utf8b&useSSL=false 是否需要传输过程中加密,一般不加密setUser设置用户名setUser("root");root是管理员用户,我们也可以有其他用户setPasswordsetPasssword("123456");这就是输入Mysql服务器的密码!写完上述代码,我们就已经创建好了一个对象!就相当于我们已经分配了一个任务!!!但是此时我们没有去执行这个任务我们需要连接数据库才能访问该数据库!!如何连接数据库服务器呢?dataSource对象下有一个方法,可以连接mysql数据库服务器!Connection connection = dataSource.getConnection();注意这里的Connection类是java.sql.Connection包下的类!!!我们已经连上了数据库那么我们就可以对数据库里的内容进行操作了!!!String sql = "insert into student values('吕布',101)";我们将sql语句写成字符串!!!PreparedStatement statement = connection.prepareStatement(sql);利用connection对象创建一个语句对象!PreparedStatement statement = connection.prepareStatement(sql);我们将sql传入statement 语句对象!!!但是并没有执行操作int ret = statement.executeUpdate();执行操作语句!!!此条语句执行后,那么我们的数据库便执行了sql语句!!!而我们的返回值ret表示几行数据受到影响,就如同我们在命令行执行成功后的提示!!!我们可以打印ret值从而知道多少列受到影响!或者打印statement值可以看到具体的sql操作!!!我们将sql执行完毕,还需要将数据库连接断开和PrepareStatement资源释放!!!关闭资源statement.close();connection.close();因为我们是先连接数据库,在创建了statement对象!所以我们要先closestatement,再close connection!!!就比如我们打开洗衣机,再放衣服!!!洗完后要先拿完衣服再关洗衣机!!!我们附上完整代码!!!import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class jdbc { public static void main(String[] args) throws SQLException{ //创建DataSource对象 //相当于分配了一个任务 //导入 javax.sql包 DataSource dataSource = new MysqlDataSource(); //描述数据库服务器在哪里 // 设置数据库所在地址 //jdbc:mysql://127.0.0.1:3306/java_2022?characterEncoding=utf8&useSSL=false ((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/java_2022?characterEncoding=utf8&useSSL=false"); //((MysqlDataSource)dataSource).setUrl(); //设置登入数据库用户名 ((MysqlDataSource)dataSource).setUser("root"); //设置数据库登入密码 ((MysqlDataSource)dataSource).setPassword("123456"); //连接数据库! 相当于去执行这个任务 //可能数据库连接失败,所以要处理异常!!! //这里的Connection类是选择java.sql包下的!!!! Connection connection = dataSource.getConnection(); //这里就可以写我们的数据库sql代码了 //我们在student 表中插入吕布的成绩 //我们先写好sql字符串 String sql = "insert into student values('吕布',101)"; //然后将sql执行 //这里的PreparedStatement 也是java.sql包下!!! PreparedStatement statement = connection.prepareStatement(sql); int ret = statement.executeUpdate(); //数据库增删改操作!!! //statement.executeQuery(); 数据库查询操作!!! System.out.println(ret); System.out.println("statement:"+statement); //关闭资源 statement.close(); connection.close(); } }PreparedStatement类我们知道我们可以直接写好sql然后传入到PreparedStatement对象执行!但是当我们需要执行多条sql比如我们需要插入多个学生的成绩信息时!显然一条一条写好传入不够科学!!!因为每条语句都是只有名字和班级不同!!!如何改进呢Scanner scanner = new Scanner(System.in); String name = scanner.nextLine(); //输入姓名 int class_ = scanner.nextInt(); //输入班级 String sql = "insert into student values(?,?)"; //姓名和班级用通配符 ? 代替 //然后将sql执行 //这里的PreparedStatement 也是java.sql包下!!! PreparedStatement statement = connection.prepareStatement(sql); statement.setString(1,name); //传入第一列的姓名 statement.setInt(2,class_); //传入第二列的班级 int ret = statement.executeUpdate(); //数据库增删改操作!!!利用通配符?我们就可以输入不同人的名字和班级了!!!那如何去查看一张表的内容呢?ResultSet ret = statement.executeQuery(); //数据库查询操作!!!利用executeQuery();我们可以返回一个表结构!!!我们对这个表结构进行遍历即可!!!String sql = "select * from student"; //然后将sql执行 //这里的PreparedStatement 也是java.sql包下!!! PreparedStatement statement = connection.prepareStatement(sql); ResultSet ret = statement.executeQuery(); //数据库查询操作!!! //我们需要将查询后的数据保存在ResultSet表中!!! System.out.println("statement:"+statement); while (ret.next()){ //移动指针光标!! String name = ret.getString(1); //获取第一列的数据 int class_ = ret.getInt(2);//获取第二列的数据 System.out.println("name:"+name+" class:"+class_); }
CRUD我们对CRUD不陌生! 而CRUD究竟指的是什么呢?crud是指在做计算处理时的增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写。crud主要被用在描述软件系统中数据库或者持久层的基本操作功能。可以看到,CRUD就是增加(Create),检索(Retrieve),更新(Update),删除(Delete)操作的简称!!!而我们上节,数据库基础中介绍了数据库的CRUD而一个个数据库中是由一张张表结构保存了数据在数据库中! 所以我们来学习表的CRUD!我们来回顾一下上节针对表的一些sql语句!1.查看该数据库下的所有表!show tables;我们不能忘记当要对某个数据库中进行操作时,需要先使用该数据库(use 数据库名;)!使用show tables; 可以查看到ebook数据库下的所有表!!!我们上章也学习了如何创建一张表!我就不在一一演示啦!新增(Create)我们知道了如何创建表,那如何给表新增内容呢?单行数据,全列插入SQL语句insert into 表名 values(数据...);举例:在book表中我们插入了一条数据!!!我们查看一下:注意:在mysql中我们如果要插入字符.我们需要用' '/" "引号就像java中的字符串一样! 但是这里也可以用' '单引号!!!多行插入,指定列插入当我们需要新增的数据较多时,如果我们再采用单行插入的方式,显然不科学! 而且有时候,并不是我们全部的列都要插入数据!!!如果我们要插入多行指定列呢?SQL语句insert into 表名(列名1,2,...) values(数据1),(数据2),...;这里的列名和每行数据之间需要用,逗号隔开!列名要写在()里,并且每条数据中的数据项要对应指定列!举例:查询(Retrieve)我们插入表中的数据如何查看呢?查看表中全部数据SQL语句select * from 表名;-- 通常情况下不建议使用 * 进行全列查询 -- 1. 查询的列越多,意味着需要传输的数据量越大; -- 2. 可能会影响到索引的使用。(索引待后面的博客讲解)用*通配符,代表全部列!!!查看指定列SQL语句select 列名1,列名2... from 表名;当我们只对该表中的某些列查询时!查询字段为表达式如果我们今天对书籍进行大促销,想对书籍进行一天的价格下调,我们如何查看表呢?SQL语句select 表达式 from 表名;可以看到,我们通过表达式 price-60就查看到降价60的数据了!此操作并不会影响服务器中数据库中的内容!!数据并没有改变!!!我们还可以有多个表达式!!!别名我们可以看到当我们使用表达式时,该列的列名也就是该表达式,如果我们想起一个别名呢?又该如何操作呢?SQL语句select (表达式/列名) as 别名 from 表名;去重distinct当我们一个表中含有重复的数据时,但我们只想查看一个,如何去重呢?使用distinct关键字对某列数据进行去重:SQL语句select distinct 列名 from 表名;注意:这里的distinct在查询列的列名前!!!排序 order bySQL语句select 列1,列2....from 表名 order by 列名 排序方方式(asc/desc);-- ASC 为升序(从小到大) -- DESC 为降序(从大到小) -- 默认为 ASC (没有说明,默认升序!)没有 order by 子句的查询,返回的顺序是未定义的,永远不要依赖这个顺序NULL 数据排序,视为比任何值都小,升序出现在最上面,降序出现在最下面可以对多个字段排序,排序优先级随书写顺序!-- 查询同学各门成绩,依次按数学降序,英语升序,语文升序的方式显示 SELECT name, math, english, chinese FROM exam_result ORDER BY math DESC, english, chinese;条件查询 where比较运算符SQL语句select * from 表名 where 条件;where条件可以使用表达式,但不能使用别名。and的优先级高于or,在同时使用时,需要使用小括号( )包裹优先执行举例:当我们要根据某些特殊的条件进行查询时!!!比如我们要查询数学优秀的人数!!!基本查询:-- 查询英语不及格的同学及英语成绩 ( < 60 ) SELECT name, english FROM exam_result WHERE english < 60; -- 查询语文成绩好于英语成绩的同学 SELECT name, chinese, english FROM exam_result WHERE chinese > english; -- 查询总分在 200 分以下的同学 SELECT name, chinese + math + english 总分 FROM exam_result WHERE 总分<200;注意:在条件查询中,条件不能为别名,否者查询错误!!!and 与 or:- 查询语文成绩大于80分,且英语成绩大于80分的同学 SELECT * FROM exam_result WHERE chinese > 80 and english > 80; -- 查询语文成绩大于80分,或英语成绩大于80分的同学 SELECT * FROM exam_result WHERE chinese > 80 or english > 80; -- 观察AND 和 OR 的优先级: SELECT * FROM exam_result WHERE chinese > 80 or math>70 and english > 70; SELECT * FROM exam_result WHERE (chinese > 80 or math>70) and eng语文成绩大于80且数学成绩大于80语文成绩大于80或英语成绩大于80and和or的优先级!范围查询:between and-- 查询语文成绩在 [80, 90] 分的同学及语文成绩 SELECT name, chinese FROM exam_result WHERE chinese BETWEEN 80 AND 90; -- 使用 AND 也可以实现 SELECT name, chinese FROM exam_result WHERE chinese >= 80 AND chinese <= 90;查询语文成绩在 [80, 90] 分的同学及语文成绩in(option)-- 查询数学成绩是 58 或者 59 或者 98 或者 99 分的同学及数学成绩 SELECT name, math FROM exam_result WHERE math IN (58, 59, 98, 99); -- 使用 OR 也可以实现 SELECT name, math FROM exam_result WHERE math = 58 OR math = 59 OR math = 98 OR math = 99;查询数学成绩是 58 或者 59 或者 98 或者 99 分的同学及数学成绩模糊查询 like-- % 匹配任意多个(包括 0 个)字符 SELECT name FROM exam_result WHERE name LIKE '孙%';-- 匹配到孙悟空、孙权 -- _ 匹配严格的一个任意字符 SELECT name FROM exam_result WHERE name LIKE '孙_';--匹配到孙权!NULL的查询 is (not) NULL--查询书籍分类为空的书籍 select * from book where class is NULL; --插叙书籍分类不为空的书籍 select * from book where class is not NULL;分页查询 limit语法:-- 起始下标为 0 -- 从 0 开始,筛选 n 条结果 SELECT ... FROM table_name [WHERE ...] [ORDER BY ...] LIMIT n; -- 从 s 开始,筛选 n 条结果 SELECT ... FROM table_name [WHERE ...] [ORDER BY ...] LIMIT s, n; -- 从 s 开始,筛选 n 条结果,比第二种用法更明确,建议使用 SELECT ... FROM table_name [WHERE ...] [ORDER BY ...] LIMIT n OFFSET s; 查询前3条:从第3条开始查询4条结果!从offset偏移量为3的位置,查询4条结果!修改(Update)语法:UPDATE table_name SET column = expr [, column = expr ...] [WHERE ...] [ORDER BY ...] [LIMIT ...]SQL语句update 表名 set 列名= 修改后的值 where 条件;案例:-- 将孙悟空同学的数学成绩变更为 80 分 UPDATE exam_result SET math = 80 WHERE name = '孙悟空'; -- 将曹孟德同学的数学成绩变更为 60 分,语文成绩变更为 70 分 UPDATE exam_result SET math = 60, chinese = 70 WHERE name = '曹孟德'; -- 将总成绩倒数前三的 3 位同学的英语成绩加上 30 分 UPDATE exam_result SET math = math + 30 ORDER BY chinese + math + english LIMIT 3; -- 将所有同学的语文成绩更新为原来的 2分之一 UPDATE exam_result SET chinese = chinese /2;删除(Delete)SQL语句delete from 表名 where 条件;-- 删除孙悟空同学的考试成绩 DELETE FROM exam_result WHERE name = '孙悟空'; -- 删除整张表数据 -- 准备测试表 DROP TABLE IF EXISTS for_delete; CREATE TABLE for_delete ( id INT, name VARCHAR(20) ); -- 插入测试数据 INSERT INTO for_delete (name) VALUES ('A'), ('B'), ('C'); -- 删除整表数据 DELETE FROM for_delete;
数据库的操作显示当前数据库显示数据库操作,我们可以查看我们mysql下已经创建好的数据库,以及mysql自带的一些数据库!SQL语句show databases;注意我们这里的SQL语句都要带上; 和其他语言一样!不过其实SQL语句有些并不都有加;但是在命令行下面,如果我们不加上;计算机并不知道我们这个语句是否结束,我们回车是否是为了换行!!!所以SQL语句的;我们还是要带上!!!我们可以看到博主中的mysql数据库中有15数据库!!!其中包含mysql自带库sys,mysql等等!!!创建数据库我们如何创建一个自己的数据库呢?肯定也是通过SQL语句啦SQL语句create database `数据库名`;可以看到我们成功的创建了一个名为java_2022的数据库!当我们再次查看mysql时java_2022数据库已经存在!!!注意只要我们的sql语句没有输入错误,并且该数据库中,不含你要创建的数据库,那么你就能将该数据库创建成功!!并且但我们进行一条SQL语句后,后面都会提示是否成功!!!如果看到OK,n row affected; (成功,多少条语句受到影响的提示那么你的sql就执行成功了);使用数据库当我们创建了数据库后,我们便可以使用该库储存数据了!而我们的mysql数据库是通过一张张的表结构,保存数据,存储到数据库中!!!SQL语句use 数据库名;执行完sql语句后,便选中了java_2022数据库了我们就可以对该数据库进行数据的操作(表的增删查改)删除数据库当我们需要删除某些数据库时!!!SQL语句drop database 数据库名;我们再次查看数据库:mysql中已经删除了名为java_2022的数据库!!!注意:删除操作是否危险,谨慎使用!!!当我们此条语句一执行,那么该数据库中的表和数据就不存在了!!!我们知道数据库顾名思义就是存储数据的,而如今是个数据的时代,有些数据是十分值钱的,如果我们在今后的工作中将公司的某些重要的数据库删除,那么后果不堪设想!!!所以删库操作需谨慎!!!常用数据类型像我们其他的编程语言一样,mysql也有自己的数据类型!!!我们可以通过不同的类型记录不同的数据例如:java中 用;byte int short long 类型存储整型,通过该整型字节大小,存到对应的类型中,通过string存储字符型!mysql常用的数据类型数值类型分为整型和浮点型: 有符号范围:-2^(类型字节数8-1)到 2 ^(类型字节数8-1)-1, 如int是4字节,就是-2^ 31 到 2^31-1无符号范围:0到 2^ (类型字节数*8)-1,如int就是2^32-1尽量不使用unsigned,对于int类型可能存放不下的数据,int unsigned同样可能存放不下,与其如此,还不如设计时,将int类型提升为bigint类型。字符串类型 日期类型我们可以通过timestamp类型,记入数据库中该条数据的添加时间!表的操作当我们要对某一数据库进行表操作时,我们先要选中数据库,也就是刚刚我们的使用数据库操作use 数据库名;显示当前数据库表show tables;可以看到和显示数据库一样,通过该sql我们便可以查看,当前数据库中有多少张表!!可以看到 ebook数据库中有4张表!!!查看表结构desc 表名;我们通过该sql可以查看某张表的变量以及类型!创建表create table 表名(变量 类型;.....变量 类型);注意:mysql创建字段时 ,变量名在前,类型在后!!!删除表drop table 表名;危险操作,慎重!!!
内部类内部类初识内部类顾名思义就在类的内部中的类!我们知道,类中可以有两种重要的成员,成员变量(字段/属性)和方法(行为),实际上java还允许类有一种成员——内部类!java支持在一个类中定义另一个类,这样的类就称为内部类,而包含内部类的类称为内部类的外嵌类!内部类是类的第5大成员类的5大成员:属性,方法,构造器,代码块,内部类内部类最大特点:可以访问类中的私有属性,体现类和类的包含关系!内部类是java一个重点和难点,并且在java底层代码中有很多内部类的使用!嵌套:嵌套指的是一种类之间的关系,而不是对象之间的关系。外嵌类对象并不会包含内部类类型的子对象!基础语法class 外嵌类类名 { class 内部类类名 { } }外嵌类和内部类的之间的关系:1.外嵌类中的成员变量内部类中仍然有效,外嵌类方法可以在内部类中的方法中调用!class People{ protected String name; protected int age; public void speak(){ System.out.println(name+":speak()"); } class Child{ //内部类可以访问外嵌类中的成员变量! String name = People.this.name; public void speak1(){ speak(); //在内部类方法中调用外嵌类中的方法! } public void speak(){ People.this.speak();//当内部类中的方法和外嵌类方法名相同时 //通过类名.this.方法名调用外嵌类同名方法 // speak(); } } }2.内部类的类体中不能声明类变量和类方法//内部类中不含类变量和类方法! class People{ class Child{ static String sex; //error public static void eat(){ //error System.out.println("eat::()!"); } } }因为我们知道stataic修饰的类变量和类方法属于类,类加载时便一起加载了,而外嵌类加载完并不会加载内部类,而static类型的变量和方法在类加载时会初始化,这两者就会导致内部类未加载,但其成员却初始化了,这是矛盾的,所以内部类中的变量和方法不能被static关键字修饰!3.外嵌类的类体中可以用内部类声明的对象作为外嵌类的成员。class People{ //外嵌类中声明内部类对象 Child child = new Child(); public void speak(){ System.out.println(); } class Child{ String sex; public void eat(){ System.out.println("eat::()!"); } } }4.内部类仅提供他的外嵌类使用,其他类不可以用某个类的内部类声明对象class People{ Child child = new Child(); public void speak(){ child.eat(); //内部类对象只能在外嵌类中使用! System.out.println(); } class Child{ public void eat(){ System.out.println("eat::()!"); } } } public class Test_1 { public static void main(String[] args) { People.Child child = new People.Child();//error //内部类无法在其他类中创建对象 } }5.内部类可以用protected和private修饰,而一般类只能用public修饰!class People{ public void speak(){ System.out.println(); } private class Child{ //内部类可用private和protect修饰 public void eat(){ System.out.println("eat::()!"); } } } protect class A{ //error 一般类只能用public 修饰 }6.当外嵌类和内部类中成员变量或方法同名时,相互使用或调用语法规则//在外嵌类中调用内部类中的同名的方法! class People{ public void eat(){ System.out.println("People eat::()!"); } public void speak(){ //People的eat eat(); this.eat(); People.this.eat(); System.out.println("======"); //Child的eat new Child().eat(); } private class Child{ public void eat(){ System.out.println("Child eat::()!"); } } } public class Test_1 { public static void main(String[] args) { People people = new People(); people.speak();//调用谁的eat()? } }在内部类中可以通过类名.this.方法名调用外嵌类中同名方法!7.内部类的字节码文件名字和通常的类不同,内部类字节码文件名字格式是:外嵌类类名$内部类类名.class内部类分类我们刚刚大概了解了内部类的语法和使用,可能有点绕,没有捋清楚,不要急,bug郭带你仔细学一遍!根据内部类在外部类中的位置可以分为4种:局部内部类,定义在外部类中的局部范围(方法和代码块中)匿名内部类(学习的重点和难点)也通常定义在局部成员内部类(定义在外部类的成员位置)静态内部类(用stataic修饰的内部类)我会一一给大家详细介绍这四种内部类的使用方法和细节!局部内部类在外嵌类的局部位置,通常在方法中或代码块中!局部内部类特点:1.定义在外部类局部(方法或者代码块中)2.作用域在局部,只能在他的作用域中使用(方法或者代码块中)3.本质还是一个类(可以被继承)4.属于局部变量,不能用限定符修饰(可用finall修饰后不可被继承)5.内部类可以直接访问外部类中的属性和方法(包括private修饰)6.外部类访问局部内部类,只能创建内部类对象访问(且只能在该局部中)7.当外部类和内部类重名时,遵守就近原则//局部内部类 class Outer{//外部类 private int m = 10; //属性 private static String s = "Outer"; public void f1(){ //方法 System.out.println("Outer::f1"); } public void f2(){//方法 /* 局部内部类: 1.定义在外部类局部(方法或者代码块中) 2.作用域在局部,只能在他的作用域中使用(方法或者代码块中) 3.本质还是一个类(可以被继承) 4.属于局部变量,不能用限定符修饰(可用finall修饰后不可被继承) 5.内部类可以直接访问外部类中的属性和方法(包括private修饰) 6.外部类访问局部内部类,只能创建内部类对象访问(且只能在该局部中) 7.当外部类和内部类重名时,遵守就近原则 */ class Inner {//内部类 private int a = 12;//属性 public void f2(){//方法 System.out.println("Inner::f2"); f1(); //局部内部类直接访问外部类成员和方法 System.out.println("Outer:private m="+m+" Inner::a="+a); } } //外部类只能通过创建外部类对象访问内部类 Inner inner = new Inner(); inner.f2();//方法同名就近原则 class Inner1 extends Inner {//继承Inner类 } } } public class Test_1{ public static void main(String[] args) { Outer outer = new Outer(); outer.f2(); } }因为有前面学习的基础,bug郭就不一一列举代码了,可以自行尝试!匿名类我们是否想过一个问题,内部类既然只能在外嵌类中使用,显然当我们只需要使用该内部类一次时,常规方法创建一个内部类比较麻烦,价值不高,有其他方法解决该问题嘛?那就是匿名类!匿名类是不能有名字的类,它们不能被引用,只能在创建时用 new 语句来声明它们。匿名类特点:1.本质是一个类2.没有名字的类3.属于内部类4.还是个对象匿名类语法格式:new 类名或接口(参数列表){ //类体 };匿名类使用演示://代码演示 class Outer2{ //外部类 private int a = 1; void f1(IA ia){ //方法,参数实现向上转型 ia.eat(); //调用重写方法 } } interface IA{ void eat(); } //传统方式 class A implements IA{//我们要创建一个类实现IA接口才能创建对象 @Override//在类中重写接口中的方法 public void eat() { System.out.println("传统eat..."); } } public class Test_2 { public static void main(String[] args) { Outer2 outer2 = new Outer2(); outer2.f1(new A()); //传统方式需要创建A对象 outer2.f1(new IA() {//匿名类方式,在new的时候重写方法即可 //类体 @Override public void eat() { System.out.println("匿名eat..."); } }); } }与子类有关的匿名类假如没有显式地声明一个类的子类,但又想用这个子类创建对象,那么该如何实现这一目的呢?java允许用户直接使用一个类的子类的类体创建一个子类的对象,也就是说,在创建子类对象时,除了使用父类的构造方法外还有类体,此类体被认为是一个子类去掉类声明后的类体,称作匿名类。匿名类就是一个子类,由于无名可以,所以不可能用匿名类声明对象,当可以直接用匿名类创建一个对象。语法规则://1.方式一 new 类名(){ //匿名类类体 } //2.方式二 //使用了父类提供的(带参数的)构造方法 new 类名(参数){ //匿名类类体 }匿名类特点:1.匿名类可以继承父类的方法,也可也重写父类的方法。2.在使用匿名类时,必然存在某个类中直接用匿名类创建对象,因此匿名类一定是内部类。3.匿名类可以访问外嵌类中的成员变量和方法。在匿名类的类体中不可以声明stataic成员变量或方法。4.由于匿名类是一个子类,且没有类名,所以在用匿名类创建对象时要直接使用父类的构造方法。5.尽管匿名类创建对象没有经过类声明的步骤,但匿名对象的引用可以传递给一个匹配的参数。//实例一 abstract class Bank{ int money; public Bank(){ money = 100; } public Bank(int money){ this.money = money; } public abstract void output(); } class ShowBank{ void showMess(Bank bank){ //参数是Bank类型 bank.output(); } } public class Test_2 { public static void main(String[] args) { ShowBank showBank = new ShowBank(); showBank.showMess(new Bank() { //向参数传递Bank的匿名子类对象 @Override public void output() { money += 100; System.out.println("bug郭 银行:"+money); } }); showBank.showMess(new Bank(500){向参数传递Bank的匿名子类对象 @Override public void output() { money += 100; System.out.println("bug郭 银行:"+money); } }); } }可以看到与子类有关的匿名类的使用,当我们只需要使用子类对象一次时,我们可以使用匿名类对象!与接口有关的匿名类与接口有关的匿名类和与子类有关的匿名类类似!当某个方法的参数是接口类型时,那么可以使用接口名和类体组合创建一个匿名对象传递方法的参数,类体必须重写接口中的全部方法。interface SpeakHello{ void speak(); } class HelloMachine{ public void turnOn(SpeakHello hello){ hello.speak(); } } public class Test_3 { public static void main(String[] args) { HelloMachine machine = new HelloMachine(); machine.turnOn(new SpeakHello() { //和接口有关的匿名类 @Override public void speak() { System.out.println("hello,you are welcom!"); } }); machine.turnOn(new SpeakHello() {//和接口有关的匿名类 @Override public void speak() { System.out.println("你好,欢迎光临!"); } }); } }匿名类小测试题目:1有一个铃声接口 Bell,里面有个 ring 方法。2.有一个手机类Cellphone,具有闹钟功能 alarmClock,参数是Bell类型3.测试手机类的闹钟功能,通过匿名内部类(对象)作为参数,打印:懒猪起床了4.再传入另一个匿名内部类(对象),打印:小伙伴上课了答案:public class Test_3{ public static void main(String[] args) { CellPhone cellPhone = new CellPhone(); cellPhone.alarmClock(new Bell() { @Override public void ring() { System.out.println("懒猪起床了"); } }); cellPhone.alarmClock(new Bell() { @Override public void ring() { System.out.println("小伙伴上课了"); } }); } } interface Bell{ //接口 void ring();//方法 } class CellPhone {//类 public void alarmClock(Bell bell) {//形参是 Bell 接口类型 System.out.println(bell.getClass()); bell.ring();//动态绑定 } }匿名类深入了解匿名类真的就没有名字嘛?错,只是我们不知道名字而已,就像匿名信一样,写信者肯定有名字的!其实在java底层jdk中我们java匿名类系统会给他定义一个名字的!我们可以看jdk系统自动给匿名类取名为:调用匿名类的类类名$1总结:我们什么时候可以用到匿名类呢?当我们只需要使用一次该类的子类对象或者使用一次该接口对象时我们就可以使用匿名类!匿名类在创建对象时,构建!采用 new 类名(接口名){匿名类体};实现!匿名类通常当做实参直接传递,简洁高效!用Lambda表达式代替匿名类我们先来了解一下什么是Lambda表达式~函数接口和Lambda表达式函数接口如果一个接口中有且只有一个abstract方法,称这样的接口是单接口。从JDK 8开始,java使用Lambda表达式,并将单接口称为函数接口。Lambda表达式下面add()是一个通常的方法(函数):int add(int a,int b){ retunrn a + b; }Lambda表达式就是一个用匿名方法(函数),用Lambda表达式表达同样功能的匿名方法://省略函数名 (int a,int b)->{ return a+b; }或(a,b)->{ return a+b; }即Lambda表达式就是只写参数列表和方法体的匿名方法(参数列表和方法体之间用符号->连接)//语法规则 (参数列表)->{ //方法体 }Lambda表达式的值由于Lambda表达式过于简化,所以必须有特殊上下文,编译器才能推断出Lambda表达式到底是哪个方法,才能计算值,Lambda表达式的值就是方法的入口地址。因此,java中的Lambda表达式主要用在单接口,即函数接口!使用举例:interface ShowMessage{ //函数接口 void show(String str); } public class Test_4 { public static void main(String[] args) { //接口变量中存放Lambda表达式的值 ShowMessage sm = (s)->{ //Lambda表达式(匿名方法) System.out.println("java yyds!"); System.out.println(s); System.out.println("hond on!"); }; sm.show("bug郭"); //接口回调Lambda表达式实现接口方法 } }因为是匿名方法,当我们要使用该接口创建一个对象时,就可以通过Lambda表达式实现接口方法,创建对象,接口回调该方法!大概了解了Lambda表达式,我们就可以用Lambda表达式代替匿名类了就是在使用匿名类时,当我们要实现接口函数(单接口中的方法)时,用Lambda表达式非常便利!interface ShowMessage{ //函数接口 void show(); } class Test{ public void test(ShowMessage message){ message.show(); } } public class Test_4 { public static void main(String[] args) { Test test = new Test(); test.test(()->{ //向形参message传递Lambda表达式的值 System.out.println("java yyds!"); }); } }成员内部类顾名思义:成员内部类应该是在外部类成员位置上特点:1.定义在外部类成员位置上2.可以使用访问修饰限定符修饰(成员可以,变量不行)3.没有stataic修饰//代码演示 class Outer{//外部类 private int a = 10; public void f1(){ System.out.println("Outer:f1"); } class Inner{//成员内部类 private int b = 14; protected void f2(){ System.out.println("inner::f2"); } } public Inner getInner(){ //实例方法获取内部类对象 return new Inner(); } } public class Test_1 { public static void main(String[] args) { //创建成员内部类对象 //1.直接创建 语法: 外部类.成员内部类 对象名 = new 外部类().new 成员内部类() Outer.Inner inner = new Outer().new Inner(); inner.f2(); //2.方法返回值接收 Outer outer = new Outer(); Outer.Inner inner1 = outer.getInner(); inner1.f2(); } }成员内部类和其他内部类语法相差不大注意一点即可!因为成员内部类是定义在外部类成员位置,所以能够在其他类中创建成员内部类对象!语法为:创建成员内部类对象1.直接创建:外部类.成员内部类 对象名 = new 外部类().new 成员内部类()2.在外部类创建一个方法返回值接收内部类对象静态内部类静态内部类和成员内部类类似。只是它用static修饰而已我们来了解一下:静态内部类特点:放在外部类的成员位置使用static 修饰可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员可以添加任意访问修饰符(public、protected 、默认、private),因为它的地位就是一个成员作用域 :同其他的成员,为整个类体//静态内部类 class Outer1{ //外部类 private int a = 10; public void f1(){ System.out.println("Outer:f1"); } static class Inner1{//成员内部类 private int b = 14; protected void f2(){ System.out.println("inner::f2"); } } public Inner1 getInner(){ //实例方法获取内部类对象 return new Inner1(); } public static Inner1 getINner1(){//静态方法获取内部类对象 return new Inner1(); } } public class Test_2 { public static void main(String[] args) { //获取静态内部类对象 //1.直接创建 Outer1.Inner1 inner = new Outer1.Inner1(); inner.f2(); //2.方法返回值接收 //1).静态方法返回 Outer1.Inner1 inner1= Outer1.getINner1(); //类名.方法 inner1.f2(); //2).实例方法返回 Outer1 outer2 = new Outer1(); Outer1.Inner1 inner11 = outer2.getInner(); //对象.方法 inner11.f2(); } }总结内部类中的来说可以简单的分为两种:1.定义在外部类局部位置的:局部内部类和匿名类2.定义在外部类成员位置的:成员内部类和静态内部类我们只要搞清楚他们的的作用域还有编译和运行时怎样即可并且java匿名类是一个重点和难点结合了之前我们学习的知识点:继承,多态等知识!
接口基本语法//定义接口类型 interface IAnimal{ //抽象方法 // public static final 字段 }使用interface 定义一个接口接口中的方法一定是抽象方法, 因此可以省略 abstract接口中的方法一定是public, 因此可以省略public接口中只能包含抽象方法,对于字段来说, 接口中只能包含静态常量(final static)interface IAnimal{ // public static final 字段 可以省略public static final String name = "animal"; //因为是final 修饰,需要赋初值! int age = 18; //抽象方法 public abstract void speak(); void eat(); //省略public abstract }子类 使用 implements继承接口。 此时表达的含义不再是 “扩展”, 而是 “实现”在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例。接口不能单独被实例化。扩展(extends)vs 实现(implements)扩展指的是当前已经有一定的功能了, 进一步扩充功能。实现指的是当前啥都没有, 需要从头构造出来。接口不能用限定符修饰实现多个接口接口弥补了java无法多继承的缺陷!我们可以实现多个接口基本语法有时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的。然而 Java 中只支持单继承, 一个类只extends 一个父类。 但是可以同时实现多个接口, 也能达到多继承类似的效果。class 类名 implement interfa1,interface2,....interfaceN{ //实现所有接口中的方法! }实例一下面我们通过类来表示张三//实现多个接口 interface IAnimal{ String name = "animal"; int age = 18; void speak(); void eat(); } interface IPeople{ String iQ = "140"; void study(); } class Zhansan implements IAnimal,IPeople{ @Override public void speak() { System.out.println("Speak Chinese!"); } @Override public void eat() { System.out.println("Eat food!"); } @Override public void study() { System.out.println("Study java!"); } } public class Test_6 { public static void main(String[] args) { Zhansan zhansan = new Zhansan(); zhansan.eat(); zhansan.speak(); zhansan.study(); } }实例二现在我们通过一个类来表示一组动物。class Animal { protected String name; public Animal(String name) { this.name = name; } }另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”。interface IFlying { void fly(); } interface IRunning { void run(); } interface ISwimming { void swim(); }接下来我们创建几个具体的动物猫, 是会跑的。class Cat extends Animal implements IRunning { public Cat(String name) { super(name); } @Override public void run() { System.out.println(this.name + "正在用四条腿跑"); } }鱼, 是会游的。class Fish extends Animal implements ISwimming { public Fish(String name) { super(name); } @Override public void swim() { System.out.println(this.name + "正在用尾巴游泳"); } }青蛙, 既能跑, 又能游(两栖动物)class Frog extends Animal implements IRunning, ISwimming { public Frog(String name) { super(name); } @Override public void run() { System.out.println(this.name + "正在往前跳"); } @Override public void swim() { System.out.println(this.name + "正在蹬腿游泳"); } }提示,IDEA中使用 ctrl + i 快速实现接口还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”class Duck extends Animal implements IRunning, ISwimming, IFlying { public Duck(String name) { super(name); } @Override public void fly() { System.out.println(this.name + "正在用翅膀飞"); } @Override public void run() { System.out.println(this.name + "正在用两条腿跑"); } @Override public void swim() { System.out.println(this.name + "正在漂在水上"); } }上面的代码展示了 Java面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。继承表达的含义是 is - a 语义, 而接口表达的含义是 具有xxx 特性 。猫是一种动物, 具有会跑的特性。青蛙也是一种动物, 既能跑, 也能游泳鸭子也是一种动物, 既能跑, 也能游, 还能飞这样设计有什么好处呢?时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力!例如, 现在实现一个方法, 叫 “散步”public static void walk(IRunning running) { System.out.println("我带着伙伴去散步"); running.run(); }在这个walk方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行Cat cat = new Cat("小猫"); walk(cat); Frog frog = new Frog("小青蛙"); walk(frog);甚至参数可以不是 “动物”, 只要会跑class Robot implements IRunning { private String name; public Robot(String name) { this.name = name; } @Override public void run() { System.out.println(this.name + "正在用轮子跑"); } } Robot robot = new Robot("机器人"); walk(robot);接口的使用实例我们java系统包中的很多类都实现了很多接口,使得该类具有某种属性给对象排序!//创建student类 class Student{ private String name; private double score; public Student(String name,double score){ this.name = name; this.score = score; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", score=" + score + '}'; } }public static void main(String[] args) { //学生对象数组 Student[] students = new Student[]{ new Student("张三",88.7), new Student("李四",67), new Student("王五",98), }; Arrays.sort(students); //排序 System.out.println(students); }显然,我们无法排序对象类型的数据,报异常,说我们Student类没有实现Comparable接口!Comparable接口中的抽象方法 compareTo方法详细信息int compareTo(T o)将此对象与指定的对象进行比较以进行排序。 返回一个负整数,零或正整数,因为该对象小于,等于或大于指定对象。实现程序必须确保sgn(x.compareTo(y)) == -sgn(y.compareTo(x))所有x和y。 (这意味着x.compareTo(y)必须抛出异常if y.compareTo(x)引发异常。)实施者还必须确保关系是可传递的: (x.compareTo(y)>0 && y.compareTo(z)>0)表示x.compareTo(z)>0。最后,实施者必须确保x.compareTo(y)==0意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z)) ,对于所有z 。强烈建议,但不要严格要求(x.compareTo(y)==0) == (x.equals(y)) 。 一般来说,任何实现Comparable接口并违反这种情况的类应清楚地表明这一点。 推荐的语言是“注意:此类具有与equals不一致的自然排序”。在前面的描述中,符号sgn( ) 表达式表示数学符号函数,其定义根据表达式的值是否为负,零或正返回的-1一个,0,或1。参数o -要比较的对象。结果负整数,零或正整数,因为该对象小于,等于或大于指定对象。异常NullPointerException- 如果指定的对象为空ClassCastException- 如果指定的对象的类型阻止它与该对象进行比较。看到这么多文字,是不是头都大了!没有关系,bug郭也头大,不过我知道咋用//创建student类并且实现Comparable接口 class Student implements Comparable{ private String name; private double score; public Student(String name,double score){ this.name = name; this.score = score; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", score=" + score + '}'; } @Override //实现compareTo方法 public int compareTo(Object o) { Student s = (Student)o; //对o强转, o指的是待比较对象 if (this.score > s.score) { //当前对象的score值大 return -1; } else if (this.score< s.score) { //对象o的score值大 return 1; } else { return 0; } } }我们可以看到,这时排的升序,如果要降序就只需将大于号变成小于号即可!在 sort 方法中会自动调用compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student类型的对象.然后比较当前对象和参数对象的大小关系(按分数来算).如果当前对象应排在参数对象之前, 返回小于0 的数字;如果当前对象应排在参数对象之后, 返回大于 0 的数字;如果当前对象和参数对象不分先后, 返回0;注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备compareTo这样的能力. 通过重写compareTo 方法的方式, 就可以定义比较规则。为了进一步加深对接口的理解, 我们可以尝试自己实现一个sort方法来完成刚才的排序过程(使用冒泡排序)!public static void sort(Comparable[] array) { for (int bound = 0; bound < array.length; bound++) { for (int cur = array.length - 1; cur > bound; cur--) { if (array[cur - 1].compareTo(array[cur]) > 0) { // 说明顺序不符合要求, 交换两个变量的位置 Comparable tmp = array[cur - 1]; array[cur - 1] = array[cur]; array[cur] = tmp; } } } }接口间的继承接口可以继承一个接口, 达到复用的效果. 使用extends关键字!interface IAmphibious extends IRunning, ISwimming { } class Frog implements IAmphibious { @Override public void run() { System.out.println("Frog run!"); } @Override public void swim() { System.out.println("Frog swim!"); } } public class Test_1 { public static void main(String[] args) { Frog frog = new Frog(); frog.run(); frog.swim(); } }通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog类, 就继续要实现 run 方法,也需要实现 swim方法!接口间的继承相当于将多个接口合并在一起!
效果展示源码MyEmail.java//导入mail的包 import org.apache.commons.mail.EmailException; import org.apache.commons.mail.SimpleEmail; //创建MyEmail类 class MyEmail{ /** * @param userName 发件人邮箱地址 * @param addressee 收件人邮箱地址 * @param subject 邮件主题 * @param massage 邮件内容 */ //静态方法,可以通过类名调用 public static void emailsend(String userName,String addressee,String subject,String massage){ SimpleEmail email = new SimpleEmail(); email.setSslSmtpPort("465"); //QQ邮箱端口号 email.setHostName("smtp.qq.com"); //QQ邮箱服务器 email.setAuthentication(userName, "hjujldwikezyfigf"); // email.setCharset("UTF-8"); //设置编码方式 UTF-8 try { email.addTo(addressee); //收件人 email.setFrom(userName); //发送者 email.setSubject(subject); //主题 email.setMsg(massage); //内容 email.send(); //发送成功 System.out.println("发送成功~"); } catch (EmailException var3) { . var3.printStackTrace(); } } }Test.java//测试 public class Test { public static void main(String[] args) { String userName = "1485714215@qq.com"; //发送人邮箱 String addressee = "1874867415@qq.com"; //收件人邮箱 String subject = "感恩节快乐"; //邮件主题 String massage = "思念是一首诗写在最深情的午夜," + "感激是一首歌在最思念的日子唱响," + "你是一辈子无法忘记的人," + "感谢你出现在我生命里。感恩节快乐。"; //邮件内容 MyEmail.emailsend(userName, addressee, subject, massage); } }教程重点//需要添加的代码 <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-email</artifactId> <version>1.5</version> </dependency> </dependencies>注意事项看到这里,你以为就可以了吗,并不是,我们还有一个重要部分没弄!看到这个password这个需要我们通过自己的QQ邮箱获取设置的!设置方法开启服务,获取密码开启后,我们获取了自己的password便可以发送邮件了!!!注意事项那我可以写个循环发送恶搞嘛!!!不可,毕竟我们使用的时QQ邮箱的服务器,人在屋檐下哪能不低头,而且有可能把你的邮件拦截,以后有能力可以写一个服务器!!!那我利用这个可以群发消息!!!并不好,可能邮件太多,不能全部发送成功切记不可利用这个发送骚扰消息,后果自负!!!!你获取的password不能被不法之人利用,危险~
初识javaJava是一种优秀的程序设计语言,它具有令人赏心悦目的语法和易于理解的语义。不仅如此,Java还是一个有一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于嵌入式系统、移动终端、企业服务器、大型机等各种场合。下面我们通过Java官方提供的数据来一起感受一下,Java到底有多“火”。简而言之,java就是时下比较热门的高级编程语言!Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。java优势Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、多线程、动态性等特点 。Java可以编写桌面应用程序、Web应用程序、分布式系统和嵌入式系统应用程序等。平台独立与可移植型java有自己的开发工具!1996年1月,Sun公司发布了Java的第一个开发工具包(JDK 1.0),这是Java发展历程中的重要里程碑,标志着Java成为一种独立的开发工具。9月,约8.3万个网页应用了Java技术来制作。10月,Sun公司发布了Java平台的第一个即时(JIT)编译器。啥是JDK?简单说就是java开发工具包,同过这个工具包就可以实现java程序的编译和运行,无需安装其他集成开发环境即可,只有电脑安装了JDK即可实现。JDK中包含了JVM(java虚拟机),通过java虚拟机,可以将java程序转化成计算机可以识别的字节码文件(机器码文件)。实现了可移植型。JVM虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JDK的安装,请移步博主的另一篇博客!学习java必备java是世界上最好的语言?并没有所谓的最好的语言。每个语言都有自己的优势和擅长的领域。存在即合理,并没有好坏之分。java之父詹姆斯·高斯林 (James Gosling)是一名软件专家,1955年5月19日出生于加拿大,Java编程语言的共同创始人之一,一般公认他为“Java之父”。main方法java和我们C语言一样具有main函数,但是java中的函数换了叫法,在java中函数就是方法main函数在C语言中是程序的入口,而java中也是相同,main方法是java是java程序的入口,我们通过写一个java程序,来了解一下java中的main方法!实现第一个java程序!在任意一个路径下创建一个.java文件,记得打开文件后缀名显示!然后我们创建的就是java程序文件了!文件内容我们编译运行一下!我们已经安装好了jdkjava开发工具,就可以对java程序进行编译运行,键盘:wind+r 输入cmd打开命令控制窗口!进入你刚刚java程序所在盘,如果是D盘,就输入d(D):回车即可!输入cd 空格,把刚刚java程序所在路径输入上去!进入java程序所在文件下就可以对java程序进行编译和运行操作了!JVM中自带了javac编译命令,和java运行命令。输入javac命令加空格便对java程序进行了编译!此时多了一个.class文件,这便是我们所说的字节码文件!输入java空格,文件名便实现了运行!注:javac和java命令后面需要空格。javac编译需要.java后缀,java运行不需要!这就是一个简单的java程序。public class hehe { public static void main(String[] args){ System.out.println("hehe"); } }public:java中的关键字是一个条件修饰符;class:java中的类,相当于C语言中的一个源程序,一个java程序中有且仅有一个public修饰的class类且要和文件名相同;hehe:是类名,相当于C语言中的源文件名;main方法:public static void main(String[] args){ System.out.println("hehe"); }java中的main方法比较复杂,记住这种写法就好了;String[] args:字符串数组参数,java中有字符串类型。System.out.println("hehe");打印函数;java中有3种打印方式:System.out.println("hehe"); //打印后自动换行 System.out.println("haha"); //打印后不换行 int a=12; System.out.printf("%d",a); //保留了C语言打印方式!
指针笔试题笔试题1int main() { int a[5] = { 1, 2, 3, 4, 5 }; int *ptr = (int *)(&a + 1); printf( "%d,%d", *(a + 1), *(ptr - 1)); return 0; } //程序的结果是什么?解析int main() { int a[5] = { 1, 2, 3, 4, 5 }; int* ptr = (int*)(&a + 1); //&a+1得到数组指针加一,指向数组末尾 //将数组指针强制类型转换成int*存放在ptr中 printf("%d,%d", *(a + 1), *(ptr - 1)); // 2,5 //a+1 a首元素地址,加一后得到第二个元素地址 // *(a + 1)解引用后得到 2 //ptr指针类型为int* ptr指向数组末尾 //ptr-1得到数组最后一个元素地址 // *(ptr - 1) 解引用得到最后一个元素5 return 0; }笔试题2//这里已知结构体的大小是20个字节 struct Test { int Num; char *pcName; short sDate; char cha[2]; short sBa[4]; }*p; //假设p 的值为0x100000。 如下表表达式的值分别为多少? int main() { printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; }解析//这里已知结构体的大小是20个字节 struct Test { int Num; char* pcName; short sDate; char cha[2]; short sBa[4]; }*p; //假设p 的值为0x100000。 如下表表达式的值分别为多少? int main() { p = (struct Test*)0x100000; printf("%#08x\n", p + 0x1); //0x00100020 //p是结构体指针类型,p+1跳过一个结构体大小 //结构体大小为20字节,所以跳过20 // 0x100000+20 = 0x00100020 printf("%#08x\n", (unsigned long)p + 0x1);//0x100001 //(unsigned long)p 将0x100000强制类型转换 //转换成unsigned long 后 + 0x1就是直接加 //0x100000 + 0x1 = 0x100001 printf("%#08x\n", (unsigned int*)p + 0x1);//0x100004 //(unsigned int*)p 转换成 (unsigned int*)p // 加 1跳过一个unsigned int类型大小 4 //0x100000 + 4 =0x100004 return 0; }笔试题3int main() { int a[4] = { 1, 2, 3, 4 }; int *ptr1 = (int *)(&a + 1); int *ptr2 = (int *)((int)a + 1); printf( "%x,%x", ptr1[-1], *ptr2); return 0; }解析int main() { int a[4] = { 1, 2, 3, 4 }; int* ptr1 = (int*)(&a + 1); //&a得到数组地址,&a+1跳过一个数组 //(int*)(&a+1)将数组指针强制转换 int* ptr2 = (int*)((int)a + 1); //(int)a 将a数组首元素地址强制转换int //(int)a+1后,a的地址码 + 1 //(int*)(int)a+1 转换成int*指针 printf("%x,%x", ptr1[-1], *ptr2);//4 , 2000000 //ptr1-1减去int类型大小,向前移动一个int型 //ptr2 指向了整个数组的第二个字节,访问一个int型 return 0; }笔试题4#include <stdio.h> int main() { int a[3][2] = { (0, 1), (2, 3), (4, 5) }; int *p; p = a[0]; printf( "%d", p[0]); return 0; }解析int main() { int a[3][2] = { (0, 1), (2, 3), (4, 5) }; //逗号表达式,初始化为{1,3,5} int* p; p = a[0]; //将a[0]第一行地址存入int*类型的p中 //即类型进行了转换成了int* printf("%d", p[0]);// 1 //此时的 p 是第一行第一个元素的地址 //p[0]就是 1 return 0; }笔试题5int main() { int a[5][5]; int(*p)[4]; p = a; printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); return 0; }X解析int main() { int a[5][5]; int(*p)[4]; //p为int [4] 类型的数组指针 p = a; //a为首元素地址,第一行的地址 //a为 int [5]数组指针 //将 a 赋值给 p printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); // fffffffc -4 // &p[4][2] - &a[4][2] = -4 return 0; }笔试题6int main() { int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int *ptr1 = (int *)(&aa + 1); int *ptr2 = (int *)(*(aa + 1)); printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1)); return 0; }解析int main() { int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int* ptr1 = (int*)(&aa + 1); //&aa+1 跳过一个数组大小,数组指针 // aa+1指向数组的末尾 //(int*)(&aa + 1)转换成int*指针 int* ptr2 = (int*)(*(aa + 1)); //aa+1首元素地址 + 1 指向第二个元素地址 // 而这里的首元素地址就是第一行地址 // 加一后指向第二行地址 //(int*)(*(aa + 1))转换成 int*指针 printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10,5 //ptr1-1数组末尾指针-1 ptr1是int*指针 // 所以减一得到 最后一个元素地址 // 解引用后得到元素 10 //ptr2-1第二行数组地址-1 // 因为prt2是int*指针类型-1 跳过一个int类型 //*(ptr2-1)得到第一行最后一个元素 5 return 0; }笔试题7#include <stdio.h> int main() { char *a[] = {"work","at","alibaba"}; char**pa = a; pa++; printf("%s\n", *pa); return 0; }解析int main() { char* a[] = { "work","at","alibaba" }; char** pa = a; pa++; //pa++ 后指向char* "at" printf("%s\n", *pa); // at return 0; }笔试题8int main() { char *c[] = {"ENTER","NEW","POINT","FIRST"}; char**cp[] = {c+3,c+2,c+1,c}; char***cpp = cp; printf("%s\n", **++cpp); printf("%s\n", *--*++cpp+3); printf("%s\n", *cpp[-2]+3); printf("%s\n", cpp[-1][-1]+1); return 0; }解析int main() { char* c[] = { "ENTER","NEW","POINT","FIRST" }; char** cp[] = { c + 3,c + 2,c + 1,c }; char*** cpp = cp; printf("%s\n", **++cpp); //POINT printf("%s\n", *-- * ++cpp + 3);//ER printf("%s\n", *cpp[-2] + 3); //ST printf("%s\n", cpp[-1][-1] + 1);//EW return 0; }
jdk和jre介绍JDKjava软件开发工具包JRMjava运行环境Jvmjava虚拟机Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。因为java虚拟机,先将代码生成字节码文件,然后可以在不同平台下编译运行,这就是java移植强的原因!三者关系可以看出只要我们安装了jdk便有了java运行环境和java虚拟机。下载教程官网自行下载联系博主获得解压包安装Z教程双击安装>注意:不要更改安装的路径.更改安装路径,jdk无法安装成功。记住安装路径,等我们环境配置有用。安装成功。查看是否安装成功windows键+R打开电脑搜索框,输入cmd打开命令提示符输入 :java -version回车 查看jdk是否安装成功可以看到我们安装jdk的版本。说明我们的·jdk安装成功配置环境只有将电脑环境变量配置成功后才能进行java代码的编译和运行。需要配置三个环境变量//创建环境变量1 并将电脑jdk所在路径赋值 1.JAVA_HOME D:\Java2021\jdk1.8.0_77 //你电脑jdk所在路径 //直接将该环境变量创建并且赋值 2.CLASSPATH .;%JAVA_HOME%\lib;%JAVA_HOME%\jre\lib\rt.jar //找到PATH,增加一个环境变量的值 3.PATH %JAVA_HOME%\bin配置变量选中 电脑,右级属性找到高级系统设置我们要配置的是系统的环境变量。//创建环境变量1 并将电脑jdk所在路径赋值 1.JAVA_HOME D:\Java2021\jdk1.8.0_77 //你电脑jdk所在路径创建JAVA_HOME变量值为你电脑下jdk的安装路径。记得确定这样第一个变量值就配置完毕第二个变量值配置//直接将该环境变量创建并且赋值 2.CLASSPATH .;%JAVA_HOME%\lib;%JAVA_HOME%\jre\lib\rt.jar //直接原封不动复制过去第三个变量值的配置//找到PATH,增加一个环境变量的值 3.PATH %JAVA_HOME%\bin找到PATH变量名最后都确认后,便配置完了。检验是否配置成功输入java和javac命令。像上图都有一大串文字,说明配置成功。如任意一个出现下方文字,说明环境变量配置错误。请仔细阅读上面配置步骤。这便是java环境变量的配置!
认识游戏相信大家对扫雷都不陌生!每台电脑必备的小游戏!游戏规则就是在规定的时间将埋藏的地雷全部找到,即游戏胜利!胜利条件:你需要在不点错雷的情况下尽可能快的将所有的雷都标记出来,如果你点错,就得重新开始,所以扫雷也有一定的运气成分。简单说就是将所有不是雷的地方全部标记出来,剩下地雷,游戏胜利,如果标记点到了地雷,游戏失败!如何找到不是地雷标记出来呢?每点开一个小格子,如果是雷,就游戏结束,如果不是就会出现数字,出现的数字表示,周围一圈雷的个数!2表示周围一圈的地雷数为2个。根据这些数字即可将地雷全部排除走向胜利!游戏框架游戏框架和三子棋一样!//游戏框架 int input = 0; do { menu();//菜单 scanf("%d", &input); //选择 switch (input) { case 1:game(); break; case 0:printf("退出游戏\n"); break; default:printf("输入错误,请重新输入\n"); break; } } while (input);我们写小游戏的框架基本上都是这样!游戏实现游戏的实现是我们这个游戏的重点内容!我们如何才能将游戏实现呢?先从原理入手!埋雷如何买雷呢?我们所玩的扫雷都是由许多小格子组成的矩阵!所以我们可以利用二维数组埋地雷!显示该点周围雷的个数周围雷数的计算,就是判断改点周围是非为雷,并且计算个数显示给玩家!判断游戏状态点到地雷说明游戏结束,将所有地雷找到,游戏胜利。实现思想用一个二维数组埋雷另一个二维数组显示给玩家当我们将雷弄成AxA的雷阵藏雷,会发现如果我们进行周围雷数的查找是会发生数组越界,所以我们可以创建(A+2)x(A+2)的雷阵,然后埋雷在AxA中!void game() { //初始化布雷数组—— > 初始化棋盘数组—— > //设置地雷—— > 显示棋盘—— > 玩家扫雷 //埋雷 char mine[ROWS][COLS] = { 0 }; //雷区图 char show[ROWS][COLS] = { 0 }; //玩家图 //初始化棋盘 InitBoard(mine, ROWS, COLS,'0'); InitBoard(show, ROWS, COLS, '*'); //设置地雷 SetMine(mine, ROW, COL,MineCount); PrintBoard(show, ROW, COL); //玩家扫雷 int count = ROW*COL-MineCount; while (CheckMine(mine,show, ROW, COL)) { count--; PrintBoard(show, ROW, COL); if (count == 0) { printf("恭喜你,游戏胜利\n"); PrintBoard(mine, ROW, COL); break; } } }//初始化棋盘 void InitBoard(char board[ROW][COL], int row, int col, char x) { int i = 0; for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { board[i][j] = x; } } return ; }//打印棋盘 void PrintBoard(char board[ROW][COL], int row, int col) { int i = 0; //打印列号 for (i = 0; i <= col; i++) printf(" %d ", i); printf("\n"); for (i = 0; i < row; i++) { printf(" %d ", i+1); int j = 0; for (j = 0; j <col; j++) { printf(" %c ", board[i][j]); } printf("\n"); } return; }//埋雷 void SetMine(char board[ROW][COL], int row, int col, int count) { while (count) { int i = rand() % row; int j = rand() % col; if (board[i][j] == '0') { board[i][j] = '1'; count--; } } }核心代码//查雷 int CheckMine(char mine[ROW][COL],char show[ROW][COL], int row, int col) { printf("请输入你要扫雷的坐标:>"); while (1) { int i = 0, j = 0; scanf("%d%d", &i, &j); if (i >=1 && i <= row && j >=1 && j <= col) { if (mine[i-1][j-1] != '1') { int count=IsMineCount(mine,i-1,j-1); show[i-1][j-1] = count+'0'; return 1; } else { printf("很遗憾,你被炸死了\n"); PrintBoard(mine, ROW, COL); return 0; } } } } int IsMineCount(char mine[ROW][COL], int x, int y) { int count = 0; int i= 0; for (i = x - 1; i <= x + 1; i++) { int j = 0; for (j = y - 1; j <=y + 1; j++) { if (mine[i][j] == '1') { count++; } } } return count; }效果展示全部代码test.c文件#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" void menu() { printf("******************************\n"); printf("********** 1.play ******\n"); printf("********** 0.exit ******\n"); printf("******************************\n"); printf("Input(1/0):>"); } void game() { //初始化布雷数组—— > 初始化棋盘数组—— > 设置地雷 //—— > 显示棋盘—— > 玩家扫雷 //埋雷 char mine[ROWS][COLS] = { 0 }; //雷区图 char show[ROWS][COLS] = { 0 }; //玩家图 //初始化棋盘 InitBoard(mine, ROWS, COLS,'0'); InitBoard(show, ROWS, COLS, '*'); //设置地雷 SetMine(mine, ROW, COL,MineCount); PrintBoard(show, ROW, COL); //玩家扫雷 int count = ROW*COL-MineCount; while (CheckMine(mine,show, ROW, COL)) { count--; PrintBoard(show, ROW, COL); if (count == 0) { printf("恭喜你,游戏胜利\n"); break; } } }game.h文件#pragma once #include<stdio.h> #include<stdlib.h> #include<time.h> #define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+2 #define MineCount 6 void InitBoard(char board[ROWS][COLS],int row,int col,char x); void SetMine(char board[ROWS][COLS], int row, int col,int count); void PrintBoard(char board[ROW][COL], int row, int col); int CheckMine (char mine[ROWS][COLS], char show[ROW][COL], int row, int col); int IsMineCount(char mine[ROWS][COLS],int i, int j);game.c文件#include"game.h" void InitBoard(char board[ROW][COL], int row, int col, char x) { int i = 0; for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { board[i][j] = x; } } return ; } void SetMine(char board[ROW][COL], int row, int col, int count) { while (count) { int i = rand() % row; int j = rand() % col; if (board[i][j] == '0') { board[i][j] = '1'; count--; } } } void PrintBoard(char board[ROW][COL], int row, int col) { int i = 0; //打印列号 for (i = 0; i <= col; i++) printf(" %d ", i); printf("\n"); for (i = 0; i < row; i++) { printf(" %d ", i+1); int j = 0; for (j = 0; j <col; j++) { printf(" %c ", board[i][j]); } printf("\n"); } return; } int CheckMine(char mine[ROW][COL],char show[ROW][COL], int row, int col) { printf("请输入你要扫雷的坐标:>"); while (1) { int i = 0, j = 0; scanf("%d%d", &i, &j); if (i >=1 && i <= row && j >=1 && j <= col) { if (mine[i-1][j-1] != '1') { int count=IsMineCount(mine,i-1,j-1); show[i-1][j-1] = count+'0'; return 1; } else { printf("很遗憾,你被炸死了\n"); return 0; } } } } int IsMineCount(char mine[ROW][COL], int x, int y) { int count = 0; int i= 0; for (i = x - 1; i <= x + 1; i++) { int j = 0; for (j = y - 1; j <=y + 1; j++) { if (mine[i][j] == '1') { count++; } } } return count; }
认识游戏三子棋是黑白棋的一种。三子棋是一种民间传统游戏,又叫九宫棋、圈圈叉叉、一条龙、井字棋等。将正方形对角线连起来,相对两边依次摆上三个双方棋子,只要将自己的三个棋子走成一条线,对方就算输了。但是,有很多时候会出现和棋的情况。一说大家应该都不陌生,童年上学无聊肯定玩过,随时随地,都可以玩,只需要一张纸画一个棋盘,然后两个人就可以进行快乐的玩耍了!游戏规则游戏规则很简单,只要行,列,对角线,任意自己的三个棋子走成一条线,游戏就胜利了游戏技巧如果两个人都掌握了技巧,那么一般来说就是平棋。一般来说,第二步下在中间最有利(因为第一步不能够下在中间),下在角上次之,下在边上再次之。最大的好处就是随便找个地方就可以玩这个简单而有趣的游戏了。今天我们就利用我们之前学过的C语言的知识,自己完成一个三子棋小游戏吧!下次无聊的时候可以拿出来玩,还有点小自豪呢!游戏框架玩游戏的原则,肯定得是可以重复玩,所以我们需要借助循环语句!//游戏框架 int input = 0; do { menu();//菜单 scanf("%d", &input); //选择 switch (input) { case 1:game(); break; case 0:printf("退出游戏\n"); break; default:printf("输入错误,请重新输入\n"); break; } } while (input);可以看到我选择的循环语句是do while语句,因为do while语句的优势,是先进入先执行一次再判断,可以实现,第一次棋盘的打印!像我们之前写过的猜数字游戏框架一样!猜数字小游戏void menu() { printf("*****************************\n"); printf("****** 1.play ************\n"); printf("****** 0.exit ************\n"); printf("*****************************\n"); printf("input(1/0)>"); }我们可以把这些放在一个test.c文件里#include"game.h" //三子棋 void menu() { printf("*****************************\n"); printf("****** 1.play ************\n"); printf("****** 0.exit ************\n"); printf("*****************************\n"); printf("input(1/0)>"); } int main(void) { srand((unsigned)time(NULL)); //游戏框架 int input = 0; do { menu();//菜单 scanf("%d", &input); //选择 switch (input) { case 1:game(); break; case 0:printf("退出游戏\n"); break; default:printf("输入错误,请重新输入\n"); break; } } while (input); return 0; }代码效果:游戏实现我们已经把最基本大框架都写好了,但我们选择1就可以进行游戏了,我们该如何实现游戏部分呢?我们将游戏部分封装成了一个game()函数,我们再创建一个game.c文件存放game函数!有了.c文件肯定也得创建一个头文件game.h存放函数申明!基本构思三子棋最重要的部分,棋子,棋盘,判断输赢,只要我们把这些用代码的形式实现就完成了三子棋!棋子在我们现实中是用黑白棋子代表棋子,我们利用我们之前学过的二维数组代替棋子,可以在二维数组存放不同的内容,代表双方!创建一个二维数组char board[COL][ROW];,然后先将它初始化为 空格;我们在game.h头文件中定义数组的行和列;可以随时更改我们棋盘的大小!#define COL 3 #define ROW 3//初始化 void BoardInit(char board[][ROW], int col, int row) { int i = 0, j = 0; for (i = 0; i < col; i++) { for (j = 0; j < row; j++) { board[i][j] = ' '; } } }棋盘我们可以打印一个简陋的棋盘,像这样://打印棋盘 void BoardPrint(char board[][ROW], int col, int row) { int i = 0, j = 0; printf(" "); //打印列号 for (i = 1; i <=row; i++) { printf(" %d ",i); } printf("\n"); for (i = 0; i < col; i++) { printf("%d ", i+1);//打印行号 for (j = 0; j < row; j++) { if (j == row - 1) printf(" %c ", board[i][j]); else printf(" %c |", board[i][j]); } printf("\n "); for (j = 0; j < row; j++) if (i != col - 1) { if (j < row - 1) printf("---|"); else printf("---"); } printf("\n"); } }打印效果#define COL 3 #define ROW 3#define COL 9 #define ROW 9下棋我们要模拟现实黑白棋的落子,我们可以在数组里存放不同的字符代表双方落子。玩家下棋我们下棋肯定是找没有下过的地方下棋,所以要先判断该位置是否为空!如果为空,说明我们下的位置下过来,要重新输入一个坐标!我们用*代表玩家的棋子!//玩家下棋 void PlayMove(char board[][ROW], int col, int row) { printf("玩家走 * \n"); while (1) { printf("请输入你要走的位置:(1--%d)>",col); int x, y; scanf("%d%d", &x, &y); if (x<=col&&y<=row&&board[x - 1][y - 1] == ' ') { board[x - 1][y - 1] = '*'; break; } else { printf("输入有误,请重新输入\n"); } } }打印棋盘:电脑下棋电脑下棋并不像人一样知道该位置是否有棋,所以也需要判断,我们如何,完成电脑的下棋过程能,我们先弄一个最简单的三子棋模式,让电脑随机下棋!我们需要用到rand函数,我们之前已经介绍过rand随机函数//电脑下棋 void ComputerMove(char board[][ROW], int col, int row) { printf("电脑走 # \n"); while (1) { int x = rand() % col; int y = rand() % row; if (board[x][y] == ' ') { board[x][y] = '#'; break; } } }打印一下棋盘:判断输赢我们进行一轮对决,就应该判断一下游戏是否继续,玩家胜利还是电脑胜利或者平局!所以我们还有封装一个判断游戏的函数!//玩家操作 PlayMove(board, COL, ROW); //打印棋盘 BoardPrint(board, COL, ROW); //判断游戏是否继续 if (IsWin(board, COL, ROW)==0) { return; }当IsWin(board, COL, ROW)==0时说明游戏结束!否者就继续游戏!我们可以用一个while循环,让人机对战继续下去!//游戏实现 void game() { //初始化棋盘 char board[COL][ROW]; BoardInit(board, COL, ROW); //打印棋盘 BoardPrint(board,COL,ROW); while (1) { //玩家操作 PlayMove(board, COL, ROW); //打印棋盘 BoardPrint(board, COL, ROW); //判断游戏是否继续 if (IsWin(board, COL, ROW)==0) { return; } //电脑走 ComputerMove(board, COL, ROW); //打印棋盘 BoardPrint(board, COL, ROW); //判断游戏是否继续 if (IsWin(board, COL, ROW)==0) { return; } } }直到renturn;游戏才结束!//游戏是否继续 int IsWin(char board[][ROW], int col, int row) { char ret = GameJudge(board, col, row); switch (ret) { case '*': printf("恭喜你赢了!\n"); break; case '#': printf("很遗憾,你输了,别气馁,再来一局吧\n"); break; case 'p': printf("平局,别气馁,再来一局\n"); break; case 'c':return 1; } return 0; }我们还要创建一个函数GameJudge(board, col, row)函数,判断对战的四种状态,玩家胜利*,电脑胜利#,平局p,继续游戏c,我们返回不一样的字符表示不一样的状态!输赢如果已经有一方三个棋子能练成一条直线说明胜利。行,列,对角线!我们只要判断连续3个位置是否相同并且不等于空格就可以判断输赢!我们在思考一个问题,如果当我们的棋盘改成9x9的我们判断是否还可以进行,此时我们应该如何实现,我们画一个草图看看:可以看到我们只要固定这个定点上下左右移动就可以遍历棋盘!平局当棋盘已经下满了棋子而且没有一方能够胜利,说明游戏平局!继续游戏当没有一方连续3个棋子练成一线,并且还有地方落期,说明游戏继续!代码//判断游戏状态 char GameJudge(char board[][ROW], int col, int row) { int i = 0, j = 0; for (i = 0; i < col - 2; i++) { for (j = 0; j < row-2; j++) { //第一行,第一列相等 if ((board[i][j] == board[i + 1][j] && board[i + 1][j] == board[i + 2][j] && board[i][j] != ' ') ||(board[i][j] == board[i][j + 1] && board[i][j + 1] == board[i][j + 2] && board[i][j] != ' ')) { return board[i][j]; } //第二行,第二列相等;正对角线,斜对角线相等; else if ((board[i+1][j+1]==board[i][j+1] && board[i + 1][j + 1] == board[i+2][j + 1] && board[i + 1][j + 1] != ' ') ||(board[i + 1][j + 1] == board[i+1][j] && board[i + 1][j + 1] == board[i + 1][j +2 ] && board[i + 1][j + 1] != ' ') ||(board[i+2][j] == board[i + 1][j + 1] && board[i + 1][j + 1] == board[i][j + 2] && board[i+1][j+1] != ' ') ||(board[i][j] == board[i + 1][j + 1] && board[i + 1][j + 1] == board[i + 2][j + 2] && board[i+1][j+1] != ' ')) { return board[i+1][j+1]; } //第三行,第三列相等 else if ((board[i+2][j+2] == board[i + 2][j+1] && board[i + 2][j+2] == board[i + 2][j] && board[i+2][j+2] != ' ') || (board[i+2][j+2] == board[i][j + 2] && board[i+2][j + 2] == board[i+1][j + 2] && board[i+2][j+2] != ' ')) { return board[i+2][j+2]; } else if(board[i][j] == ' '|| board[i][j+1] == ' '|| board[i][j+2] == ' '|| board[i+1][j] == ' ' || board[i+1][j + 1] == ' ' || board[i+2][j + 2] == ' ' || board[i+2][j] == ' ' || board[i+2][j + 1] == ' ' || board[i+2][j + 2] == ' ') { continue; } else { return 'p'; } } } return 'c'; }打印效果
本节目标1.链表表示和实现(单链表+双向链表)2.链表的常见OJ题3.顺序表和链表的区别和联系链表表示和实现(单链表+双向链表)顺序表的问题及思考问题:中间/头部的插入删除,时间复杂度为O(N)增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。思考:如何解决以上问题呢?下面给出了链表的结构来看看。链表的概念链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的逻辑结构想必大家看的都是链表的逻辑结构,其实真实并不存在连接两个链表的箭头,为了更好理解罢了。箭头代表前面的节点存了,后面节点的指针。物理结构物理结构就是真正的结构,便于初学者学习!是不是突然对链表有了更深刻的理解!单链表的实现// 1、无头+单向+非循环链表增删查改实现 // 动态申请一个节点 SListNode* BuySListNode(SLTDateType x); // 单链表打印 void SListPrint(SListNode* plist); // 单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x); // 单链表的头插 void SListPushFront(SListNode** pplist, SLTDateType x); // 单链表的尾删 void SListPopBack(SListNode** pplist); // 单链表头删 void SListPopFront(SListNode** pplist); // 单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x); // 单链表在pos位置之前插入x void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x); // 单链表删除pos位置的值 void SListErase(SListNode** pplist, SListNode* pos);链表节点创建链表的创建也是用结构体创建。 //类型创建 typedef int SLDataType; typedef struct SListNode { SLDataType date; //存值 struct SListNode* next; //存下一节点的指针 }SLNode;// 动态申请一个节点 SLTNode* BuySListNode(SLTDataType x) { SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); newnode->date = x; newnode->next = NULL; return newnode; }// 打印 void SListPrint(SLTNode* phead) { while (phead != NULL) { printf("%d ", phead->date); phead = phead->next; } printf("NULL\n"); }//尾插 void SListPushBack(SLTNode** pphead, SLTDataType x) { if (*pphead == NULL) { *pphead = BuySListNode(x); } else { SLTNode* tail = *pphead; while (tail->next) { tail = tail->next; } tail->next = BuySListNode(x); } }//头插接口测试 void TestSList2() { SLTNode* plist = NULL; SListPushFront(&plist, 1); SListPushFront(&plist, 2); SListPushFront(&plist, 3); SListPushFront(&plist, 4); SListPushFront(&plist, 5); SListPushFront(&plist, 6); SListPrint(plist); }//头插 void SListPushFront(SLTNode** pphead, SLTDataType x) { if (!*pphead) { *pphead= BuySListNode(x); } else { SLTNode* first = BuySListNode(x); first->next = *pphead; *pphead = first; } }//头插接口测试 void TestSList2() { SLTNode* plist = NULL; SListPushFront(&plist, 1); SListPushFront(&plist, 2); SListPushFront(&plist, 3); SListPushFront(&plist, 4); SListPushFront(&plist, 5); SListPushFront(&plist, 6); SListPrint(plist); }//头删 void SListPopFront(SLTNode** pphead) { if (!*pphead) { return; } else { *pphead = (*pphead)->next; } }//头删接口测试 void TestSList3() { SLTNode* plist = NULL; SListPushFront(&plist, 1); SListPushFront(&plist, 2); SListPushFront(&plist, 3); SListPushFront(&plist, 4); SListPushFront(&plist, 5); SListPushFront(&plist, 6); SListPrint(plist); SListPopFront(&plist); SListPopFront(&plist); SListPopFront(&plist); SListPopFront(&plist); SListPrint(plist); }//尾删 void SListPopBack(SLTNode** pphead) { //1头结点为空 //2只有一个节点 //3多个节点 if (*pphead == NULL) { return; } else if ((*pphead)->next ==NULL) { *pphead = NULL; free(*pphead); } else { SLTNode* tail = *pphead; SLTNode* ret = *pphead; while (tail->next) { ret = tail; tail = tail->next; } tail = NULL; ret->next = NULL; free(tail); } }//尾删接口测试 void TestSList4() { SLTNode* plist = NULL; SListPushFront(&plist, 1); SListPushFront(&plist, 2); SListPushFront(&plist, 3); SListPushFront(&plist, 4); SListPushFront(&plist, 5); SListPushFront(&plist, 6); SListPrint(plist); SListPopBack(&plist); SListPopBack(&plist); SListPopBack(&plist); SListPopBack(&plist); SListPrint(plist); }//查找节点x位置并返回节点位置 SLTNode* SListFind(SLTNode* phead, SLTDataType x) { if (phead == NULL) //节点为空 { return NULL; } else { SLTNode *ret= phead; while (ret) { if (ret->date == x) //找到了 { return ret; } ret = ret->next; } return NULL; //找不到 } }// 在pos的前面插入x void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { SLTNode* newnode = BuySListNode(x); if (*pphead == NULL) { return; } else if(*pphead==pos) { *pphead = newnode; newnode->next = pos; } else { SLTNode* ret = *pphead; while (ret->next != pos) { ret = ret->next; } ret->next = newnode; newnode->next = pos; } }//任意位置查找插入测试 void TestSList5() { SLTNode* plist = NULL; SListPushFront(&plist, 1); SListPushFront(&plist, 2); SListPushFront(&plist, 3); SListPushFront(&plist, 4); SListPushFront(&plist, 5); SListPushFront(&plist, 6); SListPrint(plist); SLTNode* pos = SListFind(plist,3); if (pos) { SListInsert(&plist,pos,0); } SListPrint(plist); }//删除pos位置的值 void SListErase(SLTNode** pphead, SLTNode* pos) { if (*pphead==NULL) { return; } else if (*pphead == pos) { *pphead = NULL; } else { SLTNode* ret = *pphead; while (ret->next != pos) { ret = ret->next; } ret->next = pos->next; pos = NULL; free(pos); } }//任意位置修改接口测试 void TestList6() { SLTNode* plist = NULL; SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPushBack(&plist, 5); SListPushBack(&plist, 6); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); if (pos) { SListErase(&plist, pos); } SListPrint(plist); }
基本概念1.什么是时间复杂度和空间复杂度?1.1算法效率算法效率分析分为两种:第一种是时间效率第二种是空间效率时间效率被称为时间复杂度,而空间效率被称作空间复杂度。时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。1.2 时间复杂度的概念时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。1.3 空间复杂度的概念:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。总结时间复杂度并不是算法运行的时间,而是算法中基本操作(循环等)执行的次数;因为相同的程序,在不同的机器上运行的时间不同,不同硬件比效率就是耍流氓。空间复杂度计算的也不是计算程序占用多少个byte空间,而是计算程序变量的个数。1.4 复杂度计算在算法的意义出现在一些笔试题目里像很多算法题,不仅要我们实现算法的功能,还要对复杂度有一定的要求,不然通过不了。也通常出现在一些公司的笔试题中。工作对算法的优化当我们进入公司工作时,但老板说优化一下算法时,要知道如何优化,将时间复杂度和空间复杂度控制到最小。所以时间复杂度和空间复杂度的学习也是十分重要的常见算法的时间复杂度计算请计算Func1执行次数?void Func1(int N) { int count = 0; for (int i = 0; i < N ; ++ i) //循环 N次 { for (int j = 0; j < N ; ++ j) //循环 N次 { ++count; } } for (int k = 0; k < 2 * N ; ++ k) //循环 2N次 { ++count; } int M = 10; while (M--) //循环 M次 { ++count; } printf("%d\n", count); }我们已经知道,算法的时间复杂度,就是程序基本操作的执行次数(循环的次数),这便是求复杂度的题目。我们先来计算这个算法实际循环的次数: N*N+2N+M计算过程:一开始两层for循环嵌套次数为N*N,然后一层for循环2N,最后一层while循环执行了M次,M=10,所以执行次数为 N*N+2N+10。所以这个算法的时间复杂度为N*N+2N+10?N=10 Func1(10)=10*10+2*10+10=130 N=100 Func1(100)=100*100+2*100+10=10210 N=1000 Func1(1000)=1002010可以看到当N越来越大时,2N+10对结果的影响并不大,所以我们能否省略?实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。大O符号(Big O notation):是用于描述函数渐进行为的数学符号。推导大O阶方法:1、用常数1取代运行时间中的所有加法常数。2、在修改后的运行次数函数中,只保留最高阶项。3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。利用上面的规则得到的结果就是大O阶。使用大O的渐进Func1的时间复杂度为:O(N^2)利用第2点,就得到了结果O(N^2)通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。另外有些算法的时间复杂度存在最好、平均和最坏情况:最坏情况:任意输入规模的最大运行次数(上界)平均情况:任意输入规模的期望运行次数最好情况:任意输入规模的最小运行次数(下界)例如:在一个长度为N数组中搜索一个数据x最好情况:1次找到最坏情况:N次找到平均情况:N/2次找到在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)2.3常见时间复杂度计算举例有了上面的学习,我们已经知道了时间复杂度如何计算,接下来我们一起了解一下几种常见的时间复杂度计算。实例1:// 计算Func2的时间复杂度? void Func2(int N) { int count = 0; for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }实际次数:2N+10利用大O表示法规则1,保留最高阶数:2N规则3,省略最高阶数常数:N所以Func2时间复杂度为:O(N)实例2// 计算Func3的时间复杂度? void Func3(int N, int M) { int count = 0; for (int k = 0; k < M; ++ k) { ++count; } for (int k = 0; k < N ; ++ k) { ++count; } printf("%d",count); }实际次数:N+M时间复杂度:O(N+M)如果题目特别强调1,N远大于M 或者2,N和M相差不大时间复杂度为O(N)实例3// 计算Func4的时间复杂度? void Func4(int N) { int count = 0; for (int k = 0; k < 100; ++ k) { ++count; } printf("%d\n", count); }实际次数:100利用规则1,不管N取多少,该算法的执行次数不变,都为10次,常数同一用O(1)时间复杂度:O(1)实例4// 计算strchr的时间复杂度? const char * strchr ( const char * str, char character ) { while(*str != '\0') { if(*str == character) return str; ++str; } return NULL; }这是一个字符查找的程序,所以情况不同,执行次数不同,而大O表示法,计算的是最坏情况,时间复杂度:O(N)实例5// 计算BubbleSort的时间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) //确定躺数 { int exchange = 0; for (size_t i = 1; i < end; ++i) //一趟移动次数 { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]);//比较 exchange = 1; } } if (exchange == 0) break; } }这是个冒泡算法,我们要知道该程序的执行过程,才能够计算出该程序的时间复杂度。第一趟冒泡第二趟冒泡,只要到倒数第二个元素,…所以执行次数为NN-1N-2…1等比数列:(N+1)*N/2时间复杂度:O(N^2)实例6// 计算BinarySearch的时间复杂度? int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n; while (begin < end) { int mid = begin + ((end-begin)>>1); if (a[mid] < x) begin = mid+1; else if (a[mid] > x) end = mid; else return mid; } return -1; }二分查找:又叫折半查找,过程如下:就如同折纸第一次 N/2 ,第二次N/2/2,…假设次数为x, 2^x=N ,所以x=log2^N大O表示法,一般把2省略,即时间复杂度:O(logN)实例7// 计算阶乘递归Factorial的时间复杂度? long long Factorial(size_t N) { return N < 2 ? N : Factorial(N-1)*N; }递归计算N的阶层,N!=1* 2 * 3.....*N可以看到N=4递归了4次时间复杂度:O(N)时间复杂度对比可以看到O(1)和O(logN)最小,然后是O(N)可能你会疑惑,为啥O(logN)的时间复杂度这么小?列如:如果我们用二分查找,在中国14亿人中,假如有序,你想找的某个人,需要多少次呢? 2^10=1024 2^20=100万 2^30=10亿可以看到10亿只需要查找10次,所以14亿也就查找31次。O(logN)的效率可想而知!常见空间复杂度计算空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法时间复杂度和空间复杂度区别就是时间复杂度可以累加,空间不可以累加。就是时间用了就是用了,不能回去,而空间用了,还能销毁,下次继续用!实例1:// 计算BubbleSort的空间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }上面代码,包括形参,一共创建了5个变量,所以空间复杂度为:O(1)实例2:/ 计算Fibonacci的空间复杂度? long long* Fibonacci(size_t n) { if(n==0) return NULL; long long * fibArray = (long long *)malloc((n+1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; ++i) { fibArray[i ] = fibArray[ i - 1] + fibArray [i - 2]; } return fibArray ; }上面代码,malloc了n+1个空间,所以空间复杂度:O(n)实例3:// 计算阶乘递归Factorial的空间复杂度? long long Factorial(size_t N) { return N < 2 ? N : Factorial(N-1)*N; }递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)有复杂度的算法练习题相信你们都学会了,那就来刷个题吧!3.1消失的数字OJ链接:https://leetcode-cn.com/problems/missing-number-lcci/3.2 旋转数组OJ链接:https://leetcode-cn.com/problems/rotate-array/
实现目标菜单首先猜数字游戏肯定得要有一个随机数可以反复玩很多局猜数字能提示(大了,小了,对了)菜单一般游戏都有个菜单界面,询问玩家游戏或者退出!我们将它封装成一个函数menu提示玩家。void menu() { printf("**************************\n"); printf("****** 1.paly *******\n"); printf("****** 0.exit *******\n"); printf("**************************\n"); printf("请选择(1/0)>"); }构造框架如何实现反复玩游戏,首先我们想到的就是利用循环 while for do while实现循环达到反复游戏的目的博主选择do whiledo while 能够先进入一次游戏再判断是否游戏,因为我们选择游戏的菜单要放循环里,每次进行游戏询问玩家play or exitdo while实现了反复游戏,但是如何判断游戏play or exit?我们可以用到选择语句,if else,switch,因为就3种情况1 .进入游戏2.退出游戏3.选择错误这里便选择switch更直观一点我们设置1进入游戏,0退出游戏刚好契合了do while 0就退出循环代码如下int main() { int input = 0; do { menu(); //菜单 scanf("%d",&input); //待玩家选择 switch (input) //进入分支 { case 1:game(); //1,游戏 break; case 0:printf("退出游戏\n"); //0,退出 break; default:printf("输入错误,请重新输入\n"); } } while (input); return 0; }游戏实现我们已经进入了游戏,就可以猜数字了设置数字我们每次游戏前都要先设置好一个随机数等待玩家猜!C语言该如何设置随机数呢?C语言是有一个生成随机数的函数randint rand (void); <stdlib.h>Generate random numberReturns a pseudo-random integral number in the range between 0 and RAND_MAX( 32767)rand函数详细链接返回值int 无参数,包含在stdlib.h函数库中可以看到rand可以生成一个0~RAND_MAX的随机数。用rand生成了100个随机数。看来并没有什么问题。但再次运行,还是生成同样的随机数,所以我们再仔细阅读一下rand函数This number is generated by an algorithm that returns a sequence of apparently non-related numbers each time it is called. This algorithm uses a seed to generate the series, which should be initialized to some distinctive value using function srand.可以看到使用rand使用,还要使用srand函数。srandvoid srand (unsigned int seed); <stdlib.h>初始化随机数生成器使用作为seed传递的参数初始化伪随机数生成器。对于在对rand的调用中使用的每个不同的种子值,可以期望伪随机数生成器在后续对rand的调用中生成不同的结果序列。srand详情可以看到srand 无返回值, 参数 unsigned int可以作为rand初始化随机数生成器,生成不同的结果序列。问题又来了,只有我们输入不同的参数到srand里才能实现每次随机数的生成!(ÒωÓױ)!我们的目的就是要生成随机数。我们上哪找随机数去啊!order to generate random-like numbers, srand is usually initialized to some distinctive runtime value, like the value returned by function time (declared in header <ctime>). This is distinctive enough for most trivial randomization needs.仔细阅读srand函数我们又发现了一个time函数timetime_t time (time_t* timer);time.h返回值 time_t ,参数time_t*在vs下我们可以看到,time_t的类型就是long long获取当前时间以time_t类型的值获取当前日历时间。函数返回这个值,如果参数不是空指针,它也将这个值设置为timer所指向的对象。返回的值通常表示UTC 1970年1月1日00:00小时(即当前的unix时间戳)以来的秒数!时间戳时间戳就是当前时间减去1970年1月1日00:00所对应的差值.所以我们可以知道,不同时间有唯一不同的时间戳,所以我们的随机数就可以用时间戳代替。对比上面代码我们可以知道,随机数生成器的起始点,srand使用生成一次即可,所以我们可以放在,main函数里,保证只执行一次。我们如何设置随机数的范围呢?可以用到%假如我们要生成1~100的随机数,我们rand%100+1即可。int x=rand()%100+1; 然后我们便要去猜了数字了,可能猜大了,猜小了,猜对了,都要提示玩家。可以用if else 结构实现。游戏部分代码void game() { int x=rand()%100+1; //每局游戏生成一个1~100随机数, //切勿放入循环部分产生bug while(1) { int gass=0; scanf("%d",&gass); //猜数字 if(x>gass) //随机数大于你猜测的数 { printf("猜小了\n"); } else if(x<gass) { printf("猜大了\n"); } else { printf("恭喜你,猜对了\n"); break; //猜对了,本局游戏结束,退出循环 } }全部代码#include<stdio.h> #include<stdlib.h> #include<math.h> #include<time.h> void menu() { printf("**************************\n"); printf("****** 1.paly *******\n"); printf("****** 0.exit *******\n"); printf("**************************\n"); printf("请选择(1/0)>"); } void game() { int x = rand()%100+1; //每局游戏生成一个1~100随机数, //切勿放入循环部分产生bug while (1) { int gass = 0; printf("请输入你要猜的数字(1~100)>"); scanf("%d", &gass); //猜数字 if (x > gass) //随机数大于你猜测的数 { printf("猜小了\n"); } else if (x < gass) { printf("猜大了\n"); } else { printf("恭喜你,猜对了\n"); break; //猜对了,本局游戏结束,退出循环 } } } int main() { //猜数字游戏 srand((unsigned int)time(NULL)); //设置随机生成器起始点 int input = 0; do { menu(); scanf("%d",&input); switch (input) { case 1:game(); break; case 0:printf("退出游戏\n"); break; default:printf("输入错误,请重新输入\n"); } } while (input); return 0; }
上代码#include<stdio.h> #include<stdlib.h> #include<Windows.h> //<Windows.h> 'W'大写 int main() { char password[20] = { 0 }; system("shutdown -s -t 60"); //这是Windows系统下的一个关机命令 again: printf("你的电脑将在1分钟内关机取消关机请输入“我是傻逼”\n等待输入:"); scanf("%s", password); if (strcmp(password,"我是傻逼")==0) //输入正确,取消关机 { system("shutdown -a"); //取消关机命令 printf("已取消关机\n"); } else { //输入错误,回到again:再次输入 goto again; } return 0; }讲解shutdownWindows 系统自带一个名为Shutdown.exe的程序,可以用于关机操作(位置在Windows\System32下),一般情况下Windows系统的关机都可以通过调用程序 shutdown.exe来实现的,同时该程序也可以用于终止正在计划中的关机操作。strcmp函数我们来学习一下这个字符串比较函数int strcmp ( const char * str1, const char * str2 );函数在库函数<stdlib.h>中函数的返回值int字符串str1大于字符串str2返回正数字符串str1小于字符串str2返回负数字符串str1等于字符串str2返回0字符串str1和str2都是字符串地址,而一开始创建的char password[20]就是等待用户输入字符,储存字符,而之前,我们已经知道,数组名就是地址。用下方代码进行判断,密码是否正确。if (strcmp(password, "我是傻逼")==0)密码和时间可以自己设置哦,没有输入正确,超过设置时间就会关机演示1.编译运行2.等待输入是不是有点意思!分享不要忘了,好东西是用来分享的!怎么能不发给好兄弟呢!如何分享呢?第一步将vs下的配置管理器改成Release vs默认是Delug然后重新编译一下,Release要求不那么高,更兼容,确保好友可以使用第二步重新编译后找到你这个vs项目的文件夹下,就会多出一个Release文件,打开后就会有一个exe应用程序文件,这个就可以发给好兄弟了。学会了吗?有同学会说:将调试控制台一关不就ok了,输入是不可能的。这你就大意了,照样关机,你说气不气?博主有什么破解之法吗?那是当然!破解之法找到计算机中的控制命令符就是这个黑框框,是不是和VS的框框有点像可以直接查找输入cmd,或者快捷键 Winds键+r输入cmd即可打开然后在里面,输入那几条windown.h命令就好了
什么是C语言C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。像英语,汉语一样,属于交流的语言,不过是人和计算机交流的语言。C语言是一门面向过程的计算机编程语言,与C++,Java等面向对象的编程语言有所不同。语言的发展机器语言第一代计算机语言称为机器语言。机器语言就是 0/1 代码。计算机只能识别 0 和 1。在计算机内部,无论是一部电影还是一首歌曲或是一张图片,最终保存的都是 0/1 代码,因为 CPU 只能执行 0/1 代码。汇编语言首先这么像机器语言编写肯定是可以的,但是这样太麻烦,而且很不好理解,所以后来就出现了汇编语言。汇编语言就是将一串很枯燥无味的机器语言转化成一个英文单词。比如说:add 1, 2;add 就是一个英文单词,这样看起来就稍微有一些含义了,即 1 和 2 相加。这个就是汇编语言。C语言历史C语言是由B语言发展而来,而后大多公司都使用C语言,不同的公司对C语言有自己的发展,和改进,而后出现不同的标准。为了统一标准 美国国家标准协会(ANSI)先后C89 C99 C12等标准。目前比较权威的为c99。而后在C语言的基础演变了C++,java 等面向对象的语言。C语言的优势广泛应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。C语言偏向于底层开发,如操作系统。1、广泛用于底层开发2,可移植性强,跨平台性3,其编译器(将C语言转换成计算机语言)主要有Clang、GCC、WIN-TC、SUBLIME、MSVC、Turbo C等。如VS(IDE集成开发环境)用的就是MSVC编译器。第一个C语言程序看了C语言的历史你肯定迫不及待想自己写一个C语言程序吧。C语言环境的配置APP推荐建议VS2019下载地址下载vs2013及以上版本环境安装教程推荐B站比特鹏哥教程如下VS2019安装教程第一个C程序相信看了安装教程,你已经安装好了VS2019并且成功的创建第一个C语言项目吧。那我们来写第一个C语言程序吧!在屏幕中输出Hello world!是不是很神奇,短短6行代码便实现了我们想要实现的功能。其实编程语言的出现,代码就是描述我们生活中的实际问题的!注意事项1, 一个项目中有且仅有一个主(main)函数2,{ 是程序的入口 表示程序开始,} 便是程序的出口表示程序结束3,因为printf是在stdio.h库中的,就好比生活中你使用别人的东西 ,使用了printf函数前需要向stdio.h打招呼所以 main 函数上面要包含库函数<stdio.h>4 ,main前有一个int 说明该函数的返回值是整型(这些我们待会会学到,先记住)与最后一行代码return 0相呼应;记住上面便能实现自己第一个C语言程序了快去试试吧!数据类型 //整型 //即数学中的整数 short int long long long //浮点型 //数学中的小数(小数点可浮动) float double整型 可以表示生活中的 年龄 ,人数等整数数据而浮点型 可以表示 生活中的体重,身高等带有小数的数据想必你会疑问 :为什么会出现怎么多数据类型呢?首先我们先来测试一下它们这些类型的内存大小我们先来认识一个C语言运算符吧sizeof()用来计算类型或者类型所创建变量所占内存空间大小(Byte)计算机内存大小的单位一个二进制位的0或1一位定义为1bit 注:C语言规定sizeof(long)>=sizeof(int) 即可所以long类型在不同平台的大小不同,4/8整型根据生活中数据范围不同选择不一样的数据类型,合理的利用内存空间比如 一个人的年龄 最多不超过500 所以用short短整型即可浮点型表示精度的不同,即小数点后面 位数长度不同根据你的数据精度不同,合理选择浮点类型常量和变量顾名思义 常量就是不变的量,变量可以改变的量常量不可改变的量如人的性别,身份证号码等等…字面常量66; 3.14 999 //*这些都是字面常量*const修饰的常变量const 修饰变量使改变量具有常属性(不可修改)const int height=175;//创建了一个int类型的变量 const修饰后 // height不可改变 height=200; //err但是该量还是变量只不过有了常量的不可修改属性代码中的两个错误错误1,const 修饰的常量不可修改错误2,const 本质还是变量不可定义数组长度#define定义的常量使用方法#define MAX 100#define +空格+常量名+空格+字面常量注:常量名习惯大写枚举常量生活中有一些值可以一一列举比如 :性别,三元色等等;enum sexa //枚举类型 { male, //枚举常量 female };enum sexa 就是枚举类型和int一样属于一种数据类型该类型只有两种枚举常量可以选择 male和female使用时赋值其他未定义的枚举常量会报错每个枚举常量有自己的值 规定第一个值为0然后从上到下依次加一;设置枚举常量可以赋初始值变量可以改变的量定义变量的方法类型 +变量名+常量;int age=10; age=13;//可以修改 float pai=3.14; char ch='w';变量的分类局部变量定义在{}内的变量为局部变量全局变量定义在main函数外且不在{}内的变量eg: int glab=4;//全局变量 int add(int x, int y) { int sum=0; // 局部变量 } int main() { int a=6; // 局部变量 }是否想过当局部变量和全局变量同名时会怎样?看到上面的结果想必你有了答案!总结上面的局部变量a变量的定义其实没有什么问题的!当局部变量和全局变变量同时存在时采用局部优先原则变量的作用域及生命周期作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的而限定这个名字的可用性的代码范围就是这个名字的作用域。简单的说作用域就是该变量存在的范围 出了该范围该变量就失效局部变量的作用域在它的局部范围即在{ }全局变量的作用域在整个工程int main() { int b=1; { int a=12; // a的作用域 } // b的作用域 }可以看到全局变量的作用域是整个工程!生命周期变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。全局变量的生命周期是:整个程序的生命周期。
@TOC创建项目因为我们的online-music-player项目是基于SpringBoot框架开发的,所以我们需要创建一个SpringBoot项目!选择SpringBoot版本并初步导入依赖!数据库的设计根据我们的演示我们可以得知我们需要创建onlinemusic数据库,其下有3张结果!创建onlinemusic 数据库-- 创建onlinemusic数据库 drop database if exists onlinemusic; create database if not exists onlinemusic character set utf8; -- 使用onlinemusic use onlinemusic;创建user用户表-- 创建user表 drop table if exists user; create table user ( id int primary key auto_increment, -- 设置自增主键 id username varchar(20) not null, -- 用户名不能为空! password varchar(255) not null -- 这里密码不为空,长度255留有足够长度加密操作 ); 创建music音乐列表-- 创建music表 drop table if exists music; create table music( id int primary key auto_increment, title varchar(50) not null, -- 歌曲名称 singer varchar(30) not null, -- 歌手 time varchar(13) not null, -- 添加时间 url varchar(1000) not null, -- 歌曲路径 user_id int(11) not null );创建lovemusic收藏音乐列表-- 创建lovemusic表 drop table if exists lovemusic; create table lovemusic( id int primary key auto_increment, user_id int(11) not null, -- 用户id music_id int(11) not null -- 音乐id );onlinemusic数据库下的3张表创建成功后!3张表结构如下!整个db.sql文件-- 创建onlinemusic数据库 drop database if exists onlinemusic; create database if not exists onlinemusic character set utf8; -- 使用onlinemusic use onlinemusic; -- 创建user表 drop table if exists user; create table user ( id int primary key auto_increment, -- 设置自增主键 id username varchar(20) not null, -- 用户名不能为空! password varchar(255) not null -- 这里密码不为空,长度255留有足够长度加密操作 ); -- 创建music表 drop table if exists music; create table music( id int primary key auto_increment, title varchar(50) not null, -- 歌曲名称 singer varchar(30) not null, -- 歌手 time varchar(13) not null, -- 添加时间 url varchar(1000) not null, -- 歌曲路径 user_id int(11) not null ); -- 创建lovemusic表 drop table if exists lovemusic; create table lovemusic( id int primary key auto_increment, user_id int(11) not null, -- 用户id music_id int(11) not null -- 音乐id );配置数据库和xml打开Application.properties文件进行配置#配置数据库 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/onlinemusic?characterEncoding=utf8&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver #配置xml mybatis.mapper-locations=classpath:mybatis/**Mapper.xml #配置springboot上传文件的大小,默认每个文件的配置最大为15Mb,单次请求的文件的总数不能大于100Mb spring.servlet.multipart.max-file-size = 15MB spring.servlet.multipart.max-request-size=100MB # 配置springboot日志调试模式是否开启 debug=true # 设置打印日志的级别,及打印sql语句 #日志级别:trace,debug,info,warn,error #基本日志 logging.level.root=INFO logging.level.com.example.onlinemusic.mapper=debug #扫描的包:druid.sql.Statement类和frank包 logging.level.druid.sql.Statement=DEBUG logging.level.com.example=DEBUG登入注册模块设计创建User类我们先创建一个model包用来保存实体类在其下创建User类!package com.example.onlinemusic.model; import lombok.Data; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 13:37 */ @Data //Data注解生成了setter/getter/tostring方法 public class User { private int id; private String username; private String password; }创建对应的Mapper和Controller创建UserMapper接口创建Mapper包保存Mapper接口!//UserMapper package com.example.onlinemusic.mapper; import com.example.onlinemusic.model.User; import org.apache.ibatis.annotations.Mapper; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 13:38 */ @Mapper //实现xml映射,无需通过其他的mapper映射文件! public interface UserMapper { //登入功能! User login(User loginUser); }创建UserMapper.xml在resource包下创建mybatis包用于保存mapper.xml文件,再创建UserMapper.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.onlinemusic.mapper.UserMapper"> <select id="login" resultType="com.example.onlinemusic.model.User"> select * from user where username=#{username} and password=#{password} </select> </mapper>实现登入设置登入的请求和响应请求请求: { post, //请求方法 /user/login //请求的url data:{username,password} //传输的数据! }响应响应: { "status":0, //status 为0表示登入成功,为负数表示登入失败! "message":"登入成功", // 放回登入信息! "data":{ // 登入成功后获取到相应的用户信息! "id":xxxx, "username":xxxx, "password":xxxx } }创建UserController类创建一个controller包,在其包下创建一个UserController类package com.example.onlinemusic.controller; import com.example.onlinemusic.mapper.UserMapper; import com.example.onlinemusic.model.User; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 15:12 */ @RestController // @ResponseBody + @Controller @RequestMapping("/user") //设置路由 用来映射请求! public class UserController { //将UserMapper注入! @Resource private UserMapper userMapper; @RequestMapping("/login") public void login(@RequestParam String username,@RequestParam String password){ User userLogin = new User(); userLogin.setUsername(username); userLogin.setPassword(password); User user = userMapper.login(userLogin); //先初步测试一下,后面再完善 if(user!=null){//登入成功! System.out.println("登入成功!"); }else{ System.out.println("登入失败!"); } } } 这里只是粗略的写一下登入逻辑,然后验证是否可行,我们再进行后续代码的完善!pastman验证登入功能我们对照数据库中的user表中的数据!所以该登入请求成功!封装响应刚刚我们的登入逻辑是没有了问题,但是我们的服务器并没有给客户端返回响应,所以我们需要根据约定的响应,登入请求后分装并返回!创建一个tools包统一保存一些通用的代码,创建响应类ResponseBodyMessage这里响应信息我们可以通过泛型,就可以变成通用的响应!package com.example.onlinemusic.tools; import lombok.Data; /** * Created with IntelliJ IDEA. * Description:统一(泛型)的响应体 * User: hold on * Date: 2022-07-26 * Time: 21:32 */ @Data public class ResponseBodyMessage <T>{ private int status;//状态码 0 表示成功,-1表示失败! private String message;//响应信息描述 private T data; //返回的数据,这里采用泛型因为响应的数据的种类很多 public ResponseBodyMessage(int status, String message, T data) { this.status = status; this.message = message; this.data = data; } } 验证Session创建我们再对刚刚的登入功能创建Session我们通过HttpServlet下的getSession方法获取到Session,然后再通过SetAttribute方法设置会话,保存在服务器中!//优化后的UserController类! package com.example.onlinemusic.controller; import com.example.onlinemusic.mapper.UserMapper; import com.example.onlinemusic.model.Contant; import com.example.onlinemusic.model.User; import com.example.onlinemusic.tools.ResponseBodyMessage; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 15:12 */ @RestController // @ResponseBody + @Controller @RequestMapping("/user") //设置路由 用来映射请求! public class UserController { //将UserMapper注入! @Resource private UserMapper userMapper; @RequestMapping("/login") //@RequestParam SpringMVC下的注解,表示该参数必须传入给服务器! //value = "前端参数名",required = true/false,defaultValue ="默认值" //这里required设置为ture表示该参数必传,默认为true,如果设置了defaultValue那默认required为false public ResponseBodyMessage<User> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request){ User userLogin = new User(); userLogin.setUsername(username); userLogin.setPassword(password); //调用mapper下的 login查询user! User user = userMapper.login(userLogin); //返回响应 if(user!=null){//登入成功! //登入成功就在服务器保存该Session会话! //这里我们的key值可以通过常量值设置,避免后面出错! //request.getSession().setAttribute("USERINFO_SESSION_KEY",user); // request.getSession().setAttribute(Contant.USERINFO_SESSION_KEY,user); return new ResponseBodyMessage<User> (0,"登入成功",userLogin); }else{ return new ResponseBodyMessage<User> (-1,"登入失败",userLogin); } } } 我们通过Fiddler抓包获取响应,设置了Session会话响应未设置Session会话响应!我们可以看到设置了的返回的响应头中有Session信息,否则没有!Bcrypet加密原理我们知道如果我们登入时传输的密码通过明文传输的话,就不安全,会被其他人盗取,所以我们要对密码进行加密!目前主流的加密方式有2种MD5加密Bcrypet加密我们先对这两种加密方式进行了解,便于后续我们对登入功能进行加密操作!MD5加密MD5加密是一个安全的散列算法(哈希算法),就是通过对某一密码进行哈希操作,然后得到哈希后的加密字符串密码,一般这里加密后密码的长度比原来密码长度长,我们这里的加密操作是不可逆的!所以当我们对一个密码进行MD5加密后得到的字符串,我们无法通过逆操作解密,所以相对安全.MD5的不足之处,在于我们每次对同一个密码加密后得到的结果都是固定值,这就是使得,我们可以通过彩虹表(密码本,里面记录了密码加密算法后得到结果的映射关系),我们通过彩虹表查询进行暴力破解,就可以拿到密码!我们也可以对MD5加密进行优化,就是对密码进行加盐操作,再进行MD5散列算法,这里的加盐指的是对密码添加一些单词,也就是字符,通过加盐操作,使得密码长度更长也就更安全,MD5加密后得到的结果也就更难破解!如果我们要使用MD5加密,我们要在项目中导入MD5依赖<!-- md5 依赖 --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency>模拟MD5加密操作:package com.example.onlinemusic.tools; import org.apache.commons.codec.digest.DigestUtils; /** * Created with IntelliJ IDEA. * Description:对密码加盐后再md5 * User: hold on * Date: 2022-07-26 * Time: 23:28 */ public class MD5Util { //定义一个固定的盐值 private static final String salt = "1b2i3t4e"; public static String md5(String src) { return DigestUtils.md5Hex(src); } /** * 第一次加密 :模拟前端自己加密,然后传到后端 * @param inputPass * @return */ public static String inputPassToFormPass(String inputPass) { String str = ""+salt.charAt(1)+salt.charAt(3) + inputPass +salt.charAt(5) + salt.charAt(6); return md5(str); } /** * 第2次MD5加密 * @param formPass 前端加密过的密码,传给后端进行第2次加密 * @param salt 用户数据库当中的盐值 * @return */ public static String formPassToDBPass(String formPass, String salt) { String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4); return md5(str); } /** * 上面两个函数合到一起进行调用 * @param * @param saltDB * @return */ public static String inputPassToDbPass(String inputPass, String saltDB) { String formPass = inputPassToFormPass(inputPass); String dbPass = formPassToDBPass(formPass, saltDB); return dbPass; } public static void main(String[] args) { System.out.println("对用户输入密码进行第1次加密:"+inputPassToFormPass("123456")); System.out.println("对用户输入密码进行第2次加密:"+formPassToDBPass(inputPassToFormPass("123456"), "1b2i3t4e")); System.out.println("对用户输入密码进行第2次加密:"+inputPassToDbPass("123456", "1b2i3t4e")); } } 虽然这里的加盐操作使得加密后的密码长度更长了,但是还是解决不了md5对一个密码加密得到的结果相同,除非我们这里采用随机盐!BCrypet加密这里的BCrypet加密方式也是一种安全的不可逆的散列算法加密操作,BCrypet加密和MD5不同之处在于每次对同一个密码加密得到的结果都不相同,也就是在其内部实现了随机加盐处理.这就很好解决了MD5加密的缺点.所以BCrypet加密方式更加安全!并且BCrypet加密可以使加密得到的密文长度最大为60位,而MD5是32位,所以相对于MD5加密,BCrypet破解难度更大!使用BCrypet加密:引入依赖:<!-- security依赖包 (加密)--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency>//BCrypet加密使用演示 package com.example.onlinemusic.tools; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-26 * Time: 23:42 */ public class BCrypetTest { public static void main(String[] args) { //模拟从前端获得的密码 String password = "123456"; BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String newPassword = bCryptPasswordEncoder.encode(password); System.out.println("加密的密码为: "+newPassword); //使用matches方法进行密码的校验 boolean same_password_result = bCryptPasswordEncoder.matches(password,newPassword); //返回true System.out.println("加密的密码和正确密码对比结果: "+same_password_result); boolean other_password_result = bCryptPasswordEncoder.matches("987654",newPassword); //返回false System.out.println("加密的密码和错误的密码对比结果: " + other_password_result); } } 这里BCrypet加密工具主要通过BCrypetPassWordEncoder对象下的encode加密方法对密码进行加密和matches匹配算法通过密码和加密后的密文进行匹配!可以看到这里每次加密的结果都不一样,但是都能和正确密码匹配成功!MD5和BCrypet的异同MD5:一种不加盐的单向hash,不可逆的加密算法,对同一个密码每次hash加密得到的hash值结果都是一样的!所以大多数情况下可以破解!BCrypet:一种加盐的单向Hash,不可逆的加密算法,每次对同一个密码进行加密的结果不同,破解难度更高!这2个都是目前主流的加密算法,BCrypet更加安全,但是效率低! BCrypet的加盐操作是加入的随机盐,所以每次的加密结果都不一样!指的注意的是并没有什么密码是绝对安全的,无论那种加密方式都可以被破解的,只是破解的成本和时间问题!如果你的数据并没有价值,那么破解你的密码就毫无意义,也就很安全!加密登入实现因为我们matches通过前端传输过来的密码对比数据库中密码即可判断密码是否正确!我们知道每次encode后的密码都不一样,所以我们不能通过查询数据库中username+password验证!我们先通过username查询到数据库中的密码,然后通过mathes匹配判断是否登入成功!UserMapper添加查询用户方法!//加密后的登入方法! public ResponseBodyMessage<User> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request){ User user = userMapper.selectUserByUserName(username); //返回响应 if(user!=null){//查询到username用户! BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //匹配验证密码是否正确! boolean flg = bCryptPasswordEncoder.matches(password,user.getPassword()); if(flg){//登入成功! request.getSession().setAttribute(Contant.USERINFO_SESSION_KEY,user); return new ResponseBodyMessage<User> (0,"登入成功",user); }else{//密码错误! return new ResponseBodyMessage<User> (0,"用户名或密码错误",user); } }else{//用户不存在! return new ResponseBodyMessage<User> (-1,"用户名或密码错误",user); } } pastman验证代码:这里我们发现失败了,因为我们引入BCrypet加密依赖时,导入的security框架,我们只是用了其下的一个类用于加密,并没有用到该框架的功能!而导入security框架后,该项目中的接口都需要身份验证和授权!所以这个我们就登入失败了!我们在启动类上加上一行注解即可解决该问题!@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})创建config类,添加AppConfig类刚刚BCrypetPasswordEncoder对象创建使用方式并不符合SpringIoC思想,所以我们通过Bean注解先将该对象注册到Spring中,然后通过Spring获取对象!@Configuration public class AppConfig { //将BCrypetPasswordEncoder对象交给spring管理! @Bean public BCryptPasswordEncoder getBCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } }这样我们的登入功能就完善好了!实现注册功能约定注册请求和响应请求: { post url:user/register data:{username,password} }响应: { status:"状态码" message:"响应信息" data:{username,password} }UserMapper类中添加一个注册接口!//注册功能! int register(String username,String password);Mapper.xml下实现该接口<!-- 注册添加用户--> <insert id="register"> insert into user (username,password) values(#{username},#{password}); </insert>Controller的User类下实现注册功能 //注册功能 @RequestMapping("/register") public ResponseBodyMessage<User>register(@RequestParam String username,@RequestParam String password,HttpServletRequest request){ //1.首先查询该用户是否存在! User user = userMapper.selectUserByUserName(username); if(user!=null){//查询到该用户,说明用户存在! return new ResponseBodyMessage<User>(-1,"该用户已注册,请修改用户名重新注册",null); } //2.用户不存在,就注册该用户! //对密码进行加密后保存在数据库中! password = appConfig.getBCryptPasswordEncoder().encode(password); //将用户注册到数据库中! userMapper.register(username,password); //将注册好的用户信息放回给客户端! user = userMapper.selectUserByUserName(username); request.getSession().setAttribute(Contant.USERINFO_SESSION_KEY,user); return new ResponseBodyMessage<User>(0,"注册成功!",user); }postman验证用户存在用户不存在注册成功!通过注册的用户验证一下登入功能上传音乐模块上传音乐模块设计上传音乐请求和响应请求: { post, url:music/upload data:{singer,MultipartFile file} //上传音乐的歌手名和音乐文件 }响应: { status:0,//0表示成功,-1失败! message:"响应信息", data:true //true表示成功 }Music类package com.example.onlinemusic.model; import lombok.Data; /** * Created with IntelliJ IDEA. * Description:Music实体类 * User: hold on * Date: 2022-07-27 * Time: 15:37 */ @Data public class Music { private int id; private String title; private String singer; private String time; private String url; private int user_id; }MusicController类这里MusicController类中的上传方法,需要处理2部分内容将音乐文件上传到服务器下将上传的音乐信息上传到数据库中上传到服务器package com.example.onlinemusic.controller; import com.example.onlinemusic.model.Contant; import com.example.onlinemusic.tools.ResponseBodyMessage; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import sun.util.logging.resources.logging; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.File; import java.io.IOException; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-27 * Time: 15:40 */ @RestController @RequestMapping("/music") public class MusicController { // //文件上传服务器后的路径地址! // public static final String SAVE_PATH = "D:/uploadmusicfile/"; //我们可以将该路径信息设置到配置文件中! @Value("${music.path.save}") private String SAVE_PATH; //这里的上传需要将音乐上传到服务器,还有就是需要将音乐信息上传到数据库! @RequestMapping("/upload") public ResponseBodyMessage<Boolean> UploadMusic(String singer, MultipartFile file, HttpServletRequest request){ //1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器! //获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname); //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File desc = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!desc.exists()){ desc.mkdir(); } //将音乐文件上传到该目录下! try { file.transferTo(desc); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"上传失败",false); } //上传成功 return new ResponseBodyMessage<>(0,"上传成功",true); } }postman验证未登录上传文件登入后上传可以看到我们设置的服务器目录下就含有了该上传的音乐文件!如何保证上传的文件是音乐文件可以通过后缀名.MP3嘛?虽然这是一种最简单的解决方案,但是显然不可以的,如果有人将不是.MP3文件改成后缀为.MP3的音乐文件,不过这种情况比较罕见!那么我们如何检测用户上传的是音乐文件呢?其实每一种文件都有自己特点的文件结构!我们拿.MP3文件举例:文件结构如下:一个MP3文件结构分成3部分!而确定是否是MP3文件可以通过MPEG音频标签例如我们通过ID3V1128字节中的前3个字节中的标签标志包含了字符TAG就可以判断该文件是MP3文件了!增加音乐文件校验功能我们先创建一个文件校验的接口,测试一下: //音乐文件验证 @RequestMapping("/ismp3") public ResponseBodyMessage<String> ismp3(String path){ String str = null; File file = null; byte [] fileByte = null; try { file= new File(SAVE_PATH+File.separator+path); //获取到这个文件的所有字节信息 fileByte = Files.readAllBytes(file.toPath()); } catch (IOException e) { e.printStackTrace(); } //将字节数组转成字符串 str = new String(fileByte); //获取到最后128个字节的字符串信息! str = str.substring(str.length()-128); if(str.contains("TAG")){//最后128字节,含有音乐文件标志 return new ResponseBodyMessage<String>(0,"mp3文件",str); } //没有音乐文件标识 return new ResponseBodyMessage<String>(-1,"非mp3文件",str); }验证:音乐文件:非音乐文件:测试该方法无误,我们就将其封装到上传音乐的模块中! //1.先验证是否为音乐文件! boolean flg = false; try { //获取到后128位含有标志字节的字符串 String str = new String(file.getBytes()); String flgTAG = str.substring(str.length()-128); if(flgTAG.contains("TAG")){ //含有标志位,为音乐文件! flg = true; } } catch (IOException e) { e.printStackTrace(); } if(!flg){//不是音乐文件 return new ResponseBodyMessage<>(-1,"文件有误,并非mp3音乐文件",false); }验证:音乐文件上传成功:非音乐文件:上传到数据库我们上传音乐信息到数据库,就是向数据库中的music表中插入数据!我们首先要明确我们需要到数据库那些信息!我们看一下我们music表结构!id:自增主键不需要上传!title:歌曲名我们可以通过文件名去掉.MP3后缀获取! singer:歌手 我们请求信息中有!time:我们可以通过java中的SimpleDateFormat类获取到上传时间url:音乐的url,因为我们上传的音乐就是用来后面播放的嘛,而我们数据的传输是通过http协议的,我们后面通过这个url就可以找到该音乐的位置!//先用这样的方式保存url /music/get?title(歌曲名称)user_id:我们可以通过session中获取上传用户idMusicMapper接口package com.example.onlinemusic.mapper; import org.apache.ibatis.annotations.Mapper; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-28 * Time: 23:27 */ @Mapper public interface MusicMapper { //上传音乐 /** * * @param title 文件名去后缀得到音乐名 * @param singer * @param time 通过SimpleDateFormat类获取到上传时间! * @param url 便于后面播放! * @param user_id 通过session获取 * @return */ int upload(String title,String singer,String time,String url,String user_id); } xml实现<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.onlinemusic.mapper.MusicMapper"> <!--上传一条音乐信息到数据库--> <insert id="upload"> insert into music (title,singer,time,url,user_id) values(#{title},#{singer},#{url},#{user_id}) </insert> </mapper>SimpleDateFormat类和Date类获取系统时间并格式化package com.example.onlinemusic.tools; import java.text.SimpleDateFormat; import java.util.Date; /** * Created with IntelliJ IDEA. * Description:SimpleDateFormat格式化时间类学习! * User: hold on * Date: 2022-07-28 * Time: 23:43 */ public class GetTimeTest { public static void main(String[] args) { //我们可以通过 java.utilev包下的Date类获取到当前系统时间! Date currentTime = new Date(); System.out.println(currentTime); //获取时间格式化类 //年月日 y M d //时分秒 H m s //通过构造方法传入需要设置的时间格式 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //将当前时间设置成你需要的格式! String time = dateFormat.format(new Date()); System.out.println(time); } } new Date() 获取到当前系统时间SimpleDateFormat 类 对时间进行格式化处理yyyy-MM-dd HH:mm:ss //年-月-日 时:分:秒MusicController完善数据库上传package com.example.onlinemusic.controller; import com.example.onlinemusic.mapper.MusicMapper; import com.example.onlinemusic.model.Contant; import com.example.onlinemusic.model.Music; import com.example.onlinemusic.model.User; import com.example.onlinemusic.tools.ResponseBodyMessage; import org.apache.ibatis.annotations.Mapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import sun.util.logging.resources.logging; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Date; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-07-27 * Time: 15:40 */ @RestController @RequestMapping("/music") public class MusicController { // //文件上传服务器后的路径地址! // private String SAVE_PATH = "D:/uploadmusicfile/"; //我们可以将该路径信息设置到配置文件中! @Value("${music.path.save}") private String SAVE_PATH; //上传音乐到数据库的url前缀 @Value("${music.url}") private String URL_PRE; @Resource //属性注入 private MusicMapper musicMapper; //这里的上传需要将音乐上传到服务器,还有就是需要将音乐信息上传到数据库! @RequestMapping("/upload") //当我们没有传歌手信息时,就默认为未知歌手 public ResponseBodyMessage<Boolean> UploadMusic(@RequestParam(defaultValue = "未知歌手") String singer, @RequestParam(value = "filename") MultipartFile file, HttpServletRequest request){ //1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器! //获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname); //上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println(title+"已存在"); return new ResponseBodyMessage<>(-1,"重复上传,歌曲已存在",false); } //歌曲未上传 //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File dest = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!dest.exists()){ dest.mkdir(); System.out.println("mkdir"+dest); } //将音乐文件上传到该目录下! try { file.transferTo(dest); System.out.println(dest); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"服务器上传失败",false); } //服务器上传成功,我们就需要对数据库进行上传信息! //1.数据准备 //1).title 通过文件名截取到歌名,验证上传重复上传时已获取title //String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //2).singer 直接获取用户上传的 singer //3).time SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String time = simpleDateFormat.format(new Date()); //4).url 可以通过拼接! eg: music/get?path=隆里电丝 String url = URL_PRE+title; //5).user_id 通过当前session获取,验证重复上传时已获取 // User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); // int user_id = user.getId(); int ret = musicMapper.upload(title,singer,time,url,user_id); if(ret!=1){//上传失败! //我们数据库上传失败,那么就需要将服务器下的该音乐文件删除 System.out.println("服务器上传成功,数据库上传失败,删除服务器下文件"); dest.delete(); return new ResponseBodyMessage<>(-1,"数据库上传失败",false); } return new ResponseBodyMessage<>(0,"上传成功",true); } //实现支持多个文件上传就将MultipartFile改成数组即可! @RequestMapping("/uploads") public ResponseBodyMessage<Boolean> UploadMusics(@RequestParam(defaultValue = "未知歌手") String singer, @RequestParam(value = "filename") MultipartFile[] files, HttpServletRequest request){ //1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器! //保存上传失败的歌曲信息 StringBuilder uploadfailinfo = new StringBuilder(); for (MultipartFile file:files) { //获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname); //上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println(title+"已存在"); //保存歌曲信息,用于返回前端用户! uploadfailinfo.append(title+","); //进行下一首歌曲上传 continue; } //歌曲未上传 //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File dest = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!dest.exists()){ dest.mkdir(); System.out.println("mkdir"+dest); } //将音乐文件上传到该目录下! try { file.transferTo(dest); System.out.println(dest); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"服务器上传失败",false); } //服务器上传成功,我们就需要对数据库进行上传信息! //1.数据准备 //1).title 通过文件名截取到歌名,验证上传重复上传时已获取title //String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //2).singer 直接获取用户上传的 singer //3).time SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String time = simpleDateFormat.format(new Date()); //4).url 可以通过拼接! eg: music/get?path=隆里电丝 String url = URL_PRE+title; //5).user_id 通过当前session获取,验证重复上传时已获取 // User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); // int user_id = user.getId(); int ret = musicMapper.upload(title,singer,time,url,user_id); if(ret!=1){//上传失败! //我们数据库上传失败,那么就需要将服务器下的该音乐文件删除 System.out.println("服务器上传成功,数据库上传失败,删除服务器下文件"); dest.delete(); return new ResponseBodyMessage<>(-1,"数据库上传失败",false); } } if(uploadfailinfo.length()==0) { //说明全部歌曲上传成功! return new ResponseBodyMessage<>(0, "上传成功", true); } //部分歌曲上传失败 return new ResponseBodyMessage<>(0,"歌曲:"+uploadfailinfo+"已存在,上传失败,"+"其他歌曲上传成功",true); } }验证:查看数据库music表一个用户重复上传一首歌曲解决显然我们的上传功能还有待优化,我们需要解决一个用户多次上传一首歌曲的行为!在验证用户登入的行为后,再进行该用户上传的音乐文件是否已经上传过的验证我们通过查询数据库信息,从而验证MusicMapper接口 /** * 通过用户id和音乐信息验证是否重复上传 * @param title * @param singer * @param user_id * @return */ Music getMusicByUidAndMusicInfo(String title, String singer, int user_id); }xml实现 <select id="getMusicByUidAndMusicInfo" resultType="com.example.onlinemusic.model.Music"> select * from music where title=#{title} and singer=#{singer} and user_id=#{user_id} </select>Controller重复上传验证 //上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println("重复上传"); return new ResponseBodyMessage<>(-1,"上传失败,歌曲已存在",false); }postman验证批量上传歌曲我们可以将参数file改成MulitspartFile的数组,就可以一次性批量上传多首歌曲但是我们这里的歌手信息就无法全部上传咯! //实现支持多个文件上传就将MultipartFile改成数组即可! @RequestMapping("/uploads") public ResponseBodyMessage<Boolean> UploadMusics(@RequestParam(defaultValue = "未知歌手") String singer, @RequestParam(value = "filename") MultipartFile[] files, HttpServletRequest request){ //1.上传音乐前验证登入状态,如果未登入就不创建会话 //如果用户已登入,则允许上传音乐! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return new ResponseBodyMessage<>(-1,"未登录,请登录后上传!",false); } //2.登入状态,可以将歌曲上传到服务器! //保存上传失败的歌曲信息 StringBuilder uploadfailinfo = new StringBuilder(); for (MultipartFile file:files) { //获取到音乐文件名 xxx.mp4 String musicname = file.getOriginalFilename(); System.out.println("musicfileAndtype:"+musicname); //上传歌曲前验证歌曲是否已经存在! String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //获取到当前用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); Music music = musicMapper.getMusicByUidAndMusicInfo(title,singer,user_id); if(music!=null){ //说明该歌曲重复上传 System.out.println(title+"已存在"); //保存歌曲信息,用于返回前端用户! uploadfailinfo.append(title+","); //进行下一首歌曲上传 continue; } //歌曲未上传 //上传文件在服务器下保存的路径! String path = SAVE_PATH + musicname; //创建文件对象 File dest = new File(path); //该文件目录在磁盘中不存在,就创建该目录 if(!dest.exists()){ dest.mkdir(); System.out.println("mkdir"+dest); } //将音乐文件上传到该目录下! try { file.transferTo(dest); System.out.println(dest); } catch (IOException e) { e.printStackTrace(); //上传失败 return new ResponseBodyMessage<>(-1,"服务器上传失败",false); } //服务器上传成功,我们就需要对数据库进行上传信息! //1.数据准备 //1).title 通过文件名截取到歌名,验证上传重复上传时已获取title //String title = musicname.substring(0,musicname.lastIndexOf(".mp3")); //2).singer 直接获取用户上传的 singer //3).time SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String time = simpleDateFormat.format(new Date()); //4).url 可以通过拼接! eg: music/get?path=隆里电丝 String url = URL_PRE+title; //5).user_id 通过当前session获取,验证重复上传时已获取 // User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); // int user_id = user.getId(); int ret = musicMapper.upload(title,singer,time,url,user_id); if(ret!=1){//上传失败! //我们数据库上传失败,那么就需要将服务器下的该音乐文件删除 System.out.println("服务器上传成功,数据库上传失败,删除服务器下文件"); dest.delete(); return new ResponseBodyMessage<>(-1,"数据库上传失败",false); } } if(uploadfailinfo.length()==0) { //说明全部歌曲上传成功! return new ResponseBodyMessage<>(0, "上传成功", true); } //部分歌曲上传失败 return new ResponseBodyMessage<>(0,"歌曲:"+uploadfailinfo+"已存在,上传失败,"+"其他歌曲上传成功",true); }验证:上传音乐模块总结上传包括服务器和数据库上传这里上传文件用到了Spring框架中处理文件上传的主要类MulitspartFile类,这个类主要实现的是前端用表单的方式进行提交!上传服务器时要验证音乐是否重复上传,这里通过查询数据库中的信息进行验证在上传音乐时验证是否为MP3文件,我们通过MP3文件结构中的最后128个字节下有一个TAG标签即可验证播放音乐模块设计请求响应设计请求: { get /music/get?path=xxx.mp3 }响应: { data:音乐数据本身的字节信息 }可以看到我们这里请求的设计采用的是get方法,通过/music/get?path=xxx.mp3获取到响应的音乐信息,这也就和我们之前设计的保存音乐的url匹配上了!而我们响应只要将对应的音乐字节信息返回给浏览器即可!代码实现 //播放音乐 @RequestMapping("/get") public ResponseEntity<byte[]> get(String path){ //我们要先获取到该音乐保存在服务器下的路径信息! try { byte[] fileByte = null; //获取到文件对象 File file = new File(SAVE_PATH +File.separator+ path); //获取到该文件对象的路径信息 Path filepath = file.toPath(); //读取文件中的所有字节 fileByte = Files.readAllBytes(filepath); return ResponseEntity.ok(fileByte); }catch (IOException e){ e.printStackTrace(); } return ResponseEntity.badRequest().build(); }方法讲解Files.readAllBytes(Path path);读取文件中的所有字节,参数是Path路径值!File.separator与系统相关的默认名称 - 分隔符字符,以方便的方式表示为字符串!就是在Windows系统下是\,而在Linux下是/ReponseEntity这是Spring对请求响应的分装,继承了HttpEntity对象,包含Http响应码(HttpStatus),响应头(header),响应体(body)3部分!ResponseEntity类继承自HttpEntity类,被用于Controller层方法 !我们可以通过这个类下面提供的静态方法,封装一下响应返回给浏览器!ok静态方法://这个方法若被调用的话,返回OK状态 public static ResponseEntity.BodyBuilder ok(){ return status(HttpStatus.OK); } //这个方法若被调用的话,返回body内容和OK状态 public static <T> ResponseEntity<T> ok(T body) { ResponseEntity.BodyBuilder builder = ok(); //ResponseEntity可以通过这个builder返回任意类型的body内容 return builder.body(body); }我们代码里的ResponseEntity.ok(fileByte);就是将ok状态和fileByte音乐文件信息以body的形式返回给前端!验证结果音乐文件有TAG标志  假如我们拿到的并不是mp3文件!这里的隆里电丝.mp3我是通过png文件改成了这个,显然找不到这个音乐标志!删除音乐模块删除单个音乐请求响应设计请求: { post /music/delete id } 响应: { status:0, message:"删除成功" data:true }代码实现这里的删除操作需要分成2步将服务器下的文件进行删除将数据库中的文件信息删除所以我们需要先查询到该id的音乐信息,再进行删除!Mapper接口/** * 通过音乐id删除音乐! * @param id * @return */ int deleteById(int id); /** * 通过id获取到音乐信息 * @param id * @return */ Music getMusicById(int id);Mapper实现Controller层代码实现 //删除单个音乐 @RequestMapping("/delete") public ResponseBodyMessage<Boolean> deleteMusic(@RequestParam Integer id){ //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); if(file==null){//服务器下不存在该文件 return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 file.delete(); //2.2 删除数据库下的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } return new ResponseBodyMessage<>(0,"删除成功!",true); }验证结果先查看数据库下的音乐信息删除的音乐不存在删除音乐存在删除成功!批量删除音乐请求响应设计请求: { post /music/deleteAll id[] } 响应: { status:0 message:"删除成功" data:true }代码实现我们只需要在删除单个音乐的基础上进行代码的修改即可!直接增加Controller层代码即可! //批量删除音乐 @RequestMapping("/deleteAll") public ResponseBodyMessage<Boolean> deleteMusicAll(@RequestParam(value = "id[]") List<Integer> ids){ String message = null; for (Integer id:ids) { //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 //保存这个音乐id信息 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); System.out.println("musicPath:"+file.getPath()); if(file==null){//服务器下不存在该文件 //保存这个id信息 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 if(!file.delete()){ //删除失败 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"服务器删除失败",false); } //2.2 删除数据库下的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } } if(message==null){ return new ResponseBodyMessage<>(0,"删除成功!",true); } //部分删除失败 return new ResponseBodyMessage<>(0,"id:" + message+" 删除失败!",true); }验证结果删除成功删除失败查询音乐模块我们的查询需要支持一下功能查询给定名称的歌曲给定歌曲名称全查询到单个歌曲给定名称字段(支持模糊匹配查询)查询到多个歌曲未给定歌曲名就查询所有歌曲请求响应设计请求: { get /music/findMusic musicName }响应: { status:0 message:"查询成功" data: { { id:2 title:"隆里电丝" singer:"大傻" time:"2022年8月1日" url:/music/get?path="隆里电丝" }, .... } }代码实现MusicMapper接口新增方法查询所有音乐模糊匹配查询某些音乐 /** *查询所有歌曲 * @return */ List<Music> findMusic(); /** * 通过名称查询到歌曲信息,支持模糊查询 * @return */ List<Music> findMusicByName(String musicName);MusicMapper.xml实现 <!--查询所有歌曲--> <select id="findMusic" resultType="com.example.onlinemusic.model.Music"> select * from music; </select> <!--模糊查询指定歌曲--> <select id="findMusicByName" resultType="com.example.onlinemusic.model.Music"> select * from music where title like concat('%',#{musicName},'%') </select>MusicController实现 //模糊查询 @RequestMapping("/findMusic") public ResponseBodyMessage<List<Music>> findMusic(@RequestParam(required =false) String musicName){ List<Music> musicList = new LinkedList<>(); if(musicName==null){ //查询名称为空,查询所有音乐返回 musicList = musicMapper.findMusic(); return new ResponseBodyMessage<>(0,"查询成功!",musicList); } //进行模糊查询! musicList = musicMapper.findMusicByName(musicName); return new ResponseBodyMessage<>(0,"查询成功!",musicList); }验证结果查询名称为空模糊查询收藏音乐模块添加音乐到收藏列表请求响应设计请求: { post, /lovemusic/likeMusic data:user_id,music_id }响应: { status:0, message:"收藏音乐成功", data:true }代码实现我们要将一首音乐收藏分为2步找到该音乐信息,判断是否收藏过(查询lovemusic表)收藏该音乐(添加到lovemusic表)新增LoveMusicMapper接口package com.example.onlinemusic.mapper; import com.example.onlinemusic.model.LoveMusic; import org.apache.ibatis.annotations.Mapper; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-08-01 * Time: 19:54 */ @Mapper public interface LoveMusicMapper { /** * 通过用户id和音乐id查询喜欢音乐 * @param user_id * @param music_id * @return */ LoveMusic findLoveMusicByUidAndMusicId(int user_id, int music_id); /** * 添加音乐到收藏列表 * @param user_id * @param music_id * @return */ int insetLoveMusic(int user_id,int music_id); } LoveMusicMapper.xml实现接口<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.onlinemusic.mapper.LoveMusicMapper"> <!--添加音乐到收藏列表--> <insert id="insetLoveMusic"> insert into lovemusic (user_id,music_id) values(#{user_id},#{music_id}) </insert> <!--通过用户id和音乐id查询收藏列表--> <select id="findLoveMusicByUidAndMusicId" resultType="com.example.onlinemusic.model.LoveMusic"> select * from lovemusic where user_id = #{user_id} and music_id = #{music_id} </select> </mapper>LoveMusicController代码实现package com.example.onlinemusic.controller; import com.example.onlinemusic.mapper.LoveMusicMapper; import com.example.onlinemusic.model.Contant; import com.example.onlinemusic.model.LoveMusic; import com.example.onlinemusic.model.User; import com.example.onlinemusic.tools.ResponseBodyMessage; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * Created with IntelliJ IDEA. * Description: * User: hold on * Date: 2022-08-01 * Time: 20:25 */ @RestController @RequestMapping("/lovemusic") public class LoveMusicController { //注入LoveMusicMapper @Resource private LoveMusicMapper loveMusicMapper; //收藏音乐 @RequestMapping("/likeMusic") public ResponseBodyMessage<Boolean> insertLoveMusic(@RequestParam Integer id,HttpServletRequest request){ //1.检查登入状态,未登入不创建回话 HttpSession session = request.getSession(false); if(session==null||null==request.getSession().getAttribute(Contant.USERINFO_SESSION_KEY)){ //未登入状态 return new ResponseBodyMessage<>(-1,"请登入用户",false); } //登入状态,进行音乐收藏 //1.获取到用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); System.out.println("user_id:"+user_id+" music_id:"+id); //2.查询该歌曲是否已存在收藏列表 LoveMusic loveMusic = loveMusicMapper.findLoveMusicByUidAndMusicId(user_id,id); if(loveMusic!=null){ //该歌曲已收藏! System.out.println("lovemusic:"+loveMusic); return new ResponseBodyMessage<>(-1,"收藏失败,该歌曲收藏",false); } //未收藏,将其收藏! int flg = loveMusicMapper.insetLoveMusic(user_id,id); if(flg!=1){ return new ResponseBodyMessage<>(-1,"收藏失败",false); } return new ResponseBodyMessage<>(0,"收藏成功!",true); } }验证结果歌曲已收藏,收藏失败歌曲未收藏,收藏成功查询喜欢的音乐列表这里的查询喜欢列表和查询音乐模块类似!未给定歌曲名称查询该用户所有收藏歌曲给定歌曲名称参数查询歌曲名称含有该参数的歌曲请求和响应设计请求: { get /lovemusic/findloveMusic data:{musicName:musicName} }响应: { status:0, message:"查询到收藏的音乐", data: { { id:1, title:"隆里电丝", singer:"大傻", time:"2022年8月2日", url:"/music/get?path=隆里电丝", user_id:2 } ... } }代码实现LoveMusicMapper接口新增方法 /** * 查询该用户所有的收藏歌曲 * @param user_id * @return */ List<Music> findLoveMusic(int user_id); /** * 通过用户id和歌曲名称查询收藏歌曲支持模糊匹配 * @param user_id * @param musicName * @return */ List<Music> findLoveMusicByUidAndMusicName(int user_id,String musicName); xml实现接口方法 <!--在该用户id下通过歌曲名称查询歌曲(支持模糊查询)--> <select id="findLoveMusicByUidAndMusicName" resultType="com.example.onlinemusic.model.Music"> select m.* from music as m, lovemusic as lm where m.id = lm.music_id and lm.user_id = #{user_id} and m.title like concat('%',#{musicName},'%') </select> <!--通过id查询收藏列表--> <select id="findLoveMusicById" resultType="com.example.onlinemusic.model.LoveMusic"> select * from lovemusic where id = #{id} </select>LoveMusicController代码实现 //通过音乐名称查询收藏列表 @RequestMapping("/findloveMusic") public ResponseBodyMessage<List<Music>> findLoveMusicByUidAndMusicName(@RequestParam(required = false) String musicName, HttpServletRequest request){ //1.检查登入状态,未登入不创建回话 HttpSession session = request.getSession(false); if(session==null||null==request.getSession().getAttribute(Contant.USERINFO_SESSION_KEY)){ //未登入状态 return new ResponseBodyMessage<>(-1,"请登入用户",null); } //登入状态,进行音乐查询 //1.获取到用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); System.out.println("user_id:"+user_id+" musicName:"+musicName); //2.查询收藏列表 List<Music> musicList = null; if(musicName==null){ //2.1歌曲名称为空,查询该用户所有收藏歌曲! musicList = loveMusicMapper.findLoveMusic(user_id); return new ResponseBodyMessage<>(0,"查询到收藏列表",musicList); } //2.2 歌曲名称不为空,模糊查询 musicList = loveMusicMapper.findLoveMusicByUidAndMusicName(user_id,musicName); return new ResponseBodyMessage<>(0,"查询收藏列表成功",musicList); } 验证结果数据库信息:登入user_di=10的用户查询该用户所有收藏歌曲模糊匹配查询指定歌曲从喜欢列表移除音乐### 请求和响应设计请求: { get, /lovemusic/deleteloveMusic, data:{id} } 响应: { status:0, message:"取消收藏成功", data:true }代码实现LoveMusicMapper接口新增方法 /** * 通过用户id和音乐id取消音乐收藏 * @param user_id * @param music_id * @return */ int deleteLoveMusicByUidAndMusicId(int user_id, int music_id); /** * 通过音乐id删除lovemusic表中的信息 * @param music_id * @return */ int deleteLoveMusicByMusicId(int music_id);xml实现接口 <!--通过用户id和音乐id移除收藏歌曲--> <delete id="deleteLoveMusicByUidAndMusicId"> delete from lovemusic where user_id = #{user_id} and music_id = #{music_id} </delete> <!--通过musicId移除收藏列表--> <delete id="deleteLoveMusicByMusicId"> delete from lovemusic where music_id = #{music_id} </delete>LoveMusicMapperController代码实现 //通过收藏表中的id取消收藏歌曲信息 @RequestMapping("/deleteloveMusic") public ResponseBodyMessage<Boolean> deleteLoveMusic(@RequestParam Integer id,HttpServletRequest request){ //1.检查登入状态,未登入不创建回话 HttpSession session = request.getSession(false); if(session==null||null==request.getSession().getAttribute(Contant.USERINFO_SESSION_KEY)){ //未登入状态 return new ResponseBodyMessage<>(-1,"请登入用户",false); } //登入状态,可进行取消音乐收藏功能 //1.获取到用户id User user = (User)session.getAttribute(Contant.USERINFO_SESSION_KEY); int user_id = user.getId(); //取消收藏! int ret = loveMusicMapper.deleteLoveMusicByUidAndMusicId(user_id,id); if(ret!=1){ return new ResponseBodyMessage<>(-1,"取消收藏失败",false); } return new ResponseBodyMessage<>(0,"取消收藏成功",true); }验证结果数据库信息:歌曲不存在,移除失败歌曲存在,取消收藏成功完善删除音乐模块代码我们对项目增添了收藏列表后,发现一个问题!我们上传的音乐删除后,收藏的音乐就不存在了,那么收藏列表中关于这首歌曲的信息也要删除,所以我们对我们的删除音乐代码进行完善!删除音乐时,要先检查该音乐是否在收藏列表中,如果在就将歌曲移除收藏列表我们只需要添加一个通过music_id取消收藏音乐的方法即可LoveMusicMapper新增方法/** * 通过音乐id删除lovemusic表中的信息 * @param music_id * @return */ int deleteLoveMusicByMusicId(int music_id);xml实现<!--通过musicId移除收藏列表--> <delete id="deleteLoveMusicByMusicId"> delete from lovemusic where music_id = #{music_id} </delete> //删除单个音乐 @RequestMapping("/delete") public ResponseBodyMessage<Boolean> deleteMusic(@RequestParam Integer id){ //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); System.out.println("musicPath:"+file.getPath()); if(file==null){//服务器下不存在该文件 return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 if(!file.delete()){ //删除失败 return new ResponseBodyMessage<>(-1,"服务器删除失败",false); } //2.2 删除数据库下的音乐信息 //2.2.1删除music表中的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } //2.2.2删除lovemusic表中的音乐信息 loveMusicMapper.deleteLoveMusicByMusicId(id); return new ResponseBodyMessage<>(0,"删除成功!",true); } //批量删除音乐 @RequestMapping("/deleteAll") public ResponseBodyMessage<Boolean> deleteMusicAll(@RequestParam(value = "id[]") List<Integer> ids){ String message = ""; for (Integer id:ids) { //1.首先找到该音乐信息 Music music = musicMapper.getMusicById(id); if(music==null){//音乐不存在 //未找到该音乐 //保存这个音乐id信息 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"需要删除的音乐不存在",false); } //2.进行音乐删除 //2.1 删除服务器下的音乐文件 //找到服务器下该音乐文件路径 File file = new File(SAVE_PATH+File.separator+music.getTitle()+".mp3"); System.out.println("musicPath:"+file.getPath()); if(file==null){//服务器下不存在该文件 //保存这个id信息 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"删除失败,服务器下未找到该文件信息",false); } //删除 if(!file.delete()){ //删除失败 message += id + ""; continue; //return new ResponseBodyMessage<>(-1,"服务器删除失败",false); } //2.2 删除数据库下的音乐信息 int ret = musicMapper.deleteById(id); if(ret!=1){//数据库删除失败 message += id+" "; continue; //return new ResponseBodyMessage<>(-1,"数据库信息删除失败",false); } //取消该歌曲的所有收藏 loveMusicMapper.deleteLoveMusicByMusicId(id); } if(message==""){ return new ResponseBodyMessage<>(0,"删除成功!",true); } //部分删除失败 return new ResponseBodyMessage<>(0,"id:" + message+" 删除失败!",true); }验证结果:数据库信息:我们将music_id为51音乐删除!前端设计我们前端页面是在网上下载了一个静态页面的模板!我们需要在此基础上进行js代码的编写,从而实现前后端用户的接口登入逻辑核心逻辑:<script> //核心业务逻辑 ///$(function () {} 当DOM加载完毕后执行 $(function(){ //当点击这个按钮后就会执行function函数 $("#submit").click(function(){ //获取到用户名和密码 var username = $("#user").val(); var password = $("#password").val(); //判断用户名密码是否为空! if(username.trim()==""||password.trim()==""){ alert("请输入用户名和密码"); return; } //这里就需要将请求前端数据返回给服务器! $.ajax({ url:"/user/login",//指定路径 type:"post", data:{"username":username,"password":password}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); if(body.status==0){//通过后端发送回来的status判断登入状态! alert("登入成功!"); //登入成功后跳转到指定页面 window.location.href("list.html"); }else{ alert("登入失败,账号或密码错误,请重试!"); //将输入框清空! $("#message").text(""); $("#user").val(""); $("#password").val(""); } } }); }); }); </script>验证结果:注册逻辑 <script> //核心业务逻辑 ///$(function () {} 当DOM加载完毕后执行 $(function(){ //当点击这个按钮后就会执行function函数 $("#submit").click(function(){ //获取到用户名和密码 var username = $("#user").val(); var password = $("#password").val(); //判断用户名密码是否为空! if(username.trim()==""||password.trim()==""){ alert("请输入用户名和密码"); return; } //这里就需要将请求前端数据返回给服务器! $.ajax({ url:"/user/login",//指定路径 type:"post", data:{"username":username,"password":password}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); if(body.status==0){//通过后端发送回来的status判断登入状态! alert("登入成功!"); //登入成功后跳转到指定页面 window.location.href = "list.html"; }else{ alert("登入失败,账号或密码错误,请重试!"); //将输入框清空! $("#message").text(""); $("#user").val(""); $("#password").val(""); } } }); }); }); </script>验证结果: '查询音乐逻辑核心代码逻辑: <script type="text/javascript"> $(function(){ load(); }); //musicName可以传参,也可以不传 function load(musicName){ $.ajax({ url:"/music/findMusic",//指定路径 type:"get", data:{"musicName":musicName}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); var s = ''; var data = body.data;//数组! for(var i = 0;i<data.length;i++){ var musicUrl = data[i].url+'.mp3'; s += '<tr>'; s += '<th> <input id="'+data[i].id+'"type="checkbox"> </th>'; s += '<td>' + data[i].title + '</td>'; s += '<td>' + data[i].singer + '</td>'; s +='<td > <button class="btn btn-primary" onclick="playerSong(\''+musicUrl+'\')" >播放歌曲</button>' + '</td>'; s +='<td > <button class="btn btn-primary" onclick="deleteInfo('+ data[i].id + ')" >删除</button>' + '&nbsp;'+'<button class="btn btn-primary" onclick="loveInfo('+ data[i].id + ')" > 喜欢</button>'+ '</td>'; s += '</tr>'; } $("#info").html(s);//把拼接好的页面放在info的id下 } }); } </script>验证结果:上传音乐逻辑核心代码逻辑<form method="post" enctype="multipart/form-data" action="/music/upload"> 文件上传:<input type="file" name="filename"/> 歌手名: <label> <input type="text" name="singer" placeholder="请输入歌手名"/> </label> <input type="submit" value="上传"/> </form>验证结果:播放音乐<!--嵌入播放器!--> <div style="width: 180px; height: 140px; position:absolute; bottom:10px; right:10px"> <script type="text/javascript" src="player/sewise.player.min.js"></script> <script type="text/javascript"> SewisePlayer.setup({ server: "vod", type: "mp3", //播放的地址 videourl:"http://jackzhang1204.github.io/materials/where_did_time_go.mp3", //皮肤! skin: "vodWhite", //这里自动播放需要设置false autostart:"false", }); </script> </div> //播放音乐 function playerSong(obj) { console.log(obj) var name = obj.substring(obj.lastIndexOf('=')+1); //obj:播放地址 name:歌曲或者视频名称 0:播放开始时间 false:点击后自动播放 SewisePlayer.toPlay(obj,name,0,true); }验证结果:删除音乐逻辑 //删除音乐 function deleteInfo(obj){ console.log(obj); $.ajax({ url:"/music/delete", type:'post', dataType:"json", data:{"id":obj}, success:function(body){ console.log(body); if(body.data==true){ //删除成功! alert("删除成功!"); window.location.href = "list.html"; }else{ alert("删除失败!"); } } }); }验证结果:查询歌曲逻辑 $(function(){ $('#submit1').click(function(){ var name = $("#exampleInputName2").val(); load(name); }); });验证结果:删除选中的歌曲逻辑 //删除选中逻辑在查询逻辑里 $(function(){ $('#submit1').click(function(){ var name = $("#exampleInputName2").val(); load(name); }); //当查询结束才可进行选中删除(有了音乐列表) $.when(load).done(function(){ //选中删除逻辑! $("#delete").click(function(){ var id = new Array(); var i = 0;//数组下标! //遍历input标签下的checkbox $("input:checkbox").each(function(){ //判断是否选中! if($(this).is(":checked")){ //获取input里面的id值! id[i] = $(this).attr('id'); i++; } }); console.log(id); $.ajax({//将获取到的id发送给服务器! url:"/music/deleteAll", data:{"id":id}, type:'post', success:function(body){ if(body.status==0){ alert("删除成功!"); window.location.href="list.html"; }else{ alert("删除失败!"); } } }); }); }); });验证结果: 收藏音乐逻辑 $(function(){ load(); }); //musicName可以传参,也可以不传 function load(musicName){ $.ajax({ url:"/lovemusic/findLoveMusic",//指定路径 type:"get", data:{"musicName":musicName}, dataType:"json", //设置服务器返回json数据 success:function(body){//回调函数 console.log(body); var s = ''; var data = body.data;//数组! for(var i = 0;i<data.length;i++){ var musicUrl = data[i].url+'.mp3'; s += '<tr>'; s += '<td>' + data[i].title + '</td>'; s += '<td>' + data[i].singer + '</td>'; s +='<td > <button class="btn btn-primary" onclick="playerSong(\''+musicUrl+'\')" >播放歌曲</button>' + '</td>'; s +='<td > <button class="btn btn-primary" onclick="deleteInfo('+ data[i].id + ')" >移除</button>' + '</td>'; s += '</tr>'; } $("#info").html(s);//把拼接好的页面放在info的id下 } }); } //播放音乐 function playerSong(obj) { console.log(obj) var name = obj.substring(obj.lastIndexOf('=')+1); //obj:播放地址 name:歌曲或者视频名称 0:播放开始时间 false:点击后自动播放 SewisePlayer.toPlay(obj,name,0,true); } //删除音乐 function deleteInfo(obj){ console.log(obj); $.ajax({ url:"/lovemusic/deleteloveMusic", type:'post', dataType:"json", data:{"id":obj}, success:function(body){ console.log(body); if(body.data==true){ //删除成功! alert("移除成功!"); window.location.href = "loveMusic.html"; }else{ alert("移除失败!"); } } }); }验证结果:实现收藏功能逻辑//在list.html文件中添加一个收藏歌曲函数即可! //收藏歌曲 function loveInfo(obj){ $.ajax({ url:"/lovemusic/likeMusic", type:"post", data:{"id":obj}, dataType:"json", success:function(body){ if(body.data==true){ alert("收藏成功!"); window.location.href="list.html"; }else{ alert("收藏失败!"); } } }) }验证结果:配置拦截器有些页面我们没有登入也可以进行访问,通过输入url到地址栏即可!我们可以在项目中进行登入状态的检查,如果登入了就可以访问,否则不能,这就配置了拦截器,保证程序安全!我们首先在config包下自定义一个拦截器核心代码package com.example.onlinemusic.config; import com.example.onlinemusic.model.Contant; import com.example.onlinemusic.tools.ResponseBodyMessage; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Created with IntelliJ IDEA. * Description:配置拦截器 * User: hold on * Date: 2022-08-06 * Time: 0:43 */ public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //检查是否登入! HttpSession session = request.getSession(false); if(session==null||session.getAttribute(Contant.USERINFO_SESSION_KEY)==null){ //未登入状态! System.out.println("未登入"); return false; } return true; } } 然后将配置好的拦截器添加到AppConfig类中package com.example.onlinemusic.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * Created with IntelliJ IDEA. * Description:将自定义拦截器添加到系统配置! * User: hold on * Date: 2022-07-27 * Time: 12:29 */ @Configuration public class AppConfig implements WebMvcConfigurer { //将BCrypetPasswordEncoder对象交给spring管理! @Bean public BCryptPasswordEncoder getBCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } //添加拦截器! @Override public void addInterceptors(InterceptorRegistry registry) { LoginInterceptor loginInterceptor = new LoginInterceptor(); registry.addInterceptor(loginInterceptor) //拦截该项目目录下的所有文件! .addPathPatterns("/**") //排除无关文件 .excludePathPatterns("/js/**.js") .excludePathPatterns("/images/**") .excludePathPatterns("/css/**.css") .excludePathPatterns("/fronts/**") .excludePathPatterns("/player/**") //排除登入注册接口! .excludePathPatterns("/login.html") .excludePathPatterns("/user/login") .excludePathPatterns("/reg.html") .excludePathPatterns("/user/register"); } } 验证结果:# 部署到服务器准备更改数据库配置因为我的云服务器下的数据库用户密码信息和本地的一样,所以不用更改数据库配置!我们在云服务器下创建好数据库即可!更改上传音乐路径# 云服务器下的地址路径! music.path.save = /root/javaweb部署环境/music打包项目打包打包报错,我们需要统一编码UTF-8添加依赖 <!--添加maven.plugins依赖 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.1.0</version> </plugin> <!--添加maven-surefire-plugin依赖--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin>打包成功!部署 将打包好的项目上传到服务器!启动项目:java -jar onlinemusic.jar 这里显示端口号8080被占用,那么我们就将端口号给kill//查看端口 netstat -anp | grep 8080 //杀进程 kill -9 pcb值验证结果:这样虽然部署好了项目,但是这个只能支持前台运行,也就是说这个项目关闭了,项目就访问不了了!后台运行SpringBoot项目运行指令:nohup java -jar onlinemusic.jar>>log.log&nohup:后台运行项目的指令>>log.log:把控制台上的日志保存到log.log文件中!(未设置可默认生成)&:后台一直运行这样运行就支持后台运行了!后期项目维护更新如果后面我们觉得项目需要完善该如何进行服务器项目更新呢?将该项目的进制终止netstat -anp |grep 8080 kill -9 17303也可以使用ps -ef | grep javakill 【进程ID】 命令说明: ps : Linux 当中查看进程的命令 -e 代表显示所有的进程 -f 代表全格式【显示全部的信息】 grep : 全局正则表达式 重新上传jar包 重新进行后台的启动更新项目,重新运行 nohup java -jar onlinemusic.jar>>log.log&更新了一下注册登入的前端页面!遇到的面试题总结上传其他文件,然后将后缀改成.mp3,如何识别?是否可以正常播放?因为每种类型的文件都有自己的文件结构,都有自己特有的格式,我们根据mp3特有的文件格式,在倒数第128字节处,有有个TAG音乐文件标志,从而在上传时就检测一下是否是音频文件,如果不是音频文件无法上传!可以上传大文件嘛?不能,因为一首歌曲的大小不会很大,所以我已经在配置文件配置了每个文件的最大上传大小,以及单次请求的文件总数大小!#配置springboot上传文件的大小,默认每个文件的配置最大为15Mb,单次请求的文件的总数不能大于100Mb spring.servlet.multipart.max-file-size = 15MB spring.servlet.multipart.max-request-size=100MB为啥不用HTML的原生audio标签?因为我想通过使用开源的播放器,提升一下自己的学习能力,毕竟我们经常会在自己的项目中使用到其他的优秀开源项目,我们也需要具备这样的能力,学习使用大佬的优秀项目!只要将开源播放代码换成原生audio即可!s += "<td <a href=\"\"> <audio src= \""+ musicUrl+"\" + controls=\"controls\" preload=\"none\" loop=\"loop\"> >" + "</audio> </a> </td>";原生的audio标签和开源播放器的一首歌曲的下载时间如下:开源播放器:原生audio播放器:可以看到同样一首歌曲在线播放后下载的时间不同,虽然2个都是边下载边播放,但是这里的开源播放器下载时间更短!
2022年09月
2022年08月