• 默认 全屏
  • 默认 双列 三列
宝宝图像
我可以被点击哦

林子宸的博客

松花酿酒,春水煎茶。
  • Java响应式编程之Reactor

    1.reactor简介Reactor是一个用于JVM的完全非阻塞的响应式编程框架,具备高效的需求管理(即对“背压(backpressure)”的控制)能力。它与Java8函数式API直接集成,比如CompletableFuture,Stream,以及Duration。它提供了异步序列APIFlux(用于[N]个元素)和Mono(用于[0|1]个元素),并完全遵循和实现了“响应式扩展规范”(ReactiveExtensionsSpecification)。Reactor的reactor-ipc组件还支持非阻塞的进程间通信(inter-processcommunication,IPC)。ReactorIPC为HTTP(包括Websockets)、TCP和UDP提供了支持背压的网络引擎,从而适合应用于微服务架构。并且完整支持响应式编解码(reactiveencodinganddecoding)。2.为什么需要响应式编程2.1阻塞是对资源的浪费现代应用需要应对大量的并发用户,而且即使现代硬件的处理能力飞速发展,软件性能仍然是关键因素。广义来说我们有两种思路来提升程序性能:*并行化(parallelize):使用更多的线程和硬件资源。[异步]*基于现有的资源来提高执行效率。通常,Java开发者使用阻塞式(blocking)编写代码。这没有问题,在出现性能瓶颈后,我们可以增加处理线程,线程中同样是阻塞的代码。但是这种使用资源的方式会迅速面临资源竞争和并发问题。更糟糕的是,阻塞会浪费资源。具体来说,比如当一个程序面临延迟(通常是I/O方面,比如数据库读写请求或网络调用),所在线程需要进入idle状态等待数据,从而浪费资源。所以,并行化方式并非银弹。这是挖掘硬件潜力的方式,但是却带来了复杂性,而且容易造成浪费。2.2异步可以解决问题吗第二种思路——提高执行效率——可以解决资源浪费问题。通过编写异步非阻塞的代码,(任务发起异步调用后)执行过程会切换到另一个使用同样底层资源的活跃任务,然后等异步调用返回结果再去处理。但是在JVM上如何编写异步代码呢?Java提供了两种异步编程方式:●回调(Callbacks):异步方法没有返回值,而是采用一个callback作为参数(lambda或匿名类),当结果出来后回调这个callback。常见的例子比如Swings的EventListener。●Futures:异步方法立即返回一个Future,该异步方法要返回结果的是T类型,通过Future封装。这个结果并不是立刻可以拿到,而是等实际处理结束才可用。比如,ExecutorService执行Callable任务时会返回Future对象。这些技术够用吗?并非对于每个用例都是如此,两种方式都有局限性。回调很难组合起来,因为很快就会导致代码难以理解和维护(即所谓的“回调地狱(callbackhell)”)。比如官方列举了一个例子,这里不再说明,可通过点击这里查看。3.从命令式编程到响应式编程类似Reactor这样的响应式库的目标就是要弥补上述“经典”的JVM异步方式所带来的不足,此外还会关注一下几个方面:●可编排性(Composability)以及可读性(Readability)●使用丰富的操作符来处理形如流的数据●在订阅(subscribe)之前什么都不会发生●背压(backpressure)具体来说即消费者能够反向告知生产者生产内容的速度的能力●高层次(同时也是有高价值的)的抽象,从而达到并发无关的效果3.1可编排性与可读性可编排性,指的是编排多个异步任务的能力。比如我们将前一个任务的结果传递给后一个任务作为输入,或者将多个任务以分解再汇总(fork-join)的形式执行,或者将异步的任务作为离散的组件在系统中进行重用。这种编排任务的能力与代码的可读性和可维护性是紧密相关的。随着异步处理任务数量和复杂度的提高,编写和阅读代码都变得越来越困难。就像我们刚才看到的,回调模式是简单的,但是缺点是在复杂的处理逻辑中,回调中会层层嵌入回调,导致回调地狱(CallbackHell)。你能猜到(或有过这种痛苦经历),这样的代码是难以阅读和分析的。Reactor提供了丰富的编排操作,从而代码直观反映了处理流程,并且所有的操作保持在同一层次(尽量避免了嵌套)。3.2就像装配流水线你可以想象数据在响应式应用中的处理,就像流过一条装配流水线。Reactor既是传送带,又是一个个的装配工或机器人。原材料从源头(最初的Publisher)流出,最终被加工为成品,等待被推送到消费者(或者说Subscriber)。原材料会经过不同的中间处理过程,或者作为半成品与其他半成品进行组装。如果某处有齿轮卡住,或者某件产品的包装过程花费了太久时间,相应的工位就可以向上游发出信号来限制或停止发出原材料。3.3操作符(Operators)在Reactor中,操作符(operator)就像装配线中的工位(操作员或装配机器人)。每一个操作符对Publisher进行相应的处理,然后将Publisher包装为一个新的Publisher。就像一个链条,数据源自第一个Publisher,然后顺链条而下,在每个环节进行相应的处理。最终,一个订阅者(Subscriber)终结这个过程。请记住,在订阅者(Subscriber)订阅(subscribe)到一个发布者(Publisher)之前,什么都不会发生。理解了操作符会创建新的Publisher实例这一点,能够帮助你避免一个常见的问题,这种问题会让你觉得处理链上的某个操作符没有起作用。虽然响应式流规范(ReactiveStreamsspecification)没有规定任何操作符,类似Reactor这样的响应式库所带来的最大附加价值之一就是提供丰富的操作符。包括基础的转换操作,到过滤操作,甚至复杂的编排和错误处理操作。3.4subscribe()之前什么都不会发生在Reactor中,当你创建了一条Publisher处理链,数据还不会开始生成。事实上,你是创建了一种抽象的对于异步处理流程的描述(从而方便重用和组装)。当真正“订阅(subscrib)”的时候,你需要将Publisher关联到一个Subscriber上,然后才会触发整个链的流动。这时候,Subscriber会向上游发送一个request信号,一直到达源头的Publisher。3.5背压向上游传递信号这一点也被用于实现背压,就像在装配线上,某个工位的处理速度如果慢于流水线速度,会对上游发送反馈信号一样。在响应式流规范中实际定义的机制同刚才的类比非常接近:订阅者可以无限接受数据并让它的源头“满负荷”推送所有的数据,也可以通过使用request机制来告知源头它一次最多能够处理n个元素。中间环节的操作也可以影响request。想象一个能够将每10个元素分批打包的缓存(buffer)操作。如果订阅者请求一个元素,那么对于源头来说可以生成10个元素。此外预取策略也可以使用了,比如在订阅前预先生成元素。这样能够将“推送”模式转换为“推送+拉取”混合的模式,如果下游准备好了,可以从上游拉取n个元素;但是如果上游元素还没有准备好,下游还是要等待上游的推送。3.6热vs冷在Rx家族的响应式库中,响应式流分为“热”和“冷”两种类型,区别主要在于响应式流如何对订阅者进行响应:●一个“冷”的序列,指对于每一个Subscriber,都会收到从头开始所有的数据。如果源头生成了一个HTTP请求,对于每一个订阅都会创建一个新的HTTP请求。●一个“热”的序列,指对于一个Subscriber,只能获取从它开始订阅之后发出的数据。不过注意,有些“热”的响应式流可以缓存部分或全部历史数据。通常意义上来说,一个“热”的响应式流,甚至在即使没有订阅者接收数据的情况下,也可以发出数据(这一点同“Subscribe()之前什么都不会发生”的规则有冲突)。4.使用Reactor4.1引入依赖<dependencyManagement><dependencies><dependency><groupId>io.projectreactor</groupId><artifactId>reactor-bom</artifactId><version>2023.0.1</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>io.projectreactor</groupId><artifactId>reactor-core</artifactId></dependency><dependency><groupId>io.projectreactor</groupId><artifactId>reactor-test</artifactId><scope>test</scope></dependency></dependencies>4.2创建Flux或MonoFlux用于创建0|N个元素的数据流。Mono用户创建0|1个元素的数据流。开始使用Flux或Mono最简单的方式是在它们的类中站到各自的工厂方法:Flux<String>seq1=Flux.just("foo","bar","foobar");List<String>iterable=Arrays.asList("foo","bar","foobar");Flux<String>seq2=Flux.fromIterable(iterable);Flux<Integer>numbersFromFiveToSeven=Flux.range(5,3);Mono<String>noData=Mono.empty();Mono<String>data=Mono.just("foo");4.3订阅简单订阅在订阅方面,Flux和Mono使用Java8lambda。我们有广泛的选择.subscribe()变体,将lambda用于不同的回调组合://订阅并触发序列。subscribe();//对每个产生的值做些什么subscribe(Consumer<?superT>consumer);//处理值,并且对错误做出反应。subscribe(Consumer<?superT>consumer,Consumer<?superThrowable>errorConsumer);//处理值和错误,并且在序列成功完成时运行一些代码。subscribe(Consumer<?superT>consumer,Consumer<?superThrowable>errorConsumer,RunnablecompleteConsumer);//处理值、错误和成功完成,并且传入自定义消费者,对订阅时做一些事情subscribe(Consumer<?superT>consumer,Consumer<?superThrowable>errorConsumer,RunnablecompleteConsumer,Consumer<?superSubscription>subscriptionConsumer);下面是一个生产数据流并订阅消费的例子:Flux.just(1,2,3,4,0,6).map(item->10/item).subscribe(v->System.out.println("正常:"+v),error->System.out.println("错误:"+error.getMessage()),()->System.out.println("流正常完成"));/**结果:正常:10正常:5正常:3正常:2错误:/byzero*/自定义订阅者还有一个额外的subscribe方法,它更通用,需要一个成熟的Subscriber,而不是用lambda编写。为了帮助写作这样的Subscriber,我们提供了一个可扩展的类,称为BaseSubscriber。Flux<Integer>flux=Flux.just(1,2,3,4,5).doOnRequest(item->System.out.println("上游收到订阅者请求="+item));flux.subscribe(newBaseSubscriber<Integer>(){//生命周期钩子1:订阅关系绑定的时候触发@OverrideprotectedvoidhookOnSubscribe(Subscriptionsubscription){//流被订阅的时候触发System.out.println("订阅者绑定了..."+subscription);//找发布者要数据request(1);//要1个数据//requestUnbounded();//要无限数据}@OverrideprotectedvoidhookOnNext(Integervalue){System.out.println("订阅者数据到达,正在处理:"+value);request(1);//要1个数据}//hookOnComplete、hookOnError二选一执行@OverrideprotectedvoidhookOnComplete(){System.out.println("流正常结束...");}@OverrideprotectedvoidhookOnError(Throwablethrowable){System.out.println("流异常..."+throwable);}@OverrideprotectedvoidhookOnCancel(){System.out.println("流被取消...");}@OverrideprotectedvoidhookFinally(SignalTypetype){System.out.println("最终回调...一定会被执行");}});/**订阅者绑定了...reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber@3a709cc7上游收到订阅者请求=1订阅者数据到达,正在处理:1上游收到订阅者请求=1订阅者数据到达,正在处理:2上游收到订阅者请求=1订阅者数据到达,正在处理:3上游收到订阅者请求=1订阅者数据到达,正在处理:4上游收到订阅者请求=1订阅者数据到达,正在处理:5上游收到订阅者请求=1流正常结束...最终回调...一定会被执行*/4.4流的取消消费者调用cancle()取消流的订阅;Flux<Integer>flux=Flux.just(1,2,3,4,5).doOnRequest(item->System.out.println("上游收到订阅者请求="+item));flux.subscribe(newBaseSubscriber<Integer>(){//生命周期钩子1:订阅关系绑定的时候触发@OverrideprotectedvoidhookOnSubscribe(Subscriptionsubscription){//流被订阅的时候触发System.out.println("订阅者绑定了..."+subscription);//找发布者要数据request(1);//要1个数据}@OverrideprotectedvoidhookOnNext(Integervalue){System.out.println("订阅者数据到达,正在处理:"+value);if(value==3){cancel();}request(1);//要1个数据}//hookOnComplete、hookOnError二选一执行@OverrideprotectedvoidhookOnComplete(){System.out.println("流正常结束...");}@OverrideprotectedvoidhookOnError(Throwablethrowable){System.out.println("流异常..."+throwable);}@OverrideprotectedvoidhookOnCancel(){System.out.println("流被取消...");}@OverrideprotectedvoidhookFinally(SignalTypetype){System.out.println("最终回调...一定会被执行");}});/**订阅者绑定了...reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber@6e49b011上游收到订阅者请求=1订阅者数据到达,正在处理:1上游收到订阅者请求=1订阅者数据到达,正在处理:2上游收到订阅者请求=1订阅者数据到达,正在处理:3流被取消...最终回调...一定会被执行*/4.5背压(Backpressure)和请求重塑(ReshapeRequests)背压在reactor中实现背压,就是订阅者向上游发送request请求。例如:request(1)表示找发布者要1个数据。上限为Long.MAX_VALUE,代表无界请求(意思是“尽可能快地生产”——基本上禁用反向压力)。自定义原始请求的最简单方法是使用hookOnSubscribe方法被覆盖的BaseSubscriber进行subscribe,如下例所示:Flux.range(1,10).doOnRequest(r->System.out.println("requestof"+r)).subscribe(newBaseSubscriber<Integer>(){@OverridepublicvoidhookOnSubscribe(Subscriptionsubscription){request(1);}@OverridepublicvoidhookOnNext(Integerinteger){System.out.println("Cancellingafterhavingreceived"+integer);cancel();}});/**requestof1Cancellingafterhavingreceived1*/在操作请求时,必须小心产生足够的需求推进序列,否则Flux可能会“卡住”。这就是为什么BaseSubscriber默认为hookOnSubscribe中的无界请求。覆盖此钩子时,通常应该request至少一次。缓冲区buffer在订阅级别表达的请求可以由上游链中的每个运算符重塑。即buffer(N)运算符:如果它收到一个request(2),它被解释为对两个完整缓冲区的请求。因此,由于缓冲区需要N元素才能被视为满,因此buffer运算符将请求重塑为2xN。Flux<List<Integer>>flux=Flux.range(1,10)//原始流10个.buffer(3);//缓冲区:缓冲3个元素:消费一次最多可以拿到三个元素;凑满数批量发给消费者flux.subscribe(value->System.out.println("value="+value));/**value=[1,2,3]value=[4,5,6]value=[7,8,9]value=[10]*/4.6编程方式创建序列在ProjectReactor的Flux中,提供了generate和create等方法,用于编程方式创建序列。generate方法generate方法允许你按需生成序列的元素。它接受一个SynchronousSink参数,你可以在其中定义生成元素的逻辑,并使用sink发射元素。Flux<String>generatedFlux=Flux.generate(()->0,//初始状态(state,sink)->{sink.next("Element"+state);if(state==9){sink.complete();//完成序列的生成}returnstate+1;//返回新的状态});上面的例子中,generate从状态0开始生成序列,每次生成一个带有当前状态的元素,直到状态为9时完成序列的生成。应用场景:适用于按需生成元素的场景,即你可以根据当前状态的逻辑来生成元素。通常用于基于状态的逐个生成序列元素的情况。特点:状态驱动:generate接受一个初始状态和一个生成元素的逻辑。在每次生成元素时,你可以根据当前状态决定下一个元素的值。条件完成:可以在生成元素的逻辑中定义条件,当满足条件时,通过sink.complete()来完成序列的生成。返回新状态:生成元素的逻辑中,你需要返回一个新的状态,作为下一次生成元素时的初始状态。create方法create方法提供了更灵活的方式,可以通过FluxSink手动控制序列的生成,发射元素,并在需要时完成序列。Flux<String>createdFlux=Flux.create(sink->{for(inti=0;i<10;i++){sink.next("Element"+i);}sink.complete();//完成序列的生成});在这个例子中,使用Flux.create手动调用sink.next发射元素,并最后调用sink.complete完成序列的生成。这些方法使得在Reactor中以编程方式创建序列变得非常方便,允许你按照自己的逻辑和需求生成和控制数据流。应用场景:适用于手动控制序列生成和发射元素的场景,提供更灵活的控制。通常用于一次性生成完整序列的情况。特点:手动控制:使用Flux.create时,你需要手动调用sink.next来发射每个元素,以及在需要时调用sink.complete来完成序列的生成。灵活性:相对于generate,create提供更大的灵活性,因为你完全掌控了生成元素和序列完成的时机。一次性生成:create适用于一次性生成完整序列的情况,而不需要状态的迭代。返回新状态:生成元素的逻辑中,你需要返回一个新的状态,作为下一次生成元素时的初始状态。4.7自定义元素处理规则通过handle()方法可以自定义流中元素的处理。Flux.just(1,2,3,4,5).handle((value,sink)->{sink.next("pre"+value);}).subscribe(v->System.out.println("v="+v));/**v=pre1v=pre2v=pre3v=pre4v=pre5*/map和handle的区别map:用于转换Flux中的每个元素。返回一个新的Flux,其中包含经过转换的元素。特点:适用于元素级别的转换。返回一个包含转换后元素的新Flux。handle:用于处理Flux中的元素,通常是一种条件性的处理。允许根据条件决定是否处理元素,以及如何处理。```javaFluxoriginalFlux=Flux.just(1,2,3);originalFlux.handle((item,sink)->{if(item%2==0){sink.next(item*2);}});```特点:1.适用于条件性的元素处理。2.使用sink来决定是否发射处理后的元素。map和handle总结map用于元素级别的转换,返回一个新的包含转换后元素的Flux。handle用于条件性的元素处理,可以根据条件决定是否处理元素,并使用sink来发射处理后的元素。在Reactor中,这两者通常一起使用,map用于常规的元素转换,而handle用于更灵活的条件性元素处理.4.8线程和调度首先来看个简单的例子:Flux.just(1,2,3).doOnNext(value->System.out.println("发布者:"+Thread.currentThread().getName()+value)).subscribe(value->System.out.println("订阅者:"+Thread.currentThread().getName()+value));/**发布者:main1订阅者:main1发布者:main2订阅者:main2发布者:main3订阅者:main3*/发布者和订阅者默认都是同一个线程。再看一个例子:订阅者在新创建的线程中:Flux<Integer>flux=Flux.just(1,2,3).doOnNext(value->System.out.println("发布者:"+Thread.currentThread().getName()+value));newThread(()->{flux.subscribe(value->System.out.println("订阅者:"+Thread.currentThread().getName()+value));},"Thread线程").start();/**发布者:Thread线程1订阅者:Thread线程1发布者:Thread线程2订阅者:Thread线程2发布者:Thread线程3订阅者:Thread线程3*/总结:当订阅者用了新的线程后,发布者默认用订阅者的线程。线程调度SchedulesSchedulers是Reactor框架中用于管理异步操作的组件。它提供了不同类型的调度器,允许你指定在哪个线程上执行Reactor流的不同部分。Schedulers的使用可以帮助你有效地处理并发、并行和异步编程。Schedulers提供了很多的线程池:immediate():默认,无执行上下文,即在当前线程下执行所有操作。single():使用固定的一个单线程。boundElastic():有界、弹性的线程池:不是无限扩充的线程池。线程池中有10*cpu核心的线程。队列默认10w,100k。fromExecutor():自定义线程池。@TestpublicvoidtestSchedule(){Flux.just(1).subscribeOn(Schedulers.parallel())//在parallel线程池上执行订阅.map(i->{System.out.println("subscribeOn,map中:"+getThreadName()+i);returni;}).publishOn(Schedulers.boundedElastic())//在elastic线程池上执行map操作.doOnNext(v->System.out.println("doOnNext,publishOn后,:"+getThreadName()+v)).subscribe(v->System.out.println("subscribe:"+getThreadName()+v));}publicstaticStringgetThreadName(){returnThread.currentThread().getName()+":";}/**subscribeOn,map中:parallel-1:1doOnNext,publishOn后,:boundedElastic-1:1subscribe:boundedElastic-1:1*/subscribeOn(Schedulers.parallel()):使用subscribeOn操作符将订阅操作放在parallel()线程池上执行。这会影响整个订阅链中的订阅操作。在这个例子中,Flux.just(1)的订阅操作将在parallel()线程池中执行。publishOn(Schedulers.boundedElastic()):使用publishOn操作符将之后的操作放在boundedElastic()线程池上执行。这会影响doOnNext和subscribe操作。在这个例子中,map操作之后的操作将在boundedElastic()线程池中执行。doOnNext(v->…):在doOnNext操作中,输出了当前线程的名称和值。这是为了显示在publishOn中执行的线程。subscribe(v->…):最后,通过subscribe订阅操作。输出了当前线程的名称和值。这是为了显示整个订阅链结束时所在的线程。4.9错误处理在ReactiveStreams中,错误是终端事件。一旦发生错误,它就会停止序列并沿着运算符链传播到最后一步,即Subscriber的订阅服务器及其onError方法。此类错误仍应在应用程序级别处理。对于实例,您可能在UI中显示错误通知或在REST中发送有意义的错误有效负载因此,应始终定义订阅者的onError方法。reactor还提供了处理链中间错误的替代方法,例如错误处理运算符。以下示例显示了如何执行此操作:Flux.just(1,2,0).map(i->"100/"+i+"="+(100/i))//thistriggersanerrorwith0.onErrorReturn("Dividedbyzero:(");//errorhandlingexamplereactor序列中的任何错误都是最终事件。即使错误处理使用运算符时,它不会让原始序列继续。相反,它将onError信号转换为新序列(兜底序列)的开始。在换句话说,它取代了它上游的终止序列。在命令式编程中,有很多处理错误方式,比如trycatch,throw等,下面就针对命令式编程中的错误处理,看下在响应式编程中是如何操作的。trycatch捕获异常,并打印异常信息。try{for(inti=1;i<11;i++){Stringv1=doSomethingDangerous(i);Stringv2=doSecondTransform(v1);System.out.println("RECEIVED"+v2);}}catch(Throwablet){System.err.println("CAUGHT"+t);}Flux<String>s=Flux.range(1,10).map(v->doSomethingDangerous(v)).map(v->doSecondTransform(v));s.subscribe(value->System.out.println("RECEIVED"+value),error->System.err.println("CAUGHT"+error));trycatchreturn捕获异常,并返回错误信息try{returndoSomethingDangerous(10);}catch(Throwableerror){return"RECOVERED";}Flux.just(10).map(this::doSomethingDangerous).onErrorReturn("RECOVERED");此方法也可以利用断言来匹配是否是我们想要的异常,从而单独做处理:Flux.just(10).map(this::doSomethingDangerous).onErrorReturn(e->e.getMessage().equals("boom10"),"recovered10");fallback兜底捕获异常并执行另一个方法Stringv1;try{v1=callExternalService("key1");}catch(Throwableerror){v1=getFromCache("key1");}Stringv2;try{v2=callExternalService("key2");}catch(Throwableerror){v2=getFromCache("key2");}Flux.just("key1","key2").flatMap(k->callExternalService(k).onErrorResume(e->getFromCache(k)));DynamicFallback动态兜底捕获异常并返回另一个方法的结果try{Valuev=erroringMethod();returnMyWrapper.fromValue(v);}catch(Throwableerror){returnMyWrapper.fromError(error);}erroringFlux.onErrorResume(error->Mono.just(MyWrapper.fromError(error)));throw自定义异常try{returncallExternalService(k);}catch(Throwableerror){thrownewBusinessException("oops,SLAexceeded",error);}Flux.just("timeout1").flatMap(k->callExternalService(k)).onErrorResume(original->Flux.error(newBusinessException("oops,SLAexceeded",original)));或者Flux.just("timeout1").flatMap(k->callExternalService(k)).onErrorMap(original->newBusinessException("oops,SLAexceeded",original));trycatchfinallyFlux.just(1,0,3).map(item->1/item).doOnError(err->System.out.println("err="+err)).doFinally(signalType->System.out.println("signalType="+signalType)).subscribe(v->System.out.println("v="+v));运行结果:v=1err=java.lang.ArithmeticException:/byzero2024-01-15T20:31:14.406+08:00ERROR20864---[main]reactor.core.publisher.Operators:OperatorcalleddefaultonErrorDroppedreactor.core.Exceptions$ErrorCallbackNotImplemented:java.lang.ArithmeticException:/byzeroCausedby:java.lang.ArithmeticException:/byzero跳过异常继续执行Flux.just(1,0,3).map(item->1/item).onErrorContinue((e,v)->{System.out.println("onErrorContinueerror="+e);System.out.println("onErrorContinuevalue="+v);}).subscribe(v->System.out.println("v="+v),e->System.out.println("e="+e));/**v=1onErrorContinueerror=java.lang.ArithmeticException:/byzeroonErrorContinuevalue=0v=0*/异常后正常结束Flux.just(1,0,3).map(item->1/item).onErrorComplete().subscribe(v->System.out.println("v="+v),e->System.out.println("e="+e),()->System.out.println("完成"));/**v=1完成*/

    Java
    17
    0
    2024-01-15 20:46:49
  • Java坐标工具类

    gis开发中关于坐标的常用工具类,包含但不限于墨卡托转经纬度(思极)、获取面的凸包算法已经凸包算法边角圆滑操作墨卡托转经纬度staticdoubleM_PI=Math.PI;//墨卡托转经纬度publicstaticdouble[]Mercator2lonLat(doublemercatorX,doublemercatorY){double[]xy=newdouble[2];doublex=mercatorX/20037508.34*180;doubley=mercatorY/20037508.34*180;y=180/M_PI*(2*Math.atan(Math.exp(y*M_PI/180))-M_PI/2);xy[0]=x;xy[1]=y;returnxy;}凸包算法工具类importjava.math.BigDecimal;importjava.util.*;importjava.util.stream.Collectors;/***通过经纬度计算凸包坐标,并返回经纬度坐标集**@authorleolinz*@date2022/9/13*/publicclassConvexHullUtil{/***计算凸包坐标*@parampointList*@return经纬度集合*/publicstaticList<Double[]>getConvexHull(List<Pair<Double[],Double[]>>pointList){pointList=newArrayList<>(newHashSet<>(pointList));returngetConvexHullByAndrew(pointList);//returngetConvexHullByGraham(pointList);}/***Andrew算法计算凸包*@parampointList*@return*/privatestaticList<Double[]>getConvexHullByAndrew(List<Pair<Double[],Double[]>>pointList){List<Double[]>result=newArrayList<>();//按照x大小进行排序,如果x相同,则按照y的大小进行排序pointList=pointList.stream().sorted((o1,o2)->{Double[]o1Target=o1.getTarget();Double[]o2Target=o2.getTarget();intcmp=BigDecimalUtil.compareTo(o1Target[0],o2Target[0]);if(cmp==0){returnBigDecimalUtil.compareTo(o1Target[1],o2Target[1]);}returncmp;}).collect(Collectors.toList());intn=pointList.size();if(n<4){for(Pair<Double[],Double[]>pair:pointList){result.add(pair.getSource());}returnresult;}List<Integer>hull=newArrayList<>();boolean[]visited=newboolean[n];//hull[0]需要入栈两次,不需要标记hull.add(0);//计算下半壳for(inti=1;i<n;i++){while(hull.size()>1&&cross(pointList.get(hull.get(hull.size()-2)).getTarget(),pointList.get(hull.get(hull.size()-1)).getTarget(),pointList.get(i).getTarget())<0){//凸包夹角小于0需要弹出上一个坐标visited[hull.get(hull.size()-1)]=false;hull.remove(hull.size()-1);}//新坐标在凸包上visited[i]=true;hull.add(i);}//下半壳大小intlowerHullSize=hull.size();//计算上半壳for(inti=n-2;i>=0;i--){if(visited[i])continue;while(hull.size()>lowerHullSize&&cross(pointList.get(hull.get(hull.size()-2)).getTarget(),pointList.get(hull.get(hull.size()-1)).getTarget(),pointList.get(i).getTarget())<0){visited[hull.get(hull.size()-1)]=false;hull.remove(hull.size()-1);}visited[i]=true;hull.add(i);}//hull[0]同时参与凸包的上半部分检测,因此需去掉重复的hull[0]hull.remove(hull.size()-1);for(Integerindex:hull)result.add(pointList.get(index).getSource());returnresult;}/***Graham算法计算凸包*@parampointList*@return*/privatestaticList<Double[]>getConvexHullByGraham(List<Pair<Double[],Double[]>>pointList){List<Double[]>result=newArrayList<>();intn=pointList.size();if(n<4){for(Pair<Double[],Double[]>pair:pointList){result.add(pair.getSource());}returnresult;}intbottom=0;/*找到y最小的点bottom*/for(inti=0;i<n;i++){if(BigDecimalUtil.compareTo(pointList.get(i).getTarget()[1],pointList.get(bottom).getTarget()[1])<0)bottom=i;}swap(pointList,0,bottom);/*以bottom原点,按照极坐标的角度大小进行排序*/Double[]originCoor=pointList.get(0).getTarget();pointList=pointList.stream().sorted((o1,o2)->{intdiff=cross(originCoor,o1.getTarget(),o2.getTarget());if(diff==0)returnBigDecimalUtil.compareTo(distance(originCoor,o1.getTarget()),distance(originCoor,o2.getTarget()));elsereturn-diff;}).collect(Collectors.toList());/*对于凸包最后且在同一条直线的元素按照距离从大到小进行排序*/intr=n-1;while(r>=0&&cross(originCoor,pointList.get(n-1).getTarget(),pointList.get(r).getTarget())==0){r--;}for(intl=r+1,h=n-1;l<h;l++,h--){swap(pointList,l,h);}Deque<Integer>stack=newArrayDeque<Integer>();stack.push(0);stack.push(1);for(inti=2;i<n;i++){inttop=stack.pop();/*如果当前元素与栈顶的两个元素构成的向量顺时针旋转,则弹出栈顶元素*/while(!stack.isEmpty()&&cross(pointList.get(stack.peek()).getTarget(),pointList.get(top).getTarget(),pointList.get(i).getTarget())<0){top=stack.pop();}stack.push(top);stack.push(i);}while(!stack.isEmpty()){result.add(pointList.get(stack.pop()).getSource());}returnresult;}privatestaticintcross(Double[]p,Double[]q,Double[]r){BigDecimalpx=BigDecimal.valueOf(p[0]);BigDecimalpy=BigDecimal.valueOf(p[1]);BigDecimalqx=BigDecimal.valueOf(q[0]);BigDecimalqy=BigDecimal.valueOf(q[1]);BigDecimalrx=BigDecimal.valueOf(r[0]);BigDecimalry=BigDecimal.valueOf(r[1]);/*(q[0]-p[0])*(r[1]-q[1])-(q[1]-p[1])*(r[0]-q[0])*/returnqx.subtract(px).multiply(ry.subtract(qy)).subtract(qy.subtract(py).multiply(rx.subtract(qx))).compareTo(BigDecimal.ZERO);}privatestaticdoubledistance(Double[]p,Double[]q){BigDecimalpx=BigDecimal.valueOf(p[0]);BigDecimalpy=BigDecimal.valueOf(p[1]);BigDecimalqx=BigDecimal.valueOf(q[0]);BigDecimalqy=BigDecimal.valueOf(q[1]);/*(p[0]-q[0])*(p[0]-q[0])+(p[1]-q[1])*(p[1]-q[1])*/returnpx.subtract(qx).multiply(px.subtract(qx)).add(py.subtract(qy).multiply(py.subtract(qy))).doubleValue();}privatestaticvoidswap(List<Pair<Double[],Double[]>>pointList,inti,intj){Pair<Double[],Double[]>pair=pointList.get(i);Pair<Double[],Double[]>set=pointList.set(j,pair);pointList.set(i,set);}publicstaticPair<Double[],Double[]>getPair(Double[]source,Double[]target){returnnewPair<>(source,target);}/***pair类,用于一对一匹配*@param<T>经纬度坐标*@param<R>米勒坐标*/publicstaticclassPair<T,R>{privateTsource;privateRtarget;publicPair(Tsource,Rtarget){this.source=source;this.target=target;}publicTgetSource(){returnsource;}publicRgetTarget(){returntarget;}@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;if(o==null||getClass()!=o.getClass())returnfalse;Pair<?,?>pair=(Pair<?,?>)o;if(pair.getTarget()instanceofDouble[]&&targetinstanceofDouble[]){Double[]pairTarget=(Double[])pair.getTarget();Double[]obj=(Double[])target;returnBigDecimalUtil.compareTo(pairTarget[0],obj[0])==0&&BigDecimalUtil.compareTo(pairTarget[1],obj[1])==0;}returnObjects.equals(source,pair.source)&&Objects.equals(target,pair.target);}@OverridepublicinthashCode(){if(targetinstanceofDouble[]){Double[]obj=(Double[])target;returnobj[0].hashCode()^obj[1].hashCode();}returnObjects.hash(source,target);}}}上方的BigDecimalUtil类importjava.math.BigDecimal;importjava.math.MathContext;importjava.math.RoundingMode;/***BigDecimal计算的工具类*@authorleolinzc*/publicclassBigDecimalUtil{/***默认除法运算精度*/privatestaticfinalintDEF_DIV_SCALE=15;/***提供精确的加法运算。**@paramv1被加数*@paramv2加数*@return两个参数的和*/publicstaticdoubleadd(doublev1,doublev2){BigDecimalb1=BigDecimal.valueOf(v1);BigDecimalb2=BigDecimal.valueOf(v2);returnb1.add(b2).doubleValue();}/***提供精确的减法运算。**@paramv1被减数*@paramv2减数*@return两个参数的差*/publicstaticdoublesubtract(doublev1,doublev2){BigDecimalb1=BigDecimal.valueOf(v1);BigDecimalb2=BigDecimal.valueOf(v2);returnb1.subtract(b2).doubleValue();}/***提供精确的乘法运算。**@paramv1被乘数*@paramv2乘数*@return两个参数的积*/publicstaticdoublemultiply(doublev1,doublev2){BigDecimalb1=BigDecimal.valueOf(v1);BigDecimalb2=BigDecimal.valueOf(v2);returnb1.multiply(b2).doubleValue();}/***提供(相对)精确的除法运算,当发生除不尽的情况时,精确到*小数点以后10位,以后的数字四舍五入。**@paramv1被除数*@paramv2除数*@return两个参数的商*/publicstaticdoubledivide(doublev1,doublev2){returndivide(v1,v2,DEF_DIV_SCALE);}/***提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指*定精度,以后的数字四舍五入。**@paramv1被除数*@paramv2除数*@paramscale表示表示需要精确到小数点以后几位。*@return两个参数的商*/publicstaticdoubledivide(doublev1,doublev2,intscale){if(scale<0){thrownewIllegalArgumentException("Thescalemustbeapositiveintegerorzero");}BigDecimalb1=BigDecimal.valueOf(v1);BigDecimalb2=BigDecimal.valueOf(v2);returnb1.divide(b2,scale,RoundingMode.HALF_UP).doubleValue();}/***提供精确的小数位四舍五入处理。**@paramv需要四舍五入的数字*@paramscale小数点后保留几位*@return四舍五入后的结果*/publicstaticdoubleround(doublev,intscale){if(scale<0){thrownewIllegalArgumentException("Thescalemustbeapositiveintegerorzero");}BigDecimalb=BigDecimal.valueOf(v);BigDecimalone=newBigDecimal("1");returnb.divide(one,scale,RoundingMode.HALF_UP).doubleValue();}publicstaticdoublesqrt(doublevalue){returnsqrt(BigDecimal.valueOf(value),DEF_DIV_SCALE);}publicstaticdoublesqrt(doublevalue,intscale){returnsqrt(BigDecimal.valueOf(value),scale);}/****@paramvalue*@paramscale*@return*/publicstaticdoublesqrt(BigDecimalvalue,intscale){BigDecimalnum_2=BigDecimal.valueOf(2);intprecision=50;MathContextmc=newMathContext(precision,RoundingMode.HALF_UP);BigDecimaldeviation=value;intcnt=0;while(cnt<precision){deviation=(deviation.add(value.divide(deviation,mc))).divide(num_2,mc);cnt++;}deviation=deviation.setScale(scale,BigDecimal.ROUND_HALF_UP);returndeviation.doubleValue();}publicstaticvoidmain(String[]args){doublesqrt=sqrt(100,15);System.out.println("sqrt="+sqrt);}/***提供精确的类型转换(Float)**@paramv需要被转换的数字*@return返回转换结果*/publicstaticfloatconvertToFloat(doublev){BigDecimalb=newBigDecimal(v);returnb.floatValue();}publicstaticDoubleconvertsToDouble(Objecto){if(o==null)returnnull;if(oinstanceofString)returnDouble.valueOf((String)o);if(oinstanceofDouble)return(Double)o;returnnull;}/***提供精确的类型转换(Int)不进行四舍五入**@paramv需要被转换的数字*@return返回转换结果*/publicstaticintconvertsToInt(doublev){BigDecimalb=newBigDecimal(v);returnb.intValue();}/***提供精确的类型转换(Long)**@paramv需要被转换的数字*@return返回转换结果*/publicstaticlongconvertsToLong(doublev){BigDecimalb=newBigDecimal(v);returnb.longValue();}/***返回两个数中大的一个值**@paramv1需要被对比的第一个数*@paramv2需要被对比的第二个数*@return返回两个数中大的一个值*/publicstaticdoublereturnMax(doublev1,doublev2){BigDecimalb1=newBigDecimal(v1);BigDecimalb2=newBigDecimal(v2);returnb1.max(b2).doubleValue();}/***返回两个数中小的一个值**@paramv1需要被对比的第一个数*@paramv2需要被对比的第二个数*@return返回两个数中小的一个值*/publicstaticdoublereturnMin(doublev1,doublev2){BigDecimalb1=newBigDecimal(v1);BigDecimalb2=newBigDecimal(v2);returnb1.min(b2).doubleValue();}/***精确对比两个数字**@paramv1需要被对比的第一个数*@paramv2需要被对比的第二个数*@return如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1*/publicstaticintcompareTo(doublev1,doublev2){BigDecimalb1=BigDecimal.valueOf(v1);BigDecimalb2=BigDecimal.valueOf(v2);returnb1.compareTo(b2);}}米勒坐标工具类/***米勒坐标工具类*@authorunlockz*@date2022/8/22*/publicclassMillerUtil{/***地球周长*/privatestaticfinaldoubleL=6381372*Math.PI*2;/***平面展开后,x轴等于周长*/privatestaticfinaldoubleW=L;/***y轴约等于周长一半*/privatestaticfinaldoubleH=L/2;/***米勒投影中的一个常数,范围大约在正负2.3之间*/privatestaticfinaldoubleMILL=2.3;/***经纬度转换为miller平面坐标*@paramlongitude*@paramlatitude*@return坐标轴的x,y*/publicstaticDouble[]lonLat2Miller(doublelongitude,doublelatitude){//将经度从度数转换为弧度doublex=longitude*Math.PI/180;//将纬度从度数转换为弧度doubley=latitude*Math.PI/180;//米勒投影的转换y=1.25*Math.log(Math.tan(0.25*Math.PI+0.4*y));//弧度转为实际距离x=(W/2)+(W/(2*Math.PI))*x;y=(H/2)-(H/(2*MILL))*y;returnnewDouble[]{x,y};}publicstaticDouble[]miller2LonLat(doublex,doubley){doublelon;lon=(x-W/2)*360/W;doublelat;lat=((H/2-y)*2*MILL)/(1.25*H);lat=((Math.atan(Math.exp(lat))-0.25*Math.PI)*180)/(0.4*Math.PI);returnnewDouble[]{lon,lat};}publicstaticDoublemiller2Lon(doublex){doublelon;lon=(x-W/2)*360/W;returnlon;}publicstaticDoublemiller2Lat(doubley){doublelat;lat=((H/2-y)*2*MILL)/(1.25*H);lat=((Math.atan(Math.exp(lat))-0.25*Math.PI)*180)/(0.4*Math.PI);returnlat;}/***计算平面上两点之间的距离*@parama*@paramb*@return*/publicstaticdoublegetDistance(Double[]a,Double[]b){doublediff_x=BigDecimalUtil.subtract(a[0],b[0]);doublediff_y=BigDecimalUtil.subtract(a[1],b[1]);doublediff_x_2=BigDecimalUtil.multiply(diff_x,diff_x);doublediff_y_2=BigDecimalUtil.multiply(diff_y,diff_y);doubledistance=BigDecimalUtil.sqrt(BigDecimalUtil.add(diff_x_2,diff_y_2));returndistance;}}凸包算法使用//面的数据,这里是二维数组,而非三维数组!!!List<Double[]>polygon=newArrayList<Double[]>(){{add(newDouble[]{122.1234332,31.234312});add(newDouble[]{122.1765454332,31.2346312});add(newDouble[]{122.435234332,31.765312});add(newDouble[]{122.23434332,31.312});}};List<ConvexHullUtil.Pair<Double[],Double[]>>pointList=newArrayList<>(polygon.size());doublelon,lat;for(Double[]coordinates:polygon){lon=coordinates[0];lat=coordinates[1];pointList.add(ConvexHullUtil.getPair(newDouble[]{lon,lat},MillerUtil.lonLat2Miller(lon,lat)));}//凸包算法之后的坐标List<Double[]>convexHull=ConvexHullUtil.getConvexHull(pointList);凸包算法夹角平滑化maven依赖<dependency><groupId>org.locationtech.jts</groupId><artifactId>jts-core</artifactId><version>1.17.1</version></dependency>方法实现//由于上方凸包算法后的面是一个二维数组,不是一个GeoJson的Polygon,//需要将坐标再转三维数组com.alibaba.fastjson.JSONArrayarrar=newcom.alibaba.fastjson.JSONArray();Coordinate[]coordinates=newCoordinate[convexHull.size()+1];for(inti=0;i<convexHull.size();i++){Double[]doubles=convexHull.get(i);coordinates[i]=newCoordinate(doubles[0],doubles[1]);}coordinates[coordinates.length-1]=coordinates[0];//要形成首位闭合的多边形区域,否则会报错GeometryFactorygeometryFactory=newGeometryFactory();LinearRinglinearRing=geometryFactory.createLinearRing(coordinates);Polygonpolygon1=geometryFactory.createPolygon(linearRing,null);//执行缓冲区分析,这里设置缓冲区距离为0.001个单位(根据您的数据需要调整)BufferParametersbufferParameters=newBufferParameters();bufferParameters.setEndCapStyle(BufferParameters.CAP_ROUND);GeometrybufferedGeometry=BufferOp.bufferOp(polygon1,0.001,bufferParameters);//获取缓冲区的顶点坐标Coordinate[]bufferedCoordinates=bufferedGeometry.getCoordinates();//将坐标转换回三维数组形式List<List<Double>>outerRing=newArrayList<>();for(Coordinatecoordinate:bufferedCoordinates){List<Double>point=newArrayList<>();point.add(coordinate.getX());point.add(coordinate.getY());outerRing.add(point);}arrar.add(outerRing);returnarrar;

    Java
    12
    1
    2024-01-12 00:28:43
  • SpringBoot打包瘦身

    背景现有的应用开发中,不论是单体架构、微服务架构,如果项目采用的是springboot、springcLoud来作为底层架,打包时最终都会以iar包的方式打包、部署。这是就会面临一个问题,就是jar包非常大,单体应用还好,但是如果是微服务就非常痛苦,几十个微服务就要拆分打包几十个iar包,每个jar包都很大(几百M),合起来就好几个GB,非常占用空间。如果是内网部署,遇到动辄CB的升级包还能勉强接受,最多就是运维心里默默地吐槽一下,但是如果在外网云环境、或者客户现场,那令人痛不欲生的带宽加上各种网络转换限制,运维心里各种*&……%¥#。因为这个你的领导又给你各种批头盖脸一顿。你觉得很委屈,确实不是你的代码问题,但是这确实是我们该考虑的。因此呢,基于以上原因我们要为项目进行瘦身!如何瘦身瘦身思路实际上瘦身并不是减少jar包的体积,而是将代码和依赖分离开来。项目成型后,所引用的依赖一般不会发生太大的改动,没有必要每次打包时都将同样的依赖放到一起。完全可以将依赖单独抽取成一个文件夹,放到服务器上,然后纯代码的jar包启动时,指定依赖文件夹所在的位置即可。创建应用,打包查看创建应用创建一个SpringBoot的单体应用,引入相关的依赖。打包部署打包之后,看目前的jar包体积为20.5M修改pom修改pom,将jar包里的依赖提取到外部。*我们需要添加一个org.apache.maven.plugins的插件,用于抽取外部依赖。*需要修改spring-boot-maven-plugin,去除打包的依赖。<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><mainClass>com.example.javademojdk17.JavaDemoJdk17Application</mainClass><!--必须,切换layout为ZIP模式,确保项目启动成功--><layout>ZIP</layout><includes><!--必须,这里填写的是打包是需要包含进去的依赖。比如聚合项目引用一些common、utils等自定义的模块,如果经常改动代码,建议放在此处。如果没有则non-exists,表示不打包依赖--><include><groupId>non-exists</groupId><artifactId>non-exists</artifactId></include></includes></configuration><!--必填--><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin><!--必填,此插件用于抽取依赖包--><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.1.1</version><executions><execution><id>copy-dependencies</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals><configuration><outputDirectory>${project.build.directory}/lib</outputDirectory><!--是否排除传递性--><excludeTransitive>false</excludeTransitive><!--是否去除jar包版本信息--><stripVersion>false</stripVersion><!--包含范围--><includeScope>runtime</includeScope></configuration></execution></executions></plugin></plugins></build>验证此时再重新打包,返现jar包体积只有200多K,并且同级目录下多了一个lib文件夹。启动需要用-Dloader.path指定依赖的位置。java-jar-Dloader.path=./libjava-demo-jdk17-0.0.1-SNAPSHOT.jar

    Spring系列
    19
    1
    2024-01-05 18:07:33
  • flex 布局最后一行左对齐

    一、justify-content对齐问题描述在CSSflex布局中,justify-content属性可以控制列表的水平对齐方式,例如space-between值可以实现两端对齐。但是,如果最后一行的列表的个数不满,则就会出现最后一行没有完全垂直对齐的问题。如下代码:.container{display:flex;justify-content:space-between;flex-wrap:wrap;}.list{width:24%;height:100px;background-color:skyblue;margin-top:15px;}然后列表的个数不多不少正好7个:<divclass="container"><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div></div>此时最后一行的小方块的排列就显得很尴尬了:此时,最后一行应该左对齐排列才是我们想要的效果,如何实现呢?其实实现的思路和display:inline-block的两端对齐是一样的。二、如果每一行列数是固定的如果每一行列数是固定的,则下面两种方法可以实现最后一行左对齐。方法一:模拟space-between和间隙也就是我们不使用justify-content:space-between声明在模拟两端对齐效果。中间的gap间隙我们使用margin进行控制。例如:.container{display:flex;flex-wrap:wrap;}.list{width:24%;height:100px;background-color:skyblue;margin-top:15px;}.list:not(:nth-child(4n)){margin-right:calc(4%/3);}此时,布局效果是这样的:方法二:根据个数最后一个元素动态margin由于每一列的数目都是固定的,因此,我们可以计算出不同个数列表应当多大的margin值才能保证完全左对齐。例如,假设每行4个元素,结果最后一行只有3个元素,则最后一个元素的margin-right大小是“列表宽度+间隙大小”的话,那最后3个元素也是可以完美左对齐的。然后,借助树结构伪类数量匹配技术(这篇文章“伪类匹配列表数目实现微信群头像CSS布局的技巧”中的布局技巧就是借助这种技术实现),我们可以知道最后一行有几个元素。例如:.list:last-child:nth-child(4n-1)说明最后一行,要么3个元素,要么7个元素…….list:last-child:nth-child(4n-2)说明最后一行,要么2个元素,要么6个元素……在本例中,一行就4个元素,因此,我们可以有如下CSS设置:.container{display:flex;/*两端对齐*/justify-content:space-between;flex-wrap:wrap;}.list{width:24%;height:100px;background-color:skyblue;margin-top:15px;}/*如果最后一行是3个元素*/.list:last-child:nth-child(4n-1){margin-right:calc(24%+4%/3);}/*如果最后一行是2个元素*/.list:last-child:nth-child(4n-2){margin-right:calc(48%+8%/3);}效果如下GIF示意,删除列表后,布局依然稳稳地左对齐。三、如果每一子项宽度不固定有时候,每一个flex子项的宽度都是不固定的,这个时候希望最后一行左对齐该如何实现呢?由于此时间隙的大小不固定,对齐不严格,因此,我们可以直接让最后一行左对齐即可。具体方法有两个:方法一:最后一项margin-right:autoCSS代码如下:.container{display:flex;justify-content:space-between;flex-wrap:wrap;}.list{background-color:skyblue;margin:10px;}/*最后一项margin-right:auto*/.list:last-child{margin-right:auto;}方法二:创建伪元素并设置flex:auto或flex:1CSS代码如下:.container{display:flex;justify-content:space-between;flex-wrap:wrap;}.list{background-color:skyblue;margin:10px;}/*使用伪元素辅助左对齐*/.container::after{content:'';flex:auto;/*或者flex:1*/}最终效果如下GIF:四、如果每一行列数不固定如果每一行的列数不固定,则上面的这些方法均不适用,需要使用其他技巧来实现最后一行左对齐。这个方法其实很简单,也很好理解,就是使用足够的空白标签进行填充占位,具体的占位数量是由最多列数的个数决定的,例如这个布局最多7列,那我们可以使用7个空白标签进行填充占位,最多10列,那我们需要使用10个空白标签。如下HTML示意:<divclass="container"><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><i></i><i></i><i></i><i></i><i></i></div>相关CSS如下,实现的关键就是占位的元素宽度和margin大小设置得和.list列表元素一样即可,其他样式都不需要写。.container{display:flex;justify-content:space-between;flex-wrap:wrap;margin-right:-10px;}.list{width:100px;height:100px;background-color:skyblue;margin:15px10px00;}/*和列表一样的宽度和margin值*/.container>i{width:100px;margin-right:10px;}由于<i>元素高度为0,因此,并不会影响垂直方向上的布局呈现。最后的效果如下GIF图示:五、如果列数不固定HTML又不能调整然而有时候,由于客观原因,前端重构人员没有办法去调整html结构,同时布局的列表个数又不固定,这个时候该如何实现我们最后一行左对齐效果呢?我们不妨可以试试使用Grid布局。Grid布局天然有gap间隙,且天然格子对齐排布,因此,实现最后一行左对齐可以认为是天生的效果。CSS代码如下:.container{display:grid;justify-content:space-between;grid-template-columns:repeat(auto-fill,100px);grid-gap:10px;}.list{width:100px;height:100px;background-color:skyblue;margin-top:5px;}可以看到CSS代码非常简洁。HTML代码就是非常规整非常普通的代码片段:<divclass="container"><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div><divclass="list"></div></div>六、这几种实现方法点评首先最后一行需要左对齐的布局更适合使用CSSgrid布局实现,但是,repeat()函数兼容性有些要求,IE浏览器并不支持。如果项目需要兼容IE,则此方法需要斟酌。然后,适用范围最广的方法是使用空的元素进行占位,此方法不仅适用于列表个数不固定的场景,对于列表个数固定的场景也可以使用这个方法。但是有些人代码洁癖,看不惯这种空的占位的html标签,则可以试试一开始的两个方法,一是动态计算margin,模拟两端对齐,另外一个是根据列表的个数,动态控制最后一个列表元素的margin值实现左对齐。累计6种方法,各有各的优缺点,大家根据自己项目的实际场景,选择合适的方法。*本文参考web前端开发公众号*

    HTML&CSS
    40
    0
    2023-03-02 01:28:28
  • nginx静态资源合并

    浏览器在向服务端发起请求时,同时发起的数量是有限制的。如果页面上有大量请求,那么会进行请求排队,对于一些复杂的网站(大量css或js),或者类似秒杀类的高并发网站,不应该把时间浪费在静态资源上,要保证核心业务数据及时的响应,所以我们应该对网站静态数据进行优化。nginx-http-concat静态资源请求优化有很多种,比如客户端缓存,cdn分发等,本文主要聊一下nginx-http-concat。nginx-http-concat可以进行静态资源请求合并,即一次http请求加载多个静态文件。它是由阿里团队基于nginx开发的一个模块,文档可参考其github:https://github.com/alibaba/nginx-http-concat本文主要聊聊如何基于原生nginx和Docker使用nginx-http-concat。原生Nginx安装nginx1、安装必要依赖yuminstall-ygcc-c++pcrepcre-develzlibzlib-developensslopenssl-devel2、下载nginxwgethttp://nginx.org/download/nginx-1.18.0.tar.gz如果提示:-bash:wget:未找到命令则需要安装wget:yum-yinstallwget3、解压nginx压缩包tarzxvfnginx-1.18.0.tar.gz4、进入解压文件夹cdnginx-1.18.05、安装到指定位置./configure--prefix=/usr/local/nginx如果提示:checkingforOS+Linux3.10.0-1160.el7.x86_64x86_64checkingforCcompiler...notfound./configure:error:Ccompilerccisnotfound那么需要安装编译工具,然后再执行上面指令:yum-yinstallgccgcc-c++autoconfautomakemake6、编译并且安装make&&makeinstall7、启动nginx进入/usr/local/nginx/sbin:./nginx8、开放80端口firewall-cmd--zone=public--add-port=80/tcp--permanent9、重启防火墙firewall-cmd--reload10、访问验证在浏览器输入nginx所在服务器的地址出现上面页面,表示nginx已经安装配置成功。配置nginx-http-concat1、关闭nginx服务进入/usr/local/nginx/sbin目录./nginx-sstop2、下载nginx-http-concat压缩包在其github上,下载zip:3、上传服务器将下载的压缩包上传到服务器,这里就上传到/usr/local目录下,也可以指定其他目录。4、解压unzipnginx-http-concat-master.zip如果提示:-bash:unzip:未找到命令则需要安装unzip后,再进行上面指令:yuminstallunzip-y5、添加模块在nginx的解压目录下执行:注意,是nginx的解压目录(即【安装nginx】的第4步目录),而不是安装目录./configure--prefix=/usr/local/nginx--add-module=/usr/local/nginx-http-concat-master6、重新编译make7、验证进入objs目录./nginx-V如果出现configurearguments:–prefix=/usr/local/nginx–add-module=/usr/local/nginx-http-concat-master代表添加模块成功8、替换nginx脚本将objs目录中的nginx脚本,替换掉安装目录下sbin中的脚本cdobjscpnginx/usr/local/nginx/sbincp:是否覆盖“/usr/local/nginx/sbin/nginx”?y9、修改nginx.conf修改nginx.conf,让其支持静态资源合并vi/usr/local/nginx/conf/nginx.conf10、启动nginxcd/usr/local/nginx/sbin/./nginx11、编写css写两个css文件cd/usr/local/nginx/htmlvia.cssbody{background-color:#ccc;}vib.cssbody{color:red;}12、访问http://192.168.2.3/??a.css,b.css注意访问格式,需要两个??,并且多个资源之间用英文,隔开。Docker安装concat我们希望在创建容器之后,默认自带nginx-http-concat模块,所以需要自定义nginx的镜像。根据上面原生nginx的配置方式,对应的编写Dockerfile即可。Dockerfile1、创建目录在宿主机创建/mydocker/nginx目录来管理nginx容器相关的数据mkdir-p/mydocker/nginx2、创建Dockerfile文件创建Dockerfile文件,并写入以下内容:FROMcentos:7#安装依赖RUNyuminstall-ygcc-c++pcrepcre-develzlibzlib-developensslopenssl-devel&&\yuminstall-ywgetunzip&&\yuminstall-ygccgcc-c++autoconfautomakemakeRUNcd/usr/local&&\wgethttp://nginx.org/download/nginx-1.18.0.tar.gz&&\wgethttps://codeload.github.com/alibaba/nginx-http-concat/zip/refs/heads/master&&\unzipmaster&&\tarzxvfnginx-1.18.0.tar.gz&&\cdnginx-1.18.0&&\./configure--prefix=/usr/local/nginx--add-module=/usr/local/nginx-http-concat-master&&\make&&makeinstall&&\cd..&&\rm-rfmasternginx-1.18.0.tar.gzENVPATH/usr/local/nginx/sbin:$PATHVOLUME/usr/local/nginx/htmlVOLUME/usr/local/nginx/confVOLUME/usr/local/nginx/logs#暴露Nginx的端口EXPOSE80#启动NginxCMD["nginx","-g","daemonoff;"]3、构建镜像dockerbuild-tmynginx:1.1.4、查看镜像[root@localhostnginx]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmynginx1.1e7187738af1f19minutesago572MB5、创建容器我们将nginx容器的配置文件挂在到宿主机上,所以先临时创建一个docker容器,将内容内部的配置文件拷贝到宿主机上。[root@localhostnginx]#dockerrun-p80:80-de718e170bf531bd72bf5b67df524e52b96ba57667974a0b978407f4f57eb0fc713546、拷贝配置文件在/mydocker/nginx目录下,创建conf和html目录,分别存放拷贝出来的配置文件和页面文件。mkdirconfmkdirhtmldockercpe170:/usr/local/nginx/conf/nginx.conf/mydocker/nginx/confdockercpe170:/usr/local/nginx/html/index.html/mydocker/nginx/html7、删除临时容器文件拷贝出来后,这个容器删除即可,后面会创建基于数据卷挂载的nginx容器。dockerstope170dockerrme1708、修改配置文件修改拷贝出来的配置文件,添加支持静态资源合并的配置。vi/mydocker/nginx/conf/nginx.conf9、创建css进入/mydocker/nginx/html目录,创建两个css文件,分别写入样式。cd/mydocker/nginx/htmlecho"body{background-color:#ccc;}">a.cssecho"body{color:red;}">b.css10、引入样式这里我们在index.html中引入样式。11、启动容器启动容器,并且挂载数据卷。dockerrun\>-v/mydocker/nginx/conf/nginx.conf:/usr/local/nginx/conf/nginx.conf\>-v/mydocker/nginx/html:/usr/local/nginx/html\>-p80:80-de71812、启动成功后,访问即可。

    中间件 / Linux / Docker
    37
    0
    2023-02-19 18:27:58
  • 基于 Netty + WebSocket 小案例

    需求实现基于netty和websocket的长连接及交互。1、客户端浏览器发送消息,服务器实时接受,并写回给客户端。2、客户端和服务器相互感知,比如服务端对客户端连接、关闭状态进行监听;客户端也能感知到服务端关闭。Server端importio.netty.bootstrap.ServerBootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelPipeline;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioServerSocketChannel;importio.netty.handler.codec.http.HttpObjectAggregator;importio.netty.handler.codec.http.HttpServerCodec;importio.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;importio.netty.handler.stream.ChunkedWriteHandler;/***服务器端*/publicclassMyServer{publicstaticvoidmain(String[]args){//创建boss循环事件组,用来关注accept事件NioEventLoopGroupbossEventLoopGroup=newNioEventLoopGroup(1);//创建work循环事件组,关注读写事件NioEventLoopGroupworkEventLoopGroup=newNioEventLoopGroup(8);try{//创建服务器实例ServerBootstrapserverBootstrap=newServerBootstrap().group(bossEventLoopGroup,workEventLoopGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer<SocketChannel>(){@OverrideprotectedvoidinitChannel(SocketChannelsocketChannel)throwsException{//因为基于http协议,使用http的编码和解码器ChannelPipelinepipeline=socketChannel.pipeline();pipeline.addLast(newHttpServerCodec());//防止文件或者码流传输过程中可能发生的内存溢出问题pipeline.addLast(newChunkedWriteHandler());//多段http数据聚合pipeline.addLast(newHttpObjectAggregator(8192));//将http协议升级为ws协议,保持长连接,对应前端请求的uripipeline.addLast(newWebSocketServerProtocolHandler("/hello"));//自定义处理handler,处理业务pipeline.addLast(newMyTextWebSocketFrameHandler());}});//启动服务器ChannelFuturechannelFuture=serverBootstrap.bind(8989).sync();System.out.println("服务器启动成功………………");//对关闭通道进行监听,防止服务直接关闭channelFuture.channel().closeFuture().sync();}catch(Exceptione){thrownewRuntimeException(e);}finally{//优雅关闭bossEventLoopGroup.shutdownGracefully();workEventLoopGroup.shutdownGracefully();}}}自定义消息处理器importio.netty.channel.ChannelHandlerContext;importio.netty.channel.SimpleChannelInboundHandler;importio.netty.handler.codec.http.websocketx.TextWebSocketFrame;importjava.time.LocalDateTime;/***处理文本协议数据,处理TextWebSocketFrame类型的数据,*websocket专门处理文本的frame就是TextWebSocketFrame*/publicclassMyTextWebSocketFrameHandlerextendsSimpleChannelInboundHandler<TextWebSocketFrame>{/***接受消息触发的方法*@paramctx保存channel上下文信息*@parammsgws数据帧*@throwsException*/@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,TextWebSocketFramemsg)throwsException{System.out.println("服务器收到消息"+msg.text());//回复消息,传递给客户端ctx.channel().writeAndFlush(newTextWebSocketFrame("服务器时间"+LocalDateTime.now()+""+msg.text()));}//当web客户端连接后,触发方法@OverridepublicvoidhandlerAdded(ChannelHandlerContextctx)throwsException{//id表示唯一的值,LongText是唯一的ShortText不是唯一System.out.println("客户端连接"+ctx.channel().id().asLongText());}//客户端断开触发的方法@OverridepublicvoidhandlerRemoved(ChannelHandlerContextctx)throwsException{System.out.println("客户端断开"+ctx.channel().id().asLongText());}//连接异常触发的方法@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{System.out.println("异常发生"+cause.getMessage());ctx.close();//关闭连接}}Client端<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>Title</title></head><body><script>varsocket;//判断当前浏览器是否支持websocketif(window.WebSocket){//goonsocket=newWebSocket("ws://localhost:8989/hello");//相当于channelReado,ev收到服务器端回送的消息socket.onmessage=function(ev){varrt=document.getElementById("responseText");rt.value=rt.value+"\n"+ev.data;}//相当于连接开启(感知到连接开启)socket.onopen=function(ev){varrt=document.getElementById("responseText");rt.value="连接开启了.."}//相当于连接关闭(感知到连接关闭)socket.onclose=function(ev){varrt=document.getElementById("responseText");rt.value=rt.value+"\n"+"连接关闭了.."}}else{alert("当前浏览器不支持websocket")}//发送消息到服务器functionsend(message){if(!window.socket){//先判断socket是否创建好return;}if(socket.readyState==WebSocket.OPEN){//通过socket发送消息socket.send(message)}else{alert("连接没有开启");}}</script><formonsubmit="returnfalse"><textareaname="message"style="height:300px;width:300px"></textarea><inputtype="button"value="发生消息"onclick="send(this.form.message.value)"><textareaid="responseText"style="height:300px;width:300px"></textarea><inputtype="button"value="清空内容"onclick="document.getElementById('responseText').value=''"></form></body></html>测试启动服务器启动浏览器启动浏览器,并且发送消息测试:监听客户端连接断开:

    网络编程 / Java
    39
    2
    2023-01-28 20:21:31
  • 记一次 SpringBoot + Docker 内部容器通信失败问题

    年底了,最近公司项目颇忙,又加上服务器到期了,没有续费(穷),所以重新购买了一个阿里的服务器,但因为域名是在腾讯云办理的,又需要将域名转到阿里旗下,前前后后的,耽误了不少时间,所以最近一直没有更新博客。(给懒找几个借口)。在之前服务器上,所有的服务都是原生部署的,考虑到以后会经常更换新的服务器,每次配置都需要花大把的时间,所以本次及以后就通过docker容器来部署项目,并通过dockercompose来管理容器,这样就方便多了。嗯,说干就干。docker-compose.yml通过百度+CV大法,啪,很快啊,就将需要的配置CV过来了。version:"3"services:#给SpringBoot起一个自定义的服务名myblog:#将来Dockerfile构建的镜像名及版本号image:myblog:1.1#指定容器名称container_name:myblog#开放端口ports:-"9090:9090"#配置容器连接的自定义网络,跟mysql、redis属于同一个网络networks:-app_net#容器启动时依赖于以下服务depends_on:-mysql-redis#mysql服务mysql:image:mysql:5.7ports:-"13306:3306"volumes:-/mydocker/mysql/data:/var/lib/mysql-/mydocker/mysql/conf.d:/etc/mysql/conf.denvironment:MYSQL_ROOT_PASSWORD:'root'networks:-app_net#redis服务redis:image:redis:6.0ports:-"16379:6379"volumes:-/mydocker/redis/data:/data-/mydocker/redis/redis.conf:/usr/local/etc/redis/redis.confnetworks:-app_netcommand:redis-server/usr/local/etc/redis/redis.conf#nginx服务nginx:image:nginx:1.22volumes:-/mydocker/nginx/nginx.conf:/etc/nginx/nginx.conf-/mydocker/nginx/default.conf:/etc/nginx/conf.d/default.conf-/mydocker/nginx/html:/usr/share/nginx/html-/usr/local/app:/usr/local/appports:-"80:80"networks:-app_netnetworks:app_net:上面将mysql的3306端口映射到13306,redis的6379映射到16379的,就是因为端口映射,导致后面找了半天的BUG……application.yml使用docker-compose的好处之一就是在项目的配置文件中,连接第三方服务的ip不用固定写死了,直接通过docker服务名来连接,这样即使以后容器更换了ip,我们也无需再去改动项目配置了,nice。又是一顿CV。spring:datasource:type:com.alibaba.druid.pool.DruidDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://mysql/blog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTCusername:rootpassword:rootredis:host:redisport:16379上面mysql和redis的host地址不再使用固定ip了,这就很香。部署接下来的步骤就很简单了,CV大法编写Dockerfile,打jar包,上传服务器,builddockerimage,开放端口,最后通过docker-composeup一键启动,看日志:nice!!启动成功,测一下mysql和redis的连接情况,通过navicat和RedisDesktopManager分别连接一下,发现世间最美的单词successful,nice,激动的心,颤抖的手,眼看即将成功了,最后一步,访问项目:嗯?报错了,连不上端口16379的redis服务,小问题,不就是连接不上嘛,查配置呗。排查查docker-compose.yml,是不是配置错误了,容器卷挂错了,检查一通,没问题。查防火墙,16379端口也已开放,已重启,没问题。查redis.conf,是不是配置有问题,但是RedisDesktopManager都连接成功了,没问题。查application.yml,是不是写的有问题,发现端口啥的用的也是映射后的端口,貌似也没问题。查dockernetworkinspect,是不是redis没有连接到自定义网络,也没有问题。什么情况,而且经过测试,项目连mysql可以,但唯独redis连接不上,嘿!奇了怪了。查百度,谷歌,也没有搜索相关的情况。不要灰心,百度不到,问群友,毕竟加了那么多技术扯皮群,群里个个都是人才,说话又好听,超爱那里的。把相关问题描述,问题截图往群里一扔,虽然是大半夜,但还是有群友给出了回复:因为项目里用的是jedis的连接池,所以我又把jedis改回来原来的lettuce,重新打包构建启动,看日志,嘿,不管用:然后群友又回复了,会不会java启的时候,redis还没起来,但是docker-compose.yml中已经配置了depends_on,所以这个情况应该也不存在。还是未能解决。询问未果,最终还是在网上找到了一篇帖子,原来在docker网络中运行所有内容(redis、replica和spring)时,应该使用端口6379而不是映射端口隧将项目的application.yml中redis的端口,从16379改回原6379,再打包构建测试,nice,终于可以了。总结docker内部容器通信时,应该使用默认端口。如果外部服务通信,需要使用映射的端口。以上结论来自个人测试,如有问题,欢迎指出。

    Docker / BUG集
    45
    3
    2023-01-16 02:46:33
  • docker-compose(二):SpringBoot + Docker Compose

    在上篇docker-compose(一):SpringBoot+Docker案例一文中,通过一个简单的SpringBoot项目整合了基于容器运行的mysql和redis,并通过nginx做了接口的代理转发。在运维方面,其实是存在几个痛点的:容器启动的先后顺序要求固定,SpringBoot容器启动一定要在其所依赖的容器启动完成后,方可启动。多个容器,需要执行多条run命令。作为一个整体项目,各个容器之间没有被统一编排,统一管理。针对以上问题,我们可以使用Docker官方的开源项目dockercompose来实现对Docker容器集群的快速编排。简介Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排。从功能上看,跟OpenStack中的Heat十分类似。其代码目前在https://github.com/docker/compose上开源。Compose定位是「定义和运行多个Docker容器的应用(Definingandrunningmulti-containerDockerapplications)」,其前身是开源项目Fig。我们知道使用一个Dockerfile模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个Web项目,除了Web服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。Compose恰好满足了这样的需求。它允许用户通过一个单独的docker-compose.yml模板文件(YAML格式)来定义一组相关联的应用容器为一个项目(project)。Compose中有两个重要的概念:*服务(service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。*项目(project):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml文件中定义。Compose的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。Compose项目由Python编写,实现上调用了Docker服务提供的API来对容器进行管理。因此,只要所操作的平台支持DockerAPI,就可以在其上利用Compose来进行编排管理。安装卸载官方下载地址:https://docs.docker.com/compose/install/在官方页面中,我们选择安装独立版本的dockercompose【InstalltheComposestandalone】第一步:根据页面上的链接进行安装,但是国内访问github速度超慢,可以用以下地址来替代:curl-Lhttps://get.daocloud.io/docker/compose/releases/download/v2.12.2/docker-compose-`uname-s`-`uname-m`>/usr/local/bin/docker-compose第二步:开放权限chmod+x/usr/local/bin/docker-compose第三步:查看版本[root@localhost/]#docker-composeversionDockerComposeversionv2.12.2卸载:如果使用上述方式安装的,那么卸载命令为rm/usr/local/bin/docker-compose使用Dockercompose发布SpringBoot1.准备镜像在使用dockercompose之前,我们需要先把项目中依赖的镜像下载下来。[root@localhost~]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEnginx1.2240435939482013daysago142MBredis6.05e9f874f2d5011monthsago112MBmysql5.7c20987f18b1311monthsago448MB2.编写docker-compose.yml最终我们需要有三个文件:*SpringBoot的jar文件*Dockerfile文件*docker-compose.yml文件所以我们在服务器/mydocker/app中存放这三个文件。docker-compose.yml:version:"3"services:#给SpringBoot起一个自定义的服务名myappService:#将来Dockerfile构建的镜像名及版本号image:myapp:1.2#指定容器名称container_name:myapp#开放端口ports:-"12311:12311"#配置容器连接的自定义网络,跟mysql、redis属于同一个网络networks:-app_net#容器启动时依赖于以下服务depends_on:-redisServer-mysqlServer#mysql服务mysqlServer:image:mysql:5.7ports:-"3306:3306"volumes:-/mydocker/mysql/data:/var/lib/mysql-/mydocker/mysql/conf.d:/etc/mysql/conf.denvironment:MYSQL_ROOT_PASSWORD:'root'networks:-app_net#redis服务redisServer:image:redis:6.0ports:-"6379:6379"volumes:-/mydocker/redis/data:/data-/mydocker/redis/redis.conf:/usr/local/etc/redis/redis.confnetworks:-app_netcommand:redis-server#nginx服务nginxServer:image:nginx:1.22volumes:-/mydocker/nginx/nginx.conf:/etc/nginx/nginx.conf-/mydocker/nginx/default.conf:/etc/nginx/conf.d/default.conf-/mydocker/nginx/html:/usr/share/nginx/htmlports:-"80:80"networks:-app_netnetworks:app_net:3.修改application.yml在docker-compose.yml中定义了mysqlServer和redisServer,所以配置文件中服务地址不再写对应的ip了,而是改成服务名。server:port:12311spring:datasource:driver-class-name:com.mysql.jdbc.Driverurl:jdbc:mysql://mysqlServer/db_docker?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=falseusername:rootpassword:rootredis:host:redisServerport:63794.编写Dockerfile#基于java8FROMopenjdk:8#创建容器的工作目录WORKDIR/app#将jar包添加到容器中,并改命为demo.jarADDspring-docker-compose-0.0.1-SNAPSHOT.jar/app/myapp.jar#暴露端口EXPOSE12311#运行jar包ENTRYPOINT["java","-jar"]CMD["myapp.jar"]5.上传服务器将SpringBoot打成jar,然后上传至服务器,需要保证jar包、Dockerfile、docker-compose.yml在同一个目录下。这里上传到/mydocker/app目录下:[root@localhost/]#cd/mydocker/app[root@localhostapp]#ll总用量27800-rw-r--r--.1rootroot136212月516:13docker-compose.yml-rw-r--r--.1rootroot28411月2320:00Dockerfile-rw-r--r--.1rootroot2845897012月516:16spring-docker-compose-0.0.1-SNAPSHOT.jar6.构建SpringBoot镜像需要保证构建的镜像名称及版本号,和docker-compose.yml中定义的一致。[root@localhostapp]#dockerbuild-tmyapp:1.2.#查看镜像是否构建成功[root@localhostapp]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmyapp1.2e786e60a85aa9minutesago554MBnginx1.224043593948202weeksago142MBopenjdk8e24ac15e052e11monthsago526MBredis6.05e9f874f2d5011monthsago112MBmysql5.7c20987f18b1311monthsago448MB7.执行docker-composeup[root@localhostapp]#docker-composeup出现以下日志,就说明命令执行没有问题。默认情况,docker-composeup启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。当通过Ctrl-C停止命令时,所有容器将会停止。如果使用docker-composeup-d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。8.查看运行容器[root@localhost~]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMESfa60e0fabf10myapp:1.2"java-jarmyapp.jar"AboutaminuteagoUpAboutaminute0.0.0.0:12311->12311/tcp,:::12311->12311/tcpmyapp002f18d9ba02redis:6.0"docker-entrypoint.s…"AboutaminuteagoUpAboutaminute0.0.0.0:6379->6379/tcp,:::6379->6379/tcpapp-redisServer-156b831972238mysql:5.7"docker-entrypoint.s…"AboutaminuteagoUpAboutaminute0.0.0.0:3306->3306/tcp,:::3306->3306/tcp,33060/tcpapp-mysqlServer-1b9162bf9fb8enginx:1.22"/docker-entrypoint.…"AboutaminuteagoUpAboutaminute0.0.0.0:80->80/tcp,:::80->80/tcpapp-nginxServer-1docker-compose使用对比通过上面对比图可以发现,在服务不多的情况下,使用docker-compose和不使用区别不大。但是前者需要多少个服务,就得dockerrun多少次,在服务多或者搭建集群的场景下还是适合用docker-compose。dockerrun时需要注意服务的启动顺序,比如启动SpringBoot时需要先启动mysql和redis,而docker-compose只需要指定depends_on即可。总体来说,使用docker-compose可以将一组应用容器组成一个完整的业务单元,一个项目可以由多个服务(容器)关联而成,Compose面向项目进行管理。当服务器上一个镜像运行了很多个容器后,我们很难搞清哪个容器对应哪个服务,使用docker-compose可以一目了然的发现各个容器之间的协作关系,方便运维管理。

    Docker / Spring系列
    42
    2
    2022-12-05 17:08:50
  • docker-compose(一):SpringBoot + Docker 案例

    本文主要演示通过docker在linux服务器上安装mysql、redis及nginx,并通过一个简单的SpringBoot项目进行连接并测试,最后通过nginx代理我们后端的接口请求。安装mysql镜像:mysql:5.7库:db_docker表:t_user1、拉取镜像dockerpullmysql:5.72、创建mysql容器dockerrun--namemysql\-v/mydocker/mysql/data:/var/lib/mysql\-v/mydocker/mysql/conf.d:/etc/mysql/conf.d\-eMYSQL_ROOT_PASSWORD=root\-p3306:3306-dmysql:5.73、进入mysql容器内部[root@localhost/]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES0102c03d7766mysql:5.7"docker-entrypoint.s…"3minutesagoUp3minutes0.0.0.0:3306->3306/tcp,:::3306->3306/tcp,33060/tcpmysql[root@localhost/]#dockerexec-it0102c03d7766bash4、mysql连接root@0102c03d7766:/#mysql-uroot-proot5、创建库和表mysql>createdatabasedb_docker;QueryOK,1rowaffected(0.00sec)mysql>usedb_docker;Databasechangedmysql>createtablet_user(->idintprimarykeyauto_increment,->namevarchar(55)default"",->ageintnotnull);QueryOK,0rowsaffected(0.01sec)6、测试远程连接由于是自己的虚拟机,所以不需要开放端口也能访问,如果是购买的服务器,则需要开放端口,并重启防火墙。安装redis镜像:redis:6.01、拉取镜像dockerpullredis:6.02、自定义redis配置在创建之前,我们需要对redis进行如下配置:1).开启远程访问2).开启aof持久化在/mydocker/redis目录下创建redis.conf文件,并加入以下内容:[root@localhost/]#cat/mydocker/redis/redis.confbind0.0.0.0appendonlyyes3、创建redis容器dockerrun--nameredis\-v/mydocker/redis/data:/data\-v/mydocker/redis/redis.conf:/usr/local/etc/redis/redis.conf\-p6379:6379\-dredis:6.0\redis-server3、测试远程连接由于是自己的虚拟机,所以不需要开放端口也能访问,如果是购买的服务器,则需要开放端口,并重启防火墙。安装Nginx镜像:nginx:1.221、拉取镜像dockerpullnginx:1.222、暂时启动个nginx实例,将其内部的配置文件,拷贝到宿主机#1.启动nginxdockerrun-p80:80-dnginx:1.22#2.宿主机在/mydocker目录下创建nginx目录[root@localhost/]#cd/mydocker/[root@localhostmydocker]#mkdirnginx#3.拷贝配置文件(aa6b12782323为nginx容器的id)dockercpaa6b12782323:/etc/nginx/nginx.conf/mydocker/nginx/nginx.confdockercpaa6b12782323:/etc/nginx/conf.d/default.conf/mydocker/nginx/default.conf#4.将刚刚创建的nginx容器删除[root@localhostmydocker]#dockerstopaa6b12782323aa6b12782323[root@localhostmydocker]#dockerrmaa6b12782323aa6b127823232、创建nginx容器dockerrun\-v/mydocker/nginx/nginx.conf:/etc/nginx/nginx.conf\-v/mydocker/nginx/default.conf:/etc/nginx/conf.d/default.conf\-v/mydocker/nginx/html:/usr/share/nginx/html\-p80:80-dnginx:1.223、在/mydocker/nginx/html目录下创建index.html,并写入内容[root@localhosthtml]#pwd/mydocker/nginx/html[root@localhosthtml]#lsindex.html[root@localhosthtml]#catindex.htmlHelloNginx3、测试访问连接SpringBoot项目主要测试mysql和redis的功能,所以简单提供两个接口,一个是对mysql的测试,一个是对redis的测试。接口描述/testMysql往数据库中添加一条记录/testRedis往redis中添加一条记录1、pom.xml<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--MySQL连接Java的驱动程序--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.27</version></dependency><!--支持通过jdbc连接数据库库--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>2、application.ymlserver:port:12311spring:datasource:driver-class-name:com.mysql.jdbc.Driverurl:jdbc:mysql://192.168.2.123:3306/db_docker?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=falseusername:rootpassword:rootredis:host:192.168.2.123port:63793、Controller@RestControllerpublicclassTestController{@AutowiredprivateJdbcTemplatejdbcTemplate;@AutowiredprivateStringRedisTemplateredisTemplate;@GetMapping("/testMysql")publicStringtestMysql(){Stringsql="insertintot_user(name,age)values(?,?)";Object[]args={"tom",18};intresult=jdbcTemplate.update(sql,args);returnresult==1?"mysql添加成功":"mysql添加失败";}@GetMapping("/testRedis")publicStringtestRedis(){Stringkey="user:1";redisTemplate.opsForValue().set(key,"tom");Stringvalue=redisTemplate.opsForValue().get(key);returnvalue;}}构建SpringBoot镜像将上面的SpringBoot项目打成jar包,上传到/mydocker/app目录下:[root@localhost/]#clear[root@localhost/]#cd/mydocker/app/[root@localhostapp]#lsspring-docker-compose-0.0.1-SNAPSHOT.jar在同级目录创建Dockerfile文件:#基于java8FROMopenjdk:8#创建容器的工作目录WORKDIR/app#将jar包添加到容器中,并改命为myapp.jarADDspring-docker-compose-0.0.1-SNAPSHOT.jar/app/myapp.jar#暴露端口EXPOSE12311#运行jar包ENTRYPOINT["java","-jar"]CMD["myapp.jar"]构建镜像:dockerbuild-tmyapp启动容器:[root@localhostapp]#dockerrun-p12311:12311-d532c2435808afd6c851f0445b86b9946c092923fb188756d3a3e308935362fb94804d422cfff[root@localhostapp]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMESfd6c851f0445532c2435808a"java-jarmyapp.jar"4secondsagoUp2seconds0.0.0.0:12311->12311/tcp,:::12311->12311/tcpbusy_varahamihira查看容器是否启动成功:nginx代理接口配置nginx的80端口代理到我们服务的12311接口.[root@localhost/]#cd/mydocker/nginx/[root@localhostnginx]#videfault.confdefault.conf:location/{#root/usr/share/nginx/html;#indexindex.htmlindex.htm;proxy_passhttp://192.168.2.123:12311/;}重启nginx容器:[root@localhostnginx]#dockerpsdCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMESfd6c851f0445532c2435808a"java-jarmyapp.jar"17minutesagoUp17minutes0.0.0.0:12311->12311/tcp,:::12311->12311/tcpbusy_varahamihira6b0ac8c0fb1anginx:1.22"/docker-entrypoint.…"AboutanhouragoUpAboutanhour0.0.0.0:80->80/tcp,:::80->80/tcpsilly_napierc7ab53b1aab2redis:6.0"docker-entrypoint.s…"2hoursagoUp2hours0.0.0.0:6379->6379/tcp,:::6379->6379/tcpredis0102c03d7766mysql:5.7"docker-entrypoint.s…"2hoursagoUp2hours0.0.0.0:3306->3306/tcp,:::3306->3306/tcp,33060/tcpmysql[root@localhostnginx]#dockerrestart6b0ac8c0fb1a6b0ac8c0fb1a接口访问测试mysql:去数据库查询:mysql>select*fromt_user;+----+------+-----+|id|name|age|+----+------+-----+|1|tom|18|+----+------+-----+1rowinset(0.00sec)测试redis:root@c7ab53b1aab2:/data#redis-cli127.0.0.1:6379>getuser:1"tom"127.0.0.1:6379>至此,通过docker安装服务并访问测试的demo就已经完成了。下篇文章说一下基于dockercompose来管理docker容器,以及它和传统的dockerrun有哪些区别。

    Docker / Spring系列
    48
    2
    2022-11-23 23:41:07
  • Dockerfile 实践

    前言上篇说到我们可以基于exec进入容器内部并更改部分配置,然后通过commit打包成一个新的镜像发布到我们自己的docker仓库。这种情况只适用于针对此容器做一些简单的修改,如果我们要修改的内容过于复杂,比如在镜像运行前后增加一些指令等情况,就需要用Dockerfile来实现。定义Dockerfile是用来构建Docker镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。实际上我们从dockerhub拉取下来的各个镜像,都是由Dockerfile编写并打包成一个个镜像供我们下载。Dockerfile保留字指令实际上Dockerfile就是由一条条保留字指令构成,所以我们详细来说下各个保留字指令。保留字作用FROM当前镜像是基于哪个镜像的第一个指令必须是FROMMAINTAINER镜像维护者的姓名和邮箱地址RUN构建镜像时需要运行的指令EXPOSE当前容器对外暴露出的端口号WORKDIR指定在创建容器后,终端默认登录进来的工作目录,一个落脚点ENV用来在构建镜像过程中设置环境变量ADD将宿主机目录下的文件拷贝进镜像且ADD命令会自动处理URL和解压tar包COPY类似于ADD,拷贝文件和目录到镜像中将从构建上下文目录中<原路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置VOLUME容器数据卷,用于数据保存和持久化工作CMD指定一个容器启动时要运行的命令Dockerfile中可以有多个CMD指令,但只有最后一个生效,CMD会被dockerrun之后的参数替换ENTRYPOINT指定一个容器启动时要运行的命令ENTRYPOINT的目的和CMD一样,都是在指定容器启动程序及其参数FROM基于哪个镜像进行构建新的镜像,在构建时会自动从dockerhub拉取base镜像必须作为Dockerfile的第一个指令出现语法:FROM<image>FROM<image>[:<tag>]使用版本不写为latestFROM<image>[@<digest>]使用摘要MAINTAINER镜像维护者的姓名和邮箱地址[废弃]语法:MAINTAINER<name>RUN在容器构建的时候就会执行run后面的命令。语法:#1.RUN<命令行命令>等同于直接在终端写入的shell命令RUNjava-jarapp.jar#2.RUN["可执行文件","参数1","参数2","参数..."]RUN["java","-jar","app.jar"]EXPOSE用来指定构建的镜像在运行为容器时对外暴露的端口语法:EXPOSE80/tcp如果没有显示指定则默认暴露都是tcpEXPOSE80/udpCMD用来为启动的容器指定执行的命令,在Dockerfile中只能有一条CMD指令。如果列出多个命令,则只有最后一个命令才会生效。注意:**Dockerfile中只能有一条CMD指令。如果列出多个命令,则只有最后一个命令才会生效。**语法:CMD["executable","param1","param2"](execform,thisisthepreferredform)CMD["param1","param2"](asdefaultparameterstoENTRYPOINT)CMDcommandparam1param2(shellform)WORKDIR用来为Dockerfile中的任何RUN、CMD、ENTRYPOINT、COPY和ADD指令设置工作目录。如果WORKDIR不存在,即使它没有在任何后续Dockerfile指令中使用,它也将被创建。语法:WORKDIR/path/to/workdirWORKDIR/aWORKDIRbWORKDIRc`注意:WORKDIR指令可以在Dockerfile中多次使用。如果提供了相对路径,则该路径将与先前WORKDIR指令的路径相对`ENV用来为构建镜像设置环境变量。这个值将出现在构建阶段中所有后续指令的环境中。语法:ENV<key><value>ENV<key>=<value>...#例如:ENVMY_PATH/usr/localWORKDIR$MY_PATHADD将宿主机目录下的文件拷贝进镜像且会自动处理URL和解压tar压缩包,也可以从远程文件url进行复制。语法:ADDhom*/mydir/通配符添加多个文件ADDhom?.txt/mydir/通配符添加ADDtest.txtrelativeDir/可以指定相对路径ADDtest.txt/absoluteDir/也可以指定绝对路径ADDurlCOPY类似ADD,拷贝文件和目录到镜像中。但不会自动解压。语法:COPYsrcdestCOPY["<src>",..."<dest>"]#src:源文件或源目录#dest:容器内的路径,不需要提前创建VOLUME用来定义容器运行时可以挂在到宿主机的目录语法:VOLUME["/data"]ENTRYPOINT用来指定容器启动时执行的命令和CMD类似语法:ENTRYPOINT["executable","param1","param2"]ENTRYPOINT后面可以跟CMD指令,此时CMD不再作为容器启动时的执行命令,而是将CMD作为参数传递到ENTRYPOINT治理中。例如:FROMjava:8ENTRYPOINT["java","-jar"]CMD["app.jar"]#等同于在终端中写:java-jarapp.jarDockerfile构建springboot项目部署1、准备springboot可运行项目,并且最终通过maven打成jar包。2、将可运行项目放入linux虚拟机或服务器中3、编写Dockerfile跟jar包同目录下,新建Dockerfile文件#基础镜像使用java8FROMopenjdk:8#创建并设置容器的工作目录WORKDIR/ems#将jar包添加到容器中ADDems_pink-0.0.1-SHAPSHOT.jar/ems#暴露端口EXPOSE8989#运行jar包ENTRYPOINT["java","-jar"]CMD["ems_pink-0.0.1-SHAPSHOT.jar"]4、构建镜像#后面不要漏了.[root@localhostems]#dockerbuild-tems.5、运行镜像[root@localhostems]#dockerrun-p8989:8989ems

    Docker
    23
    1
    2022-11-21 00:18:28
热门文章 最新评论
  • InnoDB索引数据结构

    8
  • thymeleaf 整合 pjax 无刷新跳转

    5
  • wmware创建虚拟机

    4
  • SQL响应慢如何分析解决

    4
  • JS 生成文章目录树

    3
网站信息 访客统计
  • 文章数目
    28
  • 评论数目
    58
  • 运行天数
    610天