[Open Source and Project Combat: Open Source Combat] 86 | Open Source Combat IV (Part 2): Summarize the 11 Design Patterns Used in the Spring Framework

In the last lesson, we explained two design patterns that support extended functions in Spring: observer pattern and template pattern. These two modes can help us create extension points, allowing framework users to customize framework functions based on extension points without modifying the source code.

In fact, there are many design patterns used in the Spring framework, no less than a dozen. We will summarize and list them today. Due to space limitations, it is impossible for me to explain each design pattern in great detail. Some have been mentioned before or are relatively simple, so I will stop here. If there is something you don't understand very well, you can read the source code and consult the previous theoretical explanations to figure it out yourself. If you have been following my courses, I believe you already have such a learning ability.
Without further ado, let's officially start today's study!

Application of Adapter Pattern in Spring

In Spring MVC, the most common way to define a Controller is to mark a class as a Controller class through the @Controller annotation, and to mark the URL corresponding to the function through the @RequesMapping annotation. However, there is much more to defining a Controller than just this one approach. We can also define a Controller by having a class implement the Controller interface or the Servlet interface. For these three definition methods, I wrote three pieces of sample code, as follows:

// 方法一:通过@Controller、@RequestMapping来定义
@Controller
public class DemoController {
    @RequestMapping("/employname")
    public ModelAndView getEmployeeName() {
        ModelAndView model = new ModelAndView("Greeting");        
        model.addObject("message", "Dinesh");       
        return model; 
    }  
}
// 方法二:实现Controller接口 + xml配置文件:配置DemoController与URL的对应关系
public class DemoController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        ModelAndView model = new ModelAndView("Greeting");
        model.addObject("message", "Dinesh Madhwal");
        return model;
    }
}
// 方法三:实现Servlet接口 + xml配置文件:配置DemoController类与URL的对应关系
public class DemoServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
  }
  
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().write("Hello World.");
  }
}

When the application starts, the Spring container will load these Controller classes, and parse out the processing function corresponding to the URL, encapsulate it into a Handler object, and store it in the HandlerMapping object. When a request comes, DispatcherServlet looks up the Handler corresponding to the request URL from the HanderMapping, then invokes and executes the function code corresponding to the Handler, and finally returns the execution result to the client.

However, the definitions of functions (function names, input parameters, return values, etc.) of Controllers defined in different ways are not uniform. As shown in the sample code above, the definition of the function in method 1 is very random and not fixed, the function definition in method 2 is handleRequest(), and the function definition in method 3 is service() (it seems to define doGet(), doPost(), in fact, the template mode is used here, the service() in the Servlet calls the doGet() or doPost() method, and the DispatcherServlet calls the service() method). DispatcherServlet needs to call different functions according to different types of Controllers. The following is the specific pseudocode:

Handler handler = handlerMapping.get(URL);
if (handler instanceof Controller) {
  ((Controller)handler).handleRequest(...);
} else if (handler instanceof Servlet) {
  ((Servlet)handler).service(...);
} else if (hanlder 对应通过注解来定义的Controller) {
  反射调用方法...
}

From the code, we can see that there are many if-else branch judgments in this implementation method, and if we want to add a new Controller definition method, we need to add a section of the above pseudo-code correspondingly in the DispatcherServlet class code The if logic shown. This obviously does not conform to the principle of opening and closing.

In fact, we can use the adapter pattern to transform the code so that it satisfies the principle of opening and closing and better supports extensions. In Lesson 51, we said that one of the roles of adapters is to "unify the interface design of multiple classes". Using the adapter pattern, we adapt the functions in the Controller class defined in different ways to a unified function definition. In this way, we can remove the if-else branch judgment logic in the DispatcherServlet class code and call the unified function.
Just talked about the general design ideas, let's look at the code implementation of Spring in detail.

Spring defines a unified interface HandlerAdapter, and defines the corresponding adapter class for each Controller. These adapter classes include: AnnotationMethodHandlerAdapter, SimpleControllerHandlerAdapter, SimpleServletHandlerAdapter, etc. I posted the source code below, you can take a look at it together.

public interface HandlerAdapter {
  boolean supports(Object var1);
  ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;
  long getLastModified(HttpServletRequest var1, Object var2);
}
// 对应实现Controller接口的Controller
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
  public SimpleControllerHandlerAdapter() {
  }
  public boolean supports(Object handler) {
    return handler instanceof Controller;
  }
  public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return ((Controller)handler).handleRequest(request, response);
  }
  public long getLastModified(HttpServletRequest request, Object handler) {
    return handler instanceof LastModified ? ((LastModified)handler).getLastModified(request) : -1L;
  }
}
// 对应实现Servlet接口的Controller
public class SimpleServletHandlerAdapter implements HandlerAdapter {
  public SimpleServletHandlerAdapter() {
  }
  public boolean supports(Object handler) {
    return handler instanceof Servlet;
  }
  public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    ((Servlet)handler).service(request, response);
    return null;
  }
  public long getLastModified(HttpServletRequest request, Object handler) {
    return -1L;
  }
}
//AnnotationMethodHandlerAdapter对应通过注解实现的Controller,
//代码太多了,我就不贴在这里了

In the DispatcherServlet class, we don't need to treat different Controller objects differently, just call the handle() function of HandlerAdapter uniformly. The pseudo code implemented according to this idea is shown below. You see, so there is no annoying if-else logic, right?

// 之前的实现方式
Handler handler = handlerMapping.get(URL);
if (handler instanceof Controller) {
  ((Controller)handler).handleRequest(...);
} else if (handler instanceof Servlet) {
  ((Servlet)handler).service(...);
} else if (hanlder 对应通过注解来定义的Controller) {
  反射调用方法...
}
// 现在实现方式
HandlerAdapter handlerAdapter = handlerMapping.get(URL);
handlerAdapter.handle(...);

Application of Strategy Pattern in Spring

As we mentioned earlier, Spring AOP is implemented through dynamic proxy. Students who are familiar with Java should know that when it comes to code implementation, Spring supports two dynamic proxy implementation methods, one is the dynamic proxy implementation method provided by JDK, and the other is the dynamic proxy implementation method provided by Cglib.
The former requires abstract interface definitions for the class being proxied, while the latter does not (please research on Baidu for more differences between the two dynamic proxy implementation methods). For different proxied classes, Spring will dynamically select different dynamic proxy implementations at runtime. This application scenario is actually a typical application scenario of the strategy pattern.
As we said earlier, the strategy pattern consists of three parts, the definition, creation and use of strategies. Next, let's take a closer look at how these three parts are reflected in the Spring source code.

In the strategy pattern, the definition of the strategy is very simple. We only need to define a strategy interface, and let different strategy classes implement this strategy interface. Corresponding to the Spring source code, AopProxy is a policy interface, and JdkDynamicAopProxy and CglibAopProxy are two policy classes that implement the AopProxy interface. Among them, the definition of AopProxy interface is as follows:

public interface AopProxy {
  Object getProxy();
  Object getProxy(ClassLoader var1);
}

In the strategy pattern, the creation of strategies is generally implemented through factory methods. Corresponding to the Spring source code, AopProxyFactory is a factory class interface, and DefaultAopProxyFactory is a default factory class for creating AopProxy objects. The source code of both is as follows:

public interface AopProxyFactory {
  AopProxy createAopProxy(AdvisedSupport var1) throws AopConfigException;
}
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
  public DefaultAopProxyFactory() {
  }
  public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
      return new JdkDynamicAopProxy(config);
    } else {
      Class<?> targetClass = config.getTargetClass();
      if (targetClass == null) {
        throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
      } else {
        return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
      }
    }
  }
  //用来判断用哪个动态代理实现方式
  private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
    Class<?>[] ifcs = config.getProxiedInterfaces();
    return ifcs.length == 0 || ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]);
  }
}

The typical application scenario of the strategy pattern is to dynamically determine which strategy to use through environment variables, state values, calculation results, etc. Corresponding to the Spring source code, we can refer to the code implementation of the createAopProxy() function in the DefaultAopProxyFactory class just given. Among them, the 10th line of code is the judgment condition of which strategy to choose dynamically.

The Application of Composite Pattern in Spring

When we talked about Spring's "re-encapsulation and re-abstraction" design idea in the last class, we mentioned Spring Cache. Spring Cache provides a set of abstract Cache interfaces. Using it we can unify the different access methods of different cache implementations (Redis, Google Guava...). Different cache access classes for different cache implementations in Spring all rely on this interface, such as: EhCacheCache, GuavaCache, NoOpCache, RedisCache, JCacheCache, ConcurrentMapCache, CaffeineCache. The source code of the Cache interface is as follows:

public interface Cache {
  String getName();
  Object getNativeCache();
  Cache.ValueWrapper get(Object var1);
  <T> T get(Object var1, Class<T> var2);
  <T> T get(Object var1, Callable<T> var2);
  void put(Object var1, Object var2);
  Cache.ValueWrapper putIfAbsent(Object var1, Object var2);
  void evict(Object var1);
  void clear();
  public static class ValueRetrievalException extends RuntimeException {
    private final Object key;
    public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) {
      super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
      this.key = key;
    }
    public Object getKey() {
      return this.key;
    }
  }
  public interface ValueWrapper {
    Object get();
  }
}

In actual development, a project may use many different caches, such as Google Guava cache and Redis cache. In addition, the same cache instance can also be divided into multiple small logical cache units (or namespaces) according to different services.
To manage multiple caches, Spring also provides cache management functionality. However, the functions it contains are very simple. There are two main parts: one is to obtain the Cache object according to the cache name (the name attribute should be set when creating the Cache object); the other is to obtain the name list of all caches managed by the manager. The corresponding Spring source code is as follows:

public interface CacheManager {
  Cache getCache(String var1);
  Collection<String> getCacheNames();
}

What has just been given is the definition of the CacheManager interface, so how to implement these two interfaces? In fact, this uses the combination mode we talked about before.
As we said earlier, the combination mode is mainly applied to a set of data that can be represented as a tree structure. The nodes in the tree are divided into leaf nodes and intermediate nodes. Corresponding to the Spring source code, EhCacheManager, SimpleCacheManager, NoOpCacheManager, RedisCacheManager, etc. represent leaf nodes, and CompositeCacheManager represents intermediate nodes.

The leaf node contains the Cache object it manages, and the intermediate node contains other CacheManager managers, which can be CompositeCacheManager or specific managers, such as EhCacheManager, RedisManager, etc.
I posted the code of CompositeCacheManger below, you can read it together with the explanation. Among them, the implementation of getCache() and getCacheNames() both use recursion. This is where the tree structure can play the most advantage.

public class CompositeCacheManager implements CacheManager, InitializingBean {
  private final List<CacheManager> cacheManagers = new ArrayList();
  private boolean fallbackToNoOpCache = false;
  public CompositeCacheManager() {
  }
  public CompositeCacheManager(CacheManager... cacheManagers) {
    this.setCacheManagers(Arrays.asList(cacheManagers));
  }
  public void setCacheManagers(Collection<CacheManager> cacheManagers) {
    this.cacheManagers.addAll(cacheManagers);
  }
  public void setFallbackToNoOpCache(boolean fallbackToNoOpCache) {
    this.fallbackToNoOpCache = fallbackToNoOpCache;
  }
  public void afterPropertiesSet() {
    if (this.fallbackToNoOpCache) {
      this.cacheManagers.add(new NoOpCacheManager());
    }
  }
  public Cache getCache(String name) {
    Iterator var2 = this.cacheManagers.iterator();
    Cache cache;
    do {
      if (!var2.hasNext()) {
        return null;
      }
      CacheManager cacheManager = (CacheManager)var2.next();
      cache = cacheManager.getCache(name);
    } while(cache == null);
    return cache;
  }
  public Collection<String> getCacheNames() {
    Set<String> names = new LinkedHashSet();
    Iterator var2 = this.cacheManagers.iterator();
    while(var2.hasNext()) {
      CacheManager manager = (CacheManager)var2.next();
      names.addAll(manager.getCacheNames());
    }
    return Collections.unmodifiableSet(names);
  }
}

Application of Decorator Pattern in Spring

We know that caches are generally used in conjunction with databases. If the write cache succeeds, but the database transaction is rolled back, there will be dirty data in the cache. In order to solve this problem, we need to put the cache write operation and the database write operation into the same transaction, either succeed or both fail.
To achieve such a function, Spring uses the decorator pattern. TransactionAwareCacheDecorator adds support for transactions, and processes Cache data separately when transactions are committed and rolled back.

TransactionAwareCacheDecorator implements the Cache interface, and delegates all operations to targetCache, and adds transaction functions to the write operations. This is a typical application scenario and code implementation of the decorator pattern, so I won't explain it much.

public class TransactionAwareCacheDecorator implements Cache {
  private final Cache targetCache;
  public TransactionAwareCacheDecorator(Cache targetCache) {
    Assert.notNull(targetCache, "Target Cache must not be null");
    this.targetCache = targetCache;
  }
  public Cache getTargetCache() {
    return this.targetCache;
  }
  public String getName() {
    return this.targetCache.getName();
  }
  public Object getNativeCache() {
    return this.targetCache.getNativeCache();
  }
  public ValueWrapper get(Object key) {
    return this.targetCache.get(key);
  }
  public <T> T get(Object key, Class<T> type) {
    return this.targetCache.get(key, type);
  }
  public <T> T get(Object key, Callable<T> valueLoader) {
    return this.targetCache.get(key, valueLoader);
  }
  public void put(final Object key, final Object value) {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        public void afterCommit() {
          TransactionAwareCacheDecorator.this.targetCache.put(key, value);
        }
      });
    } else {
      this.targetCache.put(key, value);
    }
  }
  
  public ValueWrapper putIfAbsent(Object key, Object value) {
    return this.targetCache.putIfAbsent(key, value);
  }
  public void evict(final Object key) {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        public void afterCommit() {
          TransactionAwareCacheDecorator.this.targetCache.evict(key);
        }
      });
    } else {
      this.targetCache.evict(key);
    }
  }
  public void clear() {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        public void afterCommit() {
          TransactionAwareCacheDecorator.this.targetCache.clear();
        }
      });
    } else {
      this.targetCache.clear();
    }
  }
}

Application of factory pattern in Spring

In Spring, the most classic application of the factory pattern is to implement the IOC container. The corresponding Spring source code is mainly the BeanFactory class and ApplicationContext related classes (AbstractApplicationContext, ClassPathXmlApplicationContext, FileSystemXmlApplicationContext...). In addition, in the theoretical part, I also take you to implement a simple IOC container. You can go back and look at it again.

In Spring, there are many ways to create beans, such as the pure constructor mentioned above, no-argument constructor plus setter method. I wrote an example to illustrate these two creation methods, the code is as follows:

public class Student {
  private long id;
  private String name;
  
  public Student(long id, String name) {
    this.id = id;
    this.name = name;
  }
  
  public void setId(long id) {
    this.id = id;
  }
  
  public void setName(String name) {
    this.name = name;
  }
}
// 使用构造函数来创建Bean
<bean id="student" class="com.xzg.cd.Student">
    <constructor-arg name="id" value="1"/>
    <constructor-arg name="name" value="wangzheng"/>
</bean>
// 使用无参构造函数+setter方法来创建Bean
<bean id="student" class="com.xzg.cd.Student">
    <property name="id" value="1"></property>
    <property name="name" value="wangzheng"></property>
</bean>

In fact, in addition to these two ways of creating beans, we can also create beans through factory methods. Still in the example just now, if you create a Bean in this way, it will look like this:

public class StudentFactory {
  private static Map<Long, Student> students = new HashMap<>();
  
  static{
    map.put(1, new Student(1,"wang"));
    map.put(2, new Student(2,"zheng"));
    map.put(3, new Student(3,"xzg"));
  }
 
  public static Student getStudent(long id){
    return students.get(id);
  }
}
// 通过工厂方法getStudent(2)来创建BeanId="zheng""的Bean
<bean id="zheng" class="com.xzg.cd.StudentFactory" factory-method="getStudent">
    <constructor-arg value="2"></constructor-arg>           
</bean>

Application of other patterns in Spring

The application of the previous modes in Spring is explained in more detail. Most of the next few modes are what we have talked about before. Here is just a brief summary. So far, if you have forgotten which one , you can go back and look at the explanation of the theoretical part.
SpEL, the full name is Spring Expression Language, is an expression language commonly used in Spring to write configuration. It defines a series of grammar rules. As long as we write expressions according to these grammatical rules, Spring can parse out the meaning of the expressions. In fact, this is a typical application scenario of the interpreter pattern we mentioned earlier.

Because the interpreter mode does not have a very fixed code implementation structure, and there are many SpEL-related codes in Spring, so I will not take you to read the source code here. If you are interested or you just want to implement similar functions in the project, you can read and learn from its code implementation. The code is mainly concentrated under the spring-expresssion module.

When I talked about the singleton mode earlier, I mentioned that the singleton mode has many disadvantages, such as unfriendly unit testing. The coping strategy is to manage the object through the IOC container, and realize the control of the uniqueness of the object through the IOC container. In fact, the singleton implemented in this way is not a real singleton, and its unique scope is only within the same IOC container.

In addition, Spring also uses observer mode, template mode, responsibility chain mode, and proxy mode. Among them, the observer mode and the template mode have been discussed in detail in the previous lesson.
In fact, in Spring, as long as the class with Template suffix is ​​basically a template class, and most of them are implemented with Callback callback, such as JdbcTemplate, RedisTemplate and so on. The application of the remaining two patterns in Spring should be well known. The application of chain of responsibility pattern in Spring is interceptor (Interceptor), and the classic application of proxy pattern is AOP.

key review

Well, that's all for today's content. Let's summarize and review together, what you need to focus on.
There are 11 design patterns we mentioned today, they are Adapter Pattern, Strategy Pattern, Combination Pattern, Decorator Pattern, Factory Pattern, Singleton Pattern, Interpreter Pattern, Observer Pattern, Template Pattern, Responsibility Chain Pattern, Proxy Patterns, which basically account for half of the 23 design patterns. This is just what I know. In fact, there may be more design patterns used by Spring. You see, the design pattern is not "fancy fists and embroidered legs", it does have many applications in actual project development, and it can indeed play a big role.

Again, for today's content, you don't need to remember which class uses which design pattern. You just need to follow my explanation and understand the application scenarios of each design pattern in Spring. When you see similar code, you can immediately recognize which design pattern it uses; when you see similar application scenarios, you can immediately reflect which pattern to use to solve it, which means that you have mastered it well enough.

class disscussion

As we mentioned earlier, in addition to pure constructors, constructors plus setter methods, and factory methods, there is another pattern that is often used to create objects, the Builder pattern. If we let Spring support the creation of beans through the Builder pattern, how should we write code and configure it? Can you design it?

Guess you like

Origin blog.csdn.net/qq_32907491/article/details/131365710