修订日期 | 内容 |
---|---|
2021-3-7 | 初稿 |
手写spring mvc-简单了解spring bean,ioc 容器,spring mvc原理,超级简单(附源码)
简介
本章目的:
- 了解spring mvc中一个请求是如何到达Controller的方法的
- 了解spring ioc容器的创建过程
先看一段Spring的代码
@Controller
@RequestMapping("/order")
public class OrderController {
@Autowired
OrderService orderService;
@RequestMapping(value = "/send")
@ResponseBody
public String send(){
return orderService.send();
}
相信这段代码大家已经非常熟悉了,实现了一个请求在前端(浏览器)调用http://xxxx/order/send,则会进入到send()
方法中调用orderService
的方法并返回客户端的过程。
这里面有两个问题:
- 请求’http://xxxx/order/send’是如何进入到使用了注解
@RequestMapping(value = "/send")
的send()
方法中的? - 使用了
@Autowired
的类OrderService orderService
并没有看见创建对象(new OrderService),那么这个orderService.send()
是如何能够被调用?
简单实现(手撸一个简单的mvc)
下面介绍思路与实现步骤,项目完整源码地址:https://github.com/jurnea/my-mvc/tree/simple
1.先创建一个普通的web
项目,引入相关maven依赖
<!-- web项目必须添加-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<!-- 添加日志依赖logback(不需要输出日志可以不用添加该依赖)-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
- 模仿Spring代码,编写我们自己的注解
这里我们先使用如下注解
- Autowire
- Controller
- RequestMapping
- Service
分别创建注解类
// 在自己的项目中分别创建如下4个注解
@Target({
ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowire {
}
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
@Target({
ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value() default "";
}
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
- 创建自己的
Controller
、Service
类并使用我们刚刚创建的自己的注解
// 这里面的注解都是我们自己定义的
@Controller
@RequestMapping(value = "/order")
public class OrderController {
private Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowire
private OrderService orderService;
@RequestMapping(value = "/query")
public Object query(){
logger.info("request url invoked controller method");
String result = orderService.queryOrder();
return result;
}
// 这里面的注解都是我们自己定义的
@Service
public class OrderService {
public String queryOrder(){
return "order service 订单查询成功...";
}
}
- 创建我们的核心处理Servlet,并重写init、doGet、doPost方法
spring的ioc容器创建类,与请求转发将在该类实现,具体实现细节情况核心实现
public class DispatcherServlet extends HttpServlet{
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
- 在
web.xml
中配置自定义的servlet,将所有的请求都转发到我们的DispatcherServlet
<servlet>
<servlet-name>my-mvc</servlet-name>
<!-- 上面直接定义的DispatcherServlet-->
<servlet-class>org.chenglj.mvc.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>my-mvc</servlet-name>
<!--拦截所有的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
DispatcherServlet
核心实现
思路
- 我们应该扫描包下面所有使用了
@Controller
、@Service
的类,并创建实列对象 - 对
Controller
中使用的@Autowire
的属性赋值 - 对使用了
@RequestMapping
的类与方法与前端请求对应起来,使得请求能够一一对应的Controller方法上
实现
0. 初始化
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
//1。扫描包
scanPackage("");
//2.创建对象
newInstance();
//3.添加依赖注入
addDependency();
//4.映射url与方法
addMapping();
}
1. 扫描所有的包
//收集项目下所有的Class-》目的,找出含有`@Controller`、`@Service`的类,并为他们创建对象
List<String> classNames = new ArrayList<>();
public void scanPackage(String backPackage) {
logger.info("扫描包:backPackage->{}",backPackage);
String path = DispatcherServlet.class.getClassLoader().getResource(backPackage).getPath();
File filePath = new File(path);
File[] files = filePath.listFiles();
for (File file : files) {
if(file.isDirectory()){
scanPackage(backPackage+file.getName()+"/");
} else {
if(!file.getName().endsWith(".class")){
continue;
}
//不是目录,加载完整类名 simpleName -> OrderController
String simpleName = file.getName().substring(0,file.getName().lastIndexOf("."));
String className = backPackage.replaceAll("\\/",".")+simpleName;
classNames.add(className);
}
}
}
2. 为Controller
、@Service
创建实列对象,并存入Bean容器
// 创建容器-手机实列对象bean 存储:full className -> instance
private Map<String,Object> beans = new ConcurrentHashMap();
private void newInstance() {
try {
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className);
if(clazz.isAnnotationPresent(Controller.class)
|| clazz.isAnnotationPresent(Service.class)){
Object obj = clazz.newInstance();
beans.put(clazz.getName(),obj);
logger.info("init bean :{}",clazz.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
// 初始化bean完成后className销毁
classNames.clear();
classNames = null;
} catch (Exception e) {
logger.error("new instance error",e);
}
}
3. 对使用的@Autowire
添加依赖
private void addDependency() {
logger.info("staring add bean dependency");
try {
for (Map.Entry<String, Object> entry : beans.entrySet()) {
//获取所有的属性field
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
Autowire annotation = field.getAnnotation(Autowire.class);
if(annotation != null){
field.setAccessible(true);
String className = field.getType().getName();
field.set(entry.getValue(),beans.get(className));
}
}
}
} catch (Exception e) {
logger.error("add bean dependency error !",e);
}
}
4.添加映射
对Controller中类上与方法上的@RequestMapping
添加url映射关系
//收集映射关系 url-> (className,Method)
private Map<String, Mapping> mappings = new ConcurrentHashMap<>();
private void addMapping() {
try {
for (Map.Entry<String, Object> entry : beans.entrySet()) {
//获取所有的属性
Class<?> beanClazz = entry.getValue().getClass();
boolean controllerPresent = beanClazz.isAnnotationPresent(Controller.class);
RequestMapping requestMappingAnnotation = beanClazz.getDeclaredAnnotation(RequestMapping.class);
//既有Controller注解也有RequestMapping注解
if(controllerPresent && requestMappingAnnotation != null){
//类上的注解@RequestMapping中的值
String classMappingValue = requestMappingAnnotation.value();
Method[] declaredMethods = beanClazz.getDeclaredMethods();
for (Method method : declaredMethods) {
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
if(annotation == null){
continue;
}
//方法中@RequestMapping的值
String methodMappingValue = annotation.value();
String url = classMappingValue+methodMappingValue;
mappings.put(url,new Mapping(entry.getKey(),method));
logger.info("初始化[{}}] -> {}",url,method.getName());
}
}
}
} catch (Exception e) {
logger.error("add mapping error",e);
}
}
// 用于存放className 与 Method
class Mapping {
private String className;
private Method method;
public Mapping(String className, Method method) {
this.className = className;
this.method = method;
}
// getting/setting
}
请求转发(完成)
对doGet
/doPost
方法转发请求
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
// 项目名
String contextPath = req.getContextPath();
String url = requestURI.replaceAll(contextPath, "");
logger.info("request url [{}]",url);
Mapping mapping = mappings.get(url);
if( mapping !=null){
Object controllerObj = beans.get(mapping.getClassName());
Method method = mapping.getMethod();
try {
Object invokeResult = method.invoke(controllerObj);
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write((String)invokeResult);
logger.info("返回内容:{}",invokeResult);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
这样一个简单的Spring mvc项目就完成了,启动项目使用http://xxxx/order/query请求访问吧。
本项目简单实现源码:https://github.com/jurnea/my-mvc/tree/simple
优化
上面的项目已经实现了一个简单的原理介绍,当然里面还有很多不完善的地方,比如
- 前端传入参数
- 大多Service都是通过接口编程
- 有些请求是要转到一个页面的
- …
大家有兴趣可以继续完善该项目