title: springboot
date: 2020-11-08 21:07:43
tags:
杂记
- springboot项目打jar包之后,找不到文件的问题,解决在收藏中,暂时只知道不能写./static/…
相对路径,而是要写classpath,类路径,用classPathResource路径就可以
springboot的static和template详解
- 两者的区别看这里,简单说就是static用来存放静态html,css和js之类文件
- controller return的解析,当没有动spring.view.prefix和suffix的情况时,并且没有加载thymeleaf等模板时,controller中的返回值默认就是static目录下的静态资源。而当引入模板之后,动态就会覆盖静态,返回值默认是template下的文件。如果此时还想访问static静态资源的话,那么就要使用重定向。
- 注意事项,想要直接访问static下的资源的时候,不需要在路径下写/static,那样反而会找不到文件。
- 视图解析器的前后缀,这个是最牛逼的,视图解析器就是靠前后坠来拼接逻辑视图的完整路径,可以指定任何路径
spring的数据源
- 启动spring工程首先要配置数据源,如果未配置数据源就会报错无法启动。
- 配置数据源时注意,如果使用的是properties那么密码是数字000000可以的,但是如果是yml,那么000000就会被转换成0,因为在yml中是区分数据类型的,或者将00000两端加上单引号表示这是一个字符串。
- 如果使用的是yml的自动提示,那么username和password前面会加上data-,这样的话就会报错,将data去掉就可以正常连接数据库。详细看这里
springSecurity
- 使用jdbc验证之后,spring5之后版本规定必须要使用密码编码器,明文密码经过密码编码器编码之后就会产生一个字符串,数据库中的密码应该存储的是这个字符串而不是明文密码。
- 使用httpSecurity来对url进行权限控制的时候,角色名要存储为ROLE_ADMIN的类似形式,security才能进行权限的控制。
- 先挖个坑,HTTPS的使用方式还未知。
杂记
- 调整日志的级别的时候,不能使用代码智能提示,和之前配置数据源一样,要注意,在logging.level.root要新增一个root目录
logging:
level:
root: debug
#而不是代码提示的那样
logging:
level: debug
JMs
- jms连接池上我看的书有些错误
<!--activemq的连接池-->
<!-- <dependency>-->
<!--这种是2.0以下版本的连接池-->
<!-- <groupId>org.apache.activemq</groupId>-->
<!-- <artifactId>activemq-pool</artifactId>-->
<!-- </dependency>-->
<!--这种是2.0以上版本的连接池,应该使用这种-->
<dependency>
<groupId>org.messaginghub</groupId>
<artifactId>pooled-jms</artifactId>
</dependency>
- 发送消息和接受消息的都必须是同一种类型的数据,否则会报错无法映射(unable to cast).
单元测试
- 单元测试需要使用@RunWith和@SpingBootTest,前者需要引入junit,后者需要使用starter-test
mybatis-plus
- 在controller中注入的时候应该注入service的接口,而不是注入实现类,因为spring基于接口代理,所以会无法代理注入
- 另外一种情况可能也会引发这种情况,就是开启了事务管理,可能也会出错
- 解决办法有两种,一种是将实现类换为接口,另一种方法是在application.yml中进行配置,spring.aop.proxy-target-class=true
- 详情看这里
- 详情看这里
- 详情看这里
- 详情看这里
将数据库中的datetime类型的数据经常作为字符串进行存储,rom框架会只能转换
mp自动填充功能
StringUtils的isempty弃用,建议使用hasLenght或者hasText,另一种方法是使用ObjectUtils.isempty方法
mybatis-plus的数据传送时数据库无法更新,检查字段的拼写和数据库中的是否相同,如果相同仍然不行,检查getsset方法,因为mybaits是通过setget方法传递数据的
mapper接口和xml文件的名字尽量一致,否则可能会由莫名的bug
教程中有错,spring-web-starter中已经没有hebernate-validation依赖了,集成到了validation-starter中了,要单独引入
过滤器,拦截器,切面的区别与联系
过滤器 | 拦截器 | AOP | |
---|---|---|---|
实现方式 | 集成Filter实现 | springmvc提供的拦截器接口 | 通过aspect注解实现 |
可获取内容 | 原始的http请求与响应 | 获取请求访问的类与方法 | 获取访问的类、方法以及参数值 |
无法获取内容 | 请求要访问的类与方法,以及参数(例如:拿不到你请求的控制器和请求控制器中的方法的信息) | 请求参数的值. (例如:可以拿到你请求的控制器和方法,却拿不到请求方法的参数),具体可根据dispatcherServlet跟踪源码 | http原始的请求与响应的对象 |
拦截内容 | URL | URL | 类的元数据(包、类、方法名、参数等) |
依赖及实现 | 依赖于servlet容器,基于函数回调 | 依赖于web框架,基于Java的反射机制,属于面向切面编程(AOP)的一种运用 | 依赖spring,基于AOP实现 |
应用场景 | 在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter(自定义过滤器)),如:过滤低俗文字、危险字符等。请求参数做过滤和修改,同时FilterChain过滤链执行完,并且完成业务流程后,会返回到过滤器,此时也可以对请求的返回数据做处理。登陆校验,设置字符编码,鉴权操作 | 国际化,做主题更换,过滤等 | AOP常和事务结合:Spring的事务管理:声明式事务管理(切面),日志记录,使用日志,事务,请求参数安全验证 |
- Filter与Interceptor联系与区别
- 拦截器是基于java的反射机制,使用代理模式,而过滤器是基于函数回调。
- 拦截器不依赖servlet容器,过滤器依赖于servlet容器。
- 拦截器只能对action起作用,而过滤器可以对几乎所有的请求起作用(可以保护资源)。
- 拦截器可以访问action上下文,堆栈里面的对象,而过滤器不可以。
- 执行顺序:过滤前-拦截前-Action处理-拦截后-过滤后。
3.1 作用域不同
- 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用
- 拦截器依赖于spring容器,可以在spring容器中调用,不管此时Spring处于什么环境
3.2 细粒度的不同
- 过滤器的控制比较粗,只能在请求进来时进行处理,对请求和响应进行包装
- 拦截器提供更精细的控制,可以在controller对请求处理之前或之后被调用,也可以在渲染视图呈现给用户之后调用
3.3 中断链执行的难易程度不同
- 拦截器可以 preHandle方法内返回 false 进行中断
- 过滤器就比较复杂,需要处理请求和响应对象来引发中断,需要额外的动作,比如将用户重定向到错误页面
- 注意拦截器提供三个基本的切入点
-
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o)表示被拦截的URL对应的方法执行前的自定义处理
-
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView)表示此时还未将modelAndView进行渲染,被拦截的URL对应的方法执行后的自定义处理,。
-
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e)表示此时modelAndView已被渲染,执行拦截器的自定义处理。
springboot+redis
- redis的详细介绍看这里。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依赖redis的异步客户端lettuce-->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入redis的客户端驱动,类似于mysql-connector-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
redis的体系结构
-
配置工厂的代码在开头的连接中,配置工厂主要就是配置连接池。
redisTemplate
- 使用工厂方法每次需要从工厂获取连接,使用后在关闭,于是spring封装了redisTemplate方便操作。
- redis在springboot的自动配置中已经创建好了,可以直接使用。但是如果你自己配置了redis,那么自动配置就失效了。这里介绍一下自己配置redis
@Configuration
public class RedisConfig {
private RedisConnectionFactory redisConnectionFactory = null;
/**
* 配置连接工厂,使用单例模式。避免重复创建,浪费资源
* @return
*/
@Bean(name = "RedisConnectionFactory")
public RedisConnectionFactory initRedisConnectionFactory() {
if (this.redisConnectionFactory!=null) {
return redisConnectionFactory;
}
// 首先配置连接池
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(30);
config.setMaxTotal(50);
config.setMaxWaitMillis(2000);
JedisConnectionFactory factory = new JedisConnectionFactory(config);
// 获取单机配置
RedisStandaloneConfiguration standaloneConfiguration = factory.getStandaloneConfiguration();
// 配置单机redis信息
standaloneConfiguration.setHostName("192.168.1.101");
standaloneConfiguration.setPort(6379);
standaloneConfiguration.setPassword("123456");
this.redisConnectionFactory = factory;
return redisConnectionFactory;
}
/**
* 初始化redisTemplate
* @return
*/
@Bean
public RedisTemplate<Object,Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
}
-
我们设置好了之后开始set值,但是可以看到客户端中存储的并不是字符串而是二进制字符串,这是因为redis存储的是java对象序列化之后的东西,通过这个原理,spring提供了不同的序列化器,可以将java对象序列化成指定的样子。序列化器的体系结构入下图所示
-
这里主要介绍stringRedisSerializer和jdkSerializationRedisSerializer。其中jdkSerializationRedisSerializer是其默认序列化器
spring默认使用jdkSerializationRedisSerializer。但是键用二进制存储不利于阅读,因此一般将键的序列化器改为stringRedisSerializer。
/**
* 初始化redisTemplate
* @return
*/
@Bean
public RedisTemplate<Object,Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// redisTemplate已经把StringSerializer初始化完成,直接用就可以。
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
// 设置key,hashkey和hashvalue的序列化器都是StringSerializer
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
spring对redis数据类型操作的封装
操作接口 | 功能 | 备注 |
---|---|---|
GeoOperation | 单元格 | 单元格 |
单元格 | 单元格 | 单元格 |
-
简单的操作接口就不展示了,直接redisTempalte.opsFor。。。直接提示出来。
-
有时需要对一个接口进行多次操作,就需要绑定一个接口。
redisTemplate.boundHashOps("hashkey");
// 绑定操作接口类型以及绑定的key
sessionCallback和redisCallback
- redis默认是每个操作都是一个单独的连接,想要一个连接执行多条操作可以用SessionBack和redisCallback。
- session的操作抽象级别高,平常使用这个比较多,而redis的比较底层,使用较少。
public void useRedisCallback() {
redisTemplate.execute((RedisConnection rc) -> {
rc.set("key1".getBytes(), "field".getBytes());
rc.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
return null;
}
);
}
public void userSessionCallback() {
redisTemplate.execute((RedisOperations ro)->{
ro.opsForValue().set("key1","value1");
ro.opsForHash().put("hash","field","hvalue");
return null;
});
}
在springboot中配置和使用redis
- 只需要引入依赖(在上面),在配置一下就可以使用
spring:
redis:
jedis:
pool:
min-idle: 5 # 最大空闲数
max-active: 10 # 最大活跃数
max-idle: 10 # 最大空闲时间
max-wait: 2000 # 最大等待时间
port: 6379
host: 192.168.1.101
password: 123456
timeout: 1000 # 连接超时,ms
- springboot会自动生成并装配redistemplate所需的bean,但是使用的还是默认jdk序列化器,我们需要改为string序列化器。
@Configuration
public class RedisConfig1 {
@Autowired
private RedisTemplate redisTemplate;
// 在构造方法执行完之后执行。先让spring为我们初始化redistemplate,在修改redistemplate的序列化器
@PostConstruct
public void init() {
initRedisTemplate();
}
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
}
redistemplate使用实战
redis的事务
- redis中使用事务,通过watch…multi…exec,watch是监控redis的键,multi是开始事务,注意开始事务后,redis指令不会立即执行,而是放在任务队列中,如果在此时调用redis的返回结果,都是空。只有exec才是开始执行命令,这是redis的返回值就不是null。如果键发生了改变(即使是重新赋值和原来一样的值,也被认为是改变了值)。
/**
* 测试事务
*/
public void testTransaction() {
redisTemplate.opsForValue().set("key1","value1");
List execute = (List) redisTemplate.execute((RedisOperations ro) -> {
// 设置要监控的key1
ro.watch("key1");
// 开启事务,在exec前都是只进入队列,不执行
ro.multi();
ro.opsForValue().set("key2", "value2");
ro.opsForValue().increment(1);
System.out.println("ro.opsForValue().get(\"key2\") = null,因为任务只是进入队列,未执行" + ro.opsForValue().get("key2"));
ro.opsForValue().set("key3", "value3");
Object value3 = ro.opsForValue().get("key3");
return ro.exec();
});
}
- 注意redis和sql事务的不同,如果命令执行过程中抛出异常,那么哪个命令会失败,但是后面的任务仍然会执行。redis只是将任务放入到队列,并不会检查任务是否会成功。
redis流水线
- redis的流水线相当于sql的批量执行,避免redis命令一条条发送,性能不高。因为reids的瓶颈不是io而是网络传输速度。
/**
* 测试redis流水线
*/
public void testPipLine() {
long start = System.currentTimeMillis();
redisTemplate.executePipelined((RedisOperations ro)->{
for (int i = 0; i < 10000; i++) {
ro.opsForValue().set("pipline"+i,"value"+i);
if (i==10000){
System.out.println("命令只是进入队列,所以值为空");
}
}
});
System.out.println("耗时:"+(System.currentTimeMillis()-start));
}
redis发布订阅
- 发布订阅模式,redis提供一个通道,让消息能够发送到这个通道上,多个系统可以监听这个通道,当有消息到通道上的时候,通道就会通知它的监听者。
- 发布订阅模式是经典的设计模式,需要创建消息监听器监听发送的消息。
@Component
public class RedisMessageListener implements MessageListener {
/**
* 当有消息时,会调用此方法
* @param message
* @param bytes
*/
@Override
public void onMessage(Message message, byte[] bytes) {
// 消息体
String body = new String(message.getBody());
// 通道名称
String topic = new String(bytes);
System.out.println(body);
System.out.println(topic);
}
}
- 随后在创建配置文件,配置任务线程池和监听器容器
@Configuration
public class RedisConfig1 {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private MessageListener messageListener;
// 任务线程池
private ThreadPoolTaskScheduler taskScheduler;
/**
* 初始化线程池,用来运行线程等待处理redis的信息
* @return
*/
@Bean
public ThreadPoolTaskScheduler initTaskSceduler() {
if (taskScheduler!=null) {
return taskScheduler;
}
taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(20);
return taskScheduler;
}
/**
* 定义redis的监听容器
* @return
*/
@Bean
public RedisMessageListenerContainer initRedisContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.setTaskExecutor(taskScheduler);
// 指定接受topic1的消息
ChannelTopic topic1 = new ChannelTopic("topic1");
container.addMessageListener(messageListener,topic1);
return container;
}
}
- 随后在redis客户端中输入命令,
publish topic1 msg
- 在spring中,使用redistemplate发送消息,
redisTemplate.convertAndSend(channel,message);
使用Lua脚本
使用spring缓存注解操作redis
-
spring缓存注解可以简化redis的操作,在这之前需要配置spring的缓存管理器
-
缓存管理器能提供一些重要的信息,比如缓存类型,超时时间等。spring可以支持多种类型的缓存的使用。因此存在多种缓存处理器。提供了缓存处理器的接口CacheMapper。
-
主要使用的是redisCacheManager。springboot可以使用配置文件生成缓存管理器
-
配置文件
spring:
cache:
cache-names: # 如果由底层的缓存管理器支持创建,以逗号分割的列表来缓存名称
caffeine:
spec: # caffeine缓存配置细节
couchbase:
expiration: 0ms # couchbase缓存超时时间,默认是永不超时
ehcache:
config: # 配置ehcache的缓存初始化文件路径
infinispan:
config: # infinispan缓存配置文件
jcache:
config: # jcache缓存配置文件
provider: # jcache缓存提供者配置
redis:
cache-null-values: # 是否允许redis缓存空值
key-prefix: # redis的键的前缀
time-to-live: 0ms #缓存超时时间戳,配置为0则不设置超时时间
use-key-prefix: #是否启用redis的键前缀
type: # 缓存类型,在默认的情况下,spring会自动根据上下文探测
- 这里只需要配置redis,只需要关注几个配置就可以
srping:
cache:
type: REDIS
cache-names: redisCache
这样就完成了配置缓存管理器,type指的是缓存类型。为Redis,sprinboot会自动生成RedisCacheManager,而cache-name是缓存名称。多个名称用逗号分割,便于注解使用
6. 不要忘了在启动类上加@EnableCaching
缓存注解实例
- 首先介绍几个注解
-
@CachePut
表示将方法结果返回存放到缓存中 -
@Cacheable
先从缓存中通过定义的键查询,如果可以查询到数据,则返回,否则执行方法,将返回结果保存到缓存中 -
@CacheEvict
通过定义的键移除缓存,有一个boolean的配置项beforeInvocation,表示在方法之前或者之后移除缓存,默认值是false,也就是方法之后移除缓存。
public class RedisCacheTest {
@Transactional
@CachePut(value = "redisCache",key = "'redis_user_'+#result.id")
public User insert(User user) {
return null;
}
@Transactional
@Cacheable(value = "redisCache",key = "'redis_user_'+#id")
public User getUser(Long id) {
return null;
}
@Transactional
@CachePut(value = "redisCache",condition = "#result!='null'",key = "'redis_user_'+#id")
public User update(Long id,String username) {
User user = this.getUser(id);
if (user==null) {
return null;
}
return null;
}
@Transactional
public List<User> findUsers(String username,String note) {
return null;
}
@Transactional
@CacheEvict(value = "redisCache",key = "'redis_user_'+#id",beforeInvocation = false)
public int delete(Long id) {
return 0;
}
}
-
可以看到3个缓存中都配置了value=redisCache,这是因为我们配置了
spring.cache.cache-name=redisCache
,即对应的缓存名称是redisCache。键配置项是springEl表达式,'redis'+#id
,#id
表示参数,通过参数名称来匹配,要求方法存在一个参数且名称为id。 -
#result.id
,是有时候我们想要使用返回结果的一些数据,比如insertUser,在插入之前是没有id的,mybatis回写之后才会有,于是就用result表示返回结果,id是其中的一个属性。 -
可以看到update方法可能返回null,如果为null则不需要缓存任何数据,所以加入的condition配置,只有当返回结果不为空时再缓存。
-
可以看到在update方法中我们先调用了getuser方法,因为不要依赖缓存,缓存存在脏数据的可能,比如数据库中数据已经改变,但是缓存中的数据没有变,就会出现数据不一致,因此需要先从数据库中查询数据。这里有一个误区,认为getUser上面有cache注解,就是从缓存中取的数据,不是的,因为缓存注解是依靠AOP代理实现的,而update方法调用insert方法是类内部方法的自调用,不存在代理对象的调用,也就不会使用缓存了。解决这个问题,可以拆分为两个service,service之间的调用可以看作是不同的类之间的调用,也就是AOP。或者直接从IOC容器中获取代理对象操作。
-
findUser没有使用缓存,这是因为查询的条件多样,使得缓存的命中率很低,不适合使用缓存。
缓存脏数据说明
- 两个连接对同一个数据进行操作,如果第二个连接将数据的key修改了,那么此时原来的key对应的值就是脏数据了。一般对于读操作,允许不是实时数据,比如排行榜刷新有一定延迟。但是脏数据不能一直存在,应该设置缓存的过期时间。对于实时性要求高的数据,需要将超时时间设置的更短。对于写操作,一般不轻信缓存的数据,优先考虑从数据库中读取数据。在更新数据,避免将缓存的脏数据写入数据库。
自定义缓存处理器
- 采取上面的配置,没有设置redis的超时时间。并且redis默认使用#{cacheName}:#{key}的形式作为键保存数据。有时我们想设置自定义的key或者,自定义超时时间,就需要自定义超时时间。
- 有两种方式,一种是通过增加配置文件项,另一种是完全自定义代码。
# 配置文件
spring:
cache:
redis:
use-key-prefix: false # 禁用前缀
cache-null-value: true # 允许保存空值
key-prefix: # 自定义前缀
time-to-live: 600000 # 超时时间,ms
自定义缓存管理器
@Configuration
public class RedisCacheManagerConfiguration {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean(name = "redisCacheManager")
public RedisCacheManager initRedisCacheManage() {
// redis加锁的写入器
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory);
// 启动redis缓存的默认配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置jdk默认序列化器
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer())
);
// 禁用前缀
config = config.disableKeyPrefix();
// 设置10min超时
config = config.entryTtl(Duration.ofMinutes(10));
// 创建redis缓存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
return redisCacheManager;
}
}
springboot中的注解
spring的注解
依赖值(属性)注入有关注解
@value和@configurationproperties
- 详细教程在这里
- 这里用表格简单描述一下两者的区别
| | @ConfigurationProperties | @value |
| :-----| ----: | :----: |
| 功能 | 批量注入配置文件中的属性 | 一个个指定 |
| 松散绑定(松散语法) | 支持 | 不支持 |
| SpEL | 不支持 | 支持 |
| JSR303数据校验 | 支持 | 不支持 |
| 复杂类型封装 | 支持 | 不支持 | - 所谓松散语法也就是属性命名规则(Relaxed binding)
- person.firstName:使用标准方式
- person.first-name:大写用-
- person.first_name:大写用_
- PERSON_FIRST_NAME: 系统属性推荐使用这种写法
也就是说,如果使用@Value注入,是不能像yml中springboot其他配置一样用-连接,而是只能用驼峰,而@ConfigurationProperties可以自动转换。
@configurationproperties的功能详解
- 详情看这里
- @enableConfigurationProperties只有在@Configuration中才能自动注入
- 注解中有数组形式的参数要用{},
@AutoConfigurationAfter({ListPushInfo.class,hashtest.class})
@Value
- 类中的属性不要用@Value的注解进行拼接。因为@Value自动注入使用的是代理,这个时候类肯定已经实例化了,属性也实例化了,就会出现null指针
@Value
private String urlPrefix;
private String url = urlPrefix+"/test";
// 这样的话,url中的urlPrefix是null的。
- final不能和@Value一起使用。原因和上面一样。final的实例化时@Value还没有赋值。
指定配置文件加载的顺序
- @AutoConfigureAfter
- @AutoConfigureBefore
- @AutoConfigureOrder
- @Order
- 前两个是直接写在那个具体配置类的先后执行,后两个是配置优先级,控制粒度没有前两个好。
@AutoConfigureAfter(LIstINfo.class)
- 如果在配置类中有依赖其他bean,spring非常智能,会优先加载依赖的bean
postConstruct
- 想要在类初始化的时候执行某些操作,需要用@PostConstruct注解,不要再构造方法中写自己的方法逻辑。因为这时候类中的某些依赖项还没初始化。除非在构造方法中先初始化。
热部署
- spring的devtools只是将页面和classpath进行了热部署(其实是快速重启)