Java进击框架:Spring-综合(十)

前言

这一部分涵盖了Spring Framework与许多技术的集成。

Rest Clients

Spring框架为调用REST端点提供了以下选择:

  • RestClient-带有流畅API的同步客户端。
  • WebClient-采用流畅API的非阻塞、反应式客户端。
  • RestTemplate-带有模板方法API的同步客户端。
  • HTTP接口-带有生成的动态代理实现的注释接口。

RestClient是一个同步HTTP客户端,它提供了一个现代、流畅的API。它提供了对HTTP库的抽象,允许从Java对象到HTTP请求的方便转换,以及从HTTP响应创建对象。

  • 创建一个RestClient

RestClient是使用静态create()方法之一创建的。你也可以使用builder来获得一个带有更多选项的builder,比如指定要使用哪个HTTP库(参见客户端请求工厂)和要使用哪个消息转换器(参见HTTP消息转换),设置默认URI,默认路径变量,默认请求头或uriBuilderFactory,或者注册拦截器和初始化器。

一旦创建(或构建),RestClient就可以被多个线程安全地使用。

下面的示例展示了如何创建一个默认的RestClient,以及如何构建一个自定义的RestClient

RestClient defaultClient = RestClient.create();

RestClient customClient = RestClient.builder()
  .requestFactory(new HttpComponentsClientHttpRequestFactory())
  .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
  .baseUrl("https://example.com")
  .defaultUriVariables(Map.of("variable", "foo"))
  .defaultHeader("My-Header", "Foo")
  .requestInterceptor(myCustomInterceptor)
  .requestInitializer(myCustomInitializer)
  .build();
  • 使用RestClient

当使用RestClient,首先要指定使用哪种HTTP方法。这可以通过method(HttpMethod),或者用方便的方法get(), head(), post(),等等。

  • 请求URL

接下来,可以用uri方法。这一步是可选的,如果RestClient配置了默认的URIURL通常指定为String,带有可选的URI模板变量。默认情况下,字符串URL是编码的,但这可以通过使用自定义的uriBuilderFactory.

URL也可以提供有函数,或作为java.net.URI,两者都没有编码。

  • 请求标题和正文

如果需要,可以通过添加带有header(String, String), headers(Consumer<HttpHeaders>,或者用方便的方法accept(MediaType…​), acceptCharset(Charset…​)诸如此类。对于可以包含主体(POST, PUT,以及PATCH),还可以使用其他方法:contentType(MediaType),以及contentLength(long)

请求主体本身可以通过以下方式设置body(Object),内部使用HTTP消息转换。或者,可以使用ParameterizedTypeReference,允许您使用泛型。最后,可以将主体设置为一个回调函数,该函数写入OutputStream

  • 正在检索响应

一旦建立了请求,就可以通过调用retrieve()。可以使用以下方法访问响应正文body(Class),或者body(ParameterizedTypeReference)对于列表等参数化类型。这body方法将响应内容转换为各种类型,例如字节可以转换为StringJSON转换成对象,等等。

String result = restClient.get() //	设置获取请求
  .uri("https://example.com") //指定要连接的URL
  .retrieve() //检索响应
  .body(String.class); //将响应转换为字符串

System.out.println(result); //打印结果

通过ResponseEntity提供对响应状态码和报头的访问:

ResponseEntity<String> result = restClient.get() 
  .uri("https://example.com") 
  .retrieve()
  .toEntity(String.class); 

System.out.println("Response status: " + result.getStatusCode()); 
System.out.println("Response headers: " + result.getHeaders()); 
System.out.println("Contents: " + result.getBody()); 

RestClient可以使用Jackson库将JSON转换为对象。注意这个示例中uri变量的使用,并且Accept头被设置为JSON

int id = ...;
Pet pet = restClient.get()
  .uri("https://petclinic.example.com/pets/{id}", id) //使用URI变量
  .accept(APPLICATION_JSON) //将Accept报头设置为application/json
  .retrieve()
  .body(Pet.class); //将JSON响应转换为Pet域对象

在下一个示例中,RestClient用于执行包含JSONPOST请求,JSON同样使用Jackson进行转换。

Pet pet = ... 
ResponseEntity<Void> response = restClient.post() 
  .uri("https://petclinic.example.com/pets/new") 
  .contentType(APPLICATION_JSON) //将Content-Type头设置为application/json
  .body(pet) //使用pet作为请求体
  .retrieve()
  .toBodilessEntity(); //将响应转换为没有主体的响应实体。
  • 错误处理

默认情况下,当检索带有4xx5xx状态码的响应时,RestClient抛出RestClientException的子类。此行为可以使用onStatus覆盖。

String result = restClient.get() 
  .uri("https://example.com/this-url-does-not-exist") 
  .retrieve()
  .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { //为所有4xx状态码设置状态处理程序
      throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) //抛出自定义异常
  })
  .body(String.class);
  • 交换

对于更高级的场景,RestClient通过交换方法提供对底层HTTP请求和响应的访问,可以使用交换方法代替retrieve()。交换时不应用状态处理程序,因为交换函数已经提供了对完整响应的访问,允许您执行任何必要的错误处理。

Pet result = restClient.get()
  .uri("https://petclinic.example.com/pets/{id}", id)
  .accept(APPLICATION_JSON)
  .exchange((request, response) -> { //Exchange提供请求和响应
    if (response.getStatusCode().is4xxClientError()) { //当响应的状态码为4xx时抛出异常
      throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); 
    }
    else {
      Pet pet = convertResponse(response); //将响应转换为Pet域对象
      return pet;
    }
  });

WebClient

WebClient是执行HTTP请求的非阻塞、反应式客户端。它是在5.0中引入的,为RestTemplate,支持同步、异步和流式场景。

public class MyRestClient {

    public static void main(String[] args) {
        // 创建一个默认的 WebClient 实例
        WebClient defaultClient = WebClient.create();

        // 发起 GET 请求,并接收响应内容
        defaultClient.get()
                .uri("https://example.com/api/resource")
                .retrieve()
                .bodyToMono(String.class)
                .subscribe(responseBody -> {
                    // 处理响应内容
                    System.out.println("Response Body: " + responseBody);
                });

        // 发起 POST 请求,并发送请求体
        defaultClient.post()
                .uri("https://example.com/api/resource")
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .bodyValue("{ \"key\": \"value\" }")
                .retrieve()
                .toBodilessEntity()
                .subscribe(response -> {
                    // 处理响应
                    System.out.println("Response Code: " + response.getStatusCode());
                    System.out.println("Response Headers: " + response.getHeaders());
                });
    }
}

在这个示例中,我们创建了一个默认的 WebClient 实例 defaultClient,然后使用它来发起 GETPOST 请求。

对于 GET 请求,我们使用 get() 方法指定了请求的 URI,并使用 retrieve() 方法来获取响应。通过 bodyToMono() 方法,我们将响应体转换为 String 类型,并通过 subscribe() 方法订阅响应内容,并在回调中处理。

对于 POST 请求,我们使用 post() 方法指定了请求的 URI,并使用 header() 方法设置请求头,使用 bodyValue() 方法设置请求体。通过 retrieve() 方法获取响应,并使用 toBodilessEntity() 方法将响应转换为无内容的 ResponseEntity。最后,我们通过 subscribe() 方法订阅响应,并在回调中处理。

RestTemplate

RestTemplate以经典Spring Template类的形式在HTTP客户端库上提供了一个高级API

public class MyRestClient {

    public static void main(String[] args) {
        // 创建一个默认的 RestTemplate 实例
        RestTemplate defaultClient = new RestTemplate();

        // 发起 GET 请求,并接收响应内容
        ResponseEntity<String> getResponse = defaultClient.exchange(
                "https://example.com/api/resource",
                HttpMethod.GET,
                null,
                String.class
        );
        String getResponseBody = getResponse.getBody();
        System.out.println("GET Response Body: " + getResponseBody);
    }
}

对于 GET 请求,我们使用 exchange() 方法指定了请求的 URIHTTP 方法和请求体。我们将响应转换为 String 类型,并通过 getBody() 方法获取响应体。

RestClient为同步HTTP访问提供了一个更现代的API。对于异步和流场景,考虑响应式WebClient

它公开了以下几组重载方法:

(1)getForObject()方法,用于发送 HTTP GET 请求并获取响应结果。

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

其中,参数含义如下:

  • url:需要发送 GET 请求的 URL 地址;
  • responseType:期望得到的 HTTP 响应结果的 Java 类型,可以是基本类型、JavaBean 类型、集合类型等;
  • uriVariables:可选的 URL 路径参数。
public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        User user = restTemplate.getForObject("https://api.example.com/user/{id}", User.class, 12345);
    }
}

你也可以使用getForEntity()方法,用于发送 HTTP GET 请求并获取完整的响应实体,包括响应状态、头部信息和响应体。

public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<User> response = restTemplate.getForEntity("https://api.example.com/user/{id}", User.class, 12345);
        User user = response.getBody();
    }
}

(2)headForHeaders()方法,用于发送 HTTP HEAD 请求并获取响应的头部信息。

public HttpHeaders headForHeaders(String url, Object... uriVariables) throws RestClientException;

其中,参数含义如下:

  • url:需要发送 HEAD 请求的 URL 地址;
  • uriVariables:可选的 URL 路径参数。
public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = restTemplate.headForHeaders("https://api.example.com/user/{id}", 12345);
        MediaType contentType = headers.getContentType();
    }
}

(3)postForLocation()方法,用于发送 HTTP POST 请求,并返回响应中的 Location 头部信息。

public URI postForLocation(String url, @Nullable Object request, Object... uriVariables) throws RestClientException;

其中,参数含义如下:

  • url:需要发送 POST 请求的 URL 地址;
  • request:可选的请求体,可以是任意类型的对象;
  • uriVariables:可选的 URL 路径参数。
public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        URI location = restTemplate.postForLocation("https://api.example.com/user", requestBody);
        String path = location.getPath();
        String host = location.getHost();
    }
}

你也可以使用postForObject()方法,发送 POST 请求并返回响应体的 Java 对象表示形式。

public class Test {
    public static void main(String[] args) {
        // 创建一个默认的 RestTemplate 实例
        RestTemplate defaultClient = new RestTemplate();
        // 发起 POST 请求,并发送请求体
        HttpHeaders postHeaders = new HttpHeaders();
        postHeaders.setContentType(MediaType.APPLICATION_JSON);
        String requestBody = "{ \"key\": \"value\" }";
        String postResponse = defaultClient.postForObject("https://example.com/api/resource", requestBody, String.class);
        System.out.println("POST Response Body: " + postResponse);
    }
}

还有postForEntity()方法,发送 HTTP POST 请求,并返回完整的响应信息。

public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<MyResponse> responseEntity = restTemplate.postForEntity("https://api.example.com/user", requestBody, MyResponse.class);
        MyResponse response = responseEntity.getBody();
    }
}

通过调用 getBody() 方法,我们可以获取到响应体的内容。

(3)put()方法,用于向指定的 URL 地址更新资源,发送 PUT 请求。

public void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException;

其中,参数含义如下:

  • url:需要发送 PUT 请求的 URL 地址;
  • request:可选的请求体,可以是任意类型的对象;
  • uriVariables:可选的 URL 路径参数。
public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.put("https://api.example.com/user/{id}", requestBody, 123);
    }
}

(4)patchForObject()方法,可以发送 PATCH 请求并返回响应体的 Java 对象表示形式。

public class Test {
    public static void main(String[] args) {
        // 创建一个默认的 RestTemplate 实例
        RestTemplate defaultClient = new RestTemplate();

        // 发起 PATCH 请求,并发送请求体
        HttpHeaders patchHeaders = new HttpHeaders();
        patchHeaders.setContentType(MediaType.APPLICATION_JSON);
        String requestBody = "{ \"key\": \"value\" }";
        String patchResponse = defaultClient.patchForObject(
                "https://example.com/api/resource",
                requestBody,
                String.class
        );
        System.out.println("PATCH Response Body: " + patchResponse);
    }
}

(5)delete()方法,用于发送 DELETE 请求。

public class Test {
    public static void main(String[] args) {
        // 创建一个默认的 RestTemplate 实例
        RestTemplate defaultClient = new RestTemplate();

        // 发起 DELETE 请求
        defaultClient.delete("https://example.com/api/resource/{id}",1);
    }
}

参数可选。

(6)optionsForAllow()方法,用于发送 HTTP OPTIONS 请求,并返回允许的 HTTP 方法。

public class Test {
    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();
        Set<HttpMethod> allowedMethods = restTemplate.optionsForAllow("https://api.example.com/user");
    }
}

参数可选。

(7)exchange()方法,用于发送 HTTP 请求并返回 ResponseEntity 对象,该对象包含了 HTTP 响应的状态码、响应头和响应体等信息。

public class Test {
    public static void main(String[] args) {
        // 创建一个默认的 RestTemplate 实例
        RestTemplate defaultClient = new RestTemplate();

        // 发起 GET 请求,并获取响应体和响应头
        HttpHeaders getHeaders = new HttpHeaders();
        getHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        HttpEntity<String> getRequestEntity = new HttpEntity<>(getHeaders);
        ResponseEntity<String> getResponse = defaultClient.exchange(
                "https://example.com/api/resource",
                HttpMethod.GET,
                getRequestEntity,
                String.class
        );
        System.out.println("GET Response Status Code: " + getResponse.getStatusCode());
        System.out.println("GET Response Headers: " + getResponse.getHeaders());
        System.out.println("GET Response Body: " + getResponse.getBody());
    }
}

我们使用 exchange() 方法指定了请求的 URIHTTP 方法、请求实体和响应体类型。由于我们希望获取响应体的字符串表示形式,因此将响应体类型设置为 String 类型。

(8)execute()方法,它允许你执行任意类型的 HTTP 请求并使用自定义的请求执行器来处理请求和响应。

public class Test {
    public static void main(String[] args) {
        // 创建一个默认的 RestTemplate 实例
        RestTemplate defaultClient = new RestTemplate();

        // 发起任意类型的 HTTP 请求,并获取响应体
        Object response = defaultClient.execute(
                "https://example.com/api/resource",
                HttpMethod.GET,
                null,
                clientHttpResponse -> {
                    // 在这里可以对 HTTP 响应进行自定义处理
                    // 例如,可以读取响应体、状态码和响应头等信息
                    return "Custom response handling";
                }
        );
    }
}

HTTP接口

Spring框架允许您使用@HttpExchange方法将HTTP服务定义为Java接口。你可以将这样的接口传递给HttpServiceProxyFactory来创建一个代理,该代理通过HTTP客户端(如RestClientWebClient)执行请求。您还可以从@Controller实现用于服务器请求处理的接口。

首先用@HttpExchange方法创建接口:

interface RepositoryService {

	@GetExchange("/repos/{owner}/{repo}")
	Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	// more HTTP exchange methods...

}

现在,您可以创建一个代理,在调用方法时执行请求。

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

RepositoryService service = factory.createClient(RepositoryService.class);

@HttpExchange在类型级别被支持,它适用于所有方法:

@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
interface RepositoryService {

	@GetExchange
	Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	@PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
	void updateRepository(@PathVariable String owner, @PathVariable String repo,
			@RequestParam String name, @RequestParam String description, @RequestParam String homepage);

}

JMS (Java消息服务)

Spring提供了一个JMS集成框架,它简化了JMS API的使用,就像Spring集成JDBC API一样。

JMS可以大致分为两个功能领域,即消息的产生和消费。b类用于消息生成和同步消息接收。对于类似于Jakarta EE的消息驱动bean风格的异步接收,Spring提供了许多可用于创建消息驱动pojo的消息侦听器容器。Spring还提供了一种声明式的方式来创建消息侦听器。

Spring Framework 5开始,SpringJMS包完全支持JMS 2.0,并且需要在运行时提供JMS 2.0 API。我们建议使用与JMS 2.0兼容的提供程序。

使用Spring JMS

JmsTemplate类是JMS核心包中的核心类。它简化了JMS的使用,因为它在发送或同步接收消息时处理资源的创建和释放。

使用JmsTemplate的代码只需要实现为其提供明确定义的高级契约的回调接口。当给出由JmsTemplate中的调用代码提供的Session时,MessageCreator回调接口将创建一条消息。为了允许更复杂的JMS API使用,SessionCallback提供了JMS会话,而ProducerCallback公开了sessionMessageProducer对。

JMS API公开了两种类型的发送方法,一种采用交付模式、优先级和生存时间作为服务质量(QOS)参数,另一种不采用QOS参数并使用默认值。

为了方便起见,JmsTemplate还公开了一个基本的请求-应答操作,该操作允许发送消息并在作为该操作的一部分创建的临时队列上等待应答。

JmsTemplate类的实例在配置后是线程安全的。这很重要,因为这意味着您可以配置JmsTemplate的单个实例,然后安全地将这个共享引用注入多个协作者。需要明确的是,JmsTemplate是有状态的,因为它维护一个对ConnectionFactory的引用,但是这个状态不是会话状态。
Spring Framework 4.1开始,JmsMessagingTemplate构建在JmsTemplate之上,并提供了与消息抽象(即org.springframework.messaging.Message)的集成。这允许您以通用方式创建要发送的消息。

依赖文件

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
            <version>2.4.0</version>
        </dependency>

发送消息

JmsTemplate包含许多方便的方法来发送消息。Send方法通过使用jakarta.jms.Destination对象指定目的地,其他方法通过在JNDI查找中使用String指定目的地。不接受destination参数的send()方法使用默认目的地。

下面的例子使用MessageCreator回调从提供的Session对象创建文本消息:

public class JmsQueueSender {
    private JmsTemplate jmsTemplate;
    private Queue queue;

    public void setConnectionFactory(ConnectionFactory cf) {
        this.jmsTemplate = new JmsTemplate(cf);
    }

    public void setQueue(Queue queue) {
        this.queue = queue;
    }

    public void simpleSend() {
        this.jmsTemplate.send(this.queue, new MessageCreator() {
            public Message createMessage(Session session) throws JMSException {
                return session.createTextMessage("hello queue world");
            }
        });
    }
}

上述代码JmsTemplate是通过传递对ConnectionFactory的引用来构造的。send(String destinationName, MessageCreator creator)方法允许您使用目的地的字符串名称发送消息。如果您创建了JmsTemplate并指定了默认目的地,则send(MessageCreator c)将向该目的地发送消息。

  • 使用消息转换器

为了方便域模型对象的发送,JmsTemplate具有各种发送方法,这些方法将Java对象作为消息数据内容的参数。JmsTemplate中的重载方法convertAndSend()receiveAndConvert()将转换过程委托给MessageConverter接口的实例。通过使用转换器,您和您的应用程序代码可以将重点放在通过JMS发送或接收的业务对象上,而不必关心如何将其表示为JMS消息的细节。

    public void sendWithConversion() {
        Map map = new HashMap();
        map.put("Name", "Mark");
        map.put("Age", new Integer(47));
        //jmsTemplate.convertAndSend(destination, message);
        jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
            public Message postProcessMessage(Message message) throws JMSException {
                message.setIntProperty("AccountID", 1234);
                message.setJMSCorrelationID("123-00001");
                return message;
            }
        });
    }

我们首先创建一个包含键值对的 Map 对象,表示需要发送的消息体。然后,我们使用 jmsTemplate.convertAndSend() 方法将该消息体转换为 JMS 消息并发送到名为 "testQueue" 的队列。

此外,我们还通过实现 MessagePostProcessor 接口来对消息进行进一步处理。在 postProcessMessage() 方法中,我们设置了消息的整型属性 “AccountID” 和 JMS 关联 ID "123-00001"。这些操作可根据实际需求进行定制,以便在发送消息之前对消息进行必要的修改。

这将产生以下形式的消息:

MapMessage={
Header={
… standard headers …
CorrelationID={123-00001}
}
Properties={
AccountID={Integer:1234}
}
Fields={
Name={String:Mark}
Age={Integer:47}
}
}

接收消息

以类似于EJB世界中的消息驱动Bean (MDB)的方式,消息驱动POJO (MDP)充当JMS消息的接收者。下面的例子展示了一个MDP的简单实现:

@Component
public class MyMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            try {
                String text = ((TextMessage) message).getText();
                // 在这里处理接收到的消息
                System.out.println("接收到消息:" + text);
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

一旦您实现了MessageListener,是时候创建消息侦听器容器了。Spring JMS 提供了两种类型的消息侦听器容器:

  1. DefaultMessageListenerContainer:适用于大多数情况,提供了丰富的配置选项,并支持动态调整并发消费者数量。

  2. SimpleMessageListenerContainer:适用于简单的使用场景,提供了较少的配置选项,但性能较高。

以下是一个使用 DefaultMessageListenerContainer 的示例:

    @Bean
    @Bean
    public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory, MyMessageListener listener) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);

        SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
        endpoint.setId("myEndpoint");
        endpoint.setMessageListener(listener);
        factory.createListenerContainer(endpoint);
        return factory;
    }

Spring还通过使用@JmsListener注释,并提供开放式基础结构以编程方式注册端点。到目前为止,这是设置异步接收器最方便的方式。

@Component
public class MyMessageListener {

    @JmsListener(destination = "myQueue")
    public void onMessage(String message) {
        // 在这里处理接收到的消息
        System.out.println("接收到消息:" + message);
    }
}
  • 同步接收

虽然JMS通常与异步处理相关联,但您可以同步地使用消息。重载的receive(..)方法提供了这个功能。在同步接收期间,调用线程阻塞,直到消息可用为止。这可能是一个危险的操作,因为调用线程可能被无限期地阻塞。receiveTimeout属性指定接收者在放弃等待消息之前应该等待多长时间。

public class JmsExample {

    private final JmsTemplate jmsTemplate;

    public JmsExample(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public Message receiveMessage() throws JMSException {
    	jmsTemplate.setReceiveTimeout(5000);
        return jmsTemplate.receive("testQueue");
    }
}
  • 使用SessionAwareMessageListener连接

SessionAwareMessageListener 接口定义如下:

public interface SessionAwareMessageListener<T extends Message> extends MessageListener {
    void onMessage(T message, Session session) throws JMSException;
}

使用 SessionAwareMessageListener 可以更方便地进行与 JMS 会话相关的操作,例如确认消息、创建和关闭临时队列等。

@Component
public class MyMessageListener implements SessionAwareMessageListener<Message> {
    @Override
    public void onMessage(Message message, Session session) throws JMSException {
        // 处理消息
        try {
            // 确认消息
            message.acknowledge();
            // 创建临时队列
            javax.jms.Queue tempQueue = session.createTemporaryQueue();
            // ... 执行其他与 JMS 会话相关的操作 ...
        } catch (JMSException e) {
            // 处理异常
        }
    }
}
  • 使用MessageListenerAdapter

MessageListenerAdapterSpring 提供的一个适配器,用于将普通的 Java 类转换为消息监听器。它可以帮助你将现有的方法转换为消息处理方法,从而更容易地将消息监听器集成到 Spring 应用程序中。

public class MyMessageListenerAdapter extends MessageListenerAdapter {  
    @Override  
    public void onMessage(Message message) {  
        // 处理接收到的消息  
        // ...  
    }  
}
  • 处理事务中的消息

在事务中调用消息侦听器只需要重新配置侦听器容器。您可以通过侦听器容器定义上的sessionTransacted标志激活本地资源事务。然后,每个消息侦听器调用在活动JMS事务中操作,在侦听器执行失败的情况下回滚消息接收。

	@Bean
    public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory, MyMessageListener listener) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setSessionTransacted(true);
        return factory;
    }

要参与外部管理的事务,您需要配置事务管理器并使用支持外部管理事务的侦听器容器(通常是DefaultMessageListenerContainer)。

下面的bean定义创建一个事务管理器:

    @Bean
    public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory, PlatformTransactionManager transactionManager) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setTransactionManager(transactionManager);
        return factory;
    }

完整示例代码:

@Configuration
public class JmsConfig {

    // 设置连接工厂
    @Bean
    public ConnectionFactory connectionFactory() {
        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
        connectionFactory.setBrokerURL("tcp://localhost:61616");
        // 设置其他连接工厂属性
        return connectionFactory;
    }

    // 设置消息队列名称
    @Bean
    public Queue queue() {
        return new ActiveMQQueue("myQueue");
    }
    // 创建一个消息监听器容器并关联消息监听器
    @Bean
    public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory, MyMessageListener listener) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageListener(listener);
        return factory;
    }
}
@Component
public class MyMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            try {
                String text = ((TextMessage) message).getText();
                // 在这里处理接收到的消息
                System.out.println("接收到消息:" + text);
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}
@Component
public class MessageSender {

    @Autowired
    private JmsTemplate jmsTemplate;

    @Autowired
    private Queue queue;

    public void sendMessage(String message) {
        jmsTemplate.convertAndSend(queue, message);
        System.out.println("消息发送成功:" + message);
    }
}

注释驱动的侦听器端点

异步接收消息的最简单方法是使用带注释的侦听器端点基础结构。简而言之,它允许您将受管bean的方法公开为JMS侦听器端点。以下示例显示了如何使用它:

@Component
public class MyService {

	@JmsListener(destination = "myDestination")
	public void processOrder(String data) { ... }
}
  • 启用侦听器端点注释

您可以添加@EnableJms给你的一个@Configuration类,如下例所示:

@Configuration
@EnableJms
public class AppConfig {

	@Bean
	public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory());
		factory.setDestinationResolver(destinationResolver());
		factory.setSessionTransacted(true);
		factory.setConcurrency("3-10");
		return factory;
	}
}

默认情况下,基础结构会查找名为jmsListenerContainerFactory作为工厂用来创建消息侦听器容器的源。在这种情况下(忽略JMS基础设施设置),您可以调用processOrder方法,核心轮询大小为三个线程,最大池大小为十个线程。

  • 编程端点注册

JmsListenerEndpoint提供JMS端点的模型,并负责为该模型配置容器。除了由检测到的端点之外,基础结构还允许您以编程方式配置端点JmsListener注释。以下示例显示了如何实现这一点:

@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {

	@Override
	public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
		SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
		endpoint.setId("myJmsEndpoint");
		endpoint.setDestination("anotherQueue");
		endpoint.setMessageListener(message -> {
			// processing
		});
		registrar.registerEndpoint(endpoint);
	}
}

注意,您可以完全跳过@JmsListener的使用,只通过JmsListenerConfigurer以编程方式注册端点。

  • 带注释的端点方法签名

到目前为止,我们已经注射了一种简单的String但是它实际上可以有一个非常灵活的方法签名。在下面的示例中,我们将其重写为注入Order使用自定义页眉:

@Component
public class MyService {

	@JmsListener(destination = "myDestination")
	public void processOrder(Order order, @Header("order_type") String orderType) {
		...
	}
}

@Header-带注释的方法参数,用于提取特定的头值,包括标准JMS头。

  • 响应管理

MessageListenerAdapter中的现有支持已经允许您的方法具有非空返回类型。在这种情况下,调用的结果将封装在jakarta.jms中。消息,在原始消息的JMSReplyTo头中指定的目的地中发送,或者在侦听器上配置的默认目的地中发送。现在可以通过使用消息传递抽象的@SendTo注释来设置默认目的地。

假设我们的processOrder()方法现在应该返回一个OrderStatus,我们可以编写它来自动发送响应,如下面的示例所示:

@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
	// order processing
	return status;
}

如果有几个带有@JmsListener注释的方法,还可以在类级别放置@SendTo注释,以共享默认应答目的地。

如果需要以独立于传输的方式设置附加标头,可以返回Message相反,使用类似下面的方法:

@JmsListener(destination = "myDestination")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
	// order processing
	return MessageBuilder
			.withPayload(status)
			.setHeader("code", 1234)
			.build();
}

如果需要在运行时计算响应目的地,可以将响应封装在JmsResponse实例,该实例还提供运行时使用的目标。我们可以将前面的示例改写如下:

@JmsListener(destination = "myDestination")
public JmsResponse<Message<OrderStatus>> processOrder(Order order) {
	// order processing
	Message<OrderStatus> response = MessageBuilder
			.withPayload(status)
			.setHeader("code", 1234)
			.build();
	return JmsResponse.forQueue(response, "status");
}

最后,如果您需要为响应指定一些QoS值,例如优先级或生存时间,您可以相应地配置JmsListenerContainerFactory,如下面的示例所示:

@Configuration
@EnableJms
public class AppConfig {

	@Bean
	public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory());
		QosSettings replyQosSettings = new QosSettings();
		replyQosSettings.setPriority(2);
		replyQosSettings.setTimeToLive(10000);
		factory.setReplyQosSettings(replyQosSettings);
		return factory;
	}
}

JMX

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

Spring Framework 中,你可以使用 JMXJava Management Extensions)来监控和管理应用程序的运行时状态。Spring 提供了一些集成 JMX 的功能和类,以便你能够方便地将应用程序的各个组件暴露为可管理的 JMX MBean

依赖文件

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jmx</artifactId>
    <version>2.0.8</version>
</dependency>

首先,在应用程序的配置文件中启用 SpringJMX 支持。你可以在 XML 配置文件中添加以下代码:

@Configuration
@EnableMBeanExport(defaultDomain = "com.example")
public class AppConfig {
    // ...
}

在需要被管理的组件上添加 @ManagedResource 注解。例如,假设你有一个名为 "JmxTestBean" 的类,你可以在类上添加 @ManagedResource 注解,并指定该 MBean 的名称和描述:

@ManagedResource(objectName = "com.example:name=JmxTestBean", description = "JmxTestBean")
public class JmxTestBean {
    private String name;
    private int age;
    private boolean isSuperman;

    //getter setter
}

如果你想暴露某个方法作为 MBean 的操作,可以在该方法上添加 @ManagedOperation 注解。例如,假设你想暴露 UserService 类的一个方法作为 MBean 的操作:

@ManagedOperation(description = "Get user by ID")
public User getUserById(long id) {
    // ...
}

启动应用程序后,你可以使用 JMX 客户端工具(如 JConsoleVisualVM)连接到应用程序,并查看和管理暴露的 MBean。在 JConsole 中,你可以选择 "MBeans" 选项卡,然后浏览已注册的 MBean,查看属性、操作和通知。

Email

Spring Framework为发送电子邮件提供了一个有用的实用程序库,它使您不必了解底层邮件系统的细节,并负责代表客户端进行低级资源处理。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <version>2.4.0</version>
        </dependency>

首先找到对应邮箱,启用客户端POP3/SMTP服务(这里以QQ邮箱为例)。

在这里插入图片描述
然后使用授权码当作密码

在这里插入图片描述

Spring Framework 提供了 JavaMailSenderSimpleMailMessage 来简化邮件发送。

示例代码如下:

@Configuration
public class AppConfig {
    @Bean
    public JavaMailSenderImpl mailSender() {
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        //    qq邮箱为smtp.qq.com          端口号465或587
        //    # sina    smtp.sina.cn
        //    # aliyun  smtp.aliyun.com
        //    # 163     smtp.163.com       端口号465或994
        mailSender.setHost("smtp.qq.com");
        mailSender.setPort(587);
        mailSender.setUsername("18****14@qq.com");
        mailSender.setPassword("xxyisglypqmtbigj");//授权码

        Properties javaMailProperties = new Properties();
        javaMailProperties.put("mail.smtp.auth", "true");
        javaMailProperties.put("mail.smtp.starttls.enable", "true");

        mailSender.setJavaMailProperties(javaMailProperties);

        return mailSender;
    }
}
@Component
public class MailSenderExample {
    @Autowired
    private JavaMailSenderImpl mailSender;
    public void sendEmail() {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setSubject("Test email");
        message.setText("This is a test email from Spring Framework.");
        message.setFrom("185****14@qq.com");
        message.setTo("144****41@qq.com");

        mailSender.send(message);
    }
}
@Controller
public class TestController {
    @Autowired
    private MailSenderExample mailSenderExample;

    @GetMapping("/sendEmail")
    @ResponseBody
    public void sendEmail(){
        try {

            mailSenderExample.sendEmail();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

最终展示

在这里插入图片描述
还可以使用JavaMailMimeMessageHelper(并添加附件)

@Component
public class MailSenderExample {
    @Autowired
    private JavaMailSenderImpl mailSender;

    public void setMailSender(){
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = null;
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setTo("14****41@qq.com");
            helper.setSubject("Test email");
            helper.setText("This is a test email from Spring Framework.");
            helper.setFrom("18****14@qq.com");
            // 添加附件
            File file = new File("D:\\untitled.iml");
            helper.addAttachment(file.getName(), file);

            mailSender.send(message);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }

    }
}
  • 内嵌资源

下面的例子向你展示了如何使用MimeMessageHelper发送带有内联图像的电子邮件:

@Component
public class MailSenderExample {
    @Autowired
    private JavaMailSenderImpl mailSender;

    public void setMailSender(){
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = null;
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setTo("1441681841@qq.com");
            helper.setSubject("Test email");
            helper.setFrom("185356214@qq.com");
            //使用true标志表示包含的文本是HTML
            helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);
            File file = new File("D:\\untitled.png");
            helper.addInline("identifier1234", file);
            mailSender.send(message);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}

在典型的企业应用程序中,开发人员通常不会使用前面介绍的方法创建电子邮件的内容,原因有很多:

  1. Java代码创建基于HTML的电子邮件内容既繁琐又容易出错。

  2. 显示逻辑和业务逻辑之间没有明确的分离。

  3. 更改电子邮件内容的显示结构需要编写Java代码、重新编译、重新部署等等。

通常,解决这些问题的方法是使用模板库(如FreeMarker)来定义电子邮件内容的显示结构。这使得您的代码只负责创建要在电子邮件模板中呈现的数据并发送电子邮件。

任务执行和调度

Spring框架为任务的异步执行和调度提供了抽象TaskExecutorTaskScheduler接口。Spring还提供了那些接口的实现,这些接口在应用服务器环境中支持线程池或CommonJ委托。最终,在公共接口背后使用这些实现消除了Java SEJakarta EE环境之间的差异。

Spring TaskExecutor 抽象

Executors 是线程池概念的JDK名称。之所以使用“执行器”命名,是因为无法保证底层实现实际上是一个池。执行器可以是单线程的,甚至可以是同步的。Spring的抽象隐藏了Java SEJakarta EE环境之间的实现细节。

SpringTaskExecutor接口与java.util.concurrent.Executor接口相同。事实上,最初,它存在的主要原因是在使用线程池时抽象出对Java 5的需求。接口有一个方法(execute(Runnable task)),该方法根据线程池的语义和配置接受要执行的任务。

TaskExecutor最初是为了给其他Spring组件提供一个抽象的线程池而创建的。ApplicationEventMulticasterJMSAbstractMessageListenerContainerQuartz集成等组件都使用TaskExecutor抽象来池化线程。但是,如果您的bean需要线程池行为,您也可以根据自己的需要使用此抽象。

public class MyTaskExecutor {
    public static void main(String[] args) {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.initialize();
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            taskExecutor.execute(() -> {
                System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
                // 执行异步任务
            });
        }
    }
}

Spring包含了许多TaskExecutor的预构建实现。在所有可能的情况下,您都不需要实现自己的。Spring提供的变量如下:

(1)SyncTaskExecutor:它会在当前线程中同步执行任务,而不是使用线程池等机制异步执行任务。它主要用于不需要多线程的情况,例如简单的测试用例。

(2)SimpleAsyncTaskExecutor:会为每个任务创建一个新的线程来执行。但是,它支持并发限制,该限制会阻止任何超出限制的调用,直到某个插槽被释放。如果您正在寻找真正的池化,请参见ThreadPoolTaskExecutor

(3)ConcurrentTaskExecutor:这个实现是java.util.concurrent.Executor实例的适配器。还有一种替代方法(ThreadPoolTaskExecutor),它将Executor配置参数公开为bean属性。很少需要直接使用ConcurrentTaskExecutor。然而,如果ThreadPoolTaskExecutor不够灵活,ConcurrentTaskExecutor是一个替代方案。

(4)ThreadPoolTaskExecutor:这种实现是最常用的。它公开了用于配置java.util.concurrent.ThreadPoolExecutor并用一个TaskExecutor。如果你需要适应一种不同的java.util.concurrent.Executor,我们建议您使用ConcurrentTaskExecutor

(5)DefaultManagedTaskExecutor:此实现在JSR-236兼容的运行时环境(如Jakarta EE应用服务器)中使用jndi获得的ManagedExecutorService,取代CommonJ WorkManager

示例代码如下:

@Configuration
public class TaskConfig {
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        return executor;
    }
}
@Component
public class MyTaskExecutor {
    @Autowired
    private TaskExecutor taskExecutor;
    public void printMessages() {
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            taskExecutor.execute(() -> {
                System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
                // 执行异步任务
            });
        }
    }
}

Spring TaskScheduler 抽象

除了TaskExecutor抽象之外,Spring还有一个TaskScheduler SPI,它提供了各种方法来调度任务,以便在将来某个时候运行。下面的清单显示了TaskScheduler接口定义:

在这里插入图片描述
最简单的方法是名为schedule的方法,它只接受一个Runnable和一个Instant。这会导致任务在指定时间之后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但是接受Trigger的方法要灵活得多。

  • schedule(Runnable task, Trigger trigger):它可以让你在指定的时间点或时间间隔内执行一个任务。其中,task 参数是要执行的任务,必须实现 Runnable 接口;trigger 参数是触发器,用于指定任务的执行时间和执行频率等信息。

示例代码如下:

@Configuration
public class TaskConfig {
    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        // 设置线程池大小
        scheduler.setPoolSize(10);
        // 设置线程名前缀
        scheduler.setThreadNamePrefix("my-task-");
        // 设置是否等待所有任务完成后才销毁线程池
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        // 设置线程池关闭时的等待时间(单位为秒)
        scheduler.setAwaitTerminationSeconds(60);
        // 设置线程池的拒绝策略(这里使用默认的AbortPolicy)
        // scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        return scheduler;
    }
}
@Component
public class MyTaskExecutor {
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;
    public void printMessages() {
        taskScheduler.schedule(()->{
            System.out.println("threadName:"+Thread.currentThread().getName());
        },new CronTrigger("0 0/5 * * * ?"));
    }
}

Cron表达式可以自行了解(生成表达式),需要注意的是,Trigger 接口有多个实现类,例如 CronTriggerPeriodicTrigger 等,可以根据不同的需求选择不同的实现类。

  • schedule(Runnable task, Instant startTime):用于在指定的时间开始执行任务。

示例代码如下:

@Component
public class MyTaskExecutor {
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;
    public void printMessages() {
        // 在当前时间的 5 秒后执行任务
        Instant startTime = Instant.now().plusSeconds(5);
        taskScheduler.schedule(()->{
            System.out.println("threadName:"+Thread.currentThread().getName());
        },startTime);
    }
}

我们创建了一个 Runnable 的任务,并将其传递给 scheduleTask() 方法。我们还定义了一个 startTime 变量,指定任务应该在当前时间的 5 秒后开始执行。

从6.1开始,ThreadPoolTaskScheduler通过Spring的生命周期管理提供了暂停/恢复功能和优雅的关闭功能。还有一个名为SimpleAsyncTaskScheduler的新选项,它与JDK 21的虚拟线程保持一致,使用单个调度器线程,但为每个计划任务执行启动一个新线程(除了固定延迟的任务,它们都在单个调度器线程上操作,所以对于这个虚拟线程对齐的选项,建议使用固定速率和cron触发器)。

支持调度和异步执行的注释

  • 启用计划注释

要启用对@Scheduled注释的支持,你可以在你的@Configuration类中添加@EnableScheduling注释,如下面的例子所示:

@Configuration
@EnableScheduling
public class AppConfig {
}
  • @Scheduled注释

您可以添加@Scheduled方法的注释以及触发器元数据。例如,下面的方法每五秒钟(5000毫秒)调用一次,延迟时间是固定的,这意味着该时间段从每次调用的完成时间开始计算。

@Component
public class MyTaskExecutor {
    @Scheduled(fixedDelay = 5000)
    public void doSomething(){
        System.out.println("执行中");
    }

}

默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。如果您想使用不同的时间单位,如秒或分钟,您可以通过timeUnit属性在@Scheduled

例如,前面的示例也可以写成如下形式。

@Component
public class MyTaskExecutor {
    @Scheduled(fixedDelay = 5000,timeUnit = TimeUnit.SECONDS)
    public void doSomething(){
        System.out.println("执行中");
    }
}

对于固定延迟和固定速率的任务,您可以通过指示方法第一次执行之前等待的时间量来指定初始延迟,如下面的fixedRate示例所示:

@Component
public class MyTaskExecutor {
	@Scheduled(initialDelay = 1000, fixedRate = 5000)
	public void doSomething() {
		// something that should run periodically
	}
}

如果简单的周期性调度不够表达,可以提供cron表达式。以下示例仅在工作日运行:

@Component
public class MyTaskExecutor {
	@Scheduled(cron="*/5 * * * * MON-FRI")
	public void doSomething() {
		// something that should run on weekdays only
	}
}

请注意,要调度的方法必须具有void返回,并且不能接受任何参数。如果该方法需要与应用程序上下文中的其他对象进行交互,通常会通过依赖注入来提供。
如果在同一个方法上发现了几个预定的声明,则每个声明都将被独立处理,并为每个声明触发一个单独的触发器。因此,这种共处一地的时间表可能重叠并并行或紧接着执行多次。请确保您指定的cron表达式等不会意外重叠。

  • @Async注释

要启用对@Async注释的支持,你可以在你的@Configuration类中添加@EnableAsync注释,如下面的例子所示:

@Configuration
@EnableAsync
public class AppConfig {
}

您可以在方法上提供@Async注释,以便异步调用该方法。换句话说,调用者在调用后立即返回,而方法的实际执行发生在已提交给Spring TaskExecutor的任务中。在最简单的情况下,你可以将注释应用于返回void的方法,如下例所示:

@Controller
public class TaskController {
    @Autowired
    private MyTaskExecutor myTaskExecutor;

    @GetMapping("task")
    @ResponseBody
    public void task(){
        try {
            myTaskExecutor.doSomething();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
@Component
public class MyTaskExecutor {
    @Async
    public void doSomething() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("执行中");
    }

}

与使用@Scheduled注释注释的方法不同,这些方法可以期望参数。

task命名空间

从3.0版本开始,Spring包含了一个用于配置的XML名称空间TaskExecutorTaskScheduler实例。它还提供了一种便捷的方式来配置要用触发器调度的任务。

  • scheduler元素

下面的元素用指定的线程池大小创建一个ThreadPoolTaskScheduler实例:

<task:scheduler id="scheduler" pool-size="10"/>

id属性提供的值用作池中线程名称的前缀。scheduler元素相对简单。如果不提供pool-size属性,默认线程池只有一个线程。调度程序没有其他配置选项。

  • executor元素

下面创建了一个ThreadPoolTaskExecutor实例:

<task:executor id="executor" pool-size="10"/>

与前一节所示的调度器一样,首先,ThreadPoolTaskExecutor的线程池本身更易于配置。

<task:executor
		id="executorWithCallerRunsPolicy"
		pool-size="5-25"
		queue-capacity="100"
		rejection-policy="CALLER_RUNS"
        keep-alive="120"/>

上述代码中,pool-size属性也接受min-max形式的范围。下面的示例设置最小值为5,最大值为25;queue-capacity属性指定任务队列的容量。当线程池中的线程都在执行任务时,新的任务将会被放入队列等待执行;rejection-policy属性 是一种拒绝策略,它表示拒绝的任务将由调用者所在的线程来执行;keep-alive属性指定线程的空闲时间。如果线程池中的线程在空闲时间超过这个设定值时,将会被回收。

除此之外你也可是使用Quartz调度程序。

缓存抽象

从3.1版本开始,Spring框架支持透明地向现有的Spring应用程序添加缓存。类似于交易支持,缓存抽象允许在对代码影响最小的情况下一致使用各种缓存解决方案。

在其核心,缓存抽象将缓存应用于Java方法,从而减少了基于缓存中可用信息的执行次数。也就是说,每次调用目标方法时,抽象都会应用一个缓存行为,检查该方法是否已针对给定参数被调用。如果它已经被调用,则返回缓存的结果,而不必调用实际的方法。如果尚未调用该方法,则调用该方法,并将结果缓存并返回给用户,以便下次调用该方法时,返回缓存的结果。

这种方法只适用于无论调用多少次都保证给定输入(或参数)返回相同输出(结果)的方法。

基于声明性注释的缓存

@Cacheable 注解是 Spring 框架提供的一个用于缓存方法返回结果的注解。当使用 @Cacheable 注解时,Spring 会自动检查缓存中是否已经存在该方法的结果,如果存在,则直接从缓存中获取结果而不会执行方法体。

  • 启用缓存注释

你需要确保已经配置了缓存支持

@Configuration
@EnableCaching
public class CacheConfig {
}

使用 @Cacheable() 注解标记方法。

@Controller
public class CacheController {
    @Autowired
    private CacheService cacheService;

    @GetMapping("/cache")
    @ResponseBody
    public void cache(){
        try {
            String data = cacheService.getData();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
@Service
public class CacheService {
    @Cacheable("myCache")
    public String myCache() throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}

你可以使用key 属性用于指定缓存中存储数据时的键值。

@Service
public class CacheService {
    @Cacheable(cacheNames = "myCache",key = "#name")
    public String myCache(String name) throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}
  • 默认缓存解决

缓存抽象使用一个简单的CacheResolver,它通过使用配置的CacheManager检索在操作层定义的缓存。

要提供不同的默认缓存解析器,需要实现org.springframework.cache.interceptor.CacheResolver接口。

  • 自定义缓存解决

默认缓存分辨率非常适合使用单个CacheManager并且没有复杂缓存分辨率需求的应用程序。

对于使用多个缓存管理器的应用程序,您可以设置每个操作使用的缓存管理器,如下面的示例所示:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager anotherCacheManager() {
        return new ConcurrentMapCacheManager("books");
    }
}
@Service
public class CacheService {
    @Cacheable(cacheNames = "myCache",cacheManager = "anotherCacheManager")
    public String getData() throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}

您还可以以类似于替换密钥生成的方式完全替换CacheResolver。每个缓存操作都请求解析,让实现根据运行时参数实际解析要使用的缓存。下面的示例展示了如何指定CacheResolver

public class RuntimeCacheResolver implements CacheResolver {

    private CacheManager cacheManager;

    public RuntimeCacheResolver(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Override
    public Collection<Cache> resolveCaches(Caching caching, Class<?> type) {
        // 在这里可以根据需求动态获取或创建缓存实例
        // 这里我们只简单地返回默认的缓存实例
        return cacheManager.getCacheNames().stream()
                .map(cacheManager::getCache)
                .collect(Collectors.toList());
    }
}
@Service
public class CacheService {

    @Cacheable(cacheNames="myCache", cacheResolver="runtimeCacheResolver")
    public String myCache(String isbn) {
        // 执行一些耗时操作或业务逻辑
        // ...
        return "bookDetails";
    }
}
  • 同步缓存

在多线程环境中,可能会为同一个参数同时调用某些操作(通常在启动时)。默认情况下,缓存抽象不锁定任何东西,并且相同的值可能会被计算多次,这违背了缓存的目的。

对于这些特殊情况,您可以使用sync属性指示基础缓存提供程序在计算值时锁定缓存项。因此,只有一个线程忙于计算值,而其他线程被阻塞,直到缓存中的条目被更新。下面的示例演示如何使用sync属性:

@Service
public class CacheService {
    @Cacheable(cacheNames = "myCache",sync = true)
    public String getData() throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}
  • 条件缓存

有时,一个方法可能不适合一直缓存(例如,它可能依赖于给定的参数)。缓存注释通过condition参数,该参数采用SpEL表达式,其计算结果为true或者false。如果true,则缓存该方法。如果没有,它的行为就好像该方法没有被缓存一样(也就是说,无论缓存中有什么值或使用什么参数,每次都调用该方法)。例如,只有当参数name长度短于5:

@Service
public class CacheService {
    @Cacheable(cacheNames = "myCache",condition = "#name.length() < 5")
    public String getData(String name) throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}

除了condition参数之外,还可以使用unless参数否决向缓存添加值。与condition不同,unless调用方法后计算表达式。例如,只有当请求参数小于5,返回的数据长度大于4,才会缓存(即 为 false):

@Service
public class CacheService {
    @Cacheable(cacheNames = "myCache",condition = "#name.length() < 5",unless = "#result.length()<4")
    public String getData(String name) throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}

如果一个可选值不存在,null将存储在关联的缓存中。#result总是指向业务实体,而不是一个受支持的包装器,所以前面的例子可以重写如下:

@Cacheable(cacheNames = "myCache",condition = "#name.length() < 5",unless = "#result?.length()<4")

注意,#result仍然指向返回参数,而不是可选的Optional<response>。因为它可能是空的。

除此之外,Spring的缓存抽象还提供了一组Java注释:

  • @CachePut注释:用于将方法的返回值放入缓存中。它可以用于更新缓存中的数据或者在缓存中添加新的数据。
@Service
public class CacheService {
    @CachePut(value = "usersCache",key = "#name")
    public String getData(String name) throws InterruptedException {
        // 执行一些耗时操作或业务逻辑
        Thread.sleep(5000);
        // ...
        return "data";
    }
}
  • @CacheEvict注释:用于从缓存中删除指定的数据。

通过 key 属性指定要清除的缓存键

@Service
public class CacheService {
    @CacheEvict(value = "usersCache", key = "#userId")
    public void deleteUser(String userId) {
        // 执行删除用户的逻辑
    }
}

通过 allEntries 属性设置为 true,可以清除指定缓存中的所有数据。

@Service
public class CacheService {
    @CacheEvict(value = "usersCache", allEntries = true)
    public void deleteUser(String userId) {
        // 执行删除用户的逻辑
    }
}

@CacheEvict 注解默认在方法执行之后清除缓存。如果希望在方法执行之前清除缓存,可以使用 beforeInvocation 属性,将其设置为 true。例如:

@Service
public class CacheService {
    @CacheEvict(value = "usersCache",beforeInvocation = true)
    public void deleteUser(String userId) {
        // 执行删除用户的逻辑
    }
}
  • @Caching注释:用于组合多个缓存注解,可以同时应用于同一个方法上。
@Caching(
    cacheable = {
        @Cacheable(value = "usersCache", key = "#userId")
    },
    put = {
        @CachePut(value = "usersCache", key = "#result.id"),
        @CachePut(value = "usersCache", key = "#result.username")
    },
    evict = {
        @CacheEvict(value = "allUsersCache", allEntries = true)
    }
)
public User getUserById(String userId) {
    // 执行查询用户的逻辑,并返回 User 对象
}
  • @CacheConfig注释:一个类级别的注解,用于为该类中的所有缓存注解提供默认的缓存名称和配置。
@CacheConfig(cacheNames = "usersCache", keyGenerator = "customKeyGenerator")
public class UserService {
    
    @Cacheable(key = "#userId")
    public User getUserById(String userId) {
        // 执行查询用户的逻辑,并返回 User 对象
    }
    
    @CachePut(key = "#user.id")
    public void updateUser(User user) {
        // 执行更新用户的逻辑
    }
    
    @CacheEvict(allEntries = true)
    public void clearCache() {
        // 执行清除缓存的逻辑
    }
}

在上述示例中,@CacheConfig 注解为 UserService 类提供了默认的缓存名称和键生成器。

从4.1版本开始,Spring的缓存抽象完全支持JCache标准(JSR-107)注释。

SpringJSR-107
@Cacheable@CacheResult
@CachePut@CachePut
@CacheEvict@CacheRemove
@CacheEvict(allEntries=true)@CacheRemoveAll
@CacheConfig@CacheDefaults

JVM检查点恢复

Spring框架集成了由Project CRaC实现的检查点/恢复,以便允许实现能够减少基于SpringJava应用程序在JVM上启动和预热时间的系统。

使用此功能需要:

  • 启用了检查点/恢复的JVM(目前仅支持Linux)。
  • 组织的存在。类路径中的crack库(支持1.4.0及以上版本)。
  • 指定所需的java命令行参数,如-XX:CRaCCheckpointTo=PATH-XX:CRaCRestoreFrom=PATH

当请求检查点时,在-XX:CRaCCheckpointTo= path指定的路径中生成的文件包含正在运行的JVM内存的表示,其中可能包含秘密和其他敏感数据。在使用此特性时,应该假设JVM“看到”的任何值(例如来自环境的配置属性)都将存储在这些CRaC文件中。因此,应该仔细评估在何处以及如何生成、存储和访问这些文件的安全含义。

从概念上讲,检查点和恢复与单个bean的Spring生命周期契约保持一致。

正在运行的应用程序的按需检查点/恢复

可以按需创建检查点,例如使用以下命令jcmd application.jar JDK.checkpoint。在创建检查点之前,Spring会停止所有正在运行的beans,让它们有机会通过实现Lifecycle.stop。还原后,相同的beans将重新启动,并使用Lifecycle.start允许beans在相关时重新打开资源。对于不依赖于Spring的库,可以通过实现org.crac.Resource并注册相关实例。

启动时自动检查点/恢复

-Dspring.context.checkpoint=onRefresh设置了JVM系统属性,则在LifecycleProcessor.onRefresh阶段。这个阶段完成后,所有非惰性初始化的单例都已实例化,并且InitializingBean#afterPropertiesSet回调已被调用;但是生命周期还没有开始ContextRefreshedEvent尚未出版。

出于测试目的,还可以利用-Dspring.context.exit=onRefresh触发类似行为的JVM系统属性,但它不是创建检查点,而是在相同的生命周期阶段退出Spring应用程序,而不需要项目CraC依赖/JVMLinux。这有助于检查在beans未启动时是否需要连接到远程服务,并有可能改进配置以避免这种情况。

类数据共享

类数据共享是一种JVM功能这有助于减少Java应用程序的启动时间和内存占用。

要使用这个特性,应该为应用程序的特定类路径创建一个CDS归档。Spring框架提供了一个挂钩点来简化归档的创建。一旦存档可用,用户应该通过JVM标志选择使用它。

创建CDS档案

当应用程序退出时,可以创建应用程序的CDS档案。Spring框架提供了一种操作模式,一旦ApplicationContext已刷新。在这种模式下,所有非惰性初始化的单例都已被实例化,并且InitializingBean#afterPropertiesSet回调已被调用;但是生命周期还没有开始ContextRefreshedEvent尚未出版。

要创建归档文件,必须指定两个额外的JVM标志:

  • -XX:ArchiveClassesAtExit=application.jsa:退出时创建CDS存档

  • -Dspring.context.exit=onRefresh:启动然后立即退出Spring应用程序,如上所述。

创建CD归档,您的JDK必须有一个基本映像。如果您将上面的标志添加到启动脚本中,您可能会收到类似以下内容的警告:

-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info.

可以通过发出以下命令创建基本CDS归档文件:

$ java -Xshare:dump

使用存档

归档文件可用后,添加-XX:SharedArchiveFile=application.jsa以使用它,假设application.jsa工作目录中的文件。

为了了解缓存的有效性,您可以通过添加一个额外的属性来启用类加载日志:-Xlog:class+load:file=cds.log。这将创建一个cds.log每次尝试加载类及其源代码时。从缓存中加载的类应该有一个“共享对象文件”源,如下例所示:
在这里插入图片描述

XML模式

附录的这一部分列出了与集成技术相关的XML模式。

  • jee (计划或理论的)纲要

使用中的元素jee模式,您需要在Spring XML配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的架构,因此jee命名空间可供您使用:

<?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:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="
		http://www.springframework.org/schema/jee
		https://www.springframework.org/schema/jee/spring-jee.xsd">

	<!-- bean definitions here -->

</beans>
  • jms (计划或理论的)纲要

使用jms模式,您需要在Spring XML配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的架构,因此jms命名空间可供您使用:

<?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:jms="http://www.springframework.org/schema/jms"
	xsi:schemaLocation="
		http://www.springframework.org/schema/jms
		https://www.springframework.org/schema/jms/spring-jms.xsd">

	<!-- bean definitions here -->

</beans>
  • cache (计划或理论的)纲要

使用中的元素cache模式,您需要在Spring XML配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的架构,因此cache命名空间可供您使用:

<?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:cache="http://www.springframework.org/schema/cache"
	xsi:schemaLocation="
		http://www.springframework.org/schema/cache
		https://www.springframework.org/schema/cache/spring-cache.xsd">

	<!-- bean definitions here -->

</beans>
  • 27
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值