Java后端发展历程

前言

java后端经过多年的发展,涉及的规范、概念、框架非常多,目前使用广泛的开发模式中涉及的技术暴漏给开发者的API封装层次较高,以下是从spring快速入门文档中摘取的hello-world代码,

@SpringBootApplication
@RestController
public class DemoApplication {
    @GetMapping("/helloworld")
    public String hello() {
        return "Hello World!";
    }
}

看起来短短十行代码一个web server就跑起来了但背后却经历了很多过程。站在业务开发视角,侧重快速落地、尽早的提供业务价值,因此API的高度封装是很有必要的,但对于学习和排查问题来说就不友好了,既然如此就让我们回到了90年代java刚出现时,顺着时间线结合当时的背景,每碰到一个技术时猜测推理下它存在的意义是什么以及解决了什么问题,另外思考下如果让我们来做会怎么设计。正所谓“物有本末,事有终始,知所先后,则近道矣”。

servlet

远在java出现之前,HTTP作为客户端和服务端之间请求和应答的标准已被广泛接受,sun公司想让java切入web开发的市场,首先实现一个高性能的http server就是顺理成章的事情了,我们知道http底层是采用TCP协议实现的,使用JDK提供的java.net.Socket可以很容易的实现一个http server,比如下面的实现

package com.example.helloworld;

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class SimpleHTTPServer {
    public static void main(String[] args) throws Throwable {
        ServerSocket serverSocket = new ServerSocket(8080);
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (;;) {
            final Socket socket = serverSocket.accept();
            threadPool.execute(() -> {
                OutputStream os = null;
                InputStream is = null;
                try {
                    is = socket.getInputStream();
                    os = socket.getOutputStream();
                    byte[] bytes = new byte[is.available()];
                    int result = is.read(bytes);
                    if (result != -1)
                        System.out.println(new String(bytes));

                    String body = "<html><body><h1>Hello world!</h1></body></html>";
                    String response = "HTTP/1.1 200 OK\r\n" +
                            "Content-Length: " + body.getBytes().length + "\r\n" +
                            "Content-Type: text/html\r\n" +
                            "\r\n" +
                            body + "\r\n";
                    os.write(response.getBytes());
                    os.flush();
                    socket.shutdownInput();
                    socket.shutdownOutput();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

,有了tcp的基础能力大家八仙过海各显神通写出了很多http server出来,不同厂商的实现之间API互相不兼容,谁也不服谁也干不掉谁。项目一旦选择了某个实现后再想换实现就很难了,这个时候sun公司出面说"这样玩可不行打不过隔壁家PHP和ASP啊,我来定义一组java接口,只要实现这一组接口其它的大家随意怎么玩",然后给这一组java接口起了个名字叫Servlet规范 于是乎大家拿着sun公司给的Servlet API 文档开心的回家造轮子去了,比较出名的轮子有apache-tomcat、jboss、weblogic等。

我们来写一个小demo来看下怎么用的,创建hello-servlet工程编写pom.xml文件如下,引入Servlet API的jar包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example.jwl</groupId>
    <artifactId>hello-servlet</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>hello-servlet</finalName>
    </build>
</project>

写一个只处理GET请求的HelloServlet

package com.example.helloservlet;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        PrintWriter out = resp.getWriter();
        out.write("<html><body><h1>Hello world!</h1></body></html>");
        out.flush();
    }
}

在src/main/webapp/WEB-INF目录下,创建web.xml,做如下的配置

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Hello servlet</display-name>
    <servlet>
        <servlet-name>helloServlet</servlet-name>
        <servlet-class>com.example.helloservlet.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>helloServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

在hello-servlet工程根目录下执行mvn clean package命令,在target目录中找到最终产物hello-servlet.war,我们来看下文件中包含哪些内容:

image.png

我们使用tomcat 8.5版本来跑下hello-servlet.war,下载tomcat并解压(点我下载apache-tomcat-8.5.81.tar.gz),进入tomcat目录使用sh bin/startup.sh启动服务

image.png

服务启动成功后,把hello-servlet.war放入tomcat的webapps目录下,等待一会tomcat会自动解压&加载hello-servlet.war,尝试访问一下curl -i http://localhost:8080/hello-servlet/,输出的结果就是HelloServlet.java中对应的内容了

image.png

JSP

我们再来看下前面的HelloServlet.java,像这种返回内容较少的网页时拼接字符串还勉强能接受,如果是几百几千行的网页根据业务逻辑动态的拼接,你受得了?

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        PrintWriter out = resp.getWriter();
        out.write("<html><body><h1>Hello world!</h1></body></html>");
        out.flush();
    }
}

于是乎sun公司主导创建了一种动态网页技术标准(JSP),它将Java代码和特定变动内容嵌入到静态的页面中,实现以静态页面为模板,动态生成其中的部分内容,我们来看一个例子,把以下内容放入tomcat的webapps/ROOT/hello.jsp文件里

<html>
    <body>
            <h1>JSP page</h1>
           <%
              out.println("Hello " + request.getParameter("name"));
           %>
    </body>
</html>

等待几秒后访问在浏览器上访问http://localhost:8080/hello.jsp?name=world

image.png

客户端发出请求访问JSP网页时,web容器将要访问的JSP文件转译成Servlet的源代码,然后将产生的Servlet的源代码经过编译,生成.class文件,并加载到内存执行,最后把结果响应返回给客户端。Tomcat 8把为JSP页面创建的Servlet源文件和class类文件放置在apache-tomcat-8.5.81\work\Catalina\localhost\<应用程序名>\目录中,Tomcat将JSP页面翻译成的Servlet的包名为org.apache.jsp(即:apache-tomcat-8.5.81/work/Catalina/localhost/ROOT/org/apache/jsp目录下),在这个目录下会有两个对应的文件,一个是hello_jsp.class文件一个是hello_jsp.java文件

/*
 * Generated by the Jasper component of Apache Tomcat
 * Version: Apache Tomcat/8.5.81
 * Generated at: 2022-08-09 09:45:13 UTC
 * Note: The last modified time of this file was set to
 *       the last modified time of the source file after
 *       generation to assist with modification tracking.
 */
package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  private static final java.util.Set<java.lang.String> _jspx_imports_packages;

  private static final java.util.Set<java.lang.String> _jspx_imports_classes;

  static {
    _jspx_imports_packages = new java.util.HashSet<>();
    _jspx_imports_packages.add("javax.servlet");
    _jspx_imports_packages.add("javax.servlet.http");
    _jspx_imports_packages.add("javax.servlet.jsp");
    _jspx_imports_classes = null;
  }

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set<java.lang.String> getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set<java.lang.String> getClassImports() {
    return _jspx_imports_classes;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    final java.lang.String _jspx_method = request.getMethod();
    if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSP 只允许 GET、POST 或 HEAD。Jasper 还允许 OPTIONS");
      return;
    }

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;

    try {
      response.setContentType("text/html");
      pageContext = _jspxFactory.getPageContext(this, request, response,
                              null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("<html>\n");
      out.write("    <body>\n");
      out.write("            <h1>JSP page</h1>\n");
      out.write("           ");

              out.println("Hello " + request.getParameter("name"));

      out.write("\n");
      out.write("    </body>\n");
      out.write("</html>");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}

上面这段代码就是hello.jsp编译生成的java代码,可以发现jsp就是servlet。

模版引擎

模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的html文档,市面上比较常用的模版引擎有以下这三个

  • Velocity
  • <u>FreeMarker</u>
  • Thymeleaf

本质上JSP也属于模版引擎,与JSP相比提供了更好用的DSL而已,底层的实现原理都是一样的,这里就不再细说了。

前后端分离

无论是使用jsp还是使用模版引擎来开发web服务,模版文件里都包含了代码和html两部分内容,在分工协作中,一般是由前端同学写好静态页面,然后交给后端同学改造成模版文件加上渲染逻辑。在这个开发模式下后端同学需要有一定的前端知识,碰到老的页面改版时,后端同学需要拿最新的静态文件,取增量的内容塞进模版文件中,这样很容易出错,因此在迭代中后端的工作量相对前端来说比例过重。

基于这样的背景有大佬提出了“前后端分离”的理念,核心思想是前端html页面通过 Ajax调用后端的 API 并使用json数据进行交互,页面渲染放在客户端来做,前后端分离可以很好的解决前后端分工不均的问题,将更多的交互逻辑分配给前端来处理,而后端则可以专注于其本职工作,比如提供 API 接口,进行权限控制以及进行运算工作。

JDBC

java语言流行起来后,数据库厂商陆续提供了客户端,比如mysql提供了这样的api

class MysqlConnection {
    void connect();
    void disconnect();
    Result execSql(String sql);
}

oracle提供了这样的api

class OracleClient {
    void start();
    void shutdown();
    Object[][] execute(String sql, String ...params);
}

........

这样就造成了项目一旦选型了某个数据库后,在想更换就非常困难了。sun公司看到了大家这个痛点,于是乎就参考大家的API自己定了一套规范(一堆接口)命名为JDBC,数据库厂商的java客户端只需要实现这些接口就行了参考文档

我们来使用mysql来玩下,首先把表建出来

CREATE DATABASE 'demo';
USE demo;

create table student (
   id  int(3) NOT NULL AUTO_INCREMENT,
   name varchar(120) NOT NULL,
   email varchar(220) NOT NULL,
   PRIMARY KEY (id)
);

加入mysql的驱动包

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
</dependency>

获取数据库连接

protected Connection getConnection() {
    Connection connection = null;
    try {
        Class.forName("com.mysql.jdbc.Driver");
        connection = DriverManager.getConnection(jdbcURL, jdbcUsername, jdbcPassword);
    } catch (SQLException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    return connection;
}

添加记录

public void save(Student stu) {
    try (Connection conn = getConnection();
         PreparedStatement preparedStatement = conn.prepareStatement("insert into student(name, email) values (?,?)");
         PreparedStatement preparedStatement2 = conn.prepareStatement("select LAST_INSERT_ID();");) {
        preparedStatement.setString(1, stu.getName());
        preparedStatement.setString(2, stu.getEmail());
        preparedStatement.executeUpdate();

        ResultSet rs2 = preparedStatement2.executeQuery();
        if (rs2.next()) {
            stu.setId(rs2.getInt(1));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

删除记录

public boolean delete(Student stu) {
    boolean deleted;
    try (Connection connection = getConnection();
         PreparedStatement statement = connection.prepareStatement("delete from student where id = ?");) {
        statement.setInt(1, stu.getId());
        deleted = statement.executeUpdate() > 0;
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
    return deleted;
}

修改记录

public boolean update(Student stu) {
    boolean updated;
    try (Connection connection = getConnection();
         PreparedStatement statement = connection.prepareStatement("update student set name=?,email=? where id=?");) {
        statement.setString(1, stu.getName());
        statement.setString(2, stu.getEmail());
        statement.setInt(3, stu.getId());
        System.out.println(statement);
        updated = statement.executeUpdate() > 0;
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
    return updated;
}

根据ID查询

public Student findById(int id) {
    Student stu = null;
    try (Connection connection = getConnection();
         PreparedStatement preparedStatement = connection.prepareStatement("select name, email from student where id = ?;");) {
        preparedStatement.setLong(1, id);
        ResultSet rs = preparedStatement.executeQuery();
        if (rs.next()) {
            String name = rs.getString("name");
            String email = rs.getString("email");
            stu = new Student(id, name, email);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return stu;
}

示例代码StudentDaoJDBCImpl.java

ORM

一个项目中涉及的表往往有很多张,假如我们要实现一个学生信息管理系统,其中包含学生表、班级信息表,我们来看下相应dao的save方法

学生表

public void save(Student stu) {
    try (Connection conn = getConnection();
         PreparedStatement preparedStatement = conn.prepareStatement("insert into student(name, email) values (?,?)");
         PreparedStatement preparedStatement2 = conn.prepareStatement("select LAST_INSERT_ID();");) {
        preparedStatement.setString(1, stu.getName());
        preparedStatement.setString(2, stu.getEmail());
        preparedStatement.executeUpdate();

        ResultSet rs2 = preparedStatement2.executeQuery();
        if (rs2.next()) {
            stu.setId(rs2.getInt(1));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

班级表

public void save(Class sls) {
    try (Connection conn = getConnection();
         PreparedStatement preparedStatement = conn.prepareStatement("insert into class(student_num, head_teacher_id) values (?,?)");
         PreparedStatement preparedStatement2 = conn.prepareStatement("select LAST_INSERT_ID();");) {
        preparedStatement.setInt(1, stu.getStudentNum());
        preparedStatement.setInt(2, stu.getHeadTeacherId());
        preparedStatement.executeUpdate();

        ResultSet rs2 = preparedStatement2.executeQuery();
        if (rs2.next()) {
            stu.setId(rs2.getInt(1));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

可以看出用jdbc来写数据库交互的时会存在大量模版代码,我们仔细看下这两个save方法有差异的点有两处

  1. sql不一样
insert into student(name, email) values (?,?)
insert into class(student_num, head_teacher_id) values (?,?)
  1. PreparedStatement设置参数
preparedStatement.setString(1, stu.getName());
preparedStatement.setString(2, stu.getEmail());

preparedStatement.setInt(1, stu.getStudentNum());
preparedStatement.setInt(2, stu.getHeadTeacherId());   

如果让我们写一个签名如下的通用save方法,我们应该怎么来做呢?

public static <T> void save(Connection conn, T model);

很容易能想到通过反射来屏蔽差异性,对于对象名来映射表名,通过对象的属性来映射表的字段,通过对象属性的类型来确定调用preparedStatement的setX方法进而映射到表字段的属性,废话不多说上代码

DBUtils.java

package com.example.jwl;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DBUtils {
    public static <T> void save(Connection conn, T model) throws Throwable {
        StringBuilder sqlBuilder = new StringBuilder("insert into ");
        StringBuilder placeholderBuilder = new StringBuilder();
        String sep = "";
        sqlBuilder.append(getTableName(model.getClass()));
        sqlBuilder.append("(");
        for (Field field : model.getClass().getDeclaredFields()) {
            if (field.equals(getPrimaryField(model.getClass()))) {
                continue;
            }
            sqlBuilder.append(sep).append(getFieldName(field));
            placeholderBuilder.append(sep).append("?");
            sep = ",";
        }
        sqlBuilder.append(") values(");
        sqlBuilder.append(placeholderBuilder);
        sqlBuilder.append(")");

        String sql = sqlBuilder.toString();
        System.out.println(sql);
        try (PreparedStatement preparedStatement = conn.prepareStatement(sql);
             PreparedStatement preparedStatement2 = conn.prepareStatement("select LAST_INSERT_ID();");) {
            int idx = 1;
            for (Field field : model.getClass().getDeclaredFields()) {
                if (field.equals(getPrimaryField(model.getClass()))) {
                    continue;
                }
                field.setAccessible(true);
                Object val = field.get(model);
                if (field.getType() == String.class) {
                    preparedStatement.setString(idx++, (String) val);
                } else if (field.getType() == int.class || field.getType() == Integer.class) {
                    preparedStatement.setInt(idx++, (Integer) val);
                } else {
                    //TODO 暂不实现
                }
            }
            System.out.println(preparedStatement);
            preparedStatement.executeUpdate();
            ResultSet rs2 = preparedStatement2.executeQuery();
            if (rs2.next()) {
                Field primaryField = getPrimaryField(model.getClass());
                primaryField.setAccessible(true);
                primaryField.set(model, rs2.getInt(1));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private static Field getPrimaryField(java.lang.Class clazz) throws NoSuchFieldException {
        return clazz.getDeclaredField("id");
    }

    private static String getFieldName(Field field) {
        return field.getName().toLowerCase();
    }

    private static <T> String getTableName(java.lang.Class clazz) {
        return clazz.getSimpleName().toLowerCase();
    }
}

封装一下搬砖的效率大大增加,以前写一坨代码的地方,现在一行搞定

    public void save(Student stu) {
//        try (Connection conn = getConnection();
//             PreparedStatement preparedStatement = conn.prepareStatement("insert into student(name, email) values (?,?)");
//             PreparedStatement preparedStatement2 = conn.prepareStatement("select LAST_INSERT_ID();");) {
//            preparedStatement.setString(1, stu.getName());
//            preparedStatement.setString(2, stu.getEmail());
//            preparedStatement.executeUpdate();
//
//            ResultSet rs2 = preparedStatement2.executeQuery();
//            if (rs2.next()) {
//                stu.setId(rs2.getInt(1));
//            }
//        } catch (SQLException e) {
//            e.printStackTrace();
//        }
        try {
            DBUtils.save(getConnection(), stu);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

我们接着用同样的套路实现下更新、删除、查询

public static boolean update(Connection conn, Object model) throws Throwable;
public static boolean delete(Connection conn, Object model) throws Throwable;
public static <T> findById(Connection conn, Class<T> clazz, int id) throws Throwable;
public static boolean update(Connection conn, Object model) throws Throwable {
    //update student set name=?,email=? where id=?
    boolean updated = false;
    StringBuilder sqlBuilder = new StringBuilder("update ");
    String sep = "";
    sqlBuilder.append(getTableName(model.getClass()));
    sqlBuilder.append(" set ");
    for (Field field : model.getClass().getDeclaredFields()) {
        if (field.equals(getPrimaryField(model.getClass()))) {
            continue;
        }
        sqlBuilder.append(sep).append(getFieldName(field)).append("=?");
        sep = ",";
    }
    sqlBuilder.append(" where id = ?");

    String sql = sqlBuilder.toString();
    System.out.println(sql);
    try (PreparedStatement preparedStatement = conn.prepareStatement(sql);) {
        int idx = 1;
        for (Field field : model.getClass().getDeclaredFields()) {
            if (field.equals(getPrimaryField(model.getClass()))) {
                continue;
            }
            field.setAccessible(true);
            Object val = field.get(model);
            if (field.getType() == String.class) {
                preparedStatement.setString(idx++, (String) val);
            } else if (field.getType() == int.class || field.getType() == Integer.class) {
                preparedStatement.setInt(idx++, (Integer) val);
            } else {
                //TODO 暂不实现
            }
        }
        Field primaryField = getPrimaryField(model.getClass());
        primaryField.setAccessible(true);
        preparedStatement.setInt(idx, (Integer) primaryField.get(model));
        System.out.println(preparedStatement);
        updated = preparedStatement.executeUpdate() > 0;
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return updated;
}

public static boolean delete(Connection conn, Object model) throws Throwable {
    //delete from student where id = ?
    boolean deleted = false;
    String sql = "delete from " + getTableName(model.getClass()) + " where id = ?";
    Field primaryField = getPrimaryField(model.getClass());
    primaryField.setAccessible(true);
    try (PreparedStatement preparedStatement = conn.prepareStatement(sql);) {
        preparedStatement.setInt(1, (Integer) primaryField.get(model));
        System.out.println(preparedStatement);
        deleted = preparedStatement.executeUpdate() > 0;
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return deleted;
}

public static <T> T findById(Connection conn, java.lang.Class<T> clazz, int id) throws Throwable {
    //select id, name, email from student where id = ?
    T result = null;
    StringBuilder sqlBuilder = new StringBuilder("select ");
    String sep = "";
    for (Field field : clazz.getDeclaredFields()) {
        sqlBuilder.append(sep).append(getFieldName(field));
        sep = ",";
    }
    sqlBuilder.append(" from ").append(getTableName(clazz));
    sqlBuilder.append(" where id = ?");
    String sql = sqlBuilder.toString();
    System.out.println(sql);

    Field primaryField = getPrimaryField(clazz);
    primaryField.setAccessible(true);
    try (PreparedStatement preparedStatement = conn.prepareStatement(sql);) {
        preparedStatement.setInt(1, id);
        System.out.println(preparedStatement);
        preparedStatement.executeQuery();
        ResultSet rs = preparedStatement.executeQuery();

        if (rs.next()) {
            result = clazz.newInstance();
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                if (field.getType() == String.class) {
                    field.set(result, rs.getString(getFieldName(field)));
                } else if (field.getType() == int.class || field.getType() == Integer.class) {
                    field.set(result, rs.getInt(getFieldName(field)));
                } else {
                    //TODO 暂不实现
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return result;
}

我们再来看一个场景,假如表名和类名不一致,比如班级表叫t_cls,如果想要用上面的代码那么学生类的名字需要命名为T_stu,对于java开发人员来说这个命名大家都接受不了,因此需要有一个机制来配置表名,显然用注解是非常合适的,那么我们来定义一个@Tablle注解

package com.example.jwl;

@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Table {
    java.lang.String name() default "";
}

然后在学生类配置一下

package com.example.jwl;

@Table(name = "t_cls")
public class Class {
}

DBUtils的getTableName方法加上处理逻辑

private static String getTableName(java.lang.Class clazz) {
    Table table = (Table) clazz.getAnnotation(Table.class);
    if (table != null) {
        return table.name();
    }
    return clazz.getSimpleName().toLowerCase();
}

表的字段命名方式用下划线做连接用的比较广泛,而java类的字段采用的是驼峰,可以和表名用同样的处理方式,定义@Columu注解

package com.example.jwl;

import java.lang.annotation.ElementType;

@java.lang.annotation.Target({ElementType.FIELD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Column {
    String name() default "";
}
package com.example.jwl;

@Table(name = "t_cls")
public class Class {
    private int id;
    @Column(name = "head_teacher_id")
    private int headTeacherId;
    @Column(name = "student_num")
    private int studentNum;
}
private static String getFieldName(Field field) {
    Columu column = field.getAnnotation(Columu.class);
    if (columu != null) {
        return column.name();
    }
    return field.getName().toLowerCase();
}

上述代码中,把主键默认的当作了名字为id类型为int的自增Id,实际的使用场景中主键有时候会使用字符串类型,甚至不叫id,这种场景就需要再定义一个注解来标识主键,就给这个注解起个名字叫@Id吧

package com.example.jwl;

import java.lang.annotation.ElementType;

@java.lang.annotation.Target({ElementType.FIELD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Id {
}

班级类做如下修改

@Table(name = "t_cls")
public class Class {
    @Id
    private String uuid;
    @Column(name = "head_teacher_id")
    private int headTeacherId;
    @Column(name = "student_num")
    private int studentNum;
}

修改getPrimaryField方法逻辑

private static Field getPrimaryField(java.lang.Class clazz) throws NoSuchFieldException {
    Field targetField = null;
    for (Field field : clazz.getDeclaredFields()) {
        Id id = field.getAnnotation(Id.class);
        if (id != null) {
            return field;
        }
    }
    return clazz.getDeclaredField("id");
}

findById方法的id参数的类型和逻辑也需要做下修正

public static <T> T findById(Connection conn, java.lang.Class<T> clazz, int id) throws Throwable;

->

public static <T> T findById(Connection conn, java.lang.Class<T> clazz, Object id) throws Throwable;
preparedStatement.setInt(1, id);

->

if (id.getClass() == String.class) {
    preparedStatement.setString(1, (String) id);
} else if (id.getClass() == Integer.class) {
    preparedStatement.setInt(1, (Integer) id);
} else {
    //TODO 暂不实现
}

我们知道关系数据库表之间的关联有三种关系,分别是一对一一对多多对多,假如学生表需增加了一个"对象字段"(男女对象的对象V_V)obj_id,我们当然也可以在学生类直接增加一个objId的属性

@Table(name = "student")
public class Student {
    private int id;
    private String name;
    private String email;
    @Column(name = "obj_id")
    private int objId;
    @OneToOne
    private Student obj;
 }

先通过findById查询某个学生的信息查询出来,在用过它的objId把它对象的信息查询出来,然后再设置到obj属性上

Student stu = studentDao.findById(1);
if (stu.objId != 0) {
    stu.setObj(studentDao.findById(stu.getObjId()));
}

既然是为了提高搬砖的效率,能一行搞定的事情就别写四行代码,上述代码中objId属性代表的是另外一个Student类的id,既然如此我们能不能直接用private Student obj;来代替private int objId;来做关联呢?答案显然是可以的,不过需要在增加一个注解代表需要走这个处理逻辑,这个关系是一对一的关系那注解就叫@OneToOne吧

package com.example.jwl;

import java.lang.annotation.ElementType;

@java.lang.annotation.Target({ElementType.FIELD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface OneToOne {
    String joinField() default "";
}

findById方法拼接sql时增加关联字段,遇到一对一关系时递归的调用findById

public static <T> T findById(Connection conn, java.lang.Class<T> clazz, Object id) throws Throwable {
    return findById(conn, clazz, id, new HashMap());
}

private static <T> T findById(Connection conn, java.lang.Class<T> clazz, Object id, Map memos) throws Throwable {
    //select id, name, email from student where id = ?
    T result = null;
    StringBuilder sqlBuilder = new StringBuilder("select ");
    String sep = "";
    for (Field field : clazz.getDeclaredFields()) {
        sqlBuilder.append(sep);
        OneToOne oneToOne = field.getAnnotation(OneToOne.class);
        if (oneToOne != null) {
            sqlBuilder.append(oneToOne.joinColumn());
        } else {
            sqlBuilder.append(getFieldName(field));
        }
        sep = ",";
    }
    sqlBuilder.append(" from ").append(getTableName(clazz));
    sqlBuilder.append(" where id = ?");
    String sql = sqlBuilder.toString();
    System.out.println(sql);

    Field primaryField = getPrimaryField(clazz);
    primaryField.setAccessible(true);
    try (PreparedStatement preparedStatement = conn.prepareStatement(sql);) {
        if (id.getClass() == String.class) {
            preparedStatement.setString(1, (String) id);
        } else if (id.getClass() == Integer.class) {
            preparedStatement.setInt(1, (Integer) id);
        } else {
            //TODO 暂不实现
        }
        System.out.println(preparedStatement);
        preparedStatement.executeQuery();
        ResultSet rs = preparedStatement.executeQuery();

        if (rs.next()) {
            result = clazz.newInstance();
            memos.put(id, result);
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                if (field.getType() == String.class) {
                    field.set(result, rs.getString(getFieldName(field)));
                } else if (field.getType() == int.class || field.getType() == Integer.class) {
                    field.set(result, rs.getInt(getFieldName(field)));
                } else {
                    OneToOne oneToOne = field.getAnnotation(OneToOne.class);
                    if (oneToOne != null) {
                        Object val = rs.getObject(oneToOne.joinColumn());
                        field.set(result, memos.getOrDefault(id, findById(conn, field.getType(), val)));
                    }
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return result;
}

一对多多对多和上述的处理类似,定义@OneToMany、@ManyToMany两个注解,其中一对多需要在的一方加映射,多对多需要增加一个关联表,这个就不多做介绍了

我们再来看一个场景,在增删改查之前必须要先把数据表建好,比如前面所说的学生表

create table student (
   id  int(3) NOT NULL AUTO_INCREMENT COMMENT '自增id',
   name varchar(120) NOT NULL COMMENT '名字',
   email varchar(220) NOT NULL,
   PRIMARY KEY (id)
);

前面我们用对象映射了增删改查的sql,对于建表同样可以来映射,表名、字段名、字段类型使用前面相同的方式映射,NOT NULL、COMMENT这些可以在@Columu注解上增加属性,AUTO_INCREMENT可以在@ID注解上增加属性、PRIMARY KEY也是根据@ID注解来映射

下面要做的就是选择一个时机根据数据库表是否存在的情况把映射出来的sql执行进去,能想到的方式有三种

  1. 作为一个独立的数据库迁移工具,在应用启动之前触发
  2. 服务启动的过程中
  3. 第一次执行对某张表的操作时

第一种方式隔离型比较好但是迁移和服务启动分成了两端给部署带来了复杂性,第三种由于是在运行期进行,如果迁移速度比较慢或者映射出来的sql有问题会造成线上问题,因为我们选择服务启动过程中作为触发时机,那么问题又来了启动时需要知道哪些类需要映射,前面我们定义了一个@Table注解,但是这个注解是做表名的映射的对于使用者来说是可选的,我们仅仅是需要一个标记告诉迁移代码是否为实体类,那么我们就定义一个新的注解吧,起个名字叫@Entity,迁移代码大致的实现步骤如下

public void migrate(Connection conn, String rootPackage) {
    //扫描某个包下所有类,获取所有的实体类
    List<java.lang.Class<?>> entityClasses = getEntityClasses(rootPackage);
    //查询数据库里存在哪些表
    List<String> existsTables = getExistsTables(conn);
    for (java.lang.Class clazz : entityClasses) {
        if (existsTables.contains(getTableName(clazz))) {
            updateTable(conn, clazz);
        } else {
            createTable(conn, clazz);
        }
    }
}
private List<String> getExistsTables(Connection conn) {
    return new ArrayList<>();//拼接show tables,执行获取结果
}

public List<java.lang.Class<?>> getEntityClasses(String rootPackage) {
    return ClassUtil.getClasses(rootPackage).stream().filter(item -> item.getAnnotation(Entity.class) != null).collect(Collectors.toList());
}

public static <T> void createTable(Connection conn, java.lang.Class<?> clazz) {
    //1、拼接create table
    //2、执行
}

public static <T> void updateTable(Connection conn, java.lang.Class<?> clazz) {
    //1、拼接alert table
    //2、执行
}

至此DBUtils在加上这几个注解实际上一个ORM的雏形就出来了,在此基础上实现迁移功能、查询缓存就是早期比较流行的hibernate,这仅是一个demo要实现一个功能完备的ORM需要考虑的点很多,比如上面是以mysql演示的,需要对数据库的方言做适配

JPA

sun公司看到了orm的流行趋势,准备定一套ORM的规范,奈何hibernate太流行了,已经形成了事实上的ORM规范,直接照抄怕被喷,于是乎就把hibernate的创始人招过来负责制定JPA规范,Gavin King把hibernate中注解类copy过来改个包名,略作修改就完成了制定JPA规范的OKR

我们来尝试把DBUtils改造成符合JPA规范的实现,为了演示方便假设JPA的接口如下

package com.example.jwl.jpa;

import com.example.jwl.Student;

public interface JpaRepository<T, ID> {
    void save(T t);

    boolean update(T t);

    Student findById(ID id);

    boolean delete(T t);
}
package com.example.jwl.jpa;

/**
 * @author tong
 */
public interface JpaRepositoryFactory {
    <T, ID> JpaRepository<T, ID> createRepository(Class<? extends JpaRepository<T, ID>> repoClass);
}

JpaRepository规定了DAO类需要包含哪些方法,JpaRepositoryFactory是为了解耦具体的ORM实现,所以只需要实现DBUtils自己的JpaRepositoryFactory类就好了,这里可以使用动态代理来拦截调用转发到DBUtils

package com.example.jwl.myjpa;

import com.example.jwl.DBUtils;
import com.example.jwl.jpa.JpaRepository;
import com.example.jwl.jpa.JpaRepositoryFactory;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * @author tong
 */
public class DBUtilsRepositoryFactory implements JpaRepositoryFactory {

    private String jdbcURL = "jdbc:mysql://localhost:3306/demo?useSSL=false";
    private String jdbcUsername = "root";
    private String jdbcPassword = "root";

    protected Connection getConnection() {
        Connection connection = null;
        try {
            java.lang.Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection(jdbcURL, jdbcUsername, jdbcPassword);
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return connection;
    }

    public <T, ID> JpaRepository<T, ID> createRepository(Class<? extends JpaRepository<T, ID>> repoClass) {
        return (JpaRepository<T, ID>) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{ repoClass }, (proxy, method, args) -> {
            if ("save".equals(method.getName())) {
                Object entity = args[0];
                DBUtils.save(getConnection(), entity);
                return null;
            } else {
                //TODO 暂时不实现
            }
            return null;
        });
    }
}

写一个student表的dao类和测试类

package com.example.jwl.myjpa;

import com.example.jwl.Student;
import com.example.jwl.jpa.JpaRepository;

public interface StudentRepository extends JpaRepository<Student, Integer> {

}
package com.example.jwl;

import com.example.jwl.jpa.JpaRepository;
import com.example.jwl.jpa.JpaRepositoryFactory;
import com.example.jwl.myjpa.DBUtilsRepositoryFactory;
import com.example.jwl.myjpa.StudentRepository;
import org.junit.Test;

/**
 * @author tong
 */
public class DBUtilsRepositoryFactoryTest {
    @Test
    public void test1() {
        JpaRepositoryFactory jpaRepositoryFactory = new DBUtilsRepositoryFactory();
        JpaRepository<Student, Integer> repository = jpaRepositoryFactory.createRepository(StudentRepository.class);

        Student student = new Student(0, "DBUtilsRepositoryFactory", "DBUtilsRepositoryFactory@qq.com");
        repository.save(student);
    }
}

springmvc

回顾下前面提到的servlet,开发中经常会写出臃肿无比的servlet,比如下面这样的代码

package com.example.helloservlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author tong
 */
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        if ("/hello".equals(req.getRequestURI())) {
            //do something
            PrintWriter out = resp.getWriter();
            out.write("<html><body><h1>Hello world!</h1></body></html>");
            out.flush();
        } else if ("/getUserInfo".equals(req.getRequestURI())) {
            //do something
        } else if ("/getBaselines".equals(req.getRequestURI())) {
            //do something
        } else if ("/getComponents".equals(req.getRequestURI())) {
            //do something
        } else if ("/getRepos".equals(req.getRequestURI())) {
            //do something
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if ("/createUser".equals(req.getRequestURI())) {
            //do something
        } else if ("/updateUser".equals(req.getRequestURI())) {
            //do something
        } else if ("/createBaseline".equals(req.getRequestURI())) {
            //do something
        } else if ("/updateBaseline".equals(req.getRequestURI())) {
            //do something
        }
    }
}

这种方式不可避免的要把表现层与业务逻辑代码混合在一起,往往大量的代码花费在参数解析响应内容拼接上,给前期开发与后期维护带来巨大的复杂度。为了摆脱上述的约束与局限,把业务逻辑代码从表现层中清晰的分离出来,大佬们陆续开发了很多框架比较出名的有Struts、springmvc,后者和spring整合度较好目前市面上使用较为广泛,文章开头引用的代码就是使用springmvc写的hello world

@SpringBootApplication
@RestController
public class DemoApplication {
    @GetMapping("/helloworld")
    public String hello() {
        return "Hello World!";
    }
}

我们来想下它是怎么实现的,首先由于servlet的规范限制,必须要先实现一个servlet来转发请求,看到注解@RestController、@GetMapping应该就能想到是反射调用的,另外还需要有一个时机去扫描项目中都有哪些标记@RestController注解的类,然后在扫描标记@GetMapping注解的方法,取出url建立与方法的映射,可以尝试自己实现以下,方便起见不考虑带参的path(/users/{id}),用map来保存映射Map<http method, Map<path, Method>>

private final Map<String, Map<String, Method>> fixedMapping = new HashMap<>();

public void init(String rootPackage) {
    //获取某个包下所有的class
    Set<Class<?>> classes = ClassUtil.getClasses(rootPackage);

    for (Class clazz : classes) {
        Controller controller = (Controller) clazz.getAnnotation(Controller.class);
        if (controller == null) {
            continue;
        }
        try {
            controllerMap.put(clazz, clazz.newInstance());
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        for (Method method : clazz.getMethods()) {
            GetMapping getMapping = (GetMapping) method.getAnnotation(GetMapping.class);
            if (getMapping == null) {
                continue;
            }
            for (String path : getMapping.value()) {
                String joinedPath = joinPath(controller.value(), path);
                Map<String, BindMethod> methodMapping = fixedMapping.computeIfAbsent("GET", k -> new HashMap<>());
                if (methodMapping.containsKey(joinedPath)) {
                    throw new RuntimeException("重复了");
                }
                methodMapping.put(joinedPath, method));
            }
        }
    }
}

在servlet中有个service可以拿到分发到这个servlet上所有的请求,可以在这里做二次路由,调用controller实现类并做响应

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if ("GET".equals(req.getMethod())) {
        Map<String, Method> pathBindMethodMap = fixedMapping.get(req.getMethod());
        String path = req.getRequestURI();
        Method bindMethod = pathBindMethodMap.get(path);
        if (bindMethod != null) {
            Object instance = controllerMap.get(bindMethod.getClass());
            Object[] args = new Object[bindMethod.getParameterCount()];
            for (int i = 0, size = bindMethod.getParameterCount(); i < size; i++) {
                Parameter parameter = bindMethod.getParameters()[i];
                Class<?> mType = parameter.getType();
                if (mType == HttpServletRequest.class) {
                    args[i] = req;
                } else if (mType == HttpServletResponse.class) {
                    args[i] = req;
                } else {
                    Method typeResolver = primitiveTypeResolverMap.get(mType);
                    if (typeResolver == null) {
                        throw new RuntimeException("unsupported type: " + mType.getName());
                    }
                    RequestParam anno = parameter.getAnnotation(RequestParam.class);
                    String queryStringName = null;
                    boolean required = true;
                    String defaultValue = "";
                    if (anno == null) {
                        queryStringName = parameter.getName();
                    } else {
                        queryStringName = anno.value();
                        required = anno.required();
                        defaultValue = anno.defaultValue();
                        if (queryStringName == null || queryStringName.trim().length() == 0) {
                            queryStringName = parameter.getName();
                        }
                    }
                    String value = req.getParameter(queryStringName);
                    if (value == null && required) {
                        resp.sendError(400, "参数异常");
                        return;
                    }
                    try {
                        Object parsedValue = value == null ? defaultValue : typeResolver.invoke(null, value);
                        args[i] = parsedValue;
                    } catch (Throwable e) {
                        resp.sendError(400, "参数异常");
                        return;
                    }
                    PrintWriter writer = resp.getWriter();
                    try {
                        Object ret = bindMethod.invoke(instance, args);
                        if (ret == null || primitiveTypeResolverMap.containsKey(ret.getClass())) {
                            writer.write(String.valueOf(ret));
                            writer.flush();
                        } else {
                            writer.write(new Gson().toJson(ret));
                            writer.flush();
                        }
                    } catch (Throwable e) {
                        resp.sendError(500, "出错了");
                        return;
                    }
                }
            }
        }
    } else {
        //暂不实现
        super.service(req, resp);
    }
}

private String joinPath(String prefix, String suffix) {
    StringBuilder sb = new StringBuilder();
    if (prefix != null && prefix.trim().length() > 0) {
        sb.append(prefix.trim());
        if (sb.charAt(0) != '/') {
            sb.insert(0, '/');
        }
    }
    if (suffix == null || suffix.trim().length() == 0) {
        throw new IllegalArgumentException("suffix不能为空");
    }
    suffix = suffix.trim();
    sb.append(suffix.startsWith("/") ? suffix : ("/" + suffix));
    if (!suffix.endsWith("/")) {
        sb.append(suffix);
    }
    return sb.toString();
}

这段代码就是springmvc的雏形了,处理流程可以分为三步

  1. 解析并校验参数根据处理方法的参数信息组织数据
  2. 调用处理方法
  3. 根据处理结果作出响应

DispatcherServlet.java

spring

基线上面我们实现的myspringmvc来开发时,经常会有多个controller实例共享同一个对象的场景,比如前面说JPA时创建的StudentRepository实例

JpaRepositoryFactory jpaRepositoryFactory = new DBUtilsRepositoryFactory();
StudentRepository repository = jpaRepositoryFactory.createRepository(StudentRepository.class);

需要有一个地方放创建出来的对象(这些对象同一称之为bean),由于controller实例是由容器创建的多个实例之间互相不持有引用,因此需要在controller之外存放bean,我们可以使用一个单例类来作为bean容器,再进一步扩展把这个类做成应用全局的上下文管理类,让bean容器(BeanFactory)成为上下文的一部分

package com.example.myspring;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author tong
 */
public class MySpringApplicationContext {
    private static final MySpringApplicationContext INSTANCE = new MySpringApplicationContext();

    public static MySpringApplicationContext getInstance() {
        return INSTANCE;
    }

    private BeanFactory beanFactory = new BeanFactory();
    private MySpringApplicationContext() {
    }

    public void init() {
    }

    public BeanFactory getBeanFactory() {
        return beanFactory;
    }
    
    public class BeanFactory {
        /** Cache of singleton objects: bean name to bean instance. */
        private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

        public Object getBean(String name) {
            return this.singletonObjects.get(name);
        }

        public <T> T getBean(Class<T> requiredType) {
            return (T) this.singletonObjects.get(requiredType.getName());
        }

        public void registerSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                Object oldObject = this.singletonObjects.get(beanName);
                if (oldObject != null) {
                    throw new IllegalStateException("Could not register object [" + singletonObject +
                            "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
                }
                this.singletonObjects.put(beanName, singletonObject);
            }
        }
    }
}

业务代码里可以这样使用MySpringApplicationContext

package com.example.myspring;

import com.example.myspring.core.MySpringApplicationContext;

/**
 * @author tong
 */
public class BaseController {
    private static volatile boolean initialized = false;

    public BaseController() {
        if (!initialized) {
            //需要加锁
            MySpringApplicationContext.getInstance().init();

            JpaRepositoryFactory jpaRepositoryFactory = new DBUtilsRepositoryFactory();
            StudentRepository repository = jpaRepositoryFactory.createRepository(StudentRepository.class);
            MySpringApplicationContext.getInstance().getBeanFactory().registerSingleton(StudentRepository.class, repository);
        }
    }
}

public class Hello1Controller extends BaseController {
    private StudentRepository studentRepository;

    public Hello1Controller() {
        studentRepository = MySpringApplicationContext.getInstance().getBeanFactory().getBean(StudentRepository.class);
    }

    @GetMapping
    @ResponseBody
    public String hello(@RequestParam(value = "name", defaultValue = "world") String name) {
        return "hello: " + name;
    }
}

public class Hello2Controller extends BaseController {
    private StudentRepository studentRepository;

    public Hello1Controller() {
        studentRepository = MySpringApplicationContext.getInstance().getBeanFactory().getBean(StudentRepository.class);
    }

    @GetMapping
    @ResponseBody
    public String hello(@RequestParam(value = "name", defaultValue = "world") String name) {
        return "hello: " + name;
    }
}

这两个类都是继承自BaseController,并且都在init方法里把的实现类从BeanFactory取出来
private StudentRepository studentRepository;

@Override
public void init() throws ServletException {
    super.init();
    studentRepository = MySpringApplicationContext.getInstance().getBeanFactory().getBean(StudentRepository.class);
}

本身不多写一行代码的原则,可以在BaseServlet的init方法通过反射来设置studentRepository,为了标记哪些类需要动态的设置(后面称做注入)增加@Autowired注解

package com.example.myservlet;

import java.lang.annotation.ElementType;

/**
 * @author tong
 */
@java.lang.annotation.Target({ElementType.FIELD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Autowired {
}

在MySpringApplicationContext类增加inject方法,实现如下

public class MySpringApplicationContext {
    public void inject(Object component) {
        Class<?> aClass = component.getClass();
        for (Field field : aClass.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.getAnnotation(Autowired.class) != null) {
                Object bean = getBeanFactory().getBean(field.getType());
                if (bean != null) {
                    field.set(component, bean);
                }
            }
        }
    }
}

BaseController的构造方法做改造
public class BaseController {
    private static volatile boolean initialized = false;

    public BaseController() {
        if (!initialized) {
            //需要加锁
            MySpringApplicationContext.getInstance().init();

            JpaRepositoryFactory jpaRepositoryFactory = new DBUtilsRepositoryFactory();
            StudentRepository repository = jpaRepositoryFactory.createRepository(StudentRepository.class);
            MySpringApplicationContext.getInstance().getBeanFactory().registerSingleton(StudentRepository.class, repository);
        }

        MySpringApplicationContext.getInstance().inject(this);
    }
}

改造以后业务代码就不用自己调用beanFactory获取实现类了,是不是一下子清爽多了

public class Hello1Controller extends BaseController {
    @Autowired
    private StudentRepository studentRepository;
}

public class Hello1Controller extends BaseController {
    @Autowired
    private StudentRepository studentRepository;
}

至此我们做到了通过bean容器和注入来简化开发中的操作,这个模式可以很大程度上接耦接口层和实现层,也就是传说中的依赖反转原则
接下来还有问题需要解决,注入操作是在BaseController完成的,这里算是业务层,能否找个时机自动完成这个操作呢?所有的业务层的controller都是在前面实现的DispatcherServlet类里创建的,在这里做注入显然是很合适的,但是myspring和myspringmvc是分别属于两个不同通用的模块,如何做到框架层面的解耦合呢,在联想下myspringmvc和myorm实现里面都有扫描全部代码的功能,那我们是不是可以把实现框架的通用代码单独剥离出来,前面定义了一个应用全局的上下文管理类MySpringApplicationContext,那么就放在这里吧,由MySpringApplicationContext来统一的扫描代码,然后制定一个规范扫描的过程中来分发给其它的框架,其它的框架回调里各自完成自己这一层的事情,比如myspringmvc动态创建controller类完成注入,接下来我们来实现下
我们来定一个ResourceLoaderAware接口来负责分发的通信

package com.example.myspring.core;

/**
 * @author tong
 */
public interface ResourceLoaderAware {
    void processClass(Class<?> clazz, MySpringApplicationContext context);
}

扫描资源的代码放在迁移到MySpringApplicationContext里,遇到ResourceLoaderAware就分发过去
package com.example.myspring.core;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author tong
 */
public class MySpringApplicationContext {
    public void init() {
        scan(获取扫描的包名);
    }

    public void scan(String basePackage) {
        Set<Class<?>> classes = ClassUtil.getClasses(basePackage);
        List<ResourceLoaderAware> resourceLoaderAwares = new ArrayList<>();
        List<Class> otherClasses = new ArrayList<>();
        for (Class clazz : classes) {
           if (clazz instanceof ResourceLoaderAware) {
               Object loader = clazz.newInstance();
               inject(loader);
               resourceLoaderAwares.add(loader);
               getBeanFactory().registerSingleton(clazz, loader);
           } else {
               otherClasses.add(clazz);
           }
        }
    
        for (Class<?> clazz : classes) {
            for (ResourceLoaderAware resourceLoaderAware : resourceLoaderAwares) {
                resourceLoaderAware.processClass(clazz, this);
            }
        }
    }
}

myspringmvc模块提供钩子类

package com.example.myspring.myspringmvc;

import com.example.myspring.core.MySpringApplicationContext;
import com.example.myspring.core.ResourceLoaderAware;

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

/**
 * @author tong
 */
public class MySpringmvcResourceLoaderAware implements ResourceLoaderAware {
    final Map<Class<?>, Object> controllerMap = new HashMap<>();
    final Map<String, Map<String, Method>> fixedMapping = new HashMap<>();

    @Override
    public void processClass(Class<?> clazz, MySpringApplicationContext context) {
        Controller controller = (Controller) clazz.getAnnotation(Controller.class);
        if (controller == null) {
            return;
        }
        try {
            Object bean = clazz.newInstance();
            context.inject(bean);//完成注入
            controllerMap.put(clazz, bean);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        for (Method method : clazz.getMethods()) {
            GetMapping getMapping = (GetMapping) method.getAnnotation(GetMapping.class);
            if (getMapping == null) {
                continue;
            }
            for (String path : getMapping.value()) {
                String joinedPath = joinPath(controller.value(), path);
                Map<String, Method> methodMapping = fixedMapping.computeIfAbsent("GET", k -> new HashMap<>());
                if (methodMapping.containsKey(joinedPath)) {
                    throw new RuntimeException("重复了");
                }
                methodMapping.put(joinedPath, method);
            }
        }
    }
}

DispatcherServlet类做相应改造

package com.example.myspring.myspringmvc;

import com.example.myspring.core.MySpringApplicationContext;
import com.google.gson.Gson;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author tong
 */
public class DispatcherServlet extends HttpServlet {
 
    @Overrid
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if ("GET".equals(req.getMethod())) {
            MySpringmvcResourceLoaderAware mySpringmvcResourceLoaderAware = MySpringApplicationContext.getInstance().getBeanFactory().getBean(MySpringmvcResourceLoaderAware.class);
            Map<Class<?>, Object> controllerMap = mySpringmvcResourceLoaderAware.controllerMap;
            Map<String, Map<String, Method>> fixedMapping = mySpringmvcResourceLoaderAware.fixedMapping;
            //参考之前的代码
        } else {
            //暂不实现
            super.service(req, resp);
        }
    }
}

myorm自己建表模块也接入下

package com.example.myspring.myspringmvc;

import com.example.myspring.core.Autowired;
import com.example.myspring.core.MySpringApplicationContext;
import com.example.myspring.core.ResourceLoaderAware;

/**
 * @author tong
 */
public class MyOrmResourceLoaderAware implements ResourceLoaderAware {
    @Autowired
    private Connection connection;

    @Override
    public void processClass(Class<?> clazz, MySpringApplicationContext context) {
        if (item.getAnnotation(Entity.class) == null) {
            return;
        }
        List<String> existsTables = getExistsTables(connection);
        if (existsTables.contains(getTableName(clazz))) {
            updateTable(conn, clazz);
        } else {
            createTable(conn, clazz);
        }
    }
}

这样通用层就剥离改造完成了
有bean容器和注入的基础能力能做的事情还有很多,我们来看下BeanFactory的注册bean方法

public class BeanFactory {
    public void registerSingleton(Class<?> type, Object singletonObject) {
      //把singletonObject替换为代理类
    }
}

实例Object singletonObject的类型是Class<?> type,如果动态生成一个代理类扔进bean容器,那么整个工程里拿到实例都是代理类,在MySpringApplicationContext层面提供一些机制分发方法调用带来极大的灵活性,比如前面讲JPA时的JpaRepository类,拦截save方法可以统一为实体类设置createTime,拦截update方法可以统一设置updateTime,这就是传说中的面向切面编程

以下内容是摘抄自网上,可以结合上面的内容理解下

Spring Framework,有两个核心设计思想是要掌握的,就是 控制反转(Inversion of Control,简称 IoC) 和 面向切面编程(Aspect Oriented Programming,简称 AOP)。说到控制反转,还有一个概念也要理解,叫 依赖注入(Dependency Injection,简称 DI),区别就是,控制反转是一种设计思想,而依赖注入则是其中一种实现的方式,还有另一个实现方式叫依赖查找(Dependency Lookup)。不过,大部分都是采用依赖注入,Spring 使用的也是依赖注入的方式。另外,Spring 核心还有一个控制反转容器(IoC Container),主要就是通过配置文件以及利用反射在运行时创建所需要的实现类。

springboot

前面说JPA时,这段代码用来指定jpa的具体实现使用myorm,如何从全局的角度来自动配置实现呢?

JpaRepositoryFactory jpaRepositoryFactory = new DBUtilsRepositoryFactory();

另外所有的数据库配置都是写死在代码里的

private String jdbcURL = "jdbc:mysql://localhost:3306/demo?useSSL=false";
private String jdbcUsername = "root";
private String jdbcPassword = "root";

正常情况下需要根据开发、测试、生产这些环境做不同的配置,怎么隔离?

目前为止启动服务还是把应用打出war包,启动tomcat扔进webapps里面,这样开发的时候不是很方便,开发中能否像javase程序一样执行下main方法服务就启动了呢?部署的时候直接java -jar appserver.jar?

springboot的出现就是为了解决上述问题的,把web容器收敛进来,提供一套统一的参数配置机制,另外给框架的实现者可以一个简化框架初始化的机制

微服务

在早期的后端开发中,一般是所有的模块的源码集中的同一个仓库里,将所有功能打包在一个容器中运行,一个实例中集成了一个系统的所有功能,通过负载均衡实现多实例调用,大家称之为单体架构。单体架构的应用比较容易部署、测试, 在项目的初期,单体应用可以很好地运行。然而随着需求的不断增加, 越来越多的人加入开发团队,代码库也在飞速地膨胀。慢慢地,单体应用变得越来越臃肿,可维护性、灵活性逐渐降低,维护成本越来越高

image.png

首先想到的是分仓,每个业务域的源代码放在自己的仓库里独立出jar包,然后把所有业务域的jar和基础组件聚合打出war包

image.png

分仓后对代码耦合后会有所改善,本地开发的编译速度也会提升很多,但是仍有大量时间花在服务启动上面,另外随着业务量的增加单台机器已经不足于支撑业务,很容易引起单点故障,因此采用了多台机器起多个实例


image.png

至此还是有很多类似于下面的这些问题还没有解决

  • 扩展性差
    很难梳理功能依赖清单,一个功能点的变更往往很难评估其影响模块进而无法有效地组织测试,测试与发布都会需要整体部署,非常耗时
  • 无法实现复杂业务
    一个容器中实现所功能,服务耦合性高,需要极为精巧的设计
  • 技术升级困难
    牵一发而动全身,无法模块化地实现技术框架地升级
  • 开发效率低
    每个成员都需要有完整的环境依赖,开发环境的搭建成本高,协同开发时版本冲突频繁
  • 部署的成本高
    比如A模块吃内存,当其它模块无法承载当前的吞吐量时,需要多部署一个实例,准备机器时考虑A模块的情况需要把内存给的很大,A模块即使不加机器吞吐量也是可以满足的,那么增加机器多出来的内存资源实际上是浪费的

基于这个背景有两位大佬(James Lewis、Martin Fowler)提出了微服务架构,具体内容可以参考https://martinfowler.com/articles/microservices.html
其核心思想是把之前的一个物理部署单位切分为多个小的部署单位,每个拆分出来的服务都可以独立的开发、部署,每个拆分出来的服务可以根据不同的业务场景选择不同的技术栈和编程语言,甚至不同的操作系统和CPU平台

根据各位大佬们的实践,一般是先按业务领域拆,如果有相同功能需要聚合,则进行下沉(垂直),然后再按功能定位拆(水平),最后还可以按照按重要程度拆分区分核心功能和非核心功能。

image.png

不同模块之间的调用在单体架构下是直接通过方法调用完成的(同一个进程),服务拆分后多个服务可以部署在同一台机器也可以部署在不同的机器,无论哪种情况服务之间通过方法调用肯定是不行了(不同进程、不同机器),所以需要有一种RPC机制来完成服务间的通信

  • 多个服务部署在同一台机器(不同进程)
    linux提供了很多种进程间通信方式,比如管道、共享内存、套接字
  • 多个服务部署在不同的机器
    需要走网卡,只能走套接字
    为了方便起见减少复杂性,对性能不是很敏感的业务,这两种场景统一使用了套接字,一般大家更愿意使用套接字更上层的http协议来通信,方法调用变成套接字这一变带来了很多问题,比如下面这些
  1. 使用ConcurrentHashMap/Ehcached来做缓存
    多个服务需要复用同一份缓存,缓存放在哪里?
  2. synchronized、ReentrantLock
    单体架构下用来做不同线程间的同步,现在问题更复杂了
  3. 发布/订阅
    单体架构下使用观察者模式,callback的方式来做
    .......
    各位开源大佬和商业公司为了解决上述问题,开发了很多组件,经过在市场上的厮杀,有些方案受到了广泛的认可

不同模块之间的调用在单体架构下是直接通过方法调用完成的(同一个进程),服务拆分后多个服务可以部署在同一台机器也可以部署在不同的机器,无论哪种情况服务之间通过方法调用肯定是不行了(不同进程、不同机器),所以需要有一种RPC机制来完成服务间的通信

  • 多个服务部署在同一台机器(不同进程)
    linux提供了很多种进程间通信方式,比如管道、共享内存、套接字
  • 多个服务部署在不同的机器
    需要走网卡,只能走套接字
    为了方便起见减少复杂性,对性能不是很敏感的业务,这两种场景统一使用了套接字,一般大家更愿意使用套接字更上层的http协议来通信,方法调用变成套接字这一变带来了很多问题,比如下面这些
  1. 使用ConcurrentHashMap/Ehcached来做缓存
    多个服务需要复用同一份缓存,缓存放在哪里?
  2. synchronized、ReentrantLock
    单体架构下用来做不同线程间的同步,现在问题更复杂了
  3. 发布/订阅
    单体架构下使用观察者模式,callback的方式来做
    .......
    各位开源大佬和商业公司为了解决上述问题,开发了很多组件,经过在市场上的厮杀,有些方案受到了广泛的认可
image.png

这些组件大家称之为中间件其它的诸如网关、注册中心、配置中心、智能路由、负载均衡、断路器、监控跟踪等等这里就不多说了

只要开发中有比较通用的场景就会有开源组织/商业公司来抢占市场,spring看到上述说的这些比较通用的中间件的业务场景,于是乎就定义了一套微服务接入、实现的规范叫做Spring Cloud规范

  • 奈飞公司实现了一套叫Spring Cloud Netflix,主要由 Eureka、Ribbon、Feign、Hystrix 等组件组成
  • 阿里实现了一套叫Spring Cloud Alibaba ,主要由 Nacos、Sentinel、Seata 等组件组成

做个总结站在技术的视角,从微服务的发展过程中可以看出本质就是同一个进程变成分散在不同机器上的多个进程,因此方法调用需要换成套接字,开源组织&商业公司抓住通用的场景,实现了很多开箱即用的中间件。说了这么多微服务并不是说它就是最优解,任何技术方案的选择都需要根据业务规模和具体场景来决定,业务规模比较小时往往单体架构更简单高效

结语

老子在《道德经》里说过"有道无术术尚可求,有术无道止于术",对于java后端来说"道"就是技术出现的背景、框架的思想以及演变过程,有了这些思维遇到新框架时可能猜都能猜到实现原理,一通百通学会一个同一类其它的都不在话下,"术"可以理解为一个一个技术、一个一个框架,如果只关注"术"本身只会让我们迷失在层出不穷的框架里不能自拔。
前面我们从java后端发展的进程中走过来,觉得这些难吗?我们不是不会后端开发只是还没有学而已,只要有"道"给我们足够的时间一个个框架均可手撕,学习后端框架最好的方式是看官方文档先学会怎么用,思考它存在的意义和实现原理,用之前所学的技术做类比,然后对它说Talk is cheap. Show me the code

我们真的需要这么多框架吗?也许只是别人都这么用,所以我们也要跟着这样用而已!!!各种规范都是最优解吗?也许只是开始时有帮人订成这样了,所以我们只能跟着这样来罢了!!!

相关资料
https://github.com/typ0520/jwl
https://www.runoob.com/servlet/servlet-tutorial.html
https://www.runoob.com/jsp/jsp-tutorial.html
https://zh.wikipedia.org/wiki/JSP
https://developer.aliyun.com/article/931067
https://spring.io/quickstart
https://segmentfault.com/a/1190000039765982
https://en.wikipedia.org/wiki/Java_Database_Connectivity
https://gudaoxuri.gitbook.io/microservices-architecture/fu-wu-jia-gou-yan-yi/microservices
https://martinfowler.com/articles/microservices.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容