前言

最近在项目中做了一项优化,对业务代码进行解耦。我们部门做的是警用系统,通俗的说,可理解为110报警。一条警情,会先后经过接警员、处警调度员、一线警员,警情是需要记录每一步的日志,是要可追溯的,比如报警人张小三在2019-12-02 00:02:01时间报警,接警员A在1分钟后,将该警情记录完成,并分派给处警调度员B,调度员B在5分钟后,分派给一线警员C,C赶到现场后,花了1个小时处理完成。

这中间,每一个接口,需要做的事情,可能就包括了:警情日志记录;警员当前任务数统计,包括待处置的任务和已经处置完成的任务;我们其实还有一个操作,就是发mq,去通知其他相关人,比如接警员A接警完成后,要发mq通知其主管。

以前的代码可能是这样的:

  1. ## 接口1里, 接收警情service里完成以下操作
  2. void 接收警情(xxxReqVo reqVo){
  3. 1:写库
  4. 2:记录警情跟踪日志
  5. 3:增加当前接警员的接警数
  6. 4:发mq通知其他相关人
  7. }
  8. ##接口2里,分派警情的service里完成以下操作
  9. void 分派警情(xxxReqVo reqVo){
  10. 1:写库
  11. 2:记录警情跟踪日志
  12. 3:增加当前处警调度警员的处警数
  13. 4:发mq通知其他相关人
  14. }

这样的问题是什么呢?

  1. 在每一个相关接口里,都要“显式”调用:记录跟踪日志的相关方法、统计相关的方法、发mq相关的方法;但凡有一个地方忘记了,都会导致问题,比如统计数量不准,mq忘发,跟踪日志遗漏等。
  2. 业务逻辑和这类通用业务揉在一起,假设下次又需要给报警人发个短信,岂不是又得去改核心代码?这不符合我们“对修改关闭,对扩展开放”的开闭原则啊;假设脑残的产品经理,这次说要给报警人发短信,过两天又不要了,难道每个接口,挨个挨个改吗,想想都想打死产品经理,但是这个又犯法,还是想想其他办法?

这个问题,我们可以用类似mq的方法来解决,即,发送消息,各个消费者去消费。一般,mq的方式适用于微服务之间,而我们这里,将使用事件-发布机制来解决这个问题。

源码地址(直接dubug跟一下,很简单,比看文章来得快):

https://gitee.com/ckl111/spring-event-publish-demo

先说说ApplicationListener

spring boot之前的spring 时代,想必一些同学用过org.springframework.context.ApplicationListener,正好我手里有一个老项目,也用到了这个东西,我就拿这个举个例子:

在我们的项目中,需要在启动后,初始化一些东西,比如预热缓存,最早的代码呢,可能是大家各自实现org.springframework.beans.factory.InitializingBean,但是这样呢,初始化代码散落在各个service中;还有一些直接使用@PostContruct注解,然后在对应方法里去完成一些初始化操作。但是总体来说,这些方式,在spring的启动过程中,被调用的时机比较靠前,有些候某些bean可能还没初始化完成,而导致一些奇怪的问题。

所以,我们后来统一去掉了这些初始化代码,全部采用以下机制来实现:

  1. import org.springframework.context.ApplicationListener;
  2. import org.springframework.context.event.ContextRefreshedEvent;
  3. @Service
  4. public class InitRunner implements ApplicationListener<ContextRefreshedEvent> {
  5. @Autowired
  6. private InitService initService;
  7. @Override
  8. public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
  9. //root application context,因为是web项目,
  10. if (contextRefreshedEvent.getApplicationContext().getParent() == null) {
  11. initService.init();
  12. }
  13. }

在这个类中,我们实现了org.springframework.context.ApplicationListener<ContextRefreshedEvent> ,这个 listener的定义如下:

  1. public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
  2. /**
  3. * Handle an application event.
  4. * @param event the event to respond to
  5. */
  6. void onApplicationEvent(E event);
  7. }

接口 EventListener 是 jdk 的一个 marker interface:

  1. package java.util;
  2. /**
  3. - A tagging interface that all event listener interfaces must extend.
  4. - @since JDK1.1
  5. */
  6. public interface EventListener {
  7. }

我们在实现listener时,指定了本listener感兴趣的事件:ContextRefreshedEvent,这个事件的类继承关系如下:

那么,这个事件是什么意思呢?

  1. /**
  2. * Event raised when an {@code ApplicationContext} gets initialized or refreshed.
  3. *
  4. * @author Juergen Hoeller
  5. * @since 04.03.2003
  6. * @see ContextClosedEvent
  7. */
  8. @SuppressWarnings("serial")
  9. public class ContextRefreshedEvent extends ApplicationContextEvent {
  10. /**
  11. * Create a new ContextRefreshedEvent.
  12. * @param source the {@code ApplicationContext} that has been initialized
  13. * or refreshed (must not be {@code null})
  14. */
  15. public ContextRefreshedEvent(ApplicationContext source) {
  16. super(source);
  17. }
  18. }

注释说:Event raised when an {@code ApplicationContext} gets initialized or refreshed.,那么意思就是,该事件,是在上下文初始化完成后被发布。

这样的话,就能保证,在我们listener监听到这个事件的时候,整个应用上下文已经可以使用了。

一览Spring事件监听机制

我们再通过debug,来看看其真实的调用时机:

上图红框处,对spring上下文进行refresh,refresh就是spring 最核心的部分了,基本上,看懂了这个函数,就懂了一半:

  1. // Prepare this context for refreshing.
  2. prepareRefresh();
  3. // Tell the subclass to refresh the internal bean factory.
  4. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  5. // Prepare the bean factory for use in this context.
  6. // 实例化beanFactory
  7. prepareBeanFactory(beanFactory);
  8. try {
  9. // Allows post-processing of the bean factory in context subclasses.
  10. // 对beanFactory进行处理
  11. postProcessBeanFactory(beanFactory);
  12. // Invoke factory processors registered as beans in the context.
  13. // BeanFactoryPostProcessor开始作用的地方,这里会调用所有的beanFactory后置处理器
  14. invokeBeanFactoryPostProcessors(beanFactory);
  15. // Register bean processors that intercept bean creation.
  16. // 注册 bean的后置处理器到beanFactory,注意,截止目前,还没开始实例化bean(除了少数几个内部bean)
  17. registerBeanPostProcessors(beanFactory);
  18. // Initialize message source for this context.
  19. // 注册国际化相关bean
  20. initMessageSource();
  21. // Initialize event multicaster for this context.
  22. // 注册事件发布器,这个和本文主题大有关系
  23. initApplicationEventMulticaster();
  24. // Initialize other special beans in specific context subclasses.
  25. //注意上面这行注释,这个类是交给子类覆盖的,比如,在 org.springframework.web.context.support.AbstractRefreshableWebApplicationContext中,实例化了 org.springframework.ui.context.ThemeSource
  26. onRefresh();
  27. // Check for listener beans and register them.
  28. // 从spring容器上下文中,查找ApplicationListener类型的监听器,添加到前两步,初始化的事件发布器中
  29. registerListeners();
  30. // Instantiate all remaining (non-lazy-init) singletons.
  31. //注意:截止到目前为止,beanFactory里面基本还是空空如也,没有bean,只有BeanDefinition,在这一步才会 //根据那些BeanDefinition来实例化那些:非lazy-init的bean
  32. finishBeanFactoryInitialization(beanFactory);
  33. // Last step: publish corresponding event.
  34. // 发布:容器完成初始化的事件
  35. finishRefresh();
  36. }

上面基本都加了注释,比较容易懂,需要重点关注的是:

  1. 事件发布器初始化
  1. initApplicationEventMulticaster();

这一步会生成一个org.springframework.context.event.ApplicationEventMulticaster,存储在org.springframework.context.support.AbstractApplicationContext#applicationEventMulticaster

该事件发布器的接口主要有(去除了无关方法):

  1. /**
  2. * Add a listener to be notified of all events.
  3. * @param listener the listener to add
  4. */
  5. void addApplicationListener(ApplicationListener<?> listener);
  6. /**
  7. * Multicast the given application event to appropriate listeners.
  8. * <p>Consider using {@link #multicastEvent(ApplicationEvent, ResolvableType)}
  9. * if possible as it provides a better support for generics-based events.
  10. * @param event the event to multicast
  11. */
  12. void multicastEvent(ApplicationEvent event);

从上面可以看出,该接口主要是维护监听器ApplicationListener,以及进行事件发布。

  1. 注册监听器

    1. // 注册listeners
    2. protected void registerListeners() {
    3. // Register statically specified listeners first.
    4. for (ApplicationListener<?> listener : getApplicationListeners()) {
    5. getApplicationEventMulticaster().addApplicationListener(listener);
    6. }
    7. // Do not initialize FactoryBeans here: We need to leave all regular beans
    8. // uninitialized to let post-processors apply to them!
    9. //这里的这句注释也很魔性,哈哈,侧面说明了,截至目前,beanFactory都是没有bean实例存在的,bean还没 //有实例化
    10. String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
    11. for (String listenerBeanName : listenerBeanNames) {
    12. getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    13. }
  2. beanFactory初始化完成后,发布事件

    1. protected void finishRefresh() {
    2. // Publish the final event.
    3. // 发布上下文refresh完毕的事件,通知listener
    4. publishEvent(new ContextRefreshedEvent(this));
    5. }

    这里,publishEvent实现如下:

    1. protected void publishEvent(Object event, ResolvableType eventType) {
    2. // Decorate event as an ApplicationEvent if necessary
    3. ApplicationEvent applicationEvent;
    4. if (event instanceof ApplicationEvent) {
    5. applicationEvent = (ApplicationEvent) event;
    6. }
    7. getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    8. }

方案1:参考spring,实现自己的事件监听机制,解耦业务代码

项目源码地址:https://gitee.com/ckl111/spring-event-publish-demo.git

项目结构如下:

  1. 定义与实现事件发布器

    1. package com.ceiec.base.event;
    2. import org.springframework.context.event.ApplicationEventMulticaster;
    3. /**
    4. * desc:
    5. * 参考spring的设计
    6. * {@link ApplicationEventMulticaster}
    7. * @author : ckl
    8. * creat_date: 2019/11/16 0016
    9. * creat_time: 10:40
    10. **/
    11. public interface ICommonApplicationEventMulticaster {
    12. /**
    13. * Add a listener to be notified of all events.
    14. * @param listener the listener to add
    15. */
    16. void addApplicationListener(ICommonApplicationEventListener<?> listener);
    17. /**
    18. * Multicast the given application event to appropriate listeners.
    19. * @param event the event to multicast
    20. */
    21. void multicastEvent(CommonApplicationEvent event);
    22. }
    1. package com.ceiec.base.event;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.context.event.SimpleApplicationEventMulticaster;
    4. import org.springframework.stereotype.Component;
    5. import java.util.LinkedHashSet;
    6. import java.util.Set;
    7. /**
    8. * desc:
    9. * 参考spring
    10. * {@link SimpleApplicationEventMulticaster}
    11. *
    12. * @author : ckl
    13. * creat_date: 2019/11/16 0016
    14. * creat_time: 10:40
    15. **/
    16. @Slf4j
    17. @Component
    18. public class CommonApplicationEventMulticaster implements ICommonApplicationEventMulticaster {
    19. public final Set<ICommonApplicationEventListener<?>> applicationListeners = new LinkedHashSet<>();
    20. @Override
    21. public void addApplicationListener(ICommonApplicationEventListener<?> listener) {
    22. applicationListeners.add(listener);
    23. }
    24. @Override
    25. public void removeApplicationListener(ICommonApplicationEventListener<?> listener) {
    26. applicationListeners.remove(listener);
    27. }
    28. @Override
    29. public void removeAllListeners() {
    30. applicationListeners.clear();
    31. }
    32. @Override
    33. @SuppressWarnings({"rawtypes", "unchecked"})
    34. public void multicastEvent(CommonApplicationEvent event) {
    35. try {
    36. for (ICommonApplicationEventListener applicationListener : applicationListeners) {
    37. //判断listener是否支持处理该事件,如果支持,则丢给listener处理
    38. if (applicationListener.supportsEventType(event)) {
    39. applicationListener.onApplicationEvent(event);
    40. }
    41. }
    42. } catch (Exception e) {
    43. log.error("{}",e);
    44. }
    45. }
    46. }
  2. 定义listener

    1. package com.ceiec.base.event;
    2. import org.springframework.context.ApplicationListener;
    3. import java.util.EventListener;
    4. /**
    5. * desc:
    6. * 参考spring
    7. * {@link ApplicationListener}
    8. * @author : ckl
    9. * creat_date: 2019/11/16 0016
    10. * creat_time: 10:45
    11. **/
    12. public interface ICommonApplicationEventListener<E extends CommonApplicationEvent> extends EventListener {
    13. boolean supportsEventType(E event );
    14. /**
    15. * Handle an application event.
    16. * @param event the event to respond to
    17. */
    18. void onApplicationEvent(E event);
    19. }
  3. 定义事件类

    1. package com.ceiec.base.event;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Data;
    4. import lombok.experimental.Accessors;
    5. import java.util.EventObject;
    6. /**
    7. * desc:
    8. * 参考spring的设计
    9. * {@link org.springframework.context.ApplicationEvent}
    10. **/
    11. @Data
    12. @AllArgsConstructor
    13. @Accessors(chain = true)
    14. public class CommonApplicationEvent<T>{
    15. /**
    16. * 事件类型
    17. */
    18. private IEventType iEventType;
    19. /**
    20. * 事件携带的数据
    21. */
    22. private T data;
    23. }
  4. listener的样例实现,下面的实现,用于警情的跟踪日志记录

    1. package com.ceiec.base.listener;
    2. import com.ceiec.base.applicationevent.SystemEventType;
    3. import com.ceiec.base.event.CommonApplicationEvent;
    4. import com.ceiec.base.event.ICommonApplicationEventListener;
    5. import com.ceiec.base.eventmsg.*;
    6. import com.ceiec.base.service.IIncidentTraceService;
    7. import lombok.extern.slf4j.Slf4j;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.stereotype.Component;
    10. /**
    11. * desc:
    12. * 接警统计listener
    13. * @author : ckl
    14. * creat_date: 2019/11/16 0016
    15. * creat_time: 9:56
    16. **/
    17. @Component
    18. @Slf4j
    19. public class IncidentTraceListener implements ICommonApplicationEventListener{
    20. @Autowired
    21. private IIncidentTraceService iIncidentTraceService;
    22. @Override
    23. public boolean supportsEventType(CommonApplicationEvent event) {
    24. return true;
    25. }
    26. @Override
    27. public void onApplicationEvent(CommonApplicationEvent event) {
    28. log.info("{}",event);
    29. Object data = event.getData();
    30. if (event.getIEventType() == SystemEventType.FINISH_INCIDENT_APPEAL) {
    31. FinishIncidentDisposalEventMsg msg = (FinishIncidentDisposalEventMsg) data;
    32. iIncidentTraceService.finishIncidentDisposal(msg);
    33. }
    34. }
    35. }
  5. 启动程序时,注册listener到事件发布器

    1. package com.ceiec.base.init;
    2. import com.ceiec.base.event.CommonApplicationEventMulticaster;
    3. import com.ceiec.base.event.ICommonApplicationEventListener;
    4. import lombok.extern.slf4j.Slf4j;
    5. import org.springframework.beans.BeansException;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.boot.CommandLineRunner;
    8. import org.springframework.context.ApplicationContext;
    9. import org.springframework.context.ApplicationContextAware;
    10. import org.springframework.stereotype.Component;
    11. import java.util.Collection;
    12. import java.util.Map;
    13. /**
    14. * desc:
    15. *
    16. * @author : ckl
    17. * creat_date: 2019/11/11 0011
    18. * creat_time: 15:46
    19. **/
    20. @Component
    21. @Slf4j
    22. public class InitRunner implements CommandLineRunner,ApplicationContextAware {
    23. private ApplicationContext applicationContext;
    24. @Autowired
    25. private CommonApplicationEventMulticaster commonApplicationEventMulticaster;
    26. @Override
    27. public void run(String... args) throws Exception {
    28. Map<String, ICommonApplicationEventListener> map = applicationContext.getBeansOfType(ICommonApplicationEventListener.class);
    29. Collection<ICommonApplicationEventListener> listeners = map.values();
    30. for (ICommonApplicationEventListener listener : listeners) {
    31. /**
    32. * 注册事件listener到事件发布器
    33. */
    34. log.info("register listener:{}",listener);
    35. commonApplicationEventMulticaster.addApplicationListener(listener);
    36. }
    37. }
    38. @Override
    39. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    40. this.applicationContext = applicationContext;
    41. }
    42. }
  6. 定义endpoint,在service中进行事件发布

    controller:

    1. @Autowired
    2. private IIncidentService iIncidentService;
    3. @RequestMapping("/test.do")
    4. public String finishIncident() {
    5. iIncidentService.finishIncident();
    6. return "success";
    7. }

    service:

    1. @Slf4j
    2. @Service
    3. public class IIncidentServiceImpl implements IIncidentService {
    4. @Autowired
    5. private CommonApplicationEventMulticaster commonApplicationEventMulticaster;
    6. @Override
    7. public void finishIncident() {
    8. FinishIncidentDisposalEventMsg msg = new FinishIncidentDisposalEventMsg();
    9. msg.setIncidentInformationId(1111L);
    10. msg.setDesc("处置完成");
    11. CommonApplicationEvent event = new CommonApplicationEvent(SystemEventType.FINISH_INCIDENT_APPEAL,msg);
    12. commonApplicationEventMulticaster.multicastEvent(event);
    13. }
    14. }
  7. 效果展示

    启动时,注册listener:

    1. 2019-12-03 16:49:47.477 INFO 493432 --- [ main] com.ceiec.base.BootStrap : Started BootStrap in 1.436 seconds (JVM running for 2.22)
    2. 2019-12-03 16:49:47.478 INFO 493432 --- [ main] com.ceiec.base.init.InitRunner : register listener:com.ceiec.base.listener.IncidentStatisticsListener@c6b2dd9
    3. 2019-12-03 16:49:47.479 INFO 493432 --- [ main] com.ceiec.base.init.InitRunner : register listener:com.ceiec.base.listener.IncidentTraceListener@3f985a86
    4. 2019-12-03 16:49:47.479 INFO 493432 --- [ main] com.ceiec.base.init.InitRunner : register listener:com.ceiec.base.listener.MqListener@57a2ed35

    浏览器中,请求http://localhost:8081/test.do,日志如下:

方案2:直接使用spring内置的事件发布器,解耦业务代码

源码:https://gitee.com/ckl111/spring-event-publish-demo/tree/master/spring-event-use-builtin-multicaster

这部分,比上面的方案相比,少了很多东西,只包含如下部分:

总的来说,listener直接继承spring的ApplicationListener,事件发布器直接使用spring的org.springframework.context.ApplicationEventPublisher

核心代码:

  1. package com.ceiec.base.service.impl;
  2. import com.ceiec.base.applicationevent.SystemEventType;
  3. import com.ceiec.base.event.CommonApplicationEvent;
  4. import com.ceiec.base.eventmsg.FinishIncidentDisposalEventMsg;
  5. import com.ceiec.base.service.IIncidentService;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.context.ApplicationEvent;
  9. import org.springframework.context.ApplicationEventPublisher;
  10. import org.springframework.stereotype.Service;
  11. /**
  12. * desc:
  13. * 发布事件的业务代码示例
  14. * @author : ckl
  15. * creat_date: 2019/12/2 0002
  16. * creat_time: 14:27
  17. **/
  18. @Slf4j
  19. @Service
  20. public class IIncidentServiceImpl implements IIncidentService {
  21. @Autowired
  22. private ApplicationEventPublisher applicationEventPublisher;
  23. @Override
  24. public void finishIncident() {
  25. FinishIncidentDisposalEventMsg msg = new FinishIncidentDisposalEventMsg();
  26. msg.setIncidentInformationId(1111L);
  27. msg.setDesc("处置完成");
  28. CommonApplicationEvent event = new CommonApplicationEvent(SystemEventType.FINISH_INCIDENT_APPEAL,msg);
  29. applicationEventPublisher.publishEvent(event);
  30. }
  31. }
  1. package com.ceiec.base.listener;
  2. import com.ceiec.base.applicationevent.SystemEventType;
  3. import com.ceiec.base.event.CommonApplicationEvent;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.context.ApplicationListener;
  6. import org.springframework.stereotype.Component;
  7. /**
  8. * desc:
  9. * 这里,直接继承 spring 的listener
  10. * @author : ckl
  11. * creat_date: 2019/11/16 0016
  12. * creat_time: 9:56
  13. **/
  14. @Component
  15. @Slf4j
  16. public class IncidentStatisticsListener implements ApplicationListener<CommonApplicationEvent> {
  17. @Override
  18. public void onApplicationEvent(CommonApplicationEvent event) {
  19. log.info("receive event:{}",event);
  20. }
  21. }

总结

以上两种都可以用,一个是自己仿的,定制性强一点;一个直接用spring的。大家自由选择即可。

通过这样的方式,我们的业务代码,可以做到解耦,大体和mq其实是类似的。

从spring源码汲取营养:模仿spring事件发布机制,解耦业务代码的更多相关文章

  1. spring源码分析系列 (3) spring拓展接口InstantiationAwareBeanPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.InstantiationAwareBeanPostProcessor简述与demo示例 二.InstantiationAwareBean ...

  2. Spring源码深度解析之Spring MVC

    Spring源码深度解析之Spring MVC Spring框架提供了构建Web应用程序的全功能MVC模块.通过策略接口,Spring框架是高度可配置的,而且支持多种视图技术,例如JavaServer ...

  3. Spring 源码学习笔记10——Spring AOP

    Spring 源码学习笔记10--Spring AOP 参考书籍<Spring技术内幕>Spring AOP的实现章节 书有点老,但是里面一些概念还是总结比较到位 源码基于Spring-a ...

  4. Spring 源码学习笔记11——Spring事务

    Spring 源码学习笔记11--Spring事务 Spring事务是基于Spring Aop的扩展 AOP的知识参见<Spring 源码学习笔记10--Spring AOP> 图片参考了 ...

  5. spring源码分析系列 (8) FactoryBean工厂类机制

    更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...

  6. spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析

    更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...

  7. spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor简述与demo示例 ...

  8. spring源码分析系列 (2) spring拓展接口BeanPostProcessor

    Spring更多分析--spring源码分析系列 主要分析内容: 一.BeanPostProcessor简述与demo示例 二.BeanPostProcessor源码分析:注册时机和触发点 (源码基于 ...

  9. Spring源码剖析2:Spring IOC容器的加载过程

    spring ioc 容器的加载流程 1.目标:熟练使用spring,并分析其源码,了解其中的思想.这篇主要介绍spring ioc 容器的加载 2.前提条件:会使用debug 3.源码分析方法:In ...

随机推荐

  1. 使用grep过滤make的输出内容

    make的输出内容其实分为两种,有些是到标准输出,有些是到标准错误,由于标准输出和标准错误默认都是屏幕,所以平时区分不出来, 实际上一般是error和warning信息到标准错误,其余的到标准输出. ...

  2. Docker安装方法整理

    目录 安装准备 在线安装 离线安装 Raspbian便捷脚本安装 卸载 安装准备: 卸载旧版本 较旧版本的Docker被称为docker或docker-engine.如果已安装,请卸载它们: sudo ...

  3. ArcGIS Engine制作DIY地图工具

    本节将向你介绍,利用ToolStrip制作自定义GIS工具条. 步骤如下: ①向ToolStrip中添加一个Button ②向该Button的lmg属性添加图片素材,并将Button的图片比例(Ima ...

  4. 盘点一下Creator星球上的开源工具包!

    晓衡开始写公众号,最早是从上架 Cocos 商店的 pbkiller 插件开始的,到至今有2年2个月了.在这期间,又陆续在公众号上分享了多个实用工具包,在这里统一盘点一下,方便与大家交流学习. 一.u ...

  5. CTR@因子分解机(FM)

    1. FM算法 FM(Factor Machine,因子分解机)算法是一种基于矩阵分解的机器学习算法,为了解决大规模稀疏数据中的特征组合问题.FM算法是推荐领域被验证效果较好的推荐算法之一,在电商.广 ...

  6. Mysql常用数据类型归纳总结1

    一直在用Mysql数据库,Mysql的数据类型也最常打交道的.但关于Mysql的一些常用数据类型了解程度仅限于一知半解,仅仅能满足满足于平时一些最简单的操作.而Mysql常用数据类型的定义以及规范理解 ...

  7. 学习笔记25_MVC前台API

    **当请求url的规则有所改变时,前台的所有超链接的href都得改,为了避免多处修改,可以href = "< %=url.Action("Controller",& ...

  8. [考试反思]0926csp-s模拟测试52:审判

    也好. 该来的迟早会来. 反思再说吧. 向下跳过直到另一条分界线 %%%cbx也拿到了他的第一个AK了呢. 我的还是遥不可及. 我恨你,DeepinC. 我恨透你了.你亲手埋葬所有希望,令我无比气愤. ...

  9. Function题解

    这个题最优策略一定是向左上走到某一列再往上一直走. n*q在线暴力可做,离线按y排序,单调栈维护凸壳. 具体来说:对于i<j若A[i]>A[j] 即j的斜率小而且纵截距小,一定比i优,并且 ...

  10. Redis集群--Redis集群之哨兵模式

    echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!!! 搭建R ...