《架構(gòu)探險 從零開始寫Java Web框架》筆記
第二章牵寺,為Web應(yīng)用添加業(yè)務(wù)功能
需求分析與系統(tǒng)設(shè)計
這一章書中舉了一個簡單的例子悍引,完全基于Servlet API,實(shí)現(xiàn)顧客信息的增刪改查帽氓。系統(tǒng)分為四層:model(模型層), view(視圖層), controller(控制器層), service(服務(wù)層)吗铐。模型層定義了Customer JavaBean;視圖層存放JSP視圖杏节;這里比標(biāo)準(zhǔn)的MVC架構(gòu)多了一個服務(wù)層唬渗,作為銜接控制器層與數(shù)據(jù)庫之間的橋梁》苡妫控制器層調(diào)用服務(wù)層镊逝,獲取指定的Bean或BeanList.
由于一個Servlet只能處理對一個路徑的請求:
@WebServlet("/customer_list")
public class CustomerListServlet extends HttpServlet {
init(); doGet(); doPost()...
}
所以必定還要寫CustomerShowServlet,CustomerCreateServlet嫉鲸,CustomerEditServlet撑蒜,CustomerEditServlet等等,隨著業(yè)務(wù)需求的不斷擴(kuò)展玄渗,Servlet的數(shù)量勢必不斷增多座菠,將大大增加維護(hù)工作量。所以藤树,后面框架的作用之一就是浴滴,一個請求路徑對應(yīng)一個方法,而不是一個類岁钓,這樣就可以將上面Customer相關(guān)的業(yè)務(wù)邏輯都集中到一個CustomerController中升略。
第三章,搭建輕量級Java Web框架
確定目標(biāo)
我們的目標(biāo)是打造一個輕量級MVC框架屡限,而Controller是MVC的核心品嚣。我們想要的是這樣的Controller代碼:
/**
* 處理客戶管理相關(guān)請求
* /
@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方法中,調(diào)用了Service的方法來完成具體的業(yè)務(wù)邏輯劣挫。若返回View對象压固,則表示JSP頁面;若返回Data對象坎炼,則表示一個JSON數(shù)據(jù)拦键。
開發(fā)一個類加載器
我們需要開發(fā)一個“類加載器”來加載該基礎(chǔ)包名下的所有類芬为,比如使用了某注解的類蟀悦,或?qū)崿F(xiàn)了某接口的類日戈,再或者繼承了某父類的所有子類等浙炼。
Tomcat等Java Web服務(wù)器又被稱作“Servlet容器”弯屈。我們編寫的Servlet并不需要我們自己去手動加載恋拷、實(shí)例化梅掠,而是由框架自動實(shí)現(xiàn)。發(fā)現(xiàn)并加載類文件就是第一步酪我。
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 {
}
結(jié)合cls.isAnnotationPresent(Controller.class/Service.class)
方法就可以加載并識別出包下所有被@Controller/@Service
注解的類都哭。
實(shí)現(xiàn)Bean容器
加載了類之后欺矫,下一步就是通過反射來實(shí)例化對象穆趴,并將這些對象全部放到一個Map<Class<?>, Object>
中遇汞,即所謂“Bean容器”空入。
通過Class對象實(shí)例化對象很簡單:
cls.newInstance();
然后放進(jìn)“Bean容器”中:BEAN_MAP.put(beanClass, obj);
(Bean類與Bean實(shí)例的映射關(guān)系)
反射能做到的事情:實(shí)例化一個類(Class<?>對象);獲取一個類的所有屬性化戳、方法点楼,并設(shè)置屬性值、調(diào)用方法藏斩。
// 獲取所有的屬性却盘;
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)
}
實(shí)現(xiàn)依賴注入功能
我們在Controller中定義了Service屬性帆赢,并在Action方法中調(diào)用Service的方法线梗。那么仪搔,如何實(shí)例化Service屬性呢?
不是開發(fā)者通過new的方式來實(shí)例化偏陪,而是通過框架自身來實(shí)例化笛谦,像這類實(shí)例化過程昌阿,稱為IoC(Inversion of Control宝泵,控制反轉(zhuǎn))】蛲控制不是由開發(fā)者決定的椰弊,而是反轉(zhuǎn)給框架了。一般的贤重,我們將控制反轉(zhuǎn)稱為DI(Dependency Injection清焕,依賴注入),可以理解為將某各類依賴的成員注入到這個類中滚停。
在上面的步驟中键畴,我們加載了所有的類起惕,通過反射實(shí)例化咏删,并放到Bean容器中饵婆。這里,我們可以遍歷所有的Bean定義(即Bean容器的鍵集合草穆,Class<?>對象)悲柱,然后遍歷Bean類定義的所有成員變量些己,看其是否帶有@Inject
注解段标,若有,則通過反射蛇更,(從Bean容器中取出對應(yīng)Service類實(shí)例并)設(shè)置該成員變量的值派任,即完成了依賴注入。
加載Controller
前面我們將Customer相關(guān)的所有Servlet代碼都集中到一個CustomerController類中师逸,每個請求對應(yīng)一個Action方法篓像;那么皿伺,我們的框架如何加載這個Controller類呢?
我們需要創(chuàng)建一個ControllerHelper類心傀,讓它來處理如下邏輯:
通過ClassHelper脂男,我們可以獲取所有定義了Controller注解的類宰翅,可以通過反射獲取該類中所有帶有Action注解的方法汁讼,獲取Action注解中的請求表達(dá)式阔墩,進(jìn)而獲取請求方法與請求路徑啸箫,封裝一個請求對象(Request)與處理對象(Handler),最后將Request與Handler建立一個映射關(guān)系蝉娜,放入一個Action Map中召川,并提供一個可根據(jù)請求方法與請求路徑獲取處理對象的方法胸遇。
class Request {
private String requestMethod; // 請求方法;
private String requestPath; // 請求路徑坛增;
...
}
class Handler {
private Class<?> controllerClass; // 該方法所屬的Controller類收捣;
// 通過反射調(diào)用Method方法的時候必須提供該方法所屬的對象,所以這里記錄下該方法所屬的類楣颠,然后可以通過Bean容器找到Bean對象童漩;
private Method actionMethod; // Action方法矫膨;
...
}
加載Controller類的步驟:
- 遍歷Bean容器期奔,找出所有
@Controller
注解的類呐萌; - 遍歷Controller類的方法,找出所有
@Action
注解的Action方法罗晕; - 解析請求方法與請求路徑小渊,構(gòu)成Request對象茫叭;Class<?>類定義與Method方法構(gòu)成Handler對象杂靶;
- 將Request與Handler的映射放進(jìn)ACTION_MAP中;
請求轉(zhuǎn)發(fā)器
以上過程都是在為這一步做準(zhǔn)備垛吗。我們現(xiàn)在需要編寫一個Servlet怯屉,讓它來處理所有請求。從HttpServletRequest對象中獲取請求方法與請求路徑赌躺,通過ControllerHelper#getHandler獲取Handler對象(通過Handler對象獲取Controller類羡儿,進(jìn)而通過Bean容器獲取Controller實(shí)例對象)掠归;從HttpServletRequest對象中取出請求參數(shù),構(gòu)成Param對象(一個Map<String, Object>)肤粱;調(diào)用Handler對象對應(yīng)的Action方法领曼,返回View或Data悯森;若返回值是View類型的視圖對象绪撵,則返回一個JSP頁面音诈;若是Data對象细溅,則返回一個JSON數(shù)據(jù)儡嘶。
public class View {
private String path; // 視圖路徑蹦狂;
private Map<String, Object> model; // 模型數(shù)據(jù);
...
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實(shí)例;
Class<?> controllerClass = handler.getControllerClass();
Object controllerBean = BeanHelper.getBean(controllerClass);
// 創(chuàng)建請求參數(shù)Map矫废;
Map<String, Object> paramMap = new HashMap<>();
...// request.getParameterNames(); request.getParameter(paramName);
Param param = new Param(paramMap);
// 調(diào)用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類則集中到一起通過一個入口程序來加載它們物蝙,實(shí)際上是執(zhí)行它們的靜態(tài)塊诬乞。
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());
}
}
}
總結(jié)
在本章中震嫉,我們搭建了一個簡單的MVC框架牡属,定義了一系列注解:通過Controller注解來定義Controller類湃望;通過Inject注解來實(shí)現(xiàn)依賴注入痰驱;通過Action注解來定義Action方法担映。通過一系列的Helper類來初始化MVC框架蝇完;通過DispatcherServlet來處理所有的請求矗蕊;根據(jù)請求方法與請求路徑來調(diào)用具體的Action方法傻咖,判斷Action方法的返回值,若為View類型警检,則跳轉(zhuǎn)到JSP頁面(或轉(zhuǎn)發(fā)請求)扇雕,若為Data類型窥摄,則返回JSON數(shù)據(jù)崭放。
使框架具備AOP特性
代理
代理,或稱為Proxy。意思就是你不用去做道伟,別人代替你去處理使碾。它在程序開發(fā)中起到了非常重要的作用票摇,比如AOP,就是針對代理的一種應(yīng)用盆色。
靜態(tài)代理
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");
}
這里摩梧,代理類必須實(shí)現(xiàn)與被代理類相同的接口仅父,兩者高度耦合笙纤。
JDK動態(tài)代理
代理類與被代理類分離组力。
public class DynamicProxy implements InvocationHandler { // 代理類無需實(shí)現(xiàn)任何其他接口忿项;
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動態(tài)代理
JDK動態(tài)代理有一個“Are You Kidding Me”的缺點(diǎn):被代理類必須實(shí)現(xiàn)接口伐弹。若沒有實(shí)現(xiàn)任何接口惨好,或接口里沒有任何方法日川,則代理類生成的是一個“空對象”矩乐,或“空類”——動態(tài)代理是動態(tài)生成了類的(可以用代理對象的getClass().getName()
方法驗(yàn)證)散罕,這個生成類實(shí)現(xiàn)了與被代理類相同的接口——也僅僅是實(shí)現(xiàn)這些接口,被代理類接口之外的成員方法是不會出現(xiàn)在生成類中的职抡。也就是說缚甩,JDK動態(tài)代理是基于接口的代理。
Spring岳遥、Hibernate等框架都使用了名為CGlib(Code Generation lib)的動態(tài)代理工具裕寨。它能在運(yùn)行期間動態(tài)生成字節(jié)碼,也就是動態(tài)生成代理類了捻艳。
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); // 看這里认轨,參數(shù)由之前的對象變成了類月培;
helloProxy.say("Jack");
}
CGlib個我們提供的是方法級別的代理杉畜,也可以理解為對方法的攔截(這不就是傳說中的“方法攔截器”嗎?)纯续。
與JDK動態(tài)代理不同的是灭袁,這里不需要提供任何接口信息,對誰都可以生成動態(tài)代理對象倦炒。
AOP
什么是AOP
AOP软瞎,Aspect-Oriented Programming铜涉,面向切面編程芒划。
切面是AOP中的一個術(shù)語旭旭,表示從業(yè)務(wù)邏輯中分離出來的“橫切邏輯”页滚,比如性能監(jiān)控裹驰、日志記錄片挂、權(quán)限控制等,這些功能都可以從核心的業(yè)務(wù)邏輯中分離出去沪饺。也就是說整葡,通過AOP可以解決代碼耦合問題讥脐,讓職責(zé)更單一。
Spring AOP
前置增強(qiáng)俱萍、后置增強(qiáng)鼠次、環(huán)繞增強(qiáng)芋齿,拋出增強(qiáng);
引入增強(qiáng):上面都是對方法的增強(qiáng)赦役,叫Weaving(織入)掂摔;而對類的增強(qiáng)叫Introduction(引入)乙漓。
// 定義一個新接口释移;
public interface Apology {
void saySorry(String name);
}
// 引入增強(qiáng)類;
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);
}
}
這就是引入增強(qiáng)帶給我們的新特性涩蜘,也就是“接口動態(tài)實(shí)現(xiàn)”功能。
Spring AOP:切面
之前談到的AOP框架其實(shí)可以將它理解為一個攔截器框架粤策,但這個攔截器似乎非常武斷误窖。比如說霹俺,它攔截了一個類吭服,那么它就攔截了這個類中的所有方法。而我們常常需要在代碼中對所攔截的方法名加以判斷艇棕,才能過濾出我們需要攔截的方法,這種做法確實(shí)不太優(yōu)雅北苟。于是打瘪,Spring AOP引入了一個“切面(Advisor)”的概念來解決這個問題闺骚。
切面封裝了增強(qiáng)和切點(diǎn)(攔截匹配條件)僻爽。
開發(fā)AOP框架
略。
ThreadLocal簡介
ThreadLocal敦捧,線程本地變量兢卵。其實(shí)就是一個容器秽荤,用于存放線程的局部變量。
先看ThreadLocal的用法:
public static void main(String[] args) {
CGLibProxy cgLibProxy = new CGLibProxy();
Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class); // 看這里,參數(shù)由之前的對象變成了類雁乡;
helloProxy.say("Jack");
}
public class Sequence {
private ThreadLocal<Integer> numberContainer = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return 0; // 設(shè)置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
...
自己實(shí)現(xiàn)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());
}
}