解決的問題
- servlet的數量會隨業務功能的擴展而不斷增加,我們有必要減少servlet的數量,交給controller處理,它負責調用service的相關方法,並將返回值放入request或response中。
- service目前是通過new的方式來創建的,這樣導致一個應用中會創建多個對象,這樣是不科學的。我們可以通過一種“依賴注入”的思想,讓框架來為我們創建所需要的對象。
掌握的技能
- 如何快速搭建開發框架
- 如何加載並讀取配置文件
- 如何實現一個簡單的IOC容器
- 如何加載指定的類
- 如何初始化框架
實現IOC
框架在實現IOC(Inversion of Control,控制反轉)的時候,通過框架自身來示例化Bean。
Bean從哪里來,怎么獲取Bean,怎么實現Bean的管理?
第一步:加載Bean
第二部:實例化Bean
第三部:根據注解,將Bean注入
開發一個類加載器
如果對類的加載機制不熟的話,可以先看看《深入理解Java虛擬機》的第七章——虛擬機類加載機制。
在Java語言里面,類型的加載和連接過程都是在運行期間完成的,這樣會在類加載時稍微增加一些性能開銷,但是卻能為Java應用程序提供高度的靈活性,Java中天生可以動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特點實現的。例如,如果編寫一個使用接口的應用程序,可以等到運行時再指定其實際的實現。這種組裝應用程序的方式廣泛應用於Java程序之中。
目標:獲取指定包名下的所有類。
分析:由className通過類加載器可以加載到內存中,將包下面的class加載並放入到Set<Class<?>>。包名可能是一個file或者一個jar包。
具體實現:
/**
* 類操作工具類
*/
public final class ClassUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);
/**
* 獲取類加載器
*/
public static ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
/**
* 加載類
*/
public static Class<?> loadClass(String className, boolean isInitialized) {
Class<?> cls;
try {
cls = Class.forName(className, isInitialized, getClassLoader());
} catch (ClassNotFoundException e) {
LOGGER.error("load class failure", e);
throw new RuntimeException(e);
}
return cls;
}
/**
* 加載類(默認將初始化類)
*/
public static Class<?> loadClass(String className) {
return loadClass(className, true);
}
/**
* 獲取指定包名下的所有類
*/
public static Set<Class<?>> getClassSet(String packageName) {
Set<Class<?>> classSet = new HashSet<Class<?>>();
try {
Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
String protocol = url.getProtocol();
if (protocol.equals("file")) {
String packagePath = url.getPath().replaceAll("%20", " ");
addClass(classSet, packagePath, packageName);
} else if (protocol.equals("jar")) {
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
if (jarURLConnection != null) {
JarFile jarFile = jarURLConnection.getJarFile();
if (jarFile != null) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class")) {
String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
doAddClass(classSet, className);
}
}
}
}
}
}
}
} catch (Exception e) {
LOGGER.error("get class set failure", e);
throw new RuntimeException(e);
}
return classSet;
}
private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
File[] files = new File(packagePath).listFiles(new FileFilter() {
public boolean accept(File file) {
return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
}
});
for (File file : files) {
String fileName = file.getName();
if (file.isFile()) {
String className = fileName.substring(0, fileName.lastIndexOf("."));
if (StringUtil.isNotEmpty(packageName)) {
className = packageName + "." + className;
}
doAddClass(classSet, className);
} else {
String subPackagePath = fileName;
if (StringUtil.isNotEmpty(packagePath)) {
subPackagePath = packagePath + "/" + subPackagePath;
}
String subPackageName = fileName;
if (StringUtil.isNotEmpty(packageName)) {
subPackageName = packageName + "." + subPackageName;
}
addClass(classSet, subPackagePath, subPackageName);
}
}
}
private static void doAddClass(Set<Class<?>> classSet, String className) {
Class<?> cls = loadClass(className, false);
classSet.add(cls);
}
}
以下是兩篇我對類加載器的總結:
關於類加載器
類加載器與Web容器
自定義注解
目標:在控制器類上使用Controller注解,在控制器類的方法上使用Action注解,在服務類上使用Service注解,在控制器類中可使用Inject注解將服務類依賴注入進來。
分析:JDK 5中引入了源代碼中的注解(annotation)這一機制。注解使得Java源代碼中不但可以包含功能性的實現代碼,還可以添加元數據。注解的功能類似於代碼中的注釋,所不同的是注解不是提供代碼功能的說明,而是實現程序功能的重要組成部分。Java注解已經在很多框架中得到了廣泛的使用,用來簡化程序中的配置。
Action方法注解代碼如下:
/**
* Action 方法注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
/**
* 請求類型與路徑
*/
String value();
}
- @Target – 這個注解可以讓你來指定你的注解應該被用在那個java元素上. 可能的目標類型是 ANNOTATION_TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER 和 TYPE. 在我們的 @ReconField 注解中他被指定到了 FIELD 級別.
- @Retention – 它可以讓你指定注解在何時生效. 可能的值有 CLASS, RUNTIME 和 SOURCE. 因為我們將會在運行時 RUNTIME 處理這個注解, 所以那就是我們需要設置的值.
- @interface用來聲明一個注解,其中的每一個方法實際上是聲明了一個配置參數。
根據注解獲取相應的類
之前我們已經可以通過ClassUtil類加載指定包下所有的類,這樣我們可以通過循環所有加載的類,根據class的isAnnotationPresent()來判斷是否是對應注解下的類,並將獲取的java.lang.class實例放進classSet。
比如獲取應用包名下所有的Service類:
public static Set<Class<?>> getServiceClassSet() {
Set<Class<?>> classSet = new HashSet<Class<?>>();
for (Class<?> cls : CLASS_SET) {
if (cls.isAnnotationPresent(Service.class)) {
classSet.add(cls);
}
}
return classSet;
}
實現Bean容器
1.通過反射實例化對象。
調用class的newInstance()方法來實例化實例。
/**
* 反射工具類
*/
public final class ReflectionUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtil.class);
/**
* 創建實例
*/
public static Object newInstance(Class<?> cls) {
Object instance;
try {
instance = cls.newInstance();
} catch (Exception e) {
LOGGER.error("new instance failure", e);
throw new RuntimeException(e);
}
return instance;
}
/**
* 創建實例(根據類名)
*/
public static Object newInstance(String className) {
Class<?> cls = ClassUtil.loadClass(className);
return newInstance(cls);
}
/**
* 調用方法
*/
public static Object invokeMethod(Object obj, Method method, Object... args) {
Object result;
try {
method.setAccessible(true);
result = method.invoke(obj, args);
} catch (Exception e) {
LOGGER.error("invoke method failure", e);
throw new RuntimeException(e);
}
return result;
}
/**
* 設置成員變量的值
*/
public static void setField(Object obj, Field field, Object value) {
try {
field.setAccessible(true);
field.set(obj, value);
} catch (Exception e) {
LOGGER.error("set field failure", e);
throw new RuntimeException(e);
}
}
}
2.將實例化的對象放進Map中,通過kay去獲取對應的value(Bean對象)。
實現方法:
/**
* Bean 助手類
*/
public final class BeanHelper {
private static final Map<Class<?>, Object> BEAN_MAP = new HashMap<Class<?>, Object>();
static {
Set<Class<?>> beanClassSet = ClassHelper.getBeanClassSet();
for (Class<?> beanClass : beanClassSet) {
Object obj = ReflectionUtil.newInstance(beanClass);
BEAN_MAP.put(beanClass, obj);
}
}
/**
* 獲取 Bean 映射
*/
public static Map<Class<?>, Object> getBeanMap() {
return BEAN_MAP;
}
/**
* 獲取 Bean 實例
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> cls) {
if (!BEAN_MAP.containsKey(cls)) {
throw new RuntimeException("can not get bean by class: " + cls);
}
return (T) BEAN_MAP.get(cls);
}
/**
* 設置 Bean 實例
*/
public static void setBean(Class<?> cls, Object obj) {
BEAN_MAP.put(cls, obj);
}
}
實現依賴注入功能
至此,我們基本已經完成了所有的工具類,現在將開發IocHelper來實現依賴注入。
只需要在IocHelper的靜態代碼塊中實現相關邏輯,就能完成IOC容器的初始化工作。
當IocHelper這個類被加載的時候,就會加載它的靜態代碼塊。后面統一在一個地方加載這個IocHelper。
代碼如下:
/**
* 依賴注入助手類
*/
public final class IocHelper {
static {
//獲取所有的Bean類與Bean實例之間的映射關系
Map<Class<?>, Object> beanMap = BeanHelper.getBeanMap();
if (CollectionUtil.isNotEmpty(beanMap)) {
//遍歷Bean Map
for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
//從BeanMap中獲取Bean類與Bean實例
Class<?> beanClass = beanEntry.getKey();
Object beanInstance = beanEntry.getValue();
//獲取Bean類定義的所有成員變量(簡稱Bean Field)
Field[] beanFields = beanClass.getDeclaredFields();
if (ArrayUtil.isNotEmpty(beanFields)) {
//遍歷Bean Field
for (Field beanField : beanFields) {
//判斷當前Bean Field是否帶有Inject注解
if (beanField.isAnnotationPresent(Inject.class)) {
//在Bean Map中獲取Bean Field對應的實例
Class<?> beanFieldClass = beanField.getType();
Object beanFieldInstance = beanMap.get(beanFieldClass);
if (beanFieldInstance != null) {
//通過反射初始化BeanField的值
ReflectionUtil.setField(beanInstance, beanField, beanFieldInstance);
}
}
}
}
}
}
}
}
此時,在這個Ioc框架中所管理的對象都是單例的。IOC從BeanHelper中獲取BeanMaP,而BeanMap中的對象都是事先創建好,放入這個Bean容器的。
加載controller
思路:通過ClassHelper,我們可以獲取所有定義了Controller注解的類,可以通過反射獲取該類中所有帶有Action注解的方法(簡稱Action方法),獲取action注解中的請求表達式,進而獲取請求方法與請求路徑,封裝一個請求對象(request)與處理對象(Handler),最后將Request於Handler建立一個隱射關系,放入一個action Map中,並提供一個可根據請求方法與請求路徑獲取處理對象的方法。
ControllerHelper代碼如下:
/**
* 控制器助手類
*/
public final class ControllerHelper {
/**
* 用於存放請求與處理器的映射關系(簡稱Action Map)
*/
private static final Map<Request, Handler> ACTION_MAP = new HashMap<Request, Handler>();
static {
//獲取所有的Controller類
Set<Class<?>> controllerClassSet = ClassHelper.getControllerClassSet();
if (CollectionUtil.isNotEmpty(controllerClassSet)) {
//遍歷這些Controller類
for (Class<?> controllerClass : controllerClassSet) {
//獲取Controller類中定義的方法
Method[] methods = controllerClass.getDeclaredMethods();
if (ArrayUtil.isNotEmpty(methods)) {
//遍歷這些Controller類中定義的方法
for (Method method : methods) {
//判斷當前方法中是否帶有action注解
if (method.isAnnotationPresent(Action.class)) {
//從action注解中獲取Url映射規則
Action action = method.getAnnotation(Action.class);
String mapping = action.value();
//驗證Url映射規則
if (mapping.matches("\\w+:/\\w*")) {
String[] array = mapping.split(":");
if (ArrayUtil.isNotEmpty(array) && array.length == 2) {
//獲取請求方法與請求路徑
String requestMethod = array[0];
String requestPath = array[1];
Request request = new Request(requestMethod, requestPath);
Handler handler = new Handler(controllerClass, method);
//初始化Action Map
ACTION_MAP.put(request, handler);
}
}
}
}
}
}
}
}
/**
* 獲取 Handler
*/
public static Handler getHandler(String requestMethod, String requestPath) {
Request request = new Request(requestMethod, requestPath);
return ACTION_MAP.get(request);
}
}
初始化框架
我們創建了ClassHelper、BeanHelper、IOCHelper、ControllerHelper,這四個Helper類需要通過一個入口來加載他們,實際上就是加載他們的靜態代碼塊。
/**
* 加載相應的 Helper 類
*/
public final class HelperLoader {
public static void init() {
Class<?>[] classList = {
ClassHelper.class,
BeanHelper.class,
AopHelper.class,
IocHelper.class,
ControllerHelper.class
};
for (Class<?> cls : classList) {
ClassUtil.loadClass(cls.getName());
}
}
}
現在就可以直接調用HelperLoader的init方法來加載這些Helper了,這里只是為了加載更集中。
請求轉發器
以上過程都是在為這一步做准備。我們現在需要寫一個Servlet,讓他處理所有的請求。
思路:從HttpServletRequest對象中獲取請求方法與請求路徑,通過ControllerHelper的getHandler方法來獲取handler對象。當拿到Handler對象后,我們可以方便的獲取Controller的類,通過BeanHelper的getBean方法獲取Controller的實例對象。隨后可以從HttpServletRequest對象中的所有請求參數,並將其初始化到一個param的對象中。
在Param中會有一系列的get方法,可通過參數名獲取指定類型的參數值,也可以獲取所有參數的map結構。
還可從Handler對象中獲取Action的方法返回值,該返回值可能有兩種情況:
(1)若返回值是View類型的視圖對象,則返回一個jsp頁面。
(2)若返回值是Data類型的數據對象,則返回一個JSON數據。
一下便是MVC框中最核心的DispatcherServlet類,代碼如下:
/**
* 請求轉發器
*/
@WebServlet(urlPatterns = "/*", loadOnStartup = 0)
public class DispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
//初始化相關的Helper類
HelperLoader.init();
//獲取ServletContext對象(用於注冊Servlet)
ServletContext servletContext = servletConfig.getServletContext();
//注冊Servlet
registerServlet(servletContext);
UploadHelper.init(servletContext);
}
private void registerServlet(ServletContext servletContext) {
//注冊處理JSP的servlet
ServletRegistration jspServlet = servletContext.getServletRegistration("jsp");
jspServlet.addMapping("/index.jsp");
jspServlet.addMapping(ConfigHelper.getAppJspPath() + "*");
//注冊處理靜態資源的默認的servlet
ServletRegistration defaultServlet = servletContext.getServletRegistration("default");
defaultServlet.addMapping("/favicon.ico");
defaultServlet.addMapping(ConfigHelper.getAppAssetPath() + "*");
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletHelper.init(request, response);
try {
//獲取請求方法與請求路徑
String requestMethod = request.getMethod().toLowerCase();
String requestPath = request.getPathInfo();
//獲取Action處理器
Handler handler = ControllerHelper.getHandler(requestMethod, requestPath);
if (handler != null) {
//獲取Controller類機器Bean實例
Class<?> controllerClass = handler.getControllerClass();
Object controllerBean = BeanHelper.getBean(controllerClass);
//創建請求參數對象
Param param;
if (UploadHelper.isMultipart(request)) {
param = UploadHelper.createParam(request);
} else {
param = RequestHelper.createParam(request);
}
//調用Action方法
Object result;
Method actionMethod = handler.getActionMethod();
if (param.isEmpty()) {
result = ReflectionUtil.invokeMethod(controllerBean, actionMethod);
} else {
result = ReflectionUtil.invokeMethod(controllerBean, actionMethod, param);
}
//處理Action返回值
if (result instanceof View) {
handleViewResult((View) result, request, response);
} else if (result instanceof Data) {
handleDataResult((Data) result, response);
}
}
} finally {
ServletHelper.destroy();
}
}
private void handleViewResult(View view, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String path = view.getPath();
if (StringUtil.isNotEmpty(path)) {
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);
}
}
}
private void handleDataResult(Data data, HttpServletResponse response) throws IOException {
Object model = data.getModel();
if (model != null) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
String json = JsonUtil.toJson(model);
writer.write(json);
writer.flush();
writer.close();
}
}
}
通過這個DispatcherServlet來處理所有的請求,根據請求信息從ControllerHelper中獲取對應的Action方法,然后通過反射技術調用Action方法,同時需要具體的傳入方法參數,最后拿到返回值並判斷返回值的類型,進行相應的處理。
總結
在這一章,搭建了一個簡單的MVC框架,定義了一系列的注解;通過一系列的Helper類來初始化MVC框架;通過DispatcherServlet來處理所有的請求;根據請求方法和請求路徑來來調用具體的Action方法,判斷Action方法的返回值,若為View類型,則調轉到JSP頁面,若為Data類型,則返回json數據。
在這一章中,學習了jdk中的類加載器、注解,對Java的認識又更深一步,期間感謝在黃老師創建的群里耐心回答我的問題的朋友。