Spring Boot 中同时开启并配置 MongoDB、MySQL 和 Redis 的事务支持
在现代应用开发中，事务管理是保证数据一致性的重要手段。当我们同时使用多种数据库（如 MySQL、MongoDB 和 Redis）时，如何配置和管理它们的事务显得尤为关键。本文将详细介绍如何在 Spring Boot 项目中同时开启并配置 MongoDB、MySQL 和 Redis 的事务支持，并避免常见的 Bean 重复定义和冲突问题。1. 前置依赖与环境准备确保项目已经添加了以下依赖：Maven 依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>环境要求MySQL ：使用支持事务的存储引擎（如 InnoDB）。MongoDB ：确保 MongoDB 集群运行在副本集模式（Replica Set）。Redis ：确认 Redis 启用了事务功能。2. 配置 MySQL 的事务支持通过 DataSourceTransactionManager 管理 MySQL 的事务，并命名为唯一名称：@Configuration @EnableTransactionManagement public class MySQLConfig { @Bean(name = "myBatisTransactionManager") public PlatformTransactionManager myBatisTransactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }3. 配置 MongoDB 的事务支持MongoDB 从 4.0 版本开始支持多文档事务，前提是运行在副本集模式下。通过 MongoTransactionManager 管理 MongoDB 的事务，并命名为唯一名称：@Configuration public class MongoConfig { @Bean(name = "mongoTransactionManager") public MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory dbFactory) { return new MongoTransactionManager(dbFactory); } }4. 配置 Redis 的事务支持Redis 的事务支持基于流水线（Pipeline）和 MULTI/EXEC 指令。在 application.yml 或 application.properties 中启用 Redis 的事务支持：spring: redis: lettuce: pool: enabled: true transaction: true5. 配置 ChainedTransactionManager为了实现 MySQL、MongoDB 和 Redis 的全局事务，需要使用 ChainedTransactionManager。以下是 ChainedTransactionManager 的完整配置：@Configuration public class ChainedTransactionConfig { @Bean(name = "chainedTransactionManager") @Primary public ChainedTransactionManager chainedTransactionManager( @Qualifier("myBatisTransactionManager") PlatformTransactionManager myBatisTransactionManager, @Qualifier("mongoTransactionManager") MongoTransactionManager mongoTransactionManager) { return new ChainedTransactionManager(myBatisTransactionManager, mongoTransactionManager); } }@Primary 注解将 chainedTransactionManager 设置为默认事务管理器。6. 避免 Bean 重复定义和冲突问题在 Spring Boot 中，如果多个配置类定义了相同名称的 Bean，会导致 BeanDefinitionOverrideException 或 Bean 注入冲突。以下是避免问题的设计：方法 1：为每个 Bean 提供唯一名称确保每个事务管理器 Bean 都有唯一名称：// MySQLConfig.java @Bean(name = "myBatisTransactionManager") public PlatformTransactionManager myBatisTransactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // MongoConfig.java @Bean(name = "mongoTransactionManager") public MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory dbFactory) { return new MongoTransactionManager(dbFactory); }方法 2：使用 @Qualifier 指定 Bean在 ChainedTransactionManager 配置中显式指定 Bean 的名称：@Bean(name = "chainedTransactionManager") public ChainedTransactionManager chainedTransactionManager( @Qualifier("myBatisTransactionManager") PlatformTransactionManager myBatisTransactionManager, @Qualifier("mongoTransactionManager") MongoTransactionManager mongoTransactionManager) { return new ChainedTransactionManager(myBatisTransactionManager, mongoTransactionManager); }7. 全局事务示例通过 @Transactional 注解实现全局事务：@Service public class GlobalService { @Autowired private MySQLService mySQLService; @Autowired private MongoService mongoService; @Autowired private RedisService redisService; @Transactional(transactionManager = "chainedTransactionManager") public void executeGlobalTransaction() { // 操作 MySQL mySQLService.executeMySQLTransaction(); // 操作 MongoDB mongoService.executeMongoTransaction(); // 操作 Redis redisService.executeRedisTransaction(); } }8. 避免多个事务管理器冲突的策略在项目中定义了多个事务管理器时，Spring 可能会因为未指定具体的事务管理器而抛出 NoUniqueBeanDefinitionException。以下是解决方法：通过 @Qualifier 明确注入在使用的地方显式指定事务管理器：@Transactional(transactionManager = "chainedTransactionManager") public void executeGlobalTransaction() { // 业务逻辑 }设置默认事务管理器使用 @Primary 标记一个事务管理器为默认：@Bean(name = "chainedTransactionManager") @Primary public ChainedTransactionManager chainedTransactionManager( @Qualifier("myBatisTransactionManager") PlatformTransactionManager myBatisTransactionManager, @Qualifier("mongoTransactionManager") MongoTransactionManager mongoTransactionManager) { return new ChainedTransactionManager(myBatisTransactionManager, mongoTransactionManager); }仅使用必要的事务管理器如果某些事务管理器不参与当前业务逻辑，可以在配置中移除未使用的事务管理器 Bean。9. 注意事项MongoDB 必须使用副本集模式 ：单节点 MongoDB 不支持事务。Redis 的事务有限 ：只保证命令的顺序性，不支持回滚。MySQL 必须使用支持事务的存储引擎 ：如 InnoDB。分布式事务协调器 ：如果需要严格的一致性，可以使用分布式事务协调器（如 Atomikos、Seata）。
前言本课程和笔记来源于b站：三更草堂链接地址：https://b23.tv/rrFbpDd配套资料：https://pan.baidu.com/s/1UaRJXasdVeF6hCXsQl1nrw提取码：SpringSecurity从入门到精通课程介绍0. 简介Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro，它提供了更丰富的功能，社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多，因为相比与SpringSecurity，Shiro的上手更加的简单。一般Web应用的需要进行认证和授权。认证：验证当前访问系统的是不是本系统的用户，并且要确认具体是哪个用户授权：经过认证后判断当前用户是否有权限进行某个操作而认证和授权也是SpringSecurity作为安全框架的核心功能。1. 快速入门1.1 准备工作我们先要搭建一个简单的SpringBoot工程① 设置父工程 添加依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.0</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>② 创建启动类@SpringBootApplication public class SecurityApplication { public static void main(String[] args) { SpringApplication.run(SecurityApplication.class,args); } }③ 创建Controllerimport org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping("/hello") public String hello(){ return "hello"; } }1.2 引入SpringSecurity在SpringBoot项目中使用SpringSecurity我们只需要引入依赖即可实现入门案例。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面，默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。2. 认证2.1 登陆校验流程2.2 原理初探想要知道如何实现自己的登陆流程就必须要先知道入门案例中SpringSecurity的流程。2.2.1 SpringSecurity完整流程SpringSecurity的原理其实就是一个过滤器链，内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。图中只展示了核心过滤器，其它的非核心过滤器并没有在图中展示。UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。ExceptionTranslationFilter：处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。FilterSecurityInterceptor：负责权限校验的过滤器。我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。2.2.2 认证流程详解概念速查:Authentication接口: 它的实现类，表示当前访问系统的用户，封装了用户相关信息。AuthenticationManager接口：定义了认证Authentication的方法UserDetailsService接口：加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。UserDetails接口：提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。2.3 解决问题2.3.1 思路分析登录①自定义登录接口调用ProviderManager的方法进行认证 如果认证通过生成jwt把用户信息存入redis中②自定义UserDetailsService在这个实现类中去查询数据库校验：①定义Jwt认证过滤器获取token解析token获取其中的userid从redis中获取用户信息存入SecurityContextHolder2.3.2 准备工作①添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>② 添加Redis相关配置import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import com.alibaba.fastjson.parser.ParserConfig; import org.springframework.util.Assert; import java.nio.charset.Charset; /** * Redis使用FastJson序列化 * * @author sg */ public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class<T> clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } protected JavaType getJavaType(Class<?> clazz) { return TypeFactory.defaultInstance().constructType(clazz); } }import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean @SuppressWarnings(value = { "unchecked", "rawtypes" }) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }③ 响应类import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { /** * 状态码 */ private Integer code; /** * 提示信息，如果有错误时，前端可以获取该字段进行提示 */ private String msg; /** * 查询到的结果数据， */ private T data; public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } }④工具类import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID; /** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "sangeng"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的数据（json格式） * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的数据（json格式） * @param ttlMillis token超时时间 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("sg") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } public static void main(String[] args) throws Exception { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg"; Claims claims = parseJWT(token); System.out.println(claims); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }import java.util.*; import java.util.concurrent.TimeUnit; @SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象，Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象，Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功；false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功；false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 删除Hash中的数据 * * @param key * @param hkey */ public void delCacheMapValue(final String key, final String hkey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hkey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class WebUtils { /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }⑤实体类import java.io.Serializable; import java.util.Date; /** * 用户表(User)实体类 * * @author 三更 */ @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private static final long serialVersionUID = -40356785423868312L; /** * 主键 */ private Long id; /** * 用户名 */ private String userName; /** * 昵称 */ private String nickName; /** * 密码 */ private String password; /** * 账号状态（0正常 1停用） */ private String status; /** * 邮箱 */ private String email; /** * 手机号 */ private String phonenumber; /** * 用户性别（0男，1女，2未知） */ private String sex; /** * 头像 */ private String avatar; /** * 用户类型（0管理员，1普通用户） */ private String userType; /** * 创建人的用户id */ private Long createBy; /** * 创建时间 */ private Date createTime; /** * 更新人 */ private Long updateBy; /** * 更新时间 */ private Date updateTime; /** * 删除标志（0代表未删除，1代表已删除） */ private Integer delFlag; }2.3.3 实现2.3.3.1 数据库校验用户从之前的分析我们可以知道，我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。准备工作我们先创建一个用户表， 建表语句如下：CREATE TABLE `sys_user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名', `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称', `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码', `status` CHAR(1) DEFAULT '0' COMMENT '账号状态（0正常 1停用）', `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱', `phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号', `sex` CHAR(1) DEFAULT NULL COMMENT '用户性别（0男，1女，2未知）', `avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像', `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型（0管理员，1普通用户）', `create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id', `create_time` DATETIME DEFAULT NULL COMMENT '创建时间', `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人', `update_time` DATETIME DEFAULT NULL COMMENT '更新时间', `del_flag` INT(11) DEFAULT '0' COMMENT '删除标志（0代表未删除，1代表已删除）', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'引入MybatisPuls和mysql驱动的依赖<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>配置数据库信息spring: datasource: url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver定义Mapper接口public interface UserMapper extends BaseMapper<User> { }修改User实体类类名上加@TableName(value = "sys_user") ,id字段上加 @TableId配置Mapper扫描@SpringBootApplication @MapperScan("com.sangeng.mapper") public class SimpleSecurityApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(SimpleSecurityApplication.class); System.out.println(run); } }添加junit依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>测试MP是否能正常使用@SpringBootTest public class MapperTest { @Autowired private UserMapper userMapper; @Test public void testUserMapper(){ List<User> users = userMapper.selectList(null); System.out.println(users); } }核心代码实现创建一个类实现UserDetailsService接口，重写其中的方法。更加用户名从数据库中查询用户信息@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名查询用户信息 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); //如果查询不到数据就通过抛出异常来给出提示 if(Objects.isNull(user)){ throw new RuntimeException("用户名或密码错误"); } //TODO 根据用户查询权限信息 添加到LoginUser中 //封装成UserDetails对象返回 return new LoginUser(user); } }因为UserDetailsService方法的返回值是UserDetails类型，所以需要定义一个类，实现该接口，把用户信息封装在其中。@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }注意：如果要测试，需要往用户表中写入用户数据，并且如果你想让用户的密码是明文存储，需要在密码前加{noop}。例如这样登陆的时候就可以用sg作为用户名，1234作为密码来登陆了。2.3.3.2 密码加密存储实际项目中我们不会把密码明文存储在数据库中。默认使用的PasswordEncoder要求数据库中的密码格式为：{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中，SpringSecurity就会使用该PasswordEncoder来进行密码校验。我们可以定义一个SpringSecurity的配置类，SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。[scode type="yellow" size=""]最新版本提示WebSecurityConfigurerAdapter已过时，只需要在SecurityConfig类上加上，@EnableWebSecurity注解就行了，无需继承WebSecurityConfigurerAdapter。[/scode]@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }以下是测试类，可以复制下面加密字符串初始化数据库密码。@Test public void TestBCryptPasswordEncoder() { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode = passwordEncoder.encode("123456"); System.out.println(encode); boolean matches = passwordEncoder.matches("123456", "$2a$10$PVeWykdFeWYa18pcFGyr0equVVAGe2w7WyQWbNVLHbHh.WHnjI4a2"); System.out.println(matches); }2.3.3.3 登陆接口接下我们需要自定义登陆接口，然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。认证成功的话要生成一个jwt，放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户，我们需要把用户信息存入redis，可以把用户id作为key。@RestController public class LoginController { @Autowired private LoginServcie loginServcie; @PostMapping("/user/login") public ResponseResult login(@RequestBody User user){ return loginServcie.login(user); } }configure中HttpSecurity的使用相关使用[post cid="1721" cover="" size=""/]@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }@Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } //使用userid生成token LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userId); //authenticate存入redis redisCache.setCacheObject("login:"+userId,loginUser); //把token响应给前端 HashMap<String,String> map = new HashMap<>(); map.put("token",jwt); return new ResponseResult(200,"登陆成功",map); } }2.3.3.4 认证过滤器我们需要自定义一个过滤器，这个过滤器会去获取请求头中的token，对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { //放行 filterChain.doFilter(request, response); return; } //解析token String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //从redis中获取用户信息 String redisKey = "login:" + userid; LoginUser loginUser = redisCache.getCacheObject(redisKey); if(Objects.isNull(loginUser)){ throw new RuntimeException("用户未登录"); } //存入SecurityContextHolder //TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //把token校验过滤器添加到过滤器链中 //addFilterBefore表示在...之前 // 这里表示我们的jwtAuthenticationTokenFilter在UsernamePasswordAuthenticationFilter之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }2.3.3.5 退出登陆我们只需要定义一个登陆接口，然后获取SecurityContextHolder中的认证信息，删除redis中对应的数据即可。@Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } //使用userid生成token LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userId); //authenticate存入redis redisCache.setCacheObject("login:"+userId,loginUser); //把token响应给前端 HashMap<String,String> map = new HashMap<>(); map.put("token",jwt); return new ResponseResult(200,"登陆成功",map); } @Override public ResponseResult logout() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); redisCache.deleteObject("login:"+userid); return new ResponseResult(200,"退出成功"); } }3. 授权3.0 权限系统的作用例如一个学校图书馆的管理系统，如果是普通学生登录就能看到借书还书相关的功能，不可能让他看到并且去使用添加书籍信息，删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了，应该就能看到并使用添加书籍信息，删除书籍信息等功能。总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样，如果有人知道了对应功能的接口地址就可以不通过前端，直接去发送请求来实现相关功能操作。所以我们还需要在后台进行用户权限的判断，判断当前用户是否有相应的权限，必须具有所需权限才能进行相应的操作。3.1 授权基本流程在SpringSecurity中，会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication，然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。3.2 授权实现3.2.1 限制访问资源所需权限SpringSecurity为我们提供了基于注解的权限控制方案，这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。但是要使用它我们需要先开启相关配置。@EnableGlobalMethodSecurity(prePostEnabled = true)然后就可以使用对应的注解。@PreAuthorize@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('test')") public String hello(){ return "hello"; } }3.2.2 封装权限信息我们前面在写UserDetailsServiceImpl的时候说过，在查询出用户后还要获取对应的权限信息，封装到UserDetails中返回。我们先直接把权限信息写死封装到UserDetails中进行测试。我们之前定义了UserDetails的实现类LoginUser，想要让其能封装权限信息就要对其进行修改。package com.sangeng.domain; import com.alibaba.fastjson.annotation.JSONField; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; //存储权限信息 private List<String> permissions; public LoginUser(User user,List<String> permissions) { this.user = user; this.permissions = permissions; } //存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(authorities!=null){ return authorities; } //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中 authorities = permissions.stream(). map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }LoginUser修改完后我们就可以在UserDetailsServiceImpl中去把权限信息封装到LoginUser中了。我们写死权限进行测试，后面我们再从数据库中查询权限信息。package com.sangeng.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.sangeng.domain.LoginUser; import com.sangeng.domain.User; import com.sangeng.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if(Objects.isNull(user)){ throw new RuntimeException("用户名或密码错误"); } //TODO 根据用户查询权限信息 添加到LoginUser中 List<String> list = new ArrayList<>(Arrays.asList("test")); return new LoginUser(user,list); } }我们已经设置好了用户权限信息，现在需要去JwtAuthenticationTokenFilter类中把用户中的authorities权限封装到UsernamePasswordAuthenticationToken中，然后交给SecurityContextHolder。// 存入SecurityContextHolder // TODO 获取权限信息封装到 setAuthentication 中 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);3.2.3 从数据库查询权限信息3.2.3.1 RBAC权限模型RBAC权限模型（Role-Based Access Control）即：基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。3.2.3.2 准备工作CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; USE `sg_security`; /*Table structure for table `sys_menu` */ DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名', `path` varchar(200) DEFAULT NULL COMMENT '路由地址', `component` varchar(255) DEFAULT NULL COMMENT '组件路径', `visible` char(1) DEFAULT '0' COMMENT '菜单状态（0显示 1隐藏）', `status` char(1) DEFAULT '0' COMMENT '菜单状态（0正常 1停用）', `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标', `create_by` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `del_flag` int(11) DEFAULT '0' COMMENT '是否删除（0未删除 1已删除）', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表'; /*Table structure for table `sys_role` */ DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串', `status` char(1) DEFAULT '0' COMMENT '角色状态（0正常 1停用）', `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag', `create_by` bigint(200) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(200) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; /*Table structure for table `sys_role_menu` */ DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id', PRIMARY KEY (`role_id`,`menu_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; /*Table structure for table `sys_user` */ DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名', `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称', `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码', `status` char(1) DEFAULT '0' COMMENT '账号状态（0正常 1停用）', `email` varchar(64) DEFAULT NULL COMMENT '邮箱', `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号', `sex` char(1) DEFAULT NULL COMMENT '用户性别（0男，1女，2未知）', `avatar` varchar(128) DEFAULT NULL COMMENT '头像', `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型（0管理员，1普通用户）', `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_by` bigint(20) DEFAULT NULL COMMENT '更新人', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `del_flag` int(11) DEFAULT '0' COMMENT '删除标志（0代表未删除，1代表已删除）', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; /*Table structure for table `sys_user_role` */ DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id', `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = 2 AND r.`status` = 0 AND m.`status` = 0package com.sangeng.domain; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; /** * 菜单表(Menu)实体类 * * @author makejava * @since 2021-11-24 15:30:08 */ @TableName(value="sys_menu") @Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Menu implements Serializable { private static final long serialVersionUID = -54979041104113736L; @TableId private Long id; /** * 菜单名 */ private String menuName; /** * 路由地址 */ private String path; /** * 组件路径 */ private String component; /** * 菜单状态（0显示 1隐藏） */ private String visible; /** * 菜单状态（0正常 1停用） */ private String status; /** * 权限标识 */ private String perms; /** * 菜单图标 */ private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; /** * 是否删除（0未删除 1已删除） */ private Integer delFlag; /** * 备注 */ private String remark; }3.2.3.3 代码实现我们只需要根据用户id去查询到其所对应的权限信息即可。所以我们可以先定义个mapper，其中提供一个方法可以根据userid查询权限信息。import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.sangeng.domain.Menu; import java.util.List; public interface MenuMapper extends BaseMapper<Menu> { List<String> selectPermsByUserId(Long id); }尤其是自定义方法，所以需要创建对应的mapper文件，定义对应的sql语句<?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.sangeng.mapper.MenuMapper"> SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </mapper>在application.yml中配置mapperXML文件的位置spring: datasource: url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml然后我们可以在UserDetailsServiceImpl中去调用该mapper的方法查询权限信息封装到LoginUser对象中即可。@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if(Objects.isNull(user)){ throw new RuntimeException("用户名或密码错误"); } List<String> permissionKeyList = menuMapper.selectPermsByUserId(user.getId()); // //测试写法 // List<String> list = new ArrayList<>(Arrays.asList("test")); return new LoginUser(user,permissionKeyList); } }4. 自定义失败处理我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json，这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。在SpringSecurity中，如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。所以如果我们需要自定义异常处理，我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。①自定义实现类@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }②配置给SpringSecurity先注入对应的处理器@Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler;然后我们可以使用HttpSecurity对象的方法去配置。http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint). accessDeniedHandler(accessDeniedHandler);[scode type="red" size=""]如果自定义了，统一异常拦截器，无法使用该配置，可在统一异常拦截器中配置。[/scode]5. 跨域浏览器出于安全的考虑，使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略，否则就是跨域的HTTP请求，默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信，即协议、域名、端口号都完全一致。前后端分离项目，前端项目和后端项目一般都不是同源的，所以肯定会存在跨域请求的问题。所以我们就要处理一下，让前端能进行跨域请求。①先对SpringBoot配置，允许跨域请求@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 设置允许跨域的路径 registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 设置允许的请求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 设置允许的header属性 .allowedHeaders("*") // 跨域允许时间 .maxAge(3600); } }②开启SpringSecurity的跨域访问由于我们的资源都会收到SpringSecurity的保护，所以想要跨域访问还要让SpringSecurity运行跨域访问。@Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //添加过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置异常处理器 http.exceptionHandling() //配置认证失败处理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允许跨域 http.cors(); }6. 遗留小问题其它权限校验方法我们前面都是使用@PreAuthorize注解，然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如：hasAnyAuthority，hasRole，hasAnyRole等。这里我们先不急着去介绍这些方法，我们先去理解hasAuthority的原理，然后再去学习其他方法你就更容易理解，而不是死记硬背区别。并且我们也可以选择定义校验方法，实现我们自己的校验逻辑。hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority，大家只要断点调试既可知道它内部的校验原理。它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。hasAnyAuthority方法可以传入多个权限，只有用户有其中任意一个权限都可以访问对应资源。@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')") public String hello(){ return "hello"; }hasRole要求有对应的角色才可以访问，但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。@PreAuthorize("hasRole('system:dept:list')") public String hello(){ return "hello"; }hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。@PreAuthorize("hasAnyRole('admin','system:dept:list')") public String hello(){ return "hello"; }自定义权限校验方法我们也可以定义自己的权限校验方法，在@PreAuthorize注解中使用我们的方法。@Component("ex") public class SGExpressionRoot { public boolean hasAuthority(String authority){ //获取当前用户的权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判断用户权限集合中是否存在authority return permissions.contains(authority); } }在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法@RequestMapping("/hello") @PreAuthorize("@ex.hasAuthority('system:dept:list')") public String hello(){ return "hello"; }基于配置的权限控制我们也可以在配置类中使用使用配置的方式对资源进行权限控制。@Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //添加过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置异常处理器 http.exceptionHandling() //配置认证失败处理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允许跨域 http.cors(); }CSRFCSRF是指跨站请求伪造（Cross-site request forgery），是web常见的攻击之一。https://blog.csdn.net/freeking101/article/details/86537087SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token，前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验，如果没有携带或者是伪造的就不允许访问。我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token，而token并不是存储中cookie中，并且需要前端代码去把token设置到请求头中才可以，所以CSRF攻击也就不用担心了。认证成功处理器实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候，如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。我们也可以自己去自定义成功处理器进行成功后的相应处理。@Component public class SGSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("认证成功了"); } }@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler successHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().successHandler(successHandler); http.authorizeRequests().anyRequest().authenticated(); } }认证失败处理器实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候，如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。我们也可以自己去自定义失败处理器进行失败后的相应处理。@Component public class SGFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("认证失败了"); } }@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailureHandler failureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 配置认证成功处理器 .successHandler(successHandler) // 配置认证失败处理器 .failureHandler(failureHandler); http.authorizeRequests().anyRequest().authenticated(); } }登出成功处理器@Component public class SGLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("注销成功"); } }@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailureHandler failureHandler; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 配置认证成功处理器 .successHandler(successHandler) // 配置认证失败处理器 .failureHandler(failureHandler); http.logout() //配置注销成功处理器 .logoutSuccessHandler(logoutSuccessHandler); http.authorizeRequests().anyRequest().authenticated(); } }其他认证方案畅想
学习目标能够基于MyBatisPlus完成标准Dao开发能够掌握MyBatisPlus的条件查询能够掌握MyBatisPlus的字段映射与表名映射能够掌握id生成策略控制能够理解代码生成器的相关配置一、MyBatisPlus简介1. 入门案例问题导入MyBatisPlus环境搭建的步骤？1.1 SpringBoot整合MyBatisPlus入门程序①：创建新模块，选择Spring初始化，并配置模块相关基础信息②：选择当前模块需要使用的技术集（仅保留JDBC）③：手动添加MyBatisPlus起步依赖<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency>注意事项1：由于mp并未被收录到idea的系统内置配置，无法直接选择加入注意事项2：如果使用Druid数据源，需要导入对应坐标④：制作实体类与表结构（类名与表名对应，属性名与字段名对应）create database if not exists mybatisplus_db character set utf8; use mybatisplus_db; CREATE TABLE user ( id bigint(20) primary key auto_increment, name varchar(32) not null, password varchar(32) not null, age int(3) not null , tel varchar(32) not null ); insert into user values(null,'tom','123456',12,'12345678910'); insert into user values(null,'jack','123456',8,'12345678910'); insert into user values(null,'jerry','123456',15,'12345678910'); insert into user values(null,'tom','123456',9,'12345678910'); insert into user values(null,'snake','123456',28,'12345678910'); insert into user values(null,'张益达','123456',22,'12345678910'); insert into user values(null,'张大炮','123456',16,'12345678910');public class User { private Long id; private String name; private String password; private Integer age; private String tel; //自行添加getter、setter、toString()等方法 }⑤：设置Jdbc参数（application.yml）spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC username: root password: root⑥：定义数据接口，继承BaseMapperpackage com.itheima.dao; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itheima.domain.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserDao extends BaseMapper<User> { }⑦：测试类中注入dao接口，测试功能package com.itheima; import com.itheima.dao.UserDao; import com.itheima.domain.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class Mybatisplus01QuickstartApplicationTests { @Autowired private UserDao userDao; @Test void testGetAll() { List<User> userList = userDao.selectList(null); System.out.println(userList); } }2. MyBatisPlus概述问题导入通过入门案例制作，MyBatisPlus的优点有哪些？2.1 MyBatis介绍MyBatisPlus（简称MP）是基于MyBatis框架基础上开发的增强型工具，旨在简化开发、提高效率官网：https://mybatis.plus/ https://mp.baomidou.com/2.2 MyBatisPlus特性无侵入：只做增强不做改变，不会对现有工程产生影响强大的 CRUD 操作：内置通用 Mapper，少量配置即可实现单表CRUD 操作支持 Lambda：编写查询条件无需担心字段写错支持主键自动生成内置分页插件……二、标准数据层开发1. MyBatisPlus的CRUD操作package com.itheima; import com.itheima.dao.UserDao; import com.itheima.domain.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest class Mybatisplus01QuickstartApplicationTests { @Autowired private UserDao userDao; @Test void testSave() { User user = new User(); user.setName("黑马程序员"); user.setPassword("itheima"); user.setAge(12); user.setTel("4006184000"); userDao.insert(user); } @Test void testDelete() { userDao.deleteById(1401856123725713409L); } @Test void testUpdate() { User user = new User(); user.setId(1L); user.setName("Tom888"); user.setPassword("tom888"); userDao.updateById(user); } @Test void testGetById() { User user = userDao.selectById(2L); System.out.println(user); } @Test void testGetAll() { List<User> userList = userDao.selectList(null); System.out.println(userList); } }2. Lombok插件介绍问题导入有什么简单的办法可以自动生成实体类的GET、SET方法？Lombok，一个Java类库，提供了一组注解，简化POJO实体类开发。<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency>常用注解：@Data，为当前实体类在编译期设置对应的get/set方法，无参/无参构造方法，toString方法，hashCode方法，equals方法等package com.itheima.domain; import lombok.*; /* 1 生成getter和setter方法：@Getter、@Setter 生成toString方法：@ToString 生成equals和hashcode方法：@EqualsAndHashCode 2 统一成以上所有：@Data 3 生成空参构造： @NoArgsConstructor 生成全参构造： @AllArgsConstructor 4 lombok还给我们提供了builder的方式创建对象,好处就是可以链式编程。 @Builder【扩展】 */ @Data public class User { private Long id; private String name; private String password; private Integer age; private String tel; }3. MyBatisPlus分页功能问题导入思考一下Mybatis分页插件是如何用的？3.1 分页功能接口3.2 MyBatisPlus分页使用①：设置分页拦截器作为Spring管理的beanpackage com.itheima.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ //1 创建MybatisPlusInterceptor拦截器对象 MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor(); //2 添加分页拦截器 mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mpInterceptor; } }②：执行分页查询//分页查询 @Test void testSelectPage(){ //1 创建IPage分页对象,设置分页参数 IPage<User> page=new Page<>(1,3); //2 执行分页查询 userDao.selectPage(page,null); //3 获取分页结果 System.out.println("当前页码值："+page.getCurrent()); System.out.println("每页显示数："+page.getSize()); System.out.println("总页数："+page.getPages()); System.out.println("总条数："+page.getTotal()); System.out.println("当前页数据："+page.getRecords()); }3.3 开启MyBatisPlus日志spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC username: root password: root # 开启mp的日志（输出到控制台） mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl3.4 解决日志打印过多问题3.4.1 取消初始化spring日志打印做法：在resources下新建一个logback.xml文件，名称固定，内容如下：<?xml version="1.0" encoding="UTF-8"?> <configuration> </configuration>关于logback参考播客：https://www.jianshu.com/p/75f9d11ae0113.4.2 取消SpringBoot启动banner图标spring: main: banner-mode: off # 关闭SpringBoot启动图标(banner)3.4.3 取消MybatisPlus启动banner图标# mybatis-plus日志控制台输出 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: off # 关闭mybatisplus启动图标三、DQL编程控制1. 条件查询方式MyBatisPlus将书写复杂的SQL查询条件进行了封装，使用编程的形式完成查询条件的组合1.1 条件查询1.1.1 方式一：按条件查询//方式一：按条件查询 QueryWrapper<User> qw=new QueryWrapper<>(); qw.lt("age", 18); List<User> userList = userDao.selectList(qw); System.out.println(userList);1.1.2 方式二：lambda格式按条件查询//方式二：lambda格式按条件查询 QueryWrapper<User> qw = new QueryWrapper<User>(); qw.lambda().lt(User::getAge, 10); List<User> userList = userDao.selectList(qw); System.out.println(userList);1.1.3 方式三：lambda格式按条件查询（推荐）//方式三：lambda格式按条件查询 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); lqw.lt(User::getAge, 10); List<User> userList = userDao.selectList(lqw); System.out.println(userList);1.2 组合条件1.2.1 并且关系（and）//并且关系 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //并且关系：10到30岁之间 lqw.lt(User::getAge, 30).gt(User::getAge, 10); List<User> userList = userDao.selectList(lqw); System.out.println(userList);1.2.2 或者关系（or）//或者关系 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //或者关系：小于10岁或者大于30岁 lqw.lt(User::getAge, 10).or().gt(User::getAge, 30); List<User> userList = userDao.selectList(lqw); System.out.println(userList);1.3 NULL值处理问题导入如下搜索场景，在多条件查询中，有条件的值为空应该怎么解决？1.3.1 if语句控制条件追加Integer minAge=10; //将来有用户传递进来,此处简化成直接定义变量了 Integer maxAge=null; //将来有用户传递进来,此处简化成直接定义变量了 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); if(minAge!=null){ lqw.gt(User::getAge, minAge); } if(maxAge!=null){ lqw.lt(User::getAge, maxAge); } List<User> userList = userDao.selectList(lqw); userList.forEach(System.out::println);1.3.2 条件参数控制Integer minAge=10; //将来有用户传递进来,此处简化成直接定义变量了 Integer maxAge=null; //将来有用户传递进来,此处简化成直接定义变量了 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //参数1：如果表达式为true，那么查询才使用该条件 lqw.gt(minAge!=null,User::getAge, minAge); lqw.lt(maxAge!=null,User::getAge, maxAge); List<User> userList = userDao.selectList(lqw); userList.forEach(System.out::println);1.3.3 条件参数控制（链式编程）Integer minAge=10; //将来有用户传递进来,此处简化成直接定义变量了 Integer maxAge=null; //将来有用户传递进来,此处简化成直接定义变量了 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //参数1：如果表达式为true，那么查询才使用该条件 lqw.gt(minAge!=null,User::getAge, minAge) .lt(maxAge!=null,User::getAge, maxAge); List<User> userList = userDao.selectList(lqw); userList.forEach(System.out::println);2. 查询投影-设置【查询字段、分组、分页】2.1 查询结果包含模型类中部分属性/*LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); lqw.select(User::getId, User::getName, User::getAge);*/ //或者 QueryWrapper<User> lqw = new QueryWrapper<User>(); lqw.select("id", "name", "age", "tel"); List<User> userList = userDao.selectList(lqw); System.out.println(userList);2.2 查询结果包含模型类中未定义的属性QueryWrapper<User> lqw = new QueryWrapper<User>(); lqw.select("count(*) as count, tel"); lqw.groupBy("tel"); List<Map<String, Object>> userList = userDao.selectMaps(lqw); System.out.println(userList);3. 查询条件设定问题导入多条件查询有哪些组合？范围匹配（> 、 = 、between）模糊匹配（like）空判定（null）包含性匹配（in）分组（group）排序（order）……3.1 查询条件用户登录（eq匹配）LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //等同于= lqw.eq(User::getName, "Jerry").eq(User::getPassword, "jerry"); User loginUser = userDao.selectOne(lqw); System.out.println(loginUser);购物设定价格区间、户籍设定年龄区间（le ge匹配 或 between匹配）LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //范围查询 lt le gt ge eq between lqw.between(User::getAge, 10, 30); List<User> userList = userDao.selectList(lqw); System.out.println(userList);查信息，搜索新闻（非全文检索版：like匹配）LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>(); //模糊匹配 like lqw.likeLeft(User::getName, "J"); List<User> userList = userDao.selectList(lqw); System.out.println(userList);统计报表（分组查询聚合函数）QueryWrapper<User> qw = new QueryWrapper<User>(); qw.select("gender","count(*) as nums"); qw.groupBy("gender"); List<Map<String, Object>> maps = userDao.selectMaps(qw); System.out.println(maps);3.2 查询API更多查询条件设置参看 https://mybatis.plus/guide/wrapper.html#abstractwrapper3.3 练习：MyBatisPlus练习题目：基于MyBatisPlus_Ex1模块，完成Top5功能的开发。说明：①：Top5指根据销售量排序（提示：对销售量进行降序排序）②：Top5是仅获取前5条数据（提示：使用分页功能控制数据显示数量）4. 字段映射与表名映射问题导入思考表的字段和实体类的属性不对应，查询会怎么样？4.1 问题一：表字段与编码属性设计不同步在模型类属性上方，使用@TableField 属性注解，通过value属性，设置当前属性对应的数据库表中的字段关系。4.2 问题二：编码中添加了数据库中未定义的属性在模型类属性上方，使用@TableField注解，通过exist属性，设置属性在数据库表字段中是否存在，默认为true。此属性无法与value合并使用。4.3 问题三：采用默认查询开放了更多的字段查看权限在模型类属性上方，使用@TableField注解，通过select属性：设置该属性是否参与查询。此属性与select()映射配置不冲突。4.4 问题四：表名与编码开发设计不同步在模型类上方，使用@TableName注解，通过value属性，设置当前类对应的数据库表名称。@Data @TableName("tbl_user") public class User { /* id为Long类型，因为数据库中id为bigint类型， 并且mybatis有自己的一套id生成方案，生成出来的id必须是Long类型 */ private Long id; private String name; @TableField(value = "pwd",select = false) private String password; private Integer age; private String tel; @TableField(exist = false) //表示online字段不参与CRUD操作 private Boolean online; }四、DML编程控制1. id生成策略控制（Insert）问题导入主键生成的策略有哪几种方式？不同的表应用不同的id生成策略日志：自增（1,2,3,4，……）购物订单：特殊规则（FQ23948AK3843）外卖单：关联地区日期等信息（10 04 20200314 34 91）关系表：可省略id……1.1 id生成策略控制（@TableId注解）名称：@TableId类型：属性注解位置：模型类中用于表示主键的属性定义上方作用：设置当前类中主键属性的生成策略相关属性type：设置主键属性的生成策略，值参照IdType枚举值1.2 全局策略配置mybatis-plus: global-config: db-config: id-type: assign_id table-prefix: tbl_id生成策略全局配置表名前缀全局配置2. 多记录操作（批量Delete/Select）问题导入MyBatisPlus是否支持批量操作？2.1 按照主键删除多条记录//删除指定多条数据 List<Long> list = new ArrayList<>(); list.add(1402551342481838081L); list.add(1402553134049501186L); list.add(1402553619611430913L); userDao.deleteBatchIds(list);2.2 根据主键查询多条记录//查询指定多条数据 List<Long> list = new ArrayList<>(); list.add(1L); list.add(3L); list.add(4L); userDao.selectBatchIds(list);3. 逻辑删除（Delete/Update）问题导入在实际环境中，如果想删除一条数据，是否会真的从数据库中删除该条数据？删除操作业务问题：业务数据从数据库中丢弃逻辑删除：为数据设置是否可用状态字段，删除时设置状态字段为不可用状态，数据保留在数据库中3.1 逻辑删除案例①：数据库表中添加逻辑删除标记字段②：实体类中添加对应字段，并设定当前字段为逻辑删除标记字段package com.itheima.domain; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; @Data public class User { private Long id; //逻辑删除字段，标记当前记录是否被删除 @TableLogic private Integer deleted; }③：配置逻辑删除字面值mybatis-plus: global-config: db-config: table-prefix: tbl_ # 逻辑删除字段名 logic-delete-field: deleted # 逻辑删除字面值：未删除为0 logic-not-delete-value: 0 # 逻辑删除字面值：删除为1 logic-delete-value: 1逻辑删除本质：逻辑删除的本质其实是修改操作。如果加了逻辑删除字段，查询数据时也会自动带上逻辑删除字段。4. 乐观锁（Update）问题导入乐观锁主张的思想是什么？业务并发现象带来的问题：秒杀4.1 乐观锁案例①：数据库表中添加锁标记字段②：实体类中添加对应字段，并设定当前字段为逻辑删除标记字段package com.itheima.domain; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.Version; import lombok.Data; @Data public class User { private Long id; @Version private Integer version; }③：配置乐观锁拦截器实现锁机制对应的动态SQL语句拼装package com.itheima.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MpConfig { @Bean public MybatisPlusInterceptor mpInterceptor() { //1.定义Mp拦截器 MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor(); //2.添加乐观锁拦截器 mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return mpInterceptor; } }④：使用乐观锁机制在修改前必须先获取到对应数据的verion方可正常进行@Test public void testUpdate() { /*User user = new User(); user.setId(3L); user.setName("Jock666"); user.setVersion(1); userDao.updateById(user);*/ //1.先通过要修改的数据id将当前数据查询出来 //User user = userDao.selectById(3L); //2.将要修改的属性逐一设置进去 //user.setName("Jock888"); //userDao.updateById(user); //1.先通过要修改的数据id将当前数据查询出来 User user = userDao.selectById(3L); //version=3 User user2 = userDao.selectById(3L); //version=3 user2.setName("Jock aaa"); userDao.updateById(user2); //version=>4 user.setName("Jock bbb"); userDao.updateById(user); //verion=3?条件还成立吗？ }五、快速开发-代码生成器问题导入如果只给一张表的字段信息，能够推演出Domain、Dao层的代码？1. MyBatisPlus提供模板Mapper接口模板实体对象类模板2. 工程搭建和基本代码编写第一步：创建SpringBoot工程，添加代码生成器相关依赖，其他依赖自行添加 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency>第二步：编写代码生成器类package com.itheima; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; public class Generator { public static void main(String[] args) { //1. 创建代码生成器对象，执行生成代码操作 AutoGenerator autoGenerator = new AutoGenerator(); //2. 数据源相关配置：读取数据库中的信息，根据数据库表结构生成代码 DataSourceConfig dataSource = new DataSourceConfig(); dataSource.setDriverName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC"); dataSource.setUsername("root"); dataSource.setPassword("root"); autoGenerator.setDataSource(dataSource); //3. 执行生成操作 autoGenerator.execute(); } }3. 开发者自定义配置设置全局配置//设置全局配置 GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setOutputDir(System.getProperty("user.dir")+"/mybatisplus_04_generator/src/main/java"); //设置代码生成位置 globalConfig.setOpen(false); //设置生成完毕后是否打开生成代码所在的目录 globalConfig.setAuthor("黑马程序员"); //设置作者 globalConfig.setFileOverride(true); //设置是否覆盖原始生成的文件 globalConfig.setMapperName("%sDao"); //设置数据层接口名，%s为占位符，指代模块名称 globalConfig.setIdType(IdType.ASSIGN_ID); //设置Id生成策略 autoGenerator.setGlobalConfig(globalConfig);设置包名相关配置//设置包名相关配置 PackageConfig packageInfo = new PackageConfig(); packageInfo.setParent("com.aaa"); //设置生成的包名，与代码所在位置不冲突，二者叠加组成完整路径 packageInfo.setEntity("domain"); //设置实体类包名 packageInfo.setMapper("dao"); //设置数据层包名 autoGenerator.setPackageInfo(packageInfo);策略设置//策略设置 StrategyConfig strategyConfig = new StrategyConfig(); strategyConfig.setInclude("tbl_user"); //设置当前参与生成的表名，参数为可变参数 strategyConfig.setTablePrefix("tbl_"); //设置数据库表的前缀名称，模块名 = 数据库表名 - 前缀名 例如： User = tbl_user - tbl_ strategyConfig.setRestControllerStyle(true); //设置是否启用Rest风格 strategyConfig.setVersionFieldName("version"); //设置乐观锁字段名 strategyConfig.setLogicDeleteFieldName("deleted"); //设置逻辑删除字段名 strategyConfig.setEntityLombokModel(true); //设置是否启用lombok autoGenerator.setStrategy(strategyConfig);说明：在资料中也提供了CodeGenerator代码生成器类，根据实际情况修改后可以直接使用。
学习目标理解分模块开发的意义能够使用聚合工程快速构建项目能够使用继承简化项目配置能够根据需求配置生产、开发、测试环境，并在各环境间切换运行一、分模块开发与设计1. 分模块开发的意义问题导入分模块开发对工程有什么好处？模块拆分原则目的：项目的扩展性变强了，方便其他项目引用相同的功能。将原始模块按照功能拆分成若干个子模块，方便模块间的相互调用，接口共享2. 分模块开发（模块拆分）问题导入一个完整的工程依据什么来进行模块的拆分？2.1 创建Maven模块2.2 书写模块代码注意事项：分模块开发需要先针对模块功能进行设计，再进行编码。不会先将工程开发完毕，然后进行拆分2.3 通过maven指令安装模块到本地仓库（install指令）mvn install注意事项：团队内部开发需要发布模块功能到团队内部可共享的仓库中（私服）二、依赖管理依赖管理指当前项目运行所需的jar，一个项目可以设置多个依赖格式： <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>1. 依赖传递问题导入A依赖B，B依赖C，A是否依赖于C呢？依赖具有传递性直接依赖：在当前项目中通过依赖配置建立的依赖关系间接依赖：被资源的资源如果依赖其他资源，当前项目间接依赖其他资源特殊优先：当同级配置了相同资源的不同版本，后配置的覆盖先配置的2. 可选依赖问题导入A依赖B，B依赖C，如果A不想将C依赖进来，是否可以做到？可选依赖指对外隐藏当前所依赖的资源————不透明<dependency> <groupId>com.itheima</groupId> <artifactId>maven_03_pojo</artifactId> <version>1.0-SNAPSHOT</version> <optional>false</optional> </dependency>3. 排除依赖问题导入A依赖B，B依赖C，如果A不想将C依赖进来，是否可以做到？排除依赖指主动断开依赖的资源，被排除的资源无需指定版本————不需要排除依赖资源仅指定GA即可，无需指定V<dependency> <groupId>com.itheima</groupId> <artifactId>maven_04_dao</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> </exclusion> </exclusions> </dependency>4 可选依赖和排除依赖的区别三、聚合与继承1. 聚合工程问题导入什么叫聚合？聚合：将多个模块组织成一个整体，同时进行项目构建的过程称为聚合聚合工程：通常是一个不具有业务功能的”空“工程（有且仅有一个pom文件）作用：使用聚合工程可以将多个工程编组，通过对聚合工程进行构建，实现对所包含的模块进行同步构建当工程中某个模块发生更新（变更）时，必须保障工程中与已更新模块关联的模块同步更新，此时可以使用聚合工程来解决批量模块同步构建的问题2. 聚合工程开发问题导入工程的打包方式有哪几种？2.1 创建Maven模块，设置打包类型为pom<packaging>pom</packaging>注意事项：每个maven工程都有对应的打包方式，默认为jar，web工程打包方式为war2.2 设置当前聚合工程所包含的子模块名称<modules> <module>../maven_ssm</module> <module>../maven_pojo</module> <module>../maven_dao</module> </modules>注意事项：聚合工程中所包含的模块在进行构建时会根据模块间的依赖关系设置构建顺序，与聚合工程中模块的配置书写位置无关。参与聚合的工程无法向上感知是否参与聚合，只能向下配置哪些模块参与本工程的聚合。3. 继承关系问题导入什么叫继承？概念：继承描述的是两个工程间的关系，与java中的继承相似，子工程可以继承父工程中的配置信息，常见于依赖关系的继承作用：简化配置减少版本冲突4. 继承关系开发4.1 创建Maven模块，设置打包类型为pom<packaging>pom</packaging>注意事项：建议父工程打包方式设置为pom4.2 在父工程的pom文件中配置依赖关系（子工程将沿用父工程中的依赖关系）<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> …… </dependencies>4.3 配置子工程中可选的依赖关系<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> …… </dependencies> </dependencyManagement>4.3 在子工程中配置当前工程所继承的父工程 <parent> <groupId>com.itheima</groupId> <artifactId>maven_parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../maven_parent/pom.xml</relativePath> </parent>4.5 在子工程中配置使用父工程中可选依赖的坐标<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> </dependencies>注意事项：子工程中使用父工程中的可选依赖时，仅需要提供群组id和项目id，无需提供版本，版本由父工程统一提供，避免版本冲突子工程中还可以定义父工程中没有定义的依赖关系5. 聚合与继承的区别问题导入聚合和继承的作用？作用聚合用于快速构建项目继承用于快速配置相同点：聚合与继承的pom.xml文件打包方式均为pom，可以将两种关系制作到同一个pom文件中聚合与继承均属于设计型模块，并无实际的模块内容不同点：聚合是在当前模块中配置关系，聚合可以感知到参与聚合的模块有哪些继承是在子模块中配置关系，父模块无法感知哪些子模块继承了自己四、属性管理1. 属性问题导入定义属性有什么好处？1.1 属性配置与使用①：定义属性 <properties> <spring.version>5.2.10.RELEASE</spring.version> <junit.version>4.12</junit.version> </properties>②：引用属性<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency>1.2 资源文件引用属性①：定义属性 <properties> <spring.version>5.2.10.RELEASE</spring.version> <junit.version>4.12</junit.version> <jdbc.url>jdbc:mysql://127.0.0.1:3306/ssm_db</jdbc.url> </properties>②：配置文件中引用属性jdbc.driver=com.mysql.jdbc.Driver jdbc.url=${jdbc.url} jdbc.username=root jdbc.password=root③：开启资源文件目录加载属性的过滤器<build> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </build>④：配置maven打war包时，忽略web.xml检查<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.2.3</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin>1.3 其他属性（了解）属性列表自定义属性（常用）内置属性Setting属性Java系统属性环境变量属性2. 版本管理问题导入项目开发的版本可以分为哪几种？2.1 工程版本SNAPSHOT（快照版本）项目开发过程中临时输出的版本，称为快照版本快照版本会随着开发的进展不断更新RELEASE（发布版本）项目开发到进入阶段里程碑后，向团队外部发布较为稳定的版本，这种版本所对应的构件文件是稳定的即便进行功能的后续开发，也不会改变当前发布版本内容，这种版本称为发布版本2.2 发布版本alpha版beta版纯数字版五、多环境配置与应用1. 多环境配置作用问题导入多环境配置有什么好处？maven提供配置多种环境的设定，帮助开发者使用过程中快速切换环境2. 多环境配置步骤2.1 定义多环境 <profiles> <profile> <id>env_dep</id> <properties> <jdbc.url>jdbc:mysql://127.0.0.1:3306/ssm_db</jdbc.url> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>env_pro</id> …… </profile> </profiles>2.2 使用多环境（构建过程）【命令】： mvn 指令 –P 环境定义id 【范例】： mvn install –P pro_env2. 跳过测试（了解）问题导入跳过测试会不会影响项目的构建过程？2.1 应用场景功能更新中并且没有开发完毕快速打包……2.2 跳过测试命令mvn install –D skipTests注意事项：执行的项目构建指令必须包含测试生命周期，否则无效果。例如执行compile生命周期，不经过test生命周期。2.3 细粒度控制跳过测试<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> <configuration> <skipTests>true</skipTests> <includes> <include>**/User*Test.java</include> </includes> <excludes> <exclude>**/User*TestCase.java</exclude> </excludes> </configuration> </plugin>六、私服1. 私服介绍问题导入这里的私服和平时我们听的国服、体验服、欧服等等有什么区别？1.1 介绍团队开发现状分析私服是一台独立的服务器，用于解决团队内部的资源共享与资源同步问题NexusSonatype公司的一款maven私服产品下载地址：https://help.sonatype.com/repomanager3/download1.2 Nexus安装与启动启动服务器（命令行启动）nexus.exe /run nexus访问服务器（默认端口：8081）http://localhost:8081修改基础配置信息安装路径下etc目录中nexus-default.properties文件保存有nexus基础配置信息，例如默认访问端口。修改服务器运行配置信息安装路径下bin目录中nexus.vmoptions文件保存有nexus服务器启动对应的配置信息，例如默认占用内存空间。1.3 私服资源操作流程分析2. 私服仓库分类问题导入私服仓库分为哪几种？3. 资源上传与下载问题导入往私服上传资源是否需要身份认证？在哪里设置认证信息？3.1 从私服中下载依赖【第一步】在maven的settings.xml中\<mirrors>标签中配置，此时就需要注释掉aliyun的配置。<mirror> <id>nexus-heima</id> <mirrorOf>*</mirrorOf> <url>http://localhost:8081/repository/maven-public/</url> </mirror>【第二步】在nexus中设置允许匿名下载，如果不允许将不会从私服中下载依赖如果私服中没有对应的jar，会去中央仓库下载，速度很慢。可以配置让私服去阿里云中下载依赖。3.2 上传依赖到私服中【第一步】配置本地仓库访问私服的权限（在maven的settings.xml的servers标签中配置）<server> <id>heima-nexus</id> <username>admin</username> <password>123456</password> </server>【第一步】配置当前项目访问私服上传资源的保存位置（项目的pom.xml文件中配置）<distributionManagement> <repository> <id>heima-nexus</id> <url>http://localhost:8081/repository/heima-releases/</url> </repository> <snapshotRepository> <id>heima-nexus</id> <url>http://localhost:8081/repository/heima-snapshots/</url> </snapshotRepository> </distributionManagement>[scode type="yellow" size=""]注意：要和maven的settings.xml中server中定义的\<id>heima-nexus\</id>对应[/scode]【第三步】发布资源到私服命令mvn deploy
学习目标基于SpringBoot框架的程序开发步骤熟练使用SpringBoot配置信息修改服务器配置基于SpringBoot的完成SSM整合项目开发一、SpringBoot简介1. 入门案例问题导入SpringMVC的HelloWord程序大家还记得吗？SpringBoot是由Pivotal团队提供的全新框架，其设计目的是用来简化Spring应用的初始搭建以及开发过程原生开发SpringMVC程序过程1.1 入门案例开发步骤①：创建新模块，选择Spring初始化，并配置模块相关基础信息②：选择当前模块需要使用的技术集③：开发控制器类@RestController @RequestMapping("/books") public class BookController { @GetMapping("/{id}") public String getById(@PathVariable Integer id) { System.out.println("id ==> " + id); return "hello , spring boot! "; } }④：运行自动生成的Application类最简SpringBoot程序所包含的基础文件<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.0</version> </parent> <groupId>com.itheima</groupId> <artifactId>springboot-01-quickstart</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }Spring程序与SpringBoot程序对比注意事项：基于idea开发SpringBoot程序需要确保联网且能够加载到程序框架结构1.2 基于SpringBoot官网创建项目1.3 SpringBoot项目快速启动① 对SpringBoot项目打包（执行Maven构建指令package）② 执行启动指令java -jar springboot_01_quickstart.jar # 项目的名称根据实际情况修改注意事项：jar支持命令行启动需要依赖maven插件支持，请确认打包时是否具有SpringBoot对应的maven插件。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>2. SpringBoot概述问题导入学习了SpringBoot入门案例之后，感觉对比SpringMVC哪一个更加方便简洁？SpringBoot是由Pivotal团队提供的全新框架，其设计目的是用来简化Spring应用的初始搭建以及开发过程Spring程序缺点配置繁琐依赖设置繁琐SpringBoot程序优点自动配置起步依赖（简化依赖配置）辅助功能（内置服务器，……）2.1 起步依赖starterSpringBoot中常见项目名称，定义了当前项目使用的所有项目坐标，以达到减少依赖配置的目的<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.0</version> </parent> <groupId>com.itheima</groupId> <artifactId>springboot-01-quickstart</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project><project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.5.0</version> <packaging>pom</packaging> <properties> <servlet-api.version>4.0.1</servlet-api.version> ... </properties> </project>parent所有SpringBoot项目要继承的项目，定义了若干个坐标版本号（依赖管理，而非依赖），以达到减少依赖冲突的目的spring-boot-starter-parent（2.5.0）与 spring-boot-starter-parent（2.4.6）共计57处坐标版本不同<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.5.0</version> </parent> <artifactId>spring-boot-starter-parent</artifactId> <packaging>pom</packaging> ... </project>实际开发使用任意坐标时，仅书写GAV中的G和A，V由SpringBoot提供如发生坐标错误，再指定version（要小心版本冲突）<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet-api.version}</version> </dependency><?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.0</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>2.2 辅助功能SpringBoot程序启动@SpringBootApplication public class Springboot01QuickstartApplication { public static void main(String[] args) { SpringApplication.run(Springboot01QuickstartApplication.class, args); } }SpringBoot在创建项目时，采用jar的打包方式SpringBoot的引导类是项目的入口，运行main方法就可以启动项目使用maven依赖管理变更起步依赖项Jetty比Tomcat更轻量级，可扩展性更强（相较于Tomcat），谷歌应用引擎（GAE）已经全面切换为Jetty<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> </dependencies>二、基础配置1. 配置文件格式问题导入框架常见的配置文件有哪几种形式？1.1 修改服务器端口http://localhost:8080/books/1 >>> http://localhost/books/1SpringBoot提供了多种属性配置方式application.propertiesserver.port=80application.ymlserver: port: 81application.yamlserver: port: 821.2 自动提示功能消失解决方案操作步骤：1.3 SpringBoot配置文件加载顺序（了解）application.properties > application.yml > application.yaml注意事项：SpringBoot核心配置文件名为applicationSpringBoot内置属性过多，且所有属性集中在一起修改，在使用时，通过提示键+关键字修改属性2. yaml问题导入什么是yaml，和properties有什么区别？YAML（YAML Ain't Markup Language），一种数据序列化格式优点：容易阅读容易与脚本语言交互以数据为核心，重数据轻格式YAML文件扩展名.yml（主流）.yaml2.1 yaml语法规则大小写敏感属性层级关系使用多行描述，每行结尾使用冒号结束使用缩进表示层级关系，同层级左侧对齐，只允许使用空格（不允许使用Tab键）属性值前面添加空格（属性名与属性值之间使用冒号+空格作为分隔）#表示注释核心规则：数据前面要加空格与冒号隔开2.2 yaml数组数据数组数据在数据书写位置的下方使用减号作为数据开始符号，每行书写一个数据，减号与数据间空格分隔2.3 yaml数据读取使用@Value读取单个数据，属性名引用方式：${一级属性名.二级属性名……}封装全部数据到Environment对象自定义对象封装指定数据【常用】public class Enterprise { private String name; private Integer age; private String tel; private String[] subject; //自行添加getter、setter、toString()等方法 }自定义对象封装数据警告解决方案<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>3. 多环境开发配置问题导入在实际开发中，项目的开发环境、测试环境、生产环境的配置信息是否会一致？如何快速切换？3.1 多环境启动配置yaml文件多环境启动properties文件多环境启动#主启动配置文件 application.properties spring.profiles.active=pro#环境分类配置文件 application-pro.properties server.port=80#环境分类配置文件 application-dev.properties server.port=81#环境分类配置文件application-test.properties server.port=823.2 多环境启动命令格式带参数启动SpringBootjava –jar springboot.jar --spring.profiles.active=test java –jar springboot.jar --server.port=88 java –jar springboot.jar --server.port=88 --spring.profiles.active=test参数加载优先顺序参看文档：https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config3.3 多环境开发控制Maven与SpringBoot多环境兼容（步骤）①：Maven中设置多环境属性<profiles> <profile> <id>dev_env</id> <properties> <profile.active>dev</profile.active> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>pro_env</id> <properties> <profile.active>pro</profile.active> </properties> </profile> <profile> <id>test_env</id> <properties> <profile.active>test</profile.active> </properties> </profile> </profiles>②：SpringBoot中引用Maven属性③：执行Maven打包指令Maven指令执行完毕后，生成了对应的包，其中类参与编译，但是配置文件并没有编译，而是复制到包中解决思路：对于源码中非java类的操作要求加载Maven对应的属性，解析${}占位符④：对资源文件开启对默认占位符的解析<build> <plugins> <plugin> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>utf-8</encoding> <useDefaultDelimiters>true</useDefaultDelimiters> </configuration> </plugin> </plugins> </build>Maven打包加载到属性，打包顺利通过4. 配置文件分类问题导入SpringBoot的配置文件可以放在项目的哪些地方？java –jar springboot.jar --spring.profiles.active=test --server.port=85 --server.servlet.context-path=/heima --server.tomcat.connection-timeout=-1 ... ...SpringBoot中4级配置文件1级： file ：config/application.yml 【最高】2级： file ：application.yml3级：classpath：config/application.yml4级：classpath：application.yml 【最低】作用：1级与2级留做系统打包后设置通用属性3级与4级用于系统开发阶段设置通用属性三、整合第三方技术1. 整合JUnit问题导入回忆一下Spring整合JUnit的步骤？1.1 Spring整合JUnit（复习）1.2 SpringBoot整合JUnit【第一步】添加整合junit起步依赖(可以直接勾选)<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>【第二步】编写测试类，默认自动生成了一个@SpringBootTest class Springboot07JunitApplicationTests { @Autowired private BookService bookService; @Test public void testSave() { bookService.save(); } }2. 基于SpringBoot实现SSM整合问题导入回忆一下Spring整合MyBatis的核心思想？2.1 Spring整合MyBatis（复习）SpringConfig导入JdbcConfig导入MyBatisConfig@Configuration @ComponentScan("com.itheima") @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class, MyBatisConfig.class}) public class SpringConfig { }JDBCConfig定义数据源（加载properties配置项：driver、url、username、password）#jdbc.properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/spring_db jdbc.username=root jdbc.password=itheimapublic class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String userName; @Value("${jdbc.password}") private String password; @Bean public DataSource getDataSource() { DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } }MyBatisConfig定义SqlSessionFactoryBean定义映射配置@Bean public SqlSessionFactoryBean getSqlSessionFactoryBean(DataSource dataSource) { SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean(); ssfb.setTypeAliasesPackage("com.itheima.domain"); ssfb.setDataSource(dataSource); return ssfb; }@Bean public MapperScannerConfigurer getMapperScannerConfigurer() { MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("com.itheima.dao"); return msc; }2.2 SpringBoot整合MyBatisSpringBoot整合Spring（不存在）SpringBoot整合SpringMVC（不存在）SpringBoot整合MyBatis（主要）①：创建新模块，选择Spring初始化，并配置模块相关基础信息②：选择当前模块需要使用的技术集（MyBatis、MySQL）③：设置数据源参数spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC username: root password: root type: com.alibaba.druid.pool.DruidDataSource注意事项：SpringBoot版本低于2.4.3(不含)，Mysql驱动版本大于8.0时，需要在url连接串中配置时区，或在MySQL数据库端配置时区解决此问题jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC④：定义数据层接口与映射配置@Mapper public interface UserDao { @Select("select * from tbl_book where id=#{id}") Book getById(Integer id); }⑤：测试类中注入dao接口，测试功能@SpringBootTest class Springboot08MybatisApplicationTests { @Autowired private BookDao bookDao; @Test public void testGetById() { Book book = bookDao.getById(1); System.out.println(book); } }2.3 案例-SpringBoot实现ssm整合【第一步】创建SpringBoot工程，添加druid依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency>【第二步】复制springmvc_11_page工程各种资源(主java类、页面、测试类)【第三步】删除config包中的所有配置，在BookDao接口上加@Mapper注解//todo 3 在BookDao接口上加@Mapper注解，让SpringBoot给接口创建代理对象 @Mapper public interface BookDao { //... }【第四步】将application.properties修改成application.yml，配置端口号和连接参数server: port: 80 # todo 4 配置数据库连接参数 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_db username: root password: root type: com.alibaba.druid.pool.DruidDataSource【第五步】修改BookServiceTest配置类，进行配置// todo 5 修改单元测试类，添加@SpringBootTest主键，修复@Test注解导包 @SpringBootTest public class BookServiceTest { @Autowired private BookService bookService; @Test public void testGetById(){ Book book = bookService.getById(2); //传递参数1会抛出异常 System.out.println(book); } @Test public void testGetAll(){ List<Book> all = bookService.getAll(); System.out.println(all); } }【第六步】在static目录中提供index.html页面，跳转到"pages/books.html" 最后：运行引导类即可访问
[scode type="yellow" size=""]问题：我们常用到的dao、pojo、service、controller、mapper、Impl、bo、vo、po、domain都是什么？[/scode]1. 前言学习ssm框架时看视频遇到这些概念，一开始都被搞混了，现在来区分一下。首先了解两个概念：MVC是model+ view + controllerSSM 是spring + springMVC + mybatis2.关系示意图记住两句话:业务层调数据层。控制层调业务层在java中可以表示成下面这种情况：3.区别和联系DAO(mapper)，DAO= Data Acess Object, 数据持久层，对数据库进行持久化操作，负责跟数据库打交道。通常我们在DAO层里写接口，里面有与数据打交道的方法。SQL语句通常写在mapper文件里。Service,业务层或服务层，主要负责业务模块的逻辑应用设计。 Service层的实现，具体调用到已经定义的DAO接口，封装service层的业务逻辑有利于通用的业业务逻辑的独立性和重复利用性。如果把Dao层当作积木，那么Service层则是对积木的搭建。Controller, 负责具体的业务模块流程的控制。此层要调用Service层的接口去控制业务流程。Pojo 全称Plain Ordinary Java Object ，数据库实体类，有的地方也直接写成entity。也可以理解为domain，一般是跟数据库对应好的一个实体类。Bo ，bussiness object，表示业务对象的意思。bo就是把业务逻辑封装成一个对象，这个对象可以包括一个或多个对象。通过调用dao方法，结合Po或Vo进行业务操作。Vo ,value object表示值对象的也i是，通常用于业务层之间的数据传递。Po, persistant object, 代表持久层对象的意思，对应数据库中表的字段，可以理解为一个po就是数据库中的一条记录。Impl 全称是 implement, 实现的意思，主要用于实现接口。
今日内容能够掌握SSM整合的流程能够编写SSM整合功能模块类能够使用Result统一表现层响应结果能够编写异常处理器进行项目异常能够完成SSM整合前端页面发送请求实现增删改查操作能够编写拦截器并配置拦截器一、SSM整合【重点】1 SSM整合配置问题导入请描述“SSM整合流程”中各个配置类的作用？1.1 SSM整合流程创建工程SSM整合SpringSpringConfigMyBatisMybatisConfigJdbcConfigjdbc.propertiesSpringMVCServletConfigSpringMvcConfig功能模块表与实体类dao（接口+自动代理）service（接口+实现类）业务层接口测试（整合JUnit）controller表现层接口测试（PostMan）1.2 SSM整合配置1.2.1 创建工程，添加依赖和插件<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.1</version> <configuration> <port>80</port> <path>/</path> </configuration> </plugin> </plugins> </build>1.2.2 Spring整合Mybatis创建数据库和表-- 创建ssm_db数据库 CREATE DATABASE IF NOT EXISTS ssm_db CHARACTER SET utf8; -- 使用ssm_db数据库 USE ssm_db; -- 创建tbl_book表 CREATE TABLE tbl_book( id INT PRIMARY KEY AUTO_INCREMENT, -- 图书编号 TYPE VARCHAR(100), -- 图书类型 NAME VARCHAR(100), -- 图书名称 description VARCHAR(100) -- 图书描述 ); -- 添加初始化数据 INSERT INTO tbl_book VALUES(NULL,'计算机理论','Spring实战 第5版','Spring入门经典教材，深入理解Spring原理技术内幕'); INSERT INTO tbl_book VALUES(NULL,'计算机理论','Spring 5核心原理与30个类手写实战','十年沉淀之作,手写Spring精华思想'); INSERT INTO tbl_book VALUES(NULL,'计算机理论','Spring 5设计模式','深入Spring源码剖析,Spring源码蕴含的10大设计模式'); INSERT INTO tbl_book VALUES(NULL,'市场营销','直播就该这么做:主播高效沟通实战指南','李子柒、李佳琦、薇娅成长为网红的秘密都在书中'); INSERT INTO tbl_book VALUES(NULL,'市场营销','直播销讲实战一本通','和秋叶一起学系列网络营销书籍'); INSERT INTO tbl_book VALUES(NULL,'市场营销','直播带货:淘宝、天猫直播从新手到高手','一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');jdbc.properties属性文件jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm_db jdbc.username=root jdbc.password=rootJdbcConfig配置类public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; //配置连接池 @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } //Spring事务管理需要的平台事务管理器对象 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager ds = new DataSourceTransactionManager(); ds.setDataSource(dataSource); return ds; } }MybatisConfig配置类public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){ SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage("com.itheima.domain"); return factoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("com.itheima.dao"); return msc; } }SpringConfig配置类@Configuration @ComponentScan({"com.itheima.service"}) @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class,MyBatisConfig.class}) @EnableTransactionManagement //开启Spring事务管理 public class SpringConfig { }1.2.3 Spring整合SpringMVCSpringMvcConfig配置类@Configuration @ComponentScan("com.itheima.controller") @EnableWebMvc public class SpringMvcConfig { }ServletConfig配置类，加载SpringMvcConfig和SpringConfig配置类public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } }2 功能模块开发2.1 数据层开发(BookDao)Book实体类public class Book { private Integer id; private String type; private String name; private String description; //同学们自己添加getter、setter、toString()方法 }BookDao接口public interface BookDao { //@Insert("insert into tbl_book values(null,#{type},#{name},#{description})") @Insert("insert into tbl_book (type,name,description) values(#{type},#{name},#{description})") public int save(Book book); //返回值表示影响的行数 @Update("update tbl_book set type = #{type}, name = #{name}, description = #{description} where id = #{id}") public int update(Book book); @Delete("delete from tbl_book where id = #{id}") public int delete(Integer id); @Select("select * from tbl_book where id = #{id}") public Book getById(Integer id); @Select("select * from tbl_book") public List<Book> getAll(); }2.2 业务层开发(BookService/BookServiceImpl)BookService接口@Transactional //表示所有方法进行事务管理 public interface BookService { /** * 保存 * @param book * @return */ public boolean save(Book book); /** * 修改 * @param book * @return */ public boolean update(Book book); /** * 按id删除 * @param id * @return */ public boolean delete(Integer id); /** * 按id查询 * @param id * @return */ public Book getById(Integer id); /** * 查询全部 * @return */ public List<Book> getAll(); }BookServiceImpl实现类@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; public boolean save(Book book) { bookDao.save(book); return true; } public boolean update(Book book) { bookDao.update(book); return true; } public boolean delete(Integer id) { bookDao.delete(id); return true; } public Book getById(Integer id) { return bookDao.getById(id); } public List<Book> getAll() { return bookDao.getAll(); } }2.3 表现层开发(BookController)@RestController @RequestMapping("/books") public class BookController { @Autowired private BookService bookService; @PostMapping public boolean save(@RequestBody Book book) { return bookService.save(book); } @PutMapping public boolean update(@RequestBody Book book) { return bookService.update(book); } @DeleteMapping("/{id}") public boolean delete(@PathVariable Integer id) { return bookService.delete(id); } @GetMapping("/{id}") public Book getById(@PathVariable Integer id) { return bookService.getById(id); } @GetMapping public List<Book> getAll() { return bookService.getAll(); } }3 接口测试3.1 Spring整合Junit测试业务层方法@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class BookServiceTest { @Autowired private BookService bookService; @Test public void testGetById(){ Book book = bookService.getById(1); System.out.println(book); } @Test public void testGetAll(){ List<Book> all = bookService.getAll(); System.out.println(all); } }3.2 postman测试表现层接口测试保存图书这里就以保存图书为例，其他接口同学们自己测试二、表现层数据封装【重点】问题导入目前我们表现层响应给客户端的数据有哪几种？1 表现层响应数据的问题问题：我们表现层增删改方法返回true或者false表示是否成功，getById()方法返回一个json对象，getAll()方法返回一个json对象数组，这里就出现了三种格式的响应结果，极其不利于前端解析。解决：我们需要统一响应结果的格式2 定义Result类封装响应结果2.1 Result类封装响应结果public class Result { //描述统一格式中的数据 private Object data; //描述统一格式中的编码，用于区分操作，可以简化配置0或1表示成功失败 private Integer code; //描述统一格式中的消息，可选属性 private String msg; public Result() { } public Result(Integer code,Object data) { this.data = data; this.code = code; } public Result(Integer code, Object data, String msg) { this.data = data; this.code = code; this.msg = msg; } //同学们自己添加getter、setter、toString()方法 }注意事项：Result类中的字段并不是固定的，可以根据需要自行增减2.2 Code类封装响应码//状态码 public class Code { public static final Integer SAVE_OK = 20011; public static final Integer DELETE_OK = 20021; public static final Integer UPDATE_OK = 20031; public static final Integer GET_OK = 20041; public static final Integer SAVE_ERR = 20010; public static final Integer DELETE_ERR = 20020; public static final Integer UPDATE_ERR = 20030; public static final Integer GET_ERR = 20040; }注意事项：Code类的常量设计也不是固定的，可以根据需要自行增减，例如将查询再进行细分为GET_OK，GET_ALL_OK，GET_PAGE_OK3 表现层数据封装返回Result对象@RestController @RequestMapping("/books") public class BookController { @Autowired private BookService bookService; @PostMapping public Result save(@RequestBody Book book) { boolean flag = bookService.save(book); return new Result(flag ? Code.SAVE_OK:Code.SAVE_ERR,flag); } @PutMapping public Result update(@RequestBody Book book) { boolean flag = bookService.update(book); return new Result(flag ? Code.UPDATE_OK:Code.UPDATE_ERR,flag); } @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) { boolean flag = bookService.delete(id); return new Result(flag ? Code.DELETE_OK:Code.DELETE_ERR,flag); } @GetMapping("/{id}") public Result getById(@PathVariable Integer id) { Book book = bookService.getById(id); Integer code = book != null ? Code.GET_OK : Code.GET_ERR; String msg = book != null ? "" : "数据查询失败，请重试！"; return new Result(code,book,msg); } @GetMapping public Result getAll() { List<Book> bookList = bookService.getAll(); Integer code = bookList != null ? Code.GET_OK : Code.GET_ERR; String msg = bookList != null ? "" : "数据查询失败，请重试！"; return new Result(code,bookList,msg); } }三、异常处理器【理解】问题导入问题1：项目各个个层级均可能出现异常，异常处理代码书写在哪一层？1 异常介绍程序开发过程中不可避免的会遇到异常现象，我们不能让用户看到这样的页面数据出现异常现象的常见位置与常见诱因如下：框架内部抛出的异常：因使用不合规导致数据层抛出的异常：因外部服务器故障导致（例如：服务器访问超时）业务层抛出的异常：因业务逻辑书写错误导致（例如：遍历业务书写操作，导致索引异常等）表现层抛出的异常：因数据收集、校验等规则导致（例如：不匹配的数据类型间导致异常）工具类抛出的异常：因工具类书写不严谨不够健壮导致（例如：必要释放的连接长期未释放等）2 异常处理器2.2.1 编写异常处理器@RestControllerAdvice //用于标识当前类为REST风格对应的异常处理器 public class ProjectExceptionAdvice { //统一处理所有的Exception异常 @ExceptionHandler(Exception.class) public Result doOtherException(Exception ex){ return new Result(666,null); } }使用异常处理器之后的效果2.2.2 @RestControllerAdvice注解介绍名称：@RestControllerAdvice类型：[font color="#FF0000"]类注解[/font]位置：Rest风格开发的控制器增强类定义上方作用：为Rest风格开发的控制器类做增强说明：此注解自带@ResponseBody注解与@Component注解，具备对应的功能2.2.3 @ExceptionHandler注解介绍名称：@ExceptionHandler类型：[font color="#FF0000"]方法注解[/font]位置：专用于异常处理的控制器方法上方作用：设置指定异常的处理方案，功能等同于控制器方法，出现异常后终止原始控制器执行，并转入当前方法执行说明：此类方法可以根据处理的异常不同，制作多个方法分别处理对应的异常四、项目异常处理方案【理解】问题导入请说出项目当前异常的分类以及对应类型异常该如何处理？1 项目异常分类业务异常（BusinessException）规范的用户行为产生的异常不规范的用户行为操作产生的异常系统异常（SystemException）项目运行过程中可预计且无法避免的异常其他异常（Exception）编程人员未预期到的异常2 项目异常处理方案业务异常（BusinessException）[font color="#FF0000"]发送对应消息传递给用户，提醒规范操作[/font]系统异常（SystemException）[font color="#FF0000"]发送固定消息传递给用户，安抚用户[/font]发送特定消息给运维人员，提醒维护记录日志其他异常（Exception）[font color="#FF0000"]发送固定消息传递给用户，安抚用户[/font]发送特定消息给编程人员，提醒维护（纳入预期范围内）记录日志3 项目异常处理代码实现3.1 根据异常分类自定义异常类3.1.1 自定义项目系统级异常//自定义异常处理器，用于封装异常信息，对异常进行分类 public class SystemException extends RuntimeException{ private Integer code; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public SystemException(Integer code, String message) { super(message); this.code = code; } public SystemException(Integer code, String message, Throwable cause) { super(message, cause); this.code = code; } }3.1.2 自定义项目业务级异常//自定义异常处理器，用于封装异常信息，对异常进行分类 public class BusinessException extends RuntimeException{ private Integer code; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public BusinessException(Integer code, String message) { super(message); this.code = code; } public BusinessException(Integer code,String message,Throwable cause) { super(message, cause); this.code = code; } }3.2 自定义异常编码（持续补充）public class Code { //之前其他状态码省略没写，以下是新补充的状态码，可以根据需要自己补充 public static final Integer SYSTEM_ERR = 50001; public static final Integer SYSTEM_TIMEOUT_ERR = 50002; public static final Integer SYSTEM_UNKNOW_ERR = 59999; public static final Integer BUSINESS_ERR = 60002; }3.3 触发自定义异常@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; //在getById演示触发异常，其他方法省略没有写进来 public Book getById(Integer id) { //模拟业务异常，包装成自定义异常 if(id <0){ throw new BusinessException(Code.BUSINESS_ERR,"请不要使用你的技术挑战我的耐性!"); } } }3.4 在异常通知类中拦截并处理异常@RestControllerAdvice //用于标识当前类为REST风格对应的异常处理器 public class ProjectExceptionAdvice { //@ExceptionHandler用于设置当前处理器类对应的异常类型 @ExceptionHandler(SystemException.class) public Result doSystemException(SystemException ex){ //记录日志 //发送消息给运维 //发送邮件给开发人员,ex对象发送给开发人员 return new Result(ex.getCode(),null,ex.getMessage()); } @ExceptionHandler(BusinessException.class) public Result doBusinessException(BusinessException ex){ return new Result(ex.getCode(),null,ex.getMessage()); } //除了自定义的异常处理器，保留对Exception类型的异常处理，用于处理非预期的异常 @ExceptionHandler(Exception.class) public Result doOtherException(Exception ex){ //记录日志 //发送消息给运维 //发送邮件给开发人员,ex对象发送给开发人员 return new Result(Code.SYSTEM_UNKNOW_ERR,null,"系统繁忙，请稍后再试！"); } }测试：在postman中发送请求访问getById方法，传递参数-1，得到以下结果：五、SSM整合页面开发【重点】1 准备工作为了确保静态资源能够被访问到，需要设置静态资源过滤@Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**") .addResourceLocations("/pages/"); registry.addResourceHandler("/css/**") .addResourceLocations("/css/"); registry.addResourceHandler("/js/**") .addResourceLocations("/js/"); registry.addResourceHandler("/plugins/**") .addResourceLocations("/plugins/"); } }2 列表查询功能前端代码//列表 getAll() { //发送ajax请求 axios.get("/books").then((res)=>{ this.dataList = res.data.data; }); }3 添加功能前端代码//弹出添加窗口 handleCreate() { this.dialogFormVisible = true; this.resetForm(); }, //重置表单 resetForm() { this.formData = {}; }, //添加 handleAdd () { //发送ajax请求 axios.post("/books",this.formData).then((res)=>{ console.log(res.data); //如果操作成功，关闭弹层，显示数据 if(res.data.code == 20011){ this.dialogFormVisible = false; this.$message.success("添加成功"); }else if(res.data.code == 20010){ this.$message.error("添加失败"); }else{ this.$message.error(res.data.msg); } }).finally(()=>{ this.getAll(); }); },后台代码改进@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; //增删改的方法判断了影响的行数是否大于0，而不是固定返回true public boolean save(Book book) { return bookDao.save(book) > 0; } //增删改的方法判断了影响的行数是否大于0，而不是固定返回true public boolean update(Book book) { return bookDao.update(book) > 0; } //增删改的方法判断了影响的行数是否大于0，而不是固定返回true public boolean delete(Integer id) { return bookDao.delete(id) > 0; } public Book getById(Integer id) { if(id < 0){ throw new BusinessException(Code.BUSINESS_ERR,"请不要使用你的技术挑战我的耐性!"); return bookDao.getById(id); } } public List<Book> getAll() { return bookDao.getAll(); } }4 修改功能显示弹出框查询图书信息//弹出编辑窗口 handleUpdate(row) { // console.log(row); //row.id 查询条件 //查询数据，根据id查询 axios.get("/books/"+row.id).then((res)=>{ // console.log(res.data.data); if(res.data.code == 20041){ //展示弹层，加载数据 this.formData = res.data.data; this.dialogFormVisible4Edit = true; }else{ this.$message.error(res.data.msg); } }); }保存修改后的图书信息//编辑 handleEdit() { //发送ajax请求 axios.put("/books",this.formData).then((res)=>{ //如果操作成功，关闭弹层，显示数据 if(res.data.code == 20031){ this.dialogFormVisible4Edit = false; this.$message.success("修改成功"); }else if(res.data.code == 20030){ this.$message.error("修改失败"); }else{ this.$message.error(res.data.msg); } }).finally(()=>{ this.getAll(); }); }5 删除功能// 删除 handleDelete(row) { //1.弹出提示框 this.$confirm("此操作永久删除当前数据，是否继续？","提示",{ type:'info' }).then(()=>{ //2.做删除业务 axios.delete("/books/"+row.id).then((res)=>{ if(res.data.code == 20021){ this.$message.success("删除成功"); }else{ this.$message.error("删除失败"); } }).finally(()=>{ this.getAll(); }); }).catch(()=>{ //3.取消删除 this.$message.info("取消删除操作"); }); }六、拦截器【理解】1 拦截器简介问题导入问题1：拦截器拦截的对象是谁？问题2：拦截器和过滤器有什么区别？1.1 拦截器概念和作用拦截器（Interceptor）是一种动态拦截方法调用的机制，在SpringMVC中动态拦截控制器方法的执行作用：在指定的方法调用前后执行预先设定的代码阻止原始方法的执行总结：增强核心原理：AOP思想1.2 拦截器和过滤器的区别归属不同：Filter属于Servlet技术，Interceptor属于SpringMVC技术拦截内容不同：Filter对所有访问进行增强，Interceptor仅针对SpringMVC的访问进行增强2 入门案例问题导入定义拦截器需要实现什么接口？2.1 拦截器代码实现【第一步】定义拦截器做法：定义一个类，实现HandlerInterceptor接口即可@Component //注意当前类必须受Spring容器控制 //定义拦截器类，实现HandlerInterceptor接口 public class ProjectInterceptor implements HandlerInterceptor { @Override //原始方法调用前执行的内容 //返回值类型可以拦截控制的执行，true放行，false终止 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle..."+contentType); return true; } @Override //原始方法调用后执行的内容 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); } @Override //原始方法调用完成后执行的内容 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); } }【第二步】配置加载拦截器@Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { @Autowired private ProjectInterceptor projectInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { //配置拦截器 registry.addInterceptor(projectInterceptor) .addPathPatterns("/books","/books/*"); } }使用标准接口WebMvcConfigurer简化开发（注意：侵入式较强）@Configuration @ComponentScan({"com.itheima.controller"}) @EnableWebMvc //实现WebMvcConfigurer接口可以简化开发，但具有一定的侵入性 public class SpringMvcConfig implements WebMvcConfigurer { @Autowired private ProjectInterceptor projectInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //配置多拦截器 registry.addInterceptor(projectInterceptor) .addPathPatterns("/books","/books/*"); } }2.2 拦截器流程分析3 拦截器参数问题导入postHandle()和afterCompletion()方法都是处理器方法执行之后执行，有什么区别？3.1 前置处理//原始方法调用前执行的内容 //返回值类型可以拦截控制的执行，true放行，false终止 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle..."+contentType); return true; }参数request:请求对象response:响应对象handler:被调用的处理器对象，本质上是一个方法对象，对反射技术中的Method对象进行了再包装返回值返回值为false，被拦截的处理器将不执行。3.2 后置处理//原始方法调用后执行的内容 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); }参数modelAndView：如果处理器执行完成具有返回结果，可以读取到对应数据与页面信息，并进行跳转注意：如果处理器方法出现异常了，该方法不会执行3.3 完成后处理//原始方法调用完成后执行的内容 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); }参数ex:如果处理器执行过程中出现异常对象，可以针对异常情况进行单独处理注意：无论处理器方法内部是否出现异常，该方法都会执行。4 拦截器链配置问题导入什么是拦截器链？4.1 多个拦截器配置定义第二个拦截器@Component public class ProjectInterceptor2 implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle...222"); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle...222"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion...222"); } }配置第二个拦截器@Configuration @ComponentScan({"com.itheima.controller"}) @EnableWebMvc //实现WebMvcConfigurer接口可以简化开发，但具有一定的侵入性 public class SpringMvcConfig implements WebMvcConfigurer { @Autowired private ProjectInterceptor projectInterceptor; @Autowired private ProjectInterceptor2 projectInterceptor2; @Override public void addInterceptors(InterceptorRegistry registry) { //配置多拦截器 registry.addInterceptor(projectInterceptor) .addPathPatterns("/books","/books/*"); registry.addInterceptor(projectInterceptor2) .addPathPatterns("/books","/books/*"); } }4.2 多个连接器工作流程分析当配置多个拦截器时，形成拦截器链拦截器链的运行顺序参照拦截器添加顺序为准当拦截器中出现对原始处理器的拦截，后面的拦截器均终止运行当拦截器运行中断，仅运行配置在前面的拦截器的afterCompletion操作
今日目标能够知道SpringMVC的优点能够编写SpringMVC入门案例能够使用PostMan发送请求能够掌握普通类型参数传递能够掌握POJO类型参数传递能够掌握json数据参数传递能够掌握响应json数据能够掌握rest风格快速开发能够完成基于restful页面数据交互案例一、SpringMVC简介1 SpringMVC概述问题导入SpringMVC框架有什么优点？1.1 SpringMVC概述SpringMVC是一种基于Java实现MVC模型的轻量级Web框架优点使用简单，开发便捷（相比于Servlet）灵活性强2 入门案例【重点】问题导入在Controller中如何定义访问路径，如何响应数据？2.1 实现步骤1 创建web工程（Maven结构） 2 设置tomcat服务器，加载web工程(tomcat插件） 3 导入坐标（SpringMVC+Servlet） 4 定义处理请求的功能类（UserController） 5 编写SpringMVC配置类，加载处理请求的Bean。 6 加载SpringMVC配置，并设置SpringMVC请求拦截的路径2.2 代码实现【第一步】创建web工程（Maven结构）【第二步】设置tomcat服务器，加载web工程(tomcat插件）<build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.1</version> <configuration> <port>80</port> <path>/</path> </configuration> </plugin> </plugins> </build>【第三步】导入坐标（SpringMVC+Servlet）<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>注意事项：课程版本基于Spring主版本5.2.10.RELEASE制作导入spring-webmvc坐标自动依赖spring相关坐标【第四步】定义处理请求的功能类（UserController）//定义表现层控制器bean @Controller public class UserController { //设置映射路径为/save，即外部访问路径 @RequestMapping("/save") //设置当前操作返回结果为指定json数据（本质上是一个字符串信息） @ResponseBody public String save(){ System.out.println("user save ..."); return "{'info':'springmvc'}"; } }注意事项：[scode type="yellow" size=""]对于SpringMVC而言，Controller方法返回值默认表示要跳转的页面，没有对应的页面就会报错。如果不想跳转页面而是响应数据，那么就需要在方法上使用@ResponseBody注解。[/scode]【第五步】编写SpringMVC配置类，加载处理请求的Bean。//springmvc配置类，本质上还是一个spring配置类 @Configuration @ComponentScan("com.itheima.controller") public class SpringMvcConfig { }【第六步】加载SpringMVC配置，并设置SpringMVC请求拦截的路径//web容器配置类 public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { //加载springmvc配置类，产生springmvc容器（本质还是spring容器） protected WebApplicationContext createServletApplicationContext() { //初始化WebApplicationContext对象 AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); //加载指定配置类 ctx.register(SpringMvcConfig.class); return ctx; } //设置由springmvc控制器处理的请求映射路径 protected String[] getServletMappings() { return new String[]{"/"}; } //加载spring配置类 protected WebApplicationContext createRootApplicationContext() { return null; } }2.3 运行结果2.4 案例注解和类解析2.4.1 @Controller注解名称：@Controller类型：类注解位置：SpringMVC控制器类定义上方作用：设定SpringMVC的核心控制器bean范例@Controller public class UserController { }2.4.2 @RequestMapping注解名称：@RequestMapping类型：方法注解位置：SpringMVC控制器方法定义上方作用：设置当前控制器方法请求访问路径范例@RequestMapping("/save") public void save(){ System.out.println("user save ..."); }注意：其实@RequestMapping注解还可以写到类上面，笔记后面会介绍到。2.4.3 @ResponseBody注解名称：@ResponseBody类型：方法注解位置：SpringMVC控制器方法定义上方作用：设置当前控制器方法响应内容为当前返回值，无需解析范例@RequestMapping("/save") @ResponseBody public String save(){ System.out.println("user save ..."); return "{'info':'springmvc'}"; }2.4.4 AbstractDispatcherServletInitializer类AbstractDispatcherServletInitializer类是SpringMVC提供的快速初始化Web3.0容器的抽象类AbstractDispatcherServletInitializer提供三个接口方法供用户实现createServletApplicationContext()方法，创建Servlet容器时，加载SpringMVC对应的bean并放入WebApplicationContext对象范围中，而WebApplicationContext的作用范围为ServletContext范围，即整个web容器范围。//加载springmvc配置类，产生springmvc容器（本质还是spring容器） protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(SpringMvcConfig.class); return ctx; }getServletMappings()方法，设定SpringMVC对应的请求映射路径，设置为/表示拦截所有请求，任意请求都将转入到SpringMVC进行处理。//设置由springmvc控制器处理的请求映射路径 protected String[] getServletMappings() { return new String[]{"/"}; }createRootApplicationContext()方法，如果创建Servlet容器时需要加载非SpringMVC对应的bean，使用当前方法进行，使用方式同createServletApplicationContext()//加载spring配置类 protected WebApplicationContext createRootApplicationContext() { return null; }2.5 入门程序开发总结(1+N)一次性工作创建工程，设置服务器，加载工程导入坐标创建web容器启动类，加载SpringMVC配置，并设置SpringMVC请求拦截路径SpringMVC核心配置类（设置配置类，扫描controller包，加载Controller控制器bean）多次工作定义处理请求的控制器类定义处理请求的控制器方法，并配置映射路径（@RequestMapping）与返回json数据（@ResponseBody）3 入门案例工作流程分析【理解】3.1 启动服务器初始化过程服务器启动，执行ServletContainersInitConfig类，初始化web容器执行createServletApplicationContext方法，创建了WebApplicationContext对象加载SpringMvcConfig配置类执行@ComponentScan加载对应的bean加载UserController，每个@RequestMapping的名称对应一个具体的方法执行getServletMappings方法，定义所有的请求都通过SpringMVC3.2 单次请求过程发送请求localhost/saveweb容器发现所有请求都经过SpringMVC，将请求交给SpringMVC处理解析请求路径/save由/save匹配执行对应的方法save(）执行save()检测到有@ResponseBody直接将save()方法的返回值作为响应求体返回给请求方4 Controller加载控制问题导入因为功能不同，如何避免Spring错误的加载到SpringMVC的bean？4.1 Controller加载控制与业务bean加载控制SpringMVC相关bean（表现层bean）Spring控制的bean业务bean（Service）功能bean（DataSource等）SpringMVC相关bean加载控制SpringMVC加载的bean对应的包均在com.itheima.controller包内Spring相关bean加载控制方式一：Spring加载的bean设定扫描范围为com.itheima，排除掉controller包内的bean方式二：Spring加载的bean设定扫描范围为精准范围，例如service包、dao包等方式三：不区分Spring与SpringMVC的环境，加载到同一个环境中4.2 方式一代码实现名称：@ComponentScan类型：类注解范例@Configuration @ComponentScan(value = "com.itheima", excludeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = Controller.class ) ) public class SpringConfig { }属性excludeFilters：排除扫描路径中加载的bean，需要指定类别（type）与具体项（classes）includeFilters：加载指定的bean，需要指定类别（type）与具体项（classes）4.3 Bean的加载格式public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(SpringMvcConfig.class); return ctx; } protected WebApplicationContext createRootApplicationContext() { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(SpringConfig.class); return ctx; } protected String[] getServletMappings() { return new String[]{"/"}; } }简化格式public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer{ protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class} }; protected String[] getServletMappings() { return new String[]{"/"}; } protected Class<?>[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } }5 PostMan【掌握】5.1 PostMan介绍Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。作用：常用于进行接口测试特征简单实用美观大方5.2 PostMan安装双击资料中的“Postman-win64-8.3.1-Setup.exe”即可自动按照，打开之后需要注册，如果底部有如下链接，可以点击跳过注册5.3 PostMan的使用5.3.1 创建WorkSpace工作空间5.3.2 发送请求获取json数据5.3.3 保存当前请求注意：第一次请求需要创建一个新的目录，后面就不需要创建新目录，直接保存到已经创建好的目录即可。二、请求与响应1 请求映射路径【重点】问题导入@RequestMapping注解注解可以写在哪？有什么作用？1.1 @RequestMapping注解名称：@RequestMapping类型：方法注解 类注解位置：SpringMVC控制器方法定义上方作用：设置当前控制器方法请求访问路径，如果设置在类上统一设置当前控制器方法请求访问路径前缀范例@Controller //类上方配置的请求映射与方法上面配置的请求映射连接在一起，形成完整的请求映射路径 @RequestMapping("/user") public class UserController { //请求路径映射 @RequestMapping("/save") //此时save方法的访问路径是：/user/save @ResponseBody public String save(){ System.out.println("user save ..."); return "{'module':'user save'}"; } }2 请求参数2.1 发送普通类型参数【重点】问题导入如何解决POST请求中文乱码问题？2.1.1 请求方式GET请求POST请求2.1.2 GET请求传递普通参数普通参数：url地址传参，地址参数名与形参变量名相同，定义形参即可接收参数//普通参数：请求参数与形参名称对应即可完成参数传递 @RequestMapping("/commonParam") @ResponseBody public String commonParam(String name ,int age){ System.out.println("普通参数传递 name ==> "+name); System.out.println("普通参数传递 age ==> "+age); return "{'module':'common param'}"; }问题：如果同学们传递的参数是中文试试，你们会发现接收到的参数出现了中文乱码问题。原因：tomcat 8.5版本之后GET请求就不再出现中文乱码问题，但是我们使用的是tomcat7插件，所以会出现GET请求中文乱码问题。解决：在pom.xml添加tomcat7插件处配置UTF-8字符集，解决GET请求中文乱码问题。<build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.1</version> <configuration> <port>80</port> <path>/</path> <uriEncoding>UTF-8</uriEncoding> </configuration> </plugin> </plugins> </build>2.1.3 POST请求传递普通参数普通参数：form表单post请求传参，表单参数名与形参变量名相同，定义形参即可接收参数//普通参数：请求参数与形参名称对应即可完成参数传递 @RequestMapping("/commonParam") @ResponseBody public String commonParam(String name ,int age){ System.out.println("普通参数传递 name ==> "+name); System.out.println("普通参数传递 age ==> "+age); return "{'module':'common param'}"; }问题：我们发现，POST请求传递的参数如果包含中文那么就会出现中文乱码问题，说明我们之前配置的tomcat插件uri路径编解码字符集无法解决POST请求中文乱码问题。那么如何解决呢？2.1.4 POST请求中文乱码处理在加载SpringMVC配置的配置类中指定字符过滤器。public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[0]; } protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } //乱码处理 @Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); return new Filter[]{filter}; } }2.2 五种类型参数传递问题导入当请求参数名与形参变量名不同，该如何接收请求参数？2.2.1 五种类型参数介绍普通参数POJO类型参数嵌套POJO类型参数数组类型参数集合类型参数2.2.2 普通参数【重点】普通参数：当请求参数名与形参变量名不同，使用@RequestParam绑定参数关系//普通参数：请求参数名与形参名不同时，使用@RequestParam注解关联请求参数名称与形参名称之间的关系 @RequestMapping("/commonParamDifferentName") @ResponseBody public String commonParamDifferentName(@RequestParam("name") String userName , int age){ System.out.println("普通参数传递 userName ==> "+userName); System.out.println("普通参数传递 age ==> "+age); return "{'module':'common param different name'}"; }名称：@RequestParam类型：形参注解位置：SpringMVC控制器方法形参定义前面作用：绑定请求参数与处理器方法形参间的关系参数：required：是否为必传参数defaultValue：参数默认值2.2.3 POJO类型参数【重点】POJO参数：请求参数名与形参对象属性名相同，定义POJO类型形参即可接收参数public class User { private String name; private int age; //同学们自己添加getter/setter/toString()方法 }//POJO参数：请求参数与形参对象中的属性对应即可完成参数传递 @RequestMapping("/pojoParam") @ResponseBody public String pojoParam(User user){ System.out.println("pojo参数传递 user ==> "+user); return "{'module':'pojo param'}"; }[scode type="yellow" size=""]注意事项：请求参数key的名称要和POJO中属性的名称一致，否则无法封装。[/scode]2.2.4 嵌套POJO类型参数POJO对象中包含POJO对象public class User { private String name; private int age; private Address address; //同学们自己添加getter/setter/toString()方法 } public class Address { private String province; private String city; private Address address; }嵌套POJO参数：请求参数名与形参对象属性名相同，按照对象层次结构关系即可接收嵌套POJO属性参数//嵌套POJO参数：嵌套属性按照层次结构设定名称即可完成参数传递 @RequestMapping("/pojoContainPojoParam") @ResponseBody public String pojoContainPojoParam(User user){ System.out.println("pojo嵌套pojo参数传递 user ==> "+user); return "{'module':'pojo contain pojo param'}"; }[scode type="yellow" size=""]注意事项：请求参数key的名称要和POJO中属性的名称一致，否则无法封装。[/scode]2.2.5 数组类型参数数组参数：请求参数名与形参对象属性名相同且请求参数为多个，定义数组类型即可接收参数//数组参数：同名请求参数可以直接映射到对应名称的形参数组对象中 @RequestMapping("/arrayParam") @ResponseBody public String arrayParam(String[] likes){ System.out.println("数组参数传递 likes ==> "+ Arrays.toString(likes)); return "{'module':'array param'}"; }2.2.6 集合类型参数集合保存普通参数：请求参数名与形参集合对象名相同且请求参数为多个，@RequestParam绑定参数关系//集合参数：同名请求参数可以使用@RequestParam注解映射到对应名称的集合对象中作为数据 @RequestMapping("/listParam") @ResponseBody public String listParam(@RequestParam List<String> likes){ System.out.println("集合参数传递 likes ==> "+ likes); return "{'module':'list param'}"; }2.3 json数据参数传递问题导入问题：@EnableWebMvc注解和@ResponseBody注解有什么用？2.3.1 json数据参数介绍json普通数组（["","","",...]）json对象（{key:value,key:value,...}）json对象数组（[{key:value,...},{key:value,...}]）2.3.2 传递json普通数组2.3.2.1 代码演示添加json数据转换相关坐标<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency>设置发送json数据（请求body中添加json数据）开启自动转换json数据的支持@Configuration @ComponentScan("com.itheima.controller") //开启json数据类型自动转换 @EnableWebMvc public class SpringMvcConfig { }注意事项：@EnableWebMvc注解功能强大，该注解整合了多个功能，此处仅使用其中一部分功能，即json数据进行自动类型转换在Controller中编写方法接收json参数//集合参数：json格式 //1.开启json数据格式的自动转换，在配置类中开启@EnableWebMvc //2.使用@RequestBody注解将外部传递的json数组数据映射到形参的集合对象中作为数据 @RequestMapping("/listParamForJson") @ResponseBody public String listParamForJson(@RequestBody List<String> likes){ System.out.println("list common(json)参数传递 list ==> "+likes); return "{'module':'list common for json param'}"; }2.3.2.2 @EnableWebMvc注解介绍名称：@EnableWebMvc类型：配置类注解位置：SpringMVC配置类定义上方作用：开启SpringMVC多项辅助功能范例：@Configuration @ComponentScan("com.itheima.controller") @EnableWebMvc public class SpringMvcConfig { }2.3.2.3 @RequestBody注解介绍名称：@RequestBody类型：形参注解位置：SpringMVC控制器方法形参定义前面作用：将请求中请求体所包含的数据传递给请求参数，此注解一个处理器方法只能使用一次范例：@RequestMapping("/listParamForJson") @ResponseBody public String listParamForJson(@RequestBody List<String> likes){ System.out.println("list common(json)参数传递 list ==> "+likes); return "{'module':'list common for json param'}"; }2.3.3 传递json对象POJO参数：json数据与形参对象属性名相同，定义POJO类型形参即可接收参数//POJO参数：json格式 //1.开启json数据格式的自动转换，在配置类中开启@EnableWebMvc //2.使用@RequestBody注解将外部传递的json数据映射到形参的实体类对象中，要求属性名称一一对应 @RequestMapping("/pojoParamForJson") @ResponseBody public String pojoParamForJson(@RequestBody User user){ System.out.println("pojo(json)参数传递 user ==> "+user); return "{'module':'pojo for json param'}"; }2.3.4 传递json对象数组POJO集合参数：json数组数据与集合泛型属性名相同，定义List类型形参即可接收参数//集合参数：json格式 //1.开启json数据格式的自动转换，在配置类中开启@EnableWebMvc //2.使用@RequestBody注解将外部传递的json数组数据映射到形参的保存实体类对象的集合对象中，要求属性名称一一对应 @RequestMapping("/listPojoParamForJson") @ResponseBody public String listPojoParamForJson(@RequestBody List<User> list){ System.out.println("list pojo(json)参数传递 list ==> "+list); return "{'module':'list pojo for json param'}"; }2.3.5 @RequestBody与@RequestParam区别区别@RequestParam用于接收url地址传参，表单传参【application/x-www-form-urlencoded】@RequestBody用于接收json数据【application/json】应用后期开发中，发送json格式数据为主，@RequestBody应用较广如果发送非json格式数据，选用@RequestParam接收请求参数3 日期类型参数传递【重点】问题导入@DateTimeFormat注解的作用是什么？3.1 代码演示日期类型数据基于系统不同格式也不尽相同[scode type="blue" size="simple"]2088-08-182088/08/1808/18/2088[/scode]//日期参数 http://localhost:80/dataParam?date=2088/08/08&date1=2088-08-18&date2=2088/08/28 8:08:08 //使用@DateTimeFormat注解设置日期类型数据格式，默认格式yyyy/MM/dd @RequestMapping("/dataParam") @ResponseBody public String dataParam(Date date, @DateTimeFormat(pattern="yyyy-MM-dd") Date date1, @DateTimeFormat(pattern="yyyy/MM/dd HH:mm:ss") Date date2){ System.out.println("参数传递 date ==> "+date); System.out.println("参数传递 date1(yyyy-MM-dd) ==> "+date1); System.out.println("参数传递 date2(yyyy/MM/dd HH:mm:ss) ==> "+date2); return "{'module':'data param'}"; }3.2 @DateTimeFormat注解介绍名称：@DateTimeFormat类型：形参注解位置：SpringMVC控制器方法形参前面作用：设定日期时间型数据格式属性：pattern：指定日期时间格式字符串3.3 工作原理其内部依赖Converter接口public interface Converter<S, T> { @Nullable T convert(S var1); }请求参数年龄数据（String→Integer）json数据转对象（json → POJO）日期格式转换（String → Date）3.4 注意事项传递日期类型参数必须在配置类上使用@EnableWebMvc注解。其功能之一：根据类型匹配对应的类型转换器。4 响应问题导入如何响应json数据？4.1响应页面【了解】@Controller public class UserController { //响应页面/跳转页面 //返回值为String类型，设置返回值为页面名称，即可实现页面跳转 @RequestMapping("/toJumpPage") public String toJumpPage(){ System.out.println("跳转页面"); return "page.jsp"; } }<%@ page contentType="text/html;charset=UTF-8" language="java" %> Title Hello Spring MVC! 4.2 文本数据【了解】//响应文本数据 //返回值为String类型，设置返回值为任意字符串信息，即可实现返回指定字符串信息，需要依赖@ResponseBody注解 @RequestMapping("/toText") @ResponseBody public String toText(){ System.out.println("返回纯文本数据"); return "response text"; }4.3 json数据【重点】//响应POJO对象 //返回值为实体类对象，设置返回值为实体类类型，即可实现返回对应对象的json数据，需要依赖@ResponseBody注解和@EnableWebMvc注解 @RequestMapping("/toJsonPOJO") @ResponseBody public User toJsonPOJO(){ System.out.println("返回json对象数据"); User user = new User(); user.setName("itcast"); user.setAge(15); return user; }//响应POJO集合对象 //返回值为集合对象，设置返回值为集合类型，即可实现返回对应集合的json数组数据，需要依赖@ResponseBody注解和@EnableWebMvc注解 @RequestMapping("/toJsonList") @ResponseBody public List<User> toJsonList(){ System.out.println("返回json集合数据"); User user1 = new User(); user1.setName("传智播客"); user1.setAge(15); User user2 = new User(); user2.setName("黑马程序员"); user2.setAge(12); List<User> userList = new ArrayList<User>(); userList.add(user1); userList.add(user2); return userList; }[scode type="yellow" size=""]注意：需要添加jackson-databind依赖以及在SpringMvcConfig配置类上添加@EnableWebMvc注解[/scode]三、REST风格1 REST简介问题导入Rest风格使用了几种请求方式定义访问行为？1.1 REST介绍REST（Representational State Transfer），表现形式状态转换传统风格资源描述形式http://localhost/user/getById?id=1http://localhost/user/saveUserREST风格描述形式http://localhost/user/1http://localhost/user优点：隐藏资源的访问行为，无法通过地址得知对资源是何种操作书写简化1.2 RESTful介绍按照REST风格访问资源时使用行为动作区分对资源进行了何种操作http://localhost/users 查询全部用户信息 （查询）http://localhost/users/1 查询指定用户信息（查询）http://localhost/users 添加用户信息（新增/保存）http://localhost/users 修改用户信息（修改/更新）http://localhost/users/1 删除用户信息（删除）根据REST风格对资源进行访问称为RESTful1.3 注意事项上述行为是约定方式，约定不是规范，可以打破，所以称REST风格，而不是REST规范描述模块的名称通常使用复数，也就是加s的格式描述，表示此类资源，而非单个资源，例如：users、books、accounts……2 RESTful入门案例问题导入Rest风格如何通过路径传递参数？2.1 快速入门做法：在Controller中定义方法时设定"http请求动作(请求方式)"和"设定请求参数（路径变量）"@Controller public class UserController { //设置当前请求方法为POST，表示REST风格中的添加操作 @RequestMapping(value = "/users",method = RequestMethod.POST) @ResponseBody public String save(){ System.out.println("user save..."); return "{'module':'user save'}"; } //设置当前请求方法为DELETE，表示REST风格中的删除操作 //@PathVariable注解用于设置路径变量（路径参数），要求路径上设置对应的占位符，并且占位符名称与方法形参名称相同 @RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE) @ResponseBody public String delete(@PathVariable Integer id){ System.out.println("user delete..." + id); return "{'module':'user delete'}"; } //设置当前请求方法为PUT，表示REST风格中的修改操作 @RequestMapping(value = "/users",method = RequestMethod.PUT) @ResponseBody public String update(@RequestBody User user){ System.out.println("user update..."+user); return "{'module':'user update'}"; } //设置当前请求方法为GET，表示REST风格中的查询操作 //@PathVariable注解用于设置路径变量（路径参数），要求路径上设置对应的占位符，并且占位符名称与方法形参名称相同 @RequestMapping(value = "/users/{id}" ,method = RequestMethod.GET) @ResponseBody public String getById(@PathVariable Integer id){ System.out.println("user getById..."+id); return "{'module':'user getById'}"; } //设置当前请求方法为GET，表示REST风格中的查询操作 @RequestMapping(value = "/users",method = RequestMethod.GET) @ResponseBody public String getAll(){ System.out.println("user getAll..."); return "{'module':'user getAll'}"; } }2.2 @PathVariable介绍名称：@PathVariable类型：形参注解位置：SpringMVC控制器方法形参定义前面作用：绑定路径参数与处理器方法形参间的关系，要求路径参数名与形参名一一对应2.3 @RequestBody、@RequestParam、@PathVariable区别和应用区别@RequestParam用于接收url地址传参或表单传参@RequestBody用于接收json数据@PathVariable用于接收路径参数，使用{参数名称}描述路径参数应用后期开发中，发送请求参数超过1个时，以json格式为主，@RequestBody应用较广如果发送非json格式数据，选用@RequestParam接收请求参数采用RESTful进行开发，当参数数量较少时，例如1个，可以采用@PathVariable接收请求路径变量，通常用于传递id值3 REST快速开发【重点】3.1 代码中的问题以上截图中的代码和我们之前写的UserController中的方法类似，其中图中两个方法都有三处是有问题的，可以进行优化。存在的问题如下：问题1：每个方法的@RequestMapping注解中都定义了访问路径/books，重复性太高。问题2：每个方法的@RequestMapping注解中都要使用method属性定义请求方式，重复性太高。问题3：每个方法响应json都需要加上@ResponseBody注解，重复性太高。3.2 Rest快速开发解决以上三个问题解决问题1：在Controller类上使用@RequestMapping定义共同的访问路径。@Controller @RequestMapping("/books") public class BookController { @RequestMapping(method = RequestMethod.POST) public String save(@RequestBody Book book){ System.out.println("book save..." + book); return "{'module':'book save'}"; } @RequestMapping(value = "/{id}" ,method = RequestMethod.DELETE) public String delete(@PathVariable Integer id){ System.out.println("book delete..." + id); return "{'module':'book delete'}"; } @RequestMapping(method = RequestMethod.PUT) public String update(@RequestBody Book book){ System.out.println("book update..."+book); return "{'module':'book update'}"; } @RequestMapping(value = "/{id}" ,method = RequestMethod.GET) public String getById(@PathVariable Integer id){ System.out.println("book getById..."+id); return "{'module':'book getById'}"; } @RequestMapping(method = RequestMethod.GET) public String getAll(){ System.out.println("book getAll..."); return "{'module':'book getAll'}"; } }解决问题2：使用@GetMapping @PostMapping @PutMapping @DeleteMapping代替@RequestMapping(method=RequestMethod.XXX)@Controller @RequestMapping("/books") public class BookController { // @RequestMapping( method = RequestMethod.POST) @PostMapping//使用@PostMapping简化Post请求方法对应的映射配置 public String save(@RequestBody Book book){ System.out.println("book save..." + book); return "{'module':'book save'}"; } // @RequestMapping(value = "/{id}" ,method = RequestMethod.DELETE) @DeleteMapping("/{id}") //使用@DeleteMapping简化DELETE请求方法对应的映射配置 public String delete(@PathVariable Integer id){ System.out.println("book delete..." + id); return "{'module':'book delete'}"; } // @RequestMapping(method = RequestMethod.PUT) @PutMapping //使用@PutMapping简化Put请求方法对应的映射配置 public String update(@RequestBody Book book){ System.out.println("book update..."+book); return "{'module':'book update'}"; } // @RequestMapping(value = "/{id}" ,method = RequestMethod.GET) @GetMapping("/{id}") //使用@GetMapping简化GET请求方法对应的映射配置 public String getById(@PathVariable Integer id){ System.out.println("book getById..."+id); return "{'module':'book getById'}"; } // @RequestMapping(method = RequestMethod.GET) @GetMapping //使用@GetMapping简化GET请求方法对应的映射配置 public String getAll(){ System.out.println("book getAll..."); return "{'module':'book getAll'}"; } }名称：@GetMapping @PostMapping @PutMapping @DeleteMapping类型：方法注解位置：基于SpringMVC的RESTful开发控制器方法定义上方作用：设置当前控制器方法请求访问路径与请求动作，每种对应一个请求动作，例如@GetMapping对应GET请求属性：value（默认）：请求访问路径解决问题3：在Controller类上使用@RestController注解，等同于@Controller与@ResponseBody两个注解组合功能@RestController //使用@RestController注解替换@Controller与@ResponseBody注解，简化书写 @RequestMapping("/books") public class BookController { //方法省略了没写 }名称：@RestController类型：类注解位置：基于SpringMVC的RESTful开发控制器类定义上方作用：设置当前控制器类为RESTful风格，等同于@Controller与@ResponseBody两个注解组合功能4案例：基于RESTful页面数据交互4.1 案例效果和环境准备4.1.1 案例效果4.1.2 环境准备//POJO实体类 public class Book { private Integer id; private String type; private String name; private String description; //同学们自己重写getter、setter、toString()方法... } //SpringMVC容器初始化类 public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[0]; } protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } //乱码处理 @Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); return new Filter[]{filter}; } } //SpringMVC配置类 @Configuration @ComponentScan({"com.itheima.controller","com.itheima.config"}) @EnableWebMvc public class SpringMvcConfig { }4.2 代码实现4.2.1 制作SpringMVC控制器，并通过PostMan测试接口功能@RestController @RequestMapping("/books") public class BookController { @PostMapping public String save(@RequestBody Book book){ System.out.println("book save ==> "+ book); return "{'module':'book save success'}"; } @GetMapping public List<Book> getAll(){ System.out.println("book getAll is running ..."); List<Book> bookList = new ArrayList<Book>(); Book book1 = new Book(); book1.setType("计算机"); book1.setName("SpringMVC入门教程"); book1.setDescription("小试牛刀"); bookList.add(book1); Book book2 = new Book(); book2.setType("计算机"); book2.setName("SpringMVC实战教程"); book2.setDescription("一代宗师"); bookList.add(book2); Book book3 = new Book(); book3.setType("计算机丛书"); book3.setName("SpringMVC实战教程进阶"); book3.setDescription("一代宗师呕心创作"); bookList.add(book3); return bookList; } }4.2.2 设置对静态资源的访问放行@Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { //设置静态资源访问过滤，当前类需要设置为配置类，并被扫描加载 @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { //当访问/pages/????时候，从/pages目录下查找内容 registry.addResourceHandler("/pages/**") .addResourceLocations("/pages/"); registry.addResourceHandler("/js/**") .addResourceLocations("/js/"); registry.addResourceHandler("/css/**") .addResourceLocations("/css/"); registry.addResourceHandler("/plugins/**") .addResourceLocations("/plugins/"); } }4.2.3 前端页面通过异步提交访问后台控制器//添加 saveBook () { axios.post("/books",this.formData).then((res)=>{ }); }, //主页列表查询 getAll() { axios.get("/books").then((res)=>{ this.dataList = res.data; }); },
今日目标能够理解AOP的作用能够完成AOP的入门案例能够理解AOP的工作流程能够说出AOP的五种通知类型能够完成"测量业务层接口万次执行效率"案例能够掌握Spring事务配置一、AOP1 AOP简介问题导入[scode type="yellow" size=""]问题1：AOP的作用是什么？问题2：连接点和切入点有什么区别，二者谁的范围大？问题3：请描述什么是切面？[/scode]1.1 AOP简介和作用【理解】AOP(Aspect Oriented Programming)面向切面编程，一种编程范式，指导开发者如何组织程序结构OOP(Object Oriented Programming)面向对象编程作用：在不惊动原始设计的基础上为其进行功能增强。简单的说就是在不改变方法源代码的基础上对方法进行功能增强。Spring理念：无入侵式/无侵入式1.2 AOP中的核心概念【理解】连接点（JoinPoint）：正在执行的方法，例如：update()、delete()、select()等都是连接点。切入点（Pointcut）：进行功能增强了的方法，例如:update()、delete()方法，select()方法没有被增强所以不是切入点，但是是连接点。在SpringAOP中，一个切入点可以只描述一个具体方法，也可以匹配多个方法一个具体方法：com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法匹配多个方法：所有的save方法，所有的get开头的方法，所有以Dao结尾的接口中的任意方法，所有带有一个参数的方法通知（Advice）：在切入点前后执行的操作，也就是增强的共性功能在SpringAOP中，功能最终以方法的形式呈现通知类：通知方法所在的类叫做通知类切面（Aspect）：描述通知与切入点的对应关系，也就是哪些通知方法对应哪些切入点方法。2 AOP入门案例【重点】问题导入[scode type="yellow" size=""]问题1：在通知方法中如何定义切入点表达式？问题2：如何配置切面？问题3：在配置类上如何开启AOP注解功能？[/scode]2.1 AOP入门案例思路分析案例设定：测定接口执行效率简化设定：在接口执行前输出当前系统时间开发模式：XML or [font color="#FF0000"]注解[/font]思路分析：导入坐标（pom.xml）制作连接点方法（原始操作，dao接口与实现类）制作共性功能（通知类与通知）定义切入点绑定切入点与通知关系（切面）2.2 AOP入门案例实现【第一步】导入aop相关坐标<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> </dependencies>【第二步】定义dao接口与实现类public interface BookDao { public void save(); public void update(); } @Repository public class BookDaoImpl implements BookDao { public void save() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } public void update(){ System.out.println("book dao update ..."); } }【第三步】定义通知类，制作通知方法//通知类必须配置成Spring管理的bean @Component public class MyAdvice { public void method(){ System.out.println(System.currentTimeMillis()); } }【第四步】定义切入点表达式、配置切面(绑定切入点与通知关系)//通知类必须配置成Spring管理的bean @Component //设置当前类为切面类类 @Aspect public class MyAdvice { //设置切入点，@Pointcut注解要求配置在方法上方 @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} //设置在切入点pt()的前面运行当前操作(前置通知) @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }【第五步】在配置类中进行Spring注解包扫描和开启AOP功能@Configuration @ComponentScan("com.itheima") //开启注解开发AOP功能 @EnableAspectJAutoProxy public class SpringConfig { }测试类和运行结果public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); bookDao.update(); } }3 AOP工作流程【理解】问题导入[scode type="yellow" size=""]什么是目标对象？什么是代理对象？[/scode]3.1 AOP工作流程Spring容器启动读取所有切面配置中的切入点初始化bean，判定bean对应的类中的方法是否匹配到任意切入点匹配失败，创建原始对象匹配成功，创建原始对象（目标对象）的代理对象获取bean执行方法获取的bean是原始对象时，调用方法并执行，完成操作获取的bean是代理对象时，根据代理对象的运行模式运行原始方法与增强的内容，完成操作3.2 AOP核心概念目标对象（Target）：被代理的对象，也叫原始对象，该对象中的方法没有任何功能增强。代理对象（Proxy）：代理后生成的对象，由Spring帮我们创建代理对象。3.3 在测试类中验证代理对象public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); bookDao.update(); //打印对象的类名 System.out.println(bookDao.getClass()); } }4 AOP切入点表达式问题导入[scode type="yellow" size=""]在切入点表达式中如何简化包名和参数类型书写？[/scode]4.1 语法格式切入点：要进行增强的方法切入点表达式：要进行增强的方法的描述方式描述方式一：执行com.itheima.dao包下的BookDao接口中的无参数update方法execution(void com.itheima.dao.BookDao.update())描述方式二：执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法execution(void com.itheima.dao.impl.BookDaoImpl.update())切入点表达式标准格式：动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名）execution(public User com.itheima.service.UserService.findById(int))动作关键字：描述切入点的行为动作，例如execution表示执行到指定切入点访问修饰符：public，private等，可以省略返回值：写返回值类型包名：多级包使用点连接类/接口名：方法名：参数：直接写参数的类型，多个类型用逗号隔开异常名：方法定义中抛出指定异常，可以省略4.2 通配符目的：可以使用通配符描述切入点，快速描述。*：单个独立的任意符号，可以独立出现，也可以作为前缀或者后缀的匹配符出现匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法execution（public * com.itheima.*.UserService.find*(*)).. ：多个连续的任意符号，可以独立出现，常用于简化包名与参数的书写匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法execution（public User com..UserService.findById(..))+：专用于匹配子类类型execution(* *..*Service+.*(..))4.3 书写技巧所有代码按照标准规范开发，否则以下技巧全部失效描述切入点通[font color="#FF0000"]常描述接口[/font]，而不描述实现类访问控制修饰符针对接口开发均采用public描述（[font color="#FF0000"]可省略访问控制修饰符描述[/font]）返回值类型对于增删改类使用精准类型加速匹配，对于查询类使用*通配快速描述[font color="#FF0000"]包名[/font]书写[font color="#FF0000"]尽量不使用..匹配[/font]，效率过低，常用*做单个包描述匹配，或精准匹配[font color="#FF0000"]接口名/类名[/font]书写名称与模块相关的采用*匹配，例如UserService书写成*Service，绑定业务层接口名[font color="#FF0000"]方法名[/font]书写以[font color="#FF0000"]动词[/font]进行[font color="#FF0000"]精准匹配[/font]，名词采用*匹配，例如getById书写成getBy*,selectAll书写成selectAll参数规则较为复杂，根据业务方法灵活调整通常[font color="#FF0000"]不使用异常[/font]作为[font color="#FF0000"]匹配[/font]规则5 AOP通知类型【重点】问题导入[scode type="yellow" size=""]请描述一下如何定义环绕通知方法？[/scode]5.1 AOP通知分类AOP通知描述了抽取的共性功能，根据共性功能抽取的位置不同，最终运行代码时要将其加入到合理的位置AOP通知共分为5种类型前置通知：在切入点方法执行之前执行后置通知：在切入点方法执行之后执行，无论切入点方法内部是否出现异常，后置通知都会执行。[font color="#FF0000"]环绕通知(重点)：[/font]手动调用切入点方法并对其进行增强的通知方式。返回后通知(了解)：在切入点方法执行之后执行，如果切入点方法内部出现异常将不会执行。抛出异常后通知(了解)：在切入点方法执行之后执行，只有当切入点方法内部出现异常之后才执行。5.2 AOP通知详解5.2.1 前置通知名称：@Before类型：[font color="#FF0000"]方法注解[/font]位置：通知方法定义上方作用：设置当前通知方法与切入点之间的绑定关系，当前通知方法在原始切入点方法前运行范例：@Before("pt()") public void before() { System.out.println("before advice ..."); }5.2.2 后置通知名称：@After类型：[font color="#FF0000"]方法注解[/font]位置：通知方法定义上方作用：设置当前通知方法与切入点之间的绑定关系，当前通知方法在原始切入点方法后运行范例：@After("pt()") public void after() { System.out.println("after advice ..."); }5.2.3 返回后通知名称：@AfterReturning（了解）类型：[font color="#FF0000"]方法注解[/font]位置：通知方法定义上方作用：设置当前通知方法与切入点之间的绑定关系，当前通知方法在原始切入点方法正常执行完毕后运行范例：@AfterReturning("pt()") public void afterReturning() { System.out.println("afterReturning advice ..."); }5.2.4 抛出异常后通知名称：@AfterThrowing（了解）类型：[font color="#FF0000"]方法注解[/font]位置：通知方法定义上方作用：设置当前通知方法与切入点之间的绑定关系，当前通知方法在原始切入点方法运行抛出异常后执行范例：@AfterThrowing("pt()") public void afterThrowing() { System.out.println("afterThrowing advice ..."); }5.2.5 环绕通知名称：@Around（重点，常用）类型：[font color="#FF0000"]方法注解[/font]位置：通知方法定义上方作用：设置当前通知方法与切入点之间的绑定关系，当前通知方法在原始切入点方法前后运行范例：：@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; }[font color="#FF0000"]环绕通知注意事项[/font][scode type="red" size=""]环绕通知方法形参必须是ProceedingJoinPoint，表示正在执行的连接点，使用该对象的proceed()方法表示对原始对象方法进行调用，返回值为原始对象方法的返回值。环绕通知方法的返回值建议写成Object类型，用于将原始对象方法的返回值进行返回，哪里使用代理对象就返回到哪里。[/scode]二、AOP案例1 案例-测量业务层接口万次执行效率问题导入[scode type="yellow" size=""]能不能描述一下环绕通知里面的实现步骤？[/scode]1.1 需求和分析需求：任意业务层接口执行均可显示其执行效率（执行时长）分析：[scode type="yellow" size=""]业务功能：业务层接口执行前后分别记录时间，求差值得到执行效率通知类型选择前后均可以增强的类型——环绕通知[/scode]1.2 代码实现【前置工作】环境准备Spring整合mybatis对spring_db数据库中的Account进行CRUD操作Spring整合Junit测试CRUD是否OK。在pom.xml中添加aspectjweaver切入点表达式依赖... ...【第一步】编写通知类@Component @Aspect public class ProjectAdvice { //匹配业务层的所有方法 @Pointcut("execution(* com.itheima.service.*Service.*(..))") private void servicePt(){} //设置环绕通知，在原始操作的运行前后记录执行时间 @Around("ProjectAdvice.servicePt()") //本类类名可以省略不写 public void runSpeed(ProceedingJoinPoint pjp) throws Throwable { //获取执行的签名对象 Signature signature = pjp.getSignature(); //获取接口/类全限定名 String className = signature.getDeclaringTypeName(); //获取方法名 String methodName = signature.getName(); //记录开始时间 long start = System.currentTimeMillis(); //执行万次操作 for (int i = 0; i < 10000; i++) { pjp.proceed(); } //记录结束时间 long end = System.currentTimeMillis(); //打印执行结果 System.out.println("万次执行："+ className+"."+methodName+"---->" +(end-start) + "ms"); } }【第二步】在SpringConfig配置类上开启AOP注解功能@Configuration @ComponentScan("com.itheima") @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class,MybatisConfig.class}) @EnableAspectJAutoProxy //开启AOP注解功能 public class SpringConfig { }【第三步】运行测试类，查看结果@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTestCase { @Autowired private AccountService accountService; @Test public void testFindById(){ Account account = accountService.findById(2); } @Test public void testFindAll(){ List<Account> list = accountService.findAll(); } }2 AOP切入点数据获取问题导入[scode type="yellow" size=""]在环绕通知中可以获取到哪些数据？[/scode]2.1 获取参数说明：在前置通知和环绕通知中都可以获取到连接点方法的参数们JoinPoint对象描述了连接点方法的运行状态，可以获取到原始方法的调用参数@Before("pt()") public void before(JoinPoint jp) { Object[] args = jp.getArgs(); //获取连接点方法的参数们 System.out.println(Arrays.toString(args)); }ProccedJointPoint是JoinPoint的子类@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); //获取连接点方法的参数们 System.out.println(Arrays.toString(args)); Object ret = pjp.proceed(); return ret; }2.2 获取返回值说明：在返回后通知和环绕通知中都可以获取到连接点方法的返回值抛出异常后通知可以获取切入点方法中出现的异常信息，使用形参可以接收对应的异常对象@AfterReturning(value = "pt()",returning = "ret") public void afterReturning(String ret) { //变量名要和returning="ret"的属性值一致 System.out.println("afterReturning advice ..."+ret); }环绕通知中可以手工书写对原始方法的调用，得到的结果即为原始方法的返回值@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { // 手动调用连接点方法，返回值就是连接点方法的返回值 Object ret = pjp.proceed(); return ret; }2.3 获取异常说明：在抛出异常后通知和环绕通知中都可以获取到连接点方法中出现的异常抛出异常后通知可以获取切入点方法中出现的异常信息，使用形参可以接收对应的异常对象@AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) {//变量名要和throwing = "t"的属性值一致 System.out.println("afterThrowing advice ..."+ t); }抛出异常后通知可以获取切入点方法运行的异常信息，使用形参可以接收运行时抛出的异常对象@Around("pt()") public Object around(ProceedingJoinPoint pjp) { Object ret = null; //此处需要try...catch处理，catch中捕获到的异常就是连接点方法中抛出的异常 try { ret = pjp.proceed(); } catch (Throwable t) { t.printStackTrace(); } return ret; }3 案例-百度网盘密码数据兼容处理问题导入[scode type="yellow" size=""]请说出我们该使用什么类型的通知来完成这个需求？[/scode]3.1 需求和分析需求：对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理分析：①：在业务方法执行之前对所有的输入参数进行格式处理——trim()②：使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用3.2 代码实现【前置工作】环境准备//-------------service层代码----------------------- public interface ResourcesService { public boolean openURL(String url ,String password); } @Service public class ResourcesServiceImpl implements ResourcesService { @Autowired private ResourcesDao resourcesDao; public boolean openURL(String url, String password) { return resourcesDao.readResources(url,password); } } //-------------dao层代码----------------------- public interface ResourcesDao { boolean readResources(String url, String password); } @Repository public class ResourcesDaoImpl implements ResourcesDao { public boolean readResources(String url, String password) { System.out.println(password.length()); //模拟校验 return password.equals("root"); } }【第一步】编写通知类@Component @Aspect public class DataAdvice { @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))") private void servicePt(){} @Around("DataAdvice.servicePt()") public Object trimStr(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); for (int i = 0; i < args.length; i++) { //判断参数是不是字符串 if(args[i].getClass().equals(String.class)){ args[i] = args[i].toString().trim(); } } Object ret = pjp.proceed(args); return ret; } }【第二步】在SpringConfig配置类上开启AOP注解功能@Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy public class SpringConfig { }【第三步】运行测试类，查看结果public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); ResourcesService resourcesService = ctx.getBean(ResourcesService.class); boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root "); System.out.println(flag); } }4 AOP开发总结4.1 AOP的核心概念概念：AOP(Aspect Oriented Programming)面向切面编程，一种编程范式作用：在不惊动原始设计的基础上为方法进行功能增强核心概念代理（Proxy）：SpringAOP的核心本质是采用代理模式实现的连接点（JoinPoint）： 在SpringAOP中，理解为任意方法的执行切入点（Pointcut）：匹配连接点的式子，也是具有共性功能的方法描述通知（Advice）：若干个方法的共性功能，在切入点处执行，最终体现为一个方法切面（Aspect）：描述通知与切入点的对应关系目标对象（Target）：被代理的原始对象成为目标对象4.2 切入点表达式语法切入点表达式标准格式：动作关键字(访问修饰符 返回值 包名.类/接口名.方法名（参数）异常名)execution(* com.itheima.service.*Service.*(..))切入点表达式描述通配符：作用：用于快速描述，范围描述*：匹配任意符号（常用）.. ：匹配多个连续的任意符号（常用）+：匹配子类类型切入点表达式书写技巧1.按标准规范开发2.查询操作的返回值建议使用*匹配3.减少使用..的形式描述包4.对接口进行描述，使用*表示模块名，例如UserService的匹配描述为*Service5.方法名书写保留动词，例如get，使用*表示名词，例如getById匹配描述为getBy*6.参数根据实际情况灵活调整4.3 五种通知类型前置通知后置通知环绕通知（重点）环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用环绕通知可以隔离原始方法的调用执行环绕通知返回值设置为Object类型环绕通知中可以对原始方法调用过程中出现的异常进行处理返回后通知抛出异常后通知三、Spring事务管理1 Spring事务简介【重点】问题导入[scode type="yellow" size=""]Spring提供的事务管理是数据层的事务还是业务层的事务?[/scode]1.1 Spring事务作用事务作用：在数据层保障一系列的数据库操作同成功同失败Spring事务作用：在数据层或[font color="#FF0000"]业务层[/font]保障一系列的数据库操作同成功同失败1.2 需求和分析需求：实现任意两个账户间转账操作需求微缩：A账户减钱，B账户加钱分析：①：数据层提供基础操作，指定账户减钱（outMoney），指定账户加钱（inMoney）②：业务层提供转账操作（transfer），调用减钱与加钱的操作③：提供2个账号和操作金额执行转账操作④：基于Spring整合MyBatis环境搭建上述操作结果分析：①：程序正常执行时，账户金额A减B加，没有问题②：程序出现异常后，转账失败，但是异常之前操作成功，异常之后操作失败，整体业务失败1.3 代码实现【前置工作】环境准备Spring整合Mybatis相关代码(依赖、JdbcConfig、MybatisConfig、SpringConfig)省略。public interface AccountDao { @Update("update tbl_account set money = money + #{money} where name = #{name}") void inMoney(@Param("name") String name, @Param("money") Double money); @Update("update tbl_account set money = money - #{money} where name = #{name}") void outMoney(@Param("name") String name, @Param("money") Double money); } public interface AccountService { /** * 转账操作 * @param out 传出方 * @param in 转入方 * @param money 金额 */ public void transfer(String out,String in ,Double money) ; } @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; public void transfer(String out,String in ,Double money) { accountDao.outMoney(out,money); int i = 1/0; accountDao.inMoney(in,money); } }【第一步】在业务层接口上添加Spring事务管理public interface AccountService { //配置当前接口方法具有事务 @Transactional public void transfer(String out,String in ,Double money) ; }注意事项Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中，降低耦合注解式事务可以添加到业务方法上表示当前方法开启事务，也可以添加到接口上表示当前接口所有方法开启事务【第二步】设置事务管理器(将事务管理器添加到IOC容器中)说明：可以在JdbcConfig中配置事务管理器//配置事务管理器，mybatis使用的是jdbc事务 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager dtm = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; }注意事项事务管理器要根据实现技术进行选择MyBatis框架使用的是JDBC事务【第三步】开启注解式事务驱动@Configuration @ComponentScan("com.itheima") @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class,MybatisConfig.class}) //开启注解式事务驱动 @EnableTransactionManagement public class SpringConfig { }【第四步】运行测试类，查看结果@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer() throws IOException { accountService.transfer("Tom","Jerry",100D); } }2 Spring事务角色【理解】问题导入[scode type="yellow" size=""]什么是事务管理员，什么是事务协调员？[/scode]2.1 Spring事务角色事务管理员：发起事务方，在Spring中通常指代业务层开启事务的方法事务协调员：加入事务方，在Spring中通常指代数据层方法，也可以是业务层方法3 Spring事务相关配置问题导入[scode type="yellow" size=""]什么样的异常，Spring事务默认是不进行回滚的？[/scode]3.1 事务配置说明：对于RuntimeException类型异常或者Error错误，Spring事务能够进行回滚操作。但是对于编译器异常，Spring事务是不进行回滚的，所以需要使用rollbackFor来设置要回滚的异常。3.2 案例：转账业务追加日志需求和分析需求：实现任意两个账户间转账操作，并对每次转账操作在数据库进行留痕需求微缩：A账户减钱，B账户加钱，数据库记录日志分析：①：基于转账操作案例添加日志模块，实现数据库中记录日志②：业务层转账操作（transfer），调用减钱、加钱与记录日志功能实现效果预期：无论转账操作是否成功，均进行转账操作的日志留痕存在的问题：日志的记录与转账操作隶属同一个事务，同成功同失败实现效果预期改进：无论转账操作是否成功，日志必须保留事务传播行为：事务协调员对事务管理员所携带事务的处理态度【准备工作】环境整备USE spring_db; CREATE TABLE tbl_log( id INT PRIMARY KEY AUTO_INCREMENT, info VARCHAR(255), createDate DATE );public interface LogService { //propagation设置事务属性：传播行为设置为当前操作需要新事务 @Transactional void log(String out, String in, Double money); } @Service public class LogServiceImpl implements LogService { @Autowired private LogDao logDao; public void log(String out,String in,Double money ) { logDao.log("转账操作由"+out+"到"+in+",金额："+money); } } public interface LogDao { @Insert("insert into tbl_log (info,createDate) values(#{info},now())") void log(String info); }【第一步】在AccountServiceImpl中调用logService中添加日志的方法@Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Autowired private LogService logService; public void transfer(String out,String in ,Double money) { try{ accountDao.outMoney(out,money); int i = 1/0; accountDao.inMoney(in,money); }finally { logService.log(out,in,money); } } }【第二步】在LogService的log()方法上设置事务的传播行为public interface LogService { //propagation设置事务属性：传播行为设置为当前操作需要新事务 @Transactional(propagation = Propagation.REQUIRES_NEW) void log(String out, String in, Double money); }【第三步】运行测试类，查看结果@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer() throws IOException { accountService.transfer("Tom","Jerry",50D); } }3.3 事务传播行为
今日目标能够掌握注解开发定义Bean对象能够掌握纯注解开发模式能够配置注解开发依赖注入能够配置注解开发管理第三方Bean能够配置注解开发为第三方Bean注入资源能够使用Spring整合Mybatis能够使用Spring整合Junit一、第三方资源配置管理说明：以管理DataSource连接池对象为例讲解第三方资源配置管理，DataSource是获取物理数据源连接的工厂类。1 管理DataSource连接池对象问题导入配置数据库连接参数时，注入驱动类名是用driverClassName还是driver？1.1 管理Druid连接池【重点】数据库准备create database if not exists spring_db character set utf8; use spring_db; create table if not exists tbl_account( id int primary key auto_increment, name varchar(20), money double ); insert into tbl_account values(null,'Tom',1000); insert into tbl_account values(null,'Jerry',1000);【第一步】添加Druid连接池依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>注意：除了添加以上两个依赖之外，别忘了添加spring-context依赖。【第二步】配置DruidDataSource连接池Bean对象<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/spring_db"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean>【第三步】在测试类中从IOC容器中获取连接池对象并打印public class App { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); DataSource dataSource = (DataSource) ctx.getBean("dataSource"); System.out.println(dataSource); } }1.2 管理c3p0连接池【第一步】添加c3p0连接池依赖<dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency>【第二步】配置c3p0连接池Bean对象<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_db"/> <property name="user" value="root"/> <property name="password" value="root"/> <property name="maxPoolSize" value="1000"/> </bean>注意：同一个Spring容器中不能有两个id="dataSource"的连接池。【第三步】在测试类中从IOC容器中获取连接池对象并打印public class App { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); DataSource dataSource = (DataSource) ctx.getBean("dataSource"); System.out.println(dataSource); } }2 加载properties属性文件【重点】目的：将数据库的连接参数抽取到一个单独的文件中，与Spring配置文件解耦。问题导入问题1：如何解决使用EL表达式读取属性文件中的值结果读取到了系统属性问题？问题2：加载properties文件写法标准写法该怎么写？2.1 基本用法【第一步】编写jdbc.properties属性文件jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db jdbc.username=root jdbc.password=root【第二步】在applicationContext.xml中开启开启context命名空间，加载jdbc.properties属性文件[scode type="green" size=""]小技巧：如果同学们觉得上述复制粘贴方式不好改或者容易改错，其实idea是有提示功能的，注意不要选错就行了。有些版本的idea没有这个提示，那么就按照上面复制粘贴的方式改，改完之后可以做成live template模板，后期直接用。[/scode]<context:property-placeholder location="jdbc.properties"/>【第三步】在配置连接池Bean的地方使用EL表达式获取jdbc.properties属性文件中的值<bean class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>配置完成之后，运行之前的获取Druid连接池代码，可以获取到连接池对象就表示配置成功。2.2 配置不加载系统属性问题如果属性文件中配置的不是jdbc.username，而是username=root666，那么使用${username}获取到的不是root666，而是计算机的名称。原因系统属性的优先级比我们属性文件中的高，替换了我们的username=root666。解决解决1：换一个名称，例如不叫username，叫jdbc.username。解决2：使用system-properties-mode="NEVER"属性表示不使用系统属性。<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>2.3 加载properties文件写法不加载系统属性<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>加载多个properties文件<context:property-placeholder location="jdbc.properties,msg.properties"/>加载所有properties文件<context:property-placeholder location="*.properties"/>加载properties文件[font color="#FF0000"]标准格式[/font]<context:property-placeholder location="classpath:*.properties"/>加载properties文件标准格式<context:property-placeholder location="classpath*:*.properties"/>二、Spring容器1 Spring核心容器介绍问题导入问题：按照Bean名称获取Bean有什么弊端，按照Bean类型获取Bean有什么弊端？1.1 创建容器方式一：类路径加载配置文件ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");方式二：文件路径加载配置文件ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\applicationContext.xml");加载多个配置文件ApplicationContext ctx = new ClassPathXmlApplicationContext("bean1.xml", "bean2.xml");1.2 获取bean对象方式一：使用bean名称获取弊端：需要自己强制类型转换BookDao bookDao = (BookDao) ctx.getBean("bookDao");[font color="#FF0000"]方式二：使用bean名称获取并指定类型（推荐使用）[/font]BookDao bookDao = ctx.getBean("bookDao", BookDao.class);方式三：使用bean类型获取弊端：如果IOC容器中同类型的Bean对象有多个，此处获取会报错BookDao bookDao = ctx.getBean(BookDao.class);1.3 容器类层次结构1.4 BeanFactory类路径加载配置文件Resource resources = new ClassPathResource("applicationContext.xml"); BeanFactory bf = new XmlBeanFactory(resources); BookDao bookDao = bf.getBean("bookDao", BookDao.class); bookDao.save();BeanFactory创建完毕后，所有的Bean均为延迟加载，也就是说我们调用getBean()方法获取Bean对象时才创建Bean对象并返回给我们2 Spring核心容器总结2.1 容器相关BeanFactory是IoC容器的顶层接口，初始化BeanFactory对象时，加载的bean延迟加载ApplicationContext接口是Spring容器的核心接口，初始化时bean立即加载ApplicationContext接口提供基础的bean操作相关方法，通过其他接口扩展其功能ApplicationContext接口常用初始化类[font color="#FF0000"]ClassPathXmlApplicationContext(常用)[/font]FileSystemXmlApplicationContext2.2 bean相关2.3 依赖注入相关三、Spring注解开发1 注解开发定义Bean对象【重点】目的：xml配置Bean对象有些繁琐，使用注解简化Bean对象的定义问题导入问题1：使用什么标签进行Spring注解包扫描？问题2：@Component注解和@Controller、@Service、@Repository三个衍生注解有什么区别？1.1 基本使用【第一步】在applicationContext.xml中开启Spring注解包扫描<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="cn.fivk"/> </beans>【第二步】在类上使用@Component注解定义Bean。//@Component定义bean @Component("bookDao") public class BookDaoImpl implements BookDao { public void save() { System.out.println("book dao save ..."); } } @Component public class BookServiceImpl implements BookService { private BookDao bookDao; public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } public void save() { System.out.println("book service save ..."); bookDao.save(); } }补充说明：如果@Component注解没有使用参数指定Bean的名称，那么类名首字母小写就是Bean在IOC容器中的默认名称。例如：BookServiceImpl对象在IOC容器中的名称是bookServiceImpl。【第三步】在测试类中获取Bean对象public class AppForAnnotation { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); System.out.println(bookDao); //按类型获取bean BookService bookService = ctx.getBean(BookService.class); System.out.println(bookService); } }注意：在测试类中不要调用bookService的save方法，因为还没有给BookServiceImpl中的bookDao赋值，调用bookService的save方法会出现空指针异常。运行结果1.2 @Component三个衍生注解说明：加粗的注解为常用注解Spring提供@Component注解的三个衍生注解@Controller：用于表现层bean定义@Service：用于业务层bean定义@Repository：用于数据层bean定义@Repository("bookDao") public class BookDaoImpl implements BookDao { } @Service public class BookServiceImpl implements BookService { } 2 纯注解开发模式【重点】问题导入问题1：配置类上使用什么注解表示该类是一个配置类？问题2：配置类上使用什么注解进行Spring注解包扫描？2.1 纯注解开发模式介绍Spring3.0开启了纯注解开发模式，使用Java类替代配置文件，开启了Spring快速开发赛道Java类代替Spring核心配置文件@Configuration注解用于设定当前类为配置类@ComponentScan注解用于设定扫描路径，此注解只能添加一次，多个数据请用数组格式@ComponentScan({cn.fivk.service","cn.fivk.dao"})读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象//加载配置文件初始化容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //加载配置类初始化容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);2.2 代码演示【第一步】定义配置类代替配置文件//声明当前类为Spring配置类 @Configuration //Spring注解扫描，相当于<context:component-scan base-package="cn.fivk"/> @ComponentScan("cn.fivk") //设置bean扫描路径，多个路径书写为字符串数组格式 //@ComponentScan({"cn.fivk.service","cn.fivk.dao"}) public class SpringConfig { }【第二步】在测试类中加载配置类，获取Bean对象并使用public class AppForAnnotation { public static void main(String[] args) { //AnnotationConfigApplicationContext加载Spring配置类初始化Spring容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); System.out.println(bookDao); //按类型获取bean BookService bookService = ctx.getBean(BookService.class); System.out.println(bookService); } }3 注解开发Bean作用范围和生命周期管理问题导入在类上使用什么注解定义Bean的作用范围？3.1 bean作用范围注解配置使用@Scope定义bean作用范围@Repository @Scope("singleton") public class BookDaoImpl implements BookDao { }3.2 bean生命周期注解配置使用@PostConstruct、@PreDestroy定义bean生命周期@Repository @Scope("singleton") public class BookDaoImpl implements BookDao { public BookDaoImpl() { System.out.println("book dao constructor ..."); } @PostConstruct public void init(){ System.out.println("book init ..."); } @PreDestroy public void destroy(){ System.out.println("book destory ..."); } }[scode type="red" size=""]注意：@PostConstruct和@PreDestroy注解是jdk中提供的注解，从jdk9开始，jdk中的javax.annotation包被移除了，也就是说这两个注解就用不了了，可以额外导入一下依赖解决这个问题。[/scode]<dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency>4 注解开发依赖注入【重点】问题导入问题1：请描述@Autowired注解是如何进行自动装配的？问题2：请描述@Qualifier注解的作用4.1 使用@Autowired注解开启自动装配模式（按类型）@Service public class BookServiceImpl implements BookService { //@Autowired：注入引用类型，自动装配模式，默认按类型装配 @Autowired private BookDao bookDao; public void save() { System.out.println("book service save ..."); bookDao.save(); } }说明：不管是使用配置文件还是配置类，都必须进行对应的Spring注解包扫描才可以使用。@Autowired默认按照类型自动装配，如果IOC容器中同类的Bean有多个，那么默认按照变量名和Bean的名称匹配，建议使用@Qualifier注解指定要装配的bean名称[scode type="red" size=""]注意：自动装配基于反射设计创建对象并暴力反射对应属性为私有属性初始化数据，因此无需提供setter方法。[/scode]4.2 使用@Qualifier注解指定要装配的bean名称目的：解决IOC容器中同类型Bean有多个装配哪一个的问题@Service public class BookServiceImpl implements BookService { //@Autowired：注入引用类型，自动装配模式，默认按类型装配 @Autowired //@Qualifier：自动装配bean时按bean名称装配 @Qualifier("bookDao") private BookDao bookDao; public void save() { System.out.println("book service save ..."); bookDao.save(); } }[scode type="red" size=""]注意：@Qualifier注解无法单独使用，必须配合@Autowired注解使用[/scode]4.3 使用@Value实现简单类型注入@Repository("bookDao") public class BookDaoImpl implements BookDao { //@Value：注入简单类型（无需提供set方法） @Value("${name}") private String name; public void save() { System.out.println("book dao save ..." + name); } }以上@Value注解中使用${name}从属性文件中读取name值，那么就需要在配置类或者配置文件中加载属性文件。@Configuration @ComponentScan("cn.fivk") //@PropertySource加载properties配置文件 @PropertySource({"classpath:jdbc.properties"}) //{}可以省略不写 public class SpringConfig { }[scode type="red" size=""]注意：@PropertySource()中加载多文件请使用数组格式配置，不允许使用通配符[/scode]5 注解开发管理第三方Bean【重点】问题导入导入自己定义的配置类有几种方式？【第一步】单独定义配置类public class JdbcConfig { //@Bean：表示当前方法的返回值是一个bean对象，添加到IOC容器中 @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setUrl("jdbc:mysql://localhost:3306/spring_db"); ds.setUsername("root"); ds.setPassword("root"); return ds; } }【第二步】将独立的配置类加入核心配置方式1：@Import注解导入式@Configuration @ComponentScan("cn.fivk") //@Import:导入配置信息 @Import({JdbcConfig.class}) public class SpringConfig { }方式2：@ComponentScan扫描式@Configuration @ComponentScan({"cn.fivk.config","cn.fivk.service","cn.fivk.dao"}) //只要cn.fivk.config包扫到了就行，三个包可以合并写成cn.fivk public class SpringConfig { }6 注解开发为第三方Bean注入资源【重点】问题导入配置类中如何注入简单类型数据，如何注入引用类型数据？6.1 简单类型依赖注入public class JdbcConfig { //1.定义一个方法获得要管理的对象 @Value("com.mysql.jdbc.Driver") private String driver; @Value("jdbc:mysql://localhost:3306/spring_db") private String url; @Value("root") private String userName; @Value("root") private String password; //2.@Bean：表示当前方法的返回值是一个bean对象，添加到IOC容器中 @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } }说明：如果@Value()中使用了EL表达式读取properties属性文件中的内容，那么就需要加载properties属性文件。6.2 引用类型依赖注入//Spring会自动从IOC容器中找到BookDao对象赋值给参数bookDao变量，如果没有就会报错。 @Bean public DataSource dataSource(BookDao bookDao){ System.out.println(bookDao); DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; }说明：引用类型注入只需要为bean定义方法设置形参即可，容器会根据类型自动装配对象7 注解开发总结四、Spring整合其他技术【重点】1 Spring整合mybatis【重点】1.1 思路分析问题导入mybatis进行数据层操作的核心对象是谁？1.1.1 MyBatis程序核心对象分析1.1.2 整合MyBatis使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息使用MapperScannerConfigurer加载Dao接口，创建代理对象保存到IOC容器中1.2 代码实现问题导入问题1：Spring整合mybatis的依赖叫什么？问题2：Spring整合mybatis需要管理配置哪两个Bean，这两个Bean作用分别是什么？【前置工作】在pom.xml中添加spring-context、druid、mybatis、mysql-connector-java等基础依赖。准备service和dao层基础代码public interface AccountService { void save(Account account); void delete(Integer id); void update(Account account); List<Account> findAll(); Account findById(Integer id); } @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; public void save(Account account) { accountDao.save(account); } public void update(Account account){ accountDao.update(account); } public void delete(Integer id) { accountDao.delete(id); } public Account findById(Integer id) { return accountDao.findById(id); } public List<Account> findAll() { return accountDao.findAll(); } } public interface AccountDao { @Insert("insert into tbl_account(name,money)values(#{name},#{money})") void save(Account account); @Delete("delete from tbl_account where id = #{id} ") void delete(Integer id); @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ") void update(Account account); @Select("select * from tbl_account") List<Account> findAll(); @Select("select * from tbl_account where id = #{id} ") Account findById(Integer id); }【第一步】导入Spring整合Mybatis依赖<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency>【第二步】创建JdbcConfig配置DataSource数据源jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false jdbc.username=root jdbc.password=rootpublic class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String userName; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(userName); ds.setPassword(password); return ds; } }【第三步】创建MybatisConfig整合mybatispublic class MybatisConfig { //定义bean，SqlSessionFactoryBean，用于产生SqlSessionFactory对象 @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){ SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean(); ssfb.setTypeAliasesPackage("cn.fivk.domain"); ssfb.setDataSource(dataSource); return ssfb; } //定义bean，返回MapperScannerConfigurer对象 @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("cn.fivk.dao"); return msc; } }【第四步】创建SpringConfig主配置类进行包扫描和加载其他配置类@Configuration @ComponentScan("cn.fivk") //@PropertySource：加载类路径jdbc.properties文件 @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class,MybatisConfig.class}) public class SpringConfig { }【第五步】定义测试类进行测试public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); AccountService accountService = ctx.getBean(AccountService.class); Account ac = accountService.findById(1); System.out.println(ac); } }2 Spring整合Junit单元测试【重点】问题导入Spring整合Junit的两个注解作用分别是什么？【第一步】导入整合的依赖坐标spring-test <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.9.RELEASE</version> </dependency>【第二步】使用Spring整合Junit专用的类加载器【第三步】加载配置文件或者配置类//【第二步】使用Spring整合Junit专用的类加载器 @RunWith(SpringJUnit4ClassRunner.class) //【第三步】加载配置文件或者配置类 @ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类 //@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件 public class AccountServiceTest { //支持自动装配注入bean @Autowired private AccountService accountService; @Test public void testFindById(){ System.out.println(accountService.findById(1)); } @Test public void testFindAll(){ System.out.println(accountService.findAll()); } }[scode type="red" size=""]注意：junit的依赖至少要是4.12版本,可以是4.13等版本,否则出现如下异常：[/scode]