一、原生MVC服务框架

1、组件化重构

(1)分而治之

将大问题分解成小问题

  • 视图View:用户界面的呈现方式,通常由HTML、CSS和JavaScript等前端技术组成,用于展示数据和与用户进行交互。
  • 视图解析器ViewResolver:一个用于查找并解析视图的组件。将视图查找和渲染算法抽取这个类中更有利于软件维护。
  • 控制器 Controller: MVC 设计模式中的一个组件,它负责接收客户端发来的请求,并根据请求中的参数或路径,决定调用哪个业务逻辑处理程序或模型进行处理,然后将处理结果返回给客户端。
  • 控制器映射 HandlerMapping: MVC中的一个关键组件,它的作用是将请求映射到相应的处理器(即控制器)上。

拆分MVC组件

2、视图和视图解析器

动态化的网页写在java中不方便,应该将html进行模版化,将可能会变化的数据用${}括起来

/**
 * @document: 视图解析器,把Thymeleaf模版文件转换为HTML文件
 * @Author:SmallG
 * @CreateTime:2023/8/18+15:37
 */

public class ViewResolver {
    private static ViewResolver instance = new ViewResolver();
    private TemplateEngine templateEngine;

    private ViewResolver() {
        //初始化模版引擎
        templateEngine = new TemplateEngine();
        //基于Thymeleaf的类加载器
        ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
        //加载模版文件目录
        resolver.setPrefix("/template/");
        templateEngine.setTemplateResolver(resolver);
    }

    public static ViewResolver getInstance() {
        return instance;
    }

    /**
     * 定义一个渲染模版的方法
     *
     * @param view  视图模版文件
     * @param model 数据集合
     */
    public String render(String view, Map<String, Object> model) {
        Context context = new Context();
        //替换数据
        context.setVariables(model);
        return templateEngine.process(view, context);
    }
}

3、控制器

Web应用程序的核心组件,它负责接收并处理客户端请求,根据请求的URI来调用相应的业务逻辑,并将处理结果封装为数据模型。控制器的主要职责是将请求和业务逻辑分离,使得代码更加清晰和易于维护。

控制器

使用注解和反射实现控请求路由

引入两个自定义的注解:@Controller 和 @RequestMapping。

  1. @Controller 注解用于标记控制器类,表示该类是一个控制器,负责处理请求和业务逻辑。
  2. @RequestMapping 注解用于标记控制器类中的方法,指明处理的请求路径。
  3. 在控制器类中,我们可以使用 @Controller 注解标记类,并在需要处理请求的方法上使用 @RequestMapping 注解指明对应的请求路径。
  4. 在 DispatcherServlet 中利用反射API实现路由功能。

请求路由

/**
 * @document:
 * @Author:SmallG
 * @CreateTime:2023/8/18+17:02
 */
@Controller //声明这是一个控制器类
public class AccountController {
    /**
     * 注册
     *
     * @param request  请求
     * @param response 响应
     * @param model    请求参数
     * @return 模版页面
     */
    @RequestMapping("/accounts/register") //声明请求地址访问的方法
    public String register(HttpServletRequest request,
                           HttpServletResponse response,
                           Map<String, Object> model) {
        //获取注册数据
        String username = request.getParameter("username");
        String gender = request.getParameter("gender");
        String email = request.getParameter("email");
        String hometown = request.getParameter("hometown");
        //把注册数据存入model对象中
        model.put("username", username);
        model.put("gender", gender);
        model.put("email", email);
        model.put("hometown", hometown);
        return "templateRegister.html";
    }

    /**
     * 登录
     *
     * @param request  请求
     * @param response 响应
     * @param model    请求参数
     * @return 模版页面
     */
    @RequestMapping("/accounts/login") //声明请求地址访问的方法
    public String login(HttpServletRequest request,
                        HttpServletResponse response,
                        Map<String, Object> model) {
        //获取登录数据
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        //把注册数据存入model对象中
        model.put("username", username);
        model.put("password", password);

        return "template.html";
    }

}

4、映射处理器

抽取HandlerMapping

/**
 * @document: 根据用户请求的uri,查找匹配的处理器
 * 找到那些类加了@Controller注解和那些方法加了@RequestMapping注解
 * @Author:SmallG
 * @CreateTime:2023/8/19+14:09
 */

public class HandlerMapping {
    private static HandlerMapping instance = new HandlerMapping();
    //保存uri所对应的方法 保存加了@RequestMapping注解的方法
    private Map<String, Method> handlers = new HashMap<>();

    //私有化构造方法
    private HandlerMapping() {
        //加载所有的控制器类(具有注解@Controller的类)
        try {
            loadHandlers();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    //提供获取当前类的对象的方法
    public static HandlerMapping getInstance() {
        return instance;
    }

    //根据用户请求的uri,获取需要调用的方法
    public Method getHandlerMethod(String uri){
        return handlers.get(uri);
    }

    //加载所有的加了@RequestMapping注解的方法
    private void loadHandlers() throws URISyntaxException, ClassNotFoundException {
        //找到控制器所在的包
        File dir = new File(HandlerMapping.class
                .getClassLoader().getResource("cn/highedu/server01").toURI());
        //加载这个包里的所有的class文件
        File[] files = dir.listFiles(file -> file.getName().endsWith(".class"));
        //遍历数组逐个查看class文件 是否包含@Controller注解
        for (File file : files) {
            System.out.println("找到class文件:" + file);
            //获取文件名(*.class)
            String fileName = file.getName();
            //获取类名
            String className = fileName.substring(0, fileName.lastIndexOf("."));
            //获取类的对象
            Class cla = Class.forName("cn.highedu.server01." + className);
            //判断这个类是否有controller注解
            if (cla.isAnnotationPresent(Controller.class)) {
                System.out.println("找到标注了Controller注解的类" + cla);
                //获取这个控制器类里所有的方法
                Method[] methods = cla.getDeclaredMethods();
                //循环遍历查看是否有@RequstMapping注解
                for (Method method : methods) {
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        System.out.println("找到了标注@RequstMapping注解的方法:" + method);
                        //获取注解的value值
                        //先获取注解
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        String value = requestMapping.value();
                        //把uri和方法保存到handlers集合中
                        handlers.put(value, method);
                    }
                }
            }
        }
    }
}

二、线程池和性能优化

1、线程池

**线程池有5个重要的参数,包括:

  • 核心线程数(Core Pool Size):线程池中最小的线程数量。即使线程池中没有任务要执行,核心线程也会一直存在,不会被销毁。
  • 最大线程数(Maximum Pool Size):线程池中最大的线程数量。当任务数量超过核心线程数时,线程池会创建新的线程,直到达到最大线程数。
  • 任务队列(Work Queue):用于保存待执行的任务的队列。当线程池中的线程都在执行任务且任务数量达到最大线程数时,新提交的任务会被放入任务队列中等待执行。
  • 空闲线程存活时间(Keep-Alive Time):当线程池中的线程数量超过核心线程数,且空闲时间达到指定时间时,多余的线程会被销毁,保持线程池的大小不超过核心线程数。
  • 拒绝策略(Rejected Execution Handler)。拒绝策略定义了当线程池已满并且任务队列也已满时,如何处理新提交的任务。

自定义参数创建Java线程池

我们创建一个自定义线程池CustomThreadPool,使用ThreadPoolExecutor类来实现。在这里,我们设置了以下参数:

  • corePoolSize:将核心线程数设置为5,表示线程池中始终保持5个线程在运行。这些线程会一直存在,即使它们处于空闲状态,以便快速响应新任务的到来。
  • maximumPoolSize:将最大线程数设置为10,表示线程池允许创建的最大线程数为10。当任务数量超过核心线程数,并且任务队列已满时,线程池将创建新的线程,但不会超过最大线程数。
  • keepAliveTime:将线程空闲时间设置为5000毫秒,表示如果线程在空闲时间内没有任务可执行,那么它将被终止以释放资源。
  • queueCapacity:将任务队列容量设置为20,表示线程池可以同时接受的最大任务数为20。当任务数量超过核心线程数时,新的任务会被放入任务队列中等待执行。
  • ThreadFactory:使用ThreadFactoryBuilder创建线程工厂,并设置线程名称格式为"CustomThreadPool-%d",方便我们在日志中识别不同的线程。
  • RejectedExecutionHandler:使用AbortPolicy拒绝策略,表示当任务队列和线程池都已满时,直接抛出异常。这样可以避免任务被丢弃,而是让调用者进行适当的处理。
/**
 * @document:
 * @Author:SmallG
 * @CreateTime:2023/8/19+15:36
 */

public class CustomerThreadPool {
    public static void main(String[] args) {
        // 定义线程池参数
        int corePoolSize = 5; //线程的核心线程数
        int maximumPoolSize = 10; //线程池的最大线程数
        long keepAliveTime = 5000; //线程最大空闲时间
        int queueCapacity = 20; //队列容量

        // 创建自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                //任务队列,使用LinkedBlockingDeque,线程池的最大容量
                new LinkedBlockingDeque<>(queueCapacity),
                //线程工厂 使用ThreadFactoryBuilder 线程的名字CustomerThreadPool-%d
                new ThreadFactoryBuilder().setNameFormat("CustomerThreadPool-%d").build(),
                //拒绝策略 如果线程池不够了就使用AbortPolicy直接抛出异常
                new ThreadPoolExecutor.AbortPolicy()
        );
        for (int i = 0; i < 30; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "在运行" + taskId);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        //任务结束,关闭线程池
        executor.shutdown();
    }
}

2、性能优化

/**
 * @document: 接收客户端的HTTP请求
 * @Author:SmallG
 * @CreateTime:2023/8/16+10:43
 */

public class ServerBootApplication {
    private ServerSocket serverSocket;
    private ThreadPoolExecutor executor;  //定义线程池

    /**
     * 初始化线程池方法
     */
    private void initThreadPool() {
        //定义线程池参数
        int corePoolSize = 10; //核心线程数
        int maximumPoolSize = 50; //最大线程数
        long keepAliveTime = 5000; //非核心线程最大等待时间,超时则销毁
        int queueCapacity = 20; //等待队列的大小
        //创建线程池
        executor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque<>(queueCapacity),
                new ThreadFactoryBuilder().setNameFormat("WebServer-%d").build(),
                new ThreadPoolExecutor.AbortPolicy() //拒绝策略 抛异常
        );
    }

    public void start() {
        try {
            //先初始化线程池
            initThreadPool();
            // 创建ServerSocket对象,监听8088端口
            serverSocket = new ServerSocket(8088);
            while (true) {
                System.out.println("等待客户端连接...");
                // 等待客户端连接
                Socket socket = serverSocket.accept();

                //创建线程对象
                ClientHandler handler = new ClientHandler(socket);
                //提交任务
                executor.submit(handler);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ServerBootApplication application = new ServerBootApplication();
        //启动服务器
        application.start();
    }
}

三、校园管理系统

到此我们已经基本打造了原生Web服务框架,这是一个基于 Java 的Web框架,它使用模型-视图-控制器(MVC)的架构模式,可以帮助我们更好地组织和管理Web应用程序的代码。

原生Web框架的完整请求处理流程如下:

1、客户端向服务器发送一个HTTP请求,这个请求首先被ServerSocket接收到,并且创建了对应线程任务ClientHandler,然后将线程任务提交给线程池执行。

2、线程负责处理HTTP请求及响应:首先创建HttpServletRequest解析请求消息,然后创建了HttpServletResponse缓存响应信息,创建负责请求流程的DispatcherServlet对象,执行其service方法处理请求,最后HttpServletResponse对象负责发送响应消息。

3、通过 DispatcherServlet对请求进行详细处理:DispatcherServlet 是原生Web框架的核心组件,它作为中央调度器协调所有的请求处理过程。

四、练习

1 编写视图解析器(View Resolver)

视图解析器(View Resolver)是一个用于查找并解析视图的组件,编写要求如下:

1、视图解析器采用单例模式;

2、视图解析器中的 render() 方法,根据传入的视图名和模型数据,读取对应的模板文件,进行替换并渲染出 HTML 代码,然后返回渲染后的 HTML;

3、视图解析器提供一个 main() 方法用于测试视图解析器的功能,传入一些模型数据,输出渲染后的 HTML 代码。

提示:

练习1

编写被解析的视图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>地址信息</title>
</head>
<body>
${user}的收货地址是${address}
</body>
</html>

添加视图解析器和测试案例

/**
 * @document: 视图解析器
 * @Author:SmallG
 * @CreateTime:2023/8/21+8:27
 */

public class ViewResolver {
    //单例模式中的饿汉式
    private static ViewResolver instance = new ViewResolver();

    private ViewResolver() {
    }

    public static ViewResolver getInstance() {
        return instance;
    }

    private static File root;
    private static File templateDir; //路径

    static {
        try {
            //类加载路径:target/classes
            root = new File(
                    ViewResolver.class.getClassLoader().getResource(".").toURI());
            templateDir = new File(root, "templates");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    public String render(String view, Map<String, Object> model) throws IOException {
        File file = new File(templateDir, view);
        byte[] bytes = new byte[(int) file.length()];
        FileInputStream in = new FileInputStream(file);
        in.read(bytes);
        in.close(); //关闭输入流
        String html = new String(bytes, StandardCharsets.UTF_8);
        for (Map.Entry<String, Object> entry : model.entrySet()) {
            String key = entry.getKey();
            //避免空指针异常
            String value = entry.getValue() + "";
            //正则表达式
            String tag = "\\$\\{" + key + "\\}";
            //替换模版
            html = html.replaceAll(tag, value);
        }
        return html;
    }

    /**
     * 模版渲染测试方法
     */
    public static void main(String[] args) throws IOException {
        HashMap<String, Object> model = new HashMap<>();
        model.put("user", "SmallG");
        model.put("address", "河北");
        String html = getInstance().render("address.html", model);
        System.out.println(html);
    }
}

2 编写HandlerMapping类

HandlerMapping是MVC中的一个关键组件,它的作用是将请求映射到相应的处理器(即控制器)上。编写要求:

1、定义HandlerMapping的单例类;

2、加载所有 cn.highedu.boot.controller 包下的类,并在这些类中查找 @Controller 注解,以及这些类中的 @RequestMapping 注解的方法,并将它们添加到 HashMap<String, Method> handlers 中;

3、getHandlerMethod 方法接受一个 URI,返回与之匹配的处理方法。如果找不到匹配的处理器,则会返回一个null。

编写注解:

/**
 * @document: 用于注解控制器
 * @Author:SmallG
 * @CreateTime:2023/8/21+18:04
 */

@Target(ElementType.TYPE) //用于注解在类上
@Retention(RetentionPolicy.RUNTIME) //用于反射
public @interface Controller {
}
/**
 * @document: 用于映射请求路径到控制器方法
 * @Author:SmallG
 * @CreateTime:2023/8/21+18:06
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value();
}

注解标注UserController

/**
 * @document:
 * @Author:SmallG
 * @CreateTime:2023/8/21+18:03
 */

@Controller
public class UserController {
    @RequestMapping("/register")
    public String register(HttpServletRequest request,
                           HttpServletResponse response,
                           Map<String, Object> model) {
        //获取请求
        String name = request.getParameter("name");
        String gender = request.getParameter("male");
        //注册逻辑
        model.put("name", name);
        model.put("gender", gender);
        return "register.html";
    }

    @RequestMapping("/submit")
    public String submit(HttpServletRequest request,
                         HttpServletResponse response,
                         Map<String, Object> model) {
        String name = request.getParameter("name");
        String gender = request.getParameter("male");
        //表单提交测试逻辑
        model.put("name", name);
        model.put("gender", gender);
        return "view.html";
    }
}

定义HandlerMapping

/**
 * HandlerMapping会根据请求的URL和其他相关信息,查找匹配的处理器
 * 如果找到匹配的处理器,则HandlerMapping会将其返回给DispatcherServlet进行进一步处理
 * 如果找不到匹配的处理器,则会返回一个null
 */

public class HandlerMapping {
    //创建HandlerMapping的唯一实例 饿汉模式
    private static HandlerMapping instance = new HandlerMapping();

    //获取HandlerMapping的实例
    public static HandlerMapping getInstance() {
        return instance;
    }

    //私有构造函数,只能通过getInstance()获取实例
    private HandlerMapping() {
        try {
            loadHandlers();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    //保存处理器的URI和对应的方法
    private HashMap<String, Method> handlers = new HashMap<>();

    //加载所有的处理器
    private void loadHandlers() throws URISyntaxException, ClassNotFoundException {
        //获取处理器所在目录
        File dir = new File(HandlerMapping.class
                .getResource("/cn/highedu/boot/controller").toURI());
        //获取目录下的所有类文件
        File[] files = dir.listFiles(file -> file.getName().endsWith(".class"));
        for (File file : files) {
            //获取类名
            String fileName = file.getName();
            String className = fileName.substring(0, fileName.lastIndexOf("."));
            //加载类
            Class clazz = Class.forName("cn.highedu.boot.controller." + className);
            //如果类上有@Controller注解
            if (clazz.isAnnotationPresent(Controller.class)) {
                //遍历类中所有的方法
                for (Method method : clazz.getDeclaredMethods()) {
                    //如果方法上有@RequestMapping注解
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        //获取@RequestMapping注解中的值,uri
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        String path = requestMapping.value();
                        //将URI和方法保存到handlers中
                        handlers.put(path,method);
                    }
                }
            }
        }
    }
    public Method getHandlerMethod(String uri){
        return handlers.get(uri);
    }
    /**
     * 测试案例
     */
    public static void main(String[] args) {
        HandlerMapping mapping = getInstance();
        System.out.println(mapping.handlers);
        Method method = mapping.getHandlerMethod("/register");
        System.out.println(method);
    }
}

3 编写使用自定义参数创建 Java 线程池的示例

线程池参数要求如下:

  • 线程池核心线程数 3
  • 线程池最大线程数 20
  • 线程空闲时间 20秒
  • 等待队列容量 50
  • 使用LinkedBlockingQueue 作为等待队列
  • 自定义线程名称为 "Worker=%d"
  • 拒绝策略为:AbortPolicy当工作队列已满,且线程池的线程数已经达到最大值时,抛出一个异常
  • 创建50个线程任务进行测试,任务结束后关闭线程池
/**
 * @document: 编写使用自定义参数创建 Java 线程池
 * @Author:SmallG
 * @CreateTime:2023/8/21+18:51
 */

public class CustomThreadPool {
    public static void main(String[] args) {
        //定义线程池参数
        int corePoolSize = 3;
        int maximumPoolSize = 20;
        long keepAliveTime = 20;
        int queueCapacity = 50;

        //创建自定义线程
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(queueCapacity),
                new ThreadFactoryBuilder().setNameFormat("Worker-%d").build(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        //向线程池提交任务
        for (int i = 1; i <= 50; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName()
                        + " is running task" + taskId);
            });
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //关闭线程池
        executor.shutdown();
    }
}

4 编写原生MVC服务框架其他组件

要求:

1、使用注解标注控制器类和方法;

2、使用HandlerMapping查找URL对应的控制器方法;

3、使用ViewResolver解析视图;

4、使用MVC框架实现注册和登录功能,账户数据采用对象序列化方式保存到文件中。

最后修改:2023 年 08 月 22 日
如果觉得我的文章对你有用,请随意赞赏