前言
为什么今天想发这篇文章呢?其实我先前公司使用的技术栈很落后,我一直想在项目中使用注解,也多次尝试使用,但都没有真正成功过!
在项目中使用自己定义的注解,时常成了我心中一个坎。由于各种借口,但都不了了之了。当初对Java注解非常感兴趣,那是因为使用Java类库时,发现Java注解有很多好处,也想在自己项目中使用自己定义的注解,表现自己牛批的一面,但就是不明白其自运行的原理,在网上搜索相关资料也没有尝试成功。
这次项目中需要增加一个数据权限的需求,项目模块有19个,都要进行添加业务逻辑代码感觉非常的繁琐,要写大量的代码,因此进修者又想到了Java注解。但是这次与往常不同,这次是用的 Spring MVC 项目,而且也有了AOP面向切面编程的一些思想。于是就想同时使用Java注解和AOP来完成这次的业务需求。经过了大量尝试和学习,我心中的这道坎总算跨过了!
下面给大家分享一下,也希望可以帮到需要的伙伴们!
正文
在进入今天的内容分享之前,我先说明一些需要掌握的技术点。需要了解Spring MVC开发模式,熟悉Java反射机制和注解的知识,同时还要掌握Spring AOP面向切面的思想。下面进修者也会给与一些这方面的知识。
辅助知识
Java反射机制
反射是框架设计的灵魂,我们开发中使用的任何框架都会用到Java反射的知识。Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
Java注解
Java注解(Java标注)是在 JDK5 时引入的新特性,有些地方称它为元数据。Java注解提供了一种安全的类似注释的机制,用来将任何的信息或元数据与程序元素(类、方法、成员变量等)进行关联。一般会附加在代码中的一些元信息上,通常用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。
Java注解的一些应用如下:
- 生成文档:它是最常见也是最早提供的Java注解;
- 格式检查:在编译时进行格式检查,如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;
- 替代配置:跟踪代码依赖性,实现替代配置文件功能,比较常见的是spring 2.5 开始的基于注解配置,作用就是减少配置;
- 反射注解:在反射的 Class、Method和Field 等函数中,有许多于 Annotation 相关的接口,可以在反射中解析并使用 Annotation。
Spring AOP原理
Spring AOP实现原理简单点说是通过动态代理实现的。如果我们为Spring的某一个Bean配置了切面,那么Spring在创建这个Bean的时候,实际上创建了这个Bean的一个代理对象,我们后续对Bean中方法的调用,其实调用的是代理类重写的代理方法。Spring AOP使用了2种动态代理方式实现,分别是 JDK动态代理和Cglib动态代理。这里不再继续说明,不明白的可以在网上搜索相关资料。
Java注解应用
结合业务需求,自定义一个Java注解,该注解的功能主要进行方法标注。方法上面标注了该注解的都要进行统一的数据权限处理。如下面的Java代码:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 数据权限配置注解类
* @author trainer
* @date 2023/8/20
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataRoleConfig {
Class<?> value() default Object.class;
}
Spring AOP应用
创建切面类
在项目中定义和业务相关的切面类,负责处理相关的业务需求。如:获取业务数据并统一给参数赋值并传递到SQL中。部分代码如下:
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
/**
* 数据权限配置切面类
* @author trainer
* @date 2023/8/20
*/
@Slf4j
@Aspect
@Component
public class DataConfigAspect {
@Autowired
private DictionaryService dictionaryService;
/**
* 将切点切到指定注解上
*/
@Pointcut("@annotation(com.trainer.aop.annotation.DataRoleConfig)")
public void dataConfigPointCut() { }
/**
* 前置通知
* @param joinPoint
*/
@Before("dataConfigPointCut()")
public void dataConfigBefore(JoinPoint joinPoint) throws Exception {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(true);
// 获取内容
String mpIds = ObjectUtil.defaultIfNull(session.getAttribute("mpIds")
, Constant.DATA_CONFIG_DEFAULT_MONITOR_POINTS).toString();
// 注解内容,暂时不处理
DataRoleConfig dataRoleConfig = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(DataRoleConfig.class);
Class<?> bean = dataRoleConfig.value();
boolean dataConfigSwitch = dataConfigSwitch();
// 处理方法或属性
// dealMethod(joinPoint);
Object[] args = joinPoint.getArgs();
for(Object o : args){
if (o instanceof List) {
if(dataConfigSwitch) {
((List) o).addAll(Arrays.asList(mpIds.split(",")));
}
continue;
}
if (ObjectUtil.isNull(o) || o instanceof JSONObject || o instanceof Long
|| o instanceof Integer || o instanceof String) {
continue;
}
if (dataConfigSwitch) {
// 对象属性赋值
((PageBean)o).setDataConfigMpIds(Arrays.asList(mpIds.split(",")));
} else {
((PageBean)o).setDataConfigMpIds(null);
}
}
}
/**
* 前置通知
* @param joinPoint
*/
// @Around("dataConfigPointCut()")
public void dataConfigAround(ProceedingJoinPoint joinPoint) {
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
// 获取入参
Object[] param = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
for(Object o : param){
sb.append(o + "; ");
}
log.debug("进入【{}】方法,参数为: {}", methodName, sb.toString());
// 继续执行方法
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
/**
* 后置处理逻辑
* @param joinPoint
*/
@After("dataConfigPointCut()")
public void dataConfigAfter (JoinPoint joinPoint) { }
/**
* 数据权限配置开关,数据从字典中获取
* @return
*/
private boolean dataConfigSwitch() throws Exception {
// 获取开关状态
List<Dictionary> dictionaryList = dictionaryService.queryDictionaryByCode(Constant.DATA_CONFIG_SWITCH_CATEGORY
, Constant.DATA_CONFIG_SWITCH_ITEM_CATEGORY);
// 没有配置,默认关闭
if (CollectionUtil.isEmpty(dictionaryEntityList)) {
throw new Exception("数据权限配置开关没有配置,请在字典中进行配置!");
}
Dictionary dictionaryEntity = dictionaryList.get(0);
String itemValue = dictionaryEntity.getValue1();
if (!"true".equalsIgnoreCase(itemValue) && !"false".equalsIgnoreCase(itemValue)) {
throw new Exception("数据权限配置开关,字典配置有误,请检查配置!");
}
return Boolean.valueOf(itemValue);
}
/**
* 处理方法或属性(暂时未用)
* @param joinPoint
*/
private void dealMethod (JoinPoint joinPoint) {
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
log.debug("开始执行方法:{} ", methodName);
// 加载类
String clazzPathName = joinPoint.getSignature().getDeclaringTypeName();
// Class clazz = joinPoint.getSignature().getDeclaringType();
try {
Class clazz = Class.forName(clazzPathName);
// clazz.getSuperclass().getDeclaredField("mpIdList");
Field field = clazz.getField("mpIdList");
Object obj = clazz.newInstance();
field.set(obj, Arrays.asList(DataConfigTarget.mpIds.split(",")));
System.out.println(field.get(obj));
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定义注解使用
在项目sevice层中具体查询逻辑的方法上使用,如下面的部分代码所示:
/**
* 列表查询
* @param monitorPoint
* @return
*/
@DataRoleConfig
public List<MonitorPoint> queryList(MonitorPoint monitorPoint){
return monitorPointMapper.queryPointList(monitorPoint);
}
部分Mybatis SQL配置代码如下所示:
<!-- 统一调用的SQL拼接串 -->
<sql id="dataRoleConfigSql">
<!-- 数据权限过滤 -->
<if test="dataConfigMpIds != null and dataConfigMpIds.size() > 0">
AND mp_id in
<foreach collection="dataConfigMpIds" open="(" close=")" item="dcMpid" separator=",">
#{dcMpid}
</foreach>
</if>
</sql>
通过该种方式,可以在项目中针对需要修改的19个功能模块进行注解标注,这样可以简化代码结构、避免重复编码、松散耦合等。
小结
通过学习上面的内容,进修者相信读者已经学会了Java注解和AOP在项目开发过程中的联合使用,也知道了它们各自的好处。
这里再阐述一下,Spring Boot项目只是在Spring MVC项目上对原有开发框架进行了一系列的整合;做到开发简单化,同时解决了包引入冲突等原生开发过程中的一些问题,其特点也不仅于此,希望同学们进行深入学习和挖掘!
温情提示:学习不是一蹴而就的,希望大家不忘初心,继续前行!
感谢您读完了进修者的内容分享,欢迎留言区一起聊聊天,聊聊关于您对“Spring MVC项目开发中,注解和AOP联合的使用”有什么更好的想法,都可以随时叨扰!
我是进修者,期待与您肩并肩,一起进化成长!