這篇文章,我們開始 Spring AMQP 專案實戰旅程。
介紹
通過這個專案實戰旅程,你會學習到如何使用 Spring Boot 整合 Spring AMQP,並且使用 RabbitMQ 的訊息佇列機制傳送郵件。其中,訊息生產者負責將使用者的郵件訊息傳送至訊息佇列,而訊息消費者從訊息佇列中獲取郵件訊息進行傳送。這個過程,你可以理解成郵局:當你將要釋出的郵件放在郵箱中時,您可以確信郵差最終會將郵件傳送給收件人。
準備
本教程假定 RabbitMQ 已在標準埠(5672) 的 localhost 上安裝並執行。如果使用不同的主機,埠,連線設定將需要調整。
host = localhostusername = guestpassword = guestport = 5672vhost = /複製程式碼
實戰旅程
準備工作
這個實戰教程會構建兩個工程專案:email-server-producer 與 email-server-consumer。其中,email-server-producer 是訊息生產者工程,email-server-consumer 是訊息消費者工程。
在教程的最後,我會將完整的程式碼提交至 github 上面,你可以結合原始碼來閱讀這個教程,會有更好的效果。
現在開始旅程吧。我們使用 Spring Boot 整合 Spring AMQP,並通過 Maven 構建依賴關係。(由於篇幅的問題,我並不會貼上完整的 pom.xml 配置資訊,你可以在 github 原始碼中檢視完整的配置檔案)
<
dependencies>
<
!-- spring boot-->
<
dependency>
<
groupId>
org.springframework.boot<
/groupId>
<
artifactId>
spring-boot-starter<
/artifactId>
<
exclusions>
<
exclusion>
<
groupId>
org.springframework.boot<
/groupId>
<
artifactId>
spring-boot-starter-logging<
/artifactId>
<
/exclusion>
<
/exclusions>
<
/dependency>
<
dependency>
<
groupId>
org.springframework.boot<
/groupId>
<
artifactId>
spring-boot-starter-test<
/artifactId>
<
scope>
test<
/scope>
<
/dependency>
<
dependency>
<
groupId>
org.springframework.boot<
/groupId>
<
artifactId>
spring-boot-starter-amqp<
/artifactId>
<
/dependency>
<
dependency>
<
groupId>
org.springframework<
/groupId>
<
artifactId>
spring-context-support<
/artifactId>
<
/dependency>
<
dependency>
<
groupId>
javax.mail<
/groupId>
<
artifactId>
mail<
/artifactId>
<
version>
${javax.mail.version
}<
/version>
<
/dependency>
<
/dependencies>
複製程式碼
構建訊息生產者
我們使用 Java Config 的方式配置訊息生產者。
@Configuration@ComponentScan(basePackages = {"com.lianggzone.rabbitmq"
})@PropertySource(value = {"classpath:application.properties"
})public class RabbitMQConfig {
@Autowired private Environment env;
@Bean public ConnectionFactory connectionFactory() throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(env.getProperty("mq.host").trim());
connectionFactory.setPort(Integer.parseInt(env.getProperty("mq.port").trim()));
connectionFactory.setVirtualHost(env.getProperty("mq.vhost").trim());
connectionFactory.setUsername(env.getProperty("mq.username").trim());
connectionFactory.setPassword(env.getProperty("mq.password").trim());
return connectionFactory;
} @Bean public CachingConnectionFactory cachingConnectionFactory() throws Exception {
return new CachingConnectionFactory(connectionFactory());
} @Bean public RabbitTemplate rabbitTemplate() throws Exception {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory());
rabbitTemplate.setChannelTransacted(true);
return rabbitTemplate;
} @Bean public AmqpAdmin amqpAdmin() throws Exception {
return new RabbitAdmin(cachingConnectionFactory());
} @Bean Queue queue() {
String name = env.getProperty("mq.queue").trim();
// 是否持久化 boolean durable = StringUtils.isNotBlank(env.getProperty("mq.queue.durable").trim())? Boolean.valueOf(env.getProperty("mq.queue.durable").trim()) : true;
// 僅建立者可以使用的私有佇列,斷開後自動刪除 boolean exclusive = StringUtils.isNotBlank(env.getProperty("mq.queue.exclusive").trim())? Boolean.valueOf(env.getProperty("mq.queue.exclusive").trim()) : false;
// 當所有消費客戶端連線斷開後,是否自動刪除佇列 boolean autoDelete = StringUtils.isNotBlank(env.getProperty("mq.queue.autoDelete").trim())? Boolean.valueOf(env.getProperty("mq.queue.autoDelete").trim()) : false;
return new Queue(name, durable, exclusive, autoDelete);
} @Bean TopicExchange exchange() {
String name = env.getProperty("mq.exchange").trim();
// 是否持久化 boolean durable = StringUtils.isNotBlank(env.getProperty("mq.exchange.durable").trim())? Boolean.valueOf(env.getProperty("mq.exchange.durable").trim()) : true;
// 當所有消費客戶端連線斷開後,是否自動刪除佇列 boolean autoDelete = StringUtils.isNotBlank(env.getProperty("mq.exchange.autoDelete").trim())? Boolean.valueOf(env.getProperty("mq.exchange.autoDelete").trim()) : false;
return new TopicExchange(name, durable, autoDelete);
} @Bean Binding binding() {
String routekey = env.getProperty("mq.routekey").trim();
return BindingBuilder.bind(queue()).to(exchange()).with(routekey);
}
}複製程式碼
其中,定義了佇列、交換器,以及繫結。事實上,通過這種方式當佇列或交換器不存在的時候,Spring AMQP 會自動建立它。(如果你不希望自動建立,可以在 RabbitMQ 的管理後臺開通佇列和交換器,並註釋掉 queue() 方法和 exchange() 方法)。此外,我們為了更好地擴充套件,將建立佇列或交換器的配置資訊抽離到了配置檔案 application.properties。其中,還包括 RabbitMQ 的配置資訊。
mq.host=localhostmq.username=guestmq.password=guestmq.port=5672mq.vhost=/mq.exchange=email_exchangemq.exchange.durable=truemq.exchange.autoDelete=falsemq.queue=email_queuemq.queue.durable=truemq.queue.exclusive=falsemq.queue.autoDelete=falsemq.routekey=email_routekey複製程式碼
此外,假設一個生產者傳送到一個交換器,而一個消費者從一個佇列接收訊息。此時,將佇列繫結到交換器對於連線這些生產者和消費者至關重要。在 Spring AMQP 中,我們定義一個 Binding 類來表示這些連線。我們使用 BindingBuilder 來構建 “流式的 API” 風格。
BindingBuilder.bind(queue()).to(exchange()).with(routekey);
複製程式碼
現在,我們離大功告成已經很近了,需要再定義一個傳送郵件任務存入訊息佇列的方法。此時,為了更好地擴充套件,我們定義一個介面和一個實現類,基於介面程式設計嘛。
public interface EmailService {
/** * 傳送郵件任務存入訊息佇列 * @param message * @throws Exception */ void sendEmail(String message) throws Exception;
}複製程式碼
它的實現類中重寫 sendEmail() 方法,將訊息轉碼並寫入到訊息佇列中。
@Servicepublic class EmailServiceImpl implements EmailService{
private static Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class);
@Resource( name = "rabbitTemplate" ) private RabbitTemplate rabbitTemplate;
@Value("${mq.exchange
}") private String exchange;
@Value("${mq.routekey
}") private String routeKey;
@Override public void sendEmail(String message) throws Exception {
try {
rabbitTemplate.convertAndSend(exchange, routeKey, message);
}catch (Exception e){
logger.error("EmailServiceImpl.sendEmail", ExceptionUtils.getMessage(e));
}
}
}複製程式碼
那麼,我們再模擬一個 RESTful API 介面呼叫的場景,來模擬真實的場景。
@RestController()@RequestMapping(value = "/v1/emails")public class EmailController {
@Resource private EmailService emailService;
@RequestMapping(method = RequestMethod.POST) public JSONObject add(@RequestBody JSONObject jsonObject) throws Exception {
emailService.sendEmail(jsonObject.toJSONString());
return jsonObject;
}
}複製程式碼
最後,再寫一個 main 方法,將 Spring Boot 服務執行起來吧。
@RestController@EnableAutoConfiguration@ComponentScan(basePackages = {"com.lianggzone.rabbitmq"
})public class WebMain {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebMain.class, args);
}
}複製程式碼
至此,已經大功告成了。我們可以通過 Postman 傳送一個 HTTP 請求。(Postman是一款功能強大的網頁除錯與傳送網頁HTTP請求的Chrome外掛。)
{
"to":"lianggzone@163.com", "subject":"email-server-producer", "text":"<
html>
<
head>
<
/head>
<
body>
<
h1>
郵件測試<
/h1>
<
p>
hello!this is mail test。<
/p>
<
/body>
<
/html>
"
}複製程式碼
請參見圖示。
來看看 RabbitMQ 的管理後臺吧,它會出現一個未處理的訊息。(地址:http://localhost:15672/#/queues)
注意的是,千萬別向我的郵箱發測試訊息喲,不然我的郵箱會郵件爆炸的/(ㄒoㄒ)/~~。
構建訊息消費者
完成訊息生產者之後,我們再來構建一個訊息消費者的工程。同樣地,我們使用 Java Config 的方式配置訊息消費者。
@Configuration@ComponentScan(basePackages = {"com.lianggzone.rabbitmq"
})@PropertySource(value = {"classpath:application.properties"
})public class RabbitMQConfig {
@Autowired private Environment env;
@Bean public ConnectionFactory connectionFactory() throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(env.getProperty("mq.host").trim());
connectionFactory.setPort(Integer.parseInt(env.getProperty("mq.port").trim()));
connectionFactory.setVirtualHost(env.getProperty("mq.vhost").trim());
connectionFactory.setUsername(env.getProperty("mq.username").trim());
connectionFactory.setPassword(env.getProperty("mq.password").trim());
return connectionFactory;
} @Bean public CachingConnectionFactory cachingConnectionFactory() throws Exception {
return new CachingConnectionFactory(connectionFactory());
} @Bean public RabbitTemplate rabbitTemplate() throws Exception {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory());
rabbitTemplate.setChannelTransacted(true);
return rabbitTemplate;
} @Bean public AmqpAdmin amqpAdmin() throws Exception {
return new RabbitAdmin(cachingConnectionFactory());
} @Bean public SimpleMessageListenerContainer listenerContainer( @Qualifier("mailMessageListenerAdapter") MailMessageListenerAdapter mailMessageListenerAdapter) throws Exception {
String queueName = env.getProperty("mq.queue").trim();
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(cachingConnectionFactory());
simpleMessageListenerContainer.setQueueNames(queueName);
simpleMessageListenerContainer.setMessageListener(mailMessageListenerAdapter);
// 設定手動 ACK simpleMessageListenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return simpleMessageListenerContainer;
}
}複製程式碼
聰明的你,應該發現了其中的不同。這個程式碼中多了一個 listenerContainer() 方法。是的,它是一個監聽器容器,用來監聽訊息佇列進行訊息處理的。注意的是,我們這裡設定手動 ACK 的方式。預設的情況下,它採用自動應答,這種方式中訊息佇列會傳送訊息後立即從訊息佇列中刪除該訊息。此時,我們通過手動 ACK 方式,如果消費者因當機或連結失敗等原因沒有傳送 ACK,RabbitMQ 會將訊息重新傳送給其他監聽在佇列的下一個消費者,保證訊息的可靠性。
當然,我們也定義 application.properties 配置檔案。
mq.host=localhostmq.username=guestmq.password=guestmq.port=5672mq.vhost=/mq.queue=email_queue複製程式碼
此外,我們建立了一個 MailMessageListenerAdapter 類來消費訊息。
@Component("mailMessageListenerAdapter")public class MailMessageListenerAdapter extends MessageListenerAdapter {
@Resource private JavaMailSender mailSender;
@Value("${mail.username
}") private String mailUsername;
@Override public void onMessage(Message message, Channel channel) throws Exception {
try {
// 解析RabbitMQ訊息體 String messageBody = new String(message.getBody());
MailMessageModel mailMessageModel = JSONObject.toJavaObject(JSONObject.parseObject(messageBody), MailMessageModel.class);
// 傳送郵件 String to = mailMessageModel.getTo();
String subject = mailMessageModel.getSubject();
String text = mailMessageModel.getText();
sendHtmlMail(to, subject, text);
// 手動ACK channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}catch (Exception e){
e.printStackTrace();
}
} /** * 傳送郵件 * @param to * @param subject * @param text * @throws Exception */ private void sendHtmlMail(String to, String subject, String text) throws Exception {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage);
mimeMessageHelper.setFrom(mailUsername);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(text, true);
// 傳送郵件 mailSender.send(mimeMessage);
}
}複製程式碼
在 onMessage() 方法中,我們完成了三件事情:
- 從 RabbitMQ 的訊息佇列中解析訊息體。
- 根據訊息體的內容,傳送郵件給目標的郵箱。
- 手動應答 ACK,讓訊息佇列刪除該訊息。
這裡,JSONObject.toJavaObject() 方法使用 fastjson 將 json 字串轉換成實體物件 MailMessageModel。注意的是,@Data 是 lombok 類庫的一個註解。
@Datapublic class MailMessageModel {
@JSONField(name = "from") private String from;
@JSONField(name = "to") private String to;
@JSONField(name = "subject") private String subject;
@JSONField(name = "text") private String text;
@Override public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("Email{from:").append(this.from).append(", ");
sb.append("to:").append(this.to).append(", ");
sb.append("subject:").append(this.subject).append(", ");
sb.append("text:").append(this.text).append("
}");
return sb.toString();
}
}複製程式碼
Spring 對 Java Mail 有很好的支援。其中,郵件包括幾種型別:簡單文字的郵件、 HTML 文字的郵件、 內嵌圖片的郵件、 包含附件的郵件。這裡,我們封裝了一個簡單的 sendHtmlMail() 進行郵件傳送。
對了,我們還少了一個郵件的配置類。
@Configuration@PropertySource(value = {"classpath:mail.properties"
})@ComponentScan(basePackages = {"com.lianggzone.rabbitmq"
})public class EmailConfig {
@Autowired private Environment env;
@Bean(name = "mailSender") public JavaMailSender mailSender() {
// 建立郵件傳送器, 主要提供了郵件傳送介面、透明建立Java Mail的MimeMessage、及郵件傳送的配置 JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
// 如果為普通郵箱, 非ssl認證等 mailSender.setHost(env.getProperty("mail.host").trim());
mailSender.setPort(Integer.parseInt(env.getProperty("mail.port").trim()));
mailSender.setUsername(env.getProperty("mail.username").trim());
mailSender.setPassword(env.getProperty("mail.password").trim());
mailSender.setDefaultEncoding("utf-8");
// 配置郵件伺服器 Properties props = new Properties();
// 讓伺服器進行認證,認證使用者名稱和密碼是否正確 props.put("mail.smtp.auth", "true");
props.put("mail.smtp.timeout", "25000");
mailSender.setJavaMailProperties(props);
return mailSender;
}
}複製程式碼
這些配置資訊,我們在配置檔案 mail.properties 中維護。
mail.host=smtp.163.commail.port=25mail.username=使用者名稱mail.password=密碼複製程式碼
最後,我們寫一個 main 方法,將 Spring Boot 服務執行起來吧。
至此,我們也完成了一個訊息消費者的工程,它將不斷地從訊息佇列中處理郵件訊息。
原始碼
相關示例完整程式碼: github.com/lianggzone/…
(完)
更多精彩文章,盡在「服務端思維」微信公眾號!