由SpringMVC中的Controller注解@RequestMapping引发的思考

1、前言

在Spring MVC的设计中,Controller用于接收客户端发来的Request请求,进行相应的处理后,或者返回RequstBody(Restful设计风格用来返回Json数据),或者返回JSP的名称等。实现这种设计,使用了注解@RequestMapping等。由此,引发了一些思考,为什么只需要在方法前添加一个注解@RequestMapping便可以实现该种设计。

Spring MVC中的用法如下:

@Controller
public class MainController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index() {
        return "index";
    }
}

2、注解

注解也称为元数据,为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻方便的使用这些数据。其作用有三个。

  • 编译时检查,通过某些注解如@override实现在编译时期对方法进行检查,如没有复写父类的方法,则在编译时期报错;
  • 程序运行时,通过反射机制实现,对代码进行分析,实现特定的功能,Spring中的@RequestMapping就是这种作用;
  • 生成文档,通过注解,编写外部文档。
Java中内置了三种标准注解以及四种元注解。标准注解为:
  • @Override,表示当前的方法定义将覆盖父类中的方法,如没覆盖,编译时会报错;
  • @Deprecated,若使用了该注解,表示该方法已经被弃用了,编译器会报警告;
  • @SuppressWarnings,关闭不当的编译器警告信息。
四种元注解(用于自定义注解)为:
  • @Target 表示该注解用于什么地方。可能的ElementType参数包括:CONSTRUCTOR(构造器的声明)、FIELD(域声明)、LOCAL_VARIABLE(局部变量声明)、METHOD(方法声明)、PACKAGE(包声明)、TYPE(类、接口或枚举)
  • @Retention表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:SOURCE(注解将被编译器丢弃)、CLASS(注解在class字节码文件中可用,但会被JVM丢弃)、RUNTIME(JVM在运行期也会保留该注解,因此可利用反射机制读取该注解的信息,用于代码分析)
  • @Documented讲该注解包含在Javadoc中
  • @Inherited允许子类继承父类中的注解

3、反射

使用javac编译完java类文件后会生成一个.class字节码文件,每个字节码文件都对应一个Class类。Class类和java.lang.reflect类库一起对反射做出了支持,该类库包括Filed、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样就可以使用Constructor创建新的对象,用get()和set()方法读取和修改Field对象关联的字段,用invoke()方法调用与Method对象关联的方法,还可以使用getFiled()、getMethod()和getContructors()等方法。

4、例子

首先,使用元注解自定义一个注解,用它来跟踪一个项目中的用例,如果一个方法或者一组方法实现了某个用例的需求,则程序员为该方法加上该注解。项目经理通过计算已经实现的用例就可以很好的掌握项目的进展。若运维人员需要修改业务,也可以很方便的在代码中找到该用例。
用例注解代码如下:
package thinkinginjava;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	public int id();
	public String description() default "no description";
}

下面的类中有三个方法被注解为用例:
package thinkinginjava;

public class PasswordUtils {
	@UseCase(id = 47, description = "Password must contain at least one numeric")
	public boolean validatePassword(String password) {
		return (password.matches("\\w*\\d\\w*"));
	}

	@UseCase(id = 48)
	public String encrypPassword(String password) {
		return new StringBuilder(password).reverse().toString();
	}
}
如果没有注解读取的工具,那么注解也就没有意义了,外部工具apt可以帮助程序员解析带有注解的java源代码。下面为一个简单的注解处理器,用它来读取PasswordUtils类,并利用反射机制查找@UseCase标记。其中提供了一组id值,然后它会列出在PasswordUtils里中找到的用例以及缺失的用例。
package thinkinginjava;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class UseCaseTracker {
	public static void trackUseCases(List useCases, Class cl) {
		for (Method m : cl.getDeclaredMethods()) {
			System.out.println(m.toString());
			UseCase uc = m.getAnnotation(UseCase.class);
			if (uc != null) {
				System.out.println("Found Use Case : " + uc.id() + " " + uc.description());
				if(uc.id() == 48){
					try {
						Object pwdu = cl.newInstance();
						System.out.println(cl.isInstance(pwdu));
						System.out.println(m.invoke(pwdu, "jichenxiao"));
					} catch (IllegalAccessException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (IllegalArgumentException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (InstantiationException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				useCases.remove(new Integer(uc.id()));
			}
		}
		for(int i : useCases){
			System.out.println("Warning: Missing Use Case - " + i);
		}
	}

	public static void main(String[] args){
		List useCases = new ArrayList();
		Collections.addAll(useCases, 47,48,49);
		trackUseCases(useCases, PasswordUtils.class);
	}
}

上述程序中使用了反射,getDeclaredMethods()和getAnnotation()均属于AnnotationElement接口(Class、Method和Filed等类都实现了该接口)。通过getDeclaredMethod()返回类中的方法,类型为Method,然后再使用getAnnotation()返回UseCase注解对象。如果被注解的方法中没有该类型的注解,则返回null。通过id()和description()方法从UseCase中提取元素的值。通过注解命中一个方法,并使用invoke()去调用该方法。最终的输出结果为:
public boolean thinkinginjava.PasswordUtils.validatePassword(java.lang.String)
Found Use Case : 47 Password must contain at least one numeric
public java.lang.String thinkinginjava.PasswordUtils.encrypPassword(java.lang.String)
Found Use Case : 48 no description
true
oaixnehcij
Warning: Missing Use Case - 49

5、思考

Sping MVC中的注解,通过@RequstMapping去执行其命中的方法,并经过相应的处理返回RequstBody或者JSP页面的名称。该设计中,猜测是同样的实现方式,通过注解处理器和Java的反射机制去执行相应的处理。接下来会研读一下Spring的源码,如有不对,届时再做更正。
注:本文中例子来源于《Java编程思想》。

你可能感兴趣的:(Java)