Spring是一个轻量级的容器框架,目的是用于简化企业应用程序的开发,Spring 的本质核心是创建和管理应用程序的对象,因为代码中是抽象耦合,那么把 new 具体类对象放在 Spring 框架中管理,通过 Spring 框架根据用户的需求,把对象注入到需要的地方,相当于在代码中没有使用任何具体子类的实现。
主要包括以下七个模块:
什么是IOC
就是把 new 对象不放在具体类中去 new,把 new 对象的控制权反转给第三方 Spring 容器去 new 对象。指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。
Spring 容器初始化
ApplicationContext context = new FileSystemXmlApplicationContext()
AbstractApplicationContext context = new ClassPathXmlApplicationContext()
Spring 容器中获取对象
Spring IOC 原理
Spring 中 Bean 的作用域通常有下面几种:
如何配置 bean 的作用域呢
xml 方式:
注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {return new Person();
}
单例和多例优缺点
大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Controller、Dao、Service等),这种情况下, Bean 是线程安全的。
如果Spring容器创建,对象立即创建,则称为立即加载。如果 Spring 容器创建,对象在被使用的时候创建,则称为懒加载。
注解:@Lazy 表示为懒加载
懒加载XML写法
lazy-init 是否懒加载 | scope 单例多例 | 对象的创建结果 |
---|---|---|
true | singleton | 懒加载 |
true | prototype | 懒加载 |
default/flase | singleton | 立即加载 |
default/flase | prototype | 懒加载 |
只要对象是多例模式 则都是懒加载! 在单例模式中控制懒加载才有效
Hello 类
public class Hello {public Hello() {System.out.println("Hello()");}
}
直接通过 Hello 类无参构造创建。
特点:把创建的对象放在 Spring 容器中,Spring 帮你创建和管理对象。
所谓的静态工厂,通过类的名字调用类中的静态方法,由静态方法生产一个对象,把生产出来的对象交给 Spring 容器来管理,class 是一个类,可以是抽象类,可以是普通类,factory-method 一定是类中静态方法。
public class StaticFactory_Hello {/*** 这是一个静态工厂*/public static Hello getObject(){ return new Hello();}
}
特点:对象是被动渠道创建的,但是交给了 Spring 来管理。
所谓的实例工厂,生产对象的方法一定是非静态的,首先要创建一个实例工厂的对象,class类不能是抽象类,通过对象来调用非静态的方法。
public class Instance_Factory {/*** 这是一个实例工厂*/public Hello getObject() {return new Hello();}
}
特点:对象是被动渠道创建的,但是交给了 Spring 来管理。
所谓的 Spring 工厂要求必须实现FactoryBean接口,自动调用重写getObject方法,此方法返回一个对象并且一定要返回一个对象,然后把对象放置到 Spring 容器中。
特点:对象是被动渠道创建的,但是交给了 Spring 来管理。
从spring容器中取出容器中的对象,然后把对象注入到需要的地方。
一共有两种注入方式,第一中是构造函数的方式,另一种是setter方式
构造函数方式
public class Constructor {private Hello hello;private String name;public Constructor(Hello hello, String name) {super();this.hello = hello;this.name = name;}@Overridepublic String toString() {return "Constructor [hello=" + hello + ", name=" + name + "]";}
}
setter方式
bean 节点用来告知 Spring 实例化或管理对象,property 节点用来告知 Spring 有对象注入关系,相当于告知spring要做对象的关系管理。每一个property 节点都必须对应一个 setter 方法 和 name 属性,把 name 属性的值的第一个字母大写,前面加上set构建出字符串,拿这个字符串去 类中寻找是否有此名称的方法 ,如果有setter方法,就反射调用这个setter方法。
① 对象注入
ref 是要引入对象,ref 引用的对象,一定是引自 Spring 容器且是容器中的唯一的id。
② 单值注入
③ 集合注入(直接结合注入和间接集合注入)
直接集合注入
北京 上海 广州 北京 上海 广州
间接集合注入,集合的对象要交给spring容器来管理
北京
上海
广州
北京
上海
广州
北京
上海
广州
④ 表达式注入
必须有属性文件,表达式注入就是把属性文件通过 Spring 注入给某个对象中。
mysql.propertiesjdbc_driverClass=com.mysql.jdbc.Driverjdbc_url=jdbc:mysql://localhost:3306/tesdbjdbc_userName=rootjdbc_userPassword=root
mysql.propertses
属性注入的方式有两种
(1)用 ${} 方式 location属性可以放置多个属性文,用,间隔,建议用类路径classpath取,仅针对spring框架。
(2)用 #{} 方式,逗号间隔,必须给一个id 把属性文件的数据存储给spring容器,容器以manyProperty,值是若干属性键值对。
⑤ 空值注入
Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
Annotaion | Package | Source |
---|---|---|
@Autowired | org.springframework.bean.factory | Spring 2.5+ |
@Resource | javax.annotation | Java JSR-250 |
@Inject | javax.inject | Java JSR-250 |
@Autowired 和@Resource使用的比较多一些。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为切面(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
① JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起。
② 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
jdk动态代理 和 cglib动态代理的区别
正常情况:环绕前置 ==> 前置通知@Before ==> 目标方法执行 ==> 后置返回通知@AfterReturning ==> 后置通知@After ==> 环绕返回 ==> 环绕最终
异常情况:环绕通知 ==> 前置通知@Before ==> 目标方法执行 ==> 后置异常通知@AfterThrowing ==> 后置通知@After ==> 环绕异常 ==> 环绕最终
// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {
@Component
@Aspect
public class LoggingAspect implements Ordered {// ....@Overridepublic int getOrder() {// 返回值越小优先级越高return 1;}
}
@Autowired、@Resource
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过 redo log 和 undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。
事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。正确的事务传播行为可能的值如下:
① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。
② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。
③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘
④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。
⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。
⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。
④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。
⑤ ISOLATION_SERIALIZABLE:所有事务逐个依次执行。
假如我们有下面一个类:
Entity(name="USER")
public class User {@Id@GeneratedValue(strategy = GenerationType.AUTO)@Column(name = "ID")private Long id;@Column(name="USER_NAME")private String userName;@Column(name="PASSWORD")private String password;private String secrect;}
如果我们想让secrect 这个字段不被持久化,也就是不被数据库存储怎么办?我们可以采用下面几种方法:
static String transient1; // not persistent because of static
final String transient2 = "Satish"; // not persistent because of final
transient String transient3; // not persistent because of transient
@Transient
String transient4; // not persistent because of @Transient
一般使用后面两种方式比较多,我个人使用注解的方式比较多。
审计功能主要是帮助我们记录数据库操作的具体行为比如某条记录是谁创建的、什么时间创建的、最后修改人是谁、最后修改时间是什么时候。
@Data
@AllArgsConstructor
@NoArgsConstructor
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class AbstractAuditBase {@CreatedDate@Column(updatable = false)@JsonIgnoreprivate Instant createdAt;@LastModifiedDate@JsonIgnoreprivate Instant updatedAt;@CreatedBy@Column(updatable = false)@JsonIgnoreprivate String createdBy;@LastModifiedBy@JsonIgnoreprivate String updatedBy;
}
@OneToOne : 一对一。
@ManyToMany :多对多。
@OneToMany : 一对多。
@ManyToOne :多对一。
利用 @ManyToOne 和 @OneToMany 也可以表达多对多的关联关系
如果我们需要保存密码这类敏感数据到数据库的话,需要先加密再保存。Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。这些加密算法实现类的父类是 PasswordEncoder ,如果你想要自己实现一个加密算法的话,也需要继承 PasswordEncoder。
PasswordEncoder 接口一共也就 3 个必须实现的方法。
public interface PasswordEncoder {// 加密也就是对原始密码进行编码String encode(CharSequence var1);// 比对原始密码和数据库中保存的密码boolean matches(CharSequence var1, String var2);// 判断加密密码是否需要再次进行加密,默认返回 falsedefault boolean upgradeEncoding(String encodedPassword) {return false;}
}
官方推荐使用基于 bcrypt 强哈希函数的加密算法实现类。
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为Controller 层(控制层,返回数据给前台页面)、 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)。
流程说明(重要):
HandlerMapping :处理器映射器,根据 uri 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。HandlerAdapter :处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler;Handler :请求处理器,处理实际请求的处理器。ViewResolver :视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端
请求转发与重定向的区别
Spring MVC 设定请求转发
@RequestParam("/login")
public String redirect(User user){if{//登录成功...}else{//登录失败,转发到登录页面,在返回值前面加"forward:"。return "forward:tologin";}
}
Spring MVC 设定重定向
@RequestParam("/login")
public String redirect(User user){if{//登录成功...}else{//登录失败,重定向到登录页面,在返回值前面加"redirect:"。例如我们在登录的时候,登录失败会重定向到登录页面。return "redirect:tologin";}
}
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。
(1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。
(2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。
(3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。
(1)解决post请求乱码问题:在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8
CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding utf-8
CharacterEncodingFilter /*
(2)get请求中文参数出现乱码解决方法有两个:
①修改tomcat配置文件添加编码与工程编码一致,如下:
②另外一种方法对参数进行重新编码:
String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8")
ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。
有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可
https://blog.csdn.net/yy139926/article/details/127916974
可以在@RequestMapping注解里面加上method=RequestMethod.GET。
@RequestMapping(value="/toLogin",method = RequestMethod.GET)
public ModelAndView toLogin(){}
可以使用@GetMapping注解。
@GetMapping(value="/toLogin")
public ModelAndView toLogin(){}
是单例模式,在多线程访问的时候有线程安全问题,解决方案是在控制器里面不能写可变状态量,如果需要使用这些可变状态,可以使用ThreadLocal机制解决,为每个线程单独生成一份变量副本,独立操作,互不影响。
直接在控制器方法的形参里面声明这个参数就可以,但名字必须和传过来的参数名称一样,否则参数映射失败。
下面方法形参中的userId,就会接收从前端传来参数名称为userId的值。
@RequestMapping("/deleteUser")
public void deleteUser(Long userId){//删除用户操作...
}
直接在控制器方法的形参里面声明这个参数就可以,SpringMvc就会自动会请求参数赋值到这个对象的属性中。
下面方法形参中的user用来接收从前端传来的多个参数,参数名称需要和User实体类属性名称一致。
@RequestMapping("/saveUser")
public void saveUser(User user){//保存用户操作...
}
1234
@Data
public class User {private Long userId;private String username;private String password;//...
}
@RequestMapping("/getUser")
public String getUser(Map map,Model model,ModelMap modelMap){//1.放在map里 map.put("name", "xq");//2.放在model里,一般是使用这个model.addAttribute("habbit", "Play");//3.放在modelMap中 modelMap.addAttribute("city", "gd");modelMap.put("gender", "male");return "userDetail";
}
@RequestMapping("/getUser")
public String getUser(Map map,Model model,ModelMap modelMap,HttpServletRequest request){//放在request里 request.setAttribute("user", userService.getUser());return "userDetail";
}
@RequestMapping("/getUser")
public ModelAndView getUser(ModelAndView modelAndView) {mav.addObject("user", userService.getUser()); mav.setViewName("userDetail"); return modelAndView;
}
直接在控制器方法的形参中声明request,session,SpringMvc就会自动把它们注入。
@RequestMapping("/login")
public ModelAndView login(HttpServletRequest request, HttpSession session){}
在类上添加@SessionAttributes注解将指定的Model数据存储到session中。
@SessionAttributes
@SessionAttributes参数
@SessionAttributes(value={"names"},types={Integer.class})
@Controller
public class session{@RequestMapping("/session")public String session(Model model){model.addAttributes("names", Arrays.asList("caoyc","zhh","cjx"));model.addAttributes("age", 22);return "/session";}
}
在上面代码中,在类上添加@SessionAttributes注解,并指定将names名称的Model数据存储到session域中,以及将Integer类型的Model数据存储到session域中。
虽然现在SpringBoot框架很火,但是SpringBoot并不能处理以及响应客户端的请求,最终还是要依赖SpringMVC框架,所以接下来介绍SpringMVC Controller方法的返回值类型,涵盖所有返回值类型。这篇博客只是扫盲点,没具体深入。
我们在使用SpringMVC的时候,经常返回ModelAndView类型,现在前后端分离后,后端都是返回JSON格式数据为主。返回 ModelAndView类型,我们可以在ModelAndView对象中指定视图名称,然后也可以绑定数据,如下面代码:
@RequestMapping("/userList")
public ModelAndView getAllUser(ModelAndView mv) {List users= userService.getAllUser();//添加数据模型到request域中mv.addObject("users", users);mv.setViewName("userList");//指定视图名return mv;
}
如果返回值为void的话,并不是真正没有返回值,而是会出现以下几种情况:
如果方法内真的没有返回值,那么SpringMVC默认把deleteUser(映射的URL)当成视图名称来解析,如果存在该视图,就返回给客户端;如果不存在该视图,就会报视图找不到异常。
@RequestMapping("/deleteUser")
public void deleteUser() {//删除操作
}
通过加@ResponseBody来修改默认行为,加上该注解表示返回JSON数据,这里返回空JSON数据,而不是把URL当成视图名称来解析
@RequestMapping("/deleteUser")
@ResponseBody
public void deleteUser() {//删除操作
}
请求转发
@GetMapping("/")
public void root(HttpServletRequest req,HttpServletResponse resp) {req.getRequestDispatcher("/WEB-INF/jsp/index.jsp").forward(req,resp);
}
重定向
@RequestMapping("/")
@ResponseBody
public void root(HttpServletResponse resp){resp.setStatus(302);resp.addHeader("Location","/WEB-INF/jsp/index.jsp");
}
@RequestMapping("/")
@ResponseBody
public void root(HttpServletResponse resp){resp.sendRedirect("/WEB-INF/jsp/index.jsp");
}
当方法的返回值为String的时候,也会出现下面几种情况:
逻辑视图名:返回String最常见的是逻辑视图名,这种时候一般利用默认的参数Model来传递数据
@RequestMapping("/deleteUser")
//方法返回JSON数据
@ResponseBody
public String deleteUser(Model model) {model.addAttribute("msg","删除成功");return "userList";
}
重定向:登录失败的时候重定向到登录页面。
@RequestParam("/login")
public String redirect(User user){if{//登录成功...}else{//登录失败,重定向到登录页面return "redirect:tologin";}
}
请求转发:登录失败的时候请求转发到登录页面。
@RequestParam("/login")
public String redirect(User user){if{//登录成功...}else{//登录失败,转发到登录页面return "forward:tologin";}
}
真的返回String,相当于JSON格式的数据
@RequestMapping("/deleteUser")
@ResponseBody
public String deleteUser() {try{//删除成功return "删除成功";}catch(Exception e){return "删除失败";}
}
现在前后端分离的情况下,大部分后端只需要返回JSON数据即可,List 集合、Map集合,实体类等都可以返回,这些数据由 HttpMessageConverter自动转为JSON ,如果使用了Jackson或者Gson,不需要额外配置就可以自动返回JSON了,因为框架帮我们提供了对应的HttpMessageConverter ,如果使用了Alibaba的Fastjson的话,则需要自己手动提供一个相应的 HttpMessageConverter的实例,方法的返回值如下面代码:
@GetMapping("/getUser")
@ResponseBody
public User getUser() {User user = userService.getUser();return user;
}
@RequestMapping("/userList")
@ResponseBody
public List getAllUser() {List users = userService.getAllUser();return users;
}
MyBatis是一个半ORM框架(模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术,将Java中的对象和数据库中的表关联对应起来,理解为:Java对象与数据库表的映射管理框架),它内部封装了JDBC,开发的时候只需要关注SQL语句本身就可以了,我们不需要花太多精力去处理原生的JDBC那一套流程,比如 加载驱动、创建connection连接、创建statement等。
Hibernate是全自动ORM框架,而Mybatis是半自动的。hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理
// 实例化
PreparedStatement pstmt = con.prepareStatement(sql);// 装载占位符
pstmt.setString(1, "6");
pstmt.setString(2, "bb");// 执行sql语句
ResultSet rs = pstmt.executeQuery();
${param} 传入的参数会被当成SQL语句中的一部分,举例:order by ${param},则解析成的sql为:order by id
(1)第一种是使用
标签,逐一定义列名和实体类对象属性名之间的映射关系。
(2)第二种是使用在SQL中定义列的别名,将列的别名与实体类对象的属性名一一对应起来
(1)第一种是使用 @param 注解的方式。比如:
user selectUser(@param("username") string username, @param("password") string password);
(2)第二种是使用Java对象的方式。此时,在Java对象中可以有多个属性,每一个属性其实都是一个参数,这样也可以实现在Mapper中传递多个参数。
(3)第三种是使用map集合的方式。此时,需要使Mapper接口方法的输入参数类型和mapper.xml中定义的每个SQL的parameterType的类型都相同。
接口绑定,就是在MyBatis中定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口中的方法就可以操作数据库了。这样比起直接使用SqlSession对象提供的原生态的方法,更加灵活与简单。
(1)第一种是通过注解绑定,也就是说 在接口的方法上面加上 @Select、@Update 等注解,注解里面包含SQL语句来进行绑定。这种方式可以省去SQL的 xml 映射文件,对于简单的SQL来说比较适用,后期维护比较困难,平时在业务中基本不怎么使用。
(2)第二种是通过在SQL的xml映射文件里面写SQL来进行绑定, 在这种情况下,要指定 xml 映射文件里面的 namespace 参数必须为接口的全类名。不管是SQL简单还是复杂,xml 文件的方式 都比较简单高效,也是最常用的。
一般情况下,在日常开发的时候,会遵循一个mapper.xml映射文件对应于一张表的增删改查。
通常一个 xml 映射文件,都会写一个 Dao 接口与之对应。Dao 接口就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement
举例: com.mybatis3.mappers. StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers. StudentDao 下面 id = findStudentById 的 MappedStatement,在 MyBatis 中,每一个 、
、
、
标签,都会被解析为一个 MappedStatement 对象。
Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复。
/*** Mapper接口里面方法重载*/
public interface StuMapper {List getAllStu();List getAllStu(@Param("id") Integer id);
}
然后在 StuMapper.xml 中利用 Mybatis 的动态 sql 就可以实现。
能正常运行,并能得到相应的结果,这样就实现了在 Dao 接口中写重载方法。Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。
MyBatis的动态SQL标签,主要有以下几类:(1) if 标签,配合 test 标签用来做简单的条件判断。(2)choose 标签,配合 when、otherwise 标签,相当于Java语言中的switch…case语句实现分支选择功能。(3)where 标签,主要是用来简化SQL语句中where条件判断的,能智能的处理 and、or,不用担心有多余的 and 或者 or 导致语法错误。(4)set 标签,主要用来做数据update的时候。(5)trim 标签,对包含的内容加上前缀 prefix、或者后缀 suffix。 (6)foreach 标签,主要用在 Mybatis in 语句中。
(1)if 标签
name->Name->getName 用getName去parameterType指定的类中寻找是否有此方法,如果有就反射调用,调用完反射结果不为null就拼装sql语句 null就不拼装,address同理。
如果两个条件都不为null
select * from user where age=20 username like ? and address like ?
(2)choose when otherwise 标签
choose when otherwise 标签 多个when条件同时成立,就取第一个条件成立的when。
(3)where 标签
where标签是为了给sql语句添加where关键字,where标签中的条件都不成立,where关键字就不添加了,如果两个条件都成立
select * from t_user where username like ? and address like ?
如果第一个不成立,第二个条件成立
select * from t_user where and address like ?
他会自动去掉and关键字
(4)set 标签
update userusername=#{uname}, address=#{uaddress} where id=#{uid}
set 标签只能用于更新语句,第一个条件成立,第二条件不成立,则自动取消逗号。
(5)trim 标签
trim 替换where标签
trim标签替换 set标签
update t_userusername=#{uname}, address=#{uaddress} where id=#{uid}
可以替换where标签和set标签。
(6)foreach 标签
虽然 MyBatis 解析 xml 映射文件是按照顺序解析的,但是,被引用的 B 标签依然可以定义在任何地方,MyBatis 都可以正确识别。
原理是,MyBatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,MyBatis 会将 A 标签标记为未解析状态,然后继续解析余下的标签,包含 B 标签,待所有标签解析完毕,MyBatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了。
Mapper接口,它是没有实现类的。当调用接口方法的时候,它是采用了JDK的动态代理的方式。
UserMapper userMapper=session.getMapper(UserMapper.class);
每次通过调用接口方法操作数据库的时候,Mybatis都会利用MapperProxyFactory创建当前Mapper接口对应的MapperProxy代理实现类,在此代理类定义的增强中,会利用sqlSession、接口、方法等信息构造MapperMethod。MapperMethod是Mybatis对Mapper的接口方法生成的对应封装类,此封装类定义了真正的操作数据库的代码实现,最终对数据库的操作就是依赖他实现的。
一对一关联查询举例如下
select Id, Name, ClassId, Status, AddTime, UpdateTime from RUN_Student
一对多关联查询举例如下
select c.Id, c.ClassName, c.Status, c.AddTime, c.UpdateTime,s.Id as sId, s.Name as sName, s.ClassId as sClassId, s.Status as sStatus, s.AddTime as sAddTime, s.UpdateTime as sUpdateTimefrom RUN_Class c join RUN_Student s on c.Id = s.classId
(1)在MyBatis中,是使用RowBounds对象进行分页的,它是针对ResultSet结果集执行的逻辑分页,而不是物理分页。
(2)另外,我们可以在SQL内,直接书写带有物理分页的参数来完成物理分页功能,也可以使用第三方的分页插件PageHelper来完成物理分页。
(3)分页插件的原理(物理分页):就是使用MyBatis提供的插件接口,来实现自定义插件。在自定义插件的拦截方法内,拦截待执行的SQL,然后根据设置的分页参数 重写SQL ,生成带有分页语句的SQL,最终执行的是重写之后的SQL,从而实现分页。 举例:
select * from student,分页插件拦截SQL后 重写SQL为:select t.* from (select * from student)t limit 0,10;
在 MyBatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给 SqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数。
BaseExecutor:基础抽象类,实现了Executor接口的大部分方法,主要提供了缓存管理和事务管理的能力,使用了模板模式,doUpdate、doQuery、doQueryCursor 等方法的具体实现交给不同的子类去实现。
SimpleExecutor:BaseExecutor的具体子类实现,且为默认配置,在doQuery方法中使用PrepareStatement对象访问数据库,每次访问都要创建新的PrepareStatement对象,用完立刻关闭PrepareStatement。
ReuseExecutor:BaseExecutor的具体子类实现,与SimpleExecutor不同的是,在doQuery方法中,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map
BatchExecutor:BaseExecutor的具体子类实现,在doUpdate方法中,提供批量执行多条SQL语句的能力。将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。
CachingExecutor:直接实现Executor接口,使用装饰器模式提供二级缓存能力。先从二级缓存中查询,缓存没有命中再从数据库中查询,最后将结果添加到缓存中再返回给用户。如果在xml文件中配置了节点,则会创建 CachingExecutor。
一级缓存
一级缓存在 mybatis 中默认是开启的并且是 session 级别,它的作用域为一次 sqlSession 会话。 一个SqlSession对象中创建一个本地缓存(local cache),在同一个SqlSession中,执行相同的SQL查询时,会尝试去本地缓存中查找是否在缓存,如果在缓存中,就直接从缓存中取出,然后返回给用户,否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
代码演示:
@Test@Transactional(rollbackFor = Throwable.class)public void testFistCache(){// 第一次查询,缓存到一级缓存userMapper.selectById(1);// 第二次查询,直接读取一级缓存userMapper.selectById(1);}
console 2023-03-15 14:53:58.084 DEBUG [BaseJdbcLogger.java:137] : ==> Preparing: select * from user where id = ?
console 2023-03-15 14:53:58.084 DEBUG [BaseJdbcLogger.java:137] : ==> Parameters: 12(Integer)
console 2023-03-15 14:53:58.103 DEBUG [BaseJdbcLogger.java:137] : <== Total: 1
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
可以看到,虽然进行了两次查询,但最终只请求了一次数据库,第二次查询命中了一级缓存,直接返回了数据。
这里有两点需要说明一下:
使用了数据库连接池,默认每次查询完之后自动 commite,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。当我们开启了事务,两次查询都在同一个 sqlSession 中,从而让第二次查询命中了一级缓存。
一级缓存的作用域有两种:session(默认)和 statment,可通过设置 local-cache-scope 的值来切换,默认为 session。二者的区别在于 session 会将缓存作用于同一个 sqlSesson,而 statment 仅针对一次查询,所以,local-cache-scope: statment 可以理解为关闭一级缓存。
mybatis 默认的 session 级别一级缓存,由于 springboot 中默认使用了 hikariCP,所以基本没用,需要开启事务才有用。但一级缓存作用域仅限同一 sqlSession 内,无法感知到其他 sqlSession 的增删改,所以极易产生脏数据。
二级缓存
默认情况下,mybatis 打开了二级缓存,但它并未生效,因为二级缓存的作用域是 namespace,所以还需要在 Mapper.xml 文件中配置一下才能使二级缓存生效
测试 单表二级缓存:
/*** 测试二级缓存效果* 需要*Mapper.xml开启二级缓存**/@Testpublic void testSecondCache(){userMapper.selectById(1);userMapper.selectById(1);}
console 2023-03-15 14:52:54.318 DEBUG [LoggingCache.java:60] : Cache Hit Ratio [com.example.canal.mybatis.mapper.UserMapper]: 0.5
console 2023-03-15 14:52:54.319 DEBUG [LoggingCache.java:60] : Cache Hit Ratio [com.example.canal.mybatis.mapper.UserMapper]: 0.6
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
这里可以看到,第二次查询直接命中了缓存,日志还打印了该缓存的命中率。读者可以自行关闭二级缓存查看效果,通过注掉对应 mapper.xml 的 cache 标签,或者 cache-enabled: false 均可
第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放代该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。
缓存的优先级
通过 mybatis 发起的查询,作用顺序为:二级缓存 -> 一级缓存 -> 数据库 ,其中任何一个环节查到不为空的数据,都将直接返回结果。
如果多表联查的二级缓存,user 表 left join user_order 表 on user.id = user_order.user_id
我们考虑这样一种情况,该联查执行两次,第二次联查前更新 user_order 表,如果只使用 cache 配置,将会查不到更新的 user_orderxi,因为两个 mapper.xml 的作用域不同,要想合到一个作用域,就需要用到 cache-ref
userOrderMapper.xml
userMapper.xml
二级缓存可通过 cache-ref 让多个 mapper.xml 共享同一 namespace,从而实现缓存共享,但多表联查时配置略微繁琐。所以生产环境建议将一级缓存设置为 statment 级别(即关闭一级缓存),如果有必要,可以开启二级缓存
MyBatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 MyBatis 配置文件中,可以配置是否启用延迟加载 。
什么是延迟加载
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
局部延迟加载
在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。
全局延迟加载
在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。
局部的加载策略的优先级高于全局的加载策略
延迟加载原理实现
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName() ,拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理。
Mybatis也是用的预编译进行防护SQL注入。其实在框架底层,是JDBC中的PreparedStatement类在起作用。
@Test
public void test02() throws SQLException {Connection con = JdbcUtil.getConn();String sql = "SELECT * FROM test WHERE ID = ? AND NAME = ?";// 实例化PreparedStatement pstmt = con.prepareStatement(sql);// 装载占位符pstmt.setString(1, "6");pstmt.setString(2, "bb");// 执行sql语句ResultSet rs = pstmt.executeQuery();System.out.println(rs);while (rs.next()) {System.out.println(rs.getInt("id") + " " + rs.getString("name"));}JdbcUtil.close(con);
}
上一篇:如何借助AIGC提高研究效率
下一篇:字典树(前缀树)