0%

SpringBoot高级

Spring缓存

缓存原理

  • 缓存自动配置类CacheAutoConfiguration,导入CacheConfigurationImportSelector

  • CacheConfigurationImportSelector,获取所有的CacheConfiguration

    image-20210922151815235

  • 默认SimpleCacheConfiguration生效:给容器中注册了一个CacheManager:ConcurrentMapCacheManager

  • ConcurrentMapCacheManager可以获取和创建ConcurrentMapCache类型的缓存组件,将数据保存在ConCurrentMap中

运行流程

  1. 方法运行之前,先去查询缓存组件,按照cacheNames指定的名字获取相应的缓存(如果没有Cache组件则会自动创建)

  2. 去Cache中查找缓存内容,使用key(默认方法参数)默认使用simpleKeyGenerator生成key

    simpleKeyGenerator策略:

    • 如果没有参数,key=new SimpleKey();
    • 如果只有一个参数,key=参数值
    • 如果有多个参数,key=new SimpleKey(params);
  3. 没有查到缓存就会调用目标方法,如果缓存中存在key,则从缓存中获取值

  4. 将目标方法返回的结果放回缓存中

使用缓存

注解属性

1
2
3
4
5
6
7
8
cacheNames/value #指定缓存组件名字,可以以数组形式指定多个缓存组件
key #指定key,默认为方法参数的值,可以使用SpEL表达式
keyGenerator #指定key生成器,与key二选一使用
cacheManager #指定缓存管理器
cacheResolver #指定缓存解析器,与cacheManager二选一使用
condition #指定条件下再缓存
unless #否定缓存,与condition相反,指定条件下不缓存
sync #异步缓存模式
  • 主类注解@EnableCaching开启缓存功能

  • 方法注解@Cacheable缓存返回结果,如果有缓存直接查询缓存而不调用方法

    • key

      1
      2
      @Cacheable(value = "userInfo", key = "#root.method.getName() + '[' + #id + ']'")
      public User getById(Serializable id) {...}
    • 自定义keyGenerator

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Bean("myKeyGenerator")
      public KeyGenerator keyGenerator(){
      return new KeyGenerator(){
      @Override
      public Object generate(Object o, Method method, Object... objects) {
      return method.getName()+ Arrays.asList(objects);
      }
      };
      }
      1
      2
      @Cacheable(value = "userInfo", keyGenerator = "myKeyGenerator")
      public User getById(Serializable id) {...}
    • condition&unless

      1
      2
      3
      //第一个参数大于1缓存,并且等于2时不缓存
      @Cacheable(value = "userInfo", condition = "#a0>1", unless = "#a0==2")
      public User getById(Serializable id) {...}
  • 方法注解@CachePut调用方法之后保存或更新缓存结果

    1
    2
    @CachePut(value = "userInfo", key = "'userInfo' + '[' + #entity.id + ']'")
    public boolean updateById(User entity) {...}
  • 方法注解@CacheEvict缓存清除

    1
    2
    @CacheEvict(value = "userInfo", key = "'userInfo' + '[' + #id + ']'")
    public boolean removeById(Serializable id) {...}
    • AllEntries属性:默认为false,为true时清空所有缓存
    • beforeInvocation属性:默认为false,是否在方法之前执行
  • 方法注解Caching配置复杂的缓存情景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Caching(
    cacheable = {
    @Cacheable(value = "byName", key = "name")
    },
    put = {
    @CachePut(value = "byId", key = "id"),
    @CachePut(value = "byAge", key = "age"),
    }
    )
    @Override
    public boolean save(User entity) {...}
  • 类注解CacheConfig,用来配置一整个类中的缓存配置,如下例类中所有的缓存注解都不需要再次配置cacheNames/value属性

    1
    2
    3
    @CacheConfig(cacheNames = "user")
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {...}

如果要使用同一个缓存,需要注意这些注解的key是否含义相同(不是简单的代码相同!!)

切换缓存中间件

一般来说,只要容器中注入了相应的Bean,SpringBoot就会自动切换CacheManager,如注入RedisTemplate组件之后RedisCacheManager会自动开启,并关闭默认的ConcurrentMapCacheManager

自定义RedisTemplate和RedisCacheManager,使用json序列化替代默认的jdk序列化

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
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);

//自定义Jackson序列化配置
Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
template.setDefaultSerializer(jsonRedisSerializer);

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(objectMapper);

template.afterPropertiesSet();

return template;
}

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){
//初始化一个RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
//设置CacheManager的值序列化方式为json序列化
RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer();
RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair)
.entryTtl(Duration.ofSeconds(10));//设置过期时间
//初始化RedisCacheManager
return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
}
}

消息队列

概念图解

image-20210925104140962

基础概念

  • 消息服务的对象两个类型

    • 消息代理message broker
    • 目的地destination

    消息发送这发送消息后由消息代理接管并传递到目的地

  • 消息队列的目的地形式

    • 队列queue:点对点通信

      如果有多个接收者,一旦有接收者最先拿到消息,这个消息就会被删除,其他接收者不会接收到消息

    • 主题topic:发布和订阅消息通信

      所有接收者都能接收到发布的主题消息

  • 消息代理规范

    • JMS(Java Message Service)

      基于JVM消息代理的规范,代表:ActiveMQ,HornetMQ

    • AMQP(Advanced Message Queing Protocol)

      高级消息队列协议,兼容JMS,代表:RabbitMQ

原理简析

  • RabbitAutoConfiguration配置了连接工厂ConnectionFactory
  • RabbitProperties封装了RabbitMQ的配置,对应前缀spring.rabbitmq
  • 注入了模板RabbitTemplate
  • 注入了AmqpAdmin:RabbitMQ的系统管理组件

配置环境

包依赖

1
2
3
4
5
<!--引入amqp协议,即RabbitMQ的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置文件

1
2
3
4
5
spring:
rabbitmq:
host: x.x.x.x
username: guest
password: xxx

RabbitTemplate

使用RabbitTemplate发送和接收消息

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
@SpringBootTest
class Springboot02ApplicationTests {
@Autowired
RabbitTemplate rabbitTemplate;

/**
* 发送消息
*/
@Test
void sendTest(){
//方式一:rabbitTemplate.send(交换机,key,message)
//这种方式需要我们自定义消息体内容和消息头

//方式二:rabbitTemplate.convertAndSend(交换机,key,object)
//只要传入需要发送的对象,就会被自动序列化并发送给RabbitMQ服务器
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "this is a msg from client");
map.put("list", Arrays.asList(true, 2, "object3"));
rabbitTemplate.convertAndSend("springboot.mq","user.warning",map);
System.out.println("=========>消息发送完成");
}

/**
* 从que1接收消息,对应key:user.*
*/
@Test
void getTest1(){
//接收消息并自动反序列化
Object que1 = rabbitTemplate.receiveAndConvert("que1");
System.out.println(que1.getClass());
System.out.println(que1);
}

/**
* 从que2接收消息,对应key:*.info
*/
@Test
void getTest2(){
Object que2 = rabbitTemplate.receiveAndConvert("que2");
System.out.println(que2.getClass());
System.out.println(que2);
}
}

由于默认使用的是jdk序列化,如果我们需要可以注入我们自己的MessageConverter,使其转换为json序列化

1
2
3
4
5
6
7
@Configuration
public class MQConfig {
@Bean
public Jackson2JsonMessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}

注解开发RabbitMQ

主类上添加@EnableRabbit以开启注解功能

使用注解监听队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class RabbitService {
//转换器会将消息内容自动转换为实体类
@RabbitListener(queues = "que1")
public void listenQue1(User user){
System.out.println(user.getClass());
System.out.println(user.toString());
}

//使用Message接收消息能够拿到消息的完整信息
@RabbitListener(queues = "que2")
public void listenQue2(Message message){
System.out.println(message.getClass());
System.out.println(message.toString());
}
}

image-20210925195337485

AmqpAdmin

通过AmqpAdmin就可以进行消息队列的一系列操作

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
@SpringBootTest
class Springboot02ApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;

/**
* 创建交换器
*/
@Test
void createExchange(){
amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
}

/**
* 创建队列
*/
@Test
void createQue(){
amqpAdmin.declareQueue(new Queue("amqpadmin.que",true));
}

/**
* 创建绑定
*/
@Test
void createBinding(){
amqpAdmin.declareBinding(new Binding("amqpadmin.que", Binding.DestinationType.QUEUE, "amqpadmin.exchange","amqp.info",null));
}
}

ElasticSearch检索

SpringBoot支持两种交互技术:

  • Jest(默认不生效)

    需要导入工具包

  • SpringData Elasticsearch

    • 使用ElasticsearchTemplate来操作ES
    • 编写ElasticsearchRepository子接口来操作ES

Jest

1
2
3
4
5
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<version>6.3.1</version>
</dependency>

注入JestClient

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class JestConfig {
@Bean
public JestClient jestCline() {
JestClientFactory factory = new JestClientFactory();
//配置服务端信息
factory.setHttpClientConfig(new HttpClientConfig
.Builder("http://47.113.225.244:9200")
.multiThreaded(true)
.build());
return factory.getObject();
}
}

实体类

1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Article {
//指定文档id
@JestId
private Integer id;
private String author;
private String title;
private String content;
}

测试类

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
@SpringBootTest
class JestApplicationTests {
@Autowired
JestClient jestClient;

//增加索引并添加数据
@Test
void testPut() throws IOException {
Article article = new Article(1, "张三", "title", "hello,world!");
Index index = new Index.Builder(article).index("docs").type("news").build();
DocumentResult result = jestClient.execute(index);
System.out.println(result);
}

//查询数据
@Test
void testSearch() throws IOException {
String json = "{\n" +
" \"query\": {\n" +
" \"match\": {\n" +
" \"author\": \"张三\"\n" +
" }\n" +
" }\n" +
"}";
Search search = new Search.Builder(json).addIndex("docs").addType("news").build();
SearchResult result = jestClient.execute(search);
System.out.println(result);
}
}

实现Repository接口

1
2
public interface UserRepository extends ElasticsearchRepository<User, Integer> {
}

实体类

1
2
3
4
5
6
7
8
9
10
11
12
//在这里指定索引名
@Document(indexName = "docs2")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
//指定文档id
@Id
private int id;
private String name;
private int age;
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
public class RepositoryTest {
@Autowired
UserRepository userRepository;

@Test
void testIndex(){
User user = new User(1,"张三",3);
userRepository.save(user);
}

@Test
void testSearch(){
Iterable<User> users = userRepository.findAll();
System.out.println(users);
}
}

使用Template

这部分的使用,以及ElasticSearch的安装的其他事项在我的另一篇博客ElasticSearch中有详细介绍

异步

异步任务

主运行类上添加注解开启异步功能@EnableAsync

处理访问请求

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;

@RequestMapping("/hello")
public String hello(){
asyncService.hello();
return "success";
}
}

假设AsyncService类处理后台业务需要三秒时间(这里使用线程睡眠表达含义),访问/hello路径需要等待三秒才能访问页面内容

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class AsyncService {
//标识这是一个异步方法
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("处理数据中......");
}
}

当标识异步方法之后,访问/hello路径不再需要等待三秒才能访问页面内容

定时任务

运行主类上添加注解开启定时功能@EnableScheduling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 定时注解格式'* * * * * *'共六项
* 每项代表下面的一个时间节点,并用空格分隔
* ‘*’代表任意时刻,示例:
* `0 * * * * MON_FRI`代表周一到周五每分钟执行一次
* second
* minute
* hour
* day of month
* month
* day of week
*/
@Scheduled(cron = "0 * * * * MON-FRI")
public void scheduled(){
System.out.println("Scheduled Hello......");
}

image-20211001162806033

一些常用参数举例

image-20211001163014197

邮件任务

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

使用发送邮箱的服务,需要自己在对应邮箱设置中开启smtp服务并获取授权码,这里的password不是登录邮箱用的密码,而是开启服务时获得的授权码

配置文件

1
2
3
4
5
6
7
8
9
10
11
spring:
mail:
host: smtp.163.com
username: jteam6register@163.com
password: xxx
properties:
mail:
smtp:
ssl:
enable:
true

安全

安全框架的主要作用:

  • 认证Authentication:建立用户
  • 授权Authorization:给用户授予访问权限

SpringSecurity

1
2
3
4
5
6
7
8
9
10
<!--SpringSecurity启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--整合thymeleaf-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

配置类

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定制授权规则
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//开启登录功能
//1.'/login'请求来到登录页(未授权自动重定向)
//2.重定向到'login?error'表示登录失败
//3.默认发送post方法的'/login'请求来进行登录认证
//4.自定义登录页中发送post方法的loginPage(与页面url相同)请求来进行登录认证
http.formLogin().loginPage("/loginPage").usernameParameter("user").passwordParameter("pwd");
//开启注销功能
//1.'/logout'请求来到用户注销,清空session
//2.默认注销成功重定向到到'/login?logout'页面
http.logout().logoutSuccessUrl("/");
//开启记住功能
//登录成功后会将cookie发给浏览器保存,之后访问页面只要带上cookie就会自动登录
//注销会删除cookie
http.rememberMe();
}

/**
* 定制认证规则
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//需要选择密码的加密方式,否则会报错
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("lan5th").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}

视图解析类

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
@Controller
public class ModelController {
@RequestMapping("/")
public String hello(){
return "index";
}

@RequestMapping("/level1")
public String level1(){
return "level1/vip1";
}

@RequestMapping("/level2")
public String level2(){
return "level2/vip2";
}

@RequestMapping("/level3")
public String level3(){
return "level3/vip3";
}

@RequestMapping("/loginPage")
public String loginPage() {
return "login";
}
}

权限分流页面

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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<div sec:authorize="isAuthenticated()">
当前用户:<span sec:authentication="name"></span><br>
当前角色:<span sec:authentication="principal.authorities"></span>
</div>
<div>
<a th:href="@{/level1}">vip1</a>
<a th:href="@{/level2}">vip2</a>
<a th:href="@{/level3}">vip3</a>
</div>
<div>
<form th:action="@{/upload}" enctype="multipart/form-data" method="post">
<input type="file" name="headerImg"><br>
<input type="file" name="photos" multiple><br>
<input type="submit">
</form>
</div>
<div>
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/loginPage}">登录</a>
</div>
<div sec:authorize="isAuthenticated()">
<a th:href="@{/logout}">注销</a>
</div>
</div>
</body>
</html>

登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div>这是登录页面</div>
<div>
<form th:action="@{/loginPage}" method="post">
用户名:<input name="user"><br>
密码:<input type="password" name="pwd"><br>
<input type="checkbox" name="rememberme">记住我<br>
<input type="submit" value="登录">
</form>
</div>
</body>
</html>

使用数据库中的数据来进行用户授权

UserDetailsService实现类

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 UserDetailImpl implements UserDetailsService {
@Autowired
private UserService userService;

/**
* 这里因为本项目中也使用了User类,只能使用全类名进行引用
* @param s
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//这里继承了MybatisPlus的实现,可以选择其他的实现方式来查询数据库
QueryWrapper<com.example.mybatisplus.model.domain.User> wrapper = new QueryWrapper<>();
wrapper.eq("name", s);
com.example.mybatisplus.model.domain.User userInfo = userService.getOne(wrapper);
if (userInfo == null) {
System.out.println("用户不存在");
return null;
}
//这里也必须指定密码的编码方式
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//设定用户名、加密密码 和 相应权限
return new User(userInfo.getName(), encoder.encode(userInfo.getPassword()), AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}

配置类

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定制授权规则,详细功能在上面已经给出,这里不再赘述
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/api/user/test").hasRole("ADMIN");
http.formLogin().usernameParameter("user").passwordParameter("pwd");
http.logout().logoutSuccessUrl("/");
http.rememberMe();
}

//注入UserDetailsService的实现类
@Bean
public UserDetailImpl userDetails() {
return new UserDetailImpl();
}

/**
* 定制认证规则
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//需要选择密码的加密方式,否则会报错
//使用数据库用户验证
auth.userDetailsService(userDetails()).passwordEncoder(new BCryptPasswordEncoder());
}
}

单点登录

token简介

token一般包含三部分:

  • header

    一般包含typ(token类型)和alg(加密算法)

  • payload

    保存用户信息(比如id)

  • signature

    由header和payload联合加密形成的签名

最后组合起来的token格式为:header.payload.signature

服务器进行token的有效验证时,先将收到的前两部分header和payload用相应算法形成结果签名,再将得到的签名与第三部分的signature进行对比,如果相同则验证通过

JWT

导包

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>

自定义注解

@PassToken跳过token验证

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}

@UserLoginToken进行用户token验证

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}

获取JWT token

1
JWT.create().withAudience(String.valueOf(user.userId)).sign(Algorithm.HMAC256(user.getPassword()));

自定义拦截器

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
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;

@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
//解码
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}

@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}

配置类

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class JWTConfig implements WebMvcConfigurer {
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**");
}
}

自定义注解

进行访问时带上之前服务器签名过的token就能进行用户信息的验证

image-20211210192706212

分布式

分布式架构遇到的四个核心问题:

  1. 这么多服务,客户端应该如何去访问

    api网管,服务路由

  2. 这么多服务,服务之间应该如何通信

    Http或RPC框架,异步调用

  3. 这么多服务,如何管理

    服务注册与发现

  4. 服务宕机应该怎么做

    熔断机制,服务降级

注重原因:网络是不可靠的

RPC

Remote Procedure Call:远程过程调用

是一种进程间的通信方式,是一种技术思想,不是规范,突出程序调用另一个网络空间中的过程或函数

image-20211001224205471

两个核心:

  • 通信
  • 序列化

Dubbo+Zookeeper

Dubbo

Dubbo是一个高性能、轻量级的RPC通信框架,提供了三大核心能力:

  • 面向接口的远程方法调用
  • 智能容错和负载均衡
  • 服务启动注册和发现

现在已经被Apache基金会接管

image-20211001225230412

Dubbo默认端口20880

ZooKeeper

zookeeper是一款Dubbo的注册中心服务器程序

docker安装zookeeper,默认端口2181

1
2
docker pull zookeeper:3.7.0
docker run --name zookeeper -p 2181:2181 --restart always -d zookeeper:3.7.0

Dubbo-admin

dubbo-admin是一个监控管理后台,可以查看我们注册和消费了的服务的具体信息

1
2
3
4
5
6
7
8
9
docker pull chenchuxin/dubbo-admin

docker run -it -d --name dubbo-admin \
-v /xxx/xxx/dubbo-admin:/data \
-p 8080:8080 \
-e dubbo.registry.address=zookeeper://47.113.225.244:2181 \
-e dubbo.admin.root.password=root \
-e dubbo.admin.guest.password=root \
chenchuxin/dubbo-admin

这时访问8080端口并输入用户名和密码就能看到管理页面了

依赖

服务端和客户端都需要引入dubbo和zkclient的依赖

服务端和客户端都需要引入dubbo和zkclient的依赖

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
<!--不要导成alibaba的依赖了-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!--不导入下面这些包会报一大堆的错-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

服务端

  1. 引入依赖dubbo和zkclient的依赖
  2. 配置dubbo扫描包和注册中心地址
  3. 使用@Service发布服务
  4. 启动应用

配置文件

1
2
3
dubbo.application.name=provider
dubbo.registry.address=zookeeper://47.113.225.244:2181
dubbo.scan.base-package=com.example.provider.service

服务接口

1
2
3
4
5
package com.example.provider.service;

public interface TicketService {
public String getTicket();
}

服务实现类

1
2
3
4
5
6
7
8
9
10
//注意这里的@Service是dubbo的Service,目的是注册服务
//@Service注解会向注册中心注册TicketService的全类名,包括完整路径
@Component
@Service
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "一张电影票";
}
}

消费者

配置文件

1
2
dubbo.application.name=provider
dubbo.registry.address=zookeeper://47.113.225.244:2181

服务接口

==警告:这里的全类名路径(包路径)必须与服务端完全相同,不然会报错==

1
2
3
4
5
package com.example.provider.service;

public interface TicketService {
public String getTicket();
}

用户服务类

1
2
3
4
5
6
7
8
9
10
11
12
//这里的@Service是Spring的注解
@Service
public class UserService {
//@Reference注解会将TicketService全类名去注册中心进行匹配,
//将注册中心已经注册的相同全类名的实现类进行注入
@Reference
TicketService ticketService;

public String getTicket(){
return ticketService.getTicket();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class ConsumerApplicationTests {
@Autowired
UserService userService;

@Test
void contextLoads() {
String ticket = userService.getTicket();
System.out.println("买到票了:"+ticket);
}
}

SpringCloud

image-20211001222612665

待补充


热部署

四种情况实现热部署

  • 禁用模板引擎的缓存,项目运行过程中build就可以重新编译页面并生效
  • SpringLoaded:Spring官方的热部署程序
  • JRebel:收费的热部署插件
  • SpringBoot Devtools

SpringBoot Devtools

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

在运行项目时修改文件后只需要Build Project(快捷键ctrl+F9)就可以将改动部署到运行中的应用上,实时查看效果,不需要重启项目