- Controller
只做和HTTP请求响应相关的事情
- Service
做的事情最多,处理业务逻辑
- DAO
只做数据库操作相关的事情
Springboot为我们准备了很多为初学者准备的模块,称为starter
1
2
3
4
5
|
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
|
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
|
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一样了
之前我们了解到,在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);
}
|
在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
注解,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");
}
}
|
这个等价于上面的用法
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
也不会警告
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);
}
|
面向切面编程 Aspect-oriented programming,它相对的概念是OOP(Object-oriented programming),在进入某个切面的瞬间将它拦截下来,做我们想做的事,什么是切面?反映在方法栈中,栈帧被调用的瞬间就是一个切面,AOP关注的是一个统一的切面,而不是某个具体的对象或方法,说白了AOP思想就像一个拦截器一样,对某个统一的状态拦截下来,做统一处理
答:使用装饰器模式
在不改变原来业务逻辑的情况下,在外部在包一层,添加我们自己的功能,然后再调用原来的功能
装饰器模式,一般配合接口使用,例如有如下示例:
1
2
3
|
public interface Hello {
void hello();
}
|
1
2
3
4
5
6
|
public class HelloWorld implements Hello {
@Override
public void hello() {
System.out.println("Hello World!");
}
}
|
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(); // 调用原来的功能
}
}
|
1
2
3
4
5
6
|
public class Main {
public static void main(String[] args) {
Hello hello = new HelloMavenDecorator(new HelloWorld());
hello.hello();
}
}
|
这就是装饰器模式
下面我们通过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;
}
}
|
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();
}
|
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!");
}
}
|
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
,ByteBuddy
之前在注解章节 已经演示过了,这里就不再演示了
我们把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!");
}
}
|
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类,因为继承的原因嘛
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
|
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是内存型数据库
1
|
sudo docker run --name redis -p 6379:6379 -d redis:6.2.6
|
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>
|
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;
}
}
|