最近手頭正好有些時間,想着寫點什麼好呢?後來看到了一篇帖子說面試的時候有面試官問他能不能手寫一套SpringMvc出來,不拉不拉的…不多說了。
所以想着就寫寫試試,捋了捋思路,無非就是三點(大神勿噴!):
- 實例化
- 注入
- url映射
- aop暫時先不寫,後面會寫到
連起來說就是對加了@Controller、@Service註解的對象進行實例化,然後對這些對象中的某些加了@Autowired註解的屬性進行依賴注入,然後對Controller中加了@RequestMapping註解的方法做url映射,用於請求來到的時候根據url映射到需要執行的方法,同時將傳遞的參數注入到方法中。
額。。。這個說的有點敷衍,確實打字太費勁了,最喜歡直接貼代碼了!不過本着敬業的原則還是再重新說一下這個項目具體實現了哪些功能及使用方法。
- 實例化規則,加了@Controller、@Service註解的對象默認beanName就是類名首字母小寫,同時也可以寫別名,那麼beanName就是別名
- 注入規則,加了@Autowired註解的屬性,默認是通過這個屬性的類型來找他的實現類,如果該接口實現類有多個那麼拋出異常!那麼我非得有兩個實現類怎麼辦呢?可以,你需要給實現類起個別名比如@Service(“woShiNumberOne”),然後注入的時候@Autowired(“woShiNumberOne”)這樣注入就可以了。
- url映射方法,就是掃描每個Controller,如果類名上加了@RequestMapping註解那麼這算是根目錄,然後逐個掃描方法,只要方法上加了@RequestMapping註解那麼這就是子目錄,最後會將根目錄+子目錄拼接到一起映射到當前controller的當前方法。
- 方法參數注入規則,自動識別當前方法有多少個參數,除了request和response這兩個參數以外的其他任何參數都需要加@RequestParam註解,用來給這個參數定義別名,否則無法注入進來!而且由於是簡化版這裏只支持基本數據類型的注入,不支持對象的注入,這點還沒來得及寫。
這樣可以了吧,下面可以開心的來讀代碼啦!!!
想要下載源碼的小夥伴可以看這裏,代碼已經上傳到了碼雲大家可以通過git下載:
https://git.oschina.net/sunchenbin/SpringMvcSimulate.git
不下源碼的直接來看下項目結構:
OK下面我們先來自定義5個註解:
/**
* @Description controller註冊的註解
* @author chenbin.sun
* @date 2017年8月30日下午5:12:55
*
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
/**
* 表示給controller註冊別名
* @return
*/
String value() default "";
}
/**
* @Description service註冊的註解
* @author chenbin.sun
* @date 2017年8月30日下午5:12:40
*
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
/**
* 表示給service註冊別名
* @return
*/
String value() default "";
}
/**
* @Description controller和方法上的註解
* @author chenbin.sun
* @date 2017年8月30日下午5:16:05
*
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
/**
* 表示訪問該方法的url
* @return
*/
String value() default "";
}
/**
* @Description 自動注入註解(如果不加別名自動通過接口類型注入實現類)
* @author chenbin.sun
* @date 2017年8月30日下午5:12:40
*
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
/**
* 表示給filed注入的bean的name
* @return
*/
String value() default "";
}
/**
* @Description 用作請求傳參數的別名
* @author chenbin.sun
* @date 2017年8月31日下午2:31:42
*
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
/**
* 表示參數的別名,必填
* @return
*/
String value();
}
這五個註解的作用上面都描述了,有一點值得注意@RequestParam這個註解是用於寫在方法的參數上的,用於給這個參數起別名的,前端傳過來的參數的key必須和這個相同才能注入進來,否則注入失敗,當然這裏其實還可以擴展一些應用場景,比方說該參數是否必須傳入啊等等,但這些都要相應的方法去做對應的實現,目前我沒有寫那麼多。
下面看我的控制層,很簡單,幾種測試場景我都寫上去了
/**
* @Description 測試控制器類
* @author chenbin.sun
* @date 2017年8月31日下午6:57:35
*
*/
@Controller
@RequestMapping("/test")
public class TestController {
@Autowired("testServiceImpl")
private TestService testService;
@Autowired
private TestService2 testService2;
@RequestMapping("/doTest")
public void test(HttpServletRequest request, HttpServletResponse response, @RequestParam("param") String param){
String result = testService.test();
try {
response.getWriter().println("do service result:" + result);
} catch (IOException e) {
e.printStackTrace();
}
}
@RequestMapping("/doTest2")
public void test2(HttpServletRequest request, HttpServletResponse response){
String result = testService2.test2();
try {
response.getWriter().println("do service2 result:" + result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
然後是業務邏輯層,先上兩個接口
public interface TestService {
String test();
}
public interface TestService2 {
String test2();
}
so easy對不對!繼續看兩個實現類,爲了測試service之間也能相互注入,所以寫了兩個接口兩個實現類
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestService2 testService2;
@Override
public String test() {
System.out.println(testService2.test2());
return "method test do success!";
}
}
@Service
public class TestServiceImpl2 implements TestService2 {
@Override
public String test2() {
return "method test2 do success!";
}
}
OK,下面重點來了,核心代碼支持全部註解特性的功能都在這裏
/**
* @Description 請求幾種處理類
* @author chenbin.sun
* @date 2017年8月30日下午5:23:54
*
*/
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1378531571714153483L;
/** 要掃描的包,只有在這個包下並且加了註解的纔會唄掃描到 */
private static final String PACKAGE = "chenbin.sun";
private static final String CONTROLLER_KEY = "controller";
private static final String METHOD_KEY = "method";
/** 存放Controller中url和方法的對應關係,格式:{url:{controller:實例化後的對象,method:實例化的方法}} */
private static Map<String, Map<String, Object>> urlMethodMapping = new HashMap<>();
public DispatcherServlet() {
super();
}
/**
* 初始化方法,用於實例化掃描到的對象,並做注入和url映射(注:該方法邏輯上已經判斷了,只執行一次)
*/
@Override
public void init(ServletConfig config) throws ServletException {
// 只處理一次
if (urlMethodMapping.size() > 0) {
return;
}
// 開始掃描包下全部class文件
Set<Class<?>> classes = ClassTools.getClasses(PACKAGE);
// 存放Controller和Service的Map,格式:{beanName:實例化後的對象}
Map<String, Object> instanceNameMap = new HashMap<String, Object>();
// 存放Service接口類型與接口實例對象的Map,格式:{Service.instance.class:實現類實例化後的對象}
Map<Class<?>, Object> instanceTypeMap = new HashMap<Class<?>, Object>();
// 組裝instanceMap
buildInstanceMap(classes, instanceNameMap, instanceTypeMap);
// 開始注入
doIoc(instanceNameMap, instanceTypeMap);
// 注入完之後開始映射url和method
buildUrlMethodMapping(instanceNameMap, urlMethodMapping);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 完整路徑
String url = req.getRequestURI();
// 跟路徑
String path = req.getContextPath();
// 計算出method上配置的路徑
String finallyUrl = url.replace(path, "");
// 取出這個url對應的Controller和method
Map<String, Object> map = urlMethodMapping.get(finallyUrl);
if (map == null) {
throw new RuntimeException("請求地址不存在!");
}
Method method = (Method) map.get(METHOD_KEY);
try {
// 封裝需要注入的參數,目前只支持request和response以及加了@RequestParam標籤的基本數據類型的參數注入
List<Object> paramValue = buildParamObject(req, resp, method);
// 沒有參數的場合
if (paramValue.size() == 0) {
method.invoke(map.get(CONTROLLER_KEY));
}else {
// 有參數的場合
method.invoke(map.get(CONTROLLER_KEY), paramValue.toArray());
}
} catch (Exception e) {
throw new RuntimeException("執行url對應的method失敗!");
}
}
/**
* 封裝需要注入的參數,目前只支持request和response以及加了@RequestParam標籤的基本數據類型的參數注入
* @param req
* @param resp
* @param method
* @return
*/
private List<Object> buildParamObject(HttpServletRequest req, HttpServletResponse resp, Method method) {
// 封裝需要注入的參數,目前只支持request和response以及加了@RequestParam標籤的基本數據類型的參數注入
Parameter[] parameters = method.getParameters();
List<Object> paramValue = new ArrayList<>();
for (Parameter parameter : parameters) {
// 當前參數有別名註解並且別名不爲空
if(parameter.isAnnotationPresent(RequestParam.class) && !parameter.getAnnotation(RequestParam.class).value().isEmpty()){
// 我們獲取
String value = req.getParameter(parameter.getAnnotation(RequestParam.class).value());
paramValue.add(value);
}else if (parameter.getParameterizedType().getTypeName().contains("HttpServletRequest")) {
paramValue.add(req);
}else if (parameter.getParameterizedType().getTypeName().contains("HttpServletResponse")) {
paramValue.add(resp);
}else{
paramValue.add(null);
}
// 這裏只做了request和response以及基本數據類型的參數注入,如果要做對象的注入也是可以寫,這裏暫時就不寫了
// TODO: 做對象的注入
}
return paramValue;
}
/**
* 注入完之後開始映射url和method
* @param instanceMap
* @param urlMethodMapping
*/
private void buildUrlMethodMapping(Map<String, Object> instanceMap,
Map<String, Map<String, Object>> urlMethodMapping) {
// 注入完之後開始映射url和method
// 組裝urlMethodMapping
for (Entry<String, Object> entry : instanceMap.entrySet()) {
// 迭代出所有的url
String parenturl = "";
// 判斷Controller上是否加了requestMapping
if (entry.getValue().getClass().isAnnotationPresent(RequestMapping.class)) {
parenturl = entry.getValue().getClass().getAnnotation(RequestMapping.class).value();
}
// 取出全部的method
Method[] methods = entry.getValue().getClass().getMethods();
// 迭代全部的方法,檢查哪些方法上加了requestMaping註解
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
// 得到一個完整的url請求
String url = parenturl + method.getAnnotation(RequestMapping.class).value();
Map<String, Object> value = new HashMap<>();
value.put(CONTROLLER_KEY, entry.getValue());
value.put(METHOD_KEY, method);
urlMethodMapping.put(url, value );
}
}
}
}
/**
* 根據實例Map開始注入
* @param instanceMap
*/
private void doIoc(Map<String, Object> instanceMap, Map<Class<?>, Object> instanceTypeMap) {
// 開始注入,我們只對加了@Controller和@Service標籤中的,屬性加了@autowired的進行注入操作
for (Entry<String, Object> entry : instanceMap.entrySet()) {
// 取出全部的屬性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
// 循環屬性校驗哪些是加了@autowired註解的
for (Field field : fields) {
field.setAccessible(true);// 可訪問私有屬性
// 有註解的時候
if (field.isAnnotationPresent(Autowired.class)) {
// 沒有配別名注入的時候
if (field.getAnnotation(Autowired.class).value().isEmpty()) {
// 直接獲取
try {
// 根據類型來獲取他的實現類
Object object = instanceTypeMap.get(field.getType());
field.set(entry.getValue(), object);
} catch (IllegalArgumentException | IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
try {
// 將被注入的對象
Object object = instanceMap.get(field.getAnnotation(Autowired.class).value());
field.set(entry.getValue(), object);
} catch (Exception e) {
throw new RuntimeException("開始注入時出現了異常");
}
}
}
}
}
}
/**
* 組裝instanceMap
* @param classes
* @param instanceMap
*/
private void buildInstanceMap(Set<Class<?>> classes, Map<String, Object> instanceMap, Map<Class<?>, Object> instanceTypeMap) {
// 開始循環全部class
for (Class<?> clasz : classes) {
// 組裝instanceMap
// 判斷是否是是加了Controller註解的java對象
if (clasz.isAnnotationPresent(Controller.class)) {
try {
// 實例化對象
Object obj = clasz.newInstance();
Controller controller = clasz.getAnnotation(Controller.class);
// 如果沒有設置別名,那麼用類名首字母小寫做key
if (controller.value().isEmpty()) {
instanceMap.put(firstLowerName(clasz.getSimpleName()), obj);
}else{
// 如果設置了別名那麼用別名做key
instanceMap.put(controller.value(), obj);
}
} catch (Exception e) {
throw new RuntimeException("初始化instanceMap時在處理Controller註解時出現了異常");
}
}else if(clasz.isAnnotationPresent(Service.class)) {
// 實例化對象
Object obj = null;
try {
// 實例化對象
obj = clasz.newInstance();
Service service = clasz.getAnnotation(Service.class);
// 如果沒有設置別名,那麼用類名首字母小寫做key
if (service.value().isEmpty()) {
instanceMap.put(firstLowerName(clasz.getSimpleName()), obj);
}else{
// 如果設置了別名那麼用別名做key
instanceMap.put(service.value(), obj);
}
} catch (Exception e) {
throw new RuntimeException("初始化instanceMap時在處理Service註解時出現了異常");
}
// 實現的接口數組
Class<?>[] interfaces = clasz.getInterfaces();
for (Class<?> class1 : interfaces) {
if (instanceTypeMap.get(class1) != null) {
throw new RuntimeException(class1.getName() + "接口不能被多個類實現!");
}
instanceTypeMap.put(class1, obj);
}
}else {
continue;
}
}
}
/**
* 首字母小寫
* @param name
* @return
*/
private String firstLowerName(String name) {
name = name.substring(0, 1).toLowerCase() + name.substring(1);
return name;
}
}
因爲掃包的代碼太長了,所以我單獨寫成了工具類,代碼如下
public class ClassTools {
/**
* 從包package中獲取所有的Class
*
* @param pack
* @return
*/
public static Set<Class<?>> getClasses(String pack) {
// 第一個class類的集合
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
// 是否循環迭代
boolean recursive = true;
// 獲取包的名字 並進行替換
String packageName = pack;
String packageDirName = packageName.replace('.', '/');
// 定義一個枚舉的集合 並進行循環來處理這個目錄下的things
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
// 循環迭代下去
while (dirs.hasMoreElements()) {
// 獲取下一個元素
URL url = dirs.nextElement();
// 得到協議的名稱
String protocol = url.getProtocol();
// 如果是以文件的形式保存在服務器上
if ("file".equals(protocol)) {
System.err.println("file類型的掃描");
// 獲取包的物理路徑
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式掃描整個包下的文件 並添加到集合中
findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes);
} else if ("jar".equals(protocol)) {
// 如果是jar包文件
// 定義一個JarFile
System.err.println("jar類型的掃描");
JarFile jar;
try {
// 獲取jar
jar = ((JarURLConnection) url.openConnection()).getJarFile();
// 從此jar包 得到一個枚舉類
Enumeration<JarEntry> entries = jar.entries();
// 同樣的進行循環迭代
while (entries.hasMoreElements()) {
// 獲取jar裏的一個實體 可以是目錄 和一些jar包裏的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 如果是以/開頭的
if (name.charAt(0) == '/') {
// 獲取後面的字符串
name = name.substring(1);
}
// 如果前半部分和定義的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
// 如果以"/"結尾 是一個包
if (idx != -1) {
// 獲取包名 把"/"替換成"."
packageName = name.substring(0, idx).replace('/', '.');
}
// 如果可以迭代下去 並且是一個包
if ((idx != -1) || recursive) {
// 如果是一個.class文件 而且不是目錄
if (name.endsWith(".class") && !entry.isDirectory()) {
// 去掉後面的".class" 獲取真正的類名
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
// 添加到classes
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log
// .error("添加用戶自定義視圖類錯誤
// 找不到此類的.class文件");
e.printStackTrace();
}
}
}
}
}
} catch (IOException e) {
// log.error("在掃描用戶定義視圖時從jar包獲取文件出錯");
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
/**
* 以文件的形式來獲取包下的所有Class
*
* @param packageName
* @param packagePath
* @param recursive
* @param classes
*/
public static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive,
Set<Class<?>> classes) {
// 獲取此包的目錄 建立一個File
File dir = new File(packagePath);
// 如果不存在或者 也不是目錄就直接返回
if (!dir.exists() || !dir.isDirectory()) {
// log.warn("用戶定義包名 " + packageName + " 下沒有任何文件");
return;
}
// 如果存在 就獲取包下的所有文件 包括目錄
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定義過濾規則 如果可以循環(包含子目錄) 或則是以.class結尾的文件(編譯好的java類文件)
public boolean accept(File file) {
return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
}
});
// 循環所有文件
for (File file : dirfiles) {
// 如果是目錄 則繼續掃描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive,
classes);
} else {
// 如果是java類文件 去掉後面的.class 只留下類名
String className = file.getName().substring(0, file.getName().length() - 6);
try {
// 添加到集合中去
// classes.add(Class.forName(packageName + '.' +
// className));
// 經過回覆同學的提醒,這裏用forName有一些不好,會觸發static方法,沒有使用classLoader的load乾淨
classes.add(
Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log.error("添加用戶自定義視圖類錯誤 找不到此類的.class文件");
e.printStackTrace();
}
}
}
}
/**
* 取出list對象中的某個屬性的值作爲list返回
*
* @param objList
* @param fieldName
* @return
*/
public static <T, E> List<E> getPropertyValueList(List<T> objList, String fieldName) {
List<E> list = new ArrayList<E>();
try {
for (T object : objList) {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
list.add((E) field.get(object));
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
然後將這個Servlet配置到web.xml中,代碼如下
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>SpringMvcSimulate</display-name>
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>chenbin.sun.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- ... -->
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
好了現在這個簡化版的springmvc就完成了,啓動tomcat後,輸入請求url地址:
http://localhost:8080/SpringMvcSimulate/test/doTest?param=aaaaa
可以看到方法執行完畢後返回的信息。