Spring学习02-Spring中的设计模式(一)
1.1单例模式(Singleton Pattern)
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
简单的来说,该类至于允许有一个实例化的对象,可以避免该类的重复实例化对象的消耗
Instance
饿汉式单例模式
饿汉单例模式在类加载的时候就立即初始化,并创建单例对象,它绝对的线程安全,在程序还没有出现以前就已经实例化了,不存在访问安全。
优点:没有任何枷锁的机制、执行效率高,用户体验好
缺点:不管用不用都占着空姐,浪费内存。
在Spring中IoC容器 ApplicationContext 本身就是典型的饿汉式单例模式
public class HungrySingleton {
private static final HungrySingleton obj = new HungrySingleton();
private HungrySingleton(){};
public static HungrySingleton getInstance(){return obj;}
}
/**
* 写法二,使用静态代码块
*/
public class HungryStaticSingleton {
private static final HungryStaticSingleton obj;
static {
obj = new HungryStaticSingleton();
}
private HungryStaticSingleton(){};
public static HungryStaticSingleton getInstance(){return obj;}
}
懒汉式单例模式
懒汉单例模式,被外部类调用的时候内部类才会加载,性能更优,用的也比较多。
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() throws Exception {}
public static LazySimpleSingleton getInstance() throws Exception {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
但上述代码可能存线程安全隐患,当线程1和2同时执行到 if(instance == null) 时两个线程同时执行完,这样就会创建出两个对象。
那么最简单的方式就是对getInstance()加上synchronized关键字,但是线程数量比较多的情况下,如果CPU分配压力上升,则会导致线程阻塞,导致程序性能大福大下降。
于是,我们可以使用双重检查的加锁机制来改进。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance = null;
private LazyDoubleCheckSingleton(){};
public static LazyDoubleCheckSingleton getInstance(){
if (instance == null){
synchronized (LazyDoubleCheckSingleton.class){
if (instance == null){
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
这样一来只需要第一次并发执行的时候需要加锁,之后的在此允许都不要加锁,大大增加运行效率。
关于volatile关键字和synchronized关键字的运行原理和作用会在之后的面试学习中会更新。
简单的来说,volatile保证了线程之间的可见性和禁止进行指令重排序。
但是我们仍然可以使用反射来破坏单例模式
public static void main(String[] args) throws Exception{
Class<?> clazz = LazyDoubleCheckSingleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
Object o2= constructor.newInstance();
System.out.println(o1 == o2);//false
}
由于反射可以强制访问私有的方法属性,我们可以建立多个instance。
最后,我们在私有构造器下加入了判断和抛出异常,就得到了一个比较好的单例模式!
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance = null;
private LazyDoubleCheckSingleton() throws Exception {
if (instance != null){
throw new Exception("Constructor is banded!");
}
};
public static LazyDoubleCheckSingleton getInstance() throws Exception {
if (instance == null){
synchronized (LazyDoubleCheckSingleton.class){
if (instance == null){
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
但是但是,我们还是可以通过序列化来破坏单例模式,能力有限,就不跟新了。。。
小结
单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。单例模式看似非常简单,实现起来其实也有一定技巧。
2.1简单工厂模式(Simple Factory Pattern)
简单工厂模式是指一个工厂对象决定创建哪一种产品的实例。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
Instance
以书店为例,每次需要什么书籍,就想BookFactory索要,自己不生产书籍
public interface IBook {
}
public class JavaBook implements IBook {
}
public class PythonBook implements IBook {
}
public class BookFactory {
public static IBook produce(String name){
if ("java".equals(name)){
return new JavaBook();
}else if ("python".equals(name)){
return new PythonBook();
}else{
return null;
}
}
}
当我们需要使用javabook的类的时候,就可以直接想BookFactory索要
public static void main(String[] args) {
IBook java = BookFactory.produce("java");
}
此时,客户端的调用变简单了,但是随着业务扩展,可能需要生成各种各样的书籍,这是就需要修改BookFactory代码,这不符合开闭原则,因此我们还可以继续优化,采用反射技术。
public static IBook produce(String className){
try {
if ( ! (null == className || "".equals(className))){
return (IBook) Class.forName(className).getDeclaredConstructor().newInstance();
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
IBook book = BookRefFactory.produce("factory.simplefactory.JavaBook"); //是从source root下的相对路径 也就是java蓝色包下路径
System.out.println(book);//factory.simplefactory.JavaBook@27d6c5e0
}
如果找不到,可以使用idea 然后把/改成. 再把.java去掉。
但是该方法参数是字符串,可控性有待提升,还需要强制转型
public static IBook produce2(Class<? extends IBook> clazz){
try {
if (null != clazz){
return clazz.getDeclaredConstructor().newInstance();
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
IBook iBook = BookRefFactory.produce2(JavaBook.class);
System.out.println(iBook);//factory.simplefactory.JavaBook@4f3f5b24
}
2.2 工厂方法模式(Factory Method Pattern)
工厂方法模式是指定义一个创捷对象的接口,但让实现这个接口的类来决定实例化哪个对象,工厂方法模式让类的实例化推迟到子类中进行。
工厂方法模式主要是解决产品扩展的问题,在简单工厂模式中,随着产品链的丰富,如果每本书的创建逻辑有区别,则工厂职责会越变越多,不便于维护。根据单一职责原则,我们需要将职能拆分。
Instance
public interface IBookFactory {
IBook produce();
}
public class JavaBookFactory implements IBookFactory{
@Override
public IBook produce() {
return (IBook) new JavaBook();
}
}
public class PythonBookFactory implements IBookFactory{
@Override
public IBook produce() {
return (IBook) new PythonBook();
}
}
客户端代码
public static void main(String[] args) {
IBookFactory factory = new PythonBookFactory();
IBook book = factory.produce();
factory = new JavaBookFactory();
book = factory.produce();
}
也就是说将特定的工厂生成特定的类,但这些工厂都实现同一个接口
小节
工厂方法模式适用于
1.创建对象需要大量复制代码
2.客户端(应用层)不依赖于产品类实例如何创建,实现等细节
3.一个类通过其子类来指定创建哪个对象
缺点:
1.类的个数容易过多,增加复杂度
2.增加了系统的抽象性和理解难度
2.3 抽线工厂模式(Abstract Factory Pattern)
抽象工厂模式是指提供一个创建一些列相关或相互依赖的对象接口,无须指定他们具体的类。
理解抽象工厂模式,我们要了解两个概念:产品等级结构和产品族。
比如美的电器生产多种家用家电,有美的洗衣机,美的空调,美的热水器等,这些都是美的品牌,属于美的电器这个产品族。
我们再从空调结构看,空调可以有美的的,也可以有格力,奥克斯等等,这些就属于同一产品结构。
Instance
下面,我们具体来看一个业务场景,一课程为例。一培训机构推出的课程,不仅要提供视频播放功能,也要提供老师的课堂笔记。
此时,在产品等级中就需要增加两个产品:IVideo和INote两个功能。也就是多出了两个两个产品等级,在产品族中Java课程也要多包涵了两个功能。
但我们并不能顶死每次录制的都是Java课程,所以抽象工厂模式,生产的都是抽象类。
public interface INote {
void edit();
}
public interface IVideo {
void record();
}
在创建工厂模式类
public interface CourseFactory {
INote createNote();
IVideo createVideo();
}
其实,抽象工厂模式和简单工厂模式主要的区别就是抽象工厂模式生产的实例还是抽象类型的,而且本身自己也是抽象接口,适用于不同的产品族。
接下来我们创建Java产品族
public class JavaVideo implements IVideo{
@Override
public void record() {
System.out.println("录制Java课程");
}
}
public class JavaNote implements INote{
@Override
public void edit() {
System.out.println("编写Java笔记");
}
}
创建Java产品族的具体工厂
public class JavaCourseFactory implements CourseFactory{
@Override
public INote createNote() {
return new JavaNote();
}
@Override
public IVideo createVideo() {
return new JavaVideo();
}
}
来看看客服端调用的代码
public static void main(String[] args) {
CourseFactory CourseFactory = new JavaCourseFactory();
CourseFactory.createNote().edit();
CourseFactory.createVideo().record();
}
上面代码完整描述了Java课程的产品族,我们也可以创建别的不同产品族如Pyton,也描述了两个产品等级:视频和笔记。
抽象工厂模式非常完美地描述了这一次复杂的关系。
那么此时我们还需要增加一个产品等级,如增加提供源码这一功能,我们显然需要修改CourseFactory接口,这就违反了开闭原则,这也是抽象工厂模式的缺点。
但是实际应用中,产品等级结构升级时非常正常的一件事情,只要不频繁升级,根据实际情况可以不遵守开闭原则!
总结
简单工厂:唯一工厂类,一个产品抽象类,工厂类的创建方法依据入参判断并创建具体产品对象。
工厂方法:多个工厂类,一个产品抽象类,利用多态创建不同的产品对象,避免了大量的if-else判断。
抽象工厂:多个工厂类,多个产品抽象类,产品子类分组,同一个工厂实现类创建同组中的不同产品,减少了工厂子类的数量。
3.1 静态代理模式
代理模式是指为其他对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用。简单的来说就是在原有的方法上下还可以增加功能,来时原有的方法增强。
Instance
假设一个场景,张三想要起诉李四,但他并不懂法律,他在法庭上只能阐述事实的经过,这就需要一个代理律师来帮他利用相关的法律条纹来控诉李四。
那么这些人在法庭上都是以一个Speaker的身份进行讲话,所以我们可以定义一个Speaker接口
public interface Speaker {
void speak();
}
public class ZhangSan implements Speaker{
@Override
public void speak() {
System.out.println("阐述事实");//张三需要做的只是阐述事实
}
}
//而律师在法庭上就需要对张三的控诉进行增强,在阐述事实前后进行辩论。
public class Lawyer implements Speaker{
private ZhangSan zhangSan = new ZhangSan();
@Override
public void speak() {
System.out.println("引用法律条文");
zhangSan.speak();
System.out.println("李四犯法了");
}
}
public static void main(String[] args) {
Speaker speaker = new Lawyer();
speaker.speak();
}
//out
//引用法律条文
//阐述事实
//李四犯法了
所以到了法庭上,律师就成功的帮张三进行了控诉代理。
但是,此时律师和张三已经耦合死了,律师只能够替张三进行诉讼代理,如果想要题王五,赵六等等人进行代理,需要重新创建新的类,十分麻烦。这就需要动态代理了。
3.2 jdk动态代理
public class LawyerProxy implements InvocationHandler {
private Speaker client;;
//通过set注入需要代理的对象
public void setClient(Speaker client) {
this.client = client;
}
//参数: object是被代理的对象,Method是被代理对象的方法,和被代理对象的方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("speak".equals(method.getName())){
System.out.println("引用法律条文");
method.invoke(client,args);
System.out.println("李四犯法了");
}
return null;
}
}
public static void main(String[] args) {
LawyerProxy lawyerProxy = new LawyerProxy();
Speaker client = new ZhangSan();
lawyerProxy.setClient(client);
ClassLoader loader = client.getClass().getClassLoader();
Class[] interfaces = client.getClass().getInterfaces();
Speaker speaker = (Speaker) Proxy.newProxyInstance(loader, interfaces, lawyerProxy);
speaker.speak();
}
这样一来,只要是实现了Speaker接口的对象都可以被律师代理,十分方便
动态代理具体步骤:
- 通过实现 InvocationHandler 接口创建自己的调用处理器;
- 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
- 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
3.3CGLIB动态代理
首先需要导入cblib jar包,如果使用maven的化可以使用依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
之后在实现MethodInterceptor接口,几乎与InvocationHandler一模一样
CGLIB动态代理和JDK动态代理比较
- JDK动态代理是实现了被代理对象的接口,CGLIB动态代理是继承了被代理对象
- JDK和CGLIB都是在运行时生成字节码,JDK是直接生成Class字节码,CGLIB是使用ASM框架写Class字节码,CGLIB代理实现更复杂,因此生成代理类比JDK效率低
- JDK调用代理方法是通过反射机制调用,而CGLIB是通过FastClass机制直接调用方法,因此CGLIB执行效率更高。
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import proxy.staticproxy.Speaker;
import java.lang.reflect.Method;
public class LawyerInterceptor implements MethodInterceptor {
private Speaker client;;
public void setClient(Speaker client) {
this.client = client;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if ("speak".equals(method.getName())){
System.out.println("引用法律条文");
method.invoke(client,objects);
System.out.println("李四犯法了");
}
return null;
}
}
public static void main(String[] args) {
LawyerInterceptor lawyerInterceptor = new LawyerInterceptor();
ZhangSan zhangSan = new ZhangSan();
lawyerInterceptor.setClient(zhangSan);
ZhangSan zhangsanLawyer = (ZhangSan) Enhancer.create(ZhangSan.class, lawyerInterceptor);
zhangsanLawyer.speak();
}
Spring 中的代理选择的原则
- 当Bean有实现接口时,Spring就会使用JDK的动态代理
- 当Bean没有实现接口时,Spring使用Cglib动态代理
- Spring可以强制使用Cglib动态代理,只需要在Spring的配置文件中加如下代码
<aop:aspectj-autoproxy proxy-target-class="true"/>
Reference
主要材料:《Spring 5核心原理与30个类手写实战》
工厂模式1:https://www.zhihu.com/question/27125796/answer/1615074467
工厂模式2:https://www.runoob.com/design-pattern/factory-pattern.html
代理模式:https://www.bilibili.com/video/BV1cz41187Dk?spm_id_from=333.999.0.0
java动态代理:https://www.jianshu.com/p/9bcac608c714
动态代理2:https://www.jianshu.com/p/8aee43cbc373
产品等级结构和产品族:https://www.jianshu.com/p/f1e837cab952