新建一个项目测试,引入相应的模块:JDBC API、MySQL Driver 等
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
编写yaml配置文件连接数据库,注意:要删除原本的properties配置文件(大坑)!
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
此时,SpringBoot 已经默认进行了自动配置,在测试类测试如下:
package com.cwlin;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
class SpringBoot04DataApplicationTests {
//SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看默认数据源
System.out.println(dataSource.getClass());
//获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//关闭数据库连接
connection.close();
}
}
得到结果:SpringBoot 默认自动配置的数据源为 class com.zaxxer.hikari.HikariDataSource
。
查看数据源配置类:DataSourceProperties
@ConfigurationProperties(
prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName = true;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private DataSourceInitializationMode initializationMode;
private String platform;
private List<String> schema;
private String schemaUsername;
private String schemaPassword;
private List<String> data;
private String dataUsername;
private String dataPassword;
private boolean continueOnError;
private String separator;
private Charset sqlScriptEncoding;
private EmbeddedDatabaseConnection embeddedDatabaseConnection;
private DataSourceProperties.Xa xa;
private String uniqueName;
//......
}
查看数据源的自动配置类:DataSourceAutoConfiguration
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
public DataSourceAutoConfiguration() {
}
//......
static class PooledDataSourceCondition extends AnyNestedCondition {
PooledDataSourceCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@Conditional({DataSourceAutoConfiguration.PooledDataSourceAvailableCondition.class})
static class PooledDataSourceAvailable {
PooledDataSourceAvailable() {
}
}
@ConditionalOnProperty(
prefix = "spring.datasource",
name = {"type"}
)
static class ExplicitType {
ExplicitType() {
}
}
}
@Configuration(
proxyBeanMethods = false
)
@Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}
//......
}
这里通过 Import
注解导入的类,都在 DataSourceConfiguration
配置类下。Spring Boot 2.3.7 默认使用 HikariDataSource 政伟数据源,而以前版本如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源。
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;可以使用 spring.datasource.type 指定自定义的数据源类型,值为要使用的连接池实现的完全限定名。
有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),连接后就可以使用原生的 JDBC 语句来操作数据库。
即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用。
JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure.jdbc;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.JdbcProperties.Template;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean({JdbcOperations.class})
class JdbcTemplateConfiguration {
JdbcTemplateConfiguration() {
}
@Bean
@Primary
JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
Template template = properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}
}
JdbcTemplate主要提供以下几类方法:
Spring Boot 默认提供了数据源和 org.springframework.jdbc.core.JdbcTemplate,JdbcTemplate 会自己注入数据源,用于简化 JDBC操作;能够自动提交事务;还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试
package com.cwlin.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
public class JdbcController {
@Autowired
JdbcTemplate jdbcTemplate;
//查询数据库的所有信息 没有实体类,数据库中的东西,通过map获取
@GetMapping("/userList")
public List<Map<String,Object>> userList(){
String sql = "select * from user";
List<Map<String, Object>> mapList = jdbcTemplate.queryForList(sql);
return mapList;
}
@GetMapping("/insertUser")
public String insertUser(){
String sql = "insert into mybatis.user(id,name,pwd) values(6,'cwlin','111111')";
jdbcTemplate.update(sql);
return "insert: OK!";
}
@GetMapping("/updateUser/{id}")
public String updateUser(@PathVariable("id") int id){
String sql = "update mybatis.user set name=?,pwd=? where id="+id;
//封装
Object[] objects = new Object[2];
objects[0] = "coder_lcw";
objects[1] = "123456";
jdbcTemplate.update(sql, objects);
return "update: OK!";
}
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") int id){
String sql = "delete from mybatis.user where id=?";
jdbcTemplate.update(sql, id);
return "delete: OK!";
}
}
逐个测试CURD操作的请求!
配置 | 缺省值 | 说明 |
---|---|---|
name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:“DataSource-” + System.identityHashCode(this) | |
jdbcUrl | 连接数据库的url,不同数据库不一样。例如: mysql:jdbc:mysql://10.20.153.104:3306/druid2 oracle:jdbc:oracle:thin:@10.20.149.85:1521:ocnauto |
|
username | 连接数据库的用户名 | |
password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter,详细看这里。 | |
driverClassName | 根据url自动识别 | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) |
initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
maxActive | 8 | 最大连接池数量 |
maxIdle | 8 | 已经不再使用,配置了也没效果 |
minIdle | 最小连接池数量 | |
maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
maxOpenPreparedStatements | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 | |
validationQueryTimeout | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 | |
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 |
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 1分钟(1.0.14) | 有两个含义:1) Destroy线程会检测连接的间隔时间,2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
numTestsPerEvictionRun | 不再使用,一个DruidDataSource只支持一个EvictionRun | |
minEvictableIdleTimeMillis | 30分钟(1.0.14) | 连接保持空闲而不被驱逐的最长时间 |
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | |
proxyFilters | 类型是List |
在pom.xml中,导入 Druid 数据源依赖
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.6version>
dependency>
切换数据源:Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # Druid数据源
切换数据源后,在测试类中注入 DataSource,测试数据库连接
class com.alibaba.druid.pool.DruidDataSource
设置数据源连接初始化大小、最大连接数、等待时间、最小连接数等设置项
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
导入Log4j 的依赖
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
现在需要我们自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了,即添加 DruidDataSource 组件到容器中,并绑定属性
package com.cwlin.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DruidConfig {
//将自定义的Druid配置进IOC容器
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource(){
return new DruidDataSource();
}
}
在测试类中进行测试!
package com.cwlin;
import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
class SpringBoot04DataApplicationTests {
//SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看默认数据源
System.out.println(dataSource.getClass());
//获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//测试Druid数据源
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
//关闭数据库连接
connection.close();
}
}
从输出结果中可以看到配置参数已经生效!
class com.alibaba.druid.pool.DruidDataSource
com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@699d96bc
druidDataSource 数据源最大连接数:20
druidDataSource 数据源初始化连接数:6
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。
设置 Druid 的后台管理页面,比如登录账号、密码等,并配置后台管理
package com.cwlin.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DruidConfig {
//将自定义的Druid配置进IOC容器
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource(){
return new DruidDataSource();
}
//配置 Druid 监控管理后台的Servlet
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(
new StatViewServlet(), "/druid/*");
//后台需要有人登录,账号密码配置
HashMap<String, String> initParameters = new HashMap<>();
//增加配置
//这些参数可以在com.alibaba.druid.support.http.StatViewServlet的父类ResourceServlet中找到
initParameters.put("loginUsername","admin"); //后台管理界面的登录账号
initParameters.put("loginPassword","123456"); //后台管理界面的登录密码
//Druid 后台允许谁可以访问 allow
//initParameters.put("allow", "localhost"):表示只有本机可以访问
//initParameters.put("allow", ""):为空或者为null时,表示允许所有访问
initParameters.put("allow", "");
//Druid 后台禁止谁可以访问deny
initParameters.put("deny", "192.168.1.66"); //表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParameters);
return bean;
}
}
配置完毕,访问:http://localhost:8080/druid/login.html,并登录账号密码;进行一次SQL查询,再返回查看Druid后台如下
配置 Druid web 监控 filter 过滤器
//配置 Druid 监控 之 web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
Map<String, String> initParameters = new HashMap<>();
initParameters.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParameters);
//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
在工作中,按需求进行配置即可,主要用作监控!
官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
Maven仓库地址:org.mybatis.spring.boot » mybatis-spring-boot-starter
导入 MyBatis 所需要的依赖
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
配置数据库连接信息(不变)
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # Druid数据源
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
测试数据库是否连接成功(不变)
package com.cwlin;
import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
class SpringBoot04DataApplicationTests {
//SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看默认数据源
System.out.println(dataSource.getClass());
//获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//测试Druid数据源
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
//关闭数据库连接
connection.close();
}
}
在 pojo 包下创建实体类 User,并导入 Lombok
package com.cwlin.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String pwd;
}
在 mappe 包下创建对应的 Mapper 接口,或者在main方法上添加注解 @MapperScan(“com.cwlin.mapper”)
package com.cwlin.mapper;
import com.cwlin.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
//这个注解表示这是MyBatis的Mapper类
@Mapper
@Repository
public interface UserMapper {
//public static final int age =18; //公共静态常量
List<User> selectUserList();
User selectUserById(int id);
int insertUser(User user);
int updateUser(User user);
int deleteUser(int id);
}
整合 mybatis
#整合mybatis
mybatis:
type-aliases-package: com.cwlin.pojo
#解决绑定异常:mapper.xml最好和接口的包名路径一致
mapper-locations: classpath:mybatis/mapper/*.xml
在 resources/mybatis 路径下创建对应的Mapper映射文件,或者使用 SQL 注解标记 Mapper 接口中的方法
<mapper namespace="com.hxh.mapper.UserMapper">
<select id="selectUserList" resultType="User">
select * from user
select>
<select id="selectUserById" resultType="User">
select * from user where id = #{id}
select>
<insert id="insertUSer" parameterType="User">
insert into User (id,name,pwd) values (#{id},#{name},#{pwd})
insert>
<update id="updateUser" parameterType="User">
update user set name=#{name},pwd=#{pwd} where id=#{id}
update>
<delete id="deleteUser" parameterType="User">
delete from user where id = #{id}
delete>
mapper>
package com.cwlin.mapper;
import com.cwlin.pojo.User;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
//这个注解表示这是MyBatis的Mapper类
@Mapper
@Repository
public interface UserMapper {
//public static final int age =18; //公共静态常量
@Select("select * from user")
List<User> selectUserList();
@Select("select * from user where id = #{id}")
User selectUserById(int id);
@Insert("insert into User (id,name,pwd) values (#{id},#{name},#{pwd})")
int insertUser(User user);
@Update("update user set name=#{name},pwd=#{pwd} where id=#{id}")
int updateUser(User user);
@Delete("delete from user where id = #{id}")
int deleteUser(int id);
}
maven配置资源过滤问题(可以省略)
<resources>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
<filtering>truefiltering>
resource>
resources>
编写 User 类的 UserController,并进行测试:
package com.cwlin.controller;
import com.cwlin.mapper.UserMapper;
import com.cwlin.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/mybatis")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/selectUserList")
public List<User> selectUserList(){
List<User> userList = userMapper.selectUserList();
for (User user : userList) {
System.out.println(user);
}
return userList;
}
@GetMapping("/selectUserById")
public User selectUserById(){
User user = userMapper.selectUserById(1);
System.out.println(user);
return user;
}
@GetMapping("/insertUser")
public String addUser(){
int i = userMapper.insertUser(new User(6, "coder_lcw", "123456"));
System.out.println("返回在数据库中影响的行数为:"+i);
return "insertUser: OK!";
}
@GetMapping("/updateUser")
public String updateUser(){
int i = userMapper.updateUser(new User(6, "lcw", "123456"));
System.out.println("返回在数据库中影响的行数为:"+i);
return "updateUser: OK!";
}
@GetMapping("/deleteUser")
public String deleteUser(){
int i = userMapper.deleteUser(6);
System.out.println("返回在数据库中影响的行数为:"+i);
return "deleteUser: OK!";
}
}
MyBatis 主要提供了以下CRUD注解:
配置驼峰映射
mybatis:
configuration:
#配置项:开启下划线到驼峰的自动转换. 作用:将数据库字段根据驼峰规则自动注入到对象属性。
map-underscore-to-camel-case: true
为了解决对象属性和字段驼峰不一致的问题,我们可以使用映射注解@Results来指定映射关系。
Mybatis主要提供这些映射注解:
例如上面的list方法,我们可以在查询SQL的基础上,指定返回的结果集的映射关系,其中property表示实体对象的属性名,column表示对应的数据库字段名。
@Results({
@Result(property = "userId", column = "USER_ID"),
@Result(property = "username", column = "USERNAME"),
@Result(property = "password", column = "PASSWORD"),
@Result(property = "mobileNum", column = "PHONE_NUM")
})
@Select("select * from t_user")
List<User> list();
为了方便演示和免除手工编写映射关系的烦恼,这里提供了一个快速生成映射结果集的方法,具体内容如下:
//用于获取结果集的映射关系
public static String getResultsStr(Class origin) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("@Results({\n");
for (Field field : origin.getDeclaredFields()) {
String property = field.getName();
//映射关系:对象属性(驼峰)->数据库字段(下划线)
String column = new PropertyNamingStrategy.SnakeCaseStrategy().translate(field.getName()).toUpperCase();
stringBuilder.append(String.format("@Result(property = \"%s\", column = \"%s\"),\n", property, column));
}
stringBuilder.append("})");
return stringBuilder.toString();
}
MyBatis-3 主要提供了以下CRUD的高级注解:
这些高级注解主要用于动态SQL,这里以@SelectProvider 为例,主要包含两个注解属性,其中type表示工具类,method 表示工具类的某个方法,用于返回具体的SQL。
添加UserMapper接口用于数据查询:
package com.cwlin.mapper;
@Mapper
public interface UserMapper {
//方式1:使用注解编写SQL。
@Select("select * from t_user")
List<User> list();
//方式2:使用注解指定某个工具类的方法来动态编写SQL.
@SelectProvider(type = UserSqlProvider.class, method = "listByUsername")
List<User> listByUsername(String username);
//延伸:上述两种方式都可以附加@Results注解来指定结果集的映射关系.
//注意:如果符合下划线转驼峰的匹配项可以直接省略不写。
@Results({
@Result(property = "userId", column = "USER_ID"),
@Result(property = "username", column = "USERNAME"),
@Result(property = "password", column = "PASSWORD"),
@Result(property = "mobileNum", column = "PHONE_NUM")
})
@Select("select * from t_user")
List<User> listSample();
//延伸:无论什么方式,如果涉及多个参数,则必须加上@Param注解;否则无法使用EL表达式获取参数。
@Select("select * from t_user where username like #{username} and password like #{password}")
User get(@Param("username") String username, @Param("password") String password);
@SelectProvider(type = UserSqlProvider.class, method = "getBadUser")
User getBadUser(@Param("username") String username, @Param("password") String password);
}
添加UserSqlProvider,用于生成SQL的工具类:
package com.cwlin.mapper;
//作用:根据复杂的业务需求来动态生成SQL
//目标:使用Java工具类来替代传统的XML文件(例如:UserSqlProvider.java <-- UserMapper.xml)
public class UserSqlProvider {
//方式1:在工具类的方法里,可以自己手工编写SQL
public String listByUsername(String username) {
return "select * from t_user where username =#{username}";
}
//方式2:也可以根据官方提供的API来编写动态SQL
public String getBadUser(@Param("username") String username, @Param("password") String password) {
return new SQL() {{
SELECT("*");
FROM("t_user");
if (username != null && password != null) {
WHERE("username like #{username} and password like #{password}");
} else {
WHERE("1=2");
}
}}.toString();
}
}
在web开发中,安全第一位。即使,这是非功能性需求
认证(登录)、授权(VIP1,VIP2等)、功能权限、访问权限、菜单权限
我们使用过滤器、拦截器需要写大量的原生代码,这样很不方便
因此,在网址设计之初,就应该考虑到权限验证的安全问题,其中 SpringSecurity、Shiro 使用很多
Spring Security 是针对 spring 项目的安全框架,也是 Springboot 底层安全模块默认的技术选型。它可以实现强大的Web安全控制,只需要引入 spring-boot-spring-security
模块的依赖,进行少量的配置,就可以实现强大的安全管理。
SpringBoot 中的 SpringSecurity 依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
记住几个类:
WebSecurityConfigurerAdapter
:自定义Security策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启WebSecurity模式两个单词:(en是认证,or是权限)
Authentication
Authorization
导入静态资源(略)
在 controller
包下,编写 RouterController
类,实现页面跳转
package com.cwlin.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RouterController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "views/login";
}
@RequestMapping("/{level}/{id}")
public String level1(@PathVariable("level") String level, @PathVariable("id") String id){
return "views/"+level+"/"+id;
}
}
写一个 SecurityConfig
类继承 WebSecurityConfigurerAdapter
,使用 @EnableWebSecurity
注解开启web安全服务
HttpSecurity security
AuthenticationManagerBuilder builder
package com.cwlin.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//请求授权的规则:首页所有人都可访问,功能页需要相应权限
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限,默认跳转到登录页面
http.formLogin();
}
//认证
//Spring Boot 2.1.x 可以直接使用
//Spring Security 5.0 以后默认需要密码加密方式,设置密码编码 passwordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这些数据一般从数据库中读取,这里是在内存中测试数据
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("cwlin").password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip2","vip3")
.and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1");
}
}
登录
.formLogin()
,SpringBoot默认的登录页面:/login
.loginPage("/toLogin")
.usernameParameter("username").passwordParameter("password")
.loginProcessingUrl("/user/login")
//没有权限,跳转到默认登录页面:http://localhost:8080/login
http.formLogin().loginPage("/toLogin") //自定义登录页面
.usernameParameter("username").passwordParameter("password") //自定义用户名和密码的参数
.loginProcessingUrl("/user/login"); //指定表单提交url
注销
.logout()
,SpringBoot默认的注销页面:/logout
"/"
控制器.csrf().disable()
//开启注销功能,跳转到默认注销首页:http://localhost:8080/logout
http.logout().logoutSuccessUrl("/"); //注销成功后跳转到 "/" 的Controller
http.csrf().disable(); //版本不同问题,可能会出现注销失败,关闭csrf
记住我
.rememberMe()
,本质就是存一个cookies,默认保存2周.rememberMeParameter("remember")
login.html
中添加 checkbox 复选框,与记住我的 name 属性绑定//开启记住我功能,本质就是记住一个cookies,默认保存2周
http.rememberMe().rememberMeParameter("remember");
<div class="field">
<input type="checkbox" name="remember"> 记住我
div>
要实现前端根据用户权限选择性展示元素,使用 SpringSecurity 和 thymeleaf 的整合包,即导入依赖:thymeleaf-extras-springsecurity5
,并在 html 中进行引用
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
在 RouterController 类中,添加 toUserCenter 方法,用于跳转到用户中心
@RequestMapping("/user/toUserCenter")
public String toUserCenter(){
return "views/userCenter";
}
修改前端页面 index.html
sec:authorize="!isAuthenticated()"
<div class="right menu">
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon">i>登录
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/user/toUserCenter}">
<i class="address card icon">i>
用户名:<span sec:authentication="principal.username">span>
角色:<span sec:authentication="principal.authorities">span>
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon">i>注销
a>
div>
div>
根据用户角色的权限,动态展现功能菜单
<div>
<br>
<div class="ui three column stackable grid">
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon">i> Level-1-1a>div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon">i> Level-1-2a>div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon">i> Level-1-3a>div>
div>
div>
div>
div>
<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon">i> Level-2-1a>div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon">i> Level-2-2a>div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon">i> Level-2-3a>div>
div>
div>
div>
div>
<div class="column" sec:authorize="hasRole('vip3')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon">i> Level-3-1a>div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon">i> Level-3-2a>div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon">i> Level-3-3a>div>
div>
div>
div>
div>
div>
div>
什么是 Shiro:
Shiro 的功能:
Shiro架构(外部)
Subject
:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者。SecurityManager
:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色。Realm
:Shiro从Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource。Subject
:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。SecurityManager
:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。Realm
: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。Shiro架构(内部)
官方文档:http://shiro.apache.org/tutorial.html
创建模块 SpringBoot-06-Shiro/Shiro-01-QuickStart
,导入pom依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringBoot-06-ShiroartifactId>
<groupId>com.cwlingroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>Shiro-01-QuickStartartifactId>
<dependencies>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jcl-over-slf4jartifactId>
<version>2.0.0-alpha3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>2.0.0-alpha3version>
dependency>
dependencies>
<properties>
<maven.compiler.source>15maven.compiler.source>
<maven.compiler.target>15maven.compiler.target>
properties>
project>
在 resources 路径下,创建 shiro 配置文件 log4j.properties 和 shiro.ini(需要下载ini插件)
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache=WARN
# Spring
log4j.logger.org.springframework=WARN
# Default Shiro logging
log4j.logger.org.apache.shiro=INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
# =============================================================================
# Quickstart INI Realm configuration
#
# For those that might not understand the references in this file, the
# definitions are all based on the classic Mel Brooks' film "Spaceballs". ;)
# =============================================================================
# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
在 Java 文件夹下,创建 Quickstart 类,测试 Shiro 使用
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);
// Now that a simple Shiro environment is set up, let's see what you can do:
// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();
// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//all done - log out!
currentUser.logout();
System.exit(0);
}
}
输出日志结果
2021-08-13 17:35:12,781 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler...
2021-08-13 17:35:13,240 INFO [Quickstart] - Retrieved the correct value! [aValue]
2021-08-13 17:35:13,248 INFO [Quickstart] - User [lonestarr] logged in successfully.
2021-08-13 17:35:13,249 INFO [Quickstart] - May the Schwartz be with you!
2021-08-13 17:35:13,249 INFO [Quickstart] - You may use a lightsaber ring. Use it wisely.
2021-08-13 17:35:13,250 INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
源码分析
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 0、根据shiro.ini配置文件,创建factory对象,设置SecurityUtils工具类
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 1、获取当前的用户对象Subject
Subject currentUser = SecurityUtils.getSubject();
// 2、通过当前用户拿到Session(不需要 Web 或 EJB 容器!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// 3、判断当前的用户是否被认证
if (!currentUser.isAuthenticated()) {
// 3.1、通过用户名和密码生成一个token令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// 3.2、设置记住我功能
token.setRememberMe(true);
// 3.3、执行登录操作
try {
currentUser.login(token);
} catch (UnknownAccountException uae) { // 未知账户异常
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) { // 密码错误异常
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) { //账户锁定异常
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) { // 认证异常
//unexpected condition? error?
}
}
// 4、获得当前用户的认证
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 5、判断当前用户是否拥有什么角色,eg: schwartz
//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
// 6、判断当前用户是否具有权限(粗粒度)
//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
// 6、判断当前用户是否具有权限(细粒度)
//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
// 7、注销当前用户
currentUser.logout();
System.exit(0);
}
}
方法总结(在 Spring Security 中都有,只是方法名称不一样)
获取当前的用户对象Subject:Subject currentUser = SecurityUtils.getSubject();
通过当前用户拿到Session:Session session = currentUser.getSession();
判断当前的用户是否被认证:currentUser.isAuthenticated()
执行登录操作:currentUser.login(token);
通过用户名和密码生成一个token令牌:new UsernamePasswordToken(“lonestarr”, “vespa”);
设置记住我功能:token.setRememberMe(true);
获得当前用户的认证:currentUser.getPrincipal()
判断当前用户是否拥有什么角色,eg: schwartz:currentUser.hasRole(“schwartz”)
判断当前用户是否具有权限(粗粒度):currentUser.isPermitted(“lightsaber:wield”)
判断当前用户是否具有权限(细粒度):currentUser.isPermitted(“winnebago:drive:eagle5”)
注销当前用户:currentUser.logout();
<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.0modelVersion>
<groupId>com.cwlingroupId>
<artifactId>Shiro-02-SpringBootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>Shiro-02-SpringBootname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASEspring-boot.version>
properties>
<dependencies>
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.1.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.23version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.6version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.7.RELEASEversion>
<configuration>
<mainClass>com.cwlin.Shiro02SpringBootApplicationmainClass>
configuration>
<executions>
<execution>
<id>repackageid>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
新建一个模块 Shiro-02-SpringBoot,勾选依赖:Spring Web、Thymeleaf、Lombox等
<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.0modelVersion>
<groupId>com.cwlingroupId>
<artifactId>Shiro-02-SpringBootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>Shiro-02-SpringBootname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASEspring-boot.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.7.RELEASEversion>
<configuration>
<mainClass>com.cwlin.Shiro02SpringBootApplicationmainClass>
configuration>
<executions>
<execution>
<id>repackageid>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
编写三个前端页面:index.html、add.html、update.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页title>
head>
<body>
<div>
<h1>首页h1>
<p th:text="${msg}">p>
<hr>
<a th:href="@{/user/insert}">inserta> | <a th:href="@{/user/update}">updatea>
div>
body>
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>inserth1>
body>
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>updateh1>
body>
html>
在 controller 包下,新建一个 MyController 类用于测试
package com.cwlin.Controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model) {
model.addAttribute("msg","Hello, Shiro!");
return "index";
}
@RequestMapping("/user/insert")
public String add() {
return "user/insert";
}
@RequestMapping("/user/update")
public String update() {
return "user/update";
}
}
在pom中,导入Shiro整合Spring的包
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.1version>
dependency>
在config包下,新建 ShiroConfig 配置类
使用自定义UserRealm类,创建Realm对象
创建DefaultWebSecurityManager对象,并关联Realm对象
创建ShiroFilterFactoryBean对象,关联SecurityManager对象
package com.cwlin.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
//自定义UserRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权操作:doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证操作:doGetAuthenticationInfo");
return null;
}
}
package com.cwlin.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
//1.使用自定义UserRealm类,创建Realm对象
@Bean(name="Realm")
public UserRealm getUserRealm(){
return new UserRealm();
}
//2.创建DefaultWebSecurityManager对象,并关联Realm对象
@Bean(name="SecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(
@Qualifier("Realm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联Realm对象
securityManager.setRealm(userRealm);
return securityManager;
}
//3.创建ShiroFilterFactoryBean对象,关联SecurityManager对象
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//关联SecurityManager对象
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
}
在 ShiroConfig
类的 getShiroFilterFactoryBean
方法中,添加Shiro的内置过滤器,具体配置如下:
记住我
功能才能用//创建ShiroFilterFactoryBean对象,关联SecurityManager对象
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//关联SecurityManager对象
bean.setSecurityManager(defaultWebSecurityManager);
//添加Shiro的内置过滤器
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/insert","authc");
filterMap.put("/user/update","authc");
//这里支持使用通配符
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
此时,点击首页的 insert 或者 update 之后,返回 404 错误。
编写拦截后跳转的登录页面 login.html
(这里直接使用9.1节中的 login.html
),注意登录跳转请求:th:action="@{/login}"
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>登录title>
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
head>
<body>
<div class="ui container">
<div class="ui segment">
<div style="text-align: center">
<h1 class="header">登录h1>
div>
<div class="ui placeholder segment">
<div class="ui column very relaxed stackable grid">
<div class="column">
<div class="ui form">
<form th:action="@{/login}" method="post">
<div class="field">
<label>Usernamelabel>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="username">
<i class="user icon">i>
div>
div>
<div class="field">
<label>Passwordlabel>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon">i>
div>
div>
<div class="field">
<input type="checkbox" name="remember"> 记住我
div>
<input type="submit" class="ui blue submit button"/>
form>
div>
div>
div>
div>
<div style="text-align: center">
<div class="ui label">
i>注册
div>
<br><br>
<small>https://blog.csdn.net/coder_lcwsmall>
div>
<div class="ui segment" style="text-align: center">
<h3>Spring Security Study by cwlinh3>
div>
div>
div>
<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}">script>
<script th:src="@{/qinjiang/js/semantic.min.js}">script>
body>
html>
在 MyController
类中添加 toLogin
方法,实现登录请求跳转;在 ShiroConfig
类的 getShiroFilterFactoryBean
方法中,设置登录请求页面
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
//设置登录请求页面
bean.setLoginUrl("/toLogin");
在 MyController
类中添加 login
方法,处理用户在登录页面提交的表单,注意:@RequestMapping("/login")
要对应 login.html
中的登录跳转请求 th:action="@{/login}"
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//执行登录操作
try {
subject.login(token);
return "index";
} catch (UnknownAccountException e) { //用户名不存在
model.addAttribute("msg","用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) { //密码错误
model.addAttribute("msg","密码错误");
return "login";
} catch (LockedAccountException e) { //账户锁定
model.addAttribute("msg","账户锁定");
return "login";
} catch (AuthenticationException e) { //认证异常
model.addAttribute("msg","认证出现异常");
return "login";
}
}
在 login.html
中,添加 “msg” 的显示
<div><p th:text="${msg}" style="color: red;">p>div>
此时,用户输入登录信息,会提示用户名错误。
在自定义的 UserRealm
类中,重写 doGetAuthenticationInfo
方法,实现用户认证!
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证操作:doGetAuthenticationInfo");
String username = "admin";
String password = "123456";
//获取当前用户的数据令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//判断用户名
if(!token.getUsername().equals((username))){
return null; //抛出 UnknownAccountException 异常
}
//判断密码(Shiro实现)
return new SimpleAuthenticationInfo("",password,"");
}
关于 new SimpleAuthenticationInfo("", currentUser.getPwd(), "")
的解释:
AuthorizationInfo
使用;realmName
。在pom中导入依赖:mysql、druid、log4j、mybatis-spring-boot-starter
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.23version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.6version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
编写配置文件 application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # Druid数据源
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#整合mybatis
mybatis:
type-aliases-package: com.cwlin.pojo
编写 User 类和 UserMapper 类
package com.cwlin.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String pwd;
}
package com.cwlin.mapper;
import com.cwlin.pojo.User;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface UserMapper {
@Select("select * from user where name = #{name}")
User selectUserByName(String name);
}
编写 UserService 接口和 UserServiceImpl 实现类
package com.cwlin.service;
import com.cwlin.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface UserService {
@Select("select * from user where name = #{name}")
User selectUserByName(String name);
}
package com.cwlin.service;
import com.cwlin.mapper.UserMapper;
import com.cwlin.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User selectUserByName(String name) {
return userMapper.selectUserByName(name);
}
}
在测试类中进行测试
package com.cwlin;
import com.cwlin.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Shiro02SpringBootApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
System.out.println(userService.selectUserByName("cwlin"));
}
}
在自定义的 UserRealm
类中,重写 doGetAuthenticationInfo
方法,通过连接真实数据库实现用户认证!
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证操作:doGetAuthenticationInfo");
//获取当前用户的数据令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//连接真实数据库
User user = userService.selectUserByName(token.getUsername());
//判断用户名
if(user == null){
return null; //抛出 UnknownAccountException 异常
}
//判断密码(Shiro实现)
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
测试密码加密类型:
修改 MyController 类中的登录请求 login,提交方式改为 RequestMethod.POST
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(String username, String password, Model model){
//......
}
在 ShiroConfig
类的 getShiroFilterFactoryBean
方法中,修改授权配置
//权限授权,访问url需要权限,没有授权会跳转到为授权页面
filterMap.put("/user/insert", "perms[user:insert]");
filterMap.put("/user/update", "perms[user:update]");
访问页面并登录,点击 insert 按钮,页面报错如下:
There was an unexpected error (type=Unauthorized, status=401).
在 MyController
类中添加 login
方法,处理未授权的跳转请求;同时,在 ShiroConfig
类的 getShiroFilterFactoryBean
方法中设置未授权请求页面
@RequestMapping("/unauthorized")
@ResponseBody
public String unauthorized() {
return "未经授权,无法访问此页面!";
}
//设置未授权请求页面
bean.setUnauthorizedUrl("/unauthorized");
再次测试,页面显示:"未经授权,无法访问此页面!"
。因此,需要在自定义的 UserRealm
类中进行用户授权
在 mybatis 数据库中,为 user 表添加 perms 字段,同时修改 User 实体类属性
id | name | pwd | perms |
---|---|---|---|
1 | cwlin | 123456 | user:update |
2 | 张三 | abcdef | None |
3 | 李四 | 987654 | None |
4 | 王五 | 654321 | None |
5 | 李五 | 666666 | None |
6 | coder_lcw | 123456 | user:update |
7 | admin | 123456 | user:insert,update |
8 | guest | 123456 | user:select |
在自定义的 UserRealm
类中,重写 doGetAuthorizationInfo
方法,修改 doGetAuthenticationInfo
方法的返回值,实现用户授权!
package com.cwlin.config;
import com.cwlin.pojo.User;
import com.cwlin.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
//自定义UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权操作:doGetAuthorizationInfo");
//初始化授权消息对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取当前登录用户
Subject subject = SecurityUtils.getSubject();
//获取User对象
User currentUser = (User) subject.getPrincipal();
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证操作:doGetAuthenticationInfo");
//获取当前用户的数据令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//连接真实数据库
User user = userService.selectUserByName(token.getUsername());
//判断用户名
if(user == null){
return null; //抛出 UnknownAccountException 异常
}
//判断密码(Shiro实现)
return new SimpleAuthenticationInfo(user, user.getPwd(),"");
}
}
测试各个用户的权限!
导入 thymeleaf-extras-shiro 整合包
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.1.0version>
dependency>
在 ShiroConfig
类的 getShiroFilterFactoryBean
方法中,配置 ShiroDialect
//配置ShiroDialect,用于整合Shiro-Thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
修改 index.html 页面,根据用户权限动态显示前端页面
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>首页title>
head>
<body>
<div>
<h1>首页h1>
<p th:text="${msg}">p>
<div shiro:notAuthenticated>
<a th:href="@{/toLogin}">登录a>
div>
<hr>
<div shiro:hasPermission="user:insert">
<a th:href="@{/user/insert}">inserta>
div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">updatea>
div>
div>
body>
html>
学习目标
前后端分离
产生的问题
解决方案
Swagger
SpringBoot集成Swagger:springfox,需要两个jar包:
新建一个 SpringBoot 项目:SpringBoot-07-Swagger,勾选依赖:Spring Web、Thymeleaf、Lombox等
添加 Maven 依赖:springfox-swagger2
、springfox-swagger-ui
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>3.0.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>3.0.0version>
dependency>
在 controller 包下,编写 HelloController 类,用于测试项目正常运行,测试请求:http://localhost:8080/hello
package com.cwlin.swagger.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public String hello(){
return "Hello, swagger!";
}
}
在 config 包下,编写 SwaggerConfig 类(空类),用于配置 Swagger,测试请求:http://localhost:8080/swagger-ui.html
package com.cwlin.swagger.config;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
}
此时,页面出现 404 错误。这是因为,在 Swagger3.0 中,swagger-ui.html的位置发生了变化
解决方案:导入 springfox-boot-starter 3.0.0
依赖,代替springfox-swagger2
、springfox-swagger-ui
,并在主程序上添加 @EnableOpenApi
注解
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-boot-starterartifactId>
<version>3.0.0version>
dependency>
package com.cwlin.swagger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.oas.annotations.EnableOpenApi;
@SpringBootApplication
@EnableOpenApi
public class SpringBoot07SwaggerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot07SwaggerApplication.class, args);
}
}
测试请求:http://localhost:8080/swagger-ui/index.html,可以访问 swagger 界面!其内容包括:Swagger信息、接口信息、实体类信息、分组信息
Swagger 实例 Bean 是 Docket。因此,编写 docket() 方法,后面将通过配置 Docket 实例来配置 Swaggger 的上述各类信息!
//配置Swagger的docket实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2);
}
编写 apiInfo() 方法,通过 ApiInfo 对象来配置 Swagger 文档信息
//配置Swagger信息,即 apiInfo()
private ApiInfo apiInfo(){
Contact authorContact = new Contact("cwlin", "https://blog.csdn.net/coder_lcw", "[email protected]");
return new ApiInfo(
"cwlin's Swagger Api Documentation", //标题
"愿世界依旧热闹,愿我永远是自己。", //描述
"v1.0", //版本
"https://blog.csdn.net/coder_lcw", //组织链接
authorContact, //联系人信息
"Apache 2.0", //许可
"http://www.apache.org/licenses/LICENSE-2.0", //许可链接
new ArrayList<>()); //扩展
}
将 Docket 实例关联上 apiInfo() 方法
package com.cwlin.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//配置Swagger的docket实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
//配置Swagger信息,即 apiInfo()
private ApiInfo apiInfo(){
Contact authorContact = new Contact("cwlin", "https://blog.csdn.net/coder_lcw", "[email protected]");
return new ApiInfo(
"cwlin's Swagger Api Documentation", //标题
"愿世界依旧热闹,愿我永远是自己。", //描述
"v1.0", //版本
"https://blog.csdn.net/coder_lcw", //组织链接
authorContact, //联系人信息
"Apache 2.0", //许可
"http://www.apache.org/licenses/LICENSE-2.0", //许可链接
new ArrayList<>()); //扩展
}
}
重启项目,访问测试:http://localhost:8080/swagger-ui/index.html
在 SwaggerConfig
类的 docket()
方法中,配置 Docket 实例时,通过 select()
方法配置如何扫描接口
apis()
:扫描类paths()
:扫描路径//配置Swagger的docket实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller")
.and(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class)))
.paths(PathSelectors.any())
.build();
}
重启项目进行测试,由于根据上述配置扫描接口,因此只能看到一个 HelloController 类
除了通过扫描指定包的方式外,还可以通过 RequestHandlerSelectors 类配置其他方式扫描类:
此外,还可以通过 PathSelectors 类配置其他方式扫描路径:
在 SwaggerConfig
类的 docket()
方法中,通过 enable(true)
方法配置 Swagger 文档是否启动
//配置Swagger的docket实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(true)
.select()
.apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller")
.and(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class)))
.paths(PathSelectors.ant("/**"))
.build();
}
在 application.properties 配置文件中,设置项目环境:dev、pro、test,新建 application-dev.properties 和 application-dev.properties 两种环境下的配置文件
# 应用名称
spring.application.name=SpringBoot-07-Swagger
# 应用环境
spring.profiles.active=dev
根据开发环境或生产环境,配置 Swagger 文档是否启动
//配置Swagger的docket实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev","test");
//获取项目环境
//String[] activeProfiles = environment.getActiveProfiles();
//通过环境监听判断是否处在设置显示的环境中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(flag)
.select()
.apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller")
.and(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class)))
.paths(PathSelectors.ant("/**"))
.build();
}
在 SwaggerConfig
类的 docket()
方法中,通过 groupName()
方法配置 Swagger 分组信息。如果没有配置分组,默认是default
//配置Swagger的docket实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev","test");
//获取项目环境
//String[] activeProfiles = environment.getActiveProfiles();
//通过环境监听判断是否处在设置显示的环境中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(flag)
.groupName("cwlin")
.select()
.apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller")
.and(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class)))
.paths(PathSelectors.ant("/**"))
.build();
}
在 SwaggerConfig
类,配置多个 Docket 实例,设置多个 API 分组
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
}
新建一个 User 实体类,并给类及其属性加上注解,在 Swagger 中显示为文档注释:@ApiModel 为类添加注释,@ApiModelProperty 为类的属性添加注释
package com.cwlin.swagger.pojo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
//@Api(tags = "实体类")
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("ID")
private Integer id;
@ApiModelProperty("用户名")
private String name;
@ApiModelProperty("密码")
private String pwd;
@ApiModelProperty("权限")
private String perms;
}
**只要这个实体类在请求接口的返回值上(即使是泛型),都能映射到实体项中。**注意:这里并不是因为 @ApiModel 这个注解让实体类显示在这里,而是只要出现在接口方法的返回值上的实体类都会显示;而 @ApiModel 和 @ApiModelProperty 这两个注解只是为实体类添加注释的。
@PostMapping("/getUser")
public User getUser(){
return new User();
}
还可以给 Controller 类、类的方法、方法的参数设置文档注释,注意:Controller 类使用的注解为 @Api(tags = "")
package com.cwlin.swagger.controller;
import com.cwlin.swagger.pojo.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Api(tags = "Hello控制类")
public class HelloController {
@RequestMapping(value = "/hello")
public String hello(){
return "Hello, swagger!";
}
@PostMapping("/getUser")
public User getUser(){
int num = 6/0; //500错误
return new User();
}
@PostMapping("/helloUser")
@ApiOperation("helloUser方法")
public String helloUser(@ApiParam("用户名") @RequestParam("username") String username){
return "Hello, " + username + "!";
}
}
注意:Swagger 可以为一些比较难理解的属性或者接口,增加一些配置信息。但是,出于安全考虑、节省内存的运行,在正式发布时应关闭Swagger,否则会暴露项目接口信息!
Swagger 的所有注解定义在 io.swagger.annotations包下。下面列举一些常用注解:
Swagger注解 | 简单说明 |
---|---|
@Api(tags = “xxx模块说明”) | 作用在模块类上 |
@ApiOperation(“xxx接口说明”) | 作用在接口方法上 |
@ApiModel(“xxxPOJO说明”) | 作用在模型类上:如VO、BO |
@ApiModelProperty(value = “xxx属性说明”,hidden = true) | 作用在类方法和属性上,hidden设置为true可以隐藏该属性 |
@ApiParam(“xxx参数说明”) | 作用在参数、方法和字段上,类似@ApiModelProperty |
【注意】可以通过导入不同的包实现不同的皮肤定义
默认皮肤:http://localhost:8080/swagger-ui/index.html
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-boot-starterartifactId>
<version>3.0.0version>
dependency>
bootstrap-ui:http://localhost:8080/doc.html(实用)
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>1.9.6version>
dependency>
Layui-ui:http://localhost:8080/docs.html(好看)
<dependency>
<groupId>com.github.caspar-chengroupId>
<artifactId>swagger-ui-layerartifactId>
<version>1.1.3version>
dependency>
mg-ui:http://localhost:8080/document.html
<dependency>
<groupId>com.zyplayergroupId>
<artifactId>swagger-mg-uiartifactId>
<version>1.0.6version>
dependency>
新建一个 SpringBoot 项目:SpringBoot-08-Task,勾选依赖:Spring Web、Thymeleaf、Lombox等
在 service 包下创建一个 AsyncService 类,编写 hello 方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况
package com.cwlin.service;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在处理数据......");
}
}
在 controller 包下创建一个 AsyncController 类
package com.cwlin.controller;
import com.cwlin.service.AsyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@RequestMapping("/hello")
public String hello(){
asyncService.hello(); //停止5秒
return "Success!";
}
}
测试访问:http://localhost:8080/hello,3秒后页面出现 Success,这是同步等待的结果!
如果想让用户直接得到消息,那么在后台使用多线程的方式进行处理即可,但是每次都需要手动去编写多线程的实现,太麻烦了。可以通过给 hello 方法添加 @Async 注解,告诉Spring这是一个异步方法。此时,SpringBoot 会自动开启一个线程池,进行调用!
package com.cwlin.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
@Async //告诉Spring这是一个异步方法
public void hello(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在处理数据......");
}
}
但是,要让这个注解 @Async 生效,需要在主程序上添加一个注解 @EnableAsync,开启异步注解功能
package com.cwlin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync //开启异步注解功能
public class SpringBoot08TaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot08TaskApplication.class, args);
}
}
测试访问:http://localhost:8080/hello,页面瞬间响应出现 Success,后台代码依旧执行,实现了异步处理!
异步处理还是十分常用的,比如在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功。因此,一般会采用多线程的方式去处理这些任务。
导入 Maven 依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
点击查看 spring-boot-starter-mail(源码),发现它引用了 jakarta.mail
依赖
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0modelVersion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
<version>2.3.7.RELEASEversion>
<name>spring-boot-starter-mailname>
<description>Starter for using Java Mail and Spring Framework's email sending supportdescription>
<url>https://spring.io/projects/spring-booturl>
<organization>
<name>Pivotal Software, Inc.name>
<url>https://spring.iourl>
organization>
<licenses>
<license>
<name>Apache License, Version 2.0name>
<url>https://www.apache.org/licenses/LICENSE-2.0url>
license>
licenses>
<developers>
<developer>
<name>Pivotalname>
<email>[email protected]email>
<organization>Pivotal Software, Inc.organization>
<organizationUrl>https://www.spring.ioorganizationUrl>
developer>
developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.gitconnection>
<developerConnection>scm:git:ssh://[email protected]/spring-projects/spring-boot.gitdeveloperConnection>
<url>https://github.com/spring-projects/spring-booturl>
scm>
<issueManagement>
<system>GitHubsystem>
<url>https://github.com/spring-projects/spring-boot/issuesurl>
issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>2.3.7.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
<version>5.2.12.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.sun.mailgroupId>
<artifactId>jakarta.mailartifactId>
<version>1.6.5version>
<scope>compilescope>
dependency>
dependencies>
project>
双击 shift 键,查看自动配置类:MailSenderAutoConfiguration(源码)
package org.springframework.boot.autoconfigure.mail;
import javax.activation.MimeType;
import javax.mail.internet.MimeMessage;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
import org.springframework.mail.MailSender;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({MimeMessage.class, MimeType.class, MailSender.class})
@ConditionalOnMissingBean({MailSender.class})
@Conditional({MailSenderAutoConfiguration.MailSenderCondition.class})
@EnableConfigurationProperties({MailProperties.class})
@Import({MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class})
public class MailSenderAutoConfiguration {
public MailSenderAutoConfiguration() {
}
static class MailSenderCondition extends AnyNestedCondition {
MailSenderCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnProperty(
prefix = "spring.mail",
name = {"jndi-name"}
)
static class JndiNameProperty {
JndiNameProperty() {
}
}
@ConditionalOnProperty(
prefix = "spring.mail",
name = {"host"}
)
static class HostProperty {
HostProperty() {
}
}
}
}
点击 MailProperties 配置类(源码),查看邮件配置属性
package org.springframework.boot.autoconfigure.mail;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
private String host;
private Integer port;
private String username;
private String password;
private String protocol = "smtp";
private Charset defaultEncoding;
private Map<String, String> properties;
private String jndiName;
public MailProperties() {
this.defaultEncoding = DEFAULT_CHARSET;
this.properties = new HashMap();
}
//getter and setter
static {
DEFAULT_CHARSET = StandardCharsets.UTF_8;
}
}
在 application.properties
中,添加 mail 配置
# 邮件配置
# 用户名
spring.mail.username=******@qq.com
# 授权码,获取:QQ邮箱设置->账户->开启pop3和smtp服务
spring.mail.password=******
# 服务器
spring.mail.host=smtp.qq.com
# QQ邮箱需要开启加密验证
spring.mail.properties.mail.smtl.ssl.enable=true
在测试类中,测试邮件发送
package com.cwlin;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
@SpringBootTest
class SpringBoot08TaskApplicationTests {
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
//邮件设置1:一个简单的邮件
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("通知--秋季开学");
mailMessage.setText("于2021.08.01开学,请按时到校报到!");
mailMessage.setTo("******@qq.com");
mailMessage.setFrom("******@qq.com");
mailSender.send(mailMessage);
}
@Test
void contextLoads2() throws MessagingException {
//邮件设置2:一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true,"utf-8"); //支持多文件
mimeMessageHelper.setSubject("通知--秋季开学");
mimeMessageHelper.setText("于2021.08.01开学,请按时到校报到!",true); //支持html
mimeMessageHelper.addAttachment("1.jpg", new File("C:/Users/***/Desktop/1.jpg"));
mimeMessageHelper.addAttachment("2.jpg", new File("C:/Users/***/Desktop/2.jpg"));
mimeMessageHelper.setTo("******@qq.com");
mimeMessageHelper.setFrom("******@qq.com");
mailSender.send(mimeMessage);
}
}
在测试类中,将上述的复杂邮件发送封装为 sendMailMessage 方法
@Test
/**
* @author cwlin
* @creed Talk is cheap,show me the code
* @description TODO
* @date 2021/8/16 13:22
* @param multipart: 是否支持多文件
* @param encoding: 邮件编码
* @param subject: 标题
* @param text: 正文
* @param html: 是否支持html
* @return void
*/
void sendMailMessage(boolean multipart, String encoding, String subject, String text, boolean html) throws MessagingException {
//邮件设置2:一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, multipart, encoding);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(text, html);
mimeMessageHelper.addAttachment("1.jpg", new File("C:/Users/醉染/Desktop/1.jpg"));
mimeMessageHelper.addAttachment("2.jpg", new File("C:/Users/醉染/Desktop/2.jpg"));
mimeMessageHelper.setTo("[email protected]");
mimeMessageHelper.setFrom("[email protected]");
mailSender.send(mimeMessage);
}
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息。Spring为我们提供了异步执行任务调度的方式,提供了两个接口:TaskExecutor 接口、TaskScheduler 接口
package org.springframework.core.task;
import java.util.concurrent.Executor;
@FunctionalInterface
public interface TaskExecutor extends Executor {
void execute(Runnable var1);
}
package org.springframework.scheduling;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
import org.springframework.lang.Nullable;
public interface TaskScheduler {
default Clock getClock() {
return Clock.systemDefaultZone();
}
@Nullable
ScheduledFuture<?> schedule(Runnable var1, Trigger var2);
default ScheduledFuture<?> schedule(Runnable task, Instant startTime) {
return this.schedule(task, Date.from(startTime));
}
ScheduledFuture<?> schedule(Runnable var1, Date var2);
default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
return this.scheduleAtFixedRate(task, Date.from(startTime), period.toMillis());
}
ScheduledFuture<?> scheduleAtFixedRate(Runnable var1, Date var2, long var3);
default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
return this.scheduleAtFixedRate(task, period.toMillis());
}
ScheduledFuture<?> scheduleAtFixedRate(Runnable var1, long var2);
default ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
return this.scheduleWithFixedDelay(task, Date.from(startTime), delay.toMillis());
}
ScheduledFuture<?> scheduleWithFixedDelay(Runnable var1, Date var2, long var3);
default ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) {
return this.scheduleWithFixedDelay(task, delay.toMillis());
}
ScheduledFuture<?> scheduleWithFixedDelay(Runnable var1, long var2);
}
在主程序上添加 @EnableScheduling
注解,开启定时任务功能,后续才能使用 @Scheduled
注解
package com.cwlin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableAsync //开启异步注解功能
@EnableScheduling //开启定时任务功能
public class SpringBoot08TaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot08TaskApplication.class, args);
}
}
在 service 包下,创建一个 ScheduledService 类,编写一个执行定时任务的方法 hello,@Scheduled
表示在约定时间执行该方法
package com.cwlin.service;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class ScheduledService {
//在约定时间执行计划好的方法,需要用到Cron表达式
//Cron表达式:秒 分 时 日 月 星期 {年}(0-7均表示星期日)
@Scheduled(cron = "0 27 14 * * ?") //每天14点27分执行一次
@Scheduled(cron = "0 * * * * 0-7") //每分钟执行一次
//@Scheduled(cron = "30 0/10 10,18 * * ?") //每天10点和18点,每隔10分钟执行一次
//@Scheduled(cron = "15 30 10 ? * 1-6") //每个月的周一到周六10点30分15秒执行一次
public void hello(){
System.out.println("Hello, Scheduler!");
}
}
接下来介绍一下 Cron 表达式:
格式
允许值
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒数 | 0-59 | , - * / |
分钟 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12 或者 JAN-DEC | , - * / |
星期 | 1-7 或者 SUN-SAT | , - * ? / L C # |
年份(可为空) | 留空, 1970-2099 | , - * / |
特殊字符(Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感)
Cron 表达式生成器
参考博客
举例
"0/2 * * * * ?" 表示每2秒 执行任务
"0 0/2 * * * ?" 表示每2分钟 执行任务
"0 0 2 1 * ?" 表示在每月的1日的凌晨2点调整任务
"0 15 10 ? * MON-FRI" 表示周一到周五每天上午10:15执行作业
"0 15 10 ? 6L 2002-2006" 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
"0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?" 朝九晚五工作时间内每半小时
"0 0 12 ? * WED" 表示每个星期三中午12点
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
SpringBoot 操作数据都是使用 SpringData:jpa、jdbc、mongodb、redis等
SpringData 也是和 SpringBoot 齐名的项目
说明:在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce
?
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.3.5.RELEASE</version>
<scope>compile</scope>
</dependency>
Jedis 和 lettuce 区别:
在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名,Redis也不例外
spring-boot-autoconfigure-2.3.7.RELEASE.jar
META-INF
spring.factories
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
点击查看 RedisAutoConfiguration 自动配置类,发现绑定了一个 RedisProperties 配置文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure.data.redis;
import java.net.UnknownHostException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
点击 RedisProperties 配置文件,查看 redis 可以配置的属性,以及连接池相关的配置。注意:使用时一定要使用 Lettuce 连接池
@ConfigurationProperties(
prefix = "spring.redis"
)
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private String password;
private int port = 6379; // 默认端口
private boolean ssl;
private Duration timeout;
private String clientName;
private RedisProperties.Sentinel sentinel;
private RedisProperties.Cluster cluster;
private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
//......
public static class Pool {
private int maxIdle = 8;
private int minIdle = 0;
private int maxActive = 8;
private Duration maxWait = Duration.ofMillis(-1);
private Duration timeBetweenEvictionRuns;
//......
}
}
回到 RedisAutoConfiguration 自动配置类,发现两个 Template 模板方法,可以使用这些 Template 来间接操作组件
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
//可以自定义一个redisTemplate来替换默认的Redis模板!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//默认的 RedisTemplate 没有过多的设置,redis对象都是需要序列化!
//两个泛型都是 Object, Object 的类型,我们后使用需要强制转换
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
//由于String是redis中最常使用的类型,因此单独提出来一个bean!
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
再注意到 RedisAutoConfiguration 自动配置类导入的两个类
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
先看 Jedis 的实现类 JedisConnectionConfiguration,其中 @ConditionalOnClass 注解中有两个类 GenericObjectPool.class 和 Jedis.class 默认是不存在的,所以 Jedis 是无法生效的
@ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
再看 Lettuce 的实现类 LettuceConnectionConfiguration,其中 @ConditionalOnClass 注解的 RedisClient.class 类是可用的
@ConditionalOnClass({RedisClient.class})
显然,SpringBoot 更推荐使用 Lettuce 来实现 Redis。
新建一个 SpringBoot-09-Redis 项目,导入依赖。其中,spring-boot-starter-data-redis
依赖是 SpringBoot 整合 Redis 的包
4.0.0
com.cwlin
SpringBoot-09-Redis
0.0.1-SNAPSHOT
SpringBoot-09-Redis
Demo project for Spring Boot
1.8
UTF-8
UTF-8
2.3.7.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
1.8
UTF-8
org.springframework.boot
spring-boot-maven-plugin
2.3.7.RELEASE
com.cwlin.SpringBoot09RedisApplication
repackage
repackage
编写 application.yml 配置文件
# Redis配置
spring:
redis:
host: 127.0.0.1
port: 6379
在测试类中进行测试
package com.cwlin;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class SpringBoot09RedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
/* redisTemplate 操作不同的数据类型,API 和 Redis 中的是一样的
* opsForValue 类似于 Redis 中的 String
* opsForList 类似于 Redis 中的 List
* opsForSet 类似于 Redis 中的 Set
* opsForHash 类似于 Redis 中的 Hash
* opsForZSet 类似于 Redis 中的 ZSet
* opsForGeo 类似于 Redis 中的 Geospatial
* opsForHyperLogLog 类似于 Redis 中的 HyperLogLog
*/
//除了基本的操作,常用的命令都可以直接通过redisTemplate操作,比如事务和CURD。和数据库相关的操作都需要通过连接操作
/* 获取连接对象
* RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
* connection.flushDb();
* connection.flushAll();
*/
redisTemplate.opsForValue().set("myKey", "cwlin");
System.out.println(redisTemplate.opsForValue().get("myKey"));
}
}
由于没有安装 Redis,因此测试失败。在后续的学习中,将会系统地学习 Redis 的使用!
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理
步骤解析:
HTTP 和 RPC 的比较:
协议是 RPC 的核心,它规范了数据在网络中的传输内容和格式。除必须的请求、响应数据外,通常还会包含额外控制数据,如单次请求的序列化方式、超时时间、压缩方式和鉴权信息等。
协议的内容包含三部分
RPC 协议的设计需要考虑以下内容:
下载 zookeeper:https://mirror.bit.edu.cn/apache/zookeeper/,下载最新版 apache-zookeeper-3.7.0-bin.tar.gz
解压 zookeeper 到 Java 环境文件夹下,运行 /bin/zkServer.cmd
,初次运行会报错,没有 zoo.cfg
配置文件
修改 zoo.cfg
配置文件:将 conf 文件夹下面的 zoo_sample.cfg 复制一份改名为 zoo.cfg 即可,再次启动zookeeper。注意几个重要属性:
使用 zkCli.cmd 测试:
ls /
:列出zookeeper根下保存的所有节点
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
create –e /cwlin 123
:创建一个 cwlin 节点,值为123
[zk: localhost:2181(CONNECTED) 1] create -e /cwlin 123
Created /cwlin
get /cwlin
:获取 cwlin 节点的值
[zk: localhost:2181(CONNECTED) 2] get /cwlin
123
ls /
:再次查看所有节点
[zk: localhost:2181(CONNECTED) 3] ls /
[cwlin, zookeeper]
下载 dubbo-admin:https://github.com/apache/dubbo-admin/tree/master
解压得到 dubbo-admin-master-0.2.0
,打开 \dubbo-admin\src\main\resources\application.properties
,查看 zookeeper 注册中心地址(这是旧版的内容!新版的 dubbo-admin-master
运行出现错误!)
server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
在项目目录下打包 dubbo-admin。第一次打包的过程有点慢,需要耐心等待!
mvn clean package -Dmaven.test.skip=true
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for dubbo-admin 0.3.0:
[INFO]
[INFO] dubbo-admin ........................................ SUCCESS [04:06 min]
[INFO] dubbo-admin-ui ..................................... SUCCESS [07:19 min]
[INFO] dubbo-admin-server ................................. SUCCESS [04:10 min]
[INFO] dubbo-admin-distribution ........................... SUCCESS [ 8.592 s]
[INFO] dubbo-admin-test ................................... SUCCESS [01:59 min]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18:08 min
[INFO] Finished at: 2021-08-17T17:55:43+08:00
[INFO] ------------------------------------------------------------------------
执行 \dubbo-admin-master\dubbo-admin-server\target
下的 dubbo-admin-server-0.3.0.jar
,注意:zookeeper的服务一定要打开!
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
执行完毕,访问:http://localhost:7001/;此时,需要输入登录账户和密码,默认是 root-root;登录成功后,即可查看界面。
在 IDEA 中创建一个空项目,创建一个 provider-server 模块,实现服务提供者,选择相应的 web 依赖,修改端口号为 8081
编写一个服务,比如卖票的服务,其 service 接口和实现类如下:
package com.cwlin.service;
public interface TicketService {
String getTicket();
}
package com.cwlin.service;
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《觉醒年代》";
}
}
创建一个 consumer-server 模块,实现服务消费者,选择相应的 web 依赖,修改端口号为 8082
编写一个服务,比如用户的服务,编写 service 类如下:
package com.cwlin;
public class UserService {
//需要去拿注册中心的服务
}
需求:现在用户想使用买票的服务,要怎么处理?
将服务提供者注册到注册中心,需要整合 Dubbo 和 zookeeper,导入相关依赖
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>2.7.8version>
dependency>
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>5.1.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>5.1.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.6.1version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
在 springboot 配置文件中,配置 dubbo 的相关参数
# 应用名称
spring.application.name=provider-service
# 应用服务 WEB 访问端口
server.port=8081
#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=com.cwlin.service
在 service 的实现类 TicketServiceImpl 中配置 Dubbo 服务注解 @DubboService
注意导包问题!不过,在新版Dubbo中,注解 @Service 改为 @DubboService!
逻辑理解:应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!
package com.cwlin.service;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Component;
@DubboService //可以被扫描到,在项目一启动就自动注册到注册中心
@Component //使用Dubbo后尽量不要用@Service注解,Dubbo包里也有@Service注解,容易出现导包问题!
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《觉醒年代》";
}
}
先启动 zookeeper,运行 dubbo-admin-0.0.1-SNAPSHOT.jar,再运行 provider-server 模块
请求访问:http://localhost:7001/,并登录 Dubbo Admin 进行测试
导入和 provider-service 模块中相同的依赖
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>2.7.8version>
dependency>
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>5.1.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>5.1.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.6.1version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
在 springboot 配置文件中,配置 dubbo 的相关参数
# 应用名称
spring.application.name=consumer-server
# 应用服务 WEB 访问端口
server.port=8082
#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
在 service 包下,完善服务消费者的 service 类
package com.cwlin.service;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service //注入到容器中
public class UserService {
//想要拿到provider-server提供的票,需要去拿注册中心的服务
@DubboReference //引用,Pom坐标,可以定义路径相同的接口名
TicketService ticketService;
public void bugTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到"+ticket);
}
}
编写测试类进行测试,运行 consumer-server 模块
package com.cwlin;
import com.cwlin.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
userService.bugTicket();
}
}
刷新 Dubbo Admin 监控中心,查看消费者:
spring
SpringBoot
Spring Cloud NetFlix,提出了一套一站式解决方案,可以直接使用
Apache Dubbo zookeeper:第二套解决系统
SpringCloud Alibaba:一站式解决方案
目前又提出一种方法:
服务网格:下一代微服务标准,Service Mesh;代表解决方案:Istio