Redis的安装和使用

引言

Redis 是基于内存的、采用Key-Value结构化存储的NoSQL数据库,底层采用单线程和多路IO复用模型加快查询速度。

  • 支持多种数据格式的存储;
  • 支持持久化存储;
  • 支持集群部署。

安装

Windows安装

Redis 官方不支持Windows的安装,通过启用windows自带的WSL2 ((Windows Subsystem for Linux)Linux子系统工具可以使用和安装。
Windows 版本要求大于10 。

具体安装流程和Linux安装一致。

Linux安装

大多数Linux发行版本提供了Redis的安装包,通过安装软件包命令可以从远程安装对应的工具。

Ubuntu/Debian 系统的安装流程如下:

  1. 添加Redis官方软件仓库源到Apt软件。
1
2
3
4
5
6
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

sudo apt-get update
sudo apt-get install redis
  1. 前置要求:如果运行的像是docker这类最小发行版本需要先安装 lsb-release
1
sudo apt install lsb-release

Redis启动

Linux Ubuntu/Debian 系统启动命令如下:

1
sudo service redis-server start

Redis访问

通过官方的Cli工具访问:

1
2
3
4
5
6
7
8
9
10
11
redis-cli 

Usage: redis-cli [OPTIONS] [cmd [arg [arg ...]]]
-h <hostname> Server hostname (default: 127.0.0.1).
-p <port> Server port (default: 6379).
-s <socket> Server socket (overrides hostname and port).
-a <password> Password to use when connecting to the server.
You can also use the REDISCLI_AUTH environment
variable to pass this password more safely
(if both are used, this argument takes precedence).

Redis配置

详细配置例子查看 https://redis.io/docs/management/config-file/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379


# Close the connection after a client is idle for N seconds (0 to disable)
timeout 60

# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility
# layer on top of the new ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH <password> as usually, or more explicitly with AUTH default <password>
# if they follow the new protocol: both will work.
#
# The requirepass is not compatible with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
#
requirepass redis1234

分布式部署

需要修改集群的相关配置,此处从略。 详情可参考网络或者 https://redis.io/docs/management/replication/

1
2
3
4
5
6
7
################################ REDIS CLUSTER  ###############################

# Normal Redis instances can't be part of a Redis Cluster; only nodes that are
# started as cluster nodes can. In order to start a Redis instance as a
# cluster node enable the cluster support uncommenting the following:
#
cluster-enabled yes

Springboot集成redis

  1. 修改依赖加入Redis启动项目,此处以maven为例子说明。
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 修改配置,指定redis启动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
# Redis 配置
redis:
# Redis 服务器地址
host: 127.0.0.1
# 连接端口号
port: 6379
# 数据库索引(0 - 15)
password: redis1234
database: 0
# 连接超时时间(毫秒)
timeout: 600000
# lettuce 参数
lettuce:
pool:
# 最大连接数(使用负值表示没有限制) 默认为 8
max-active: 10
# 最大阻塞等待时间(使用负值表示没有限制) 默认为 -1 ms
max-wait: -1
# 最大空闲连接 默认为 8
max-idle: 5
# 最小空闲连接 默认为 0
min-idle: 0
  • 增加 RedisTemplate 序列化配置,以FastJSON2为例子说明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class RedisConfiguration {

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);

GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
redisTemplate.setDefaultSerializer(fastJsonRedisSerializer);//设置默认的Serialize,包含 keySerializer & valueSerializer
redisTemplate.setKeySerializer(fastJsonRedisSerializer);//单独设置keySerializer
redisTemplate.setValueSerializer(fastJsonRedisSerializer);//单独设置valueSerializer
return redisTemplate;
}
}
  • 使用的demo,通过RedisTemplate访问。
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
@RestController
@Api(tags = "demo")
@RequestMapping("/v1/demo/")
@Slf4j
public class RedisDemo {
@Autowired
private RedisTemplate redisTemplate;

@ApiOperation(value = "get")
@PostMapping("get")
@ResponseBody
public Object get(String key) {
ValueOperations valueOperations = redisTemplate.opsForValue();
Object val = valueOperations.get(key);
return val;
}

@ApiOperation(value = "set")
@PostMapping("set")
@ResponseBody
public Boolean set(String key,String val) {
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key,val);
return true;
}

@ApiOperation(value = "delete")
@PostMapping("delete")
@ResponseBody
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
}

参考

Java函数式编程

总结

  • 现在主流的编程范式主要有三种,面向过程、面向对象和函数式编程。函数式编程作为一种补充,有很大存在、发展和学习的意义。
  • 函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。
  • 函数式接口可以将函数作为一个参数传入方法中进行使用。

概述

函数式编程因其编程的特殊性,仅在科学计算、数据处理、统计分析等领域,才能更好地发挥它的优势,所以它并不能完全替代更加通用的面向对象编程范式。但是作为一种补充,它也有很大存在、发展和学习的意义。

函数式编程更符合数学上函数映射的思想。具体到编程语言层面,我们可以使用Lambda表达式来快速编写函数映射,函数之间通过链式调用连接到一起,完成所需业务逻辑。Java的Lambda表达式是后来才引入的,由于函数式编程在并行处理方面的优势,正在被大量应用在大数据计算领域。

编程范式

现在主流的编程范式主要有三种,面向过程、面向对象和函数式编程。

面向对象编程最大的特点是:以类、对象作为组织代码的单元以及它的四大特性。

面向过程编程最大的特点是:以函数作为组织代码的单元,数据与方法相分离。

  • 函数式编程并非一个很新的东西,早在50多年前就已经出现了。近几年,函数式编程越来越被人关注,出现了很多新的函数式编程语言,比如Clojure、Scala、Erlang等。一些非函数式编程语言也加入了很多特性、语法、类库来支持函数式编程,比如Java、Python、Ruby、JavaScript等。除此之外,Google Guava也有对函数式编程的增强功能。
  • 函数式编程因其编程的特殊性,仅在科学计算、数据处理、统计分析等领域,才能更好地发挥它的优势,所以它并不能完全替代更加通用的面向对象编程范式。但是作为一种补充,它也有很大存在、发展和学习的意义。
  • 函数式编程中的“函数”,并不是指我们编程语言中的“函数”概念,而是指数学“函数”或者“表达式”(例如:y=f(x))。不过,在编程实现的时候,对于数学“函数”或“表达式”,我们一般习惯性地将它们设计成函数。
  • 函数式编程最独特的地方在于它的编程思想。函数式编程认为程序可以用一系列数学函数或表达式的组合来表示。函数式编程是程序面向数学的更底层的抽象,将计算过程描述为表达式。
  • 并不是所有的程序都适合这么做。函数式编程有它自己适合的应用场景,比如科学计算、数据处理、统计分析等。在这些领域,程序往往比较容易用数学表达式来表示,比起非函数式编程,实现同样的功能,函数式编程可以用很少的代码就能搞定。但是,对于强业务相关的大型业务系统开发来说,费劲吧啦地将它抽象成数学表达式,硬要用函数式编程来实现,显然是自讨苦吃。相反,在这种应用场景下,面向对象编程更加合适,写出来的代码更加可读、可维护。
  • 函数式编程跟面向过程编程一样,也是以函数作为组织代码的单元。不过,它跟面向过程编程的区别在于,它的函数是无状态的。何为无状态?简单点讲就是,函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的。这实际上就是数学函数或数学表达式的基本要求。

Java对函数式编程的支持

Java为函数式编程引入了三个新的语法概念:Stream类、Lambda表达式和函数接口(Functional Inteface)。Stream类用来支持通过“.”级联多个函数操作的代码编写方式;引入Lambda表达式的作用是简化代码编写;函数接口的作用是让我们可以把函数包裹成函数接口,来实现把函数当做参数一样来使用(Java 不像C那样支持函数指针,可以把函数直接当参数来使用)。

  • stream: stream “.”表示调用某个对象的方法。为了支持上面这种级联调用方式,我们让每个函数都返回一个通用的Stream类对象。在Stream类上的操作有两种:中间操作和终止操作。中间操作返回的仍然是Stream类对象,而终止操作返回的是确定的值结果。
  • map、filter是中间操作,返回Stream类对象,可以继续级联其他操作;max是终止操作,返回的是OPTIONAL类对象。
  • lambda: lambda表达式在Java中只是一个语法糖而已,底层是基于函数接口来实现的。Lambda表达式包括三部分:输入、函数体、输出。
  • 函数接口: Java没有函数指针这样的语法。所以它通过函数接口,将函数包裹在接口中,当作变量来使用。实际上,函数接口就是接口。不过,它也有自己特别的地方,那就是要求只包含一个未实现的方法。因为只有这样,Lambda表达式才能明确知道匹配的是哪个方法。如果有两个未实现的方法,并且接口入参、返回值都一样,那Java在翻译Lambda表达式的时候,就不知道表达式对应哪个方法了。
1
2
3
4
5
6
7
public static void stream(String[] args) {
Optional<Integer> result = Stream.of("f", "ba", "hello") // of返回Stream<String>对象
.map(s -> s.length()) // map返回Stream<Integer>对象
.filter(l -> l <= 3) // filter返回Stream<Integer>对象
.max((o1, o2) -> o1 - o2); // max终止操作:返回Optional<Integer>
System.out.println(result.get()); // 输出2
}

函数式接口

@FunctionalInterface注解使用场景: 一个接口只要满足只有一个抽象方法的条件,即可以当成函数式接口使用,有没有 @FunctionalInterface 都无所谓。

如果使用了此注解,再往接口中新增抽象方法,编译器就会报错,编译不通过。换句话说,@FunctionalInterface 就是一个承诺,承诺该接口世世代代都只会存在这一个抽象方法。因此,凡是使用了这个注解的接口,开发者可放心大胆的使用Lambda来实例化。当然误用 @FunctionalInterface 带来的后果也是极其惨重的:如果哪天你把这个注解去掉,再加一个抽象方法,则所有使用Lambda实例化该接口的客户端代码将全部编译错误。

自定义函数式编程接口过程:

  • 通过 @FunctionalInterface 注解,申明一个函数式接口。
  • 在方法中使用函数接口作为入参使用;
  • 调用方法,传入函数接口的实现方法。
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
public class MyFuncInterface {
/**
* 声明一个函数式接口
* @param <T>
*/
@FunctionalInterface
public interface ToLongFunction<T> {
long applyAsLong(T value);
}

/**
* 工具函数定义使用函数接口作为参数
*/
public static class Util{
public static Long mapToLong(ToLongFunction<? super Collection> mapper, List<String> val) {
Objects.requireNonNull(mapper);
return mapper.applyAsLong(val);
}
}

/**
* 使用例子
* @param args
*/
public static void main(String[] args) {
List<String> arr = Arrays.asList("ddd", "222", "3333");
Long size = Util.mapToLong((item)-> item.size(),arr);
System.out.println(size);
}
}

参考

Java 注解机制和应用

总结

  • Java注解是一种很常见的开发辅助模式,Java语言中的类、方法、变量、参数和包等都可以被标注。
  • 通过自定义注解的使用可以优化业务开发的使用。

概述

Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便的使用这些数据。

Java语言中的类、方法、变量、参数和包等都可以被标注。和Javadoc不同,Java标注可以通过反射获取注解内容。在编译器生成类文件时,注解可以被嵌入到字节码中。Java虚拟机可以保留注解内容,在运行时可以获取到注解内容。

常见注解

Java内置注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

1、作用在代码的注解是

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。

2、作用在其他注解的注解(或者说元注解)是:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

3、从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

常见的库中的注解

日常开发使用的库中也有着大量的注解,例如Jackson、SpringMvc等,下面就简单介绍下常见库中的常见注解使用

  • Jackson
    Jackson是一个通用的序列化库,程序员使用过程中可以使用它提供的注解机制对序列化进行定制化操作,比如:

  • 使用@JsonIgnore和@JsonIgnoreProperties配置序列化的过程中忽略部分字段

  • 使用@JsonManagedReference和@JsonBackReference可以配置实例之间的互相引用

  • 使用@JsonProperty和@JsonFormat配置序列化的过程中字段名称和属性字段的格式等

  • Servlet3.0

随着web开发技术的发展,Java web已经发展到了Servlet3.0,在早期使用Servlet的时候,我们只能在web.xml中配置,但是当我们使用Servlet3.0的时候开始,已经开始支持注解了,比如我们可以使用@WebServlet配置一个类为Servlet类。

  • SpringMvc

同样的,在web开发中,我们往往还会使用SpringMvc框架来简化开发,其框架的大量注解可以帮助我们减少大量的业务代码,例如一个请求的参数和字段/实例之间的映射关系,一个方法使用的是Http的什么请求方法,对应请求的某个路径,同样的请求如何解析,返回的响应报文格式定义等,这些都可以使用注解来简化实现,一个简单的Mvc操作如下:

其中@Controller注解标明当前的类是SpringMvc接管的一个Bean实例,@RequestMapping(“/hello”)则是代表当前Bean的前置请求路径比如是/hello开头, @GetMapping(“/test”)则是表示test方法被访问必须是Http请求的get请求,并且路径必须是/hello/test为路径前置,@ResponseBody注解则是标明了当前请求的相应信息按照默认的格式返回(根据后缀名来确定格式)

注解创建

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Label {
String value() default "";
}

@Target注解表示当前注解可以使用在什么类型的元素上,这里的值可以多选,即一个注解可以作用在多种不同类型的元素上,具体的可选值在ElementType枚举类中,值如下:

取值 解释
TYPE 表示作用在类、接口上
FIELD 表示作用在字段,包括枚举常量中
METHOD 表示作用在方法中
PARAMETER 表示作用在方法中的参数中
CONSTRUCTOR 表示作用在构造方法中
LOCAL_VARIABLE 表示作用在本地常量中
MODULE 表示作用在部分模块中(Java9引入的概念)
ANNOTATION_TYPE 表示当前注解作用在定义其他注解中,即元注解
PACKAGE 表示当前注解使用在包的申明中
TYPE_PARAMETER 表明当前注解使用在类型参数的申明中(Java8新增)
TYPE_USE 表明当前注解使用在具体使用类型中(Java8新增)

当使用多个作用域范围的时候,使用{}包裹多个参数,比如@SuppressWarnings注解的Target就有多个,在Java7中的定义为:

1
2
3
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value();
}

@Retention

@Retention注解则是表明了当前注解可以保留到Java多个阶段的哪一个阶段,参数类型为RetentionPolicy枚举类,可取值如下:

取值 解释
SOURCE 此注解仅在源代码阶段保留,编译后即丢失注解部分
CLASS 表示编译后依然保留在Class字节码中,但是加载时不一定会在内存中
RUNTIME 表示不仅保留在Class字节码中,一直到内存使用时仍然存在

此注解有默认值,即当我们没有申明@Retention的时候,默认则是Class取值范围

@Documented

@Documented注解没有具体的参数,使用此元注解,则表示带有类型的注解将由javadoc记录

@Inherited

  • @Inherited 注解与注解的继承有关系,具体关系为如果使用了当前的元注解,则表示此注解可以被其他的注解的子类直接继承,但是需要注意的是对已实现接口上的注解将没有作用。
  • @Inherited 注释表明注释类型可以从超类继承。当用户查询注释类型并且该类没有此类型的注释时,将查询类的超类以获取注释类型(默认情况下不是这样)。此注释仅适用于类声明。

应用

业务开发的使用

基于Spring提供的AOP开发方法,可以简化业务代码开发中冗余的业务代码,对接口调用过程的前置处理、过程处理、后置处理。

  • 金融借贷系统对接了很多第三方的风控接口。调用接口前需要校验报文体中的签名字段 sign
    • 每个方法开头都写一份签名验签的代码。
    • 将验签代码抽取成方法,方便复用。
    • 新建 @SignCheck 注解,在切面里面写业务逻辑。
  • 应用开发过程的日志记录需要保存到消息中间件或者数据库。
    • 每个方法开头都写一份记录日志的代码。
    • 将记录日志抽取成方法,方便复用。
    • 新建 @LogAccess 注解,在切面里面写日志记录逻辑。

工具开发的使用

  • Lombok 通过注解 @Getter @Setter 等主机,在源码编译时添加对应的模板方法。
  • Fastjson 通过 @JSONField 定制序列化方法。指定JSON代码文本生成的别名。

自定义注解例子

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

import lombok.Getter;
import lombok.Setter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class AnnotationTestAll {
/**
* 自定义注解 Format
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Format {
String pattern() default "yyyy-MM-dd HH:mm:ss";

String timezone() default "GMT+8";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Label {
String value() default "";
}
/**
* 通过反射 Format Label,并且获取值做运算
*/
public static class SimpleFormatter {
public static String format(Object obj) {
try {
Class<?> cls = obj.getClass();
StringBuilder builder = new StringBuilder();
for (Field field : cls.getDeclaredFields()) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
// 获取Label注解-输出的字段名称
Label label = field.getAnnotation(Label.class);
String name = null == label ? field.getName() : label.value();
Object value = field.get(obj);
if (value != null && field.getType() == Date.class) {
value = formatter(field, value);
}
builder.append(String.format("%s ? %s \n", name, value));
}
return builder.toString();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("格式化输出失败:" + e.getMessage());
}
}

private static Object formatter(Field field, Object value) {
Format format = field.getAnnotation(Format.class);
if (null == format) {
return value;
}
String pattern = format.pattern();
String timezone = format.timezone();
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
sdf.setTimeZone(TimeZone.getTimeZone(timezone));
return sdf.format(value);
}
}

/**
* 测试的一个bo
*/
@Getter
@Setter
public static class Student {
@Label("姓名")
private String name;
@Label("出生日期")
@Format(pattern = "yyyy/MM/dd")
private Date born;
@Label("分数")
private Double score;
}

/**
* 测试运算结果
* @param args
*/
public static void main(String[] args) {
Student student = new Student();
student.setBorn(new Date());
student.setName("张三");
student.setScore(244.0);
System.out.println(SimpleFormatter.format(student));
/**
* 输出:
* 姓名 ? 张三
* 出生日期 ? 2023/03/12
* 分数 ? 244.0
*/
}
}

参考

Java泛型机制和应用

总结

  • 泛型解决了参数类型缺少检查造成的问题。
  • 泛型可以在类、接口、函数上使用。
  • 通配符是为了让Java泛型支持范围限定,这样使得泛型的灵活性提升,同时也让通用性设计有了更多的空间。

概述

编译期是指把源码交给编译器编译成计算机可执行文件的过程。运行期是指把编译后的文件交给计算机执行,直到程序结束。在Java中就是把.java文件编译成.class文件,再把编译后的文件交给JVM加载执行。

泛型又叫“参数化类型”。泛型就是在定义类、接口、方法的时候指定某一种特定类型(碗),让类、接口、方法的使用者来决定具体用哪一种类型的参数(盛的东西)。Java的泛型是在1.5引入的,只在编译期做泛型检查,运行期泛型就会消失,我们把这称为“泛型擦除”,最终类型都会变成 Object

泛型主要解决的问题:

  • 集合对元素类型没有任何限制引发的业务问题。
  • 把对象写入集合,在获取对象的时候进行强制类型转换出现问题。

语法规则

使用菱形语法表示泛型,例如 List<String> strList= new ArrayList<>();

泛型允许在定义类、接口、方法时使用类型参数,这个类型形参将在变量声明、创建对象、调用方法时动态得指定。

泛型类

类上定义泛型,作用于类的成员变量与函数,代码实例如下:

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

public class Apple<T> {
// 使用T类型形参定义实际变量
private T info;

public T getInfo() {
return info;
}

public void setInfo(T info) {
this.info = info;
}

public Apple(T info) {
this.info = info;
}
public static void main(String[] args) {
// 创建变量时候指定泛型类型,构造器只能使用对应类型
Apple<String> a1 = new Apple<>("苹果");
System.out.printf(a1.getInfo());
Apple<Double> a2 = new Apple<>(2.13);
System.out.printf(a2.getInfo()+"");
}
}

泛型接口

接口上定义泛型,作用于函数,代码实例如下:

1
2
3
4
5
6
7
8
public interface GenericInterface<T> {
public T get();
public void set(T t);
public T delete(T t);
default T defaultFunction(T t){
return t;
}
}

泛型函数

函数返回类型旁加上泛型,作用于函数,代码实例如下:

1
2
3
4
5
6
7
8
9
10
public class GenericFunction {
public <T> void function(T t) {
}
public <T> T functionTwo(T t) {
return t;
}
public <T> String functionThree(T t) {
return "";
}
}

通配符

通配符是为了让Java泛型支持范围限定,这样使得泛型的灵活性提升,同时也让通用性设计有了更多的空间。

  • <?>:无界通配符,即类型不确定,任意类型
  • <? extends T>:上边界通配符,即?是继承自T的任意子类型,遵守只读不写
  • <? super T>:下边界通配符,即?T的任意父类型,遵守只写不读

通配符限定的范围是体现在确认“参数化类型”的时候,而不是“参数化类型”填充后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 1.创建泛型为Number的List类,Integer、Double、Long等都是Number的子类
* new ArrayList<>() 等价于 new ArrayList<Number>()
*/
List<Number> numberList = new ArrayList<Number>();

/**
* 2.添加不同子类
*/
numberList.add(1);//添加Integer类型
numberList.add(0.5);//添加Double类型
numberList.add(10000L);//添加Long类型

/**
* 3.创建泛型为Number的List类,Integer、Double、Long等都是Number的子类
* 引用是泛型类别是Number,但具体实现指定的泛型是Integer
*/
List<Number> numberListTwo = new ArrayList<Integer>();//err 异常编译不通过

/**
* 4.创建泛型为Integer的List类,把该对象的引用地址指向泛型为Number的List
*/
List<Integer> integerList = new ArrayList<Integer>();
List<Number> numberListThree = integerList;//err 异常编译不通过

上边界通配符只读不写,下边界通配符只写不读。

  • <? extends T>上边界通配符不作为函数入参,只作为函数返回类型,比如List<? extends T>的使用add函数会编译不通过,get函数则没问题。
  • <? super T>下边界通配符不作为函数返回类型,只作为函数入参,比如List<? super T>的add函数正常调用,get函数也没问题,但只会返回Object。

设计原则可以参考 PECS (producer-extends,consumer-super)原则。PECS原则也就是说,如果参数化类型表示一个生产者E,就使用<? extends E>,如果参数化类型表示一个消费者E,则使用<? super E>。

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
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

参考

Java反射机制与应用

总结

  • Java的反射机制提供了运行时分析处理类的能力。
  • Spring框架的IOC容器使用了反射技术,可以简化代码编写。
  • 使用Spring+策略模式可以解决代码中if或switch代码块的代码耦合问题。

概述

反射机制提供的功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行的时候构造任意一个类的对象
  • 在运行时判断一个类所具有的成员变量和方法
  • 在运行时调用任何一个对象的方法
  • 生成动态代理

反射机制

Java反射机制类

1
2
3
4
5
java.lang.Class; //类
java.lang.reflect.Constructor;//构造方法
java.lang.reflect.Field; //类的成员变量
java.lang.reflect.Method;//类的方法
java.lang.reflect.Modifier;//访问权限

优点和缺点

  • 优点:运行期类型的判断,动态类加载,动态代理使用反射。
  • 缺点:性能是一个问题,反射相当于一系列解释操作,通知jvm要做的事情,性能比直接的java代码要慢很多。

反射机制的应用场景

  • 逆向代码 ,例如反编译
  • 与注解相结合的框架 例如Retrofit
  • 单纯的反射机制应用框架 例如EventBus 2.x
  • 动态生成类框架 例如Gson

反射机制的应用

Spring框架的IOC

IOC中最基本的技术就是“反射(Reflection)”编程,,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才决定到底是哪一种对象。只是在Spring中要生产的对象都在配置文件中给出定义,目的就是提高灵活性和可维护性。

我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。

Spring反射的策略模式

如果不是用设计模式来做的情况下,会出现很多个 if-else 或者 switch 语句块。这样的话,代码耦合性也会非常高,将来再增加一个需求,则会导致一直增加判断语句块。也违反了面向对象的开闭原则。耦合性也会非常高,将来再增加一个需求,则会导致一直增加判断语句块。也违反了面向对象的开闭原则。反射+策略模式解决代码中if或switch代码块的代码耦合问题。

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
@Component
public class MyStragtrgyReflexContent implements ApplicationContextAware,InitializingBean {
private Map<String,MyStragtegy> beanMap ;
private ApplicationContext applicationContext;
/**
* 实现ApplicationContextAware接口,Spring容器会在创建MyStragtrgyReflexContent类之后,
* 自动调用实现接口的setApplicationContextAware()方法,
* 调用该方法时,会将ApplicationContext(容器本身)作为参数传给该方法,
* 我们可以在该方法中将Spring传入的参数ApplicationContext赋给MyStragtrgyReflexContent对象的applicationContext实例变量,因此接下来可以通过该applicationContext实例变量来访问容器本身。
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 实现InitializingBean接口,该接口提供了afterPropertiesSet方法。
* spirng容器在初始化bean的时候会执行afterPropertiesSet方法,
* 我们可以在该方法中调用applicationContext接口提供的getBeansOfType方法获得实现MyStragtegy类的Bean,将之存储至map集合中
*/
@Override
public void afterPropertiesSet() throws Exception {
Map<String,MyStragtegy> map = applicationContext.getBeansOfType(MyStragtegy.class);
this.beanMap = map;
}
public MyStragtegy getMyStragtegy(String beanName){
return this.beanMap.get(beanName);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class StragtegyReflexService {

@Autowired
private MyStragtrgyReflexContent reflexContent;

public String play(String type){
MyStragtegy myStragtegy = reflexContent.getMyStragtegy(type);
if (myStragtegy!=null){
return myStragtegy.play();
}else {
return "还没有这个宠物哟!~";
}
}

}

参考

网文大纲从零开始编写

概述

  • 编写大致框架;
  • 撰写设定落地;
  • 撰写完善细纲。

教程

准备阶段

  • 记录灵感。
  • 选定脑洞,刷同类文+查资料。

大纲阶段

第一部分:编写大致框架

  • 步骤一: 一句话核心梗。
    • 一句话概括模板,谁(和谁)遭遇了什么,然后做(经历)了什么,最后结局如何。
    • 一个好的作品,主题鲜明统一,有且仅有一个主题。
  • 步骤二:三百字~五百字简纲(全文梗概)。对一句话核心进行扩写。
  • 步骤三:一干字~五干字粗纲(进一步细化的全文梗概)。

第二部分:设定落地。

  • 步骤一:世界观(设定)。
    • 用一个文档写定这个故事的世界观。
    • 世界观和设定写的越详细越好。
  • 步骤二:人设(人物小传)。
    • Ta经历过些什么,ta被他人、被世界如何对待,从而让ta成为了现在的性格与模样?
    • Ta是一个这样的人,为什么?
    • 编写人物基本资料卡。
    • 构思一个具体的场景,把不同人物放进去看是什么反应。
  • 步骤三:人物线。
    • 固定某个角色的视角,把你的故事(粗纲)重新讲一遍。
    • 从每个角色的视角,重新经历一遍这个故事。
    • 两个关键点,人物动机(为什么这么做)和行动(做了什么)。
    • 在人物线路,你要百分百尊重这个角色。

第三部分:完善细纲。

  • 步骤一:阶段
  • 步骤二:事件(具体时间)
  • 步骤三:细节(章纲)

将步骤一到三用一个Excel罗列编写。

  • 步骤四:人物弧光(性格成长线+感情线)

参考

开源软件靠什么赚钱

使用CSDN的 ChatGPT ( https://so.csdn.net/so/chat )询问“开源项目靠什么赚钱”会得到如下答案。

开源项目通常是免费使用的,但是这并不意味着开发者不能赚钱。开源项目可以通过以下方式获得收益:

  • 提供商业支持和服务:开发者可以为使用开源项目的企业或个人提供商业支持和服务,例如帮助他们安装、调试、维护和升级软件。
  • 开发附加功能或插件:开发者可以为开源项目开发附加功能或插件,并向使用者收费。
  • 接受捐赠:开发者可以在自己的网站上提供捐赠按钮,供感谢他们的用户捐赠。
  • 在线广告:如果开源项目有一个网站或在线应用程序,开发者可以在其中放置广告来获得收益。
  • 开展培训或咨询业务:开发者可以通过提供培训或咨询服务来获得收益,帮助使用者更好地使用开源项目。
  • 其他方式:还有其他许多方法可以让开源项目开发者赚钱,例如出版书籍、举办会议等。

通过知乎网站(个人搞开源怎么赚钱? - OSCHINA的文章 - 知乎
https://zhuanlan.zhihu.com/p/355411897 )获取答案会获得更加全面的回答,相比chatgpt的简洁回答,知乎回答的更有理有据。

“在不违反开源许可证以及开源社区基本道德准则的前提下,开发者从开源项目中合理获利并不是一件坏事。 自由软件运动领袖 Richard Stallman (RMS)曾鼓励自由与开源软件作者合理利用开源软件赚钱,以更好地维持项目的研发和推广。Linux Kernel 维护者 Linus Torvalds 也接受着来自各大厂商的资助,从而能够全职进行 Linux Kernel 的维护工作。”

  1. Bug 赏金计划

BUG 赏金计划是很多大型开源机构面向个人开发者推出的一种赚钱的方式。开发者可以通过报告开源项目上的 BUG,也可以直接修复这些 BUG 获得来自项目官方不同程度的奖金。

  1. 提供付费支持

提供付费支持是很多开源项目团队获利的经典方式之一。

如果开发者决定开始为某开源项目提供付费支持,则可以向其他人提供他们需要的有偿服务,这里有些例子:帮助进行基本安装或使用。代码和错误修复。添加新的和额外的功能。提供书面文件(例如书籍和教程)。但要注意,如果你提供的支持包括对项目源代码进行更改,则需要核对项目采用的开源许可证,以确保其允许该类型的活动。当然,你不能只修改项目原作者的原始代码,你必须维护自己的产品副本。

  1. 出售增强功能/插件

例如,开发人员销售 WordPress 插件。用户可以免费下载并安装 WordPress,但WordPress 拥有丰富的付费插件生态,用户会根据自己的需求购买用于修改或增强现有功能的付费插件,开发者可以通过开发并出售这些插件获利。

  1. 出售代码以外的内容
  • 写和出售关于该项目的书籍教程。这可以通过与出版社推荐主题来完成。后者将负责编辑过程和发布步骤,让你专注于写作。当然,你必须与出版社分配收入,但这是值得的。
  • 为一些内容付费平台创建视频课程。这样做会产生被动收入,就像书出版后一样。另外,这些平台通常会提供视频培训。
  • 撰写有关产品的推广帖子。这并不意味着开源产品的所有者要向您付费以撰写这些帖子,而是使某些博客对该主题感兴趣,并愿意向你付费以提供有关其他主题的内容。
  1. 来自用户的捐赠

GitHub、Gitee 等代码托管平台都拥有用户捐赠的功能,开源项目拥有者可以从这些平台获得来自用户的自发捐赠,当然这项收入的大小要取决于你的项目是否足够强大,以及你所在地区的用户是否足够慷慨。

  1. 用参与开源提升自己的职场竞争力

有时单纯的参与开源项目的代码贡献并不能带来直接的金钱利益,但开发者可以将这段经历写进简历里,以获得更多的就业机会。也可以通过参与大型公司旗下的开源项目,从而获得进入这家公司工作的机会。

CSDN的 ChatGPT 也是使用开源模型建立的,开源产品也可以商业化也就是一个例子。通过开源产品能赚钱,但是有难度。但总的来说对于技术的提升、个人影响力的提升都是很重要的一种方法。

使用Springboot集成MybatisGenerator

概述

Mybatis Generator 可以生成mybatis的模板代码,包括动态脚本、实体类、Mapper映射访问类。
Mybatis Generator 有多种使用方式,此处介绍一种线上环境比较用的多的场景,通过Maven插件使用。

使用方法如下:

  1. 通过核心jar包cmd使用。 例如 java -jar mybatis-generator-core-x.x.x.jar -configfile \temp\generatorConfig.xml -overwrite
  2. 通过 Ant task 使用。
  3. 通过 java程序使用。
  4. 通过 Maven Plugin 使用。

前置条件

  • JDK 1.8+
  • SpringBoot 2.1+
  • mybatis 3+

引入 MybatisGenerator

  1. 引入mybatis,用于后续的代码使用。
1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
  1. MybatisGenerator Maven Plugin的引入和配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<configuration>
<!-- 指定配置文件地址-->
<configurationFile>${basedir}/src/main/resources/generatorConfigSqlLite.xml</configurationFile>
</configuration>
</plugin>
</plugins>
</build>

MBG配置使用

MBG配置有两种形式,一种是xml、一种是java代码。此处演示使用的是xml配置的方式。

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<!-- 指定数据库连接池用到的依赖 -->
<classPathEntry location="E:\repo\mvn-repo\org\xerial\sqlite-jdbc\3.40.1.0\sqlite-jdbc-3.40.1.0.jar" />

<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="true" />
</commentGenerator>
<!-- 指定 数据库链接配置 -->
<jdbcConnection driverClass="org.sqlite.JDBC"
connectionURL="jdbc:sqlite:E:\ws-research\backend\rssboot\src\main\resources\db\rssboot.db"
userId=""
password="">
</jdbcConnection>

<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>

<!-- 指定javaBean生成的位置 -->
<javaModelGenerator targetPackage="io.rainforest.rss.dao.po"
targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
</javaModelGenerator>

<!--指定sql映射文件生成的位置 -->
<sqlMapGenerator targetPackage="mapper" targetProject="./src/main/resources/mybatis">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>

<!-- 指定dao接口生成的位置,mapper接口 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="io.rainforest.rss.dao.mapper" targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>

<!-- table指定每个表的生成策略 -->
<table tableName="rss_follow" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false">
<generatedKey column="id" sqlStatement="MySql" identity="true" />
</table>

</context>
</generatorConfiguration>

参考

使用Springboot集成Sqlite

概述

SQLite,是一款轻型的数据库,是遵守ACID的关系型数据库管理系统,它包含在一个相对小的C库中。它是D.RichardHipp建立的公有领域项目。它的设计目标是嵌入式的,而且已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。

简单来说,通过一个文件就能启动和使用关系型数据库管理。

前置条件

  • JDK 1.8+
  • SpringBoot 2.1+

引入sqlite

  1. 引入sqlite依赖和数据库依赖:
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.40.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
  1. 修改启动配置:
1
2
3
4
5
## 数据库连接池
## 驱动使用sqlite
spring.datasource.driver-class-name=org.sqlite.JDBC
# 指定数据库位置,相对或者绝对定位
spring.datasource.url=jdbc:sqlite::resource:db/rssboot.db

使用

  1. 通过mybatis操作数据库。
  2. 通过JdbcTemplate操作数据库。
  3. 通过 JPA 操作数据库。

参考

02-Spring中的设计模式之模板模式

模板模式

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

模板模式有两大作用:复用和扩展。其中复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

除此之外,我们还讲到回调。它跟模板模式具有相同的作用:代码复用和扩展。在一些框架、类库、组件等的设计中经常会用到,比如 JdbcTemplate 就是用了回调。相对于普通的函数调用,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。

回调可以细分为同步回调和异步回调。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。回调跟模板模式的区别,更多的是在代码实现上,而非应用场景上。回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。

Spring中的模板方法

  • DefaultListableBeanFactory 中的 BeanFactoryPostProcessor 。
  • DefaultListableBeanFactory 中的 BeanPostProcessor 。
1
2
3
4
5
org.springframework.context.annotation.internalConfigurationAnnotationProcessor // 注解配置处理
org.springframework.context.annotation.internalAutowiredAnnotationProcessor // 自动注入处理
org.springframework.context.annotation.internalCommonAnnotationProcessor // 通用注解处理
org.springframework.context.event.internalEventListenerProcessor // 事件处理类
org.springframework.context.event.internalEventListenerFactory // 事件监听类
  • 同一调用postProcessBeanFactory接口方法执行对BeanFactory的后置处理。
  • 同一调用 postProcessBeforeInitialization postProcessAfterInitialization 接口方法执行对Bean的后置处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface
public interface BeanFactoryPostProcessor {
/**
* Modify the application context's internal bean factory after its standard
* initialization. All bean definitions will have been loaded, but no beans
* will have been instantiated yet. This allows for overriding or adding
* properties even to eager-initializing beans.
* @param beanFactory the bean factory used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;

/**
* Factory hook that allows for custom modification of new bean instances &mdash;
* for example, checking for marker interfaces or wrapping beans with proxies.
*/
public interface BeanPostProcessor {

@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

}

参考代码

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
package engineer.spring.design;

import java.util.ArrayList;
import java.util.List;

/**
* 模板方法设计模式
*
* @author Q
*/
public class TemplateMethodTest {
public static void main(String[] args) {

MyBeanFactory beanFactory = new MyBeanFactory();
beanFactory.addBeanPostProcessor((bean) -> System.out.println("注入 @Autowired" + bean));
beanFactory.addBeanPostProcessor((bean) -> System.out.println("注入 @Resource" + bean));
beanFactory.addBeanPostProcessor((bean) -> System.out.println("注入 @Configuration" + bean));

System.out.println(beanFactory.getBean());

}

static class MyBeanFactory {
public Object getBean() {
Object bean = new Object();
System.out.println("构造 " + bean);
// 1. 解析@Autowired
// 2. 解析@Resource
System.out.println("依赖注入 " + bean);
postProcessorList.forEach(beanPostProcessor -> {
beanPostProcessor.inject(bean);
});
System.out.println("初始化 " + bean);
// 扩展功能需要修改代码
return bean;
}

public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
postProcessorList.add(beanPostProcessor);
}

private List<BeanPostProcessor> postProcessorList = new ArrayList<>();

}

static interface BeanPostProcessor {
// 依赖注入模板方法,通过调用接口,具体得实现将某些步骤推迟到子类中实现
void inject(Object bean);
}

}

参考