《架构探险 从零开始写Java Web框架》笔记
第二章,为Web应用添加业务功能
需求分析与系统设计
这一章书中举了一个简单的例子,完全基于Servlet API,实现顾客信息的增删改查。系统分为四层:model(模型层), view(视图层), controller(控制器层), service(服务层)。模型层定义了Customer JavaBean;视图层存放JSP视图;这里比标准的MVC架构多了一个服务层,作为衔接控制器层与数据库之间的桥梁。控制器层调用服务层,获取指定的Bean或BeanList.
由于一个Servlet只能处理对一个路径的请求:
@WebServlet("/customer_list")
public class CustomerListServlet extends HttpServlet {
init(); doGet(); doPost()...
}
所以必定还要写CustomerShowServlet,CustomerCreateServlet,CustomerEditServlet,CustomerEditServlet等等,随着业务需求的不断扩展,Servlet的数量势必不断增多,将大大增加维护工作量。所以,后面框架的作用之一就是,一个请求路径对应一个方法,而不是一个类,这样就可以将上面Customer相关的业务逻辑都集中到一个CustomerController中。
第三章,搭建轻量级Java Web框架
确定目标
我们的目标是打造一个轻量级MVC框架,而Controller是MVC的核心。我们想要的是这样的Controller代码:
/**
* 处理客户管理相关请求
* /
@Controller
public class CustomerController {
@Inject
private CustomerService customerService;
@Action("get:/customer_list")
public View index(Param param) {
List<Customer> customerList = customerService.getCustomerList();
return new View("customer_list.jsp").addModel("customerList", customerList);
}
@Action("post:/customer_create")
public Data createSubmit(Param param) {
Map<String, Object> fieldMap = param.getMap();
boolean result = customerService.createCustomer(fieldMap);
return new Data(result);
}
}
通过Controller注解来定义Controller类,在该类中,可通过Inject注解定义一个Service成员变量,这就是“依赖注入”。此外,有一系列被Action注解所定义的方法,在这些Action方法中,调用了Service的方法来完成具体的业务逻辑。若返回View对象,则表示JSP页面;若返回Data对象,则表示一个JSON数据。
开发一个类加载器
我们需要开发一个“类加载器”来加载该基础包名下的所有类,比如使用了某注解的类,或实现了某接口的类,再或者继承了某父类的所有子类等。
Tomcat等Java Web服务器又被称作“Servlet容器”。我们编写的Servlet并不需要我们自己去手动加载、实例化,而是由框架自动实现。发现并加载类文件就是第一步。
public static Class<?> loadClass(String className, boolean isInitialized) {
Class<?> cls;
try {
cls = Class.forName(className, isInitialized, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
...
}
return cls;
}
定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
结合cls.isAnnotationPresent(Controller.class/Service.class)
方法就可以加载并识别出包下所有被@Controller/@Service
注解的类。
实现Bean容器
加载了类之后,下一步就是通过反射来实例化对象,并将这些对象全部放到一个Map<Class<?>, Object>
中,即所谓“Bean容器”。
通过Class对象实例化对象很简单:
cls.newInstance();
然后放进“Bean容器”中:BEAN_MAP.put(beanClass, obj);
(Bean类与Bean实例的映射关系)
反射能做到的事情:实例化一个类(Class<?>对象);获取一个类的所有属性、方法,并设置属性值、调用方法。
// 获取所有的属性;
for (Field field : emailBeanClass.getDeclaredFields()) {
System.out.println(field.getName());
// field.set(obj, value); // obj是该属性所属的对象;
}
// 获取所有方法;
for (Method method : emailBeanClass.getMethods()) {
System.out.println(method.getName());
// method.invoke(obj, Object... args)
}
实现依赖注入功能
我们在Controller中定义了Service属性,并在Action方法中调用Service的方法。那么,如何实例化Service属性呢?
不是开发者通过new的方式来实例化,而是通过框架自身来实例化,像这类实例化过程,称为IoC(Inversion of Control,控制反转)。控制不是由开发者决定的,而是反转给框架了。一般的,我们将控制反转称为DI(Dependency Injection,依赖注入),可以理解为将某各类依赖的成员注入到这个类中。
在上面的步骤中,我们加载了所有的类,通过反射实例化,并放到Bean容器中。这里,我们可以遍历所有的Bean定义(即Bean容器的键集合,Class<?>对象),然后遍历Bean类定义的所有成员变量,看其是否带有@Inject
注解,若有,则通过反射,(从Bean容器中取出对应Service类实例并)设置该成员变量的值,即完成了依赖注入。
加载Controller
前面我们将Customer相关的所有Servlet代码都集中到一个CustomerController类中,每个请求对应一个Action方法;那么,我们的框架如何加载这个Controller类呢?
我们需要创建一个ControllerHelper类,让它来处理如下逻辑:
通过ClassHelper,我们可以获取所有定义了Controller注解的类,可以通过反射获取该类中所有带有Action注解的方法,获取Action注解中的请求表达式,进而获取请求方法与请求路径,封装一个请求对象(Request)与处理对象(Handler),最后将Request与Handler建立一个映射关系,放入一个Action Map中,并提供一个可根据请求方法与请求路径获取处理对象的方法。
class Request {
private String requestMethod; // 请求方法;
private String requestPath; // 请求路径;
...
}
class Handler {
private Class<?> controllerClass; // 该方法所属的Controller类;
// 通过反射调用Method方法的时候必须提供该方法所属的对象,所以这里记录下该方法所属的类,然后可以通过Bean容器找到Bean对象;
private Method actionMethod; // Action方法;
...
}
加载Controller类的步骤:
- 遍历Bean容器,找出所有
@Controller
注解的类; - 遍历Controller类的方法,找出所有
@Action
注解的Action方法; - 解析请求方法与请求路径,构成Request对象;Class<?>类定义与Method方法构成Handler对象;
- 将Request与Handler的映射放进ACTION_MAP中;
请求转发器
以上过程都是在为这一步做准备。我们现在需要编写一个Servlet,让它来处理所有请求。从HttpServletRequest对象中获取请求方法与请求路径,通过ControllerHelper#getHandler获取Handler对象(通过Handler对象获取Controller类,进而通过Bean容器获取Controller实例对象);从HttpServletRequest对象中取出请求参数,构成Param对象(一个Map<String, Object>);调用Handler对象对应的Action方法,返回View或Data;若返回值是View类型的视图对象,则返回一个JSP页面;若是Data对象,则返回一个JSON数据。
public class View {
private String path; // 视图路径;
private Map<String, Object> model; // 模型数据;
...
public View addModel(String key, Object value) {
model.put(key, value);
return this;
}
}
以下便是MVC框架中最核心的DispatcherServlet类,代码如下:
@WebServlet(urlPatterns = "/*", loadOnStartup = 0)
public class DispatcherServlet extends HttpServlet {
@Override
public void service(HttpServletRequest request, HttpServletResponse response) {
// 获取请求方法与请求路径;
String requestMethod = request.getMethod().toLowerCase();
String requestPath = request.getPathInfo();
// 获取Handler,即Action处理器;
Handler handler = ControllerHelper.getHandler(requestMethod, requestPath);
if (handler == null) {...}
// 获取Controller类及其Bean实例;
Class<?> controllerClass = handler.getControllerClass();
Object controllerBean = BeanHelper.getBean(controllerClass);
// 创建请求参数Map;
Map<String, Object> paramMap = new HashMap<>();
...// request.getParameterNames(); request.getParameter(paramName);
Param param = new Param(paramMap);
// 调用Action方法;
Method actionMethod = handler.getActionMethod();
Object result = actionMethod.invoke(controllerBean, param);
// 处理Action方法返回值;
if (result instanceof View) {
// 返回JSP页面;
View view = (View) result;
String path = view.getPath();
if (path.startsWith("/")) {
response.sendRedirect(request.getContextPath + path);
} else {
Map<String, Object> model = view.getModel();
for (Map.Entry<String, Object> entry : model.entrySet()) {
request.setAttribute(entry.getKey(), entry.getValue());
}
request.getRequestDispatcher(ConfigHelper.getAppJspPath() + path)
.forward(request, response);
}
} else if (result instanceof Data) {
Object model = ((Data) request).getModel();
String json = JsonUtil.toJson(model);
response.getWriter().write(json);
response.getWriter().close();
}
}
}
另外,我们是通过一系列的Helper类来初始化MVC框架的,即框架的工作主要是在各种Helper类中完成;而Helper类则集中到一起通过一个入口程序来加载它们,实际上是执行它们的静态块。
public final class HelperLoader {
public static void init() {
Class<?>[] classList = {
ClassHelper.class,
BeanHelper.class,
IocHelper.class,
ControllerHelper.class
};
for (Class<?> cls : classList) {
ClassUtil.loadClass(cls.getName());
}
}
}
总结
在本章中,我们搭建了一个简单的MVC框架,定义了一系列注解:通过Controller注解来定义Controller类;通过Inject注解来实现依赖注入;通过Action注解来定义Action方法。通过一系列的Helper类来初始化MVC框架;通过DispatcherServlet来处理所有的请求;根据请求方法与请求路径来调用具体的Action方法,判断Action方法的返回值,若为View类型,则跳转到JSP页面(或转发请求),若为Data类型,则返回JSON数据。
使框架具备AOP特性
代理
代理,或称为Proxy。意思就是你不用去做,别人代替你去处理。它在程序开发中起到了非常重要的作用,比如AOP,就是针对代理的一种应用。
静态代理
public interface HelloInterface {
void say(String name);
}
public class HelloImpl implements HelloInterface {
@Override
public void say(String name) {
System.out.println("Hello! " + name);
}
}
public class HelloProxy implements HelloInterface {
private HelloInterface hello;
public HelloProxy(HelloInterface hello) {
this.hello = hello; // 传入被代理的对象;
}
@Override
public void say(String name) {
before();
hello.say(name);
after();
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println(After);
}
}
public static void main(String[] args) {
HelloInterface hello = new HelloImpl();
HelloInterface helloProxy = new HelloProxy(hello);
helloProxy.say("Smith");
}
这里,代理类必须实现与被代理类相同的接口,两者高度耦合。
JDK动态代理
代理类与被代理类分离。
public class DynamicProxy implements InvocationHandler { // 代理类无需实现任何其他接口;
private Object target;
public DynamicProxy(Object target) {
this.target = target; // 传入被代理的对象;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
...
}
public static void main(String[] args) {
HelloInterface hello = new HelloImpl();
DynamicProxy dynamicProxy = new DynamicProxy(hello);
// “生成”代理对象;
HelloInterface helloProxy = (HelloInterface) Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
hello.getClass().getInterfaces(),
dynamicProxy
);
helloProxy.say("Smith");
}
CGlib动态代理
JDK动态代理有一个“Are You Kidding Me”的缺点:被代理类必须实现接口。若没有实现任何接口,或接口里没有任何方法,则代理类生成的是一个“空对象”,或“空类”——动态代理是动态生成了类的(可以用代理对象的getClass().getName()
方法验证),这个生成类实现了与被代理类相同的接口——也仅仅是实现这些接口,被代理类接口之外的成员方法是不会出现在生成类中的。也就是说,JDK动态代理是基于接口的代理。
Spring、Hibernate等框架都使用了名为CGlib(Code Generation lib)的动态代理工具。它能在运行期间动态生成字节码,也就是动态生成代理类了。
public class CGLibProxy implements MethodInterceptor {
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(obj, args);
after();
return result;
}
...
}
public static void main(String[] args) {
CGLibProxy cgLibProxy = new CGLibProxy();
Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class); // 看这里,参数由之前的对象变成了类;
helloProxy.say("Jack");
}
CGlib个我们提供的是方法级别的代理,也可以理解为对方法的拦截(这不就是传说中的“方法拦截器”吗?)。
与JDK动态代理不同的是,这里不需要提供任何接口信息,对谁都可以生成动态代理对象。
AOP
什么是AOP
AOP,Aspect-Oriented Programming,面向切面编程。
切面是AOP中的一个术语,表示从业务逻辑中分离出来的“横切逻辑”,比如性能监控、日志记录、权限控制等,这些功能都可以从核心的业务逻辑中分离出去。也就是说,通过AOP可以解决代码耦合问题,让职责更单一。
Spring AOP
前置增强、后置增强、环绕增强,抛出增强;
引入增强:上面都是对方法的增强,叫Weaving(织入);而对类的增强叫Introduction(引入)。
// 定义一个新接口;
public interface Apology {
void saySorry(String name);
}
// 引入增强类;
public class GreetingIntroAdvice extends DelegatingIntroductionIntercepter implements Apology {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return super.invoke(invocation);
}
@Override
public void saySorry(String name) {
System.out.println("Sorry, " + name);
}
}
这就是引入增强带给我们的新特性,也就是“接口动态实现”功能。
Spring AOP:切面
之前谈到的AOP框架其实可以将它理解为一个拦截器框架,但这个拦截器似乎非常武断。比如说,它拦截了一个类,那么它就拦截了这个类中的所有方法。而我们常常需要在代码中对所拦截的方法名加以判断,才能过滤出我们需要拦截的方法,这种做法确实不太优雅。于是,Spring AOP引入了一个“切面(Advisor)”的概念来解决这个问题。
切面封装了增强和切点(拦截匹配条件)。
开发AOP框架
略。
ThreadLocal简介
ThreadLocal,线程本地变量。其实就是一个容器,用于存放线程的局部变量。
先看ThreadLocal的用法:
public static void main(String[] args) {
CGLibProxy cgLibProxy = new CGLibProxy();
Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class); // 看这里,参数由之前的对象变成了类;
helloProxy.say("Jack");
}
public class Sequence {
private ThreadLocal<Integer> numberContainer = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return 0; // 设置ThreadLocal变量的初始值;
}
}
public int getNumber() {
numberContainer.set(numberContainer.get() + 1);
return numberContainer.get();
}
public static void main(String[] args) {
Sequence sequence = new Sequence();
new ClientThread(sequence).start();
new ClientThread(sequence).start();
new ClientThread(sequence).start();
}
}
output:
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
...
自己实现ThreadLocal
public class MyThreadLocal<T> {
private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<>());
protected T initialValue() {
return null;
}
public void set(T value) {
container.put(Thread.currentThread(), value);
}
public T get() {
if (!container.containsKey(Thread.currentThread())) {
container.put(Thread.currentThread(), initialValue());
}
return container.get(Thread.currentThread());
}
public void remove() {
container.remove(Thread.currentThread());
}
}