文章目录

  • 从框架源码学习结构型设计模式
    • 适配器模式
      • 应用实例
      • 案例一:dubbo框架日志适配器
        • Logger接口
        • 日志实现类
        • Logger适配器接口
        • LoggerAdapter实现类
        • Logger日志工厂
    • 桥接模式
      • 应用场景
      • 案例:dubbo源码-远程调用模块channelHandler设计
        • ChannelHandler是一个SPI拓展接口,用户可以定制自己的Handler
        • 抽象类:桥接ChannelHandler并实现自己的Handler
        • ChannelHandler实现类
        • 抽象类实现类
    • 组合模式
    • 装饰器模式
      • 案例:apache common-collections源码List分析
        • jdk List接口源码
        • List装饰器抽象方法
        • 序列化装饰器抽象方法
        • FixedSizeList源码
        • LazyList实现类
    • 外观模式(门面模式)
      • 案例:sl4j日志使用
    • 享元模式
      • RocketMQ过滤器工厂创建Filter
    • 代理模式
      • 静态代理
        • 案例:rocketmq中滚动记录文件,先将日志记录到指定文件代理类实现
      • 动态代理
        • JDK动态代理
          • 案例:dubbo中jdk动态代理工厂实现
        • CGLIB动态代理
          • 案例:dubbo中cglib创建代理类
        • javassist动态代码创建

从框架源码学习结构型设计模式

模式 包括
结构型模式 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。 适配器模式(Adapter Pattern) 桥接模式(Bridge Pattern) 过滤器模式(Filter、Criteria Pattern) 组合模式(Composite Pattern) 装饰器模式(Decorator Pattern) 外观模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern)

适配器模式

使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。最典型的例子如:读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。

应用实例

JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。

public class EnumerationAdapter<E> implements Iterator<E> {Enumeration<E> enumeration;public EnumerationAdapter(Enumeration<E> enumeration) {this.enumeration = enumeration;}@Overridepublic boolean hasNext() {return enumeration.hasMoreElements();}@Overridepublic E next() {return enumeration.nextElement();}@Overridepublic void remove() {throw new UnsupportedOperationException("remove");}
}

案例一:dubbo框架日志适配器

dubbo中通过LoggerFactory日志工厂创建log对象,流行的日志框架有log4j、sl4j、log4j2、logback等而他们创建日志的对象各不相同,如果不用适配器直接在LoggerFactory上创建日志对象,那么每新加入一个新的日志框架都需要去修改LoggerFactory类,而利用适配器的话新加入的日志框架可以基于SPI实现LoggerAdapter接口实现对象创建逻辑,并且通过Adapter封装统一的getLogger()、setLogger()、setLevel()统一方法,然后通过LoggerFactory#setLoggerAdapter()方法将对象放入日志工厂中。

Logger接口

public interface Logger {/*** Logs a message with trace log level.** @param msg log this message*/void trace(String msg);/*** Logs an error with trace log level.** @param e log this cause*/void trace(Throwable e);/*** Logs an error with trace log level.** @param msg log this message* @param e   log this cause*/void trace(String msg, Throwable e);/*** Logs a message with debug log level.** @param msg log this message*/void debug(String msg);/*** Logs an error with debug log level.** @param e log this cause*/void debug(Throwable e);/*** Logs an error with debug log level.** @param msg log this message* @param e   log this cause*/void debug(String msg, Throwable e);/*** Logs a message with info log level.** @param msg log this message*/void info(String msg);/*** Logs an error with info log level.** @param e log this cause*/void info(Throwable e);/*** Logs an error with info log level.** @param msg log this message* @param e   log this cause*/void info(String msg, Throwable e);/*** Logs a message with warn log level.** @param msg log this message*/void warn(String msg);/*** Logs a message with warn log level.** @param e log this message*/void warn(Throwable e);/*** Logs a message with warn log level.** @param msg log this message* @param e   log this cause*/void warn(String msg, Throwable e);/*** Logs a message with error log level.** @param msg log this message*/void error(String msg);/*** Logs an error with error log level.** @param e log this cause*/void error(Throwable e);/*** Logs an error with error log level.** @param msg log this message* @param e   log this cause*/void error(String msg, Throwable e);/*** Is trace logging currently enabled?** @return true if trace is enabled*/boolean isTraceEnabled();/*** Is debug logging currently enabled?* * @return true if debug is enabled*/boolean isDebugEnabled();/*** Is info logging currently enabled?** @return true if info is enabled*/boolean isInfoEnabled();/*** Is warn logging currently enabled?** @return true if warn is enabled*/boolean isWarnEnabled();/*** Is error logging currently enabled?** @return true if error is enabled*/boolean isErrorEnabled();}

日志实现类

public class Log4jLogger implements Logger {private static final String FQCN = FailsafeLogger.class.getName();private final org.apache.log4j.Logger logger;public Log4jLogger(org.apache.log4j.Logger logger) {this.logger = logger;}@Overridepublic void trace(String msg) {logger.log(FQCN, Level.TRACE, msg, null);}@Overridepublic void trace(Throwable e) {logger.log(FQCN, Level.TRACE, e == null ? null : e.getMessage(), e);}@Overridepublic void trace(String msg, Throwable e) {logger.log(FQCN, Level.TRACE, msg, e);}@Overridepublic void debug(String msg) {logger.log(FQCN, Level.DEBUG, msg, null);}@Overridepublic void debug(Throwable e) {logger.log(FQCN, Level.DEBUG, e == null ? null : e.getMessage(), e);}@Overridepublic void debug(String msg, Throwable e) {logger.log(FQCN, Level.DEBUG, msg, e);}@Overridepublic void info(String msg) {logger.log(FQCN, Level.INFO, msg, null);}@Overridepublic void info(Throwable e) {logger.log(FQCN, Level.INFO, e == null ? null : e.getMessage(), e);}@Overridepublic void info(String msg, Throwable e) {logger.log(FQCN, Level.INFO, msg, e);}@Overridepublic void warn(String msg) {logger.log(FQCN, Level.WARN, msg, null);}@Overridepublic void warn(Throwable e) {logger.log(FQCN, Level.WARN, e == null ? null : e.getMessage(), e);}@Overridepublic void warn(String msg, Throwable e) {logger.log(FQCN, Level.WARN, msg, e);}@Overridepublic void error(String msg) {logger.log(FQCN, Level.ERROR, msg, null);}@Overridepublic void error(Throwable e) {logger.log(FQCN, Level.ERROR, e == null ? null : e.getMessage(), e);}@Overridepublic void error(String msg, Throwable e) {logger.log(FQCN, Level.ERROR, msg, e);}@Overridepublic boolean isTraceEnabled() {return logger.isTraceEnabled();}@Overridepublic boolean isDebugEnabled() {return logger.isDebugEnabled();}@Overridepublic boolean isInfoEnabled() {return logger.isInfoEnabled();}@Overridepublic boolean isWarnEnabled() {return logger.isEnabledFor(Level.WARN);}@Overridepublic boolean isErrorEnabled() {return logger.isEnabledFor(Level.ERROR);}}

Logger适配器接口

@SPI
public interface LoggerAdapter {/*** Get a logger** @param key the returned logger will be named after clazz* @return logger*/Logger getLogger(Class<?> key);/*** Get a logger** @param key the returned logger will be named after key* @return logger*/Logger getLogger(String key);/*** Get the current logging level** @return current logging level*/Level getLevel();/*** Set the current logging level** @param level logging level*/void setLevel(Level level);/*** Get the current logging file** @return current logging file*/File getFile();/*** Set the current logging file** @param file logging file*/void setFile(File file);
}

LoggerAdapter实现类

public class Log4jLoggerAdapter implements LoggerAdapter {private File file;@SuppressWarnings("unchecked")public Log4jLoggerAdapter() {try {org.apache.log4j.Logger logger = LogManager.getRootLogger();if (logger != null) {Enumeration<Appender> appenders = logger.getAllAppenders();if (appenders != null) {while (appenders.hasMoreElements()) {Appender appender = appenders.nextElement();if (appender instanceof FileAppender) {FileAppender fileAppender = (FileAppender) appender;String filename = fileAppender.getFile();file = new File(filename);break;}}}}} catch (Throwable t) {}}private static org.apache.log4j.Level toLog4jLevel(Level level) {if (level == Level.ALL) {return org.apache.log4j.Level.ALL;}if (level == Level.TRACE) {return org.apache.log4j.Level.TRACE;}if (level == Level.DEBUG) {return org.apache.log4j.Level.DEBUG;}if (level == Level.INFO) {return org.apache.log4j.Level.INFO;}if (level == Level.WARN) {return org.apache.log4j.Level.WARN;}if (level == Level.ERROR) {return org.apache.log4j.Level.ERROR;}// if (level == Level.OFF)return org.apache.log4j.Level.OFF;}private static Level fromLog4jLevel(org.apache.log4j.Level level) {if (level == org.apache.log4j.Level.ALL) {return Level.ALL;}if (level == org.apache.log4j.Level.TRACE) {return Level.TRACE;}if (level == org.apache.log4j.Level.DEBUG) {return Level.DEBUG;}if (level == org.apache.log4j.Level.INFO) {return Level.INFO;}if (level == org.apache.log4j.Level.WARN) {return Level.WARN;}if (level == org.apache.log4j.Level.ERROR) {return Level.ERROR;}// if (level == org.apache.log4j.Level.OFF)return Level.OFF;}@Overridepublic Logger getLogger(Class<?> key) {return new Log4jLogger(LogManager.getLogger(key));}@Overridepublic Logger getLogger(String key) {return new Log4jLogger(LogManager.getLogger(key));}@Overridepublic Level getLevel() {return fromLog4jLevel(LogManager.getRootLogger().getLevel());}@Overridepublic void setLevel(Level level) {LogManager.getRootLogger().setLevel(toLog4jLevel(level));}@Overridepublic File getFile() {return file;}@Overridepublic void setFile(File file) {}}

Logger日志工厂

public class LoggerFactory {private static final ConcurrentMap<String, FailsafeLogger> LOGGERS = new ConcurrentHashMap<>();private static volatile LoggerAdapter LOGGER_ADAPTER;// search common-used logging frameworksstatic {String logger = System.getProperty("dubbo.application.logger", "");switch (logger) {case "slf4j":setLoggerAdapter(new Slf4jLoggerAdapter());break;case "jcl":setLoggerAdapter(new JclLoggerAdapter());break;case "log4j":setLoggerAdapter(new Log4jLoggerAdapter());break;case "jdk":setLoggerAdapter(new JdkLoggerAdapter());break;case "log4j2":setLoggerAdapter(new Log4j2LoggerAdapter());break;default:List<Class<? extends LoggerAdapter>> candidates = Arrays.asList(Log4jLoggerAdapter.class,Slf4jLoggerAdapter.class,Log4j2LoggerAdapter.class,JclLoggerAdapter.class,JdkLoggerAdapter.class);for (Class<? extends LoggerAdapter> clazz : candidates) {try {setLoggerAdapter(clazz.newInstance());break;} catch (Throwable ignored) {}}}}private LoggerFactory() {}public static void setLoggerAdapter(String loggerAdapter) {if (loggerAdapter != null && loggerAdapter.length() > 0) {setLoggerAdapter(ExtensionLoader.getExtensionLoader(LoggerAdapter.class).getExtension(loggerAdapter));}}/*** Set logger provider** @param loggerAdapter logger provider*/public static void setLoggerAdapter(LoggerAdapter loggerAdapter) {if (loggerAdapter != null) {Logger logger = loggerAdapter.getLogger(LoggerFactory.class.getName());logger.info("using logger: " + loggerAdapter.getClass().getName());LoggerFactory.LOGGER_ADAPTER = loggerAdapter;for (Map.Entry<String, FailsafeLogger> entry : LOGGERS.entrySet()) {entry.getValue().setLogger(LOGGER_ADAPTER.getLogger(entry.getKey()));}}}/*** Get logger provider** @param key the returned logger will be named after clazz* @return logger*/public static Logger getLogger(Class<?> key) {return LOGGERS.computeIfAbsent(key.getName(), name -> new FailsafeLogger(LOGGER_ADAPTER.getLogger(name)));}/*** Get logger provider** @param key the returned logger will be named after key* @return logger provider*/public static Logger getLogger(String key) {return LOGGERS.computeIfAbsent(key, k -> new FailsafeLogger(LOGGER_ADAPTER.getLogger(k)));}/*** Get logging level** @return logging level*/public static Level getLevel() {return LOGGER_ADAPTER.getLevel();}/*** Set the current logging level** @param level logging level*/public static void setLevel(Level level) {LOGGER_ADAPTER.setLevel(level);}/*** Get the current logging file** @return current logging file*/public static File getFile() {return LOGGER_ADAPTER.getFile();}}

桥接模式

我们大家都熟悉,顾名思义就是用来将河的两岸联系起来的。而此处的桥是用来将两个独立的结构联系起来,而这两个被联系起来的结构可以独立的变化,所有其他的理解只要建立在这个层面上就会比较容易。

它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

优先文章链接

  1. 《菜鸟教程-桥接模式》
  2. 《知乎-秒懂设计模式》
  3. 《设计模式-桥接模式(诺基亚:我觉得我还能抢救下)》

应用场景

按照手机分类有智能手机、按钮手机、翻盖手机等,按照商品又有华为、小米、诺基亚等等

public class BridgeModel {/*** 品牌*/public interface Brand{void start();void shutdown();}/*** 手机抽象类*/public abstract class Phone{Brand brand;/*** 这段代码就是"桥接"思想,将另一个类和抽象类组合* @param brand*/public Phone(Brand brand) {this.brand = brand;}void start(){this.brand.start();}void shutdown(){this.brand.shutdown();}}class HuaWei implements Brand{@Overridepublic void start() {System.out.println("华为 开机");}@Overridepublic void shutdown() {System.out.println("华为 关机");}}class XiaoMi implements Brand{@Overridepublic void start() {System.out.println("小米 开机");}@Overridepublic void shutdown() {System.out.println("小米 关机");}}class Nokia implements Brand{@Overridepublic void start() {System.out.println("诺基亚 开机");}@Overridepublic void shutdown() {System.out.println("诺基亚 关机");}}/*** 带键盘按钮手机*/class ButtonIphone extends Phone{public ButtonIphone(Brand brand) {super(brand);}}/*** 智能手机*/class SmartIphone extends Phone{public SmartIphone(Brand brand) {super(brand);}}@Testpublic void runTest() {ButtonIphone buttonIphone = new ButtonIphone(new Nokia());buttonIphone.start();buttonIphone.shutdown();SmartIphone huawei = new SmartIphone(new HuaWei());huawei.start();huawei.shutdown();SmartIphone xiaomi = new SmartIphone(new XiaoMi());xiaomi.start();xiaomi.shutdown();}}

案例:dubbo源码-远程调用模块channelHandler设计

ChannelHandler是一个SPI拓展接口,用户可以定制自己的Handler

@SPI
public interface ChannelHandler {/*** on channel connected.** @param channel channel.*/void connected(Channel channel) throws RemotingException;/*** on channel disconnected.** @param channel channel.*/void disconnected(Channel channel) throws RemotingException;/*** on message sent.** @param channel channel.* @param message message.*/void sent(Channel channel, Object message) throws RemotingException;/*** on message received.** @param channel channel.* @param message message.*/void received(Channel channel, Object message) throws RemotingException;/*** on exception caught.** @param channel   channel.* @param exception exception.*/void caught(Channel channel, Throwable exception) throws RemotingException;}

抽象类:桥接ChannelHandler并实现自己的Handler

public abstract class AbstractChannelHandlerDelegate implements ChannelHandlerDelegate {protected ChannelHandler handler;protected AbstractChannelHandlerDelegate(ChannelHandler handler) {Assert.notNull(handler, "handler == null");this.handler = handler;}@Overridepublic ChannelHandler getHandler() {if (handler instanceof ChannelHandlerDelegate) {return ((ChannelHandlerDelegate) handler).getHandler();}return handler;}@Overridepublic void connected(Channel channel) throws RemotingException {handler.connected(channel);}@Overridepublic void disconnected(Channel channel) throws RemotingException {handler.disconnected(channel);}@Overridepublic void sent(Channel channel, Object message) throws RemotingException {handler.sent(channel, message);}@Overridepublic void received(Channel channel, Object message) throws RemotingException {handler.received(channel, message);}@Overridepublic void caught(Channel channel, Throwable exception) throws RemotingException {handler.caught(channel, exception);}
}

ChannelHandler实现类

public class MockedChannelHandler implements ChannelHandler {//    ConcurrentMap<String, Channel> channels = new ConcurrentHashMap<String, Channel>();ConcurrentHashSet<Channel> channels = new ConcurrentHashSet<Channel>();@Overridepublic void connected(Channel channel) throws RemotingException {channels.add(channel);}@Overridepublic void disconnected(Channel channel) throws RemotingException {channels.remove(channel);}@Overridepublic void sent(Channel channel, Object message) throws RemotingException {channel.send(message);}@Overridepublic void received(Channel channel, Object message) throws RemotingException {//echo channel.send(message);}@Overridepublic void caught(Channel channel, Throwable exception) throws RemotingException {throw new RemotingException(channel, exception);}public Set<Channel> getChannels() {return Collections.unmodifiableSet(channels);}
}

抽象类实现类

public class MultiMessageHandler extends AbstractChannelHandlerDelegate {public MultiMessageHandler(ChannelHandler handler) {super(handler);}@SuppressWarnings("unchecked")@Overridepublic void received(Channel channel, Object message) throws RemotingException {if (message instanceof MultiMessage) {MultiMessage list = (MultiMessage) message;for (Object obj : list) {handler.received(channel, obj);}} else {handler.received(channel, message);}}
}

组合模式

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

具体可以参考该文章:http://c.biancheng.net/view/1373.html

装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

案例:apache common-collections源码List分析

apache common-collections是对jdk collections功能的补充,用了大量的装饰器设计模式,一下摘取List为例

commons-collections使用介绍之List

jdk List接口源码

public interface List<E> extends Collection<E> {//继承了Collection接口int size();boolean isEmpty();boolean contains(Object o);Iterator<E> iterator();Object[] toArray();<T> T[] toArray(T[] a);boolean add(E e);boolean remove(Object o);boolean containsAll(Collection<?> c);boolean addAll(Collection<? extends E> c);boolean removeAll(Collection<?> c);boolean retainAll(Collection<?> c);boolean equals(Object o);int hashCode();default Spliterator<E> spliterator() {return Spliterators.spliterator(this, Spliterator.ORDERED);}
}

List装饰器抽象方法

public abstract class AbstractListDecorator extends AbstractCollectionDecorator implements List {protected AbstractListDecorator() {}protected AbstractListDecorator(List list) {super(list);}protected List getList() {return (List)this.getCollection();}public void add(int index, Object object) {this.getList().add(index, object);}public boolean addAll(int index, Collection coll) {return this.getList().addAll(index, coll);}public Object get(int index) {return this.getList().get(index);}public int indexOf(Object object) {return this.getList().indexOf(object);}public int lastIndexOf(Object object) {return this.getList().lastIndexOf(object);}public ListIterator listIterator() {return this.getList().listIterator();}public ListIterator listIterator(int index) {return this.getList().listIterator(index);}public Object remove(int index) {return this.getList().remove(index);}public Object set(int index, Object object) {return this.getList().set(index, object);}public List subList(int fromIndex, int toIndex) {return this.getList().subList(fromIndex, toIndex);}
}

序列化装饰器抽象方法

public abstract class AbstractSerializableListDecorator extends AbstractListDecorator implements Serializable {private static final long serialVersionUID = 2684959196747496299L;protected AbstractSerializableListDecorator(List list) {super(list);}private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject();out.writeObject(this.collection);}private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();this.collection = (Collection)in.readObject();}
}

FixedSizeList源码

FixedSizeList修饰另一个列表防止添加/删除并且固定列表大小。add、remove、clear和retain操作是不被支持的,set方法是允许的但是不会影响列表大小。

public class FixedSizeList extends AbstractSerializableListDecorator implements BoundedCollection {private static final long serialVersionUID = -2218010673611160319L;public static List decorate(List list) {return new FixedSizeList(list);}protected FixedSizeList(List list) {super(list);}public boolean add(Object object) {throw new UnsupportedOperationException("List is fixed size");}public void add(int index, Object object) {throw new UnsupportedOperationException("List is fixed size");}public boolean addAll(Collection coll) {throw new UnsupportedOperationException("List is fixed size");}public boolean addAll(int index, Collection coll) {throw new UnsupportedOperationException("List is fixed size");}public void clear() {throw new UnsupportedOperationException("List is fixed size");}public Object get(int index) {return this.getList().get(index);}public int indexOf(Object object) {return this.getList().indexOf(object);}public Iterator iterator() {return UnmodifiableIterator.decorate(this.getCollection().iterator());}public int lastIndexOf(Object object) {return this.getList().lastIndexOf(object);}public ListIterator listIterator() {return new FixedSizeList.FixedSizeListIterator(this.getList().listIterator(0));}public ListIterator listIterator(int index) {return new FixedSizeList.FixedSizeListIterator(this.getList().listIterator(index));}public Object remove(int index) {throw new UnsupportedOperationException("List is fixed size");}public boolean remove(Object object) {throw new UnsupportedOperationException("List is fixed size");}public boolean removeAll(Collection coll) {throw new UnsupportedOperationException("List is fixed size");}public boolean retainAll(Collection coll) {throw new UnsupportedOperationException("List is fixed size");}public Object set(int index, Object object) {return this.getList().set(index, object);}public List subList(int fromIndex, int toIndex) {List sub = this.getList().subList(fromIndex, toIndex);return new FixedSizeList(sub);}public boolean isFull() {return true;}public int maxSize() {return this.size();}static class FixedSizeListIterator extends AbstractListIteratorDecorator {protected FixedSizeListIterator(ListIterator iterator) {super(iterator);}public void remove() {throw new UnsupportedOperationException("List is fixed size");}public void add(Object object) {throw new UnsupportedOperationException("List is fixed size");}}
}

LazyList实现类

LazyList修饰另一个列表,当调用get方法时,如果索引超出列表长度,列表会自动增长,我们可以通过一个工厂获得超出索引位置的值。LazyList和GrowthList都可以实现对修饰的列表进行增长,但是LazyList发生在get时候,而GrowthList发生在set和add时候,我们也可以混合使用这两种列表。

public class LazyList extends AbstractSerializableListDecorator {private static final long serialVersionUID = -1708388017160694542L;protected final Factory factory;public static List decorate(List list, Factory factory) {return new LazyList(list, factory);}protected LazyList(List list, Factory factory) {super(list);if (factory == null) {throw new IllegalArgumentException("Factory must not be null");} else {this.factory = factory;}}public Object get(int index) {int size = this.getList().size();Object object;if (index < size) {object = this.getList().get(index);if (object == null) {object = this.factory.create();this.getList().set(index, object);return object;} else {return object;}} else {for(int i = size; i < index; ++i) {this.getList().add((Object)null);}object = this.factory.create();this.getList().add(object);return object;}}public List subList(int fromIndex, int toIndex) {List sub = this.getList().subList(fromIndex, toIndex);return new LazyList(sub, this.factory);}
}

外观模式(门面模式)

其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。用一张图来表示门面模式的结构为:

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

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

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

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

日志门面(抽象层) 日志实现
JCL(java.util.logging) JUL
SLF4j log4j、log4j2、logback

slf4j的作用:只要所有代码都使用门面对象slf4j,我们就不需要关心其具体实现,最终所有地方使用一种具体实现即可,更换、维护都非常方便

sl4j作用以及原理

案例:sl4j日志使用

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class HelloWorld {public static void main(String[] args) {// HelloWorld.class 就是你要打印的指定类的日志,// 如果你想在其它类中打印,那就把 HelloWorld.class 替换成目标类名.class 即可。Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.info("Hello World");}
}

享元模式

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。

RocketMQ过滤器工厂创建Filter

public class FilterFactory {public static final FilterFactory INSTANCE = new FilterFactory();protected static final Map<String, FilterSpi> FILTER_SPI_HOLDER = new HashMap<String, FilterSpi>(4);static {FilterFactory.INSTANCE.register(new SqlFilter());}/*** Register a filter.* <br>* Note:* <li>1. Filter registered will be used in broker server, so take care of it's reliability and performance.</li>*/public void register(FilterSpi filterSpi) {if (FILTER_SPI_HOLDER.containsKey(filterSpi.ofType())) {throw new IllegalArgumentException(String.format("Filter spi type(%s) already exist!", filterSpi.ofType()));}FILTER_SPI_HOLDER.put(filterSpi.ofType(), filterSpi);}/*** Un register a filter.*/public FilterSpi unRegister(String type) {return FILTER_SPI_HOLDER.remove(type);}/*** Get a filter registered, null if none exist.*/public FilterSpi get(String type) {return FILTER_SPI_HOLDER.get(type);}

代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

静态代理

静态代理的代理关系在编译期间就已经确定了的。它适合于代理类较少且确定的情况

案例:rocketmq中滚动记录文件,先将日志记录到指定文件代理类实现

    private static InternalLogger createLogger(final String loggerName) {String clientLogLevel = System.getProperty(CLIENT_LOG_LEVEL, "INFO");boolean additive = "true".equalsIgnoreCase(System.getProperty(CLIENT_LOG_ADDITIVE));InternalLogger logger = InternalLoggerFactory.getLogger(loggerName);InnerLoggerFactory.InnerLogger innerLogger = (InnerLoggerFactory.InnerLogger) logger;Logger realLogger = innerLogger.getLogger();//if (rocketmqClientAppender == null) {//   createClientAppender();//}realLogger.addAppender(new AppenderProxy());realLogger.setLevel(Level.toLevel(clientLogLevel));realLogger.setAdditivity(additive);return logger;}    static class AppenderProxy extends Appender {private Appender proxy;@Overrideprotected void append(LoggingEvent event) {if (null == proxy) {proxy = ClientLogger.createClientAppender();}proxy.doAppend(event);}@Overridepublic void close() {if (null != proxy) {proxy.close();}}}

动态代理

代理类在程序运行时创建的代理方式叫动态代理

JDK动态代理

JDK动态代理是基于接口的方式,换句话来说就是代理类和目标类都实现同一个接口,利用jdk自带的包通过目标类实现类创建代理类,具体参考我另一个博客jdk、cglib动态代理代码示例

案例:dubbo中jdk动态代理工厂实现
public class JdkProxyFactory extends AbstractProxyFactory {@Override@SuppressWarnings("unchecked")public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));}@Overridepublic <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {return new AbstractProxyInvoker<T>(proxy, type, url) {@Overrideprotected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) throws Throwable {Method method = proxy.getClass().getMethod(methodName, parameterTypes);return method.invoke(proxy, arguments);}};}
}

其他相关类源码

public interface Node {URL getUrl();boolean isAvailable();void destroy();
}
public interface Invoker<T> extends Node {Class<T> getInterface();Result invoke(Invocation invocation) throws RpcException;
}
public abstract class AbstractProxyInvoker<T> implements Invoker<T> {Logger logger = LoggerFactory.getLogger(AbstractProxyInvoker.class);private final T proxy;private final Class<T> type;private final URL url;public AbstractProxyInvoker(T proxy, Class<T> type, URL url) {if (proxy == null) {throw new IllegalArgumentException("proxy == null");}if (type == null) {throw new IllegalArgumentException("interface == null");}if (!type.isInstance(proxy)) {throw new IllegalArgumentException(proxy.getClass().getName() + " not implement interface " + type);}this.proxy = proxy;this.type = type;this.url = url;}@Overridepublic Class<T> getInterface() {return type;}@Overridepublic URL getUrl() {return url;}@Overridepublic boolean isAvailable() {return true;}@Overridepublic void destroy() {}@Overridepublic Result invoke(Invocation invocation) throws RpcException {try {Object value = doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());CompletableFuture<Object> future = wrapWithFuture(value);CompletableFuture<AppResponse> appResponseFuture = future.handle((obj, t) -> {AppResponse result = new AppResponse();if (t != null) {if (t instanceof CompletionException) {result.setException(t.getCause());} else {result.setException(t);}} else {result.setValue(obj);}return result;});return new AsyncRpcResult(appResponseFuture, invocation);} catch (InvocationTargetException e) {if (RpcContext.getContext().isAsyncStarted() && !RpcContext.getContext().stopAsync()) {logger.error("Provider async started, but got an exception from the original method, cannot write the exception back to consumer because an async result may have returned the new thread.", e);}return AsyncRpcResult.newDefaultAsyncResult(null, e.getTargetException(), invocation);} catch (Throwable e) {throw new RpcException("Failed to invoke remote proxy method " + invocation.getMethodName() + " to " + getUrl() + ", cause: " + e.getMessage(), e);}}private CompletableFuture<Object> wrapWithFuture(Object value) {if (RpcContext.getContext().isAsyncStarted()) {return ((AsyncContextImpl)(RpcContext.getContext().getAsyncContext())).getInternalFuture();} else if (value instanceof CompletableFuture) {return (CompletableFuture<Object>) value;}return CompletableFuture.completedFuture(value);}protected abstract Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable;@Overridepublic String toString() {return getInterface() + " -> " + (getUrl() == null ? " " : getUrl().toString());}}
public class InvokerInvocationHandler implements InvocationHandler {private static final Logger logger = LoggerFactory.getLogger(InvokerInvocationHandler.class);private final Invoker<?> invoker;private ConsumerModel consumerModel;private URL url;private String protocolServiceKey;public InvokerInvocationHandler(Invoker<?> handler) {this.invoker = handler;this.url = invoker.getUrl();String serviceKey = this.url.getServiceKey();this.protocolServiceKey = this.url.getProtocolServiceKey();if (serviceKey != null) {this.consumerModel = ApplicationModel.getConsumerModel(serviceKey);}}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getDeclaringClass() == Object.class) {return method.invoke(invoker, args);}String methodName = method.getName();Class<?>[] parameterTypes = method.getParameterTypes();if (parameterTypes.length == 0) {if ("toString".equals(methodName)) {return invoker.toString();} else if ("$destroy".equals(methodName)) {invoker.destroy();return null;} else if ("hashCode".equals(methodName)) {return invoker.hashCode();}} else if (parameterTypes.length == 1 && "equals".equals(methodName)) {return invoker.equals(args[0]);}RpcInvocation rpcInvocation = new RpcInvocation(method, invoker.getInterface().getName(), protocolServiceKey, args);String serviceKey = invoker.getUrl().getServiceKey();rpcInvocation.setTargetServiceUniqueName(serviceKey);// invoker.getUrl() returns consumer url.RpcContext.setRpcContext(invoker.getUrl());if (consumerModel != null) {rpcInvocation.put(Constants.CONSUMER_MODEL, consumerModel);rpcInvocation.put(Constants.METHOD_MODEL, consumerModel.getMethodModel(method));}return invoker.invoke(rpcInvocation).recreate();}
}
@SPI("javassist")
public interface ProxyFactory {/*** create proxy.** @param invoker* @return proxy*/@Adaptive({PROXY_KEY})<T> T getProxy(Invoker<T> invoker) throws RpcException;/*** create proxy.** @param invoker* @return proxy*/@Adaptive({PROXY_KEY})<T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException;/*** create invoker.** @param <T>* @param proxy* @param type* @param url* @return invoker*/@Adaptive({PROXY_KEY})<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;
public abstract class AbstractProxyFactory implements ProxyFactory {private static final Class<?>[] INTERNAL_INTERFACES = new Class<?>[]{EchoService.class, Destroyable.class};@Overridepublic <T> T getProxy(Invoker<T> invoker) throws RpcException {return getProxy(invoker, false);}@Overridepublic <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {Set<Class<?>> interfaces = new HashSet<>();String config = invoker.getUrl().getParameter(INTERFACES);if (config != null && config.length() > 0) {String[] types = COMMA_SPLIT_PATTERN.split(config);for (String type : types) {// TODO can we load successfully for a different classloader?.interfaces.add(ReflectUtils.forName(type));}}if (generic) {if (!GenericService.class.isAssignableFrom(invoker.getInterface())) {interfaces.add(com.alibaba.dubbo.rpc.service.GenericService.class);}try {// find the real interface from urlString realInterface = invoker.getUrl().getParameter(Constants.INTERFACE);interfaces.add(ReflectUtils.forName(realInterface));} catch (Throwable e) {// ignore}}interfaces.add(invoker.getInterface());interfaces.addAll(Arrays.asList(INTERNAL_INTERFACES));return getProxy(invoker, interfaces.toArray(new Class<?>[0]));}public abstract <T> T getProxy(Invoker<T> invoker, Class<?>[] types);}

CGLIB动态代理

CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法

案例:dubbo中cglib创建代理类
    @Testpublic void testCglibProxy() throws Exception {ITest test = (ITest) Proxy.getProxy(ITest.class).newInstance((proxy, method, args) -> {System.out.println(method.getName());return null;});Enhancer enhancer = new Enhancer();enhancer.setSuperclass(test.getClass());enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> null);try {enhancer.create();} catch (IllegalArgumentException e) {e.printStackTrace();Assertions.fail();}}

javassist动态代码创建

javassist的强大之处在于它操作字节码的能力,可以动态的修改类,加载类,添加删除字段、方法等操作。当然也可以实现动态代理

简单使用案例

public class JavassistProxyFactory extends AbstractProxyFactory {@Override@SuppressWarnings("unchecked")public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));}@Overridepublic <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);return new AbstractProxyInvoker<T>(proxy, type, url) {@Overrideprotected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) throws Throwable {return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);}};}

动态字节码生成

    public static Wrapper getWrapper(Class<?> c) {while (ClassGenerator.isDynamicClass(c)) // can not wrapper on dynamic class.{c = c.getSuperclass();}if (c == Object.class) {return OBJECT_WRAPPER;}return WRAPPER_MAP.computeIfAbsent(c, Wrapper::makeWrapper);}private static Wrapper makeWrapper(Class<?> c) {if (c.isPrimitive()) {throw new IllegalArgumentException("Can not create wrapper for primitive type: " + c);}String name = c.getName();ClassLoader cl = ClassUtils.getClassLoader(c);StringBuilder c1 = new StringBuilder("public void setPropertyValue(Object o, String n, Object v){ ");StringBuilder c2 = new StringBuilder("public Object getPropertyValue(Object o, String n){ ");StringBuilder c3 = new StringBuilder("public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws " + InvocationTargetException.class.getName() + "{ ");c1.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }");c2.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }");c3.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }");Map<String, Class<?>> pts = new HashMap<>(); // <property name, property types>Map<String, Method> ms = new LinkedHashMap<>(); // <method desc, Method instance>List<String> mns = new ArrayList<>(); // method names.List<String> dmns = new ArrayList<>(); // declaring method names.// get all public field.for (Field f : c.getFields()) {String fn = f.getName();Class<?> ft = f.getType();if (Modifier.isStatic(f.getModifiers()) || Modifier.isTransient(f.getModifiers())) {continue;}c1.append(" if( $2.equals(\"").append(fn).append("\") ){ w.").append(fn).append("=").append(arg(ft, "$3")).append("; return; }");c2.append(" if( $2.equals(\"").append(fn).append("\") ){ return ($w)w.").append(fn).append("; }");pts.put(fn, ft);}Method[] methods = c.getMethods();// get all public method.boolean hasMethod = hasMethods(methods);if (hasMethod) {c3.append(" try{");for (Method m : methods) {//ignore Object's method.if (m.getDeclaringClass() == Object.class) {continue;}String mn = m.getName();c3.append(" if( \"").append(mn).append("\".equals( $2 ) ");int len = m.getParameterTypes().length;c3.append(" && ").append(" $3.length == ").append(len);boolean overload = false;for (Method m2 : methods) {if (m != m2 && m.getName().equals(m2.getName())) {overload = true;break;}}if (overload) {if (len > 0) {for (int l = 0; l < len; l++) {c3.append(" && ").append(" $3[").append(l).append("].getName().equals(\"").append(m.getParameterTypes()[l].getName()).append("\")");}}}c3.append(" ) { ");if (m.getReturnType() == Void.TYPE) {c3.append(" w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");").append(" return null;");} else {c3.append(" return ($w)w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");");}c3.append(" }");mns.add(mn);if (m.getDeclaringClass() == c) {dmns.add(mn);}ms.put(ReflectUtils.getDesc(m), m);}c3.append(" } catch(Throwable e) { ");c3.append("     throw new java.lang.reflect.InvocationTargetException(e); ");c3.append(" }");}c3.append(" throw new " + NoSuchMethodException.class.getName() + "(\"Not found method \\\"\"+$2+\"\\\" in class " + c.getName() + ".\"); }");// deal with get/set method.Matcher matcher;for (Map.Entry<String, Method> entry : ms.entrySet()) {String md = entry.getKey();Method method = entry.getValue();if ((matcher = ReflectUtils.GETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) {String pn = propertyName(matcher.group(1));c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }");pts.put(pn, method.getReturnType());} else if ((matcher = ReflectUtils.IS_HAS_CAN_METHOD_DESC_PATTERN.matcher(md)).matches()) {String pn = propertyName(matcher.group(1));c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }");pts.put(pn, method.getReturnType());} else if ((matcher = ReflectUtils.SETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) {Class<?> pt = method.getParameterTypes()[0];String pn = propertyName(matcher.group(1));c1.append(" if( $2.equals(\"").append(pn).append("\") ){ w.").append(method.getName()).append("(").append(arg(pt, "$3")).append("); return; }");pts.put(pn, pt);}}c1.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" field or setter method in class " + c.getName() + ".\"); }");c2.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" field or getter method in class " + c.getName() + ".\"); }");// make classlong id = WRAPPER_CLASS_COUNTER.getAndIncrement();ClassGenerator cc = ClassGenerator.newInstance(cl);cc.setClassName((Modifier.isPublic(c.getModifiers()) ? Wrapper.class.getName() : c.getName() + "$sw") + id);cc.setSuperClass(Wrapper.class);cc.addDefaultConstructor();cc.addField("public static String[] pns;"); // property name array.cc.addField("public static " + Map.class.getName() + " pts;"); // property type map.cc.addField("public static String[] mns;"); // all method name array.cc.addField("public static String[] dmns;"); // declared method name array.for (int i = 0, len = ms.size(); i < len; i++) {cc.addField("public static Class[] mts" + i + ";");}cc.addMethod("public String[] getPropertyNames(){ return pns; }");cc.addMethod("public boolean hasProperty(String n){ return pts.containsKey($1); }");cc.addMethod("public Class getPropertyType(String n){ return (Class)pts.get($1); }");cc.addMethod("public String[] getMethodNames(){ return mns; }");cc.addMethod("public String[] getDeclaredMethodNames(){ return dmns; }");cc.addMethod(c1.toString());cc.addMethod(c2.toString());cc.addMethod(c3.toString());try {Class<?> wc = cc.toClass();// setup static field.wc.getField("pts").set(null, pts);wc.getField("pns").set(null, pts.keySet().toArray(new String[0]));wc.getField("mns").set(null, mns.toArray(new String[0]));wc.getField("dmns").set(null, dmns.toArray(new String[0]));int ix = 0;for (Method m : ms.values()) {wc.getField("mts" + ix++).set(null, m.getParameterTypes());}return (Wrapper) wc.newInstance();} catch (RuntimeException e) {throw e;} catch (Throwable e) {throw new RuntimeException(e.getMessage(), e);} finally {cc.release();ms.clear();mns.clear();dmns.clear();}}

优秀文章:菜鸟教程

从框架源码中学习结构型设计模式相关推荐

  1. 从框架源码中学习创建型设计模式

    文章目录 从框架源码中解读创建型设计模式 工厂模式 案例一:RocketMQ源码-创建Producer生产者 案例二:RocketMQ源码-创建过滤器工厂 抽象工厂 案例一:Dubbo源码-创建缓存的 ...

  2. rust墙壁升级点什么_分享:如何在阅读Rust项目源码中学习

    今天做了一个Substrate相关的小分享,公开出来. 因为我平时也比较忙,昨天才选定了本次分享的主题,准备比较仓促,细节可能不是很充足,但分享的目的也是给大家提供一个学习的思路,更多的细节大家可以在 ...

  3. MVVM架构~knockoutjs系列之从Knockout.Validation.js源码中学习它的用法

    说在前 有时,我们在使用一个插件时,在网上即找不到它的相关API,这时,我们会很抓狂的,与其抓狂,还不如踏下心来,分析一下它的源码,事实上,对于JS这种开发语言来说,它开发的插件的使用方法都在它的源码 ...

  4. 「从源码中学习」面试官都不知道的Vue题目答案

    前言 当回答面试官问及的Vue问题,我们除了照本宣科的回答外,其实还可以根据少量的源码来秀一把,来体现出你对Vue的深度了解. 本文会陆续更新,此次涉及以下问题: "new Vue()做了什 ...

  5. MyBatis学习总结(26)——Mybatis源码中使用了哪些设计模式?

    分享一个大神的人工智能教程.零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到人工智能的队伍中来!点击浏览教程 虽然我们都知道有26个设计模式,但是大多停留在概念层面,真实开发中很少遇到,Myba ...

  6. android源码使用方法,android源码中使用到的设计模式(创建型)

    1.单例模式 1.1定义 确保某个类只有一个实例,而且自行实例化并向整个系统提供者个实例. 1.2单例的形式 饿汉模式:第一次就加载,用空间换时间. public class SingleTon { ...

  7. 手把手带你撸一把springsecurity框架源码中的认证流程

    提springsecurity之前,不得不说一下另外一个轻量级的安全框架Shiro,在springboot未出世之前,Shiro可谓是颇有统一J2EE的安全领域的趋势. 有关shiro的技术点 1.s ...

  8. 从shiro源码角度学习工厂方法设计模式

    绪论 shiro是一个简单易用,功能强大的Java安全框架,学习其源码设计思想对我们的编码水平的提高大有裨益.现在,就从源码角度带大家学习一下shiro里面的工厂方法模式. 这里的前提是读者有过使用s ...

  9. glide源码中包含了那种设计模式_腾讯阿里华为小米等大厂Android高端面试题145题(含部分详解)-Go语言中文社区...

    前言 本篇是结合我之前面试别人的经验,以及跟一些在大厂待过的朋友,讨论总结出的一份很全面的大公司需要用到的一些高端Android技术.这里也专门整理了一个文档,重点和难点都有详细解析. 这些题目有点技 ...

最新文章

  1. OpenCV-Python:K值聚类
  2. 【Android 逆向】类加载器 ClassLoader ( 启动类加载器 | 扩展类加载器 | 应用类加载器 | 类加载的双亲委托机制 )
  3. IT人士,你的知识需要管理。
  4. InternetOpen InternetOpenUrl InternetReadFile 和 InternetCloseHandle
  5. 点对点信道互连以太网实验_以太网防雷器通讯参数测试(二)——防雷器对高速链路影响的参数...
  6. 转:关掉Archlinux中烦人的响铃
  7. 物联网项目--基于RFID的智能咖啡馆(Visual Studio+物联网虚拟仿真实验平台)
  8. NameError: name “ ” is not defined
  9. 【深度学习】人物图片标签生成
  10. LCD1602开始--麒麟座OK
  11. 浏览器+js实现超强的搜索访问功能
  12. LeetCode-1873. 计算特殊奖金
  13. pq分解法潮流计算的matlab,第四节PQ分解法潮流计算
  14. Fastq文件大小和测序覆盖度初探
  15. 罗技M545鼠标是不是垃圾鼠标中的战斗机?
  16. php 子都接受邮件,php iamp 接收邮件,收取邮件,获取邮件列表
  17. [MicroPython]STM32F407开发板DIY声光控开关
  18. 普元王文斌:微服务架构开发模式需要全栈团队
  19. Java 开发日常归纳
  20. 【网站建设大全】网站建设动态网站和静态网站的区别

热门文章

  1. ssm后台数据是为什么是空值_网易后台开发实习生面试总结
  2. 2020 idea 查看内存消耗_idea内存如何设置
  3. java编程中的持有对方引用是什么意思?有什么作用?
  4. seo自动发外链_一套节约成本全网营销方案-小小课堂SEO培训教程
  5. qt的如何调整显示不为科学记数法_Excel操作技巧:如何将信息快速准确的录入Excel?...
  6. 计算机网络中的数据链路层
  7. C++ STL 容器的一些总结 --- set(multiset)和map(multimap)
  8. C++ STL 容器 string
  9. java 第二天,Java复习第二天
  10. centos7 mysql 安装_CentOS7安装MySQL8.0图文教程(相对最齐全)