2023年3月

简介

建造者模式(Builder Pattern),也叫生成器模式,属于创建型模式。它使用多个简单的对象一步一步构建成一个复杂的对象。它允许你使用相同的创建代码生成不同类型和形式的对象。

当你希望使用代码创建不同形式的产品 (例如各种商品和订单) 时, 一些基本部件不会变,而其组合经常变化的时候,就可以考虑建造者模式。

作用

  1. 当需要创建复杂对象的时候,由各个部分的子对象来逐步构建,以适应复杂多变的情况。
  2. 具体的建造者类之间是相互独立的,这有利于系统的扩展。
  3. 将变与不变分离开。主管类隐藏了产品构造细节, 客户端只需要将一个生成器与主管类关联,就能从生成器处构造对象。

实现步骤

  1. 定义主管类,可以制造所有形式的产品。
  2. 在基本生成器接口中声明创建产品的步骤。
  3. 为每个形式的产品创建具体生成器类,并实现其构造步骤。
  4. 客户端同时创建生成器和主管类,所有产品都遵循相同的接口,构造结果通过主管类获取。

UML

代码

创建建造者接口

//Builder.java 建造者接口,定义基本建造方法
public interfaceBuilder {public voidreset();public voidsetName(String name);public voidsetScreen(Integer[] screen);public voidsetGPU(Integer no); 
}

具体建造者类,可以多个

//ManualBuilder.java 使用手册建造者类也实现了建造者接口
public class ManualBuilder implementsBuilder {privateManual manual;public voidreset() {this.manual = newManual();
}
public voidsetName(String name) {this.manual.setName(name);
}
public voidsetScreen(Integer[] screen) {this.manual.setScreen(screen);
}
public voidsetGPU(Integer no) {this.manual.setGpuType(no);
}
publicManual getProduct() {return this.manual;
}
}
//PhoneBuilder.java 手机建造者实现了建造者接口 public class PhoneBuilder implementsBuilder {privatePhone phone;public voidreset() {this.phone = newPhone();
}
public voidsetName(String name) {this.phone.setName(name);
}
public voidsetScreen(Integer[] screen) {this.phone.setScreen(screen);
}
public voidsetGPU(Integer no) {this.phone.setGpuType(no);
}
publicPhone getProduct() {return this.phone;
}
}

定义具体产品类,不同建造者建造不同产品

//Manual.java 手册产品类
public classManual {private String name = "PhoneManualName";private Integer[] screen = { 0, 0};private Integer gpuType = 0;private Integer pages = 0;public voidsetName(String name) {this.name =name;
}
publicString getName() {return this.name;
}
public voidsetScreen(Integer[] screen) {this.screen =screen;
}
publicInteger[] getScreen() {return this.screen;
}
......

}
//Phone.java 手机产品类 public classPhone {private String name = "PhoneName";private Integer[] screen = { 0, 0};private Integer gpuType = 0;public voidsetName(String name) {this.name =name;
}
publicString getName() {return this.name;
}
public voidsetScreen(Integer[] screen) {this.screen =screen;
}
publicInteger[] getScreen() {return this.screen;
}
......

}

指挥调度类

//Director.java 指挥调度类,负责利用建造者建造产品,隔离需求与功能
public classDirector {//建造phone1
  public voidbuildIPhone(Builder builder) {
builder.reset();
Integer[] screen
= { 120, 500};
builder.setName(
"iPhone");
builder.setScreen(screen);
builder.setGPU(
100);
}
//建造phone2 public voidbuildHuaweiPhone(Builder builder) {
builder.reset();
Integer[] screen
= { 130, 600};
builder.setName(
"HuaweiPhone");
builder.setScreen(screen);
builder.setGPU(
102);
}
//建造phone3 public voidbuildMiPhone(Builder builder) {
builder.reset();
Integer[] screen
= { 120, 650};
builder.setName(
"MiPhone");
builder.setScreen(screen);
builder.setGPU(
103);
}

}

测试调用

    /*** 建造者模式是使用多个简单的对象一步一步构建出一个复杂的对象来。
* 分为主管类和建造这类,主管类负责具体指挥调度,建造负责具体实施。
* 主管类通过一步一步调用各种建造者实现复杂对象。
*/ //声明指挥者 Director director = newDirector();//创建手机 PhoneBuilder phoneBuilder = newPhoneBuilder();
director.buildMiPhone(phoneBuilder);
Phone miPhone
=phoneBuilder.getProduct();
System.out.println(
"miPhone:" + miPhone.getName() + " | " +miPhone.getGpuType().toString());//创建手册 ManualBuilder manualBuilder = newManualBuilder();
director.buildMiPhone(manualBuilder);
Manual manual
=manualBuilder.getProduct();
System.out.println(
"manual:" + manual.getName() + " | " + manual.getGpuType().toString());

更多语言版本

不同语言实现设计模式:
https://github.com/microwind/design-pattern

先说一下,
为什么写这篇文章?
最近在写一个
Http协议栈
当涉及
CRLF
控制字符写入时,发现自己对
CRLF

\r\n
的关系不太了解,因此决定详细学习一下;查阅资料的同时,又遇到
UTF-8

ASCII
编码的疑问。

一、ASCII 编码

ASCII (American Standard Code for Information Interchange 美国信息交换标准代码)
由128个字符构成,是基于拉丁字母的一套电脑编码系统,
主要用于显示现代英语
,其对应的国际标准为
ISO/IEC 646

ASCII 码表

ASCII 由电报码发展而来,第一版标准发布于1963年,最后一次更新则是在1986年,至今为止共
128个字符

  • 其中
    33个
    字符为
    不可显示的控制字符
    ,主要用于控制设备或调整文本格式;
  • 在33个字符之外是
    95个可显示字符

    包含
    26个英文大小写字母

    10个阿拉伯数字
    以及包含
    空格
    在内的
    33个标点与特殊符号

1.1 EASCII

EASCII(Extended ASCII)
是ASCII码的扩展版本,其将ASCII码由7位扩充为8位,由128个字符扩展为256个字符,因此EASCII可以部分支持西欧语言。

1.2 转义字符

ASCII码表中的转义字符是一种约定写法
,是以反斜杠
\
开头的
特殊字符序列
,作用是告诉计算机如何显示与输入控制字符。

33个控制字符以及对应的转义字符

转义字符的由来可以追溯到电传打字机和电传打字设备的使用。
在这些设备中,许多字符都是由多个部分组成的,需要多次按键才能输入。例如,换行符通常需要按下回车键和换行键,而退格符需要按下后退键。为了简化这个过程,制定了一些简化输入这些字符的规则,这些规则最终成为了ASCII转义字符的标准。

转义字符并非ASCII控制字符的某种编码方式,而是一种约定俗成的写法
。当向计算机输入转义字符时(如
\r\n
),其将自动将转移字符替换为
CRLF
控制字符。
以下使用Java语言编写了一个测试程序,当计算机遇到
\r\n
时,计算机自动将其替换为了
CRLF
控制字符,
每个控制字符占一个字节

转义字符自动替换为回车换行符

二、Unicode 编码

Unicode (The Unicode Standard)
译作万国码、统一字元码、统一字符编码,是信息技术领域的业界标准,其整理、编码了世界上大部分的文字系统,使得电脑能以统一字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案。

Unicode由非营利机构Unicode联盟(Unicode Consortium)维护,该机构致力让Unicode标准取代既有的字符编码方案,因为既有方案编码空间有限,不适用于多语环境。Unicode伴随着通用字符集
ISO/IEC 10646
的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符,目前最新的版本为2022年9月公布的15.0.0,已经收录
超过14万个字符

Unicode 编码是一个二进制字符集
,其
字符占用2~3个字节
。目前分为
17个组
进行编排,每个组称为一个平面,
每个平面拥有65536个编码点
,且当前只使用了少数平面。
因此,Unicode有足够的编码空间,可以将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码,是名副其实的万国码。

Unicode字符平面映射

三、UTF-8 编码

UTF-8
的英文全称是(
8-bit Unicode Transformation Format
),其
为 Unicode 的实现方式之一
,也是目前互联网上使用最广的一种 Unicode 编码的实现方式。

为什么UTF-8成为互联网使用最广泛的一种编码方式?

前边说过
Unicode 编码是一个二进制字符集
,其只规定了字符的二进制代码,却没有规定这些二进制代码应该如何存储。

比如:

  • 大写英文字母A,其对应的ASCII二进制编码为8位 ( 01000001 ),也就是说表示ASCII字符需1个字节 ;
  • 汉字

    的 Unicode 十六进制标识为
    590F
    ,二进制表示有16位(
    0101100100006666661
    ),也就是说采用Unicode表示这个字符至少需要2个字节;
  • 而对于Unicode编码第14~16平面的字符,可能需要3个字节表示。

因此,在计算机中如何进行Unicode编码的存储,出现了以下两个问题:

  • 若所有的字符均按3个字节进行表示
    :由于计算机存储空间有限,将造成不小的空间浪费;
  • 若按1~3字节对所有字符进行表示
    :计算机该如何区分ASCII 与 Unicode(计算机如何知道是一个字节表示一个字符,还是三个字节表示一个字符)?

在这种情况下UTF-8应运而生,UTF-8 最大的特点
是一种可变长的编码方式
,其使用
1~4个字节
表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

  • 对于单字节的符号,字节的第一位设为
    0
    ,后面7位为这个符号的 Unicode 码。
    因此,对于英语字母UTF-8 编码和 ASCII 码是相同的。
  • 对于
    n
    字节的符号(
    n > 1
    ):
    第一个字节的前
    n
    位都设为
    1
    ,第
    n + 1
    位设为
    0

    后面字节的前两位一律设为
    10

    剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母
x
表示可用编码的位:

Unicode符号范围        |   UTF-8编码方式
(十六进制)             |  (二进制)
----------------------+----------------------------------
0000 0000 ~ 0000 007F | 0xxxxxxx
0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF | 6666660xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF | 66666610xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8编码为什么最多占用4个字节?

前边我们了解到 Unicode 编码的17个平面,最多使用3个字节可全部表示。但为什么 UTF-8 编码最多却是要使用4个字节呢?

这是由 UTF-8 编码的编码规则决定的,对于编码点
U+10000 到 U+10FFFF
范围内的字符,UTF-8 编码使用了 4 个字节来表示。

Unicode符号范围        |   UTF-8编码方式
(十六进制)             |  (二进制)
----------------------+----------------------------------
0001 0000 ~ 0010 FFFF | 66666610xxx 10xxxxxx 10xxxxxx 10xxxxxx

其中:

  • 前面的字节以“66666610”开始,用于标识使用了 4 个字节来表示一个字符。
  • 后面的 3 个字节的前两个字节以“10”开始,用于标识这是一个多字节字符的后续字节。

使用四个字节的好处还在于,可以为未来的 Unicode 字符预留更多的编码空间。这是因为,Unicode 是一个持续发展的标准,每年都有新的字符被添加到其中。如果所有的字符都只使用三个字节表示,那么当 Unicode 标准新增更多字符时,就会出现编码空间不足的问题。因此,使用四个字节来表示这些字符可以保证未来的字符也能够被正确编码。

参考

wikipedia ASCII:
https://zh.wikipedia.org/zh-hans/ASCII

wikipedia Unicode:
https://zh.wikipedia.org/zh-cn/Unicode

wikipedia Unicode字符平面映射:
https://zh.wikipedia.org/zh-hans/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84

wikipedia UTF-8:
https://zh.wikipedia.org/wiki/UTF-8

ISO/IEC 646 ASCII:
http://www.kostis.net/charsets/iso646.irv.htm

ISO/IEC 10646 Unicode:
http://www.kostis.net/charsets/iso10646.htm

ascii-code:
https://www.ascii-code.com/

Unicode 编码表:
http://www.chi2ko.com/tool/CJK.htm

ASCII Unicode 和 UTF-8:
https://blog.csdn.net/qq_38310578/article/details/78433726

= THE END =

文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,欢迎关注我的公众号。

欢迎关注我的公众号

重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。

重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。

为了方便说明,先假设我们想要进行重试的方法如下:

@Slf4j
@Component
public class HelloService {
 
    private static AtomicLong helloTimes = new AtomicLong();
 
    public String hello(){
        long times = helloTimes.incrementAndGet();
        if (times % 4 != 0){
            log.warn("发生异常,time:{}", LocalTime.now() );
            throw new HelloRetryException("发生Hello异常");
        }
        return "hello";
    }
}

调用处:

@Slf4j
@Service
public class HelloRetryService implements IHelloService{
 
    @Autowired
    private HelloService helloService;
 
    public String hello(){
        return helloService.hello();
    }
}

也就是说,这个接口每调4次才会成功一次。

手动重试

先来用最简单的方法,直接在调用的时候进重试:

// 手动重试
public String hello(){
    int maxRetryTimes = 4;
    String s = "";
    for (int retry = 1; retry <= maxRetryTimes; retry++) {
        try {
            s = helloService.hello();
            log.info("helloService返回:{}", s);
            return s;
        } catch (HelloRetryException e) {
            log.info("helloService.hello() 调用失败,准备重试");
        }
    }
    throw new HelloRetryException("重试次数耗尽");
}
发生异常,time:10:17:21.079413300
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
helloService返回:hello
service.helloRetry():hello

程序在极短的时间内进行了4次重试,然后成功返回。

这样虽然看起来可以解决问题,但实践上,由于没有重试间隔,很可能当时依赖的服务尚未从网络异常中恢复过来,所以极有可能接下来的几次调用都是失败的。

而且,这样需要对代码进行大量的侵入式修改,显然,不优雅。

代理模式

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。

所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。你会发现又是代理。

@Slf4j
public class HelloRetryProxyService implements IHelloService{
   
    @Autowired
    private HelloRetryService helloRetryService;
    
    @Override
    public String hello() {
        int maxRetryTimes = 4;
        String s = "";
        for (int retry = 1; retry <= maxRetryTimes; retry++) {
            try {
                s = helloRetryService.hello();
                log.info("helloRetryService 返回:{}", s);
                return s;
            } catch (HelloRetryException e) {
                log.info("helloRetryService.hello() 调用失败,准备重试");
            }
        }
        throw new HelloRetryException("重试次数耗尽");
    }
}

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了,分工明确。比如,现在想要在重试之间加上一个延迟,只需要做一点点修改即可:

@Override
public String hello() {
    int maxRetryTimes = 4;
    String s = "";
    for (int retry = 1; retry <= maxRetryTimes; retry++) {
        try {
            s = helloRetryService.hello();
            log.info("helloRetryService 返回:{}", s);
            return s;
        } catch (HelloRetryException e) {
            log.info("helloRetryService.hello() 调用失败,准备重试");
        }
        // 延时一秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    throw new HelloRetryException("重试次数耗尽");
}

代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。

如果每个类都写这么一长串类似的代码,显然,不优雅!

JDK动态代理

这时候,动态代理就闪亮登场了。只需要写一个代理处理类就ok了。

@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
 
    private final Object subject;
 
    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        return null;
    }
 
    /**
     * 获取动态代理
     *
     * @param realSubject 代理对象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }
 
}

咱们测试一下:

@Test
public void helloDynamicProxy() {
    IHelloService realService = new HelloService();
    IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);
 
    String hello = proxyService.hello();
    log.info("hello:{}", hello);
}


输出结果如下:
hello times:1
发生异常,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
发生异常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
发生异常,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello

在重试了4次之后输出了Hello,符合预期。

动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。

不过不要高兴的太早,这里因为被代理的HelloService是一个简单的类,没有依赖其它类,所以直接创建是没有问题的,但如果被代理的类依赖了其它被Spring容器管理的类,则这种方式会抛出异常,因为没有把被依赖的实例注入到创建的代理实例中。

这种情况下,就比较复杂了,需要从Spring容器中获取已经装配好的,需要被代理的实例,然后为其创建代理类实例,并交给Spring容器来管理,这样就不用每次都重新创建新的代理类实例了。

话不多说,撸起袖子就是干。

新建一个工具类,用来获取代理实例:

@Component
public class RetryProxyHandler {
 
    @Autowired
    private ConfigurableApplicationContext context;
 
    public Object getProxy(Class clazz) {
        // 1. 从Bean中获取对象
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
        Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
        Set<Map.Entry<String, Object>> entries = beans.entrySet();
        if (entries.size() <= 0){
            throw new ProxyBeanNotFoundException();
        }
        // 如果有多个候选bean, 判断其中是否有代理bean
        Object bean = null;
        if (entries.size() > 1){
            for (Map.Entry<String, Object> entry : entries) {
                if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){
                    bean = entry.getValue();
                }
            };
            if (bean != null){
                return bean;
            }
            throw new ProxyBeanNotSingleException();
        }
 
        Object source = beans.entrySet().iterator().next().getValue();
        Object source = beans.entrySet().iterator().next().getValue();
 
        // 2. 判断该对象的代理对象是否存在
        String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;
        Boolean exist = beanFactory.containsBean(proxyBeanName);
        if (exist) {
            bean = beanFactory.getBean(proxyBeanName);
            return bean;
        }
 
        // 3. 不存在则生成代理对象
        bean = RetryInvocationHandler.getProxy(source);
 
        // 4. 将bean注入spring容器
        beanFactory.registerSingleton(proxyBeanName, bean);
        return bean;
    }
}

使用的是JDK动态代理:

@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
 
    private final Object subject;
 
    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("retry times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        return null;
    }
 
    /**
     * 获取动态代理
     *
     * @param realSubject 代理对象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }
 
}

至此,主要代码就完成了,修改一下HelloService类,增加一个依赖:

@Slf4j
@Component
public class HelloService implements IHelloService{
 
    private static AtomicLong helloTimes = new AtomicLong();
 
    @Autowired
    private NameService nameService;
 
    public String hello(){
        long times = helloTimes.incrementAndGet();
        log.info("hello times:{}", times);
        if (times % 4 != 0){
            log.warn("发生异常,time:{}", LocalTime.now() );
            throw new HelloRetryException("发生Hello异常");
        }
        return "hello " + nameService.getName();
    }
}

NameService其实很简单,创建的目的仅在于测试依赖注入的Bean能否正常运行。

@Service
public class NameService {
 
    public String getName(){
        return "Frank";
    }
}

测试一下:

@Test
public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);
}


结果:
hello times:1
发生异常,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
发生异常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
发生异常,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank

完美,这样就不用担心依赖注入的问题了,因为从Spring容器中拿到的Bean对象都是已经注入配置好的。当然,这里仅考虑了单例Bean的情况,可以考虑的更加完善一点,判断一下容器中Bean的类型是Singleton还是Prototype,如果是Singleton则像上面这样进行操作,如果是Prototype则每次都新建代理类对象。

另外,这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了。

Spring AOP

想要无侵入式的修改原有逻辑?想要一个注解就实现重试?用Spring AOP不就能完美实现吗?使用AOP来为目标调用设置切面,即可在目标方法调用前后添加一些额外的逻辑。

先创建一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    int retryTimes() default 3;
    int retryInterval() default 1;
}

有两个参数,retryTimes 代表最大重试次数,retryInterval代表重试间隔。

@Retryable(retryTimes = 4, retryInterval = 2)
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello " + nameService.getName();
}

接着,进行最后一步,编写AOP切面:

@Slf4j
@Aspect
@Component
public class RetryAspect {
 
    @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
    private void retryMethodCall(){}
 
    @Around("retryMethodCall()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        // 获取重试次数和重试间隔
        Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class);
        int maxRetryTimes = retry.retryTimes();
        int retryInterval = retry.retryInterval();
 
        Throwable error = new RuntimeException();
        for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
            try {
                Object result = joinPoint.proceed();
                return result;
            } catch (Throwable throwable) {
                error = throwable;
                log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);
            }
            Thread.sleep(retryInterval * 1000);
        }
        throw new RetryExhaustedException("重试次数耗尽", error);
    }
}

开始测试:

@Autowired
private HelloService helloService;
 
@Test
public void helloAOP(){
    String hello = helloService.hello();
    log.info("hello:{}", hello);
}


打印结果:
hello times:1
发生异常,time:16:49:30.224649800
调用发生异常,开始重试,retryTimes:1
hello times:2
发生异常,time:16:49:32.225230800
调用发生异常,开始重试,retryTimes:2
hello times:3
发生异常,time:16:49:34.225968900
调用发生异常,开始重试,retryTimes:3
hello times:4
hello:hello Frank

这样就相当优雅了,一个注解就能搞定重试,简直不要更棒。

Spring 的重试注解

实际上Spring中就有比较完善的重试机制,比上面的切面更加好用,还不需要自己动手重新造轮子。

那让我们先来看看这个轮子究竟好不好使。

先引入重试所需的jar包:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

然后在启动类或者配置类上添加@EnableRetry注解,接下来在需要重试的方法上添加@Retryable注解(嗯?好像跟我自定义的注解一样?竟然抄袭我的注解【手动狗头】)

@Retryable
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello " + nameService.getName();
}

默认情况下,会重试三次,重试间隔为1秒。当然我们也可以自定义重试次数和间隔。这样就跟我前面实现的功能是一毛一样的了。

但Spring里的重试机制还支持很多很有用的特性,比如说,可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。默认为空,会对所有异常都重试。

@Retryable{value = {HelloRetryException.class}}
public String hello(){
    ...
}

也可以使用include和exclude来指定包含或者排除哪些异常进行重试。

可以用maxAttemps指定最大重试次数,默认为3次。

可以用interceptor设置重试拦截器的bean名称。

可以通过label设置该重试的唯一标志,用于统计输出。

可以使用exceptionExpression来添加异常表达式,在抛出异常后执行,以判断后续是否进行重试。

此外,Spring中的重试机制还支持使用backoff来设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数。

举个例子:

@Retryable(value = {HelloRetryException.class}, maxAttempts = 5,
           backoff = @Backoff(delay = 1000, multiplier = 2))
public String hello(){
    ...
}

该方法调用将会在抛出HelloRetryException异常后进行重试,最大重试次数为5,第一次重试间隔为1s,之后以2倍大小进行递增,第二次重试间隔为2s,第三次为4s,第四次为8s。

重试机制还支持使用@Recover 注解来进行善后工作,当重试达到指定次数之后,将会调用该方法,可以在该方法中进行日志记录等操作。

这里值得注意的是,想要@Recover 注解生效的话,需要跟被@Retryable 标记的方法在同一个类中,且被@Retryable 标记的方法不能有返回值,否则不会生效。

并且如果使用了@Recover注解的话,重试次数达到最大次数后,如果在@Recover标记的方法中无异常抛出,是不会抛出原异常的。

@Recover
public boolean recover(Exception e) {
    log.error("达到最大重试次数",e);
    return false;
}

除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:

@Test
public void normalSpringRetry() {
    // 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
    Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
    exceptionMap.put(HelloRetryException.class, true);
 
    // 构建重试模板实例
    RetryTemplate retryTemplate = new RetryTemplate();
 
    // 设置重试回退操作策略,主要设置重试间隔时间
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    long fixedPeriodTime = 1000L;
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);
 
    // 设置重试策略,主要设置重试次数
    int maxRetryTimes = 3;
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
 
    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);
 
    Boolean execute = retryTemplate.execute(
        //RetryCallback
        retryContext -> {
            String hello = helloService.hello();
            log.info("调用的结果:{}", hello);
            return true;
        },
        // RecoverCallBack
        retryContext -> {
            //RecoveryCallback
            log.info("已达到最大重试次数");
            return false;
        }
    );
}

此时唯一的好处是可以设置多种重试策略:

NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
 
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
 
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
 
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
 
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
 
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
 
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

可以看出,Spring中的重试机制还是相当完善的,比上面自己写的AOP切面功能更加强大。

这里还需要再提醒的一点是,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效。

但也还是存在一定的不足,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验。

单元测试、反射

一、单元测试

1.1 单元测试快速入门

所谓单元测试,就是针对最小的功能单元,编写测试代码对其进行正确性测试。

我们想想,咱们之前是怎么进行测试的呢?

比如说我们写了一个学生管理系统,有添加学生、修改学生、删除学生、查询学生等这些功能。要对这些功能这几个功能进行测试,我们是在main方法中编写代码来测试的。

但是在main方法中写测试代码有如下的几个问题,如下图所示:

为了测试更加方便,有一些第三方的公司或者组织提供了很好用的测试框架,给开发者使用。这里给同学们介绍一种Junit测试框架。

Junit是第三方公司开源出来的,用于对代码进行单元测试的工具(IDEA已经集成了junit框架)。相比于在main方法中测试有如下几个优点。


我们知道单元测试是什么之后,接下来带领同学们使用一下。由于Junit是第三方提供的,所以我们需要把jar包导入到我们的项目中,才能使用,具体步骤如下图所示:

接下来,我们就按照上面的步骤,来使用一下.

先准备一个类,假设写了一个StringUtil工具类,代码如下

public classStringUtil{public static voidprintNumber(String name){
System.out.println(
"名字长度:"+name.length());
}
}

接下来,写一个测试类,测试StringUtil工具类中的方法能否正常使用。

public classStringUtilTest{
@Test
public voidtestPrintNumber(){
StringUtil.printNumber(
"admin");
StringUtil.printNumber(
null);
}
}

写完代码之后,我们会发现测试方法左边,会有一个绿色的三角形按钮。点击这个按钮,就可以运行测试方法。

1.2 单元测试断言

接下来,我们学习一个单元测试的断言机制。
所谓断言:意思是程序员可以预测程序的运行结果,检查程序的运行结果是否与预期一致。

我们在StringUtil类中新增一个测试方法

 public static intgetMaxIndex(String data){if(data == null){return -1;
}
returndata.length();
}

接下来,我们在StringUtilTest类中写一个测试方法

public classStringUtilTest{
@Test
public voidtestGetMaxIndex(){int index1 = StringUtil.getMaxIndex(null);
System.out.println(index1);
int index2 = StringUtil.getMaxIndex("admin");
System.out.println(index2);
//断言机制:预测index2的结果 Assert.assertEquals("方法内部有Bug",4,index2);
}
}

运行测试方法,结果如下图所示,表示我们预期值与实际值不一致

1.3 Junit框架的常用注解

同学们,刚才我们以及学习了@Test注解,可以用来标记一个方法为测试方法,测试才能启动执行。

除了@Test注解,还有一些其他的注解,我们要知道其他注解标记的方法什么时候执行,以及其他注解在什么场景下可以使用。

接下来,我们演示一下其他注解的使用。我们在StringUtilTest测试类中,再新增几个测试方法。代码如下

public classStringUtilTest{
@Before
public voidtest1(){
System.out.println(
"--> test1 Before 执行了");
}
@BeforeClass
public static voidtest11(){
System.out.println(
"--> test11 BeforeClass 执行了");
}
@After
public voidtest2(){
System.out.println(
"--> test2 After 执行了");
}
@AfterClass
public static voidtest22(){
System.out.println(
"--> test22 AfterCalss 执行了");
}
}

执行上面的测试类,结果如下图所示,观察执行结果特点如下

1.@BeforeClass标记的方法,执行在所有方法之前
2.@AfterCalss标记的方法,执行在所有方法之后
3.@Before标记的方法,执行在每一个@Test方法之前
4.@After标记的方法,执行在每一个@Test方法之后

我们现在已经知道每一个注解的作用了,那他们有什么用呢?应用场景在哪里?

我们来看一个例子,假设我想在每个测试方法中使用Socket对象,并且用完之后,需要把Socket关闭。代码就可以按照下面的结构来设计

public classStringUtilTest{private staticSocket socket;
@Before
public voidtest1(){
System.out.println(
"--> test1 Before 执行了");
}
@BeforeClass
public static voidtest11(){
System.out.println(
"--> test11 BeforeClass 执行了");//初始化Socket对象 socket = newSocket();
}
@After
public voidtest2(){
System.out.println(
"--> test2 After 执行了");
}
@AfterCalss
public static voidtest22(){
System.out.println(
"--> test22 AfterCalss 执行了");//关闭Socket socket.close();
}
}

二、反射

什么是反射。其实API文档中对反射有详细的说明,我们去了解一下。在java.lang.reflect包中对反射的解释如下图所示

翻译成人话就是:
反射技术,指的是加载类的字节码到内存,并以编程的方法解刨出类中的各个成分(成员变量、方法、构造器等)。

反射有啥用呢?其实反射是用来写框架用的,但是现阶段同学们对框架还没有太多感觉。为了方便理解,我给同学们看一个我们见过的例子:平时我们用IDEA开发程序时,用对象调用方法,IDEA会有代码提示,idea会将这个对象能调用的方法都给你列举出来,供你选择,如果下图所示

问题是IDEA怎么知道这个对象有这些方法可以调用呢? 原因是对象能调用的方法全都来自于类,IDEA通过反射技术就可以获取到类中有哪些方法,并且把方法的名称以提示框的形式显示出来,所以你能看到这些提示了。

为反射获取的是类的信息,那么反射的第一步首先获取到类才行。由于Java的设计原则是万物皆对象,获取到的类其实也是以对象的形式体现的,
叫字节码对象
,用Class类来表示。获取到字节码对象之后,再通过字节码对象就可以获取到类的组成成分了,这些组成成分其实也是对象,其中
每一个成员变量用Field类的对象来表示

每一个成员方法用Method类的对象来表示

每一个构造器用Constructor类的对象来表示

如下图所示:

1.1 获取类的字节码

反射的第一步:是将字节码加载到内存,我们需要获取到的字节码对象。

比如有一个Student类,获取Student类的字节码代码有三种写法。不管用哪一种方式,获取到的字节码对象其实是同一个。

public classTest01_Class {
public static void main(String[] args) throwsClassNotFoundException {/*字节码文件对象获取三种方式*/ //1:根据类名.class Class c1 = Student.class;
System.out.println(c1);
System.out.println(c1.getName());
//获取全类名 包名+类名 System.out.println(c1.getSimpleName());//获取简单类名 //2:根据类的全路径 (包名+类名)获取 Class c2 = Class.forName("com.itheima.d2_reflect.Student");//c1 c2 是不是一个对象? 类只加载一次!! System.out.println(c1==c2);
//3:得到对象之后,通过对象的方法获取 运行时类 Student stu = newStudent();
Class c3
=stu.getClass();
System.out.println(c1
==c3);
}
}

1.2 获取类的构造器

同学们,上一节我们已经可以获取到类的字节码对象了。接下来,我们学习一下通过字节码对象获取构造器,并使用构造器创建对象。

获取构造器,需要用到Class类提供的几个方法,如下图所示:

想要快速记住这个方法的区别,给同学们说一下这些方法的命名规律,按照规律来记就很方便了。

get:获取
Declared: 有这个单词表示可以获取任意一个,没有这个单词表示只能获取一个public修饰的
Constructor: 构造方法的意思
后缀s: 表示可以获取多个,没有后缀s只能获取一个

话不多少,上代码。假设现在有一个Cat类,里面有几个构造方法,代码如下

public classCat{privateString name;private intage;publicCat(){

}
private Cat(String name, intage){

}
}

    1. 接下来,我们写一个测试方法,来测试获取类中所有的构造器

@Testpublic voidtestGetConstructors(){//反射第一步 获取字节码对象
        Class c= Cat.class;
//获取 所有的 public 修饰的 构造器//Constructor[] constructors = c.getConstructors();//System.out.println(constructors.length);//for (Constructor constructor : constructors) {//System.out.println("构造器全称:"+constructor);//System.out.println("构造器名称:"+constructor.getName());//获取构造器的名字//System.out.println("参数个数:"+constructor.getParameterCount());//}//获取 所有存在的 构造器 (无所谓修饰符) Constructor[] constructors =c.getDeclaredConstructors();
System.out.println(
"总共:"+constructors.length+"个构造器");for(Constructor constructor : constructors) {
System.out.println(
"构造器全称:"+constructor);
System.out.println(
"构造器名称:"+constructor.getName());//获取构造器的名字 System.out.println("参数个数:"+constructor.getParameterCount());
System.out.println(
"==============================");
}

}

运行测试方法打印结果如下

    1. 刚才演示的是获取Cat类中所有的构造器,接下来,我们演示单个构造器试一试

//获取指定的构造器
@Testpublic void testContructor() throwsNoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {//获取构造器(方法 )  是不需要指定名字的 因为构造方法名字是类名!!!
//第一步 获取 字节码对象
        Class c = Cat.class;//获取 无参这个构造器  public
        Constructor constructor1 =c.getConstructor();//打印构造器 相关信息
        System.out.println("构造器全称:"+constructor1);
System.out.println(
"构造器名称:"+constructor1.getName());//获取构造器的名字 System.out.println("参数个数:"+constructor1.getParameterCount());

Cat cat1
=(Cat)constructor1.newInstance();
System.out.println(
"查看对象:"+cat1);
System.out.println(
"==============================");//String类型?String.class //获取两个参数的构造器 忽略修饰符 Constructor constructor2 = c.getDeclaredConstructor(String.class, int.class);//() 参数的顺序和类型来匹配指定的构造器 System.out.println("构造器全称:"+constructor2);
System.out.println(
"构造器名称:"+constructor2.getName());//获取构造器的名字 System.out.println("参数个数:"+constructor2.getParameterCount());//针对私有的成员 需要绕过权限访问 constructor2.setAccessible(true);
Cat cat2
=(Cat)constructor2.newInstance("小花",3);//参数 俩 第一个参数String,第二个是int System.out.println(cat2);
}

打印结果如下

1.3 反射获取构造器的作用

同学们,刚才上一节我们已经获取到了Cat类中的构造器。获取到构造器后,有什么作用呢?

其实构造器的作用:
初始化对象并返回

这里我们需要用到如下的两个方法,注意:这两个方法时属于Constructor的,需要用Constructor对象来调用。

如下图所示,constructor1和constructor2分别表示Cat类中的两个构造器。现在我要把这两个构造器执行起来

由于构造器是private修饰的,先需要调用
setAccessible(true)
表示禁止检查访问控制,然后再调用
newInstance(实参列表)
就可以执行构造器,完成对象的初始化了。

代码如下:为了看到构造器真的执行, 故意在两个构造器中分别加了两个打印语句

代码的执行结果如下图所示:

1.4 反射获取成员变量&使用

同学们,上一节我们已经学习了获取类的构造方法并使用。接下来,我们再学习获取类的成员变量,并使用。

其实套路是一样的,在Class类中提供了获取成员变量的方法,如下图所示。

这些方法的记忆规则,如下

get:获取
Declared: 有这个单词表示可以获取任意一个,没有这个单词表示只能获取一个public修饰的
Field: 成员变量的意思
后缀s: 表示可以获取多个,没有后缀s只能获取一个
  • 假设有一个Cat类它有若干个成员变量,用Class类提供 的方法将成员变量的对象获取出来。

执行完上面的代码之后,我们可以看到控制台上打印输出了,每一个成员变量的名称和它的类型。

 
public classTest03_Field {

@Test
public void getFields() throwsNoSuchFieldException, IllegalAccessException {//获取所有的属性//1:获取字节码对象 Class c = Cat.class;//2:获取所有的成员属性 Field[] fields =c.getDeclaredFields();//3:遍历得到每个成员属性的 对象形式 for(Field field : fields) {
System.out.println(
"全称:"+field+" 查小名:"+field.getName()+" 属性的类型:"+field.getType());
}
System.out.println(
"=========================");//需求 对某个对象的某个属性完成 赋值 取值 Cat cat = newCat();//给cat对象的 name 属性 赋值 "加菲猫"//给cat对象的 age 属性 赋值 3//通过反射方式 找到指定的属性对象 Field fName = c.getDeclaredField("name");
Field fAge
= c.getDeclaredField("age");
//只要是在反射形式调用 可以绕过权限检查 fName.setAccessible(true);
fAge.setAccessible(
true);//完成 赋值//fName fAge 属性的对象形式 fName.set(cat,"加菲猫");
fAge.set(cat,
3);

System.out.println(cat);
//获取到 指定对象的指定属性的值? cat.getName() cat.name//属性对象反向调用 String name = (String) fName.get(cat);//cat.name System.out.println(name);
}
}

 

  • 获取到成员变量的对象之后该如何使用呢?

在Filed类中提供给给成员变量赋值和获取值的方法,如下图所示。

再次强调一下设置值、获取值的方法时Filed类的需要用Filed类的对象来调用,而且不管是设置值、还是获取值,都需要依赖于该变量所属的对象。代码如下

执行代码,控制台会有如下的打印

1.5 反射获取成员方法

各位同学,上面几节我们已经学习了反射获取构造方法、反射获取成员变量,还剩下最后一个就是反射获取成员方法并使用了。

在Java中反射包中,每一个成员方法用Method对象来表示,通过Class类提供的方法可以获取类中的成员方法对象。如下下图所示

接下来我们还是用代码演示一下:假设有一个Cat类,在Cat类中红有若干个成员方法

public classCat{privateString name;private intage;publicCat(){
System.out.println(
"空参数构造方法执行了");
}
private Cat(String name, intage){
System.out.println(
"有参数构造方法执行了");this.name=name;this.age=age;
}
private voidrun(){
System.out.println(
"(>^ω^<)喵跑得贼快~~");
}
public voideat(){
System.out.println(
"(>^ω^<)喵爱吃猫粮~");
}
privateString eat(String name){return "(>^ω^<)喵爱吃:"+name;
}
public voidsetName(String name){this.name=name;
}
publicString getName(){returnname;
}
public void setAge(intage){this.age=age;
}
public intgetAge(){returnage;
}
}

接下来,通过反射获取Cat类中所有的成员方法,每一个成员方法都是一个Method对象

packagecom.itheima.d2_reflect;
importorg.junit.Test;
importjava.lang.reflect.InvocationTargetException;importjava.lang.reflect.Method;
public classTest04_Method {

@Test
public void testGetMethod() throwsNoSuchMethodException, InvocationTargetException, IllegalAccessException {//1:获取字节码对象 Class c = Cat.class;//2:获取所有的方法 Method[] methods =c.getDeclaredMethods();//3:遍历 for(Method method : methods) {
System.out.println(
"全称:"+method+" 简称:"+method.getName()+" 返回值类型:"+method.getReturnType()+" 参数个数:"+method.getParameterCount());
}
System.out.println(
"==============");//获取指定的方法//run Method run = c.getDeclaredMethod("run");//因为类中 是这么区分方法的 方法名和参数列表 System.out.println(" 简称:"+run.getName()+" 返回值类型:"+run.getReturnType()+" 参数个数:"+run.getParameterCount());


Method eat
= c.getDeclaredMethod("eat",String.class);//因为类中 是这么区分方法的 方法名和参数列表 System.out.println(" 简称:"+eat.getName()+" 返回值类型:"+eat.getReturnType()+" 参数个数:"+eat.getParameterCount());
}
}


执行上面的代码,运行结果如下图所示:
打印输出每一个成员方法的名称、参数格式、返回值类型

也能获取单个指定的成员方法,如下图所示


获取到成员方法之后,有什么作用呢?

在Method类中提供了方法,可以将方法自己执行起来。

下面我们演示一下,把
run()
方法和
eat(String name)
方法执行起来。看分割线之下的代码

packagecom.itheima.d2_reflect;
importorg.junit.Test;
importjava.lang.reflect.InvocationTargetException;importjava.lang.reflect.Method;
public classTest04_Method {

@Test
public void testGetMethod() throwsNoSuchMethodException, InvocationTargetException, IllegalAccessException {//1:获取字节码对象 Class c = Cat.class;//2:获取所有的方法 Method[] methods =c.getDeclaredMethods();//3:遍历 for(Method method : methods) {
System.out.println(
"全称:"+method+" 简称:"+method.getName()+" 返回值类型:"+method.getReturnType()+" 参数个数:"+method.getParameterCount());
}
System.out.println(
"==============");//获取指定的方法//run Method run = c.getDeclaredMethod("run");//因为类中 是这么区分方法的 方法名和参数列表 System.out.println(" 简称:"+run.getName()+" 返回值类型:"+run.getReturnType()+" 参数个数:"+run.getParameterCount());


Method eat
= c.getDeclaredMethod("eat",String.class);//因为类中 是这么区分方法的 方法名和参数列表 System.out.println(" 简称:"+eat.getName()+" 返回值类型:"+eat.getReturnType()+" 参数个数:"+eat.getParameterCount());

Cat cat
= newCat();//方法对象是主体 由方法对象完成调用//run.invoke(cat,实际参数); run.setAccessible(true); //绕过权限检查 Object returnValue1 = run.invoke(cat);//run方法执行//run方法被 cat 对象调用执行 System.out.println("run方法调用之后的返回值:"+returnValue1);
eat.setAccessible(
true);
Object returnValue2
= eat.invoke(cat,"鱼儿");//eat方法执行 System.out.println("eat方法调用之后的返回值:"+returnValue2);
}
}

打印结果如下图所示:run()方法执行后打印
猫跑得贼快~~
,返回
null
; eat()方法执行完,直接返回
猫最爱吃:鱼儿

1.6 反射的应用

各位小伙伴,按照前面我们学习反射的套路,我们已经充分认识了什么是反射,以及反射的核心作用是用来获取类的各个组成部分并执行他们。但是由于同学们的经验有限,对于反射的具体应用场景还是很难感受到的(这个目前没有太好的办法,只能慢慢积累,等经验积累到一定程度,就会豁然开朗了)。

我们一直说反射使用来写框架的,接下来,我们就写一个简易的框架,简单窥探一下反射的应用。反射其实是非常强大的,这个案例也仅仅值小试牛刀。

需求是让我们写一个框架,能够将任意一个对象的属性名和属性值写到文件中去。不管这个对象有多少个属性,也不管这个对象的属性名是否相同。

分析一下该怎么做

1.先写好两个类,一个Student类和Teacher类
2.写一个ObjectFrame类代表框本架
在ObjectFrame类中定义一个saveObject(Object obj)方法,用于将任意对象存到文件中去
参数:Object obj: 就表示要存入文件中的对象

3.编写方法内部的代码,往文件中存储对象的属性名和属性值
1)参数obj对象中有哪些属性,属性名是什么实现值是什么,中有对象自己最清楚。
2)接着就通过反射获取类的成员变量信息了(变量名、变量值)
3)把变量名和变量值写到文件中去

写一个ObjectFrame表示自己设计的框架,代码如下图所示

 
public classObjectFrame {
/*** 告诉对象 我帮你解剖
* 属性 属性值解析出来 并写到指定文件中。
* 1: 根据传递的对象 即系 属性名 和属性值
* 2: 通过流的形式把内容写到文件中
* 方法三要素: 方法名 参数列表 返回值类型
*/ public static void saveTxt(Object obj) throwsIllegalAccessException, FileNotFoundException {//已知条件 obj对象 PrintStream ps = new PrintStream(new FileOutputStream("day14\\src\\data.txt",true));//1:根据obj对象获取其对应的字节码对象。 Class c =obj.getClass();//获取类名 String className= c.getSimpleName();//获取类名 小名 ps.println("-----------"+className+"------------");
System.out.println(
"-----------"+className+"------------");//2:根据字节码对象获取所有的属性对象。 Field[] fields =c.getDeclaredFields();//3:遍历每个属性对象,获取属性的名字 以及 其对应的值 if(fields!=null && fields.length>0){ //健壮性判断 for(Field field : fields) {//取消权限检查 field.setAccessible(true);//获取属性名字 String name =field.getName();
Object value
=field.get(obj);
ps.println(name
+"="+value);
System.out.println(name
+"="+value);
}
}
//4: 再去思考用流完成 数据的打印。 ps.close();
}
}

 

使用自己设计的框架,往文件中写入Student对象的信息和Teacher对象的信息。

先准备好Student类和Teacher类

public class Student{
   private String name;
   private int age;
   private char sex;
   private double height;
   private String hobby;
}
public class Teacher{
   private String name;
   private double salary;
}

创建一个测试类,在测试中类创建一个Student对象,创建一个Teacher对象,用ObjectFrame的方法把这两个对象所有的属性名和属性值写到文件中去。

public classTest5Frame{
@Test
public void save() throwsException{
Student s1
= new Student("黑马吴彦祖",45, '男', 185.3, "篮球,冰球,阅读");
Teacher s2
= new Teacher("播妞",999.9);

ObjectFrame.save(s1);
ObjectFrame.save(s2);
}
}

打开data.txt文件,内容如下图所示,就说明我们这个框架的功能已经实现了

三、注解

3.1 认识注解&定义注解

注解和反射一样,都是用来做框架的,我们这里学习注解的目的其实是为了以后学习框架或者做框架做铺垫的。

那注解该怎么学呢?和反射的学习套路一样,我们先充分的认识注解,掌握注解的定义和使用格式,然后再学习它的应用场景。

先来认识一下什么是注解?

Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。

比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。

再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。

  • 注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。

上面我们说的@Test注解、@Overide注解是别人定义好给我们用的,将来如果需要自己去开发框架,就需要我们自己定义注解。

接着我们学习自定义注解

自定义注解的格式如下图所示

比如:现在我们自定义一个MyTest注解

public @interfaceMyTest1{
String aaa();
boolean bbb() default true; //default true 表示默认值为true,使用时可以不赋值。 String[] ccc();
}

定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下

@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})public classAnnotationTest1{
@MyTest1(aaa
="铁扇公主",bbb=false, ccc={"Python","前端","Java"})public voidtest1(){

}
}

注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解

public @interfaceMyTest2 {
String value();
String name()
default "太上老君";
//value可以省略属性 前提 其他的属性不用赋值 }

定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下

@MyTest1(aaa="小黑子",ccc = {"鸡你太美","打篮球rap"})public class AnnotationTest1 { //Class

@MyTest1(aaa
="ikun" ,bbb = false,ccc="跳")
@MyTest2(
"红孩儿")public void test1(){} //Method @MyTest2("铁扇公主")private String name;//Field对象 }


到这里关于定义注解的格式、以及使用注解的格式就学习完了。

注解本质是什么呢?

想要搞清楚注解本质是什么东西,我们可以把注解的字节码进行反编译,使用XJad工具进行反编译。经过对MyTest1注解字节码反编译我们会发现:

1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。 【别着急,继续往下看,再解析注解时会用到】

3.2 元注解

各位小伙伴,刚才我们已经认识了注解以及注解的基本使用。接下来我们还需要学习几种特殊的注解,叫做元注解。

什么是元注解?

元注解是修饰注解的注解
。这句话虽然有一点饶,但是非常准确。我们看一个例子

别一下@Target注解和@Retention注解有什么作用,如下图所示

@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期

  • @Target元注解的使用:比如定义一个MyTest3注解,并添加@Target注解用来声明MyTest3的使用位置

@Target(ElementType.TYPE)//声明@MyTest3注解只能用在类上
public @interface MyTest3{
   
}

接下来,我们把@MyTest3用来类上观察是否有错,再把@MyTest3用在方法上、变量上再观察是否有错

如果我们定义MyTest3注解时,使用@Target注解属性值写成下面样子

//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyTest3{
   
}

此时再观察,@MyTest用在类上、方法上、变量上是否有错

到这里@Target元注解的使用就演示完毕了。

  • @Retetion元注解的使用:定义MyTest3注解时,给MyTest3注解添加@Retetion注解来声明MyTest3注解保留的时期

@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
@Retetion(RetetionPloicy.SOURCE): 注解保留到源代码时期、字节码中就没有了
@Retetion(RetetionPloicy.CLASS): 注解保留到字节码中、运行时注解就没有了
@Retetion(RetetionPloicy.RUNTIME):注解保留到运行时期
【自己写代码时,比较常用的是保留到运行时期】
//声明@MyTest3注解只能用在类上和方法上 @Target({ElementType.TYPE,ElementType.METHOD})//控制使用了@MyTest3注解的代码中,@MyTest3保留到运行时期 @Retetion(RetetionPloicy.RUNTIME)public @interfaceMyTest3{

}

3.3 解析注解

各位小伙伴,通过前面的学习我们能够自己定义注解,也能够把自己定义的注解标记在类上或者方法上等位置,但是总感觉有点别扭,给类、方法、变量等加上注解后,我们也没有干什么呀!!!

接下来,我们就要做点什么。我们可以通过反射技术把类上、方法上、变量上的注解对象获取出来,然后通过调用方法就可以获取注解上的属性值了。
我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。

解析注解套路如下

1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解

解析来看一个案例,来演示解析注解的代码编写

按照需求要求一步一步完成

① 先定义一个MyTest4注解

//声明@MyTest4注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})//控制使用了@MyTest4注解的代码中,@MyTest4保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)public @interfaceMyTest4{
String value();
double aaa() default 100;
String[] bbb();
}

② 定义有一个类Demo

@MyTest4(value = "杉木",aaa =1000 ,bbb={"喝水","晒太阳"})public classDemo {

@MyTest4(value
= "熏悟空",bbb = {"喝牛奶","吃香蕉"})public voidtest1(){

}
}

③ 写一个测试类AnnotationTest3解析Demo类上的MyTest4注解

packagecom.itheima.d3_annotation;
importorg.junit.Test;
importjava.lang.annotation.Annotation;importjava.lang.reflect.Method;public classAnnotationTest3 {

@Test
public voidparseClassAnnotation() {//解析 Demo类上的 MyTest4注解//1:获取Demo字节码文件对象 Class c = Demo.class;//2: 判断该字节码对象上 有没有使用MyTest4类型的 注解 boolean b = c.isAnnotationPresent(MyTest4.class);
if(b){//3: 如果有就获取出来 注解对象//Annotation annotation = c.getAnnotation();//MyTest4 extends Annotation 向下转型 MyTest4 myTest4 =(MyTest4) c.getAnnotation(MyTest4.class);//4: 解析它的属性 double aaa =myTest4.aaa();
System.out.println(aaa);
System.out.println(myTest4.bbb());
System.out.println(myTest4.value());

}

}

@Test
public void parseMethodAnnotation() throwsNoSuchMethodException {//解析 Demo类上的 MyTest4注解//1:获取Demo字节码文件对象 Class c = Demo.class;//2: 根据字节码对象获取 指定的方法对象 Method method = c.getDeclaredMethod("test1");
//2: 判断该字节码对象上 有没有使用MyTest4类型的 注解 boolean b = method.isAnnotationPresent(MyTest4.class);
if(b){//3: 如果有就获取出来 注解对象//Annotation annotation = c.getAnnotation();//MyTest4 extends Annotation 向下转型 MyTest4 myTest4 =(MyTest4) method.getAnnotation(MyTest4.class);//4: 解析它的属性 double aaa =myTest4.aaa();
System.out.println(aaa);
System.out.println(myTest4.bbb());
System.out.println(myTest4.value());

}
}
}

3.4 注解的应用场景

各位同学,关于注解的定义、使用、解析注解就已经学习完了。接下来,我们再学习一下注解的应用场景,注解是用来写框架的,比如现在我们要模拟Junit写一个测试框架,要求有@MyTest注解的方法可以被框架执行,没有@MyTest注解的方法不能被框架执行。

第一步:先定义一个MyTest注解

@Target(ElementType.METHOD)
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest{
   
}

第二步:写一个测试类AnnotationTest4,在类中定义几个被@MyTest注解标记的方法

public classAnnotationTest4{
@MyTest
public voidtest1(){
System.out.println(
"=====test1====");
}

@MyTest
public voidtest2(){
System.out.println(
"=====test2====");
}

public voidtest3(){
System.out.println(
"=====test2====");
}
public static voidmain(String[] args){
AnnotationTest4 a
= newAnnotationTest4();//1.先获取Class对象 Class c = AnnotationTest4.class;//2.解析AnnotationTest4类中所有的方法对象 Method[] methods =c.getDeclaredMethods();for(Method m: methods){//3.判断方法上是否有MyTest注解,有就执行该方法 if(m.isAnnotationPresent(MyTest.class)){
m.invoke(a);
}
}
}
}

恭喜小伙伴们,学习到这里,关于注解的使用就学会了(
^▽^
)

四、动态代理

4.1 动态代理介绍、准备功能

各位同学,这节课我们学习一个Java的高级技术叫做动态代理。首先我们认识一下代理长什么样?我们以大明星“杨超越”例。

假设现在有一个大明星叫杨超越,它有唱歌和跳舞的本领,作为大明星是要用唱歌和跳舞来赚钱的,但是每次做节目,唱歌的时候要准备话筒、收钱,再唱歌;跳舞的时候也要准备场地、收钱、再唱歌。杨超越越觉得我擅长的做的事情是唱歌,和跳舞,但是每次唱歌和跳舞之前或者之后都要做一些繁琐的事情,有点烦。于是杨超越就找个一个经济公司,请了一个代理人,代理杨超越处理这些事情,如果有人想请杨超越演出,直接找代理人就可以了。如下图所示

我们说杨超越的代理是中介公司派的,那中介公司怎么知道,要派一个有唱歌和跳舞功能的代理呢?

解决这个问题,Java使用的是接口,杨超越想找代理,在Java中需要杨超越实现了一个接口,接口中规定要唱歌和跳舞的方法。Java就可以通过这个接口为杨超越生成一个代理对象,只要接口中有的方法代理对象也会有。

接下来我们就先把有唱歌和跳舞功能的接口,和实现接口的大明星类定义出来。

public interfaceStar {
String sing(String name);
voiddance();
}
/***@author:石破天
* @date :Created in 2023年02月07日
* @description :
*
@version: 1.0*/ public class BigStar implementsStar{
privateString name;
publicBigStar(String name){this.name =name;
}

@Override
publicString sing(String name) {
System.out.println(
"您点了:"+name+"歌曲,"+this.name+"就唱着歌曲");return "栓Q!!!";
}

@Override
public voiddance() {
System.out.println(
this.name+"正在跳 freestyle dance!!");
}
}

4.2 生成动态代理对象

下面我们写一个为BigStar生成动态代理对象的工具类。这里需要用Java为开发者提供的一个生成代理对象的类叫Proxy类。

通过Proxy类的newInstance(...)方法可以为实现了同一接口的类生成代理对象。 调用方法时需要传递三个参数,该方法的参数解释可以查阅API文档,如下。

/*** @date :Created in 2023年02月07日
* @description :
* 经济公司
* 给 明星 产生代理对象 帮助明星 完成功能的增强
*
@version: 1.0*/ public classProxyFactory {/*方法返回值 写 Star 接收代理对象
代理对象和 明星 需要具备相同的功能
所以代理对象 也是 Star接口实现类对象
* @param bigStar 明星对象
* @return 代理对象
*/ public staticStar createProxy(BigStar bigStar){//JDK 产生代理的方式 是通过 /*public static Object newProxyInstance(ClassLoader loader, 用于指定加载类 到内存的一个类加载器
Class<?>[] interfaces, 明星实现的接口
InvocationHandler h) 用于指定代理对象干什么!!!
*/Star startProxy= (Star) Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(),//获取了当前类的加载器,后面负责代理类的加载 new Class[]{Star.class},//bigStar.getClass().getInterfaces() newInvocationHandler() {
/*** 用于拦截 明星的唱歌跳舞 对唱歌跳舞进行增强控制
*
@paramproxy 代理对象
*
@parammethod 拦截的方法 sing("好日子") dance()
*
@paramargs 参数 {"好日子"}
*
@return方法的返回值 执行方法的返回值
*
@throwsThrowable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throwsThrowable {
if(method.getName().equals("sing")){//代理对象拦住 唱歌方法 进行增强 System.out.println("准备话筒,收钱200~");//大明星唱歌 method 反射调用 }else if(method.getName().equals("dance")){
System.out.println(
"准备场地,收钱30W~");//大明星跳舞 method 反射调用 }
return method.invoke(bigStar,args);//反射执行 被代理人的功能 }
});
returnstartProxy;
}
}

调用我们写好的ProxyUtil工具类,为BigStar对象生成代理对象

/*** @date :Created in 2023年02月07日
* @description :
* 动态代理
* 在不修改原来的基础上,实现功能的增强
* 谁来谁先增强,是代理对象。
* 只不过代理对象的生成非常的麻烦。
*
@version: 1.0*/ public classTest {public static voidmain(String[] args) {//通过经纪公司给 杨超越产生一个代理 Star star = ProxyFactory.createProxy(new BigStar("杨超越"));

String sing
= star.sing("好日子");
System.out.println(sing);

star.dance();

}
}

运行测试类,结果如下图所示

恭喜同学们,当你把上面的案例写出来,并且理解,那么动态代理的基本使用就学会了。

4.3 动态代理应用

学习完动态代理的基本使用之后,接下来我们再做一个应用案例。

现有如下代码

/***  用户业务接口*/
public interfaceUserService {//登录功能
    void login(String loginName,String passWord) throwsException;//删除用户
    void deleteUsers() throwsException;//查询用户,返回数组的形式。
    String[] selectUsers() throwsException;
}

下面有一个UserService接口的实现类,下面每一个方法中都有计算方法运行时间的代码。

/*** 用户业务实现类(面向接口编程)*/
public class UserServiceImpl implementsUserService{
@Override
public void login(String loginName, String passWord) throwsException {long time1 =System.currentTimeMillis();if("admin".equals(loginName) && "123456".equals(passWord)){
System.out.println(
"您登录成功,欢迎光临本系统~");
}
else{
System.out.println(
"您登录失败,用户名或密码错误~");
}
Thread.sleep(
1000);long time2 =System.currentTimeMillis();
System.out.println(
"login方法耗时:"+(time2-time1));
}

@Override
public void deleteUsers() throwsException{long time1 =System.currentTimeMillis();
System.out.println(
"成功删除了1万个用户~");
Thread.sleep(
1500);long time2 =System.currentTimeMillis();
System.out.println(
"deleteUsers方法耗时:"+(time2-time1));
}

@Override
public String[] selectUsers() throwsException{long time1 =System.currentTimeMillis();
System.out.println(
"查询出了3个用户");
String[] names
= {"张全蛋", "李二狗", "牛爱花"};
Thread.sleep(
500);long time2 =System.currentTimeMillis();
System.out.println(
"selectUsers方法耗时:"+(time2-time1));returnnames;
}
}

观察上面代码发现有什么问题吗?

我们会发现每一个方法中计算耗时的代码都是重复的,我们可是学习了动态代理的高级程序员,怎么能忍受在每个方法中写重复代码呢!况且这些重复的代码并不属于UserSerivce的主要业务代码。

所以接下来我们打算,把计算每一个方法的耗时操作,交给代理对象来做。

先在UserService类中把计算耗时的代码删除,代码如下

/*** 用户业务实现类(面向接口编程)*/
public class UserServiceImpl implementsUserService{
@Override
public void login(String loginName, String passWord) throwsException {if("admin".equals(loginName) && "123456".equals(passWord)){
System.out.println(
"您登录成功,欢迎光临本系统~");
}
else{
System.out.println(
"您登录失败,用户名或密码错误~");
}
Thread.sleep(
1000);
}

@Override
public void deleteUsers() throwsException{
System.out.println(
"成功删除了1万个用户~");
Thread.sleep(
1500);
}

@Override
public String[] selectUsers() throwsException{

System.out.println(
"查询出了3个用户");
String[] names
= {"张全蛋", "李二狗", "牛爱花"};
Thread.sleep(
500);
returnnames;
}
}

然后为UserService生成一个动态代理对象,在动态代理中调用目标方法,在调用目标方法之前和之后记录毫秒值,并计算方法运行的时间。代码如下

public classProxyUtil {public staticUserService createProxy(UserService userService){
UserService userServiceProxy
=(UserService) Proxy.newProxyInstance(
ProxyUtil.
class.getClassLoader(),new Class[]{UserService.class},newInvocationHandler() {
@Override
publicObject invoke( Object proxy,
Method method,
Object[] args)
throws Throwable { if(
method.getName().equals(
"login") || method.getName().equals("deleteUsers")||method.getName().equals("selectUsers")){//方法运行前记录毫秒值 long startTime =System.currentTimeMillis();//执行方法 Object rs =method.invoke(userService, args);//执行方法后记录毫秒值 long endTime =System.currentTimeMillis();

System.out.println(method.getName()
+ "方法执行耗时:" + (endTime - startTime)/ 1000.0 + "s");returnrs;
}
else{
Object rs
=method.invoke(userService, args);returnrs; }
} });
//返回代理对象 returnuserServiceProxy;
}
}

在测试类中为UserService创建代理对象

/*** 目标:使用动态代理解决实际问题,并掌握使用代理的好处。*/
public classTest {public static void main(String[] args) throwsException{//1、创建用户业务对象。
        UserService userService = ProxyUtil.createProxy(newUserServiceImpl());
//2、调用用户业务的功能。 userService.login("admin", "123456");
System.out.println(
"----------------------------------");

userService.deleteUsers();
System.out.println(
"----------------------------------");

String[] names
=userService.selectUsers();
System.out.println(
"查询到的用户是:" +Arrays.toString(names));
System.out.println(
"----------------------------------");

}
}

执行结果如下图所示

动态代理对象的执行流程如下图所示,每次用代理对象调用方法时,都会执行InvocationHandler中的invoke方法。

高级函数

1.炸裂函数
UDTF

通常是将数组或者集合中或者结构体(涉及到数据类型-------复杂数据类型)中的元素单个输出

特点:接收一行数据,输出一行或多行数据

2.窗口函数/开窗函数

概念:能为每行数据划分一个窗口,然后对窗口范围内的数据进行计算,最后将计算结果返回给该行数据

包括两部分:窗口范围和函数

窗口范围:用于定义计算范围(通俗讲就是几行到当前行或者哪一个值到当前值)

函数:计算逻辑(函数包括求和、平均值、统计个数等)

窗口范围:

分类:

基于行 (要求每行数据的窗口为上一行到当前行)

基于值 (当前值-1到当前值)

分区:

可以指定分区字段

常用窗口函数

1)聚合函数

max:最大值

min:最小值

sum:求和

avg:平均值

count:计数

2)跨行取值函数

lead和lag:获取当前行的上/下某行、某个字段的值

不支持自定义窗口