Spring Boot AOP+阿里Dubbo构建微服务应用
AOP
AOP和IOC是Spring的两大特性。AOP为Aspect Oriented Programming的缩写,意为:面向切面的编程。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP的成员
PointCut:即在哪个地方进行切入,它可以指定某一个点,也可以指定多个点。
Advice:在切入点干什么,指定在PointCut地方做什么事情(增强),打日志、执行缓存、处理异常等等。
Advisor/Aspect:PointCut + Advice 形成了切面Aspect,这个概念本身即代表切面的所有元素。
关于AOP更多更详细的概念在此就不多赘述了。读者可以自行google或百度一下。
应用场景
在实际项目开发中,经常会遇到一些功能点,比如记录某一个流程的日志或在OA系统中,提交申请后需要邮件通知审批人做后续操作。相比较流程的审批而言,日志记录或邮件通知功能是一种辅助性功能。针对这些辅助性功能,就可以考虑使用AOP的思想来实现。
Spring Boot AOP
在Spring Boot项目中引入AOP的功能需要将spring-boot-starter-aop引入到实际项目中。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
项目的组织结构
下图为整个演示项目的结构图,其中aop为项目的父级目录,aop-api定义了各个模块所需要的接口,aop-domain定义了项目中所需要的异常类和枚举,aop-mail模块为邮件发送功能的具体实现,最后为aop-app为业务接口的具体实现。
aop模块
aop为一个普通的maven工程,在该工程中定义了一些maven的版本属性以及主要依赖的版本管理。具体的pom定义如下xml文件所示。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mc</groupId>
<artifactId>aop</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>aop-mail</module>
<module>aop-api</module>
<module>aop-app</module>
<module>aop-domain</module>
</modules>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.1.3.RELEASE</spring-boot.version>
<dubbo.version>2.7.0</dubbo.version>
<lombok.version>1.18.4</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.mc</groupId>
<artifactId>aop-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
aop-api模块
在该模块中,定义了两个接口,一个是com.mc.aop.api.mail.EmailService。该接口主要定义的方法为sendEmail。另外一个是com.mc.aop.api.oa.OaService。该接口中定义了两个方法分别为apply和feedBack。具体功能对应着提交申请和申请审批。
EmailService的具体定义
package com.mc.aop.api.mail;
* @author M.C
* @description EmailService
* @date 2019-02-20 9:00
public interface EmailService {
* sendMail
* @param receivers
* @param subject
* @param htmlMsg
public void sendMail(String receivers, String subject, String htmlMsg);
OaService的具体定义
package com.mc.aop.api.oa;
import com.mc.aop.domain.enums.Biz;
import com.mc.aop.domain.exception.OaException;
* @author M.C Yang
* @description OAService
* @date 2019-02-25 17:20
public interface OaService {
* @param biz
public void apply(Biz biz) throws OaException;
* @param result
* @return
public boolean feedBack(boolean result) throws OaException;
POM的具体定义
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>aop</artifactId>
<groupId>com.mc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>aop-api</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!--项目之间的依赖-->
<dependency>
<groupId>com.mc</groupId>
<artifactId>aop-domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
通过该POM文件我们可以看到aop-api模块依赖于aop-domain模块。
aop-domain模块
该模块没有什么需要特殊说明的,就是为了定义系统中所需要的一些pojo,exception,enum等信息。其中该模块的依赖仅为lombok。除此之外无其它额外的maven依赖。
在此仅仅把具体的实现代码贴出来,方便文章的阅读。
枚举的定义
package com.mc.aop.domain.enums;
public enum Biz {
OVER_TIME(0,"加班审批"),
SICK_LEAVE(1,"病假审批"),
MATERNITY_LEAVE(2,"产假审批"),
SKIP_LEVEL(3,"跨级审批");
private int code;
private String name;
Biz(int code, String name) {
this.code = code;
this.name = name;
public static Biz findByValue(Integer value) {
for (Biz each : Biz.values()) {
if (value.equals(each.getCode())) {
return each;
return null;
public int getCode() {
return code;
public String getName() {
return name;
}
自定义系统异常
package com.mc.aop.domain.exception;
import lombok.*;
* @author M.C
* @description QaException
* @date 2019-02-25 17:31
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class OaException extends RuntimeException {
private int resultCode;
private String resultMessage;
aop-mail模块
该模块是做为一个dubbo的provider模块存在的。所以在该模块中引用了dubbo-spring-boot-starter。除此之外,provider的注册机制采用的是dubbo registry zookeeper。具体的使用手册可以参考 apache/incubator-dubbo-spring-boot-project 。Spring Boot在整合dubbo时,除了apache提供的starter外,还有阿里也提供了相关的starter。对应的starter的地址为 alibaba/dubbo-spring-boot-starter 。
apache/dubbo-spring-boot-project中定义的spring boot版本为2.1.2.RELEASE,同时该starter也对之前的版本做了兼容处理。alibaba/dubbo-spring-boot-project中定义的spring boot版本为1.5.10.RELEASE。
关于starter选择,由于 incubator-dubbo-spring-boot-project 为apache的顶级项目。同时该项目使用的是spring boot的2.1.3.RELEASE。所以在aop-mail中我们选用了 incubator-dubbo-spring-boot-project 。
POM文件的定义
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>aop</artifactId>
<groupId>com.mc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>aop-mail</artifactId>
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--项目之间的依赖-->
<dependency>
<groupId>com.mc</groupId>
<artifactId>aop-api</artifactId>
</dependency>
</dependencies>
</project>
Spring Boot工程的属性文件
server.port=8081
# Spring boot application
spring.application.name=mail-application
# Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service
dubbo.scan.base-packages=com.mc.aop.mail
# Dubbo Application
## The default value of dubbo.application.name is ${spring.application.name}
## dubbo.application.name=${spring.application.name}
embedded.zookeeper.port = 2181
# Dubbo Protocol
dubbo.protocol.name=dubbo
## Random port
dubbo.protocol.port=-1
## Dubbo Registry
dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port}
## DemoService version
demo.service.version=1.0.0
spring.mail.protocol=smtp
spring.mail.host=smtp.126.com
spring.mail.port=25
spring.mail.username=邮箱用户名
spring.mail.password=邮箱密码
spring.mail.default-encoding=utf-8
Spring Boot启动类
package com.mc.aop.mail;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;
* @author M.C
* @description AppApplication
* @date 2019-02-25 16:59
@SpringBootApplication(scanBasePackages = {"com.mc.aop.mail"})
public class MailApplication {
* main
* @param args
public static void main(String[] args) {
new SpringApplicationBuilder(MailApplication.class)
.listeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) event -> {
Environment environment = event.getEnvironment();
int port = environment.getProperty("embedded.zookeeper.port", int.class);
new EmbeddedZooKeeper(port, false).start();
.run(args);
EmbeddedZooKeeper该类的具体实现可以直接从 apache/incubator-dubbo-spring-boot-project 里面获取到,在此就不做粘贴。
邮件发送类的具体实现与dubbo服务的发布
package com.mc.aop.mail.service.impl;
import com.mc.aop.api.mail.EmailService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.util.Date;
* @author M.C
* @description EmailServiceImpl
* @date 2019-02-20 9:01
@Service(version = "${demo.service.version}")
@Component
@Slf4j
public class EmailServiceImpl implements EmailService {
@Resource
private JavaMailSender javaMailSender;
@Override
@SneakyThrows
public void sendMail(String receivers, String subject, String htmlMsg){
log.info("收件人为{},邮件主题为{},邮件内容为{}",receivers,subject,htmlMsg);
MimeMessage mail = javaMailSender.createMimeMessage();
mail.addRecipients(Message.RecipientType.TO, InternetAddress.parse(receivers));
mail.setFrom(new InternetAddress("yang_ustber@126.com"));
mail.setSentDate(new Date());
mail.setSubject(subject);
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setContent(htmlMsg, "text/html;charset=utf-8");
Multipart multipart = new MimeMultipart();
multipart.addBodyPart(mimeBodyPart);
mail.setContent(multipart);
javaMailSender.send(mail);
log.info("AOP方式发送邮件,RPC方式调用Mail服务,成功发送邮件!!");
aop-app模块
该模块是做为一个dubbo的consumer模块存在的。消费的接口为aop-email模块中的EmailService。具体的POM依赖与aop-mail基本类似。不同点为,在aop-app模块中需要引入aop相关的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
aop-app的Spring Boot启动类定义
package com.mc.aop.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
* @author M.C
* @description AppApplication
* @date 2019-02-25 16:59
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class AppApplication {
* main
* @param args
public static void main(String[] args) {
SpringApplication.run(AppApplication.class, args);
针对该启动类的@EnableAspectJAutoProxy,做一个说明。虽然在这个主入口类中添加了@EnableAspectJAutoProxy(proxyTargetClass=true)。但是实际上该配置不是必须配置的,如果已经引入了spring-boot-starter-aop的引入。具体原因可以通过分析spring-boot-aop-autoconfigure来进行获知。
项目的属性文件
server.port=8082
spring.application.name=app-application
demo.service.version=1.0.0
embedded.zookeeper.port = 2181
dubbo.registry.address=zookeeper://127.0.0.1:${embedded.zookeeper.port}
因为在aop-app项目的属性文件中没有定义任何跟aop相关的属性,也是就是说默认spring.aop.auto=true spring.aop.proxy-target-class=false。
proxy-target-class属性值决定是基于接口的还是基于类的代理被创建。如果proxy-target-class 属性值被设置为true,那么基于类的代理将起作用(这时需要cglib库)。如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用。具体的不同可以参考该文章 Lorenzo:Cglib proxy探秘
OAService的具体实现类
package com.mc.aop.app.service.impl;
import com.mc.aop.api.oa.OaService;
import com.mc.aop.domain.enums.Biz;
import com.mc.aop.domain.exception.OaException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
* @author M.C
* @description OaServiceImpl
* @date 2019-02-25 17:27
@Service
@Slf4j
public class OaServiceImpl implements OaService {
@Override
public void apply(Biz biz) {
switch (biz.getCode()){
case 0:
log.info("加班审批");break;
case 1:
log.info("病假审批");break;
case 2:
log.info("产假审批");break;
case 3:
log.info("无效审批流程");
throw new OaException(-100,"无效审批流程");
@Override
public boolean feedBack(boolean result) {
if(result){
return true;
return false;
定义完业务流程后,我们看看切面的定义
package com.mc.aop.app.monitor;
import com.mc.aop.api.mail.EmailService;
import com.mc.aop.domain.enums.Biz;
import com.mc.aop.domain.exception.OaException;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
* @author M.C
* @description OaServiceMonitor
* @date 2019-02-25 18:05
@Aspect
@Component
@Slf4j
public class OaServiceMonitor {
@Reference(version = "${demo.service.version}")
private EmailService emailService;
@Pointcut("execution(* com.mc.aop.app.service.impl.OaServiceImpl.apply(..))")
public void applyPointcut(){}
@Pointcut("execution(* com.mc.aop.app.service.impl.OaServiceImpl.feedBack(..))")
public void feedBackPointcut(){}
@Before(value = "applyPointcut()")
public void applyBefore(JoinPoint joinPoint){
log.info("applyBefore");
sendMail(joinPoint);
@Around(value = "applyPointcut()")
public void applyAround(ProceedingJoinPoint joinPoint){
log.info("applyAround1");
sendMail(joinPoint);
joinPoint.proceed();
sendMail(joinPoint);
log.info("applyAround2");
@After(value = "applyPointcut()")
public void applyAfter(JoinPoint joinPoint){
log.info("applyAfter");
sendMail(joinPoint);
@AfterThrowing(throwing = "ex", value = "applyPointcut()")
public void applyAfterThrowing(JoinPoint joinPoint, OaException ex){
log.info("applyAfterThrowing {},{}",ex.getResultCode(),ex.getResultMessage());
sendMail(joinPoint);
@AfterReturning(returning = "result",value = "feedBackPointcut()" )
public void applyAfterReturning(JoinPoint joinPoint,Object result){
log.info("applyAfterReturning {}",result.toString());
sendMail(joinPoint);
* sendMail
* @param joinPoint
private void sendMail(ProceedingJoinPoint joinPoint){
log.info("开始发送邮件");
String receiver = "yang_ustber@126.com";
String clazzName = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
Biz biz= (Biz)joinPoint.getArgs()[0];
StringBuffer bs= new StringBuffer();
bs.append(clazzName).append("->").append(methodName).append("->").append(biz.getName());
emailService.sendMail(receiver,"aop演示程序",bs.toString());
log.info("发送邮件完成");
* sendMail
* @param joinPoint
private void sendMail(JoinPoint joinPoint){
log.info("开始发送邮件");
String receiver = "yang_ustber@126.com";
String clazzName = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
Object biz = joinPoint.getArgs()[0];
StringBuffer bs= new StringBuffer();
if("apply".equalsIgnoreCase(methodName)){
bs.append(clazzName).append("->").append(methodName).append("->").append(((Biz)biz).getName());
}else{
bs.append(clazzName).append("->").append(methodName).append("->").append(((Boolean)biz).toString());