JAVA27-Springboot与装饰器模式

WEB后端开发中的三层结构

  • Controller 只做和HTTP请求响应相关的事情
  • Service 做的事情最多,处理业务逻辑
  • DAO 只做数据库操作相关的事情

为Springboot添加MyBatis模块

Springboot为我们准备了很多为初学者准备的模块,称为starter

  • 添加MyBatis模块
1
2
3
4
5
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
  • 配置Springboot数据源
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/mybatis-config.xml
  • 配置MyBatis
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?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>
    <!-- 因为mybatis的环境配置已经在Spring中配置了,因此这里只需要Mapper配置即可 -->
    <mappers>
        <mapper resource="db/mybatis/mybatis-mapper.xml"/>
    </mappers>
</configuration>

其他的操作就和正常使用MyBatis一样了

关于Bean

之前我们了解到,在MyBatis中配置SQL可以用接口的方式,只要声明一个接口,获取一个Mapper对象,就可以直接执行SQL了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * 删除一个用户。
 *
 * @param id 待删除的用户ID
 */
public void deleteUserById(Integer id) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
        DeleteUserByIdMapper mapper = sqlSession.getMapper(DeleteUserByIdMapper.class); // 可以看到,必须获取Mapper对象
        mapper.deleteUserById(id);
    }
}

interface DeleteUserByIdMapper {
    @Delete("DELETE FROM user WHERE ID = #{id}")
    void deleteUserById(@Param("id") Integer id);
}

@Mapper注解

在Springboot中,我们通过一个@Mapper注解,Mybatis就会帮我们自动进行依赖注入

1
2
3
4
5
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #{id}")
    User getUserById(@Param("id") Integer id);
}

但我们自己的类,如何告诉Spring这是一个Bean,并且帮我进行依赖注入呢?
因为如果你不告诉Spring,那么它就是一个普通的类,普通的类就意味着它必须实例化
而且需要你自己传入传出

@Service 注解

我们通过给普通的类加上@Service注解,Springboot就可以识别它是一个Bean了,
这时候你通过@Autowired注解就可以自动注入了

1
2
3
4
5
6
7
8
9
@Service
public class RankDao {
    @Autowired
    private SqlSession sqlSession;

    public List<Rank> selectRankList(){
        return sqlSession.selectList("com.github.wjinlei.mapper.selectRankList");
    }
}

优先使用构造器注入,而不要使用@Autowired字段注入

这个等价于上面的用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Service
public class RankDao {
    private final SqlSession sqlSession;

    public RankDao(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    public List<Rank> selectRankList(){
        return sqlSession.selectList("com.github.wjinlei.mapper.selectRankList");
    }
}

采用构造器注入可以避免污染测试,而且IDEA也不会警告

为Springboot添加 Freemarker 模板引擎

  • 添加依赖
1
2
3
4
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
  • Freemarker规定,模板文件放在resources/templates目录中,文件名以.ftlh结尾

添加一个模板文件index.ftlh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
    <title>
        排行榜
    </title>
</head>
<body>
<table>
    <tr>
        <th>排名</th>
        <th>名称</th>
        <th>分数</th>
    </tr>
    <#list ranks as item>
        <tr>
            <td>${item?index+1}</td>
            <td>${item.user.name}</td>
            <td>${item.score}</td>
        </tr>
    </#list>
</table>
</body>
  • Controller中使用ModelAndView将数据渲染到模板中
1
2
3
4
5
6
7
8
@GetMapping("/rank")
@ResponseBody
public ModelAndView rank() {
    HashMap<String, Object> hashMap = new HashMap<>();
    List<Rank> rankList = rankService.getRankList();
    hashMap.put("ranks", rankList);
    return new ModelAndView("index", hashMap);
}

AOP(面向切面编程 Aspect-oriented programming)

面向切面编程 Aspect-oriented programming,它相对的概念是OOP(Object-oriented programming),在进入某个切面的瞬间将它拦截下来,做我们想做的事,什么是切面?反映在方法栈中,栈帧被调用的瞬间就是一个切面,AOP关注的是一个统一的切面,而不是某个具体的对象或方法,说白了AOP思想就像一个拦截器一样,对某个统一的状态拦截下来,做统一处理

AOP的适用场景 需要做统一处理的地方

  • 日志
  • 缓存
  • 鉴权

如果用OOP的思想应该怎么做?

答:使用装饰器模式

装饰器模式

在不改变原来业务逻辑的情况下,在外部在包一层,添加我们自己的功能,然后再调用原来的功能
装饰器模式,一般配合接口使用,例如有如下示例:

  • Hello 接口
1
2
3
public interface Hello {
    void hello();
}
  • HelloWorld 实现类
1
2
3
4
5
6
public class HelloWorld implements Hello {
    @Override
    public void hello() {
        System.out.println("Hello World!");
    }
}
  • Main
1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Hello hello = new HelloWorld();
        hello.hello();
    }
}

现在,想要给HelloWorld的hello方法添加一个功能,输出"Hello Maven",在不改变代码的情况洗下我们应该怎么做?

  • 新建一个HelloMavenDecorator实现类,内部包裹一个HelloWorld成员

一般约定,装饰器类,名称后面添加Decorator后缀,表示这是一个装饰器类,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class HelloMavenDecorator implements Hello {
    private HelloWorld helloWorld;

    public HelloMavenDecorator(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }

    @Override
    public void hello() {
        System.out.println("Hello Maven"); // 我们的新功能
        helloWorld.hello(); // 调用原来的功能
    }
}
  • Main 这时候调用就变成了如下的样子
1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Hello hello = new HelloMavenDecorator(new HelloWorld());
        hello.hello();
    }
}

这就是装饰器模式

AOP的两种实现,JDK动态代理和字节码生成

JDK动态代理

下面我们通过JDK的动态代理功能,实现和上面装饰器模式同样的功能

  • 创建一个代理类,今后所有被拦截的方法,都会进入这个类的invoke方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class HelloProxy implements InvocationHandler {
    private Hello delegate; // 被代理对象

    public HelloProxy(Hello delegate) {
        this.delegate = delegate;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Hello Proxy"); // 我们的功能
        method.invoke(delegate, args); // 原来的功能
        return null;
    }
}
  • Main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Main {
    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        Hello hello = (Hello) Proxy.newProxyInstance(
                helloWorld.getClass().getClassLoader(), // 被代理对象的ClassLoader
                new Class[]{Hello.class}, // 要代理的接口,只有被代理的接口未来被调用的时候才会被拦截
                new HelloProxy(helloWorld)); // 代理对象实例,说白了当拦截发生时交给谁去处理
        hello.hello();
    }
}

到现在为止,你可以看不出AOP的好处,那么请看下面的示例

  • Hello 接口 现在我们的Hello接口有2个方法
1
2
3
4
public interface Hello {
    void hello();
    void uuid();
}
  • HelloWorld实现类 覆盖这两个方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class HelloWorld implements Hello {
    @Override
    public void uuid() {
        System.out.println(UUID.randomUUID());
    }

    @Override
    public void hello() {
        System.out.println("Hello World!");
    }
}
  • HelloMavenDelegate 装饰器类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class HelloMavenDecorator implements Hello {
    private HelloWorld helloWorld;

    public HelloMavenDecorator(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }

    @Override
    public void hello() {
        System.out.println("Hello Maven"); // 这里实现了一次
        helloWorld.hello();
    }

    @Override
    public void uuid() {
        System.out.println("Hello Maven"); // 这里实现了一次
        helloWorld.uuid();
    }
}

从上面就可以看出,装饰器模式同样的逻辑实现了两次,很麻烦,如果有100个方法呢?

接下来看看动态代理怎么处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        Hello hello = (Hello) Proxy.newProxyInstance(
                helloWorld.getClass().getClassLoader(),
                new Class[]{Hello.class},
                new HelloProxy(helloWorld));
        hello.hello();
        hello.uuid(); // 动态代理什么都不需要做? 你直接调用新方法就可以了,它直接就实现了同样的逻辑
    }
}

JDK动态代理的限制,只能代理接口,如果我们想代理一个普通的类,而不是接口,就不能用JDK的动态代理了

1
2
3
4
5
6
public static Object newProxyInstance(ClassLoader loader, // 被代理对象的ClassLoader
                                          Class<?>[] interfaces, // 要代理的接口
                                          InvocationHandler h) // 交给谁处理
        throws IllegalArgumentException
{
}

CGLIB/ByteBuddy

要想代理普通类,我们可以通过CGLIB或者ByteBuddy,ByteBuddy之前在注解章节 已经演示过了,这里就不再演示了

  • 下面我们演示下,使用CGLIB来代理普通类

我们把HelloWorld改成普通类

1
2
3
4
5
6
7
8
9
public class HelloWorld {
    public void uuid() {
        System.out.println(UUID.randomUUID());
    }

    public void hello() {
        System.out.println("Hello World!");
    }
}
  • 引入CGLIB
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>
  • 创建代理类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class HelloWorldProxy implements MethodInterceptor {
    private HelloWorld delegate;

    public HelloWorldProxy(HelloWorld delegate) {
        this.delegate = delegate;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("Hello Proxy");
        method.invoke(delegate, objects);
        return null;
    }
}
  • 代理对象
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        Enhancer enhancer = new Enhancer(); // 创建Enhancer对象
        enhancer.setSuperclass(HelloWorld.class); // 设置父类,因为它实际是动态生成一个子类对象来进行扩展,和ByteBuudy一样
        enhancer.setCallback(new HelloWorldProxy(helloWorld)); // 设置代理对象,当拦截发生时给谁处理
        HelloWorld proxy =(HelloWorld) enhancer.create(); // 创建代理对象
        proxy.hello();
        proxy.uuid();
    }
}

可以看到和JDK动态代理几乎一样,只不过好处是可以代理普通类,但不能代理final和final/private类,因为继承的原因嘛

在Springboot中使用AOP

  • 添加依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.6.0</version>
</dependency>
  • 配置AOP模式(是使用JDK动态代理还是CGLIB)
1
2
# 表示使用CGLIB
spring.aop.proxy-target-class=true
  • 新建Aspect(切面)
1
2
3
4
5
6
7
8
9
@Aspect // @Aspect注解声明这是一个切面类(代理类)
@Configuration
public class CacheAspect {
    @Around("@annotation(annotation.Cache)") // 所有标注了@Cache注解的都会被拦截
    public Object cache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("cache is run...");
        return proceedingJoinPoint.proceed(); // 让这个方法继续运行,处理它该做的事情
    }
}

关于切面的拦截方式

  • @Before 在切入点开始处切入内容
  • @After 在切入点结尾处切入内容
  • @Around 自己控制何时执行切入点切入内容

Redis

Redis是内存型数据库

  • docker安装redis
1
sudo docker run --name redis -p 6379:6379 -d redis:6.2.6

在Springboot中使用Redis

  • 添加依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.0</version>
</dependency>
  • 配置Springboot
1
2
spring.redis.host=localhost
spring.redis.port=6379
  • 创建Redis配置类,说白了就是一个Bean,它返回一个Redis模板,用于方便操作Redis
1
2
3
4
5
6
7
8
@Configuration
public class RedisConfig {
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}
  • 将redisTremplate注入后,就可以使用了,下面是一个缓存例子
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
@Configuration
public class CacheAspect {
    private final RedisTemplate<String, Object> redisTemplate;

    public CacheAspect(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Around("@annotation(annotation.Cache)")
    public Object cache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        String methodSignatureName = methodSignature.getName();
        Object cacheV = redisTemplate.opsForValue().get(methodSignatureName);
        if (cacheV != null) {
            System.out.println("return value from redis...");
            return cacheV;
        }
        Object value = proceedingJoinPoint.proceed();
        redisTemplate.opsForValue().set(methodSignatureName, value);
        return value;
    }
}
updatedupdated2025-03-012025-03-01