0%

1、Spring注解编程发展过程

1.1 Spring 1.x

2004年3月24日,Spring1.0正式发布,提供了IoC,AOP及XML配置的方式。

在Spring1.x版本中提供的是纯XML配置的方式,也就是在该版本中我们必须要提供xml的配置文件,在该文件中我们通过<bean>标签来配置需要被IoC容器管理的bean。

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd">

   <bean class="com.liyyao.demo.UserService" />
</beans>
1
2
3
4
public static void main(String[] args) {
   ApplicationContext ac = new FileSystemXmlApplicationContext("classpath:applicationContext01.xml");
   System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}

在Spring1.2版本的时候,提供了**@Transaction**(org.springframework.transaction.annotation)注解。简化了事件的操作。

1.2 Spring 2.x

在2006年10月3日Spring2.0版本问世,在2.x版本中,比较重要的特点是增加了很多注解。

1.2.1 Spring2.5之前

Spring 2.5之前新增的有@Required @Repository @Aspect,同时也扩展了XML的配置能力,提供了第三方的扩展标签,比如<dubbo>

1.2.1 @Required

如果你在某个java类的某个set方法上使用了该注解,那么该set方法对应的属性在xml配置文件中必须被设置,否则应付报错。

1
2
3
4
5
6
7
8
9
10
public class UserService {
private String username;
public String getUsername() {
return username;
}
@Required
public void setUsername(String username) {
this.username = username;
}
}

设置好属性后就没有错误提示了

源码中可以看到@Required从2.0开始提供

1.2.2 @Repository

@Repository对应数据访问层Bean,这个注解在Spring2.0版本就提供了

1.2.3 @Aspect

@Aspect是AOP相关的一个注解,用来标识配置类。

1.2.2 Spring2.5之后

在2007年11月19日,Spring更新到了2.5版本,新增了很多常用注解,大大的简化配置操作。

注解 说明
@Autowired 依赖注入
@Qualifier 配置@Autowired注解使用
@Component 声明组件
@Service 声明业务层组件
@Controller 声明控制层组件
@RequestMapping 声明请求对应的处理方法

在这些注解的作用下,我们可以不用在xml文件中去注册每个bean,这里我们只需要指定扫描路径,然后在对应的Bean头部添加相关的注解即可,这大大的简化了我们的配置及维护工作。案例如下。

  • 我们在配置文件中只需要配置扫描路径即可
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.liyyao"/>
</beans>
  • 持久层代码
1
2
3
4
5
6
@Repository
public class UserDao {
public void query() {
System.out.println("dao query...");
}
}
  • 业务逻辑层代码
1
2
3
4
5
6
7
8
9
@Service
public class UserService {
@Autowired
private UserDao userDao;

public void query() {
userDao.query();
}
}
  • 控制层代码
1
2
3
4
5
6
7
8
@Controller
public class UserController {
@Autowired
private UserService service;
public void query() {
service.query();
}
}
  • 测试代码
1
2
3
4
5
6
7
public class App {
public static void main( String[] args ) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring2.0.xml");
UserController bean = ac.getBean(UserController.class);
bean.query();
}
}

虽然在Spring2.5版本提供了很多的注解,也大大的简化了开发,但是仍然没有摆脱XML配置驱动

1.3 Spring 3.x

在2009年12月16日发布了Spring3.0版本,这是一个注解编程发展的里程碑版本,在该版本中全面拥抱Java5,提供了@Configuration注解,目的就是去XML化,同时通过@ImportResource来实现Java配置类和XML配置的混合使,用来平稳过渡。

1.3.1 @Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Configuration 标注的Java类,相当于application.xml配置
*/
@Configuration
public class JavaConfig {
/**
* @Bean 注解标注的方法就相当于<bean></bean>标签
* 也是Spring3.0提供的注解
* @return
*/
@Bean
public UserService userService() {
return new UserService();
}
}

在Spring3.1版本之前配置扫描路径我们还只能在XML配置文件中通过component-scan标签来实现,在3.1之前还不能够完全实现去XML配置,在3.1版本提供了一个@ComponentScan注解,该注解的作用是替换掉XML中的component-scan标签,是注解编程很大的进步,也是Spring实现无配置化的坚实基础。

1.3.2 @ComponentScan

@ComponentScan的作用是指定扫描路径,用来替代在XML中的<component-scan>标签,默认的扫描路径是当前注解标注的类所在的包及其子包。

定义UserService

1
2
3
@Service
public class UserService {
}

创建对应的Java配置类

1
2
3
4
5
6
7
@ComponentScan
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}
}

输出结果

当然也可以指定扫描的路径

1.3.3 @Import

@Import注解只能用在类上,作用是快速的将实例导入到Spring的IoC容器中,将实例导入到IoC容器中的方式有很多种,比如@Bean注解,@Import注解可以用于导入第三方包。具体的使用方式有三种。

1.3.3.1 静态导入

静态导入的方式是直接将我们需要导入到IoC容器中的对象类型直接添加进去即可。这种方式的好处是简单,直接,但是缺点就是如果要导入的类比较多,就不方便了,不够灵活。

1
2
3
@Service
public class UserService {
}
1
2
3
4
5
6
7
@Import(value = {UserService.class})
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}
}

1.3.3.2 ImportSelector导入

@Import注解中我们也可以添加一个实现了ImportSelector接口的类型,这时不会将该类型导入IoC容器中,而是会调用ImportSelector接口中定义的selectImports方法,将该方法的返回的字符串数组的类型添加到容器中。

定义两个业务

1
2
3
4
public class Cache {
}
public class Logger {
}

定义ImportSelector接口的实现,方法返回的是需要添加到IoC容器中的对象对应的类型的类全路径的字符串数组,我们可以根据不同的业务需求导入不同的类型的类,会更加灵活些。

1
2
3
4
5
6
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Logger.class.getName(), Cache.class.getName()};
}
}

测试代码

1
2
3
4
5
6
7
8
9
@Import(value = {MyImportSelector.class})
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}

输出结果

1.3.3.3 ImportBeanDefinitionRegistrar导入

除了上面所介绍的ImportSelector方式灵活导入以外,还提供了ImportBeanDefinitionRegistrar接口,也可以实现Bean的导入,相比ImportSelector接口的方式,ImportBeanDefinitionRegistrar的方式是直接在定义的方法中提供了BeanDefinitionRegistry,自己在方法中实现注册。

1
2
3
4
5
6
7
8
9
10
11
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//将需要注册的对象封装为RootBeanDefinition对象
RootBeanDefinition cache = new RootBeanDefinition(Cache.class);
registry.registerBeanDefinition("cache", cache);

RootBeanDefinition logger = new RootBeanDefinition(Logger.class);
registry.registerBeanDefinition("logger", cache);
}
}

测试代码

1
2
3
4
5
6
7
8
9
@Import(value = {MyImportBeanDefinitionRegistrar.class})
public class JavaConfig {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}

输出结果

1.3.4 @EnableXXX

@Enable模块驱动,其实是在系统中我们先开发好各个功能独立的模块,比如Web MVC模块,AspectJ代理模块,Caching模块等。

案例说明,先定义好功能模块

1
2
3
4
5
6
7
8
9
10
/**
* 定义一个Java配置类
*/
@Configuration
public class HelloWorldConfiguration {
@Bean
public String helloWorld() {
return "Hello World";
}
}

然后定义@Enable注解

1
2
3
4
5
6
7
8
9
10
/**
* 定义@Enable注解
* 在该注解中通过@Import注解导入我们自定义的模块,使之生效。
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(HelloWorldConfiguration.class)
public @interface EnableHelloWorld {
}

测试代码

1
2
3
4
5
6
7
8
9
//加载自定义模块
@EnableHelloWorld
public class JavaMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(JavaMain.class);
String helloWorld = ac.getBean("helloWorld", String.class);
System.out.println("helloWorld = " + helloWorld);
}
}

输出结果

1.4 Spring 4.x

2013年11月1日更新的Spring4.0,完全支持Java8。这是一个注解完善的版本,提供的核心注解是@Conditional条件注解。@Conditional注解的作用是按照一定的条件进行判断,满足条件就给容器注册Bean实例。

@Conditional的定义行为

1
2
3
4
5
6
7
8
9
10
//该注解可以在类和方法中使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* 注解中添加的类型必须是实现了Condition接口的类型
*/
Class<? extends Condition>[] value();
}

Condition是个接口,需要实现matches方法,返回true则注入bean,返回false则不注入。

案例讲解

1
2
3
4
5
6
7
8
9
/**
* 定义一个Condition接口的实现
*/
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false; //默认返回false
}
}

创建Java配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JavaConfig {
/**
* 条件注解,添加的类型必须是实现了Condition接口的类型
* MyCondition的matches方法返回true则注入,返回false则不注入
*/
@Conditional(MyCondition.class)
@Bean
public PersonService personService() {
return new PersonService();
}

public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
}

测试

所以@Conditional的作用就是给我们提供了对象导入IoC容器的条件机制,这也是SpringBoot中的自动装配的核心关键。当然在4.x还提供了一些其他的注解,比如@EventListener作为ApplicationListener接口编程的第二选择,@AliasFor解除注解派生的时候冲突限制。@CrossOrigin作为浏览器跨域资源的解决方案等。

1.5 Spring 5.x

在2017年9月28日,Spring来到了5.0版本。5.0同时也是SpringBoot2.0的底层。注解驱动的性能提升方面不是很明显。在SpringBoot应用场景中,大量使用@ComponentScan扫描,导致Spring模式的注解解析时间增大,因此,5.0版本引入**@Indexed**注解,为Spring模式添加索引。

当我们在项目中使用了@Indexed之后,编译打包的时候会在项目中自动生成META-INF/spring.components文件,当Spring应用上下文执行ComponentScan扫描时,META-INF/spring.components将会被CandidateComponentsIndexLoader读取并加载,转换为CandidateComponentsIndex对象,这样的话@ComponentScan不再扫描指定的package,而是读取CandidateComponentsIndex对象,从而达到提升性能的目的。

1
2
3
4
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-indexer</artifactId>
</dependency>

使用@Indexed注解

1
2
3
@Indexed
public class UserService {
}

编译打包后的结果

1、SpringBoot自动装配

Spring Framework一直在致力于解决一个问题,就是如何让Bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装配。所以,所谓的自动装配,实际上就是如何自动将Bean装载到IoC容器中来。

实际上在Spring 3.x版本中,Enable模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在Spring 4.x版本中,Conditional条件注解的出现,接下来我们来看一下SpringBoot的自动装配是怎么一回事。

1.1 自动装配例子

我们在SpringBoot中使用redis时,只需要添加redis依赖,然后进行配置即可使用,如下面配置

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
spring:
redis:
host: 127.0.0.1
port: 6379
1
2
@Autowired
private RedisTemplate<String,String>redisTemplate;

按照上面配置好后,就可以使用RedisTemplate了,那么,为什么RedisTemplate可以被直接注入呢?它是什么时候加入到IoC容器的呢?这就是自动装配。自动装配可以使得classpath下依赖的包的相关的bean,被自动装载到Spring IoC容器中,那么这又是怎么做到的呢?

1.2 EnableAutoConfiguration

EnableAutoConfiguration的主要作用其实就是帮助SpringBoot应用把所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器中。我们看EnableAutoConfiguration这个注解中,它的import是这样

1
2
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

但是从EnableAutoConfiguration上面的import注解来看,这里并不是引入另一个Configuration,而是一个ImportSelector,那么这个ImportSelector又是什么东西呢?可以看下Spring中@Import相关资料。了解了ImportSelector和ImportBeanDefinitionRegistrar后,对于EnableAutoConfiguration的理解就容易些了。

结合下面AutoConfigurationImportSelector类,其实EnableAutoConfiguration会帮助SpringBoot应用把所有符合@Configuration配置都加载到当前SpringBoot创建的IoC容器,而这里借助了Spring框架提供的一个工具类SpringFactoriesLoader的支持,以及用到了Spring提供的条件注解@Conditional,选择性的针对需要加载的bean进行条件过滤。

1.3 AutoConfigurationImportSelector

Spring中的@Import注解可以配置三种不同的class:

  • 基于普通的bean或者带有@Configuration的bean进行静态注入
  • 实现ImportSelector接口进行动态注入
  • 实现ImportBeanDefinitionRegistrar接口进行动态注入

看一下AutoConfigurationImportSelector类的继承关系

AutoConfigurationImportSelector类是实现了ImportSelector接口,所以它本质上是基于ImportSelector来实现bean的动态加载。ImportSelector接口中的selectImports方法返回的数组(类的全类名)都会被纳入到Spring容器中,那么猜想这里的实现原理也是一样的,定位到AutoConfigurationImportSelector这个类的selectImports方法

1
2
3
4
5
6
7
8
9
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//获取所有候选配置类EnableAutoConfiguration
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
//获取元注解中的属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//使用SpringFactoriesLoader加载classpath路径下META-INF\spring.factories中
//key=org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的value
//这里不同的版本可能会有些不同,我看2.7版本是放到META-INF\spring文件夹下,单独拎出来了
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//去重
configurations = removeDuplicates(configurations);
//应用exclusion属性
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
//过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
configurations = getConfigurationClassFilter().filter(configurations);
//广播事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

1.4 SpringFactoriesLoader

这里简单分析一下SpringFactoriesLoader这个工具类的使用。它其实和Java中的SPI机制的原理是一样的,不过它比SPI更好的点在于不会一次性加载所有的类,而是根据key进行加载。SpringFactoriesLoader的作用是从classpath/META-INF/spring.factories文件中,根据key来加载对应的类到Spring IoC容器中。项目案例如下。

  • 创建外部项目jar
1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.22</version>
</dependency>
1
2
3
4
5
6
public class TestCore {
public String study() {
System.out.println("good good study, day day up");
return "liyyao.com";
}
}
1
2
3
4
5
6
7
@Configuration
public class TestConfig {
@Bean
public TestCore testCore() {
return new TestCore();
}
}
  • 创建另外一个工程(spring-boot)

把前面的工程打成jar包,当前项目依赖该jar包

1
2
3
4
5
<dependency>
<groupId>com.liyyao.springboot</groupId>
<artifactId>SpringFactoriesLoaderDemo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

通过下面代码获取依赖包中的属性

1
2
3
4
5
6
7
8
@SpringBootApplication
public class SpringFactoriesLoaderTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ca = SpringApplication.run(SpringFactoriesLoaderTestApplication.class, args);
TestCore bean = ca.getBean(TestCore.class);
System.out.println(bean.study());
}
}

运行会报错,因为TestCore并没有被Spring的IoC容器所加载,也就是没有被EnableAutoConfiguration导入。解决方案,在TestCore项目resources下新建文件夹META-INF,在文件夹下面新建spring.factories文件,文件中配置key为自动配置类EnableAutoConfiguration的全路径,value是配置类的全路径

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.liyyao.springboot.TestConfig

重新打包,重新运行SpringFactoriesLoaderTestApplication这个类,可以发现我们编写的那个类就被加载进来了。

1.5 SpringBoot中的条件过滤

在分析AutoConfigurationImportSelector的源码时,会先扫描spring-autoconfiguration-metadata.properties文件,最后在扫描spring.factories对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢?因为很多的@Configuration其实是依托其他的框架来加载的,如果当前的classpath环境下没有相关联的依赖,则意味着这些类没必要进行加载,所以通过这种条件过滤可以有效的减少@Configuration类的数量从而降低SpringBoot的启动时间。

在1.4案例中,修改TestCore项目,在META-INF下增加配置文件,spring-autoconfigure-metadata.properties。

1
com.liyyao.springboot.TestConfig.ConditionalOnClass=com.liyyao.springboot.TestClass

格式:自动配置的类全名.条件=值

上面这段配置意思就是如果当前的classpath下存在TestClass,则会对TestConfig这个Configuration进行加载。

沿用1.4案例中spring-boot工作的测试代码,直接运行main方法,发现原本能够被加载的TestCore类现在在IoC容器中找不到了。在当前工程中指定的包com.liyyao.springboot下创建一个TestClass以后,再运行代码,发现程序就能够正常运行了。

1.6 手写Starter

通过手写Starter来加深对于自动装配的理解

1.6.1 创建一个Maven项目,quick-starter

  • 定义相关的依赖
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
<!-- 可选 -->
<optional>true</optional>
</dependency>
  • 定义Format接口

定义一个格式转换接口,并且定义两个实现类

1
2
3
4
5
6
7
8
9
public interface FormatProcessor {
/**
* 定义一个格式化的方法
* @param object
* @return
* @param <T>
*/
<T> String format(T object);
}
1
2
3
4
5
6
public class JsonFormatProcessor implements FormatProcessor {
@Override
public <T> String format(T object) {
return "JsonFormatProcessor: " + JSON.toJSON(object);
}
}
1
2
3
4
5
6
public class StringFormatProcessor implements FormatProcessor {
@Override
public <T> String format(T object) {
return "StringFormatProcessor: " + object.toString();
}
}
  • 定义配置类

首先定义格式化加载的Java配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class FormatAutoConfiguration {

@ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
@Bean
@Primary //优先加载
public FormatProcessor stringFormatProcessor() {
return new StringFormatProcessor();
}

@ConditionalOnClass(name = "com.alibaba.fastjson.JSON")
@Bean
public FormatProcessor jsonFormatProcessor() {
return new JsonFormatProcessor();
}
}

  • 定义模板工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloFormatTemplate {

private FormatProcessor formatProcessor;

public HelloFormatTemplate(FormatProcessor processor) {
this.formatProcessor = processor;
}

public <T> String doFormat(T obj) {
StringBuilder builder = new StringBuilder();
builder.append("Execute format : ").append("<br>");
builder.append("Object format result : ").append(formatProcessor.format(obj));
return builder.toString();
}
}
  • 整合到SpringBoot中去的Java配置类
1
2
3
4
5
6
7
8
9
@Configuration
@Import(FormatAutoConfiguration.class)
public class HelloAutoConfiguration {

@Bean
public HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor) {
return new HelloFormatTemplate(formatProcessor);
}
}
  • 创建spring.factories文件

在resources下创建META-INF目录,再在其下创建spring.factories文件

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.liyyao.springboot.config.HelloAutoConfiguration
  • install打包,然后就可以在SpringBoot项目中依赖该项目来操作了。

1.6.2 测试

  • 在SpringBoot项目中引入依赖
1
2
3
4
5
<dependency>
<groupId>com.liyyao.springboot</groupId>
<artifactId>FormatProject</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
  • 在Controller中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class UserController {

@Autowired
private HelloFormatTemplate helloFormatTemplate;

@GetMapping("/format")
public String format() {
User user = new User();
user.setName("liyyao");
user.setAge(18);
return helloFormatTemplate.doFormat(user);
}
}

1.6.3 自定义Starter关联配置信息

有些情况下我们需要用户在使用的时候动态的传递相关的配置信息,比如Redis的IP,端口等,这些信息显然是不能直接写到代码中的,这里我们就可以通过SpringBoot的配置类来实现。

  • 上面项目中导入依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.7.3</version>
</dependency>
  • 创建对应的属性类
1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix = HelloProperties.HELLO_FORMAT_PREFIX)
public class HelloProperties {

public static final String HELLO_FORMAT_PREFIX = "liyyao.hello.format";

private String name;
private int age;
private Map<String, Object> info;
...getter setter...
}
  • 在Java配置类中关联
1
2
3
4
5
6
7
8
9
10
@Configuration
@Import(FormatAutoConfiguration.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {

@Bean
public HelloFormatTemplate helloFormatTemplate(HelloProperties helloProperties, FormatProcessor formatProcessor) {
return new HelloFormatTemplate(helloProperties, formatProcessor);
}
}
  • 调整模板方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloFormatTemplate {

private FormatProcessor formatProcessor;

private HelloProperties helloProperties;

public HelloFormatTemplate(HelloProperties helloProperties, FormatProcessor processor) {
this.formatProcessor = processor;
this.helloProperties = helloProperties;
}

public <T> String doFormat(T obj) {
StringBuilder builder = new StringBuilder();
builder.append("Execute format : ").append("<br>");
builder.append("HelloProperties : ").append(formatProcessor.format(helloProperties.getInfo())).append("<br>");
builder.append("Object format result : ").append(formatProcessor.format(obj));
return builder.toString();
}
}
  • 增加提示

在这个工程的META-INF/下创建一个additional-spring-configuration-metadata.json,这个是设置属性的提示类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"properties": [
{
"name": "liyyao.hello.format.name",
"type": "java.lang.String",
"description": "账号信息",
"defaultValue": "root"
},{
"name": "liyyao.hello.format.age",
"type": "java.lang.Integer",
"description": "年龄",
"defaultValue": 18
}
]
}

  • 重新打包进行测试

在SpringBoot项目中的properties文件中进行属性信息的配置

1
2
3
4
liyyao.hello.format.name=liyyao
liyyao.hello.format.age=25
liyyao.hello.format.info.key="zhangsan"
liyyao.hello.format.info.value="haha"

重新启动SpringBoot项目进行访问

1、执行hexo deploy时,出现权限问题

1
2
3
Permission denied (publickey). 
fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.

大致意思是说:没有权限,无法读取远程仓库。请确保你有正确的访问权限或者确认仓库的存在。

解决方案

简单粗暴的方法:直接删除已有的SSH文件然后重新设置并添加到github上。

具体操作

打开终端,输入:ssh-keygen -t rsa -C "你的邮箱地址" 这个命令会生成一个以你的邮箱为标签的SSH Key。

然后bash会显示:

Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/xxx/.ssh/id_rsa):

直接回车,然后出现:

/c/Users/xxx/.ssh/id_rsa already exists.
Overwrite (y/n)?

这说明你之前已经生成有SSH Key,此时你可以根据情况选择是否覆盖。

接下来将SSH Key添加到GitHub

  • 打开自己的GitHub,点击自己的头像找到Settings,然后在侧栏找到SSH and GPG keys,点击new SSH,复制刚才生成的id_rsa.pub中的所有内容到Key框中,在Title框中输入方便查找的名字保存即可。

  • 打开终端,输入:ssh -T git@github.com 会在bash中显示:

    Hi liyyao! You've successfully authenticated, but GitHub does not provide shell access.
    Connection to github.com closed.

    看到此提示表示已经配置好了,再执行hexo deploy就不会有问题了。

1、什么是SPI?

SPI全称为Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如现在的Dubbo、JDBC中都使用到了SPI机制。为什么要学SPI,因为在SpringBoot的自动装配中其实有使用到SPI机制,所以掌握了这部分对于SpringBoot的学习还是很有帮助的。

​ 总结:SPI就是我定义接口,你去实现这些接口,然后在某个文件里告诉我实现是什么即可。

2、案例

​ 我们Java在进行数据库连接时使用的JDBC,数据库的种类比较多,我们常用的就是MySQL和Oracle,如果没有一个访问数据库的统一接口,那么连接不同的数据库就需要写不同的程序,这样对程序员来说比较繁琐,而JDBC就提供了一些接口,定义了操作数据库的抽象行为,由各个数据库厂商自己实现。

2.1 程序是怎么知道这些接口是由哪些Java类实现的呢?

​ 基于这个点,SPI定义了一个规范,实现者要在META-INF/services下创建接口名的文件,然后在文件中是接口的具体实现类。

2.2 案例介绍

​ 先定义接口项目

​ 然后创建一个扩展的实现,先导入上面接口项目的依赖

1
2
3
4
5
<dependency>
<groupId>com.liyyao</groupId>
<artifactId>MyDataBase</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

​ 然后创建接口的实现

1
2
3
4
5
6
7
8
9
10
/**
* SPI:MySQL对于getUrl的一种实现
*/
public class MySqlConnection implements Connection {
@Override
public String getUrl() {
System.out.println("mysql...");
return null;
}
}

​ 然后在resources目录下创建META-INF/services目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。

同样的再创建一个扩展的实现

然后创建一个测试的项目,导入扩展的项目,然后进行测试,测试代码如下

1
2
3
4
5
6
7
8
public static void main(String[] args) {
ServiceLoader<Connection> load = ServiceLoader.load(Connection.class);
Iterator<Connection> iterator = load.iterator();
while (iterator.hasNext()) {
Connection con = iterator.next();
con.getUrl();
}
}

根据不同的导入,执行的逻辑会有不同

3、源码查看

3.1、ServiceLoader

​ 首先来看下ServiceLoader的类结构

1
2
3
4
5
6
7
8
9
10
11
12
// 配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务 类或者接口
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// 访问权限的上下文对象
private final AccessControlContext acc;
// 保存已经加载的服务类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 内部类,真正加载服务类
private LazyIterator lookupIterator;

3.2、load

​ load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class ServiceLoader<S> implements Iterable<S> {
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//要加载的接口
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//访问控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext():null;
reload();
}
public void reload() {
//先清空
providers.clear();
//实例化内部类
lookupIterator = new LazyIterator(service, loader);
}
}

​ 查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法时,实际上调用的都是LazyIterator的相应方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
String fullName = PREFIX + service.getName();
//将文件路径转成URL对象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件对象,读取内容,最后返回
pending = parse(service, configs.nextElement());
}
//拿到第一个实现类的类名
nextName = pending.next();
return true;
}
}

​ 创建实例对象,当然,调用next方法时,实际调用到的是lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private class LazyIterator implements Iterator<S> {
private S nextService() {
//全限定类名
String cn = nextName;
nextName = null;
//创建类的Class对象
Class<?> c = Class.forName(cn, false, loader);
//通过newInstance实例化
S p = service.cast(c.newInstance());
//放入集合,返回实例
providers.put(cn, p);
return p;
}
}

​ 看到这儿应该很清楚了,获取到类的实例,然后就可以做事情了。

进行技术选型,首先应该是站在一个细化的角度,从各个方面去对比技术。

哪个技术最热门,用的人最多,那么就选用哪个技术。如果都很热门,用的人都很多,怎么选?
分场景,每种技术适合什么场景;
分公司,每种技术适合小公司还是大公司;

选型:Apollo、SpringCloudConfig、Nacos

apollo: 架构比较复杂,比较完善的,功能上也很完善,中小型公司并不一定要使用,中大型公司可以考虑使用。
https://github.com/ctripcorp/apollo

nacos: 作为一个服务注册中心本身就包含了配置中心的功能,没必要花很多时间再去部署一套apollo,完全可以满足很多中小型公司的配置中心的需求,哪怕是大公司也是可以用的,apollo确实用的公司很多,中大型公司都会去用apollo,而且他的功能也很完善。

spring cloud config: 如果用的不是spring cloud alibaba,用的是apring cloud netflix,可以配合那个技术栈,直接使用spring cloud提供的config项目作为配置中心就可以了,因为这是属于spring cloud原生技术栈里提供的。

1.查看防火墙服务状态
# systemctl status firewalld
2.查看防火墙状态
# firewall-cmd --state
3.开启、重启、关闭防火墙服务
# 开启
# service firewalld start 或 # systemctl start firewalld.service
# 重启
# service firewalld restart
# 关闭
# service firewalld stop 或 # systemctl stop firewalld.service
# 关闭防火墙开机启动
# systemctl disable firewalld.service
# 开启防火墙开机启动
# systemctl enable firewalld.service
4.查看防火墙规则
# firewall-cmd --list-all
5.查询、开放、关闭端口
# 查询端口是否开放
# firewall-cmd --query-port=8080/tcp
# 开放80端口
# firewall-cmd --permanent --add-port=80/tcp
# 移除端口
# firewall-cmd --permanent --remove-port=8080/tcp

#重启防火墙(修改配置后要重启防火墙)
# firewall-cmd --reload

一、安装步骤

1.从activemq官网下载activemq

http://activemq.apache.org/

2.将下载好的activemq的gz包上传到Linux服务器
3.解压activemq包
# tar zvxf apache-activemq-5.16.2-bin.tar.gz 
4.启动activemq
首先进入到mq的bin目录,然后通过./activemq start启动,以下表示启动成功!
# ./activemq start 

启动activemq

二、安装遇到的问题

1.查看activemq安装状态
# ./activemq status
2.查看activemq运行日志
# ./activemq consile

三、访问ActiveMQ页面

待ActiveMQ安装启动好,访问http://ip:8161/admin,登录名和密码都是admin(在配置文件中可修改),进入ActiveMQ的主页:
activemq web页面

页面访问不成功问题排查:
1.Linux服务器的8161端口是否开放,如果没有开放,就开放8161端口

# 查看端口是否开放
# firewall-cmd --query-port=8161/tcp  
# 开放端口
# firewall-cmd --permanent --add-port=8161/tcp
# 重启防火墙
# firewall-cmd --reload

2.查看日志,是否有存储大小设置过大,如果过大,修改配置文件

# vim ./conf/activemq.xml

activemq.xml配置文件修改

3.如果是用ip访问,而不是localhost或127.0.0.1访问,查看conf/jetty.xml配置文件,注释掉127.0.0.1这行

# vim ./conf/jetty.xml

jetty.xml配置文件修改

四、ActiveMQ页面介绍

1.Queue消息队列页面

jetty.xml配置文件修改

Name: 消息队列名称
Number Of Pending Messages: 未被消费的消息数目
Number Of Consumers: 消费者的数量
Message Enqueued: 进入队列的消息;进入队列的总消息数目,包括已经被消费的和未被消费的。这个数量只增不减。
Message Dequeued: 已出队列的消息,即已被消费掉的消息数目。因为一个消息只会被成功消费一次,所以在Queues里它和进入队列的总数量相等,如果不等是因为有消息未被消费。
2.Topic主题页面

jetty.xml配置文件修改

Name: 主题名称
Number Of Pending Messages: 未被消费的消息数目
Number Of Consumers: 消费者的数量
Message Enqueued: 进入队列的消息;进入队列的总消息数目,包括已经被消费的和未被消费的。这个数量只增不减。
Message Dequeued: 已出队列的消息,即已被消费掉的消息数目。在Topics里,因为多消费者从而导致数量会比入队列数高。
3.Subscribers查看订阅者页面

jetty.xml配置文件修改

查看订阅者信息,只在Topics消息类型中这个页面才会有数据。
4.Connections查看连接数页面

jetty.xml配置文件修改

五、ActiveMQ简单使用

引入jar包

<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-core</artifactId>
    <version>5.7.0</version>
</dependency>

1.点对点(P2P)模型
  点对点模型,采用的是队列(Queue)作为消息载体。在该模式中,一条消息只能被一个消费者消费,只能留在队列中,等待被消费,或者超时。例如:生产者生产了一个消息,只能由一个消费者进行消费,代码演示如下:

Provider步骤:
第一步:创建ConnectionFactory对象,需要指定服务端ip及端口号。
第二步:使用ConnectionFactory对象创建一个Connection对象。
第三步:开启连接,调用Connection对象的start方法。
第四步:使用Connection对象创建一个Session对象。
第五步:使用Session对象创建一个Destination对象(topic、queue),此处创建一个Queue对象。
第六步:使用Session对象创建一个Producer对象。
第七步:创建一个Message对象,创建一个TextMessage对象。
第八步:使用Producer对象发送消息。
第九步:关闭资源。

public class Provider {
    private static String queue = "demo";
    public static void main(String[] args) throws JMSException {
        //第一步:创建ConnectionFactory对象,需要指定服务端ip及端口号
        //brokerURL服务器的ip及端口号
        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.31.3:61616");
        //第二步:使用ConnectionFactory对象创建一个Connection对象。
        Connection connection = connectionFactory.createConnection();
        //第三步:开启连接,调用Connection对象的start方法。
        connection.start();
        //第四步:使用Connection对象创建一个Session对象。
        //第一个参数:是否开启事务。当为true时,开启事务,同时第二个参数可以忽略
        //第二个参数:消息的应答模式:1、自动应答;2、手动应答。当第一个参数为false时,才有意义。
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //第五步:使用Session对象创建一个Destination对象(topic、queue),此处创建一个Queue对象。
        //参数:队列名称
        Queue queue = session.createQueue(Provider.queue);
        //每六步:使用Session对象创建一个Producer对象。
        MessageProducer producer = session.createProducer(queue);
        for (int i = 0; i < 10; i++) {
            //第七步:创建一个Message对象,创建一个TextMessage对象。
            TextMessage textMessage = session.createTextMessage("消费者你好,我来了-" + i);
            //第八步:使用Producer对象发送消息。
            producer.send(textMessage);
        }
        System.out.println("----生产结束----");
        //第九步:关闭资源
        producer.close();
        session.close();
        connection.close();
    }
} 

Consumer步骤:
第一步:创建一个ConnectionFactory对象。
第二步:从ConnectionFactory对象中获得一个Connection对象。
第三步:开启连接。调用Connection对象的start方法。
第四步:使用Connection对象创建一个Session对象。
第五步:使用Session对象创建一个Destination对象。和发送端保持一致queue,并且队列的名称一致。
第六步:使用Session对象创建一个Consumer对象。
第七步:接收消息。
第八步:打印消息。
第九步:关闭资源

public class Comsoner {
    private static String queue = "demo";
    public static void main(String[] args) throws JMSException, IOException {
        System.out.println("----消费结束----");
        //第一步:创建一个ConnectionFactory对象
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.31.3:61616");
        //第二步:从ConnectionFactory对象中获得一个Connection对象。
        Connection connection = factory.createConnection();
        //第三步:开启连接。调用Connection对象的start方法。
        connection.start();
        //第四步:使用Connection对象创建一个Session对象。
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //第五步:使用Session对象创建一个Destination对象。和发送端保持一致queue,并且队列的名称一致。
        Queue queue = session.createQueue(Comsoner.queue);
        //第六步:使用Session对象创建一个Consumer对象。
        MessageConsumer consumer = session.createConsumer(queue);
        //第七步:接收消息
        consumer.setMessageListener(new MessageListener() {
            public void onMessage(Message message) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    String text = textMessage.getText();
                    System.out.println(text);
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        //第八步:关闭资源
        consumer.close();
        session.close();
        connection.close();
    }
}

2.发布/订阅(Pub/Sub)模型
  发布/订阅模型采用的是主题(Topic)作为消息通讯载体。该模式类似微信公众号的模式。发布者发布一条信息,然后将该信息传递给所有的订阅者。注意:订阅者想要接收到该消息,必须在该信息发布之前订阅。代码演示如下:

Provider步骤:
第一步:创建ConnectionFactory对象,需要指定服务端ip及端口号。
第二步:使用ConnectionFactory对象创建一个Connection对象。
第三步:开启连接,调用Connection对象的start方法。
第四步:使用Connection对象创建一个Session对象。
第五步:使用Session对象创建一个Destination对象(topic、queue),此处创建一个Queue对象。
第六步:使用Session对象创建一个Producer对象。
第七步:创建一个Message对象,创建一个TextMessage对象。
第八步:使用Producer对象发送消息。
第九步:关闭资源。

public class Comsoner {
    private static String queue = "topic";
    public static void main(String[] args) throws JMSException, IOException {
        //第一步:创建ConnectionFactory对象,需要指定服务端ip及端口号。
        //brokerURL服务器的ip及端口号
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.31.3:61616");
        //第二步:使用ConnectionFactory对象创建一个Connection对象。
        Connection connection = factory.createConnection();
        //第三步:开启连接,调用Connection对象的start方法。
        connection.start();
        //第四步:使用Connection对象创建一个Session对象。
        //第一个参数:是否开启事务。true:开启,开启时第二个参数可以忽略
        //第二个参数:当第一个参数为false时,这个参数才有意义。消息的应答模式:1-自动应答;2-手动应答
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //第五步:使用Session对象创建一个Destination对象。和发送端保持一致topic,并且话题和名称一致
        Topic topic = session.createTopic(queue);
        //第六步:使用Session对象创建一个Consumer对象
        MessageConsumer consumer = session.createConsumer(topic);
        //第七步:接收消息
        consumer.setMessageListener(new MessageListener() {
            public void onMessage(Message message) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    String text = textMessage.getText();
                    System.out.println(text);
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("topic的消费端。。。。");
        //第八步:关闭资源
        consumer.close();
        session.close();
        connection.close();
    }
}

Consumer步骤:
第一步:创建一个ConnectionFactory对象。
第二步:从ConnectionFactory对象中获得一个Connection对象。
第三步:开启连接。调用Connection对象的start方法。
第四步:使用Connection对象创建一个Session对象。
第五步:使用Session对象创建一个Destination对象。和发送端保持一致queue,并且队列的名称一致。
第六步:使用Session对象创建一个Consumer对象。
第七步:接收消息。
第八步:打印消息。
第九步:关闭资源

public class Comsoner {
    private static String queue = "demo";
    public static void main(String[] args) throws JMSException, IOException {
        System.out.println("----消费结束----");
        //第一步:创建一个ConnectionFactory对象
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.31.3:61616");
        //第二步:从ConnectionFactory对象中获得一个Connection对象。
        Connection connection = factory.createConnection();
        //第三步:开启连接。调用Connection对象的start方法。
        connection.start();
        //第四步:使用Connection对象创建一个Session对象。
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //第五步:使用Session对象创建一个Destination对象。和发送端保持一致queue,并且队列的名称一致。
        Queue queue = session.createQueue(Comsoner.queue);
        //第六步:使用Session对象创建一个Consumer对象。
        MessageConsumer consumer = session.createConsumer(queue);
        //第七步:接收消息
        consumer.setMessageListener(new MessageListener() {
            public void onMessage(Message message) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    String text = textMessage.getText();
                    System.out.println(text);
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        //第八步:关闭资源
        consumer.close();
        session.close();
        connection.close();
    }
}

3.两种模式对比
1)由以上,我们可以总结出ActiveMQ的实现步骤:

  • 建立ConnectionFactory工厂对象,需要填入用户名、密码、连接地址
  • 通过ConnectionFactory对象创建一个Connection连接
  • 通过Connection对象创建Session会话
  • 通过Session对象创建Destination对象;在P2P的模式中,Destination被称作队列(Queue),在Pub/Sub模式中,Destination被称作主题(Topic)
  • 通过Session对象创建消息的发送和接收对象
  • 发送消息
  • 关闭资源

2)可以看出,P2P模式和Pub/Sub模式,在实现上的区别是通过Session创建的Destination对象不一样,在P2P的模式中,Destination被称作队列(Queue),在Pub/Sub模式中,Destination被称作主题(Topic)

六、JMS消息可靠机制

ActiveMQ消息签收机制:客户端成功接收一条消息的标志是一条消息被签收,成功应答。

消息的签收情形分两种:
1、带事务的session。如果session带的事务,并且事务成功提交,则消息被自动签收。如果事务回滚,则消息会被再次传送。
2、不带事务的session。不带事务的session的签收方式,取决于session的配置。

ActiveMQ支持以下三种模式:
1、Session.AUTO_ACKNOWLEDGE 消息自动签收
2、Session.CLIENT_ACKNOWLEDGE客户端调用acknowledge方法手动签收。
  textMessage.acknowledge(); //手动签收
3、Session.DUPS_OK_ACKNOWLEDGE不是必须签收。
  消息可能会重复发送。在第二次重新传送消息的时候,消息只有在被确认之后,才认为已经被成功地消费了。消息的成功消费通常包含三个阶段:客户接收消息、客户处理消息和消息被确认。在事务性会话中,当一个事务被提交的时候,确认自动发生。在非事务性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgement mode)。该参数有以下三个可选值:
Number Of Consumers 消费者 这个 是消费者端的消费者数量
Numbwr Of Pending Messages等待消费的消息 这个是当前未出队列的数量。可以理解为总接收数-总出队列数
Messages Enqueued 进入队列的消息 进入队列的总数量,包括出队列的。这个数量只增不减
Messages Dequeued 出了队列的消息 可以理解为是消费者消费掉的数量。

七、JMS可靠消息机制-持久话机制

PERSISTENT:指示JMS provider持久保存消息,以保证消息不会因为JMS provider的失败而丢失
NON_PERSISTENT:不要求JMS provider持久保存消息
// 设置消息持久化 producer.setDeliveryMode(DeliveryMode.PERSISTENT);

ifconfig命令用来查看ip信息,Linux最小化安装是没有ifconfig的,以下演示如果安装ifconfig。

1.输入ifconfig提示不存在

-bash: ifconfig: command not found

2.先确认是否因为环境变量没有ifconfig导致的

环境变量
表示系统没有安装ifconfig

3.安装ifconfig
# yum install ifconfig  

安装ifconfig

4.提示没有ifconfig安装包。再使用yum search ifconfig来搜索下ifconfig的相关
# yum search ifconfig  

查看ifconfig相关

5.查看ifconfig匹配的是net-tools.x86_64包,安装net-tools.x86_64包
# yum install net-tools.x86_64 -y  

安装net-tools.x86_64

6.输入ifconfig查看效果
# ifconfig

查看ip

7.输入ip addr也可以查询
# ip addr

查看ip

1.hexo init

用于初始化本地文件夹为网站的根目录  
$ hexo init [folder]   
  folder 可选参数,用以指定初始化目录的路径,若无指定则默认为当前目录 

2.hexo new

命令用于新建文章,一般可以简写为 hexo n  
$ hexo new [layout] <title>  
  1.layout 可选参数,用以指定文章类型,若无指定则默认由配置文件中的 default_layout 选项决定
  2.title 必填参数,用以指定文章标题,如果参数值中含有空格,则需要使用双引号包围

3.hexo generate

命令用于生成静态文件,一般可以简写为 hexo g  
$ hexo generate  
  -d 选项,指定生成后部署,与 hexo d -g 等价

4.hexo server

命令用于启动本地服务器,一般可以简写为 hexo s  
$ hexo server  
  1.-p 选项,指定服务器端口,默认为 4000
  2.-i 选项,指定服务器 IP 地址,默认为 0.0.0.0
  3.-s 选项,静态模式 ,仅提供 public 文件夹中的文件并禁用文件监视  
  说明 :运行服务器前需要安装 hexo-server 插件 $ npm install hexo-server --save 
  详细信息请参考:[https://hexo.io/docs/server.html](https://hexo.io/docs/server.html)

5.hexo deploy

命令用于部署网站,一般可以简写为 hexo d  
$ hexo deploy  
  -g 选项,指定生成后部署,与 hexo g -d 等价

6.hexo clean

命令用于清理缓存文件,是一个比较常用的命令  
$ hexo clean  
  网站显示异常时可尝试此操作

seata

下载地址:http://seata.io/zh-cn/blog/download.html
在seata官网,下载对应的版本安装包,binary的seata-server-xx.zip,然后scp上传到机器上,unzip命令解压。

yum -y install unzip

seata server 要存储很多数据,两种模式,一个是file,一个是db,建议用file,因为db的性能有点差,默认就是file模式的:sh -seata-server.sh -p 8901 -h 192.168.31.155 -m file,如果要用db模式,参考官网,还得先建一大堆表

registry.conf:就是seata server注册到比如nacos去,作为一个服务,然后你各个服务就不用手动配置seata server地址了,直接发现就可以了,不过不用这个手动配置其实也行。

file.conf,是seata server的配置信息,他还可以支持集成nacos配置中心,把配置放到nacos里去,然后seata server自动加载配置,一般你不用也可以。

正常启动,会要求1GB内存,如果不够的话,可以修改启动文件里的内存分配(seata-sever.sh 找-XX:MaxDirectMemorySize)。

nohup sh seata-server.sh -p 8901 -h 192.168.31.155 -m file > /dev/null 2>&1 &