动态代理实现原理
装饰模式 vs (静态)代理模式中提到了静态代理模式的局限性,下面就来介绍一下动态代理模式及其实现原理,它也是Spring中AOP(Aspect Oriented Programming)的实现原理。
装饰模式 vs (静态)代理模式中提到了静态代理模式的局限性,下面就来介绍一下动态代理模式及其实现原理,它也是Spring中AOP(Aspect Oriented Programming)的实现原理。
这两个设计模式都属于结构型模式,且非常相似,其UML图如下:装饰模式: (静态)代理模式:提到代理模式一般是指静态代理模式,动态代理模式会在动态代理实现原理中专门讲解 共同点: 装饰者与被装饰者,代理类与被代理类,都是继承自同一个接口,可以令他们在被调用时相互替换不同点: 被装饰者往往被作为装饰者的构造器参数传入装饰者,强调被装饰者功能的增强;被代理类往往在代理类内部被创建,所以这里用UML里组合的关系,强调对被代理类的访问控制。装饰者里持有的是被装饰者的接口类型,所以可以装饰所有实现同一接口的类;代理类是针对某一个具体的类进行代理,所以对每一个类都要实现一个对应的代理类,这是静态代理模式的局限,可以使用动态代理模式来弥补。
在面向对象设计领域里,SOLID是非常经典的设计原则,可以认为它是道,设计模式是术,只有深刻理解了道,才能用好术。下面简单总结一下SOLID原则: Single Responsibility Principle: 每个类只能有一个被修改的原因Open-Close Principle: 对扩展开发,对修改关闭Liskov's Substitution Principle: 派生类必须能够完全替换基类Liskov's Substitution Principle(LSP)Interface Segregation Principle:客户端不应该被强制依赖他们不需要使用的接口Dependency Inversion Principle: 高层次的模块不应该依赖低层次的模块, 双方都应该依赖抽象。抽象不应该依赖具体细节。细节应该依赖抽象。Dependency Inversion Principle下面以工厂模式为例,说明一下SOLID原则在设计模式里的体现:工厂模式属于创建型模式,主要分三种: 简单工厂模式工厂方法模式抽象工厂模式个人觉得第三种模式使用场景较少且比较鸡肋,主要介绍前两种。先来看下简单工厂模式: public abstract class Operation{ private double value1; private double value2; public double getValue1() { return value1; } public void setValue1(double value1) { this.value1 = value1; } public double getValue2() { return value2; } public void setValue2(double value2) { this.value2 = value2; } protected abstract double getResult();}public class OperationAdd extends Operation { @Override protected double getResult(){ return getValue1() + getValue2(); }}public class OperationMinus extends Operation { @Override protected double getResult(){ return getValue1() - getValue2(); }}public class OperationMul extends Operation { @Override protected double getResult(){ return getValue1() * getValue2(); }}public class OperationFactory{ public static Operation createOperation(String operation){ Operation operation = null; switch(operation){ case "+": operation = new OperationAdd(); break; case "-": operation = new OperationMinus(); break; case "*": operation = new OperationMul(); break; default: throw new UnsupportedOperationException("Unsupported Operation:" + operation); } return operation; }}首先,我们必须令Operation的派生类遵循Liskov's Substitution Principle,才能放心的说,无论我们在工厂中创建出哪种Operation的派生类,都能够利用多态替换其后对Operation的引用。其次,工厂模式返回抽象类,使调用工厂的高层模块依赖Operation这个抽象类而不是其某个具体的派生类,这满足了Dependency Inversion Principle。但是,OperationFactory类中包含了所有Operation派生类的创建,后面如果不断的需要增加新的Operation派生类,就需要不断的修改OperationFactory,这违反了Open-Close Principle,就需要引入工厂方法模式: ...
单例模式被认为是最简单的设计模式,也经常被用到,下面以我在实际项目中用到的一个单例模式为例,看下如何利用经典的两次判空方法令其高效、安全得工作在多线程环境(见代码中注释)。 package core;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.io.IOException;import java.io.Reader;import java.util.Properties;public class SqlSessionFactorySingleton { private static Logger logger = LoggerFactory.getLogger("SqlSessionFactorySingleton"); private static final String MYBATIS_CONFIG_FILE = "mybatis-config.xml"; // 使用volatile关键字令A线程的修改对B线程立即可见 private static volatile SqlSessionFactory factory = null; // 屏蔽默认的公共构造函数 private SqlSessionFactorySingleton() { } public static SqlSessionFactory getInstance() { if (factory == null) { // 第一次判空 // 只有创建SqlSessionFactory实例时才需要同步,不直接在方法上加synchronized关键字可以避免在每次判断实例是否创建时加锁,极大得提高并发效率 synchronized (SqlSessionFactorySingleton.class) { // 如果A、B两个线程同时通过第一次判空,A获得锁,B等待,等A创建完SqlSessionFactory实例释放锁,B获得锁,此时B需要再次判断实例是否已创建来避免重复创建 if (factory == null) { // 第二次判空 String configFile = "config.properties"; try (Reader configReader = Resources.getResourceAsReader(configFile); Reader mybatisReader = Resources.getResourceAsReader(MYBATIS_CONFIG_FILE)) { Properties properties = new Properties(); properties.load(configReader); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); factory = builder.build(mybatisReader, properties); } catch (IOException e) { logger.error("Exception when reading {} and {}:", configFile, MYBATIS_CONFIG_FILE, e); } } } } return factory; }}
Java中的栈Java中的Stack是早期的遗留类,Sun/Oracle已经不推荐使用,现在只是为了兼容遗留代码而保留.遗留实现如下面的代码所示,java.util.Stack中的实现是基于动态数组实现的,而Vector同样是一个被废弃的类。个人看来,这个实现有两个问题基于Vector,需要同步因而性能损失很严重基于数组而非链表,栈很大时需要多次扩容,带来不必要的性能损失publicclass Stack<E> extends Vector<E> { /** * Creates an empty Stack. / public Stack() { } public E push(E item) { addElement(item); return item; } public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); } public boolean empty() { return size() == 0; } public synchronized int search(Object o) { int i = lastIndexOf(o); if (i >= 0) { return size() - i; } return -1; } /* use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = 1224463164541339165L;}自己简单的封装下面是基于LinkedList封装的Stack这里的栈是一个装饰器。import java.util.LinkedList;import java.util.NoSuchElementException;public class Stack<T> { private LinkedList<T> stack; Stack() { stack = new LinkedList<>(); } public void push(T o) { stack.add(o); } public T pop() { if (size() <= 0) { throw new NoSuchElementException(“Stack is Empty.”); } return stack.removeLast(); } public T peek() { if (size() <= 0) { throw new NoSuchElementException(“Stack is Empty.”); } return stack.getLast(); } public boolean empty() { return stack.size() == 0; } public int size() { return stack.size(); }} ...
学习对象:https://github.com/tmrts/go-p…。这个repo使用go语言实现了一些设计模式,包括常用的Builder模式,Singleton模式等,也有列举出还未用go实现的模式,如Bridge模式等。本文并非完整地介绍和解析这个repo里的每一行代码,只对个人认为值得学习和记录的地方进行说明,阅读过repo代码后再阅读本文比较合适。Functional Options这个模式是一种优雅地设置对象初始化参数的方式。考虑的点是:如何友好地扩展初始化的选填参数如何友好地处理默认值问题函数签名见名知意比较以下几种初始化对象参数的方法://name是必填参数, timeout和maxConn是选填参数,如果不填则设置为默认值// pattern #1func NewServer(name string, timeout time.Duration, maxConn uint) (*Server, error) {…}// 这种方法最直观, 但也是最不合适的, 因为对于扩展参数需要修改函数签名, 且默认值需要通过文档获知// pattern #2type ServerConf struct { Timeout time.Duration MaxConn uint}func NewServer(name string, conf ServerConf) (*Server, error) {…} // 1)func NewServer(name string, conf *ServerConf) (*Server, error) {…} // 2)func NewServer(name string, conf …ServerConf) (*Server, error) {…} // 3)// 改进: 使用了参数结构体, 增加参数不需要修改函数签名// 1) conf现在是必传, 实际上里面的是选填参数// 2) 避免nil; conf可能在外部被改变.// 3) 都使用默认值的时候可以不传, 但多个conf可能在配置上有冲突// conf的默认空值对于Server可能是有意义的.// pattern #3: Functional Optionstype ConfSetter func(srv *Server) errorfunc ServerTimeoutSetter(t time.Duration) ConfSetter { return func(srv *Server) error { srv.timeout = t return nil }}func ServerMaxConnSetter(m uint) ConfSetter { return func(srv *Server) error { srv.maxConn = m return nil }}func NewServer(name string, setter …ConfSetter) (*Server, error) { srv := new(Server) … for _, s := range setter { err := s(srv) } …}// srv, err := NewServer(“name”, ServerTimeoutSetter(time.Second))// 使用闭包作为配置参数. 如果不需要配置选填参数, 只需要填参数name.上面的pattern#2尝试了三种方法来优化初始化参数的问题,但每种方法都有自己的不足之处。pattern#3,也就是Functional Options,通过使用闭包来做优化,从使用者的角度来看,已经是足够简洁和明确了。当然,代价是初次理解这种写法有点绕,不如前两种写法来得直白。trade off欲言又止稍加思考,容易提出这个问题:这跟Builder模式有什么区别呢?个人认为,Functional Options模式本质上就是Builder模式:通过函数来设置参数。参考文章:Functional options for friendly APIsCircuit-Breaker熔断模式:如果服务在一段时间内不可用,这时候服务要考虑主动拒绝请求(减轻服务方压力和请求方的资源占用)。等待一段时间后(尝试等待服务变为可用),服务尝试接收部分请求(一下子涌入过多请求可能导致服务再次不可用),如果请求都成功了,再正常接收所有请求。// 极其精简的版本, repo中版本详尽一些type Circuit func() error// Counter 的实现应该是一个状态机type Counter interface { OverFailureThreshold() UpdateFailure() UpdateSuccess()}var cnt Counterfunc Breaker(c Circuit) Circuit { return func() { if cnt.OverFailureThreshold() { return fmt.Errorf(“主动拒绝”) } if err := c(); err != nil { cnt.UpdateFailure() return err } cnt.UpdateSuccess() return nil }}熔断模式更像是中间件而不是设计模式:熔断器是一个抽象的概念而不是具体的代码实现;另外,如果要实现一个实际可用的熔断器,要考虑的方面还是比较多的。举些例子:需要提供手动配置熔断器的接口,避免出现不可控的请求情况;什么类型的错误熔断器才生效(恶意发送大量无效的请求可能导致熔断器生效),等等。参考文章:Circuit Breaker pattern参考实现:gobreakerSemaphorego的标准库中没有实现信号量,repo实现了一个:)repo实现的实质是使用chan。chan本身已经具备互斥访问的功能,而且可以设定缓冲大小,只要稍加修改就可以当作信号量使用。另外,利用select语法,可以很方便地实现超时的功能。type Semaphore struct { resource chan struct{} // 编译器会优化struct{}类型, 使得所有struct{}变量都指向同一个内存地址 timeout time.Duration // 用于避免长时间的死锁}type TimeoutError errorfunc (s *Semaphore) Aquire() TimeoutError { select { // 会从上到下检查是否阻塞 // 如果timeout为0, 且暂时不能获得/解锁资源, 会立即返回超时错误 case: <-s.resource: return nil case: <- time.After(s.timeout): return fmt.Errorf(“timeout”) } }func (s *Semaphore) Release() TimeoutError { select { // 同Aquire() case: s.resource <- struct{}{}: return nil case: <- time.After(s.timeout): return fmt.Errorf(“timeout”) } }func NewSemaphore(num uint, timeout time.Duration) (*Semaphore, error) { if num == 0 { return fmt.Errorf(“invalid num”) //如果是0, 需要先Release才能Aquire. } return &Semaphore{ resource: make(chan strcut{}, num), timeout: timeout, }, nil //其实返回值类型也不影响Semaphore正常工作, 因为chan是引用类型}Object Pool标准库的sync包已经有实现了一个对象池,但是这个对象池接收的类型是 interface{} (万恶的范型),而且池里的对象如果不被其它内存引用,会被gc回收(同java中弱引用的collection类型类似)。repo实现的对象池是明确类型的(万恶的范型+1),而且闲置不会被gc回收。但仅仅作为展示说明,repo的实现没有做超时处理。下面的代码尝试加上超时处理。也许对使用者来说,额外增加处理超时错误的代码比较繁琐,但这是有必要的,除非使用者通读并理解了你的代码。trade offtype Pool struct { pool chan *Object timeout time.Duration}type TimeoutError errorfunc NewPool(total int, timeout time.Duration) *Pool { p := &Pool { pool: make(Pool, total), timeout: timeout, } //pool是引用类型, 所以返回类型可以不是指针 for i := 0; i < total; i++ { p.pool <- new(Object) } return p}func (p *Pool) Aquire() (*Object, TimeoutError) { select { case obj <- p.pool: return obj, nil case <- time.After(timeout): return nil, fmt.Errorf(“timeout”) }}func (p *Pool) Release(obj *Object) TimeoutError { select { case p.pool <- obj: return nil case <- time.After(timeout): return nil, fmt.Errorf(“timeout”) }}chan and goroutine解析一下repo里goroutine和chan的使用方式,也不算是设计模式。Fan-in pattern 主要体现如何使用sync.WaitGroup同步多个goroutine。思考:这里的实现是如果cs的长度为n, 那个要开n个goroutine, 有没有办法优化为开常数个goroutine?// 将若干个chan的内容合并到一个chan当中func Merge(cs …<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup wg.Add(len(cs)) // 将send函数在for循环中写成一个不带参数的匿名函数, 看起来会使代码更简洁, // 但实际上所有for循环里的所有goroutine会公用一个c, 代码不能正确实现功能. send := func(c <-chan int) { for n := range c { out <- n } wg.Done() } for _, c := range cs { go send(c) } // 开一个goroutine等待wg, 然后关闭merge的chan, 不阻塞Merge函数 go func() { wg.Wait() close(out) } return out}Fan-out pattern 将一个主chan的元素循环分发给若干个子chan(分流)。思路比较简单就不贴代码了。思考:reop实现的代码,如果其中一个子chan没有消费元素,那么整个分发流程都会卡住。是否可以优化?Bounded Parallelism Pattern 比较完成的例子来说明如何时候goroutine. 并发计算目录下文件的md5.func MD5All(root string) (map[string][md5.Size]byte, error) { //因为byte是定长的, 使用数据更合适, 可读且性能也好一点 done := make(chan struct{}) //用于控制整个流程是否暂停. 其实这里是用context可能会更好. defer close(done) paths, errc := walkFiles(done, root) c := make(chan result) var wg sync.WaitGroup const numDigesters = 20 wg.Add(numDigesters) for i := 0; i < numDigesters; i++ { go func() { digester(done, paths, c) wg.Done() }() } // 同上, 开goroutine等待所有digester结束 go func() { wg.Wait() close(c) }() m := make(map[string][md5.Size]byte) for r := range c { if r.err != nil { return nil, r.err } m[r.path] = r.sum } // 必须放在m处理结束后才检查errc. 否则, 要等待walkFiles结束了才能开始处理m // 相反, 如果errc有信号, c肯定已经close了 if err := <-errc; err != nil { return nil, err } return m, nil}func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) { paths := make(chan string) // 这里可以适当增加缓冲, 取决于walkFiles快还是md5.Sum快 errc := make(chan error, 1) //必须有缓冲, 否则死锁. 上面的代码paths close了才检查errc go func() { defer close(paths) // 这里的defer不必要. defer是运行时的, 有成本. errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } select { case paths <- path: case <-done: return errors.New(“walk canceled”) } return nil }) }() return paths, errc}type result struct { path string sum [md5.Size]byte err error}func digester(done <-chan struct{}, paths <-chan string, c chan<- result) { for path := range paths { data, err := ioutil.ReadFile(path) select { // 看md5.Sum先结束还是done信号先到来 case c <- result{path, md5.Sum(data), err}: case <-done: return } }} ...
过去几年容器逐渐成为了打包和部署代码的流行的方式。容器镜像解决很多现有的打包和部署工具所带来的问题,初次以外,还为我们提供了构建分布式应用的全新的思路。就如SOA提倡将应用拆分为模块化的内聚的服务,容器应当进一步提倡将这些服务拆分为紧密协作的模块化容器。通过构建应用边界,容器使用户能够使用模块化,可重用的组件构建其服务,从而使得服务比单机容器构建的应用程序更可靠,更具可扩展性并且构建速度更快。从VM向容器的演变从各种角度来说就如同当年从单机应用转化为模块化的面向对象的应用程序。容器镜像提供的抽象层与面向对象编程中类的抽象边界有很大的共同点,而且也提高了开发者的效率和程序的质量。就像正确的编码方式是将关注点分离为模块化对象一样,在容器中打包应用程序的正确方法是将关注点分离为模块化容器。根本上来说,这意味着不仅要将整个应用程序分解,而且要将任何一个服务器中的各个部分分解为多个模块化容器,这些容器易于参数化和重复使用。这就像现代语言中的标准语言库,大多数应用程序开发人员可以将由其他人编写的模块化容器组合在一起,并使用更高质量的组件更快地构建应用程序。从模块化容器方面进行思考的好处很多,特别是模块化容器提供以下内容:加快应用的开发,因为容器可以在团队甚至是大型社区之间进行复用支持敏捷团队,因为容器边界是一个天然的边界,划分给各个团队。支持关注点分离,并专注于开发特定功能从而减少复杂的依赖和不可测试组件。从模块化容器构建应用程序意味着考虑协作提供服务容器的共生组,而不是一个容器提供一个服务。在Kubernetes中,这种模块化容器服务的实施者是Pod。一个Pod是指一组共享文件系统,内核命名空间和IP地址的一组容器。Pod在K8s集群中是调度的基本单位,正是因为Pod中容器的共生特性要求它们共同安排在同一台机器上,而可靠地实现这一点的唯一方法是将容器组作为原子调度单元。当你从Pod的角度思考时,自然会出现一些模块化应用程序开发的通用模式,这些模式会多次重复出现。我相信,随着我们在Kubernetes的开发中向前发展,将会发现更多这些模式,但这里有三个我们常见的模式:例子1:Sidecar容器Sidecar容器拓展并且加强主容器,他们融合当前已有的容器并且将它们完善。举个例子,假设有一个运行这Nginx web应用的容器。添加另一个容器将文件系统与git仓库同步,在容器间共享文件系统,从而实现git的提交并部署。但是这种模块化实现使得git同步器可以交给另一个容器开发,并且跨不同的web服务器复用。因为这种模块化,你只需要编写并测试单个git同步应用并且提供给多个应用使用。而如果有别的团队开发了这个工具,你甚至不需要重复开发。例子2:Ambassador容器Ambassador容器代理外界至本地的连接。比如,现在有一个Redis集群,包含多个读者和单个写者。 你可以创建一个Pod,包含主应用和Redis ambassador容器。ambassador容器作为代理分离读写请求分别交给对应的服务器。因为这两个容器共享一个网络命名空间,即他们共享一个IP地址,因此主应用可以用localhost访问ambassador服务,无需通过服务发现。从主应用的视角来看,就仿佛在localhost上连接了redis集群。这种方式非常方便,不仅因为不同的团队可以管理自己的组件,而且因为在开发环境中,你可以跳过代理,直接连接到Redis集群上。例子3:Adapter容器Adapter容器标准化输入输出。假设现在需要监控N个应用,每个应用可能使用了不同的方法来输出监控数据(比如JMX, StatsD等)。但是每个监控系统都希望用一个一致的数据模型来管理收集的数据。通过使用Adapter模式来组合容器,你可以创建一个pod将应用容器和适配器容器组合起来,从而将同质的监控数据转化为单个同一个的表现形式。同样的,因为这些Pod共享命名空间和文件系统,这两个容器间的协作简单明。RefrenceSidecar PatternAmbassador Pattern