1. Spring简介
1.1 Spring 核心设计思想
1.1.1 Spring 是什么?
- Spring 是包含了众多⼯具⽅法的 IoC 容器。
- Spring 指的是 Spring Framework(Spring 框架),它是⼀个开源框架,Spring ⽀持⼴泛的应⽤场景,它可以让 Java 企业级的应⽤程序开发起来更简单。
1.1.1.1 什么是IoC容器?
- 容器是⽤来容纳某种物品的装置。
- IoC = Inversion of Control 翻译成中⽂是“控制反转”的意思,控制权发⽣的反转,不再是上级对象创建并控制下级对象了,⽽是下级对象把注⼊将当前对象中,下级的控制权不再由上级类控制了,这样即使下级类发⽣任何改变,当前类都是不受影响的,这就是 IoC 的实现思想。
- Spring具备两个核心功能:将对象存⼊到容器,从容器中取出对象。对象的创建和销毁的权利都交给 Spring 来管理了,它本身⼜具备了存储对象和获取对象的能⼒。
1.1.1.2 DI 概念说明
DI 是 Dependency Injection 的缩写,翻译成中⽂是“依赖注⼊”的意思。依赖注⼊就是由 IoC 容器在运⾏期间,动态地将某种依赖关系注⼊到对象之中。所以,依赖注⼊(DI)和控制反转(IoC)是从不同的⻆度的描述的同⼀件事情,就是指通过引⼊ IoC 容器,利⽤依赖关系注⼊的⽅式,实现对象之间的解耦,IoC 是“⽬标”也是⼀种思想,⽽⽬标和思想只是⼀种指导原则,最终还是要有可⾏的落地⽅案,⽽ DI就属于具体的实现。
1.1.1.3 IoC容器和普通程序开发的区别
- 将对象存储在 IoC 容器相当于将以后可能⽤的所有⼯具制作好都放到仓库中,需要的时候直接取就⾏了,⽤完再把它放回到仓库。
- new 对象的⽅式相当于,每次需要工具了才现做,用完扔掉了也不会保存,下次再用的时候还得重新做。
1.2 Spring 创建和使用
1.2.1 创建 Spring 项目
使⽤ Maven ⽅式来创建⼀个 Spring 项⽬,创建 Spring 项⽬和 Servlet 类似。
- 创建⼀个 Maven 项⽬
然后跳转到了这个页面:
- 添加 Spring 框架⽀持
在项⽬的 pom.xml 中添加 Spring 框架的⽀持spring-context(spring 上下⽂)和spring-beans(管理对 象的模块),xml 配置如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
</dependencies>
- 添加启动类
最后在创建好的项⽬ java ⽂件夹下创建⼀个启动类,包含 main ⽅法即可:
public class App {
public static void main(String[] args) {
}
}
到此为止,一个spring项目就搭建好了。
1.2.2 存储 Bean 对象
Bean 就是 Java 语⾔中的⼀个普通对象,例如:
public class User { public String sayHi(String name) { return name + " hello!"; } }
- 创建 Bean
public class User {
public String sayHi(String name) {
return name + " hello!";
}
}
- 将 Bean 注册到容器
<<1>>在创建好的项⽬中添加 Spring 配置⽂件 spring-config.xml,将此⽂件放到 resources 的根⽬录下, 如下图所示:
<<2>> spring-config.xml 配置⽂件的固定格式为以下内容,复制粘贴即可
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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">
</beans>
<<3>> 将 User 对象注册到 Spring 中就可以,在 标签中添加配置 ,格式如下:
<beans>
<bean id="user" class="com.bit.User"></bean>
</beans>
1.2.3 获取 Bean 对象
- 创建 Spring 上下⽂对象
<<1>>使⽤ ApplicationContext
ApplicationContext context = new ClassPathXmlApplicationContext("spring-con fig.xml");
<<2>>使⽤ BeanFactory
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
ApplicationContext VS BeanFactory
- 继承关系和功能⽅⾯:Spring 容器有两个顶级的接⼝:BeanFactory 和 ApplicationContext。其中 BeanFactory 提供了基础的访问容器的能⼒,⽽ ApplicationContext 属于 BeanFactory 的⼦类,它除了继承了 BeanFactory 的所有功能之外,它还拥有独特的特性, 还添加了对国际化⽀持、资源访问⽀持、以及事件传播等⽅⾯的⽀持。
- 性能⽅⾯:ApplicationContext 是⼀次性加载并初始化所有的 Bean 对象,⽽ BeanFactory 是需要那个才去加载那个,因此更加轻量。
<<3>>使用ClassPathXmlApplicationContext
ClassPathXmlApplicationContext 属于 ApplicationContext 的⼦类,拥有 ApplicationContext 的所有功能,通过 xml 的配置来获取所有的 Bean 容器。
- 获取指定的 Bean 对象
// 1.得到 Spring 上下⽂对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring-con fig.xml");
// 2.加载某个 bean
User user = (User) context.getBean("user");
Bean 的 Id 要⼀⼀对应,如下图所示:
getBean ⽅法的更多⽤法
- 根据类型获取 Bean:
UserController user = context.getBean(UserController.class);
- 名称 + 类型获取 Bean:
UserController user = context.getBean("user", UserController.class);
当有⼀个类型被重复注册到 spring-config.xml 中时,只能使⽤根据名称获取了,⽐如以 下场景就会导致程序报错:
1.2.4 使用 Bean
public class App {
public static void main(String[] args) {
// 1.得到 Spring 上下⽂对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2.加载某个 bean
User user = (User) context.getBean("user");
// 3.调⽤相应的⽅法
System.out.println(user.sayHi("Java"));
}
}
1.2.4 使用注解读取和存储对象
- 配置扫描路径
配置⼀下存储对象的扫描包路径,只有在被配置的包下且添加了注解的类才能被正确的识别并保存到 Spring 中。
在 spring-config.xml 添加如下配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:content="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<content:component-scan base-package="com.bit.service"></content:component-scan>
</beans>
- 添加注解存储 Bean 对象
将对象存储在 Spring 中,有两种注解类型可以实现:
- 类注解:@Controller(表示业务逻辑层)、@Service(服务层)、@Repository(持久层)、@Component、@Configuration(配置层)。
- ⽅法注解:@Bean
类注解是添加到某个类上的,⽅法注解是放到某个⽅法上的。
Bean 命名规则:
通常 bean 使⽤的都是标准的⼤驼峰命名,读取的时候⾸字⺟⼩写:
当⾸字⺟和第⼆个字⺟都是⼤写时,不能正常读取到 bean :
Spring中 bean 存储时⽣成的命名规则:
<1> 在 Idea 中使⽤搜索关键字“beanName”可以看到以下内容:
它使⽤的是 JDK Introspector 中的 decapitalize ⽅法,源码如下:public static String decapitalize(String name) { if (name == null || name.length() == 0) { return name; } // 如果第⼀个字⺟和第⼆个字⺟都为⼤写的情况,是把 bean 的⾸字⺟也⼤写存储了 if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))){ return name; } // 否则就将⾸字⺟⼩写 char chars[] = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); }
所以对于上⾯报错的代码,只要改为以下代码就可以正常运⾏了:
类注解使用示例:
- @Controller(控制器存储)
使⽤ @Controller 存储 bean:@Controller // 将对象存储到 Spring 中 public class UserController { public void sayHi(String name) { System.out.println("Hi," + name); } }
先使⽤之前读取对象的⽅式读取上⾯的 UserController 对象:
public class Application { public static void main(String[] args) { // 1.得到 spring 上下⽂ ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2.得到 bean UserController userController = (UserController) context.getBean("userController"); // 3.调⽤ bean ⽅法 userController.sayHi("Bit"); } }
- @Service(服务存储)
使⽤ @Service 存储 bean@Service public class UserService { public void sayHi(String name) { System.out.println("Hi," + name); } }
读取 bean
class App { public static void main(String[] args) { // 1.得到 spring 上下⽂ ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2.得到 bean UserService userService = (UserService) context.getBean("userService"); // 3.调⽤ bean ⽅法 userService.sayHi("Bit"); } }
- @Repository(仓库存储)
使⽤ @Repository 存储 bean@Repository public class UserRepository { public void sayHi(String name) { System.out.println("Hi," + name); } }
读取 bean
class App { public static void main(String[] args) { // 1.得到 spring 上下⽂ ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2.得到某个 bean UserRepository userRepository = (UserRepository) context.getBean("userRepository"); // 3.调⽤ bean ⽅法 userRepository.sayHi("Bit"); } }
- @Component(组件存储)
使⽤ @Component 存储 bean@Component public class UserComponent { public void sayHi(String name) { System.out.println("Hi," + name); } }
读取 bean
class App { public static void main(String[] args) { // 1.得到 spring 上下⽂ ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2.得到某个 bean UserComponent userComponent = (UserComponent) context.getBean("userComponent"); // 3.调⽤ bean ⽅法 userComponent.sayHi("Bit"); } }
- @Configuration(配置存储)
@Configuration public class UserConfiguration { public void sayHi(String name) { System.out.println("Hi," + name); } }
读取 bean
class App { public static void main(String[] args) { // 1.得到 spring 上下⽂ ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2.得到某个 bean UserConfiguration userConfiguration = (UserConfiguration) context.getBean("userConfiguration"); // 3.调⽤ bean ⽅法 userConfiguration.sayHi("Bit"); } }
为什么有这么多功能一样的注解:
它们的功能都是一样的,让程序员看到类注解之后,就能直接了解当前类的⽤途。
程序的⼯程分层,调⽤流程:
查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现,这些注解⾥⾯都有⼀个注解 @Component,说明它们本身就是属于 @Component 的“⼦类”。
⽅法注解@Bean 使用示例:
⽅法注解要配合类注解使⽤
在 Spring 框架的设计中,⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中
- 使用@Bean存储 bean
@Component public class Users { @Bean public User user1() { User user = new User(); user.setId(1); user.setName("Java"); return user; } }
读取bean
public class Application { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); User user = (User) context.getBean("user1"); System.out.println(user.toString()); } }
- 重命名 Bean
可以通过设置 name 属性给 Bean 对象进⾏重命名。重@Component public class Users { @Bean(name = {"u1"}) public User user1() { User user = new User(); user.setId(1); user.setName("Java"); return user; } }
命名的 name 其实是⼀个数组,⼀个 bean 可以有多个名字。
@Bean(name = {"u1", "us1"}) public User user1() { User user = new User(); user.setId(1); user.setName("Java"); return user; }
name={} 可以省略
@Bean({"u1", "us1"}) public User user1() { User user = new User(); user.setId(1); user.setName("Java"); return user; }
此时使⽤ u1/us1 就可以获取到 User 对象了
class App { public static void main(String[] args) { // 1.得到 spring 上下⽂ ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2.得到某个 bean User user = (User) context.getBean("u1"); // 3.调⽤ bean ⽅法 System.out.println(user); } }
- 对象装配
获取 bean 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注⼊。
<1> 属性注⼊
属性注⼊使⽤ @Autowired 实现。
属性注⼊的优点是简洁,使⽤⽅便;缺点是只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只 有在使⽤的时候才会出现 NPE(空指针异常)。
示例:
Service 类的实现代码如下:
import org.springframework.stereotype.Service; @Service public class UserService { /** * 根据 ID 获取⽤户数据 * * @param id * @return */ public User getUser(Integer id) { // 伪代码,不连接数据库 User user = new User(); user.setId(id); user.setName("Java-" + id); return user; } }
- Controller 类的实现代码如下:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @Controller public class UserController { // 注⼊⽅法1:属性注⼊ @Autowired private UserService userService; public User getUser(Integer id) { return userService.getUser(id); } }
核⼼实现:
获取 Controller 中的 getUser ⽅法:import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserControllerTest { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); UserController userController = context.getBean(UserController.cla ss); System.out.println(userController.getUser(1).toString()); } }
运行结果:
<2>构造⽅法注⼊
构造⽅法注⼊是在类的构造⽅法中实现注⼊。
构造⽅法注⼊是 Spring 推荐的注⼊⽅式,它的缺点是如果有多个注⼊会显得⽐较臃肿,但出现这 种情况应该考虑⼀下当前类是否符合程序的单⼀职责的设计模式,优点是通⽤性,在使⽤之前⼀定能把保证注⼊的类不为空。
示例:
@Controller public class UserController2 { // 注⼊⽅法2:构造⽅法注⼊ private UserService userService; // 创建userService引用 @Autowired public UserController2(UserService userService) { // 注入并通过构造方法让上面的引用指向注入进来的这个对象 this.userService = userService; } public User getUser(Integer id) { return userService.getUser(id); } }
<<1>> 如果只有⼀个构造⽅法,那么 @Autowired 注解可以省略
<<2>>如果类中有多个构造⽅法,那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法,否则 程序会报错
<3> Setter 注⼊
Setter 注⼊和 构造方法注入 实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注解。
Setter ⽅式是 Spring 前期版本推荐的注⼊⽅式,但通⽤性不如构造⽅法,所有 Spring 现版本已 经推荐使⽤构造⽅法注⼊的⽅式来进⾏类注⼊了。
示例:
@Controller public class UserController3 { // 注⼊⽅法3:Setter注⼊ private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } public User getUser(Integer id) { return userService.getUser(id); } }
<4> @Resource注入
@Autowired 和 @Resource 的区别:
- @Autowired 来⾃于 Spring,⽽ @Resource 来⾃于 JDK 的注解
- 相⽐于 @Autowired ,@Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean。
- @Autowired 可⽤于 Setter 注⼊、构造方法注⼊和 属性注⼊。⽽ @Resource 只能⽤于 Setter 注⼊和 属性注⼊ 不能⽤于构造函数注⼊。
<5>使用同⼀类型多个 Bean 报错处理,当出现以下多个 Bean,返回同⼀对象类型时程序会报错。
示例:
@Component public class Users { // 第一个bean @Bean public User user1() { User user = new User(); user.setId(1); user.setName("Java"); return user; } // 第二个bean @Bean public User user2() { User user = new User(); user.setId(2); user.setName("MySQL"); return user; } }
在另⼀个类中获取 User 对象:
@Controller public class UserController4 { // 注⼊ @Resource private User user; public User getUser() { return user; } }
执⾏结果:
解决方法:
<<1>>使⽤ @Resource(name=“XXX”)来指定bean@Controller class UserController4 { // 注⼊ @Resource(name = "user1") private User user; public User getUser() { return user; } }
<2> 使⽤ @Qualifier来指定bean
@Controller
public class UserController5 {
// 注⼊
@Autowired
@Qualifier(value = "user2")
private User user;
public User getUser() {
return user;
}
}
2. Bean 详解
2.1 Bean的作用域
限定程序中变量的可⽤范围叫做作⽤域,或者说在源代码中定义变量的某个区域就叫做作⽤域。
Bean 的作⽤域是指 Bean 在 Spring 整个框架中的某种⾏为模式。
Bean 有 6 种作⽤域:singleton(单例作⽤域),prototype(原型作⽤域/多例作⽤域), request(请求作⽤域),session(回话作⽤域),application(全局作⽤域),websocket(HTTP WebSocket 作⽤域)。后 4 种状态是 Spring MVC 中的值,在普通的 Spring 项⽬中只有前两种。
2.1.1 singleton
Spring默认选择该作⽤域,该作⽤域下的Bean在IoC容器中只存在⼀个实例:获取Bean(即通过applicationContext.getBean等⽅法获取)及装配Bean(即通过@Autowired等注⼊)都是同⼀个对象。通常⽆状态的Bean(Bean对象的属性状态不需要更新)使⽤该作⽤域,单例可以很⼤程度上提⾼性能。
示例:
公共bean
@Component
public class Users {
@Bean
public User user1() {
User user = new User();
user.setId(1);
user.setName("Java"); // 【重点:名称是 Java】
return user;
}
}
A ⽤户使⽤时,进⾏了修改操作:
@Controller
public class BeanScopesController {
@Autowired
private User user1;
public User getUser1() {
User user = user1;
System.out.println("Bean 原 Name:" + user.getName());
user.setName("悟空"); // 【重点:进⾏了修改操作】
return user;
}
}
B ⽤户再去使⽤公共 Bean :
@Controller
public class BeanScopesController2 {
@Autowired
private User user1;
public User getUser1() {
User user = user1;
return user;
}
}
查看 A ⽤户和 B ⽤户公共 Bean 的值:
public class BeanScopesTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
BeanScopesController beanScopesController = context.getBean(BeanScopesController.class);
System.out.println("A 对象修改之后 Name:" + beanScopesController.getUser1().toString());
BeanScopesController2 beanScopesController2 = context.getBean(BeanScopesController2.class);
System.out.println("B 对象读取到的 Name:" + beanScopesController2.getUser1().toString());
}
}
执⾏结果:
2.1.2 prototype
每次对该作⽤域下的Bean的请求都会创建新的实例:获取Bean(即通过applicationContext.getBean等⽅法获取)及装配Bean(即通过@Autowired注⼊)都是新的对象实例。通常有状态(Bean对象的属性状态需要更新)的Bean使⽤该作⽤域。
2.1.3 request
每次http请求会创建新的Bean实例,类似于prototype。用于⼀次http的请求和响应的共享Bean,限定SpringMVC中使⽤。
2.1.4 session
在⼀个http session中,定义⼀个Bean实例,用于用户回话的共享Bean,⽐如:记录⼀个⽤户的登陆信息。限定SpringMVC中使⽤。
2.1.5 application
在⼀个http servlet Context中,定义⼀个Bean实例,用于Web应⽤的上下⽂信息,⽐如:记录⼀个应⽤的共享信息。限定SpringMVC中使⽤。
application 是 Spring Web 中的作⽤域,作⽤于 Servlet 容器。singleton 是 Spring Core 的作⽤域,singleton 作⽤于 IoC 的容器。
2.1.6 websocket
在⼀个HTTP WebSocket的⽣命周期中,定义⼀个Bean实例,用于WebSocket的每次会话中,保存了⼀个Map结构的头信息,将⽤来包裹客户端消息头。第⼀次初始化后,直到WebSocket结束都是同⼀个Bean。限定Spring WebSocket中使⽤。
2.2 设置Bean的作用域
使⽤ @Scope 标签就可以⽤来声明 Bean 的作⽤域。
<1> 直接设置值:@Scope(“prototype”)
<2> 使⽤枚举设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
示例;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
public class Users {
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean(name = "u1")
public User user1() {
User user = new User();
user.setId(1);
user.setName("Java"); // 【重点:名称是 Java】
return user;
}
}
2.2 Bean的生命周期
2.2.1 Spring 执行流程
2.2.2 Bean 的执行流程
启动 Spring 容器 --> 实例化 Bean(分配内存空间,从⽆到有) --> Bean注册到Spring中(存操作) --> 将Bean装配到需要的类中(取操作)。
2.2.3 Bean的生命周期
⽣命周期指的是⼀个对象从诞⽣到销毁的整个⽣命过程,我们把这个过程就叫做⼀个对象的⽣命周期。
- 实例化 Bean(为 Bean 分配内存空间)
- 设置属性(Bean 注⼊和装配)
- Bean 初始化
3.1 实现了各种 Aware 通知的⽅法,如 BeanNameAware、BeanFactoryAware ApplicationContextAware >的接⼝⽅法;
3.2 执⾏ BeanPostProcessor 初始化前置⽅法;
3.3 执⾏ @PostConstruct 初始化⽅法,依赖注⼊操作之后被执⾏;
3.4 执⾏⾃⼰指定的 init-method ⽅法(如果有指定的话);
3.5 执⾏ BeanPostProcessor 初始化后置⽅法。- 使⽤ Bean
- 销毁 Bean。执行销毁容器的各种⽅法,如 @PreDestroy、DisposableBean 接⼝⽅法、destroy-method。
- 实例化和初始化的区别:
实例化和属性设置是 Java 级别的系统“事件”,其操作过程不可⼈⼯⼲预和修改;⽽初始化是给开发者 提供的,可以在实例化之后,类加载完成之前进⾏⾃定义“事件”处理。
- 如何理解Bean生命周期:
Bean 的⽣命流程看似繁琐,但咱们可以以⽣活中的场景来理解它,⽐如我们现在需要买⼀栋房⼦,那么我们的流程是这样的:
- 先买房(实例化,从⽆到有);
- 装修(设置属性);
- 买家电,如洗⾐机、冰箱、电视、空调等([各种]初始化);
- ⼊住(使⽤ Bean);
- 卖出去(Bean 销毁)。
3. Spring AOP
3.1 什么是 Spring AOP?
AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,通过预编译和运行期间动态代理来实现程序功能的统一维护的一种技术,它是对某⼀类事情的集中处理。Spring AOP 是⼀个框架,是⼀种对 AOP 思想的实现,它们的关系和IoC 与 DI 类似。
面向对象编程的缺陷:
面向对象的特点是继承、多态和封装。封装就要求将功能分散到不同的对象中去称为职责分配,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了,好处是降低了代码的复杂程度,使类可重用。在分散代码的同时,也增加了代码的重复性,比如说,我们在两个类中,可能都需要在每个方法中做日志,按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容:
也可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是这样一来,这两个类跟这个独立的类就有耦合了,它的改变会影响这两个类。
如何理解AOP的好处?
某天小明一觉醒来,发现自己光着腚穿越到三亿年前的非洲大草原,为了填饱肚子他只能天天靠捕猎一些小动物生存下去,考虑到卫生问题,需要弄熟了再吃,他把猎物先放到一边开始接着进行钻木取火,终于火苗着了,小明开始烤肉了,饱餐了一顿。每次钻木取火都很费劲,小明总是累得满头大汗才得到一点点小火星,但是没办法,原始社会只能这样。
<小明每天都要钻木取火>:
假如是另外一种情况:同样是某一天,小明一觉醒来发现自己穿越到了三亿年前的非洲大草原上,万幸的是他一摸兜里还揣着打火机,但是他还得打猎要不然就得饿肚子,和上次不一样的是,他不用再费劲巴拉的钻木取火了,直接“啪”一点,打火机就能冒出火苗,直接就能点火烤肉了,然后他美美地饱餐了一顿。每次需要用到火时,拿出打火机啪一点就行了。
类比小明的故事,当我们在做后台系统时,如果没有AOP,所有需要判断用户登录的页面中的方法都要各自实现或调用用户验证的方法,而且用户登录验证都是的相同方法,当功能越多时,要写的登录验证也就越多,代码会更臃肿,修改和维护成本也会升高。
有了AOP之后,只需要在某一处配置一下,所有需要判断用户登录页面中的方法就可以全部实现用户登录验证了,不需要每个方法中都写相同的用户登录验证了。
AOP是OOP的补充,OOP从纵上区分出一个个的类来,而AOP则从横向上向对象中加入特定的代码,使OOP由原来的二维变为三维了,由平面变成立体了。
AOP的用途:
统一日志记录,统一方法执行时间统计,统一的返回格式设置,统一的异常处理,事务的开启和提交等
3.2 AOP相关概念
3.2.1 Aspect
Aspect 是 包含了 Advice ,Pointcut,和 Aspect 的类,相当于AOP实现的某个功能的集合。
3.2.2 Join Point
程序执行过程中插入 Aspect 的一个点,这个点可以是方法调用时、抛出异常时、甚至修改字段时,Aspect 能够利用这些点插入到程序的正常流程中并添加新的行为。
3.2.3 Pointcut
Pointcut 提供了一组规则(这个规则使用 AspectJ pointcut expression language 描述)来匹配Join Point,给满足规则的 Join Point 添加 Advce。Pointcut 相当于保存了众多 Join Point 的一个集合,如果把 Poincut 看成一个表,而 Join Point 就是表中的一条条数据。
3.2.4 Advice
Aspect 的工作被称为 Advice。
Advice 定义了 Aspect 是什么、何时使用,描述了 Aspect 要完成的工作,还决定合适执行这个工作。
在Spring Aspect 类中,可以在方法上使用注解,使其成为 Advice 方法,在满足条件后会通知这个方法执行。
- 前置通知:@Before,Advice方法会在目标方法调用之前执行。
- 后置通知:@After,Advice方法会在目标方法返回或抛出异常后调用。
- 返回之后再通知:@AfterReturning,Advice方法会在目标方法返回后调用。
- 抛异常后通知:@AfterThrowing,Advice方法会在目标方法抛出异常后调用。
- 环绕通知:@Aroud,Advice方法包裹了目标方法,Advice方法会在目标方法调用之前和调用之后执行。
3.3 实现Spring AOP
3.1.1 添加 AOP 框架支持
在 pom.xml 中添加如下配置:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.1.2 定义 Aspect 和 Pointcut
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 表明此类为⼀个切⾯
@Component
public class UserAspect {
// 定义切点,这⾥使⽤ AspectJ 表达式语法
@Pointcut("execution(*com.example.demo.controller.UserController.*(..))")
public void pointcut(){ } // 不需要有方法体,只是起到一个标识左右,标识下面的 Advice 方法具体指的是哪个Pointcut
}
切点表达式:
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
AspectJ ⽀持的三种通配符:
- :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
- … :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤
- :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的
所有⼦类包括本身
表达式示例:execution(* com.cad.demo.User.*(..)) :匹配 User 类⾥的所有⽅法。 execution(* com.cad.demo.User+.*(..)) :匹配该类的⼦类包括该类的所有⽅法。 execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有⽅法。 execution(* com.cad..*.*(..)) :匹配 com.cad 包下、⼦孙包下所有类的所有⽅法。 execution(* >addUser(String, int)) :匹配 addUser ⽅法,且第⼀个参数类型是 String,第⼆个 参数类型是 int。
具体实现:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定义切点⽅法
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){ }
// 前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("执⾏ Before ⽅法");
}
// 后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执⾏ After ⽅法");
}
// return 之前通知
@AfterReturning("pointcut()")
public void doAfterReturning(){
System.out.println("执⾏ AfterReturning ⽅法");
}
// 抛出异常之前通知
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
System.out.println("执⾏ doAfterThrowing ⽅法");
}
// 添加环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
}
具体案例:
3.3 Spring AOP 的实现原理:
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。
Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。
默认情况下,实现了接⼝的类,使 ⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。
3.3.1 代理的⽣成时机:织⼊(Weaving)
织⼊是把Aspect应⽤到⽬标对象并创建新的代理对象的过程,Aspect在指定的 Join Point 被织⼊到⽬标对 象中。
在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
- 编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就 是以这种⽅式织 Aspect 的
- 类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器 (ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载 时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊Aspect。
- 运⾏期:Aspect在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊ Aspect 时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊Aspect 的。
3.3.1.1 动态代理
此种实现在设计模式上称为动态代理模式,在实现的技术⼿段上,都是在 class 代码运⾏期,动态的织⼊字节码。
Spring 框架中的AOP,主要基于两种⽅式:JDK 及 CGLIB 的⽅式。这两种⽅式的代 理⽬标都是被代理类中的⽅法,在运⾏期动态的织⼊字节码⽣成代理类。
CGLIB是Java中的动态代理框架,主要作⽤就是根据⽬标类和⽅法,动态⽣成代理类。
Java中的动态代理框架,⼏乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码⽂件信息,修改部
分信息,或动态⽣成⼀个 class。
3.3.1.1.1 JDK 动态代理实现
JDK 实现时,先通过实现 InvocationHandler 接⼝创建⽅法调⽤处理器,再通过 Proxy 来创建代理类。
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被
代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public PayServiceJDKInvocationHandler( Object target) {
this.target = target;
}
//proxy代理对象
@Override
public Object invoke(Object proxy, Method method, Object[] args) throw s Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler =
new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
proxy.pay();
}
}
3.3.1.1.2 CGLIB 动态代理实现
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public PayServiceCGLIBInterceptor(Object target){
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args, Method Proxy methodProxy) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
PayService proxy= (PayService) Enhancer.create(target.getClass(),n ew PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
3.3.1.1.3 JDK 和 CGLIB 实现的区别
- JDK 实现,要求被代理类必须实现接⼝,之后是通过 InvocationHandler 及 Proxy,在运⾏
时动态的在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代 理接⼝实现的⽅式),只是该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完 成。 - CGLIB 实现,被代理类可以不实现接⼝,是通过继承被代理类,在运⾏时动态的⽣成代理类
对象。
4. Spring 事务
4.1 什么是事务?
将⼀组操作封装成⼀个执⾏单元,要么全部成功要么全部失败。
- 为什么要⽤事务?
⽐如转账分为两个操作:第⼀步操作:A 账户 -100 元,第⼆步操作:B 账户 +100 元。如果没有事务,第⼀步执⾏成功了,第⼆步执⾏失败了,那么 A 账户平⽩⽆故的 100 元就“⼈间蒸发”了。如果使⽤事务就可以解决这个问题,让这⼀组操作要么⼀起成功,要么⼀起失败。
4.1.1 事务特性
事务有4 ⼤特性(ACID):原⼦性(Atomicity,或称不可分割性)、持久性(Consistency)
、⼀致性(Durability)和隔离性(Isolation,⼜称独⽴性)。
- 原⼦性:⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中
间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个
事务从来没有执⾏过⼀样。- ⼀致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完
全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯
作。- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
- 隔离性:数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务
并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Read
uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
4.2 Spring 事务的实现
MySQL 中的事务使⽤:
事务在 MySQL 有 3 个重要的操作:开启事务、提交事务、回滚事务。-- 开启事务 start transaction; -- 业务执⾏ -- 提交事务 commit; -- 回滚事务 rollback;
4.2.1 Spirng 编程式事务
步骤:
- 开启事务(获取事务)
- 提交事务
- 回滚事务
SpringBoot 内置了两个对象:DataSourceTransactionManager ⽤来开启事务、提交或回滚事务,TransactionDefinition 是事务的属性,在开启事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus。
示例:
@RestController
public class UserController {
@Resource
private UserService userService;
// JDBC 事务管理器
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
// 定义事务属性
@Resource
private TransactionDefinition transactionDefinition;
@RequestMapping("/sava")
public Object save(User user) {
// 开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
// 插⼊数据库
int result = userService.save(user);
// 提交事务
dataSourceTransactionManager.commit(transactionStatus);
// // 回滚事务
// dataSourceTransactionManager.rollback(transactionStatus);
return result;
}
}
4.2.2 Spring 声明式事务
在需要的⽅法上添加 @Transactional 注解即可,⽆需⼿动开启事务和提交事务,进⼊⽅法时⾃动开启事务,⽅法执⾏完会⾃动提交事务,如果中途发⽣了没有处理的异常会⾃动回滚事务。
示例:
@RequestMapping("/save")
@Transactional
public Object save(User user) {
int result = userService.save(user);
return result;
}
示例:使⽤以下代码,分别设置 @Transactional 注解和不设置 @Transactional
4.2.2.1 @Transactional 作⽤范围
@Transactional 可以⽤来修饰⽅法或类:
- 修饰⽅法时:需要注意只能应⽤到 public ⽅法上,否则不⽣效。推荐此种⽤法
- 修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效
4.2.2.2 @Transactional 参数
4.2.2.3 @Transactional 在异常被捕获不会进⾏事务⾃动回滚
示例:
@RestController
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/save")
@Transactional
public Object save(User user) {
// 插⼊数据库
int result = userService.save(user);
try {
// 执⾏了异常代码(0不能做除数)
int i = 10 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
}
return result;
}
}
4.2.2.3.1 解决方案1:将异常重新抛出
示例:
@RequestMapping("/save")
@Transactional(isolation = Isolation.SERIALIZABLE) public Object save(User user) {
// 插⼊数据库
int result = userService.save(user);
try {
// 执⾏了异常代码(0不能做除数)
int i = 10 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
// 将异常重新抛出去
throw e;
}
return result;
}
4.2.2.3.2 解决方案2:⼿动回滚事务
在⽅法中使⽤TransactionAspectSupport.currentTransactionStatus() 可 以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了
@RequestMapping("/save")
@Transactional(isolation = Isolation.SERIALIZABLE)
public Object save(User user) {
// 插⼊数据库
int result = userService.save(user);
try {
// 执⾏了异常代码(0不能做除数)
int i = 10 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
// ⼿动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnl y();
}
return result;
}
4.2.2.4 @Transactional 工作原理
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默 认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。 @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途 遇到的异常,则回滚事务。
4.3 Spring 事务的隔离级别
- 事务的隔离级别:数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务 并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化 (Serializable)。
4.3.1 Spring 事务的隔离级别
- MySQL 事务隔离级别:
- READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。
- READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间 的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。
- REPEATABLE READ:可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败 (因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读 (Phantom Read)。
- SERIALIZABLE:序列化,事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决
了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。
- 脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致 第⼀个事务读取的数据是错误的。
- 不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修 改了。
- 幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。
- 查询MySQL事务隔离级别:
select @@global.tx_isolation,@@tx_isolation; // 全局事务隔离级别 // 当前连接的事务隔离级别
执行结果:
4.3.1.1 Spring 中设置事务隔离级别
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
- Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。
- Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。
4.3.1.2 Spring 中事务隔离级别的设置
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置。
示例:
@RequestMapping("/save")
@Transactional(isolation = Isolation.SERIALIZABLE) public Object save(User user) {
// 业务实现
}
4.4 Spring 事务的传播机制
4.4.1 事务传播机制是什么?
Spring 事务传播机制定义了多个包含了事务的⽅法,相互调⽤时,事务是如何在这些⽅法间进⾏传递的。
4.4.2 事务传播机制的作用
事务传播机制是保证⼀个事务在多 个调⽤⽅法间的可控性的(稳定性的)。
4.4.3 Spring 事务传播机制
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
- Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以情侣关系为例来理解:
4.4.4 Spring 事务传播机制使用示例
4.4.4.1 REQUIRED
6. SpringBoot
Spring 的诞⽣是为了简化 Java 程序的开发的,⽽ Spring Boot 是为了快速开发 Spring 程序开发而诞生的。
Spring Boot 的优点:
- 快速集成框架,Spring Boot 提供了启动添加依赖的功能,⽤于秒级集成各种框架。
- 内置运⾏容器,⽆需配置 Tomcat 等 Web 容器,直接运⾏和部署程序
- 快速部署项⽬,⽆需外部容器即可启动并运⾏项⽬。
- 可以完全抛弃繁琐的 XML,使⽤注解和配置的⽅式进⾏开发。
- ⽀持更多的监控的指标,可以更好的了解项⽬的运⾏情况。
6.1 SpringBoot 创建和使用演示
6.1.1 创建
- 安装Spirng Boot Helper插件,安装好之后它的名字就变成Spirng Initialzr and Assistant 了
- 创建 Spring Boot 项目
首先把 Maven 配置为国内源:
第⼀次打开 Spring Boot 项⽬需要加载很久,因为当前 Spring Boot 框架并没有在⾃⼰的本地仓库,为了加速 Spring Boot 项⽬的下载,在打开项⽬之前,先把 Maven 配置为国内源。
<1> 打开 settings
<2>
检查 User Settings file 的 settings.xml ⽂件是否存在,如果不存在,复制下⾯配置了国内源的 settings.xml ⽂件,放到 User Settings file ⽬录下。// 配置了国内源的 settings.xml <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="htt p://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven. apache.org/xsd/settings-1.1.0.xsd"> <localRepository>C:\Users\intel\.m2\repository</localRepository> <mirrors> <mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors> </settings>
如果存在,检查 settings.xml 是否配置了国内源,对照代码修改配置即可。
<3>删除本地存储 jar ⽬录中的所有⽂件
<4> 切换到 Idea 中,重新下载 jar 包:
如果出现中⽂会导致下载了 jar 包,但是在项⽬中不能正常使⽤;如果还是下载失败那就是本地⽹速问题,尝试更好⽹络,使⽤⼿机热点或朋友的⼿机热点尝试,如果还是不⾏,间隔 4 ⼩时之后再试。
正式开始创建项⽬:
点击 Finish 就完成 Spring Boot 的项⽬创建了。
网页版创建:
不使⽤ Idea 也可以创建 Spring Boot 项⽬,我们可以使⽤ Spring 官⽅提供的⽹⻚版来创建Spring Boot 项⽬。
<1> 先访问:https://start.spring.io
点击⽣成按钮会下载⼀个 Spring Boot 的 zip 包,解压 zip 之后⽬录如下:
<2> 使⽤ Idea 打开之后,Spring Boot 项⽬就算创建成功了
6.1.1 使用
Spring项目的特点:约定大于配置。
- 约定把要注入到容器的类和启动类放到同级目录下,此时Spring Boot 项目才能将bean注入到容器中,类上标注 @SpringBootApplication 就可以启动 Spring Boot 项⽬了。
对⽐ Spring 的项⽬我们也可以看到这⼀特点,⽐如在 Spring 中也是要配置 Bean 的扫描路径的,⽽ Spring Boot 则不需要,Spring 配置如下:
项目目录介绍
- src/main/java 为 Java 源代码
- src/main/resources 为静态资源或配置⽂件,/static:静态资源⽂件夹;/templates:模版资源⽂件夹。
-
运行项目
点击启动类的 main ⽅法就可以运⾏ Spring Boot 项⽬了,
启动成功如下图所示:
-
输出 Hello world
之前 Spring 是⼀个普通 Java 项⽬,没 办法直接和浏览器进⾏互动,⽤ Spring Boot 可以直接实现和浏览器及⽤户的交互。
<1> 在创建的项⽬包路径下创建 UserController ⽂件,实现代码如下:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayhi")
public String sayHi(){
return "Hi,Spring Boot.";
}
}
<2> 重新启动项⽬,访问 http://localhost:8080/user/sayhi 最终效果如下:
6.2 SpringBoot 热部署
- 添加热部署框架⽀持
在 pom.xml 中添加如下框架引⽤:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
-
Settings 开启项⽬⾃动编译
-
开启运⾏中热部署
⾼版本 Idea 设置(IntelliJ IDEA 2021.2 之后版本)
- 使⽤ Debug 启动(⾮Run)
6.3 SpringBoot 配置文件
6.3.1 配置文件作用
整个项⽬中所有重要的数据都是在配置⽂件中配置的:
- 数据库的连接信息(包含⽤户名和密码的设置),
- 项⽬的启动端⼝
- 第三⽅系统的调⽤秘钥等信息
- ⽤于发现和定位问题的普通⽇志和异常⽇志等
如果没有配置信息,那么 Spring Boot 项⽬就不能连接和操作数据库,甚⾄是不能保存可以⽤ 于排查问题的关键⽇志,所以配置⽂件⾮常重要。
6.3.2 配置⽂件的格式
- 理论上讲 properties 可以和 yml ⼀起存在于⼀个项⽬当中,当 properties 和 yml ⼀起存在⼀个项⽬中时,如果配置⽂件中出现了同样的配置,⽐如 properties 和 yml 中都配置“server.port”, 那么这个时候会以 properties 中的配置为主,也就是 .properties 配置⽂件的优先级最⾼,但加载 完 .properties ⽂件之后,也会加载 .yml ⽂件的配置信息。实际的业务当中,我们通常会采取⼀种统⼀的配置⽂件格式,这样可以更好的维护(降低故障率)。
6.3.2.1 .properties 详解
properties 配置⽂件是最早期的配置⽂件格式,也是创建 Spring Boot 项⽬默认的配置⽂件。
- 语法:
properties 是以键值的形式配置的,key 和 value 之间是以“=”连接。
# 配置项⽬端⼝号
# 配置⽂件中使⽤“#”来添加注释信息。
server.port=8084
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?characterEncoding= utf8
spring.datasource.username=root
spring.datasource.password=root
从上述配置key看出,properties 配置⽂件中会有很多的冗余的信息:
- 读取配置⽂件
主动的读取配置⽂件中的内容,可以使⽤ @Value 注解来实现,@Value 注解使⽤“${}”的格式读取。
@Component 在 Spring Boot 启动时候会注⼊到框架中,注⼊到框架中时会执⾏ @PostConstruct初始化⽅法,这个时候就能读取到配置信息了。
示例:
import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class ReadYml {
@Value("${server.port}")
private String port;
@PostConstruct
public void postConstruct() {
System.out.println("Read YML,port:" + port);
}
}
6.3.2.2 .yml 详解
yml 是 YAML 是缩写,它的全称 Yet Another Markup Language 翻译成中⽂就是“另⼀种标记语⾔”。
- yml 是⼀个可读性⾼,写法简单、易于理解,它的语法和 JSON 语⾔类似。
- yml ⽀持更多的数据类型,它可以简单表达清单(数组)、散列表,标量等数据形态。它使⽤空⽩ 符号缩进和⼤量依赖外观的特⾊,特别适合⽤来表达或编辑数据结构、各种配置⽂件等。
- yml ⽀持更多的编程语⾔,它不⽌是 Java 中可以使⽤在 Golang、PHP、Python、Ruby、 JavaScript、Perl 中。
- 语法
- yml 是树形结构的配置⽂件,它的基础语法是“key: value”,注意 key 和 value 之间使⽤英⽂冒汗加空 格的⽅式组成的,其中的空格不可省略。
- 示例:使⽤ yml 连接数据库
spring: datasource: url: jdbc:mysql://127.0.0.0:3306/dbname?>characterEncoding=utf8 username: root password: root
properties:
yml:
- yml 配置读取
yml 读取配置的⽅式和 properties 相同,使⽤ @Value 注解即可。
import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class ReadYml {
@Value("${string.hello}")
private String hello;
@PostConstruct
public void postConstruct() {
System.out.println("Read YML,Hello:" + hello);
}
}
配置不同数据类型及 null:
# 字符串 string.value: Hello #布尔值,true或false boolean.value: true boolean.value1: false # 整数 int.value: 10 int.value1: 0b1010_0111_0100_1010_1110 # ⼆进制 # 浮点数 float.value: 3.14159 float.value1: 314159e-5 # 科学计数法 # Null,~代表null null.value: ~
- 配置字符串:
字符串默认不⽤加上单引号或者双引号,如果加英⽂的单双引号可以表示特殊的含义。
- 字符串默认不⽤加上单引号或者双引号。
- 单引号会转义特殊字符,特殊字符最终只是⼀个普通的字符串数据。
- 双引号不会转义字符串⾥⾯的特殊字符;特殊字符会作为本身想表示的意思。
配置:
string: str1: Hello \n Spring Boot. str2: 'Hello \n Spring Boot.' str3: "Hello \n Spring Boot."
读取:
import org.springframework.beans.factory.annotation.Value; >import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; @Component public class ReadYml { @Value("${string.str1}") private String str1; @Value("${string.str2}") private String str2; @Value("${string.str3}") private String str3; @PostConstruct public void postConstruct() { System.out.println("string.str1:" + str1); System.out.println("string.str2:" + str2); System.out.println("string.str3:" + str3); } }
执行结果:
- 配置对象:
student: id: 1 name: Java age: 18
# 行内写法: student: {id: 1,name: Java,age: 18}
读取对象: @ConfigurationProperties
# getter 和 setter ⽅法不能省略。 import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "student") @Component public class StudentComponent { private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "StudentComponent{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
调用类:
@Component public class ReadYml2 { @Autowired private StudentComponent studentComponent; @PostConstruct public void postConstruct() { System.out.println(studentComponent); } }
执行结果;
配置集合:
dbtypes: name: - mysql - sqlserver - db2
# ⾏内写法 dbtypes: {name: [mysql,sqlserver,db2]}
读取集合: @ConfigurationProperties
@Component @ConfigurationProperties("dbtypes") @Data public class ListConfig { private List<String> name; }
打印类:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; @Component public class ReadYml2 { @Autowired private ListConfig listConfig; @PostConstruct public void postConstruct() { System.out.println(listConfig.getName()); } }
6.3.2.3 properties VS yml
- properties 是以 key=value 的形式配置的键值类型的配置⽂件,⽽ yml 使⽤的是类似 json 格式的 树形配置⽅式进⾏配置的,yml 层级之间使⽤换⾏缩进的⽅式配置,key 和 value 之间使⽤“: ”英⽂冒号加空格的⽅式设置,并且空格不可省略。
- properties 为早期并且默认的配置⽂件格式,但其配置存在⼀定的冗余数据,使⽤ yml 可以很好的 解决数据冗余的问题。
- yml 通⽤性更好,⽀持更多语⾔,如 Java、Go、Python 等,如果是云服务器开发,可以使⽤⼀份 配置⽂件作为 Java 和 Go 的共同配置⽂件。
- yml ⽀持更多的数据类型。
SpringMVC程序开发
什么是MVC
MVC 是 Model View Controller 的缩写,它是软件⼯程中的⼀种软件架构模式,它把软件系统分 为模型、视图和控制器三个基本部分。
- Model(模型)是应⽤程序中⽤于处理应⽤程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
- View(视图)是应⽤程序中处理数据显示的部分。通常视图是依据模型数据创建的。
- Controller(控制器)是应⽤程序中处理⽤户交互的部分。通常控制器负责从视图读取数据, 控制⽤户输⼊,并向模型发送数据。
什么是SpringMVC
- Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从⼀开始就包含在 Spring 框架中。它的正式名称“Spring Web MVC”来⾃其源模块的名称(Spring-webmvc),但它通常被称为“SpringMVC”。
- MVC 是⼀种思想,⽽ Spring MVC 是对 MVC 思想的具体实现。总结来说,Spring MVC 是⼀个实现了 MVC 模式,并继承了 Servlet API 的 Web 框架。既然是 Web 框架,那么当⽤户在浏览器中输⼊了 url 之后, Spring MVC 项⽬就可以感知到⽤户的请求。
- Spring MVC 是 Spring 框架的核⼼模块,⽽ Spring Boot 是 Spring 的脚⼿架,绝⼤部分的 Java 项⽬约等于 Spring MVC 项⽬。
- 在创建 Spring Boot 项⽬时,我们勾选的 Spring Web 框架其实就是 Spring MVC 框架。
Spring MVC 创建和连接
Spring MVC 项⽬创建和 Spring Boot 创建项⽬相同(Spring MVC 使⽤ Spring Boot 的⽅式创建), 在创建的时候选择 Spring Web 就相当于创建了 Spring MVC 的项⽬。 Spring MVC 中使⽤ @RequestMapping 来实现 URL 路由映射,也就是浏览器连接程序的作⽤。
- 创建 Spring MVC 项⽬
<<1>> Spring MVC 可以基于 Spring Boot 创建,也就是创建⼀个 Spring Boot 项⽬,勾选上 Spring Web 模块即可。
<<2>> 创建⼀个 UserController 类,实现⽤户到 Spring 程序的互联互通
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;
@Controller // 让 spring 框架启动时,加载
@ResponseBody // 返回⾮⻚⾯数据
@RequestMapping("/user") // 路由器规则注册
public class UserController {
// 路由器规则注册
@RequestMapping("/hi")
public String sayHi(){
return "<h1>Hi,Spring MVC.</h1>";
}
}
<<3>> 当访问地址:http://localhost:8080/user/hi 时就能打印“hello,spring mvc”的信息了。
- @RequestMapping 注解:
@RequestMapping 是 Spring Web 应⽤程序中最常被⽤到的注解之⼀,它是⽤来注册接⼝的路 由映射的。所谓的路由映射指的是,当⽤户访问⼀个 url 时,将⽤户的请求对应到程序中某个类 的某个⽅法的过程就叫路由映射。- @RequestMapping 修饰类:
import com.example.demo.model.Person; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/p") public class PersonController { @RequestMapping("/index") public Object index(Person person){ // 获取参数 System.out.println(person.getName() +":"+ person.getPassword()); // 执⾏业务... return "/index.html"; } }
- @RequestMapping 修饰类+修饰⽅法,当修饰类和⽅法时,访问的地址是类 + ⽅法。
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import >org.springframework.web.bind.annotation.ResponseBody; @Controller @ResponseBody // 定义返回的数据格式为⾮视图(text/html) public class UserController { @RequestMapping("/hi") public String sayHi(){ return "<h1>Hi,Spring MVC.</h1>"; } }
⽅法地址:http://localhost:8080/hi
- @RequestMapping默认接收GET请求,显示的指定 @RequestMapping 来接收 POST请求
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import >org.springframework.web.bind.annotation.RequestMethod; import >org.springframework.web.bind.annotation.ResponseBody; @Controller @ResponseBody // 定义返回的数据格式为⾮⻚⾯ public class UserController { @RequestMapping(value = "/hi",method= RequestMethod.POST) public String sayHi(){ return "<h1>Hi,Spring MVC.</h1>"; } }
- GET请求的3种写法:
// 写法1 @RequestMapping("/index") // 写法2 @RequestMapping(value = "/index",method = RequestMethod.GET) // 写法3 @GetMapping("/index")
- post 请求的 2 种写法:
// 写法1 @RequestMapping(value = "/index",method = RequestMethod.POST) // 写法2 @PostMapping("/index")
获取参数
- 传递单个参数
在 Spring MVC 中可以直接⽤⽅法中的参数来实现传参。
示例:
@RequestMapping("/m1") public Object method_1(String name){ System.out.println("参数 name:"+name); return "/index.html"; }
程序的执⾏结果:
- 传递对象
Spring MVC 可以⾃动实现参数对象的赋值
示例:
定义对象:
import lombok.Data;
@Data
public class Person {
private int id;
private String name;
private String password;
}
传递对象代码实现:
@RequestMapping("/m2")
public Object method_2(Person p){
System.out.println("对象中的 name:"+p.getName());
System.out.println("对象中的 password:"+p.getPassword());
return "/index.html";
}
前端访问:
最终执⾏结果:
- 表单参数传递/传递多个参数(⾮对象)
当有多个参数时,前后端进⾏参数匹配时,是以参数的名称进⾏匹配的,因此参数的位置不影响后端获取参数的结果。
@RequestMapping("/m3")
public Object method_3(String name, String pwd) {
System.out.println("name 参数:" + name);
System.out.println("pwd 参数:" + pwd);
return "/index.html";
}
前台访问地址:
- 后端参数重命名(后端参数映射)
某些特殊的情况下,前端传递的参数 key 和我们后端接收的 key 可以不⼀致,⽐如前端传递了⼀个 time 给后端,⽽后端⼜是有 createtime 字段来接收的,这样就会出现参数接收不到的情况,如果出现 这种情况,我们就可以使⽤ @RequestParam 来重命名前后端的参数值。
@RequestMapping("/m4")
public Object method_4(@RequestParam("time") String createtime) {
System.out.println("时间:" + createtime);
return "/index.html";
}
前端访问地址:
- 设置必传参数:
如果我们是前端传递⼀个⾮ time 的参数,就会出现程序报错的情况:
这是因为后端@RequestParam 注解已经声明了前端必须传递⼀个 time 的参数,但是前端没有给后端传递,查看 @RequestParam 注解的实现细节: - ⾮必传参数设置
如果我们的实际业务前端的参数是⼀个⾮必传的参数,我们可以通过设置 @RequestParam 中的 required=false 来避免不传递时报错。
@RequestMapping("/m4")
public Object method_4(@RequestParam(value = "time", required = false) Stri ng createtime) {
System.out.println("时间:" + createtime);
return "/index.html";
}
- @RequestBody 接收JSON对象
后端接收代码:
@RequestMapping(value = "/m5", method = RequestMethod.POST) public Object method_5(@RequestBody Person person) {
System.out.println("Person:" + person);
return "redirect:/index.html";
}
- 获取URL中参数@PathVariable
后端实现代码:
@PostMapping("/m6/{name}/{password}")
public Object method_6(@PathVariable String name, @PathVariable String pass word) {
System.out.println("name:" + name);
System.out.println("password:" + password);
return "redirect:/index.html";
}
前端⽅法地址:
@PostMapping(“/m6/{name}/{password}”) 中的 {password} 参数不能省略。
- 上传⽂件@RequestPart
@RequestMapping("/param9")
public String param9(String name, @RequestPart("myfile") MultipartFile fil e) throws IOException {
// 获取⽂件后缀名
String fileName = file.getOriginalFilename().substring(file.getOrigina lFilename().lastIndexOf("."));
// ⽂件保存地址
String filePath = ClassUtils.getDefaultClassLoader().getResource("stat ic").getPath() +
"/" + UUID.randomUUID() + fileName;
// 保存⽂件
file.transferTo(new File(filePath));
return filePath + " 上传成功.";
}
获取项⽬⽬录的⼏种⽅式:
ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX).getPath(); new ClassPathResource("").getFile().getAbsolutePath(); ClassUtils.getDefaultClassLoader().getResource("").getPath(); ResourceUtils.getFile("classpath:static/").getPath();
- 获取 Request 和 Response 对象
@RequestMapping("/param10")
public String param10(HttpServletResponse response, HttpServletRequest requ est) {
String name = request.getParameter("name");
// 获取所有 cookie 信息
Cookie[] cookies = request.getCookies();
return name + " 你好.";
}
- 传统获取 header/cookie
@RequestMapping("/param10")
@ResponseBody
public String param10(HttpServletResponse response, HttpServletRequest requ est) {
String name = request.getParameter("name");
// 获取所有 cookie 信息
Cookie[] cookies = request.getCookies();
String userAgent = request.getHeader("User-Agent");
return name + ":"+userAgent;
}
- 简洁的获取 Cookie—@CookieValue
@RequestMapping("/cookie")
@ResponseBody
public String cookie(@CookieValue("bite") String bite) {
return "cookie:" + bite;
}
- 简洁获取 Header—@RequestHeader
@RequestMapping("/header")
@ResponseBody
public String header(@RequestHeader("User-Agent") String userAgent) {
return "userAgent:"+userAgent;
}
- Session 存储和获取
Session 存储:
@RequestMapping("/setsess")
@ResponseBody
public String setsess(HttpServletRequest request) {
// 获取 HttpSession 对象,参数设置为 true 表示如果没有 session 对象就创建⼀个 session
HttpSession session = request.getSession(true);
if(session!=null){
session.setAttribute("username","java");
}
return "session 存储成功";
}
读取 Session :
@RequestMapping("/sess")
@ResponseBody
public String sess(HttpServletRequest request) {
// 如果 session 不存在,不会⾃动创建
HttpSession session = request.getSession(false);
String username = "暂⽆";
if(session!=null && session.getAttribute("username")!=null){
username = (String) session.getAttribute("username");
}
return "username:"+username;
}
- 获取 Session 更简洁的⽅式:
@RequestMapping("/sess2")
@ResponseBody
public String sess2(@SessionAttribute(value = "username",required = false) String username) {
return "username:"+username;
}
返回数据
默认请求下⽆论是 Spring MVC 或者是 Spring Boot 返回的是视图 (xxx.html),⽽现在都是前后端分离的,后端只需要返给给前端数据即可,这个时候我们就需要使⽤ @ResponseBody 注解了。
@ResponseBody 返回的值如果是字符会转换成 text/html,如果返回的是对象会转换成 application/json 返回给前端。
@ResponseBody 可以⽤来修饰⽅法或者是修饰类,修饰类表示类中的所有⽅法都会返回 html 或者 json,⽽不是视图。
组合注解:@RestController,@RestController = @Controller + @ResponseBody。
- 返回静态⻚⾯
<<1>> 创建前端⻚⾯ index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1. 0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hello,spring mvc</title>
<script src="index.js"></script>
</head>
<body>
<h1>Hello,Spring MVC.</h1>
</body>
</html>
创建控制器 controller:
import com.example.demo.model.Person;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/p")
public class PersonController {
@RequestMapping("/index")
public Object index(){
// 执⾏业务...
// 返回view -> index.html
return "/index.html";
}
}
- 返回 text/html
@RequestMapping("/m7") @ResponseBody
public String method_7() {
return "<h1>Hello,HTML~</h1>";
}
示例:实现计算器功能
使⽤ postman 传递参数,或使⽤ form 表单的⽅式提交参数。
前端⻚⾯:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1. 0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>计算器示例</title>
</head>
<body>
<form action="http://localhost:8080/calc/sum">
<h1>计算器</h1>
数字1:<input name="num1" type="text"><br>
数字2:<input name="num2" type="text"><br>
<input type="submit" value=" 点击相加 ">
</form>
</body>
</html>
controler代码:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@Controller
@RequestMapping("/calc")
public class CalcController {
@RequestMapping("/sum")
public String sum(Integer num1,Integer num2){
return String.format("<h1>计算的结果是:%d</h1><a href='javascript:h istory.go(-1);'>返回</a>",num1+num2);
}
}
- 返回 JSON 对象
@RequestMapping("/m8")
@ResponseBody
public HashMap<String, String> method_8() {
HashMap<String, String> map = new HashMap<>();
map.put("Java", "Java Value");
map.put("MySQL", "MySQL Value");
map.put("Redis", "Redis Value");
return map;
}
示例:实现登录功能,前端使⽤ ajax,后端返回 json 给前端
后端代码:
@RequestMapping(value = "/login")
@ResponseBody
public HashMap<String,Object> login(String username, String password){
HashMap<String,Object> res = new HashMap<>();
int succ = 200;
if(username!=null && password!=null &&
username.equals("admin") && password.equals("admin")){
res.put("msg","登录成功");
}else{
res.put("msg","登录失败");
}
res.put("succ",succ);
return res;
}
前端代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1. 0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="js/jquery-1.9.1.min.js"></script>
<title>Document</title>
<script>
function mysub() {
var username = jQuery("#username").val();
var password = jQuery("#password").val();
jQuery.getJSON("/user/login",
{
"username":username,
"password":password
},
function (result) {
if(result.succ==200){
alert("返回结果:"+result.msg);
}else{
alert("操作失败,请重试。");
}
});
}
</script>
</head>
<body>
<div style="text-align: center;">
<h1>登录</h1>
⽤户:<input id="username">
<br>
密码:<input id="password" type="password">
<br>
<input type="button" value=" 提交 " onclick="mysub()" style="margin -top: 20px;margin-left: 50px;">
</div>
</body>
</html>
- 请求转发或请求重定向
// 请求重定向
@RequestMapping("/index")
public String index(){
return "redirect:/index.html";
}
// 请求转发
@RequestMapping("/index2")
public String index2(){
return "forward:/index.html";
}
转发是服务器帮转的,⽽重定向是让浏览器重新请求另⼀个地址
请求重定向(redirect)将请求重新定位到资源;请求转发(forward)服务器端转发。
请求重定向地址发⽣变化,请求转发地址不发⽣变化。
请求重定向与直接访问新地址效果⼀直,不存在原来的外部资源不能访问;请求转发服务器端转发有可能造成原外部资源不能访问。
请求转发 forward 导致问题演示:
请求转发如果资源和转发的⻚⾯不在⼀个⽬录下,会导致外部资源不可访问。
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/index")
public String sayHi(){
return "forward:/index.html";
}
}
程序的⽬录:
程序的执⾏结果:
将转发 foward 换成重定向 redirect,⻚⾯就可以正常获取到外部资源 js 了。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/index")
public String sayHi(){
return "redirect:/index.html";
}
}
综合示例:
示例一:使⽤ ajax 实现登录功能,⽆需连接数据库,如果⽤户名和密码都是 root,则显示登录成功,可以直 接访问 main ⻚⾯,否则显示登录失败。每次访问 main ⻚⾯时,验证是否已经登录,如果登录了显示 欢迎信息,否则让⽤户先去登录⻚⾯登录。
示例二:带头像功能的注册功能实现。
6.4 SpringBoot 单元测试
什么是单元测试?
单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试。
单元测试是开发者编写的⼀⼩段代码,⽤于检验被测代码的⼀个很⼩的、很明确的(代码)功能是否正确。执⾏单元测试就是为了证明某段代码的执⾏结果是否符合我们的预期。如果测试结果符合我们的预 期,称之为测试通过,否则就是测试未通过(或者叫测试失败)。
- 可以⾮常简单、直观、快速的测试某⼀个功能是否正确
- 使⽤单元测试可以帮我们在打包的时候,发现⼀些问题,因为在打包之前,所以的单元测试必须通 过,否则不能打包成功。
- 使⽤单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进⾏任何改 变的情况下,测试功能。
6.4.1 Spring Boot 单元测试使用
Spring Boot 项⽬创建时会默认单元测试框架 spring-boot-test,⽽这个单元测试框架主要是依靠另⼀ 个著名的测试框架 JUnit 实现的, Spring Boot 项⽬创建时⾃动添加。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
spring-boot-starter-test 的 MANIFEST.MF⾥⾯有具体的说明:
- ⽣成单元测试类
最终⽣成的代码:
import org.junit.jupiter.api.Test;
class UserControllerTest {
@Test
void save() {
}
}
这个时候,此⽅法是不能调⽤到任何单元测试的⽅法的,此类只⽣成了单元测试的框架类,具体的业务 代码要⾃⼰填充。
- 添加单元测试代码
<<1>> 添加 Spring Boot 框架测试注解:@SpringBootTest
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class UserControllerTest {
@Test
void save() {
}
}
<<2>> 添加单元测试业务逻辑
import com.example.demo.model.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class UserControllerTest {
@Resource
private UserController userController;
@Test
void save() {
User user = new User();
user.setId(1);
user.setName("Java Test");
user.setPassword("123");
boolean result = userController.save(user);
// 使⽤断⾔判断最终的结果是否符合预期
Assertions.assertEquals(true, result);
// Assertions.assertTrue(result);
}
}
如果断⾔失败,则后⾯的代码都不会执⾏
6.8 SpringBoot 日志文件
日志的功能
- 发现和定位问题
- 记录⽤户登录⽇志,⽅便分析⽤户是正常登录还是恶意破解⽤户
- 记录系统的操作⽇志,⽅便数据恢复和定位操作⼈
- 记录程序的执⾏时间,⽅便为以后优化程序提供数据⽀持
日志的使用
Spring Boot 内置了⽇志框架,默认情况下使⽤的是 info ⽇志级别将⽇志输出到控制台的。
⾃定义⽇志打印
- 在程序中得到⽇志对象
在程序中获取⽇志对象需要使⽤⽇志⼯⼚ LoggerFactory。
Logger 对象是属于 org.slf4j 包下的,不要导⼊错包,Spring Boot 中内置了⽇志框架 Slf4j,可以直接在程序中调⽤ slf4j 来输出⽇志。
代码示例:
private static Logger logger = LoggerFactory.getLogger(UserController.class );
- 使⽤⽇志对象打印⽇志
// 2.使⽤⽇志打印⽇志
logger.info("--------------要输出⽇志的内容----------------");
⽇志级别
⽇志级别的作用
- ⽇志级别可以帮你筛选出重要的信息,⽐如设置⽇志级别为 error,那么就可以只看程序的报错⽇ 志了,对于普通的调试⽇志和业务⽇志就可以忽略了,从⽽节省开发者信息筛选的时间。
- ⽇志级别可以控制不同环境下,⼀个程序是否需要打印⽇志,如开发环境我们需要很详细的信息, ⽽⽣产环境为了保证性能和安全性就会输⼊尽量少的⽇志,⽽通过⽇志的级别就可以实现此需求。
⽇志级别的分类
- trace:微量,少许的意思,级别最低
- debug:需要调试时候的关键信息打印
- info:普通的打印信息(默认⽇志级别)
- warn:警告,不影响使⽤,但需要注意的问题
- error:错误信息,级别较⾼的错误⽇志信息
- fatal:致命的,因为代码异常导致程序退出执⾏的事件
越往上接收到的消息就越少,如设置了 warn 就只能收到 warn、error、fatal 级别的⽇志了。
⽇志级别的设置
⽇志级别配置只需要在配置⽂件中设置“logging.level”配置项即可。⽇志的输出级别,默认是 info。
示例:
logging:
level:
root: error
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
// 1.得到⽇志对象
private static Logger logger =
LoggerFactory.getLogger(UserController.class);
@Value("${server.port}")
private String port;
@Value("${spring.datasource.url}")
private String url;
@RequestMapping("/sayhi")
public String sayHi() {
// 2.使⽤⽇志打印⽇志
logger.trace("================= trace ===============");
logger.debug("================= debug ===============");
logger.info("================= info ===============");
logger.warn("================= warn ===============");
logger.error("================= error ===============");
return "Hi," + url;
}
}
日志持久化
在⽣产环境上咱们需要将⽇志保存下来,以便出现问题之后追 溯问题,把⽇志保存下来的过程就叫做持久化。在配置⽂件中指定⽇志的存储⽬录或者是指定⽇志保存⽂件名之后, Spring Boot 就会将控制台的⽇志写到相应的⽬录或⽂件下了。
示例一:
# 设置⽇志⽂件的⽬录
logging:
file:
path: D:\\home\\ruoyi
# 设置⽇志⽂件的⽂件名
logging:
file:
name: D:\\home\\ruoyi\\spring-1204.log
示例二:
controller 包下 error 级别以上的⽇志保存到 log_all.log 下,将 service 下 warn 级别以上的⽇志保存到 log_all.log 下。
- 实现的关键步骤:
不同包定义不同的⽇志级别。
使⽤⽇志对象打印所有类型的⽇志。
设置固定的⽇志保存⽂件名。
使用Lombok输出日志
使⽤ @Slf4j 注解,在程序中使⽤ log 对象即可输⼊⽇志,并且只能使⽤ log 对象才能输出, 这是 lombok 提供的对象名。
- 添加 lombok 依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<optional>true</optional>
</dependency>
- 输出⽇志
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/p")
@Slf4j
public class PersonController {
@RequestMapping("/log")
public void loggerTest() {
log.error("------------------- error -----------------");
}
}
lombok 原理
打开target目录:
lombok 的更多注解:
6.6 SpringBoot 集成Redis
- 添加 redis 依赖
或将以下配置添加到 pom.xml 中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置 redis
spring.redis.database=0
spring.redis.port=6379
spring.redis.host=82.157.146.10
# 可省略
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=1ms
spring.redis.lettuce.shutdown-timeout=100ms
- ⼿动操作 redis
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class RedisController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 在 redis 存储数据
@RequestMapping("/setrs")
public String setRedis(String name, String value) {
stringRedisTemplate.opsForValue().set(name, value,
30, TimeUnit.SECONDS);
return "Set redis success.";
}
// 读取 redis 中的数据
@RequestMapping("/getrs")
public String getRedis(String name) {
Object valObj = stringRedisTemplate.opsForValue().get(name);
if (valObj != null) return valObj.toString();
return "Null";
}
}
- 注解操作 redis
<<1>> 开启缓存
@SpringBootApplication
@EnableCaching
<<2>> 注解操作 redis
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController
public class RedisAnnotationController {
@Cacheable(value = "spring:cache", key = "#name")
@RequestMapping("/setrs")
public String setRedis(String name, String value) {
if (!StringUtils.hasLength(name) || !StringUtils.hasLength(value))
{
return "请先输⼊ name 和 value";
}
return value;
}
@RequestMapping("/uprs")
@CachePut(value = "spring:cache", key = "#name")
public String updateRedis(String name, String value) {
if (!StringUtils.hasLength(name) || !StringUtils.hasLength(value))
{
return "请先输⼊ name 和 value";
}
return value;
}
@RequestMapping("/delrs")
@CacheEvict(value = "spring:cache", key = "#name")
public String delUser(String name) {
return "delete success";
}
}
MyBatis查询数据库
MyBatis 是什么?
MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具。
MyBatis 是⼀款优秀的持久层框架,它⽀持⾃定义 SQL、存储过程以及⾼级映射。MyBatis 去除了⼏ 乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置 和映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。
- JDBC 的操作流程:
- 创建数据库连接池 DataSource
- 通过 DataSource 获取数据库连接 Connection
- 编写要执⾏带 ? 占位符的 SQL 语句
- 通过 Connection 及 SQL 创建操作命令对象 Statement
- 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值 6. 使⽤ Statement 执⾏ SQL 语句
- 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
- 处理结果集
- 释放资源
对于 JDBC 来说,整个操作⾮常的繁琐,我们不但要拼接每⼀个参 数,⽽且还要按照模板代码的⽅式,⼀步步的操作数据库,并且在每次操作完,还要⼿动关闭连接等, ⽽所有的这些操作步骤都需要在每个⽅法中重复书写。
MyBatis 在整个框架中的定位,框架交互流程图:
MyBatis 也是⼀个 ORM 框架,ORM(Object Relational Mapping),即对象关系映射。在⾯向 >对象编程语⾔中,将关系型数据库中的数据与对象建⽴起映射关系,进⽽⾃动的完成数据与对象 >的互相转换:
- 将输⼊数据(即传⼊对象)+SQL 映射成原⽣ SQL
- 将结果集映射为返回对象,即输出对象
ORM 把数据库映射为对象:
数据库表(table)–> 类(class)
记录(record,⾏数据)–> 对象(object)
字段(field) --> 对象的属性(attribute)
⼀般的 ORM 框架,会将数据库模型的每张表都映射为⼀个 Java 类。也就是说使⽤ MyBatis 可以像操作对象⼀样来操作数据库中的表,可以实现对象和数据库表之间 的转换。
MyBatis的基本使用
创建数据库和表
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使⽤数据数据 use mycnblog;
-- 创建表[⽤户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
-- 创建⽂章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
)default charset 'utf8mb4';
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime datetime default now(),
updatetime datetime default now(),
uid int
)default charset 'utf8mb4';
-- 添加⼀个⽤户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1)
;
-- ⽂章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正⽂',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://ww w.baidu.com',1);
添加MyBatis框架⽀持
- ⽼项⽬添加MyBatis
添加框架⽀持:
<!-- 添加 MyBatis 框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- 添加 MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>runtime</scope>
</dependency>
使⽤EditStarters插件快速添加:
- 新项⽬添加MyBatis
新项⽬创建 Spring Boot 项⽬的时候添加引⽤就可以了。
配置连接字符串和MyBatis
- 配置连接字符串
在application.yml 添加如下内容:
如果使⽤ mysql-connector-java 是 5.x 之前的使⽤的是“com.mysql.jdbc.Driver”,如果是⼤于 5.x 使⽤的是“com.mysql.cj.jdbc.Driver”
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL =false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
- 配置 MyBatis 中的 XML 路径
# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件 mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
添加业务代码
后端开发的工程思路:
- 添加实体类
import lombok.Data;
import java.util.Date;
@Data
public class User {
private Integer id;
private String username;
private String password;
private String photo;
private Date createTime;
private Date updateTime;
}
- 添加 mapper 接⼝
import org.apache.ibatis.annotations.Mapper; import java.util.List;
@Mapper
public interface UserMapper {
public List<User> getAll();
}
- 添加 UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati s.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
</mapper>
UserMapper.xml 查询所有⽤户的具体实现 SQL:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati s.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="getAll" resultType="com.example.demo.model.User">
select * from userinfo
</select>
</mapper>
<mapper>标签:需要指定 namespace 属性,表示命名空间,值为 mapper 接⼝的全限定
名,包括全包名.类名。
<select>查询标签:是⽤来执⾏数据库的查询操作的:
id:是和 Interface(接⼝)中定义的⽅法名称⼀样的,表示对接⼝的具体实现⽅法。
resultType:是返回的数据类型,也就是开头我们定义的实体类。
- 添加 Service
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public List<User> getAll() {
return userMapper.getAll();
}
}
- 添加 Controller
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/u")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/getall")
public List<User> getAll(){
return userService.getAll();
}
}
- 使⽤ postman 测试
增、删、改操作
增加操作
controller 实现代码:
@RequestMapping(value = "/add",method = RequestMethod.POST) public Integer add(@RequestBody User user){
return userService.getAdd(user);
}
mapper interface:
Integer add(User user);
mapper.xml
<insert id="add">
insert into userinfo(username,password,photo,state)
values(#{username},#{password},#{photo},1)
</insert>
Postman 添加访问:
默认情况下返回的是受影响的⾏数。
{"username":"mysql","password":"mysql","photo":"img.png"}
返回⾃增 id
controller 实现代码:
@RequestMapping(value = "/add2", method = RequestMethod.POST) public Integer add2(@RequestBody User user) {
userService.getAdd2(user);
return user.getId();
}
mapper 接⼝:
@Mapper
public interface UserMapper {
// 添加,返回⾃增id
void add2(User user);
}
mapper.xml 实现如下:
<!-- 返回⾃增id -->
<insert id="add2" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username,password,photo,state)
values(#{username},#{password},#{photo},1)
</insert>
- useGeneratedKeys:这会令 MyBatis 使⽤ JDBC 的 getGeneratedKeys ⽅法来取出由数据 库内部⽣成的主键(⽐如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的⾃动 递增字段),默认值:false。
- keyColumn:设置⽣成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列 不是表中的第⼀列的时候,是必须设置的。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性 名称。
- keyProperty:指定能够唯⼀识别对象的属性,MyBatis 会使⽤ getGeneratedKeys 的返回 值或 insert 语句的 selectKey ⼦元素设置它的值,默认值:未设置(unset)。如果⽣成列 不⽌⼀个,可以⽤逗号分隔多个属性名称。
postman 返回结果:
修改⽤户操作
controller:
/**
* 修改操作
*
* @param id
* @param name
* @return
*/
@RequestMapping("/update")
public Integer update(Integer id, String name) {
return userService.update(id, name);
}
mapper.xml 实现代码:
<update id="update">
update userinfo set username=#{name} where id=#{id}
</update>
删除⽤户操作
<delete id="delById" parameterType="java.lang.Integer">
delete from userinfo where id=#{id}
</delete>
查询操作详解
参数占位符 #{} 和 ${}
- #{}:预编译处理。
- ${}:字符直接替换。
- 预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使⽤ PreparedStatement 的 set ⽅法来赋值。直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
- 使⽤ ${sort} 可以实现排序查询,⽽使⽤ #{sort} 就不能实现排序查询了,因为当使⽤ #{sort} 查询时, 如果传递的值为 String 则会加单引号,就会导致 sql 错误。
SQL 注⼊问题
<select id="isLogin" resultType="com.example.demo.model.User">
select * from userinfo where username='${name}' and password='${pwd}' </select>
sql 注⼊代码:“’ or 1='1”
单表查询
⽤于查询的字段,尽量使⽤ #{} 预查询的⽅式。
Controller 实现代码如下:
@RequestMapping("/getuser")
public User getUserById(Integer id) {
return userService.getUserById(id);
}
Mapper.xml 实现代码如下:
<select id="getUserById" resultType="com.example.demo.model.User">
select * from userinfo where id=#{id}
</select>
like 查询
like 使⽤ #{} 报错,相当于: select * from userinfo where username like ‘%‘username’%’。
<select id="findUserByName2" resultType="com.example.demo.model.User">
select * from userinfo where username like '%#{username}%';
</select>
使⽤ mysql 的内置函数 concat() 来处理:
<select id="findUserByName3" resultType="com.example.demo.model.User">
select * from userinfo where username like concat('%',#{usernam e},'%');
</select>
多表查询
返回值类型
如果是增、删、改返回搜影响的⾏数,那么在 mapper.xml 中是可以不设置返回的类型的。
查询不设置返回类型会报错:
controller 代码:
@RequestMapping("/getname")
public String getNameById(Integer id) {
return userService.getNameById(id);
}
mapper.xml 实现代码:
<select id="getNameById">
select username from userinfo where id=#{id}
</select>
访问接⼝执⾏结果显示运⾏了⼀个查询但没有找到结果映射:
查询标签来说⾄少需要两个属性:
id 属性:⽤于标识实现接⼝中的那个⽅法;
结果映射属性:结果映射有两种实现标签: 和 。
resultType
绝⼤数查询场景可以使⽤ resultType 进⾏返回,使⽤⽅便,直接定义到某个实体类即可。
<select id="getNameById" resultType="java.lang.String">
select username from userinfo where id=#{id}
</select>
resultMap
字段名称和程序中的属性名不同的情况,可使⽤ resultMap 配置映射;
⼀对⼀和⼀对多关系可以使⽤ resultMap 映射并查询数据。
- 字段名和属性名不同的情况
程序中的属性:
mapper.xml 代码:
<select id="getUserById" resultType="com.example.demo.model.User">
select * from userinfo where id=#{id}
</select>
使用resultType查询的结果:
使用resultMap查询:
mapper.xml:
<resultMap id="BaseMap" type="com.example.demo.model.User">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="pwd"></result>
</resultMap>
<select id="getUserById" resultMap="com.example.demo.mapper.UserMapper.Base Map">
select * from userinfo where id=#{id}
</select>
查询结果:
多表查询
在多表查询时,如果使⽤ resultType 标签,在⼀个类中包含了另⼀个对象是查询不出来被包含的对象的
实体类:
@Data
public class ArticleInfo {
private Integer id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private Integer rcount;
// 包含了 userinfo 对象
private UserInfo user;
}
程序的执⾏结果:
⼀对⼀的表映射
⼀对⼀映射要使⽤ 标签,具体实现(⼀篇⽂章只对应⼀个作者):
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id property="id" column="id"></id>
<result property="title" column="title"></result>
<result property="content" column="content"></result>
<result property="createtime" column="createtime"></result>
<result property="updatetime" column="updatetime"></result>
<result property="uid" column="uid"></result>
<result property="rcount" column="rcount"></result>
<result property="state" column="state"></result>
<association property="user"
resultMap="com.example.demo.mapper.UserMapper.BaseMap"
columnPrefix="u_">
</association>
</resultMap>
<select id="getAll" resultMap="BaseMap">
select a.*,u.username u_username from articleinfo a
left join userinfo u on a.uid=u.id
</select>
8. Spring Session 持久化
6.7 SpringBoot 统一功能处理
maven包下载失败问题, 更换国内镜像
6.5 SpringBoot 项目发布
打包 Spring Boot 项目
使⽤ Maven 打包 Spring Boot 项⽬。
在打包项⽬的时候,⼀定要检查,确保数据库连接的是远程服务器的 MySQL,确保密码正确。
准备好线上环境
- 确认服务器已安装好 Java 环境
- 确保服务器有可⽤的 MySQL(MySQL 服务端已安装好)
上传 Jar 包⾄服务器
随⽤任意 ftp ⼯具,将打包好的 Jar 包上传⾄服务器,⽐如 xshell/xftp/finalshell 都可以。
启动项目
使⽤以下命令启动 Spring Boot 项⽬:
nohup java -jar xxx.jar &
停⽌项目
- 查询出运⾏的 Spring Boot 的进程
ps -ef|grep java
- 将 Spring Boot 的进程结束掉
kill -9 进程ID