框架

SpringBoot集成RabbitMQ

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

一、pom文件增加引入

需要引入spring-boot-starter-amqp包,为我们提供RabbitMQ相关消息处理的jar包,具体如下。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

修改完毕后,弹出maven引入依赖提示,点击Import Changes。

二、配置

修改application.yaml配置,增加rabbitMQ配置,注意yaml中的中文注释去掉,我自己启动时候发现有中文无法启动,我写注释是为了大家能够理解配置的意思。

server:
  port: 8082
  servlet:
    context-path: /hello-world-new

spring:
  cache:
    type: ehcache
    config: classpath:ehcache.xml
#  MYSql配置
#  datasource:
#    url:
#    username:
#    password:
#    driver-class-name: com.mysql.cj.jdbc.Driver
  datasource:
    driver-class-name: org.sqlite.JDBC
    url: jdbc:sqlite:/Users/XuesongBu/Documents/git_code/hello-world/hello-world.db
    username:
    password:
  #针对SQLite配置方言,MySQL不需要该配置
  jpa:
    database-platform: com.enigmabridge.hibernate.dialect.SQLiteDialect
  #RabbitMQ服务器配置,地址账号密码,virtualhost等配置
  rabbitmq:
    addresses: 修改为自己RabbitMQ服务器地址
    username: 修改为自己的RabbitMQ账号
    password: 修改为自己的RabbitMQ密码
    virtual-host: 修改为自己的RabbitMQ的virtual-host

三、java实现Consumer消费者部分

增加RabbitMQConsumerConfig.java配置消费者连接,以及初始化queue,exchange,routingKey,也就是如果RabbitMQ服务器没有queue,exchange,routingKey,自动创建。

package com.example.demo;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class RabbitMQConsumerConfig {

    /**
     * 注入RabbitMQClinet的brokerContainerFactory
     * 如果RabbitMQ服务器没有对应的Queue,exchange,routing key,则初始化新建Queue,exchange,routing key
     * @param configurer
     * @param connectionFactory
     * @return
     */
    @Bean(name = "brokerContainerFactory")
    public SimpleRabbitListenerContainerFactory brokerContainerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
                                                                       ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        // 手工ACK
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        // 最小的消费者数
        factory.setConcurrentConsumers(3);
        // 消费者最大数
        factory.setMaxConcurrentConsumers(5);
        // 预拉取条数
        factory.setPrefetchCount(5);
        // 初始化并新建
        try (Connection connection = connectionFactory.createConnection();
             Channel channel = connection.createChannel(false)) {
            channel.queueDeclare("QUEUE_NAME", true, false, false, null);
            channel.exchangeDeclare("EXCHANGE_NAME", BuiltinExchangeType.DIRECT);
            channel.queueBind("QUEUE_NAME", "EXCHANGE_NAME", "ROUTING_KEY");
        } catch (Exception e) {
            log.info("Declare and bind queue error!", e);
        }
        return factory;
    }
}

新增MessageDto.java,接收消息实体。

package com.example.demo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.io.Serializable;

@Data
public class MessageDto implements Serializable {
    @JsonProperty("content")
    private String content;
}

新增MessageConsumer.java,RabbitMQ消息消费监听类,负责消息的接收处理。

package com.example.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
public class MessageConsumer {

    private static final ObjectMapper mapper = new ObjectMapper();
    /**
     * 监听消息队列,队列名称QUEUE_NAME
     * 通过brokerContainerFactory获取到对应的queue
     */
    @RabbitListener(queues = "QUEUE_NAME", containerFactory = "brokerContainerFactory")
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            log.info("Consumed message: {}", message);
            MessageDto dto = parse(new String(message.getBody()), MessageDto.class);
            log.info("Consumed message content: {}", dto);
            // 由于之前配置的手动ack,需要手动回调rabbitMQ服务器,通知已经完成消费
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch(Exception e) {
            log.error("Consumed erorr,", e);
            // 由于之前配置的手动ack,需要手动回调rabbitMQ服务器,通知出现问题
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    public static <T> T parse(String json, Class<T> clazz) {
        try {
            return mapper.readValue(json, clazz);
        } catch (IOException e) {
            log.error("IOException, json = {}, clazz = {}", json, clazz, e);
            try {
                return clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException e1) {
                log.error("InstantiationException or IllegalAccessException, clazz = {}", clazz, e1);
                return null;
            }
        }
    }
}

四、java实现Producer生产者部分

新增RabbitMQProducerConfig.java,生产者配置类,负责创建RabbitTemplate,提供消息发送的工具类。

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class RabbitMQProducerConfig {

    /**
     * 配置生产者rabbitTemplate
     * 生产者只需要配置exchange和routingKey
     */
    @Bean(name = "pointRabbitTemplate")
    public RabbitTemplate pointRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setExchange("EXCHANGE_NAME");
        rabbitTemplate.setRoutingKey("ROUTING_KEY");
        rabbitTemplate.setMandatory(true);
        return rabbitTemplate;
    }
}

新建MessageProducer.java,调用RabbitTemplate的convertAndSend发送方法来发送消息,注意消息发送前需要先序列化再发送。

package com.example.demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class MessageProducer {

    private static final ObjectMapper mapper = new ObjectMapper();
    @Autowired
    private RabbitTemplate pointRabbitTemplate;

    /**
     * 发送消息
     * @param content
     */
    public void sendMessageToMQ(String content) {
        MessageDto dto = new MessageDto();
        dto.setContent(content);
        pointRabbitTemplate.convertAndSend(this.format(dto));
    }

    public static String format(Object pojo) {
        try {
            return mapper.writeValueAsString(pojo);
        } catch (JsonProcessingException e) {
            log.error("JsonProcessingException, pojo = {}", pojo, e);
            return "{}";
        }
    }
}

新建RabbitMQController.java,负责提供rest接口,以方便测试消息发送。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RabbitMQController {
    @Autowired
    private MessageProducer messageProducer;

    @PostMapping("/sendMQMessage")
    public String sendMQMessage(@RequestParam String content) {
        messageProducer.sendMessageToMQ(content);
        return "ok";
    }
}

五、Postman验证

启动服务后,首先通过postman的post方式调用http://localhost:8082/hello-world-new/sendMQMessage接口,来发送消息

查看后台日志显示,消息Consumer消费者输出了日志,说明测试成功。

六、总结

以上就是咱们常用的SpringBoot项目集成RabbitMQ的方法,可以说不复杂,按照我上面的配置即可实现。

原文地址:https://zhuanlan.zhihu.com/p/583835016

23 种设计模式详解(全23种)

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

设计模式的分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一、创建模式(5种)

工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

1 工厂模式

1.1 简单工厂模式

定义:定义了一个创建对象的类,由这个类来封装实例化对象的行为。

举例:(我们举一个pizza工厂的例子)

pizza工厂一共生产三种类型的pizza:chesse,pepper,greak。通过工厂类(SimplePizzaFactory)实例化这三种类型的对象。类图如下:

工厂类的代码:

public class SimplePizzaFactory {
       public Pizza CreatePizza(String ordertype) {
              Pizza pizza = null;
              if (ordertype.equals("cheese")) {
                     pizza = new CheesePizza();
              } else if (ordertype.equals("greek")) {
                     pizza = new GreekPizza();
              } else if (ordertype.equals("pepper")) {
                     pizza = new PepperPizza();
              }
              return pizza;
       }
}

简单工厂存在的问题与解决方法: 简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了开闭原则,所以,从设计角度考虑,有一定的问题,如何解决?我们可以定义一个创建对象的抽象方法并创建多个不同的工厂类实现该抽象方法,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。这种方法也就是我们接下来要说的工厂方法模式。

1.2 工厂方法模式

定义:定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。

举例:(我们依然举pizza工厂的例子,不过这个例子中,pizza产地有两个:伦敦和纽约)。添加了一个新的产地,如果用简单工厂模式的的话,我们要去修改工厂代码,并且会增加一堆的if else语句。而工厂方法模式克服了简单工厂要修改代码的缺点,它会直接创建两个工厂,纽约工厂和伦敦工厂。类图如下:

OrderPizza中有个抽象的方法:

abstract Pizza createPizza();

两个工厂类继承OrderPizza并实现抽象方法:

public class LDOrderPizza extends OrderPizza {
       Pizza createPizza(String ordertype) {
              Pizza pizza = null;
              if (ordertype.equals("cheese")) {
                     pizza = new LDCheesePizza();
              } else if (ordertype.equals("pepper")) {
                     pizza = new LDPepperPizza();
              }
              return pizza;
       }
}
public class NYOrderPizza extends OrderPizza {
 
	Pizza createPizza(String ordertype) {
		Pizza pizza = null;
 
		if (ordertype.equals("cheese")) {
			pizza = new NYCheesePizza();
		} else if (ordertype.equals("pepper")) {
			pizza = new NYPepperPizza();
		}
		return pizza;
 
	}
 
}

通过不同的工厂会得到不同的实例化的对象,PizzaStroe的代码如下:

public class PizzaStroe {
       public static void main(String[] args) {
              OrderPizza mOrderPizza;
              mOrderPizza = new NYOrderPizza();
       }
}

其实这个模式的好处就是,如果你现在想增加一个功能,只需做一个实现类就OK了,无需去改动现成的代码。这样做,拓展性较好!

工厂方法存在的问题与解决方法:客户端需要创建类的具体的实例。简单来说就是用户要订纽约工厂的披萨,他必须去纽约工厂,想订伦敦工厂的披萨,必须去伦敦工厂。 当伦敦工厂和纽约工厂发生变化了,用户也要跟着变化,这无疑就增加了用户的操作复杂性。为了解决这一问题,我们可以把工厂类抽象为接口,用户只需要去找默认的工厂提出自己的需求(传入参数),便能得到自己想要产品,而不用根据产品去寻找不同的工厂,方便用户操作。这也就是我们接下来要说的抽象工厂模式。

1.3 抽象工厂模式

定义:定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类。

举例:(我们依然举pizza工厂的例子,pizza工厂有两个:纽约工厂和伦敦工厂)。类图如下:

工厂的接口:

public interface AbsFactory {
       Pizza CreatePizza(String ordertype) ;
}

工厂的实现:

public class LDFactory implements AbsFactory {
       @Override
       public Pizza CreatePizza(String ordertype) {
              Pizza pizza = null;
              if ("cheese".equals(ordertype)) {
                     pizza = new LDCheesePizza();
              } else if ("pepper".equals(ordertype)) {
                     pizza = new LDPepperPizza();
              }
              return pizza;
       }
}

PizzaStroe的代码如下:

public class PizzaStroe {
       public static void main(String[] args) {
              OrderPizza mOrderPizza;
              mOrderPizza = new OrderPizza("London");
       }
}

解决了工厂方法模式的问题:在抽象工厂中PizzaStroe中只需要传入参数就可以实例化对象。

1.4 工厂模式适用的场合

大量的产品需要创建,并且这些产品具有共同的接口 。

1.5 三种工厂模式的使用选择

简单工厂 : 用来生产同一等级结构中的任意产品。(不支持拓展增加产品)

工厂方法 :用来生产同一等级结构中的固定产品。(支持拓展增加产品)

抽象工厂 :用来生产不同产品族的全部产品。(支持拓展增加产品;支持增加产品族)

简单工厂的适用场合:只有伦敦工厂(只有这一个等级),并且这个工厂只生产三种类型的pizza:chesse,pepper,greak(固定产品)。

工厂方法的适用场合:现在不光有伦敦工厂,还增设了纽约工厂(仍然是同一等级结构,但是支持了产品的拓展),这两个工厂依然只生产三种类型的pizza:chesse,pepper,greak(固定产品)。

抽象工厂的适用场合:不光增设了纽约工厂(仍然是同一等级结构,但是支持了产品的拓展),这两个工厂还增加了一种新的类型的pizza:chinese pizza(增加产品族)。

所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线。因此,我们可以用抽象工厂模式创建工厂,而用工厂方法模式创建生产线。比如,我们可以使用抽象工厂模式创建伦敦工厂和纽约工厂,使用工厂方法实现cheese pizza和greak pizza的生产。类图如下:

总结一下三种模式:

简单工厂模式就是建立一个实例化对象的类,在该类中对多个对象实例化。工厂方法模式是定义了一个创建对象的抽象方法,由子类决定要实例化的类。这样做的好处是再有新的类型的对象需要实例化只要增加子类即可。抽象工厂模式定义了一个接口用于创建对象族,而无需明确指定具体类。抽象工厂也是把对象的实例化交给了子类,即支持拓展。同时提供给客户端接口,避免了用户直接操作子类工厂。


2 单例模式

定义:确保一个类最多只有一个实例,并提供一个全局访问点

单例模式可以分为两种:预加载和懒加载

2.1 预加载

顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。

public class PreloadSingleton {
       
       public static PreloadSingleton instance = new PreloadSingleton();
   
       //其他的类无法实例化单例类的对象
       private PreloadSingleton() {
       };
       
       public static PreloadSingleton getInstance() {
              return instance;
       }
}

很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。

2.2 懒加载

为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。

public class Singleton {
       
       private static Singleton instance=null;
       
       private Singleton(){
       };
       
       public static Singleton getInstance()
       {
              if(instance==null)
              {
                     instance=new Singleton();
              }
              return instance;
              
       }
}

2.3 单例模式和线程安全

(1)预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。

(2)懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。

不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:

memory=allocate();//1:初始化内存空间
 
ctorInstance(memory);//2:初始化对象
 
instance=memory();//3:设置instance指向刚分配的内存地址

jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。

2.4 保证懒加载的线程安全

我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     instance = new Singleton();
              }
              return instance;
       }
}

我们把sychronized加在if(instance==null)判断语句里面,保证instance未实例化的时候才加锁

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (Singleton.class) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

我们经过2.3的讨论知道new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。

public class Singleton {
       private static volatile Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (instance) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

到此,我们就保证了懒加载的线程安全。


3 生成器模式

定义:封装一个复杂对象构造过程,并允许按步骤构造。

定义解释: 我们可以将生成器模式理解为,假设我们有一个对象需要建立,这个对象是由多个组件(Component)组合而成,每个组件的建立都比较复杂,但运用组件来建立所需的对象非常简单,所以我们就可以将构建复杂组件的步骤与运用组件构建对象分离,使用builder模式可以建立。

3.1 模式的结构和代码示例

生成器模式结构中包括四种角色:

(1)产品(Product):具体生产器要构造的复杂对象;

(2)抽象生成器(Bulider):抽象生成器是一个接口,该接口除了为创建一个Product对象的各个组件定义了若干个方法之外,还要定义返回Product对象的方法(定义构造步骤);

(3)具体生产器(ConcreteBuilder):实现Builder接口的类,具体生成器将实现Builder接口所定义的方法(生产各个组件);

(4)指挥者(Director):指挥者是一个类,该类需要含有Builder接口声明的变量。指挥者的职责是负责向用户提供具体生成器,即指挥者将请求具体生成器类来构造用户所需要的Product对象,如果所请求的具体生成器成功地构造出Product对象,指挥者就可以让该具体生产器返回所构造的Product对象。(按照步骤组装部件,并返回Product)

举例(我们如果构建生成一台电脑,那么我们可能需要这么几个步骤(1)需要一个主机(2)需要一个显示器(3)需要一个键盘(4)需要一个鼠标)

虽然我们具体在构建一台主机的时候,每个对象的实际步骤是不一样的,比如,有的对象构建了i7cpu的主机,有的对象构建了i5cpu的主机,有的对象构建了普通键盘,有的对象构建了机械键盘等。但不管怎样,你总是需要经过一个步骤就是构建一台主机,一台键盘。对于这个例子,我们就可以使用生成器模式来生成一台电脑,他需要通过多个步骤来生成。类图如下:

ComputerBuilder类定义构造步骤:

public abstract class ComputerBuilder {
   
    protected Computer computer;
   
    public Computer getComputer() {
        return computer;
    }
   
    public void buildComputer() {
        computer = new Computer();
        System.out.println("生成了一台电脑!!!");
    }
    public abstract void buildMaster();
    public abstract void buildScreen();
    public abstract void buildKeyboard();
    public abstract void buildMouse();
    public abstract void buildAudio();
}

HPComputerBuilder定义各个组件:

public class HPComputerBuilder extends ComputerBuilder {
    @Override
    public void buildMaster() {
        // TODO Auto-generated method stub
        computer.setMaster("i7,16g,512SSD,1060");
        System.out.println("(i7,16g,512SSD,1060)的惠普主机");
    }
    @Override
    public void buildScreen() {
        // TODO Auto-generated method stub
        computer.setScreen("1080p");
        System.out.println("(1080p)的惠普显示屏");
    }
    @Override
    public void buildKeyboard() {
        // TODO Auto-generated method stub
        computer.setKeyboard("cherry 青轴机械键盘");
        System.out.println("(cherry 青轴机械键盘)的键盘");
    }
    @Override
    public void buildMouse() {
        // TODO Auto-generated method stub
        computer.setMouse("MI 鼠标");
        System.out.println("(MI 鼠标)的鼠标");
    }
    @Override
    public void buildAudio() {
        // TODO Auto-generated method stub
        computer.setAudio("飞利浦 音响");
        System.out.println("(飞利浦 音响)的音响");
    }
}

Director类对组件进行组装并生成产品

public class Director {
   
    private ComputerBuilder computerBuilder;
    public void setComputerBuilder(ComputerBuilder computerBuilder) {
        this.computerBuilder = computerBuilder;
    }
   
    public Computer getComputer() {
        return computerBuilder.getComputer();
    }
   
    public void constructComputer() {
        computerBuilder.buildComputer();
        computerBuilder.buildMaster();
        computerBuilder.buildScreen();
        computerBuilder.buildKeyboard();
        computerBuilder.buildMouse();
        computerBuilder.buildAudio();
    }
}

3.2 生成器模式的优缺点

优点

  • 将一个对象分解为各个组件
  • 将对象组件的构造封装起来
  • 可以控制整个对象的生成过程

缺点

  • 对不同类型的对象需要实现不同的具体构造器的类,这可能回答大大增加类的数量

3.3 生成器模式与工厂模式的不同

生成器模式构建对象的时候,对象通常构建的过程中需要多个步骤,就像我们例子中的先有主机,再有显示屏,再有鼠标等等,生成器模式的作用就是将这些复杂的构建过程封装起来。工厂模式构建对象的时候通常就只有一个步骤,调用一个工厂方法就可以生成一个对象。


4 原型模式

定义:通过复制现有实例来创建新的实例,无需知道相应类的信息。

简单地理解,其实就是当需要创建一个指定的对象时,我们刚好有一个这样的对象,但是又不能直接使用,我会clone一个一毛一样的新对象来使用;基本上这就是原型模式。关键字:Clone。

4.1 深拷贝和浅拷贝

浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。clone明显是深复制,clone出来的对象是是不能去影响原型对象的

4.2 原型模式的结构和代码示例

Client:使用者

Prototype:接口(抽象类),声明具备clone能力,例如java中得Cloneable接口

ConcretePrototype:具体的原型类

可以看出设计模式还是比较简单的,重点在于Prototype接口和Prototype接口的实现类ConcretePrototype。原型模式的具体实现:一个原型类,只需要实现Cloneable接口,覆写clone方法,此处clone方法可以改成任意的名称,因为Cloneable接口是个空接口,你可以任意定义实现类的方法名,如cloneA或者cloneB,因为此处的重点是super.clone()这句话,super.clone()调用的是Object的clone()方法。

public class Prototype implements Cloneable {  
     public Object clone() throws CloneNotSupportedException {  
         Prototype proto = (Prototype) super.clone();  
         return proto;  
     }  
}  

举例(银行发送大量邮件,使用clone和不使用clone的时间对比):我们模拟创建一个对象需要耗费比较长的时间,因此,在构造函数中我们让当前线程sleep一会

public Mail(EventTemplate et) {
              this.tail = et.geteventContent();
              this.subject = et.geteventSubject();
              try {
                     Thread.sleep(1000);
              } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
              }
       }

不使用clone,发送十个邮件

public static void main(String[] args) {
              int i = 0;
              int MAX_COUNT = 10;
              EventTemplate et = new EventTemplate("9月份信用卡账单", "国庆抽奖活动...");
              long start = System.currentTimeMillis();
              while (i < MAX_COUNT) {
                     // 以下是每封邮件不同的地方
                     Mail mail = new Mail(et);
                     mail.setContent(getRandString(5) + ",先生(女士):你的信用卡账单..." + mail.getTail());
                     mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
                     // 然后发送邮件
                     sendMail(mail);
                     i++;
              }
              long end = System.currentTimeMillis();
              System.out.println("用时:" + (end - start));
       }

用时:10001

使用clone,发送十个邮件

    public static void main(String[] args) {
              int i = 0;
              int MAX_COUNT = 10;
              EventTemplate et = new EventTemplate("9月份信用卡账单", "国庆抽奖活动...");
              long start=System.currentTimeMillis();
              Mail mail = new Mail(et);         
              while (i < MAX_COUNT) {
                     Mail cloneMail = mail.clone();
                     mail.setContent(getRandString(5) + ",先生(女士):你的信用卡账单..."
                                  + mail.getTail());
                     mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
                     sendMail(cloneMail);
                     i++;
              }
              long end=System.currentTimeMillis();
              System.out.println("用时:"+(end-start));
       }

用时:1001

4.3 总结

原型模式的本质就是clone,可以解决构建复杂对象的资源消耗问题,能再某些场景中提升构建对象的效率;还有一个重要的用途就是保护性拷贝,可以通过返回一个拷贝对象的形式,实现只读的限制。


二、结构模式(7种)

适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

5 适配器模式

定义: 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。

主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。

5.1 类适配器模式

通过多重继承目标接口和被适配者类方式来实现适配

举例(将USB接口转为VGA接口),类图如下:

USBImpl的代码:

public class USBImpl implements USB{
       @Override
       public void showPPT() {
              // TODO Auto-generated method stub
              System.out.println("PPT内容演示");
       }
}

AdatperUSB2VGA 首先继承USBImpl获取USB的功能,其次,实现VGA接口,表示该类的类型为VGA。

public class AdapterUSB2VGA extends USBImpl implements VGA {
       @Override
       public void projection() {
              super.showPPT();
       }
}

Projector将USB映射为VGA,只有VGA接口才可以连接上投影仪进行投影

public class Projector<T> {
       public void projection(T t) {
              if (t instanceof VGA) {
                     System.out.println("开始投影");
                     VGA v = new VGAImpl();
                     v = (VGA) t;
                     v.projection();
              } else {
                     System.out.println("接口不匹配,无法投影");
              }
       }
}

test代码

       @Test
       public void test2(){
              //通过适配器创建一个VGA对象,这个适配器实际是使用的是USB的showPPT()方法
              VGA a=new AdapterUSB2VGA();
              //进行投影
              Projector p1=new Projector();
              p1.projection(a);
       } 

5.2 对象适配器模式

对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承。

举例(将USB接口转为VGA接口),类图如下:

public class AdapterUSB2VGA implements VGA {
       USB u = new USBImpl();
       @Override
       public void projection() {
              u.showPPT();
       }
}

实现VGA接口,表示适配器类是VGA类型的,适配器方法中直接使用USB对象。

5.3 接口适配器模式

当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。

举例(将USB接口转为VGA接口,VGA中的b()和c()不会被实现),类图如下:

AdapterUSB2VGA抽象类

public abstract class AdapterUSB2VGA implements VGA {
       USB u = new USBImpl();
       @Override
       public void projection() {
              u.showPPT();
       }
       @Override
       public void b() {
       };
       @Override
       public void c() {
       };
}

AdapterUSB2VGA实现,不用去实现b()和c()方法。

public class AdapterUSB2VGAImpl extends AdapterUSB2VGA {
       public void projection() {
              super.projection();
       }
}

5.4 总结

总结一下三种适配器模式的应用场景:

类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。

对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。

接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

命名规则:

我个人理解,三种命名方式,是根据 src是以怎样的形式给到Adapter(在Adapter里的形式)来命名的。

类适配器,以类给到,在Adapter里,就是将src当做类,继承,

对象适配器,以对象给到,在Adapter里,将src作为一个对象,持有。

接口适配器,以接口给到,在Adapter里,将src作为一个接口,实现。

使用选择:

根据合成复用原则,组合大于继承。因此,类的适配器模式应该少用。


6 装饰者模式

定义:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性。

6.1 装饰者模式结构图与代码示例

1.Component(被装饰对象的基类)

定义一个对象接口,可以给这些对象动态地添加职责。

2.ConcreteComponent(具体被装饰对象)

定义一个对象,可以给这个对象添加一些职责。

3.Decorator(装饰者抽象类)

维持一个指向Component实例的引用,并定义一个与Component接口一致的接口。

4.ConcreteDecorator(具体装饰者)

具体的装饰对象,给内部持有的具体被装饰对象,增加具体的职责。

被装饰对象和修饰者继承自同一个超类

举例(咖啡馆订单项目:1)、咖啡种类:Espresso、ShortBlack、LongBlack、Decaf2)、调料(装饰者):Milk、Soy、Chocolate),类图如下:

被装饰的对象和装饰者都继承自同一个超类

public abstract class Drink {
       public String description="";
       private float price=0f;;
       
       
       public void setDescription(String description)
       {
              this.description=description;
       }
       
       public String getDescription()
       {
              return description+"-"+this.getPrice();
       }
       public float getPrice()
       {
              return price;
       }
       public void setPrice(float price)
       {
              this.price=price;
       }
       public abstract float cost();
       
}

被装饰的对象,不用去改造。原来怎么样写,现在还是怎么写。

public  class Coffee extends Drink {
       @Override
       public float cost() {
              // TODO Auto-generated method stub
              return super.getPrice();
       }
       
}

装饰者

装饰者不仅要考虑自身,还要考虑被它修饰的对象,它是在被修饰的对象上继续添加修饰。例如,咖啡里面加牛奶,再加巧克力。加糖后价格为coffee+milk。再加牛奶价格为coffee+milk+chocolate。

public class Decorator extends Drink {
       private Drink Obj;
       public Decorator(Drink Obj) {
              this.Obj = Obj;
       };
       @Override
       public float cost() {
              // TODO Auto-generated method stub
              return super.getPrice() + Obj.cost();
       }
       @Override
       public String getDescription() {
              return super.description + "-" + super.getPrice() + "&&" + Obj.getDescription();
       }
}

装饰者实例化(加牛奶)。这里面要对被修饰的对象进行实例化。

public class Milk extends Decorator {
       public Milk(Drink Obj) {          
              super(Obj);
              // TODO Auto-generated constructor stub
              super.setDescription("Milk");
              super.setPrice(2.0f);
       }
}

coffee店:初始化一个被修饰对象,修饰者实例需要对被修改者实例化,才能对具体的被修饰者进行修饰。

public class CoffeeBar {
       public static void main(String[] args) {
              Drink order;
              order = new Decaf();
              System.out.println("order1 price:" + order.cost());
              System.out.println("order1 desc:" + order.getDescription());
              System.out.println("****************");
              order = new LongBlack();
              order = new Milk(order);
              order = new Chocolate(order);
              order = new Chocolate(order);
              System.out.println("order2 price:" + order.cost());
              System.out.println("order2 desc:" + order.getDescription());
       }
}

6.2 总结

装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。在这里应用继承并不是实现方法的复制,而是实现类型的匹配。因为装饰者和被装饰者是同一个类型,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。根据装饰者模式的理念,我们可以在任何时候,实现新的装饰者增加新的行为。如果是用继承,每当需要增加新的行为时,就要修改原程序了。


7 代理模式

定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

举个例子来说明:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。用图表示如下:

7.1 为什么要用代理模式?

中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

代理模式分为三类:1. 静态代理 2. 动态代理 3. CGLIB代理

7.2 静态代理

举例(买房),类图如下:

第一步:创建服务类接口

public interface BuyHouse {
    void buyHosue();
}

第二步:实现服务接口

public class BuyHouseImpl implements BuyHouse {
       @Override
       public void buyHosue() {
              System.out.println("我要买房");
       }
}

第三步:创建代理类

public class BuyHouseProxy implements BuyHouse {
       private BuyHouse buyHouse;
       public BuyHouseProxy(final BuyHouse buyHouse) {
              this.buyHouse = buyHouse;
       }
       @Override
       public void buyHosue() {
              System.out.println("买房前准备");
              buyHouse.buyHosue();
              System.out.println("买房后装修");
       }
}

总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点: 代理对象与目标对象要实现相同的接口,我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

7.3 动态代理

动态代理有以下特点:

1.代理对象,不需要实现接口

2.代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)

代理类不用再实现接口了。但是,要求被代理对象必须有接口。

动态代理实现:

Java.lang.reflect.Proxy类可以直接生成一个代理对象

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)生成一个代理对象

  • 参数1:ClassLoader loader 代理对象的类加载器 一般使用被代理对象的类加载器
  • 参数2:Class<?>[] interfaces 代理对象的要实现的接口 一般使用的被代理对象实现的接口
  • 参数3:InvocationHandler h (接口)执行处理类

InvocationHandler中的invoke(Object proxy, Method method, Object[] args)方法:调用代理类的任何方法,此方法都会执行

  • 参数3.1:代理对象(慎用)
  • 参数3.2:当前执行的方法
  • 参数3.3:当前执行的方法运行时传递过来的参数

第一步:编写动态处理器

public class DynamicProxyHandler implements InvocationHandler {
       private Object object;
       public DynamicProxyHandler(final Object object) {
              this.object = object;
       }
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              System.out.println("买房前准备");
              Object result = method.invoke(object, args);
              System.out.println("买房后装修");
              return result;
       }
}

第二步:编写测试类

public class DynamicProxyTest {
    public static void main(String[] args) {
        BuyHouse buyHouse = new BuyHouseImpl();
        BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(BuyHouse.class.getClassLoader(), new
                Class[]{BuyHouse.class}, new DynamicProxyHandler(buyHouse));
        proxyBuyHouse.buyHosue();
    }
}

动态代理总结:虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏(我们要使用被代理的对象的接口),因为它的设计注定了这个遗憾。

7.4 CGLIB代理

CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。

CGLIB 底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

CGLIB缺点:对于final方法,无法进行代理。

CGLIB的实现步骤:

第一步:建立拦截器

public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
 
        System.out.println("买房前准备");
 
        Object result = methodProxy.invoke(object, args);
 
        System.out.println("买房后装修");
 
        return result;
 
    }

参数:Object为由CGLib动态生成的代理类实例,Method为上文中实体类所调用的被代理的方法引用,Object[]为参数值列表,MethodProxy为生成的代理类对方法的代理引用。

返回:从代理实例的方法调用返回的值。

其中,proxy.invokeSuper(obj,arg) 调用代理类实例上的proxy方法的父类方法(即实体类TargetObject中对应的方法)

第二步: 生成动态代理类

public class CglibProxy implements MethodInterceptor {
    private Object target;
    public Object getInstance(final Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("买房前准备");
        Object result = methodProxy.invoke(object, args);
        System.out.println("买房后装修");
        return result;
    }
}

这里Enhancer类是CGLib中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展,以后会经常看到它。

首先将被代理类TargetObject设置成父类,然后设置拦截器TargetInterceptor,最后执行enhancer.create()动态生成一个代理类,并从Object强制转型成父类型TargetObject。

第三步:测试

public class CglibProxyTest {
    public static void main(String[] args){
        BuyHouse buyHouse = new BuyHouseImpl();
        CglibProxy cglibProxy = new CglibProxy();
        BuyHouseImpl buyHouseCglibProxy = (BuyHouseImpl) cglibProxy.getInstance(buyHouse);
        buyHouseCglibProxy.buyHosue();
    }
}

CGLIB代理总结: CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。


8 外观模式

定义: 隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。

8.1 模式结构和代码示例

简单来说,该模式就是把一些复杂的流程封装成一个接口供给外部用户更简单的使用。这个模式中,设计到3个角色。

  1).门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。(客户调用,同时自身调用子系统功能)

  2).子系统角色:实现了子系统的功能。它对客户角色和Facade时未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。(实现具体功能)

  3).客户角色:通过调用Facede来完成要实现的功能(调用门面角色)。

举例(每个Computer都有CPU、Memory、Disk。在Computer开启和关闭的时候,相应的部件也会开启和关闭),类图如下:

首先是子系统类:

public class CPU {
 
	public void start() {
		System.out.println("cpu is start...");
	}
 
	public void shutDown() {
		System.out.println("CPU is shutDown...");
	}
}
 
public class Disk {
	public void start() {
		System.out.println("Disk is start...");
	}
 
	public void shutDown() {
		System.out.println("Disk is shutDown...");
	}
}
 
public class Memory {
	public void start() {
		System.out.println("Memory is start...");
	}
 
	public void shutDown() {
		System.out.println("Memory is shutDown...");
	}
}

然后是,门面类Facade

public class Computer {
 
	private CPU cpu;
	private Memory memory;
	private Disk disk;
 
	public Computer() {
		cpu = new CPU();
		memory = new Memory();
		disk = new Disk();
	}
 
	public void start() {
		System.out.println("Computer start begin");
		cpu.start();
		disk.start();
		memory.start();
		System.out.println("Computer start end");
	}
 
	public void shutDown() {
		System.out.println("Computer shutDown begin");
		cpu.shutDown();
		disk.shutDown();
		memory.shutDown();
		System.out.println("Computer shutDown end...");
	}
}

最后为,客户角色

public class Client {
 
	public static void main(String[] args) {
		Computer computer = new Computer();
		computer.start();
		System.out.println("=================");
		computer.shutDown();
	}
 
}

8.2 优点

  - 松散耦合

  使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;

  - 简单易用

  客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟Facade类交互即可。

  - 更好的划分访问层次

 有些方法是对系统外的,有些方法是系统内部相互交互的使用的。子系统把那些暴露给外部的功能集中到门面中,这样就可以实现客户端的使用,很好的隐藏了子系统内部的细节。


9 桥接模式

定义: 将抽象部分与它的实现部分分离,使它们都可以独立地变化。

9.1 案例

看下图手机与手机软件的类图

增加一款新的手机软件,需要在所有手机品牌类下添加对应的手机软件类,当手机软件种类较多时,将导致类的个数急剧膨胀,难以维护

手机和手机中的软件是什么关系?

手机中的软件从本质上来说并不是一种手机,手机软件运行在手机中,是一种包含与被包含关系,而不是一种父与子或者说一般与特殊的关系,通过继承手机类实现手机软件类的设计是违反一般规律的。

如果Oppo手机实现了wifi功能,继承它的Oppo应用商城也会继承wifi功能,并且Oppo手机类的任何变动,都会影响其子类

换一种解决思路

从类图上看起来更像是手机软件类图,涉及到手机本身相关的功能,比如说:wifi功能,放到哪个类中实现呢?放到OppoAppStore中实现显然是不合适的

引起整个结构变化的元素有两个,一个是手机品牌,一个是手机软件,所以我们将这两个点抽出来,分别进行封装

9.2 桥接模式结构和代码示例

类图:

实现:

public interface Software {
	public void run();
 
}
public class AppStore implements Software {
	 
    @Override
    public void run() {
        System.out.println("run app store");
    }
}
public class Camera implements Software {
	 
    @Override
    public void run() {
        System.out.println("run camera");
    }
}

抽象:

public abstract class Phone {
 
	protected Software software;
 
	public void setSoftware(Software software) {
		this.software = software;
	}
 
	public abstract void run();
 
}
public class Oppo extends Phone {
	 
    @Override
    public void run() {
        Coming Soon();
    }
}
public class Vivo extends Phone {
	 
    @Override
    public void run() {
        Coming Soon();
    }
}

对比最初的设计,将抽象部分(手机)与它的实现部分(手机软件类)分离,将实现部分抽象成单独的类,使它们都可以独立地变化。整个类图看起来像一座桥,所以称为桥接模式

继承是一种强耦合关系,子类的实现与它的父类有非常紧密的依赖关系,父类的任何变化 都会导致子类发生变化,因此继承或者说强耦合关系严重影响了类的灵活性,并最终限制了可复用性

从桥接模式的设计上我们可以看出聚合是一种比继承要弱的关联关系,手机类和软件类都可独立的进行变化,不会互相影响

9.3 适用场景

桥接模式通常适用于以下场景。

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

9.4 优缺点

优点:

(1)在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。

(2)桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

缺点:

桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。


10 组合模式

定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。

意图:将对象组合成树形结构以表示”部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

主要解决:它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

何时使用: 1、您想表示对象的部分-整体层次结构(树形结构)。 2、您希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

如何解决:树枝和叶子实现统一接口,树枝内部组合该接口。

关键代码:树枝内部组合该接口,并且含有内部属性 List,里面放 Component。

组合模式的主要优点有:

  1. 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
  2. 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;

其主要缺点是:

  1. 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
  2. 不容易限制容器中的构件;
  3. 不容易用继承的方法来增加构件的新功能;

10.1 模式结构和代码示例

  • 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。
  • 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。
  • 树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法

举例(访问一颗树),类图如下:

1 组件

public interface Component {
    public void add(Component c);
    public void remove(Component c);
    public Component getChild(int i);
    public void operation();
 
}

2 叶子

public class Leaf implements Component{
    
	private String name;
	
	
	public Leaf(String name) {
		this.name = name;
	}
 
	@Override
	public void add(Component c) {}
 
	@Override
	public void remove(Component c) {}
 
	@Override
	public Component getChild(int i) {
		// TODO Auto-generated method stub
		return null;
	}
 
	@Override
	public void operation() {
		// TODO Auto-generated method stub
		 System.out.println("树叶"+name+":被访问!"); 
	}
 
}

3 树枝

public class Composite implements Component {
 
	private ArrayList<Component> children = new ArrayList<Component>();
 
	public void add(Component c) {
		children.add(c);
	}
 
	public void remove(Component c) {
		children.remove(c);
	}
 
	public Component getChild(int i) {
		return children.get(i);
	}
 
	public void operation() {
		for (Object obj : children) {
			((Component) obj).operation();
		}
	}
}

11 享元模式

定义:通过共享的方式高效的支持大量细粒度的对象。

主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

何时使用

1、系统中有大量对象。

2、这些对象消耗大量内存。

3、这些对象的状态大部分可以外部化。

4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。

5、系统不依赖于这些对象身份,这些对象是不可分辨的。

如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。

关键代码:用 HashMap 存储这些对象。

应用实例: 1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。

优点:大大减少对象的创建,降低系统的内存,使效率提高。

缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

简单来说,我们抽取出一个对象的外部状态(不能共享)和内部状态(可以共享)。然后根据外部状态的决定是否创建内部状态对象。内部状态对象是通过哈希表保存的,当外部状态相同的时候,不再重复的创建内部状态对象,从而减少要创建对象的数量。

11.1 享元模式的结构图和代码示例

1、Flyweight (享元抽象类):一般是接口或者抽象类,定义了享元类的公共方法。这些方法可以分享内部状态的数据,也可以调用这些方法修改外部状态。

2、ConcreteFlyweight(具体享元类):具体享元类实现了抽象享元类的方法,为享元对象开辟了内存空间来保存享元对象的内部数据,同时可以通过和单例模式结合只创建一个享元对象。

3、FlyweightFactory(享元工厂类):享元工厂类创建并且管理享元类,享元工厂类针对享元类来进行编程,通过提供一个享元池来进行享元对象的管理。一般享元池设计成键值对,或者其他的存储结构来存储。当客户端进行享元对象的请求时,如果享元池中有对应的享元对象则直接返回对应的对象,否则工厂类创建对应的享元对象并保存到享元池。

举例(JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面)。类图如下:

(1)创建享元对象接口

public interface IFlyweight {
    void print();
}

(2)创建具体享元对象

public class Flyweight implements IFlyweight {
    private String id;
    public Flyweight(String id){
        this.id = id;
    }
    @Override
    public void print() {
        System.out.println("Flyweight.id = " + getId() + " ...");
    }
    public String getId() {
        return id;
    }
}

(3)创建工厂,这里要特别注意,为了避免享元对象被重复创建,我们使用HashMap中的key值保证其唯一。

public class FlyweightFactory {
    private Map<String, IFlyweight> flyweightMap = new HashMap();
    public IFlyweight getFlyweight(String str){
        IFlyweight flyweight = flyweightMap.get(str);
        if(flyweight == null){
            flyweight = new Flyweight(str);
            flyweightMap.put(str, flyweight);
        }
        return  flyweight;
    }
    public int getFlyweightMapSize(){
        return flyweightMap.size();
    }
}

(4)测试,我们创建三个字符串,但是只会产生两个享元对象

public class MainTest {
	public static void main(String[] args) {
        FlyweightFactory flyweightFactory = new FlyweightFactory();
        IFlyweight flyweight1 = flyweightFactory.getFlyweight("A");
        IFlyweight flyweight2 = flyweightFactory.getFlyweight("B");
        IFlyweight flyweight3 = flyweightFactory.getFlyweight("A");
        flyweight1.print();
        flyweight2.print();
        flyweight3.print();
        System.out.println(flyweightFactory.getFlyweightMapSize());
    }
 
}

三、关系模式(11种)

先来张图,看看这11中模式的关系:

第一类:通过父类与子类的关系进行实现。

第二类:两个类之间。

第三类:类的状态。

第四类:通过中间类

12 策略模式

定义: 策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。

意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。

何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。

如何解决:将这些算法封装成一个一个的类,任意地替换。

关键代码:实现同一个接口。

优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

12.1 策略模式结构和示例代码

抽象策略角色: 这个是一个抽象的角色,通常情况下使用接口或者抽象类去实现。对比来说,就是我们的Comparator接口。

具体策略角色: 包装了具体的算法和行为。对比来说,就是实现了Comparator接口的实现一组实现类。

环境角色: 内部会持有一个抽象角色的引用,给客户端调用。

举例如下( 实现一个加减的功能),类图如下:

1、定义抽象策略角色

public interface Strategy {
 
	public int calc(int num1,int num2);
}

2、定义具体策略角色

public class AddStrategy implements Strategy {
 
	@Override
	public int calc(int num1, int num2) {
		// TODO Auto-generated method stub
		return num1 + num2;
	}
 
}
public class SubstractStrategy implements Strategy {
 
	@Override
	public int calc(int num1, int num2) {
		// TODO Auto-generated method stub
		return num1 - num2;
	}
 
}

3、环境角色

public class Environment {
	private Strategy strategy;
 
	public Environment(Strategy strategy) {
		this.strategy = strategy;
	}
 
	public int calculate(int a, int b) {
		return strategy.calc(a, b);
	}
 
}

4、测试

public class MainTest {
	public static void main(String[] args) {
		
		Environment environment=new Environment(new AddStrategy());
		int result=environment.calculate(20, 5);
		System.out.println(result);
		
		Environment environment1=new Environment(new SubstractStrategy());
		int result1=environment1.calculate(20, 5);
		System.out.println(result1);
	}
 
}

13 模板模式

定义:定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。

通俗点的理解就是 :完成一件事情,有固定的数个步骤,但是每个步骤根据对象的不同,而实现细节不同;就可以在父类中定义一个完成该事情的总方法,按照完成事件需要的步骤去调用其每个步骤的实现方法。每个步骤的具体实现,由子类完成。

13.1 模式结构和代码示例

抽象父类(AbstractClass):实现了模板方法,定义了算法的骨架。

具体类(ConcreteClass):实现抽象类中的抽象方法,即不同的对象的具体实现细节。23 种设计模式详解(全23种)具体类(ConcreteClass):实现抽象类中的抽象方法,即不同的对象的具体实现细节。

举例( 我们做菜可以分为三个步骤 (1)备料 (2)具体做菜 (3)盛菜端给客人享用,这三部就是算法的骨架 ;然而做不同菜需要的料,做的方法,以及如何盛装给客人享用都是不同的这个就是不同的实现细节。)。类图如下:

a. 先来写一个抽象的做菜父类:

public abstract class Dish {    
    /**
     * 具体的整个过程
     */
    protected void dodish(){
        this.preparation();
        this.doing();
        this.carriedDishes();
    }
    /**
     * 备料
     */
    public abstract void preparation();
    /**
     * 做菜
     */
    public abstract void doing();
    /**
     * 上菜
     */
    public abstract void carriedDishes ();
}

b. 下来做两个番茄炒蛋(EggsWithTomato)和红烧肉(Bouilli)实现父类中的抽象方法

public class EggsWithTomato extends Dish {
 
	@Override
	public void preparation() {
		System.out.println("洗并切西红柿,打鸡蛋。");
	}
 
	@Override
	public void doing() {
		System.out.println("鸡蛋倒入锅里,然后倒入西红柿一起炒。");
	}
 
	@Override
	public void carriedDishes() {
		System.out.println("将炒好的西红寺鸡蛋装入碟子里,端给客人吃。");
	}
 
}
public class Bouilli extends Dish{
 
    @Override
    public void preparation() {
        System.out.println("切猪肉和土豆。");
    }
 
    @Override
    public void doing() {
        System.out.println("将切好的猪肉倒入锅中炒一会然后倒入土豆连炒带炖。");
    }
 
    @Override
    public void carriedDishes() {
        System.out.println("将做好的红烧肉盛进碗里端给客人吃。");
    }
 
}

c. 在测试类中我们来做菜:

public class MainTest {
	public static void main(String[] args) {
		Dish eggsWithTomato = new EggsWithTomato();
		eggsWithTomato.dodish();
 
		System.out.println("-----------------------------");
 
		Dish bouilli = new Bouilli();
		bouilli.dodish();
	}
 
}

13.2 模板模式的优点和缺点

优点:

 (1)具体细节步骤实现定义在子类中,子类定义详细处理算法是不会改变算法整体结构。

 (2)代码复用的基本技术,在数据库设计中尤为重要。

 (3)存在一种反向的控制结构,通过一个父类调用其子类的操作,通过子类对父类进行扩展增加新的行为,符合“开闭原则”。

缺点:

每个不同的实现都需要定义一个子类,会导致类的个数增加,系统更加庞大。


14 观察者模式

定义: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

如何解决:使用面向对象技术,可以将这种依赖关系弱化。

关键代码:在抽象类里有一个 ArrayList 存放观察者们。

优点:

1、观察者和被观察者是抽象耦合的。

2、建立一套触发机制。

缺点:

1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。

2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

14.1 模式结构图和代码示例

  • 抽象被观察者角色:也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。
  • 抽象观察者角色:为所有的具体观察者定义一个接口,在得到主题通知时更新自己
  • 具体被观察者角色:也就是一个具体的主题,在集体主题的内部状态改变时,所有登记过的观察者发出通知。
  • 具体观察者角色:实现抽象观察者角色所需要的更新接口,一边使本身的状态与制图的状态相协调。

举例(有一个微信公众号服务,不定时发布一些消息,关注公众号就可以收到推送消息,取消关注就收不到推送消息。)类图如下:

1、定义一个抽象被观察者接口

public interface Subject {
	
	  public void registerObserver(Observer o);
	  public void removeObserver(Observer o);
	  public void notifyObserver();
 
}

2、定义一个抽象观察者接口

public interface Observer {
	
	public void update(String message);
 
}

3、定义被观察者,实现了Observerable接口,对Observerable接口的三个方法进行了具体实现,同时有一个List集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。

public class WechatServer implements Subject {
 
	private List<Observer> list;
	private String message;
 
	public WechatServer() {
		list = new ArrayList<Observer>();
	}
 
	@Override
	public void registerObserver(Observer o) {
		// TODO Auto-generated method stub
		list.add(o);
	}
 
	@Override
	public void removeObserver(Observer o) {
		// TODO Auto-generated method stub
		if (!list.isEmpty()) {
			list.remove(o);
		}
	}
 
	@Override
	public void notifyObserver() {
		// TODO Auto-generated method stub
		for (Observer o : list) {
			o.update(message);
		}
	}
 
	public void setInfomation(String s) {
		this.message = s;
		System.out.println("微信服务更新消息: " + s);
		// 消息更新,通知所有观察者
		notifyObserver();
	}
 
}

4、定义具体观察者,微信公众号的具体观察者为用户User

public class User implements Observer {
 
	private String name;
	private String message;
 
	public User(String name) {
		this.name = name;
	}
 
	@Override
	public void update(String message) {
		this.message = message;
		read();
	}
 
	public void read() {
		System.out.println(name + " 收到推送消息: " + message);
	}
 
}

5、编写一个测试类

public class MainTest {
	
	 public static void main(String[] args) {
		 
	        WechatServer server = new WechatServer();
	        
	        Observer userZhang = new User("ZhangSan");
	        Observer userLi = new User("LiSi");
	        Observer userWang = new User("WangWu");
	        
	        server.registerObserver(userZhang);
	        server.registerObserver(userLi);
	        server.registerObserver(userWang);
	        server.setInfomation("PHP是世界上最好用的语言!");
	        
	        System.out.println("----------------------------------------------");
	        server.removeObserver(userZhang);
	        server.setInfomation("JAVA是世界上最好用的语言!");
	        
	    }
 
}

15 迭代器模式

定义:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。

简单来说,不同种类的对象可能需要不同的遍历方式,我们对每一种类型的对象配一个迭代器,最后多个迭代器合成一个。

主要解决:不同的方式来遍历整个整合对象。

何时使用:遍历一个聚合对象。

如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。

关键代码:定义接口:hasNext, next。

应用实例:JAVA 中的 iterator。

优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。

缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

15.1 模式结构和代码示例

(1)迭代器角色(Iterator):定义遍历元素所需要的方法,一般来说会有这么三个方法:取得下一个元素的方法next(),判断是否遍历结束的方法hasNext()),移出当前对象的方法remove(),

(2)具体迭代器角色(Concrete Iterator):实现迭代器接口中定义的方法,完成集合的迭代。

(3)容器角色(Aggregate): 一般是一个接口,提供一个iterator()方法,例如java中的Collection接口,List接口,Set接口等

(4)具体容器角色(ConcreteAggregate):就是抽象容器的具体实现类,比如List接口的有序列表实现ArrayList,List接口的链表实现LinkList,Set接口的哈希列表的实现HashSet等。

举例(咖啡厅和中餐厅合并,他们两个餐厅的菜单一个是数组保存的,一个是ArrayList保存的。遍历方式不一样,使用迭代器聚合访问,只需要一种方式)

1 迭代器接口

public interface Iterator {
	
	public boolean hasNext();
	public Object next();
	
}

2 咖啡店菜单和咖啡店菜单遍历器

public class CakeHouseMenu {
	private ArrayList<MenuItem> menuItems;
	
	
	public CakeHouseMenu() {
		menuItems = new ArrayList<MenuItem>();
		
		addItem("KFC Cake Breakfast","boiled eggs&toast&cabbage",true,3.99f);
		addItem("MDL Cake Breakfast","fried eggs&toast",false,3.59f);
		addItem("Stawberry Cake","fresh stawberry",true,3.29f);
		addItem("Regular Cake Breakfast","toast&sausage",true,2.59f);
	}
 
	private void addItem(String name, String description, boolean vegetable,
			float price) {
		MenuItem menuItem = new MenuItem(name, description, vegetable, price);
		menuItems.add(menuItem);
	}
	
 
	
	public Iterator getIterator()
	{
		return new CakeHouseIterator() ;
	}
	
	class CakeHouseIterator implements  Iterator
	 {		
		private int position=0;
		public CakeHouseIterator()
		{
			  position=0;
		}
		
		 	@Override
			public boolean hasNext() {
			// TODO Auto-generated method stub
			if(position<menuItems.size())
			{
				return true;
			}
			
			return false;
		}
 
		@Override
		public Object next() {
			// TODO Auto-generated method stub
			MenuItem menuItem =menuItems.get(position);
			position++;
			return menuItem;
		}};
	//鍏朵粬鍔熻兘浠g爜
	
}

3 中餐厅菜单和中餐厅菜单遍历器

public class DinerMenu {
	private final static int Max_Items = 5;
	private int numberOfItems = 0;
	private MenuItem[] menuItems;
 
	public DinerMenu() {
		menuItems = new MenuItem[Max_Items];
		addItem("vegetable Blt", "bacon&lettuce&tomato&cabbage", true, 3.58f);
		addItem("Blt", "bacon&lettuce&tomato", false, 3.00f);
		addItem("bean soup", "bean&potato salad", true, 3.28f);
		addItem("hotdog", "onions&cheese&bread", false, 3.05f);
 
	}
 
	private void addItem(String name, String description, boolean vegetable,
			float price) {
		MenuItem menuItem = new MenuItem(name, description, vegetable, price);
		if (numberOfItems >= Max_Items) {
			System.err.println("sorry,menu is full!can not add another item");
		} else {
			menuItems[numberOfItems] = menuItem;
			numberOfItems++;
		}
 
	}
 
	public Iterator getIterator() {
		return new DinerIterator();
	}
 
	class DinerIterator implements Iterator {
		private int position;
 
		public DinerIterator() {
			position = 0;
		}
 
		@Override
		public boolean hasNext() {
			// TODO Auto-generated method stub
			if (position < numberOfItems) {
				return true;
			}
			
			return false;
		}
 
		@Override
		public Object next() {
			// TODO Auto-generated method stub
			MenuItem menuItem = menuItems[position];
			position++;
			return menuItem;
		}
	};
}

4 女服务员

public class Waitress {
	private ArrayList<Iterator> iterators = new ArrayList<Iterator>();
 
	public Waitress() {
 
	}
 
	public void addIterator(Iterator iterator) {
		iterators.add(iterator);
 
	}
 
	public void printMenu() {
		Iterator iterator;
		MenuItem menuItem;
		for (int i = 0, len = iterators.size(); i < len; i++) {
			iterator = iterators.get(i);
 
			while (iterator.hasNext()) {
				menuItem = (MenuItem) iterator.next();
				System.out
						.println(menuItem.getName() + "***" + menuItem.getPrice() + "***" + menuItem.getDescription());
 
			}
 
		}
 
	}
 
	public void printBreakfastMenu() {
 
	}
 
	public void printLunchMenu() {
 
	}
 
	public void printVegetableMenu() {
 
	}
}

16 责任链模式

定义:如果有多个对象有机会处理请求,责任链可使请求的发送者和接受者解耦,请求沿着责任链传递,直到有一个对象处理了它为止。

主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

何时使用:在处理消息的时候以过滤很多道。

如何解决:拦截的类都实现统一接口。

关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。

16.1 模式的结构和代码示例

  1. 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  2. 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  3. 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

举例(购买请求决策,价格不同要由不同的级别决定:组长、部长、副部、总裁)。类图如下:

1 决策者抽象类,包含对请求处理的函数,同时还包含指定下一个决策者的函数

public abstract class Approver {
	 Approver successor;
	 String Name;
	public Approver(String Name)
	{
		this.Name=Name;
	}
	public abstract void ProcessRequest( PurchaseRequest request);
	public void SetSuccessor(Approver successor) {
		// TODO Auto-generated method stub
		this.successor=successor;
	}
}

2 客户端以及请求

public class PurchaseRequest {
	private int Type = 0;
	private int Number = 0;
	private float Price = 0;
	private int ID = 0;
 
	public PurchaseRequest(int Type, int Number, float Price) {
		this.Type = Type;
		this.Number = Number;
		this.Price = Price;
	}
 
	public int GetType() {
		return Type;
	}
 
	public float GetSum() {
		return Number * Price;
	}
 
	public int GetID() {
		return (int) (Math.random() * 1000);
	}
}
public class Client {
 
	public Client() {
 
	}
 
	public PurchaseRequest sendRequst(int Type, int Number, float Price) {
		return new PurchaseRequest(Type, Number, Price);
	}
 
}

3 组长、部长。。。继承决策者抽象类

public class GroupApprover extends Approver {
 
	public GroupApprover(String Name) {
		super(Name + " GroupLeader");
		// TODO Auto-generated constructor stub
 
	}
 
	@Override
	public void ProcessRequest(PurchaseRequest request) {
		// TODO Auto-generated method stub
 
		if (request.GetSum() < 5000) {
			System.out.println("**This request " + request.GetID() + " will be handled by " + this.Name + " **");
		} else {
			successor.ProcessRequest(request);
		}
	}
 
}
public class DepartmentApprover extends Approver {
 
	public DepartmentApprover(String Name) {
		super(Name + " DepartmentLeader");
 
	}
 
	@Override
	public void ProcessRequest(PurchaseRequest request) {
		// TODO Auto-generated method stub
 
		if ((5000 <= request.GetSum()) && (request.GetSum() < 10000)) {
			System.out.println("**This request " + request.GetID()
					+ " will be handled by " + this.Name + " **");
		} else {
			successor.ProcessRequest(request);
		}
 
	}
 
}

4测试

public class MainTest {
 
	public static void main(String[] args) {
 
		Client mClient = new Client();
		Approver GroupLeader = new GroupApprover("Tom");
		Approver DepartmentLeader = new DepartmentApprover("Jerry");
		Approver VicePresident = new VicePresidentApprover("Kate");
		Approver President = new PresidentApprover("Bush");
 
		GroupLeader.SetSuccessor(VicePresident);
		DepartmentLeader.SetSuccessor(President);
		VicePresident.SetSuccessor(DepartmentLeader);
		President.SetSuccessor(GroupLeader);
 
		GroupLeader.ProcessRequest(mClient.sendRequst(1, 10000, 40));
 
	}
 
}

17 命令模式

定义:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。

主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

何时使用:在某些场合,比如要对行为进行”记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将”行为请求者”与”行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。

如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。

17.1模式结构和代码示例

  1. 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
  2. 具体命令角色(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
  3. 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  4. 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。

代码举例(开灯和关灯),类图如下:

1 命令抽象类

public interface Command {
	
	public void excute();
	public void undo();
 
}

2 具体命令对象

public class TurnOffLight implements Command {
 
	private Light light;
 
	public TurnOffLight(Light light) {
		this.light = light;
	}
 
	@Override
	public void excute() {
		// TODO Auto-generated method stub
		light.Off();
	}
 
	@Override
	public void undo() {
		// TODO Auto-generated method stub
		light.On();
	}
 
}

3 实现者

public class Light {
 
	String loc = "";
 
	public Light(String loc) {
		this.loc = loc;
	}
 
	public void On() {
 
		System.out.println(loc + " On");
	}
 
	public void Off() {
 
		System.out.println(loc + " Off");
	}
 
}

4 请求者

public class Contral{
 
	public void CommandExcute(Command command) {
		// TODO Auto-generated method stub
		command.excute();
	}
 
	public void CommandUndo(Command command) {
		// TODO Auto-generated method stub
		command.undo();
	}
 
}

18 状态模式

定义: 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

简单理解,一个拥有状态的context对象,在不同的状态下,其行为会发生改变。

意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。

何时使用:代码中包含大量与对象状态有关的条件语句。

如何解决:将各种具体的状态类抽象出来。

关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。

优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对”开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

18.1 模式结构和代码示例

  • State抽象状态角色

接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。

  • ConcreteState具体状态角色

具体状态主要有两个职责:一是处理本状态下的事情,二是从本状态如何过渡到其他状态。

  • Context环境角色

定义客户端需要的接口,并且负责具体状态的切换。

举例(人物在地点A向地点B移动,在地点B向地点A移动)。类图如下:

1 state接口

public interface State {
	public void stop();
	public void move();
 
}

2 状态实例

public class PlaceA implements State {
 
	private Player context;
 
	public PlaceA(Player context) {
		this.context = context;
	}
 
	@Override
	public void move() {
		System.out.println("处于地点A,开始向B移动");
		System.out.println("--------");
		context.setDirection("AB");
		context.setState(context.onMove);
 
	}
 
	@Override
	public void stop() {
		// TODO Auto-generated method stub
		System.out.println("正处在地点A,不用停止移动");
		System.out.println("--------");
	}
 
}

3 context(player)拥有状态的对象

public class Player {
 
	State placeA;
	State placeB;
	State onMove;
	private State state;
	private String direction;
 
	public Player() {
		direction = "AB";
		placeA = new PlaceA(this);
		placeB = new PlaceB(this);
		onMove = new OnMove(this);
		this.state = placeA;
	}
 
	public void move() {
		System.out.println("指令:开始移动");
		state.move();
	}
 
	public void stop() {
		System.out.println("指令:停止移动");
		state.stop();
	}
 
	public State getState() {
		return state;
	}
 
	public void setState(State state) {
		this.state = state;
	}
 
	public void setDirection(String direction) {
		this.direction = direction;
	}
 
	public String getDirection() {
		return direction;
	}
 
}

19 备忘录模式

定义: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

备忘录模式是一种对象行为型模式,其主要优点如下。

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

其主要缺点是:资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

19.1 模式结构图和代码示例

  1. 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  2. 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  3. 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

举例(发起者通过备忘录存储信息和获取信息),类图如下:

1 备忘录接口

public interface MementoIF {
 
}

2 备忘录

public class Memento implements MementoIF{
	
	private String state;
 
	public Memento(String state) {
		this.state = state;
	}
	
	public String getState(){
		return state;
	}
	
 
}

3 发起者

public class Originator {
 
	private String state;
 
	public String getState() {
		return state;
	}
 
	public void setState(String state) {
		this.state = state;
	}
 
	public Memento saveToMemento() {
		return new Memento(state);
	}
 
	public String getStateFromMemento(MementoIF memento) {
		return ((Memento) memento).getState();
	}
 
}

4 管理者

public class CareTaker {
	
	private List<MementoIF> mementoList = new ArrayList<MementoIF>();
 
	public void add(MementoIF memento) {
		mementoList.add(memento);
	}
 
	public MementoIF get(int index) {
		return mementoList.get(index);
	}
 
}

20 访问者模式

定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离。

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

访问者(Visitor)模式的主要缺点如下。

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

20.1 模式结构和代码示例

访问者模式包含以下主要角色。

  1. 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  2. 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  3. 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  4. 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  5. 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

1 抽象访问者

public interface Visitor {
 
	abstract public void Visit(Element element);
}

2 具体访问者

public class CompensationVisitor implements Visitor {
 
	@Override
	public void Visit(Element element) {
		// TODO Auto-generated method stub
		Employee employee = ((Employee) element);
 
		System.out.println(
				employee.getName() + "'s Compensation is " + (employee.getDegree() * employee.getVacationDays() * 10));
	}
 
}

3 抽象元素

public interface Element {
	abstract public void Accept(Visitor visitor);
 
}

4 具体元素

public class CompensationVisitor implements Visitor {
 
	@Override
	public void Visit(Element element) {
		// TODO Auto-generated method stub
		Employee employee = ((Employee) element);
 
		System.out.println(
				employee.getName() + "'s Compensation is " + (employee.getDegree() * employee.getVacationDays() * 10));
	}
 
}

5 对象结构

public class ObjectStructure {
	private HashMap<String, Employee> employees;
 
	public ObjectStructure() {
		employees = new HashMap();
	}
 
	public void Attach(Employee employee) {
		employees.put(employee.getName(), employee);
	}
 
	public void Detach(Employee employee) {
		employees.remove(employee);
	}
 
	public Employee getEmployee(String name) {
		return employees.get(name);
	}
 
	public void Accept(Visitor visitor) {
		for (Employee e : employees.values()) {
			e.Accept(visitor);
		}
	}
 
}

21 中介者模式

定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

中介者模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合性,使得对象易于独立地被复用。
  2. 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

其主要缺点是:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

21.1 模式结构和代码示例

  1. 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  2. 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  3. 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  4. 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

举例(通过中介卖方),类图如下:

1 抽象中介者

public interface Mediator {
 
	void register(Colleague colleague); // 客户注册
 
	void relay(String from, String to,String ad); // 转发
 
}

2 具体中介者

public class ConcreteMediator implements Mediator {
 
	private List<Colleague> colleagues = new ArrayList<Colleague>();
 
	@Override
	public void register(Colleague colleague) {
		// TODO Auto-generated method stub
		if (!colleagues.contains(colleague)) {
			colleagues.add(colleague);
			colleague.setMedium(this);
		}
	}
 
	@Override
	public void relay(String from, String to, String ad) {
		// TODO Auto-generated method stub
		for (Colleague cl : colleagues) {
 
			String name = cl.getName();
			if (name.equals(to)) {
				cl.receive(from, ad);
			}
 
		}
 
	}
 
}

3 抽象同事类

public abstract class Colleague {
 
	protected Mediator mediator;
	protected String name;
 
	public Colleague(String name) {
		this.name = name;
	}
 
	public void setMedium(Mediator mediator) {
 
		this.mediator = mediator;
 
	}
 
	public String getName() {
		return name;
	}
 
	public abstract void Send(String to, String ad);
 
	public abstract void receive(String from, String ad);
 
}

4 具体同事类

public class Buyer extends Colleague {
 
	public Buyer(String name) {
 
		super(name);
 
	}
 
	@Override
	public void Send(String to, String ad) {
		// TODO Auto-generated method stub
		mediator.relay(name, to, ad);
	}
 
	@Override
	public void receive(String from, String ad) {
		// TODO Auto-generated method stub
		System.out.println(name + "接收到来自" + from + "的消息:" + ad);
	}
 
}

原文:https://zhuanlan.zhihu.com/p/575645658

Dubbo 入门指南:初学者必备技能

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Dubbo 与 RPC 的关系

Dubbo 是一种开源的分布式服务框架,由阿里巴巴公司开发。它为应用程序提供高性能的 RPC(远程过程调用)通信和服务治理能力,让应用程序能够在分布式环境中快速构建高可靠性和可扩展性的服务。Dubbo 核心功能包括服务注册与发现、负载均衡、服务调用和容错能力等,适用于多种语言和多种异构环境的通信。Dubbo 采用阻塞 I/O 和线程池的方式来实现高并发,同时还支持多种协议和序列化格式的扩展。其生态系统日趋完善,用户社区活跃,被广泛应用于 Web 应用,企业级中间件,大数据等领域。

Dubbo 的核心

Dubbo 的核心包括了:

  • 远程通讯:Dubbo 提供了多种远程通讯协议,如 Dubbo 协议、Http 协议、RMI 协议等。其中,Dubbo 协议是 Dubbo 自带的一种二进制、高性能的 RPC 通讯协议,具有较低的序列化和反序列化开销,适合高并发、大数据量的服务通讯。
  • 集群容错:当 Dubbo 消费者发起服务调用时,Dubbo 提供了多种集群容错策略,如 Failover、Failfast、Failsafe、Failback 等。Failover 是 Dubbo 的默认集群容错策略,其会在服务提供者出错或超时时自动切换到其他可用节点进行调用,保证调用的可靠性。
  • 自动发现:为了便于管理众多的服务提供者,Dubbo 提供了注册中心作为服务注册与发现的中心化管理工具。Dubbo 支持多种注册中心,如 ZooKeeper、Redis、Consul、Etcd 等。通过注册中心,服务消费者可以自动发现可用的服务提供者,而无需手动配置服务提供者地址,大大降低了服务调用的复杂度。

Dubbo 和 Spring Cloud区别

Dubbo 是一个高性能的 RPC 框架,主要用于构建微服务架构下的服务治理和服务通信。它可以非常方便地扩展服务,提高系统的性能和可扩展性。

Spring Cloud 是一个完整的微服务框架,它提供了一整套微服务框架的解决方案,包括服务注册与发现、配置中心、负载均衡、断路器、路由等等,比 Dubbo 更为全面。

深入了解:微服务框架对比:Spring Cloud vs Dubbo

Dubbo 的节点

  • Provider:发布服务,并将服务注册到注册中心,等待消费者调用。
  • Consumer:从注册中心订阅服务,和服务提供者进行通信,消费服务。
  • Registry:记录服务提供者的信息,以及服务提供者和服务消费者之间的关系,帮助消费者发现可用的服务实例。
  • Monitor:收集 Dubbo 节点的性能指标、服务调用统计信息等,以便运维人员进行监控和管理。
  • Container:服务的运行容器

Dubbo 的框架

Dubbo 的框架 包括了:

  • 单一应用框架:适用于流量较小的时候
  • 垂直应用框架:适用与流量较大的时候
  • 分布式服务架构:适用于垂直应用架构较多的时候
  • 流动计算架构:当流量超级大的时候,需要一个调度中心

Dubbo 服务

Dubbo 接口定义

我们可以写一个 sayHello 的方法。

 /**
* xml方式服务提供者接口
*/
public interface ProviderService {
    String SayHello(String word);
}

接着,定义它的实现类。

 /**
* xml方式服务提供者实现类
*/
public class ProviderServiceImpl implements ProviderService{
    public String SayHello(String word) {
        return word;
    }
}

然后,导入 maven 依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ouyangsihai</groupId>
    <artifactId>dubbo-provider</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <!-- https: //mvnrepository.com/artifact/com.alibaba/dubbo -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.6.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.10</version>
        </dependency>
        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.5</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.32.Final</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.8.0</version>
        </dependency>

    </dependencies>
</project>

发布 Dubbo 接口

接口写完之后,需要进行接口的发布,这样才能访问到此接口。

package com.sihai.dubbo.provider;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.ServiceConfig;
import com.alibaba.dubbo.container.Main;
import com.sihai.dubbo.provider.service.ProviderService;
import com.sihai.dubbo.provider.service.ProviderServiceImpl;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

/**
* xml方式启动
*
*/
public class App 
{
    public static void main( String[] args ) throws IOException {
        //加载xml配置文件启动
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/provider.xml");
        context.start();
        System.in.read(); // 按任意键退出
    }
}

启动 Dubbo 服务

最后就是启动 Dubbo 服务,看到以下 log,说明启动成功。

知识扩展:

原文地址:https://zhuanlan.zhihu.com/p/641879966

超详细的 Netty 入门

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Netty概述

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端

Netty 对 JDK 自带的 NIO 的 API 进行了良好的封装,解决了上述问题。且Netty拥有高性能、 吞吐量更高,延迟更低,减少资源消耗,最小化不必要的内存复制等优点。

Netty 现在都在用的是4.x,5.x版本已经废弃,Netty 4.x 需要JDK 6以上版本支持。

为什么使用Netty

NIO的缺点 NIO的主要问题是:

  • NIO的类库和API繁杂,学习成本高,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能写出高质量的NIO程序。
  • 臭名昭著的 epoll bug。它会导致Selector空轮询,最终导致CPU 100%。直到JDK1.7版本依然没得到根本性的解决。

Netty的优点

  • API使用简单,学习成本低。
  • 功能强大,内置了多种解码编码器,支持多种协议。
  • 性能高,对比其他主流的NIO框架,Netty的性能最优。
  • 社区活跃,发现BUG会及时修复,迭代版本周期短,不断加入新的功能。

Netty的使用场景

互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现。各进程节点之间的内部通信。Rocketmq底层也是用的Netty作为基础通信组件。

游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。

大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

线程模型

目前存在的线程模式:

  • 传统阻塞IO的服务模型
  • Reactor模式

根据Reactor的数量和1处理资源的线程数不同,又分3种:

  • Reactor单线程;
  • Reactor多线程;
  • 主从Reactor多线程

Netty的线程模型是基于主从Reactor多线程做了改进。

2、传统阻塞IO的线程模型 采用阻塞IO获取输入的数据,每个连接都需要独立的线程来处理逻辑。存在的问题就是,当并发数很大时,就需要创建很多的线程,占用大量的资源。连接创建后,如果当前线程没有数据可读,该线程将会阻塞在读数据的方法上,造成线程资源浪费。

3、Reactor模式(分发者模式/反应器模式/通知者模式) 针对传统阻塞IO的模型,做了以下两点改进:

  • 基于IO复用模型:多个客户端共用一个阻塞对象,而不是每个客户端都对应一个阻塞对象
  • 基于线程池复用线程资源:使用了线程池,而不是每来一个客户端就创建一个线程

Reactor模式的核心组成:

  • Reactor:Reactor就是多个客户端共用的那一个阻塞对象,它单独起一个线程运行,负责监听和分发事件,将请求分发给适当的处理程序来进行处理
  • Handler:处理程序要完成的实际事件,也就是真正执行业务逻辑的程序,它是非阻塞的

4、单线程Reactor

多个客户端请求连接,然后Reactor通过selector轮询判断哪些通道是有事件发生的,如果是连接事件,就到了Acceptor中建立连接;如果是其他读写事件,就有dispatch分发到对应的handler中进行处理。这种模式的缺点就是Reactor和Handler是在一个线程中的,如果Handler阻塞了,那么程序就阻塞了。

5、Reactor多线程

处理流程如下:

  • Reactor对象通过Selector监听客户端请求事件,通过dispatch进行分发;
  • 如果是连接事件,则由Acceptor通过accept方法处理连接请求,然后创建一个Handler对象响应事件;
  • 如果不是连接请求,则由Reactor对象调用对应handler对象进行处理;handler只响应事件,不做具体的业务处理,它通过read方法读取数据后,会分发给线程池的某个线程进行业务处理,并将处理结果返回给handler;
  • handler收到响应后,通过send方法将结果返回给client。

相比单Reactor单线程,这里将业务处理的事情交给了不同的线程去做,发挥了多核CPU的性能。但是Reactor只有一个,所有事件的监听和响应,都由一个Reactor去完成,并发性还是不好。

6、主从Reactor多线程

这个模型相比单reactor多线程的区别就是:专门搞了一个MainReactor来处理连接事件,如果不是连接事件,就分发给SubReactor进行处理。图中这个SubReactor只有一个,其实是可以有多个的,所以性能就上去了。

  • 优点:父线程与子线程的交互简单、职责明确,父线程负责接收连接,子线程负责完成后续的业务处理;
  • 缺点:编程复杂度高

Netty线程模型

netty模型是基于主从Reactor多线程模型设计的。

  • Netty有两组线程池,一个Boss Group,它专门负责客户端连接,另一个Work Group,专门负责网络读写;
  • Boss Group和Work Group的类型都是NIOEventLoopGroup;
  • NIOEventLoopGroup相当于一个事件循环组,这个组包含了多个事件循环,每一个循环都是NIOEventLoop;
  • NIOEventLoop表示一个不断循环执行处理任务的线程,每个NIOEventLoop都有一个Selector,用于监听绑定在其上的ocketChannel的网络通讯;
  • Boss Group下的每个NIOEventLoop的执行步骤有3步:

(1). 轮询accept连接事件;

(2). 处理accept事件,与client建立连接,生成一个NioSocketChannel,并将其注册到某个work group下的NioEventLoop的selector上;

(3). 处理任务队列的任务,即runAllTasks;

  • 每个Work Group下的NioEventLoop循环执行以下步骤:

(1). 轮询read、write事件;

(2). 处理read、write事件,在对应的NioSocketChannel处理;

(3). 处理任务队列的任务,即runAllTasks;

  • 每个Work Group下的NioEventLoop在处理NioSocketChannel业务时,会使用pipeline(管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据。

重要组件

NioEventLoop

NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:

I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。

非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。

NioEventLoopGroup

NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

每个EventLoopGroup里包括一个或多个EventLoop,每个EventLoop中维护一个Selector实例。

Bootstrap、ServerBootstrap

一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。

一般来说,使用Bootstrap创建启动器的步骤可分为以下几步:

group()

服务端要使用两个线程组:

bossGroup 用于监听客户端连接,专门负责与客户端创建连接,并把连接注册到workerGroup的Selector中。 workerGroup用于处理每一个连接发生的读写事件。 一般创建线程组直接使用以下new就完事了:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

既然是线程组,那线程数默认是多少呢?深入源码:

 //使用一个常量保存
    private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
    //NettyRuntime.availableProcessors() * 2,cpu核数的两倍赋值给常量
    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
            "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

    if (logger.isDebugEnabled()) {
        logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
    }
}

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
    //如果不传入,则使用常量的值,也就是cpu核数的两倍
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

默认的线程数是cpu核数的两倍。

假设想自定义线程数,可以使用有参构造器:

//设置bossGroup线程数为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//设置workerGroup线程数为16
EventLoopGroup workerGroup = new NioEventLoopGroup(16)

channel()

这个方法用于设置通道类型,当建立连接后,会根据这个设置创建对应的Channel实例。

bootstrap.group(bossGroup, workerGroup) //设置两个线程组
        // 使用NioServerSocketChannel作为服务器的通道实现
        .channel(NioServerSocketChannel.class)

通道类型有以下:

NioSocketChannel: 异步非阻塞的客户端 TCP Socket 连接。

NioServerSocketChannel: 异步非阻塞的服务器端 TCP Socket 连接。

常用的就是这两个通道类型,因为是异步非阻塞的。所以是首选。

OioSocketChannel: 同步阻塞的客户端 TCP Socket 连接。

OioServerSocketChannel: 同步阻塞的服务器端 TCP Socket 连接。

option()与childOption()

option()设置的是服务端用于接收进来的连接,也就是boosGroup线程。

childOption()是提供给父管道接收到的连接,也就是workerGroup线程。

我们看一下常用的一些设置有哪些:

SocketChannel参数,也就是childOption()常用的参数: SO_RCVBUF Socket参数,TCP数据接收缓冲区大小。 TCP_NODELAY TCP参数,立即发送数据,默认值为Ture。 SO_KEEPALIVE Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。

ServerSocketChannel参数,也就是option()常用参数:

SO_BACKLOG Socket参数,服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128

Future、ChannelFuture

  • Netty中的I/O操作都是异步的,包括bind、write和connect。这些操作会返回一个ChannelFuture对象,而不会立即返回操作结果。
  • 调用者不能立即得到返回结果,而是通过Futrue-Listener机制,用户可以主动获取或者通过通知机制获得IO操作的结果。
  • Netty的异步是建立在future和callback之上的。callback是回调,future表示异步执行的结果,它的核心思想是:假设有个方法fun(),计算过程可能非常耗时,等待fun()返回要很久,那么可以在调用fun()的时候,立马返回一个future,后续通过future去监控fun()方法的处理过程,这就是future-listener机制。
  • ChannelFuture提供操作完成时一种异步通知的方式。一般在Socket编程中,等待响应结果都是同步阻塞的,而Netty则不会造成阻塞,因为ChannelFuture是采取类似观察者模式的形式进行获取结果。
  • 用户可以通过注册监听函数,来获取操作真正的结果,ChannelFuture常用的函数如下
// 判断当前操作是否完成
isDone
// 判断当前操作是否成功
isSuccess
// 获取操作失败的原因
getCause
// 判断当前操作是否被取消
isCancelled
// 注册监听器
addListener

使用监听器: 在NettyServer中的“启动并绑定端口”下面加上如下代码:

// 5. 启动服务器并绑定端口
ChannelFuture cf = bootstrap.bind(6666).sync();
// 注册监听器
cf.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture cf) throws Exception {
        if (cf.isSuccess()) {
            System.out.println("绑定端口成功");
        } else {
            System.out.println("绑定端口失败");
        }
    }
});

Channel

Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:

1)当前网络连接的通道的状态(例如是否打开?是否已连接?)

2)网络连接的配置参数 (例如接收缓冲区大小)

3)提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。

4)调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。

5)支持关联 I/O 操作与对应的处理程序。

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。

下面是一些常用的 Channel 类型:

NioSocketChannel,异步的客户端 TCP Socket 连接。

NioServerSocketChannel,异步的服务器端 TCP Socket 连接。

NioDatagramChannel,异步的 UDP 连接。

NioSctpChannel,异步的客户端 Sctp 连接。

NioSctpServerChannel,异步的 Sctp 服务器端连接。 这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。

获取channel的状态

boolean isOpen(); //如果通道打开,则返回true
boolean isRegistered();//如果通道注册到EventLoop,则返回true
boolean isActive();//如果通道处于活动状态并且已连接,则返回true
boolean isWritable();//当且仅当I/O线程将立即执行请求的写入操作时,返回true。

获取channel的配置参数

ChannelConfig config = channel.config();//获取配置参数
//获取ChannelOption.SO_BACKLOG参数,
Integer soBackLogConfig = config.getOption(ChannelOption.SO_BACKLOG);

channel支持的IO操作

写操作,这里演示从服务端写消息发送到客户端:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.channel().writeAndFlush(Unpooled.copiedBuffer("这波啊,这波是肉蛋葱鸡~", CharsetUtil.UTF_8));
}

通过channel获取ChannelPipeline,并做相关的处理:

//获取ChannelPipeline对象
ChannelPipeline pipeline = ctx.channel().pipeline();
//往pipeline中添加ChannelHandler处理器,装配流水线
pipeline.addLast(new MyServerHandler());

Selector

Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。

当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

ChannelHandler

ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:

ChannelInboundHandler 用于处理入站 I/O 事件。

ChannelOutboundHandler 用于处理出站 I/O 操作。

或者使用以下适配器类:

ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。

ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。

ChannelInboundHandlerAdapter处理器常用的事件有:

  • 注册事件 fireChannelRegistered。
  • 连接建立事件 fireChannelActive。
  • 读事件和读完成事件 fireChannelRead、fireChannelReadComplete。
  • 异常通知事件 fireExceptionCaught。
  • 用户自定义事件 fireUserEventTriggered。
  • Channel 可写状态变化事件 fireChannelWritabilityChanged。
  • 连接关闭事件 fireChannelInactive。

ChannelOutboundHandler处理器常用的事件有:

  • 端口绑定 bind。
  • 连接服务端 connect。
  • 写事件 write。
  • 刷新时间 flush。
  • 读事件 read。
  • 主动断开连接 disconnect。
  • 关闭 channel 事件 close。

ChannelHandlerContext

在Netty中,Handler处理器是有我们定义的,上面讲过通过集成入站处理器或者出站处理器实现。这时如果我们想在Handler中获取pipeline对象,或者channel对象,怎么获取呢。

于是Netty设计了这个ChannelHandlerContext上下文对象,就可以拿到channel、pipeline等对象,就可以进行读写等操作。

ChannelPipline

在前面介绍Channel时,我们知道可以在channel中装配ChannelHandler流水线处理器,那一个channel不可能只有一个channelHandler处理器,肯定是有很多的,既然是很多channelHandler在一个流水线工作,肯定是有顺序的。

于是pipeline就出现了,pipeline相当于处理器的容器。初始化channel时,把channelHandler按顺序装在pipeline中,就可以实现按序执行channelHandler了。

ChannelPipline 保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。

ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。

在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:

一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。

read事件(入站事件)和write事件(出站事件)在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。

在Bootstrap中childHandler()方法需要初始化通道,实例化一个ChannelInitializer,这时候需要重写initChannel()初始化通道的方法,装配流水线就是在这个地方进行。代码演示如下:

//使用匿名内部类的形式初始化通道对象
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //给pipeline管道设置自定义的处理器
        socketChannel.pipeline().addLast(new MyServerHandler());
    }
});

bind()

提供用于服务端或者客户端绑定服务器地址和端口号,默认是异步启动。如果加上sync()方法则是同步。

优雅地关闭EventLoopGroup

//释放掉所有的资源,包括创建的线程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

会关闭所有的child Channel。关闭之后,释放掉底层的资源。

hello world

服务端

public class NettyServer {

    public static void main(String[] args) throws Exception {

        // 创建两个线程组bossGroup和workerGroup, 含有的子线程NioEventLoop的个数默认为cpu核数的两倍
        // bossGroup只是处理连接请求 ,真正的和客户端业务处理,会交给workerGroup完成
        EventLoopGroup bossGroup = new NioEventLoopGroup(3);
        EventLoopGroup workerGroup = new NioEventLoopGroup(8);
        try {
            // 创建服务器端的启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 使用链式编程来配置参数
            bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                    // 使用NioServerSocketChannel作为服务器的通道实现
                    .channel(NioServerSocketChannel.class)
                    // 初始化服务器连接队列大小,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
                    // 多个客户端同时来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {//创建通道初始化对象,设置初始化参数,在 SocketChannel 建立起来之前执行

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //对workerGroup的SocketChannel设置处理器
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            System.out.println("netty server start。。");
            // 绑定一个端口并且同步, 生成了一个ChannelFuture异步对象,通过isDone()等方法可以判断异步事件的执行情况
            // 启动服务器(并绑定端口),bind是异步操作,sync方法是等待异步操作执行完毕
            ChannelFuture cf = bootstrap.bind(9000).sync();
            // 给cf注册监听器,监听我们关心的事件
            /*cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("监听端口9000成功");
                    } else {
                        System.out.println("监听端口9000失败");
                    }
                }
            });*/
            // 等待服务端监听端口关闭,closeFuture是异步操作
            // 通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成,内部调用的是Object的wait()方法
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

定义NettyServerHandler

/**
 * 自定义Handler需要继承netty规定好的某个HandlerAdapter(规范)
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当客户端连接服务器完成就会触发该方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("客户端连接通道建立完成");
    }

    /**
     * 读取客户端发送的数据
     *
     * @param ctx 上下文对象, 含有通道channel,管道pipeline
     * @param msg 就是客户端发送的数据
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        //Channel channel = ctx.channel();
        //ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站
        //将 msg 转成一个 ByteBuf,类似NIO 的 ByteBuffer
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("收到客户端的消息:" + buf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 数据读取完毕处理方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ByteBuf buf = Unpooled.copiedBuffer("HelloClient".getBytes(CharsetUtil.UTF_8));
        ctx.writeAndFlush(buf);
    }

    /**
     * 处理异常, 一般是需要关闭通道
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
    }
}

客户端:

public class NettyClient {
    public static void main(String[] args) throws Exception {
        //客户端需要一个事件循环组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建客户端启动对象
            //注意客户端使用的不是ServerBootstrap而是Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            //设置相关参数
            bootstrap.group(group) //设置线程组
                    .channel(NioSocketChannel.class) // 使用NioSocketChannel作为客户端的通道实现
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //加入处理器
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            System.out.println("netty client start。。");
            //启动客户端去连接服务器端
            ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).sync();
            //对通道关闭进行监听
            cf.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

定义NettyClientHandler

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当客户端连接服务器完成就会触发该方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ByteBuf buf = Unpooled.copiedBuffer("HelloServer".getBytes(CharsetUtil.UTF_8));
        ctx.writeAndFlush(buf);
    }

    //当通道有读取事件时会触发,即服务端发送数据给客户端
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("收到服务端的消息:" + buf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

服务端开启9000端口后,客户端和服务端可以进行通讯了。

服务端:

netty server start。。 客户端连接通道建立完成 收到客户端的消息:HelloServer

客户端:

netty client start。。 收到服务端的消息:HelloClient

总结

本文主要讲述Netty的一些特性以及重要组件,希望看完之后能对Netty框架有一个比较直观的感受,希望能帮助读者快速入门Netty,减少一些弯路。

我们平常在使用的时候,只需要定义各种各样的Handler,其他的都是固定的API。

原文地址:https://zhuanlan.zhihu.com/p/514448867

SQL入门?只要记住这些基础语句就够了!

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

新手入门SQL,强烈推荐MICK的《SQL基础教程》。这本书逻辑清晰,直白易懂,介绍了SQL所有的基础语句。掌握了这本书中的内容,就可以利用SQL进行简单的数据分析了。

《SQL基础教程》

在这篇文章里我对《SQL基础教程》中的几乎所有的SQL基础语句进行了总结和摘抄,面试前可以把这些SQL语句集中记忆一遍。下一篇文章我会教大家如何利用这篇文章中提到的SQL基础语句进行数据分析的实操。欢迎大家关注我的专栏~


首先,什么是SQL?

SQL是Structured Query Language的缩写,意思是结构化查询语言,是一种在数据库管理系统(Relational Database Management System, RDBMS)中查询数据,或通过RDBMS对数据库中的数据进行更改的语言。

常见的RDBMS有:

  • Oracle Database:甲骨文公司的RDBMS
  • SQL Server :微软公司的RDBMS
  • DB2:IBM 公司的RDBMS
  • PostgreSQL:开源的RDBMS
  • MySQL :开源的RDBMS

不同RDBMS的SQL语言略有不同,由于MySQL是开源的,免费容易获取,国内很多公司用的都是MySQL,所以本篇文章汇总的是MySQL的SQL语言。

使用SQL在RDBMS中查询数据的过程是这样的:

图片来自《SQL基础教程》

用户在客户端通过SQL语言,将需要的数据和对数据进行的操作的请求发送给RDBMS,RDBMS 根据该语句的内容返回所请求的数据,或者对存储在数据库中的数据进行更新。

根据对RDBMS 赋予的指令种类的不同,SQL 语句可以分为以下三类。

●DDL(Data Definition Language,数据定义语言)

用来创建或者删除存储数据用的数据库以及数据库中的表等对象。DDL 包含以下几种指令。

CREATE: 创建数据库和表等对象

DROP: 删除数据库和表等对象

ALTER: 修改数据库和表等对象的结构

●DML(Data Manipulation Language,数据操纵语言)

用来查询或者变更表中的记录。DML 包含以下几种指令。

SELECT:查询表中的数据

INSERT:向表中插入新数据

UPDATE:更新表中的数据

DELETE:删除表中的数据

●DCL(Data Control Language,数据控制语言)

用来确认或者取消对数据库中的数据进行的变更。除此之外,还可以对RDBMS 的用户是否有权限操作数据库中的对象(数据库表等)进行设定。DCL 包含以下几种指令。

COMMIT: 确认对数据库中的数据进行的变更

ROLLBACK: 取消对数据库中的数据进行的变更

GRANT: 赋予用户操作权限

REVOKE: 取消用户的操作权限

下面就让我们具体看看这三类语句分别包括哪些基础语句吧!

Ⅰ. DDL(Data Definition Language,数据定义语言)

1、 创建数据库(CREATE)

CREATE DATABASE shop;

2、创建表(CREATE)

CREATE TABLE Product
(product_id     CHAR(4)      NOT NULL,
 product_name   VARCHAR(100) NOT NULL,
 product_type   VARCHAR(32)  NOT NULL,
 sale_price     INTEGER      ,
 purchase_price INTEGER      ,
 regist_date    DATE         ,
 PRIMARY KEY (product_id));

每一列的数据类型(后述)是必须要指定的,数据类型包括:

  • INTEGER 整数型
  • NUMERIC ( 全体位数, 小数位数)
  • CHAR 定长字符串
  • VARCHAR 可变长字符串
  • DATE 日期型

3、 删除表(DROP)

DROP TABLE Product;

4、表定义的更新(ALTER)

  • 在表中增加一列(ADD COLUMN)
ALTER TABLE Product ADD COLUMN product_name_pinyin VARCHAR(100);
  • 在表中删除一列(DROP COLUMN)
ALTER TABLE Product DROP COLUMN product_name_pinyin;
  • 变更表名(RENAME)
RENAME TABLE Poduct to Product;

Ⅱ. DML(Data Manipulation Language,数据操纵语言)

1、向表中插入数据(INSERT)

  • 包含列清单
INSERT INTO Product (product_id, product_name, product_type, sale_price, purchase_price, regist_date) VALUES ('0001', 'T恤衫',
'衣服', 1000, 500, '2009-09-20');
  • 省略列清单
START TRANSACTION; 
INSERT INTO Product VALUES ('0001', 'T恤衫', '衣服', 1000, 500, '2009-09-20');
INSERT INTO Product VALUES ('0002', '打孔器', '办公用品', 500, 320, '2009-09-11');
INSERT INTO Product VALUES ('0003', '运动T恤', '衣服', 4000, 2800, NULL);
INSERT INTO Product VALUES ('0004', '菜刀', '厨房用具', 3000, 2800, '2009-09-20');
INSERT INTO Product VALUES ('0005', '高压锅', '厨房用具', 6800, 5000, '2009-01-15');
INSERT INTO Product VALUES ('0006', '叉子', '厨房用具', 500, NULL, '2009-09-20');
INSERT INTO Product VALUES ('0007', '擦菜板', '厨房用具', 880, 790, '2008-04-28');
INSERT INTO Product VALUES ('0008', '圆珠笔', '办公用品', 100, NULL,'2009-11-11');
COMMIT;
  • 从其他表中复制数据
INSERT INTO ProductCopy (product_id, product_name, product_type,sale_price, purchase_price, regist_date)
SELECT product_id, product_name, product_type, sale_price, purchase_price, regist_date
  FROM Product;
  • INSERT 语句中的SELECT 语句,也可以使用WHERE 子句或者GROUP BY 子句等。
INSERT INTO ProductType (product_type, sum_sale_price, sum_purchase_price)
SELECT product_type, SUM(sale_price), SUM(purchase_price)
  FROM Product
 GROUP BY product_type;

2、从表中查询出需要的列(SELECT)

SELECT product_id, product_name, purchase_price
  FROM Product;
  • 查询出所有的列
SELECT *
  FROM Product;
  • 为列设定别名(AS)
SELECT product_id AS id,
       product_name AS name,
       purchase_price AS “价格”
  FROM Product;
  • 将查询出的一列指定为常数
SELECT ‘2009-02-24’ AS date, product_id, product_name
  FROM Product;
  • 从查询结果中删除重复行(DISTINCT)
SELECT DISTINCT product_type
  FROM Product;

3、指定查询的条件(WHERE)

SELECT product_name, product_type
  FROM Product;
 WHERE product_type = '衣服';

4、算数运算符和比较运算符

  • 算数运算符

加 +

减 –

乘 *

除 /

注意:所有包含NULL 的计算,结果肯定是NULL。

SELECT product_name, sale_price, sale_price * 2 AS "sale_price_x2"
  FROM Product;
  • 比较运算符

等于 =

不等于 <>

大于 >

大于等于 >=

小于 <

小于等于 <=

SELECT product_name, product_type, regist_date
  FROM Product
 WHERE regist_date < '2009-09-27';
  • 将算数运算符和比较运算符结合使用:
SELECT product_name, sale_price, purchase_price
  FROM Product
 WHERE sale_price - purchase_price >= 500;

注意:不能对NULL使用比较运算符,正确的方法是:

SELECT product_name, purchase_price
  FROM Product
 WHERE purchase_price IS NULL;

SELECT product_name, purchase_price
  FROM Product
 WHERE purchase_price IS NOT NULL;

5、逻辑运算符(NOT、AND、OR)

  • NOT
SELECT product_name, product_type, sale_price
FROM Product
WHERE NOT sale_price >= 1000; 

(也就是sale_price<1000)

  • AND

AND运算符在其两侧的查询条件都成立时整个查询条件才成立,其意思相当于“并且”。

SELECT product_name, purchase_price
  FROM Product
 WHERE product_type = '厨房用具'
   AND sale_price >= 3000;

  • OR

运算符在其两侧的查询条件有一个成立时整个查询条件都成立,其意思相当于“或者”。

SELECT product_name, purchase_price
  FROM Product
 WHERE product_type = '厨房用具'
   OR sale_price >= 3000;

6、对表进行聚合查询

常用的五个聚合函数:

  • COUNT: 计算表中的记录数(行数)
  • SUM: 计算表中数值列中数据的合计值
  • AVG: 计算表中数值列中数据的平均值
  • MAX: 求出表中任意列中数据的最大值
  • MIN: 求出表中任意列中数据的最小值
  • 计算全部数据的行数(包含NULL)
SELECT COUNT(*)
  FROM Product;
  • 计算某一列的行数(不包含NULL)
SELECT COUNT(purchase_price)
  FROM Product;
  • 计算删除重复数据后的行数
SELECT COUNT(DISTINCT product_type)
  FROM Product;

(所有的聚合函数都可以使用DISTINCT)

  • SUM/AVG函数只能对数值类型的列使用,而MAX/MIN函数原则上可以适用于任何数据类型的列
SELECT MAX(regist_date), MIN(regist_date)
  FROM Product;

7、对表进行分组(GROUP BY)

SELECT product_type, COUNT(*)
  FROM Product
 GROUP BY product_type;
  • GROUP BY和WHERE并用时SELECT语句的执行顺序:

FROM → WHERE → GROUP BY → SELECT

SELECT purchase_price, COUNT(*)
  FROM Product
 WHERE product_type = '衣服'
 GROUP BY purchase_price;
  • 为聚合结果指定条件(HAVING)
SELECT product_type, COUNT(*)
  FROM Product
 GROUP BY product_type
HAVING COUNT(*) = 2;

8、对查询结果进行排序(ORDER BY)

  • 子句的书写顺序

SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY

子句的执行顺序:

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

SELECT product_id, product_name, sale_price, purchase_price
  FROM Product
ORDER BY sale_price;
  • 升序(ASC)或降序(DESC)
SELECT product_id, product_name, sale_price, purchase_price
  FROM Product
ORDER BY sale_price DESC;

注意:默认升序

9、数据的删除(DELETE)

  • 清空表
DELETE FROM Product;
  • 指定删除对象(搜索型DELETE)
DELETE FROM Product
 WHERE sale_price >= 4000;

10、数据的更新(UPDATE)

  • 更新整列
UPDATE Product
   SET regist_date = '2009-10-10';
  • 指定条件的更新(搜索型UPDATE)
UPDATE Product
   SET sale_price = sale_price * 10
 WHERE product_type = '厨房用具';
  • 多列更新
UPDATE Product
   SET sale_price = sale_price * 10,
       purchase_price = purchase_price / 2
 WHERE product_type = '厨房用具';

11、视图

  • 创建视图(CREATE VIEW)
CREATE VIEW ProductSum (product_type, cnt_product)
AS
SELECT product_type, COUNT(*)
  FROM Product
 GROUP BY product_type;

注意:定义视图时不能使用ORDER BY子句

  • 使用视图
SELECT product_type, cnt_product
  FROM ProductSum;
  • 删除视图(DROP VIEW)
DROP VIEW ProductSum;

12、子查询(一次性视图)

-- 在FROM子句中直接书写定义视图的SELECT语句
SELECT product_type, cnt_product
  FROM ( SELECT product_type, COUNT(*) AS cnt_product
            FROM Product
          GROUP BY product_type ) AS ProductSum;
  • 标量子查询

在WHERE子句中使用标量子查询

SELECT product_id, product_name, sale_price
  FROM Product
 WHERE sale_price > (SELECT AVG(sale_price)
                     FROM Product);

注意:能够使用常数或者列名的地方,无论是SELECT 子句、GROUP BY 子句、HAVING 子句,还是ORDER BY 子句,几乎所有的地方都可以使用标量子查询。

  • 关联子查询
SELECT product_type, product_name, sale_price
  FROM Product AS P1 
 WHERE sale_price > (SELECT AVG(sale_price)
                          FROM Product AS P2 
                      WHERE P1.product_type = P2.product_type
                        GROUP BY product_type);

这里起到关键作用的就是在子查询中添加的WHERE 子句的条件。该条件的意思就是,在同一商品种类中对各商品的销售单价和平均单价进行比较。

13、函数

函数大致可以分为以下几种。

  • 算术函数(用来进行数值计算的函数)
  • 字符串函数(用来进行字符串操作的函数)
  • 日期函数(用来进行日期操作的函数)
  • 转换函数(用来转换数据类型和值的函数)
  • 聚合函数(用来进行数据聚合的函数)
  • 算数函数

ABS (数值) —— 绝对值

MOD (被除数, 除数) —— 求余

ROUND (对象数值, 保留小数的位数) —— 四舍五入

  • 字符串函数

CONCAT (字符串1, 字符串2, 字符串3) —— 拼接

LENGTH (字符串) —— 字符串长度

LOWER (字符串) —— 小写

UPPER (字符串) —— 大写

REPLACE (对象字符串,替换前的字符串,替换后的字符串) —— 替换

SUBSTRING(对象字符串 FROM 截取的起始位置 FOR 截取的字符数)—— 截取

  • 日期函数

CURRENT_DATE —— 当前日期

CURRENT_TIME —— 当前时间

CURRENT_TIMESTAMP —— 当前的日期和时间

EXTRACT (日期元素 FROM 日期)

  • 转换函数

CAST(转换前的值 AS 想要转换的数据类型)—— 类型转换

COALESCE (数据1,数据2,数据3……) —— 将NULL转换为其他值

14、谓词

  • LIKE谓词

前方一致查询:

SELECT *
  FROM SampleLike
 WHERE strcol LIKE 'ddd%';

也可用_(下划线)代替%,但_只能代表一个字符

SELECT *
FROM SampleLike
WHERE strcol LIKE 'abc_';

中间一致查询:

SELECT *
  FROM SampleLike
 WHERE strcol LIKE '%ddd%';

后方一致查询:

SELECT *
  FROM SampleLike
 WHERE strcol LIKE '%ddd';
  • BETWEEN谓词
SELECT product_name, sale_price
  FROM Product
 WHERE sale_price BETWEEN 100 AND 1000;

BETWEEN 的特点就是结果中会包含100 和1000 这两个临界值。

  • IS NULL和IS NOT NULL谓词

为了选取出某些值为NULL 的列的数据,不能使用=,而只能使用特定的谓词IS NULL

SELECT product_name, purchase_price
  FROM Product
 WHERE purchase_price IS NULL;
  • IN谓词
SELECT product_name, purchase_price
  FROM Product
 WHERE purchase_price IN (320, 500, 5000);

也可以用NOT IN

SELECT product_name, purchase_price
  FROM Product
 WHERE purchase_price NOT IN (320, 500, 5000);

注意:在使用IN 和NOT IN 时是无法选取出NULL 数据的。

使用子查询作为IN谓词的参数:

SELECT product_name, sale_price
  FROM Product
 WHERE product_id IN (SELECT product_id
                         FROM ShopProduct
                        WHERE shop_id = '000C');
  • EXIST谓词
SELECT product_name, sale_price
  FROM Product AS P 
 WHERE EXISTS (SELECT *
                  FROM ShopProduct AS SP 
                 WHERE SP.shop_id = '000C'
                   AND SP.product_id = P.product_id);

也可以用NOT EXIST

15、CASE表达式

SELECT product_name,
       CASE WHEN product_type = '衣服'
            THEN CONCAT('A:', product_type)
            WHEN product_type = '办公用品'
            THEN CONCAT('B:', product_type)
            WHEN product_type = '厨房用具'
            THEN CONCAT('C:',product_type)
            ELSE NULL
       END AS abc_product_type
  FROM Product;

16、表的加减法

  • 表的加法(UNION)
SELECT product_id, product_name
   FROM Product
UNION
SELECT product_id, product_name
  FROM Product2;

通过UNION 进行并集运算时可以使用任何形式的SELECT 语句,WHERE、GROUP BY、HAVING 等子句都可以使用,但是ORDER BY 只能在最后使用一次。

注意:UNION会删去两个表中的重复记录。如果想保留重复记录,可以在UNION后面加ALL

  • 选取表中的公共部分(INTERSECT)

MySQL不支持INTERSECT

  • 表的减法(EXCEPT)

MySQL不支持EXCEPT

17、以列为单位对表进行联结(JOIN)

  • 内联结(INNER JOIN)
SELECT SP.shop_id, SP.shop_name, SP.product_id, P.product_name, P.sale_price
  FROM ShopProduct AS SP INNER JOIN Product AS P 
    ON SP.product_id = P.product_id;

像这样使用联结运算将满足相同规则的表联结起来时,WHERE、GROUP BY、HAVING、ORDER BY 等工具都可以正常使用.

  • 外联结(OUTER JOIN)
SELECT SP.shop_id, SP.shop_name, SP.product_id, P.product_name, P.sale_price
  FROM ShopProduct AS SP LEFT OUTER JOIN Product AS P ①
    ON SP.product_id = P.product_id;
  • 三张以上的表的联结
SELECT SP.shop_id, SP.shop_name, SP.product_id, P.product_name, P.sale_price, IP.inventory_quantity
  FROM ShopProduct AS SP INNER JOIN Product AS P ①
    ON SP.product_id = P.product_id
          INNER JOIN InventoryProduct AS IP ②
             ON SP.product_id = IP.product_id
 WHERE IP.inventory_id = 'P001';

Ⅲ. DCL(Data Control Language,数据控制语言)

1、创建事务(START TRANSACTION) – 提交处理(COMMIT)

START TRANSACTION;
    -- 将运动T恤的销售单价降低1000日元
    UPDATE Product
       SET sale_price = sale_price - 1000
     WHERE product_name = '运动T恤';
    -- 将T恤衫的销售单价上浮1000日元
    UPDATE Product
       SET sale_price = sale_price + 1000
     WHERE product_name = 'T恤衫';
COMMIT;

2、取消处理(ROLLBACK)

START TRANSACTION;
    -- 将运动T恤的销售单价降低1000日元
    UPDATE Product
       SET sale_price = sale_price - 1000
     WHERE product_name = '运动T恤';
    -- 将T恤衫的销售单价上浮1000日元
    UPDATE Product
       SET sale_price = sale_price + 1000
     WHERE product_name = 'T恤衫';
ROLLBACK;

原文地址:https://zhuanlan.zhihu.com/p/52428930

redis入门,看这一篇就够了

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

正文开始!

Redis简介:

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key – value 数据库。

Redis的全称是 Remote Dictionary Server,它是一款开源的高性能的NoSQL数据库,它可以用作数据库缓存消息队列

Redis 与 其他 key – value 缓存产品有以下三个特点:

  • Redis 支持数据持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis 不仅仅支持简单的 key – value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储
  • Redis 支持数据的备份,即 master – slave 模式的数据备份

Redis优势:

  • 性能极高 – Redis 读的速度是 110000 次 /s, 写的速度是 81000 次 /s 。
  • 丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子性 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。
  • 其他特性 – Redis 还支持 publish/subscribe 通知,key 过期等特性。

NoSQL

NoSQL 最常见的解释是 non-relational,非关系型数据库,还有一种说法是 Not Only SQL,不仅仅是 SQL,NoSQL 仅仅是一个概念,泛指非关系型的数据库,区别于关系数据库,它们不保证关系数据的 ACID 特性。ACID 即

  • A (Atomicity) 原子性
  • C (Consistency) 一致性
  • I (Isolation) 独立性
  • D (Durability) 持久性

Redis 通过提供多种键值对的数据类型来适应不同场景下的存储需求。

Redis 数据类型及主要应用场景:

关于上表中的部分释义:

  1. 压缩列表是列表键和哈希键的底层实现之一。
    当一个列表键只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,Redis就会使用压缩列表来做列表键的底层实现
  2. 整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现
  • 字符串 - strings

string 字符串 是 Redis 中最简单的数据类型,它是与 Memcached 一样的类型,一个 key 对应一个 value,这种 key/value 对应的方式称为键值对。字符串对很多例子都有用,例如缓存 HTML 片段和网页。

  • 集合 - set

set 是集合,和我们数学中的集合概念相似,对集合的操作有添加删除元素,有对多个集合求交并差等操作。操作中 key 理解为集合的名字。

  • 散列 - hash

hash 是一个键值 (key=>value) 对集合;是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

  • 列表 - list

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。list类型经常会被用于消息队列的服务,以完成多程序之间的消息交换。

  • 有序集合 - zset

Redis zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员。当你需要一个有序的并且不重复的集合列表时,那么可以选择 sorted set 数据结构。

应用场景:

Redis 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。

Redis常用命令

常用管理命令

1、启动Redis

> redis-server [--port 6379]

如果命令参数过多,建议通过配置文件来启动Redis。

> redis-server [xx/xx/redis.conf]

6379是Redis默认端口号。

2、连接Redis

> ./redis-cli [-h 127.0.0.1 -p 6379]

3、停止Redis

> redis-cli shutdown

> kill redis-pid

以上两条停止Redis命令效果一样。

4、发送命令

给Redis发送命令有两种方式:

1、redis-cli带参数运行,如:

> redis-cli shutdown
not connected> 

这样默认是发送到本地的6379端口。

2、redis-cli不带参数运行,如:

> ./redis-cli

127.0.0.1:6379> shutdown
not connected> 

5、测试连通性

127.0.0.1:6379> ping
PONG

key操作命令

获取所有键

语法:keys pattern

127.0.0.1:6379> keys *
1) "javastack"

  • *表示通配符,表示任意字符,会遍历所有键显示所有的键列表,时间复杂度O(n),在生产环境不建议使用。

获取键总数

语法:dbsize

127.0.0.1:6379> dbsize
(integer) 6

获取键总数时不会遍历所有的键,直接获取内部变量,时间复杂度O(1)。

查询键是否存在

语法:exists key [key …]

127.0.0.1:6379> exists javastack java
(integer) 2

查询查询多个,返回存在的个数。

删除键

语法:del key [key …]

127.0.0.1:6379> del java javastack
(integer) 1

可以删除多个,返回删除成功的个数。

查询键类型

语法: type key

127.0.0.1:6379> type javastack
string

移动键

语法:move key db

如把javastack移到2号数据库。

127.0.0.1:6379> move javastack 2
(integer) 1
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> keys *
1) "javastack"

查询key的生命周期(秒)

秒语法:ttl key
毫秒语法:pttl key

127.0.0.1:6379[2]> ttl javastack
(integer) -1

-1:永远不过期。

设置过期时间

秒语法:expire key seconds
毫秒语法:pexpire key milliseconds

127.0.0.1:6379[2]> expire javastack 60
(integer) 1
127.0.0.1:6379[2]> ttl javastack
(integer) 55

设置永不过期

语法:persist key

127.0.0.1:6379[2]> persist javastack
(integer) 1

更改键名称

语法:rename key newkey

127.0.0.1:6379[2]> rename javastack javastack123
OK

字符串操作命令

字符串是Redis中最基本的数据类型,单个数据能存储的最大空间是512M。

存放键值

语法:set key value [EX seconds] [PX milliseconds] [NX|XX]

nx:如果key不存在则建立,xx:如果key存在则修改其值,也可以直接使用setnx/setex命令。

127.0.0.1:6379> set javastack 666
OK

获取键值

语法:get key

127.0.0.1:6379[2]> get javastack
"666"

值递增/递减

如果字符串中的值是数字类型的,可以使用incr命令每次递增,不是数字类型则报错。

语法:incr key

127.0.0.1:6379[2]> incr javastack
(integer) 667

一次想递增N用incrby命令,如果是浮点型数据可以用incrbyfloat命令递增。

同样,递减使用decr、decrby命令。

批量存放键值

语法:mset key value [key value …]

127.0.0.1:6379[2]> mset java1 1 java2 2 java3 3
OK

获取获取键值

语法:mget key [key …]

127.0.0.1:6379[2]> mget java1 java2
1) "1"
2) "2"

Redis接收的是UTF-8的编码,如果是中文一个汉字将占3位返回。

获取值长度

语法:strlen key
127.0.0.1:6379[2]> strlen javastack
(integer) 3

追加内容

语法:append key value

127.0.0.1:6379[2]> append javastack hi
(integer) 5

向键值尾部添加,如上命令执行后由666变成666hi

获取部分字符

语法:getrange key start end

> 127.0.0.1:6379[2]> getrange javastack 0 4
"javas"

集合操作命令

集合类型和列表类型相似,只不过是集合是无序且不可重复的。

集合

存储值

语法:sadd key member [member …]

// 这里有8个值(2个java),只存了7个
127.0.0.1:6379> sadd langs java php c++ go ruby python kotlin java
(integer) 7

获取元素

获取所有元素语法:smembers key

127.0.0.1:6379> smembers langs
1) "php"
2) "kotlin"
3) "c++"
4) "go"
5) "ruby"
6) "python"
7) "java"

随机获取语法:srandmember langs count

127.0.0.1:6379> srandmember langs 3
1) "c++"
2) "java"
3) "php"

判断集合是否存在元素

语法:sismember key member

127.0.0.1:6379> sismember langs go
(integer) 1

获取集合元素个数

语法:scard key

127.0.0.1:6379> scard langs
(integer) 7

删除集合元素

语法:srem key member [member …]

127.0.0.1:6379> srem langs ruby kotlin
(integer) 2

弹出元素

语法:spop key [count]

127.0.0.1:6379> spop langs 2
1) "go"
2) "java"

有序集合

和列表的区别:

1、列表使用链表实现,两头快,中间慢。有序集合是散列表和跳跃表实现的,即使读取中间的元素也比较快。

2、列表不能调整元素位置,有序集合能。

3、有序集合比列表更占内存。

存储值

语法:zadd key [NX|XX] [CH] [INCR] score member [score member …]

127.0.0.1:6379> zadd footCounts 16011 tid 20082 huny 2893 nosy
(integer) 3

获取元素分数

语法:zscore key member

127.0.0.1:6379> zscore footCounts tid
"16011"

获取排名范围排名语法:zrange key start stop [WITHSCORES]

// 获取所有,没有分数
127.0.0.1:6379> zrange footCounts 0 -1
1) "nosy"
2) "tid"
3) "huny"

// 获取所有及分数
127.0.0.1:6379> zrange footCounts 0 -1 Withscores
1) "nosy"
2) "2893"
3) "tid"
4) "16011"
5) "huny"
6) "20082"

获取指定分数范围排名语法:zrangebyscore key min max [WITHSCORES] [LIMIT offset count]

127.0.0.1:6379> zrangebyscore footCounts 3000 30000 withscores limit 0 1
1) "tid"
2) "16011"

增加指定元素分数

语法:zincrby key increment member

127.0.0.1:6379> zincrby footCounts 2000 tid
"18011"

获取集合元素个数

语法:zcard key

127.0.0.1:6379> zcard footCounts
(integer) 3

获取指定范围分数个数

语法:zcount key min max

127.0.0.1:6379> zcount footCounts 2000 20000
(integer) 2

删除指定元素

语法:zrem key member [member …]

127.0.0.1:6379> zrem footCounts huny
(integer) 1

获取元素排名

语法:zrank key member

127.0.0.1:6379> zrank footCounts tid
(integer) 1

列表操作命令

列表类型是一个有序的字段串列表,内部是使用双向链表实现,所有可以向两端操作元素,获取两端的数据速度快,通过索引到具体的行数比较慢。

列表类型的元素是有序且可以重复的。

存储值

左端存值语法:lpush key value [value …]

127.0.0.1:6379> lpush list lily sandy
(integer) 2

右端存值语法:rpush key value [value …]

127.0.0.1:6379> rpush list tom kitty
(integer) 4

索引存值语法:lset key index value

127.0.0.1:6379> lset list 3 uto
OK

弹出元素

左端弹出语法:lpop key

127.0.0.1:6379> lpop list
"sandy"

右端弹出语法:rpop key

127.0.0.1:6379> rpop list
"kitty"

获取元素个数

语法:llen key

127.0.0.1:6379> llen list
(integer) 2

获取列表元素

两边获取语法:lrange key start stop

127.0.0.1:6379> lpush users tom kitty land pony jack maddy
(integer) 6

127.0.0.1:6379> lrange users 0 3
1) "maddy"
2) "jack"
3) "pony"
4) "land"

// 获取所有
127.0.0.1:6379> lrange users 0 -1
1) "maddy"
2) "jack"
3) "pony"
4) "land"
5) "kitty"
6) "tom"

// 从右端索引
127.0.0.1:6379> lrange users -3 -1
1) "land"
2) "kitty"
3) "tom"

索引获取语法:lindex key index

127.0.0.1:6379> lindex list 2
"ketty"

// 从右端获取
127.0.0.1:6379> lindex list -5
"sady"

删除元素

根据值删除语法:lrem key count value

127.0.0.1:6379> lpush userids 111 222 111 222 222 333 222 222
(integer) 8

// count=0 删除所有
127.0.0.1:6379> lrem userids 0 111
(integer) 2

// count > 0 从左端删除前count个
127.0.0.1:6379> lrem userids 3 222
(integer) 3

// count < 0 从右端删除前count个
127.0.0.1:6379> lrem userids -3 222
(integer) 2

范围删除语法:ltrim key start stop

// 只保留2-4之间的元素
127.0.0.1:6379> ltrim list 2 4
OK

散列操作命令

redis字符串类型键和值是字典结构形式,这里的散列类型其值也可以是字典结构。

存放键值

单个语法:hset key field value

127.0.0.1:6379> hset user name javastack
(integer) 1

多个语法:hmset key field value [field value …]

127.0.0.1:6379> hmset user name javastack age 20 address china
OK

不存在时语法:hsetnx key field value

127.0.0.1:6379> hsetnx user tall 180
(integer) 0

获取字段值

单个语法:hget key field

127.0.0.1:6379> hget user age
"20"

多个语法:hmget key field [field …]

127.0.0.1:6379> hmget user name age address
1) "javastack"
2) "20"
3) "china"

获取所有键与值语法:hgetall key

127.0.0.1:6379> hgetall user
1) "name"
2) "javastack"
3) "age"
4) "20"
5) "address"
6) "china"

获取所有字段语法:hkeys key

127.0.0.1:6379> hkeys user
1) "name"
2) "address"
3) "tall"
4) "age"

获取所有值语法:hvals key

127.0.0.1:6379> hvals user
1) "javastack"
2) "china"
3) "170"
4) "20"

判断字段是否存在

语法:hexists key field

127.0.0.1:6379> hexists user address
(integer) 1

获取字段数量

语法:hlen key

127.0.0.1:6379> hlen user
(integer) 4

递增/减

语法:hincrby key field increment

127.0.0.1:6379> hincrby user tall -10
(integer) 170

删除字段

语法:hdel key field [field …]

127.0.0.1:6379> hdel user age
(integer) 1

Redis 安装

Redis 支持在 Linux、OS X 、BSD 等 POSIX 系统中安装,也支持在 Windows 中安装,在 Windows 、Linux 下的安装请参考 https://www.runoob.com/redis/redis-install.html

详细过程见:Redis 安装 – 侠客行

事务

redis 事务一次可以执行多条命令,服务器在执行命令期间,不会去执行其他客户端的命令请求。

事务中的多条命令被一次性发送给服务器,而不是一条一条地发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。

Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。

  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余命令依然被执行。也就是说 Redis 事务不保证原子性。
  • 在事务执行过程中,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务。

实例

以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:

redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
 2) "C++"
 3) "Programming"

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

这是官网上的说明 From redis docs on transactions:
It’s important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.

比如:

redis 127.0.0.1:7000> multi
OK
redis 127.0.0.1:7000> set a aaa
QUEUED
redis 127.0.0.1:7000> set b bbb
QUEUED
redis 127.0.0.1:7000> set c ccc
QUEUED
redis 127.0.0.1:7000> exec
1) OK
2) OK
3) OK

如果在 set b bbb 处失败,set a 已成功不会回滚,set c 还会继续执行。

Redis 事务命令

下表列出了 redis 事务的相关命令:

序号命令及描述1DISCARD 取消事务,放弃执行事务块内的所有命令。2EXEC 执行所有事务块内的命令。3MULTI 标记一个事务块的开始。4UNWATCH 取消 WATCH 命令对所有 key 的监视。5WATCH key [key …] 监视一个 (或多个) key ,如果在事务执行之前这个 (或这些) key 被其他命令所改动,那么事务将被打断。

持久化

Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。

RDB 持久化

将某个时间点的所有数据都存放到硬盘上。

可以将快照复制到其他服务器从而创建具有相同数据的服务器副本。

如果系统发生故障,将会丢失最后一次创建快照之后的数据。

如果数据量大,保存快照的时间会很长。

AOF 持久化

将写命令添加到 AOF 文件(append only file)末尾。

使用 AOF 持久化需要设置同步选项,从而确保写命令 同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。

选项同步频率always每个写命令都同步eyerysec每秒同步一次no让操作系统来决定何时同步

  • always 选项会严重减低服务器的性能
  • everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器几乎没有任何影响。
  • no 选项并不能给服务器性能带来多大的提升,而且会增加系统崩溃时数据丢失的数量。

随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。

复制

通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。

一个从服务器只能有一个主服务器,并且不支持主主复制。

连接过程

  1. 主服务器创建快照文件,即 RDB 文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始像从服务器发送存储在缓冲区的写命令。
  2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令。
  3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。

主从链

随着负载不断上升,主服务器无法很快的更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。

哨兵

Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举处新的主服务器。

分片

分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。

假设有 4 个 Redis 实例 R0, R1, R2, R3, 还有很多表示用户的键 user:1, user:2, … , 有不同的方式来选择一个指定的键存储在哪个实例中。

  • 最简单的是范围分片,例如用户 id 从 0 ~ 1000 的存储到实例 R0 中,用户 id 从 1001 ~ 2000 的存储到实例 R1中,等等。但是这样需要维护一张映射范围表,维护操作代价高。
  • 还有一种是哈希分片。使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道存储的实例。

根据执行分片的位置,可以分为三种分片方式:

  • 客户端分片:客户端使用一致性哈希等算法决定应当分布到哪个节点。
  • 代理分片:将客户端的请求发送到代理上,由代理转发到正确的节点上。
  • 服务器分片:Redis Cluster。

参考资料:

https://mp.weixin.qq.com/s/yPVwyXfBhqPnvwXih_ysqw

https://www.cnblogs.com/powertoolsteam/p/redis.html

https://mp.weixin.qq.com/s/yPVwyXfBhqPnvwXih_ysqw

原文地址:https://zhuanlan.zhihu.com/p/263553600

Spring框架全面详解|Spring快速入门指南

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Spring 框架是什么?
Spring 是于 2003 年兴起的一个轻量级的Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 是可以在Java SE/EE 中使用的轻量级开源框架。
Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。
Spring 根据代码的功能特点,使用Ioc 降低业务对象之间耦合度。IoC 使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring 容器统一管理,自动“注入”,注入即赋值。 而AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成“织入”。
官网:https://spring.io/
Spring的优点?
Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象,容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。
(1) 轻量
Spring 框架使用的jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需的jar 总共在 3M 左右。
Spring 框架运行占用的资源少,运行效率高。不依赖其他jar
(2) 针对接口编程,解耦合
Spring 提供了Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的对象创建方式,现在由容器完成。对象之间的依赖解耦合。
(3) AOP 编程的支持
通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统OOP 实现的功能可以通过AOP 轻松应付在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
(4) 方便集成各种优秀框架
Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring 提供了对各种优秀框架(如Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。
Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把这个插头放入插线板。不需要可以轻易的移除。
Spring 体系结构


Spring 由 20 多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、Web、面向切面编程(AOP, Aspects)、提供JVM 的代理(Instrumentation)、消息发送(Messaging)、核心容器(Core Container)和测试(Test)。
IoC 控制反转
控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值,依赖的管理。
IoC 是一个概念,是一种思想,其实现方式多种多样。当前比较流行的实现方式是依赖注入。应用广泛。
依赖:classA 类中含有classB 的实例,在 classA 中调用classB 的方法完成功能,即 classA 对 classB 有依赖。
Ioc 的实现:
依赖注入:DI(Dependency Injection),程序代码不做定位查询,这些工作由容器自行完成。
依赖注入 DI 是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。 Spring 的依赖注入对调用者与被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。
Spring 框架使用依赖注入(DI)实现IoC。
Spring 容器是一个超级大工厂,负责创建、管理所有的Java 对象,这些Java 对象被称为Bean。Spring 容器管理着容器中Bean 之间的依赖关系, Spring 使用“依赖注入”的方式来管理Bean 之间的依赖关系。使用IoC 实现对象之间的解耦和。
开发工具准备
开发工具:idea2017 以上依赖管理:maven3 以上jdk:1.8 以上
需要设置maven 本机仓库:


Spring 的第一个程序
举例:01-primay
创建maven 项目


引入maven 依赖 pom.xml


定义接口与实体类


创建Spring 配置文件
在src/main/resources/目录现创建一个xml 文件,文件名可以随意,但Spring 建议的名称为applicationContext.xml。
spring 配置中需要加入约束文件才能正常使用,约束文件是 xsd 扩展名。

  • <bean />:用于定义一个实例对象。一个实例对应一个bean 元素。
  • id:该属性是 Bean 实例的唯一标识,程序通过 id 属性访问Bean,Bean与Bean 间的依赖关系也是通过 id 属性关联的。
  • class:指定该 Bean 所属的类,注意这里只能是类,不能是接口。

定义测试类


使用spring 创建非自定义类对象
spring 配置文件加入 java.util.Date 定义:
<bean id=”myDate” class=”java.util.Date” />
MyTest 测试类中:
调用getBean(“myDate”); 获取日期类对象。
容器接口和实现类
ApplicationContext 接口(容器)
ApplicationContext 用于加载 Spring 的配置文件,在程序中充当“容器”的角色。其实现类有两个。


A、 配置文件在类路径下
若 Spring 配置文件存放在项目的类路径下,则使用
ClassPathXmlApplicationContext 实现类进行加载。


B、ApplicationContext 容器中对象的装配时机
ApplicationContext 容器,会在容器对象初始化时,将其中的所有对象一次性全部装配好。以后代码中若要使用到这些对象,只需从内存中直接获取即可。执行效率较高。但占用内存。


C、使用spring 容器创建的java 对象


基于 XML 的 DI
举例:项目di-xml
注入分类
bean 实例在调用无参构造器创建对象后,就要对bean 对象的属性进行初始化。初始化是由容器自动完成的,称为注入。
根据注入方式的不同,常用的有两类:set 注入、构造注入。
(1) set 注入(掌握)
set 注入也叫设值注入是指,通过setter 方法传入被调用者的实例。这种注入方式简单、直观,因而在 Spring 的依赖注入中大量使用。
A、 简单类型


创建java.util.Date 并设置初始的日期时间:
Spring 配置文件:


测试方法:


B、引用类型
当指定bean 的某属性值为另一bean 的实例时,通过ref 指定它们间的引用关系。ref 的值必须为某bean 的 id 值。


对于其它Bean 对象的引用,使用<bean/>标签的ref 属性


测试方法:


(2) 构造注入(理解)
构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即, 使用构造器设置依赖关系。
举例 1:


<constructor-arg />标签中用于指定参数的属性有:
name:指定参数名称。
index:指明该参数对应着构造器的第几个参数,从 0 开始。不过,该属性不要也行,但要注意,若参数类型相同,或之间有包含关系,则需要保证赋值顺序要与构造器中的参数顺序一致。
举例 2:
使用构造注入创建一个系统类 File 对象


测试类:


引用类型属性自动注入
对于引用类型属性的注入,也可不在配置文件中显示的注入。可以通过为<bean/>标签设置autowire 属性值,为引用类型属性进行隐式自动注入(默认是不自动注入引用类型属性)。根据自动注入判断标准的不同,可以分为两种:

  • byName:根据名称自动注入
  • byType: 根据类型自动注入

(1) byName 方式自动注入
当配置文件中被调用者bean 的id 值与代码中调用者bean 类的属性名相同时,可使用byName 方式,让容器自动将被调用者bean 注入给调用者bean。容器是通过调用者的bean 类的属性名与配置文件的被调用者bean 的id 进行比较而实现自动注入的。
举例:


(2)byType 方式自动注入
使用byType 方式自动注入,要求:配置文件中被调用者 bean 的class 属性指定的类,要与代码中调用者bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子类,或是实现类)。但这样的同源的被调用bean 只能有一个。多于一个,容器就不知该匹配哪一个了。
举例:


为应用指定多个 Spring 配置文件
在实际应用里,随着应用规模的增加,系统中Bean 数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将 Spring 配置文件分解成多个配置文件。
包含关系的配置文件:
多个配置文件中有一个总文件,总配置文件将各其它子文件通过<import/>引入。在Java 代码中只需要使用总配置文件对容器进行初始化即可。
举例:
代码:


Spring 配置文件:


也可使用通配符*。但,此时要求父配置文件名不能满足*所能匹配的格式,否则将出现循环递归包含。就本例而言,父配置文件不能匹配 spring-*.xml 的格式,即不能起名为spring-total.xml。


基于注解的 DI
举例:di-annotation 项目
对于 DI 使用注解,将不再需要在 Spring 配置文件中声明bean 实例。Spring 中使用注解,需要在原有 Spring 运行环境基础上再做一些改变。
需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。


指定多个包的三种方式:
1) 使用多个context:component-scan 指定不同的包路径


2) 指定 base-package 的值使用分隔符
分隔符可以使用逗号(,)分号(;)还可以使用空格,不建议使用空格。
逗号分隔:


分号分隔:


3) base-package 是指定到父包名
base-package 的值表是基本包,容器启动会扫描包及其子包中的注解,当然也会扫描到子包下级的子包。所以base-package 可以指定一个父包就可以。


或者最顶级的父包


但不建议使用顶级的父包,扫描的路径比较多,导致容器启动时间变慢。指定到目标包和合适的。也就是注解所在包全路径。例如注解的类在com.bjpowernode.beans 包中


定义Bean 的注解@Component(掌握)
需要在类上使用注解@Component,该注解的value 属性用于指定该bean 的 id 值。
举例:di01


另外,Spring 还提供了 3 个创建对象的注解:

  • @Repository 用于对DAO 实现类进行注解
  • @Service 用于对Service 实现类进行注解
  • @Controller 用于对Controller 实现类进行注解

这三个注解与@Component 都可以创建对象,但这三个注解还有其他的含义,@Service 创建业务层对象,业务层对象可以加入事务功能, @Controller 注解创建的对象可以作为处理器接收用户的请求。
@Repository,@Service,@Controller 是对@Component 注解的细化,标注不同层的对象。即持久层对象,业务层对象,控制层对象。
@Component 不指定value 属性,bean 的 id 是类名的首字母小写。


简单类型属性注入@Value(掌握)
需要在属性上使用注解@Value,该注解的value 属性用于指定要注入的值。
使用该注解完成属性注入时,类中无需setter。当然,若属性有setter, 则也可将其加到setter 上。
举例:


byType 自动注入@Autowired(掌握)
需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配Bean 的方式。
使用该注解完成属性注入时,类中无需setter。当然若属性有setter,则也可将其加到setter上。
举例:


byName 自动注入@Autowired 与@Qualifier(掌握)
需要在引用属性上联合使用注解@Autowired 与@Qualifier。@Qualifier 的value 属性用于指定要匹配的Bean 的id 值。类中无需set 方法,也可加到set 方法上。
举例:


@Autowired 还有一个属性required,默认值为true,表示当匹配失败后,会终止程序运行。若将其值设置为false,则匹配失败,将被忽略,未匹配的属性值为null。


JDK 注解@Resource 自动注入(掌握)
Spring 提供了对jdk 中@Resource 注解的支持。@Resource 注解既可以按名称匹配Bean,也可以按类型匹配Bean。默认是按名称注入。使用该注 解,要求JDK 必须是 6 及以上版本。@Resource 可在属性上,也可在set 方法上。
(1) byType 注入引用类型属性
@Resource 注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入bean,则会按照类型进行Bean 的匹配注入。
举例:


(2) byName 注入引用类型属性
@Resource 注解指定其name 属性,则name 的值即为按照名称进行匹配的Bean 的 id。
举例:


注解与 XML 的对比
注解优点是:

  • 方便
  • 直观
  • 高效(代码少,没有配置文件的书写那么复杂)。

其弊端也显而易见:以硬编码的方式写入到Java 代码中,修改是需要重新编译代码的。
XML 方式优点是:

  • 配置和代码是分离的
  • 在xml 中做修改,无需编译代码,只需重启服务器即可将新的配置加载。xml 的缺点是:编写麻烦,效率低,大型项目过于复杂。

原文:https://zhuanlan.zhihu.com/p/515791410

Nginx超详细又简单的入门教程

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

几乎所有的项目部署,都会用到Nginx。

主要用到Nginx的功能有如下:

  • 正向、反向代理
  • 过滤IP
  • 负载均衡

这三个功能是大部分公司的线上项目部署用到的,掌握Nginx并不难,主要是学会配置nginx.conf 文件,这个文件是用到了一些Nginx的语法,会用简单,但是用好却很难。

下面来简单介绍一下Nginx。

1、Nginx介绍

Nginx下载地址:http://nginx.org

Nginx 是开源、高性能、高可靠的 Web 和反向代理服务器,而且支持热部署,几乎可以做到 7 * 24 小时不间断运行,即使运行几个月也不需要重新启动,还能在不间断服务的情况下对软件版本进行热更新。

如果你是前端,相信你用过Node作为服务器。

如果你是后端,相信你用过Tomcat作为服务器。

Nginx也作为一个轻量级的服务器,其功能与Node、Tomcat是不冲突的。

Nginx的优势就是其性能,其占用内存少、并发能力强、能支持高达 5w 个并发连接数。

所以Nginx一般被用来当做服务器的第一道门,其应用场景有:

  • 正向、反向代理
  • 过滤IP,白名单
  • 负载均衡
  • 静态资源服务

2、Nginx的一些概念

在学习Nginx之前,先要了解一下下面几个概念,做简单了解即可。

2.1、CORS

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),也就是我们常说的跨域

在浏览器上当前访问的网站向另一个网站发送请求获取数据的过程就是跨域请求。因为浏览器有安全策略,不然随便访问其他网站资源很可能会造成安全隐患,而CORS就可以打破这个限制。

它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

浏览器将CORS请求分成两类:简单请求(simple request)非简单请求(not-so-simple request)。

简单的说,浏览器在发送跨域请求的时候,会先判断下是简单请求还是非简单请求,如果是简单请求,就先执行服务端程序,然后浏览器才会判断是否跨域。

以下是一些跨域的例子:

# 不同源的例子
http://example.com/app1   # 协议不同
https://example.com/app2
​
http://example.com        # host 不同
http://www.example.com
http://myapp.example.com
​
http://example.com        # 端口不同
http://example.com:8080

2.2、两种请求

2.2.1、简单请求

  1. 请求方法是 HEADGETPOST 三种之一;
  2. HTTP 头信息不超过这几个字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type
  3. 而且Content-Type 只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain

如果同时满足上面三个条件的,就属于简单请求

凡是不同时满足上面三个条件的,就属于非简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

✈️

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1 Origin: http://rain.baimuxym.cn Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0…

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求,Host是目前的域名。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://rain.baimuxym.cn Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8

2.2.2、非简单请求

常见的非简单请求有:

  • put,delete 方法的ajax请求
  • 发送json格式的ajax请求,Content-Type 值为 application/json
  • 带自定义头的ajax请求

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight),浏览器发送一次 HTTP 预检 OPTIONS请求,先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 请求方法和头信息字段。

下面是这个”预检”请求的HTTP头信息。

OPTIONS /cors HTTP/1.1 Origin: http://rain.baimuxym.cn Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0…

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

服务器收到”预检”请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

只有得到肯定答复,浏览器才会发出正式的 XHR 请求,否则报错。

以上简单介绍了一下跨域的原理,其实跨域的解决方法在前端和后端都可以配置,方法也很多,这里就不展开讲了。

2.3、正向代理和反向代理

正向代理是一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定原始服务器,然后代理向原始服务器转交请求并将获得的内容返回给客户端。代理服务器和客户端处于同一个局域网内。

比如说 你要访问 pornhub,你的浏览器是无法直接的,于是我就通过代理服务器(fanqiang)让它帮我转发,这种方式是客户端指定请求URL的。

“喂?是代理服务器吗,帮我访问一下 www.pornhub.com“,正向代理的工作原理就像一个跳板。

反向代理实际运行方式是代理服务器接受网络上的连接请求。它将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给网络上请求连接的客户端 。

代理服务器和原始服务器处于同一个局域网内。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。一般在处理跨域请求的时候比较常用。

比如说我要访问 https://rain.baimuxym.cn/images.jpg,对我来说不知道图片是不是同一个服务器返回回来的,甚至这个图片根本不在这台服务器,图片可以是服务器偷偷从其他 服务器,比如从 https://images.baimuxym.cn 拿回来的,但是用户并不知情。

代理的好处:

  • 保护了真实的web服务器,保证了web服务器的资源安全

只用于代理内部网络对Internet外部网络的连接请求,不支持外部网络对内部网络的连接请求,因为内部网络对外部网络是不可见的。所有的静态网页或者CGI程序,都保存在内部的Web服务器上。因此对反向代理服务器的攻击并不会使得网页信息遭到破坏,这样就增强了Web服务器的安全性。

  • 节约了有限的IP地址资源

共享一个在internet中注册的IP地址(内网局域网),这些服务器分配私有地址,采用虚拟主机的方式对外提供服务。

  • 减少WEB服务器压力,提高响应速度

负载的实现方式之一,不同的资源可以放在不同的服务器。(下面提到的负载均衡、动静分离也是通过反向代理实现的)

2.4、负载均衡

随着业务的发展,单个机器性能有限,无法承受巨大的请求压力,这个时候集群的概念产生了,将请求分发到各个服务器上(每台服务器的代码都是一样的),将负载分发到不同的服务器,这就是负载均衡,核心是「分摊压力」。

如何分发压力?Nginx也可以配置规则,比如说权重、轮询、hash 等等。

2.5、动静分离

动静分离的初衷是为了加快网站的访问速度。像大图片、视频、CSS 这种静态资源,如果都放在同一个服务器,在请求的时候就会造成带宽压力,如果把这些静态资源分散到不同的服务器,配合CDN,就可以减少服务器的压力。

由于 Nginx 的高并发和静态资源缓存等特性,经常将静态资源部署在 Nginx 上。

如果请求的是静态资源,直接到静态资源目录获取资源,如果是动态资源的请求,则利用反向代理的原理,把请求转发给对应后台应用去处理,从而实现动静分离。

3、安装Nginx

Nginx下载地址:https://nginx.org/en/download.html

我这里演示的Linux Centos7的安装,如果你是Windows,那就更简单了,直接解压即可。

1、gcc 安装

Nginx 是 C语言 开发,安装 Nginx 需要先将官网下载的源码进行编译,编译依赖 gcc 环境,如果没有 gcc 环境,则需要安装:

yum install gcc-c++

我这里显示已经是最新了:

[root@VM-8-8-centos ~]# yum install gcc-c++
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
Package gcc-c++-4.8.5-39.el7.x86_64 already installed and latest version
Nothing to do

2、 PCRE pcre-devel 安装 PCRE(Perl Compatible Regular Expressions) 是一个Perl库,包括 perl 兼容的正则表达式库。nginx 的 http 模块使用 pcre 来解析正则表达式,所以需要在 linux 上安装 pcre 库,pcre-devel 是使用 pcre 开发的一个二次开发库。

nginx也需要此库。命令:

yum install -y pcre pcre-devel

3、zlib 安装 zlib 库提供了很多种压缩和解压缩的方式, nginx 使用 zlib 对 http 包的内容进行 gzip ,所以需要在 Centos 上安装 zlib 库。

yum install -y zlib zlib-devel

4、OpenSSL 安装 OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用。 nginx 不仅支持 http 协议,还支持 https(即在ssl协议上传输http),所以需要在 Centos 安装 OpenSSL 库。

yum install -y openssl openssl-devel

5、Nginx解压

tar -zxvf nginx-1.15.2.tar.gz -C /var/www/web

6、编译Nginx

进入Nginx安装目录,输入 ./configure,表示使用Nginx的默认配置,开启ssl

[root@VM-8-8-centos software]# cd /var/www/web/nginx-1.15.2/
[root@VM-8-8-centos nginx-1.15.2]# ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module

注意!--prefix=/usr/local/nginx 的意思是Nginx安装目录,如果不写默认的目录也是这个,如果你自定义,在下一步需要到你自定义的目录执行makemake install

然后输入:

make

编译成功再输入:

make install

Nginx的默认安装目录是 /usr/local/nginx

7、启动Nginx

Nginx的默认配置,是指向 /usr/local/nginx/sbin/这个目录的,配置文件是这里 /usr/local/nginx/conf

Nginx常用命令:(需要在安装目录执行,我的是 /usr/local/nginx/sbin/ )

./nginx  # 启动
./nginx -s stop #停止
./nginx -s reload #重启

启动,成功找到Nginx的进程。

[root@VM-8-8-centos nginx-1.15.2]# cd /usr/local/nginx/sbin/
[root@VM-8-8-centos sbin]# ./nginx
[root@VM-8-8-centos sbin]# ps -aux|grep nginx
root     11505  0.0  0.0  20556   616 ?        Ss   18:00   0:00 nginx: master process ./nginx
nobody   11506  0.0  0.0  23092  1384 ?        S    18:00   0:00 nginx: worker process
root     11545  0.0  0.0 112712   960 pts/4    R+   18:00   0:00 grep --color=auto nginx

然后浏览器输入自己的IP,出现Welcome to Nginx 就表示成功了。

为了方便使用,我们把Nginx添加到全局变量:

[root@VM-8-8-centos sbin]# ln -s /usr/local/nginx/sbin/nginx /usr/local/bin/

添加完毕,我们在任何一个目录就可以使用Nginx命令了。

这里要记住的是你的Nginx配置文件nginx.conf 目录是:/usr/local/nginx/conf/nginx.conf

4、Nginx常见命令

这里是把Nginx添加到全局变量才能这样操作,否则需要进入到 /usr/local/nginx/sbin/ 目录,windows直接进入安装目录即可。

帮助命令:nginx -h
启动Nginx服务器 : nginx
查看进程: ps aux | grep nginx
配置文件路径:/usr/local/nginx/conf/nginx.conf
检查配置文件: nginx -t
指定启动配置文件: nginx -c /usr/local/nginx/conf/nginx.conf
暴力停止服务: nginx -s stop
优雅停止服务: nginx -s quit
重新加载配置文件: nginx -s reload

5、Nginx的配置

5.1、Nginx的构成

打开/usr/local/nginx/conf/nginx.conf 文件,windows的配置文件在安装目录的 conf/nginx.conf

main        # 全局配置,也称为Main块,对全局生效
├── events  # 配置影响 Nginx 服务器或与用户的网络连接
├── http    # 配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置
│   ├── upstream # 配置后端服务器具体地址,负载均衡配置不可或缺的部分
│   ├── server   # 配置虚拟主机的相关参数,一个 http 块中可以有多个 server 块
│   ├── server
│   │   ├── location  # server 块可以包含多个 location 块,location 指令用于匹配 uri 
│   │   ├── location
│   │   └── ...
│   └── ...
└── ...

这个就像是Java的类一样,有公共的,也有私有的,server就像是一个 if 语法块,location又是一个子if语法块,只有匹配到了才会进入最后的URL。

一个 Nginx 配置文件的结构就像 nginx.conf 显示的那样,配置文件的语法规则:

  1. 配置文件由指令与指令块构成;
  2. 每条指令以 ; 分号结尾,指令与参数间以空格符号分隔;
  3. 指令块以 {} 大括号将多条指令组织在一起;
  4. include 语句允许组合多个配置文件以提升可维护性;
  5. 使用 # 符号添加注释,提高可读性;
  6. 使用 $ 符号使用变量;
  7. 部分指令的参数支持正则表达式;

5.2、Nginx的典型配置

#-----------全局块 START-----------
user  nobody;                        # 运行用户,默认即是nginx,可以不进行设置
worker_processes  1;                # Nginx 进程数,一般设置为和 CPU 核数一样
worker_processes auto;             # Nginx 进程数 与当前 CPU 物理核心数一致
error_log  /var/log/nginx/error.log warn;   # Nginx 的错误日志存放目录,日志级别是warn
pid        /var/run/nginx.pid;      # Nginx 服务启动时的 pid 存放位置
worker_rlimit_nofile 20480; # 可以理解成每个 worker 子进程的最大连接数量。
​
#-----------全局块 END-----------
​
#-----------events块 START-----------
events {
    accept_mutex on;   #设置网路连接序列化,防止惊群现象发生,默认为on
    multi_accept on;  #设置一个进程是否同时接受多个网络连接,默认为off
     # 事件驱动模型有 select|poll|kqueue|epoll|resig|/dev/poll|eventport
    use epoll;   #使用 epoll的I/O模型,建议使用默认
    worker_connections 1024;   # 每个进程允许最大并发数
}
#-----------events块 END-----------
​
http {   # 配置使用最频繁的部分,代理、缓存、日志定义等绝大多数功能和第三方模块的配置都在这里设置
    # 设置日志自定义格式
    log_format  myFormat  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
​
    access_log  /var/log/nginx/access.log  myFormat;   # Nginx访问日志存放位置,并采用上面定义好的格式
​
    sendfile            on;   # 开启高效传输模式
    tcp_nopush          on;   # 减少网络报文段的数量
    tcp_nodelay         on;
    keepalive_timeout   65;   # 保持连接的时间,也叫超时时间,单位秒
    types_hash_max_size 2048;
​
    include             /etc/nginx/mime.types;      # 文件扩展名与类型映射表
    default_type        application/octet-stream;   # 默认文件类型
​
    include /etc/nginx/conf.d/*.conf;   # 使用include引入其他子配置项,不存在会报错
    
    server {
        keepalive_requests 120; #单连接请求上限次数。
        listen       80;       # 配置监听的端口
        server_name  localhost 127.0.0.1 baimuxym.cn;    # 配置监听的域名或者地址,可多个,用空格隔开
        
        location / {
            root   /usr/share/nginx/html;  # 网站根目录
            index  index.html index.htm;   # 默认首页文件
            deny 172.18.5.54   # 禁止访问的ip地址,可以为all
            allow 172.18.5.53;# 允许访问的ip地址,可以为all
        }
          # 图片防盗链
         location ~* \.(gif|jpg|jpeg|png|bmp|swf)$ {
            valid_referers none blocked 192.168.0.2;  # 只允许本机 IP 外链引用
            if ($invalid_referer){
              return 403;
            }
          }
        
        #新增内容,可以在自己的server单独配置错误日志
        error_log    logs/error_localhost.log    error;
        
        error_page 500 502 503 504 /50x.html;  # 默认50x对应的访问页面
        error_page 400 404 error.html;   # 同上
    }
}

特别说明一下:

1、error_log的默认值

error_log logs/error.log error;

error_log的语法格式及参数语法说明如下:

 error_log  <FILE>  <LEVEL>;
 关键字     日志文件  错误日志级别

关键字:其中关键字error_log不能改变

日志文件:可以指定任意存放日志的目录

错误日志级别:常见的错误日志级别有[debug | info | notice | warn | error | crit | alert | emerg],级别越高记录的信息越少。

生产场景一般是 warn | error | crit 这三个级别之一
注意:不要配置info等级较低的级别,会带来大量的磁盘I/O消耗。

error_log参数允许放置的标签段位置:

main, http, server, location

测试了一下,日志文件不存在,Nginx不会创建,所以要提前手动创建。

2、events 块

events 块涉及的指令主要影响 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 work process 下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 word process 可以同时支持的最大连接数等。 上述例子就表示每个 work process 支持的最大连接数为 1024,这部分的配置对 Nginx 的性能影响较大,在实际中应该灵活配置。

5.3 、Nginx的location

server 块可以包含多个 location 块,location 指令用于匹配 uri,语法:

location [ = | ~ | ~* | ^~ ]  /uri/  {
	...
}
  • = 开头表示精确匹配,如果匹配成功,不再进行后续的查找;
  • ^ 以xxx开头的匹配
  • $ 以xxx结尾的匹配
  • * 代表任意字符
  • ^~ 匹配开头以 xxx 的路径,理解为匹配 url 路径即可。nginx不对url做编码,因此请求为/static/20%/HaC,可以被规则^~ /static/ /HaC匹配到(注意是空格)
  • ~ 开头表示区分大小写的正则匹配
  • ~* 开头表示不区分大小写的正则匹配
  • ! 表示不包含xxx就匹配
  • / 通用匹配,任何请求都会匹配到。

~* 和 ~优先级都比较低,如有多个location的正则能匹配的话,则使用正则表达式最长的那个;

如果 uri 包含正则表达式,则必须要有 ~ 或 ~* 标志。

借用https://www.cnblogs.com/jpfss/p/10232980.html博客一文的例子来说明一下:

server {
    listen 80;
    server_name  localhost;
    location = / {
       #规则A
    }
    location = /login {
       #规则B
    }
    location ^~ /static/ {
       #规则C
    }
    location ~ \.(gif|jpg|png|js|css)$ {
       #规则D,注意:是根据括号内的大小写进行匹配。括号内全是小写,只匹配小写
    }
    location ~* \.png$ {
       #规则E
    }
    location !~ \.xhtml$ {
       #规则F
    }
    location !~* \.xhtml$ {
       #规则G
    }
    location / {
       #规则H
    }
}

那么产生的效果如下:

所以实际使用中,个人觉得至少有三个匹配规则定义,如下:

#直接匹配网站根,通过域名访问网站首页比较频繁,使用这个会加速处理,官网如是说。
#这里是直接转发给后端应用服务器了,也可以是一个静态首页
# 第一个必选规则
location = / {
	#反向代理
    proxy_pass http://127.0.0.1:8080/index
}
 
# 第二个必选规则是处理静态文件请求,这是nginx作为http服务器的强项
# 有两种配置模式,目录匹配或后缀匹配,任选其一或搭配使用
location ^~ /static/ {                              //以xx开头
    root /webroot/static/;
}
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {     //以xx结尾
    root /webroot/res/;
}
 
#第三个规则就是通用规则,用来转发动态请求到后端应用服务器
#非静态文件请求就默认是动态请求,自己根据实际把握
location / {
    proxy_pass http://tomcat:8080/
}

5.4、Nginx的全局变量

Nginx提供了一些全局变量,我们可以在任意的位置使用这些变量。

全局变量名功能

5.5、Nginx的语法

5.5.1、return

有点类似于Java编程,Nginx也可以使用return关键字进行返回。

语法:

return code [text];
return code URL;
return URL;

例如:

location / {
 return 404; # 直接返回状态码
}

location / {
 return 404 "pages not found"; # 返回状态码 + 一段文本
}

location / {
 return 302 /bbs ; # 返回状态码 + 重定向地址
}

location / {
 return https://www.rain.baimuxym.cn ; # 返回重定向地址
}

return相似的还有break,跳出当前作用域,回到上一层继续向下,可以在server块,location块,if块中使用。

5.5.2、rewrite

重写。(可以理解为重定向)

语法:

rewrite 正则表达式 要替换的内容 [flag];

rewrite 最后面还可以接参数:

  • redirect 返回 302 临时重定向;
  • permanent 返回 301 永久重定向;

例子:

server {
     listen       80;
     server_name  localhost;
     charset      utf-8;
      
     location / {
     # 浏览器访问 ip:80/test/,实际访问的是 http://www.baidu.com
         rewrite '^/test/' http://www.baidu.com break;
     }
     location /image/ {
     # 其中$1表示引用前面的([a-z]{3}),$2表示引用前面的'(.*)'
    	 rewrite '^/([a-z]{3})/(.*)' /web/$1/$2 permanent;
	}
}

5.5.3、if

语法:

if (condition) { # 注意,if 和左括号( 有一个空格
    ...
}

可以在server块、location块使用

condition可以是:

    =,!= : 判断变是否相等
    正则表达式: ~(区分大小写),~*(不区分大小写),!~(~取反),!~*(~*取反)
    -f,!-f: 文件时是否存在
    -d,!-d: 目录是否存在
    -e,!-e: 目录或文件是否在使用
    -x,!-x: 文件是否可执行

例子:

if ($http_user_agent ~ Chrome){
  rewrite /(.*)/browser/$1 break;
}

location / {
	if ( $uri = "/images/" ){
		rewrite (.*) /pics/ break;
	}
	if ($request_method = POST) {
   		return 405;
	}
}

6、负载均衡与限流

6.1、负载均衡

在头部使用upstream即可使用负载均衡,Nginx 默认提供的负载均衡策略:

  • 1、轮询(默认)round_robin
    每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器 down 掉,能自动剔除。
  • 2、IP 哈希 ip_hash
    每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 session 共享的问题。
    当然,实际场景下,一般不考虑使用 ip_hash 解决 session 共享。
    还有相似的url_hash,通过请求url进行hash,再通过hash值选择后端server
  • 3、最少连接 least_conn
    下一个请求将被分派到活动连接数量最少的服务器
  • 4、权重 weight
    weight的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下,达到合理的资源利用率。

还可以通过插件支持其他策略。

  • 5、fair(第三方),按后端服务器的响应时间分配,响应时间短的优先分配,依赖第三方插件 nginx-upstream-fair,需要先安装;

例如:使用weight负载均衡策略分发请求到不同的服务

	upstream mysite { 
	# ip_hash # ip_hash 方式
	# weight权重分配方式,表示 1:1
        server 127.0.0.1:8090 weight=1; 
        server 127.0.0.1:8091 weight=1;
    }

    server {
        listen       80;
        server_name  hellocoder.com ;

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
		
		location / {
		 proxy_pass http://mysite;
        }
    }

使用IP、URL hash策略:

    upstream mysite { 
     # ip_hash 策略
	# ip_hash;
	# url_hash 策略
	hash $request_uri;
     	server 127.0.0.1:8090;
        server 127.0.0.1:8091;
    }

在 upstream 内可使用的指令:

  • server 定义上游服务器地址;
  • zone 定义共享内存,用于跨 worker 子进程;
  • keepalive 对上游服务启用长连接;
  • keepalive_requests 一个长连接最多请求 HTTP 的个数;
  • keepalive_timeout 空闲情形下,一个长连接的超时时长;
  • hash 哈希负载均衡算法;
  • ip_hash 依据 IP 进行哈希计算的负载均衡算法;
  • least_conn 最少连接数负载均衡算法;
  • least_time 最短响应时间负载均衡算法;
  • random 随机负载均衡算法;

6.2、限流

ngx_http_limit_req_module 模块提供了漏桶算法(leaky bucket),可以限制单个IP的请求处理频率。

如:

正常限流:

http {
	limit_req_zone 192.168.1.1 zone=myLimit:10m rate=5r/s;
}

server {
	location / {
		limit_req zone=myLimit;
		rewrite / http://www.hac.cn permanent;
	}
}

参数解释:

key: 定义需要限流的对象。
zone: 定义共享内存区来存储访问信息。
rate: 用于设置最大访问速率。

表示基于客户端192.168.1.1进行限流,定义了一个大小为10M,名称为myLimit的内存区,用于存储IP地址访问信息。

rate 设置IP访问频率,rate=5r/s表示每秒只能处理每个IP地址的5个请求。Nginx限流是按照毫秒级为单位的,也就是说1秒处理5个请求会变成每200ms只处理一个请求。如果200ms内已经处理完1个请求,但是还是有有新的请求到达,这时候Nginx就会拒绝处理该请求。

7、Nginx代理

7.1、正向代理

我上传了一个静态网站在某个服务器目录,假如我的服务器是81.71.16.134

那要如何通过我的域名 https://learnjava.baimuxym.cn 访问它呢?

步骤如下:

  1. 首先需要把域名解析到我的IP
  2. 申请SSL证书,配置服务器
  3. 正向代理,监听443端口(因为https的端口是443),转发;监听 80 端口,http:// 强制 跳到 https://
  server {
       listen       80; #监听端口
       server_name  learnjava.baimuxym.cn; #请求域名
       return      301 https://$host$request_uri; #重定向至https访问。
   }
   
    server {
    listen 443 ssl; # 这里改的是
    server_name learnjava.baimuxym.cn; # 改为绑定证书的域名

    ssl on;
    ssl_certificate /usr/local/nginx/learnjava.baimuxym.cn/1_learnjava.baimuxym.cn_bundle.crt; # 改为自己申请得到的 crt 文件的名称或者 pem文件的名称
    ssl_certificate_key /usr/local/nginx/learnjava.baimuxym.cn/2_learnjava.baimuxym.cn.key; # 改为自己申请得到的 key 文件的名称
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;

    location / { # 匹配所有
    root  /var/www/web/LearnJavaToFindAJob/docs; # 你的静态网站项目路径,刚刚上传的目录
    index index.html;	 # 首页名称
    	}
    }

7.2 、反向代理

和正向代理差不多的配置,我这里列举一下 location的不同:

location / {
	proxy_http_version 1.1; #代理使用的http协议
	proxy_set_header Host $host; #header添加请求host信息
	proxy_set_header X-Real-IP $remote_addr; # header增加请求来源IP信息
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 增加>代理记录
	proxy_pass http://127.0.0.1:8888;
}

只需要简单使用 proxy_pass 就可以反向代理,可用于跳转到Tomcat的服务地址、springbooot项目的服务地址。

Nginx详细的实践可以参考:TODOLIST

8、开启 gzip 压缩

HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术,web服务器和客户端(浏览器)必须共同支持gzip。

目前主流的浏览器,Chrome,firefox,IE等都支持该协议。

客户端请求时:支持的浏览器会加上 Accept-Encoding: gzip 这个 header,表示自己支持 gzip 的压缩方式,

服务器处理时:Nginx 在拿到这个请求的时候,如果有相应配置,就会返回经过 gzip 压缩过的文件给浏览器,并在 response 相应的时候加上 content-encoding: gzip 来告诉浏览器自己采用的压缩方式

Nginx开启gzip的配置如下:

http {
  # 开启gzip
  gzip on;

  # 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩
  # ,建议设置成大于1k,如果小于1k可能会越压越大
  gzip_min_length 1k;

  # 压缩比率,用来指定gzip压缩比(1~9)
  # 1:压缩比最小,速度最快,9:压缩比最大,传输速度最快,但处理最慢,也比较的消耗CPU资源
  gzip_comp_level 2;

  # 进行压缩的文件类型。text/html 文件被系统强制启用
  gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

  # 是否在http header中添加Vary: Accept-Encoding,建议开启
  gzip_vary on;
}

开启gzip对网站的性能有很大的提升,比如说我的网站,这是没有开启gzip压缩前的大小,可以看到是50.2kB,time是100ms

这是开启gzip压缩后的大小,可以看到是11.5kB,time是88ms

比如说掘金的网站也是使用了gzip,可以通过 content-encoding 进行判断:

9、HTTP、IP转发到HTTPS

我们购买了SSL证书后配置了HTTPS,用户在访问 http://rain.baimuxym.cn 或者http://81.71.16.134访问的时候,我们可以让它强制跳转到 https://rain.baimuxym.cn

server {
    listen 80;
    server_name 81.71.16.134;
    return 301 https://www.baimuxym.cn$request_uri; #ip重定向跳至https访问。
}


server {
    listen       80; #监听端口
    server_name  *.baimuxym.cn #请求域名
    location  ^~ / {
    return  301 https://$server_name$request_uri; #重定向至https访问。
    
    # 或者
    # 全局非 https 协议时重定向
    if ($scheme != 'https') {
        return 301 https://$server_name$request_uri;
    }

    # 或者直接全部重定向,不用判断HTTP还是HTTPS
    return 301 https://$server_name$request_uri;
}

10、Nginx的第三方模块

Nginx提供了很多第三方的模块,相对于一个插件,这里可以查询:https://www.nginx.com/nginx-wiki/build/dirhtml/modules/

1、ngx_http_limit_req_module 限流模块

1、控制速率

ngx_http_limit_req_module 模块提供了漏桶算法(leaky bucket),可以限制单个IP的请求处理频率。

如:

1.1 正常限流:

http {
	limit_req_zone 192.168.1.1 zone=myLimit:10m rate=5r/s;
}

server {
	location / {
		limit_req zone=myLimit;
		rewrite / http://www.hac.cn permanent;
	}
}

参数解释:

key: 定义需要限流的对象。
zone: 定义共享内存区来存储访问信息。
rate: 用于设置最大访问速率。

表示基于客户端192.168.1.1进行限流,定义了一个大小为10M,名称为myLimit的内存区,用于存储IP地址访问信息。rate设置IP访问频率,rate=5r/s表示每秒只能处理每个IP地址的5个请求。Nginx限流是按照毫秒级为单位的,也就是说1秒处理5个请求会变成每200ms只处理一个请求。如果200ms内已经处理完1个请求,但是还是有有新的请求到达,这时候Nginx就会拒绝处理该请求。

1.2 突发流量限制访问频率

上面rate设置了 5r/s,如果有时候流量突然变大,超出的请求就被拒绝返回503了,突发的流量影响业务就不好了。

这时候可以加上burst 参数,一般再结合 nodelay 一起使用。

server {
	location / {
		limit_req zone=myLimit burst=20 nodelay;
		rewrite / http://www.hac.cn permanent;
	}
}

burst=20 nodelay 表示这20个请求立马处理,不能延迟,相当于特事特办。不过,即使这20个突发请求立马处理结束,后续来了请求也不会立马处理。burst=20 相当于缓存队列中占了20个坑,即使请求被处理了,这20个位置这只能按 100ms一个来释放。

2、控制并发连接数

ngx_http_limit_conn_module 提供了限制连接数功能。

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

server {
    ...
    limit_conn perip 10;
    limit_conn perserver 100;
}

limit_conn perip 10 作用的key 是 $binary_remote_addr,表示限制单个IP同时最多能持有10个连接。

limit_conn perserver 100 作用的key是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数。

注:limit_conn perserver 100 作用的key是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数。

拓展:

如果不想做限流,还可以设置白名单:

利用 Nginx ngx_http_geo_module 和 ngx_http_map_module 两个工具模块提供的功能。

##定义白名单ip列表变量
geo $limit {
    default 1;
    10.0.0.0/8 0;
    192.168.0.0/10 0;
    81.56.0.35 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
# 正常限流设置
limit_req_zone $limit_key zone=myRateLimit:10m rate=10r/s;

geo 对于白名单 将返回0,不限流;其他IP将返回1,进行限流。

具体参考:http://nginx.org/en/docs/http/ngx_http_geo_module.html

除此之外:

ngx_http_core_module 还提供了限制数据传输速度的能力(即常说的下载速度)

location /flv/ {
    flv;
    limit_rate_after 500m;
    limit_rate       50k;
}

针对每个请求,表示客户端下载前500m的大小时不限速,下载超过了500m后就限速50k/s。

2、ngx_http_ssl_module模块

ngx_http_ssl_module模块提供对HTTPS必要的支持。

常用配置:

1、ssl on | off; 
    为指定虚拟机启用HTTPS protocol,建议用listen指令代替
    可用位置:http, server

2、ssl_certificate file; 
    当前虚拟主机使用PEM格式的证书文件
    可用位置:http, server

3、ssl_certificate_key file; 
    当前虚拟主机上与其证书匹配的私钥文件
    可用位置:http, server

4、ssl_protocols [SSLv2] [SSLv3] [TLSv1] [TLSv1.1] [TLSv1.2]; 
    支持ssl协议版本,默认为后三个
    可用位置:http, server

5、ssl_session_cache off | none | [builtin[:size]] [shared:name:size]; 
    builtin[:size]:使用OpenSSL内建缓存,为每worker进程私有
    [shared:name:size]:在各worker之间使用一个共享的缓存
    可用位置:http, server

6、ssl_session_timeout time; 
    客户端连接可以复用sslsession cache中缓存的ssl参数的有效时长,默认5m
    可用位置:http, server

11、总结

以上就是一些关于Nginx的简单用法,网上的资料也很多,有兴趣的可以自己学习一下,比如说 图片服务器、文件下载服务器、动静分离、跨域解决等等的实现。

Nginx更详细的说明,建议大家去Nginx的官方网站查看。

原文:https://zhuanlan.zhihu.com/p/435009231

保姆级 Git 入门教程,10000 字详解

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Git简介

Git 是一种分布式版本控制系统,它可以不受网络连接的限制,加上其它众多优点,目前已经成为程序开发人员做项目版本管理时的首选,非开发人员也可以用 Git 来做自己的文档版本管理工具。

大概是大二的时候开始接触和使用Git,从一开始的零接触到现在的重度依赖,真是感叹 Git 的强大。

Git 的api很多,但其实平时项目中90%的需求都只需要用到几个基本的功能即可,所以本文将从 实用主义 和 深入探索 2个方面去谈谈如何在项目中使用 Git,一般来说,看完 实用主义 这一节就可以开始在项目中动手用。

“说明:本文的操作都是基于 Mac 系统

实用主义

准备阶段

进入 Git官网 下载合适你的安装包,安装好 Git 后,打开命令行工具,进入工作文件夹(为了便于理解我们在系统桌面上演示),创建一个新的demo文件夹。

进入 Github网站 注册一个账号并登录,进入 我的博客,点击 Clone or download,再点击 Use HTTPS ,复制项目地址 https://github.com/gafish/gafish.github.com.git 备用。

再回到命令行工具,一切就绪,接下来进入本文的重点。

常用操作

所谓实用主义,就是掌握了以下知识就可以玩转 Git,轻松应对90%以上的需求。以下是实用主义型的Git命令列表,先大致看一下

  • git clone
  • git config
  • git branch
  • git checkout
  • git status
  • git add
  • git commit
  • git push
  • git pull
  • git log
  • git tag

接下来,将通过对 我的博客 仓库进行实例操作,讲解如何使用 Git 拉取代码到提交代码的整个流程。

git clone

“从git服务器拉取代码

git clone https://github.com/gafish/gafish.github.com.git

代码下载完成后在当前文件夹中会有一个 gafish.github.com 的目录,通过 cd gafish.github.com 命令进入目录。

git config

“配置开发者用户名和邮箱

git config user.name gafish
git config user.email gafish@qqqq.com

每次代码提交的时候都会生成一条提交记录,其中会包含当前配置的用户名和邮箱。

git branch

“创建、重命名、查看、删除项目分支,通过 Git 做项目开发时,一般都是在开发分支中进行,开发完成后合并分支到主干。

git branch daily/0.0.0

创建一个名为 daily/0.0.0 的日常开发分支,分支名只要不包括特殊字符即可。

git branch -m daily/0.0.0 daily/0.0.1

如果觉得之前的分支名不合适,可以为新建的分支重命名,重命名分支名为 daily/0.0.1

git branch

通过不带参数的branch命令可以查看当前项目分支列表

git branch -d daily/0.0.1

如果分支已经完成使命则可以通过 -d 参数将分支删除,这里为了继续下一步操作,暂不执行删除操作

git checkout

“切换分支

git checkout daily/0.0.1

切换到 daily/0.0.1 分支,后续的操作将在这个分支上进行

git status

“查看文件变动状态

通过任何你喜欢的编辑器对项目中的 README.md 文件做一些改动,保存。

git status

通过 git status 命令可以看到文件当前状态 Changes not staged for commit:改动文件未提交到暂存区

On branch daily/0.0.1
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   README.md
no changes added to commit (use "git add" and/or "git commit -a")

git add

“添加文件变动到暂存区

git add README.md

通过指定文件名 README.md 可以将该文件添加到暂存区,如果想添加所有文件可用 git add . 命令,这时候可通过 git status 看到文件当前状态 Changes to be committed: (文件已提交到暂存区

On branch daily/0.0.1
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   README.md

git commit

“提交文件变动到版本库

git commit -m '这里写提交原因'

通过 -m 参数可直接在命令行里输入提交描述文本

git push

“将本地的代码改动推送到服务器

git push origin daily/0.0.1

origin 指代的是当前的git服务器地址,这行命令的意思是把 daily/0.0.1 分支推送到服务器,当看到命令行返回如下字符表示推送成功了。

Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 267 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local objects.
To https://github.com/gafish/gafish.github.com.git
 * [new branch]      daily/0.0.1 -> daily/0.0.1

现在我们回到Github网站的项目首页,点击 Branch:master 下拉按钮,就会看到刚才推送的 daily/00.1 分支了

git pull

“将服务器上的最新代码拉取到本地

git pull origin daily/0.0.1

如果其它项目成员对项目做了改动并推送到服务器,我们需要将最新的改动更新到本地,这里我们来模拟一下这种情况。

进入Github网站的项目首页,再进入 daily/0.0.1 分支,在线对 README.md 文件做一些修改并保存,然后在命令中执行以上命令,它将把刚才在线修改的部分拉取到本地,用编辑器打开 README.md ,你会发现文件已经跟线上的内容同步了。

如果线上代码做了变动,而你本地的代码也有变动,拉取的代码就有可能会跟你本地的改动冲突,一般情况下 Git 会自动处理这种冲突合并,但如果改动的是同一行,那就需要手动来合并代码,编辑文件,保存最新的改动,再通过 git add .和 git commit -m 'xxx' 来提交合并。

git log

“查看版本提交记录

git log

通过以上命令,我们可以查看整个项目的版本提交记录,它里面包含了提交人日期提交原因等信息,得到的结果如下:

commit c334730f8dba5096c54c8ac04fdc2b31ede7107a
Author: gafish <gafish@qqqq.com>
Date:   Wed Jan 11 09:44:13 2017 +0800
    Update README.md
commit ba6e3d21fcb1c87a718d2a73cdd11261eb672b2a
Author: gafish <gafish@qqqq.com>
Date:   Wed Jan 11 09:31:33 2017 +0800
    test
.....

提交记录可能会非常多,按 J 键往下翻,按 K 键往上翻,按 Q 键退出查看

git tag

“为项目标记里程碑

git tag publish/0.0.1
git push origin publish/0.0.1

当我们完成某个功能需求准备发布上线时,应该将此次完整的项目代码做个标记,并将这个标记好的版本发布到线上,这里我们以 publish/0.0.1 为标记名并发布,当看到命令行返回如下内容则表示发布成功了

Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/gafish/gafish.github.com.git
 * [new tag]         publish/0.0.1 -> publish/0.0.1

.gitignore

“设置哪些内容不需要推送到服务器,这是一个配置文件

touch .gitignore

.gitignore 不是 Git 命令,而在项目中的一个文件,通过设置 .gitignore 的内容告诉 Git 哪些文件应该被忽略不需要推送到服务器,通过以上命令可以创建一个 .gitignore 文件,并在编辑器中打开文件,每一行代表一个要忽略的文件或目录,如:

demo.html
build/

以上内容的意思是 Git 将忽略 demo.html 文件 和 build/ 目录,这些内容不会被推送到服务器上

小结

通过掌握以上这些基本命令就可以在项目中开始用起来了,如果追求实用,那关于 Git 的学习就可以到此结束了,偶尔遇到的问题也基本上通过 Google 也能找到答案,如果想深入探索 Git 的高阶功能,那就继续往下看 深入探索 部分。

深入探索

基本概念

工作区(Working Directory

就是你在电脑里能看到的目录,比如上文中的 gafish.github.com 文件夹就是一个工作区

本地版本库(Local Repository

工作区有一个隐藏目录 .git,这个不算工作区,而是 Git 的版本库。

暂存区(stage

本地版本库里存了很多东西,其中最重要的就是称为 stage(或者叫index)的暂存区,还有 Git 为我们自动创建的第一个分支 master,以及指向 master 的一个指针叫 HEAD

远程版本库(Remote Repository

一般指的是 Git 服务器上所对应的仓库,本文的示例所在的github仓库就是一个远程版本库

以上概念之间的关系

工作区暂存区本地版本库远程版本库之间几个常用的 Git 操作流程如下图所示:

分支(Branch

分支是为了将修改记录的整个流程分开存储,让分开的分支不受其它分支的影响,所以在同一个数据库里可以同时进行多个不同的修改

主分支(Master

前面提到过 master 是 Git 为我们自动创建的第一个分支,也叫主分支,其它分支开发完成后都要合并到 master

标签(Tag

标签是用于标记特定的点或提交的历史,通常会用来标记发布版本的名称或版本号(如:publish/0.0.1),虽然标签看起来有点像分支,但打上标签的提交是固定的,不能随意的改动,参见上图中的1.0 / 2.0 / 3.0

HEAD

HEAD 指向的就是当前分支的最新提交

“以上概念了解的差不多,那就可以继续往下看,下面将以具体的操作类型来讲解 Git 的高阶用法

操作文件

git add

“添加文件到暂存区

git add -i

通过此命令将打开交互式子命令系统,你将看到如下子命令

***Commands***
  1: status      2: update      3: revert      4: add untracked
  5: patch      6: diff      7: quit      8: help

通过输入序列号或首字母可以选择相应的功能,具体的功能解释如下:

  • status:功能上和 git add -i 相似,没什么鸟用
  • update:详见下方 git add -u
  • revert:把已经添加到暂存区的文件从暂存区剔除,其操作方式和 update类似
  • add untracked:可以把新增的文件添加到暂存区,其操作方式和 update 类似
  • patch:详见下方 git add -p
  • diff:比较暂存区文件和本地版本库的差异,其操作方式和 update 类似
  • quit:退出 git add -i 命令系统
  • help:查看帮助信息
git add -p

直接进入交互命令中最有用的 patch 模式

这是交互命令中最有用的模式,其操作方式和 update 类似,选择后 Git 会显示这些文件的当前内容与本地版本库中的差异,然后您可以自己决定是否添加这些修改到暂存区,在命令行 Stage deletion [y,n,q,a,d,/,?]? 后输入 y,n,q,a,d,/,? 其中一项选择操作方式,具体功能解释如下:

  • y:接受修改
  • n:忽略修改
  • q:退出当前命令
  • a:添加修改
  • d:放弃修改
  • /:通过正则表达式匹配修改内容
  • ?:查看帮助信息
git add -u

直接进入交互命令中的 update 模式

它会先列出工作区 修改 或 删除 的文件列表,新增 的文件不会被显示,在命令行 Update>> 后输入相应的列表序列号表示选中该项,回车继续选择,如果已选好,直接回车回到命令主界面

git add --ignore-removal .

添加工作区 修改 或 新增 的文件列表, 删除 的文件不会被添加

git commit

“把暂存区的文件提交到本地版本库

git commit -m '第一行提交原因'  -m '第二行提交原因'

不打开编辑器,直接在命令行中输入多行提交原因

git commit -am '提交原因'

将工作区 修改 或 删除 的文件提交到本地版本库, 新增 的文件不会被提交

git commit --amend -m '提交原因'

修改最新一条提交记录的提交原因

git commit -C HEAD

将当前文件改动提交到 HEAD 或当前分支的历史ID

git mv

“移动或重命名文件、目录

git mv a.md b.md -f

将 a.md 重命名为 b.md ,同时添加变动到暂存区,加 -f 参数可以强制重命名,相比用 mv a.md b.md 命令省去了 git add 操作

git rm

“从工作区和暂存区移除文件

git rm b.md

从工作区和暂存区移除文件 b.md ,同时添加变动到暂存区,相比用 rm b.md 命令省去了 git add 操作

git rm src/ -r

允许从工作区和暂存区移除目录

git status

git status -s

以简短方式查看工作区和暂存区文件状态,示例如下:

M demo.html
?? test.html

git status --ignored

查看工作区和暂存区文件状态,包括被忽略的文件

操作分支

git branch

“查看、创建、删除分支

git branch -a

查看本地版本库和远程版本库上的分支列表

git branch -r

查看远程版本库上的分支列表,加上 -d 参数可以删除远程版本库上的分支

git branch -D

分支未提交到本地版本库前强制删除分支

git branch -vv

查看带有最后提交id、最近提交原因等信息的本地版本库分支列表

git merge

“将其它分支合并到当前分支

git merge --squash

将待合并分支上的 commit 合并成一个新的 commit 放入当前分支,适用于待合并分支的提交记录不需要保留的情况

git merge --no-ff

默认情况下,Git 执行”快进式合并“(fast-farward merge),会直接将 Master分支指向 Develop 分支,使用 --no-ff 参数后,会执行正常合并,在 Master分支上生成一个新节点,保证版本演进更清晰。

git merge --no-edit

在没有冲突的情况下合并,不想手动编辑提交原因,而是用 Git 自动生成的类似 Merge branch 'test' 的文字直接提交

git checkout

“切换分支

git checkout -b daily/0.0.1

创建 daily/0.0.1 分支,同时切换到这个新创建的分支

git checkout HEAD demo.html

从本地版本库的 HEAD(也可以是提交ID、分支名、Tag名) 历史中检出 demo.html 覆盖当前工作区的文件,如果省略 HEAD 则是从暂存区检出

git checkout --orphan new_branch

这个命令会创建一个全新的,完全没有历史记录的新分支,但当前源分支上所有的最新文件都还在,真是强迫症患者的福音,但这个新分支必须做一次 git commit操作后才会真正成为一个新分支。

git checkout -p other_branch

这个命令主要用来比较两个分支间的差异内容,并提供交互式的界面来选择进一步的操作,这个命令不仅可以比较两个分支间的差异,还可以比较单个文件的差异。

git stash

“在 Git 的栈中保存当前修改或删除的工作进度,当你在一个分支里做某项功能开发时,接到通知把昨天已经测试完没问题的代码发布到线上,但这时你已经在这个分支里加入了其它未提交的代码,这个时候就可以把这些未提交的代码存到栈里。

git stash

将未提交的文件保存到Git栈中

git stash list

查看栈中保存的列表

git stash show stash@{0}

显示栈中其中一条记录

git stash drop stash@{0}

移除栈中其中一条记录

git stash pop

从Git栈中检出最新保存的一条记录,并将它从栈中移除

git stash apply stash@{0}

从Git栈中检出其中一条记录,但不从栈中移除

git stash branch new_banch

把当前栈中最近一次记录检出并创建一个新分支

git stash clear

清空栈里的所有记录

git stash create

为当前修改或删除的文件创建一个自定义的栈并返回一个ID,此时并未真正存储到栈里

git stash store xxxxxx

将 create 方法里返回的ID放到 store 后面,此时在栈里真正创建了一个记录,但当前修改或删除的文件并未从工作区移除

$ git stash create
09eb9a97ad632d0825be1ece361936d1d0bdb5c7
$ git stash store 09eb9a97ad632d0825be1ece361936d1d0bdb5c7
$ git stash list
stash@{0}: Created via "git stash store".

操作历史

git log

“显示提交历史记录

git log -p

显示带提交差异对比的历史记录

git log demo.html

显示 demo.html 文件的历史记录

git log --since="2 weeks ago"

显示2周前开始到现在的历史记录,其它时间可以类推

git log --before="2 weeks ago"

显示截止到2周前的历史记录,其它时间可以类推

git log -10

显示最近10条历史记录

git log f5f630a..HEAD

显示从提交ID f5f630a 到 HEAD 之间的记录,HEAD 可以为空或其它提交ID

git log --pretty=oneline

在一行中输出简短的历史记录

git log --pretty=format:"%h"

格式化输出历史记录

Git 用各种 placeholder 来决定各种显示内容,我挑几个常用的显示如下:

  • %H: commit hash
  • %h: 缩短的commit hash
  • %T: tree hash
  • %t: 缩短的 tree hash
  • %P: parent hashes
  • %p: 缩短的 parent hashes
  • %an: 作者名字
  • %aN: mailmap的作者名
  • %ae: 作者邮箱
  • %ad: 日期 (–date= 制定的格式)
  • %ar: 日期, 相对格式(1 day ago)
  • %cn: 提交者名字
  • %ce: 提交者 email
  • %cd: 提交日期 (–date= 制定的格式)
  • %cr: 提交日期, 相对格式(1 day ago)
  • %d: ref名称
  • %s: commit信息标题
  • %b: commit信息内容
  • %n: 换行

git cherry-pick

“合并分支的一条或几条提交记录到当前分支末梢

git cherry-pick 170a305

合并提交ID 170a305 到当前分支末梢

git reset

“将当前的分支重设(reset)到指定的 <commit> 或者 HEAD

git reset --mixed <commit>

--mixed 是不带参数时的默认参数,它退回到某个版本,保留文件内容,回退提交历史

git reset --soft <commit>

暂存区和工作区中的内容不作任何改变,仅仅把 HEAD 指向 <commit>

git reset --hard <commit>

自从 <commit> 以来在工作区中的任何改变都被丢弃,并把 HEAD 指向 <commit>

git rebase

“重新定义分支的版本库状态

git rebase branch_name

合并分支,这跟 merge 很像,但还是有本质区别,看下图:

合并过程中可能需要先解决冲突,然后执行 git rebase --continue

git rebase -i HEAD~~

打开文本编辑器,将看到从 HEAD 到 HEAD~~ 的提交如下

pick 9a54fd4 添加commit的说明
pick 0d4a808 添加pull的说明
# Rebase 326fc9f..0d4a808 onto d286baa
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#

将第一行的 pick 改成 Commands 中所列出来的命令,然后保存并退出,所对应的修改将会生效。如果移动提交记录的顺序,将改变历史记录中的排序。

git revert

“撤销某次操作,此次操作之前和之后的 commit 和 history 都会保留,并且把这次撤销作为一次最新的提交

git revert HEAD

撤销前一次提交操作

git revert HEAD --no-edit

撤销前一次提交操作,并以默认的 Revert "xxx" 为提交原因

git revert -n HEAD

需要撤销多次操作的时候加 -n 参数,这样不会每次撤销操作都提交,而是等所有撤销都完成后一起提交

git diff

“查看工作区、暂存区、本地版本库之间的文件差异,用一张图来解释

git diff --stat

通过 --stat 参数可以查看变更统计数据

 test.md | 1 -
 1 file changed, 1 deletion(-)

git reflog

reflog 可以查看所有分支的所有操作记录(包括commit和reset的操作、已经被删除的commit记录,跟 git log 的区别在于它不能查看已经删除了的commit记录

远程版本库连接

如果在GitHub项目初始化之前,文件已经存在于本地目录中,那可以在本地初始化本地版本库,再将本地版本库跟远程版本库连接起来

git init

“在本地目录内部会生成.git文件夹

git remote

git remote -v

不带参数,列出已经存在的远程分支,加上 -v 列出详细信息,在每一个名字后面列出其远程url

git remote add origin https://github.com/gafish/gafish.github.com.git

添加一个新的远程仓库,指定一个名字,以便引用后面带的URL

git fetch

“将远程版本库的更新取回到本地版本库

git fetch origin daily/0.0.1

默认情况下,git fetch 取回所有分支的更新。如果只想取回特定分支的更新,可以指定分支名。

问题排查

git blame

“查看文件每行代码块的历史信息

git blame -L 1,10 demo.html

截取 demo.html 文件1-10行历史信息

git bisect

“二分查找历史记录,排查BUG

git bisect start

开始二分查找

git bisect bad

标记当前二分提交ID为有问题的点

git bisect good

标记当前二分提交ID为没问题的点

git bisect reset

查到有问题的提交ID后回到原分支

更多操作

git submodule

“通过 Git 子模块可以跟踪外部版本库,它允许在某一版本库中再存储另一版本库,并且能够保持2个版本库完全独立

git submodule add https://github.com/gafish/demo.git demo

将 demo 仓库添加为子模块

git submodule update demo

更新子模块 demo

git gc

“运行Git的垃圾回收功能,清理冗余的历史快照

git archive

“将加了tag的某个版本打包提取

git archive -v --format=zip v0.1 > v0.1.zip

--format 表示打包的格式,如 zip-v 表示对应的tag名,后面跟的是tag名,如 v0.1

原文地址:https://zhuanlan.zhihu.com/p/387678226

Spring Boot 入门教程 | 图文讲解

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

❝ 要说近两年比较火的企业级开发框架是什么,那非 SpringBoot 莫属,这是一个非常优秀的开源框架,可能这里有的小伙伴就会有疑问了,现在那么多优秀的开源框架,为什么 SpringBoot 一出现就非常火呢?其实它那么受欢迎是有原因的,SpringBoot 是站在巨人的肩膀上起来的,那么这个巨人是谁呢,它就是 Spring 这个非常优秀的开源框架。看到这里大家都应该明白了吧,有这么一个优秀的框架做肩膀,SpringBoot 岂能不优秀呢?

那么在正式介绍 SpringBoo t框架之前,我们再来简单说一下 Spring 这个优秀的开源框架,具体介绍请阅读 Spring 这篇文章。


一、Spring框架

Spring框架是一个开放源代码的J2EE应用程序框架,由Rod Johnson发起,是针对bean的生命周期进行管理的轻量级容器。Spring解决了开发者在J2EE开发中遇到的许多常见的问题,提供了功能强大IOC、AOP及Web MVC等功能。Spring可以单独应用于构筑应用程序,也可以和Struts、Webwork、Tapestry等众多Web框架组合使用,并且可以与 Swing等桌面应用程序AP组合。因此, Spring不仅仅能应用于JEE应用程序之中,也可以应用于桌面应用程序以及小应用程序之中。

特点:

1.方便解耦,简化开发

通过Spring提供的IoC容器,我们可以将对象之间的依赖关系交由Spring进行控制,避免硬编码所造成的过度程序耦合。有了Spring,用户不必再为单实例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。

2.AOP编程的支持

通过Spring提供的AOP功能,方便进行面向切面的编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应付。

3.声明式事务的支持

在Spring中,我们可以从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。

4.方便程序的测试

可以用非容器依赖的编程方式进行几乎所有的测试工作,在Spring里,测试不再是昂贵的操作,而是随手可做的事情。例如:Spring对Junit4支持,可以通过注解方便的测试Spring程序。

5.方便集成各种优秀框架

Spring不排斥各种优秀的开源框架,相反,Spring可以降低各种框架的使用难度,Spring提供了对各种优秀框架(如Struts、Mybatis、Hessian、Quartz)等的直接支持。

6.降低Java EE API的使用难度

Spring对很多难用的Java EE API(如JDBC,JavaMail,远程调用等)提供了一个薄薄的封装层,通过Spring的简易封装,这些Java EE API的使用难度大为降低。

7.Java 源码是经典学习范例

Spring的源码设计精妙、结构清晰、匠心独运,处处体现着大师对Java设计模式灵活运用以及对Java技术的高深造诣。Spring框架源码无疑是Java技术的最佳实践范例。如果想在短时间内迅速提高自己的Java技术水平和应用开发水平,学习和研究Spring源码将会使你收到意想不到的效果。


二、SpringBoot框架

SpringBoot是基于 Spring 开发的一种轻量级的全新框架,不仅继承了 Spring 框架原有的优秀特性,而且还通过简化配置来进一步简化了 Spring 应用的整个搭建和开发过程。通过 Spring Boot,可以轻松地创建独立的,基于生产级别的基于 Spring 的应用程序。SpringBoot 也常被成为微框架。

特点:

1.可以创建独立的 Spring 应用程序,并且基于其 Maven 或 Gradle 插件,可以创建可执行的 JARs 和 WARs。

2.内嵌 Tomcat 或 Jetty 等 Servlet 容器。

3.提供自动配置的“starter”项目对象模型(POMS)以简化 Maven 配置。

4.尽可能自动配置 Spring 容器。

5.提供准备好的特性,如指标、健康检查和外部化配置。

6.绝对没有代码生成,不需要 XML 配置。

「最主要的还是减少了大量的XML配置,总的来说就是一句话,用SpringBoot框架开发项目,可以轻松地创建独立的,基于生产级别的基于Spring的应用程序」


三、SpringBoot环境搭建

相信大家都对 SpringBoot 有了个基本的认识了,前面一直在说,SpringBoot 多么多么优秀,但是你没有实际的搭建一个 SpringBoot 环境,你很难去体会 SpringBoot 的那么简洁快速开发,下面我就来为大家简单搭建一个 SpringBoot 的开发环境,让大家体会一下 SpringBoot 有多么的高效。

SpringBoot小案例目录结构

第一步,新建maven工程(以maven的形式新建SpringBoot项目),选择骨架,点击webapps,单击next,根据需要一路点下去。

第二步,引入依赖,在 pom.xml 文件中引入 SpringBoot 父项目依赖,以及 web 依赖。

<!--继承springboot的父项目-->
<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.2.4.RELEASE</version>
</parent>
<!--引入web支持-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

第三步,在 src/main/resources 目录下新建application.yml配置文件(只能在该目录下),指定 SpringBoot 项目名(可选,也可不指定)。

server:
  servlet:
    context-path: /springboot

第四步,开发 SpringBoot 的全局入口类,此类位于所有子包之上。

package cn.ppdxzz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Description: SpringBoot入口类
 * @Author: PeiChen JavaAnything
 */
@SpringBootApplication
@RestController
public class Application {
    public static void main(String[] args) {
        //启动SpringBoot应用,参数一:入口类类对象,参数二:main函数参数
        SpringApplication.run(Application.class,args);
    }

    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "SpringBoot") String name) {
        return String.format("Hello %s!", name);
    }
}

第五步,启动 SpringBoot 项目,浏览器输入http://localhost:8080/springboot/hello,就会看到输出的信息==hello SpringBoot==。

最后,给大家一个好玩的东西,那就是 SpringBoot 支持自定义 banner,SpringBoot 的 banner,这个已经被程序员玩坏了,哈哈哈。下面是我的 banner,直接网上找,有好多,也支持在线生成,使用也非常简单,在 src/main/resources 下新建一个 banner.txt,把内容拷贝进去就行了。



至此,一个简单的 SpringBoot 应用环境就搭建完成了,可能会有的人说,这不是还得书写配置文件吗,在这里我想说的是,这些都是 SpringBoot 最基本的配置文件。如果你还不认为 SpringBoot 对得起快速开发框架这个名字,你可以看下我的另一篇文章:SSM配置文件,当你看完那篇之后,你就会发现,哇,SpringBoot 的配置文件确实非常少。

❝ 其实,你之所以会那么省事,不用书写以前那么多的配置文件,这主要得力于 SpringBoot 已经在底层帮你自动配置过了,你只需要拿过来用就可以了。这篇 SpringBoot 快速入门就到这吧,下次没事的时候再跟大家讨论一下SpringBoot 在底层究竟干了些什么。

转载自:https://zhuanlan.zhihu.com/p/164759480

滚动至顶部