Spring MVC
应用
Spring MVC和Struts2⼀样,都是 为了解决表现层问题 的web框架,它们都是基于 MVC 设计模式的。而这些表现层框架的主要职责就是处理前端HTTP请求。通过⼀套注解,让⼀个简单的 Java 类成为处理请求的控制器,⽽⽆须实现任何接⼝。同时它还⽀持RESTful 编程⻛格的请求。
Spring MVC 本质可以认为是对servlet的封装,简化了我们servlet的开发
作⽤:1)接收请求 2)返回响应,跳转⻚⾯
经典三层和MVC模式
Spring MVC模式和Servlet模式
开发流程回顾
1)配置DispatcherServlet前端控制器
2)开发处理具体业务逻辑的Handler(@Controller、@RequestMapping)
3)xml配置⽂件配置controller扫描,配置springmvc三⼤件
4)将xml⽂件路径告诉springmvc(DispatcherServlet)
请求处理流程
前端控制器将任务派发给其他组件执行
1.⽤户发送请求至前端控制器DispatcherServlet
2.DispatcherServlet收到请求调⽤HandlerMapping处理器映射器
3.处理器映射器根据请求Url找到具体的Handler(后端控制器),⽣成处理器对象及处理器拦截
器(如果 有则⽣成)⼀并返回DispatcherServlet
4.DispatcherServlet调⽤HandlerAdapter处理器适配器去调⽤Handler
5.处理器适配器执行Handler
6.Handler执⾏完成给处理器适配器返回ModelAndView
7.处理器适配器向前端控制器返回 ModelAndView,ModelAndView 是SpringMVC 框架的⼀个
底层对象,包括 Model 和 View
8.前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图。
9.视图解析器向前端控制器返回View
10.前端控制器进⾏视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域
11.前端控制器向⽤户响应结果
九大组件
HandlerMapping(处理器映射器)
HandlerMapping 是⽤来查找 Handler 的,也就是处理器,具体的表现形式可以是类,也可以是⽅法。⽐如,标注了@RequestMapping的每个⽅法都可以看成是⼀个Handler。Handler负责具体实际的请求处理,在请求到达后,HandlerMapping 的作⽤便是找到请求相应的处理器Handler 和 Interceptor.
HandlerAdapter(处理器适配器)
HandlerAdapter 是⼀个适配器。因为 Spring MVC 中 Handler 可以是任意形式的,只要能处理请
求即可。但是把请求交给 Servlet 的时候,由于 Servlet 的⽅法结构都是doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理⽅法调⽤ Handler 来进⾏处理,便是 HandlerAdapter的职责。
HandlerExceptionResolver
HandlerExceptionResolver ⽤于处理 Handler 产⽣的异常情况。它的作⽤是根据异常设置ModelAndView,之后交给渲染⽅法进⾏渲染,渲染⽅法会将 ModelAndView 渲染成⻚⾯。
ViewResolver
ViewResolver即视图解析器,⽤于将String类型的视图名和Locale解析为View类型的视图,只有⼀个resolveViewName()⽅法。从⽅法的定义可以看出,Controller层返回的String类型视图名viewName 最终会在这⾥被解析成为View。View是⽤来渲染⻚⾯的,也就是说,它会将程序返回的参数和数据填⼊模板中,⽣成html⽂件。ViewResolver 在这个过程主要完成两件事情:ViewResolver 找到渲染所⽤的模板(第⼀件⼤事)和所⽤的技术(第⼆件⼤事,其实也就是找到视图的类型,如JSP)并填⼊参数。默认情况下,Spring MVC会⾃动为我们配置⼀个InternalResourceViewResolver,是针对 JSP 类型视图的。
RequestToViewNameTranslator
RequestToViewNameTranslator 组件的作⽤是从请求中获取 ViewName.因为ViewResolver 根据ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName,便要通过这个组件从请求中查找 ViewName。
LocaleResolver
ViewResolver 组件的 resolveViewName ⽅法需要两个参数,⼀个是视图名,⼀个是 Locale。LocaleResolver ⽤于从请求中解析出 Locale,⽐如中国 Locale 是 zh-CN,⽤来表示⼀个区域。这
个组件也是 i18n 的基础。
ThemeResolver
ThemeResolver 组件是⽤来解析主题的。主题是样式、图⽚及它们所形成的显示效果的集合。Spring MVC 中⼀套主题对应⼀个 properties⽂件,⾥⾯存放着与当前主题相关的所有资源,如图⽚、CSS样式等。创建主题⾮常简单,只需准备好资源,然后新建⼀个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚⾯中使⽤了。SpringMVC中与主题相关的类有ThemeResolver、ThemeSource和Theme。ThemeResolver负责从请求中解析出主题名,
ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。
MultipartResolver
MultipartResolver ⽤于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现。MultipartHttpServletRequest 可以通过 getFile() ⽅法 直接获得⽂件。如果上传多个⽂件,还可以调⽤getFileMap()⽅法得到Map<FileName,File>这样的结构,MultipartResolver 的作⽤就是封装普通的请求,使其拥有⽂件上传的功能。
FlashMapManager
FlashMap ⽤于重定向时的参数传递,⽐如在处理⽤户订单时候,为了避免重复提交,可以处理完post请求之后重定向到⼀个get请求,这个get请求可以⽤来显示订单详情之类的信息。这样做虽然可以规避⽤户重新提交订单的问题,但是在这个页面上要显示订单的信息,这些数据从哪⾥来获得呢?因为重定向时么有传递参数这⼀功能的,如果不想把参数写进URL(不推荐),那么就可以通过FlashMap来传递。只需要在重定向之前将要传递的数据写⼊请求(可以通过ServletRequestAttributes.getRequest()⽅法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE中,这样在重定向之后的Handler中Spring就会⾃动将其设置到Model中,在显示订单信息的⻚⾯上就可以直接从Model中获取数据。FlashMapManager 就是⽤来管理 FalshMap 的。
url-pattern配置及原理
拦截方式
方式一:带后缀,比如 *.action *.do *.aaa
方式二: / 不会拦截.jsp 但是会拦截.html等静态资源
项目中的web.xml继承自tomcat容器中的web.xml,tomcat中的web.xml有DefaultServlet(服务于所有的静 态资源),url-pattern为/ ;项目组的web.xml中也配置了 / ,覆写了web.xml的配置,也就使 DefaultServlet不生效了,因此也会拦截静态资源
方式三: /* 拦截所有,包括.jsp
解决 / 拦截静态资源
静态资源配置
<!-- 方案一
原理:添加该标签配置后,会在SpringMVC上下文中定义一个DefaultServletHttpRequestHandler对象
这个对象对url请求进行筛查,如果是静态资源交由web应用服务器(tomcat)默认的DefaultServlet来处理
缺点:只能将静态资源放在webapp下,不能放在WEB-INF、classpath下;
-->
<mvc:default-servlet-handler/>
<!-- 方案二 SpringMVC框架自己处理静态资源
mapping: 约定静态资源的url规则
指定多个路径 /,classpath:/
location:指定静态资源存放位置
-->
<mvc:resources location="classpath:/" mapping="/resources/**" />
封装数据
SpringMVC在handler方法上传入Map、Model和ModelMap参数,并向参数中保存数据(放到请求域),都可以在页面上获取到;
运行时的具体类型都是BindingAwareModelMap,相当于给BindingAwareModelMap中保存的数据放到请求域中。
Map jdk接口
Model spring接口
ModelMap class,实现Map接口
BindingAwareModelMap 继承了 ExtendedModelMap;ExtendedModelMap继承了ModelMap,实现了Model接口
请求参数绑定
原⽣servlet接收⼀个整型参数:
String ageStr = request.getParameter("age");
Integer age = Integer.parseInt(ageStr);
SpringMVC框架对Servlet的封装,简化了servlet的很多操作
SpringMVC在接收整型参数的时候,直接在Handler⽅法中声明形参即可
@RequestMapping("xxx")
public String handle(Integer age) {
System.out.println(age);
}
参数绑定:取出参数值绑定到handler⽅法的形参上
SpringMVC接收参数类型
原生servlet api[HttpServletRequest,HttpServletResponse,HttpSession]支持:直接在handler方法形参中声明使用即可;
简单数据类型参数(8种基本类型及其包装类):直接在handler方法形参中声明,框架会取出参数值然后绑定到对应参数上,要求形参名和声明的形参名称一致(或者在参数上加上@RequestParam)。
pojo类型参数,直接形参声明即可,类型就是Pojo的类型,形参名⽆所谓,但是要求传递的参数名必须和Pojo的属性名保持⼀致。
pojo包装类型参数[如: Vo],绑定时候直接形参声明即可;传参参数名和pojo属性保持⼀致,如果不能够定位数据项,那么通过属性名 + "." 的方式进⼀步锁定数据。
日期类型,需要定义一个SpringMVC的类型转换器 ——接口,扩展实现接口,注册实现
//扩展自定义转换器
/**
* ⾃定义类型转换器
* S:source,源类型
* T:target:⽬标类型
*/
public class DateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
// 完成字符串向⽇期的转换
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
Date parse = simpleDateFormat.parse(source);
return parse;
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
<!-- 注册自定义类型转换器-->
<bean id="conversionServiceBean" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.converter.DateConverter"></bean>
<bean class="其他转换器"></bean>
</set>
</property>
</bean>
<mvc:annotation-driven conversion-service="conversionServiceBean" />
Rest风格请求
@RequestMapping(value="/test/{id}" ,method= {RequestMethod.GET})
@PathVariable("id") 从uri中取值 ;
post 请求乱码: springmvc提供的配置过滤器
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
针对put、delete等请求方式转换过滤器
判断请求中有无_method参数,有的话就拦截处理
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Ajax Json交互
依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.0</version>
</dependency>
交互:
前端->后端: ajax发送json格式字符串,后台直接接受为pojo参数(@RequestBody)
后端->前端: 后台直接返回pojo对象,前端直接接收为json对象或字符串(@ResponseBody : 返回的数据不再走视图解析器流程,而是等同于response直接输出数据)
高级技术
监听器,过滤器,拦截器
过滤器(Filter):对Request请求起到过滤的作⽤,作⽤在Servlet之前,如果配置为/*可以对所有的资源访问(servlet、js/css静态资源等)进⾏过滤处理
监听器(Listener):实现了javax.servlet.ServletContextListener 接⼝的服务器端组件,它随Web应用的启动而启动,只初始化⼀次,然后会⼀直运⾏监视,随Web应⽤的停止而销毁。
作⽤⼀:做⼀些初始化⼯作,web应⽤中spring容器启动ContextLoaderListener
作⽤⼆:监听web中的特定事件,⽐如HttpSession,ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控,⽐如统计在线⼈数,利⽤HttpSessionLisener等。
拦截器(Interceptor):是SpringMVC、Struts等表现层框架⾃⼰的,不会拦截
jsp/html/css/image的访问等,只会拦截访问的控制器⽅法(Handler)。
拦截器
在运⾏程序时,拦截器的执⾏是有⼀定顺序的,该顺序与配置⽂件中所定义的拦截器的顺序相关。
单个拦截器
1)程序先执⾏preHandle()⽅法,如果该⽅法的返回值为true,则程序会继续向下执⾏处理器中的⽅
法,否则将不再向下执⾏。
2)在业务处理器(即控制器Controller类)处理完请求后,会执⾏postHandle()⽅法,然后会通过
DispatcherServlet向客户端返回响应。
3)在DispatcherServlet处理完请求后,才会执⾏afterCompletion()⽅法。
多个拦截器
preHandle()方法按照配置顺序执行;postHandle()和afterCompletion()方法按照配置反序执行
multipart形式数据处理
引入commons-fileupload.jar依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
客户端:form表单 【method=post,enctype=multipart,file组件】
<!-- method="post"
enctype="multipart/form-data"
type="file"
-->
<form method="post" enctype="multipart/form-data" action="upload">
<input type="file" name="file"/>
</form>
服务端:原先servlet解析文件上传流【springmvc : 重命名(给一个唯一的名字),存储到磁盘(考虑文件目录过多,可以按照日期创建新的文件夹),把文件存储路径更新到数据库】
@RequestMapping("/handle01")
public String upload(MultipartFile file, HttpSession session) throws IOException {
//重命名
//获取原名称
String originalFilename = file.getOriginalFilename();
//获取后缀 .jpg
String ext = originalFilename.substring(originalFilename.lastIndexOf("."), originalFilename.length());
String newName = UUID.randomUUID() + ext;
//存储,到指定文件夹
String realPath = session.getServletContext().getRealPath("/uploads");
String datePath = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
File folder = new File(realPath + "/" + datePath);
if (! folder.exists()) {
folder.mkdirs();
}
file.transferTo(new File(folder,newName));
//路径存库
return "success";
}
springmvc中需要配置文件上传解析器
<!-- 配置多元素解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置最大文件数量-->
<property name="maxUploadSize" value="100"/>
</bean>
异常处理机制
//注意:写在类中只会对当前类生效
@ExceptionHandler(ArithmeticException.class)
public void handleException(ArithmeticException e, HttpServletResponse response){
//异常处理逻辑
try{
}catch (Exception exception){
}
}
============================================================
//可以捕获所有controller的异常
@ControllerAdvice
public class GlobalExceptionResolver {
@ExceptionHandler(ArithmeticException.class)
public ModelAndView handleException(ArithmeticException e, HttpServletResponse response){
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg",e.getMessage());
modelAndView.setViewName("exception");
return modelAndView;
}
}
重定向参数传递
1、【return "redirect:xxx?name=" + name;】 //拼接参数安全性,参数长度都有局限
2、形参中 【RedirectAttributes redirect
redirect.addFlashAttribute("name",name)
return "redirect:xxx";】
addFlashAttribute方法设置一个flash类型属性,会暂存到session中,在跳转到页面之后该属性销毁;
自定义MVC框架
总体流程
详细流程
web.xml 配置前端控制器
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>mymvc</servlet-name>
<servlet-class>com.mvcframework.mymvc.MyDispatcherServlet</servlet-class>
<init-param>
<!-- 配置需要扫描的包的存放文件(初始化参数)-->
<param-name>scanPackagePropertiesLocation</param-name>
<param-value>mymvc.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>mymvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
public class MyDispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
//定义初始化做的事情,然后取具体实现
//加载配置文件
String contextConfigLocation = config.getInitParameter("contextConfigLocation");
doLoadConfig(contextConfigLocation);
//扫描相关的类,扫描注解
doScan(properties.getProperty("scanPackage"));
//初始化相应的bean,维护其依赖关系
doInstance();
//实现依赖注入
doAutoWired();
//构造一个handlerMapping,将配置好的url和Method建立映射关系
initHandlerMapping();
System.out.println("MyMvc 初始化完成");
//等待请求进入,请求处理
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
## mymvc.properties
scanPackage=com
定义注解类
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutowired {
String value() default "";
}
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyController {
String value() default "";
}
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestMapping {
String value() default "";
}
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService {
String value() default "";
}
加载配置文件
private Properties properties = new Properties();
private void doLoadConfig(String scanPackagePropertiesLocation) {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(scanPackagePropertiesLocation);
//将mymvc.properties加载成Properties
try {
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
}
包、注解扫描
private List<String> classNames = new ArrayList<>();
//scanPackage : com.demo
private void doPackScan(String scanPackage) {
//扫描定义的包,将带有特定注解的类的全限定名缓存起来
// 获取classpath在磁盘中的位置
// D:/workspace/...(省略)/mymvc/target/classes/
String classPath = Thread.currentThread().getContextClassLoader().
getResource("").getPath();
//获取包的路径
// com/demo
String packPath = scanPackage.replaceAll("\.", "/");
//拼接获得需要扫描的包的真实路径
// D:/workspace/...(省略)/mymvc/target/classes/com/demo
String realPath = classPath + packPath;
File folder = new File(realPath);
//获取包下所有文件(夹)
File[] files = folder.listFiles();
for (File file : files) {
if (file.isDirectory()) {
//如果包中有目录,则递归
// 参数 : com.demo.controller
doPackScan(scanPackage + file.getName());
}else if(file.getName().endsWith(".class")){
//找以class结尾的文件,获取它的全限定名称
// com.demo.controller.DemoController
String className = scanPackage + "." + file.getName().replaceAll(".class","");
//将全限定名缓存起来(list)
classNames.add(className);
}
}
}
创建IoC容器,实例化bean,维护依赖关系
private Map<String,Object> beanMap = new HashMap<>();
private void doInstance() {
//从classNames中取全限定名,实例化,根据有无别名、接口等情况生产key
try{
for (String className : classNames) {
Class<?> aClass = Class.forName(className);
String beanName = "";
if (aClass.isAnnotationPresent(MyController.class)) {
//Controller接口不考虑别名,直接用类名首字母小写作为key
String simpleName = aClass.getSimpleName();//DemoController
beanName = lowerFirstCase(simpleName);//demoController
//实例化,存入IoC容器
beanMap.put(beanName,aClass.newInstance());
}else if (aClass.isAnnotationPresent(MyService.class)){
//如果有别名,beanName就是别名;没有别名就首字母小写作为key
String value = aClass.getAnnotation(MyService.class).value();
if (!"".equals(value.trim())) {
beanName = value.trim();
beanMap.put(beanName,aClass.newInstance());
}else {
String simpleName = aClass.getSimpleName();//DemoController
beanName = lowerFirstCase(simpleName);//demoController
//实例化,存入IoC容器
beanMap.put(beanName,aClass.newInstance());
}
//Service层有方法是通过接口注入的,因此需要维护接口名和实体的关系
Class<?>[] interfaces = aClass.getInterfaces();
for (Class<?> anInterface : interfaces) {
beanMap.put(anInterface.getName(),aClass.newInstance());
}
}else{
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
完成依赖注入
private void doAutoWired() {
//遍历IoC容器,找到字段上有Autowired注解的字段,将属性赋值
if (beanMap.isEmpty()) {
return;
}
for (Map.Entry<String, Object> stringObjectEntry : beanMap.entrySet()) {
//维护的对象(DemoController)
Object o = stringObjectEntry.getValue();
//获取对象字段
Field[] declaredFields = o.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
if (! declaredField.isAnnotationPresent(MyAutowired.class)) {
continue;
}
String beanName = declaredField.getDeclaredAnnotation(MyAutowired.class).value();
if (!"".equals(beanName.trim())){
//如果注解上没有指定beanname,那么取接口名作为beanName
beanName = declaredField.getType().getName();
}
declaredField.setAccessible(true);
try {
declaredField.set(o,beanMap.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
初始化HandlerMapping
// Map<String, Method> urlMethodMap = new HashMap<>();
List<Handler> handlers = new ArrayList<>();
private void initHandlerMapping() {
//读[controller]类/方法 上有无requestMapping注解,以获取uri
if (beanMap.isEmpty()) return;
for (Map.Entry<String, Object> stringObjectEntry : beanMap.entrySet()) {
Class<?> aClass = stringObjectEntry.getValue().getClass();
if (! aClass.isAnnotationPresent(MyController.class)) {
continue;
}
String baseUrl = "";
if (aClass.isAnnotationPresent(MyRequestMapping.class)) {
baseUrl = aClass.getAnnotation(MyRequestMapping.class).value();
}
//遍历类中方法
Method[] methods = aClass.getMethods();
for (Method method : methods) {
if (! method.isAnnotationPresent(MyRequestMapping.class)) {
continue;
}
String methodUrl = method.getAnnotation(MyRequestMapping.class).value();
//维护method和url的关系
// urlMethodMap.put(baseUrl + methodUrl,method);
Handler handler = new Handler(stringObjectEntry.getValue(),
method,
Pattern.compile(baseUrl+methodUrl));
//HttpServletRequest request, HttpServletResponse response, String name
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
//如果是HttpServletRequest,HttpServletResponse,存类名
if ((parameters[i].getType() == HttpServletRequest.class) ||
parameters[i].getType() == HttpServletResponse.class) {
handler.getArgsIndexMap().put(parameters[i].getType().getSimpleName(),
i);
}else{
//将形参作为key
handler.getArgsIndexMap().put(parameters[i].getName(),i);
}
//存储handler (list)
}
handlers.add(handler);
}
}
}
============================================================
public class Handler {
//method.invoke(obj,args) 的obj
private Object controller;
private Method method;
private Pattern pattern;//uri
private Map<String,Integer> argsIndexMap;
public Handler(Object controller, Method method, Pattern pattern) {
this.controller = controller;
this.method = method;
this.pattern = pattern;
this.argsIndexMap = new HashMap<>();
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Pattern getPattern() {
return pattern;
}
public void setPattern(Pattern pattern) {
this.pattern = pattern;
}
public Map<String, Integer> getArgsIndexMap() {
return argsIndexMap;
}
public void setArgsIndexMap(Map<String, Integer> argsIndexMap) {
this.argsIndexMap = argsIndexMap;
}
}
处理请求
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// String requestURI = req.getRequestURI();
// //用map存储,缺少执行的类(controller,和参数)
// Method method = urlMethodMap.get(requestURI);
// method.invoke(obj,args);
Handler handler = getHandler(req);
if (handler == null) {
resp.getWriter().write("404 not found");
return;
}
//获取方法参数类型的数组
Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();
//获取请求的参数列表
Map<String,String[]> parameterMap = req.getParameterMap();
//创建列表,作为method.invoke的参数;长度与参数列表相同
Object[] args = new Object[parameterTypes.length];
//遍历request中请求参数
//key: 形参名称 value 参数值
for (Map.Entry<String, String[]> stringEntry : parameterMap.entrySet()) {
String value = StringUtils.join(stringEntry.getValue(),",");
if (! handler.getArgsIndexMap().containsKey(stringEntry.getKey())) {
//判断handler中参数map是否有形参名
continue;
}
Integer paramIndex = handler.getArgsIndexMap().get(stringEntry.getKey());
args[paramIndex] = value;
}
//将request,response 赋值
args[handler.getArgsIndexMap().get(HttpServletRequest.class.getSimpleName())] = req;
args[handler.getArgsIndexMap().get(HttpServletResponse.class.getSimpleName())] = resp;
try {
handler.getMethod().invoke(handler.getController(),args);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private Handler getHandler(HttpServletRequest req) {
String requestURI = req.getRequestURI();
for (Handler handler : handlers) {
Matcher matcher = handler.getPattern().matcher(requestURI);
if (!matcher.matches()) {
continue;
}
return handler;
}
return null;
}
}
流程中遇到的问题及解决
method.invoke(obj,args);
原先只存了url和method的对应关系,但是在处理请求时发现需要方法所在类及请求的参数,因此需要把这些关系全部处理起来。
源码剖析
SSM
Mybatis整合Spring
整合所需 Jar 分析
Junit测试jar(4.12版本)
Mybatis的jar(3.4.5)
Spring相关jar(spring-context、spring-test、spring-jdbc、spring-tx、spring-aop、
aspectjweaver)
Mybatis/Spring整合包jar(mybatis-spring-xx.jar)
Mysql数据库驱动jar
Druid数据库连接池的jar
流程
数据库连接池以及事务管理都交给Spring容器来完成
<!--包扫描-->
<context:component-scan base-package="com"/>
<!-- 读取外部资源文件-->
<context:property-placeholder location="classpath:jdbc.properties/"/>
<!-- 数据库连接池以及事务管理都交给Spring容器来完成-->
<!-- 数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事务管理注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
SqlSessionFactory对象应该放到Spring容器中作为单例对象管理
<!-- SqlSessionFactory对象应该放到Spring容器中作为单例对象管理-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.pojo"/>
</bean>
Mapper动态代理对象交给Spring管理,我们从Spring容器中直接获得Mapper的代理对象
<!--Mapper动态代理对象交给Spring管理,我们从Spring容器中直接获得Mapper的代理对象-->
<!--扫描mapper接⼝,⽣成代理对象,⽣成的代理对象会存储在ioc容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--mapper接⼝包路径配置-->
<property name="basePackage" value="com.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
注意点:jar包版本依赖,mapper.xml与java编译之后在同个目录(起名用com/mapper,不能是com.mapper)
整合springmvc
在已有spring+mybatis案例中添加springmvc
springmvc.xml
===========================================================
<!--包扫描-->
<context:component-scan base-package="com.controller"/>
<mvc:annotation-driven/>
===========================================================
web.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext*.xml</param-value>
</context-param>
<!--spring 启动-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- springmvc 启动-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
service层和dao层是通过spring框架加载的,controller层是通过springmvc加载的,要在controller层注入service对象,因此需要配置监听器
按层拆分xml
dao
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
">
<!--包扫描-->
<context:component-scan base-package="com.mapper"/>
<!-- 读取外部资源文件-->
<context:property-placeholder location="classpath:jdbc.properties/"/>
<!-- 数据库连接池以及事务管理都交给Spring容器来完成-->
<!-- 数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- SqlSessionFactory对象应该放到Spring容器中作为单例对象管理-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.pojo"/>
</bean>
<!--Mapper动态代理对象交给Spring管理,我们从Spring容器中直接获得Mapper的代理对象-->
<!--扫描mapper接⼝,⽣成代理对象,⽣成的代理对象会存储在ioc容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--mapper接⼝包路径配置-->
<property name="basePackage" value="com.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
service
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
">
<!--包扫描-->
<context:component-scan base-package="com.service"/>
<!-- 事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事务管理注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
Spring Data JPA
Spring Data Jpa 是应⽤于Dao层的⼀个框架,简化数据库开发的,作⽤和Mybatis框架⼀样,但是在使
⽤⽅式和底层机制是有所不同的。最明显的⼀个特点,Spring Data Jpa 开发Dao的时候,很多场景我们
连sql语句都不需要开发。由Spring出品。
介绍
Spring Data JPA 是 Spring 基于JPA 规范的基础上封装的⼀套 JPA 应⽤框架,可使开发者⽤极简的代码即可实现对数据库的访问和操作。它提供了包括增删改查等在内的常⽤功能!学习并使⽤Spring Data JPA 可以极⼤提⾼开发效率。
使⽤了Spring Data JPA,我们Dao层中只需要写接⼝,不需要写实现类,就⾃动具有了增删改查、分⻚查询等⽅法。使⽤Spring Data JPA 很多场景下不需要我们⾃⼰写sql语句
JPA规范,Hibernate的关系
JPA 是⼀套规范,内部是由接⼝和抽象类组成的,Hiberanate 是⼀套成熟的 ORM 框架,⽽且Hiberanate 实现了 JPA 规范,所以可以称 Hiberanate 为 JPA 的⼀种实现⽅式,我们使⽤ JPA 的 API 编程,意味着站在更⾼的⻆度去看待问题(⾯向接⼝编程)。
Spring Data JPA 是 Spring 提供的⼀套对 JPA 操作更加⾼级的封装,是在 JPA 规范下的专⻔⽤来进⾏数
据持久化的解决⽅案。
应用
开发步骤
构建⼯程
创建⼯程导⼊坐标(Java框架于我们⽽⾔就是⼀堆jar)
<dependencies>
<!--单元测试jar-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--spring-data-jpa 需要引⼊的jar,start-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.1-b04</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
<!--spring-data-jpa 需要引⼊的jar,end-->
<!--spring 相关jar,start-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<!--spring对orm框架的⽀持包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<!--spring 相关jar,end-->
<!--hibernate相关jar包,start-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.0.Final</version>
</dependency>
<!--hibernate对jpa的实现jar-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.0.Final</version>
</dependency>
<!--hibernate相关jar包,end-->
<!--mysql 数据库驱动jar-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!--spring-test-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
</dependencies>
配置 Spring 的配置⽂件(配置指定框架执行的细节)
<!-- 引入外部资源文件-->
<context:property-placeholder location="classpath*:jdbc.properties"/>
<!--1、创建数据库连接池druid-->
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--2、配置⼀个JPA中⾮常重要的对象,entityManagerFactory
entityManager类似于mybatis中的SqlSession
entityManagerFactory类似于Mybatis中的SqlSessionFactory
-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<!-- 配置数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 配置扫描的包路径,dao层扫pojo-->
<property name="packagesToScan" value="com.pojo"/>
<!--配置jpa实现类 hibernate -->
<property name="persistenceProvider">
<bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
</property>
<!--配置jpa方言 具体实现类 -->
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
</property>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--指定数据库类型 -->
<property name="database" value="MYSQL"/>
<!--是否显示数据库 -->
<property name="showSql" value="true"/>
<!-- 指定数据库方言:不同的数据库语法是不一样的 -->
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect"/>
<!--数据库表是否自动创建 -->
<property name="generateDdl" value="false"/>
</bean>
</property>
</bean>
<!--3、引⽤上⾯创建的entityManagerFactory
<jpa:repositories> 配置jpa的dao层细节
base-package:指定dao层接⼝所在包
-->
<jpa:repositories base-package="com.dao"
entity-manager-factory-ref="entityManagerFactory"
transaction-manager-ref="transactionManager"
/>
<!--4、事务管理器配置 jpa规范:JpaTransactionManager -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!--5、声明式事务配置-->
<!--
<tx:annotation-driven/>
-->
<!--6、配置spring包扫描-->
<context:component-scan base-package="com"/>
编写实体类 Resume,使⽤ JPA 注解配置映射关系
/**1.实体类和数据表映射关系
*/
@Entity
@Table(name = "t_user")
public class User{
//2.实体类属性和字段的映射关系
//标识主键
@Id
//生成策略strategy
//GenerationType.IDENTITY 依赖数据库中自增 Mysql
//GenerationType.SEQUENCE 依靠序列来产生主键 Oracle
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "address")
private String address;
@Column(name = "phone")
private String phone;
//setter/getter
}
编写⼀个符合 Spring Data JPA 的 Dao 层接⼝
/**
* JpaRepository<操作的实体类类型,主键类型>
* 封装了CRUD操作
* JpaSpecificationExecutor<操作的实体类类型>
* 封装了复杂查询(分页排序等)
*/
public interface UserDao extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
//jpql
@Query("from User where name like '李%'")
public List<Resume> findAllResumes();
//sql
@Query(value = "select * from t_user where name like '李%'",nativeQuery = true)
public List<Resume> findAllBySql();
//接口命名方式
public List<Resume> findByAddressLikeOrPhone(String address,String phone);
}
操作 ResumeDao 接口对象完成 Dao 层开发
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( locations = {"classpath:applicationContext.xml"})
public class JpaTest {
@Autowired
UserDao userDao;
@Test
public void test1(){
/*
* select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_, user0_.phone as phone4_0_
* from tb_user user0_
* */
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
System.out.println("=================");
/**
* select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_, user0_.phone as phone4_0_0_
* from tb_user user0_
* where user0_.id=?
*/
Optional<User> byId = userDao.findById(1L);
User user = byId.get();
System.out.println(user);
System.out.println("===================");
/**
* select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_, user0_.phone as phone4_0_
* from tb_user user0_
* where user0_.address=? and user0_.phone=? and user0_.id=1 and user0_.name=?
*/
Optional<User> one = userDao.findOne(Example.of(user));
User user1 = one.get();
System.out.println(user1);
}
@Test
public void test02(){
/**
* 有则改,无则增
* Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_, user0_.phone as phone4_0_0_ from tb_user user0_ where user0_.id=?
* Hibernate: insert into tb_user (address, name, phone) values (?, ?, ?)
*
*Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_, user0_.phone as phone4_0_0_ from tb_user user0_ where user0_.id=?
* Hibernate: update tb_user set address=?, name=?, phone=? where id=?s
*/
User user = new User();
user.setId(5l);
user.setAddress("shanghai");
user.setPhone("12312312");
user.setName("中国人");
User user1 = userDao.save(user);
System.out.println(user1);
}
@Test
public void test03(){
/**
* Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_, user0_.phone as phone4_0_0_ from tb_user user0_ where user0_.id=?
* Hibernate: delete from tb_user where id=?
*/
userDao.deleteById(5l);
}
@Test
public void testJpql(){
/**jpql
* Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_, user0_.phone as phone4_0_
* from tb_user user0_
* where user0_.name like '李%'
*/
List<User> allUsers = userDao.findAllUsers();
for (User allUser : allUsers) {
System.out.println(allUser);
}
System.out.println("===============================");
/**sql
*
* Hibernate: select * from tb_user where name like '李%'
*/
List<User> allBySql = userDao.findAllBySql();
for (User user : allBySql) {
System.out.println(user);
}
}
/**
* 接口命名查询
* 查询方法名以findBy开头,
* 属性名首字母大写,
* 查询方式(模糊查询,等价查询(默认));
*
* 多参数And/Or
*/
@Test
public void testName(){
List<User> byAddressLikeOrPhone = userDao.findByAddressLikeOrPhone("上%", "153000000");
for (User re : byAddressLikeOrPhone) {
System.out.println(re);
}
}
@Test
public void testSpecification(){
/**
* Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_, user0_.phone as phone4_0_
* from tb_user user0_
* where (user0_.address like ?) and user0_.name=?
*/
Specification<User> userSpecification = new Specification<User>() {
@Override
//root 根属性(查询所需要的任何属性都可以从根对象中获取)
//CriteriaQuery 自定义查询方式
//CriteriaBuilder 查询构造器,封装了很多的查询条件
public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path name = root.get("name");
Path address = root.get("address");
Predicate predicate1 = criteriaBuilder.like(address, "上%");
Predicate predicate2 = criteriaBuilder.equal(name, "李四");
Predicate predicate = criteriaBuilder.and(predicate1, predicate2);
return predicate;
}
};
Optional<User> one = userDao.findOne(userSpecification);
User user = one.get();
System.out.println(user);
}
@Test
public void sort(){
Sort sort = new Sort(Sort.Direction.DESC,"id");
List<Resume> resumes = resumeDao.findAll(sort);
for (Resume resume : resumes) {
System.out.println(resume);
}
}
@Test
public void page(){
// PageRequest of = PageRequest.of(0, 2);
// 分页且排序
// 整体先倒序,然后再分页
PageRequest of = PageRequest.of(0, 2, new Sort(Sort.Direction.DESC, "id"));
Page<Resume> all = resumeDao.findAll(of);
for (Resume resume : all) {
System.out.println(resume);
}
}
}
接口方法命名规则查询
查询方法名以findBy开头,属性名首字母大写,查询方式(模糊查询,等价查询(默认));
多参数And/Or
例如 findByNameLikeAndAddress("李%","上海")
动态查询
把service拿到的条件封装成一个对象传递给dao层,这个对象是Specification
interface Specification<T>
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
//root 根属性(查询所需要的任何属性都可以从根对象中获取)
//CriteriaQuery 自定义查询方式
//CriteriaBuilder 查询构造器,封装了很多的查询条件
/**动态条件封装
匿名内部类
toPredicate: 动态组装查询条件
Root 获取需要查询的对象属性
CriteriaBuilder 构建查询条件,内部封装了很多查询条件(模糊查询、精准查询)
*/
Specification<User> Specification = new Specification<User>() {
@Override
//root 根属性(查询所需要的任何属性都可以从根对象中获取)
//CriteriaQuery 自定义查询方式
//CriteriaBuilder 查询构造器,封装了很多的查询条件
public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path name = root.get("name");
Path address = root.get("address");
Predicate predicate1 = criteriaBuilder.like(address, "上%");
Predicate predicate2 = criteriaBuilder.equal(name, "李四");
Predicate predicate = criteriaBuilder.and(predicate1, predicate2);
return predicate;
}
};
Optional<User> one = userDao.findOne(Specification)
分页,排序
排序给Sort参数
Sort(排序方式,哪个字段)
分页给Pageable参数
PageRequest.of(当前页数,每页大小[,排序])