大家好,我是入错行的bug猫。(http://blog.csdn.net/qq_41399429,谢绝转载)
今天来学习Java的设计模式。网上介绍设计模式太多了,随便搜索一下,不下3页都是一模二样介绍23种设计模式。
设计模式,详情参见 https://www.runoob.com/design-pattern/design-pattern-tutorial.html
关于里氏替换
简单点说,就是父类出现的地方,子类一定可以将其替换掉。这点很重要,Java 的多态就是这个原理。
单例(Singleton)模式:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
基本上面试都必考,没有多大难点。
记住,static代码块
、static属性
、static类
,在加载时都是线程安全的。枚举也是特殊的静态类、静态属性,在其构造器执行的时候,也是单线程。
比较好的实现是采用静态内部类
加载。当且仅当静态内部类加载的时候,才会初始化实例,属于线程安全,并且仅会加载一次。
类的加载具有惰性,只有在使用的时候才会加载到内存中。因此,即使外部类有很多的实例,但是只要没有使用到内部类,那么内部类便不会被加载,其static代码块也不会执行!
不使用双重检测锁原因据说是:虚拟机在编译代码的时候,会优化重排序,也许会把创建实例的代码,从双重检测中移到其他地方。
举个栗子
package cc.bugcat.csdn23.single;
public class Singleton {
static {
System.out.println("在Singleton加载时执行");
}
// 私有构造器,其他类无法直接使用new Singleton()创建对象
private Singleton(){
System.out.println("单例初始化");
}
// 静态内部类
private static class Inner {
static {
System.out.println("在Inner加载时执行");
}
//因为是在Singleton的内部,所以可以执行Singleton的私有构造器
private static final Singleton SINGLETON = new Singleton();
}
/**
* 当Singleton类被加载的时候,Inner类没有加载,所以实例SINGLETON是null
* 仅当第一次执行getInstance方法时,Inner类才会加载,并且实例SINGLETON才会被初始化
* 后续继续调getInstance方法,直接返回实例SINGLETON
* */
public static Singleton getInstance(){
return Inner.SINGLETON;
}
}
public class SingletonTest {
/**
* 验证加载顺序
* */
@Test
public void single() throws Exception {
Class.forName("cc.bugcat.csdn23.single.Singleton"); //打印:在Singleton加载时执行
Singleton.getInstance(); //打印:在Inner加载时执行 \n 单例初始化
Singleton.getInstance(); //
Class.forName("cc.bugcat.csdn23.single.Singleton"); //
Singleton.getInstance(); //
}
}
扩展:有限个单例,对象池。
原型(Prototype)模式:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
比较容易混淆的是:深拷贝与浅拷贝,
深拷贝:我以同桌的暑假作业为原型,把内容复制到我的暑假作业上;此时有2本暑假作业。
浅拷贝:我把同桌的暑假作业上的署名,改成我的名字;此时仍然只有1本暑假作业。(同桌?什么同桌,我不认识此人)
引申到2个对象,同名属性互相赋值的问题:
推荐使用org.springframework.cglib.beans.BeanCopier
;
BeanCopier在初次执行的时候,会通过操作字节码(cglib),动态生成一个转换类,并加载到内存中,此过程会耗时较多;后续使用就和调用普通类的getter、setter效率一样;
如果遇到需要大量调用2个对象互相赋值的场景,需要把生成的转换类缓存起来,以提高新能;
我立下flag了,cglib马上补上,要不然我就把暑假作业给撕了
简单工厂、静态工厂、抽象工厂
都是把创建对象的代码,从业务逻辑代码中移走了,统一放在一个地方创建。
工厂类
,再在执行工厂类
的创建对象
这方法。其中,「执行工厂类
的创建对象
这方法」 这部分代码不用改动,传入不同的工厂类
,就可实现创建不同的对象。将创建对象操作,移花接木转化成创建工厂类了;返回一个对象
!至于这个对象是从缓存中取、还是对象池取、还是创建出来的,工厂不会有限制!特别适用于有限个单例(对象池)、二级缓存的地方;适用场景:
Spring自动注入组件,就是通过FactoryBean对象工厂实现的。
说到spring的FactoryBean,又是一堆技术栈。其中自定义扫描组件,又是用得比较多的地方;
写个呆毛
/**
* 在线支付
* */
public interface OnlinePay{
void init();
}
public class AliPay implements OnlinePay {
@Override
public void init() {
System.out.println("支付宝");
}
}
public class WeiXinPay implements OnlinePay {
@Override
public void init() {
System.out.println("微信");
}
}
public class UnionPay implements OnlinePay {
@Override
public void init() {
System.out.println("银联");
}
}
/**
* 支付方式枚举
* */
public enum PayType {
aliPay(AliPay::new),
weiXinPay(WeiXinPay::new),
unionPay(UnionPay::new);
private final OnlinePay onlinePay;
PayType(Supplier<OnlinePay> supplier) {
this.onlinePay = supplier.get();
}
private static final Map<String, PayType> enumMap = Arrays.stream(PayType.values()).collect(Collectors.toMap(en -> en.name(), Function.identity()));
public static OnlinePay getPayType(String payTypeName){
PayType payType = enumMap.get(payTypeName);
return payType.onlinePay;
}
}
public class FactoryTest {
@Test
public void factory(){
OnlinePay onlinePay = PayType.getPayType("aliPay");
onlinePay.init(); //打印:支付宝
}
}
通过工厂模式,在不用修改主方法的情况下,只修改PayType
类。根据枚举值对应的子类不同,实现了对主方法的扩展。
建造者(Builder)模式:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即对象的组成部分是不变的,但每一部分是可以灵活选择的。
使用场景: 现在有一个对象:
那么可以考虑使用建造者模式。
奖个案例
//1. 我们现在要封装一个http请求:传入请求url,和请求入参,得到响应字符串
public static String doPost(String url, Map<String, Object> params);
//2. 不行,要加上请求超时限制
public static String doPost(String url, Map<String, Object> params, int... timeouts);
//3. 好像不能自定义请求头,再重载一个
public static String doPost(String url, Map<String, Object> headers, Map<String, Object> params, int... timeouts);
//4. 好像还不能自定义字符集,我就要用GBK
public static String doPost(String url, Map<String, Object> headers, Map<String, Object> params, String charset, int... ints);
//噫,还不能使用http代理?我要用梯子!
//等等,好像使用https请求的时候,不能忽略有问题的证书?
//还有我需要获取响应的cookie啊、你这工具类不好用,呸,差评!
//使用建造者
String result = HttpUtil.creart() //创建一个建造者
.charset("GBK") //修改字符编码
.timeOut(3000, 2000) //修改http链接读取超时时间
.headers("token", "cc.bugcat") //添加请求头
.headers("Accept-Encoding", "gzip, deflate") //添加请求头
.proxyHost("http://127.0.0.1:880", "user", "pwd") //使用http代理
.ignoreSSL() //忽略https证书
.httpResponse(resp -> resp.getFirstHeader("Set-Cookie")) //处理响应Response对象
.doPost() //生成Post对象。从22行到28行,都是Http的可配置项。执行配置项方法,则修改建造者对应的配置参数,否则为建造者内置的默认配置。
//执行doPost方法,使用建造者的配置构建一个Post对象
.send(url, params); //执行Post对象的send方法,发送http请求
这不就是Jquery的链式写法吗?
再举个小栗子, Java代码规范,变量一般都使用小驼峰。但是总有一些API接口的参数,使用恶心的下划线,还要将参数按字典排序。
好好的面相对象,搞成了面相Map开发。这个时候可以借用这种设计模式:
public class MapVi {
private Map<String, Object> param;
private MapVi(Map<String, Object> param){
param.put("sign_type", "MD5");
this.param = param;
}
public static MapVi builder(){
return new MapVi(new TreeMap<>());
}
public static MapVi builder(Map<String, Object> param){
return new MapVi(param);
}
public MapVi appId(String appId) {
param.put("app_id", appId);
return this;
}
public MapVi mchId(String mchId) {
param.put("mch_id", mchId);
return this;
}
public Map<String, Object> toParamMap(){
return param;
}
}
public static void main(String[] args) throws Exception {
Map<String, Object> param = MapVi.builder().appId("bug").mchId("cat").toParamMap();
}
这种链式写法真的很爽,不明白为啥一般JavaBean的set方法,都没有返回值
在此之前,先提个问题:
类A继承类B,类A实现了接口甲,接口甲继承了接口乙,请问,类B和接口乙,有没有关系?
肯定没有关系!但是,神奇的是,在接口乙
中加了一个抽象方法,在类B
中却可以 “实现” 它。(注意,实现使用了引号)
嗯嗯,等各位理清楚原因之后,类适配器模式就知道干什么的了。(鹅不是)
现在我们要开发一个新功能。然后发现以前的屎山中有部分功能可以拿来复用的,但是又不能完全适用。
旧代码:类A中有方法a、b。
新需求,新类中有方法一、二、三。
方法一和方法a功能完全一样,于是我们表示直接借用;
方法二和方法b功能一样,但是艹蛋的是方法b的入参和响应,都是下划线,需要把方法二的输入输出,转换成下划线再调用方法b;
方法三就是全新的,需要重新开发;
我们抽象一下,如下示意:
类A {
方法a(){...};
方法b(下划线入参){...};
}
// 类适配
类壹 extend 类A {
方法一(){
super.方法a();
}
方法二(驼峰入参){
下划线入参 = 转换驼峰入参为下划线;
下划线响应 = super.方法b(下划线入参);
return 转换下划线响应为驼峰;
}
方法三(){
新方法
}
}
类适配器模式,需要适配类(类壹)继承被适配类(类A)。如果适配类(类壹)本身就已经有继承关系
,就需要使用到对象适配器。
把类A的实例α,赋值给类壹的属性。然后在类壹中执行实例α的方法,代替执行super的方法。
// 对象适配
类壹 extend 类叉叉 {
类A 实例α;
构造器(类A 实例α){ //被适配类,也可以采用set方法赋值
this.实例α = 实例α;
}
方法一(){
实例α.方法a();
}
方法二(驼峰入参){
下划线入参 = 转换驼峰入参为下划线;
下划线响应 = 实例α.方法b(下划线入参);
return 转换下划线响应为驼峰;
}
方法三(){
新方法
}
}
类适配器,主要是通过继承关系,静态绑定到一起。
而对象适配器中,是把super
变成了实例α,是通过借调
实例α的方法实现适配。
比较二者可以发现对象适配有这几个优点:
1.对象适配器中,实例α还可以是类A的子类;
2.不要求类壹与类A是否有联系;
3.适配类中不会出现被适配类的其他方法。(类适配器示例中,类壹中同样包含了方法a、方法b)
当然类适配器也不是一文不值。类适配器有个巨大的优势是,被适配类实例出现的地方,适配类的实例都可以出现 (毕竟是父子关系)。
这种设计模式,出境应该非常高!
核心思想是,使用一个抽象类,将接口与具体子类隔离开。
假设没有抽象类,如果在接口中,添加一个新的抽象方法,那么所有的子类就算用不到,也都要实现它;
反之,如果有这个层抽象类,那么可以在抽象类中,实现一个空方法(缺省方法)。
需要用到它的子类,重写这个方法就行,其他子类也不会报错。
接口甲{
方法a();
方法b();
方法c();
方法d();
}
抽象类乙 implements 接口甲{
方法a(){};
方法b(){};
方法c(){};
方法d(){};
}
类A extend 抽象类乙 {
方法a(){...};
方法b(){...};
}
类贰 extend 抽象类乙 {
方法a(){...};
方法c(){...};
}
如果接口甲中再新增一个方法e,仅需在抽象类乙中对方法e空实现即可。否则类A、类贰、其他子类等就会报错。
但是Java8及以上版本,出现了接口默认方法功能,致使这种设计模式优势就变弱了,但是默认适配这种思想长存!
看见我这Jio没?就是专门为这鞋长的
装饰器(Decorator)模式:指在不改变现有对象结构的情况下,动态地给该对象增加一些其额外功能。
核心思想:装饰类,与被装饰类,必须要实现相同的Interface。装饰类持有被装饰实例,是通过调用者,显示传入。
各种InputStream、OutputStream、Connection类,都是使用装饰模式。
装饰模式,和对象适配有点类似,都是对已经存在的实例进行增强。只不过被装饰类和装饰类,需要实现同一个接口。
对象适配模式,没有要求需要实现相同接口。被适配类出现的地方,无法使用新的适配后实例替换。
而装饰模式,由于装饰类与被装饰类,实现了同一个Interface,对象结构不变,所以可以使用里氏替换。
一个实际例子:
系统中有Jdbc连接泄露,如何快速定位代码?
Jdbc连接本来是由连接池来管理。连接泄露,意味Jdbc连接从连接池中离开之后,没有归还到连接池中。
正常情况下,连接在回滚事务、提交事务、或关闭的时候,归还到连接池中。
因此我们应该监控,长时间没有回滚、提交事务、或关闭的连接。
以 hikari
为例,Jdbc连接Connection,由HikariDataSource
创建。
而HikariDataSource
在配置文件中指定,我们可以创建其一个子类CheckDataSource
,在配置文件中指定数据源为增强后的子类CheckDataSource
。
观察HikariDataSource.getConnection方法返回的是一个Connection
,为Interface,具体的实现类未知。
/**
* HikariDataSource源码,{@link com.zaxxer.hikari.HikariDataSource#getConnection()}
* HikariDataSource返回了一个Connection的实现类,具体是哪个实现类未知
* */
@Override
public Connection getConnection() throws SQLException {
if (isClosed()) {
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
HikariPool result = pool;
if (result == null) {
synchronized (this) {
result = pool;
if (result == null) {
validate();
try {
pool = result = new HikariPool(this);
this.seal();
} catch (PoolInitializationException pie) {
if (pie.getCause() instanceof SQLException) {
throw (SQLException) pie.getCause();
} else {
throw pie;
}
}
}
}
}
return result.getConnection();
}
先对Connection增强:
由于getConnection
返回的是个Interface的实例,其具体的数据类型未知。
但是这个实例有哪方法是知道的,因此可以采用对象适配器设计思想,借调实例的方法,实现缺省配置:(对象适配器,类壹.方法一)
//对原Connection的实例增强,添加一些无关紧要的新功能
public class CheckConnection implements Connection {
//保存活跃的Connection
private static Map<String, CheckConnection> connMap = new ConcurrentHashMap<>(300);
private String id;
private long activeTime; //最后活跃时间
private String lastSql; //最后执行的sql语句
private Throwable ex; //调用者的堆栈信息
private Connection conn; //原始的Connection对象。至于具体是哪个子类,我也不晓得
public CheckConnection(Connection conn) {
this.conn = conn;
this.id = UUID.randomUUID().toString();
this.activeTime = System.currentTimeMillis();
this.ex = new Throwable(); //创建连接时的调用堆栈信息,可以快速定位到何处开启了连接
connMap.put(this.id, this);
}
public static Map<String, CheckConnection> getConnMap() {
return connMap;
}
public String getLastSql() {
return lastSql;
}
public long getActiveTime() {
return activeTime;
}
public Throwable getEx() {
return ex;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
this.lastSql = sql; //替换最后执行sql语句
return conn.prepareStatement(sql); //转身就把具体实现交给conn了
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
this.lastSql = sql; //替换最后执行sql语句
return conn.prepareCall(sql);
}
@Override
public String nativeSQL(String sql) throws SQLException {
this.lastSql = sql; //替换最后执行sql语句
return conn.nativeSQL(sql);
}
@Override
public void commit() throws SQLException {
conn.commit(); //当执行提交后,记录最后活跃时间
this.activeTime = System.currentTimeMillis();
}
@Override
public void rollback() throws SQLException {
conn.rollback(); //当执行回滚后,记录最后活跃时间
this.activeTime = System.currentTimeMillis();
}
@Override
public void close() throws SQLException {
conn.close(); //当链接关闭时,从监控中移除
connMap.remove(this.id);
}
// 其他方法,直接调用conn的同名方法
...
...
}
我们知道DataSource
的具体实现类,并且可以在初始化实例时,可以指定到具体的某个子类,所以采用类继承方式增强:
/**
* 对HikariDataSource采用类继承方式增强,在初始化时需要指定数据源为CheckDataSource
* */
public class CheckDataSource extends HikariDataSource {
private static Logger log = LoggerFactory.getLogger(HikariDataSource.class);
private static final long MAX_TIME = 1200000;
public CheckDataSource(HikariConfig configuration) {
super(configuration);
init();
}
@Override
public Connection getConnection() throws SQLException {
Connection conn = super.getConnection(); //调用父类方法,返回原始的实例
Connection checkConn = new CheckConnection(conn); //使用装饰,返回增强后的Connection
return checkConn;
}
/**
* 每隔20检查一次未关闭的链接
* */
public void init(){
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
Map<String, CheckConnection> connMap = CheckConnection.getConnMap();
long now = System.currentTimeMillis();
Iterator<CheckConnection> iter = connMap.values().iterator();
while ( iter.hasNext() ) {
CheckConnection conn = iter.next();
if( conn.getActiveTime() + MAX_TIME < now ){ //超过20分钟没有提交事务、回滚事务
log.error(">>未释放的jdbc链接:lastSql=" + conn.getLastSql(), conn.getEx());
}
}
}
}, MAX_TIME, MAX_TIME);
}
// 注意,不同于CheckConnection,以下没有其他方法了。
}
如果有Connection
创建后超过20分钟没有执行close
方法,那么在遍历connMap
的时候,就可以得到CheckConnection
对象,再打印堆栈信息和lastSql
就定位到具体代码了。
我们再看 druid
的DruidDataSource
:
/**
* com.alibaba.druid.pool.DruidDataSource#getConnection()
* */
@Override
public DruidPooledConnection getConnection() throws SQLException {
return getConnection(maxWait);
}
DruidDataSource
返回的是一个具体的DruidPooledConnection
对象。无法直接使用上述模式进行增强Connection
。
可以先采用装饰模式处理DruidDataSource
,修改getConnection
的返回数据类型 ,使其直接返回Connection
,再通过上述模式装饰Connection
增强:
/**
* 需要指定数据源为CheckDataSource
* */
public class CheckDataSource implements DataSource {
private DruidDataSource dataSource; //其实使用DataSource申明比较好,此处为了展示DruidDataSource例子
public CheckDataSource(DruidDataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Connection getConnection() throws SQLException {
DruidPooledConnection conn = dataSource.getConnection();
CheckConnection checkConn = new CheckConnection(conn); //使用装饰,返回增强后的Connection
return checkConn;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return dataSource.getConnection(username, password);
}
// 其他方法,直接调用dataSource的同名方法
...
...
}
像极了小时候就算是父母戴上手套也能在我们的屁股上留下五道杠
总结一下继承、类适配、对象适配、装饰模式:
1. 继承是静态的,只能增强某个具体的类;增强一旦生效,不可以取消;新类包含了父类所有的特性;并且在实例化对象时,必须指定为子类;
2. 类适配使用继承实现的,其特性与继承一致;
3. 对象适配,针对某个接口、抽象类、或者具体类已存在的实例,可以在运行过程中,有选择的是否需要增强;适配前、适配后的2个实例,可以没有任何关系,也可以有关系;适配前的方法数量,和适配后的方法数量,没有强制要求;
4. 装饰模式,针对某类型实例,实例必须与装饰类实现相同的Interface;可以在运行过程中,有选择的选择是否需要增强;装饰后的类特性与装饰前的类一致,可以使用里氏替换;
代理(Proxy)模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
比喻我们无法直接访问境外某个网站,但是可以采用代理服务器解决这个问题:我们电脑访问代理服务器,代理服务器再访问真实目标网站,再把网站返回结果,返回给我们电脑。
代理分为静态代理,和动态代理。动态代理又因实现原理不同,分为JDK动态代理,和cglib动态代理。
其中,JDK动态代理和装饰模式非常相似。同样要求,代理类与被代理类,实现相同的接口。
其区别大概在于:装饰模式,被装饰对象需要手动传入,调用者知道被装饰对象是谁;代理模式,不知道被代理的对象是谁;无需调用者传入。
代理模式的好处是,可以在执行被代理类的方法前后,执行统一的代码。
面相切面,就是基于动态代理技术实现的。
JDK代理,底层使用反射实现,效率比较低;
Java7及以上版本对反射进行了优化,并提供MethodHandle方式调用
// 定义个接口
public interface IMap {
String username();
String phone();
Set<String> opers();
}
public class ProxyTools {
public static Map<Class, Map<String, Object>> dbMap = new HashMap<>();
public static <T> T create(Class<T> inter){
InvocationHandler handler = new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //被代理类,每次执行任意方法时,都会执行此代码块
String methodName = method.getName();
Class classKey = method.getDeclaringClass(); //com.yjtravel.proxy.IMap
Map<String, Object> map = dbMap.get(classKey);
if( map == null ){
map = new HashMap<>();
dbMap.put(classKey, map);
}
// 反射调用。obj为被代理对象,可以通过构造器、set方法传给InvocationHandler对象。在执行此方法的前后,都可以执行自定义代码。
// 此处不能传proxy,否则会出现递归调用。对比cglib动态代理的Method、MethodProxy
// Object value = method.invoke(obj, args);
System.out.println("执行了" + methodName + "方法");
return map.get(methodName);
}
};
return (T) Proxy.newProxyInstance(inter.getClassLoader(), new Class[] { inter }, handler);
}
}
// 测试类
public class ProxyTest{
static { //准备一些测试数据
Map<String, Object> map = new HashMap<>();
Set<String> opers = new HashSet<>();
opers.add("1");
opers.add("2");
opers.add("3");
map.put("opers", opers);
map.put("username", "bugcat");
map.put("phone", "11234567890");
ProxyTools.dbMap.put(IMap.class, map);
}
@Test
public void proxy() {
IMap proxy = ProxyTools.create(IMap.class); //实际上并没有IMap的实现类
System.out.println("username=" + proxy.username()); //打印:执行了username方法 \n username=bugcat
System.out.println("phone=" + proxy.phone()); //打印:执行了phone方法 \n phone=11234567890
}
}
其实这就是Mybatis框架扒掉衣服后的模样
有一些框架如Mybatis
、FeignClient
、获取注解值
等,只看到有Interface类,却怎么都找不到其实现类,但是它仍然能正常运行,基本上都是使用动态代理实现的。
cglib代理,采用操作字节码动态生成代理类,继承被代理类。无需强制要求实现相同接口。底层调用,与原生的方法自己相互调用一样。
类A{
响应甲 方法一(入参子, 入参丑, 入参寅, 入参卯){...}
}
// cglib动态代理。动态生成一个类A的子类,大概是这个亚子
动态代理类 extend 类A{
private 响应甲 方法一(入参[] 入参数组){
切面before(super, 入参数组)
响应甲 = super.方法一(入参数组[0], 入参数组[1], 入参数组[2], 入参数组[3]);
切面after(super, 入参数组)
return 响应甲
}
final 响应甲 方法一(入参子, 入参丑, 入参寅, 入参卯){
return this.方法壹(new 入参[]{入参子, 入参丑, 入参寅, 入参卯});
}
}
当然这是最简单模式。如果切面需要环绕切面,需要再改进:
public interface IMap {
String username();
Set<String> opers();
String phone();
}
//这次使用具体的实现类
public class IMapImpl implements IMap {
@Override
public String username() {
return "bug猫";
}
@Override
public Set<String> opers() {
return null;
}
@Override
public String phone() {
return "11234567890";
}
}
//拦截器、AOP切面
public interface ProxyJoinIntercept {
Object executeInternal(ProxyJoinPoint point) throws Exception;
}
//切入点
public class ProxyJoinPoint {
private final Object target;
private final Method method;
private final Object[] args;
public ProxyJoinPoint(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object proceed() throws Exception {
System.out.println("开始执行" + method.getName());
return method.invoke(target, args); //如果cglib代理,则为MethodProxy.invokeSuper
}
}
// 创建一个切面,实现ProxyJoinIntercept
public class ProxyLoggerJoinIntercept implements ProxyJoinIntercept {
@Override
public Object executeInternal(ProxyJoinPoint point) throws Exception {
Object result = null;
try {
System.out.println("切面之前执行");
result = point.proceed(); //执行切入点方法
System.out.println("切面成功之后执行");
} catch ( Exception ex ) {
System.out.println("切面异常之后执行");
throw ex;
} finally {
System.out.println("切面之后执行");
}
return result;
}
}
public class ProxyLoggerJoinInterceptTest {
@Test
public void proxyAop(){
ProxyJoinIntercept intercept = new ProxyLoggerJoinIntercept(); //自定义的拦截器对象
IMap iMap = create(IMap.class, intercept);
String name = iMap.username();
System.out.println("name=" + name); //打印:切面之前执行 \n 开始执行username \n 切面成功之后执行 \n 切面之后执行 \n name=bug猫
}
public static <T> T create(Class<T> inter, ProxyJoinIntercept intercept){
IMap imap = new IMapImpl(); //被代理类对象
InvocationHandler handler = new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
ProxyJoinPoint point = new ProxyJoinPoint(imap, method, args); //切面与拦截器
return intercept.executeInternal(point); //执行拦截器方法
}
};
return (T) Proxy.newProxyInstance(inter.getClassLoader(), new Class[] { inter }, handler);
}
}
point.proceed()
是不是有辣味了?Σ(゚д゚;)
Spring使用注解声明的切面,也是对ProxyJoinIntercept做些封装修改。
装饰模式与JDK动态代理,都是要求实现同一个Interface,但是还是有细微差别:
1. 装饰模式,被装饰的类,是显式传入到装饰内中,调用者知道被装饰对象是谁;动态代理则相反,调用者与被代理类是隔离开的,调用者甚至无法获取到原始被代理类;
2. 装饰模式,是对被装饰类的每个方法,都可以使用不同的方式增强;动态代理是对被代理类的每个方法,都执行相同的代码块增强;
Cglib动态代理与JDK动态代理
1. cglib动态代理更加灵活,被代理类是Interface、抽象类、具体类都可以;但是,类、方法,不能被final修饰,因为其底层原理是通过继承
实现增强;被cglib代理后的代理类,无法被再次代理!因为动态生成的代理类,被final修饰;执行效率高;
2. Jdk动态代理,必须要求实现某个Interface,或者是Interfere本身;其底层原理更像是装饰模式,只不过使用反射执行被装饰类的方法;被Jdk动态代理后的代理类,可以被其他Jdk代理类继续代理;底层采用反射调用,性能依赖于Jdk本身;
桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
有的业务场景,是有多个维度的,其处理分支或者结果,是所有的维度乘积(笛卡尔积),比喻:形状有圆形、三角形、方形;颜色有白色、黑色;那么一共有3x2=6种情况(白色圆形、白色三角形、白色方形、黑色圆形、黑色三角形、黑色方形)
如果不做好设计,复杂度呈指数增加。
实际开发过程中,接触和用到比较多的数据库链接,就用到桥接模式;
例如:
链接数据库,需要对应数据库的驱动、和链接管理方式,
驱动有Mysql、Oracle、Redis等多种,是基于数据库开发的包含登录、指令、通讯协议等;
而连接池技术,有Druid、Hikari、commonPool等,各友商通过技术手段实现的一种链接管理工具;
假设没有交接模式,那么连接Mysql的Jar包应该有Mysql-Druid、Mysql-Hikari;同理也会出现Oracle-Druid、Oracle-Hikari;Redis-Druid、Redis-Hikari;那么最终数量有6种。
如果再加一种数据库、或者新增一种连接池技术,Jar包数据量呈倍数增长。
使用桥接技术,每一级模块只做差异业务,多级模块之间可以自由组合:
我可以使用mysql数据库驱动,选择Hikari管理链接池;也可以使用commonPool管理mongoDB数据库链接;
外观模式(Facade Pattern):隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。
这个模式很奇怪,不想单例模式,没有固定的套路;其核心思想是:如果要实现某一个功能点非常复杂,可以将与之相关的功能全部封装,对外提供统一的入口,至于如何实现功能,调用者不需要关心。
比喻:和某个系统通过API对接,我无需知道API结果是通过如何牛叉的技术、如何艰难地获取到的,我不管,我就要结果,给我结果就行。
老师让班长到班级内,收集学生对于老师拖堂的意见,班长此时就是充当外观模式。(这还用问?问就是沙包那么大)
就是Java 类的封装、单一职能的换一种说法
有时候多个对象互相持有对方引用,相互调用。属于多对多的关系,会增加系统的耦合度。
中介者模式是,找一个中间类,使类与类之间的多对多关系,变成类与中间类的一对一形式,减少互相持有、交叉调用。
例如:A持有B实例,在A类中执行B的方法;B类也持有A实例,B类方法也会调用A的方法;甚至还出现C类调用;三者的关系理不断剪还乱,那么这个时候引入一个中间类X,修改成A、B、C持有X,X持有A和B和C;之前调用栈时A=>B=>C=>B,修改成了A=>X=>C|B;
类似于一群学渣在一起做暑假作业,每个学渣都或多或少会做几题,然后大伙一合计,不约等于全部做出来了么?
由于每个学渣会的题目都不一样,而且分布在不同学渣的暑假作业上,找起答案不方便。
这时一个大聪明跳出来,「你们都先把答案写在我的暑假作业上不就行了?」
中介者类:我来说句公道话
对比外观模式和中介者模式,都是多个类之间的相互关系,但是它们的侧重点不同:
1. 外观模式强调对外的统一口径,由一个Facede类对外提供支持,至于类的内部是如何实现功能,可能是由多个类、或者多个模块、甚至多个系统之间共同完成;
2. 中介者模式强调多个类之间相互交叉调用,然后由中间类把相互引用,变成其他类与中间类的一对一关系;对外提供支持的,可以是其他类,也可以是中间类;
观察者(Observer)模式:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式。
核心是:向系统注册一个事件;当事件发生后,通知我。
那么可以有多个人,向系统注册了同一个事件;当事件发生后,系统就把这个消息告诉这些人:同志,醒醒,该起来写代码了!
适用场景:一般系统中使用了缓存。现在某一类型的数据在缓存中更新之后,还要支持刷新这些数据!那么可以考虑使用观察者模式了。
在所有存入缓存的地方,同时注册一个刷新缓存的事件,实现这个方法。当系统要刷新缓存时,执行这些方法。
订阅接口{
发生这个事件告诉我(事件, 触发这个事件的实例);
}
发布订阅类 {
Map<事件, List<订阅接口>> 订阅类实例Map;
发布事件(事件, 触发这个事件的实例){
List<订阅接口> 订阅类实例集合 = 订阅类实例Map.get(事件);
遍历:订阅类实例集合.发生这个事件告诉我(事件, 触发这个事件的实例); //告诉订阅类实例:醒醒,该搬砖了
}
订阅事件(事件, 订阅实例){
订阅实例类Map添加订阅事件与实例; //发生了这个“事件”,告诉我哦
}
}
库存缓存类{
发布订阅类 发布订阅类实例;
更新缓存(){
更新库存缓存;
发布订阅类实例.发布事件("库存变动了", this);
}
}
库存缓存依赖类 implements 订阅接口 {
发布订阅类 发布订阅类实例;
{
发布订阅类实例.订阅事件("库存变动了", this);
}
发生这个事件告诉我(事件, 触发事件实例){
事件 ==> "库存变动了"
触发事件实例 ==> 缓存类
}
}
我们先假设一下没有 发布订阅类 ,并且类似 库存缓存依赖类 的类有十个;
那么,库存缓存类 中,必须要全部持有十个 库存缓存依赖类 对象;
如果库存发生了变动,需要依次执行十个 库存缓存依赖类 对象的方法;
以后如果又新增了一个依赖类,需要修改 库存缓存类 ;
甚至以后有个新的事件下架
,那么,改动地方更多了。
加入 发布订阅类 后,库存缓存类仅需持有一个 发布订阅类 对象,然后 发布订阅类 分别与每个缓存依赖类交互;即使以后添加了依赖类,也只需与 发布订阅类 对象产生联系;新增一个事件下架
,增加一个依赖类,监听下架
事件即可。
对比中介者模式,中介者模式也是处理复杂关系的一把好手,其模式下的不同对象,仍然可以通过中间类相互交换数据;
观察者模式数据流只能是单向流转,可以说观察者模式是中介者模式的一个特例。
这玩意和我们去面试后被告知回家等通知吧却最后杳无音讯有什么区别?
对象池、线程池了解一下?
组合(Composite Pattern)模式:它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性。
程序猿:“看,这是一个树!”
大表哥 (金馆长.gif):“哈哈哈哈,你写代码写傻了吧,不就一堆看不懂的英文字母吗?”
可以移步bug猫另外一篇 java 通用扁平数据转换成树形结构
模板方法(Template Method)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
栗子:订单在线支付异步回调
第一步:锁订单表
第二步:判断当前状态是否可以支付
第三步:判断支付金额是否正确
第四步:记录收银流水
第五步:修改订单支付状态、支付金额、支付方式、支付流水号
第六步:释放锁。支付后处理:异步发送支付成功消息等
这一套流程适用于多种单据支付,因此对于支付主流程而言,锁哪张订单表、如果锁表、如何判断金额等等,都是具体的订单业务操作。
//主要流程 ==> 骨架
在线支付异步处理(){
订单处理抽象类 订单处理对象 = 工厂类、if-else、缓存、自动注入等等;
订单处理对象.锁表();
if 订单处理对象.判断订单状态是否正确 == false
throw 订单状态不对
if 订单处理对象.判断支付金额是否正确 == false
throw 支付金额不对
订单处理对象.记录收银流水();
订单处理对象.修改订单支付段();
订单处理对象.释放锁();
订单处理对象.支付后处理();
}
订单处理抽象类 {
abstract 锁表();
abstract 判断订单状态是否正确();
abstract 判断支付金额是否正确();
记录收银流水(){
写流水表;
}
abstract 修改订单支付段();
abstract 释放锁();
支付后处理(){
异步发短信、发应用通知;
}
}
正常订单 extend 订单处理抽象类{
锁表(){...}
判断订单状态是否正确(){...}
判断支付金额是否正确(){...}
修改订单支付段(){...}
释放锁(){...}
}
退订单 extend 订单处理抽象类{
锁表(){...}
判断订单状态是否正确(){...}
判断支付金额是否正确(){...}
修改订单支付段(){...}
释放锁(){...}
}
算法骨架定义一个主流程,具体算法,采用SPI形式嵌入到主流程中。
不同业务的算法,实现SPI接口,被动等待主流程调用即可。
模板方法模式,一般配合接口适配模式,效果更佳~
这个不就是多态的运用吗?
命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
张三家里要做饭了,张三老婆吩咐到:
在下班回家的路上,先到菜市场买一把芹菜;再到商场买瓶酱油;最后到水果超市,买一个榴莲;如果在路上碰到卖烤红薯的,就买两个;
商铺接口{ // ==> Receiver
买买买();
路过();
}
菜市场 implements 商铺接口 {
买买买(){
System.out.println("买一把芹菜");
}
路过(){
...
}
}
商场 implements 商铺接口 {
买买买(){
System.out.println("买瓶酱油");
}
路过(){
...
}
}
水果超市 implements 商铺接口 {
买买买(){
if 路上碰到卖烤红薯
System.out.println("买两个榴莲");
else
System.out.println("买一个榴莲");
}
路过(){
...
}
}
烤红薯 implements 商铺接口 {
买买买(){
}
路过(){
System.out.println("我路过");
}
}
//抽象指令
工具人接口{ // ==> Command,一般情况只有一个方法
dosomething();
}
//具体的指令
跑腿类 implements 工具人接口 {
商铺接口 商铺对象;
构造器(商铺接口 商铺对象){
this.商铺对象 = 商铺对象;
}
dosomething(){
商铺对象.买买买(); //到这个商铺买东西
}
}
//具体的指令
逛街类 implements 工具人接口 {
商铺接口 商铺对象;
构造器(商铺接口 商铺对象){
this.商铺对象 = 商铺对象;
}
dosomething(){
商铺对象.路过(); //陪老婆到这个商铺溜gai子
}
}
待办类{ // ==> Invoker
增加待办(工具人接口 要做的事情对象){ //执行具体的指令,可以在此记录、撤回
要做的事情 +1;
}
去吧皮卡丘(){
遍历:要做的事情对象.dosomething(); //去商铺干点什么
}
}
老婆来电话了(){
待办类 待办对象 = new 待办类();
待办对象.增加待办(new 跑腿类(new 菜市场)); //去菜市场跑腿 等价于==> new 菜市场().买买买();
待办对象.增加待办(new 跑腿类(new 商场)); //去商场跑腿 等价于==> new 商场().买买买();
待办对象.增加待办(new 跑腿类(new 水果超市)); //去水果超市跑腿 等价于==> new 水果超市().买买买();
待办对象.增加待办(new 逛街类(new 烤红薯)); //路过烤红薯摊 等价于==> new 烤红薯().路过();
待办对象.去吧皮卡丘(); //开始执行老婆的吩咐
}
现在我们有一个对象,根据不同的条件,需要执行这个对象的不同方法。那么第一反应肯定是很多if-else
或者使用反射。
命令模式,可以把执行同一个对象
(xx商铺) 的不同方法
(买买买、路过),变成了执行不同对象
(跑腿类、逛街类) 的同一个方法
(dosomething);
不同对象(跑腿类、逛街类),可以对原对象的不同方法(买买买、路过) 进行封装增强,然后统一方式调用执行,巧妙地避开了反射。
如果这个对象以后增加了一个新的方法,那么只需要新加一个指令类即可。
最终张三因为买了2个榴莲被他老婆狠狠指责了一顿
策略模式和命令模式及其相似。
还是以上面的例子:张三作为工具人,可以到商店买买买、可以一起逛街。
诶,他老婆的闺密也可以买买买、和逛街。
然后张三的丈母娘表示她跟得上潮流也阔以!
工具人接口 {
买买买();
逛街();
}
张三 implements 工具人接口 {
买买买(){
买酱油、买大蒜
}
逛街(){
拎包,男朋友寄存处
}
}
闺密 implements 工具人接口 {
买买买(){
买包包、买护肤品
}
逛街(){
从城南到城北,小吃一条街
}
}
丈母娘 implements 工具人接口 {
买买买(){
买保健品、买理财
}
逛街(){
养生馆,公园
}
}
待办类{ // ==> Context
工具人接口 工具人对象;
getset工具人对象(){...};
去买买买(){
工具人对象.买买买();
}
去逛街(){
工具人对象.逛街();
}
}
// 调用类 ==> Context
放假了(){
待办类 待办对象 = new 待办类();
周五:
待办对象.set工具人对象(new 丈母娘);
待办对象.去逛街(); // 周五和丈母娘逛街 等价于 ==> new 丈母娘().逛街()
周六:
待办对象.set工具人对象(new 闺密);
待办对象.去买买买(); // 周六和闺密shopping 等价于 ==> new 闺密().买买买()
周末:
待办对象.set工具人对象(new 张三);
待办对象.去逛街(); // 周末和张三逛街 等价于 ==> new 张三().逛街()
}
策略模式,强调不同的算法(工具人类)之间,可以完全相互替换;
不管你传入什么算法(工具人类),对于调用者而已,只需要执行环境类(待办类)的方法即可。
我们想一下,为什么不直接执行 工具人对象.买买买 、工具人对象.逛街 ,而是采用 待办对象 去操作?
还是为了方便扩展:无论 工具人类 具体的实现者是谁,我们统一操作 待办对象 。
在执行 待办对象 的方法前后,可以添加很多自义定的功能。类似对象适配,都是A持有B对象,在A对象方法中,对B对象增强;
如果直接采用 工具人对象.方法 执行,那么我们对 待办对象 的增强,对 工具人类 的实现类无效了。
有一天,隔壁老王高调宣布,「我也可以!」
状态(State)模式的:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
多次执行一个对象的同一个方法,对象在内部判断、处理之后,返回不同的结果。
例如在分布式系统中,应用实例健康有DOWN
UP
UNKNOWN
三种状态,网关对其处于不同状态下的处理方式不同:DOWN
直接采用熔断处理;UP
正常调用;UNKNOWN
处于熔断与正常之间,需要检查实例健康状态。
环境类{
状态类 状态类实例;
getset状态类实例(){...};
}
状态类接口{
执行(环境类 环境类实例);
}
状态Open类 implements 状态类接口 {
执行(环境类 环境类实例){
if 08:00到18:00之间 {
此时是通路
} else {
此路不通~
环境类实例.set状态类实例(状态Close类实例);
}
}
}
状态Close类 implements 状态类接口 {
执行(环境类 环境类实例){
if 08:00到18:00之间 {
此时是通路
环境类实例.set状态类实例(状态Open类实例);
} else {
此路不通~
}
}
}
主流程 {
环境类 环境类实例 set状态类实例(状态Open类实例)
...
多次执行:
状态类接口 状态类实例 = 环境类实例.get状态类实例();
状态类实例.执行(环境类实例);
...
}
主流程不停执行,当触发状态转换条件后,再继续执行,就会发现状态类里面的代码逻辑,偷偷发生了变化;
策略模式与状态模式:
如果仅看策略模式与状态模式类结构图,也会发现二者非常相似:不同的状态类,也可以看成不同的算法类。
状态模式,有个特点是,状态与状态之间,可以相互切换,甚至形成环路。不同状态切换,根据触发条件自行切换,对应调用者无感知;
策略模式,算法只能选其一种,执行之后就到下一个流程;不同算法之间可以替换,选择算法由调用者决定;
责任链(Chain of Responsibility)模式:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
一般,在链上的对象,都持有下一个流程的对象应用。当前链上的对象,处理完逻辑后,判断能否进行下一步:可以,执行下一个流程对象的方法,继续传递下去;否则就over了。
过滤器链、拦截器链,都是这么做的。
适用场景:
- 由甲到乙之间,隔着A、B、C、D多个、或者不确定有多少个电灯泡的时候,就可以使用责任链模式;
- 由甲到乙之间,隔着A、B、C、D、E,其中执行顺序,随着产品提的需求经常变动的情况;(中指.jpg)
比喻现在要开发一个文明词汇的功能,对于口吐芬芳等词语,进行不可见处理:
public class WordChainTest {
//抽象的单词处理器
public static abstract class WordChains {
protected WordChains next; //下一个
public void setNext(WordChains next) {
this.next = next;
}
public String doNext(String word){ //执行下一个
return next == null ? word : next.handleRequest(word);
}
//具体的处理方式
public abstract String handleRequest(String word);
}
public static class NullWordChains extends WordChains {
@Override
public String handleRequest(String word) {
return doNext(word == null ? "" : word.trim());
}
}
public static class LowerWordChains extends WordChains {
@Override
public String handleRequest(String word) {
return doNext(word.toLowerCase());
}
}
public static class NumberWordChains extends WordChains {
private Pattern numPat = Pattern.compile("(\\d{3})(\\d+)(\\d{2})");
@Override
public String handleRequest(String word) {
return doNext(numPat.matcher(word).replaceAll("$1***$3"));
}
}
public static class HarmoniousWordChains extends WordChains {
@Override
public String handleRequest(String word) {
return doNext(word.replace("小可爱", ">你被河蟹了<"));
}
}
public static void main(String[] args) {
List wordChains = Arrays.asList(new NullWordChains(), new LowerWordChains(), new NumberWordChains(), new HarmoniousWordChains());
WordChains first = wordChains.get(0);
WordChains last = first;
for(int idx = 1; idx < wordChains.size(); idx ++ ){
WordChains cur = wordChains.get(idx);
last.setNext(cur);
last = cur;
}
System.out.println(first.handleRequest(" NI ZHENGSHIGE小可爱 @972245132 ")); //打印:ni zhengshige>你被河蟹了< @972***32
}
}
也可以使用环境类统一管理责任链实例,然后由环境类触发 doNext 方法
看这条长链像不像我们提交的加薪审批又长又长?
剩下迭代器模式、备忘录模式、解释器模式、访问者模式,因为用到的地方比较少,说就略过了;
关于访问者模式,有兴趣可以看下cglib
(org.springframework.asm.ClassVisitor)是如何操作字节码动态生成class的。或者偷个懒贴一下别人的帖子。
ClassVisitor
是个神奇的类,执行visitAnnotation
、visitField
、visitMethod
等方法,会分别给你的类动态加上一个注解、字段、和方法;
cglib
有机会补上 (赌5毛
本次设计模式学习完毕~
~THE END~