设计模式

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。同时设计模式也是软件开发人员在软件开发过程中面临的一般问题的解决方案。

策略模式

策略模式主要是会定义一系列的算法或策略,其中的算法和策略都是独立封装,互不影响的。通过策略模式,可以在运行时选择不同的策略进行匹配,而不需要修改客户端的代码。

我们可以参考 xxl-job 里的路由策略编写逻辑,其中的路由策略就是一个标准的策略模式例子。现给出 xxl-job 的路由策略结构图。

xxl-job策略模式结构图

同时观察 xxl-job 的源码,XxlJobTrigger 中的 processTrigger() 方法中有一段关于路由策略的逻辑

  1. 根据传入的 jobInfo 获取到路由策略的参数
  2. 如果是分片广播,则for循环调用外部传入的index获取执行器地址并调用执行器。
  3. 反之根据参数获取路由策略调用获取对应地址并调用即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){

// param
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy
ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy
String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null;

// 1、save log-id
XxlJobLog jobLog = new XxlJobLog();
jobLog.setJobGroup(jobInfo.getJobGroup());
jobLog.setJobId(jobInfo.getId());
jobLog.setTriggerTime(new Date());
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog);
logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());

// 2、init trigger-param
TriggerParam triggerParam = new TriggerParam();
triggerParam.setJobId(jobInfo.getId());
triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
triggerParam.setExecutorParams(jobInfo.getExecutorParam());
triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
triggerParam.setLogId(jobLog.getId());
triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime());
triggerParam.setGlueType(jobInfo.getGlueType());
triggerParam.setGlueSource(jobInfo.getGlueSource());
triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
triggerParam.setBroadcastIndex(index);
triggerParam.setBroadcastTotal(total);

// 3、init address
String address = null;
ReturnT<String> routeAddressResult = null;
if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
if (index < group.getRegistryList().size()) {
address = group.getRegistryList().get(index);
} else {
address = group.getRegistryList().get(0);
}
} else {
routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
address = routeAddressResult.getContent();
}
}
} else {
routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
}

// 4、trigger remote executor
ReturnT<String> triggerResult = null;
if (address != null) {
triggerResult = runExecutor(triggerParam, address);
} else {
triggerResult = new ReturnT<String>(ReturnT.FAIL_CODE, null);
}

这段代码中有一个地方是用来定位到该任务的路由策略的,其中 ExecutorRouteStrategyEnum 就是一个切入点。

1
routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());

在 ExecutorRouteStrategyEnum 这个枚举类里枚举了 xxl-job 目前的策略名称,还有一个 match() 方法通过策略名称进行匹配,找到该策略所对应的 route() 方法执行相应的路由策略逻辑。

1
2
3
4
5
6
7
8
9
10
public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){
if (name != null) {
for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) {
if (item.name().equals(name)) {
return item;
}
}
}
return defaultItem;
}

策略模式的优势

  • 策略模式中定义了一个公共的抽象类,每个策略都可以通过重写抽象类中的方法实现其算法逻辑。

  • 可以以相同的方式调用所有策略,减少了各种策略类与使用策略之间的耦合。

  • 策略都是相对独立的类,策略之间互不影响,可以随业务需求拓展,简化了单元测试。

工厂模式

工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

现有一个需求,要求对前端传进来的参数做校验,并组装这些参数供后续使用,每次传过来的参数根据规则不同有增减。这个需求可以用工厂模式实现,我们不关心不同的参数如何校验和组装,只想要接收前端传来的参数,最终以它想要的形式返回回去。

现给出使用工厂模式实现该需求的结构图:

rpa业务策略结构图

基于工厂模式,创建一个策略接口,用于规范化策略的实现。

定义策略工厂类,用于创建并获取策略。

导入任务时,向业务策略工厂类传递业务策略类别,获取并执行相应的业务策略,最后组装并返回任务字段信息。

工厂模式和策略模式的区别

工厂模式关注的是对象的创建:好比想要一台电脑、想要一台计算器,工厂给你生产出来。

策略模式关注的是行为的封装:好比要开发一台电脑或者计算器,你想实现加减法。是 a+b 还是 b+a,由你决定;是 a×10÷10+b 还是 (a+b),也由你决定。对外暴露的就是加减功能,用户能知道有这俩功能就行。

工厂模式的优势

  • 对于复杂的参数的构造对象,可以很好地对外层屏蔽代码的复杂性。
  • 可以自定义对象实例化规则,例如在对象存在某个字段时做特殊操作,在工厂中统一处理。
  • 上层代码完全不了解实现层的情况,因此并不会影响到上层代码的调用,达到解耦目的。

代理模式

代理模式是为其他对象提供一种代理以控制对这个对象的访问。

现给出代理模式的结构图:

代理模式结构图

代理模式的简单实现

代理模式分为静态代理和动态代理。

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
  • 动态:在程序运行时,运用反射机制动态创建而成

静态代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//业务接口
interface DateService {
void add();
void del();
}

class DateServiceImplA implements DateService {
@Override
public void add() {
System.out.println("成功添加!");
}

@Override
public void del() {
System.out.println("成功删除!");
}
}

class DateServiceProxy implements DateService {
DateService server;

public DateServiceProxy(DateService server) {
this.server = server;
}

@Override
public void add() {
server.add();
System.out.println("程序执行add方法,记录日志.");
}
@Override
public void del() {
server.del();
System.out.println("程序执行del方法,记录日志.");
}
}

//客户端
public class Test {
public static void main(String[] args) {
DateService service = new DateServiceProxy();
service.add();
service.del();
}
}

动态代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//业务接口
interface DateService {
void add();
void del();
}

class DateServiceImplA implements DateService {
@Override
public void add() {
System.out.println("成功添加!");
}

@Override
public void del() {
System.out.println("成功删除!");
}
}

class ProxyInvocationHandler implements InvocationHandler {
private DateService service;

public ProxyInvocationHandler(DateService service) {
this.service = service;
}

public Object getDateServiceProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
var result = method.invoke(service, args); // 让service调用方法,方法返回值
System.out.println(proxy.getClass().getName() + "代理类执行" + method.getName() + "方法,返回" + result + ",记录日志!");
return result;
}
}

//客户端
public class Test {
public static void main(String[] args) {
DateService serviceA = new DateServiceImplA();
DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
serviceProxy.add();
serviceProxy.del();
}
}
/*
成功添加!
$Proxy0代理类执行add方法,返回null,记录日志!
成功删除!
$Proxy0代理类执行del方法,返回null,记录日志!
*/

我之前曾经写过一个手写缓存的项目,在自己创建一个缓存结构的时候,实现了Spring动态代理

生成缓存测试入口:

1
2
3
4
5
6
7
8
@Test
public void expireTest() throws InterruptedException {
ICache<String, String> cache = CacheBs.<String, String>newInstance()
.size(3)
.build();
cache.put("1","1");
cache.put("2","2");
}

自定义的缓存类CacheBs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ICache<K,V> build(){
Cache<K, V> cache = new Cache<>();
cache.map(map);
cache.sizeLimit(size);
cache.cacheEvict(evict);
cache.removeListeners(removeListeners);
cache.load(load);
cache.persist(persist);
cache.slowListeners(slowListeners);

//初始化
cache.init();

return CacheProxy.getProxy(cache);
}

缓存代理类 CacheProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取对象代理
* @param cache 对象代理
* @return 代理信息
*/
@SuppressWarnings("all")
public static <K,V> ICache<K,V> getProxy(final ICache<K,V> cache){
if(ObjectUtil.isNull(cache)){
return (ICache<K, V>) new NoneProxy(cache).proxy();
}
final Class clazz = cache.getClass();

//如果targetClass本身是个接口或者targetClass是JDK Proxy生成的,则使用JDK动态代理
//参考 spring AOP 判断
if(clazz.isInterface() || Proxy.isProxyClass(clazz)){
return (ICache<K, V>) new DynamicProxy(cache).proxy();
}
return (ICache<K, V>) new CglibProxy(cache).proxy();

}

JDK 代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DynamicProxy implements InvocationHandler, ICacheProxy {

/**
* 被代理的对象
* @param null
* @return
*/
private final ICache target;

public DynamicProxy(ICache target) {
this.target = target;
}

@Override
public Object proxy() {
//要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象调用其方法的
InvocationHandler handler = new DynamicProxy(target);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),target.getClass().getInterfaces(),handler);
}

/**
* 这种方式虽然实现了异步执行,但是存在一个缺陷:
* 强制用户返回值为 Future 的子类
* 如何实现才能不影响原来的值??
* @param proxy
* @param method
* @param args
* @return
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

ICacheProxyBsContext context = CacheProxyBsContext.newInstance()
.method(method).params(args).target(target);

return CacheProxyBs.newInstance().context(context).execute();
}
}

Cglib 代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CglibProxy implements MethodInterceptor, ICacheProxy {

/**
* 被代理的对象
*/
private final ICache target;

public CglibProxy(ICache target){
this.target = target;
}
@Override
public Object proxy() {
Enhancer enhancer = new Enhancer();
//目标对象类
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
//通过字节码技术创建目标对象类的子类实例作为代理
return enhancer.create();
}

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
ICacheProxyBsContext context = CacheProxyBsContext.newInstance()
.method(method).params(objects).target(target);
return CacheProxyBs.newInstance().context(context).execute();
}
}

代理模式应用

远程代理,为一个对象在不同的地址空间提供局部代表,可以隐藏一个对象存在于不同地址空间的事实。

虚拟代理,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层,通过它来存放实例化需要很长时间的真实对象。

安全代理,用来控制真实对象访问时的权限,一般用于对象应该有不同的访问权限的时候。

智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它(C++智能指针);例如上面的房产中介代理就是一种智能指引代理,代理附加了一些额外的功能,例如带看房等。

代理模式的优点

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性

单例模式

单例模式主要是用于解决一个全局使用的类频繁地创建与销毁的问题,它保证了一个类只有一个实例,并提供一个访问它的访问点。

单例模式使用了双端检锁的方式实现,其中 Spring 的 bean 单例作用域就是一个典型的单例模式。

Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。

Spring 实现单例的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

}
}
}

多线程下的双端检锁

下面的代码在多线程环境下不是原子执行的。

1
instance=new DoubleCheckSingleton();

正常的底层执行顺序会转变成三步:

1
2
3
4
5
(1) 给DoubleCheckSingleton类的实例instance分配内存

(2) 调用实例instance的构造函数来初始化成员变量

(3) 将instance指向分配的内存地址

假如现在有线程A和线程B,线程A 按照 123 的顺序执行,不会出任何问题。

但是如果线程A在重排序的情况下,上面的执行顺序会变成1,3,2。现在假设A线程按1,3,2三个步骤顺序执行,当执行到第二步的时候。B线程开始调用这个方法,那么在第一个null的检查的时候,就有可能看到这个实例不是null,然后直接返回这个实例开始使用,但其实是有问题的,因为对象还没有初始化,状态还处于不可用的状态,故而会导致异常发生。

要解决这个问题,可以通过volatile关键词来避免指令重排序,那么在变量赋值之后,会有一个内存屏障。也就说只有执行完1,2,3步操作后,读取操作才能看到,读操作不会被重排序到写操作之前。这样以来就解决了对象状态不完整的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

单例模式优势

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

定义私有化构造函数防止类被实例化

当在一个类中定义了私有构造函数时,它将限制其他代码在类外部直接实例化该类的对象。这意味着除了类内部的代码,其他代码无法通过调用类的构造函数来创建类的实例。

通过定义私有构造函数,可以实现以下几个方面的控制:

  1. 防止类被意外地实例化:私有构造函数可以确保类的实例化只能在类的内部进行。这样可以防止其他代码意外地创建该类的对象,确保该类的使用符合设计意图。
  2. 实现单例模式:单例模式是一种设计模式,它要求一个类只能有一个实例。通过在类中定义私有构造函数,并在类内部控制实例的创建和访问,可以确保只有一个类的实例存在。
  3. 提供静态工厂方法:私有构造函数可以与静态工厂方法一起使用,使类的实例化过程更加灵活和可控。静态工厂方法是类中的一个静态方法,用于创建和返回类的实例,可以在创建实例之前进行一些额外的逻辑判断或操作。

举例:

假设我们有一个名为 “Logger” 的日志记录器类,我们希望在整个应用程序中只有一个日志记录器实例。我们可以使用单例模式来实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Logger
{
private static Logger instance;
private List<string> logs;

private Logger()
{
logs = new List<string>();
}

public static Logger Instance
{
get
{
if (instance == null)
{
instance = new Logger();
}
return instance;
}
}

public void Log(string message)
{
logs.Add(message);
Console.WriteLine("Log: " + message);
}

public void PrintLogs()
{
Console.WriteLine("Logs:");
foreach (string log in logs)
{
Console.WriteLine(log);
}
}
}

在上述代码中,Logger 类被设计为单例模式。它具有一个私有的构造函数,以及一个公共的静态属性 Instance,用于获取 Logger 类的实例。当第一次访问 Instance 属性时,将创建一个 Logger 实例,并在后续的访问中返回该实例。

现在,我们可以在应用程序的任何地方使用 Logger 类来记录日志,而无需多次实例化它。例如:

1
2
3
Logger.Instance.Log("Error occurred: NullReferenceException");
Logger.Instance.Log("Warning: Invalid input detected");
Logger.Instance.PrintLogs();

通过 Logger.Instance,我们可以在不同的代码部分获取同一个 Logger 实例,并使用 Log 方法记录日志信息。最后,我们可以使用 PrintLogs 方法打印所有已记录的日志。

这样,通过单例模式,我们确保了整个应用程序中只有一个 Logger 实例存在,避免了多个日志记录器实例导致的资源浪费或日志信息的不一致性。