1 springboot概述 1.1 概述 Spring Boot是Spring提供的一个子项目,用于快速构建Spring应用程序。
1.2 传统方式构建spring应用程序 1 导入依赖繁琐。 2 项目配置繁琐。
1.3 SpringBoot特性 1.3.1 起步依赖 本质上就是一个Maven坐标,整合了完成一个功能需要的所有坐标。(解决配置繁琐的问题)
1.3.2 自动配置 遵循约定大于配置的原则,在boot程序启动后,一些bean对象会自动注入到iloc容器,不需要手动声明,简化开发。(解决项目配置繁琐的问题)
1.3.3 其他特性 1 内嵌的Tomcat、Jetty(无需部署WAR文件)。 2 外部化配置。部署完毕后,需要修改,只需要修改项目外部配置文件,直接重新启动项目就可以。 3 不需要XML配置(properties/yml)。
2 sprintboot入门 需求:使用SpringBoot开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串”hello world~”。
1 创建Maven工程。 2 导入spring-boot-starter-web起步依赖。 3 编写Controller。
1 2 3 4 5 6 7 @RestController public class HelloController { @RequestMapping ("/hello" ) public String hello () { return "Hello World~" ; } }
4 提供启动类。
1 2 3 4 5 6 7 @SpringBootApplication public class SpringbootSyqApplication { public static void main (String[] args) { SpringApplication.run(SpringbootSyqApplication.class , args ) ; } }
3 sprintboot工程创建 手动创建SpringBoot工程 1 创建Maven工程。 2 引入依赖。 3 提供启动类。
4 springboot配置文件_基本使用 4.1 学习路径 4.1.1 基础篇 配置文件。 整合MyBatis。 Bean管理。 自动配置管理。 自定义starter。
4.1.2 实战篇 项目开发。 整合三方技术。 项目部署。
4.1.3 面试篇 面试题。 源码。
4.2 配置文件 1 properties配置文件。application.properties文件。
1 2 3 4 server.port=9090 server.servlet.context-path=/start
2 yaml配置文件(实际开发中更常用)。application.yml(主要)/application.yaml。
5 yml配置信息书写和获取 5.1 三方技术配置信息 只需编写配置信息,起步依赖会自动获取配置信息。
5.2 自定义配置信息 不仅需要编写配置信息,还需要实现配置信息的获取代码。
5.3 yml配置信息书写 1 值前边必须有空格,作为分隔符。 2 使用空格作为缩进表示层级关系,相同的层级左侧对齐。
1 2 3 4 5 hobbies: - 打篮球 - 打豆豆 - 打游戏
5.4 配置信息的获取 5.4.1 @Value 1 2 3 4 5 6 @Value ("${键名}" )@Value ("${email.user}" )public String user;
在使用@Value注解时,多个层级的键名使用.来连接。
5.4.2 ConfigurationProperties(prefix=”前缀”) 实体类的成员变量名与配置文件中的键名一致。
1 @ConfigurationProperties (prefix="email" )
6 springboot整合mybatis 1 引入mybatis-spring-boot-starter依赖。pom.xml文件修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 3.0.3</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > </dependency >
2 yml文件数据库配置。数据库驱动,数据库链接,用户名,密码等。SpringBoot自动读取配置信息。
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/kongming username: root password: 181234
目标:查询User表指定id的数据,响应给浏览器。 Mapper编写:
1 2 3 4 5 6 @Mapper public interface UserMapper { @Select ("select * from user where id = #{id}" ) public User findById (Integer id) ; }
Service编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface UserService { public User findById (Integer id) ; } @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User findById (Integer id) { return userMapper.findById(id); } }
Controller编写:
1 2 3 4 5 6 7 8 9 10 @RestController public class UserController { @Autowired private UserService userService; @RequestMapping ("/findById" ) public User findById (Integer id) { return userService.findById(id); } }
7 Bean扫描 标签:
1 <context:component-scan base-package ="com.kongming" >
注解:
1 @ComponentScan (basePackages="com.kongming" )
在SpringBoot启动类的注解,实际上是一个组合注解:
1 2 3 4 5 6 7 8 9 @SpringBootApplication @ComponentScan (basePackages="com.kongming" )@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan
8 Bean注册 如果要注册的Bean对象来自于第三方(不是自定义的),是无法用@Component及衍生注解声明bean的。Spring提供了两个注解来解决问题: 1 @Bean 2 @Import
1 mvn install:install-file -Dfile=C:\SunYuQi\data\研一\工作\springboot资料\02_资料\02_Bean注册资料\common-pojo-1.0-SNAPSHOT.jar -DgroupId=cn.itcast -DartifactId=common-pojo -Dversion=1.0 -Dpackaging=jar
1 2 3 4 5 6 <!--第三方包导入--> <dependency> <groupId>cn.itcast</groupId> <artifactId>common-pojo</artifactId> <version>1.0</version> </dependency>
8.1 @Bean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration @SpringBootApplication public class SpringbootSyqApplication { public static void main (String[] args) { ApplicationContext context = SpringApplication.run(SpringbootSyqApplication.class , args ) ; Country country = context.getBean(Country.class ) ; System.out.println(country); } @Bean public Country country () { return new Country(); } }
建议的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Configuration public class CommonConfig { @Bean public Country country () { return new Country(); } @Bean public Province province () { return new Province(); } @Bean ("aa" ) @Bean () public Province province () { System.out.println("province" +country()); return new Province(); } } ```java @SpringBootApplication public class SpringbootSyqApplication { public static void main (String[] args) { ApplicationContext context = SpringApplication.run(SpringbootSyqApplication.class , args ) ; Country country = context.getBean(Country.class ) ; System.out.println(country); System.out.println(context.getBean("province" )); } }
8.2 @Import 使用@Import相当于手动扫描类。 1 导入配置类。 2 导入ImportSelector接口实现类。
1 2 3 4 5 6 7 8 9 10 11 12 13 @SpringBootApplication @Import (CommonConfig.class ) public class SpringbootSyqApplication { public static void main (String[] args) { ApplicationContext context = SpringApplication.run(SpringbootSyqApplication.class , args ) ; Country country = context.getBean(Country.class ) ; System.out.println(country); System.out.println(context.getBean("province" )); } }
1 2 3 4 5 6 7 8 9 10 @Import (CommonImportSelector.class ) public class CommonImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"config.CommonConfig" }; } }
在配置文件中,每行一个类名。实现上述代码的注释部分,完整如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class CommonImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { List<String> imports = new ArrayList<String>(); InputStream is = CommonImportSelector.class.getClassLoader().getResourceAsStream("common.imports"); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line = null ; try { while ((line = br.readLine())!=null ){ imports.add(line); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (br!=null ){ try { br.close(); } catch (IOException e) { throw new RuntimeException(e); } } } return imports.toArray(new String[0 ]); } }
1 2 3 4 5 6 7 8 9 10 @Target (ElementType.TYPE)@Retention (RetentionPolicy.RUNTIME)@Import (CommonImportSelector.class ) public @interface EnableCommonConfig {} @EnableCommonConfig
9 注册条件 9.1 需要注册条件的原因 配置文件中配置:
1 2 3 country: name: china system: socialism
在java文件中读取配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class CommonConfig { @Bean public Country country (@Value("${country.name}" ) String name, @Value ("${country.system}" ) String system) { Country country = new Country(); country.setName(name); country.setSystem(system); return country; } @Bean () public Province province (Country country) { System.out.println("province" +country); return new Province(); } }
我们希望达到的效果是,如果配置文件中配置了指定的信息,则注入,否则不注入。
9.2 注册条件 SpringBoot提供了设置注册生效条件的注解@Conditional.
1 2 3 4 5 6 7 8 9 10 @ConditionalOnProperty (prefix = "country" ,name = {"name" ,"system" })@Bean public Country country (@Value("${country.name}" ) String name, @Value ("${country.system}" ) String system) { Country country = new Country(); country.setName(name); country.setSystem(system); return country; }
1 2 3 4 5 6 @Bean ()@ConditionalOnMissingBean (Country.class ) public Province province () { return new Province(); }
1 2 3 4 5 6 7 @Bean ()@ConditionalOnClass (name="org.springframework.web.servlet.DispatcherServlet" )public Province province () { return new Province(); }
10 自动配置原理 10.1 需要学习自动配置原理的原因 1 自定义starter。 2 面试。
10.2 自动配置体验 遵循约定大于配置的原则,在boot程序启动后,起步依赖中的一些bean对象会自动注入到ioc容器。程序引入spring-boot-starter-web起步依赖,启动后,会自动往ioc容器中注入DispatcherServlet。
1 mvn install:install-file -Dfile=C:\SunYuQi\data\研一\工作\springboot资料\02_资料\03_自动配置\common-pojo-2.0-SNAPSHOT.jar -DgroupId=cn.itcast -DartifactId=common-pojo -Dversion=2.0 -Dpackaging=jar
10.3 自动配置原理 在主启动类上添加了SpringBootApplication注解,这个注解组合了EnableAutoConfiguration注解。 EnableAutoConfiguration注解又组合了Import注解,导入了AutoConfigurationImportSelector类。 实现selectImports方法,这个方法经过层层调用,最终会读取META-INF目录下的后缀名为imports的文件,当然了,boot2.7以前的版本,读取的是spring.factories文件。 读取到全类名之后,会解析注册条件,也就是@Conditional及其衍生注解,把满足注册条件的Bean对象自动注入到IOC容器中。
11 自定义starter 11.1 场景 在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将公共组件封装为SpringBoot的starter。 xxxx-autoconfigure <—自动配置功能 xxxx-starter <—依赖管理功能
11.2 自定义mybatis的starter 1 创建dmybatis-spring-boot-autoconfigure模块,提供自动配置功能,并自定义配置文件 META/INF/spring/xxx.imports. 2 创建dmybatis-spring-boot-stater模块,在starter中引入自动配置模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @AutoConfiguration public class MybatisAutoConfig { @Bean public SqlSessionFactoryBean sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer (BeanFactory beanFactory) { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); List<String> packages = AutoConfigurationPackages.get(beanFactory); String p = packages.get(0 ); mapperScannerConfigurer.setBasePackage(p); mapperScannerConfigurer.setAnnotationClass(Mapper.class ) ; return mapperScannerConfigurer; } }
1 com.kongming.config.MybatisAutoConfig
pom文件中是定义好的autoconfig和autoconfig的依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <dependency > <groupId > com.kongming</groupId > <artifactId > dmybatis-spring-boot-autoconfigure</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > <version > 3.3.1</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > <version > 3.3.1</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.14</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 3.0.3</version > </dependency > <dependency >
12 实战篇-开发模式和环境搭建 12.1 实战开发的技术栈 后台: Validation做参数校验。 Mybatis做数据库操作。 Redis做缓存。 Junit做单元测试。 SpringBoot项目部署。 前端: Vite Vue项目的脚手架。 Router路由。 Pina状态管理。 Element-Plus UI组件。
12.2 开发模式 接口文档: 1 路径和请求方式。 2 请求参数。 3 响应数据。
12.3 环境搭建 执行资料中的big_event.sql脚本,准备数据库表。 创建springboot工程,引入对应的依赖(web,mybatis,mysql驱动)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 3.0.3</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > </dependency >
配置文件application.yml中引入mybatis的配置信息。
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/kongming username: root password: 181234
创建包结构,并准备实体类。 其中pojo存放实体类,utils存放工具类。
13 实战篇-注册接口 用户模块需要开发的接口: 1 注册。 2 登录。 3 获取用户详细信息。 4 更新用户基本信息。 5 更新用户头像。 6 更新用户密码。
lowbok:在编译阶段为实体类自动生成setter,getter,toString方法 使用lowbok: (1)在pom文件中引入依赖; (2)在实体类上添加注解。
1 2 3 4 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 @Data public class User { private Integer id; private String username; private String password; private String nickname; private String email; private String userPic; private LocalDateTime createTime; private LocalDateTime updateTime; }
在其余实体类中都添加Data注解。在缺少构造方法的实体类中添加下列注解:
1 2 3 4 @NoArgsConstructor @AllArgsConstructor
13.1 注册 明确需求->阅读接口文档->思路分析->开发->测试。
13.1.1 基本信息 请求路径:/user/register 请求方式:POST 接口描述:该接口用于注册新用户
13.1.2 请求参数 请求参数格式:x-www-form-urlencoded
请求参数说明:
参数名称
说明
类型
是否必须
备注
username
用户名
string
是
5~16位非空字符
password
密码
string
是
5~16位非空字符
请求数据样例:
1 username=zhangsan&password=123456
13.1.3 响应数据 响应数据类型:application/json
响应参数说明:
名称
类型
是否必须
默认值
备注
其他信息
code
number
必须
响应码, 0-成功,1-失败
message
string
非必须
提示信息
data
object
非必须
返回的数据
响应数据样例:
1 2 3 4 5 { "code" : 0 , "message" : "操作成功" , "data" : null }
13.2 代码编写 1 Controller部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RestController @RequestMapping ("/user" )public class UserController { @Autowired private UserService userService; @PostMapping ("/register" ) public Result register (String username, String password) { User u = userService.findByUserName(username); if (u == null ){ userService.register(username,password); return Result.success(); }else { return Result.error("用户名已被占用" ); } } }
2 Service部分。
1 2 3 4 5 6 7 8 9 public interface UserService { User findByUserName (String username) ; void register (String username, String password) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User findByUserName (String username) { User u = userMapper.findByUserName(username); return u; } @Override public void register (String username, String password) { String md5String = Md5Util.getMD5String(password); userMapper.add(username,md5String); } }
3 Mapper部分。
1 2 3 4 5 6 7 8 9 10 @Mapper public interface UserMapper { @Select ("select * from user where username=#{username}" ) User findByUserName (String username) ; @Insert ("insert into user(username,password,create_time,update_time)" + "values (#{username},#{password},now(),now())" ) void add (String username, String password) ; }
14 实战篇-注册接口参数校验 在接口文档中,请求参数必须是5-16位的非空字符。后端必须保证前端传来的参数是符合要求的。
14.1 手动添加校验 在controller层添加校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController @RequestMapping ("/user" )public class UserController { @Autowired private UserService userService; @PostMapping ("/register" ) public Result register (String username, String password) { if (username != null && username.length()>=5 && username.length()<=16 && password != null && password.length()>=5 && password.length()<=16 ) { User u = userService.findByUserName(username); if (u == null ){ userService.register(username,password); return Result.success(); }else { return Result.error("用户名已被占用" ); } }else { return Result.error("参数不合法" ); } } }
14.2 Spring Validation 上述参数校验代码过于繁琐,Spring提供一个参数校验框架,使用预定义的注解完成参数校验。 1 引入Spring Validation起步依赖。
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
2 在参数前面添加@Pattern注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @RestController @RequestMapping ("/user" )@Validated public class UserController { @Autowired private UserService userService; @PostMapping ("/register" ) public Result register (@Pattern(regexp = "^\\S{5,16}$" ) String username, @Pattern (regexp = "^\\S{5,16}$" ) String password) { User u = userService.findByUserName(username); if (u == null ){ userService.register(username,password); return Result.success(); }else { return Result.error("用户名已被占用" ); } } }
3 在Controller类上添加@Validation注解。
4 在全局异常处理器中处理参数校验失败的异常。参数校验失败异常处理。@Restxxxb表示所有返回值都会被自动转化为json字符串。
1 2 3 4 5 6 7 8 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler (Exception.class ) public Result handleException (Exception e ) { e.printStackTrace(); return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败" ); } }
15 实战篇-登录主逻辑 15.1 基本信息
请求路径:/user/login 请求方式:POST 接口描述:该接口用于登录
15.2 请求参数 请求参数格式:x-www-form-urlencoded
请求参数说明:
参数名称
说明
类型
是否必须
备注
username
用户名
string
是
5~16位非空字符
password
密码
string
是
5~16位非空字符
请求数据样例:
1 username=zhangsan&password=123456
15.3 响应数据 响应数据类型:application/json
响应参数说明:
名称
类型
是否必须
默认值
备注
其他信息
code
number
必须
响应码, 0-成功,1-失败
message
string
非必须
提示信息
data
string
必须
返回的数据,jwt令牌
响应数据样例:
1 2 3 4 5 { "code" : 0 , "message" : "操作成功" , "data" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjUsInVzZXJuYW1lIjoid2FuZ2JhIn0sImV4cCI6MTY5MzcxNTk3OH0.pE_RATcoF7Nm9KEp9eC3CzcBbKWAFOL0IsuMNjnZ95M" }
jwt令牌是一串字符串。
15.4 备注说明
用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,浏览器都需要在请求头header中携带到服务端,请求头的名称为 Authorization,值为登录时下发的JWT令牌。 如果检测到用户未登录,则http响应状态码为401
15.5 代码实现 service和mapper使用用户注册中写好的即可。主要需要在controller中添加用户登录模块代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PostMapping ("/login" )public Result<String> login (@Pattern(regexp = "^\\S{5,16}$" ) String username, @Pattern (regexp = "^\\S{5,16}$" ) String password) { User loginUser = userService.findByUserName(username); if (loginUser == null ){ return Result.error("用户名错误" ); } if (Md5Util.getMD5String(password).equals(loginUser.getPassword())){ return Result.success("jwt token令牌.." ); } return Result.error("密码错误" ); }
16 实战篇-登录认证引入 登录认证-在未登录的情况下,可以访问到其他资源。 令牌就是一段字符串。 1 承载业务数据,减少后续请求查询数据库的次数。 2 防篡改,保证信息的合法性和有效性。
17 实战篇-JWT令牌 17.1 JWT简介 全称:JSON Web Token 定义了一种简洁的,自包含的格式,用于通信双方以json数据格式安全的传输信息。 组成:
第一部分:Header(头),记录令牌类型,签名算法等。例如{“alg”:”HS256”,”type”:”JWT”}。加密算法是用于防篡改。
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如{“id”:”1”,”username”:”Tom”} Base64:是一种基于64个可打印字符(A-Z a-z 0-9 +/)来表示二进制数据的编码方式。
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定密钥,通过指定签名算法计算而来。
17.2 JWT-生成 1 引入pom依赖。
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > com.auth0</groupId > <artifactId > java-jwt</artifactId > <version > 4.4.0</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency >
2 生成JWT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class JwtTest { @Test public void testGen () { Map<String, Object> claims = new HashMap<>(); claims.put("id" ,1 ); claims.put("username" ,"kongming" ); String token = JWT.create() .withClaim("user" ,claims) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 *60 *60 *12 )) .sign(Algorithm.HMAC256("kongming" )); System.out.println(token); } }
3 验证JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testParse () { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + ".eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImtvbmdtaW5nIn0sImV4cCI6MTcyMTY1NzU5NH0" + ".o2AiTydAFdgI2U_NXnqiLhuEwUEQCPnl76IQSVxjMi4" ; JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("kongming" )).build(); DecodedJWT decodedJWT = jwtVerifier.verify(token); Map<String, Claim> claims = decodedJWT.getClaims(); System.out.println(claims.get("user" )); }
JWT校验时使用的密钥签名,必须和生成JWT令牌时使用的密钥是配套的。
如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法。
18 实战篇-登录认证-完成 在UserController部分生成JWT。
1 2 3 4 5 6 7 8 9 if (Md5Util.getMD5String(password).equals(loginUser.getPassword())){ Map<String, Object> claims = new HashMap<>(); claims.put("id" ,loginUser.getId()); claims.put("username" ,loginUser.getUsername()); String token = JwtUtil.genToken(claims); return Result.success(token); }
在功能模块添加JWT验证部分。例如获取文章列表部分。
1 2 3 4 5 6 7 8 9 10 11 12 public Result<String> list (@RequestHeader(name = "Authorization" ) String token, HttpServletResponse response) { try { Map<String, Object> claims = JwtUtil.parseToken(token); return Result.success("所有的文章数据..." ); } catch (Exception e) { response.setStatus(401 ); return Result.error("未登录" ); } }
使用拦截器统一完成JWT验证。 在工程目录下创建interceptors/LoginInterceptor.class文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization" ); try { Map<String, Object> claims = JwtUtil.parseToken(token); return true ; } catch (Exception e) { response.setStatus(401 ); return false ; } } }
在工程目录下创建config/WebConfig.class文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login" ,"/user/register" ); } }
此时之前写在获取文章列表部分的登录验证代码可以注释掉了。
19 实战篇-获取用户详细信息 1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping ("/userInfo" ) public Result<User> userInfo (@RequestHeader(name = "Authorization" ) String token) { Map<String,Object> map = JwtUtil.parseToken(token); String username = map.get("username" ).toString(); User user = userService.findByUserName(username); return Result.success(user); }
防止springBoot把用户的密码返回给前端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.fasterxml.jackson.annotation.JsonIgnore;@Data public class User { private Integer id; private String username; @JsonIgnore private String password; private String nickname; private String email; private String userPic; private LocalDateTime createTime; private LocalDateTime updateTime; }
当前返回给前端的数据中创建时间和更新时间为空的原因是因为在数据库中这两个字段是下划线命名,而在User类中是驼峰命名。
1 2 3 mybatis: configuration: map-underscore-to-camel-case: true
20 实战篇-获取用户详细信息-ThreadLocal优化 在拦截器里写的解析token的代码,在userInfo方法中被再写了一次。在线程内实现代码复用,在拦截器里写的解析token代码,不会再出现在userInfo方法中。
ThreadLocal-提供线程局部变量。用来存取数据:set()/get()。使用ThreadLocal存储的数据,线程安全。使用示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ThreadLocalTest { @Test public void testThreadLocalSetAndGet () { ThreadLocal tl = new ThreadLocal(); new Thread(()->{ tl.set("诸葛亮" ); System.out.println(Thread.currentThread().getName()+": " +tl.get()); System.out.println(Thread.currentThread().getName()+": " +tl.get()); System.out.println(Thread.currentThread().getName()+": " +tl.get()); },"蓝色" ).start(); new Thread(()->{ tl.set("孔明" ); System.out.println(Thread.currentThread().getName()+": " +tl.get()); System.out.println(Thread.currentThread().getName()+": " +tl.get()); System.out.println(Thread.currentThread().getName()+": " +tl.get()); },"绿色" ).start(); } }
后端的三个组成是:Controller,Service,Dao 在过滤器中使用ThreadLocal的set方法记录userid,在Controller,Service,Dao中使用get方法获得userid。
在过滤器中存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization" ); try { Map<String, Object> claims = JwtUtil.parseToken(token); ThreadLocalUtil.set(claims); return true ; } catch (Exception e) { response.setStatus(401 ); return false ; } } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ThreadLocalUtil.remove(); } }
在userInfo中获取。(controller层中)
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping ("/userInfo" ) public Result<User> userInfo () { Map<String,Object> map = ThreadLocalUtil.get(); String username = map.get("username" ).toString(); User user = userService.findByUserName(username); return Result.success(user); }
21 实战篇-更新用户基本信息 在controller中新增updata方法。在Service层中新增updata方法。在DAO层中新增updata查询。
controller方法。
1 2 3 4 5 @PutMapping ("/update" ) public Result update (@RequestBody User user) { userService.update(user); return Result.success(); }
service方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public interface UserService { User findByUserName (String username) ; void register (String username, String password) ; void update (User user) ; } @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User findByUserName (String username) { User u = userMapper.findByUserName(username); return u; } @Override public void register (String username, String password) { String md5String = Md5Util.getMD5String(password); userMapper.add(username, md5String); } @Override public void update (User user) { user.setUpdateTime(LocalDateTime.now()); userMapper.update(user); }
DAO层,添加update方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Mapper public interface UserMapper { @Select ("select * from user where username=#{username}" ) User findByUserName (String username) ; @Insert ("insert into user(username,password,create_time,update_time)" + "values (#{username},#{password},now(),now())" ) void add (String username, String password) ; @Update ("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}" ) void update (User user) ; }
22 实战篇-更新用户基本信息_参数校验 根据接口文件中对nickname和Email的要求,对更新的用户基本信息完成校验。在注册接口中,是通过添加pattern注解对参数完成校验。
1 在实体类的成员变量加上validation提供的注解。例如’@NotNull’。 NotNull:值不能为null。 NotEmpty:值不能为null,并且内容不为空。 Email:满足邮箱格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Data public class User { @NotNull private Integer id; private String username; @JsonIgnore private String password; @NotEmpty @Pattern (regexp = "^\\S{1,10}$" ) private String nickname; @NotEmpty @Email private String email; private String userPic; private LocalDateTime createTime; private LocalDateTime updateTime; }
2 需要在具体使用该注解的函数参数部分添加@Validated注解。这样属性部分的注解(也就是上文中提到的那些)才能生效。
在controller层中。
1 2 3 4 5 @PutMapping ("/update" )public Result update (@RequestBody @Validated User user) { userService.update(user); return Result.success(); }
23 实战篇-更新用户头像 在controller层中增加updateAvatar方法。参数校验。对于avatarUrl是否是一个url地址进行校验。
1 2 3 4 5 @PatchMapping ("updateAvatar" )public Result updateAvatar (@RequestParam String avatarUrl) { userService.updateAvatar(avatarUrl); return Result.success(); }
在service层中增加updateAvatar方法。
1 2 3 4 5 6 7 8 9 10 11 12 void updateAvatar (String avatarUrl) ;@Override public void updateAvatar (String avatarUrl) { Map<String,Object> map = ThreadLocalUtil.get(); Integer id = Integer.parseInt(map.get("id" ).toString()); userMapper.updateAvatar(avatarUrl,id); }
在DAO层中增加更新头像的sql语句。
1 2 3 @Update ("update user set user_pic=#{avatarUrl},update_time=now() where id=#{id}" )void updateAvatar (String avatarUrl,Integer id) ;
在用户相关接口更新JWT令牌的时候不要把Authorization:去掉了。
24 实战篇-更新用户密码 在Controller层中新增updatePwd方法,注意添加PatchMapping注解。我们需要申明一个Map类型的参数,用来接收前端提交的json参数。之前我们使用user实体对象来接收,是因为我们之前传进来的键名,刚好和user实体对象的属性名相同。但是现在我们的请求参数的键名是old_pwd,new_pwd,re_pwd,和实体对象的属性名没有办法相同了,于是申明一个Map类型的参数。MVC框架会自动帮我们把json数据转化成map集合对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @PatchMapping ("/updatePwd" )public Result updatePwd (@RequestBody Map<String,String> params) { String oldPwd = params.get("old_pwd" ); String newPwd = params.get("new_pwd" ); String rePwd = params.get("re_pwd" ); if (!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(rePwd)){ return Result.error("缺少必要的参数" ); } Map<String,Object> map = ThreadLocalUtil.get(); String username = map.get("username" ).toString(); User loginUser = userService.findByUserName(username); if (!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){ return Result.error("原密码填写不正确" ); } if (!rePwd.equals(newPwd)){ return Result.error("两次填写的新密码不一样" ); } userService.updatePwd(newPwd); return Result.success(); }
在Service层中新增updatePwd方法完成密码更新。
1 2 3 4 5 6 7 8 9 10 11 void updatePwd (String newPwd) ;@Override public void updatePwd (String newPwd) { Map<String,Object> map = ThreadLocalUtil.get(); Integer id = Integer.parseInt(map.get("id" ).toString()); userMapper.updatePwd(Md5Util.getMD5String(newPwd),id); }
在DAO层中新增相应的sql语句。
1 2 @Update ("update user set password=#{md5String},update_time=now() where id=#{id}" )void updatePwd (String md5String, Integer id) ;
25 实战篇-新增文章分类 在CategoryController中新增add方法。使用@RequestBody注解。 使用@PostMapping注解,并没有添加映射路径,后续会在Category类上添加路径\Category。 同时在新增文章分类的controller层上增加了参数校验。
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping ("/category" )public class CategoryController { @Autowired private CategoryService categoryService; @PostMapping public Result add (@RequestBody @Validated Category category) { categoryService.add(category); return Result.success(); } }
在service层添加add方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface CategoryService { void add (Category category) ; } @Service public class CategoryServiceImpl implements CategoryService { @Autowired private CategoryMapper categoryMapper; @Override public void add (Category category) { category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); Map<String,Object> map = ThreadLocalUtil.get(); Integer userid = Integer.parseInt(map.get("id" ).toString()); category.setCreateUser(userid); categoryMapper.add(category); } }
在DAO层添加对应的sql语句。
1 2 3 4 5 6 7 @Mapper public interface CategoryMapper { @Insert ("insert into category(category_name,category_alias,create_user,create_time,update_time) " + "values(#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})" ) void add (Category category) ; }
26 实战篇-文章分类列表 和新增文章分类的请求路径都是”\category”,是通过请求方式的不同来区分这两个接口的。新增是POST,文章分类列表的查询是GET。 在CategoryController中新增list方法。
1 2 3 4 5 @GetMapping public Result<List<Category>> list(){ List<Category> cs = categoryService.list(); return Result.success(cs); }
在CategoryService中新增list方法。
1 2 3 4 5 6 7 8 9 10 11 List<Category> list () ;@Override public List<Category> list () { Map<String,Object> map = ThreadLocalUtil.get(); Integer id = Integer.parseInt(map.get("id" ).toString()); return categoryMapper.list(id); }
在mapper层中执行对应的select语句。要根据create_user字段进行查询。
1 2 3 @Select ("select * from category where create_user = #{id}" )List<Category> list (Integer id) ;
修改postman中的日期格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class Category { private Integer id; @NotEmpty private String categoryName; @NotEmpty private String categoryAlias; private Integer createUser; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime createTime; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime updateTime; }
27 实战篇-获取文章分类详情 在categoryController中新增detail方法。
1 2 3 4 5 @GetMapping ("/detail" )public Result<Category> detail (Integer id) { Category c = categoryService.findById(id); return Result.success(c); }
在categoryService中新增findById方法。
1 2 3 4 5 6 7 8 9 10 Category findById (Integer id) ;@Override public Category findById (Integer id) { Category c = categoryMapper.findById(id); return c; }
在categoryMapper中新增对应sql语句。
1 2 3 @Select ("select * from category where id = #{id}" )Category findById (Integer id) ;
28 实战篇-更新文章分类 请求路径和获取文章分类列表一样,但是请求方式与之(get)不同,为put。 在CategoryController中添加update方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @PutMapping public Result update (@RequestBody @Validated Category category) { categoryService.update(category); return Result.success(); } @Data public class Category { @NotNull private Integer id; @NotEmpty private String categoryName; @NotEmpty private String categoryAlias; private Integer createUser; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime createTime; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime updateTime; }
NotNull和NotEmpty是有区别的。NotNull是不能不传。NoEmpty如果是字符串的话,还不能是空字符串。 在CategoryService中添加update方法。
1 2 3 4 5 6 7 8 9 10 void update (Category category) ;@Override public void update (Category category) { category.setUpdateTime(LocalDateTime.now()); categoryMapper.update(category); }
在CategoryMapper中添加对应的sql语句。
1 2 3 @Update ("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id=#{id}" )void update (Category category) ;
29 实战篇-更新文章分类和添加文章分类_分组校验 把校验项进行分组,在完成不同的功能的时候,校验指定组中的校验项。 1 定义分组。 2 定义校验项时指定归属的分组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class Category { @NotNull (groups = Update.class ) private Integer id ; @NotEmpty (groups = {Add.class , Update .class }) private String categoryName ; @NotEmpty (groups = {Add.class , Update .class }) private String categoryAlias ; private Integer createUser; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime createTime; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime updateTime; public interface Add {} public interface Update {} }
plus版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Data public class Category { @NotNull (groups = Update.class ) private Integer id ; @NotEmpty private String categoryName; @NotEmpty private String categoryAlias; private Integer createUser; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime createTime; @JsonFormat (pattern = "yyyy-MM-dd HH:mm:ss" ) private LocalDateTime updateTime; public interface Add extends Default {} public interface Update extends Default {} }
3 校验时指定要校验的分组。
1 2 3 4 5 6 7 8 9 10 11 @PostMapping public Result add (@RequestBody @Validated(Category.Add.class) Category category) { categoryService.add(category); return Result.success(); } @PutMapping public Result update (@RequestBody @Validated(Category.Update.class) Category category) { categoryService.update(category); return Result.success(); }
定义校验项时如果没有指定分组,则属于Default分组,分组可以继承。
30 实战篇-更新文章 在ArticleController中添加add方法。
1 2 3 4 5 6 7 8 9 10 11 @RestController @RequestMapping ("/article" )public class ArticleController { @Autowired private ArticleService articleService; @PostMapping public Result add (@RequestBody Article article) { articleService.add(article); return Result.success(); } }
在ArticleService中添加add方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void add (Article article) ;@Service public class ArticleServiceImpl implements ArticleService { @Autowired private ArticleMapper articleMapper; @Override public void add (Article article) { article.setCreateTime(LocalDateTime.now()); article.setUpdateTime(LocalDateTime.now()); Map<String,Object> map = ThreadLocalUtil.get(); Integer userId = Integer.parseInt(map.get("id" ).toString()); article.setCreateUser(userId); articleMapper.add(article); } }
在ArticleMapper中添加insert语句。
1 2 3 4 5 6 7 @Mapper public interface ArticleMapper { @Insert ("insert into article(title,content,cover_img,state,category_id,create_user,create_time,update_time) " + "values(#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})" ) void add (Article article) ; }
31 实战篇-新增文章参数校验_自定义校验 对除了state之外的其他字段添加校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Data public class Article { private Integer id; @NotEmpty @Pattern (regexp = "^//S{1,10}$" ) private String title; @NotEmpty private String content; @NotEmpty @URL private String coverImg; private String state; @NotNull private Integer categoryId; private Integer createUser; private LocalDateTime createTime; private LocalDateTime updateTime; } @PostMapping public Result add (@RequestBody @Validated Article article) { articleService.add(article); return Result.success(); }
对于state,需要用到自定义校验。
自定义校验是指已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验(自定义校验注解)。
1 自定义注解state。 模仿NotEmpty注解写代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Documented @Constraint ( validatedBy = {StateValidation.class }//指定提供校验规则的类 ) @Target ( {ElementType.FIELD})@Retention (RetentionPolicy.RUNTIME)public @interface State { String message () default "state参数的值只能是已发布或者草稿" ; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
2 自定义校验校验数据的类StateValidation,实现ConstraintValidator接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class StateValidation implements ConstraintValidator <State ,String > { @Override public boolean isValid (String value, ConstraintValidatorContext constraintValidatorContext) { if (value == null ){ return false ; } if (value.equals("已发布" )||value.equals("草稿" )){ return true ; } return false ; } }
3 在需要校验的地方使用自定义注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data public class Article { private Integer id; @NotEmpty @Pattern (regexp = "^\\S{1,10}$" ) private String title; @NotEmpty private String content; @NotEmpty @URL private String coverImg; @State private String state; @NotNull private Integer categoryId; private Integer createUser; private LocalDateTime createTime; private LocalDateTime updateTime; }
32 实战篇-文章分类列表查询(条件分类) 在ArticleController中新增list方法。需要返回total和一个list包含多个文章。
1 2 3 4 5 6 7 8 9 10 11 @GetMapping public Result<PageBean<Article>> list( Integer pageNum, Integer pageSize, @RequestParam (required = false ) Integer categoryId, @RequestParam (required = false ) String state ){ PageBean<Article> pb = articleService.list(pageNum,pageSize,categoryId,state); return Result.success(pb); }
在ArticleService中新增list方法。构建一个PageBean对象用来封装要查询的数据。分页查询使用PageHelper(mybatis插件)来实现。 导入pagehelper依赖。
1 2 3 4 5 6 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > 1.4.7</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 PageBean<Article> list (Integer pageNum, Integer pageSize, Integer categoryId, String state) ;@Override public PageBean<Article> list (Integer pageNum, Integer pageSize, Integer categoryId, String state) { PageBean<Article> pb = new PageBean<>(); PageHelper.startPage(pageNum,pageSize); Map<String,Object> map = ThreadLocalUtil.get(); Integer userId = Integer.parseInt(map.get("id" ).toString()); List<Article> as = articleMapper.list(userId,categoryId,state); Page<Article> p = (Page<Article>) as; pb.setTotal(p.getTotal()); pb.setItems(p.getResult()); return pb; }
在ArticleMapper中新增相应的Mapper映射条件。 映射配置文件ArticleService.xml所在的路径必须和Mapper保持一致,该ArticleService.xml文件的名字必须和ArticleMapper.java的名字保持一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.kongming.mapper.ArticleMapper" > <select id ="list" resultType ="com.kongming.pojo.Article" > select * from article <where > <if test ="categoryId!=null" > category_id=#{categoryId} </if > <if test ="state!=null" > and state=#{state} </if > and create_user=#{userId} </where > </select > </mapper >
1 2 List<Article> list (Integer userId, Integer categoryId, String state) ;
pageNum表示页数,pageSize表示每页的article数量,pageNum=1表示查询的是第一页的数据,如果此时pageSize=2,则表示第一页里需要两条article数据。 controller中函数的参数名字必须和接口文档中的一致。
33 实战篇-文件上传_本地存储 前端页面三要素。 1 请求方式必须是post。 2 enctype必须是multipart/form-data。 3 文件表单下对应的tpye类型必须是file。
后端MultipartFile。
1 2 3 4 5 String getOriginalFilename () ;void transferTo (File dest) ;long getSize () ;byte [] getBytes();InputStream getInputStream () ;
在FileUploadController中新增upload方法。获取文件内容的输入流,写入到本地磁盘文件。
1 2 3 4 5 6 7 8 9 10 11 12 @RestController public class FileUploadController { @PostMapping ("/upload" ) public Result<String> upload (MultipartFile file) throws IOException { String originalFilename = file.getOriginalFilename(); String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("." )); file.transferTo(new File("C:\\SunYuQi\\data\\研一\\工作\\files\\" +filename)); return Result.success("url访问地址..." ); } }
34 实战篇-文件上传_阿里云OSS_准备工作 云:互联网上的一堆计算机。
阿里云:阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。
阿里云OSS:阿里云对象存储OSS(Object Storage Service),是一款海量,安全,低成本,高可靠的云存储服务。使用OSS,可以通过网络随时存储和调用包括文本,图片,音频和视频等在内的各种文件。
第三方服务-通用思路 准备工作->参照官方SDK编写入门程序->继承使用
SDK:Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包),代码示例等,都可以叫做SDK。
准备工作 注册登录(实名认证)->充值->开通对象存储服务(OSS)->创建bucket->获取AccessKey(密钥)->参照官方SDK编写入门程序->案例集成OSS
bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
35 实战篇-文件上传_阿里云OSS_入门程序 下载SDK。在pom.xml中引入阿里云提供的依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.17.4</version > </dependency > <dependency > <groupId > javax.xml.bind</groupId > <artifactId > jaxb-api</artifactId > <version > 2.3.1</version > </dependency > <dependency > <groupId > javax.activation</groupId > <artifactId > activation</artifactId > <version > 1.1.1</version > </dependency > <dependency > <groupId > org.glassfish.jaxb</groupId > <artifactId > jaxb-runtime</artifactId > <version > 2.3.3</version > </dependency >
文件上传demo代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class Demo { public static void main (String[] args) throws Exception { String endpoint = "https://oss-cn-beijing.aliyuncs.com" ; String accessKeyId = "" ; String accessKeySecret = "" ; String bucketName = "kongming234" ; String objectName = "001.jpg" ; String region = "cn-beijing" ; OSS ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); try { String content = "Hello OSS,你好世界" ; PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new FileInputStream("C:\\SunYuQi\\data\\研一\\工作\\files\\孔明.jpg" )); PutObjectResult result = ossClient.putObject(putObjectRequest); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason." ); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network." ); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null ) { ossClient.shutdown(); } } } }
36 实战篇-上传文件_阿里云OSS_程序集成 阿里云工具类开发,实际上就是对上面demo的修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class AliOssUtil { private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com" ; private static final String ACCESS_KEY_ID = "" ; private static final String ACCESS_KEY_SECRET = "" ; private static final String BUCKET_NAME = "kongming234" ; public static String uploadFile (String objectName, InputStream in) throws Exception { OSS ossClient = new OSSClient(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET); String url = "" ; try { String content = "Hello OSS,你好世界" ; PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, objectName, in); PutObjectResult result = ossClient.putObject(putObjectRequest); url = "https://" +BUCKET_NAME+"." +ENDPOINT.substring(ENDPOINT.lastIndexOf("/" )+1 )+"/" +objectName; } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason." ); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network." ); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null ) { ossClient.shutdown(); } } return url; } }
在controller中修改。
1 2 3 4 5 6 7 8 9 10 @PostMapping ("/upload" )public Result<String> upload (MultipartFile file) throws Exception { String originalFilename = file.getOriginalFilename(); String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("." )); String url = AliOssUtil.uploadFile(filename,file.getInputStream()); return Result.success(url); }
37 实战篇-登录优化-redis_思路分析 登录:在用户修改了密码之后,服务器会下发新令牌,旧令牌应该作废。但之前的程序中并没有作废旧的令牌。
令牌主动失效机制:(虎符)
登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中。
LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌。
当用户修改密码成功后,删除redis中存储的旧令牌。
38 实战篇-登录优化_redis_SpringBoot集成redis SpringBoot集成redis
导入spring-boot-starter-data-redis起步依赖
在redis起步依赖中,会自动往ROC容器中注入StringRedisTemplate对象。
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/kongming username: root password: 181234 data: redis: host: localhost port: 6379
调用API(StringRedisTemplate)完成字符串的存取操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SpringBootTest public class RedisTest { @Autowired private StringRedisTemplate stringRedisTemplate; @Test public void testSet () { ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set("username" ,"kongming" ); operations.set("id" ,"1" ,15 , TimeUnit.SECONDS); } @Test public void testGet () { ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); System.out.println(operations.get("username" )); } }
39实战篇-登录优化_redis_主动失效机制实现 令牌主动失效机制:(虎符)
登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中。 在userContorller中修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @PostMapping ("/login" )public Result<String> login (@Pattern(regexp = "^\\S{5,16}$" ) String username, @Pattern (regexp = "^\\S{5,16}$" ) String password) { User loginUser = userService.findByUserName(username); if (loginUser == null ){ return Result.error("用户名错误" ); } if (Md5Util.getMD5String(password).equals(loginUser.getPassword())){ Map<String, Object> claims = new HashMap<>(); claims.put("id" ,loginUser.getId()); claims.put("username" ,loginUser.getUsername()); String token = JwtUtil.genToken(claims); ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set(token,token,1 , TimeUnit.HOURS); return Result.success(token); } return Result.error("密码错误" ); }
LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization" ); try { ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); String redisToken = operations.get(token); if (redisToken == null ) { throw new RuntimeException(); } Map<String, Object> claims = JwtUtil.parseToken(token); ThreadLocalUtil.set(claims); return true ; } catch (Exception e) { response.setStatus(401 ); return false ; } }
当用户修改密码成功后,删除redis中存储的旧令牌。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @PatchMapping ("/updatePwd" )public Result updatePwd (@RequestBody Map<String,String> params,@RequestHeader("Authorization" ) String token) { String oldPwd = params.get("old_pwd" ); String newPwd = params.get("new_pwd" ); String rePwd = params.get("re_pwd" ); if (!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(rePwd)){ return Result.error("缺少必要的参数" ); } Map<String,Object> map = ThreadLocalUtil.get(); String username = map.get("username" ).toString(); User loginUser = userService.findByUserName(username); if (!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){ return Result.error("原密码填写不正确" ); } if (!rePwd.equals(newPwd)){ return Result.error("两次填写的新密码不一样" ); } userService.updatePwd(newPwd); ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.getOperations().delete(token); return Result.success(); }
40实战篇-SpringBoot项目部署 SpringBoot项目部署时,SpringBoot需要一个打包插件spring-boot-maven-plugin。
1 2 3 4 5 6 7 8 9 10 <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > 3.3.1</version > </plugin > </plugins > </build >
1 找到IDEA右侧的maven菜单,big-event->Lifecycle->package(双击开始打包)。
2 运行jar包。
1 java -jar big-event-1.0-SNAPSHOT.jar
3 注意:jar包部署,要求服务器必须有jre环境。
41实战篇-SpringBoot属性配置方式 希望部署好的端口是9090不是8080。 项目配置文件方式:
application.properties配置文件
application.yml配置文件(常用,键值对)
命令行参数方式:
1 2 java -jar big-event-1.0-SNAPSHOT.jar --键=值 java -jar big-event-1.0-SNAPSHOT.jar --server.port=10010
该参数会传递给启动类的main方法,用一个args数组来接收。
环境变量方式: 在administrator的用户环境变量中添加server.port,值为8888。 环境变量发生变化,cmd必须重新启动一下。
外部配置文件方式: 在jar包所在的路径下新建一个application.yml文件。
注意port:后面有一个空格。
配置优先级:
项目中resources目录下的application.yml.
jar包所在目录下的application.yml
操作系统环境变量
命令行参数
他们的优先级依次升高。
42实战篇-SpringBoot多环境开发-基本使用 开发环境,测试环境,生产环境
多环境开发的单文件使用 Pofiles:SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定环境下指定部分配置生效。 如何分隔不同环境的配置:
如何指定哪些配置属于哪个环境?
1 2 3 4 spring: config: activate: on-profile:环境名称
如何指定哪个环境的配置生效?
1 2 3 spring: profiles: activate: 环境名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/kongming username: root password: 181234 data: redis: host: localhost port: 6379 profiles: active: test mybatis: configuration: map-underscore-to-camel-case: true --- spring: config: activate: on-profile: dev server: port: 8081 --- spring: config: activate: on-profile: test server: port: 8082 --- spring: config: activate: on-profile: pro server: port: 8083
多环境开发的多文件使用 SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定环境下指定某些部分的配置生效。 开发 application-dev.yml 测试 application-test.yml 生产 application-pro.yml 共性配置并激活指定环境 application.yml。在该文件中使用dev,test,pro来激活相应配置文件。
43实战篇-SpringBoot多环境开发-分组 多环境开发Profiles分组功能:
服务器相关配置 application-devServer.yml
数据源相关配置 application-devDB.yml
自定义配置 application-devSelf.yml 在application.yaml中定义分组和激活指定分组。
1 2 3 4 5 6 spring: profiles: active: dev group: "dev": devServer,devDB,devSelf "test": testServier,testDB,testSelf
44实战篇-大事件前端项目开发_前置知识_js导入导出 前置知识 HTML,CSS,JS HTML:负责网页的结构。(标签:form/table/a/div/span) CSS:负责网页的表现。(样式:color/font/background/width/height) JavaScript:负责网页的行为。(交互效果)
JavaScript导入导出-按需导入 JS提供的导入导出机制,可以实现按需导入。
1 2 3 4 5 6 7 8 9 export function simpleMessage (msg ) { console .log(msg) } export function complexMessage (msg ) { console .log(new Data()+": " +msg) }
1 2 3 4 5 6 7 <script src ="showMessage.js" > </script > <script type ="module" > import {complexMessage} from './showMessage.js' <\script >
JavaScript导入导出-批量导出 js还可以实现批量导出。
1 2 3 4 5 6 7 8 9 function simpleMessage (msg ) { console .log(msg) } function complexMessage (msg ) { console .log(new Data()+": " +msg) } export {simpleMessage,complexMessage}
JavaScript导入导出-重命名 为js中函数实现重命名。
1 2 3 4 5 6 7 8 9 10 11 12 <body > <div id ="app" > <button id ="btn" > 点我展示信息</button > </div > <script type ="module" > import {complexMessage as cm} from './showMessage.js' ; document .getElementById("btn" ).onclick=function ( ) { cm('我被点击了...' ) } </script > </body >
还可以在批量导出时定义别名。
1 2 export {complexMessage as cm,simpleMessage as sm}
在html导入时要相应的修改名字。不可以使用原来的名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <body > <div id ="app" > <button id ="btn" > 点我展示信息</button > </div > <script type ="module" > import {cm} from './showMessage.js' ; document .getElementById("btn" ).onclick=function ( ) { cm('我被点击了...' ) } </script > </body > </html >
JavaScript默认导出 1 2 3 4 5 6 7 8 9 function simpleMessage (msg ) { console .log(msg) } function complexMessage (msg ) { console .log(new Data()+": " +msg) } export default {simpleMessage,complexMessage}
相应的导入代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 <body > <div id ="app" > <button id ="btn" > 点我展示信息</button > </div > <script type ="module" > import messageMethods+ from './showMessage.js' ; messageMethods.simpleMessage('aaa' ); </script > </body > </html >
45实战篇-vue概述 Vue是一款用于构建用户界面的渐进式的JavaScript框架。(官方 ) 渐进式的含义是既可以局部改造,又可以整站开发。
学习路径: 局部使用vue:
整站使用vue:
Vue项目构建工具
Vue项目目录结构
Vue项目开发流程
Element-Plus
大事件前端:
46实战篇-vue快速入门 47实战篇-vue指令_v-for 48实战篇-vue指令_v-bind 49实战篇-vue指令_v-if和v-show