在开始仿写前,我们先明确要实现的三个目标:
目标一:IOC。可以通过自定义注解@MYService
、@MYController
,将对象交给IOC容器管理
public interface IDemoService {
String get(String name);
}
@MYService
public class DemoService implements IDemoService {
public String get(String name) {
return "My name is " + name;
}
}
注意,只是标了注解还不够,我们还需要通过配置文件appliacation.properties去配置要扫描的类在哪些包
目标二:DI。可以通过自定义注解@MYAutowired
实现依赖注入
@MYController
public class DemoAction {
@MYAutowired
private IDemoService demoService;
}
目标三:MVC。可以通过自定义注解@MYRequestMapping
、@MYRequestParam
实现类似SpringMVC的请求分发与处理
@MYController
@MYRequestMapping("/demo")
public class DemoAction {
@MYAutowired
private IDemoService demoService;
@MYRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@MYRequestParam("name") String name){
String result = demoService.get(name);
// String result = "My name is " + name;
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@MYRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp,
@MYRequestParam("a") Integer a, @MYRequestParam("b") Integer b){
try {
resp.getWriter().write(a + "+" + b + "=" + (a + b));
} catch (IOException e) {
e.printStackTrace();
}
}
@MYRequestMapping("/remove")
public void remove(HttpServletRequest req, HttpServletResponse resp,
@MYRequestParam("id") Integer id){
}
}
项目的整体结构如下:
那下面我们就开始动手实现上面的三个目标!
1.自定义注解
根据上面的要求,我们首先要做的就是自定义五个注解:
@Target(ElementType.FIELD) // @Autowired是用在成员变量上的注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYAutowired {
String value() default "";
}
@Target({ElementType.TYPE}) // @Controller是用在类上的
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYController {
String value() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD}) // @RequestMapping既可以用在类上,也可以用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYRequestMapping {
String value() default "";
}
@Target({ElementType.PARAMETER}) // @RequestParam是用在方法的参数上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYRequestParam {
String value() default "";
}
@Target(ElementType.TYPE) // @Service是用在类上的
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MYService {
String value() default "";
}
更多关于自定义注解的内容可以参考这篇 【Java杂记】注解:自定义注解示例…
2.核心类 MYDispatchServlet
实现上面三个要求的代码都在MYDispatchServlet类中,那这个类该怎么写呢?我们可以从目标三这个终极目标入手,就跟 Springmvc 一样,我们也写一个DispatchServlet,然后在初始化时完成任务一(IOC)和任务二(DI)。
// 继承javax.HttpServlet
public class MYDispatchServlet extends HttpServlet {
// 初始化阶段,在容器创建Servlet时会执行初始化方法(init)
@Override
public void init(ServletConfig config) throws ServletException {
// 1.加载配置文件
// 传入配置文件路径
doLoadConfig(config.getInitParameter("contextConfigLocation"));
// 2.扫描相关类
// 传入配置文件中的scanPackage的包名
doScanner(contextConfig.getProperty("scanPackage"));
// 3.初始化扫描到的类,并将它们放入IOC容器
doInstance();
// 4.完成依赖注入
doAutowired();
// 5.初始化HandlerMappring
initHandlerMapping();
System.out.println("MYSpring framework is init");
}
// 运行阶段
// 处理post请求
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 请求分发,将请求分发个相应方法
// 注:此处,该方法还包含了分发后方法的执行
try {
doDispatch(req, resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Excetion Detail:" +Arrays.toString(e.getStackTrace()));
}
}
// 处理get请求
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
}
架子搭好了,下面就对上面定义的方法一一实现:
2.1 doLoadConfig()
private Properties contextConfig = new Properties();
// 读取配置文件信息到内存
private void doLoadConfig(String contextConfigLocation) {
// 通过当前类的ClassLoader读取Properties文件,成IO流;相当于读入了 scanPackage=com.yzh.demo 到内存中
InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
// 存到Properties对象中
contextConfig.load(fis);
} catch (IOException e) {
e.printStackTrace();
}
}
2.2 doScanner()
private List<String> classNames = new ArrayList<String>();
// 扫描指定包,得到指定包下所有类的全类名,本质是文件操作
private void doScanner(String scanPackage) {
// 1.获取需要扫描包的File对象
// 这里不能直接new File,因为new File需要的是相对路径或绝路径,
// 而上一步得到的包名什么都不是,所以我们的策略是先获取URL对象
// 注:getResource方法返回的是URL对象,用来获取指定类或包的绝对路径
// getResource获取绝对路径,首先要将包名转化成项目相对路径。
// 而最前面 / 表示从根路径中寻找(显然要找到这个包不能从当前目录下寻找)
URL url = this.getClass().getClassLoader().
getResource("/" + scanPackage.replaceAll("\\.", "/"));
// 通过getFile获取到要扫描包的绝对路径(文件夹)
File classpath = new File(url.getFile());
// 2.遍历文件夹,寻找class文件
for (File file : classpath.listFiles()) {
if (file.isDirectory()) {
// 这里是通过递归遍历文件夹,还是包就再执行上述步骤(解析路径->创建目录->遍历)
doScanner(scanPackage + "." + file.getName());
} else {
// 不是class文件的不管
if (!file.getName().endsWith("class")) {continue;}
// 这里要保存全类名(包.类名),因为后面要通过反射Class.forName获取Class对象
String className = (scanPackage + "." + file.getName()).replace(".class", "");
classNames.add(className);
}
}
}
2.3 doInstance()
private Map<String, Object> ioc = new HashMap<String, Object>();
// 将需要IOC容器管理的对象放入IOC容器
private void doInstance() {
// 如果指定要扫描的包(文件夹)下没有文件
if (classNames.isEmpty()) {return;}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
// 1.标了@MYController的类的对象需要IOC容器管理
if (clazz.isAnnotationPresent(MYController.class)) {
// 通过Class对象创建相应的实例对象!
Object instance = clazz.newInstance();
// 这里getSimpleName是只获取类名,而IOC容器的beanName只要单纯的类名就够了
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, instance);
// 2.标了@MYService的类的对象需要IOC容器管理
} else if (clazz.isAnnotationPresent(MYService.class)) {
Object instance = clazz.newInstance();
// 2.1 按类名注入(最常见的方式),一般需要类名小写(驼峰规则)
// 注:这里明确一个问题,classNames中存的是全类名,而这里要做beanName的只能是类名
String beanName = toLowerFirstCase(clazz.getSimpleName());
// 2.2 按自定义beanName注入
// 注:这里就是注解的基本操作,拿到注解,然后通过注解中定义的方法拿到参数
MYService annotation = clazz.getAnnotation(MYService.class);
if (!"".equals(annotation.value())) {
beanName = annotation.value();
}
ioc.put(beanName, instance);
// 2.3 除了上面按类名注入外,还要考虑接口。就像我们平常注入Service一样,声明时都是直接声明的接口。
// 注:这里还要考虑多实现的问题,即一个对象可能不止实现了一个接口,所以需要遍历(getInterfaces)
for (Class<?> i : clazz.getInterfaces()) {
// 如果一个接口已经有实现对象的Bean了,抛异常
if (ioc.containsKey(i.getName())) {
throw new Exception("The beanName is existed!!!");
}
ioc.put(i.getName(), instance);
}
// 3.没有注解(或配置)的类,IOC容器不管;
} else {
// 接着看下一个类
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 这里默认是只传入类名大写的,A=65, a=97
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return new String(chars);
}
2.4 doAutowired()
// 对已经被管理的对象们进行依赖注入
private void doAutowired() {
// IOC容器为空,表示没有能进行依赖注入的对象
if (ioc.isEmpty()) { return; }
// 遍历IOC容器
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
// 1.拿到当前对象所有field(注:Declared表示也能拿到private成员)
Field[] fields = entry.getValue().getClass().getDeclaredFields();
// 2.遍历所有field们
for (Field field : fields) {
// 2.1 判断这些filed中们有没有需要注入的(有@MYAtowired)
if (field.isAnnotationPresent(MYAutowired.class)) {
// 2.2 获取要注入的beanName
MYAutowired annotation = field.getAnnotation(MYAutowired.class);
// 2.2.1 自定义beanName
String beanName = annotation.value().trim();
if ("".equals(beanName)) {
// 2.2.2 若没有自定义beanName,就以字段的类名作为beanName
// 注:这就体现了上面doInstance对接口进行处理的必要性
beanName = field.getType().getName();
}
// 3.将IOC中的对象注入当前field
// setAccessible是为了对public外的字段也能进行注入
field.setAccessible(true);
try {
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
}
2.5 initHandlerMapping()
private Map<String, Method> handlerMapping = new HashMap<String, Method>();
// 保存方法与请求路径的映射关系(ps:通过method可以获取到所属类名-->根据类名拿到具体的bean)
private void initHandlerMapping() {
if (ioc.isEmpty()) return;
// 遍历所有在容器中的对象
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
// 不是Controller直接返回
if (!clazz.isAnnotationPresent(MYController.class)) continue;
// 获取Controller上的同一请求路径
String baseUrl = "";
if (clazz.isAnnotationPresent(MYRequestMapping.class)) {
MYRequestMapping annotation = clazz.getAnnotation(MYRequestMapping.class);
baseUrl = annotation.value();
}
// 获取Controller中每个方法的请求路径
Method[] methods = clazz.getMethods();
for (Method method : methods) {
// 不是处理请求的方法就直接返回
if (!method.isAnnotationPresent(MYRequestMapping.class)) continue;
MYRequestMapping annotation = method.getAnnotation(MYRequestMapping.class);
// URL处理:无论写了几个 / 都处理成一个
String url = ("/" + baseUrl + "/" + annotation.value()).replaceAll("/+", "/");
handlerMapping.put(url, method);
// 日志打印能处理的路径
System.out.println("Mapped" + url + "," + method);
}
}
}
2.6 doDispatch
// 请求分发与执行
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
// 获取请求路径
// 注:getRequestURL:http://localhost:8080/demo/query
// getRequestURI: /demo/query
// getContextPath: 获取当前Context的统一路径,一般用于一个容器部署多个Context,而此处是" "
String url = req.getRequestURI();
String contextPath = req.getContextPath();
// url处理:将contextPath去除(应用标识),将多个 / 的替换成一个 /
url = url.replaceAll(contextPath, "").replaceAll("/+","/");
// 如果没有处理这个url的method,就返回404
if (!handlerMapping.containsKey(url)) {
resp.getWriter().println("404 Not Found!!!");
return;
}
// 获取处理该请求的方法
Method method = this.handlerMapping.get(url);
// 获取该method需要的参数类型们(形参列表)
Class<?>[] parameterTypes = method.getParameterTypes();
// 用来保存当前方法的具体参数(实参列表)
Object[] paramValues = new Object[parameterTypes.length];
// 获取请求中携带的参数映射
Map<String, String[]> parameterMap = req.getParameterMap();
// 遍历形参列表,将形参与实参对应(即填充paramValues)
for (int i = 0; i < parameterTypes.length; i++) {
Class paramterType = parameterTypes[i];
// 如果是Request,直接将req放入
if (paramterType == HttpServletRequest.class) {
paramValues[i] = req;
continue;
// 如果是Response,直接将resp放入
} else if (paramterType == HttpServletResponse.class) {
paramValues[i] = resp;
continue;
// 如果是String,就要具体寻找其对应的实参了
} else if (paramterType == String.class) {
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int j = 0; j < parameterAnnotations.length; j++) {
for (Annotation a : parameterAnnotations[i]) {
// 如果是@RequestParam注解的,就是指定了请求中携带参数名的
if (a instanceof MYRequestParam) {
// 获取指定的参数名
String paramName = ((MYRequestParam)a).value();
if (!"".equals(paramName.trim())) {
// 在请求Request携带的请求参数map中,获取到对应参数
String value = Arrays.toString(parameterMap.get(paramName))
.replaceAll("\\[|\\]","")
.replaceAll("\\s",",");
// 放入实参列表
paramValues[i] = value;
}
}
}
}
}
}
// 获取具体执行该方法的对象,
// 注:此处是直接获取方法的所有类的对象
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
// 执行当前方法,传入req,resp,实参列表
// 注:此处是直接将获取的参数名写死了!!!
method.invoke(ioc.get(beanName),new Object[]{req, resp, parameterMap.get("name")[0]});
}
这就完了吗?不,servlet还需要配置要一个web.xml,将所有请求都分发到MYDispatchServlet:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>My Web Application</display-name>
<servlet>
<servlet-name>mymvc</servlet-name>
<servlet-class>com.yzh.mvcframework.servlet.MYDispatchServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mymvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
到此为止,所有代码部分结束。
关于web容器,这里采用的是 jetty,而不是 tomcat,已经提前在pom文件的plugin配置好了,那么我们就来运行一下看看吧!
运行结果
首先启动jetty,可以看到jetty已经在8080端口成功启动了。
我们先直接访问 localhost:8080,看看会是什么结果
跟预期一样,上面doDispatch方法写了如果有匹配到路径就返回 404 not found
。那现在我访问之前 DemoAction 里面定义的请求路径 /demo/query/
会是什么结果呢?
完整的代码我放在GitHub上了,需要的同学可以在这里下载…