Spring Boot是一个开源的Java框架,用于快速构建独立的、生产级别的基于Spring的应用程序。旨在简化Spring应用程序的开发、部署和管理。 Spring Boot建立在Spring框架的基础上,提供了许多开箱即用的功能,使开发人员可以更加轻松地创建现代化的Java应用程序。

源码位置

Springboot 加载jsp 404

 

项目结构

    

springmvc 配置  

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
访问页面时任然404,报如下错误,从日志中看出其实已经正确获取了jsp的路径,通过搜索引擎查找解决方式需要一个jasper依赖

providedRuntime group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper'

 

DEBUG o.s.web.servlet.DispatcherServlet - GET "/zlennon/index", parameters={}
DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.zlennon.web.controller.website.MainController#index()
DEBUG o.s.w.s.v.ContentNegotiatingViewResolver - Selected 'text/html' given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8]
DEBUG o.s.web.servlet.view.JstlView - View name '/website/index', model {}
DEBUG o.s.web.servlet.view.JstlView - Forwarding to [/WEB-INF/views//website/index.jsp]
DEBUG o.s.web.servlet.DispatcherServlet - "FORWARD" dispatch for GET "/zlennon/WEB-INF/views//website/index.jsp", parameters={}
DEBUG o.s.w.s.h.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/", "/"]
WARN  o.s.w.s.r.ResourceHttpRequestHandler - Path with "WEB-INF" or "META-INF": [WEB-INF/views/website/index.jsp]
DEBUG o.s.w.s.r.ResourceHttpRequestHandler - Resource not found
DEBUG o.s.web.servlet.DispatcherServlet - Exiting from "FORWARD" dispatch, status 404
DEBUG o.s.web.servlet.DispatcherServlet - Completed 404 NOT_FOUND
DEBUG o.s.web.servlet.DispatcherServlet - "ERROR" dispatch for GET "/zlennon/error", parameters={}
DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
DEBUG o.s.w.s.v.ContentNegotiatingViewResolver - Selected 'text/html' given [text/html, text/html;q=0.8]
DEBUG o.s.web.servlet.DispatcherServlet - Exiting from "ERROR" dispatch, status 404

加上依赖后任然是404,然后试着增加版本,项目springboot版本为2.3.3,加入上面依赖后从项目依赖树中发现tomcat-embed-jasper是9.0.37,访问项目发现任然是404。然后试着更换版本吃泡面个 9.0.* ->8.*.*,最终发现下面这个版本成。原因不是很清楚。

providedRuntime group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version:'8.5.47'

 另一个问题,在不出现404问题后又出现了下面的问题,从错误栈中注意到HttpServletRequest没有这个方法getHttpServletMapping(),查看HttpServletRequest类发现有两个,一个在

embed的tomcat中  另一个在javax.servlet-api-3.1.0.jar 二在javax.servlet-api-3.1.0.jar中的HttpServletRequest确实没有getHttpServletMapping

那就是说项目启动后使用了javax.servlet-api-3.1.0.jar的类。其实内嵌的tomcat已经包含了所有启动需要的东西,所以我们不在需要依赖额外的servlet-api包。去掉该依赖,页面访问正常。

java.lang.NoSuchMethodError: 'javax.servlet.http.HttpServletMapping javax.servlet.http.HttpServletRequest.getHttpServletMapping()'
	at org.apache.catalina.core.ApplicationHttpRequest.setRequest(ApplicationHttpRequest.java:709)
	at org.apache.catalina.core.ApplicationHttpRequest.<init>(ApplicationHttpRequest.java:115)
	at org.apache.catalina.core.ApplicationDispatcher.wrapRequest(ApplicationDispatcher.java:911)
	at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:358)
	at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:312)
	at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:394)
	at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:253)
	at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:348)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:173)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1589)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:832)

 

为什么使用Spring Boot,怎样快速开始一个Spring Boot 项目

1.为什么使用spring boot

Spring诞生以后,慢慢的取代了EJB2.0,越来越多的企业都开始使用Spring,从Strut2,Spring,Hibernate到 SpringMVC+Mybatis|JPA,开发一个web应用变得更简单了,使的开发人员更专注于业务代码的开发。

然二spirng boot出来以后,取代spring xml 繁琐的配置,使用annotaion 自动配置, 开发一个web应用变得更简单了。先如今微服务已成为开发的主流,而Spring boot 是现在最适合创建微服务的框架了。

2.快速开始一个spring boot 项目

打开官网 https://start.spring.io/     初始化一个spring boot 项目,这里我选择gradle, jar 包形式,下载zip并解压。

spirng-initializr

导入idea,等待依赖下载完成,这时候我们什么都不用配置,创建一个HelloWorldController,直接启动主类SpringbootApplication

启动后默认的端口是8080,我们可以拿postman 测试一下,成功返回 "hello world",现在spring boot应用已经完成,不过光返回字符串是没什么意义的,我们的目的是将数据展示到页面上,接下来处理怎么访问一个html页面。

在templates目录下新建一个html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
</head>
<body>
    开始spring boot 应用 hello world
</body>
</html>

然后修改一下controller

@Controller
public class HelloWorldController {

    @GetMapping("/hello")
    public String helloWorld(){
        return "hello";
    }
}

访问 http://localhost:8080/hello就可以看到html里面的内容了。

使用thymeleaf和Spring Data JPA

 首先我们引入依赖

compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '2.4.2'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.4.2'
runtimeOnly 'mysql:mysql-connector-java'

我们以Article为例来说明如何使用spring data jpa 和 thymeleaf

创建相关的表,因为jpa本省也可以通过对象来创建表,所以先创建对象,先创建表都可以

CREATE TABLE `article`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET armscii8 COLLATE armscii8_bin NULL DEFAULT NULL,
  `content` text CHARACTER SET armscii8 COLLATE armscii8_bin NULL,
  `author` varchar(255) CHARACTER SET armscii8 COLLATE armscii8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = armscii8 COLLATE = armscii8_bin ROW_FORMAT = Dynamic;

通过创建的表可以在idea中很方便的生成实体类和持久Repository

@Repository
public interface ArticleRepository extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article> {

}
@Entity
@Table(name = "article")
@Data
@EqualsAndHashCode()
public class Article  implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Integer id;

    @Column(name = "title")
    private String title;

    @Column(name = "content")
    private String content;

    @Column(name = "author")
    private String author;

    @Override
    public String toString() {
        return "Article{" +
                "id=" + id + '\'' +
                "title=" + title + '\'' +
                "content=" + content + '\'' +
                "author=" + author + '\'' +
                '}';
    }
}

application.properties中配置数据库链接和thymeleaf,thymeleaf也可以不用配置,默认是开启的。

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

#thymeleaf
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

在新建一个article_list.html 展示文章列表

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><title>文章列表</title></head>
<body>
<h1>文章列表</h1>
<table>
    <thead>
    <tr>
        <th>id</th>
        <th>标题</th>
        <th>作者</th>
        <th>内容</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="item: ${articles}">
        <td th:text="${item.id}"></td>
        <td th:text="${item.title}"></td>
        <td th:text="${item.author}"></td>
        <td th:text="${item.content}"></td>
    </tr>
    </tbody>
</table>
</body>
</html>

ok 大工搞成,我们已经学会使用thymeleaf和Spring Data JPA了,非常的简单。接下来我们使用mock测试增删改

package com.example.springboot.controller;

import com.example.springboot.model.Article;
import com.example.springboot.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/article/")
public class ArticleController {

    @Autowired
    ArticleRepository articleRepository;

    @GetMapping("/findAll")
    private String findAllArticle(Model model){
        List<Article> articles= articleRepository.findAll();
        model.addAttribute("articles",articles);
        return "article_list";
    }

    @PostMapping(value="/save")
    private ResponseEntity<Article> save(@RequestBody Article article){
        Article add= articleRepository.save(article);
        return new ResponseEntity<Article>(add, HttpStatus.CREATED);
    }

    @PutMapping("/update")
    private ResponseEntity<Article> update(@RequestBody Article article){
        return new ResponseEntity<Article>(articleRepository.save(article), HttpStatus.OK);
    }

    @DeleteMapping("/delete/{id}")
    private ResponseEntity<HttpStatus> delete(@PathVariable("id") Integer id){
        articleRepository.deleteById(id);
        return new ResponseEntity<HttpStatus>(HttpStatus.ACCEPTED);
    }

}

使用springboot测试,模拟http请求,并和方法的返回值作比较来验证程序的正确性。 

package com.example.springboot;

import com.example.springboot.model.Article;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringbootApplication.class)
@AutoConfigureMockMvc
public class ArticleCrudTest {


    @Autowired
    private MockMvc mvc;


    @Test
    public void testSave() throws Exception {
        mvc.perform(MockMvcRequestBuilders
                .post("/article/save")
                .content(asJsonString(new Article(null, "math", "数学", "张衡")))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").exists())
                .andReturn().getResponse().getOutputStream();
    }

    @Test
    public void testUpdate() throws Exception {
        mvc.perform(MockMvcRequestBuilders
                .put("/article/update")
                .content(asJsonString(new Article(2, "java", "java开发实战经典", "李新")))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.content").value("java开发实战经典"));
    }

    @Test
    public void testDelete() throws Exception {
        mvc.perform(MockMvcRequestBuilders.delete("/article/delete/{id}", 1))
                .andExpect(status().isAccepted());
    }

    public static String asJsonString(final Object obj) {
        try {
            return new ObjectMapper().writeValueAsString(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

我们使用rest风格的形式实现springboot工程的增删改查了,到目前位置我们没有做很多的配置,注意我们在跳转到thymeleaf页面时使用的时@Controller而不是@RestController

Spring boot日志配置和错误处理

 在上面的章节中我们已经了解了怎样搭建一个sping boot 应用,并实现了相应的增删改查和页面展示,下面说明日志配置的错误处理,处理程序发生异常的情况并配置日志方便快速定位错误程序。

Spring Boot使用Commons Logging进行所有内部日志记录,并且底层日志实现接口开放。 提供了Java Util Logging,Log4J2和Logback的默认配置。 在每种情况下,记录器都已预先配置为使用控制台输出,同时还提供可选文件输出。

默认日志格式

2021-02-03 22:53:10.784  INFO 35800 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
 

  • 日期和时间: 毫秒精度

  • 日志级别: ERRORWARNINFODEBUG, or TRACE.

  • 进程id.

  • --- 分隔符

  • 线程呢名称: 使用方括号括起来

  • 日志名称: 剪短的类名称

  • 日志消息

默认颜色配置

我们只需要需要在配置文件增加  spring.output.ansi.enabled=always 就能实现彩色输出

如果我们需要自定义日志输出格式,则我们需要定义一个logback.xml

<configuration>
    <!--<include resource="org/springframework/boot/logging/logback/base.xml"/>-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %cyan(%logger){36}.%M - %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

这样日志就按我们设定的格式输出了

日志输出到文件

需要增加一个appender,同时引用到需要输出日志的地方

<property name="LOGS_HOME" value="./logs/" />    
<appender name="LOGGER-FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGS_HOME}springboot.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
            </Pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- rollover daily -->
            <fileNamePattern>${LOGS_HOME}springboot.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
    </appender>


    <root level="info">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="LOGGER-FILE"/>
    </root>

404页面处理

默认情况加spirng boot 的错误页面是下面这样的。我们使用一个很友好的页面来处理

直接在templates下面建立error目录并且创建404.html就可以替换原有的404错误提示。 这里我们使用另一种方法。

自定义错误页面在error 目录下

使用 ErrorPageRegistrar 注册错误页面

public class MyErrorPageRegistrar implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/error"));
        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/error"));
        registry.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error"));
    }

}

错误请求匹配

@Controller
public class MyErrorController extends AbstractErrorController {


    public MyErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request) {
        Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);

        if (status != null) {
            Integer statusCode = Integer.valueOf(status.toString());

            if(statusCode == HttpStatus.NOT_FOUND.value()) {
                return "/error/404";
            }
            else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
                return "/error/500";
            }
        }
        return "/error/default";
    }

    @Override
    public String getErrorPath() {
        return null;
    }
}

 

spring boot profile 配置和运行

开发一个程序我们不止只是在开发环境运行,需要部署到生产环境上去,而开发和生产环境一般情况下各种配置都是不相同的,如安全策略,日志等级,数据库等。如果部署到生产环境在手动该配置,是相当麻烦和耗时的。spring boot 中可以同时配置不同环境的配置文件,通过激活profile切换不同的环境。

首先我们根据需要创建不同环境的配置文件 application-dev.properties,application-prod.properties

1.在idea中我们可以直接配置

2.构建jar文件 并激活响应的profile

//build.gradle
bootJar {
	archiveBaseName = 'springboot'
	archiveVersion = '1.0.0'
	archiveFileName = 'springboot.jar'
}

构建jar包 命令gradle build或者gradle bootJar,执行jar 并激活侧面

 java -jar build/libs/springboot.jar --spring.profiles.active=prod 或gradle bootRun --args='--spring.profiles.active=prod'

或者可以配置gradle bootRun 任务。

3.构建war包并运行

bootWar {
	baseName = 'springboot'
	version =  '1.0.0'
}

gradle build 生成war包 ,激活profile

  • 在tomcat bin 目录下创建文件 setenv.sh   
    JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=<your target profile here>"

 

spring boot 应用常用注解

  

  •  @Configuration

该注解是spring context中的类,指示一个类声明了一个或多个Spring容器管理的bean,可以替代之前的xml形式的bean定义。AnnotationConfigApplicationContext可以通过该注解标记的类获取已经配置的类。

  • @SpringBootApplication

我们只需要在主类上加上该注解,就能启动spring boot 应用。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

从源码可以看到它将@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解合在一起了。

  • @SpringBootConfiguration

标记一个类作为Spring Boot应用程序。 可以用作Spring标准@Configuration注解的替代方法,以便可以自动找到配置(例如在测试中)。 应用程序应该只包含一个@SpringBootConfiguration。

  • @EnableAutoConfiguration

spring boot 开启自动配置,自动配置在spring-boot-autoconfigure 包中,spring boot可以根据你项目中的依赖包自动配置一些需要的bean,  里面的exclude和excludeName方法可以根据类和类名排除不需要的自动配置的bean, 详细的自动配置原理在后面会再次说明。

  • @ConfigurationProperties

ConfigurationProperties 与@Value 不同,它可以将配置文件中的信息绑定到类中的属性上,不支持SpEL,可以结合JSR303完成数据校验. 注意只是使用@ConfigurationProperties 不能被注册到IOC容器,需要配合@Configuration或@EnableConfigurationProperties或者@ConfigurationPropertiesScan

@Configuration
@Data
@ConfigurationProperties("spring.datasource")
public class DataSourceConfig {
    private String url;
    private String username;
    private String password;
}

也可以将配置文件中的值直接转化为list和map

//application.properties   

test.list=one,two,three,four

user.userinfo.name=张三 user.userinfo.age=18

list

@Configuration
@ConfigurationProperties("test")
public class DevConfigProps {
   private List<String> list;

map

@Configuration
@Data
@ConfigurationProperties("user")
public class UserMapConfig {
    public Map<String,Object> userinfo;
}
  • @Conditional

顾名思义bean注册的条件,可以直接或间接用在@Component,@Configuration类标注的类上,或者@Bean方法上。该注解不支持继承。

  • @ConditionalOnClass

在类路径下找到相应的类,条件成立,可以将当前类注册到spring容器中。

  • ConditionalOnMissingBean

仅在该注解规定的类不存在于 spring容器中时,使用该注解标记的类将会注册到spring容器中,当放置在方法上时,默认时类型时方法的返回类型

@Configuration
 public class MyAutoConfiguration {

     @ConditionalOnMissingBean
     @Bean
     public MyService myService() {
         ...
     }

 }

上面示例中,如果BeanFactory中不包含MyService类型的bean,则条件将匹配。将MyServicebean注入到容器中。其他@ConditionalOnxxx套路和@ConditionalOnClass,@ConditionalOnMissingBean一样

spring boot 自动配置原理

springboot在可能让我们的配置减到最少,尝试猜测和配置您能需要的bean。根据类路径和定义的bean来应用自动配置类。在我们工程中需要添加功能而引入某些jar包时,比如我们需要配置缓存,数据库,定时任务。这通常需要我们额外的配置,spring boot 通过自动配置这一机制将需要的bean注入到spring的IOC容器中, 这减少了我们的配置。而我们在开发过程中也可以随时覆盖这些自动配置的bean。也可以通过spring.autoconfigure.exclude,注解exclude排除指定的自动配置bean。

spring boot 注册自动配置类通过\META-INF\spring.factories,注册时会根据@ConditionalOnxxx 注解的条件判断是否注册到spring 容器。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\

下面拿一个jackson的自动配置类来说

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {

	private static final Map<?, Boolean> FEATURE_DEFAULTS;

	static {
		Map<Object, Boolean> featureDefaults = new HashMap<>();
		featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
		featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
		FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults);
	}

	@Bean
	public JsonComponentModule jsonComponentModule() {
		return new JsonComponentModule();
	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
	static class JacksonObjectMapperConfiguration {

		@Bean
		@Primary
		@ConditionalOnMissingBean
		ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
			return builder.createXmlMapper(false).build();
		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(ParameterNamesModule.class)
	static class ParameterNamesModuleConfiguration {

		@Bean
		@ConditionalOnMissingBean
		ParameterNamesModule parameterNamesModule() {
			return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
		}

	}

在上面的示例中,如果ApplicationContext中不包含ObjectMapper类型的bean,它将创建ObjectMapper bean。另一个注释@ConditionalOnBean与@ConditionalOnMissingBean注释相反。该条件只能匹配到目前为止应用程序上下文已处理的bean。

设置自定义自动配置

定义自动配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DataSource.class)
public class MyAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public MyService myService() {
        return new MyService(true);
    }
}
...
---------------------------------------
@Data
public class MyService {
    boolean autoConfig;
    public MyService(boolean autoConfig) {
        this.autoConfig=autoConfig;
    }
}

在resources\META-INF下创建spring.factories 文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.springboot.config.MyAutoConfiguration

创建测试类,测试成功证明MyService成功注册在容器当中。

public class AutoConfigurationTest {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class));

    @Test
    public  void serviceNameCanBeConfigured() {
        this.contextRunner.withUserConfiguration(MyAutoConfiguration.class).run((context) -> {
            assertThat(context).hasSingleBean(MyService.class);
            assertThat(context.getBean(MyService.class).isAutoConfig()).isTrue();
        });
    }
}

如果我们将日志配置为logging.level.org.springframework=DEBUG或者使用了spring-boot-starter-actuator ,在启动spring应用时将看到自动配置的类,如下:

spring boot quartz定时任务

执行一个简单的任务

springboot中实现定时任务有多种方式,我们这里使用spring-boot-starter-quartz

引入依赖

compile group: 'org.springframework.boot', name: 'spring-boot-starter-quartz', version: '2.4.2'

 

定义一个job

public class CovidDaliyDataJob extends QuartzJobBean {

    @Autowired
    CovidService covidService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        covidService.saveDailyData();
    }
}

 

这里定时任务的内容为从新冠疫情数据统计网站获取每日的统计数据

 

@Component
public class CovidServiceImpl implements CovidService {
    Logger logger = LoggerFactory.getLogger(CovidServiceImpl.class);

    @Value("${covid.daily.data.api}")
    private String covidDaliyDataApi;
    @Autowired
    CovidRepository covidRepository;

    @Autowired
    RestTemplate restTemplate;

    @Override
    public void saveDailyData() {
        Covid[] dailyList= restTemplate.getForObject(covidDaliyDataApi,Covid[].class);
        logger.info("COVID daily data size ===>>>>>[{}]",dailyList.length);
    }
}

先将job存储类型配置为内存中。

spring.quartz.job-store-type=memory
covid.daily.data.api=https://api.covidtracking.com/v1/states/current.json

QuartzConfig配置

@Configuration
public class QuartzConfig {
 @Bean
    public JobDetail jobDetail() {
        return JobBuilder.newJob().ofType(CovidDaliyDataJob.class)
                .storeDurably()
                .withIdentity("Covid_Daily_Data_Job_Detail")
                .withDescription("获取每日的各个国家新冠病毒统计数据JobDetail")
                .build();
    }

    @Bean
    public Trigger trigger(JobDetail job) {
        return TriggerBuilder.newTrigger().forJob(job)
                .withIdentity("Covid_Daily_Data_Quartz_Trigger")
                .withDescription("获取每日的各个国家新冠病毒统计数据触发器")
                .withSchedule(simpleSchedule().repeatForever().withIntervalInSeconds(10))
                .build();
    }
}

因为springboot已经自动配置了SchedulerFactoryBean,我们可以不再配置Scheduler这样一个最简单的定时任务就可以执行了,为了便于测试这里设置10秒执行一次定时任务。

20:32:09.285 [quartzScheduler_Worker-6] DEBUG org.springframework.web.client.RestTemplate.debug - Accept=[application/json, application/*+json]
20:32:10.973 [quartzScheduler_Worker-6] DEBUG org.springframework.web.client.RestTemplate.debug - Response 200 OK
20:32:11.430 [quartzScheduler_Worker-6] DEBUG org.springframework.web.client.RestTemplate.debug - Reading to [com.example.springboot.model.Covid[]]
20:32:12.380 [quartzScheduler_Worker-6] INFO  com.example.springboot.service.impl.CovidServiceImpl.saveDailyData - COVID daily data size ===>>>>>[56]
20:32:19.293 [quartzScheduler_Worker-7] DEBUG org.springframework.web.client.RestTemplate.debug - HTTP GET https://api.covidtracking.com/v1/states/current.json
20:32:19.294 [quartzScheduler_Worker-7] DEBUG org.springframework.web.client.RestTemplate.debug - Accept=[application/json, application/*+json]
20:32:20.484 [quartzScheduler_Worker-7] DEBUG org.springframework.web.client.RestTemplate.debug - Response 200 OK
20:32:20.918 [quartzScheduler_Worker-7] DEBUG org.springframework.web.client.RestTemplate.debug - Reading to [com.example.springboot.model.Covid[]]
20:32:21.833 [quartzScheduler_Worker-7] INFO  com.example.springboot.service.impl.CovidServiceImpl.saveDailyData - COVID daily data size ===>>>>>[56]
20:32:29.293 [quartzScheduler_Worker-8] DEBUG org.springframework.web.client.RestTemplate.debug - HTTP GET https://api.covidtracking.com/v1/states/current.json
20:32:29.293 [quartzScheduler_Worker-8] DEBUG org.springframework.web.client.RestTemplate.debug - Accept=[application/json, application/*+json]
20:32:30.373 [quartzScheduler_Worker-8] DEBUG org.springframework.web.client.RestTemplate.debug - Response 200 OK
20:32:30.608 [quartzScheduler_Worker-8] DEBUG org.springframework.web.client.RestTemplate.debug - Reading to [com.example.springboot.model.Covid[]]
20:32:31.104 [quartzScheduler_Worker-8] INFO  com.example.springboot.service.impl.CovidServiceImpl.saveDailyData - COVID daily data size ===>>>>>[56]

从控制台可以看出定时任务已经执行了。

在上面的例子中,创建jobdetail和trriger也可以使用FactoryBean的形式

    @Bean
    public JobDetailFactoryBean jobDetail() {
        JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
        jobDetailFactory.setJobClass(CovidDaliyDataJob.class);
        jobDetailFactory.setName("Covid_Daily_Data_Job_Detail");
        jobDetailFactory.setDescription("获取每日的各个国家新冠病毒统计数据JobDetail");
        jobDetailFactory.setDurability(true);
        return jobDetailFactory;
    }
    @Bean
    public SimpleTriggerFactoryBean trigger(JobDetail job) {
        SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
        trigger.setJobDetail(job);
        trigger.setName("Covid_Daily_Data_Quartz_Trigger");
        trigger.setDescription("获取每日的各个国家新冠病毒统计数据触发器");
        trigger.setRepeatInterval(10);
        trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
        return trigger;
    }

   //corn 表达式触发器
    @Bean
    public CronTriggerFactoryBean trigger(JobDetail job) {
        CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
        trigger.setJobDetail(job);
        trigger.setName("Covid_Daily_Data_Quartz_Trigger");
        trigger.setDescription("获取每日的各个国家新冠病毒统计数据触发器");
        trigger.setCronExpression("30 10 1 * * ?");
        return trigger;
    }

将任务信息存储到数据中

quartz 任务默认存在内存中,要存储到数据库中,只需要配置spring.quartz.job-store-type=jdbc,启动spring应用,因为我这里使用的spring data jpa 会自动创建响应的表,也可以自己手动创建

配置自定义调动工厂bean,连接不同的数据源

我们可能需要将定时任务的表单独放在一个schema中,这样我们可以单独配置个quartz数据源。配置quartz.properties,设置定时任务的其他一些属性

spring.quartz.jdbc.initialize-schema=always
spring.datasource.quartz.jdbc-url=jdbc:mysql://127.0.0.1:3306/zlennon?serverTimezone=UTC&useSSL=false
spring.datasource.quartz.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.quartz.username=root
spring.datasource.quartz.password=123456

    @Bean
    @QuartzDataSource
    @ConfigurationProperties(prefix = "spring.datasource.quartz")
    public DataSource quartzDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public SchedulerFactoryBean scheduler(Trigger trigger, JobDetail job, DataSource quartzDataSource) {

        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));
        schedulerFactory.setJobFactory(new SpringBeanJobFactory());
        schedulerFactory.setJobDetails(job);
        schedulerFactory.setTriggers(trigger);
        schedulerFactory.setDataSource(quartzDataSource);
        return schedulerFactory;
    }

 

Springboot 整合 Caffeine 缓存

1.Caffeine是什么


Caffeine是基于Java 8的高性能缓存库,可提供接近最佳的命中率。

缓存类似于ConcurrentMap,但不完全相同。最根本的区别是ConcurrentMap会保留添加到其中的所有元素,直到将其明确删除为止。另一方面,通常将缓存配置为自动移除条目,以限制其内存占用量。在某些情况下,由于LoadCache或AsyncLoadingCache不会自动移除缓存,因此即使移除缓存也可能会很有用。

Caffeine提供灵活的构造来创建具有以下功能组合的缓存:

自动将条目自动加载到缓存中,可以选择异步加载
基于频率和新近度超过最大值时基于大小的过期策略
自上次访问或上次写入以来测得的基于时间的条目到期
发生第一个陈旧的条目请求时,异步刷新
键自动包装在弱引用中
值自动包装在弱引用或软引用中
逐出(或以其他方式删除)条目的通知
写入传播到外部资源
缓存访问统计信息的累积
为了改善集成,扩展模块中提供了JSR-107 JCache和Guava适配器。 JSR-107标准化了基于Java 6的API,以牺牲功能和性能为代价,最大限度地减少了供应商特定的代码。 Guava的Cache是​​其前身的库,适配器提供了一种简单的迁移策略。

2.SpringBoot中配置Caffeine

spring配置配置缓存,需要配置对应的缓存管理器,然后再需要缓存的地方加上缓存注解

引入需要的包,这里使用的时gradle

    compile group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '2.4.1'
    compile group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '2.8.8'

配置缓存管理器

@Configuration
@EnableCaching
public class CacheConfig {

   @Bean
    public Caffeine caffeineCache() {
        return Caffeine.newBuilder()
                .expireAfterWrite(60, TimeUnit.MINUTES);
    }

    @Bean
    public CacheManager cacheManager(Caffeine caffeine) {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(caffeine);
        return caffeineCacheManager;
    }
}

 

接下来我们创建一张表,然后使用Caffeine缓存

 

CREATE TABLE `spring_cache`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `cache_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `cache_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


-- ----------------------------
-- Records of spring_cache
-- ----------------------------
INSERT INTO `spring_cache` VALUES ('1', 'caffeine', 'caffeine');
INSERT INTO `spring_cache` VALUES ('2', 'guava', 'guava');
INSERT INTO `spring_cache` VALUES ('3', 'EhCache', 'EhCache');
INSERT INTO `spring_cache` VALUES ('4', 'Infinispan', 'Infinispan');

 

使用缓存注解配置,再springboot应用中测试

 

@Service
@Slf4j
@CacheConfig(cacheNames="caffeine")
public class SpringCacheServiceImpl extends BaseAbstractService<SpringCache,String> implements SpringCacheService<SpringCache,String> {

    private static Logger logger = LoggerFactory.getLogger(SpringCacheServiceImpl.class);


    @Autowired
    SpringCacheRepository springCacheRepository;


    @Cacheable(key = "#id")
    public SpringCache getCache(String id){
        logger.info("get spring caffeine cache,id=[{}]",id);
        return springCacheRepository.findById(id).get();
    }

    @Override
    @CacheEvict(key = "#id")
    public void removeCache(String id) {

        springCacheRepository.deleteById(id);
        logger.info("remove spring caffeine cache,id=[{}]",id);
    }

    @Override
    @CachePut( key = "#sc.id")
    public void saveCache(SpringCache sc) {

        SpringCache newSc=springCacheRepository.saveAndFlush(sc);
        logger.info("put spring caffeine cache,id=[{}]",sc.getId());
    }

}

 

 注意springboot启动应用中加@EnableCaching。

 

   
//实现CommandLineRunner接口
 public void run(String... args) throws Exception {
        logger.info("spring boot runner");
        logger.info("Spring Boot Caffeine Caching Example Configuration");
        springCacheService.getCache("1");
        springCacheService.getCache("2");
        springCacheService.getCache("3");
        springCacheService.getCache("4");

        Cache cache=cacheManager.getCache("caffeine");
        springCacheService.getCache("1");
        springCacheService.getCache("2");
    }

从控制台可以看出前四条发出sql查询,后面两条没有,证明是读取了缓存

10:20:56.668 [restartedMain] INFO  c.zlennon.web.ZlennonWebApplication - Spring Boot Caffeine Caching Example Configuration
10:20:56.785 [restartedMain] INFO  c.z.c.s.impl.SpringCacheServiceImpl - get spring caffeine cache,id=[1]
10:20:56.921 [restartedMain] DEBUG org.hibernate.SQL - select springcach0_.id as id1_9_0_, springcach0_.cache_name as cache_na2_9_0_, springcach0_.cache_type as cache_ty3_9_0_ from spring_cache springcach0_ where springcach0_.id=?
10:20:57.016 [restartedMain] INFO  c.z.c.s.impl.SpringCacheServiceImpl - get spring caffeine cache,id=[2]
10:20:57.017 [restartedMain] DEBUG org.hibernate.SQL - select springcach0_.id as id1_9_0_, springcach0_.cache_name as cache_na2_9_0_, springcach0_.cache_type as cache_ty3_9_0_ from spring_cache springcach0_ where springcach0_.id=?
10:20:57.021 [restartedMain] INFO  c.z.c.s.impl.SpringCacheServiceImpl - get spring caffeine cache,id=[3]
10:20:57.023 [restartedMain] DEBUG org.hibernate.SQL - select springcach0_.id as id1_9_0_, springcach0_.cache_name as cache_na2_9_0_, springcach0_.cache_type as cache_ty3_9_0_ from spring_cache springcach0_ where springcach0_.id=?
10:20:57.027 [restartedMain] INFO  c.z.c.s.impl.SpringCacheServiceImpl - get spring caffeine cache,id=[4]
10:20:57.029 [restartedMain] DEBUG org.hibernate.SQL - select springcach0_.id as id1_9_0_, springcach0_.cache_name as cache_na2_9_0_, springcach0_.cache_type as cache_ty3_9_0_ from spring_cache springcach0_ where springcach0_.id=?
同样我们可以查看缓存管理器的缓存

接下来如果我们执行删除和增加操作,会有相应的缓存删除和增加

        springCacheService.removeCache("4");
        SpringCache sc =new SpringCache();
        sc.setId("4");
        sc.setCacheName("Infinispan");
        sc.setCacheType("Infinispan");
        springCacheService.saveCache(sc);

3.原理探索

spring boot 整合redis

  

Redis是什么

Redis是一种开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。 Redis提供数据结构有字符串,哈希,列表,集合,带范围查询的排序集合,位图,超日志,地理空间索引和流。 Redis具有内置的复制,Lua脚本,LRU过期策略,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供高可用性。

为什么使用Redis

  1. 速度非常快
  2. 支持的数据结构比其他缓存更多
  3. 大多数语言都支持redis
  4. 它是开源且稳定的

springboot中使用Redis

引入依赖

compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.4.2'

配置缓存属性

spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=2
spring.redis.timeout=6000

 下面以对用户的增删改查说明redis的使用

用户实体对象

@Entity
@Table(name = "user")
@Data
@Accessors(chain = true)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "age")
    private Integer age;

}

 

在调用的方法上加上缓存注解以便可以缓存@CachePut,@Cacheable,@CacheEvict

 

@Service
@CacheConfig(cacheNames = "users")
public class UserServiceImpl implements UserService {

    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    UserRepository userRepository;

    @CachePut(key ="#result.id")
    @Override
    public User addUser(User user) {
        logger.debug("添加用户 ==>{}",user.toString());
        return userRepository.save(user);
    }

    @Override
    @CachePut(key ="#result.id")
    public User updateUser(User user) {
        logger.debug("更新用户 ==>{}",user.toString());
        return userRepository.save(user);
    }

    @Override
    public List<User> findAll() {
        logger.debug("查询所有用户");
        return userRepository.findAll();
    }

    @Override
    @Cacheable(key="#id")
    public User findById(Integer id) {
        logger.debug("查询所有用户");
        return userRepository.findById(id).get();
    }

    @Override
    @CacheEvict(key = "#id")
    public boolean deleteUser(Integer id) {
        logger.debug("删除用户==>{}",id);
        userRepository.deleteById(id);
        return true;
    }
}

 

rest api 调用 测试缓存

 

@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/findById/{id}")
    private User findById(@PathVariable("id") Integer id){
       return  userService.findById(id);
    }

    @GetMapping("/findAll")
    private List<User> findAllUser(Model model){
        return  userService.findAll();
    }

    @PostMapping(value="/save")
    private ResponseEntity<User> save(@RequestBody User user){
        User add= userService.addUser(user);
        return new ResponseEntity<User>(add, HttpStatus.CREATED);
    }

    @PutMapping("/update")
    private ResponseEntity<User> update(@RequestBody User user){
        User update= userService.updateUser(user);
        return new ResponseEntity<User>(update, HttpStatus.OK);
    }

    @DeleteMapping("/delete/{id}")
    private ResponseEntity<HttpStatus> delete(@PathVariable("id") Integer id){
        userService.deleteUser(id);
        return new ResponseEntity<HttpStatus>(HttpStatus.ACCEPTED);
    }

启动redis,启动类将@EnableCaching注解,启动spring boot 应用。当我们调用增删改查时,将会有缓存存入到redis中,或者从redis中删除。通过控制台的sql输出语句,和redis-cli和验证缓存的情况。

使用RedisTemplate

@Configuration
@EnableCaching
@EnableRedisRepositories
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @Primary
    public RedisProperties redisProperties() {
        return new RedisProperties();
    }
    @Bean
    JedisConnectionFactory jedisConnectionFactory()
    {
        RedisProperties properties = redisProperties();
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(properties.getHost());
        configuration.setPort(properties.getPort());
        configuration.setPassword(properties.getPassword());
        configuration.setDatabase(properties.getDatabase());
        return new JedisConnectionFactory(configuration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory());
        RedisSerializer stringSerializer = new StringRedisSerializer();//序列化为String

        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);

        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(serializer);
        return redisTemplate;
    }
}

配置redis哨兵模式

 配置哨兵模式网上介绍的文章很多,这里不做过多的解释. redis 配置好主从模式,哨兵之后在spring boot中配置也比较简单。

spring boot 配置文件

spring:
  redis:
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 10
        max-idle: 5
        max-wait: 8
        min-idle: 2
    timeout: 6000
    password: admin
    sentinel:
      master: mymaster
      nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381

 

连接工厂配置

 

    @Bean
    @Primary
    public RedisProperties redisProperties() {
        return new RedisProperties();
    }
    @Bean
    JedisConnectionFactory jedisConnectionFactory(RedisSentinelConfiguration sentinelConfig)
    {
        return new JedisConnectionFactory(sentinelConfig);
    }


    @Bean
    public RedisSentinelConfiguration sentinelConfiguration(){
        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
        RedisProperties properties = redisProperties();
        List<String> nodes=properties.getSentinel().getNodes();
        redisSentinelConfiguration.master(properties.getSentinel().getMaster());
        //配置redis的哨兵sentinel
        Set<RedisNode> redisNodeSet = new HashSet<>();
        nodes.forEach(x->{
            redisNodeSet.add(new RedisNode(x.split(":")[0],Integer.parseInt(x.split(":")[1])));
        });
        redisSentinelConfiguration.setSentinels(redisNodeSet);

        properties.getSentinel();
        redisSentinelConfiguration.setPassword(RedisPassword.of(properties.getPassword()));
        return redisSentinelConfiguration;
    }

配置redis集群

window redis集群推荐将多个redis注册为window服务,方便管理。

spring boot 配置文件

 spring:
    cluster:
      max-redirects: 3
      nodes: 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381

 

连接工厂配置及redistemplate配置

 

    @Bean
    JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPool,
                                                  RedisClusterConfiguration jedisClusterConfig)
    {

        return new JedisConnectionFactory(jedisClusterConfig,jedisPool);
    }


    @Bean
    public JedisPoolConfig jedisPool(RedisProperties properties) {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(properties.getJedis().getPool().getMaxIdle());
        jedisPoolConfig.setMaxWaitMillis(properties.getJedis().getPool().getMaxWait().toMillis());
        jedisPoolConfig.setMaxTotal(properties.getJedis().getPool().getMaxActive());
        jedisPoolConfig.setMinIdle(properties.getJedis().getPool().getMinIdle());
        return jedisPoolConfig;
    }

    @Bean
    public RedisClusterConfiguration jedisConfig() {
        RedisClusterConfiguration config = new RedisClusterConfiguration();
        RedisProperties properties = redisProperties();
        List<String> nodes=properties.getCluster().getNodes();
        Set<RedisNode> redisNodeSet = new HashSet<>();
        nodes.forEach(x->{
            redisNodeSet.add(new RedisNode(x.split(":")[0],Integer.parseInt(x.split(":")[1])));
        });
        config.setClusterNodes(redisNodeSet);
        config.setMaxRedirects(properties.getCluster().getMaxRedirects());
        config.setPassword(RedisPassword.of(properties.getPassword()));
        return config;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        RedisSerializer stringSerializer = new StringRedisSerializer();//序列化为String

        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);

        redisTemplate.setKeySerializer(stringSerializer);
        //redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
       // redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }

如果配置的集群正常,就可以使用redistemplate操作数据了。

spring boot Redis 消息订阅与发部

配置文件

spring
  reids
  channel:
    site: site-visit

 

使用redis 消息监听容器

 

    @Bean
    public RedisMessageListenerContainer listenerContainer(MessageListenerAdapter listenerAdapter,
                                                           RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, ChannelTopic.of(siteVisitChannel));
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(SiteVisitConsumer consumer) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(consumer);
        messageListenerAdapter.setSerializer(new JdkSerializationRedisSerializer());
        return messageListenerAdapter;
    }

 

设置消息生产者 

 

@Component
public class SiteVisitProducer {
    Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Value("${spring.redis.channel.site}")
    private String siteVisitChannel;

    public void sendMessage(SiteVisitInfo pv) {
        logger.info("新的访问信息==>>{}",pv);
        redisTemplate.convertAndSend(siteVisitChannel, pv);
    }
}

 

消费者端

 

public interface MessageDelegate {
    void handleMessage(SiteVisitInfo message);
    void handleMessage(String message);
    void handleMessage(Map message);
    void handleMessage(byte[] message);
    void handleMessage(Serializable message);
    // pass the channel/pattern as well
    void handleMessage(Serializable message, String channel);
}


@Component
public class SiteVisitConsumer implements MessageDelegate{
    @Autowired
    SiteVisitInfoService siteVisitInfoService;
    Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void handleMessage(SiteVisitInfo message) {
        logger.info("接受站点访问信息==>>>{}",message);
        siteVisitInfoService.saveOrUpdate(message);
    }

    @Override
    public void handleMessage(String message) {
        logger.info("接受站点访问信息==>>>{}",message);
    }

    @Override
    public void handleMessage(Map message) {
        logger.info("接受站点访问信息==>>>{}",message);
    }

    @Override
    public void handleMessage(byte[] message) {
        logger.info("接受站点访问信息==>>>{}",message);
    }

    @Override
    public void handleMessage(Serializable message) {

        logger.info("接受站点访问信息==>>>{}",message);
    }

    @Override
    public void handleMessage(Serializable message, String channel) {
        logger.info("接受站点访问信息==>>>{}",message);
    }

接下来,当我们在程序中发消息时siteVisitProducer.sendMessage(); 消费端将会接受到消息,处理接受到的消息即可。

使用Redis分布式锁

为什么要使用分布式锁

在单机情况下我们通常使用Java的内置锁来实现高并发情况,但是在分布式的情况下,如果有多个服务对外提供服务,即使使用java的锁也会造成数据不一致的情况,因为进入单个服务的请求是同步访问的,但该服务中的锁不能对其他服务起作用,最终导致数据出现错误。

springboot使用分布式锁

 redis分布式锁实现的集中方式

  • setnx+expire ,又有这两个命令不是原子的,可能会引发并发问题

  • lua脚本 或set key value [EX seconds][PX milliseconds][NX|XX] 命令 ,具备原子性
        @Autowired
        RedisTemplate redisTemplate;
        @Autowired
        JedisPool jedisPool;
    
        private static final String REDIS_LOCK="covid";
    
       public void saveDailyData() {
            Jedis jedis=jedisPool.getResource();
            String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
            Boolean exist=redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value);
            redisTemplate.expire(REDIS_LOCK,10, TimeUnit.SECONDS);
    
    /*        String lua_scripts_lock = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
                    "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
            Object result = jedis.eval(lua_scripts_lock, Collections.singletonList(REDIS_LOCK), Arrays.asList(value,"10"));*/
    
            try {
                if(exist) {
                    Covid[] dailyList = restTemplate.getForObject(covidDaliyDataApi, Covid[].class);
                    logger.info("COVID daily data size ===>>>>>[{}]", dailyList.length);
                    covidRepository.saveAll(Arrays.asList(dailyList));
                }
            } catch (RestClientException e) {
                e.printStackTrace();
            } finally {
    
                String lua_script_unlock = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Object obj=jedis.eval(lua_script_unlock , Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if(obj.toString().equals(1)){
                    logger.info("分布式锁{}删除成功",REDIS_LOCK);
                }
                jedisPool.close();
    /*            String currLockValue= (String) redisTemplate.opsForValue().get(REDIS_LOCK);
                if(currLockValue.equals(value))
                    redisTemplate.delete(REDIS_LOCK);*/
            }
        }

     

  • redis官方推荐 Redisson
    //引入依赖
    implementation group: 'org.redisson', name: 'redisson', version: '3.15.0'
    
    //在集群环境下Redisson
        @Bean
        public Redisson redisson() {
            RedisProperties properties = redisProperties();
            List<String> clusterNodes = new ArrayList<>();
            for (int i = 0; i < properties.getCluster().getNodes().size(); i++) {
                clusterNodes.add("redis://" + properties.getCluster().getNodes().get(i));
            }
            Config config = new Config();
            ClusterServersConfig clusterServersConfig = config.useClusterServers()
                    .addNodeAddress(clusterNodes.toArray(new String[clusterNodes.size()]));
            clusterServersConfig.setPassword(properties.getPassword());//设置密码
            return (Redisson) Redisson.create(config);
        }
    //RLock使用
            RLock rLock = redisson.getLock(REDIS_LOCK);
            rLock.lock();
            try {
               //业务代码
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if(rLock.isLocked()&&rLock.isHeldByCurrentThread()){
                    rLock.unlock();
                }
            }

     

 

Spring Boot 整合RabbitMQ

 高级消息队列协议(AMQP)是面向消息中间件的与平台无关的有线级别协议。 Spring AMQP项目将Spring的核心概念应用于基于AMQP的消息传递解决方案的开发。 Spring Boot为通过RabbitMQ使用AMQP提供了许多便利,包括spring-boot-starter-amqp“ Starter”。

一.spring boot中中使用RabbitMQ

引入Strater依赖

implementation 'org.springframework.boot:spring-boot-starter-amqp'

1.rabbitmq配置

spring:
  #MQ
  rabbitmq:
    host: localhost
    port: 5672
    username: admin
    password: admin
    #    指明采用发送者确认模式
    publisher-confirm-type: CORRELATED
    #    失败时返回消息
    publisher-returns: true
    virtual-host: /
    listener:
      direct:
        acknowledge-mode: manual
      simple:
        acknowledge-mode: manual
        #      每个容器的消费者数量控制,也就是线程池的大小
        concurrency: 1
        #       acknowledge-mode: none
        max-concurrency: 4
        # 开启失败时的重试
        retry:
          enabled: true
          max-attempts: 5
          max-interval: 100000   # 重试最大间隔时间
          initial-interval: 1000  # 重试初始间隔时间
        #          预取的数量,spring amqp2.0开始默认值为250,之前默认为1,最好设置稍微大些
        prefetch: 1

2.rabbitmq连接工厂配置


@Configuration
public class RabbitConfig {

    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private RabbitProperties rabbitProperties;


    @Bean
    public ConnectionFactory getConnectionFactory() {
        com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory =
                new com.rabbitmq.client.ConnectionFactory();
        rabbitConnectionFactory.setHost(rabbitProperties.getHost());
        rabbitConnectionFactory.setPort(rabbitProperties.getPort());
        rabbitConnectionFactory.setVirtualHost(rabbitProperties.getVirtualHost());
        DefaultCredentialsProvider credentialsProvider = new DefaultCredentialsProvider(rabbitProperties.getUsername(), rabbitProperties.getPassword());
        rabbitConnectionFactory.setCredentialsProvider(credentialsProvider);

        rabbitConnectionFactory.setAutomaticRecoveryEnabled(true);
        rabbitConnectionFactory.setNetworkRecoveryInterval(5000);

        ConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitConnectionFactory);

        //((CachingConnectionFactory)connectionFactory).setPublisherConfirms(rabbitProperties.isPublisherConfirms());
        ((CachingConnectionFactory) connectionFactory).setPublisherReturns(rabbitProperties.isPublisherReturns());
        ((CachingConnectionFactory) connectionFactory).setPublisherConfirmType(rabbitProperties.getPublisherConfirmType());

        return connectionFactory;
    }


    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(RabbitProperties rabbitProperties) {
        SimpleRabbitListenerContainerFactory containerFactory = new SimpleRabbitListenerContainerFactory();
        containerFactory.setConnectionFactory(getConnectionFactory());
        containerFactory.setConcurrentConsumers(1);
        containerFactory.setMaxConcurrentConsumers(20);
        containerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //containerFactory.setRetryTemplate(rabbitRetryTemplate());
        //containerFactory.setMessageConverter(new Jackson2JsonMessageConverter());
        containerFactory.setTaskExecutor(taskExecutor());
        containerFactory.setChannelTransacted(false);
        containerFactory.setAdviceChain(retryInterceptor());
        return containerFactory;
    }

    @Bean
    public RetryOperationsInterceptor retryInterceptor() {

        return RetryInterceptorBuilder.stateless()
                .retryOperations(rabbitRetryTemplate())
                .recoverer(new ImmediateRequeueMessageRecoverer())
                .build();
    }



    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.initialize();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10);
        return executor;
    }

        @Bean
    public RetryTemplate rabbitRetryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        retryTemplate.registerListener(new RetryListener() {
            @Override
            public <T, E extends Throwable> boolean open(RetryContext retryContext, RetryCallback<T, E> retryCallback) {
                // 执行之前调用 (返回false时会终止执行)
                return true;
            }

            @Override
            public <T, E extends Throwable> void close(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
                logger.warn("重试结束,已重试次数=[{}]", retryContext.getRetryCount());
            }

            @Override
            public <T, E extends Throwable> void onError(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
                //  异常 都会调用
                logger.error("-----第{}次调用", retryContext.getRetryCount());
            }
        });

        retryTemplate.setBackOffPolicy(backOffPolicy());
        retryTemplate.setRetryPolicy(retryPolicy());
        return retryTemplate;
    }

    @Bean
    public ExponentialBackOffPolicy backOffPolicy() {
        ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
        long maxInterval = rabbitProperties.getListener().getSimple().getRetry().getMaxInterval().getSeconds();
        long initialInterval = rabbitProperties.getListener().getSimple().getRetry().getInitialInterval().getSeconds();
        double multiplier = rabbitProperties.getListener().getSimple().getRetry().getMultiplier();
        // 重试间隔
        backOffPolicy.setInitialInterval(initialInterval * 1000);
        // 重试最大间隔
        backOffPolicy.setMaxInterval(maxInterval * 1000);
        // 重试间隔乘法策略
        backOffPolicy.setMultiplier(multiplier);
        return backOffPolicy;
    }

    @Bean
    public SimpleRetryPolicy retryPolicy() {
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        int maxAttempts = rabbitProperties.getListener().getSimple().getRetry().getMaxAttempts();
        retryPolicy.setMaxAttempts(maxAttempts);
        return retryPolicy;
    }


    //声明队列
    @Bean
    public Queue saveDailyDataQueue() {
        return new Queue("covid.daily.data.queue", true); // true表示持久化该队列
    }


    /**
     * direct(直接):把消息路由到那些BindingKey和RoutingKey完全匹配的队列中;
     */

    /**
     * 声明topic交互器
     * topic(主题):类似于direct,但可以使用通配符匹配规则(广播);
     * BindingKey允许使用两种符号用于模糊匹配:“*”与“#”,“#”可匹配多个或零个单词;“*”可匹配一个单词。
     */
    @Bean
    TopicExchange topicExchange() {
        return new TopicExchange("covid.daily.data.exchange");
    }

    //绑定
    @Bean
    public Binding bindingQueue() {
        return BindingBuilder.bind(saveDailyDataQueue()).to(topicExchange()).with("covid.daily.data");
    }


}

3.发送消息

public class CovidServiceImpl implements CovidService, RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    Logger logger = LoggerFactory.getLogger(CovidServiceImpl.class);

    @Autowired
    CovidRepository covidRepository;

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;


    @PostConstruct
    public void init() {
        //设置消息投递到queue失败回退时回调
        rabbitTemplate.setReturnCallback(this);
        //设置消息发送到exchange结果回调
        rabbitTemplate.setConfirmCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.error("发送消息到交换器成功,correlationData=[{}]",correlationData);
        } else {
            logger.error("发送消息到交换器失败,原因=[{}]",cause);

        }

    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        logger.error("发送消息到队列失败,响应码=[{}],CorrelationId=[{}]",replyCode,message.getMessageProperties().getCorrelationId());

    }


    @Override
    public void saveDailyData() {
        String msgId = UUID.randomUUID().toString();
        CorrelationData correlationId = new CorrelationData(msgId);
        Map<String,Object> map = new HashMap<>();
        map.put("time",LocalDate.now());
        rabbitTemplate.convertAndSend("covid.daily.data.exchange", "covid.daily.data", map, correlationId);
    }
}

4.接受消息

注意使用@RabbitListener,@RabbitHandler 就可以。

@Component
@RabbitListener(queues = "covid.daily.data.queue")
public class CovidMsgReceiver {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Value("${covid.daily.data.api}")
    private String covidDaliyDataApi;
    @Autowired
    CovidRepository covidRepository;

    @Autowired
    RestTemplate restTemplate;

    Logger logger = LoggerFactory.getLogger(CovidMsgReceiver.class);
    @RabbitHandler
    public void onMessage(Map msg, Channel channel, Message message) throws IOException {



        try {
            logger.info("HelloReceiver收到  : " + msg +",收到时间"+new Date());
            //消息发送成功,但是网络中断导致,无法接受怎么处理
            Covid[] dailyList = restTemplate.getForObject(covidDaliyDataApi, Covid[].class);
            logger.info("COVID daily data size ===>>>>>[{}]", dailyList.length);
            covidRepository.saveAll(Arrays.asList(dailyList));
            logger.info("COVID daily data 已写入数据库");
            //告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 后续还会在发
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            logger.info("receiver success:"+msg);
        } catch (IOException e) {
            e.printStackTrace();
            //丢弃这条消息
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
            logger.info("receiver fail");
        }

    }

    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        System.out.println("消息已确认");
    }

    @RabbitHandler
    public void onMessage(String message) {
        System.out.println(message);
    }

    @RabbitHandler
    public void onMessage(byte[] message) {
        System.out.println(new String(message));
    }
}
RabbitListener源码解析

1.解析注解:Spring 在启动时会扫描带有 @RabbitListener 注解的方法,并解析这些注解

2.创建消息监听器容器:对于每个带有 @RabbitListener 注解的方法,Spring 会创建一个消息监听器容器。

3.启动消息监听器容器:容器启动后,会开始监听指定队列的消息。当队列中有消息时调用监听器方法来处理。

4.添加消息: spring启动时根据配置启动循环线程,循环线程处理broker投递的消息放入队列中

 

二.对于上面使用的详细解析

1.在上面的配置我们配置了 publisher-confirm-type: CORRELATED, publisher-returns: true 开启发送者确认模式。使用CorrelationData关联发送的消息

假设我们发送消息是将exchage 设置错误,将叫调用confirm方法,消息已发送到交换器但是设置路由key错误将会调用returnedMessage

rabbitTemplate.convertAndSend("covid.daily.data.exchange_fail", "covid.daily.data_fail", map, correlationId);

2.通过配置一下信息,我们设置消费者消费消息后需要手动确认,否则认为消息未消费成功。并且配置了消费重试次数为5. 

    listener:
      direct:
        acknowledge-mode: manual
      simple:
        acknowledge-mode: manual
        #      每个容器的消费者数量控制,也就是线程池的大小
        concurrency: 1
        #       acknowledge-mode: none
        max-concurrency: 4
        # 开启失败时的重试
        retry:
          enabled: true
          max-attempts: 5
          max-interval: 100000   # 重试最大间隔时间
          initial-interval: 1000  # 重试初始间隔时间

通过使用channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); 表示消息已处理成功。但是假设这个时候在调用远程api的过程网络中断了,这个时候怎么处理呢。

在前面我们配置了重试机制,通过重试拦截器来处理retryInterceptor,如果重试成功,那么正常消费消息,如果在重试次数用完之后任然没能消费消息,这个时候我们可以选择重新发送消息到队列中,或者将消息持久化,后面通过其他方式来处理记录的消息。通过配置RepublishMessageRecoverer,RejectAndDontRequeueRecoverer,ImmediateRequeueMessageRecoverer 可以冲入队列,拒绝消息等。

23:19:56.592 [taskExecutor-1] INFO  com.zlennon.covid.common.CovidMsgReceiver.onMessage - HelloReceiver收到  : {time=2021-03-07},收到时间Sun Mar 07 23:19:56 CST 2021
23:19:56.593 [taskExecutor-1] ERROR com.zlennon.covid.config.RabbitConfig$$EnhancerBySpringCGLIB$$46ecb22d.onError - -----第2次调用
23:19:58.597 [taskExecutor-1] INFO  com.zlennon.covid.common.CovidMsgReceiver.onMessage - HelloReceiver收到  : {time=2021-03-07},收到时间Sun Mar 07 23:19:58 CST 2021
23:19:58.600 [taskExecutor-1] ERROR com.zlennon.covid.config.RabbitConfig$$EnhancerBySpringCGLIB$$46ecb22d.onError - -----第3次调用
23:20:02.612 [taskExecutor-1] INFO  com.zlennon.covid.common.CovidMsgReceiver.onMessage - HelloReceiver收到  : {time=2021-03-07},收到时间Sun Mar 07 23:20:02 CST 2021
23:20:02.616 [taskExecutor-1] ERROR com.zlennon.covid.config.RabbitConfig$$EnhancerBySpringCGLIB$$46ecb22d.onError - -----第4次调用
23:20:10.632 [taskExecutor-1] INFO  com.zlennon.covid.common.CovidMsgReceiver.onMessage - HelloReceiver收到  : {time=2021-03-07},收到时间Sun Mar 07 23:20:10 CST 2021
23:20:10.634 [taskExecutor-1] ERROR com.zlennon.covid.config.RabbitConfig$$EnhancerBySpringCGLIB$$46ecb22d.onError - -----第5次调用
23:24:23.810 [taskExecutor-1] ERROR com.zlennon.covid.config.RabbitConfig$$EnhancerBySpringCGLIB$$46ecb22d.lambda$recoverer$0 - 消息参数==>[],失败原因=[Cached Rabbit Channel: PublisherCallbackChannelImpl: AMQChannel(amqp://admin@127.0.0.1:5672/,1), conn: Proxy@2bd9722 Shared Rabbit Connection: SimpleConnection@54f3fd30 [delegate=amqp://admin@127.0.0.1:5672/, localPort= 56057], (Body:'[B@56cf1eca(byte[129])' MessageProperties [headers={spring_listener_return_correlation=f5ff3d09-09b0-4942-9387-3b7f876b8a4d, spring_returned_message_correlation=d8bf92c6-fb8d-488b-9401-09e17c888a8c}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=covid.daily.data.exchange, receivedRoutingKey=covid.daily.data, deliveryTag=6, consumerTag=amq.ctag-7eogtN6aqvpgJvqrLV6EEw, consumerQueue=covid.daily.data.queue])]
23:24:23.812 [taskExecutor-1] WARN  com.zlennon.covid.config.RabbitConfig$$EnhancerBySpringCGLIB$$46ecb22d.close - 重试结束,已重试次数=[5]

 

Spring Security

1.Spring Security 能干什么

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。 Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。 与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义要求

2.认证配置

@Configuration //springboot开启是扫描的类
@EnableWebSecurity//开启springsecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}

@Configuration
@EnableWebSecurity //开启springsecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SysUserService userService;

    @Override
    public void configure(WebSecurity web) {
        // 设置不拦截规则
        web.ignoring().antMatchers("/login", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/*", "/**").permitAll()
                .antMatchers("/**").authenticated()   // 指定所有的请求都需登录
                .anyRequest().authenticated()
                .and()
                .cors().and().csrf().disable()
                // 自定义登录页面
                .formLogin()
                .loginPage("/login")                        // 指定登录页面
                .loginProcessingUrl("/signin")                    // 执行登录操作的 URL
                .usernameParameter("username")                          // 用户请求登录提交的的用户名参数
                .passwordParameter("password")                          // 用户请求登录提交的密码参数
                .failureHandler(this.authenticationFailureHandler())    // 定义登录认证失败后执行的操作
                .successHandler(this.authenticationSuccessHandler());   // 定义登录认证曾工后执行的操作


        // 自定义注销
        http.logout().logoutUrl("/signout")                     // 执行注销操作的 URL
                .logoutSuccessUrl("/login")                             // 注销成功后跳转的页面
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID");
    }


    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    /**
     * 登录认证配置
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(this.userDetailsService())
                .passwordEncoder(new BCryptPasswordEncoder());
    }

    /**
     * 使用自定义的登录密码加密规则,需继承  LLgRi7cWeUUu5zpB7dekZFapB2XdRCvM4N
     */
    @Bean(name = "myMessageDigestPasswordEncoder")
    public PasswordEncoder messageDigestPasswordEncoder() {
        return new MyMessageDigestPasswordEncoder("md5");

    }

    /**
     * 使用自定义的登录认证失败处理类,需继承 AuthenticationFailureHandler
     */
    @Bean(name = "authenticationFailureHandlerImpl")
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new AuthenticationFailureHandlerImpl();
    }

    /**
     * 使用自定义的登录认证成功处理类,需继承 AuthenticationSuccessHandler
     */
    @Bean(name = "authenticationSuccessHandlerImpl")
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandlerImpl();
    }


    @Bean(name = "userDetailsServiceImpl")
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    // 表达式控制器
    @Bean(name = "expressionHandler")
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        return new DefaultWebSecurityExpressionHandler();
    }





    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

配置之后当我们请求/login,提交表单表单的中用户名和密码的name 时username和password,spring 会拦截并校验用户名和密码。

UserDetailsService是查询数据库中的用户和用户所对应的角色

 

public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private SysUserService sysUserService;

    @Autowired
    private SysUserRoleService sysUserRoleService;
    @Autowired
    private SysRoleService sysRoleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            throw new BadCredentialsException("用户名不能为空");
        }

        UserDetails userDetails = null;
        // 根据用户名从数据库查询用户信息,根据自己的业务规则去写
        SysUser user = (SysUser) sysUserService.findByUserName(username);
        if (user == null) {
            throw new BadCredentialsException("用户名不存在");
        }
        //获取用户角色列表
        List<SysUserRole> surList = sysUserRoleService.getUserRoleByUserId(user.getId());
        String roles = "";
        for (SysUserRole sur : surList) {
            SysRole sr = (SysRole) sysRoleService.findById(sur.getRoleId());
            roles += "ROLE_" + sr.getRole() + ",";
        }
        List<GrantedAuthority> grantedAuthorityList = AuthorityUtils.createAuthorityList(roles.substring(0, roles.length() - 1));
        userDetails = new User(user.getUsername(), user.getPassword(), // 数据库中存储的密码
                true,               // 用户是否激活
                true,               // 帐户是否过期
                true,               // 证书是否过期
                true,               // 账号是否锁定
                grantedAuthorityList);  // 用户角色列表,必须以 ROLE_ 开头
        return userDetails;
    }

这样当校验成功或失败后security 将吧控制权交到 AuthenticationSuccessHandler,AuthenticationFailureHandler。我们可以实现个类做成功或失败的处理

3.资源访问控制

假设有个列表页面,操作权限要分给不同的人。后端可以使用注解,前端可以使用springsecurity标签

@EnableGlobalMethodSecurity(prePostEnabled=true)
@PreAuthorize("hasAuthority('list')")
<sec:authorize access="hasAuthority('addArticle')"></sec>

 

引用文档

https://docs.spring.io/spring-security/reference/servlet/architecture.html

springboot国际化

1.i18n是什么

"i18n"是"internationalization"的缩写。它指的是将应用程序或软件系统进行适配,以支持多种语言和文化偏好的过程。通过国际化,应用程序可以根据用户的语言和区域设置提供本地化的用户界面和内容。

国际化的目标是使应用程序能够以一种可扩展和灵活的方式适应不同的语言和地区。这包括处理文本翻译、日期和时间格式、数字格式、货币符号、单位转换等方面的问题。

通过实施国际化,应用程序可以更好地满足不同用户群体的需求,并在全球范围内更具吸引力和可用性。它为用户提供了以他们所熟悉和喜欢的语言和格式使用应用程序的机会,提高了用户体验和可访问性。

2.springboot应用如何实现国际化

  1. 准备国际化资源文件
    创建资源文件 message.properties和message_en_US.properties
    类容分别为:

    001=登录
    001=Login

  2. 添加资源文件路径

    spring:
      messages:
        basename: i18n/messages

    或者配置bean

     

    @Configuration
    public class MessageConfig {
       @Bean(name = "messageSource")
        public ResourceBundleMessageSource getMessageSource() throws Exception {
            ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
            resourceBundleMessageSource.setDefaultEncoding("UTF-8");
            resourceBundleMessageSource.setBasenames("i18n/messages");
            return resourceBundleMessageSource;
        }
    
    }

     

  3. 配置拦截器和LocaleResolver
     

    @Configuration
    @EnableAutoConfiguration
    public class MvcConfig implements WebMvcConfigurer {
    
    
    
        @Bean
        public LocaleResolver localeResolver() {
            SessionLocaleResolver slr = new SessionLocaleResolver();
            slr.setDefaultLocale(Locale.CHINA);
            return slr;
        }
    
        @Bean
        public LocaleChangeInterceptor localeChangeInterceptor() {
            LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
            //lci.setParamName("lang");
            return lci;
        }
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(localeChangeInterceptor());
        }
    }

     

  4. 读取消息util

    @Component
    public class MessageUtils {
        @Autowired
        MessageSource messageSource;
    
        /**
         * 获取单个国际化翻译值
         */
        public  String get(String msgKey) {
            try {
                return messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale());
            } catch (Exception e) {
                return msgKey;
            }
        }
    }

     

  5. 测试验证

    @RestController
    public class I18nController {
    
        @Autowired
        MessageUtils messageUtils;
    
        @RequestMapping("/getMessage")
        public ResponseEntity getMessage(HttpServletRequest request,String code){
            String msg = messageUtils.get(code);
            return new ResponseEntity(msg, HttpStatusCode.valueOf(200));
        }
    }
    
    http://localhost:8080/getMessage?code=001&locale=en_US


     

springboot application event

1.概述

事件在应用中非常重要,包括事件的发送与接受。事件机制一般使用观察者模式实现, 在spring中,我们能够容易的使用事件

2.事件的使用

2.1 创建一个状态改变的事件

@Data
public class State {

    private int state;
}
public class StateChangeEvent<T> extends ApplicationEvent {

    private  T data;
    public StateChangeEvent(T source) {
        super(source);
        this.data=source;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

2.2发布事件

 State state = new State();
 state.setState(10);
 // 发布事件,事件源为Context
 StateChangeEvent stateChangeEvent = new StateChangeEvent(applicationContext);
 stateChangeEvent.setData(state);
 applicationContext.publishEvent(stateChangeEvent);

2.3 事件监听

2.3.1  实现ApplicationListener监听消息
@Component
@Slf4j
public class ContextStateChangeListener implements ApplicationListener<StateChangeEvent> {


    @Override
    public void onApplicationEvent(StateChangeEvent stateChangeEvent) {
        log.info("onApplicationEvent 状态改变==>>{}",stateChangeEvent.getData());
    }
}
2.3.2 注解方式监听
@Component
@Slf4j
public class StateChangeListener {

    @EventListener
    public void stateListener(StateChangeEvent<State> stateChangeEvent){
        log.info("stateListener 状态改变==>>{}",stateChangeEvent.getData());
    }
}

 

springboot-ConfigurationProperties

1.yml配置读取字符串

// yml config
language:
  type: zh-cn

@Configuration
@ConfigurationProperties(prefix = "language")
@Data
public class StringType {
    String type;
}

2.yml 转map

//yml config
map:
  maps:
    name: InjectMapFromYAML
    url: http://injectmapfromyaml.dev
    description: How To Inject a map from a YAML File in Spring Boot
  users:
    user1:
      appId: YiBaoTong
      password: 123456
    user2:
      appId: ZhiKe
      password: 123456


@Configuration
@ConfigurationProperties(prefix = "map")
@Data
public class YmalMap {

    private Map<String, String> maps;
    private Map<String, User> users;

}


@Data
public class User {

    private static final long serialVersionUID = 1L;

    private String appId;

    private String password;

}

3.yml 转数组

list:
  userlist:
    - appId: YiBaoTong
      password: 123456
    - appId: ZhiKe
      password: 123456

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

@Configuration
@ConfigurationProperties(prefix = "list")
@Data
public class YmalToArray {


    private User[] userlist;

}

4.yml转list

list:
  include:
    - /api/v1/token/api_token
    - /api/v1/yibaotong/save

  userlist:
    - appId: YiBaoTong
      password: 123456
    - appId: ZhiKe
      password: 123456


@Configuration
@ConfigurationProperties(prefix = "list")
@Data
public class YmalToList {

    private List<String> include;

    private List<User> userlist;

}

5. ConfigurationProperties详解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {
    @AliasFor("prefix")
    String value() default "";

    @AliasFor("value")
    String prefix() default "";

    boolean ignoreInvalidFields() default false;

    boolean ignoreUnknownFields() default true;
}

6.属性注入过程

bean 在初始化的之前会执行后置处理器

ConfigurationPropertiesBindingPostProcessor

 

  
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (!this.hasBoundValueObject(beanName)) {
            this.bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
        }

        return bean;
    }



//------------

    private void bind(ConfigurationPropertiesBean bean) {
        if (bean != null) {
            Assert.state(bean.asBindTarget().getBindMethod() != BindMethod.VALUE_OBJECT, "Cannot bind @ConfigurationProperties for bean '" + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");

            try {
                this.binder.bind(bean);
            } catch (Exception var3) {
                throw new ConfigurationPropertiesBindException(bean, var3);
            }
        }
    }

 

ConfigurationPropertiesBinder

 

    BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) {
        Bindable<?> target = propertiesBean.asBindTarget();
        ConfigurationProperties annotation = propertiesBean.getAnnotation();
        BindHandler bindHandler = this.getBindHandler(target, annotation);
        return this.getBinder().bind(annotation.prefix(), target, bindHandler);
    }

 

Binder

 

    private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding, boolean create) {
        try {
            Bindable<T> replacementTarget = handler.onStart(name, target, context);
            if (replacementTarget == null) {
                return this.handleBindResult(name, target, handler, context, (Object)null, create);
            } else {
                Object bound = this.bindObject(name, replacementTarget, handler, context, allowRecursiveBinding);
                return this.handleBindResult(name, replacementTarget, handler, context, bound, create);
            }
        } catch (Exception var9) {
            return this.handleBindError(name, target, handler, context, var9);
        }
    }

 

springboot websocket

WebSockets是什么

WebSocket 是一种网络通信协议,用于在 Web 应用程序和服务器之间实现全双工的、双向通信。它允许服务器和客户端之间建立持久的连接,可以随时发送消息,而无需等待请求-响应周期。WebSocket 协议的特点包括:

1. 全双工通信: WebSocket 允许服务器和客户端同时发送和接收数据,而无需等待对方的响应。

2. 低延迟: 与传统的 HTTP 请求-响应模式不同,WebSocket 连接是持久的,因此可以更快地传输实时数据。

3. 轻量级: WebSocket 协议相对较轻量,通信开销较小。

4. 跨域支持: WebSocket 协议支持跨域通信,允许不同域的服务器和客户端建立连接。

5. 实时性: WebSocket 适用于需要实时性数据传输的应用,如在线游戏、聊天应用、股票市场数据等。

WebSocket 协议的建立和维护与 HTTP 协议有所不同。在使用 WebSocket 时,通常会经历以下步骤:

1. 握手阶段: 客户端向服务器发送一个特殊的 HTTP 请求,请求升级到 WebSocket 协议。服务器接受请求后,与客户端建立 WebSocket 连接。

2. 保持连接: WebSocket 连接建立后,双方可以随时发送数据,连接保持打开状态,不会自动关闭。

3. 数据传输: 客户端和服务器可以随时发送数据帧,这些数据帧可以是文本、二进制或其他数据类型。

4. 关闭连接: 要关闭连接,客户端或服务器可以发送一个特殊的关闭帧,表示断开连接。

WebSocket 在构建实时应用程序时非常有用,因为它可以减少通信的延迟,并允许服务器主动向客户端推送数据,而不需要客户端不断地发起请求。许多现代的Web应用程序和框架都支持WebSocket协议,使得实时互动和数据传输更加容易实现。

WebSockets服务端

1.添加maven依赖

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

2.配置websocket端点

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketTextHandler(), "/user");
    }
}

3.添加一个文本处理器

@Component
public class SocketTextHandler extends TextWebSocketHandler {

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
            throws InterruptedException, IOException {

        String payload = message.getPayload();
        //JSONObject jsonObject = JSONObject.parseObject(payload);
        session.sendMessage(new TextMessage(payload + " how may we help you?"));
    }
}

客户端就可以在连接在该端点 (ws://localhost:6002/user)

WebSockets客户端

1.客户端同样需要一个消息处理器,向下面这样

@Slf4j
@Component
public class SocketTextHandler extends TextWebSocketHandler {

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
            throws InterruptedException, IOException {

        String payload = message.getPayload();
        log.info("from socker server:{}",payload);
    }
}

2.服务端连接并发送消息

public void send() throws IOException, ExecutionException, InterruptedException {
    String url = "ws://localhost:6002/user";

    StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
    WebSocketSession session = webSocketClient.doHandshake(new SocketTextHandler(), url).get();

    // 发送消息
    String message = "Hello, WebSocket!";
    session.sendMessage(new TextMessage(message));
    // 关闭WebSocket连接
    session.close();
}

3.web连接并发送消息

var ws;
function setConnected(connected) {
   $("#connect").prop("disabled", connected);
   $("#disconnect").prop("disabled", !connected);
}

function connect() {
   ws = new WebSocket('ws://localhost:6002/user');
   ws.onmessage = function(data) {
      helloWorld(data.data);
   }
   setConnected(true);
}

function disconnect() {
   if (ws != null) {
      ws.close();
   }
   setConnected(false);
   console.log("Websocket is in disconnected state");
}

function sendData() {
   var data = JSON.stringify({
      'user' : $("#user").val()
   })
   ws.send(data);
}

function helloWorld(message) {
   $("#helloworldmessage").append("<tr><td> " + message + "</td></tr>");
}

$(function() {
   $("form").on('submit', function(e) {
      e.preventDefault();
   });
   $("#connect").click(function() {
      connect();
   });
   $("#disconnect").click(function() {
      disconnect();
   });
   $("#send").click(function() {
      sendData();
   });
});

 

源码位置spring-websocket

spring-retry

1.概述

Spring Retry 是 Spring Framework 的一个模块,用于处理在应用程序中进行重试操作的库。它提供了一种简单而强大的方式来处理在异常情况下重复执行某个操作的需求,以确保操作成功或达到最大重试次数。

以下是 Spring Retry 的主要功能和用法:

  1. 注解支持: Spring Retry 提供了一组注解,包括 @Retryable@Recover,允许您将重试逻辑添加到方法上。通过在方法上添加 @Retryable 注解,您可以指定方法应该在出现特定异常时进行重试,并可以配置重试的条件和次数。

  2. 编程式重试: 除了注解支持外,Spring Retry 还提供了编程式的重试机制。您可以在代码中使用 RetryTemplateRetryCallback 来执行需要重试的操作。这允许更灵活地控制重试逻辑。

  3. 异常分类: Spring Retry 允许您对不同类型的异常进行分类,并为每种异常指定不同的重试策略。这意味着您可以根据异常类型采用不同的重试行为。

  4. 自定义回退策略: 您可以定义自定义回退策略来控制重试之间的等待时间,以避免连续的重试操作。

  5. 监听器支持: Spring Retry 允许您添加监听器以监视重试操作的事件,例如重试开始、重试成功、重试失败等。

  6. 异常恢复: 除了重试,Spring Retry 还允许您定义恢复逻辑。当重试操作无法成功时,您可以指定一个恢复方法(使用 @Recover 注解)来处理最终失败的情况。

Spring Retry 通常在需要处理不稳定的操作或外部系统的调用时非常有用,例如网络请求、数据库操作等。它可以帮助您实现更强大和健壮的应用程序,以应对意外情况和失败。

2.实现重试

2.1使用重试模板RetryTemplate

配置重试模板
@EnableRetry
public class AppConfig {

    
    @Value("${retry.maxAttempts}")
    private  int maxAttempts;

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(5000l);//延迟5秒重试
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(maxAttempts);
        retryTemplate.setRetryPolicy(retryPolicy);

        retryTemplate.registerListener(new DefaultListenerSupport());
        return retryTemplate;
    }
}
重试执行
@Test(expected = RuntimeException.class)
public void givenTemplateRetryService_whenCallWithException_thenRetry() {
    retryTemplate.execute(arg0 -> {
        myService.templateRetryService();
        return null;
    });
}
执行结果

[00:09:00:263] [INFO] - com.zlennon.scheduling.scheduling.springretry.DefaultListenerSupport.open(DefaultListenerSupport.java:27) - onOpen
[00:09:00:298] [INFO] - com.zlennon.scheduling.scheduling.springretry.MyServiceImpl.templateRetryService(MyServiceImpl.java:52) - throw RuntimeException in method templateRetryService()
[00:09:06:754] [INFO] - com.zlennon.scheduling.scheduling.springretry.DefaultListenerSupport.onError(DefaultListenerSupport.java:21) - onError

//等待5秒后重新执行了一遍
[00:09:11:771] [INFO] - com.zlennon.scheduling.scheduling.springretry.MyServiceImpl.templateRetryService(MyServiceImpl.java:52) - throw RuntimeException in method templateRetryService()
[00:09:11:776] [INFO] - com.zlennon.scheduling.scheduling.springretry.DefaultListenerSupport.onError(DefaultListenerSupport.java:21) - onError
[00:09:11:783] [INFO] - com.zlennon.scheduling.scheduling.springretry.DefaultListenerSupport.close(DefaultListenerSupport.java:15) - onClose

使用注解@Retryable
@Retryable(value = { SQLException.class }, maxAttempts = 2, backoff = @Backoff(delay = 100))
void retryServiceWithCustomization(String sql) throws SQLException;
@Test
public void givenRetryServiceWithCustomization_whenCallWithException_thenRetryRecover() throws SQLException {
   myService.retryServiceWithCustomization(null);
   verify(myService, times(Integer.parseInt(maxAttempts))).retryServiceWithCustomization(any());
}

[18:17:13:882] [INFO] - com.zlennon.retry.service.MyServiceImpl.retryServiceWithCustomization(MyServiceImpl.java:32) - throw SQLException in method retryServiceWithCustomization()
[18:17:14:041] [INFO] - com.zlennon.retry.service.MyServiceImpl.retryServiceWithCustomization(MyServiceImpl.java:32) - throw SQLException in method retryServiceWithCustomization()
[18:17:14:050] [INFO] - com.zlennon.retry.service.MyServiceImpl.recover(MyServiceImpl.java:47) - In recover method

源码位置spring-retry

springboot定时任务

概述

Spring Scheduling是Spring Framework中的一个模块,用于支持任务调度和定时任务。它提供了一种简单而强大的方式来执行周期性任务、定时任务和一次性任务。自springboot中可以有多种方式实现一个方法定时执行。

使用ThreadPoolTaskScheduler

配置ThreadPoolTaskScheduler
package com.zlennon.scheduling.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.PeriodicTrigger;

import java.util.concurrent.TimeUnit;

@Configuration
@ComponentScan(basePackages = "com.zlennon")
public class TaskSchedulerConfig {


    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(5);
        threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
        return threadPoolTaskScheduler;
    }

    @Bean
    public CronTrigger cronTrigger() {
        return new CronTrigger("10 * * * * ?");
    }

    @Bean
    public PeriodicTrigger periodicTrigger() {
        return new PeriodicTrigger(2000, TimeUnit.MICROSECONDS);
    }

    @Bean
    public PeriodicTrigger periodicFixedDelayTrigger() {
        PeriodicTrigger periodicTrigger = new PeriodicTrigger(2000, TimeUnit.MICROSECONDS);
        periodicTrigger.setFixedRate(true);
        periodicTrigger.setInitialDelay(1000);
        return periodicTrigger;
    }
}
执行
@Component
public class ThreadPoolTaskSchedulerExamples {
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    @Autowired
    private CronTrigger cronTrigger;

    @Autowired
    private PeriodicTrigger periodicTrigger;

    //@PostConstruct
    public void scheduleRunnableWithCronTrigger() {
        taskScheduler.schedule(new RunnableTask("Current Date"), new Date());
        taskScheduler.scheduleWithFixedDelay(new RunnableTask("Fixed 1 second Delay"), 1000);
        taskScheduler.scheduleWithFixedDelay(new RunnableTask("Current Date Fixed 1 second Delay"), new Date(), 1000);
        taskScheduler.scheduleAtFixedRate(new RunnableTask("Fixed Rate of 2 seconds"), new Date(), 2000);
        taskScheduler.scheduleAtFixedRate(new RunnableTask("Fixed Rate of 2 seconds"), 2000);
        taskScheduler.schedule(new RunnableTask("Cron Trigger"), cronTrigger);
        taskScheduler.schedule(new RunnableTask("Periodic Trigger"), periodicTrigger);
    }

    static class RunnableTask implements Runnable {

        private String message;

        public RunnableTask(String message) {
            this.message = message;
        }

        @Override
        public void run() {
            System.out.println("Runnable Task with " + message + " on thread " + Thread.currentThread().getName());
        }
    }

使用注解@Scheduled

使用Scheduled可以很方便的按条件执行定时任务
@Component()
@EnableScheduling
public class ScheduledAnnotationExecute {

    @Scheduled(fixedDelay = 1000)
    public void scheduleFixedDelayTask() {
        System.out.println("Fixed delay task sssssssss - " + System.currentTimeMillis() / 1000);
    }

    @Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}")
    public void scheduleFixedDelayTaskUsingExpression() {
        System.out.println("Fixed delay task - " + System.currentTimeMillis() / 1000);
    }

    @Scheduled(fixedDelay = 1000, initialDelay = 2000)
    public void scheduleFixedDelayWithInitialDelayTask() {
        System.out.println("Fixed delay task with one second initial delay - " + System.currentTimeMillis() / 1000);
    }

    @Scheduled(fixedRate = 1000)
    public void scheduleFixedRateTask() {
        System.out.println("Fixed rate task - " + System.currentTimeMillis() / 1000);
    }

    @Scheduled(fixedRateString = "${fixedRate.in.milliseconds}")
    public void scheduleFixedRateTaskUsingExpression() {
        System.out.println("Fixed rate task - " + System.currentTimeMillis() / 1000);
    }

    @Scheduled(fixedDelay = 1000, initialDelay = 1000)
    public void scheduleFixedRateWithInitialDelayTask() {
        long now = System.currentTimeMillis() / 1000;
        System.out.println("Fixed rate task with one second initial delay - " + now);
    }

    /**
     * Scheduled task is executed at 10:15 AM on the 15th day of every month
     */
    @Scheduled(cron = "0 15 10 15 * ?")
    public void scheduleTaskUsingCronExpression() {
        long now = System.currentTimeMillis() / 1000;
        System.out.println("schedule tasks using cron jobs - " + now);
    }

    @Scheduled(cron = "${cron.expression}")
    public void scheduleTaskUsingExternalizedCronExpression() {
        System.out.println("schedule tasks using externalized cron expressions - " + System.currentTimeMillis() / 1000);
    }

使用SchedulingConfigurer

SchedulingConfigurer中可以自定义下次执行的时间,如下次执行的时间tickService.getDelay()计算得出
@EnableScheduling
public class DynamicSchedulingConfig implements SchedulingConfigurer {

    @Autowired
    private TickService tickService;

    @Bean
    public Executor taskExecutor() {
        return Executors.newSingleThreadScheduledExecutor();
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(
          () -> tickService.tick(),
          context -> {
              Optional<Date> lastCompletionTime =
                Optional.ofNullable(context.lastCompletionTime());
              Instant nextExecutionTime =
                lastCompletionTime.orElseGet(Date::new).toInstant()
                  .plusMillis(tickService.getDelay());
              return Date.from(nextExecutionTime).toInstant();
          }
        );
    }

}

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

@Service
public class TickService {

    private long delay = 0;

    public long getDelay() {
        this.delay += 1000;
        System.out.println("delaying " + this.delay + " milliseconds...");
        return this.delay;
    }

    public void tick() {
        final long now = System.currentTimeMillis() / 1000;
        System.out
          .println("schedule tasks with dynamic delay - " + now);
    }

}

使用quartz

见:sprinboot-quartz

源码位置:spring-scheduling

springboot初始化

 

启动SpringbootInitApplication应用,开始执行run 方法,828行new SpringApplication对象

 

构造器中初始化初始化一些属性

执行run方法

 

configureHeadlessProperty :设置Headless 模式,Headless 模式通常用于没有图形用户界面 (GUI) 的应用程序,例如后端服务或一些命令行工具。在这种模式下,应用程序不需要图形窗口或用户界面,因此可以更轻便和高效。

    public ConfigurableApplicationContext run(String... args) {
        long startTime = System.nanoTime();
        //创建 Bootstrap 上下文
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        ConfigurableApplicationContext context = null;
        this.configureHeadlessProperty();
        //从 /classpatch下的/META-INF/spring.factories下找到SpringApplicationRunListener,并开启
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);

        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //准备基础的环境信息
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
           //打印标识 
          
            Banner printedBanner = this.printBanner(environment);
           //创建应用上下文
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            //准备应用上下文
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            ////刷新应用上下文
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
            }

            listeners.started(context, timeTakenToStartup);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var12) {
            if (var12 instanceof AbandonedRunException) {
                throw var12;
            }

            this.handleRunFailure(context, var12, listeners);
            throw new IllegalStateException(var12);
        }

        try {
            if (context.isRunning()) {
                Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
                listeners.ready(context, timeTakenToReady);
            }

            return context;
        } catch (Throwable var11) {
            if (var11 instanceof AbandonedRunException) {
                throw var11;
            } else {
                this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
                throw new IllegalStateException(var11);
            }
        }
    }

 

创建应用上下文

如果没有配置spring.aot.enabled   ,aot 是新功能指的是AOT,Ahead Of Time 指的是运行前编译,预先编译。  然后创建创建不同的AnnotaionContext

prepareContext

  1. 设置应用程序上下文的环境:

    • context.setEnvironment(environment):这一步将应用程序的环境(environment)设置到应用程序上下文(context)中。环境包括了应用程序的配置属性,它决定了应用程序如何配置自身。
  2. 应用程序上下文的后处理:

    • this.postProcessApplicationContext(context):这一步执行应用程序上下文的后处理,允许对应用程序上下文进行自定义配置和修改。
  3. 添加 AOT 生成的初始化器(如果需要):

    • this.addAotGeneratedInitializerIfNecessary(this.initializers):如果应用程序需要使用AOT(Ahead of Time)生成的初始化器,这一步将它们添加到初始化器列表中。AOT生成的初始化器通常用于优化应用程序的启动性能。
  4. 应用初始化器:

    • this.applyInitializers(context):在这一步中,应用程序的初始化器(initializers)被应用到应用程序上下文。初始化器通常用于执行应用程序的配置和准备工作。
  5. 触发 contextPrepared 事件:

    • listeners.contextPrepared(context):在这一步触发一个 contextPrepared 事件,通知所有注册的监听器,应用程序上下文已经准备好。详细看springboot-application-event
  6. 关闭 Bootstrap 上下文:

    • bootstrapContext.close(context):Bootstrap 上下文被关闭。Bootstrap 上下文是在应用程序启动时创建的,用于管理应用程序生命周期。一旦应用程序上下文准备就绪,Bootstrap 上下文可以关闭。
  7. 记录启动信息:

    • this.logStartupInfo(context.getParent() == null):如果启用了启动信息日志,这一步记录启动信息。这包括了应用程序的启动时间和其他信息。这个信息通常用于性能分析和诊断。
  8. 注册 Spring Bean:

    • 这一步注册一些 Spring Bean 到应用程序上下文中。这包括了应用程序参数(springApplicationArguments)和启动横幅(springBootBanner),它们可以在应用程序中使用。
  9. 允许循环引用:

    • 如果应用程序上下文的 beanFactory 支持,这一步设置是否允许循环引用。默认情况下,Spring 框架允许循环引用,但可以进行配置以禁用它。
  10. 懒加载:

    • 如果启用了懒加载(lazyInitialization),这一步将一个懒加载的 BeanFactory 后处理器添加到应用程序上下文中。懒加载允许在需要时才初始化 Bean,从而提高性能。
  11. 添加 BeanFactory 后处理器:

    • 在这个步骤中,添加了一些 BeanFactory 后处理器,用于定制应用程序上下文的行为。例如,LazyInitializationBeanFactoryPostProcessor 用于处理懒加载。
  12. 加载应用程序资源:

    • 如果不使用AOT生成的构件(artifacts),这一步将加载应用程序的资源,例如配置文件和其他资源。这些资源通常用于配置和初始化应用程序。
  13. 触发 contextLoaded 事件:

    • listeners.contextLoaded(context):最后,触发一个 contextLoaded 事件,通知所有注册的监听器,应用程序上下文已经加载并准备好。

 

refreshContext

  1. 准备刷新:

    • this.prepareRefresh():在刷新应用程序上下文之前,执行一些准备工作。这包括设置启动步骤和其他必要的初始化。
  2. 获取新的 Bean 工厂:

    • ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory():在这一步,获取一个新的 Bean 工厂,用于管理应用程序中的所有 Bean。这个工厂将替代之前的工厂,以确保应用程序是最新的。
  3. 准备 Bean 工厂:

    • this.prepareBeanFactory(beanFactory):对新的 Bean 工厂进行准备工作,包括设置类加载器、属性编辑器和 BeanPostProcessor 等。
  4. 后处理 Bean 工厂:

    • this.postProcessBeanFactory(beanFactory):在这一步,对 Bean 工厂进行后处理。包括注册自定义属性编辑器或 BeanPostProcessor。
  5. 执行 Bean 工厂后处理器:

    • this.invokeBeanFactoryPostProcessors(beanFactory):在这一步,调用 Bean 工厂后处理器。这些后处理器可以对应用程序上下文进行进一步的配置和初始化。
  6. 注册 Bean 后处理器:

    • this.registerBeanPostProcessors(beanFactory):注册 Bean 后处理器,这些后处理器将应用于应用程序上下文中的 Bean。Bean 后处理器可以执行自定义的操作,例如代理或包装 Bean。
  7. 初始化消息源:

    • this.initMessageSource():在这一步,初始化应用程序的消息源,它通常用于处理国际化和本地化的消息。
  8. 初始化应用程序事件多播器:

    • this.initApplicationEventMulticaster():初始化应用程序事件多播器,用于发布和监听应用程序事件。
  9. 在刷新之后执行特定操作:

    • this.onRefresh():在刷新之后,执行一些特定于应用程序上下文的操作。这是一个扩展点,允许你在应用程序上下文准备好之后执行自定义操作。
  10. 注册监听器:

    • this.registerListeners():注册应用程序上下文中的事件监听器,以便它们可以监听并响应应用程序事件。
  11. 完成 Bean 工厂初始化:

    • this.finishBeanFactoryInitialization(beanFactory):在这一步,完成 Bean 工厂的初始化,包括实例化和初始化所有非懒加载的单例 Bean

  12. 完成刷新:

    • this.finishRefresh():完成应用程序上下文的刷新。这包括发布 ContextRefreshedEvent 事件,通知应用程序上下文已经准备好。
  13. 处理异常:

    • 如果在刷新过程中出现异常,会在 catch 块中处理异常。如果异常发生,它会取消刷新尝试,销毁已经创建的 Bean,然后抛出异常以指示刷新失败。
  14. 重置常见缓存:

    • this.resetCommonCaches():最后,在 finally 块中,重置一些常见的缓存,以确保下一次刷新可以正常进行。

 

 

获取候选配置组件