ZB-047-01正则表达式

Java正则表达式

学习一次终生受益的正则表达式

1
2
3
4
5
6
7
8
[01:52:31] fdasfsdafasdfasf
[02:42:32] fdasfdsafdsa
[03:32:53] gfdgfdgfd
[04:22:43] rewrewrw
[05:12:23] tfhfghfg
[06:02:29] daadfdcvcv
[07:12:56] 322143543
[08:22:11] hgjhjhhj

把上面时间部分替换

以上文本我在 vscode 或者 sublime 里 搜索切换到正则表达式

1
2
3
4
5
6
7
8
# 这样就能匹配
\[\d\d:\d\d:\d\d\]

# 上面写的有点傻
\[\d{2}:\d{2}:\d{2}\]

# 再次简化
\[[\d:]+\]

为什么要学正则表达式

  • 实用、强大、程序员必备
    • 在支持正则表达式的文本编辑器中提高工作效率
    • 用很少代码完成复杂的文本替换工作
  • 难学、难懂、反人类
    • 一开始觉得难是正常的
    • 需要长期的实际才能掌握
  • 如何学习和读懂正则表达式
    • 耐心分析,一点点分析
    • 多用在线工具验证自己的想法

回忆一下通配符…

  • ls *.txt
  • windows资源管理器 *.txt

Java转义字符

  • 为什么需要转义字符?
    • 因为有的字符不可见,无法用一个键来代表
  • 最常用的转义字符
    • \n
    • \r
    • \t
    • \"\'
    • \\
    • \uXXXX
1
2
3
4
5
String a = "abcdef\"\'\n";
// 双引号 单引号 换行
System.out.println(a);
// 两个斜杠
System.out.println("\\\\");

常用元字符

  • ^ 开始位置
  • $ 结束位置
  • . 单个任意字符(不一定包含换行符 java里)
  • \w 单个”word”字符 字母、数字、下划线、汉字
  • \s 单个空白字符
  • \d 单个数字字符
  • \b 单词的开始或结束

这些都是匹配单个的字符

重复

1
2
3
4
5
6
7
8
{n} 正好出现n次
{n,m} 最少n次,最多m次
{n,} 最少n次,最多不限

+ <===> {1,}
? <===> {0,1}

* <===> {0,} 可以可无 不推荐使用容易误导

选择

  • [aeiou] 匹配单个的 a/e/i/o/u
  • [0-9] 单个的数字匹配
  • [A-Z] 单个大写字母
  • [A-Z0-9_] 大写字母或者数字或者下划线
  • Hi|hi等价于[Hh]i Hi 或 hi
1
Pattern p = Pattern.compile("[aeiou\\]]]"); // 匹配 a/e/i/o/u/] 由于在 java世界里 所以 ']'要添加转义 '\]'才可以

元字符/重复/选择 是最重要的三个规则

反义

  • [^aeiou] 匹配单个除了 a/e/i/o/u 之外的字符
  • [^x] 单个非x的字符
  • \W 单个非\w(字母/数字/下划线/汉字)
  • \S 单个非\s(空白)
  • \D 单个非\d(数字)字符
  • \B 非开头/结束位置

一些例子

  • ^\d{5,12}$ 5~12位的数字
  • ^(0|[1-9][0-9]*)$ 0或者非零开头的数字
  • ^(-?\d+)(\.\d+)?$ 小数
  • \d{3}-\d{8}|\d{4}-\d{7} 国内的电话号码(带区号)
  • ^\d{4}-\d{1,2}-\d{1,2} yyyy-MM-dd日期格式
  • \n\s*\r 空白行

Java中的正则表达式

  • String
    • split
    • replaceAll/replaceFirst
    • matches
  • matches
    • 尽量少用或者少编译,因为效率低
    • 每当你需要解析的时候,代码非常昂贵Pattern.compile() 5000行
    • 匹配过程非常昂贵

一些坑,匹配版本号的例子

你非常容易写成这样 a2.split(".") 实际得到的是 []

1
2
3
4
String a2 = "1.2.3";
System.out.println(Arrays.toString(a2.split(".")));
// System.out.println(a2.split("\.")); 转义 \ 需要在此转义
System.out.println(Arrays.toString(a2.split("\\.")));
  • 比如 split() 只要一些函数的形参为 regex 你就要小心点 代表它是一个正则字符串

小心 split / replace / replaceFirst / replaceAll

它们的形参名 也都叫做 regex,非常一不留神就踩坑

为什么说匹配过程非常昂贵

回溯

  • 比如你走迷宫,一条路走不通就要回到起点,走另一条路

推荐这样使用正则表达式

1
2
3
4
public class Demo1 {
// 这样就不会每次都编译
public static final Pattern p = Pattern.compile("你要匹配的正则表达式");
}

将文本的空白字符全部替换

1
"你的字符串".replaceAll("\\s","")

坑爹的 斜杠\

1
2
3
4
5
String s1 = "\\";
System.out.println(s1.matches("\\\\")); // 匹配 \

String s = "\\\\N";
System.out.println(s.matches("\\\\\\\\N")); // 匹配 \\N

匹配一个多行字符串

一些用了正则反而慢的地方

分组和捕获

  • 想要将所有符合正则表达式的文本抓出来处理
    • 实用括号来指定一个被捕获的分组
    • 分组的编号从1开始
    • 分组的编号计算只看左括号
    • (?:)不捕获和分配编号,括号只用于分组或标记优先级

匹配电话号码

\d{3}-\d{8}|\d{4}-\d{7}

现在只能判断符合不符合

1
2
3
Pattern telPattern = Pattern.compile("\\d{3}-\\d{8}|\\d{4}-\\d{7}");
System.out.println(telPattern.matcher("010-12345678").find()); // true
System.out.println(telPattern.matcher("010-1234567").find()); // false

抓去符合结果的分组

1
2
3
4
5
6
7
Pattern telPattern2 = Pattern.compile("(\\d{3})-\\d{8}|\\d{4}-\\d{7}");
Matcher matcher = telPattern2.matcher("010-12345678");

while(matcher.find()){
System.out.println(matcher.group(0)); // 0 代表整个被匹配的文本 结果是 010-12345678
System.out.println(matcher.group(1)); // 1 代表整个被匹配的文本中第一组我们感兴趣的信息 结果是 010
}

多个分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Pattern telPattern3 = Pattern.compile("(\\d{3})-(\\d{8})|(\\d{4})-(\\d{7})");
Matcher matcher = telPattern3.matcher("010-12345678\n5730-1234567\nfff-dfadsfds");

while(matcher.find()){
System.out.println(matcher.group(0));
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
System.out.println(matcher.group(3));
System.out.println(matcher.group(4));
}

运行结果:
010-12345678
010
12345678
null
null
5730-1234567
null
null
5730
1234567

分组编号只看左括号

  • 注意现在的左括号位置
  • 注意现在的左括号位置
  • 注意现在的左括号位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Pattern telPattern3 = Pattern.compile("(\\d{3})-(\\d{8})|(\\d{4})-(\\d{7})"); 
Pattern telPattern3 = Pattern.compile("(\\d{3})-(\\d{8})|(\\d{4}-(\\d{7}))");
Matcher matcher = telPattern3.matcher("010-12345678\n5730-1234567\nfff-dfadsfds");

while(matcher.find()){
System.out.println(matcher.group(0));
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
System.out.println(matcher.group(3));
System.out.println(matcher.group(4));
}

运行结果:
010-12345678
010
12345678
null
null
5730-1234567
null
null
5730-1234567
1234567

写了分组,但是我不想要这个分组 (?:)

  • (\\d{4}) 本来是第三组 matcher.group(3)
  • 如果你这样 (?:\\d{4}) 意思是 别看我有() 但是请不要甩它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pattern telPattern3 = Pattern.compile("(\\d{3})-(\\d{8})|(?:\\d{4})-(\\d{7})");
Matcher matcher = telPattern3.matcher("010-12345678\n5730-1234567\nfff-dfadsfds");
while(matcher.find()){
System.out.println(matcher.group(0));
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
System.out.println(matcher.group(3));
// System.out.println(matcher.group(4)); 限制忽略了一个分组 所以它没有第四个分组
}

运行结果:
010-12345678
010
12345678
null
5730-1234567
null
null
1234567

实战处理 GCLog

ZB-046-03AOP在Spring中应用

AOP和Spring

  • 在Spring项目里使用 AOP完成Redis缓存
  • Spring是如何切换 JDK动态代理和CGLIB的?
    • spring.aop.proxy-target-class=true
  • @Aspect声明切片
    • @Before
    • @After
    • @Around

实战:获取 Rank数据实现Redis缓存功能

RankService.doService 实现缓存功能

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class RankService {
@Autowired
private UserMapper userMapper;

@Autowired
RankDao rankDao;

public List<RankItem> doService(){
return rankDao.getRank();
}
}

仓库地址

https://github.com/slTrust/spring_ecosystem 从零开始可以从 spring生态系统开新分支

  • 搜索springboot aop starter
  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  • 我们知道spring aop有两种方式一个是 JDK动态代理(对应接口) 一个是 CGLIB(对应类)

    • 而当我们需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,不然默认使用的是标准Java的实现。
  • 在application.properties里添加这句话 spring.aop.proxy-target-class=true
    • 这一点在文档里有提到
  • 新建 CacheAspect.java
    1
    2
    3
    4
    5
    6
    // @Aspect 声明它是一个切面
    // @Configuration 是跟 spring有关的配置 运行的时候把它拷贝进来
    @Aspect
    @Configuration
    public class CacheAspect {
    }

接下来处理拦截

  • src/main/java/hello/anno/Cache.java 注解
1
2
3
4
5
6
7
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {

}

这样任何带有@Cache 的地方都可以被我们拦截到

RankService.java里添加为这个doService() 添加这个@Cache注解,来在它调用的时候拦截到它

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class RankService {
@Autowired
private UserMapper userMapper;

@Autowired
RankDao rankDao;

@Cache
public List<RankItem> doService(){
return rankDao.getRank();
}
}

回到CacheAspect.java 里指定拦截 @Cache的注解

1
2
3
4
5
6
7
8
9
10
11
// @Aspect 声明它是一个切面
// @Configuration 是跟 spring有关的配置 运行的时候把它拷贝进来
@Aspect
@Configuration
public class CacheAspect {
@Around("@annotation(hello.anno.Cache)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("method is called");
return joinPoint.proceed();
}
}

运行项目 打开地址 http://localhost:8080/rank 如果打印method is called 代表配置成功

先用 Map 做缓存

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
// @Aspect 声明它是一个切面
// @Configuration 是跟 spring有关的配置 运行的时候把它拷贝进来
@Aspect
@Configuration
public class CacheAspect {
Map<String,Object> cache = new HashMap<>();

@Around("@annotation(hello.anno.Cache)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("method is called");

MethodSignature signature = (MethodSignature)joinPoint.getSignature();
String methodName = signature.getName();
Object cacheValue = cache.get(methodName);

if(cacheValue != null){
System.out.println("cacheValue");
return cacheValue;
}else{
System.out.println("read realValue");
Object realValue = joinPoint.proceed();
cache.put(methodName,realValue);
return realValue;
}

}
}
  • 代码仓库 选择 spring-aop基于map的缓存实现 分支

Redis

  • 广泛使用的内存缓存
  • 常见的数据结构
    • String/List/Set/Hash/ZSet
  • Redis 为什么那么快
    • 完全基于内存
    • 优秀的数据结构设计
    • 单一线程,避免上下文切换开销
    • 事件驱动,非阻塞

如何用呢? 当然是 Docker Redis

  • google redis docker
    • 直接一句话就能运行 docker run --name some-redis -d redis
    • 如果使用缓存你就docker run --name some-redis -d redis redis-server --appendonly yes
  • 由于 redis是有 端口的所以 我们实际应该运行 docker run -p 6379:6379 --name some-redis -d redis

配置 redis 在 springboot 里

  • google springboot redis
  • 在application.properties 添加

    1
    2
    spring.redis.host=localhost
    spring.redis.port=6379
  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • 参考文档配置 RedisTemplate

  • 新建类 hello/config/AppConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class AppConfig {
    @Bean
    RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
    RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(factory);
    return redisTemplate;
    }
    }
  • 修改 CacheAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CacheAspect {
@Autowired
RedisTemplate<String,Object> redisTemplate;

@Around("@annotation(hello.anno.Cache)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("method is called");

MethodSignature signature = (MethodSignature)joinPoint.getSignature();
String methodName = signature.getName();
Object cacheValue = redisTemplate.opsForValue().get(methodName);

if(cacheValue != null){
System.out.println("cacheValue");
return cacheValue;
}else{
System.out.println("read realValue");
Object realValue = joinPoint.proceed();
redisTemplate.opsForValue().set(methodName,realValue);
return realValue;
}

}
}
  • 运行项目 访问 http://localhost:8080/rank
    • 结果报错了 java.io.NotSerializableException: hello.entity.RankItem 就是没法序列号
    • 我们知道java跟 redis 通讯 肯定是通过网络通讯的,那么传递的一定是字节流
    • 如何把 Java对象 变成 字节流呢?
      • 这个过程叫做 序列化
  • 给 RankItem 实现 Serializable 接口public class RankItem implements Serializable {...}
  • 给 User 也实现这个接口 public class User implements Serializable {...}
  • 此时 再次运行项目打开网址 http://localhost:8080/rank2
  • 至此我们就完成了 基于AOP和Spring实现Redis的缓存功能

为什么要用 Redis

  • 在使用上 Redis 和 Map 没有任何区别

但是为什么要用 Redis?

  • 分布式部署,你的应用 在机器里跑上面着JVM,处理请求
    • 问题来了,如果一台机子,你无法保证服务不中断,不升级,假如压力大了很容易把机器压垮了
    • 因此我们需要分布式部署,所谓分布式部署,多台机器多个 JVM 它们可以轮流处理请求,
      • 此时好处就是:容灾一个机器挂了,还能保证服务运行,分担压力
      • 在者就是 Map和 Redis的区别了
      • 多个个 JVM运行你的 应用,Map是在各自的内存里的 无法实现共享
      • 而 Redis 则是一个中心化的缓存在多个机器上共享缓存

代码仓库master分支

https://github.com/slTrust/spring_ecosystem

ZB-046-02AOP两种实现

AOP两种实现

上一篇仍然有很多不足,如 DataService 每个方法打日志,依然需要在 LogDecorator.java里每个功能都写一遍

能不能一进来的时候,所有方法进入和进出的时候都打印日志

一个疑问为什么 DataService 定义为接口(interface)而不是类(Class)

  • 接口定义了一个虚拟的协议/合同/合约
  • 变成了class咋样?
    • 每个东西继承它的时候,假设你要用抽象类有很多成员变量就会有多余的无用变量

AOP的实现

JDK的动态代理

新建 LogProxy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LogProxy implements InvocationHandler {
DataService delegate;

public LogProxy(DataService delegate) {
this.delegate = delegate;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "is start,params is " + Arrays.toString(args));
Object returnValue = method.invoke(delegate,args);
System.out.println(method.getName() + "is finised");
return returnValue;
}
}

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.aop.demo;

import java.lang.reflect.Proxy;

public class Main {
static DataService service = new LogDecorator(new DataServiceImpl());

static DataService service2 = new DataServiceImpl();

public static void main(String[] args) {
// OOP装饰器模式
System.out.println(service.a(1));
System.out.println(service.b(1));

// 动态代理
DataService dataService = (DataService) Proxy.newProxyInstance(service2.getClass().getClassLoader(), // 接口实现类
new Class[]{DataService.class}, // 代理类(必须是接口否则报错)
new LogProxy(service2) // 这个方法拦截后被谁处理
);

dataService.a(1);
dataService.b(2);
}
}

一个问题,我想要用类实现这个功能,而不是接口怎么办?

  • CGLIB/ByteBuddy 字节码生成
    • 优点:强大,不受接口的限制
    • 缺点:需要引入额外的第三方类库
      • 不能增强 final 类/final/ private方法 因为他用的继承的方式实现这些功能的

它们的本质,就是动态的class,没有具体的说明书,JDK直接通过字节码操作

google 搜索cglib methodinterceptor maven

pom.xml里添加

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

新建 DataServiceImpl2.java 注意这次是 类而不是接口了

1
2
3
4
5
6
7
8
9
public class DataServiceImpl2 {
public String a(int i) {
return UUID.randomUUID().toString();
}

public String b(int i) {
return UUID.randomUUID().toString();
}
}

新建LogInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LogInterceptor implements MethodInterceptor {
private DataServiceImpl2 delegate;

public LogInterceptor(DataServiceImpl2 delegate) {
this.delegate = delegate;
}

public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(method.getName() + " is start,params is " + Arrays.toString(objects));
Object returnValue = method.invoke(delegate,objects);
System.out.println(method.getName() + " is finised");
return returnValue;
}
}

Main.java

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
package com.aop.demo;

import net.sf.cglib.proxy.Enhancer;
import java.lang.reflect.Proxy;

public class Main {
static DataService service = new LogDecorator(new DataServiceImpl());

// AOP动态代理
static DataService service2 = new DataServiceImpl();

public static void main(String[] args) {
// OOP装饰器模式
System.out.println(service.a(1));
System.out.println(service.b(1));

System.out.println("动态代理-----------------");
// 动态代理
DataService dataService = (DataService) Proxy.newProxyInstance(service2.getClass().getClassLoader(), // 接口实现类
new Class[]{DataService.class}, // 代理类(必须是接口否则报错)
new LogProxy(service2) // 这个方法拦截后被谁处理
);

dataService.a(1);
dataService.b(2);

// AOP 动态字节码
System.out.println("动态字节码-----------------");
// 动态字节码增强
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(DataServiceImpl2.class);// 设置超类
enhancer.setCallback(new LogInterceptor(new DataServiceImpl2())); // 设置回调

// 使用动态字节码增强过的
DataServiceImpl2 enhancedService = (DataServiceImpl2) enhancer.create();
enhancedService.a(1);
enhancedService.b(2);
}
}

代码链接

https://github.com/slTrust/java-aop-demo

ZB-046-01AOP和装饰器模式

AOP和动态代理

什么是AOP Aspect-Oriented Programming

  • 相对于OOP
  • AOP是面向切片编程,关注一个统一的切面
  • AOP和Spring是不同的东西
    • 为什么考Spring总是问 AOP,因为绝大多数使用AOP的场景是在 spring容器里

OOP

1
2
3
4
5
6
a.b()
b.c()

假如我现在有个 AService 它有100个方法,如何做到调用它的每个方法的时候打印日志,结束的时候打印日志呢?

装饰器模式

AOP

1
2
3
4
5
6
7
8
9
10
11
AService 它有100个方法
AService
----|a()
----|b()
----|c()
----|d()
----|...
----|xxx()

在进入每个方法的瞬间都看作一个切面 ,每次进入这个方法的一瞬间拦截下来执行某些操作
关注的不是对象方法本身,而是在进入某个方法的一瞬间的这个切面

AOP适用于那些场景

  • 需要统一处理的场景
    • 日志
    • 缓存
    • 鉴权
  • 如果用OOP来做需要怎么办?
    • 装饰器模式

装饰器模式

  • 俄罗斯套娃
  • 在它外面包一层

新建 maven空项目

DataService.java

1
2
3
4
public interface DataService {
String a(int i);
String b(int i);
}

DataServiceImpl.java

1
2
3
4
5
6
7
8
9
10
public class DataServiceImpl implements DataService {

public String a(int i) {
return UUID.randomUUID().toString();
}

public String b(int i) {
return UUID.randomUUID().toString();
}
}

Main.java

1
2
3
4
5
6
7
8
9
10
package com.aop.demo;

public class Main {
static DataService service = new DataServiceImpl();

public static void main(String[] args) {
System.out.println(service.a(1));
System.out.println(service.b(1));
}
}

需求在DataService的任何一个方法进入的时候打印日志

你可能会这样~~~

  • 但是如果它有100个方法,你就要写100次非常容器出错
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DataServiceImpl implements DataService {
public String a(int i) {
System.out.println("a is start");
System.out.println("a is finish");
return UUID.randomUUID().toString();
}

public String b(int i) {
System.out.println("b is start");
System.out.println("b is finish");
return UUID.randomUUID().toString();
}
}

装饰器模式

  • 动态为对象增加功能,但是不改变其结构
  • 本质是一个“包装”

新建 LogDecorator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LogDecorator implements DataService {
DataService delegate;

public LogDecorator(DataService delegate) {
this.delegate = delegate;
}

public String a(int i) {
System.out.println("a is start");
String value = delegate.a(i);
System.out.println("a is finish");
return value;
}

public String b(int i) {
System.out.println("b is start");
String value = delegate.a(i);
System.out.println("b is finish");
return value;
}
}

Main.java

1
2
3
4
5
6
7
8
9
public class Main {
// 额外包了一层,不用改变原有功能
static DataService service = new LogDecorator(new DataServiceImpl());

public static void main(String[] args) {
System.out.println(service.a(1));
System.out.println(service.b(1));
}
}

带缓存功能的装饰器

CacheDecorator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CacheDecorator implements DataService {
private Map<String,String> cache = new HashMap();

private DataService delegate;

public CacheDecorator(DataService delegate) {
this.delegate = delegate;
}

public String a(int i) {
String cacheValue = cache.get("a");
if(cacheValue == null){
String realValue = delegate.a(i);
cache.put("a",realValue);
return realValue;
} else {
return cacheValue;
}
}

public String b(int i) {
return null;
}
}

代码链接

https://github.com/slTrust/java-aop-demo

ZB-045-01Spring生态系统

目标

  • 在 Spring 容器中引入 Bean
  • Spring + Mybatis(H2/MySQL/Postgres)
  • 模版引擎 后端渲染HTML
  • 前后端分离和后端渲染

初始项目结构如下仓库内容

后端开发三层

  • Controller
  • Service
  • Dao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    HTTP
^
| |
v
________________
Controller 只做和HTTP请求相关

Service 具体的业务逻辑 业务代码

Dao 只做数据库
________________
^
| |
v
db数据库

配置MyBatis

001MyBatis依赖

1
2
3
4
5
6
7
8
9
搜索 springboot mybatis starter
得到 https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
找到依赖,添加到pom.xml

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>

002数据库依赖,我们先用h2

1
2
3
4
5
6
7
8
9
10
11
12
搜索 h2 maven
得到 https://mvnrepository.com/artifact/com.h2database/h2/1.4.200
找到依赖,添加到pom.xml

<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<!--注释掉这句 -->
<!--<scope>test</scope>-->
</dependency>

003添加 src/main/resources/application.properties

1
2
3
4
5
spring.datasource.url=jdbc:h2:file:./target/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.h2.Driver
mybatis.config-location=classpath:db/mybatis/config.xml

004 新建数据库初始化文件

src/main/resources/db/migration/V1__CreateTables.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
create table user(
id bigint primary key auto_increment,
name varchar(100)
);

create table match(
id bigint primary key auto_increment,
user_id bigint,
score int
);

insert into user(id,name) values(1,'AAA');
insert into user(id,name) values(2,'BBB');
insert into user(id,name) values(3,'CCC');


insert into match(id,user_id,score) values(1,1,1000);
insert into match(id,user_id,score) values(2,1,2000);
insert into match(id,user_id,score) values(3,2,500);
insert into match(id,user_id,score) values(4,3,300);

005 安装 flyway 数据库迁移工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# pom.xml 里添加
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>6.0.7</version>
</plugin>


#配置用户名密码
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>6.0.7</version>
<configuration>
<url>jdbc:h2:file:${project.basedir}/target/test</url>
<user>root</user>
<password>root</password>
</configuration>
</plugin>

006点击maven里 plugin里的 flyway –> migrate 运行 此时数据库初始化完成

007 配置 MyBatis

  • 修改src/main/resources/application.properties.xml
1
2
3
4
5
6
spring.datasource.url=jdbc:h2:file:.target/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.h2.Driver
# 配置mybatis
mybatis.config-location=classpath:db/mybatis/config.xml
  • 新建 src/main/resources/db/mybatis/config.xml
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
<mappers>
<mapper resource="db/mybatis/MyMapper.xml"/>
</mappers>
</configuration>

008创建 Mapper和 Entity

  • 创建 src/main/java/dao/UserMappper.java
1
2
3
4
5
@Mapper
public interface UserMapper {
@Select("select * from user where id = #{id}")
public User getUserById(@Param("id") Integer id);
}
  • 创建 src/main/java/entity/User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User {
private Integer id;
private String name;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

009 新建 src/main/resources/db/mybatis/MyMapper.xml

  • 搜 mybatis 入门文档抄过来
1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<!-- 你是注解方式写的sql -->
</mapper>

010 修改 config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--<settings>-->
<!--<setting name="logImpl" value="LOG4J"/>-->
<!--</settings>-->
<mappers>
<mapper resource="db/mybatis/MyMapper.xml"/>
<mapper class="hello.dao.UserMapper"/>
</mappers>
</configuration>

如何在 Spring 中使用 Bean

@Service

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class RankService {
@Autowired
private UserMapper userMapper;

@Autowired
RankDao rankDao;

public void doService(){

}
}

待更新

模版引擎

搜索freemarker springboot starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

新建 rescources/templates/index.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<head>
<title>排行榜</title>
</head>
<body>
<h1>排行榜!</h1>
<table>
<tr>
<th>排名</th>
<th>名字</th>
<th>分数</th>
</tr>

<#list items as item>
<tr>
<td>${item?index}</td>
<td>${item.user.name}</td>
<td>${item.score}</td>
</tr>
</#list>

</table>
</body>

HelloController.java

1
2
3
4
5
6
7
8
@RequestMapping("/rank2")
public ModelAndView rank2() {
List<RankItem> items = rankService.doService();
HashMap<String,Object> model = new HashMap<>();
model.put("items",items);
// 自动去 templates目录找 index.ftl
return new ModelAndView("index",model);
}

ZB-044-01从零开始Spring应用

从零开始Spring

001新建一个目录 spring_demo001

002搜索 spring boot

新建 pom.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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework</groupId>
<artifactId>gs-spring-boot</artifactId>
<version>0.1.0</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<properties>
<java.version>1.8</java.version>
</properties>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

003按文档新建目录src/main/java/hello

004Create an Application class

  • src/main/java/hello/Application.java
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
package hello;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {

System.out.println("Let's inspect the beans provided by Spring Boot:");

String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
System.out.println(beanName);
}

};
}

}

005新建 src/main/java/hello/HelloController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hello;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}

}

006右键运行Application.java文件

打开 http://localhost:8080/

此时一个非常基础的SpringBoot 应用成功运行了

header是什么?

  • 请求头

payload是什么?

  • 负载,请求体

queryString 获取查询参数

获取 get 参数

1
2
3
4
5
6
@RequestMapping("/search")
public String search(@RequestParam String q) {
// localhost:8080/search?q=123 时
System.out.println(q);
return "search query word is:" + q;
}

get请求是没有body的吗?

  • googleget payload

  • get请求一般在生产环境里是没有payload的

  • get请求是不禁止有 payload的
  • 只是 http 中明确规定了不推荐
1
2
3
4
5
6
原文回答
Yes. In other words, any HTTP request message is allowed to contain a message body, and thus must parse messages with that in mind. Server semantics for GET, however, are restricted such that a body, if any, has no semantic meaning to the request. The requirements on parsing are separate from the requirements on method semantics.

So, yes, you can send a body with GET, and no, it is never useful to do so.

This is part of the layered design of HTTP/1.1 that will become clear again once the spec is partitioned (work in progress).

控制get参数是否必填

  • 通过注解设置参数是否必填@RequestParam(value="charset",required = false)
1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/search2")
public String search2(@RequestParam String q ,
@RequestParam(value="charset",required = false) String charset) {

return "search query word is:" + q + ",and charset is :" + charset;
}

// http://localhost:8080/search2?q=123
// search query word is:123,and charset is :null

// http://localhost:8080/search2?q=123&charset=22
// search query word is:123,and charset is :22

get请求通常不能用来传递敏感信息,如登录。

幂等性

无论多少次get请求 都应该是一种结果

  • 例如 :x无论经历多少次 绝对值都是 “一种结果”

不同于 post/put/delete等

RESTful API

  • 使用HTTP动词代表动作
    • GET 获取资源
    • POST 新建资源
    • PUT 更新资源
    • DELETE 删除资源
  • 使用 URL(名词)代表资源
    • 使用复数来代表资源列表
    • 资源里面没有动词

搜索 github api 业界规范/行业标杆

RESTful API 好处

  • 优雅
  • 方便管理url

RESTful API风格url

参考 github issues api

  • @RequestMapping("repos") 统一处理 ‘/repos路径下的请求’

新建src/main/java/hello/IssueController.java

  • unlock issue

形如 DELETE /repos/:owner/:repo/issues/:issue_number/lock

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
package hello;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("repos")
public class IssueController {

// DELETE /repos/:owner/:repo/issues/:issue_number/lock
@DeleteMapping("/{owner}/{repo}/issues/{issue_number}/lock")
public String unlock(
@PathVariable("owner") String owner,
@PathVariable("repo")String repo,
@PathVariable("issue_number")String issue_number
){
// 把当前请求路径重的参数 绑定到当前变量中去
// @PathVariable()
System.out.println(owner);
System.out.println(repo);
System.out.println(issue_number);

return "owner:" + owner + ",repo:" + repo + ",issue_number:" + issue_number;
}
}
  • 访问 localhost:8080/repos/abc/spring/issues/1111/lock

create issue

形如POST /repos/:owner/:repo/issues

  • 除了路径参数
  • 还有 请求体内容
1
2
3
4
5
6
7
8
9
10
11
{
"title": "Found a bug",
"body": "I'm having a problem with this.",
"assignees": [
"octocat"
],
"milestone": 1,
"labels": [
"bug"
]
}

添加如下方法

1
2
3
4
5
6
7
8
9
10
@PostMapping("/{owner}/{repo}/issues")
public String create(
@PathVariable("owner") String owner,
@PathVariable("repo")String repo,
@RequestBody Object object
){

System.out.println(object);
return "owner:" + owner + ",repo:" + repo + ",request body:" + object;
}

此时用 postman发请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
localhost:8080/repos/abc/vue/issues
method 是 post

body 里添加 选择 raw
{
"title": "Found a bug",
"body": "I'm having a problem with this.",
"assignees": [
"octocat"
],
"milestone": 1,
"labels": [
"bug"
]
}

raw的 type 如果是 text 结果返回是 415
{
"timestamp": "2019-11-04T03:21:56.467+0000",
"status": 415,
"error": "Unsupported Media Type",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/repos/abc/vue/issues"
}

为什么返回415?

1
2
3
4
5
6
7
8
9
10
11
12
              请求
-------->
client server
<--------
响应


受限于底层 实际传输的数据都是 “字节流”

如何区分这些不同类型的数据
必须有一个东西 标识 这些不同的数据是什么类型
media type

media type 也叫做 Content-Type

修改刚刚 postman的 header

1
Content-Type:application/json

此时访问响应正常了

IDEA工具 GsonFormat

自动将json转换为 实体类字段

新建 MyParam.java

1
2
3
command + n 选择 GsonFormat 
复制你的json字符串
然后ok

再次修改我们的 POST 请求

  • method:post
  • 请求体是 json
  • content-type:application/json
1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/{owner}/{repo}/issues")
public String create(
@PathVariable("owner") String owner,
@PathVariable("repo")String repo,
@RequestBody MyParam object
){
System.out.println(object);
object.getTitle();
object.getLabels().get(0);
return "owner:" + owner + ",repo:" + repo + ",request body:" + object;
}

form表单里的 post请求

1
2
3
4
5
6
7
8
9
10
// localhost:8080/repos/demo
// form-data 一般用于参数少的时候
@PostMapping("/demo")
public void postform(
@RequestParam String name,
@RequestParam Integer age
){
System.out.println(name);
System.out.println(age);
}

⽣成HTTP响应

  • 直接操作HttpServletResponse对象
    • 原始、简单、粗暴
  • 直接返回HTML字符串
    • 原始、简单、粗暴
  • 返回对象,并⾃动格式化成JSON
    • 常⽤
    • @ResponseBody
  • 模板引擎渲染
    • JSP/Velocity/Freemaker

操作HttpServletResponse对象

1
2
3
4
5
6
7
8
9
// 通过 HttpServletResponse 设置响应的内容
@RequestMapping("search")
public void search(
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("haha");
}

返回对象,并⾃动格式化成JSON @ResponseBody

1
2
3
4
5
6
7
8
9
// 通过 @ResponseBody 设置响应为 json
@RequestMapping("search2")
@ResponseBody
public Object search2(){
Map<String,Object> result = new HashMap<>();
result.put("name","aaa");
result.put("age",18);
return result;
}

accept设置

  • client和 server通信,通过 accept/content-type 来约定通信的格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
              请求
accept:xml
-------->
client server
<--------
响应
content-type:xml

---------------------------------------------

请求
accept:json
-------->
client server
<--------
响应
content-type:json

服务器的职责:通过客户端的请求接受的 accept mediaType,来响应对应 mediaType的数据

周边⽣态系统

知道了上面的内容,你就可以在此基础上扩展你的应用

  • HTTPS
  • 分布式部署
  • 扩展功能
    • 数据库
    • Redis缓存
    • 消息队列
    • RPC(Dubbo/Spring Cloud)
    • 微服务化

代码仓库

解决 IDEA 导入项目问题

  • 依赖项一直爆红
  • mvn clean install
  • 删除项目的 .idea文件和 .iml文件然后重新导入项目

ZB-039-02手写SpringIoc容器

仓库地址

手写精简版Ico容器

在上篇文章的基础上

001 新建 src/main/resources/ioc.properties

1
2
orderDao=com.github.hcsp.OrderDao
orderService=com.github.hcsp.OrderService

002 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
public class MyIocContainer {
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
properties.load(MyIocContainer.class.getResourceAsStream("/ioc.properties"));

Map<String,Object> beans = new HashMap<>();

// bean的初始化
properties.forEach((beanName,beanClass)->{
try {
Class<?> klass = Class.forName((String) beanClass);
Object beanInstance = klass.getConstructor().newInstance();
beans.put((String)beanName,beanInstance);
} catch (Exception e) {
throw new RuntimeException(e);
}
});

// 装配依赖
beans.forEach((beanName,beanInstance)-> dependencyInject(beanName,beanInstance,beans));

OrderService orderService = (OrderService) beans.get("orderService");
OrderDao orderDao = (OrderDao) beans.get("orderDao");
System.out.println();
}

private static void dependencyInject(String beanName, Object beanInstance, Map<String, Object> beans) {
// 把所有的 field扫描一遍
List<Field> fieldsToBeAutowried = Stream.of(beanInstance.getClass().getDeclaredFields())
.filter(field -> field.getAnnotation(Autowired.class) != null)
.collect(Collectors.toList());
// 过滤带有 @Autowired 注解的过滤出来

fieldsToBeAutowried.forEach(field -> {
try {
String fieldName = field.getName();
Object dependencyBeanInstance = beans.get(fieldName);
field.setAccessible(true);
field.set(beanInstance,dependencyBeanInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
}

最难能可贵的是,还能兼容循环引用

  • OrderService 依赖 OrderDao
  • OrderDao 还依赖 OrderService
  • 这个依赖的还是单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderService {
@Autowired
private OrderDao orderDao;

// ...
}


public class OrderDao {
@Autowired
private OrderService orderService;
// ...
}

// 循环引用,不会重复产生对象。

题外话 stream非常重要

在 junit 里 stream 无处不在

循环依赖的问题怎么解决,它是为什呢出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 循环依赖出现的问题是因为 ,构造器注入
public class OrderDao {
private OrderService orderService;

@Autowired
public OrderDao(OrderService orderService) {
this.orderService = orderService;
}
// ...
}


public class OrderService {
private OrderDao orderDao;

public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
// ...
}


这是一个在初始化过程中的循环依赖,是无法解决的

什么循环依赖是好解决的

是它们创建出来之后,在分别对他们的依赖进行设值

无状态和有状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderDao {
// 这个值 随着运行的时间而改变叫做“有状态”
private int count;
@Autowired
private OrderService orderService;

public void select(){
count++;
System.out.println("select");
}
}


// 无状态代表。在整个生命周期里,它里面的某种状态 比如 orderService的引用 始终是一个

无状态的好处是:线程安全的,无状态结果更加容易预测

而如果是有状态的。排查的过程中非常繁杂

@Autowired 的推荐用法

推荐写在构造器上注入

1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderService {
private OrderDao orderDao;

@Autowired
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}

public void doSomething(){
System.out.println(orderDao);
orderDao.select();
}
}

不推荐这样,因为这样意味着你从来不写单元测试

1
2
3
4
5
6
7
8
9
public class OrderService {
@Autowired
private OrderDao orderDao;

public void doSomething(){
System.out.println(orderDao);
orderDao.select();
}
}

为什么不推荐在Field上 注解,而是在 构造器上

因为有个非常要命的问题,因为它是 private OrderDao orderDao; private成员

spring是通过启动的时候,通过反射把它的依赖注入进去的。

但是如果你要进行单元测试

此时你就会碰到问题,因为它是private的你必须要用反射才能把它注入进去

而如果你用构造器注入的好处是你可以 mock它

Spring启动过程

  • 在xml里定义Bean
  • BeanDefinition的载入和解析
  • Bean的实例化和依赖注入
  • 对外提供服务

ZB-039-01Spring基本概念

Spring是什么

  • Java世界的事实标准
    • Spring容器 一个Ioc容器
      • Spring MVC 基于 Spring 和 Servlet的Web 应用框架
        • Spring Boot 集成度和自动化程度更高

没有 Spring 时,我们怎么做

选择一:一个 main 程序打天下

  • 非常轻量,适用于非常简单的场景
    • 易懂
  • 一旦规模上来
    • 难以维护
    • 一场灾难

选择二:拆分并手动管理

  • 拆分成多个模块
  • 优点:
    • 方便测试
    • 方便共同开发
    • 方便维护
  • 缺点:
    • 依赖关系纷繁复杂

Spring出现了,解放了双手

Spring容器的核心概念

  • Bean
    • 容器中的最小工作单元,通常为一个 Java对象
  • BeanFactory/ApplicationContext
    • 容器本身对应的Java对象
  • 依赖注入(DI)
    • 容器负责注入所有的依赖
  • 控制反转(Ioc)
    • 用户将控制权交给容器

Spring容器的实质

001新建项目

pom.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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>spring_di_ioc</groupId>
<artifactId>spring_di_ioc_demo</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>
</project>

002添加 spring依赖 搜索 spring maven

找到 spring context 添加它的依赖 到 pom.xml

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>

003新建 src/main/java/com/github/hcsp/SpringMain.java

1
2
3
4
5
public class SpringMain {
public static void main(String[] args) {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath:config.xml");
}
}

004什么是config.xml,上古时代的spring配置

搜索 spring xml 得到它的格式文件开始抄

新建 src/main/resources/config.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- bean definitions here -->

</beans>

005新建 OrderDao 和 OrderService

src/main/java/com/github/hcsp/OrderDao.java

1
2
3
4
5
public class OrderDao {
public void select(){
System.out.println("select");
}
}

src/main/java/com/github/hcsp/OrderService.java

1
2
3
4
5
6
7
8
public class OrderService {
// 依赖另一个模块
private OrderDao orderDao;

public void doSomething(){
orderDao.select();
}
}

006修改config.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- bean definitions here -->

<bean id="orderDao" class="com.github.hcsp.OrderDao"/>
<bean id="orderService" class="com.github.hcsp.OrderService"/>
</beans>

Bean是什么? 本质就是一个 java对象

那容器是什么呢? 容器就是 BeanFactory

007 修改 SpringMain.java

1
2
3
4
5
6
7
8
9
10
11
public class SpringMain {
public static void main(String[] args) {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath:config.xml");

// 此行断点
OrderService orderService = (OrderService) beanFactory.getBean("orderService");
System.out.println(orderService);

orderService.doSomething();
}
}

debug运行 你会发现 orderService 会自动加载进来

而且还是单例的,完整运行会报错,因为 orderService 依赖了 OrderDao

008 通过注解告诉java 那些是依赖

你有三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderService {
// 三种注解告诉 java 依赖内容
// @Autowired 自动装配
// @Resource
// @Inject
// private OrderDao orderDao;

@Autowired
private OrderDao orderDao;

public void doSomething(){
System.out.println(orderDao);
orderDao.select();
}
}

当你 @Autowired 的时候发生了什么?

如果你学过注解,对于标记了它之后,其实对于这个类来说其实没有变化,只是多个注解

这个注解其实没有任何卵用,其实它是 被人动态的 在运行时扫描来完成对应工作。 如果没人 扫描这个注解 其实没用

009 搜索 spring xml annotation

得知 在 config.xml 里添加一句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version = "1.0" encoding = "UTF-8"?>

<beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xmlns:context = "http://www.springframework.org/schema/context"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<!-- 添加这句:意思是 请你看到注解的时候帮我留意以下 -->
<context:annotation-config/>
<!-- bean definitions go here -->

</beans>

010再次运行 SpringMain.java

1
2
3
4
5
debug  你发现运行的时候
根据 config.xml 创建了 两个 bean实例 OrderService 和 OrderDao
而难能可贵的是
OrderService 依赖的 OrderDao
跟上面的 OrderDao 是同一个 (单例)

这就是 spring

DI 依赖注入

容器负责注入所有的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// OrderService 依赖了 OrderDao
public class OrderService {
@Autowired
private OrderDao orderDao;

public void doSomething(){
System.out.println(orderDao);
orderDao.select();
}
}

// orderDao 一开始是 null
// 虽然声明了依赖但全部都是 null
// 在一个魔法一般的过程之后
// spring容器让他们所有的依赖都变成正确的依赖
// 这就是依赖注入

Ioc 控制反转

用户将控制权交给了容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以前我们写程序要这样 手工的去创建对象
OrderService orderService = new OrderService();
orderService.select();
// 我们控制了对象的所有装配和运行
// 这是完全由人去控制 装配

// 而控制反转呢?
就是你通过声明 A 依赖了 B ,此时天上掉下来一个阿拉丁神灯。他挥了下魔法棒 biu 的一声,这些东西全部自动装配好了,变成了一个可以用的容器

这就是控制反转的过程,你不需要控制 它们所有的依赖 和装配的进行
你只需要声明 它们 谁依赖了谁。有人会自动的帮你进行装配

这就是 控制反转

这个挥魔法棒的人 叫做—— Spring 容器

代码仓库

https://github.com/slTrust/spring_ioc_demo

ZB-038-04MyBatis与设计模式

MyBatis与设计模式

多种设计模式的集大成者

每当你看到 xxxFactory 的时候,基本可以断定它是一个工厂模式

  • 抽象工厂模式: SqlSessionFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface SqlSessionFactory {

SqlSession openSession();

SqlSession openSession(boolean autoCommit);

SqlSession openSession(Connection connection);

SqlSession openSession(TransactionIsolationLevel level);

SqlSession openSession(ExecutorType execType);

SqlSession openSession(ExecutorType execType, boolean autoCommit);

SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

SqlSession openSession(ExecutorType execType, Connection connection);

Configuration getConfiguration();

}

单例模式: ErrorContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ErrorContext {

private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();

// ...

// 私有方法,意味着外部无法使用这个构造器创建实例
private ErrorContext() {
}


public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
// 保证了 context 只有一个
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}

// ...

}

单例模式有很多变体

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
// 你可以这样
public class MySingleton {

private static final MySingleton INSTANCE = new MySingleton();

private MySingleton(){}

public static MySingleton getInstance(){
return INSTANCE;
}
}

// 也可以这样,但是多线程下可能有问题
public class MySingleton {

private static MySingleton INSTANCE = null;

private MySingleton(){}

public static MySingleton getInstance(){
if(INSTANCE == null){
INSTANCE = new MySingleton();
}
return INSTANCE;
}
}

// 还可以这样 使用枚举
public enum MySingleton {

INSTANCE;

public void dosomething(){

}
}

代理模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// xml方式
public void deleteUserById(Integer id) {
try (SqlSession session = sqlSessionFactory.openSession(true)) {
session.delete("MyMapper.deleteUserById", id);
}
}

// 代理模式
interface UserMapper{
@Delete("delete from user where id =#{id}")
void deleteUserById(@Param("id") Integer id);
}
public void deleteUserById2(Integer id) {
try (SqlSession session = sqlSessionFactory.openSession(true)) {
// 此行断点,进去就可以看到 MapperProxyFactory 就是一种代理模式
UserMapper userMapper = session.getMapper(UserMapper.class);
userMapper.deleteUserById(id);
}
}

原理

  • 每当你写一个 interface UserMapper 的时候
  • 它就会把你这个接口传递给 Proxy代理,帮你动态创建代理实例
  • 通过 反射 invoke 调用接口相应的方法

装饰器模式

在已有如 Utils类 功能上增加附加功能,但是不改变 Utils类内部的逻辑

典型方式:缓存 如 CachingExecutor

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
public class CachingExecutor implements Executor {

private final Executor delegate;
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 委托查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

// ...
}

模版模式

  • BaseExecutor
1
2
3
4
5
6
7
8
9
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}

适配器模式 adapter

如电源插口

  • 中国的电源
  • 美国的电源
  • 日本的电源

MyBatis 有很多种日志 Log

为他们提供接口,自己按标准去实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Log {

boolean isDebugEnabled();

boolean isTraceEnabled();

void error(String s, Throwable e);

void error(String s);

void debug(String s);

void trace(String s);

void warn(String s);

}

ZB-038-03MyBatis动态sql

动态sql——MyBatis的灵魂

  • <if>
  • <choose>
  • <foreach>
  • <script>

Dynamic SQL

selectUserByName需求

  • if
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 如果你这样 ,name 没有时会报错
<select id="selectUserByName" resultType="User">
select * from user
where
<if test="name != null">
name like #{name}
</if>
</select>

# 它会拼成这样
# select * from user where

# 你可以这样
<select id="selectUserByName" resultType="User">
select * from user
<if test="name != null">
where name like #{name}
</if>
</select>
  • choose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select id="chooseUser" resultType="User">
select * from user
<choose>
<when test="name =='wangwu'">
where name = 'wangwu'
</when>
<otherwise>
where name = 'zhangsan'
</otherwise>
</choose>
</select>

// 这里的坑在这
<when test="name =='wangwu'">

不要写成这样
不要写成这样
不要写成这样
<when test="name ==wangwu">
这样它会在运行时把 wangwu 当作查询User的字段而不是字符串
  • where / set
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
<select id="findActiveBlogLike" resultType="Blog">
select * from Blog
where
<if test="state != null">
name like #{state}
</if>
</select>

如果条件都state = null,不满足就会导致语法错误
SELECT * FROM Blog
WHERE

正确的做法是
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>

如果条件不满足它就没有where

set语句也能动态

1
2
3
4
5
6
7
8
9
10
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
  • 批量操作 foreach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// UserMapper.xml
// item循环的对象
// index索引
// collection 结果集
<select id="selectIds" resultType="User">
select *
from user where id in
<foreach item="item" index="index" collection="ids"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

// Sql.java
Map<String,Object> param = new HashMap<>();
param.put("ids", Arrays.asList(1,2));
List<User> resUser = session.selectList("com.sql.xml.UserMapper.selectIds",param);
System.out.println(resUser);
  • script 不常用

把xml里的内容放在注解里

1
2
3
4
5
6
7
8
9
10
11
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);

MyBatis实战

涵盖内容

  • CRUD
  • 联表操作
  • association 关系映射