Spring 是一款主流的 Java EE 轻量级开源框架,用于简化 Java 企业级应用的开发难度和开发周期。Spring 框架特点如下:
组件就是可以复用的 Java 对象,比如控制层组件 XxxController、业务层组件 XxxService、持久层组件 XxxDao。由 IoC 容器管理的组件对象称为 Bean。
Spring 通过 IoC 容器读取配置文件来管理所有 Bean 的实例化(IoC 功能),以及控制 Bean 之间的依赖关系(DI 功能)。
对于配置文件的配置方式,目前最常用的配置方式是注解 + 配置类方式。
IoC 是控制反转 Inversion of Control 的简写,用于 Bean 的实例化。IoC 容器读取配置文件,通过反射完成 Bean 的实例化。
DI 是依赖注入 Dependency Injection 的简写,用于实例化对象的依赖注入。 实例化后的对象所依赖的其它对象,通过 XML 或者注解配置方式进行注入,实现对象之间的解耦。常见的注入方式有两种:
主要实现类 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 |
AnnotationContigApplicationContext | 用于读取 Java 配置类(Java 类也可以做配置文件)创建 IOC 容器对象 |
基于注解方式管理 Bean 主要有以下几步:
以下注解可以直接标注在 Java 类上,将它们定义成 Bean:
注解 | 说明 |
---|---|
@Component | 将该类定义为 Bean,可以作用在应用的任何层次,使用时将该注解标注在相应类上 |
@Repository | 作用在数据访问层(Dao 层),将该层的类定义为 Bean,其功能与 @Component 相同 |
@Service | 作用在业务层(Service 层),将该层的类定义为 Bean,其功能与 @Component 相同 |
@Controller | 作用在控制层(Controller 层),将该层的类定义为 Bean,其功能与 @Component 相同 |
注解 | 说明 |
---|---|
@PostConstruct | 执行顺序:Constructor(构造方法)-> @Autowired(依赖注入)-> @PostConstruct(注解方法) |
@PreDestroy | 执行顺序:@PreDestroy(注解方法)-> IoC 容器销毁 Bean |
@Component
public class JavaBean {
@PostConstruct
public void init(){
// 初始化逻辑
System.out.println("JavaBean init");
}
@PreDestroy
public void destroy(){
// 释放资源逻辑
System.out.println("JavaBean destroy");
}
}
@Scope 注解指定 Bean 的作用域范围。
属性 | 含义 | 创建对象时间 |
---|---|---|
singleton(默认) | Bean 在 IOC 容器中为单例 | IOC 容器初始化时 |
prototype | Bean 在 IOC 容器中为多例 | 获取 Bean 时 |
单独使用 @Autowired 注解,默认根据类型装配。
创建 UserController 类:
@Controller
public class UserController {
@Autowired
private UserService userService;
public void addUser() {
userService.addUser();
}
}
创建 UserService 接口,包含抽象方法 addUser():
public interface UserService {
void addUser();
}
创建 UserService 接口的实现类 UserServiceImpl:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser() {
userDao.addUser();
}
}
创建 UserDao 接口,包含抽象方法 addUser():
public interface UserDao {
void addUser();
}
创建 UserDao 接口的实现类 UserDaoImpl:
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
}
}
如果此时有另一个实现类 UserRedisDaoImpl 实现了 UserDao 接口,此时 UserDao 有不止一个 Bean,所以不能通过类型注入,而要根据名称注入:
@Service
public class UserServiceImpl implements UserService {
// @Qualifier 指定 Bean 的名字
@Autowired
@Qualifier("userDaoImpl")
private UserDao userDao;
@Override
public void addUser() {
userDao.addUser();
}
}
@Autowired | @Resource | |
---|---|---|
注解来源 | Spring | JDK 扩展包 |
注入方式 | @Autowired 默认根据类型注入,如果想根据名称注入,需要配合 @Qualifier 一起用 | @Resource 默认根据名称注入,通过名称找不到会通过类型注入 |
注解作用位置 | 属性上、setter方法上、构造器上、构造器形参上 | 属性上、setter 方法上 |
创建 UserController 类:
@Controller("myUserController")
public class UserController {
@Resource(name = "myUserService")
private UserService userService;
public void addUser() {
userService.addUser();
}
}
创建 UserService 接口,包含抽象方法 addUser():
public interface UserService {
void addUser();
}
创建 UserService 接口的实现类 UserServiceImpl:
@Service(value="myUserService")
public class UserServiceImpl implements UserService {
// 未指定名称时,默认使用属性名字 myUserDao
@Resource
private UserDao myUserDao;
@Override
public void addUser() {
myUserDao.addUser();
}
}
创建 UserDao 接口,包含抽象方法 addUser():
public interface UserDao {
void addUser();
}
创建 UserDao 接口的实现类 UserDaoImpl:
@Repository("myUserDao")
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
System.out.println("Dao执行成功");
}
}
配置文件 db.properties:
druid.url=xxx
druid.driver=xxx
druid.username=root
druid.password=root
以配置类 DruidConfig 为例:
@Configuration
@PropertySource("classpath:db.properties")
public class DruidConfig {
@Value("${druid.username}")
private String username;
}
@Configuration 注解标识的类为注解类。注解类可以用于包扫描注解配置、引用外部配置文件、声明第三方依赖 Bean
在 com/atguigu/spring 包下创建一个 config 包,在 config 包下创建配置类 SpringConfig:
@Configuration
@ComponentScan("com.atguigu.spring")
@PropertySource(value = "classpath:db.properties")
public class SpringConfig {
@Value("${druid.url}")
private String url;
@Value("${druid.driver}")
private String driver;
@Value("${druid.username}")
private String username;
@Value("${druid.password}")
private String password;
// 方法的名字为第三方 Bean 的 id
// 方法的返回值为 Bean 组件的类型
@Bean
public DruidDataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setDriverClassName(driver);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
// jdbcTemplate 组件引用 dataSource 组件
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
计算器接口 Calculator,包含加减两个抽象方法:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
}
创建接口 Calculator 带日志功能的实现类 CalculatorLogImpl:
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); // 核心操作前的日志
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result); // 核心操作后的日志
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
}
现有代码缺陷:附加的日志功能对核心业务功能有干扰,且日志功能分散在各个业务功能方法中,代码重复且不利于统一维护。
因此提出了代理模式,它的原理是通过提供一个代理类,在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用,让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。
创建静态代理类 CalculatorStaticProxy 实现 Calculator 接口:
public class CalculatorStaticProxy implements Calculator {
private Calculator calculator;
public CalculatorStaticProxy(Calculator calculator) {
this.calculator = calculator; // 依赖目标接口对象
}
@Override
public int add(int i, int j) {
// 增强日志功能由代理类中的代理方法来实现
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标接口对象 calculator 来实现核心业务逻辑
int addResult = calculator.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
}
静态代理的缺点:
动态代理分为 JDK 动态代理和 CGLib 动态代理。
JDK 动态代理 | CGLib 动态代理 | |
---|---|---|
来源 | JDK | 第三方工具库 CGLib |
接口要求 | 代理类和目标类必须实现同一个接口 | 目标类无接口时只能使用 CGLib 动态代理 |
生成代理类 | 在 com.sun.proxy 包下 | 继承目标类,和目标类在相同的包下 |
实现 | 目标对象和代理对象实现同样的接口 | 继承目标类并创建它的子类,在子类中重写父类的方法, 实现方法增强 |
接口和其实现类:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
}
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
}
生产代理对象的工厂类 ProxyFactory:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target; // 目标对象
}
public Object getProxy() {
// 参数 1:加载动态生成的代理类的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
// 参数 2:目标对象实现的所有接口
Class<?>[] interfaces = target.getClass().getInterfaces();
// 参数 3:设置代理对象实现目标对象方法的过程
InvocationHandler invocationHandler = new InvocationHandler() {
/**
* invoke() 代理类中重写接口中的抽象方法
* @param proxy 代理对象
* @param method 代理对象需要实现的方法,即需要重写的方法
* @param args 方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
// 方法调用前
System.out.println("[日志] " + method.getName() + ",参数:" + Arrays.toString(args));
// 调用目标方法
result = method.invoke(target, args);
// 方法调用后
System.out.println("[日志] " + method.getName() + ",结果:" + result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("[日志] " + method.getName() + ",异常:" + e.getMessage());
} finally {
System.out.println("[日志] " + method.getName() + ",方法执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
测试:
public class TestCalculator {
public static void main(String[] args) {
// 创建代理对象(动态):同一个代理对象可以代理多个被代理对象
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
// 返回代理对象
Calculator calculator = (Calculator) proxyFactory.getProxy();
calculator.add(1,2);
}
}
目标对象不需要实现接口,使用 CGLib 代理。引入 CGLib 的依赖:
<dependencies>
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.3.0version>
dependency>
dependencies>
目标对象:
public class CalculatorDao {
public void calculate(){
System.out.println("计算,使用CGLib代理");
}
}
生产代理对象的工厂类 ProxyFactory:
public class ProxyFactory implements MethodInterceptor {
private Object target;
public ProxyFactory(Object target) {
this.target = target; // 依赖目标对象
}
public Object getProxyInstance(){
Enhancer enhancer = new Enhancer(); // 创建工具类
enhancer.setSuperclass(User.class); // 设置 enhancer 的父类
enhancer.setCallback(new MyProxy()); // 设置 enhancer 的回调对象
return enhancer.create();
}
/**
* @param o cglib 生成的代理对象
* @param method 被代理对象的方法
* @param objects 传入方法的参数
* @param methodProxy 代理的方法
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLib代理开始");
Object returnVal = method.invoke(target, objects);
System.out.println("CGLib代理结束");
return returnVal ;
}
}
测试:
public class TestCalculator {
public static void main(String[] args) {
// 创建代理对象(动态):同一个代理对象可以代理多个被代理对象
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorDao());
// 返回代理对象
CalculatorDao proxyInstance = (CalculatorDao) proxyFactory.getProxy();
proxyInstance.calculate();
}
}
AOP (Aspect Oriented Programming)是一种编程思想,是面向对象编程的延续:面向切面编程。它通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。
七个相关术语如下:
通知(增强):想要增强的功能,比如安全,事务,日志等。每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。通知方法有以下几种:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>6.0.2version>
dependency>
Calculator 接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
CalculatorImpl 实现类:
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
创建切面类 LogAspect,@Aspect 标识该类为切面类:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.atguigu.spring6.aop.annotation.*.*(..))")
public void pointCut(){}
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:" + methodName + ",参数:" + args);
}
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:" + methodName + ",结果:" + result);
}
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:" + methodName + ",异常:" + ex);
}
@After("pointCut()")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:" + methodName);
}
@Around("pointCut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("环绕通知-->目标方法执行之前");
result = joinPoint.proceed();
System.out.println("环绕通知-->目标方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标方法出现异常时");
} finally {
System.out.println("环绕通知-->目标方法执行完毕");
}
return result;
}
}
切入点表达式如下:
@EnableAspectJAutoProxy 注解开启 @AspectJ 注解:
@Configuration
@ComponentScan("com.atguigu.spring6")
@EnableAspectJAutoProxy
public class AopConfig(){
}
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。使用 @Order 注解可以控制切面的优先级:@Order(较小的数):优先级高;@Order(较大的数):优先级低。
优先级高的前置先执行,后置后执行。
@Aspect
@Component
public class MethodExecutionTimeAspect() {
@Pointcut("execution(* com.atguigu.spring.xxx)")
public void pointCut() {
}
@Around("pointCut")
public object timeAroundMethod(ProceedingJoinPoint pjp) {
Object obj = null;
Object[] args = pjp.getArgs();
long start = System.currentTimeMillis();
try {
obj = pjp.proceed(args);
}catch (Throwable e) {
log.error("aop error", e);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
Spring 的事务其实就是数据库对事务的支持,Spring 事务支持编程式事务管理和声明式事务管理两种方式:
模拟用户购买图书的场景,用户可以查询图书价格,买图书后图书表库存变化,并且用户余额变化。
在 DataGrip 中创建用户表 t_user 和图书表 t_book:
use `spring`;
CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
配置类需要添加 @EnableTransactionManagement 注解开启事务注解的支持。
在配置类中将 Druid 连接池 DataSource、JDBC 封装类 JdbcTemplate 以及事务管理器 DataSourceTransactionManager 加入 IoC 容器。
@Configuration
@ComponentScan("com.atguigu.spring.tx")
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("123");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
创建 BookController 类:
@Controller
public class BookController {
@Autowired
private BookService bookService;
public void buyBook(Integer bookId, Integer userId){
bookService.buyBook(bookId, userId);
}
}
创建接口 BookService:
public interface BookService {
void buyBook(Integer bookId, Integer userId);
}
创建 BookService 的实现类 BookServiceImpl:
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
Integer price = bookDao.getPriceByBookId(bookId);
bookDao.updateStock(bookId);
bookDao.updateBalance(userId, price);
}
}
创建接口 BookDao:
public interface BookDao {
Integer getPriceByBookId(Integer bookId);
void updateStock(Integer bookId);
void updateBalance(Integer userId, Integer price);
}
创建 BookDao 的实现类 BookDaoImpl:
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
}
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}
@Override
public void updateBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql, price, userId);
}
}
处理事务一般在 Service 层处理。在 BookServiceImpl 类上添加注解 @Transactional:
@Transactional
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
Integer price = bookDao.getPriceByBookId(bookId);
bookDao.updateStock(bookId);
bookDao.updateBalance(userId, price);
}
}
假设 id 为 1 的用户,购买 id 为 1 的图书,用户余额为 50,而图书价格为 80。购买图书之后,用户的余额为 -30,数据库中余额字段设置了无符号,因此无法将 -30 插入到余额字段。
public class TransactionManagerTest {
@Test
public void testTxAllAnnotation(){
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookController bookController = context.getBean("bookController", BookController.class);
bookController.buyBook(1, 1);
}
}
此时执行 sql 语句会抛出异常:
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance = balance - ? where user_id = ?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`spring`.`t_user`.`balance` - 80)'
由于我们使用了 Spring 的声明式事务,更新库存和更新余额都没有执行,满足实际生产情况。
类中具体方法上的事务注解属性会覆盖类上的事务注解属性。
对一个查询操作来说,如果我们把它设置成只读,数据库就能够针对查询操作来进行优化。注意注解为只读事务的方法只能进行查询,不能增删改。
@Transactional(readOnly = true)
事务在执行过程中,有可能遇到 Java 程序或 MySQL 数据库超时或网络连接超时,从而长时间占用数据库资源。此时程序应该被回滚,把资源让出来,让其他正常程序可以执行。设置的超时时间单位为秒:
@Transactional(timeout = 3)
在一般情况下,如果发生运行时异常,事务才会回滚;如果发生 IO 异常,事务不会回滚。所以,我们希望发生 IO 异常时事务也进行回滚,可以使用 rollbackFor 属性。
@Transactional(rollbackFor = Exception.class)
@Transactional(noRollbackFor = IOException.class)
@Transactional(isolation = Isolation.DEFAULT)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
在 Service 类中有 a 方法和 b 方法,两个方法上都有事务,当 a 方法执行过程中调用了 b 方法,事务是如何传递的?合并到一个事务里还是开启一个新的事务?这就是事务传播行为。
@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
在开发中,我们经常遇到参数校验的需求,比如用户注册的时候,要校验用户名不能为空、用户名长度不超过20个字符、手机号是合法的手机号格式等等。
Spring Validation 允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。在 Spring 中有多种校验的方式:通过 Validator 接口实现;Bean Validation 注解实现;基于方法实现校验;自定义校验。
Spring 默认有一个实现类 LocalValidatorFactoryBean,它实现了上面 Bean Validation 中的接口,并且也实现了 Validator 接口。
@Configuration
@ComponentScan("com.sunyu.spring6.validation2")
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
注解 | 规则 |
---|---|
@NotNull | 限制必须不为 null |
@NotEmpty | 只作用于字符串类型,字符串不为空,并且长度不为 0 |
@NotBlank | 只作用于字符串类型,字符串不为空,并且 trim() 后不为空串 |
@DecimalMax(value) / @Max(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) / @Min(value) | 限制必须为一个不小于指定值的数字 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在 min 到 max 之间 |
验证注解的元素值是 Email |
public class User {
@NotNull
private String name;
@Min(0)
@Max(120)
private int age;
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;}
}
(1)使用 jakarta.validation.Validator 校验
import jakarta.validation.Validator;
@Service
public class MyValidation1 {
@Autowired
private Validator validator;
public boolean validatorByUser1(User user){
Set<ConstraintViolation<User>> sets = validator.validate(user);
return sets.isEmpty();
}
}
(2)使用 org.springframework.validation.Validator 校验
import org.springframework.validation.Validator;
@Service
public class MyValidation2 {
@Autowired
private Validator validator;
public boolean validatorByUser2(User user) {
BindException bindException = new BindException(user, user.getName());
validator.validate(user, bindException);
return bindException.hasErrors();
}
}
public class TestMethod2 {
@Test
public void testMyValidation1() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation1 validation1 = context.getBean(MyValidation1.class);
User user = new User();
user.setName("Tom");
boolean message = validation1.validatorByUser1(user);
System.out.println(message); // 输出 true 即为校验通过
}
@Test
public void testMyValidation2() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation2 validation2 = context.getBean(MyValidation2.class);
User user = new User();
user.setName("Tom");
user.setAge(-1);
boolean message= validation2.validatorByUser2(user);
System.out.println(message); // 输出 true 为校验不通过,有错误
}
}
@Configuration
@ComponentScan("com.sunyu.spring6.validation3")
public class ValidationConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
public class User {
@NotNull
private String name;
@Min(0)
@Max(120)
private int age;
@Pattern(regexp = "^1(3|4|5|7|8)\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;
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;}
public String getPhone() {return phone;}
public void setPhone(String phone) {this.phone = phone;}
}
@Service
@Validated
public class MyService {
public String testParams(@NotNull @Valid User user) {
return user.toString();
}
}
public class TestMethod3 {
@Test
public void test1() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyService myService = context.getBean(MyService.class);
User user = new User();
user.setName("Tom");
user.setAge(6);
user.setPhone("13560005555");
myService.testParams(user); // 没有报错说明校验通过
}
}
我们自定义了一个校验注解 @MyNotBlank:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {BlankValidator.class})
public @interface MyNotBlank {
String message() default "不能包含空格"; // 默认错误消息
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
MyNotBlank[] value();
}
}
public class BlankValidator implements ConstraintValidator<MyNotBlank, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null && value.contains(" ")) {
// 自定义提示信息
String defaultMessage = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultMessage);
context.disableDefaultConstraintViolation(); // 禁用默认提示信息
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation(); // 设置提示语
return false;
}
return true;
}
}
public class User {
@MyNotBlank // 应用我们的自定义注解
private String phone;
public String getPhone() {return phone;}
public void setPhone(String phone) {this.phone = phone;}
}
public class TestMethod4 {
@Test
public void test1() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyService1 myService = context.getBean(MyService.class);
User user = new User();
user.setPhone(" 13560002222 ");
myService.testParams(user);
}
}
SpringBoot 官方的启动器都是以 spring-boot-starter-XXX 命名:
启动器 | 功能 |
---|---|
spring-boot-starter-web | 使用 SpringMVC 构建 web 工程,默认使用 Tomcat 容器 |
spring-boot-starter-actuator | 提供生产环境特性,能监控管理应用 |
spring-boot-starter-json | 提供对 JSON 的读写支持 |
spring-boot-starter-logging | 日志启动器,默认使用 Logback 日志 |
当前 jar 包内部的配置文件 -> 当前 jar 包内部的 profile 多环境配置文件 -> 引用外部 jar 包的配置文件 -> 引用外部 jar 包的 profile 多环境配置文件
@Configuration 注解标识的类为配置类。配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的。其 proxyBeanMethods 属性表示代理容器的方法:
满足 Conditional 指定条件,则进行组件注入。
@ImportResource(“classpath:XXX.xml”) 可以导入外部 xml 配置文件。
@Value 与 @ConfigurationProperties 都作用在实体类中,用于获取配置文件中的属性并绑定到实体类。
@Value 注解只有一个 value 属性,支持字面量,标识在 setter 属性上或者方法上
@ConfigurationProperties 注解有四个属性:
@ConfigurationProperties 不支持字面量,标识在类和方法上
@EnableConfigurationProperties(XXX.class)标识在配置类上,实现 XXX 实体类和 properties 文件的配置绑定,并将 XXX 组件自动注册到容器中,一般与 @ConfigurationProperties 搭配使用:
# application.properties
mycar.brand=BYD
mycar.price=100000
@ConfigurationProperties(prefix = "mycar") // 绑定配置文件中前缀为 mycar 的配置
public class Car {
private String brand;
private Integer price;
}
@EnableConfigurationProperties(Car.class) // 配置类 MyConfig
public class CarConfig {}
@SpringBootApplication 有以下三个注解组成:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
选择开发中用到的配置依赖包,也可以不选择,在用到时向 pom.xml 里面添加对应的依赖:
创建模块 boot-01-helloworld,在其 pom.xml 文件中引入父工程及依赖:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
project>
在 com/sunyu/boot 下默认创建了主程序,主程序是所有启动的入口:
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
}
}
使用 Lombok 插件简化 JavaBean 开发:
@AllArgsConstructor // 自动生成全参构造器
@NoArgsConstructor // 自动生成无参构造器
@Data // 自动生成已有属性的 Getter、Setter 方法,toString 方法
@Component
public class Car {
private String brand;
private Integer price;
}
在 com/sunyu/boot/controller 下创建 HelloController.java:
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello, Spring Boot 2!";
}
}
启动运行主程序,在浏览器网址输入 http://localhost:8080/hello 即可看见我们写的 Hello, Spring Boot 2!
SpringBoot 的配置文件有 properties 和 yaml 格式,当我们配置数据时通常选择使用 yaml 配置文件。yaml 格式中,相同层级的元素左对齐,key 和 value 之间用 [冒号 + 空格] 隔开:
user:
name: Tom
password: 123
k:
k1: v1
k2: v2
k3: v3
k:
- v1
- v2
- v3
请求进来,先去找 Controller 看能不能处理,Controller 不能处理的所有请求都交给静态资源处理器。只要静态资源放在类路径 recources/static 下,通过 localhost:8080/ + 静态资源名即可访问。
例如欢迎页 index.html 就可以放在 recources/static 下。
@RestController 的作用等同于 @Controller + @ResponseBody。@ResponseBody 表示控制器方法的返回值直接以指定的格式写入 Http response body中,而不是解析为跳转路径。
@PathVariable 注解可以使 URL 中的占位符绑定到控制器方法的入参中。一般与 @GetMapping 一起使用:
<body>
<ul>
<a href="car/3/owner/Tom">测试a>
ul>
body>
@RestController
public class ParameterTestController {
// URL 中占位符参数 id 和 username 绑定到处理器方法的入参 id、username 中
@GetMapping("/car/{id}/owner/{username}")
public Map<String, Object> getCarAndUsername(@PathVariable("id") Integer id,
@PathVariable("username") String username,
@PathVariable Map<String, String> pv) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("username", username);
map.put("pv", pv);
return map;
// 浏览器中显示 {"pv":{"id":"3","username":"Tom"},"id":3,"username":"Tom"}
}
}
@RequestHeader 注解用于控制器方法获取请求头内容:
@RestController
public class ParameterTestController {
@RequestMapping("/testRequestHeader")
public Map<String, Object> getHeader(@RequestHeader("Connection") String connection,
@RequestHeader("Host") String host
) {
Map<String, Object> map = new HashMap<>();
map.put("Connection", connection);
map.put("Host", host);
return map;
// 浏览器中显示 {"Connection":"keep-alive","Host":"localhost:8080"}
}
}
@RequestParam 注解用于控制器方法获取请求参数:
<body>
<ul>
<a href="/testRequestParam?age=18&interests=basketball&interests=game">测试a><br>
ul>
body>
@RestController
public class ParameterTestController {
@GetMapping("/testRequestParam")
public Map<String, Object> getParam(@RequestParam("age") Integer age,
@RequestParam("interests") List<String> interests
) {
Map<String, Object> map = new HashMap<>();
map.put("age",age);
map.put("interests",interests);
return map;
// {"interests":["basketball","game"],"age":18}
}
}
@CookieValue 注解用于控制器方法获取 Cookie:
@RestController
public class ParameterTestController {
@GetMapping("/testCookieValue")
public Map<String, Object> getCookie(@CookieValue("Idea-8296eef3") String cookie) {
Map<String, Object> map = new HashMap<>();
map.put("Idea-8296eef3",cookieValue);
return map;
// {"Idea-8296eef3":"d93df572-e50a-4cfc-8711-ad30d3f9f25b"}
}
}
语法 | 用途 |
---|---|
${…} | 获取请求域、session 域、对象等 |
@{…} | 生成链接 |
*{…} | 获取上下文对象值 |
#{…} | 获取国际化等值 |
’ ’ | 文本值 |
package com.sunyu.admin.interceptor;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
// 控制器方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}", requestURI);
// 登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
return true;
}
// 未登录拦截住,跳转到登录页
request.setAttribute("msg", "请先登录");
request.getRequestDispatcher("/").forward(request, response);
return false;
}
// 控制器方法执行完成以后
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}", modelAndView);
}
// 页面渲染以后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}", ex);
}
}
package com.sunyu.admin.config;
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**");
}
}
package com.sunyu.admin.controller;
@Slf4j
@Controller
public class FormController {
@GetMapping("/form_layouts")
public String form_layouts() {
return "form/form_layouts";
}
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getSize(), photos.length);
if (!headerImg.isEmpty()) {
// 保存到文件服务器,OSS 服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("文件路径" + originalFilename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("文件路径" + originalFilename));
}
}
}
return "main";
}
}
要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件以使用现有机制但替换其内容。
SpringBoot 提供了一种ControlerAdvice
类,来处理控制器类抛出的所有异常。
@ControllerAdvice
配合@ExceptionHandler
实现全局异常处理@ControllerAdvice
配合@ModelAttribute
预设全局数据@ControllerAdvice
配合@InitBinder
实现对请求参数的预处理package com.sunyu.admin;
// 指定原生 Servlet 组件所在包
@ServletComponentScan(basePackages = "com.sunyu.admin")
@SpringBootApplication
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
Servlet 注入:
package com.sunyu.admin.servlet;
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666");
}
}
Filter 注入:
package com.sunyu.admin.servlet;
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Filter初始化完成");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("Filter工作");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
log.info("Filter销毁");
}
}
Listener 注入:
package com.sunyu.admin.servlet;
@Slf4j
@WebListener()
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("监听到项目销毁完成");
}
}
package com.sunyu.admin.servlet;
@Configuration
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyListener myListener = new MyListener();
return new ServletListenerRegistrationBean(myListener);
}
}
自定义配置类用 @EnableWebMvc 注解标识,以及实现 WebMvcConfigurer 接口,可以重新配置 SpringMVC 的静态资源、欢迎页等。所有功能的定制都是这些实现了 WebMvcConfigurer 接口的配置类合起来一起生效。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
修改配置项导入数据库驱动:
spring:
datasource:
url: jdbc:mysql://localhost:3306/<数据库名字>?characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
测试:
package com.sunyu.admin;
@Slf4j
@SpringBootTest
class AdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long rows = jdbcTemplate.queryForObject("select count(*) from t_user", Long.class);
log.info("记录总数:{}", rows);
}
}
在 pom.xml 中引入依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.16version>
dependency>
在配置文件 application.yml 中添加:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: zxcvbnm5237
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.sunyu.admin.* # 监控 SpringBean
filters: stat,wall # 底层开启 stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin # 监控页账户
login-password: 123 # 监控页密码
resetEnable: false
web-stat-filter: # 监控 web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat:
slow-sql-millis: 1000 # 慢查询时间(毫秒)
logSlowSql: true # 是否记录慢查询
enabled: true # stat 功能是否开启
wall:
enabled: true
config:
drop-table-allow: false # 不允许删表
两个学习文档:SpringBoot 配置 Druid,DruidDataSource 配置属性列表
在 pom.xml 中引入依赖:
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
在配置文件 application.yml 中添加:
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml # sql 映射文件位置
configuration:
map-underscore-to-camel-case: true
编写 mapper 接口:
package com.sunyu.admin.mapper;
@Mapper
public interface UserMapper {
public User getUser(Integer id);
}
编写 sql 映射文件并绑定 mapper 接口:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sunyu.admin.mapper.UserMapper">
<select id="getUser" resultType="com.sunyu.admin.bean.User">
select * from t_user where id=#{id}
select>
mapper>
在 pom.xml 中引入依赖:
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
logging.level.root=info
logging.level.com.sunyu.redis7=info
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
spring.redis.host=192.168.239.128
spring.redis.port=6379
spring.redis.password=123
spring.redis.database=0
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
配置类 RedisConfig:
package com.sunyu.redis7.config;
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.setValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
测试:
package com.sunyu.redis7;
@SpringBootTest
public class RedisTemplateTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void set() {
stringRedisTemplate.opsForValue().set("a", "1");
}
@Test
public void get() {
String value = stringRedisTemplate.opsForValue().get("a");
System.out.println(value);
}
}
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
在 src/test/java/com/sunyu/admin/AdminApplicationTests.java 测试方法下,使用 @SpringBootTest 标识:
@DisplayName("测试Junit5")
class AdminApplicationTests {
@Test
void contextLoads() {
}
}
@DisplayName 为测试类或者测试方法设置展示名称:
@DisplayName("测试Junit5")
class Junit5Tests {
@DisplayName("@DisplayName注解")
@Test
void testDisplayName() {
System.out.println(1);
}
}
@BeforeEach 表示在每个单元测试之前执行,@AfterEach 表示在每个单元测试之后执行:
@DisplayName("测试Junit5")
class Junit5Tests {
@DisplayName("测试DisplayName")
@Test
void testDisplayName(){
System.out.println(1);
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试开始");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束");
}
}
@BeforeAll 表示在所有单元测试之前执行,@AfterAll 表示在所有单元测试之后执行,两者标识的方法必须用 static 修饰:
@DisplayName("测试Junit5")
public class Junit5Test {
@DisplayName("测试DisplayName")
@Test
void testDisplayName(){
System.out.println(1);
}
@DisplayName("测试DisplayName2")
@Test
void testDisplayName2(){
System.out.println(2);
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试开始");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束");
}
@BeforeAll
static void testBeforeAll(){
System.out.println("所有测试开始...");
}
@AfterAll
static void testAfterAll(){
System.out.println("所有测试结束...");
}
}
3.4.4 @Disabled
@Disabled 表示测试类或测试方法不执行。
@Timeout 表示测试方法运行如果超过了指定时间将会返回错误:
@DisplayName("测试Junit5")
public class Junit5Test {
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
public void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
}
如果想要在自定义测试类中实现自动注入功能,需要加上 @SpringBootTest 注解:
@SpringBootTest
@DisplayName("测试Junit5")
public class Junit5Test {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void test1() {
System.out.println(jdbcTemplate);
}
}
@RepeatTest 实现重复测试:
@DisplayName("测试Junit5")
public class Junit5Test {
@RepeatedTest(3)
@Test
void test1() {
System.out.println(666);
}
}
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。断言方法都是 org.junit.jupiter.api.Assertions 的静态方法:
静态方法 | 说明 |
---|---|
assertEquals / assertNotEquals | 判断两个对象或两个原始类型是否相等 / 不相等 |
assertSame / assertNotSame | 判断两个对象引用是否指向同一对象 / 不同对象 |
assertTrue / assertFalse | 判断给定的布尔值是否为 true / false |
assertNull / assertNotNull | 判断给定的对象引用是否为 null / 是否不为 null |
assertArrayEquals | 判断两个对象或原始类型的数组是否相等 |
assertAll | 组合断言,内部全部判断断言成功才成功 |
assertThrows() | 异常断言 |
assertTimeout() | 超时断言 |
@Test
@DisplayName("assertAll")
public void assertAllTest() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
ArithmeticException.class, () -> System.out.println(1 % 0));
}
@DisplayName("测试Junit5")
public class Junit5Test {
@Test
@DisplayName("超时测试")
public void timeoutTest() {
assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(1200));
}
}
前置条件(assumptions)类似于断言,不同之处在于:不满足的断言会使得测试方法失败,而不满足的前置条件只会使测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("前置条件1")
public void assumeTest() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("前置条件2")
public void assumeThatTest() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
参数化测试 @ParameterizedTest 是 JUnit5 很重要的一个新特性,它利用注解指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
注解 | 说明 |
---|---|
@ValueSource | 为参数化测试指定入参来源,支持八大基础类以及 String 类型, Class 类型 |
@NullSource | 为参数化测试提供一个 null 的入参 |
@EnumSource | 为参数化测试提供一个枚举入参 |
@CsvFileSource | 读取指定 CSV 文件内容作为参数化测试入参 |
@MethodSource | 读取指定方法的返回值作为参数化测试入参,方法返回需要是一个流 |
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("@ValueSource测试")
public void parameterizedTest1(String string) {
System.out.println(string);
assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") // 指定读取的方法名
@DisplayName("@MethodSource测试")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot 就抽取了 Actuator 场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
最常用的是暴露所有监控信息为 HTTP,在 application.yml 中添加(为了安全起见,禁用所有的 Endpoint 然后手动开启指定的 Endpoint):
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
endpoint:
health:
show-details: always # 总是显示详细信息。可显示每个模块的状态信息
enabled: true
loggers:
enabled: true
metrics:
enabled: true
访问 http://localhost:8080/actuator/,最常用的 Endpoint 有:
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String,Object> map = new HashMap<>();
// if 条件中放业务逻辑代码
if(1 == 2){
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);
}
}
在 http://localhost:8080/actuator/health 下显示:
@Component
public class MyInfo implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example", Collections.singletonMap("key", "value"));
}
}
在 http://localhost:8080/actuator/info 下显示:
package com.sunyu.admin.actuator.endpoint;
@Component
@Endpoint(id = "myService")
public class MyEndPoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
项目环境有测试环境、生产环境,SpringBoot 的 profile 功能可以适配多环境的切换,如
# application-test.yaml
server:
port: 8000
# application-prod.yaml
server:
port: 8001
默认配置文件 application.yaml 任何时候都会加载。我们可以指定 profile 配置文件 application-{env}.yaml,env = prod 是生产环境,env = test 是测试环境。如果配置文件中出现同名配置,指定的 profile 配置文件优先。如果我们想启动指定测试环境配置文件,只需在 配置文件 application.yaml 中添加:
spring:
profiles:
active: test
此时我们的项目服务端口变为 http://localhost:8000/。
我们将项目打包为 admin-0.0.1-SNAPSHOT.jar 后,还可以使用命令行修改配置文件的任意值(命令行优先于配置文件):
此时我们的项目环境为生产环境,服务端口变为 http://localhost:8002/。
除此之外,@Profile 标识的类或方法可以实现条件装配功能,例如 @Profile(“prod”) 标识的类或方法只在生产环境中生效, @Profile(“test”) 标识的类或方法只在测试环境中生效。
MVC 是一种软件架构的思想,将软件按照模型、视图、控制器来划分:
SpringMVC 常用组件:
SpringMVC 中的视图是 View 接口,视图的作用渲染数据,将模型 Model 中的数据展示给用户。SpringMVC 的视图默认有转发视图和重定向视图。
当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被 SpringMVC 配置文件中所配置的 Thymeleaf 视图解析器解析,得到的是 ThymeleafView 视图。
当控制器方法中所设置的视图名称以 “forward:” 为前缀时,创建转发视图,此时的视图名称会将前缀去掉,剩余部分作为最终路径通过转发的方式实现跳转。
当控制器方法中所设置的视图名称以 “redirect:” 为前缀时,创建重定向视图,此时的视图名称会将前缀去掉,剩余部分作为最终路径通过重定向的方式实现跳转。
SpringMVC 的处理器拦截器类似于 Servlet 开发中的过滤器 Filter,用于对控制器进行预处理和后处理。
注意: 如果我们配置了多个拦截器。若每个拦截器的 preHandle() 都返回 true,此时多个拦截器的执行顺序和拦截器在 SpringMVC 配置文件的配置顺序有关: preHandle() 会按照配置的顺序执行,而 postHandle() 和 afterComplation() 会按照配置的反序执行。
@RequestMapping 注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。 SpringMVC 接收到指定的请求,就会寻找在映射关系中对应的控制器方法来处理这个请求。
比如以下请求的请求路径为 /test/testRequestMapping:
@Controller
@RequestMapping("/test")
public class RequestMappingController {
@RequestMapping("/testRequestMapping")
public String test(){
return "success";
}
}
value 属性指定请求的 URL 地址,是字符串数组类型。
@RequestMapping (value = {"/test1", "/test2"})
注意: ?
表示任意的单个字符,*
表示任意的 0 个或多个字符, **
表示任意的一层或多层目录,注意在使用时**
前后不能有其他字符,只能使用 /**/xxx
的方式。
method 属性指定请求类型,是数组类型,包含数组元素有:RequestMethod.GET、RequestMethod.POST、RequestMethod.PUT、RequestMethod.DELETE
@RequestMapping (value = "/test", method = {RequestMethod.GET, RequestMethod.POST})
对于处理指定请求方式的控制器方法,SpringMVC 中提供了 @RequestMapping 的派生注解:处理 get 请求的映射 --> @GetMapping;处理 post 请求的映射 --> @PostMapping;处理 put 请求的映射 --> @PutMapping;处理 delete 请求的映射 --> @DeleteMapping。
@RequestMapping (value = "/test",
method = {RequestMethod.GET, RequestMethod.POST},
params = {"username","password!=123"} ) // 请求路径必须包含 username,且 password 不能为 123
@PathVariable 注解将占位符传输的数据赋值给控制器方法的形参:
@Controller
public class RequestMappingController {
@RequestMapping(value = "/testRest/{id}/{username}")
public String test(@PathVariable("id") String id,
@PathVariable("username") String username) {
System.out.println(id + ":" + username);
return "target";
}
}
在控制器方法的形参位置,设置和请求参数同名形参:
<a th:href="@{/testParam(username='admin',password=123)}">测试a>
@Controller
public class ParamController {
@RequestMapping("/testParam")
public String test(String username, String password) {
System.out.println("username:" + username + ",password:" + password);
return "target";
}
}
若请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数。
<form th:action="@{/testParam}" method="post">
用户名:<input type="text" name = "username"><br>
密码:<input type="password" name = "password"><br>
爱好:<input type="checkbox" name = "hobby" value="a">a
<input type="checkbox" name = "hobby" value="b">b
<input type="checkbox" name = "hobby" value="c">c<br>
<input type="submit" value="测试控制器形参获取请求参数">
@Controller
public class ParamController {
@RequestMapping("/testParam")
public String test(String username, String password, String[] hobby) {
System.out.println("username:" + username + ",password:" + password + ",hobby:" + Arrays.toString(hobby));
return "target";
}
}
若请求参数名与控制器方法的形参不同名,需要在控制器上加上 @RequestParam 注解。@RequestParam 注解将请求参数和控制器方法的形参创建映射关系。
请求参数名为 user_name:
测试
控制器方法中的形参名为 username:
@Controller
public class ParamController {
@RequestMapping("/testParam")
public String test(@RequestParam("user_name") String username, String password) {
System.out.println("username:" + username + ",password:" + password);
return "target";
}
}
两种属性和用法与 @RequestParam 一样的注解 @RequestHeader 和 @CookieValue:
在控制器方法的形参位置,设置一个实体类(POJO)类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值(数据库的增删改)。
创建实体类 User:
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String sex;
// 无参、全参构造器
// Getter、Setter 方法、toString 方法
}
请求参数:
<form th:action="@{/testPojo}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
性别:<input type="radio" name="sex" value="男">男
<input type="radio" name="sex" value="女">女<br>
年龄:<input type="text" name="age"><br>
<input type="submit" value="使用POJO接受请求参数">
form>
控制器方法中的形参为实体类 User:
@Controller
public class ParamController {
@RequestMapping("/testPojo")
public String test(User user){
System.out.println(user);
return "target";
}
}
域对象主要用在 web 应用中,这个对象本身可以存储一定范围内的所有数据,通过它就能获取和存储数据。请求开始 request 域对象创建,对应响应结束 request 域对象销毁。只要调用它就可以对域内的数据进行增删改查。域对象通用方法有:
@Controller
public class ScopeController {
@RequestMapping("/testModelAndView")
public ModelAndView test() {
ModelAndView mav = new ModelAndView();
mav.addObject("testScope", "hello,ModelAndView"); // 向请求域共享数据
mav.setViewName("success"); // 设置视图名称,实现页面跳转
return mav;
}
}
@Controller
public class ScopeController {
@RequestMapping("/testModel")
public String test(Model model){
model.addAttribute("testScope", "hello,Model");
return "success";
}
}
@Controller
public class ScopeController {
@RequestMapping("/testMap")
public String test(Map<String, Object> map){
map.put("testScope", "hello,Map");
return "success";
}
}
@Controller
public class ScopeController {
@RequestMapping("/testModelMap")
public String test(ModelMap modelMap){
modelMap.addAttribute("testScope", "hello,ModelMap");
return "success";
}
}
@Controller
public class ScopeController {
@RequestMapping("/testSession")
public String test(HttpSession session){
session.setAttribute("testScope", "hello,session");
return "success";
}
}
@Controller
public class ScopeController {
@RequestMapping("/testApplication")
public String test(HttpSession session){
ServletContext application = session.getServletContext();
application.setAttribute("testScope", "hello,application");
return "success";
}
}
报文信息转换器 HttpMessageConverter 将请求报文转换为 Java 对象,或将 Java 对象转换为响应报文。
@RequestBody 标识控制器方法的形参,使当前请求的请求体就会为该形参赋值。它与第 4 章中的@RequestParam() 类似,只不过 @RequestBody 接收的是请求体里面的数据;而 @RequestParam 接收的是请求参数。
<body>
<form th:action="@{/test}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit">
form>
body>
@Controller
public class HttpController {
@RequestMapping("/test")
public String test(RequestEntity<String> requestEntity) {
System.out.println("requestHeader:" + requestEntity.getHeaders());
System.out.println("requestBody:" + requestEntity.getBody());
return "success";
}
}
@RestController 注解是一个复合注解,标识在控制器类上,就相当于为类添加了 @Controller 注解,并且为类中的每个方法添加了 @ResponseBody 注解。@ResponseBody 标识的控制器方法,该方法的返回值直接作为响应报文的响应体响应到浏览器。@ResponseBody 常用于处理 json 和 ajax。比如我们创建实体类 User,将 User 对象直接作为控制器方法的返回值返回,就会自动转换为 json 格式的字符串:
@Controller
public class HttpController {
@RequestMapping("/test")
@ResponseBody
public User test(){
return new User(1001,"Tom","123",23,"man");
// 浏览器显示:{"id":1001,"username":"Tom","password":"123","age":23,"sex":"man"}
}
}
类注解 @ControllerAdvice 常结合方法注解 @ExceptionHandler,用于捕获 Controller 中抛出的指定类型异常。
@ExceptionHandler 注解中可以添加异常类参数数组,表示需要捕获的异常,比如:
NullPointerException.class | 空指针异常 |
---|---|
ArithmeticException.class | 算数运算异常 |
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String test(Exception ex, Model model){
model.addAttribute("ex", ex);
return "error";
}
}
MyBatis 封装了 JDBC,是一个半自动的 ORM(Object Relation Mapping)框架,支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。ORM 对象-关系映射如下:
MyBatis 获取参数值的两种方式${}
和#{}
。${}
使用字符串拼接的方式拼接 sql,需要手动加单引号;#{}
使用占位符赋值的方式拼接 sql,可以自动添加单引号。
#{}
预编译的 sql 语句执行效率高,并且可以防止注入攻击,效率和安全性都优于${}
。
使用${}
和#{}
以任意名称(最好见名识意)获取参数的值。
<select id="getUserByUsername" resultType="User">
select * from t_user where username = #{username}
select>
通过 @Param 注解标识 mapper 接口中的方法参数,会将这些参数自动放在 map 集合中:
<select id="CheckLoginByParam" resultType="User">
select * from t_user where username = #{username} and password = #{password}
select>
<select id="getUserByTable" resultType="User">
select * from ${tableName}
select>
<delete id="deleteMore">
delete from t_user where id in (${ids})
delete>
在 mapper.xml 中设置两个属性:useGeneratedKeys 表示当前标签的 sql 是否使用了自增主键;keyProperty 表示自增主键对应的实体类属性:
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values (null, #{username}, #{password}, #{age}, #{sex}, #{email})
insert>
resultType 是自动映射,用于属性名和表中字段名一致的情况。
<select id="getUserById" resultType="User">
select * from t_user where id = #{id}
select>
<select id="getUserList" resultType="User">
select * from t_user
select>
<select id="getCount" resultType="Integer">
select count(id) from t_user
select>
<select id="getUserToMap" resultType="map">
select * from t_user where id = #{id}
select>
<select id="getAllUserToMap" resultType="map">
select * from t_user
select>
<select id="getUserByLike" resultType="User">
select * from t_user where username like "%"#{mohu}"%"
select>
resultMap 是自定义映射,用于字段名和属性名不一致的情况。在 DataGrip 的 mybatis 数据库下创建表 t_emp 和 t_dept:
其中数据库的字段名 emp_name 对应实体类属性 empName,二者名字不一致:
<resultMap id="empResultMap" type="Emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<result property="email" column="email"/>
resultMap>
<select id="getAllEmp" resultMap="empResultMap">
select * from t_emp
select>
需求:查询员工信息以及员工所对应的部门信息。
多个 Emp 对应一个 Dept,所以要在 Emp 中设置 Dept 的属性:
public class Emp {
// ...
private Dept dept;
// ...
}
<resultMap id="empAndDeptResultMap" type="Emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<result property="email" column="email"/>
<association property="dept" select="com.sunyu.mybatis.mapper.DeptMapper.getDept" column="did"/>
resultMap>
<select id="getEmp" resultMap="empAndDeptResultMap">
select * from t_emp where eid = #{eid}
select>
<resultMap id="EmpAndDeptResultMap" type="Dept">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
resultMap>
<select id="getDept" resultMap="EmpAndDeptResultMap">
select * from t_dept where did = #{did}
select>
需求:根据部门 id 查询该部门下所有员工信息。
一个 Dept 对应多个 Emp,在 Dept 中设置 Emp 的集合:
public class Dept {
// ...
private List<Emp> emps;
// ...
}
<resultMap id="DeptAndEmpResultMap" type="Dept">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
<collection property="emps"
select="com.sunyu.mybatis.mapper.EmpMapper.getEmps" column="did"/>
resultMap>
<select id="getDept" resultMap="DeptAndEmpResultMap">
select * from t_dept where did = #{did}
select>
<select id="getEmps" resultType="Emp">
select * from t_emp where did = #{did}
select>
动态 SQL 技术是一种根据特定条件动态拼装 sql 语句的功能,为了解决 sql 语句拼接字符串时的痛点问题。
if 标签可通过 test 属性(即传递过来的数据)的字符串表达式进行判断,若表达式的结果为 true,则标签中的内容会执行;反之标签中的内容不会执行。
where 标签和 if 标签一般结合使用:
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp
<where>
<if test="empName != null and empName !=''">
emp_name = #{empName}
if>
<if test="age != null and age !=''">
and age = #{age}
if>
<if test="sex != null and sex !=''">
and sex = #{sex}
if>
<if test="email != null and email !=''">
and email = #{email}
if>
where>
select>
常用属性:
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp
<trim prefix="where" suffixOverrides="and|or">
<if test="empName != null and empName !=''">
emp_name = #{empName} and
if>
<if test="age != null and age !=''">
age = #{age} and
if>
<if test="sex != null and sex !=''">
sex = #{sex} or
if>
<if test="email != null and email !=''">
email = #{email}
if>
trim>
select>
choose、when、otherwise 相当于 if 、else if 、else,只会执行其中一个 when 标签。
<select id="getEmpByChoose" resultType="Emp">
select * from t_emp
<where>
<choose>
<when test="empName != null and empName != ''">
emp_name = #{empName}
when>
<when test="age != null and age != ''">
age = #{age}
when>
<when test="sex != null and sex != ''">
sex = #{sex}
when>
<when test="email != null and email != ''">
email = #{email}
when>
<otherwise>
did = 1
otherwise>
choose>
where>
select>
<delete id="deleteByArray">
delete from t_emp where eid in
<foreach collection="eids" item="eid" separator="," open="(" close=")">
#{eid}
foreach>
delete>
<insert id="insertByList">
insert into t_emp values
<foreach collection="emps" item="emp" separator=",">
(null, #{emp.empName}, #{emp.age}, #{emp.sex}, #{emp.email}, null)
foreach>
insert>
sql 标签声明的字段片为公共 sql 片段,在使用的地方通过 include 标签进行引入。
<sql id="empColumns">eid, emp_name, age, sex, emailsql>
<select id="getEmpByCondition" resultType="Emp">
select <include refid="empColumns">include> from t_emp
select>
一级缓存默认开启,是 SqlSession 级别的,通过同一个 SqlSession 查询的数据会被缓存,下次查询相同的数据就会从缓存中直接获取,不会从数据库重新访问。
一级缓存失效的三种情况:
二级缓存是 SqlSessionFactory 级别,通过同一个 SqlSessionFactory 创建的 SqlSession 查询的结果会被缓存;再次执行相同的查询语句结果就会从缓存中获取
开启二级缓存的条件有:
创建 MyBatis-MBG 新模块,在 pom.xml 文件中添加依赖(dependency 中的 MySQL 与 plugin 插件中的 MySQL 驱动版本应该相同):
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.9version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.27version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.0version>
<dependencies>
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.27version>
dependency>
dependencies>
plugin>
plugins>
build>
在 src/main/resources 下创建 mybatis-config.xml:
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<typeAliases>
<package name="com.sunyu.mybatis.pojo"/>
typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
dataSource>
environment>
environments>
<mappers>
<package name="com.sunyu.mybatis.mapper"/>
mappers>
configuration>
创建 jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8
jdbc.username=root
jdbc.password=123
在 src/main/resources 下创建逆向工程配置文件,文件名必须是 generatorConfig.xml:
DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8"
userId="root"
password="zxcvbnm5237">
jdbcConnection>
<javaModelGenerator targetPackage="com.sunyu.mybatis.pojo" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
javaModelGenerator>
<sqlMapGenerator targetPackage="com.sunyu.mybatis.mapper"
targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true" />
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.sunyu.mybatis.mapper" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
javaClientGenerator>
<table tableName="t_emp" domainObjectName="Emp"/>
<table tableName="t_dept" domainObjectName="Dept"/>
context>
generatorConfiguration>
执行结果如下:
QBC(Query By Criteria)无需写 sql 语句,是一种面向对象的查询方式。它主要由 Criterion 接口和 Expression 类组成。
public class MGBTest {
@Test
public void testMBG(){
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
// 查询所有数据
List<Emp> empList = mapper.selectByExample(null);
empList.forEach(System.out::println);
/*
* 条件查询:查询名字为张三,且年龄大于 10,或 did=3 的员工集合
*/
EmpExample example = new EmpExample();
// 且逻辑用链式查询
example.createCriteria().andEmpNameEqualTo("张三").andAgeGreaterThan(10);
// 或逻辑用 example.or()
example.or().andDidEqualTo(3);
List<Emp> emps = mapper.selectByExample(example);
emps.forEach(System.out::println);
}
}