注解(Annotation)是java1.5之后提供的一种语法。其主要作用是编译检查(比如@override)和代码分析(通过代码中添加注解,利用注解解析器对添加了注解的代码进行分析,获取想要的结果,一般自定义的注解都是这一种功能)。
1.1 JDK提供的注解
JDK提供的注解最常用的是3个,@Override,@Deprecated和@SuppressWarnings.
1.1.1 @Override
@Override表示子类重写了父类的方法,或者实现了接口的方法。帮助开发者确认子类是否正确的覆盖了父类的方法,若父类中没有此方法,编译器即报错。但是,子类与父类有同样的方法,但子类的方法上没有@Override注解,是不会报错。
以基类Object的方法toString ()为例:
//正确的写法
public class ChildClass {
//@Override是帮助开发者确认子类是否正确的覆盖了父类的方法
public void read(){
System.out.println("this is a parent method!");
}
@Override
public String toString(){
return "ChildClass";
}
}
但是如果toString()不加Override,也没问题,只是简单的子类重写父类(Object)的方法。
public class ChildClass {
//@Override是帮助开发者确认子类是否正确的覆盖了父类的方法
public void read(){
System.out.println("this is a parent method!");
}
public String toString(){
return "ChildClass";
}
}
但是,如果把toString()方法改成toString1()方法就会报错。
public class ChildClass {
//@Override是帮助开发者确认子类是否正确的覆盖了父类的方法
public void read(){
System.out.println("this is a parent method!");
}
@Override
public String toString1(){
return "ChildClass";
}
}
提示错误:The method toString1() of type ChildClass must override or implement a supertype method
翻译过来就是toString1()方法必须是重写重写父类的方法或者实现相关接口。即提示父类中没有toString1() 方法。这样就通过提示来确保开发者能正确重写toString()方法。
1.1.2 @Deprecated
@Deprecated用于提示开发者,标注此注解的方法已经被弃用了。请使用另外推荐的方法
@Deprecated()
public String toString1(){
return "ChildClass";
}
public static void main(String[] args){
ChildClass child=new ChildClass();
child.toString1();
}
使用toString1()方法,编译器会提示,将toString1()画一条横线,表示此方法已经过期。
1.1.3 @SuppressWarnings
@SuppressWarnings是抑制警告的意思。比如我们新建一个变量,但是没有用,编译器会提示此变量未使用的警告。如果在方法中,添加了@SuppressWarnings的相关注解,这个警告就不会再提示了。
@SuppressWarnings({"unused"})
public static void main(String[] args){
List<Integer>list=new ArrayList<>();
}
2. 自定义注解
除了使用java自带的注解,我们也可以自定义注解,用于帮助为相关代码打上标签,然后我们在解析注解的逻辑中就可以通过这些标签来完成相关的工作,比如,权限控制,日记记录等等。
2.1 自定义注解语法
定义一个自定义注解,与定义一个接口类似,只不过在interface前加是哪个@。其内部可以添加属性值,其属性值的定义为
修饰符 返回值类型 属性名() [default value]
其中,修饰符只能用public 和abstract。 返回值为基本类型、字符串、枚举、注解以及以上类型的一维数组。
定义自定义注解,还需要用到元注解,用于修饰自定义注解,一般我们会用到两个。@Retention和@Target。
@Retention
用于确定注解的生命周期。其有三个枚举变量可选
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* SOURCE级别表示代码级别可见,经过编译器编译生成字节码对象时,此注解就没了。
* 比如@override就是代码级别可见
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
* CLASS表示字节码对象级别可见,但是字节码对象被虚拟机加载时,
* 这个注解会被抛弃,这是默认的可见级别
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
* RUNTIME表示运行时也可见,当虚拟机加载字节码对象时,此注解仍然可见。
* 因此可以通过反射获取注解信息,然后完成相应的注解解析工作,一般自定义的注解都是运行时可见。
*/
RUNTIME
}
@Target
用于修饰此注解可以用于什么类型上。比如注解可以用在类级别、方法、成员字段或者构造函数上。
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration 可以修饰类*/
TYPE,
/** Field declaration (includes enum constants) 可以修饰字段*/
FIELD,
/** Method declaration 可以修饰方法*/
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration 构造方法*/
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
下面是一个简单的自定义注解:
/**
* @author sks
* 这个注解用于日志管理 ,从修饰注解的元注解可知,这个注解可以修饰方法和类。其可见范围到运行时都可见。
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Myanno {
/** 下面时候注解的属性 **/
/** 要执行的操作类型比如:add操作 **/
public String operationType() default "";
/** 要执行的具体操作比如:添加用户 **/
public String operationName() default "";
}
2.2 自定义注解解析
上述定义的自定义注解,只是一个空的定义,没有任何的意义。因此需要我们自己定义相关的自定义注解的解析。上面提到,自定义的注解需要定义注解的可见范围。一般我们都定义为运行时可见。因此,通过反射,我们可以拿到注解的内容。通过反射拿到代码的注解内容,进行相关的逻辑处理工作,以达到注解的目的。
通过反射获得注解内容的常用方法有
T getAnnotation(Class) : 获得当前对象的指定的注解。
Annotation[] getAnnotations() X: 获得当前对象的所有注解
boolean isAnnotationPresent(annotationClass): 当前对象是否有注解。
2.3自定义注解举例
1、 定义一个注解,用于获取水果的名字。
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FruitName {
public String value() default "fruit";
}
上面这个注解定义说明此注解可以用在类,方法和成员地段上。是运行时可见的,其内有一个属性,默认值是“fruit”。
2、然后我们定义一种叫苹果的水果。其内部有个成员变量appleName。我们在这个成员字段上加上@FruitName注解。
public class Apple {
@FruitName("Apple")
private String appleName;
}
3、注解的解析
public class FruitInfoUtil {
public static void main(String[] args){
FruitInfoUtil util=new FruitInfoUtil();
util.getInfomation(Apple.class);
}
/*
*这个方法用于注解的解析,其输入是Class类型的对象。通过反射这个Class对象获取相关的注解,进行注解的解析
*/
public void getInfomation(Class<?>clazz){
//因为注解是在成员字段上,因此需要获得类的所有字段信息
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//判断这个字段上是否有相应的注解信息(FruitName.class)
if (field.isAnnotationPresent(FruitName.class)) {
FruitName fruitName = field.getAnnotation(FruitName.class);
System.out.println("水果名字是"+fruitName.value());
}
}
}
}
其结果是:
水果名字是Apple
这个例子简单的说明了自定义注解如何使用。但是在实际应用中,也许会有很多类都应用了自定义注解,即我们不知道是具体哪个类或者哪个方法使用了自定义注解。一般可以通过SpringMVC的拦截器或者SpringAOP获取添加了注解的方法,在拦截器或者AOP的通知里对注解进行处理。具体可看下面的章节。
3.自定义注解应用
一般web开发都用到spring框架。结合spring的SpringMVC的拦截器或者SpringAOP,注解可以完成权限控制,日志记录等功能。
3.1 自定义注解+SpringMVC拦截器实现权限控制功能
我们想实现这么一个注解。方法上添加了此注解的方法,不需要登录权限即可执行,否则就要查看其http请求的session中是否包含相关登录信息,以确定是否执行方法里的内容。
/**
* @author sks
* 这个注解用于权限控制
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogin {
}
实现HandlerInterceptor 接口,完成自定义SpringMVC拦截器,在拦截器内部实现注解的解析功能。
public class MyInceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (!(handler instanceof HandlerMethod)) {
System.out.println("当前操作handler不为HandlerMethod=" + handler.getClass().getName() + ",req="
+ request.getQueryString());
return false;
}
//获得经过拦截器的方法
HandlerMethod handlerMethod=(HandlerMethod) handler;
String methodName = handlerMethod.getMethod().getName();
//通过反射的getAnnotation方法获得其方法上的指定的NoLogin类型的注解。
NoLogin myanno= handlerMethod.getMethod().getAnnotation(NoLogin.class);
if (myanno!=null) { //如果获得的注解不为空的话,说明此方法不需要权限就可执行。
System.out.println("当前操作不需要登录");
return true;
}
//否则就要看其session 的属性里是否有关于LOGIN属性的信息,若没有,则拦截此方法,不执行方法的操作
if (request.getSession().getAttribute("LOGIN")==null) {
System.out.println("当前操作" + methodName + "用户未登录,ip=" + request.getRemoteAddr());
return false;
}
System.out.println("当前操作" + methodName + "用户登录:" + request.getSession().getAttribute("LOGIN"));
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
}
}
然后,在springMVC的配置文件里配置拦截器的相关信息。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<context:component-scan base-package="com.test.controller" />
<context:component-scan base-package="com.test.spring*" />
<context:component-scan base-package="com.test.aop*" />
<mvc:annotation-driven />
<!-- 配置注解驱动 -->
<!-- <mvc:annotation-driven conversion-service="conversionService"/> -->
<mvc:resources location="/img/" mapping="/img/**"/>
<mvc:resources location="/css/" mapping="/css/**"/>
<mvc:resources location="/js/" mapping="/js/**"/>
<mvc:resources location="/views/" mapping="/views/**"/>
<mvc:resources location="/ui/" mapping="/ui/**"/>
<mvc:resources location="/fonts/" mapping="/fonts/**"/>
<mvc:resources location="/bower_components/" mapping="/bower_components/**"/>
<mvc:resources location="/dist/" mapping="/dist/**"/>
<mvc:resources location="/documentation/" mapping="/documentation/**"/>
<!-- mvc:interceptors拦截器 ,注意其写法,要先写拦截的路径,然后再排除相关的不拦截的路径-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/> <!-- 拦截器拦截所有的方法 -->
<!-- 一般login申请和退出登录都不应该拦截。比如登录页面最终的http请求是/user/login
那么就要写成path="/*/login",而写成path="/login"则是不行的-->
<mvc:exclude-mapping path="/*/login" />
<mvc:exclude-mapping path="/img/**"/> <!--静态资源也不应该拦截 -->
<mvc:exclude-mapping path="/css/**"/>
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/views/**"/>
<mvc:exclude-mapping path="/ui/**"/>
<mvc:exclude-mapping path="/fonts/**"/>
<mvc:exclude-mapping path="/bower_components/**"/>
<mvc:exclude-mapping path="/dist/**"/>
<mvc:exclude-mapping path="/documentation/**"/>
<bean class="com.test.intercept.MyInceptor"/>
</mvc:interceptor>
</mvc:interceptors>
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:conf/resource.properties" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
最后,看看Controller的内容,
@Controller
public class LoginController {
@RequestMapping("/user/login")
@ResponseBody
private String loginCheck(HttpServletRequest request){
//EcgUser ecgUser=userService.queryUserById(itemId);
String username=request.getParameter("username");
String password=request.getParameter("password");
HttpSession session = request.getSession();
if (username.equals("andy")&&password.equals("123")) {
session.setAttribute("LOGIN", username);
return "success";
}
return "false";
}
@NoLogin
@RequestMapping("/login/nologin")
@ResponseBody
private String test(Model model) throws JsonProcessingException{
System.out.println("有申请");
List<Product> list = new ArrayList<Product>();
//这里把“类别名称”和“销量”作为两个属性封装在一个Product类里,每个Product类的对象都可以看作是一个类别(X轴坐标值)与销量(Y轴坐标值)的集合
list.add(new Product("衬衣", 10));
list.add(new Product("短袖", 20));
list.add(new Product("大衣", 30));
list.add(new Product("dayin", 30));
ObjectMapper mapper=new ObjectMapper();
String writeValueAsString = mapper.writeValueAsString(list);
return writeValueAsString;
}
@RequestMapping("/login/{itemId}")
@ResponseBody
private String queryUserById(@PathVariable String itemId,Model model){
return "未登录";
}
}
上面的类中,第一个方法登录方法,此方法不经过拦截器,(如果此方法拦截了,这时候确实还处于未登录状态,那么loginCheck方法就不会被执行了。因此不能拦截)。
类中的第二个方法和第三个方法都会经过拦截器。由于第二个方法加上了@NoLogin注解,表示这个方法未经过登录也可以执行。第三个方法没有@NoLogin注解,如果用户未登录,那么是不会被执行的。如果用户已登录,http请求中的session包含了LOGIN属性,那么就会执行。具体的结果就不贴了。
这样,通过自定义注解和springmvc的拦截器,可以实现一个简单的权限控制功能。
3.2 自定义注解+SpringAOP实现日志记录功能
springAOP:面向切面编程,是spring的两大核心模块之一,用于将系统中通用的模块或者功能抽取出来。其基本原理是AOP代理(分为动态代理和cglib代理)。利用aop里的通知,实现自定义注解的解析,可以完成相关的工作。
这里我们设计一个注解,即在需要进行日志记录的地方加上此注解即可实现日志自动记录的功能。
首先,仍是定义相关注解:
/**
* @author sks
* 这个注解用于日志管理
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Myanno {
/** 要执行的操作类型比如:add操作 **/
public String operationType() default "";
/** 要执行的具体操作比如:添加用户 **/
public String operationName() default "";
}
其次,定义相关的aop代理通知。这里,把注解的解析工作放在事后通知上,即下面的after方法。这里用简单的system.out来模拟日志记录功能。
/**
* @author sks
*
*/
public class MyAdvice4Anno {
public void before(JoinPoint joinPoint){
System.out.println("获取参数--》前置通知");
for (int i = 0; i < joinPoint.getArgs().length; i++) {
System.out.println("获取参数--》"+joinPoint.getArgs()[i].toString());
}
}
/*
*自定义的注解放在事后通知中
*/
public void after(JoinPoint joinPoint){
System.out.println("后置通知");
//通过连接点JoinPoint 获得代理的方法,进而获取方法上的注解信息
Method[] methods = joinPoint.getTarget().getClass().getMethods();
for (Method method : methods) {
Myanno annotation = method.getAnnotation(Myanno.class);
if (annotation!=null) {
String operationName = annotation.operationName();
String operationType = annotation.operationType();
//*========控制台输出=========*//
System.out.println("=====controller后置通知开始=====");
System.out.println("请求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()")+"."+operationType);
System.out.println("方法描述:" + operationName);
break;
}
}
}
}
//异常通知
public void afterException(){
System.out.println("出事啦!出现异常了!!");
}
}
然后在springMvc的配置文件中,做好aop的配置工作。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<context:component-scan base-package="com.test.controller" />
<context:component-scan base-package="com.test.spring*" />
<context:component-scan base-package="com.test.aop*" />
<mvc:annotation-driven />
<!-- 配置注解驱动 -->
<!-- <mvc:annotation-driven conversion-service="conversionService"/> -->
<mvc:resources location="/img/" mapping="/img/**"/>
<mvc:resources location="/css/" mapping="/css/**"/>
<mvc:resources location="/js/" mapping="/js/**"/>
<mvc:resources location="/views/" mapping="/views/**"/>
<mvc:resources location="/ui/" mapping="/ui/**"/>
<mvc:resources location="/fonts/" mapping="/fonts/**"/>
<mvc:resources location="/bower_components/" mapping="/bower_components/**"/>
<mvc:resources location="/dist/" mapping="/dist/**"/>
<mvc:resources location="/documentation/" mapping="/documentation/**"/>
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:conf/resource.properties" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<bean id="myAdvice4Anno" class="com.test.zhujie.myanno.MyAdvice4Anno"></bean>
<aop:config>
配置切入点
<aop:pointcut expression="execution(* com.test.controller.AnnoController.*(..))" id="pointcut2"/>
<aop:aspect ref="myAdvice4Anno">
<aop:before method="before" pointcut-ref="pointcut2"/>
<aop:after method="after" pointcut-ref="pointcut2"/>
<aop:after-throwing method="afterException" pointcut-ref="pointcut2"/>
</aop:aspect>
</aop:config>
</beans>
最后,看相关的控制层代码。
**
* @author sks
*
*/
@Controller
public class AnnoController {
private static Logger logger = LoggerFactory.getLogger("log");
@RequestMapping("/anno/queryData")
@ResponseBody
private E3Result queryData(Model model,String username,String password) throws JsonProcessingException{
logger.warn("查询用户id");
// userService.
List<Product> list = new ArrayList<Product>();
//这里把“类别名称”和“销量”作为两个属性封装在一个Product类里,每个Product类的对象都可以看作是一个类别(X轴坐标值)与销量(Y轴坐标值)的集合
list.add(new Product("衬衣", 10));
list.add(new Product("短袖", 20));
list.add(new Product("大衣", 30));
E3Result.ok(list);
ObjectMapper mapper=new ObjectMapper();
String writeValueAsString = mapper.writeValueAsString(list);
return E3Result.ok(list);
}
//
@Myanno(operationType="add操作",operationName="添加用户")
@RequestMapping("/anno/test")
@ResponseBody
public String test() throws JsonProcessingException{
logger.warn("查询用户id");
System.out.println("有申请");
List<Product> list = new ArrayList<Product>();
//这里把“类别名称”和“销量”作为两个属性封装在一个Product类里,每个Product类的对象都可以看作是一个类别(X轴坐标值)与销量(Y轴坐标值)的集合
list.add(new Product("衬衣", 10));
list.add(new Product("短袖", 20));
list.add(new Product("大衣", 30));
list.add(new Product("dayin", 30));
ObjectMapper mapper=new ObjectMapper();
String writeValueAsString = mapper.writeValueAsString(list);
return writeValueAsString;
}
}
上述的test方法假设需要进行日志记录操作,就在test()方法上加上@Myanno(operationType=”add操作”,operationName=”添加用户”)。
此时,访问这个方法,编译器输出:
=====controller后置通知开始=====
请求方法:com.test.controller.AnnoController.test().add操作
方法描述:添加用户
成功。
注意事项:切点所代表的方法需要是public方法,aop代理才能成功。之前,test的方法用的是private修饰,一直aop代理不成功。后查才发现是此问题。原因是aop代理要么是动态代理,要么是cglib代理,一个是接口实现,一个是继承父类,都需要其方法是public方法才能代理成功。