Springboot简码V16-V20

63 阅读10分钟

V16部分

利用反射机制重构DispatcherServlet,使得将来添加新的业务时DispatcherServlet
不必再添加分支判断(不进行修改)

实现:
1:新建包com.webserver.annotation
2:在annotation包下添加两个注解
@Controller:用于标注哪些类是处理业务的Controller类
@RequestMapping:用于标注处理某个业务请求的业务方法
3:将com.webserver.controller包下的所有Controller类添加注解@Controller
并将里面用于处理某个业务的方法标注@RequestMapping并指定该方法处理的请求
4:DispatcherServlet在处理请求时,先扫描controller包下的所有Controller类
并找到处理该请求的业务方法,使用反射调用.

DispatcherServlet

package com.webserver.core;

import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.controller.Usercontroller;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;

public class DispatcherServlet {
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File root;
    private static File staticDir;

    static {
        try {
            root = new File(
                   DispatcherServlet.class.getClassLoader().getResource(".").toURI()
            );
            staticDir = new File(root,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }
    private DispatcherServlet(){}

    public static DispatcherServlet getInstance(){
        return instance;
    }

    public void service(HttpServletRequest request, HttpServletResponse response) {
        //判断用户的请求路径不应当含有参数部分。所以uri不适用。
        String path = request.getRequestURI();
        //判断是否为请求业务
        /*
            当我们得到本次请求路径path的值后,我们首先要查看是否为请求业务:
            1:扫描controller包下的所有类
            2:查看哪些被注解@Controller标注的过的类(只有被该注解标注的类才认可为业务处理类)
            3:遍历这些类,并获取他们的所有方法,并查看哪些时业务方法
              只有被注解@RequestMapping标注的方法才是业务方法
            4:遍历业务方法时比对该方法上@RequestMapping中传递的参数值是否与本次请求
              路径path值一致?如果一致则说明本次请求就应当由该方法进行处理
              因此利用反射机制调用该方法进行处理。
            5:如果扫描了所有的Controller中所有的业务方法,均未找到与本次请求匹配的路径
              则说明本次请求并非处理业务,那么执行下面请求静态资源的操作
         */
        try {
            File dir = new File(
                    DispatcherServlet.class.getClassLoader().getResource("./com/webserver/controller").toURI()
            );
            File[] subs = dir.listFiles(f -> f.getName().endsWith(".class"));
            for (File sub : subs){
                String fileName = sub.getName();
                String className = fileName.substring(0,fileName.indexOf("."));
                Class cls = Class.forName("com.webserver.controller"+className);
                //是否为@Controller标注的类
                if (cls.isAnnotationPresent(Controller.class)){
                    Method[] method = cls.getDeclaredMethods();
                    for (Method methods : method){
                        if (methods.isAnnotationPresent(RequestMapping.class)){
                            RequestMapping rm = methods.getAnnotation(RequestMapping.class);
                            String value = rm.value();
                            if (path.equals(value)){
                                Object obj = cls.newInstance();
                                methods.invoke(obj,request,response);
                                return;
                            }
                        }
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

            File file = new File(staticDir, path);

            if (file.isFile()) {//判断请求的文件真实存在且确定是一个文件(不是目录)
                response.setContentFile(file);
            } else {//404情况
                response.setStatusCode(404);
                response.setStatusReason("NotFound");
                file = new File(staticDir, "404.html");
                response.setContentFile(file);
                response.addHeader("Content-Type", "text/html");
                response.addHeader("Content-Length", "" + file.length());
            }
            response.addHeader("Server", "BirdServer");
    }
}

v16结束


v17 DispatcherServlet

  • 将DispatcherServlet中的判断是否为请求业务的逻辑代码,放在新创建的类HandlerMapping中,将被注解的全部方法对应的请求路径装在Map集合中。通过输入路径就可以调取相应的业务方法。
  • 在DispatcherServlet中根据请求路径判断是否为请求业务,如果根据路径能够找到业务方法,则实列化业务逻辑类,并调用业务处理方法。

HandlerMapping

package com.webserver.core;

import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;

import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 用于维护所有请求路径与对应的业务处理类Controller的处理方法
 */
public class HandlerMapping {
    /*
        key:请求路径(方法上的注解@RequestMapping中的参数值)
        value:处理该请求的方法(某Controller的一个方法)
     */
    private static Map<String, Method> mapping = new HashMap<>();

    static{
        initMapping();
    }

    private static void initMapping(){
        try {
            /*
                定位引入当前BirdBoot项目类的启动类所在的包
                实际上SpringBoot的约定时:提供的所有Controller所在包至少要放到启动类所在的包里
             */
            File root = new File(
                    BirdBootApplication.primarySource.getResource(".").toURI()
            );
            //定位启动类所在目录下的controller目录(所有Controller类都应当在这里)
            File dir = new File(root,"controller");
            if(!dir.exists()){//controller目录不存在则不进行初始化
                return;
            }
            File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
            for(File sub : subs){
                String fileName = sub.getName();
                String className = fileName.substring(0,fileName.indexOf("."));
                //由于约定所有Controller类都要在名为controller的包中,而这个包必须与启动类在同一个包里
                String packageName = BirdBootApplication.primarySource.getPackage().getName();
                Class cls = Class.forName(packageName+".controller."+className);
                //是否为@Controller标注的类
                if(cls.isAnnotationPresent(Controller.class)){
                    Method[] methods = cls.getDeclaredMethods();
                    for(Method method : methods){
                        //是否该方法被@RequestMapping标注
                        if(method.isAnnotationPresent(RequestMapping.class)){
                            RequestMapping rm = method.getAnnotation(RequestMapping.class);
                            String value = rm.value();
                            //将注解参数作为key,该方法对象作为value存储
                            mapping.put(value,method);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据给定的请求路径获取对应的处理方法
     * @param path  应当与某个Controller中的业务方法上@RequestMapping注解值一致
     * @return  与path匹配的处理方法或null
     */
    public static Method getMethod(String path){
        return mapping.get(path);
    }

    public static void main(String[] args) {
        Method m = mapping.get("/regUser");
        System.out.println(m);
    }

}

DispatcherServlet

package com.webserver.core;


import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;

/**
 * DispatcherServlet实际是由SpringMVC框架提供的一个类,用于和Tomcat整合并负责
 * 接手处理请求的工作。
 * <p>
 * Servlet是JAVA EE里的一个接口,译作:运行在服务端的小程序
 * Servlet中有一个重要的抽象方法:
 * public void service(HttpServletRequest request,HttpServletResponse response)
 * 该方法用于处理某个服务
 * <p>
 * SpringMVC框架提供的DispatcherServlet就实现了该接口并重写了service方法,那么与Tomcat整合后,Tomcat在处理
 * 请求的环节就可以调用DispatcherServlet的service方法将请求对象与响应对象传递进去由SpringMVC框架完成处理请求
 * 的操作。
 */
public class DispatcherServlet {
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File root;
    private static File staticDir;

    static {
        try {
            root = new File(
                    DispatcherServlet.class.getClassLoader().getResource(".").toURI()
            );
            staticDir = new File(root, "static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private DispatcherServlet() {
    }

    public static DispatcherServlet getInstance() {
        return instance;
    }

    public void service(HttpServletRequest request, HttpServletResponse response) {
        //判断用户的请求路径不应当含有参数部分。所以uri不适用。
        String path = request.getRequestURI();

        //判断是否为请求业务
        Method method = HandlerMapping.getMethod(path);
        if(method!=null){
            try {
                //通过方法对象可以获取到该方法所属的类的类对象
                Object obj = method.getDeclaringClass().newInstance();
                method.invoke(obj,request,response);
            } catch (Exception e) {
                //若调用某个Controller的方法时出现了异常应当回复浏览器500错误
                response.setStatusCode(500);
                response.setStatusReason("Internal Server Error");
                response.setContentFile(new File(staticDir,"500.html"));
            }
        }else {
            File file = new File(staticDir, path);
            if (file.isFile()) {//判断请求的文件真实存在且确定是一个文件(不是目录)
                response.setContentFile(file);
            } else {//404情况
                response.setStatusCode(404);
                response.setStatusReason("NotFound");
                file = new File(staticDir, "404.html");
                response.setContentFile(file);
            }
        }
        response.addHeader("Server", "BirdServer");
    }
}

V17结束


V18部分

  • 创建线程池,优化线程管理。
  • 线程池
    线程池是线程的管理机制,主要解决两方面问题
    1:重复使用线程
    2:控制线程数量
package com.webserver.core;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BirdBootApplication {
    private ServerSocket serverSocket;
    private ExecutorService threadPool;

    public BirdBootApplication(){
        try {
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            threadPool = Executors.newFixedThreadPool(50);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while (true) {
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动线程来与该客户端交互
                ClientHandler handler = new ClientHandler(socket);
                threadPool.execute(handler);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        BirdBootApplication application = new BirdBootApplication();
        application.start();
    }
}

V18结束


V19部分

  • 为了数据便于管理,将客户端输入的数据放入数据库中,与数据库建立连接-JDBC。
    第一步:注册驱动
    作用是告知java程序即将要连接的数据库厂商品牌(MySQL、Oracle、SQLserver)。
    第二步:获取连接
    打开JVM进程与数据库进程之间的通道,属于进程间通信,重量级。
    第三步:获取数据库操作对象
    专门执行SQL语句的对象。
    第四步:执行SQL语句(DQL、DML)
    第五步:处理查询结果
    只有当第四步执行的是select语句时,才会处理查询后的结果集。
    第六步:释放资源
    使用完毕,一定要及时关闭连接。

JDBC java数据库连接 Java Database Connectivity
JAVA在JDBC中提供一套通用的接口,用于连接和操作数据库。不同的数据库厂商都提供了一套对应的实现类来操作自家提供的
DBMS。而这套实现类也称为连接该DBMS的驱动(Driver)
使用步骤:
1:加载对应数据库厂商提供的驱动(MAVEN添加依赖即可)
2:基于标准的JDBC操作流程操作该数据库
加入依赖代码如下:

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.15</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.21</version>
    </dependency>
</dependencies>
  • 当调用Connection的方法:prepareStatement(String sql)
    就会先将预编译SQL发送给数据库,使之生成一个执行计划。
    意思:当数据库接收到这条SQL时就会理解该SQL的作用,并生成一个内置的函数用来执行SQL表达的操作
    准备后并不会立即执行,因为还缺少数据(预编译SQL中?部分是未知的)
    当执行计划生成后,数据库方对于该SQL的语义就定死了。
    方法返回的PreparedStatement实例表示的就是数据库端已经生成的执行计划。
    我们现在需要做得就是通过PreparedStatement传递"?"对应的值就可以让数据库执行一次该执行计划。

  • UserController中处理业务逻辑代码将输出到users文件中,改为与数据库进行交互。

    • 注册逻辑:
      首先使用预编译查询数据库中是否有重复用户名,如有重复,则响应用户已存在页面,否则使用预编译sql将请求中的数据放入数据库中,响应注册成功页面。
    • 登录逻辑:
      使用预编译文件判断接收到的用户名和密码,与数据库是否一致,若成功,则响应登陆成功页面,若失败,则响应登陆成功页面。

    DBUtil

package com.webserver.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 管理数据库连接
 */
public class DBUtil {
    static {
        try {
            //DBUtil第一次被使用时先加载数据库驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/birdboot?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true","root","root");
    }
}

UserController

package com.webserver.controller;

import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.entity.User;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import com.webserver.util.DBUtil;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URISyntaxException;
import java.sql.*;

/**
 * 用于处理用户相关业务
 */
@Controller
public class UserController {

    @RequestMapping("/regUser")
    public void reg(HttpServletRequest request, HttpServletResponse response){
        System.out.println("开始处理用户注册!!!!!!!!!!");
        //对应的是reg.html页面上<input name="username">
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String nickname = request.getParameter("nickname");
        String ageStr = request.getParameter("age");
        //必要验证工作
        if(username==null||username.isEmpty()||password==null||password.isEmpty()||
                nickname==null||nickname.isEmpty()||ageStr==null||ageStr.isEmpty()||
                !ageStr.matches("[0-9]+")){
            //要求浏览器查看错误提示页面
            response.sendRedirect("/reg_info_error.html");
            return;

        }
        System.out.println(username+","+password+","+nickname+","+ageStr);
        int age = Integer.parseInt(ageStr);
        //2
        /*
            将该注册用户插入到数据库userinfo表中。
            插入成功后,响应注册成功页面
         */
        try (
                Connection conn = DBUtil.getConnection();
        ){
            //首先判断userinfo包中是否已经用该用户名的记录?如果有,则直接提示用户:have_user.html
            /*
                1:首先执行DQL语句取userinfo下寻找是否有该用户的记录
                2:判断查询结果集是否存在一条记录,若有则说明为重复用户
             */
            String sql1 = "SELECT username FROM userinfo WHERE username=?";
            PreparedStatement ps = conn.prepareStatement(sql1);
            ps.setString(1,username);
            ResultSet rs = ps.executeQuery();
            if(rs.next()){//结果集若存在记录,该用户已存在。
                response.sendRedirect("/have_user.html");
                return;
            }

            String sql2 = "INSERT INTO userinfo(username,password,nickname,age) " +
                  "VALUES (?,?,?,?)";
            ps = conn.prepareStatement(sql2);
            ps.setString(1,username);
            ps.setString(2,password);
            ps.setString(3,nickname);
            ps.setInt(4,age);
            int sum = ps.executeUpdate();
            if(sum>0){
                response.sendRedirect("/reg_success.html");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }


    }

    @RequestMapping("/loginUser")
    public void login(HttpServletRequest request,HttpServletResponse response){
        System.out.println("开始处理登录!!!");
        /*
            1:通过request获取表单信息
            2:验证表单信息是否输入有误,有误则跳转输入有误的提示页面
            3:连接数据库去userinfo表中根据输入的用户名和密码检索记录
              3.1:若查询出记录,则表明登录成功
              3.2:若没有查询出记录,则登录失败
         */
        //1
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        //2
        if(username==null||username.isEmpty()||password==null||password.isEmpty()){
            response.sendRedirect("/login_info_error.html");
            return;
        }
        //3
        try (
                Connection conn = DBUtil.getConnection();
        ){
            /*
                username=123123
                password=123' OR '1'='1

                当将上述的密码进行拼接SQL后,发现语法含义被改变了。
                这样的情况被称为【SQL注入攻击】

                String sql = "SELECT id,username,password,nickname,age " +
                             "FROM userinfo " +
                             "WHERE username='"+username+"' AND password='"+password+"'";

                SELECT id,username,password,nickname,age
                FROM userinfo
                WHERE username='123123' AND password='123' OR '1'='1'

                将数据拼接到SQL中有两个弊端:
                1:有SQL注入攻击的安全隐患
                2:编码复杂,可读性差
             */

            String sql = "SELECT id,username,password,nickname,age " +
                         "FROM userinfo " +
                         "WHERE username=? AND password=?";
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1,username);
            ps.setString(2,password);
            ResultSet rs = ps.executeQuery();
            if(rs.next()){
                response.sendRedirect("/login_success.html");
            }else{
                response.sendRedirect("/login_fail.html");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }


    }
}

V19结束


V20部分

  • DBUtil管理数据库类中,设置阿里提供的连接池,对jdbc进行相应的设置。

DBUtil

package com.webserver.util;

import com.alibaba.druid.pool.DruidDataSource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 管理数据库连接
 */
public class DBUtil {
    //阿里提供的连接池(重复使用连接,管理连接数量)
    private static DruidDataSource dds;
    static {
        init();
    }
    private static void init(){
        dds = new DruidDataSource();//实例化连接池
        //设置数据库用户名,密码。最大连接数,初始连接数。连接数据的URL
        dds.setUsername("root");
        dds.setPassword("root");
        dds.setUrl("jdbc:mysql://localhost:3306/birdboot?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true");
        dds.setInitialSize(10);//初始连接数
        dds.setMaxActive(20);//最大连接数
    }

    public static Connection getConnection() throws SQLException {
        /*
            连接池返回的Connection是对真实数据库连接的2次封装,它返回这个连接的close方法不是真将的连接关闭,
            而是相当于将连接还给连接池。
         */
        return dds.getConnection();
    }
}

V20结束


Springboot简码项目结束 😁