9000字,唠唠架构中的设计模式

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

1 设计模式概述

​ 软件设计模式(Software Design Pattern),俗称设计模式,设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。使用设计模式的目的是为了代码重用、让代码更容易被他人理解、保证代码可靠性。

设计模式:

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中一些不断重复发生的问题,以及该问题的解决方案。
复制代码

设计模式使用场景:

1、在程序设计上会使用到设计模式(宏观)
2、在软件架构设计上会使用到设计模式(程序中的体现)
复制代码

设计模式的目的:

1、提高代码的可重用性
2、提高代码的可读性
3、保障代码的可靠性
复制代码

GOF

​ 《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Gang of Four)",而这本书也就被称为"四人组(或 GOF)"书。

​ 在《设计模式》这本书的最大部分是一个目录,该目录列举并描述了 23 种设计模式。

file

GOF中共提到了23种设计模式不是孤立存在的,很多模式之间存在一定的关联关系,在大的系统开发中常常同时使用多种设计模式。这23种设计模式根据功能作用来划分,可以划分为3类:

(1)创建型模式: 用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”,单例、原型、工厂方法、抽象工厂、建造者5种设计模式属于创建型模式。

(2)结构型模式: 用于描述如何将类或对象按某种布局组成更大的结构,代理、适配器、桥接、装饰、外观、享元、组合7种设计模式属于结构型模式。

(3)行为型模式: 用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器11种设计模式属于行为型模式。

GOF的23种设计模式:

1、单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
2、原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
3、工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
4、抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
5、建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

6、代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
7、适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
8、桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
9、装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
10、外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
11、享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
12、组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。

13、模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
14、策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
15、命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
16、职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
17、状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
18、观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
19、中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
20、迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
21、访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
22、备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
23、解释器(Interpreter)模式:提供如何定义语言的放法,以及对语言句子的解释方法,即解释器。
复制代码

2 单例模式

​ 单例模式(Singleton Pattern)是 Java 中最常见的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。该类还提供了一种访问它唯一对象的方式,其他类可以直接访问该方法获取该对象实例,而不需要实例化该类的对象。

单例模式特点:

1、单例类只能有一个实例。                  A a = new A()
2、单例类必须自己创建自己的唯一实例。        
3、单例类必须给所有其他对象提供这一实例。
复制代码

单例模式优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
2、避免对资源的多重占用(比如写文件操作)。
复制代码

单例模式真实应用场景:

1、网站的计数器
2、应用程序的日志应用
3、数据库连接池设计
4、多线程的线程池设计
复制代码

2.1 单例模式-饿汉式

创建一个单例对象SingleModel,SingleModel 类有它的私有构造函数和本身的一个静态实例。

SingleModel类提供了一个静态方法,供外界获取它的静态实例。DesignTest我们的演示类使用SingleModel类来获取 SingleModel 对象。

file 创建SingleModel:

public class SingleModel {

    //创建 SingleModel 的一个对象
    private static SingleModel instance = new SingleModel();

    //让构造函数为 private,这样该类就不会被实例化
    private SingleModel(){}

    //获取唯一可用的对象
    public static SingleModel getInstance(){
        return instance;
    }

    public void useMessage(){
        System.out.println("Single Model!");
    }
}
复制代码

单例测试:

public class DemoTest {

    /****
     * 单例模式测试
     */
    @Test
    public void testSingleModel(){
        //不合法的构造函数
        //编译时错误:构造函数 SingleModel() 是不可见的
        //SingleModel singleModel = new SingleModel();

        //获取唯一可用的对象
        SingleModel singleModel1 = SingleModel.getInstance();
        SingleModel singleModel2 = SingleModel.getInstance();

        //显示消息
        singleModel1.useMessage();

        //创建的2个对象是同一个对象
        System.out.println(singleModel1 == singleModel2);
    }
}
复制代码

输入结果如下:

Single Model!
true
复制代码

我们测试创建10万个对象,用单例模式创建,仅占内存:104字节,而如果用传统方式创建10万个对象,占内存大小为2826904字节。

2.2 多种单例模式讲解

​ 单例模式有多种创建方式,刚才创建方式没有特别的问题,但是程序启动就需要创建对象,不管你用不用到对象,都会创建对象,都会消耗一定内存。因此在单例的创建上出现了多种方式。

懒汉式:

懒汉式有这些特点:

1、延迟加载创建,也就是用到对象的时候,才会创建
2、线程安全问题需要手动处理(不添加同步方法,线程不安全,添加了同步方法,效率低)
3、实现容易
复制代码

案例如下:SingleModel1

file

如果在创建对象实例的方法上添加同步synchronized,但是这种方案效率低,代码如下:

file

双重校验锁:SingleModel2

​ 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

public class SingleModel2 {

    //不实例化
    private static SingleModel2 instance;

    //让构造函数为 private,这样该类就不会被实例化
    private SingleModel2(){}

    //获取唯一可用的对象
    public static SingleModel2 getInstance(){
        //instance为空的时候才创建对象
        if(instance==null){
            //同步锁,效率比懒汉式高
            synchronized (SingleModel2.class){
                //这里需要判断第2次为空
                if(instance==null){
                    instance = new SingleModel2();
                }
            }
        }
        return instance;
    }

    public void useMessage(){
        System.out.println("Single Model!");
    }
}
复制代码

指令重排问题解决

对象创建,一般正确流程如下:

1:申请内存空间
2:创建对象
3:将创建的对象指向申请的内存空间地址
复制代码

但其实在对象创建的时候,也有可能发生 指令重排问题,也就是上面流程会被打乱:

1:申请内存空间
2:将创建的对象指向申请的内存空间地址
3:创建对象
复制代码

如果是这样的话,双检锁在多线程情况下也会出现问题,需要添加volatile属性,该属性能防止指令重排,代码如下:

public class SingleModel2 {

    //不实例化
    private static volatile SingleModel2 instance;

    //让构造函数为 private,这样该类就不会被实例化
    private SingleModel2(){}

    //获取唯一可用的对象
    public static SingleModel2 getInstance(){
        //instance为空的时候才创建对象
        if(instance==null){
            //同步锁,效率比懒汉式高
            synchronized (SingleModel2.class){
                //这里需要判断第2次为空
                if(instance==null){
                    instance = new SingleModel2();
                }
            }
        }
        return instance;
    }

    public void useMessage(){
        System.out.println("Single Model!");
    }
}
复制代码

3 SpringAOP代理模式

​ Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架,非常受企业欢迎,他解决了业务逻辑层和其他各层的松耦合问题,它将面向接口的编程思想贯穿整个系统应用。在Spring源码中拥有多个优秀的设计模式使用场景,有非常高的学习价值。

3.1 代理模式

定义:

给某对象提供一个代理对象,通过代理对象可以访问该对象的功能。主要解决通过代理去访问[不能直接访问的对象],例如租房中介,你可以直接通过中介去了解房东的房源信息,此时中介就可以称为代理。
复制代码

优点:

1、职责清晰。 
2、高扩展性。 
3、智能化。
复制代码

缺点:

 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 
 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
复制代码

代理实现方式:(代理实现技术方案)

基于接口的动态代理
    提供者:JDK官方的Proxy类。
    要求:被代理类最少实现一个接口。
基于子类的动态代理
    提供者:第三方的CGLib,如果报asmxxxx异常,需要导入asm.jar。
    要求:被代理类不能用final修饰的类(最终类)。
复制代码

3.2 JDK动态代理

JDK动态代理要点:

1、被代理的类必须实现一个接口
2、用JDK代理,被代理的过程需要实现InvocationHandler
3、代理过程在invoke中实现
4、创建代理对象Proxy.newProxyInstance实现
复制代码

​ 我们以王五租房为例,王五通过中介直接租用户主房屋,中介在这里充当代理角色,户主充当被代理角色。

创建房东接口对象:LandlordService

public interface LandlordService {
    void rentingPay(String name);
}
复制代码

创建房东对象:Landlord

public class Landlord implements LandlordService{

    /****
     * @param name
     */
    @Override
    public void rentingPay(String name){
        System.out.println(name+" 来交租!");
    }
}
复制代码

创建代理处理过程对象:QFangProxy

public class QFangProxy implements InvocationHandler{

    private Object instance;

    public QFangProxy(Object instance) {
        this.instance = instance;
    }

    /****
     * 代理过程
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        args[0] = "中介QFang带领租户"+args[0];
        Object result = method.invoke(instance, args);
        return result;
    }
}
复制代码

创建代理,并通过代理调用房东方法:JdkProxyTest

public class JdkProxyTest {
    public static void main(String[] args) {
        //给QFang产生代理
        LandlordService landlordService = new Landlord();
        QFangProxy proxy = new QFangProxy(landlordService);
        LandlordService landlordServiceProxy = (LandlordService) Proxy.newProxyInstance(LandlordService.class.getClassLoader(), new Class[]{LandlordService.class}, proxy);

        //通过代理对象调用Landlord对象的方法
        landlordServiceProxy.rentingPay("王五");
    }
}
复制代码

运行结果如下:

中介QFang带领客户 来交租!
复制代码

3.3 CGLib动态代理

CGLib动态代理要点:

1、代理过程可以实现MethodInterceptor(Callback)接口中的invoke来实现
2、通过Enhancer来创建代理对象
复制代码

在上面的案例基础上,把QFangProxy换成SFangProxy,代码如下:

public class SFangProxy implements MethodInterceptor {

    private Object instance;

    public SFangProxy(Object instance) {
        this.instance = instance;
    }

    /***
     * 代理过程
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        args[0]="S房网带租户"+args[0];
        return method.invoke(instance,args);
    }
}
复制代码

创建测试类:CGLibProxyTest,代码如下

public class CGLibProxyTest {
    public static void main(String[] args) {
        //给QFang产生代理
        LandlordService landlordService = new Landlord();
        SFangProxy proxy = new SFangProxy(landlordService);
        LandlordService landlordServiceProxy = (LandlordService) Enhancer.create(LandlordService.class,proxy);

        //通过代理对象调用Landlord对象的方法
        landlordServiceProxy.rentingPay("王五");
    }
}
复制代码

3.4 Spring AOP-动态代理

​ 基于SpringAOP可以实现非常强大的功能,例如声明式事务、基于AOP的日志管理、基于AOP的权限管理等功能,利用AOP可以将重复的代码抽取,重复利用,节省开发时间,提升开发效率。Spring的AOP其实底层就是基于动态代理而来,并且支持JDK动态代理和CGLib动态代理,动态代理的集中体现在DefaultAopProxyFactory类中,我们来解析下DefaultAopProxyFactory类。

file ​ 如果我们在spring的配置文件中不配置<aop:config proxy-target-class="true">,此时默认使用的将是JDK动态代理,如果配置了,则会使用CGLib动态代理。

​ JDK动态代理的创建JdkDynamicAopProxy如下:

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

	//创建代理对象
	@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
		}
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

	@Override
	@Nullable
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		//JDK动态代理过程
		}
	}
}
复制代码

​ CGLib动态代理的创建ObjenesisCglibAopProxy如下:

class ObjenesisCglibAopProxy extends CglibAopProxy {

	//CGLib动态代理创建过程
	@Override
	@SuppressWarnings("unchecked")
	protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
		Class<?> proxyClass = enhancer.createClass();
		Object proxyInstance = null;

		if (objenesis.isWorthTrying()) {
			try {
				proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
			}
			catch (Throwable ex) {
				logger.debug("Unable to instantiate proxy using Objenesis, " +
						"falling back to regular proxy construction", ex);
			}
		}

		if (proxyInstance == null) {
			// Regular instantiation via default constructor...
			try {
				Constructor<?> ctor = (this.constructorArgs != null ?
						proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
						proxyClass.getDeclaredConstructor());
				ReflectionUtils.makeAccessible(ctor);
				proxyInstance = (this.constructorArgs != null ?
						ctor.newInstance(this.constructorArgs) : ctor.newInstance());
			}
			catch (Throwable ex) {
				throw new AopConfigException("Unable to instantiate proxy using Objenesis, " +
						"and regular proxy instantiation via default constructor fails as well", ex);
			}
		}

		((Factory) proxyInstance).setCallbacks(callbacks);
		return proxyInstance;
	}
}
复制代码

3.5 代理模式-文件服务实战

设计模式如果只是去学习他的模式,而不投入实际应用,其实无异于闭门造猪,因此我们要将设计模式投入实际开发使用才是对设计模式真正的领悟。

案例:根据文件类型,将文件存储到不同服务

代理模式:

给一个对象创建一个代理对象,通过代理对象可以使用该对象的功能。
复制代码

CGLib和JDK是代理模式实现的技术方案。

3.5.1 文件服务应用

​ 代理模式的应用场景除了代码级别,还可以将代理模式迁移到应用以及架构级别,如下图文件上传代理服务,针对一些图片小文件,我们可以直接把文件存储到FastDFS服务,针对大文件,例如商品视频介绍,我们可以把它存储到第三方OSS

​ 用户通过文件上传代理服务可以间接访问OSS和本地FastDFS,这种分布式海量文件管理解决方案,这里不仅在代码层面充分运用了代理模式,在架构层面也充分运用了代理模式。 file

3.5.2 分布式文件代理服务器实现

1)实现分析

​ 基于代理模式,我们实现文件上传分别路由到aliyunOSSFastDFS,用例图如下:

file

讲解:

1、FileUpload抽象接口,定义了文件上传方法,分别给它写了2种实现。
2、AliyunOSSFileUpload是将文件上传到aliyunOSS,主要上传mp4和avi的视频大文件。
3、FastdfsFileUpoad是将文件上传到FastDFS,主要上传png/jpg等图片小文件。
4、FileUploadProxy是代理对象,供用户访问,调用了FileUpload的文件上传方法,为用户提供不同文件上传调用。
5、FileController是控制器,用于接收用户提交的文件,并调用代理FileUploadProxy实现文件上传。
复制代码

2)代码实现

bootstrap.yml配置:

server:
  port: 18081
logging:
  level:
    #root: debug开启dubug级别
    com.seckill.goods.dao: error
  pattern:
    console: "%msg%n"

#对应实例的id和需要处理的文件类型的映射关系
upload:
  filemap:
    aliyunOSSFileUpload: avi,mp4
    fastdfsFileUpoad: png,jpg

#FastDFS配置
fastdfs:
  url: http://192.168.211.137:28181/
#aliyun
aliyun:
  oss:
    endpoint: oss-cn-beijing.aliyuncs.com
    accessKey: a7i6rVEjbtaJdYX2
    accessKeySecret: MeSZPybPHfJtsYCRlEaUbfRtdH8gl4
    bucketName: sklll
    key: video/
    backurl: https://sklll.oss-cn-beijing.aliyuncs.com/video/ #访问地址配置

spring:
  application:
    name: seckill-goods
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  servlet:
    multipart:
      max-file-size: 100MB #上传文件大小配置
复制代码

FileUpload接口定义:

public interface FileUpload {

    /***
     * 文件上传
     * @param buffers:文件字节数组
     * @param extName:后缀名
     * @return
     */
    String upload(byte[] buffers,String extName);
}
复制代码

AliyunOSSFileUpload实现:

@Component(value = "aliyunOSSFileUpload")
public class AliyunOSSFileUpload implements FileUpload{

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    @Value("${aliyun.oss.accessKey}")
    private String accessKey;
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;
    @Value("${aliyun.oss.key}")
    private String key;
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
    @Value("${aliyun.oss.backurl}")
    private String backurl;

    /****
     * 文件上传
     *  文件类型如果是图片,则上传到本地FastDFS
     *  文件类型如果是视频,则上传到aliyun OSS
     */
    @Override
    public String upload(byte[] buffers,String extName) {
        String realName = UUID.randomUUID().toString()+"."+extName;
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKey, accessKeySecret);
        // <yourObjectName>表示上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key+realName, new ByteArrayInputStream(buffers));
        // 上传字符串。
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(FileUtil.getContentType("."+extName));
        putObjectRequest.setMetadata(objectMetadata);
        ossClient.putObject(putObjectRequest);

        // 关闭OSSClient。
        ossClient.shutdown();
        return backurl+realName;
    }
}
复制代码

FastdfsFileUpoad实现:

@Component(value = "fastdfsFileUpoad")
public class FastdfsFileUpoad implements FileUpload {

    @Value("${fastdfs.url}")
    private String url;

    /***
     * 文件上传
     * @param buffers:文件字节数组
     * @param extName:后缀名
     * @return
     */
    @Override
    public String upload(byte[] buffers, String extName) {
        /***
         * 文件上传后的返回值
         * uploadResults[0]:文件上传所存储的组名,例如:group1
         * uploadResults[1]:文件存储路径,例如:M00/00/00/wKjThF0DBzaAP23MAAXz2mMp9oM26.jpeg
         */
        String[] uploadResults = null;
        try {
            //获取StorageClient对象
            StorageClient storageClient = getStorageClient();
            //执行文件上传
            uploadResults = storageClient.upload_file(buffers, extName, null);
            return url+uploadResults[0]+"/"+uploadResults[1];
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /***
     * 初始化tracker信息
     */
    static {
        try {
            //获取tracker的配置文件fdfs_client.conf的位置
            String filePath = new ClassPathResource("fdfs_client.conf").getPath();
            //加载tracker配置信息
            ClientGlobal.init(filePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /***
     * 获取StorageClient
     * @return
     * @throws Exception
     */
    public static StorageClient getStorageClient() throws Exception{
        //创建TrackerClient对象
        TrackerClient trackerClient = new TrackerClient();
        //通过TrackerClient获取TrackerServer对象
        TrackerServer trackerServer = trackerClient.getConnection();
        //通过TrackerServer创建StorageClient
        StorageClient storageClient = new StorageClient(trackerServer,null);
        return storageClient;
    }
}
复制代码

FileUploadProxy代理实现:

@Data
@Component
@ConfigurationProperties(prefix = "upload")
public class FileUploadProxy implements ApplicationContextAware{

    private ApplicationContext act;

    //aliyunOSSFileUpload     ->  mp4,avi
    private Map<String,List<String>> filemap;

    /***
     * 文件上传
     * @param file:上传的文件
     * @return
     */
    public String upload(MultipartFile file) throws Exception{
        //文件名字  1.mp4
        String fileName = file.getOriginalFilename();
        //扩展名  mp4,jpg
        String extName = StringUtils.getFilenameExtension(fileName);

        //循环filemap
        for (Map.Entry<String, List<String>> entry : filemap.entrySet()) {
            for (String suffix : entry.getValue()) {
                //匹配当前extName和当前map中对应的类型是否匹配
                if(extName.equalsIgnoreCase(suffix)){
                    //一旦匹配,则把key作为唯一值,从容器中获取对应实例
                    return act.getBean(entry.getKey(), FileUpload.class).upload(file.getBytes(),extName);
                }
            }
        }
        return null;
    }

    //注入容器对象
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.act=applicationContext;
    }
}
复制代码

FileController控制器实现:

@RestController
@RequestMapping(value = "/file")
public class FileController {

    @Autowired
    private FileUploadProxy fileUploadProxy;

    /***
     * 文件上传
     * @param file
     * @return
     * @throws IOException
     */
    @PostMapping(value = "/upload")
    public String upload(MultipartFile file) throws IOException {
        return fileUploadProxy.upload(file.getBytes(), StringUtils.getFilenameExtension(file.getOriginalFilename()));
    }
}
复制代码

文件上传预览效果:

<https://sklll.oss-cn-beijing.aliyuncs.com/video/77df7ada-4eea-4698-bfc5-bedd2c16f240.mp4>

file

FastDFS地址:

<http://192.168.211.137:28181/group1/M00/00/00/wKjTiV7kLtGASw5TAADJ9uXzZAQ622.png> file

4 享元模式

定义:

运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
复制代码

享元模式和单利的区别:

单利是对象只能自己创建自己,整个应用中只有1个对象
享元模式根据需要共享,不限制被谁创建(有可能有多个对象实例)
复制代码

优点:

特定环境下,相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
复制代码

缺点:

为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
复制代码

4.1 享元模式实战

案例:用户下单,会话共享

4.2 会话跟踪分析

​ 会话跟踪,如果是传统项目用Session或者是Cookie,全项目通用,但在微服务项目中,不用Session也不用Cookie,所以想要在微服务项目中实现会话跟踪,是有一定难度的。

​ 当前微服务项目中,身份识别的主流方法是前端将用户令牌存储到请求头中,每次请求将请求头中的令牌携带到后台,后台每次从请求头中获取令牌来识别用户身份。

​ 我们在项目操作过程中,很多地方都会用到用户身份信息,比如下订单的时候,要知道当前订单属于哪个用户,记录下单关键日志的时候,需要记录用户操作的信息以及用户信息,关键日志记录我们一般用AOP进行拦截操作,此时没法直接把用户身份信息传给AOP。这个时候我们可以利用享元模式实现用户会话信息共享操作。操作流程如下图:

file

4.3 会话共享案例实现

​ 基于上面的分析,我们采用享元模式实现用户会话共享操作,要解决如下几个问题:

1、用户会话共享
2、会话多线程安全
3、订单数据用户信息获取
4、AOP日志记录用户信息获取
复制代码

定义共享组件Session

Session里面定义了每个线程中不变的用户身份信息usernamerolesex,其他的是可能存在变化的数据可以写一个类继承该类。

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public abstract class Session {
    //需要共享的用户信息
    private String username;
    private String name;
    private String sex;
    private String role;
    private Integer level;

    //扩展方法
    public abstract void handler();
}
复制代码

享元组件逻辑操作对象SessionShar

SessionShar该对象主要用于给当前线程填充共享数据,以及变更访问方法和访问信息等信息的逻辑操作,代码如下:

public class SessionShar extends Session {
    //方便实例化
    public SessionShar(String username, String name, String sex, String role, Integer level) {
        super(username, name, sex, role, level);
    }

    /***
     * 扩展对象
     */
    @Override
    public void handler() {
        System.out.println("扩展功能!");
    }
}
复制代码

多线程安全控制ThreadSession

​ 每个线程请求的时候,我们需要保障会话安全,比如A线程访问和B线程访问,他们的用户会话身份不能因为并发原因而发生混乱。这里我们可以采用ThreadLocal来实现。我们创建一个ThreadSession对象,并在该对象中创建ThreadLocal<Session>用户存储每个线程的会话信息,并实现ThreadLocal<Session>的操作,代码如下:

@Component
public class ThreadSession {

    //存储需要共享的对象
    private static ThreadLocal<Session> sessions = new ThreadLocal<Session>();

    /****
     * 添加用户信息记录
     */
    public void add(Session session){
        sessions.set(session);
    }

    /****
     * 获取LogComponent
     */
    public Session get(){
        return sessions.get();
    }

    /****
     * 移除
     */
    public void remove(){
        sessions.remove();
    }
}
复制代码

线程会话初始化AuthorizationInterceptor

AuthorizationInterceptor拦截器的作用是用于初始化用户访问的时候用户的身份信息,并将身份信息存储到ThreadSessionThreadLocal中,在用户访问方法结束,销毁ThreadSessionThreadLocal中会话,代码如下:

@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

    @Autowired
    private ThreadSession threadSession;
    /****
     * 将用户会话存储到ThreadLocal中
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            //获取令牌
            String authorization = request.getHeader("token");
            //解析令牌
            if(!StringUtils.isEmpty(authorization)){
                Map<String, Object> tokenMap = JwtTokenUtil.parseToken(authorization);
                //封装用户身份信息,存储到ThreadLocal中,供当前线程共享使用
                //1.封装需要共享的信息
                //2.创建一个对象继承封装信息,每次共享该对象 (不需要共享,则可以创建另外一个对象继承它)
                //3.创建共享管理对象,实现共享信息的增加、获取、移除功能
                threadSession.add(new SessionShar(
                        tokenMap.get("username").toString(),
                        tokenMap.get("name").toString(),
                        tokenMap.get("sex").toString(),
                        tokenMap.get("role").toString(),
                        Integer.valueOf(tokenMap.get("level").toString())
                ));
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //输出令牌校验失败
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().print("身份校验失败!");
        response.getWriter().close();
        return false;
    }

    /**
     * 移除会话信息
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        threadSession.remove();
    }
}
复制代码

共享信息使用:

①AOP记录日志:创建AOP切面类LogAspect用于记录日志,代码如下:

@Component
@Aspect
@Slf4j
public class LogAspect {

    @Autowired
    private ThreadSession threadSession;

    /***
     * 记录日志
     */
    @SneakyThrows
    @Before("execution(int com.itheima.shop.service.impl.*.*(..))")
    public void logRecode(JoinPoint joinPoint){
        //获取方法名字和参数
        String methodName = joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName();
        //记录日志
        log.info("用户【"+threadSession.get().toString()+"】访问:"+methodName);
    }

    /****
     * 参数获取
     */
    public String args(Object[] args){
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i <args.length ; i++) {
            buffer.append("  args("+i+"):"+args[i].toString());
        }
        return buffer.toString();
    }
}
复制代码

②添加订单获取用户信息:在添加订单方法OrderServiceImpl.add(Order order)中,从ThreadSession中获取用户会话,并填充给Order,代码如下:

file

添加订单,日志输出可以看到调用添加订单和修改库存时,都记录了日志,并且获取了用户会话,效果如下:

LogComponent(username=zhaoliu, sex=男, role=ROLE_USER, methodName=com.itheima.shop.service.impl.OrderServiceImpl.add, message=  args(0):Order(itemId=1, id=1, money=9999, status=1, num=1, username=null))

LogComponent(username=zhaoliu, sex=男, role=ROLE_USER, methodName=com.itheima.shop.service.impl.ItemServiceImpl.modify, message=  args(0):1  args(1):1)
复制代码

添加的订单数据库数据中也拥有用户信息,效果如下:

file

5 装饰者模式

定义:

动态的向一个现有的对象添加新的功能,同时又不改变其结构。它属于结构型模式。
复制代码

扩展新功能,不需要修改现有对象就能实现--->装饰者模式

优点:

装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
复制代码

缺点:

多层装饰比较复杂。
复制代码

5.1 装饰者模式实战

案例:结算价格计算,根据不同价格嵌套运算

5.2 订单结算价格实战

​ 在订单提交的时候,订单价格和结算价格其实是两码事,订单价格是当前商品成交价格,而结算价格是用户最终需要支付的金额,最终支付的金额并不是一成不变,它也并不是商品成交价格,能改变结算价格的因素很多,比如满100减10元,VIP用户再减5块。订单结算金额计算我们就可以采用装饰者模式。

file

5.3 装饰者模式价格运算实现

实现思路分析

1、创建接口(MoneyOperation),定义订单价格计算,因为所有价格波动,都是基于订单价格来波动的。
2、创建订单价格计算类(OrderPayMoneyOperation),实现MoneyOperation接口,实现订单价格计算。
3、创建装饰者对象(Decorator),以供功能扩展。
4、实现优惠券优惠金额计算功能扩展,创建Decorator的扩展类CouponsMoneyOperation,先计算订单金额,再计算优惠券使用之后的优惠金额。
5、实现金币抵现功能扩展,创建Decorator的扩展类GoldMoneyOperation,先计算订单金额,再实现金币优惠之后的金额。
复制代码

基础接口:创建接口MoneySum,该接口只用于定义计算订单金额的方法。

public interface MoneySum {

    //订单金额求和计算
    void sum(Order order);
}
复制代码

订单金额计算类:创建类OrderPayMoneyOperation实现订单金额的计算。

@Component(value = "orderMoneySum")
public class OrderMoneySum implements MoneySum {

    @Autowired
    private ItemDao itemDao;

    //总金额计算
    @Override
    public void sum(Order order) {
        //商品单价*总数量
        Item item = itemDao.findById(order.getItemId());
        order.setPaymoney(item.getPrice()*order.getNum());
        order.setMoney(item.getPrice()*order.getNum());
    }
}
复制代码

装饰者类:创建装饰者类DecoratorMoneySum供其他类扩展。

public class DecoratorMoneySum implements MoneySum {

    private MoneySum moneySum;

    public void setMoneySum(MoneySum moneySum) {
        this.moneySum = moneySum;
    }

    //计算金额
    @Override
    public void sum(Order order) {
        moneySum.sum(order);
    }
}
复制代码

满100减10元价格计算:创建类FullMoneySum扩展装饰者类,实现满减价格计算。

@Component(value = "fullMoneySum")
public class FullMoneySum extends DecoratorMoneySum{

    //原来的功能上进行增强
    @Override
    public void sum(Order order) {
        //原有功能
        super.sum(order);
        //增强
        moneySum(order);
    }

    //满100减5块
    public void moneySum(Order order){
        Integer paymoney = order.getPaymoney();
        if(paymoney>=100){
            order.setPaymoney(paymoney-10);
        }
    }
}

复制代码

VIP优惠10元价格计算:创建类VipMoneySum,实现VIP优惠计算。

@Component(value = "vipMoneySum")
public class VipMoneySum extends DecoratorMoneySum {

    //原有方法上增强
    @Override
    public void sum(Order order) {
        //原有功能
        super.sum(order);

        //增强
        vipMoneySum(order);
    }

    //Vip价格优惠-5
    public void vipMoneySum(Order order){
        order.setPaymoney(order.getPaymoney()-5);
    }
}
复制代码

支付金额计算:修改OrderServiceImpladd()方法,添加订单金额以及订单支付金额的计算功能,代码如下:

file

测试效果

测试数据中,我们选择购买1件商品,当前登录用户为王五,拥有5个金币,当前购买的商品id=1,商品单价是150元,满减100,VIP优惠5元,最终支付135元。

{
    "itemId":"1",
    "id":"1",
    "status":1,
    "num":1,
    "couponsId":"1"
}
复制代码

测试生成的订单如下:

file 不仅如此,我们可以随时撤掉满减和Vip优惠功能。

6 策略模式

定义:

策略模式是对算法的包装,把算法的使用和算法本身分隔开,委派给不同的对象管理。策略模式通常把一系列的算法包装到一系列的策略类里面,作为一个抽象策略类的子类或者接口的实现类。
复制代码

​ 简单来说就是就定义一个策略接口,策略类去实现该接口去定义不同的策略。然后定义一个环境(Context,也就是需要用到策略的对象)类,以策略接口作为成员变量,根据环境来使用具体的策略。

优点:

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

缺点:

 1、策略类会增多。 
 2、所有策略类都需要对外暴露。
复制代码

6.1 策略模式实战

案例:结算价格计算,根据Vip不同等级进行运算

6.2 不同VIP优惠价格分析

file

​ 用户在购买商品的时候,很多时候会根据Vip等级打不同折扣,尤其是在线商城中体现的淋漓尽致。我们这里也基于真实电商案例来实现VIP等级价格制:

Vip0->普通价格
Vip1->减5元
Vip2->7折
Vip3->5折
复制代码

6.3 代码实现

定义策略接口Strategy

public interface Strategy {

    //价格计算
    Integer payMoney(Integer payMoney);
}
复制代码

定义Vip0策略StrategyVipOne

@Component(value = "strategyVipOne")
public class StrategyVipOne implements Strategy {

    //普通会员,没有优惠
    @Override
    public Integer payMoney(Integer payMoney) {
        return payMoney;
    }
}
复制代码

定义Vip1策略StrategyVipTwo

@Component(value = "strategyVipTwo")
public class StrategyVipTwo implements  Strategy{

    //策略2
    @Override
    public Integer payMoney(Integer payMoney) {
        return payMoney-5;
    }
}
复制代码

定义Vip2策略StrategyVipThree

@Component(value = "strategyVipThree")
public class StrategyVipThree implements  Strategy{

    //策略3
    @Override
    public Integer payMoney(Integer payMoney) {
        return (int)(payMoney*0.7);
    }
}
复制代码

定义Vip3策略StrategyVipFour

@Component(value = "strategyVipFour")
public class StrategyVipFour implements  Strategy{

    //策略4
    @Override
    public Integer payMoney(Integer payMoney) {
        return (int)(payMoney*0.5);
    }
}
复制代码

定义策略工厂StrategyFactory

@Data
@ConfigurationProperties(prefix = "strategy")
@Component
public class StrategyFactory implements ApplicationContextAware{

    //ApplicationContext
    //1、定义一个Map存储所有策略【strategyVipOne=instanceOne】
    //                          【strategyVipTwo=instanceTwo】
    private ApplicationContext act;

    //定义一个Map,存储等级和策略的关系,通过application.yml配置注入进来
    private Map<Integer,String> strategyMap;

    //3、根据会员等级获取策略【1】【2】【3】
    public Strategy getStrategy(Integer level){
        //根据等级获取策略ID
        String id = strategyMap.get(level);
        //根据ID获取对应实例
        return act.getBean(id,Strategy.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        act=applicationContext;
    }
}
复制代码

等级策略配置:修改application.yml,将如下策略配置进去

#策略配置
strategy:
  strategyMap:
    1: strategyVipOne
    2: strategyVipTwo
    3: strategyVipThree
    4: strategyVipFour
复制代码

等级控制:修改UserHandler添加等级属性

file

修改UserHandlerShare定义等级,代码如下:

file 装饰者模式中修改VipMoneySum的价格运算,代码如下:

file

测试:

file

往期干货:

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

来源:https://juejin.cn/post/7156126788484235278
posted @ 2022-12-20 22:14  程序员小明1024  阅读(65)  评论(0编辑  收藏  举报