简介

在JAVA体系中,有支持实现事件监听机制,在Spring 中也专门提供了一套事件机制的接口,方便我们实现。比如我们可以实现当用户注册后,给他发送一封邮件告诉他注册成功的一些信息,比如用户订阅的主题更新了,通知用户注意及时查看等。

观察者模式

观察者模式还有很多其他的称谓,如发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

观察者模式一般包含以下几个对象:

Subject:

被观察的对象。它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。

ConcreteSubject:

具体的观察对象。Subject的具体实现类,在这里实现通知事件。

Observer:

观察者。这里是抽象的观察者,观察者有一个或者多个。

ConcreteObserver:

具体的观察者。在这里维护观察对象的具体操作。

Java 中的事件机制

Java中提供了基本的事件处理基类:

  1. EventObject:所有事件状态对象都将从其派生的根类;
  2. EventListener:所有事件侦听器接口必须扩展的标记接口;

非常经典的开门案例:

一、创建事件对象

@Getter
@Setter
public class DoorEvent extends EventObject{ int state; public DoorEvent(Object source){
super(source);
}
public DoorEvent(Object source,int state){
super(source);
this.state = state;
}
}

二、事件监听器

public interface DoorListener extends EventListener{
void doorEvent(DoorEvent doorEvent);
}
public class CloseDoorEvent implements DoorListener{
@Override
public void doorEvent(DoorEvent doorEvent){
if(doorEvent.getState() == -1){
System.out.println("门关上了");
}
}
}
public class OpenDoorListener implements DoorListener{
@Override
public void doorEvent(DoorEvent doorEvent){
if(doorEvent.getState() == 1){
System.out.println("门打开了");
}
}
}

三、测试

public static void main(String[] args){
List<DoorListener> list = new ArrayList<>();
list.add(new OpenDoorListener());
list.add(new CloseDoorEvent());
for(DoorListener listener : list){
listener.doorEvent(new DoorEvent(-1,-1));
listener.doorEvent(new DoorEvent(1,1));
}
}

四、输出结果

门打开了
门关上了

Spring 中的事件机制

在 Spring 容器中通过ApplicationEven类和 ApplicationListener接口来实现事件监听机制,每次Event 被发布到Spring容器中时都会通知该Listener。需要注意的是,Spring 的事件默认是同步的,调用 publishEvent 方法发布事件后,它会处于阻塞状态,直到Listener接收到事件并处理返回之后才继续执行下去。

代码示例:

一、定义事件对象

@Getter
@Setter
@ToString
public class UserDTO extends ApplicationEvent{
private Integer userId;
private String name;
private Integer age; public UserDTO(Object source){
super(source);
}
}

二、定义事件监听器,可以通过注解或者实现接口来实现。

@Component
public class UserRegisterSmsListener{ // 通过注解实现监听器
@EventListener
public void handleUserEvent(UserDTO userDTO){
System.out.println("监听到用户注册,准备发送短信,user:"+userDTO.toString());
}
} // 通过实现接口实现监听器
@Component
public class UserRegisterEmailListener implements ApplicationListener<UserDTO>{
@Override
public void onApplicationEvent(UserDTO userDTO){
System.out.println("监听到用户注册,准备发送邮件,user:" + userDTO.toString());
}
}
@Component
public class UserRegisterMessageListener implements ApplicationListener<UserDTO>{
@Override
public void onApplicationEvent(UserDTO userDTO){
System.out.println("监听到用户注册,给新用户发送首条站内短消息,user:" + userDTO.toString());
}
}

三、注册服务

public interface UserService{
void register();
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
public void register(){
UserDTO userDTO = new UserDTO(this);
userDTO.setAge(18);
userDTO.setName("admol");
userDTO.setUserId(1001);
System.out.println("register user");
eventPublisher.publishEvent(userDTO);
}
}

四、测试

@Autowired
private UserService userService; @Test
public void testUserEvent(){
userService.register();
}

五、输出结果

register user
监听到用户注册,准备发送短信,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,准备发送邮件,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,给新用户发送首条站内短消息,user:UserDTO(userId=1001, name=admol, age=18)

指定监听器的顺序

监听器的发布顺序是按照 bean 自然装载的顺序执行的,Spring 支持两种方式来实现有序

一、实现SmartApplicationListener接口指定顺序。

把上面三个Listener都改成实现SmartApplicationListener接口,并指定getOrder的返回值,返回值越小,优先级越高。

@Component
public class UserRegisterMessageListener implements SmartApplicationListener{ @Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType){
return eventType == UserDTO.class;
} @Override
public boolean supportsSourceType(Class<?> sourceType){
return true;
} @Override
public void onApplicationEvent(ApplicationEvent event){
System.out.println("监听到用户注册,给新用户发送首条站内短消息,user:" + event.toString());
} @Override
public int getOrder(){
return -1;
}
}

另外两个监听器的改造省略,指定改造后的UserRegisterSmsListener返回order为0,UserRegisterEmailListener的getOrder返回1,测试输出结果如下:

register user
监听到用户注册,给新用户发送首条站内短消息,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,准备发送短信,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,准备发送邮件,user:UserDTO(userId=1001, name=admol, age=18)

二、使用注解@Order()

@Component
public class UserRegisterSmsListener{ @Order(-2)
@EventListener
public void handleUserEvent(UserDTO userDTO){
System.out.println("监听到用户注册,准备发送短信,user:"+userDTO.toString());
}
}

测试输出结果如下:

register user
监听到用户注册,准备发送短信,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,给新用户发送首条站内短消息,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,准备发送邮件,user:UserDTO(userId=1001, name=admol, age=18)

可以发现,短信监听器最先执行。

异步支持

Spring 事件机制默认是同步阻塞的,如果 ApplicationEventPublisher 发布事件之后他会一直阻塞等待listener 响应,多个 listener 的情况下前面的没有执行完后面的会一直被阻塞。这时候我们可以利用 Spring 提供的线程池注解 @Async 来实现异步线程

一、使用 @Async 之前需要先开启线程池,在 启动类上添加 @EnableAsync 注解即可。

@EnableAsync
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

二、监听器使用异步线程

自定义异步线程池

@Configuration
public class AsyncConfig{ @Bean("asyncThreadPool")
public Executor getAsyncExecutor(){
System.out.println("asyncThreadPool init");
Executor executor = new ThreadPoolExecutor(
10,20,60L,TimeUnit.SECONDS
,new ArrayBlockingQueue<>(100),new MyThreadFactory());
return executor;
} class MyThreadFactory implements ThreadFactory{
final AtomicInteger threadNumber = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r){
Thread t = new Thread(r);
t.setName("async-thread-"+threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
}
}
}

指定监听器的线程池

@Component
public class UserRegisterSmsListener{ @Order(-2)
@Async("asyncThreadPool")
@EventListener
public void handleUserEvent(UserDTO userDTO){
System.out.println(Thread.currentThread().getName() + " 监听到用户注册,准备发送短信,user:"+userDTO.toString());
}
}

三、测试输出结果

register user
监听到用户注册,给新用户发送首条站内短消息,user:UserDTO(userId=1001, name=admol, age=18)
监听到用户注册,准备发送邮件,user:UserDTO(userId=1001, name=admol, age=18)
async-thread-0 监听到用户注册,准备发送短信,user:UserDTO(userId=1001, name=admol, age=18)

Spring事件机制原理分析

Spring事件机制涉及的重要类主要有以下四个:

ApplicationEvent:

事件对象,继承至JDK的类EventObject ,可以携带事件的时间戳

ApplicationListener:

事件监听器,继承至JDK的接口EventListener,该接口被所有的事件监听器实现,比如支持指定顺序的SmartApplicationListener

ApplicationEventMulticaster:

事件管理者,管理监听器和发布事件,ApplicationContext通过委托ApplicationEventMulticaster来 发布事件

ApplicationEventPublisher:

事件发布者,该接口封装了事件有关的公共方法,作为ApplicationContext的超级街廓,也是委托 ApplicationEventMulticaster完成事件发布。

源码展示

ApplicationEvent

事件对象ApplicationEvent的主要源代码如下,继承了JAVA的 EventObject 对象:

public abstract class ApplicationEvent extends EventObject {
private static final long serialVersionUID = 7099057708183571937L;
private final long timestamp; // 多了一个时间戳属性
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis(); // 初始当前化时间戳
}
public final long getTimestamp() {
return this.timestamp;
}
}

从上面ApplicationEvent的子类关系图种可以发现,ApplicationEvent有一个重要的子类ApplicationContextEvent,而ApplicationContextEvent又有4个重要的子类ContextStartedEventContextRefreshedEventContextClosedEventContextStoppedEvent

从名字就可以看出,这4个事件都和Spring容器有关系的:

  • ContextRefreshedEvent:当spring容器context刷新时触发
  • ContextStartedEvent:当spring容器context启动后触发
  • ContextStoppedEvent:当spring容器context停止时触发
  • ContextClosedEvent:当spring容器context关闭时触发,容器被关闭时,其管理的所有单例Bean都被销毁。

当每个事件触发时,相关的监听器就会监听到相应事件,然后触发onApplicationEvent方法。

ApplicationListener

事件监听器,继承DK的接口EventListener

/* ...
* @author Rod Johnson
* @author Juergen Hoeller
* @param <E> the specific ApplicationEvent subclass to listen to
* @see org.springframework.context.event.ApplicationEventMulticaster
*/
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /**
* Handle an application event. by jinglingwang.cn
* @param event the event to respond to
*/
void onApplicationEvent(E event); }

注释@param <E> the specific ApplicationEvent subclass to listen to@see ApplicationEventMulticaster 里面说明了事件的广播在ApplicationEventMulticaster类。

ApplicationEventMulticaster

ApplicationEventMulticaster是一个接口,负责管理监听器和发布事件,定义了如下方法:

  1. addApplicationListener(ApplicationListener<?> listener) :新增一个listener;
  2. addApplicationListenerBean(String listenerBeanName):新增一个listener,参数为bean name;
  3. removeApplicationListener(ApplicationListener<?> listener):删除listener;
  4. void removeAllListeners():删除所有的Listener
  5. removeApplicationListenerBean(String listenerBeanName):根据bean name 删除listener;
  6. multicastEvent(ApplicationEvent event):广播事件;
  7. multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType):广播事件,指定事件的source类型。

AbstractApplicationEventMulticaster 实现了 ApplicationEventMulticaster接口,SimpleApplicationEventMulticaster 继承了AbstractApplicationEventMulticaster ;

  1. AbstractApplicationEventMulticaster 主要实现了管理监听器的方法(上面接口的前5个方法)

  2. SimpleApplicationEventMulticaster 主要实现了事件广播相关的方法(上面接口的最后2个方法)

    两个类分别继承了部分上面的方法。

一、先看新增Listener方法实现逻辑:

public abstract class AbstractApplicationEventMulticaster
implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware { private final ListenerRetriever defaultRetriever = new ListenerRetriever(false); ...
@Override
public void addApplicationListener(ApplicationListener<?> listener) {
synchronized (this.retrievalMutex) { // 加排他锁
// Explicitly remove target for a proxy, if registered already,
// in order to avoid double invocations of the same listener.
Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
if (singletonTarget instanceof ApplicationListener) {
// 删除,避免重复调用
this.defaultRetriever.applicationListeners.remove(singletonTarget);
}
// 加入到Set LinkedHashSet 集合中
this.defaultRetriever.applicationListeners.add(listener);
this.retrieverCache.clear(); // 缓存
}
}
...
}

最核心的一句代码:this.defaultRetriever.applicationListeners.add(listener);

ListenerRetriever类是AbstractApplicationEventMulticaster类的内部类,里面有两个集合,用来记录维护事件监听器。

private class ListenerRetriever {

		public final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
public final Set<String> applicationListenerBeans = new LinkedHashSet<>();
...
}

这就和设计模式中的发布订阅模式一样了,维护一个List,用来管理所有的订阅者,当发布者发布消息时,遍历对应的订阅者列表,执行各自的回调handler。

二、看SimpleApplicationEventMulticaster类实现的广播事件逻辑:

@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event)); // 继续调用下面的广播方法
} @Override
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
// 遍历监听器列表
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) { // 是否指定了线程池
executor.execute(new Runnable() {
@Override
public void run() { // 线程池执行
invokeListener(listener, event);
}
});
}
else { // 普通执行
invokeListener(listener, event);
}
}
}

代码分析:

  1. 首先根据事件类型,获取事件监听器列表:getApplicationListeners(event, type)
  2. 遍历监听器列表,for循环
  3. 判断是否有线程池,如果有,在线程池执行
  4. 否则直接执行

我们再看看 invokeListener方法的逻辑:

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) { // 是否有错误处理
try {
doInvokeListener(listener, event);
} catch (Throwable err) {
errorHandler.handleError(err);
}
} else {
doInvokeListener(listener, event); // 直接执行
}
}

核心逻辑就是继续调用doInvokeListener方法:

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);// 执行监听器事件
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || msg.startsWith(event.getClass().getName())) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}

发现最后实际就是调用的 listener.onApplicationEvent(event); 也就是我们通过实现接口ApplicationListener的方式来实现监听器的onApplicationEvent实现逻辑。

ApplicationEventPublisher类

在我们的发布事件逻辑代码的地方,通过查看 eventPublisher.publishEvent(userDTO);方法可以发现ApplicationEventPublisher是一个接口,publishEvent方法的逻辑实现主要在类AbstractApplicationContext中:

public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext, DisposableBean {
...
private Set<ApplicationEvent> earlyApplicationEvents;
...
@Override
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null); // 调用下面的方法
}
// 发布事件主要逻辑
protected void publishEvent(Object event, ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Publishing event in " + getDisplayName() + ": " + event);
} // 事件装饰为 ApplicationEvent
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
} else {
applicationEvent = new PayloadApplicationEvent<Object>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType();
}
} // 容器启动的时候 earlyApplicationEvents 可能还没有初始化
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent); // 加入到集合,同一广播
} else {
// 还没初始化,直接广播事件
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
} // 通过父上下文发布事件.
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}
...
}

这段代码的主要逻辑在这:

if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}

可以发现earlyApplicationEvents也是一个Set集合,如果这个集合已经初始化了,就把事件加入到集合中,否则直接调用multicastEvent执行事件监听逻辑。

我们跟踪找到初始化这个集合的地方,发现在方法protected void prepareRefresh()中:

protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true); if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
} initPropertySources(); getEnvironment().validateRequiredProperties(); **this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();**
}

继续跟踪调用这个方法的地方,发现在AbstractApplicationContext.refresh()方法中,而这个方法是Spring容器初始化必须要调用的过程,非常的重要。

那在什么地方使用到了这个集合呢?我们继续跟踪发现在 protected void registerListeners() 方法中,代码如下:

protected void registerListeners() {
// Register statically specified listeners first.
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
} // Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them! jinglingwang.cn
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
} // 拿到集合引用
Set<ApplicationEvent> ****earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null; // 把之前的集合置为null
if (earlyEventsToProcess != null) { // 如果集合不为空,则广播里面的事件
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}

逻辑是先获得该集合的引用,然后置空之前的集合,然后遍历集合,进行广播事件multicastEvent,这个方法的逻辑上面已经说过了。

而registerListeners这个方法是在什么时候调用的呢?通过跟踪发现也是在AbstractApplicationContext.refresh()方法中。

只不过基本是在方法逻辑的最后,也就是Spring已经容器初始化完成了。

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
**prepareRefresh**(); ....
try {
onRefresh(); // Check for listener beans and register them.
**registerListeners**(); // Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event.
**finishRefresh**();
} catch (BeansException ex) {
...
} finally {
...
}
}
}

容器初始化之前和之后都有可能进行广播事件。

总结

  1. 事件监听机制和观察者模式非常相似
  2. JDK 也有实现提供事件监听机制
  3. Spring 的事件机制也是基于JDK 来扩展的
  4. Spring 的事件机制默认是同步阻塞的
  5. Spring 容器初始化前后都可能进行广播事件

Spring 事件监听机制及原理分析的更多相关文章

  1. SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)

    SpringBoot中文注释项目Github地址: https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE 本篇接 SpringApplicat ...

  2. Spring事件监听机制源码解析

    Spring事件监听器使用 1.Spring事件监听体系包括三个组件:事件.事件监听器,事件广播器. 事件:定义事件类型和事件源,需要继承ApplicationEvent. package com.y ...

  3. Spring事件监听机制

    前言 Spring中的事件机制其实就是设计模式中的观察者模式,主要由以下角色构成: 事件 事件监听器(监听并处理事件) 事件发布者(发布事件) 首先看一下监听器和发布者的接口定义 public int ...

  4. SpringBoot事件监听机制及观察者模式/发布订阅模式

    目录 本篇要点 什么是观察者模式? 发布订阅模式是什么? Spring事件监听机制概述 SpringBoot事件监听 定义注册事件 注解方式 @EventListener定义监听器 实现Applica ...

  5. Java swing(awt):事件监听机制的实现原理+简单示例

    (1)实现原理 事件监听机制的实现: 参考图:事件模型_ActionEvent 为了节省资源,系统无法对某个事件进行实时的监听.故实现的机制是当发生某个事件后,处理代码将被自动运行,类似钩子一般.(回 ...

  6. Spring ApplicationContext(八)事件监听机制

    Spring ApplicationContext(八)事件监听机制 本节则重点关注的是 Spring 的事件监听机制,主要是第 8 步:多播器注册:第 10 步:事件注册. public void ...

  7. Spring的事件监听机制

    最近公司在重构广告系统,其中核心的打包功能由广告系统调用,即对apk打包的调用和打包完成之后的回调,需要提供相应的接口给广告系统.因此,为了将apk打包的核心流程和对接广告系统的业务解耦,利用了spr ...

  8. 7_3.springboot2.x启动配置原理_3.事件监听机制

    事件监听机制配置在META-INF/spring.factories ApplicationContextInitializer SpringApplicationRunListenerioc容器中的 ...

  9. GUI编程笔记(java)05:GUI事件监听机制原理和举例说明

    1.事件监听机制:       A:事件源          事件发生的地方       B:事件             就是要发生的事情       C:事件处理       就是针对发生的事情做 ...

随机推荐

  1. 【django-simpleui】‘simpletags‘ is not a registered tag library报错的解决方法

    1:创建  templatetags文件夹 2:创建simpletags.py文件将内容粘贴进去,在下面 3:setting.py添加文件指定: 1 TEMPLATES = [ 2 { 3 'BACK ...

  2. Learn day1 变量/数据类型

    1.Python 简介 (1) 1989年开发的语言,创始人范罗苏姆(Guido van Rossum),别称:龟叔(Guido). (2) python具有非常多并且强大的第三方库,使得程序开发起来 ...

  3. python数据类型之String(字符串)

    String(字符串) 1.概述 ​ 字符串是以单引号或双引号括起来的任意文本,比如"abc",'xy'等等,请注意''或者""本身只是一种表示方式,并不是字符 ...

  4. Linux开机启动顺序启动顺序及配置开机启动

    Linux:开机启动顺序启动顺序及配置开机启动 开机启动顺序 1.加载内核 2.启动 init(/etc/inittab) pid=1 3.系统初始化 /etc/rc.d/rc.sysinit 4.运 ...

  5. python开发基础(一)-if条件判断,while循环,break,continue,

    条件语句 (1)if 基本语句 if 条件 : 内部代码块 else: .... print() (2)if 嵌套 (3)if elif 语句 (4)if 1==1: pass # if不执行,pas ...

  6. day83:luffy:添加购物车&导航栏购物车数字显示&购物车页面展示

    目录 1.添加购物车+验证登录状态 2.右上方购物车图标的小红圆圈数字 3.Vuex 4.购物车页面展示-后端接口 5.购物车页面展示-前端 6.解决一个购物车数量显示混乱的bug 1.添加购物车+验 ...

  7. 初次使用flask

    以写的一个小的例子来记录第一次使用: from flask import Flask, render_template import json # 实例化,可视为固定格式 app = Flask(__ ...

  8. Git操作:远程仓库(git remote)的添加、管理和删除

    这是你的git仓库,他已经添加了一个远程仓库,可以用git remote -v查看绑定的仓库列表,他会以<仓库名>  <仓库地址>的形式展示出来(一个仓库会显示两遍): $ g ...

  9. Spider--补充--jsonpath的使用

    # 知识点参见:https://blog.csdn.net/muzico425/article/details/102763176 # 示例:爬取示例网站的首页的评论: # 解析得到的字符串r.tex ...

  10. 手把手教你使用Vuex(四)

    3.Action Action类似于mutation,不同之处在于: Action提交的是mutation,而不是直接变更状态 Action可以包含任何异步操作 可以理解为将mutations里面处理 ...