Spring JUC 教程
今天是:
我需要三件东西:爱情友谊和图书。然而这三者之间何其相通!炽热的爱情可以充实图书的内容,图书又是人们最忠实的朋友。

基于Spring Boot 2.4.2

1. 为什么使用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里面的内容了。

2. 使用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

3. 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;
    }
}

 

4. 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>"

 

5. 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一样

6. 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应用时将看到自动配置的类,如下:

7. 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;
    }

 

8. 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();
                }
            }

     

 

9. 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));
    }
}

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

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]

 

10. 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