JavaWeb三层架构中Service和Dao层对象单例化可行性

声明:以下个人观点,仅作参考; 

阅读正文的前提知识:

一. 单例模式:

单例概念(百度): 单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。

Java中的单例模式: 从项目开始到结束, 某一Java类仅产生一个实例对象; java中的实例一般通过new调用构造创建( 工厂模式除外 ), 为了达到单例的目的,  需要将类的构造方法私有化(private), 仅提供public方法获得实例, 并在该方法中完成单例逻辑;

完整的单例模式示例如下:

饿汉式(类加载时,即第一次用到该类时;):

//饿汉式:创建对象实例的时候直接初始化  空间换时间
public class SingletonOne {
	//1、创建类中私有构造
	private SingletonOne(){ }
	//2、创建该类型的私有静态实例
	private static SingletonOne instance=new SingletonOne();
	//3、创建公有静态方法返回静态实例对象
	public static SingletonOne getInstance(){
		return instance;
	}
}

懒汉式(使用对象时, 实际创建该类,第一次使用较慢, 此处为保证前程安全, 且基于实用主义仅采用双重校验, 其它实现线程安全的方法还有(静态内部类/枚举等))

//懒汉式:类内实例对象创建时并不直接初始化,直到第一次调用get方法时,才完成初始化操作
//时间换空间
public class SingletonTwo {
	//1、创建私有构造方法
	private SingletonTwo(){}
	//2、创建静态的该类实例对象
	private static SingletonTwo instance=null;
	//3、创建开放的静态方法提供实例对象
	public static SingletonTwo getInstance(){
		if (instance == null) {
			synchronized (SingletonTwo.class) {
				if (instance == null) {
					instance = new SingletonTwo();
				}
			}
		}
		return instance;
	}
}

懒汉式和饿汉式差别在于: 实际创建对象的时机不同, 基于目前Web项目中一般时间(性能)要求大于空间要求, 故: JavaWeb中个人推荐使用饿汉式, 其特点: 利用空间换时间, 项目加载完成后运行响应快, 且直接利用类加载的时机屏蔽了线程安全问题, 使得类的定义相对简单;

|→→→→→→→→→→→→时间线→→→→→→→→→→→→→→→→→→→→

|   饿汉式:  类加载   创建实例    工厂方法被调用                    返回实例给调用者

|   懒汉式:  类加载                     工厂方法被调用   创建实例   返回实例给调用者

|→→→→→→→→→→→→时间线→→→→→→→→→→→→→→→→→→→→

单例模式在应用层面的特点:

1. 单例的成员变量: 多线程操作时, 该成员变量唯一, 故可以作为全局变量的配置 (但是需要注意的是, 多线程修改时的同步问题, 可以使用同步锁方式避免) ;

2. 单例的方法: 多线程操作时, 可以共享方法, 但是个各个线程又有自己单独的方法局部属性变量, 故可以实现单个实例方法提供全局分别使用的目的, 从而不必每个线程自行实例化对象再调用, 即节约时间又节约空间;

 

二.Java类的加载时机:

1. 类加载器分类:

    i. 启动类加载器(Bootstrap ClassLoader):  加载Java核心库, 属于JVM一部分;

    ii. 扩展类加载器(Extendsion ClassLoader): 加载java扩展库;

    iii. 应用程序类加载器(Application ClassLoader): 加载用户自定义类;

    iiii.自定义类加载器: 本人不了解, 先不讲;

2. 因为讲的都是用户自定义类,所以细讲 iii 的应用程序类加载器:

    i. 一般情况下自定义类的加载时机:

        a.  new对象时;

        b. 调用类的静态资源(静态成员变量 / 静态方法)时; 

        c. 子类加载时;

        d. 反射操作时;

        e. 带主方法的类;

    ii. 在使用类前的适当时机, 预先主动进行类加载:

      类加载的本质是将类的字节码文件放入内存中, 当我们需要获取某一个类的字节码对象时, 自然就驱动虚拟机去加载该类:

        a. Class.forName(类的全限定名);

        b. this.getClass().getClassLoader().loadClass();

        另外:在类中定义一个无关的静态成员变量, 需要加载类时, 访问一次该变量即可 (个人感觉此方法优点耍滑头的意思0.0......) ;

-----------------------------------------------------前提知识到此结束-------------------------------------------------------------

 

正文部分:

三.JavaWeb三层架构中Service和Dao层对象单例化的必要性

JavaWeb的三层架构中, 我们需要预先启动服务器, 服务器启动完毕开始接受用户访问. 而用户访问时, 需要尽可能的快速响应.

其次JavaWeb中, Servlet默认以 单例多线程模式执行,即一个Servlet类仅实例化一个对象;

故:

 

首先  假设: Service和Dao层每次使用时均需要创建对象, 即: 每次客户端请求Servlet访问, Servlet中每个方法各自创建新的Sercice层对象, 该Sercice层对象的每个方法再各自创建新的Dao层对象. 因为每收到一个请求Servlet都会创建新的线程去处理, 故当前有多少个方法被调用, 就会相应创建多少个Service层和Dao层对象, 严重浪费CPU和内存资源, 同时降低服务器响应速度;

然后  我们针对单个Servlet线程进行优化: 使得每个Servlet的线程中仅产生一个Service层和一个Dao层对象:

要实现该目的, 我们只需保证每个线程中Web层和Service层的方法共用一个Service层和Dao层对象即可, 此时仅需要将每个方法的各自实例化Service和Dao层对象提取为成员对象, 即可实现;

然后  我们针对单个Servlet对象进一步优化, 使得单个Servlet方法执行期间仅产生一个Service层和一个Dao层对象:

回顾Java整体机制, 不难找出最简单有效的方法: 即将Service层和Dao层对象的定义变为static, 因为一个类的静态变量仅此一份, 即可实现;

最后  实际项目中不可能只存在一个Servlet, 即使有BaseServlet帮助我们分模块分功能合并Servlet, 但大型项目功能和模块数量也不可小觑, 故需要再进一步优化, 使得所有Servlet的所有线程在访问同一Service层和Dao层对象时, 使用同一个对象:

 

>补充话题背景知识部分:

或许, 此时部分读者心中有疑问, 这样的话, 还能正确地进行层间参数的传递吗?

    答案是肯定的, 原因就是Servlet的单例多线程模式, 而多线程调用同一方法时, 局部变量是每个线程一份的 (因为每个线程都有单独的内存空间) . 而层间参数传递完全可由局部变量+方法参数实现;

继续最后的优化之前简单介绍一下设计模式中的 "工厂模式":  

先讲个小案例, 说明工厂模式的应用场景:

       Java中万物皆对象思想, 相比大家都已经很熟悉, 生活中一盘菜是对象, 使用的手机也是一部对象.

        菜我们可以自己做, 因为其简单, 我们知道使用什么原料, 且更改原料也很简单. 但是手机我们却没办法自己制造, 故需要从网上购买, 电商售卖的手机最终由手机厂家来生产.

        而且一般我们并不关系手机的具体原材料, 仅仅关心其功能是否完善好用. 所以,手机完全可以使用不同厂家和规格的原料和不同品牌的组件, 甚至: 同一款手机,也可以使用不同的原料和配件来实现完全相同的功能.

        另外,生活中另一个问题, 我们并不是每天都自己做饭, 尤其是快节奏的今天, 此时食堂/饭馆/外卖的意义就出现了, 将大众需求进行统一处理, 降低时间和资源成本, 此处的食堂/饭馆/外卖也可以看做一种工厂.

Java中, 我们在调用某些类的方法时, 内部实现太复杂, 调用者初始化起来比较困难, 仅需要功能, 或者说并不关心类的内部具体怎么实现. 故需要类自己去处理, 并进行自我初始化, 并提供一个静态的共有方法getInstance()返回自身的实例对象, 这就是Java中"工厂模式"达到的的目的之一: 高内聚;

另外, 在Java中, 我们Service层并不想去关心Dao层具体由哪个类实现, 以及如何实现, 就如我们并不关心手机内部天线的生产厂家一样, 故此时我们只需要按手机型号选择即可, 具体实现交给厂家;     这里就引出了Java的面向接口编程, 我们实例化Service层和Dao层对象时, 使用该类对象的接口接收, 好比规定手机型号, 具体实例的创建交给工厂类去做(工厂类加载配置文件....不再分讲). 这里就是Java中"工厂模式"达到的目的之二: 低耦合, 此时工厂类就是解耦的中间人;

第三, 在Java中, 我们有许多工作在每个类中都要进行相同流程的处理, 此时我们就需要流程化工作的抽取, 一般我们定义为工具类, 但在需要时也可以作为 "工厂模式" 的附加功能;

补充话题背景知识部分结束<

此处继续进行扩展"多线程方法局部变量作用域"和"工厂模式"之前的优化工作:

我们想要 "使得所有Servlet的所有线程在访问同一Service层和Dao层对象时, 使用同一个对象" , 就需要用到单例模式. 即在Service层和Dao层所有类均采用单例模式, 这样就初步实现了本阶段优化的目的;

但是:

    i. 目前多各类存在高度相同的业务代码, 故需要进行抽取;

    ii. 考虑公司数据量陡增, 优化各层实现逻辑等,我们需要尽可能地进行层与层间解耦;

因此:

    仅考虑解耦,则只需要结合"面向接口编程"和"工厂模式"即可,除了在调用层采用接口接收之外, 工厂类定义如下:

import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.ResourceBundle;

public class BeanFactory2 {
    public static Object getinstance(String className){//工厂方法,返回给定类名的实例化对象
        ResourceBundle bundle = ResourceBundle.getBundle("beans");//创建ResourceBundle资源加载对象
        String classLongName = bundle.getString(className);
        try {
            return Class.forName(classLongName).getConstructor().newInstance();//实例化对象
        } catch (Exception e) {
            return null;//若报错返回null
        }
    }
}

同时考虑解耦和单例模式: 我们抽取成工具类, 而该工具类专门用来实例化单例并返回, 同时考虑上述的解耦思想, 此时把工具类的处理逻辑作为工厂类的附加功能即可, 具体实现如下:

import java.util.Enumeration;
import java.util.HashMap;
import java.util.ResourceBundle;

public class BeanFactory {
    private static HashMap<String, Object> objectMap;//定义Map集合,存放类名和实体类Key-Value关系
    //饿汉模式
    //类加载时实例化出所有单例对象的集合
    static{
        ResourceBundle bundle = ResourceBundle.getBundle("beans");//创建ResourceBundle资源加载对象
        Enumeration<String> keys = bundle.getKeys();//获取配置文件中的所有Key(类名)值枚举
        //创建类Map集合,类名作为键,类实例对象作为值
        objectMap = new HashMap<>();//实例化Map集合
        //遍历枚举对象, 获取所有key值,根据key值获取对象类的全限定名,并实例化对象存放到Map中
        while(keys.hasMoreElements()){
            String classNameStr = keys.nextElement();//获取单个key
            String classLongName = bundle.getString(classNameStr);//获取对应全限定名
            try {
                Object object = Class.forName(classLongName).getConstructor().newInstance();//实例化对象
                objectMap.put(classNameStr,object);//放入Map集合中
            } catch (Exception e) {
                objectMap.put(classNameStr,null);//若报错放入null
            }
        }
    }
    public static Object getinstance(String className){//工厂方法,返回给定类名的实例化对象
        return objectMap.get(className);
    }
}

请注意: 此时因为枚举是无序的, 在使用单例模式工厂时,除非将dao层类信息单独配置beans属性文件,在service层类对象之前全部加载完毕(或使用其它方法达到类似目的), 否则不要在service层对象中定义全局dao层对象, 此时应该每个方法分别使用getInstance方法获取单例对象的地址来使用;

总结:

    1. 以上单例模式工厂类, 在博主的测试项目中运行正常

    2. 为了更进一步的提高项目加载的完整度, 可以利用文中方法在项目加载时, 也就是在ServletContextListener监听器中直接使用反射预先加载单例模式工厂类, 从而优化用户的体验;

以上就是博主对"JavaWeb三层架构中Service和Dao层对象单例化的必要性"的论述, 即实现此处的单例化是个很有优势的选择, 同时附带介绍了层间解耦思想,希望对大家有所帮助.

写博客唯一的目的: 本着对读者负责的态度,要求自己以及其严谨的态度学习研究某一方面知识.

转载请标明出处: 划船一哥 :https://blog.csdn.net/weixin_42711325/article/details/83340431

猜你喜欢

转载自blog.csdn.net/weixin_42711325/article/details/83340431