从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
@Documented
表示使用该注解的元素应被javadoc或类似工具文档化,它应用于类型声明,类型声明的注解会影响客户端对注解元素的使用。如果一个类型声明添加了Documented注解,那么它的注解会成为被注解元素的公共API的一部分。
@Retention
表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:
- SOURCE:注解将被编译器丢弃
- CLASS:注解在class文件中可用,但会被VM丢弃
- RUNTIME:VM将在运行期间保留注解,因此可以通过反射机制读取注解的信息
@Target
表示该注解可以用于什么地方,可能的ElementType参数有:
- CONSTRUCTOR:构造器的声明
- FIELD:域声明(包括enum实例)
- LOCAL_VARIABLE:局部变量声明
- METHOD:方法声明
- PACKAGE:包声明
- PARAMETER:参数声明
- TYPE:类、接口(包括注解类型)或enum声明
@Inherited
表示允许子类继承父类中的注解
(1)、引入切面依赖:
org.aspectj
aspectjrt
1.6.12
org.aspectj
aspectjweaver
1.6.12
cglib
cglib
2.2
(2)、定义一个简单的 applicationContext.xml
(3)、创建一个注解标签
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Inherited
public @interface LeopardAnno {
String key() default "";
String recodeLog() default "";
}
(4)、建立简单的实现类,使用自定义的注解(key="#name"引用了SPEL表达式,可自行了解,后面会展示如何使用)
@Service
public class AnnoService {
@LeopardAnno(key="#name")
public String getName(String name) {
System.out.println(name + " 进来了!!");
return name;
}
}
(5)、注解切面业务处理
@Component
@Aspect
public class AnnotationAspect {
//这里将自定义注解作为形参,@annotation可直接使用形参引入
@Around("@annotation(leo)")
public Object doAround(ProceedingJoinPoint pjp , LeopardAnno leo) throws Throwable{
System.out.println(leo.key());
System.out.println(leo.recodeLog());
System.out.println("-------开始拦截");
System.out.println("类路径名:"+pjp.getSourceLocation().getWithinType().getName());
System.out.println("方法名:"+pjp.getSignature().getName());
Object proceed = pjp.proceed();
System.out.println("-------结束拦截");
return proceed;
}
}
(6)、基础搭建结束。写个测试类运行:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AnnoTest {
@Autowired
private AnnoService annoService;
@Test
public void testOne(){
String name = annoService.getName("leopard");
System.out.println(name);
}
}
运行结果:
#name
null
-------就是拦截
annotation.AnnoService
getName
leopard 进来了!!
-------结束拦截
leopard
到这里,基本注解功能实现!可能会发现,注解切面获取的参数el表达式没转化,还是#name。
是因为我们尚未处理占位符的原因,下面我们写一个工具类来处理
(7)、EL表达式参数字符处理
public class SpelParser {
private static ExpressionParser ep = new SpelExpressionParser();
/**
* @param key el表达式字符串,占位符以#开头
* @param paramterNames 形参名称,可以理解为占位符名称
* @param args 多数值,可以理解为占位真实值
* @return 返回EL表达式参数替换后的字符串值
*/
public static String getKey(String key, String[] paramterNames , Object[] args){
//1、将Key字符串解析为el表达式
Expression exp = ep.parseExpression(key);
//2、将形参和形参值以配对的方式配置到赋值上下文中
EvaluationContext context = new StandardEvaluationContext();//初始化赋值上下文
if(args.length <= 0){
return null;
}
for (int i = 0; i < args.length; i++) {
context.setVariable(paramterNames[i], args[i]);
}
//3、根据赋值上下文运算EL表达式
return exp.getValue(context, String.class);
}
}
接着在(5)的基础上,加入私有处理方法
private String getKey(String key, ProceedingJoinPoint pjp) {
//不符合EL表达式占位符格式,原值返回
if (!(StringUtils.isNotBlank(key) && '#' == key.toCharArray()[0])) {
return key;
}
// 获取目标方法
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
// 根据方法获取行参列表
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
return SpelParser.getKey(key, parameterNames, pjp.getArgs());
}
修改原代码获取参数值的方法
System.out.println(getKey(leo.key(),pjp));
System.out.println(getKey(leo.recodeLog(),pjp));
再次运行测试类,得到以下结果:
leopard
null
-------开始拦截
annotation.AnnoService
getName
leopard 进来了!!
-------结束拦截
leopard
结论:注解的实现看起来实现比较繁琐,但他可以很好的解决我们的代码冗余问题。
技术解耦:如缓存操作,权限管理
业务解耦:如日志记录,第三方关联操作