local tomcat 找不到springmvc里的包_手写微型SpringMVC框架(1)

本文介绍了如何手写一个微型SpringMVC框架,模拟Spring的核心模块和Web MVC功能。首先,文章讲解了Spring的Core和应用层结构,然后通过Gradle构建项目,实现了Core(包括Beans、Context)、Web和Web MVC的集成,并添加了starter以实现类似SpringBoot的启动方式。在过程中,作者详细讨论了Tomcat服务器的集成、Servlet的注册与分发,以及动态映射优化,最后通过类扫描和反射机制实现了请求处理。
摘要由CSDN通过智能技术生成

55ae2dba6d39c5374a95fbdc47122894.png

为了对SpringMVC有个更加深入的理解,通过自定义一个类SpringMVC框架来模拟框架中的核心模块和实现。

本项目采用Gradle版本控制工具进行构建(相比Maven更加方便,而且免安装)

在书写之前,先来了解一下Spring框架的包结构。简要来说可以分为两层:

(1)第一层:Core核心层,这一层中主要的功能包括下面几个:

(a)Beans :负责Spring中对Bean对象的管理

(b)Context : Spring根据不同场景提取出来的框架接口,使得调用者可以直接接触Spring内部

(c) SpEL:Spring表达式的语言包

(2)第二层:应用层,这一层主要是使用核心层的接口为应用程序提供特定的功能

可以分为两大类:

(a)Data :用于数据的接入与处理等操作 包括JDBC,ORM等

(b)Web应用: 包括MVC,Servlet

好了,了解了Spring框架内部功能之后,开始这个项目的结构设计。

由于有些功能较为复杂,本项目主要实现为:

(1)实现Core模块,包括Core,Beans,Context包

(2)实现Web,集成Web和Web MVC

(3)添加starter,实现类SpringBoot的启动方式

开始书写项目:

在IDE中新建一个Gradle项目,建一个框架模块(framework)和一个测试模块(test)。

额外在test模块建立一个Application启动类(写一个main函数)。

在命令行运用Gradle clean build和 java - jar 文件目录的方式进行打包,此时会如下报错:

adeMacBook-Pro:SpringMVC mac$ java -jar test/build/libs/test-1.0-SNAPSHOT.jar 
test/build/libs/test-1.0-SNAPSHOT.jar中没有主清单属性 

原因在于虽然在test中新建了启动类,但是Gradle打出的包并不知道它在哪儿,所以需要人为显示地声明它,(Gradle中所有的jar包操作都要定义在jar关键字里),同时也利用from递归地打入所有依赖的jar包(it代笔当前文件,如果是只一个独立文件的话则直接打入;否则还有其他依赖,递归打入)

test模块中的build.gradle配置如下:

plugins {
    id 'java'
}

group 'ydq.mooc.com'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile(project(':framework'))
}

jar{
    manifest{
        attributes "Main-Class":"com.mooc.ydq.Application"
    }
    from{
        configurations.compile.collect{
            it.isDirectory() ? it : zipTree(it)
        }
    }
}

配置之后,重新打包,得到正确的结果:

adeMacBook-Pro:SpringMVC mac$ java -jar  test/build/libs/test-1.0-SNAPSHOT.jar 
Hello World!

接下来需要把framework包打入到test模块中:

(1)在test模块的build.gradle文件依赖中添加:

compile(project(':framework'))

(2)接着在framework模块中新建starter包,下面定义这个模块的启动类MiniAppplication,并实现一个run方法(参数为一个类和一个参数数组)

 public  static  void  run(Class<?> cls, String[] args)

(3)在test模块的启动类中调用:

public class Application {
    public static void main(String[] args) {
        System.out.println("Hello World!");
        //第一个参数为当前入口类,第二个为当前入口类的参数数组
        MiniApplication.run(Application.class, args);
    }
}

然后打包,得到结果:

adeMacBook-Pro:SpringMVC mac$ java -jar test/build/libs/test-1.0-SNAPSHOT.jar 
Hello World!
Hello min-spring!
(mini-spring为miniApplication run方法中的输出)

此时就说明这两个模块已经成功建立了联系。

于是乎,开始开发Web服务模型和Servlet。在开发之前,需要明确Web服务器的请求分发机制:

Web服务器通过监听一个TCP端口,然后将客户端的请求进行转发,经过Servlet的处理之后,最后服务器再将处理结果返回给客户端。

需要特别注意的是:Web服务器本身不会进行业务逻辑的处理,而是起到连接操作系统和应用程序的作用。

Web服务器的请求转发流程如下:

5d8ac8f55d3a36850944b5dd580a6e1d.png

而Servlet呢,就是用于实现Web服务器分发过来的业务逻辑。通常来说,一个Servlet用来实现一种特定的业务逻辑,对应一种uri。

在项目中,选择Tomcat服务器,并将服务器集成到Spring框架中:

具体做法为:先在依赖中引入Tomcat-core的依赖,调用Tomcat包,实例化一个Tomcat类并调用其start方法即可。

先上Maven中心仓库

org.apache.tomcat.embed " tomcat-embed-core​mvnrepository.com

选择Tomcat-core的8.5.23版本,复制gradle的配置,然后在框架中将其内嵌进去:

(注意引入依赖的位置一定要在framework的build.grandle文件中!!)

plugins {
    id 'java'
}

group 'ydq.mooc.com'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'

    // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
    compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '8.5.23'

}

然后实例化Tomcat,新建一个TomcatServer类:设置Tomcat端口号为6699

(代码中为了防止Tomcat服务器中途挂掉,采用了另外一个等待线程)

//专门用于处理tomcat server的类
public class TomcatServer {

    private Tomcat tomcat;

    //用于tomcat的配置参数
    private String[] args;

    //tomcat的constructor方法
    public TomcatServer(String[] args){
        this.args = args;
    }

    //启动tomcat的主方法
    public void startServer() throws LifecycleException {
        //实例化一个tomcat即可
        tomcat = new Tomcat();
        //设置tomcat的监听方法
        tomcat.setPort(6699);
        tomcat.start();

        //为了防止服务器中途退出,设置一个等待线程
        Thread awaitThread = new Thread("tomcat_await_thread"){
            @Override
            public void run(){
                //声明tomcat线程一直在等待
                TomcatServer.this.tomcat.getServer().await();
            }

        };

        //将此线程设置为非守护线程
        awaitThread.setDaemon(false);

        //调用方法,让它一直在等待
        awaitThread.start();

    }

}

书写完TomcatServer类之后呢,在MiniApplication中启动Tomcat:

public class MiniApplication {
    public  static  void  run(Class<?> cls, String[] args){
        System.out.println("Hello min-spring!");
        TomcatServer tomcatServer = new TomcatServer(args);
        try {
            tomcatServer.startServer();

        } catch (LifecycleException e) {
            //打印异常栈
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


然后将项目打包运行,可以在控制台看到Tomcat服务已经启动成功,但是用浏览器访问会报404,这里是为什么呢?

原因在于Tomcat中没有注册Servlet,所以继而定义一个Servlet类(继承Servlet父类),重写里面的service方法

//需要实现五个预定义的方法-->本项目只需要实现service方法即可
public class DispatcherServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
       //在响应内写入test即可
       res.getWriter().println("test");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

然后在Tomcat中注册该Servlet:

在书写之前呢,要了解Tomcat内部的四种容器类型:

(1)Container 容器,集装箱

(2)Engine 引擎容器

(3)Host 主机容器

(4) Servlet容器(Tomcat中级别最低的容器)里面包含了一个或多个Context

Context(上下文,背景,环境)一个Context对应于一个Web项目

一个Context中包含若干个Wrapper(容器里的封装部分)

Tomcat通过这四种类型对容器的职责进行解耦。其中和Servlet最接近的就是Context容器。

注册的代码如下:

  //启动tomcat的主方法
    public void startServer() throws LifecycleException {
        //实例化一个tomcat即可
        tomcat = new Tomcat();
        //设置tomcat的监听方法
        tomcat.setPort(6699);
        tomcat.start();

        //StandardContext为tomcat内对于context容器的标准实现
        Context context = new StandardContext();
        context.setPath("");

        //设置一个默认的监听器
        context.addLifecycleListener(new Tomcat.FixContextListener());

        //将test实例化注册到servlet容器中
        DispatcherServlet servlet = new DispatcherServlet();
        Tomcat.addServlet(context, "dispatcherServlet", servlet).setAsyncSupported(true);

        //配置一个servlet到uri的映射  (这样使得访问这个uri时可以映射到这个servlet)
        context.addServletMappingDecoded("/", "dispatcherServlet");

        //Tomcat的context容器需要依附在host容器内,这里将其注册到默认的host容器
        tomcat.getHost().addChild(context);



        //为了防止服务器中途退出,设置一个等待线程
        Thread awaitThread = new Thread("tomcat_await_thread"){
            @Override
            public void run(){
                //声明tomcat线程一直在等待
                TomcatServer.this.tomcat.getServer().await();
            }

        };

        //将此线程设置为非守护线程
        awaitThread.setDaemon(false);

        //调用方法,让它一直在等待
        awaitThread.start();

    }

打包运行一下,浏览器中会输入地址会返回 “test”,验证成功。

但是这样做会存在很大的弊端,因为如果容器中的Servlet过多,那么每定义一个新的Servlet就得在Tomcat中注入一个,在操作上显得非常繁琐,而且使得Servlet的维护成本大大升高。

因此需要进行优化,优化的点在于Tomcat要能够动态地实现Servlet与uri的映射关系。

一种做法是在web.xml中进行中心话配置,每种业务增加一个servlet,就对应增加一个URI与之对应,调度流程如下图所示:

bf6f1f63e1ba034d9467ad4c2eea0d36.png

但这样做呢,缺点也很明显:

(1)web.xml会使得Servlet的配置集中化,大而3杂,并且不易管理

(2)每次手动注入之前,都要新定义Servelt接口的实现

所以,就引出了SpringMVC框架中采用的调度模型,如下:

90ac3229e01a179f18a5c396d92e56d1.png

且听我慢慢解释:

这个模型呢,是利用一个叫做DispatcherServlet的类来进行所有Servlet的分发,对应哪一个Serlvet就去容器中的HandlerMapping去找即可,这种调度将外部不同的请求都接到一个类上(DispatcherServlet类),再进行一对多的分发。

这种调度的最大优点在于配Servlet的时候只需要在框架中进行修改即可,而不需要再Tomcat中堆Context进行操作了。

在原有的项目中Servlet的映射路径改为"/" :

   //配置一个servlet到uri的映射  (这样使得访问这个uri时可以映射到这个servlet)
        context.addServletMappingDecoded("/", "dispatcherServlet");

真正请求时再加载根路径下面的子路径即可。

然后新建一个MVC包,用于请求的分发,而每个Serlvet怎么区分呢?

这里就可以类比SpringMVC中控制器常用的三个注解:Controller, RequestMapping和RequestParam。

新建三个类来分别实现这三个注解(注意:注解的本质其实就是接口)

(1)controller注解(作用在类上):

@Documented
//需要保留到运行期
@Retention(RetentionPolicy.RUNTIME)
//说明此注解是类的注解
@Target(ElementType.TYPE)
public @interface Controller {
}

(2)RequestMapping注解(作用在方法上):

@Documented
@Retention(RetentionPolicy.RUNTIME)
//说明此注解是方法的注解
@Target(ElementType.METHOD)
public @interface RequestMapping {

    //添加一个属性,保存需要映射的uri
    String value();

}

(3)RequestParam注解(注解在参数上):

@Documented
@Retention(RetentionPolicy.RUNTIME)
//此注解用于接收参数
@Target(ElementType.PARAMETER)
public @interface RequestParam {

    String value();
}

完成注解之后,在test包下面测试一下这一个注解:

写随意书写一个Controller:

@Controller
public class SalaryController {
    @AutoWired
    private SalaryService salaryService;
    //功能:计算工资(通过员工姓名和工龄来计算)
    @RequestMapping("/get_salary.json")
    public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience){
        return 1000;
    }

}

打包之后,发现在浏览器中访问不到,这又是什么情况呢?

原因是服务器根本就不知道这个Controller,得想办法让服务器扫描到这个Controller吧。

这里就采用的Java中的类加载器,简单介绍一下:

类加载器是通过 类全限定名 来获取类的二进制字节流,再将二进制字节流进行解析,获取Class类的实例,可以加载classpath下的静态资源。

Java所有的类文件都可以看成是Resource的抽象,每个Java类文件都是与类名相对应的,包名和文件夹路径相对应。

好了,了解之后,开始书写:

在framework模块中新建一个core包,添加一个类扫描器ClassScanner:

//功能:类扫描器
public class ClassScanner {
    //传入的参数是包名---> 进行类的加载
    public static List<Class<?>> scanClasses(String packageName) throws IOException, ClassNotFoundException {

        List<Class<?>> classList = new ArrayList<>();

        //将包名转化为文件路径
        String path = packageName.replace(".", "/");

        //使用类加载器--->通过路径来加载文件
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        //遍历类加载器的url方法,返回值是一个可遍历的url资源
        Enumeration<URL>  resources = classLoader.getResources(path);

        while(resources.hasMoreElements()){
            URL resource = resources.nextElement();
            //如果resource的类型是jar包类型,则获取jar包的绝对路径
            if(resource.getProtocol().contains("jar")){
                //需要强转为jar型
                JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();

                String jarFilePath = jarURLConnection.getJarFile().getName();

                classList.addAll(getClassesFromJar(jarFilePath, path));
            }else{
                //todo
            }

        }
        return classList;

    }

    //功能:通过jar包的路径来获取到jar包下全部的类
    private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {

        //初始化容器来存储类
        List<Class<?>> classes = new ArrayList<>();

        //将jar包路径转化为jarFile实例
        JarFile jarFile = new JarFile(jarFilePath);

        //进行遍历
        Enumeration<JarEntry> jarEntries = jarFile.entries();
        while(jarEntries.hasMoreElements()){

            JarEntry jarEntry = jarEntries.nextElement();
            String entryName = jarEntry.getName();//例如com/mooc/ydq/test/Test.class
            //目标是取出路径的开头与我们传入的path参数相同的jar文件,并且结尾一定是class,这样即可拿到对应的jar包了
            if(entryName.startsWith(path) && entryName.endsWith(".class")){
                //将/换成.  将.class后缀去掉
                String classFullName = entryName.replace("/", ".").substring(0, entryName.length() - 6);

                //通过class.forName方法将类加载到jvm中
                classes.add(Class.forName(classFullName));
            }
        }
        return classes;
    }

}

在MiniApplication启动类中打印所有的类:

  List<Class<?>>  classList = ClassScanner.scanClasses(cls.getPackage().getName());

  classList.forEach(it-> System.out.println(it.getName()));

然后在进行打包测试:

9de0262b4dc29f2a7ed310fc7d1a29f2.png

可以看到,打印出了包下面所有的类。

写完Controller之后并不能直接利用,而应该将其中的MappingHandler提取出来。

这里采用Java中的反射机制来实现,使用反射,我们可以保存Controller中的MappingHandler,当有请求到来时进行调用。(每个MappingHandler都是一个请求映射器,对应于特定的URI)

书写一个请求映射器MappingHandler:

//功能:请求映射器
public class MappingHandler {

    //它对应的请求uri
    private String uri;

    //它对应的controller方法
    private Method method;


    //controller的类
    private Class<?> controller;

    private String[] args;

    MappingHandler(String uri, Method method, Class<?> cls, String[] args){
        this.uri = uri;
        this.method = method;
        this.controller = cls;
        this.args = args;
    }
}

然后再写一个HandlerManager类来管理若干Handler:

public class HandlerManager {

    //设置一个静态属性,用来保存多个mappingHandler
    public static List<MappingHandler> mappingHandlerList = new ArrayList<>();

    //把controller类挑选出来,将类中的mappingHandler初始化为mappinghandler
    public static void resolveMappingHandler(List<Class<?>> classList){
        //首先遍历类,将带有@controller注解的挑选出来
        for (Class<?> cls : classList){
            //如果这个类上有@Controller注解
            if(cls.isAnnotationPresent(Controller.class)){
                parseHandlerFromController(cls);
            }

        }
    }

其中方法的实现如下:

private static void parseHandlerFromController(Class<?> cls){
        //通过反射的方式获取类中的所有方法
        Method[] methods = cls.getDeclaredMethods();
        //然后找到被@RequestMapping的方法
        for (Method method : methods){
            if(!method.isAnnotationPresent(RequestMapping.class)){
                continue;
            }
            //从方法的属性中获取所有能构成MappingHandler的属性
            //uri
            String uri = method.getDeclaredAnnotation(RequestMapping.class).value();

            //设置一个容器来存储参数
            List<String> paramNameList = new ArrayList<>();
            for (Parameter parameter : method.getParameters()){
                //找到被@RequestParam注解的参数
                if (parameter.isAnnotationPresent(RequestParam.class)){
                        paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
                }
            }
            String[] params = paramNameList.toArray(new String[paramNameList.size()]);

            //然后构造一个mappingHandler,将参数传入进去
            MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);

            //最后把构造好的mappingHandler放到handler管理器的静态属性中
            HandlerManager.mappingHandlerList.add(mappingHandler);

        }
    }
}

写好之后呢,就可以在DispatcherServlet中使用Handler了,做法如下:

修改其中的Service方法:

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList){
            try {
                if (mappingHandler.handle(req, res)){
                    return;
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }

        }
    }

handle方法去MappingHandler类中实现:

  public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
        //将serlvetRequest转化为HttpServletRequest
        String requesturi = ((HttpServletRequest) req).getRequestURI();
        //判断uri和handler中的uri是否相等
        if(!uri.equals(requesturi)){
            return false;
        }
        //如果相等,则调用method的方法
        Object[] parameters = new Object[args.length];

        //通过参数名依次从servletRequest中获取这些参数
        for (int i = 0; i < args.length; i++){
            parameters[i] = req.getParameter(args[i]);
        }

        Object ctl = BeanFactory.getBean(controller);
        //因为controller可能会是多种类型,所以用Object类来存储结果
        //Object ctl = controller.newInstance();
        Object response = method.invoke(ctl, parameters);

        //将方法返回的结果放到servletResponse中去
        res.getWriter().println(response.toString());

        return true;
    }

最后,不要忘记在框架入口类MiniApplication中调用HandlerManager,初始化所有的MappingHandler。

HandlerManager.resolveMappingHandler(classList);

进行打包测试,浏览器访问后即可请求到“1000”,成功。

以上就完成了Context,starter以及WebMVC等功能,下一篇文章会接着实现core模块中的Bean对象部分,敬请期待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值