Spring Learning

Overview of Spring-boot

pom.xml

  • <parent>...</parent>: Specify the parent POM (Project Object Model) for the project.

  • <groupId>...</groupId>: Group identifier.

  • <artifactId>...</artifactId>: Artifact identifier.

  • <version>...</version>: Version number.

  • <scope>...</scope>: specify the scope of a dependency.

    • <scope>compile</scope> (default): The dependency is available during compilation, testing and runtime.

    • <scope>provided</scope>: The dependency is availabel during compilation and testing.

    • <scope>runtime</scope>: The dependency is availabel during runtime and testing.

    • <scope>test</scope>: The dependency is only availabel at testing.

Spring boot version is included in the POM of <parent>...</parent>.

Java version is included in <properties>...</properties>.

Spring boot plugin is included in <build><plugins>...</plugins></build>.

Boot-strap class (Or main class)

@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + ComponentScan

Run Spring-boot project

  1. Make sure maven uses the correct Java version.
./mvnw -v
  1. Package the whole project into .jar file and run it.
./mvnw package
java -jar target/the_packaged_file_name.jar
  1. Directly run it.
./mvnw spring-boot:run

Spring MVC HTTP

注解 经典用途
@GetMapping 读取资源数据
@PostMapping 创建资源
@PutMapping 更新资源
@PatchMapping 更新资源
@DeleteMapping 删除资源
@RequestMapping 通用请求处理

Spring JDBC

添加依赖

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency> 

src/main/resources/application.properties中添加MySQL数据库连接配置:

spring.datasource.url=jdbc:mysql://<公网IP>:3306/AutoVisualDB
spring.datasource.username=zt
spring.datasource.password=12345
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

创建User实体和存储库接口

这里以创建User实体为例,然后User的持久化接口为UserRepository

User.java

import java.util.Arrays;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.annotation.Id;
import lombok.Data;

@Data
@Table("User")  // 指定映射到MySQL中的表名为User
public class User implements UserDetails {

    private static final long serialVersionUID = 1L;

    @Id
    private Long id;

    private String username;
    private String password;
    private String fullname;
    private String street;
    private String city;
    private String state;
    private String zip;
    private String phone;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
      return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public boolean isAccountNonExpired() {
      return true;
    }

    @Override
    public boolean isAccountNonLocked() {
      return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
      return true;
    }

    @Override
    public boolean isEnabled() {
      return true;
    }

    public User(String username, String password, String fullname, String street, String city, String state, String zip, String phone) {
        this.username = username;
        this.password = password;
        this.fullname = fullname;
        this.street = street;
        this.city = city;
        this.state = state;
        this.zip = zip;
        this.phone = phone;
    }
}

UserRepository.java

public interface UserRepository extends CrudRepository<User, Long> {

  User findByUsername(String username);
  
}

MySQL建表代码

CREATE TABLE User (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    fullname VARCHAR(255) NOT NULL,
    street VARCHAR(255) NOT NULL,
    city VARCHAR(255) NOT NULL,
    state VARCHAR(255) NOT NULL,
    zip VARCHAR(255) NOT NULL,
    phone VARCHAR(255) NOT NULL
);

创建MyQuery实体和存储库接口

MyQuery.java

import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.annotation.Id;
import lombok.Data;

@Data
@Table("MyQuery")
public class MyQuery {

    @Id
    private Long id;

    private String question;
    @Column("predictedSQL")
    private String predictedSQL;
    @Column("visPattern")
    private String visPattern;
    @Column("dbPath")
    private String dbPath;
    @Column("dbName")
    private String dbName;

    public MyQuery() {}
    public MyQuery(String question, String predictedSQL, String visPattern, String dbPath, String dbName) {
        this.question = question;
        this.predictedSQL = predictedSQL;
        this.visPattern = visPattern;
        this.dbPath = dbPath;
        this.dbName = dbName;
    }
}

MyQueryRepository.java

import org.springframework.data.repository.CrudRepository;
import datainsights.data.MyQuery;

public interface MyQueryRepository extends CrudRepository<MyQuery, Long> {

}

MySQL建表代码

CREATE TABLE MyQuery (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    question VARCHAR(255),
    predictedSQL VARCHAR(255),
    visPattern VARCHAR(255),
    dbPath VARCHAR(255),
    dbName VARCHAR(255)
);

Spring Security

添加依赖并配置数据库连接

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

上述依赖一旦添加,基本的安全配置会被自动初始化,所有的HTTP请求路径都需要认证。

Spring Security默认开启CSRF保护(Cross-Site Request Forgery),它要求所有状态变更的HTTP方法(如POST、PUT、DELETE等)都必须验证CSRF令牌。所以想直接通过curl发送资源更改的请求是会被Forbidden的。CSRF令牌在httpSession会话创立后、在GET请求时服务器下发,存储在Cookie中。所以我认为只要启用了Spring Security以及不禁用CSRF的前提下,不执行登录则没有会话,继而也不会有服务器下发的CSRF令牌,故而不可能通过curl直接对服务器资源进行修改。

SecurityConfig

下述代码的身份验证方案是基于会话的(Session-based):在登录成功后,服务器会创建一个Session,然后将JSESSIONIDCookie返回给客户端,在后续的请求过程中客户端会携带此Cookie让服务端验证。

下述代码的.formLogin()表单登录是Session-based方案的典型特征。

框架代码

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
public class SecurityConfig {
   
    @Bean
    public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
    }
    
    @Bean
    public UserDetailsService userDetailsService(CustomUserDetailsService customUserDetailsService) {
        return customUserDetailsService;
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  
      return http
        .authorizeRequests()
          .mvcMatchers("/autovisual").hasRole("USER")  // 访问/autovisual路径时,只有身份为ROLE_USER的用户才能访问
          .anyRequest().permitAll()  // 其他路径均可以自由访问,无需身份验证

        .and()
          .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/autovisual", true)  // 登录成功后强制导航到/autovisual路径  

        .and()
        .build();
    }
}

上述三个@Bean在注册后都是在Spring Security框架中自动注入的。

需要自定义实现UserDetailsService接口

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

简洁实现代码

@Configuration
public class SecurityConfig {
   
    @Bean
    public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
    }
    
    @Bean
    public UserDetailsService userDetailsService(UserRepository userRepo) {
        return username -> {
          User user = userRepo.findByUsername(username);
          if (user != null) {
              return user;
          }
          throw new UsernameNotFoundException("User '" + username + "' not found");
        };
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  
      return http
        .authorizeRequests()
          .mvcMatchers("/autovisual").hasRole("USER")  // 访问/autovisual路径时,只有身份为ROLE_USER的用户才能访问
          .anyRequest().permitAll()  // 其他路径均可以自由访问,无需身份验证

        .and()
          .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/autovisual", true)  // 登录成功后强制导航到/autovisual路径  

        .and()
        .build();
    }
}

这里使用lambda表达式简洁实现函数式接口UserDetailsService

Registration

Registration Controller

import datainsights.data.UserRepository;

@Controller
@RequestMapping("/register")
public class RegistrationController {
  
    private UserRepository userRepo;
    private PasswordEncoder passwordEncoder;
  
    public RegistrationController(
          UserRepository userRepo, PasswordEncoder passwordEncoder) {
        this.userRepo = userRepo;
        this.passwordEncoder = passwordEncoder;
    }
    
    @GetMapping
    public String registerForm() {
        return "registration";
    }
    
    @PostMapping
    public String processRegistration(RegistrationForm form) {
        userRepo.save(form.toUser(passwordEncoder));
        return "redirect:/login";
    }
}

注册表单视图

存放于路径./src/main/resources/templates/registration.html

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" xmlns:th = "http://www.thymeleaf.org">
    <head>
        <title>User Rehistration</title>
    </head>

    <body>
        <h1>Register</h1>
        <img th:src = "@{/images/LoginImage.png}"/>
        <form method = "POST" th:action = "@{/register}" id = "registerForm">

            <label for = "username">Username: </label>
            <input type = "text" name = "username"/><br/>

            <label for = "password">Passward: </label>
            <input type = "password" name = "password"/><br/>

            <label for = "confirm">Confirm Password: </label>
            <input type = "password" name = "confirm"/><br/>

            <label for = "fullname">Full name: </label>
            <input type = "text" name = "fullname"/><br/>

            <label for = "street">Street: </label>
            <input type = "text" name = "street"/><br/>

            <label for = "city">City: </label>
            <input type = "text" name = "city"/><br/>

            <label for = "state">State: </label>
            <input type = "text" name = "state"/><br/>

            <label for = "zip">Zip: </label>
            <input type = "text" name = "zip"/><br/>

            <label for = "phone">Phone: </label>
            <input type = "text" name = "phone"/><br/>

            <input type = "submit" value = "Register">
        </form>
    </body>
</html>

点击视图中的Register按钮,这应该会触发RegisterController.java中的POST,然后将表单中的数据绑定到形参form中。

Registration Form

import datainsights.User;
import lombok.Data;

@Data
public class RegistrationForm {

    private String username;
    private String password;
    private String fullname;
    private String street;
    private String city;
    private String state;
    private String zip;
    private String phone;
  
    public User toUser(PasswordEncoder passwordEncoder) {
        return new User(
                username, passwordEncoder.encode(password),
                fullname, street, city, state, zip, phone);
    }
}

这是用于接收表单数据的类,form的类型。

常用身份校验方案

  • Session-based:上述代码正是基于Session-based进行身份校验的。

  • Token-based:客户端在登录服务端之后会获得一个Token令牌,在之后的请求中携带这个Token令牌。三方登录的身份验证也是使用的这种方式,由三方授权服务器向客户端返回Token令牌。

Spring RESTful

Rest Controller

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseStatus;

import tacos.Taco;
import tacos.data.TacoRepository;

@RestController
@RequestMapping(path = "/api/tacos", produces = "application/json")
@CrossOrigin(origins = {"http://tacocloud:8080", "http://tacocloud.com"})
public class TacoController {
    private TacoRepository tacoRepository;

    public TacoController(TacoRepository tacoRepository) {
        this.tacoRepository = tacoRepository;
    }

    @GetMapping(params  = "recent")
    public Iterable<Taco> recentTacos() {
        PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
        return tacoRepository.findAll(page).getContent();
    }

    @PostMapping(consumes = "application/json")
    @ResponseStatus(HttpStatus.CREATED)
    public Taco postTaco(@RequestBody Taco taco) {
        return tacoRepository.save(taco);
    }
    
    @PutMapping(path = "/{orderId}", consumes = "application/json")
    public TacoOrder putOrder(@PathVariable("orderId") Long orderId, @RequestBody TacoOrder order) {
      order.setId(orderId);
      return tacoRepository.save(order);
    }

    @PatchMapping(path = "/{order}", consumes = "application/json")
    public TacoOrder patchOrder(@PathVariable("orderId") Long orderId, @RequestBody TacoOrder patch) {
      TacoOrder order = tacoRepository.findById(orderId).get();
      if (patch.getDeliveryName() != null) {
        order.setDeliveryName(patch.getDeliveryName());
      }
    }

    @DeleteMapping("/{orderId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteOrder(@PathVariable("orderId") Long orderId) {
        try {
            tacoRepository.deleteById(orderId);
        } catch (EmptyResultDataAccessException e) {}
    }
}

@RestController注解可能相当于@Controller + @ResponseBody的作用。它除了有Controller的作用之外,还要求其类中所定义的方法都要把返回值写入响应体。

@RequestMapping(path = "/api/tacos", produces = "application/json")中的path="/api/tacos"定义了客户端为访问类中的方法所必须的请求头;produces="application/json"限定了类中方法的返回响应体应该是json格式。

@CrossOrigin

public Iterable recentTacos()部分

从服务器端检索数据

当我使用curl localhost:8080/api/tacos?recent时,会得到返回的json格式内容。

public Taco postTaco(@RequestBody Taco taco)部分

发送数据到服务器端

postTaco()方法会处理对/api/tacos的POST请求。consumes = "application/json"意味着请求的输入需要为json格式文件。

@RequestBody Taco taco请求体中的json被绑定到参数taco上。

如果我的taco实体由Taco.javaIngredient.java定义。则相应的json格式文件应为:

{
  "name": "Super Veggie Taco",
  "ingredients": [
    {
      "id": "1",
      "name": "Flour Tortilla",
      "type": "WRAP"
    },
    {
      "id": "2",
      "name": "Grilled Peppers",
      "type": "VEGGIES"
    }
  ]
}

通过命令行发送该POST请求(目前还未验证过下述代码的可行性):

curl -X POST http://localhost:8080/api/tacos \
    -H "Content-Type: application/json" \
    -d '{"name": "Super Veggie Taco", "ingredients": [{"id": "1", "name": "Flour Tortilla", "type": "WRAP"}, {"id": "2", "name": "Grilled Peppers", type": "VEGGIES"}]}'

@ResponseStatus(HttpStatus.CREATED)将请求成功且成功创建了一个资源的HTTP状态201传递给客户端。

public TacoOrder putOrder(@PathVariable(“orderId”) Long orderId, @RequestBody TacoOrder order) 部分

public TacoOrder patchOrder(@PathVariable(“orderId”) Long orderId, @RequestBody TacoOrder patch) 部分

public void deleteOrder(@PathVariable(“orderId”) Long orderId)部分

从服务器端删除数据

@ResponseStatus(HttpStatus.NO_CONTENT)请求成功且没有资源内容的HTTP状态码为204。

Rest Data

GET存储库

Rest Data是用来暴露存储库API的。要完成API的暴露,需要设置以下两步。

  1. 在pom.xml中添加依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
  1. 在目标存储库上添加注解:(例如)
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(path = "myqueries")
public interface MyQueryRepository extends PagingAndSortingRepository<MyQuery, Long> {
    
}

之后就可以通过命令行curl "localhost:8080/myqueries"返回存储库内容了。

调整上述API:在src\main\resources\application.properties中添加下述代码后即可通过新的APIcurl "localhost:8080/repo-api/myqueries"访问存储库内容。

spring.data.rest.base-path=/repo-api

POST新数据

(下面的过程还没有验证过,可能因为我的项目Spring Security默认启用了CSRF保护)

在目标位置创建一个mydata.json文件:

{
  "question": "How many girls are there",
  "predictedSQL": "SELECT COUNT(sex) WHERE sex='f';",
  "visPattern": "a2",
  "dbPath": "b2",
  "dbName": "c2"
}

然后使用下述命令行POST数据:

curl -X POST -H "Content-Type: application/json" -d @mydata.json "http://localhost:8080/repo-api/myqueries"

保护Restful API

JMS异步消息

引入依赖

Apache ActiveMQ Artemis 是 Apache ActiveMQ 的更新版。在添加下述依赖后,Spring-boot会自动创建一个JmsTemplate的Bean。

Artemis

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

ActiveMQ

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

配置代理

代理的配置与引入的依赖相对应。

Artemis 代理

在默认情况下,Spring会假定Artemis代理在localhost的61616端口运行。

spring:
  artemis:
    host: artemis.tacocloud.com
    port: 61616
    user: tacoweb
    password: l3tm31n
  jms:
    template:
      default-destination: tacocloud.order.queue

ActiveMQ 代理

spring:
  activemq:
    broker-url: tcp://activemq.tacocloud.com
    user: tacoweb
    password: l3tm31n
    in-memory: false
  jms:
    template:
      default-destination: tacocloud.order.queue

使用JMS发送消息

tacos.messaging

JmsOrderMessagingService.java

package tacos.messaging;

import javax.jms.JMSException;
import javax.jms.Message;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;

import tacos.TacoOrder;

@Service
public class JmsOrderMessagingService implements OrderMessagingService {

    private JmsTemplate jms;

    @Autowired
        public JmsOrderMessagingService(JmsTemplate jms) {
        this.jms = jms;
    }

    @Override
    public void sendOrder(TacoOrder order) {
        jms.convertAndSend("tacocloud.order.queue", order,
            this::addOrderSource);
    }

    private Message addOrderSource(Message message) throws JMSException {
        message.setStringProperty("X_ORDER_SOURCE", "WEB");
        return message;
    }

}

@Service是一个标识服务的注解,表示这个是Spring管理的Bean,它是一种特殊的@Component注解。

jms.convertAndSend()方法:

  1. 对order进行转换:将TacoOrder的实例转化为json格式。转换器在MessagingConfig.java中声明为了Bean。

  2. 对消息进行后处理:给消息增加了一个自定义头部。

  3. 发送消息到目的地。

MessagingConfig.java

package tacos.messaging;

import java.util.HashMap;
import java.util.Map;

import javax.jms.Destination;

import org.apache.activemq.artemis.jms.client.ActiveMQQueue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;

import tacos.TacoOrder;

@Configuration
public class MessagingConfig {

    @Bean
    public MappingJackson2MessageConverter messageConverter() {
        MappingJackson2MessageConverter messageConverter =
                                new MappingJackson2MessageConverter();
        messageConverter.setTypeIdPropertyName("_typeId");

        Map<String, Class<?>> typeIdMappings = new HashMap<String, Class<?>>();
        typeIdMappings.put("order", TacoOrder.class);
        messageConverter.setTypeIdMappings(typeIdMappings);

        return messageConverter;
    }

    @Bean
    public Destination orderQueue() {
        return new ActiveMQQueue("tacocloud.order.queue");
    }

}

上述代码将消息转换器目的地都声明为了Bean。

消息转换器中,setTypeIdPropertyName("_typeId")决定了待转换对象的全限定类名存储在json文件中的”_typeId”目录下。setTypeIdMappings(typeIdMappings)建立了String到全限定类名的映射,这样可以提高发送端和接收端消息传递的灵活性。

OrderMessagingService.java

package tacos.messaging;

import tacos.TacoOrder;

public interface OrderMessagingService {

  void sendOrder(TacoOrder order);
  
}

使用JMS接收消息(Pull model)

tacos.kitchen

JmsOrderReceiver.java(原始Message)

这里能接收到消息的原始Message。

package tacos.kitchen.messaging.jms;

import javax.jms.Message;

import org.springframework.context.annotation.Profile;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import tacos.TacoOrder;
import tacos.kitchen.OrderReceiver;

@Profile("jms-template")
@Component
public class JmsOrderReceiver implements OrderReceiver {

    private JmsTemplate jms;
    private MessageConverter converter;

    @Autowired
    public JmsOrderReceiver(JmsTemplate jms, MessageConverter converter) {
        this.jms = jms;
        this.converter = converter;
    }

    @Override
    public TacoOrder receiveOrder() {
        Message message = jms.receive("tacocloud.order.queue");
        return (TacoOrder) converter.fromMessage(message);
    }

}

JmsOrderReceiver.java(仅载荷)

这里仅接收到消息的载荷。

package tacos.kitchen.messaging.jms;

import org.springframework.context.annotation.Profile;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import tacos.TacoOrder;
import tacos.kitchen.OrderReceiver;

@Profile("jms-template")
@Component
public class JmsOrderReceiver implements OrderReceiver {

    private JmsTemplate jms;

    public JmsOrderReceiver(JmsTemplate jms) {
        this.jms = jms;
    }

    @Override
    public TacoOrder receiveOrder() {
        return (TacoOrder) jms.receiveAndConvert("tacocloud.order.queue");
    }

}

MessagingConfig.java

package tacos.kitchen.messaging.jms;

import java.util.HashMap;
import java.util.Map;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;

import tacos.TacoOrder;

@Profile({"jms-template", "jms-listener"})
@Configuration
public class MessagingConfig {

    @Bean
    public MappingJackson2MessageConverter messageConverter() {
        MappingJackson2MessageConverter messageConverter =
                                new MappingJackson2MessageConverter();
        messageConverter.setTypeIdPropertyName("_typeId");

        Map<String, Class<?>> typeIdMappings = new HashMap<String, Class<?>>();
        typeIdMappings.put("order", TacoOrder.class);
        messageConverter.setTypeIdMappings(typeIdMappings);

        return messageConverter;
    }

}

@Profile({"jms-template", "jms-listener"})表示当Spring应用程序运行时的活动配置文件包含”jms-template”或”jms-listener”中的任意一个时,MessagingConfig类中的配置才会被加载和生效。

可以通过下述命令行运行Spring时激活该Profile:

java -jar yourapp.jar --spring.profiles.active=jms-template

此外,我觉得上面代码再配置一个MessagingConfig多余,因为它里面声明为Bean的消息转换器在消息发送器的MessageConfig里面已经声明过了。

OrderReceiver.java

package tacos.kitchen;

import tacos.TacoOrder;

public interface OrderReceiver {

    TacoOrder receiveOrder();

}

使用JMSListener接收消息(Push model)

tacos.kitchen

OrderListener.java

package tacos.kitchen.messaging.jms.listener;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import tacos.TacoOrder;
import tacos.kitchen.KitchenUI;

@Profile("jms-listener")
@Component
public class OrderListener {
  
    private KitchenUI ui;

    @Autowired
    public OrderListener(KitchenUI ui) {
        this.ui = ui;
    }

    @JmsListener(destination = "tacocloud.order.queue")
    public void receiveOrder(TacoOrder order) {
        ui.displayOrder(order);
    }
  
}

@JmsListener会监听目的地的消息。当消息到达时,receiveOrder()方法会被自动调用,消息载荷会充当参数order

KitchenUI.java

package tacos.kitchen;

import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;
import tacos.TacoOrder;

@Component
@Slf4j
public class KitchenUI {

    public void displayOrder(TacoOrder order) {
        // TODO: Beef this up to do more than just log the received taco.
        //       To display it in some sort of UI.
        log.info("RECEIVED ORDER:  " + order);
    }
  
}

@Component是将标记为Spring Bean的通用注解,其派生注解有@Service@Controller@Repository

@Bean通常用于注解配置类@Configuration中的方法,它适用于更精细化地对Bean进行创建和控制。

Kafka异步消息

Apache Kafka Tutorials

先启动ZooKeeper再启动Kafka。

添加Kafka依赖

<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
</dependency>

配置代理

Spring知识点

Bean的生命周期

扫描找到Bean -> 注入对象和值,然后实例化 -> Bean的销毁。

环境变量Environment

import org.springframework.core.env.Environment;

public class MyClass {
    @Autowired
    protected Environment environment;
    
    public void myMethod() {
        System.out.println(environment.getProperty("OPENAI_BASE_URL"));
    }
}

上述代码中的environment变量可以感受到运行命令中的环境变量,例如下面的运行命令中的参数abc就会被上述代码打印出来。

java -jar my-app.jar --OPENAI_BASE_URL="abc"

environment变量还可以感受到配置文件application.properties中的变量,例如:

OPENAI_BASE_URL=abc

Spring注解

@Slf4j

@Slf4j注解用于自动在一个类中生成一个日志处理对象log,以便于在代码中直接使用log.debug(), log.info(), log.error()等方法。

下述两种代码是完全等价的。

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyService {
    public void doSomething() {
        log.info("This is an info message");
    }
}

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {
    private static final Logger log = LoggerFactory.getLogger(MyService.class);

    public void doSomething() {
        log.info("This is an info message");
    }
}

@Data

@Data=@Getter+@Setter+@ToString+@EqualsAndHashCode+@RequiredArgsConstructor

  • @Getter:为所有字段生成getter方法。

  • @Setter:为所有非final字段生成setter方法。

  • @ToString:为所有字段生成toString方法。

  • @EqualsAndHashCode:为获得注解的类生成.equals().hashCode()方法。

  • @RequiredArgsConstructor:为所有final的字段,以及标记为@NonNull且未初始化的字段生成一个构造函数。

下述两段代码是等价的。

import lombok.Data;

@Data
public class User {
    private final String id;
    private String name;
    private int age;
}

import java.util.Objects;

public class User {
    private final String id;
    private String name;
    private int age;

    public User(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age &&
                Objects.equals(id, user.id) &&
                Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

@Builder

@Builder能快速实现建造者设计模式。

import lombok.Builder;

@Builder
public class User {
    private String name;
    private int age;
    private String email;
}
User user = User.builder()
        .name("Alice")
        .age(30)
        .email("alice@example.com")
        .build();

import lombok.Builder;

public class MyClient {
    private String name;
    private OpenAiClient client;

    @Builder
    public MyClient(String name, String apiKey) {
        this.name = name;
        this.client = OpenAiClient.builder().openAiApiKey(apiKey).build();
    }
}
MyClient myClient = MyClient.builder()
        .name("Tom")
        .apiKey("123456")
        .build();

@Configuration@Bean

@Configuration注解的类是配置类,用于定义Bean的创建。@Bean注解的方法会被注册到Spring的应用上下文(ApplicationContext)中,可以被@Autowired注入使用。@Bean()中也可以指定别名,如@Bean(myAIService)。好像别名不是通过这种方式指定

带有@Component@Service@Repository@Controller注解的类都会被Spring扫描并注册为Bean

@Autowired

@Autowired注解的字段会被自动注入Spring容器中已有的Bean

当有多个实现类时,。假设ApiService有两个实现类OpenAIServiceQwenAIService

@Autowired
private ApiService apiService;  // 这里会报错,抛出NoUniqueBeanDefinitionException异常。
@Autowired
@Qualifier("openAIService")  // 或@Qualifier("qwenAIService"),默认为类的首字母小写。如果@Bean()注解中额外指定了别名,这里需要使用所指定的别名。
private ApiService apiService;  // 正确

@SpringBootApplication

@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan

  • @EnableAutoConfiguration:启用Spring-boot的自动配置机制。例如项目中如果引入了spring-boot-starter-data-mongodb,那么Spring 会自动配置 MongoDB 的连接。

  • @ComponentScan:启用组件扫描,自动发现并注册Bean。它会自动扫描被注解类所在包及其子包的组件(@Component@Service@Controller等

案例分析:

package com.tencent.supersonic;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(scanBasePackages = {"com.tencent.supersonic", "dev.langchain4j"},
        exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@EnableScheduling
@EnableAsync
public class StandaloneLauncher {

    public static void main(String[] args) {
        SpringApplication.run(StandaloneLauncher.class, args);
    }
}

@EnableScheduling

启用Spring的定时任务,表明该应用需要执行定时任务(如数据同步、缓存刷新等)。

@EnableAsync

启用Spring的异步方法执行功能,表明该应用需要处理异步任务(如邮件发送、文件处理等),提高系统吞吐量。