1 SpringDataJPA
1.1 Spring Data JPA
1.1.1 JPA简介
JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化规范(JSR 338,这些接口所在包为javax.persistence) JPA的出现主要是为了简化持久层开发以及整合ORM技术,结束Hibernate、TopLink、JDO等ORM框架各自为营的局面。JPA是在吸收现有ORM框架的基础上发展而来,易于使用,伸缩性强。总的来说,JPA包括以下3方面的技术:
- ORM映射元数据: 支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系
- API: 操作实体对象来执行CRUD操作
- 查询语言: 通过面向对象而非面向数据库的查询语言(JPQL)查询数据,避免程序的SQL语句紧密耦合
Jpa、Hibernate、Spring Data Jpa三者之间的关系
总的来说JPA是ORM规范,Hibernate、TopLink等是JPA规范的具体实现,这样的好处是开发者可以面向JPA规范进行持久层的开发,而底层的实现则是可以切换的。Spring Data Jpa则是在JPA之上添加另一层抽象(Repository层的实现),极大地简化持久层开发及ORM框架切换的成本。
Spring Data JPA通过提供基于JPA的Repository极大地减少JPA作为数据访问方案的代码量。
1.1.2 JPA使用
1.1.2.1 定义数据访问层
使用Spring Data JPA建立数据访问层十分简单,只需定义一个继承JpaRepository的接口即可,接口如下:
@RepositoryRestResource(path = "people")
public interface PersonRepository extends JpaRepository<Person, Long> {
@RestResource(path = "nameStartsWith", rel = "nameStartsWith")
Person findByNameStartsWith(@Param("name")String name);
}
继承JpaRepository接口意味着我们默认已经有了下面的数据访问操作方法:
@NoRepositoryBean
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAll(Iterable<ID> var1);
<S extends T> List<S> save(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}
1.1.2.2 配置使用Spring Data JPA
在Spring环境中,使用Spring Data JPA可通过@EnableJpaRepositories注解来开启Spring Data JPA的支持,@EnableJpaRepositories接收的value参数用来扫描数据访问层所在包下的数据访问的接口定义。
@Configuration
@EnableJpaRepositories("com.test.dao")
public class JpaConfiguration {
@Bean
public EntityManagerFactory entityManagerFactory(){
//...
return null;
}
//还需配置DataSource、PlatformTransactionManager等相关必须bean
}
1.1.2.3 定义查询方法
(1)根据属性名查询
1)常规查询。根据属性名来定义查询方法
public interface PersonRepository extends CustomRepository<Person, Long> {
/**
* 通过名字相等查询,参数name
* 相当于JPQL:select p from Person p where p.name=?
*/
List<Person> findByName(String name);
/**
* 通过名字like查询,参数为name
* 相当于JPQL:select p from Person p where p.name like ?
*/
List<Person> findByNameLike(String name);
/**
* 通过名字和地址查询,参数为name和address
* 相当于JPQL:select p from Person p where p.name = ? and p.address = ?
*/
List<Pserson> findByNameAndAddress(String name,String address);
}
从代码可以看出,这里使用了findBy、like、And这样的关键字。其中findBy可以用find、read、readBy、query、queryBy、get、getBy来代替。
2)限制结果数量。结果数量是用top和first关键字来实现的:\
public interface PersonRepository extends CustomRepository<Person, Long> {
/**
* 获取查询条件的前10条数据
* 通过名字相等查询,参数name
* 相当于JPQL:select p from Person p where p.name=?
*/
List<Person> findFirst10ByName(String name);
/**
* 获取查询条件的前10条数据
* 通过名字like查询,参数为name
* 相当于JPQL:select p from Person p where p.name like ?
*/
List<Person> findTop10ByNameLike(String name);
}
(2)使用JPA的NamedQuery查询
Spring Data JPA支持用JPA的NameQuery来定义查询方法,即一个名称映射一个查询语句。
@Entity
@NamedQuery(name = "Person.withNameAndAddressNamedQuery",
query = "select p from Person p where p.name=? and address=?")
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private Integer age;
private String address;
public Person() {
super();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
这时在接口里使用 Person withNameAndAddressNamedQuery(String name,String address) 时是使用的在上面定义的sql语句,而不是根据方法名称查询
(3)使用@Query查询
1)使用命名参数,使用名称来匹配查询参数。Spring Data JPA还支持@Query注解在接口的方法上实现查询
@Query("select p from Person p where p.name= :name and p.address= :address")
Person withNameAndAddressQuery(@Param("name")String name,@Param("address")String address);
2)使用参数索引
@Query("select p from Person p where p.name= ? and p.address= ?")
Person withNameAndAddressNamedQuery(String name,String address);
3)更新查询。Spring Data JPA支持@Modifying和@Query注解组合事件来更新查询
(4)JPA提供了基于准则查询方式,即Criteria查询。而Spring Data JPA提供了一个Specification(规范)接口让我们可以更方便的构造准则查询,Specification接口定义了一个toPredicate方法用来构造查询条件。
(5)自定义Repository的实现
spring data提供了CrudRepository和PagingAndSortingRepository,spring data JPA也提供了JpaRepository。如果我们想把自己常用的数据库操作封装起来,像JpaRepository一样提供给我们领域类的Repository接口使用,应该怎么做?
1)定义自定义Repository接口
@NoRepositoryBean
public interface CustomRepository<T, ID extends Serializable>extends JpaRepository<T, ID> ,JpaSpecificationExecutor<T>{
Page<T> findByAuto(T example,Pageable pageable);
}
- @NoRepositoryBean指明当前这个接口不是我们领域类的接口(例如PersonRepository)
- 我们自定义的Repository实现JpaRepository接口(这里也可以实现PagingAndSortingRepository接口,看具体需求),具备JpaRepository的能力
- 要定义的数据操作方法在接口中的定义
2)定义接口实现
public class CustomRepositoryImpl <T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> implements CustomRepository<T,ID> {
private final EntityManager entityManager;
public CustomRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
@Override
public Page<T> findByAuto(T example, Pageable pageable) {
return findAll(byAuto(entityManager, example),pageable); //在此处定义数据访问操作
}
}
- 首先要实现CustomRepository接口,继承SimpleJpaRepository类让我们可以使用其提供的方法(例如:findAll)
- 让数据库操作方法中可以使用entityManager
- CustomRepositoryImpl的构造函数,需当前处理的领域类类型和entityManager作为构造函数
3)自定义RepositoryFactoryBean
自定义JpaRepositoryFactoryBean替代默认RepositoryFactoryBean,我们会获得一个RepositoryFactory,RepositoryFactory将会注册我们自定义的Repository的实现
public class CustomRepositoryFactoryBean<T extends JpaRepository<S, ID>, S, ID extends Serializable>
extends JpaRepositoryFactoryBean<T, S, ID> {// 1
@Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {// 2
return new CustomRepositoryFactory(entityManager);
}
private static class CustomRepositoryFactory extends JpaRepositoryFactory {// 3
public CustomRepositoryFactory(EntityManager entityManager) {
super(entityManager);
}
@Override
@SuppressWarnings({"unchecked"})
protected <T, ID extends Serializable> SimpleJpaRepository<?, ?> getTargetRepository(
RepositoryInformation information, EntityManager entityManager) {// 4
return new CustomRepositoryImpl<T, ID>((Class<T>) information.getDomainType(), entityManager);
}
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {// 5
return CustomRepositoryImpl.class;
}
}
}
- 自定义RepositoryFactoryBean,继承JpaRepositoryFactoryBean
- 重写createRepositoryFactory方法,用当前的CustomRepositoryFactory创建实例
- 创建CustomRepositoryFactory,并继承JpaRepositoryFactory
- 重写getTargetRepository方法,获得当前自定义的Repository实现
- 重写getRepositoryBaseClass,获得当前自定义的Repository实现的类型
4)开启自定义支持使用@EnableJpaRepositories的repositoryFactoryBeanClass来指定FactoryBean即可
@SpringBootApplication
@EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class)
public class TestApplication {
@Autowired
PersonRepository personRepository;
public static void main(String[] args) {
SpringApplication.run(Ch82Application.class, args);
}
}
1.2 Spring boot的支持
1.2.1 JDBC的自动配置
spring-boot-starter-data-jpa依赖于spring-boot-starter-jdbc,而Spring Boot对JDBC做了一些自动配置。源码放置在org.springframework.boot.autoconfigure.jdbc下,如图
从源码分析可以看出,我们通过“spring.datasource”为前缀的属性自动配置dataSource,Spring Boot自动开启了注解事务的支持(@EnableTransactionManagement);还配置了一个jdbcTemplate。Spring Boot还提供了一个初始化数据的功能:放置在类路径下的schema.sql文件会自动用来初始化表结构;放置在类路径下的data.sql文件会自动用来填充表数据。
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed 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.
*/
package org.springframework.boot.autoconfigure.jdbc;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.sql.DataSource;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for configuration of a data source.
*
* @author Dave Syer
* @author Maciej Walkowiak
* @author Stephane Nicoll
* @author Benedikt Ritter
* @author Eddú Meléndez
* @since 1.1.0
*/
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties
implements BeanClassLoaderAware, EnvironmentAware, InitializingBean {
private ClassLoader classLoader;
private Environment environment;
/**
* Name of the datasource.
*/
private String name = "testdb";
/**
* Generate a random datasource name.
*/
private boolean generateUniqueName;
/**
* Fully qualified name of the connection pool implementation to use. By default, it
* is auto-detected from the classpath.
*/
private Class<? extends DataSource> type;
/**
* Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
*/
private String driverClassName;
/**
* JDBC url of the database.
*/
private String url;
/**
* Login user of the database.
*/
private String username;
/**
* Login password of the database.
*/
private String password;
/**
* JNDI location of the datasource. Class, url, username & password are ignored when
* set.
*/
private String jndiName;
/**
* Populate the database using 'data.sql'.
*/
private boolean initialize = true;
/**
* Platform to use in the schema resource (schema-${platform}.sql).
*/
private String platform = "all";
/**
* Schema (DDL) script resource references.
*/
private List<String> schema;
/**
* User of the database to execute DDL scripts (if different).
*/
private String schemaUsername;
/**
* Password of the database to execute DDL scripts (if different).
*/
private String schemaPassword;
/**
* Data (DML) script resource references.
*/
private List<String> data;
/**
* User of the database to execute DML scripts.
*/
private String dataUsername;
/**
* Password of the database to execute DML scripts.
*/
private String dataPassword;
/**
* Do not stop if an error occurs while initializing the database.
*/
private boolean continueOnError = false;
/**
* Statement separator in SQL initialization scripts.
*/
private String separator = ";";
/**
* SQL scripts encoding.
*/
private Charset sqlScriptEncoding;
private EmbeddedDatabaseConnection embeddedDatabaseConnection = EmbeddedDatabaseConnection.NONE;
private Xa xa = new Xa();
private String uniqueName;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void afterPropertiesSet() throws Exception {
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection
.get(this.classLoader);
}
/**
* Initialize a {@link DataSourceBuilder} with the state of this instance.
* @return a {@link DataSourceBuilder} initialized with the customizations defined on
* this instance
*/
public DataSourceBuilder initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType())
.driverClassName(determineDriverClassName()).url(determineUrl())
.username(determineUsername()).password(determinePassword());
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public boolean isGenerateUniqueName() {
return this.generateUniqueName;
}
public void setGenerateUniqueName(boolean generateUniqueName) {
this.generateUniqueName = generateUniqueName;
}
public Class<? extends DataSource> getType() {
return this.type;
}
public void setType(Class<? extends DataSource> type) {
this.type = type;
}
/**
* Return the configured driver or {@code null} if none was configured.
* @return the configured driver
* @see #determineDriverClassName()
*/
public String getDriverClassName() {
return this.driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
/**
* Determine the driver to use based on this configuration and the environment.
* @return the driver to use
* @since 1.4.0
*/
public String determineDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(driverClassIsLoadable(),
"Cannot load driver class: " + this.driverClassName);
return this.driverClassName;
}
String driverClassName = null;
if (StringUtils.hasText(this.url)) {
driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
throw new DataSourceBeanCreationException(this.embeddedDatabaseConnection,
this.environment, "driver class");
}
return driverClassName;
}
private boolean driverClassIsLoadable() {
try {
ClassUtils.forName(this.driverClassName, null);
return true;
}
catch (UnsupportedClassVersionError ex) {
// Driver library has been compiled with a later JDK, propagate error
throw ex;
}
catch (Throwable ex) {
return false;
}
}
/**
* Return the configured url or {@code null} if none was configured.
* @return the configured url
* @see #determineUrl()
*/
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
/**
* Determine the url to use based on this configuration and the environment.
* @return the url to use
* @since 1.4.0
*/
public String determineUrl() {
if (StringUtils.hasText(this.url)) {
return this.url;
}
String url = this.embeddedDatabaseConnection.getUrl(determineDatabaseName());
if (!StringUtils.hasText(url)) {
throw new DataSourceBeanCreationException(this.embeddedDatabaseConnection,
this.environment, "url");
}
return url;
}
private String determineDatabaseName() {
if (this.generateUniqueName) {
if (this.uniqueName == null) {
this.uniqueName = UUID.randomUUID().toString();
}
return this.uniqueName;
}
return this.name;
}
/**
* Return the configured username or {@code null} if none was configured.
* @return the configured username
* @see #determineUsername()
*/
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
/**
* Determine the username to use based on this configuration and the environment.
* @return the username to use
* @since 1.4.0
*/
public String determineUsername() {
if (StringUtils.hasText(this.username)) {
return this.username;
}
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) {
return "sa";
}
return null;
}
/**
* Return the configured password or {@code null} if none was configured.
* @return the configured password
* @see #determinePassword()
*/
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* Determine the password to use based on this configuration and the environment.
* @return the password to use
* @since 1.4.0
*/
public String determinePassword() {
if (StringUtils.hasText(this.password)) {
return this.password;
}
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) {
return "";
}
return null;
}
public String getJndiName() {
return this.jndiName;
}
/**
* Allows the DataSource to be managed by the container and obtained via JNDI. The
* {@code URL}, {@code driverClassName}, {@code username} and {@code password} fields
* will be ignored when using JNDI lookups.
* @param jndiName the JNDI name
*/
public void setJndiName(String jndiName) {
this.jndiName = jndiName;
}
public boolean isInitialize() {
return this.initialize;
}
public void setInitialize(boolean initialize) {
this.initialize = initialize;
}
public String getPlatform() {
return this.platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public List<String> getSchema() {
return this.schema;
}
public void setSchema(List<String> schema) {
this.schema = schema;
}
public String getSchemaUsername() {
return this.schemaUsername;
}
public void setSchemaUsername(String schemaUsername) {
this.schemaUsername = schemaUsername;
}
public String getSchemaPassword() {
return this.schemaPassword;
}
public void setSchemaPassword(String schemaPassword) {
this.schemaPassword = schemaPassword;
}
public List<String> getData() {
return this.data;
}
public void setData(List<String> data) {
this.data = data;
}
public String getDataUsername() {
return this.dataUsername;
}
public void setDataUsername(String dataUsername) {
this.dataUsername = dataUsername;
}
public String getDataPassword() {
return this.dataPassword;
}
public void setDataPassword(String dataPassword) {
this.dataPassword = dataPassword;
}
public boolean isContinueOnError() {
return this.continueOnError;
}
public void setContinueOnError(boolean continueOnError) {
this.continueOnError = continueOnError;
}
public String getSeparator() {
return this.separator;
}
public void setSeparator(String separator) {
this.separator = separator;
}
public Charset getSqlScriptEncoding() {
return this.sqlScriptEncoding;
}
public void setSqlScriptEncoding(Charset sqlScriptEncoding) {
this.sqlScriptEncoding = sqlScriptEncoding;
}
public ClassLoader getClassLoader() {
return this.classLoader;
}
public Xa getXa() {
return this.xa;
}
public void setXa(Xa xa) {
this.xa = xa;
}
/**
* XA Specific datasource settings.
*/
public static class Xa {
/**
* XA datasource fully qualified name.
*/
private String dataSourceClassName;
/**
* Properties to pass to the XA data source.
*/
private Map<String, String> properties = new LinkedHashMap<String, String>();
public String getDataSourceClassName() {
return this.dataSourceClassName;
}
public void setDataSourceClassName(String dataSourceClassName) {
this.dataSourceClassName = dataSourceClassName;
}
public Map<String, String> getProperties() {
return this.properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
}
static class DataSourceBeanCreationException extends BeanCreationException {
DataSourceBeanCreationException(EmbeddedDatabaseConnection connection,
Environment environment, String property) {
super(getMessage(connection, environment, property));
}
private static String getMessage(EmbeddedDatabaseConnection connection,
Environment environment, String property) {
StringBuilder message = new StringBuilder();
message.append("Cannot determine embedded database " + property
+ " for database type " + connection + ". ");
message.append("If you want an embedded database please put a supported "
+ "one on the classpath. ");
message.append("If you have database settings to be loaded from a "
+ "particular profile you may need to active it");
if (environment != null) {
String[] profiles = environment.getActiveProfiles();
if (ObjectUtils.isEmpty(profiles)) {
message.append(" (no profiles are currently active)");
}
else {
message.append(" (the profiles \""
+ StringUtils.arrayToCommaDelimitedString(
environment.getActiveProfiles())
+ "\" are currently active)");
}
}
message.append(".");
return message.toString();
}
}
}
1.2.2 对JPA的自动配置
Spring Boot对JPA的自动配置放置在org.springframework.boot.autoconfigure.orm.jpa下,如图
从HibernateJpaAutoConfiguration可以看出,Spring boot默认JPA的实现者是Hibernate;HibernateJpaAutoConfiguration依赖于DataSourceAutoConfiguration。
@Configuration
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class,
EnableTransactionManagement.class, EntityManager.class })
@Conditional(HibernateEntityManagerCondition.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class })
public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration {
private static final Log logger = LogFactory
.getLog(HibernateJpaAutoConfiguration.class);
private static final String JTA_PLATFORM = "hibernate.transaction.jta.platform";
/**
* {@code NoJtaPlatform} implementations for various Hibernate versions.
*/
private static final String[] NO_JTA_PLATFORM_CLASSES = {
"org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform",
"org.hibernate.service.jta.platform.internal.NoJtaPlatform" };
/**
* {@code WebSphereExtendedJtaPlatform} implementations for various Hibernate
* versions.
*/
private static final String[] WEBSPHERE_JTA_PLATFORM_CLASSES = {
"org.hibernate.engine.transaction.jta.platform.internal.WebSphereExtendedJtaPlatform",
"org.hibernate.service.jta.platform.internal.WebSphereExtendedJtaPlatform", };
public HibernateJpaAutoConfiguration(DataSource dataSource,
JpaProperties jpaProperties,
ObjectProvider<JtaTransactionManager> jtaTransactionManager,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
super(dataSource, jpaProperties, jtaTransactionManager,
transactionManagerCustomizers);
}
@Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Override
protected Map<String, Object> getVendorProperties() {
Map<String, Object> vendorProperties = new LinkedHashMap<String, Object>();
vendorProperties.putAll(getProperties().getHibernateProperties(getDataSource()));
return vendorProperties;
}
@Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
super.customizeVendorProperties(vendorProperties);
if (!vendorProperties.containsKey(JTA_PLATFORM)) {
configureJtaPlatform(vendorProperties);
}
}
private void configureJtaPlatform(Map<String, Object> vendorProperties)
throws LinkageError {
JtaTransactionManager jtaTransactionManager = getJtaTransactionManager();
if (jtaTransactionManager != null) {
if (runningOnWebSphere()) {
// We can never use SpringJtaPlatform on WebSphere as
// WebSphereUowTransactionManager has a null TransactionManager
// which will cause Hibernate to NPE
configureWebSphereTransactionPlatform(vendorProperties);
}
else {
configureSpringJtaPlatform(vendorProperties, jtaTransactionManager);
}
}
else {
vendorProperties.put(JTA_PLATFORM, getNoJtaPlatformManager());
}
}
private boolean runningOnWebSphere() {
return ClassUtils.isPresent(
"com.ibm.websphere.jtaextensions." + "ExtendedJTATransaction",
getClass().getClassLoader());
}
private void configureWebSphereTransactionPlatform(
Map<String, Object> vendorProperties) {
vendorProperties.put(JTA_PLATFORM, getWebSphereJtaPlatformManager());
}
private Object getWebSphereJtaPlatformManager() {
return getJtaPlatformManager(WEBSPHERE_JTA_PLATFORM_CLASSES);
}
private void configureSpringJtaPlatform(Map<String, Object> vendorProperties,
JtaTransactionManager jtaTransactionManager) {
try {
vendorProperties.put(JTA_PLATFORM,
new SpringJtaPlatform(jtaTransactionManager));
}
catch (LinkageError ex) {
// NoClassDefFoundError can happen if Hibernate 4.2 is used and some
// containers (e.g. JBoss EAP 6) wraps it in the superclass LinkageError
if (!isUsingJndi()) {
throw new IllegalStateException("Unable to set Hibernate JTA "
+ "platform, are you using the correct "
+ "version of Hibernate?", ex);
}
// Assume that Hibernate will use JNDI
if (logger.isDebugEnabled()) {
logger.debug("Unable to set Hibernate JTA platform : " + ex.getMessage());
}
}
}
private boolean isUsingJndi() {
try {
return JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable();
}
catch (Error ex) {
return false;
}
}
private Object getNoJtaPlatformManager() {
return getJtaPlatformManager(NO_JTA_PLATFORM_CLASSES);
}
private Object getJtaPlatformManager(String[] candidates) {
for (String candidate : candidates) {
try {
return Class.forName(candidate).newInstance();
}
catch (Exception ex) {
// Continue searching
}
}
throw new IllegalStateException("Could not configure JTA platform");
}
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
static class HibernateEntityManagerCondition extends SpringBootCondition {
private static String[] CLASS_NAMES = {
"org.hibernate.ejb.HibernateEntityManager",
"org.hibernate.jpa.HibernateEntityManager" };
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("HibernateEntityManager");
for (String className : CLASS_NAMES) {
if (ClassUtils.isPresent(className, context.getClassLoader())) {
return ConditionOutcome
.match(message.found("class").items(Style.QUOTE, className));
}
}
return ConditionOutcome.noMatch(message.didNotFind("class", "classes")
.items(Style.QUOTE, Arrays.asList(CLASS_NAMES)));
}
}
}
从JpaProperties的源码可以看出,配置JPA可以使用spring.jpa为前缀的属性在application.properties中配置。
从JpaBaseConfiguration的源码中可以看出,Spring boot为我们配置了transactionManager、jpaVendorAdapter、entityManagerFactory等Bean。JpaBaseConfiguration还有一个getPackagesToScan方法,可以自动扫描注解有Entity的实体类。
在web项目中我们经常会遇到在控制器或者页面访问数据的时候出现会话连接已关闭的错误,这个时候我们会配置一个Open EntityManager(Session)In View这个过滤器。令人惊喜的是,Spring boot为我们自动配置了OpenEntityManagerInViewIntercept这个bean,并注册到Spring mvc的拦截器中。
1.2.3 对Spring Data JPA的自动配置
而Spring boot对Spring Data Jpa的自动配置放置在org.springframework.boot.autoconfigure.data.jpa下,如图
从JpaRepositoriesAutoConfiguration和JPARepositoriesAutoConfigureRegistrar源码可以看出,JpaRepositoriesAutoConfiguration是依赖于HibernateJpaAutoConfiguration配置的,且Spring boot自动开启了对spring data jpa的支持,即我们无须在配置类中显示声明@EnableJpaRepositories.
总结 : 通过上面的分析,在Spring boot下使用Spring Data JPA,在项目的maven依赖里添加spring-boot-starter-data-jpa,然后只需定义DataSource、实体类和数据访问层,并在需要使用数据访问的地方注入数据访问层的Bean即可,无须任何额外配置。
1.3 示例
1.3.1 环境搭建
我们选择的数据库为mysql,所以有必要先安装一下,这里跳过mysql的安装教程,直接进入springboot项目的搭建
1.3.2 SpringBoot项目搭建
1.3.2.1 pom依赖
我们这里选择的是2.0.4.RELEASE版本进行演示
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
上面的pom依赖中,关键的是下面两个, 第一个引入的是jpa相关包,后面那个则是mysql的连接依赖,相当于指定操作mysql数据库
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
1.3.2.2 数据准备
创建一个测试表进行后续的读写操作,为了后续的事物讲解方便,我们创建一个表,里面存了每个人的钱
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
在表中随意插入几条数据,以方便后面使用
INSERT INTO `money` (`id`, `name`, `money`, `is_deleted`, `create_at`, `update_at`)
VALUES
(1, '一灰灰blog', 100, 0, '2019-04-18 17:01:40', '2019-04-18 17:01:40'),
(2, '一灰灰2', 200, 0, '2019-04-18 17:01:40', '2019-04-18 17:01:40');
1.3.2.3 属性配置
创建springboot工程之后,添加mysql的相关配置,在resources目录下,新建文件 application.properties
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
#spring.datasource.driver-class-name= com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
1.3.2.4 项目构建并测试
根据JPA的一般使用姿势,我们需要针对上面的表,创建一个对应的POJO对象,将它们关联起来,代码如下:
- 注意下几个注解 @Entity, @Table, @Column, @Id, @GeneratedValue
- 注意下POJO中字段的类型,这里保证了和db的字段定义类型一致
import lombok.Data;
import javax.persistence.*;
import java.sql.Date;
@Data
@Entity
@Table(name = "money")
public class MoneyPO {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "money")
private Long money;
@Column(name = "is_deleted")
private Byte isDeleted;
@Column(name = "create_at")
private Date createAt;
@Column(name = "update_at")
private Date updateAt;
}
表结构定义完毕之后,接下来就是定义db的操作api,jpa的使用,通过方法名来解析出对应的sql,我们这里定义一个简单的Money表的操作API:
- MoneyDemoRepository 继承自 JpaRepository
- 两个泛型参数,第一个表示这个repository操作的表绑定的POJO,第二个表示自增id类型
import com.git.hui.boot.jpa.entity.MoneyPO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface MoneyDemoRepository extends JpaRepository<MoneyPO, Integer> {
}
上面两个定义完毕之后,不需要其他的操作,就可以进行测试环境了,上面这个Repository提供了一些简单的操作
import com.git.hui.boot.jpa.demo.JpaQueryDemo;
import com.git.hui.boot.jpa.entity.MoneyPO;
import com.git.hui.boot.jpa.repository.MoneyDemoRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public Application(MoneyDemoRepository moneyDemoRepository) {
MoneyPO moneyPO = moneyDemoRepository.findById(1).get();
System.out.println(moneyPO);
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
针对上面的测试case进行简单的说明,前面定义了一个POJO对象和一个RepositoryAPI,我们想直接操作对应的表,需要借助这个RepositoryAPI对象,但是它是接口类型,我们没法直接使用的,因为我们是在Spring生态体系中,所以可以直接通过IoC注入方式使用
所以上面的测试中,MoneyDemoRepository 对象实际上是由框架生成的一个代理对象,下面我们看下执行结果
1.3.3 小结
从上面的步骤下来,会发现搭建一个jpa的项目工程属于比较简单的过程,添加必要的依赖,稍微注意的是两个
- 创建一个POJO 与我们实际的表关联起来
- 创建一个RepositoryApi继承自org.springframework.data.repository.CrudRepository
- 通过IoC/DI方式注入RepositoryApi对象,然后可以愉快的进行db操作
1.4 JPA常用注解
1.4.1 类映射到数据库表的常用注解
(1)@Entity
用来注解该类是一个实体类用来进行和数据库中的表建立关联关系,首次启动项目的时候,默认会在数据中生成一个同实体类相同名字的表(table),也可以通过注解中的 name 属性来修改表(table)名称, 如@Entity(name=“stu”) , 这样数据库中表的名称则是 stu 。 该注解十分重要,如果没有该注解首次启动项目的时候你会发现数据库没有生成对应的表。
(2)@Id
@Id 类的属性注解,该注解表明该属性字段是一个主键,该属性必须具备,不可缺少。
(3)@GeneratedValue
该注解通常和 @Id 主键注解一起使用,用来定义主键的呈现形式,
该注解通常有多种使用策略:
- @GeneratedValue(strategy= GenerationType.IDENTITY) 该注解由数据库自动生成,主键自增型,在 mysql 数据库中使用最频繁,oracle 不支持。
- @GeneratedValue(strategy= GenerationType.AUTO) 主键由程序控制,默认的主键生成策略,oracle 默认是序列化的方式,mysql 默认是主键自增的方式。
- @GeneratedValue(strategy= GenerationType.SEQUENCE) 根据底层数据库的序列来生成主键,条件是数据库支持序列,Oracle支持,Mysql不支持。
- @GeneratedValue(strategy= GenerationType.TABLE) 使用一个特定的数据库表格来保存主键,较少使用。
以上的主键生成策略当中,在数据库 mysql 当中 IDENTITY 和 AUTO 用的较多,二者当中 IDENTITY 用的多些
(4)@Column
是一个类的属性注解,该注解可以定义一个字段映射到数据库属性的具体特征,比如字段长度,映射到数据库时属性的具体名字等。
(5)@Transient
是一个属性注解,该注解标注的字段不会被应射到数据库当中。
2 SpringDataREST
2.1 SpringDataRSET
2.1.1 什么是SpringDataREST
Spring Data JPA是基于Spring Data 的Repository之上,可以将Repository自动输出为REST资源。目前Spring Data REST支持将Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j、Spring Data Gemfire以及Spring Data Cassandra的Repository自动转换成REST服务。
(1)REST(Representational State Transfer)
用来规范应用如何在 HTTP 层与 API 提供方进行数据交互
REST约束
1.客户端-服务器结构
2.无状态
3.可缓存
4.分层的系统
5.按需代码(可选)
6.统一接口。
该约束是 REST 服务的基础,是客户端和服务器之间的桥梁。该约束又包含下面4个子约束:
1) 资源标识符。每个资源都有各自的标识符。客户端在请求时需要指定该标识符。在 REST 服务中,该标识符通常是 URI。客户端所获取的是资源的表达(representation),通常使用 XML 或 JSON 格式。
2) 通过资源的表达来操纵资源。客户端根据所得到的资源的表达中包含的信息来了解如何操纵资源,比如对资源进行修改或删除。
3) 自描述的消息。每条消息都包含足够的信息来描述如何处理该消息。
4) 超媒体作为应用状态的引擎(HATEOAS)。客户端通过服务器提供的超媒体内容中动态提供的动作来进行状态转换。
(2)HATEOAS(The Hypermedia As The Engine Of Application Statue)
是REST架构的主要约束
REST成熟的模型
第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
第二个层次(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
第三个层次(Level 2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
第四个层次(Level 3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。
根据REST 成熟度模型中可以看到,使用 HATEOAS 的 REST 服务是成熟度最高的,也是推荐的做法
RESTful API最好做到Hypermedia,或HATEOAS,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么
eg:
{
"links": {
"self": { "href": "http://api.com/items" },
"item": [
{ "href": "http://api.com/items/1" },
{ "href": "http://api.com/items/2" }
]
"data": [
{"itemName":"a"},
{"itemName":"b"}
]
}
(3)HAL(Hypertext Application Language)
HAL是一种简单的格式,为 API 中的资源提供简单一致的链接
HAL可以用来实现HATEOAS
HAL 模型包括:
- 链接
- 内嵌资源
- 状态
HAL专为构建API而设计,在这些API中,客户端通过链接在客户端中浏览资源
(4)spring-boot-starter-data-rest使用Spring Boot构建RESTful API
Spring Data REST是基于Spring Data的repository之上,可以把 repository 自动输出为REST资源 Spring Data REST把我们需要编写的大量REST模版接口做了自动化实现,并符合HAL的规范
2.2 SpringMVC中配置使用Spring Data REST
Spring Data REST的配置是定义在RepositoryRestMvcConfiguration配置类中已经配置好了,我们可以通过继承此类或者直接在自己的配置类上@Import此配置类
1)继承方式演示
import org.springframework.context.annotation.Configuration;
/**
* create by jack 2017/10/5
*/
@Configuration
public class MyRepositoryRestMvcConfiguration extends RepositoryRestMvcConfiguration{
@Override
public RepositoryRestMvcConfiguration config(){
return super.config();
}
//其他可重写以config开头的方法
}
2)导入方式演示
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* create by jack 2017/10/5
*/
@Configuration
@Import(RepositoryRestMvcConfiguration.class)
public class AppConfig {
}
2.3 SpringBoot的支持
Spring Boot对Spring Data REST的自动配置放置在rest包中
通过SpringBootRestConfiguration类的源码我们可以得出,Spring Boot已经为我们自动配置了RepositoryRestConfiguration,所以在Spring boot中使用Spring Data REST只需引入spring-boot-starter-data-rest的依赖,无须任何配置即可使用。
Spring boot通过在application.properties中配置以“spring.data.rest”为前缀的属性来配置RepositoryRestConfiguration
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Order(0)
class SpringBootRepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Autowired(required = false)
private Jackson2ObjectMapperBuilder objectMapperBuilder;
@Autowired
private RepositoryRestProperties properties;
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
this.properties.applyTo(config);
}
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
if (this.objectMapperBuilder != null) {
this.objectMapperBuilder.configure(objectMapper);
}
}
}
@ConfigurationProperties(prefix = "spring.data.rest")
public class RepositoryRestProperties {
/**
* Base path to be used by Spring Data REST to expose repository resources.
*/
private String basePath;
/**
* Default size of pages.
*/
private Integer defaultPageSize;
/**
* Maximum size of pages.
*/
private Integer maxPageSize;
/**
* Name of the URL query string parameter that indicates what page to return.
*/
private String pageParamName;
/**
* Name of the URL query string parameter that indicates how many results to return at
* once.
*/
private String limitParamName;
/**
* Name of the URL query string parameter that indicates what direction to sort
* results.
*/
private String sortParamName;
/**
* Strategy to use to determine which repositories get exposed.
*/
private RepositoryDetectionStrategies detectionStrategy = RepositoryDetectionStrategies.DEFAULT;
/**
* Content type to use as a default when none is specified.
*/
private MediaType defaultMediaType;
/**
* Return a response body after creating an entity.
*/
private Boolean returnBodyOnCreate;
/**
* Return a response body after updating an entity.
*/
private Boolean returnBodyOnUpdate;
/**
* Enable enum value translation via the Spring Data REST default resource bundle.
* Will use the fully qualified enum name as key.
*/
private Boolean enableEnumTranslation;
public String getBasePath() {
return this.basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
public Integer getDefaultPageSize() {
return this.defaultPageSize;
}
public void setDefaultPageSize(Integer defaultPageSize) {
this.defaultPageSize = defaultPageSize;
}
public Integer getMaxPageSize() {
return this.maxPageSize;
}
public void setMaxPageSize(Integer maxPageSize) {
this.maxPageSize = maxPageSize;
}
public String getPageParamName() {
return this.pageParamName;
}
public void setPageParamName(String pageParamName) {
this.pageParamName = pageParamName;
}
public String getLimitParamName() {
return this.limitParamName;
}
public void setLimitParamName(String limitParamName) {
this.limitParamName = limitParamName;
}
public String getSortParamName() {
return this.sortParamName;
}
public void setSortParamName(String sortParamName) {
this.sortParamName = sortParamName;
}
public RepositoryDetectionStrategies getDetectionStrategy() {
return this.detectionStrategy;
}
public void setDetectionStrategy(RepositoryDetectionStrategies detectionStrategy) {
this.detectionStrategy = detectionStrategy;
}
public MediaType getDefaultMediaType() {
return this.defaultMediaType;
}
public void setDefaultMediaType(MediaType defaultMediaType) {
this.defaultMediaType = defaultMediaType;
}
public Boolean getReturnBodyOnCreate() {
return this.returnBodyOnCreate;
}
public void setReturnBodyOnCreate(Boolean returnBodyOnCreate) {
this.returnBodyOnCreate = returnBodyOnCreate;
}
public Boolean getReturnBodyOnUpdate() {
return this.returnBodyOnUpdate;
}
public void setReturnBodyOnUpdate(Boolean returnBodyOnUpdate) {
this.returnBodyOnUpdate = returnBodyOnUpdate;
}
public Boolean getEnableEnumTranslation() {
return this.enableEnumTranslation;
}
public void setEnableEnumTranslation(Boolean enableEnumTranslation) {
this.enableEnumTranslation = enableEnumTranslation;
}
public void applyTo(RepositoryRestConfiguration configuration) {
if (this.basePath != null) {
configuration.setBasePath(this.basePath);
}
if (this.defaultPageSize != null) {
configuration.setDefaultPageSize(this.defaultPageSize);
}
if (this.maxPageSize != null) {
configuration.setMaxPageSize(this.maxPageSize);
}
if (this.pageParamName != null) {
configuration.setPageParamName(this.pageParamName);
}
if (this.limitParamName != null) {
configuration.setLimitParamName(this.limitParamName);
}
if (this.sortParamName != null) {
configuration.setSortParamName(this.sortParamName);
}
if (this.detectionStrategy != null) {
configuration.setRepositoryDetectionStrategy(this.detectionStrategy);
}
if (this.defaultMediaType != null) {
configuration.setDefaultMediaType(this.defaultMediaType);
}
if (this.returnBodyOnCreate != null) {
configuration.setReturnBodyOnCreate(this.returnBodyOnCreate);
}
if (this.returnBodyOnUpdate != null) {
configuration.setReturnBodyOnUpdate(this.returnBodyOnUpdate);
}
if (this.enableEnumTranslation != null) {
configuration.setEnableEnumTranslation(this.enableEnumTranslation);
}
}
}
2.4 示例
2.4.1 新建SpringBoot项目
(1)新建Spring Boot项目依赖JPA,WEB,添加mysql的驱动,pom.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jack</groupId>
<artifactId>springboot6springdatarest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot6springdatarest</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql连接驱动-->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)这里的依赖除了添加了数据库相关依赖外还有Spring Data Jpa以及Spring Data Rest的依赖,项目建成后,接下来在配置文件中进行配置,配置如下:
server.port=8088
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/spring_vue?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
spring.datasource.username=root
spring.datasource.password=********
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.open-in-view=true
spring.jpa.properties.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
2.4.2 实体类User
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "users")
@Data
@Builder
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
}
2.4.3 UsersRepository
import com.example.demo.model.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UsersRepository extends JpaRepository<Users,Integer> {
}
这样就已经提供了一个关于user的rest api,简单的增、删、改、查都有了。不用写Controller,spring已经实现了
启动项目
GET http://127.0.0.1:8080
{
"_links": {
"userses": {
"href": "http://127.0.0.1:8080/userses{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://127.0.0.1:8080/profile"
}
}
}
分页+排序查询
GET http://127.0.0.1:8080/userses?page=1&size=2&sort=createTime
{
"_embedded": {
"userses": [
{
"name": "Nana",
"createTime": "2020-04-07T02:10:12.469+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/userses/5"
},
"users": {
"href": "http://127.0.0.1:8080/userses/5"
}
}
},
{
"name": "xyz",
"createTime": "2020-04-07T02:10:12.469+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/userses/3"
},
"users": {
"href": "http://127.0.0.1:8080/userses/3"
}
}
}
]
},
"_links": {
"first": {
"href": "http://127.0.0.1:8080/userses?page=0&size=2&sort=createTime,asc"
},
"prev": {
"href": "http://127.0.0.1:8080/userses?page=0&size=2&sort=createTime,asc"
},
"self": {
"href": "http://127.0.0.1:8080/userses"
},
"next": {
"href": "http://127.0.0.1:8080/userses?page=2&size=2&sort=createTime,asc"
},
"last": {
"href": "http://127.0.0.1:8080/userses?page=2&size=2&sort=createTime,asc"
},
"profile": {
"href": "http://127.0.0.1:8080/profile/userses"
}
},
"page": {
"size": 2,
"totalElements": 5,
"totalPages": 3,
"number": 1
}
}
查询某一个
GET http://127.0.0.1:8080/userses/5
{
"name": "Nana",
"createTime": "2020-04-07T02:10:12.469+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/userses/5"
},
"users": {
"href": "http://127.0.0.1:8080/userses/5"
}
}
}
新增
POST http://127.0.0.1:8080/userses
修改
PUT http://127.0.0.1:8080/userses/481
删除
DELETE http://127.0.0.1:8080/userses/481
(3)其他
注解 @RepositoryRestResource指定切入点
eg:
- 映射到 /user上
- 添加自定义查询 findByName findByNameContaining
import com.example.demo.model.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.stereotype.Repository;
import java.util.List;
@RepositoryRestResource(collectionResourceRel = "user", path = "user")
public interface UsersRepository extends JpaRepository<Users,Integer> {
List<Users> findByName(@Param("name") String name);
List<Users> findByNameContaining(@Param("name") String name);
}
注:
方法的定义,参数要有 @Param 注解
重新启动项目
GET http://127.0.0.1:8080/user
{
"_embedded": {
"user": [
{
"name": "Lili",
"createTime": "2020-04-07T02:50:27.501+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/1"
},
"users": {
"href": "http://127.0.0.1:8080/user/1"
}
}
},
{
"name": "Fiona",
"createTime": "2020-04-07T02:50:27.508+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/2"
},
"users": {
"href": "http://127.0.0.1:8080/user/2"
}
}
},
{
"name": "xyz",
"createTime": "2020-04-07T02:50:27.508+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/3"
},
"users": {
"href": "http://127.0.0.1:8080/user/3"
}
}
}
}
]
},
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://127.0.0.1:8080/profile/user"
},
"search": {
"href": "http://127.0.0.1:8080/user/search"
}
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
GET http://127.0.0.1:8080/user/search
{
"_links": {
"findByNameContaining": {
"href": "http://127.0.0.1:8080/user/search/findByNameContaining{?name}",
"templated": true
},
"findByName": {
"href": "http://127.0.0.1:8080/user/search/findByName{?name}",
"templated": true
},
"self": {
"href": "http://127.0.0.1:8080/user/search"
}
}
}
查询 name=xyz
GET http://127.0.0.1:8080/user/search/findByName?name=xyz
{
"_embedded": {
"user": [
{
"name": "xyz",
"createTime": "2020-04-07T03:14:11.921+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/3"
},
"users": {
"href": "http://127.0.0.1:8080/user/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/search/findByName?name=xyz"
}
}
}
查询 name 包含i的
GET http://127.0.0.1:8080/user/search/findByNameContaining?name=i
{
"_embedded": {
"user": [
{
"name": "Lili",
"createTime": "2020-04-07T03:14:11.913+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/1"
},
"users": {
"href": "http://127.0.0.1:8080/user/1"
}
}
},
{
"name": "Fiona",
"createTime": "2020-04-07T03:14:11.920+0000",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/2"
},
"users": {
"href": "http://127.0.0.1:8080/user/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://127.0.0.1:8080/user/search/findByNameContaining?name=i"
}
}
}
配置
在application.properties 中配置
1)定制根路径
在上面实战的例子中,我们访问的REST资源的路径是在根目录下的,即http://localhost:9090/user,如果我们需要定制根路径的话,只需在Spring Boot的application.properties下增加如下定义即可:
spring.data.rest.base-path=/rest
访问的id为81的url:http://localhost:9090/api/user/1
2)定制节点路径
上例实战中我们的节点为http://localhost:9090/user,这是spring data rest的默认规则,就是在实体类之后加“s”来形成路径。如果我们需要自定义节点路径,使用people,在类似的情况下要对映射的名称进行修改的话,我们需要在实体类Repository上使用@RepositoryRestResource注解的path属性进行修改,代码如下:
import com.example.demo.model.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository(path = "people")
public interface UsersRepository extends JpaRepository<Users,Integer> {
}
此时我们访问rest服务的地址变为:http://localhost:9090/api/people
3)添加/更新成功时是否返回添加/更新记录
spring.data.rest.return-body-on-create=true
spring.data.rest.return-body-on-update=true
注:如果为false,则返回空白
3 Spring事务
3.1 事务简介
事务作用:一组操作要不全部成功,要么全部失败,保持数据一致性。
事务特性(ACID):
- Atomic原子性:确保所有操作要么全都发生,要么都不发生
- Consitent一致性:事务前后,数据均处于正确状态,数据不应该被破坏
- Isolation隔离性:事务彼此隔离,用户间操作不相互影响
- Durable持久性:事务一旦完成,结果应该持久化
Spring对事务管理的支持:
- 1)编程式事务 编程式事务可以清晰地定义事务的边界,可以实现细粒度的事务控制。
- 2)声明式事务 只需要在配置文件中配置事务,将操作纳入事务管理,解除了和代码耦合,对应用代码影响最小。
3.2 Spring事务管理器
Spring并不直接管理事务,而是通过多种事务管理器,将管理事务的责任交给JTA或相应的持久性机制所提供的某个特定平台的事务实现。
Spring提供了顶层接口PlatformTransactionManager,并提供了扩展接口ResorceTransactionManager和抽象实现类AbstractPlatformTransactionManager。
一般而言,每一种事务实现的类图如下:
常见事务管理器: 1)DataSourceTransactionManager : 用于Spring JDBC抽象框架、iBatis或MyBatis框架事务管理 2)JpaTransactionManager : 用于JPA实现框架事务管理 3)HibernateTransactionManager : 提供对Hibernate框架事务管理 …….
- JDBC事务 使用Spring的DataSourceTransactionManager来管理事务。
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
使用ibatis、mybatis持久框架同样采用此配置。 dataSource属性定义了数据源,通过数据源获取javax.sql.Connection数据库连接,从而调用该链接的commit()和rollback(0方法来提交或回滚事务。
- Hibernate事务 使用Spring的HibernateTransactionManager来管理事务。
<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
Hibernate事务需要通过org.hibernate.Transcation对象来管理,其同样提供了commit(0和rollback()方法来提交和回滚事务。
- JPA事务 JAVA持久化标准(Java Persistent API, JPA),使用Spring提供的JpaTransactionManager来管理事务。
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
- 分布式事务(JtaTranscationManger)
pring的org.springframework.transaction.jta.JtaTransactionManager,提供了分布式事务支持。如果使用WAS的JTA支持,把它的属性改为WebSphere对应TransactionManager。
在tomcat下,是没有分布式事务的,不过可以借助于第三方软件jotm(Java Open Transaction Manager )和AtomikosTransactionsEssentials实现,在spring中分布式事务是通过jta(jotm,atomikos)来进行实现。
3.3 Spring 中事务的实现方式
Spring 中的操作主要分为两类:
- 编程式事务 (了解)
- 声明式事务 编程式事务就是手写代码操作事务, 而声明式事务是利用注解来自动开启和提交事务. 并且编程式事务用几乎不怎么用. 这就好比汽车的手动挡和自动挡, 如果有足够的的钱, 大部分人应该都会选择自动挡. 声明式事务也是如此, 它不仅好用, 还特别方便.
3.3.1 Spring 编程式事务 (了解)
编程式事务和 MySQL 中操作事务类似, 也是三个重要步骤:
1)开启事务 2)提交事务 3)回滚事务
//这是一个组合注解,它组合了@Controller和@ResponseBody
//表明这个类是一个控制器,会处理HTTP请求,并且返回的数据会自动转换为JSON或其他格式
@RestController
//这个注解表明这个控制器会处理以"/user"开头的URL
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService; //这是一个业务逻辑服务类,用于处理与用户相关的业务逻辑
//编程式事务
//Spring的事务管理可以确保当在执行数据库操作时,如果出现错误,所有的操作都可以回滚,以保证数据的一致性。
@Autowired
//transactionManager 负责管理数据库事务
private DataSourceTransactionManager transactionManager;
@Autowired
//transactionDefinition 它定义了事务的各种属性,比如隔离级别、传播行为、超时时间、是否为只读事务等。
private TransactionDefinition transactionDefinition;
@RequestMapping("/del")
public int del(Integer id) {
if(id == null || id <= 0) {
return 0;
}
//1.开启事务, 以便后续的事务操作,比如提交或回滚事务。
TransactionStatus transactionStatus = null;
int result = 0;
try {
transactionStatus = transactionManager.getTransaction(transactionDefinition);
// 业务操作, 删除用户
result = userService.del(id);
System.out.println("删除: " + result);
//2.提交事务/回滚事务
// transactionManager.commit(transactionStatus); //提交事务
}catch (Exception e) {
if(transactionStatus != null) {
transactionManager.rollback(transactionStatus);
} //回滚事务
}
return result;
}
}
- DataSourceTransactionManager 和 TransactionDefinition 是 SpringBoot 内置的两个对象.
- DataSourceTransactionManager : 用来获取事务(开启事务)、提交或回滚事务.
- TransactionDefinition : 它是事务的属性,在获取事务的时候需要将这个对象传递进去从而获得?个事务 TransactionStatus.
我们可以发现,事务成功的进行了回滚,但是这样的方式太繁琐了,我们来学习更简单的声明式事务。
3.3.2 Spring 声明式事务 ( @Transactional )
声明式事务的实现相较于编程式事务来说, 就要简单太多了, 只需要在需要的方法上添加 @Transactional注解就可以实现了.
@Transactional 注解的作用:
当进入方法的时候, 它就会自动开启事务, 当方法结束后, 它就会自动提交事务. 说白了它就是 AOP 的一个环绕通知. 只要加了 @Transactional 注解的方法, 都有一个事务的 AOP , 这都是 Spring 帮我们封装好的.
@Transactional 注解的执行流程:
方法执行之前, 先开启事务, 当方法成功执行完成之后会自动提交事务. 如果方法在执行过程中发生了异常, 那么事务会自动回滚.
//这是一个组合注解,它组合了@Controller和@ResponseBody
//表明这个类是一个控制器,会处理HTTP请求,并且返回的数据会自动转换为JSON或其他格式
@RestController
//这个注解表明这个控制器会处理以"/user"开头的URL
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional //声明式事务
public int del(Integer id) {
if(id == null || id <= 0) {
return 0;
}
return userService.del(id);
}
}
对于方法执行成功的情况就不测试了, 它和普通的插入数据没有多大区别, 重点在于理解 @Transactional注解的含义和作用即可.
3.4 事务传播行为
@Transactional(propagation=Propagation.REQUIRED) :如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED) :容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW) :不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY) :必须在一个已有的事务中执行,否则抛出异常 @Transactional(propagation=Propagation.NEVER) :必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS) :如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
@Transactional(propagation=Propagation.NESTED) :如果有活动事务,运行在潜逃的事务中,没有活动事务,则按Propagation.REQUIRED属性执行
3.5 事务超时设置
@Transactional(timeout=30) // 默认为30s
3.6 事务隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用
@Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)
@Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)
@Transactional(isolation = Isolation.SERIALIZABLE):串行化
MYSQL: 默认为REPEATABLE_READ级别
SQLSERVER: 默认为READ_COMMITTED
3.7 SpringBoot 的事务支持
3.7.1 自动配置的事务管理器
在使用 JDBC 作为数据访问技术时,配置定义如下:
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(DataSource.class)
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(this.dataSource)
}
在使用 JPA 作为数据访问技术时,配置定义如下:
@Bean
@ConditionalOnMissingBean(PlatformTransactionManager.class)
public PlatformTransactionManager transactionManager(){
return new JpaTransactionManager()
}
3.7.2 自动开启注解事务的支持
SpringBoot 专门用于配置事务的类为 org.springframework.boot.autoconfigure.transcation.TransactionAutoConfiguration,此配置类依赖于 JpaBaseConfiguration 和 DataSourceTransactionManagerAutoConfiguration。
而在 DataSourceTransactionManagerAutoConfiguration 配置里还开启了对声明式事务的支持,代码如下:
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
@Configuration
@EnableTransactionManagement
protected static class TransactionManagementConfiguration{
}
所以在 SpringBoot 中,无须显式开启使用 @EnableTransactionManagement 注解。
3.8 实战
演示如何使用 Transactional 使用异常导致数据回滚与使用异常导致数据不回滚。
- 准备工作: 1)SpringBoot 2.1.3 2)JDK 1.8 3)IDEA
3.8.1 pom.xml 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nasus</groupId>
<artifactId>transaction</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>transaction</name>
<description>transaction Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- JPA 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- web 启动类 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql 连接类 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok 插件,简化实体代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.8.2 application.yaml 配置
spring:
# \u6570\u636E\u5E93\u76F8\u5173
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true
username: root
password: 123456
# jpa \u76F8\u5173
jpa:
hibernate:
ddl-auto: update # ddl-auto:\u8BBE\u4E3A create \u8868\u793A\u6BCF\u6B21\u90FD\u91CD\u65B0\u5EFA\u8868
show-sql: true
3.8.3 实体类:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue
private Integer id;
private String name;
private Integer age;
}
3.8.4 dao 层
import com.nasus.transaction.domain.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
3.8.5 service 层
import com.nasus.transaction.domain.Student;
public interface StudentService {
Student saveStudentWithRollBack(Student student);
Student saveStudentWithoutRollBack(Student student);
}
3.8.6 实现类:
import com.nasus.transaction.domain.Student;
import com.nasus.transaction.repository.StudentRepository;
import com.nasus.transaction.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
// 直接注入 StudentRepository 的 bean
private StudentRepository studentRepository;
// 使用 @Transactional 注解的 rollbackFor 属性,指定特定异常时,触发回滚
@Transactional(rollbackFor = {IllegalArgumentException.class})
@Override
public Student saveStudentWithRollBack(Student student) {
Student s = studentRepository.save(student);
if ("高斯林".equals(s.getName())){
//硬编码,手动触发异常
throw new IllegalArgumentException("高斯林已存在,数据将回滚");
}
return s;
}
// 使用 @Transactional 注解的 noRollbackFor 属性,指定特定异常时,不触发回滚
@Transactional(noRollbackFor = {IllegalArgumentException.class})
@Override
public Student saveStudentWithoutRollBack(Student student) {
Student s = studentRepository.save(student);
if ("高斯林".equals(s.getName())){
throw new IllegalArgumentException("高斯林已存在,数据将不会回滚");
}
return s;
}
}
3.8.7 controller 层
import com.nasus.transaction.domain.Student;
import com.nasus.transaction.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/student")
public class StudentController {
// 注入 studentservice 的 bean
@Autowired
private StudentService studentService;
// 测试回滚情况
@PostMapping("/withRollBack")
public Student saveStudentWithRollBack(@RequestBody Student student){
return studentService.saveStudentWithRollBack(student);
}
// 测试不回滚情况
@PostMapping("/withOutRollBack")
public Student saveStudentWithoutRollBack(@RequestBody Student student){
return studentService.saveStudentWithoutRollBack(student);
}
}
3.8.8 Postman 测试结果
为了更清楚地理解回滚,以 debug (调试模式) 启动程序。并在 StudentServiceImpl 的 saveStudentWithRollBack 方法上打上断点。
分别访问不同的接口,查看数据库,可以观察到,测试结果成功。
4 数据缓存Cache
4.1 Spring缓存支持
4.1.1 Spring支持的CacheManger
在 Spring 3.1 中引入了多 Cache 的支持,在 spring-context 包中定义了org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 两个接口来统一不同的缓存技术。Cache 接口包含缓存的常用操作:增加、删除、读取等。CacheManager 是 Spring 各种缓存的抽象接口。 Spring 支持的常用 CacheManager 如下:
CacheManager | 描述 |
SimpleCacheManager | 使用简单的 Collection 来存储缓存 |
ConcurrentMapCacheManager | 使用 java.util.ConcurrentHashMap 来实现缓存 |
NoOpCacheManager | 仅测试用,不会实际存储缓存 |
EhCacheCacheManger | 使用EhCache作为缓存技术。EhCache 是一个纯 Java 的进程内缓存框架,特点快速、精干,是 Hibernate 中默认的 CacheProvider,也是 Java 领域应用最为广泛的缓存 |
JCacheCacheManager | 支持JCache(JSR-107)标准的实现作为缓存技术 |
CaffeineCacheManager | 使用 Caffeine 作为缓存技术。用于取代 Guava 缓存技术。 |
RedisCacheManager | 使用Redis作为缓存技术 |
HazelcastCacheManager | 使用Hazelcast作为缓存技术 |
CompositeCacheManager | 用于组合 CacheManager,可以从多个 CacheManager 中轮询得到相应的缓存 |
4.1.2 声明式缓存注解
Spring Cache 提供了 @Cacheable 、@CachePut 、@CacheEvict 、@Caching 等注解,在方法上使用。通过注解 Cache 可以实现类似事务一样、缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码。
核心思想:当我们调用一个方法时会把该方法的参数和返回结果最为一个键值对存放在缓存中,等下次利用同样的参数来调用该方法时将不会再执行,而是直接从缓存中获取结果进行返回。
4.1.2.1 Cache注解
(1)@EnableCaching
开启缓存功能,一般放在启动类上。
(2)@CacheConfig
当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"cacheName"})注解在 class 之上来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
(3)@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 查看源码,属性值如下:
属性/方法名 | 解释 |
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
unless | 条件符合则不缓存 |
sync | 是否使用异步模式,默认为false |
(4)@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。 查看源码,属性值如下:
属性/方法名 | 解释 |
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
unless | 条件符合则不缓存 |
(5)@CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上 查看源码,属性值如下:
属性/方法名 | 解释 |
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
allEntries | 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
(6)@Caching
该注解可以实现同一个方法上同时使用多种注解。可从其源码看出:
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
4.1.3 开启声明式缓存支持
开启声明式缓存很简单,只需在配置类上使用 @EnableCaching 注解即可,例如:
@Configuration
@EnableCaching
public class AppConfig{
}
4.2 SpringBoot的缓存支持
在 Spring 中使用缓存技术的关键是配置 CacheManager ,而 SpringBoot 为我们配置了多个 CacheManager 的实现。
它的自动配置放在 org.springframework.boot.autoconfigure.cache 包中。
在不做任何配置的情况下,默认使用的是 SimpleCacheConfiguration ,即使用 ConcurrentMapCacheManager。SpringBoot 支持以前缀来配置缓存。例如:
spring.cache.type= # 可选 generic、ehcache、hazelcast、infinispan、jcache、redis、guava、simple、none
spring.cache.cache-names= # 程序启动时创建的缓存名称
spring.cache.ehcache.config= # ehcache 配置文件的地址
spring.cache.hazelcast.config= # hazelcast配置文件的地址
spring.cache.infinispan.config= # infinispan配置文件的地址
spring.cache.jcache.config= # jcache配置文件的地址
spring.cache.jcache.provider= # 当多个 jcache 实现在类路径的时候,指定 jcache 实现
# 等等。。。
在 SpringBoot 环境下,使用缓存技术只需要在项目中导入相关缓存技术的依赖包,并在配置类中使用 @EnableCaching 开启缓存支持即可。
4.3 示例
本文将以 SpringBoot 默认的 ConcurrentMapCacheManager 作为缓存技术,演示 @Cacheable、@CachePut、@CacheEvict。
4.3.1 准备工作
- IDEA
- JDK 1.8
- SpringBoot 2.1.3
4.3.2 Pom.xml 文件依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nasus</groupId>
<artifactId>cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cache</name>
<description>cache Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- cache 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- web 启动类 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql 数据库连接类 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok 依赖,简化实体 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 单元测试类 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.3.3 application.yaml 文件配置
spring:
# 数据库相关
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true
username: root
password: 123456
# jpa 相关
jpa:
hibernate:
ddl-auto: update # ddl-auto: 设为 create 表示每次都重新建表
show-sql: true
4.3.4 实体类
package com.nasus.cache.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue
private Integer id;
private String name;
private Integer age;
}
4.3.5 dao 层
package com.nasus.cache.repository;
import com.nasus.cache.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student,Integer> {
}
4.3.6 service 层
package com.nasus.cache.service;
import com.nasus.cache.entity.Student;
public interface StudentService {
public Student saveStudent(Student student);
public void deleteStudentById(Integer id);
public Student findStudentById(Integer id);
}
4.3.7 实现类:
package com.nasus.cache.service.impl;
import com.nasus.cache.entity.Student;
import com.nasus.cache.repository.StudentRepository;
import com.nasus.cache.service.StudentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class StudentServiceImpl implements StudentService {
// 使用 slf4j 作为日志框架
private static final Logger LOGGER = LoggerFactory.getLogger(StudentServiceImpl.class);
@Autowired
private StudentRepository studentRepository;
@Override
@CachePut(value = "student",key = "#student.id")
// @CachePut 缓存新增的或更新的数据到缓存,其中缓存名称为 student 数据的 key 是 student 的 id
public Student saveStudent(Student student) {
Student s = studentRepository.save(student);
LOGGER.info("为id、key 为{}的数据做了缓存", s.getId());
return s;
}
@Override
@CacheEvict(value = "student")
// @CacheEvict 从缓存 student 中删除 key 为 id 的数据
public void deleteStudentById(Integer id) {
LOGGER.info("删除了id、key 为{}的数据缓存", id);
//studentRepository.deleteById(id);
}
@Override
@Cacheable(value = "student",key = "#id")
// @Cacheable 缓存 key 为 id 的数据到缓存 student 中
public Student findStudentById(Integer id) {
Student s = studentRepository.findById(id).get();
LOGGER.info("为id、key 为{}的数据做了缓存", id);
return s;
}
}
4.3.8 controller 层
package com.nasus.cache.controller;
import com.nasus.cache.entity.Student;
import com.nasus.cache.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping("/put")
public Student saveStudent(@RequestBody Student student){
return studentService.saveStudent(student);
}
@DeleteMapping("/evit/{id}")
public void deleteStudentById(@PathVariable("id") Integer id){
studentService.deleteStudentById(id);
}
@GetMapping("/able/{id}")
public Student findStudentById(@PathVariable("id") Integer id){
return studentService.findStudentById(id);
}
}
4.3.9 application 开启缓存功能
package com.nasus.cache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching // 开启缓存功能
@SpringBootApplication
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
4.3.10 测试
1)测试 @Cacheable
访问 http://localhost:8080/student/able/2 控制台打印出了 SQL 查询语句,以及指定日志。说明这一次程序是直接查询数据库得到的结果。
2019-02-21 22:54:54.651 INFO 1564 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 11 ms
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 22:54:59.725 INFO 1564 --- [nio-8080-exec-1] c.n.c.service.impl.StudentServiceImpl : 为id、key 为2的数据做了缓存
再次访问 http://localhost:8080/student/able/2 结果如下图。但控制台无 SQL 语句打印,也无为id、key 为2的数据做了缓存这句话输出。
说明 @Cacheable 确实做了数据缓存,第二次的测试结果是从数据缓存中获取的,并没有直接查数据库。
2)测试 @CachePut
如下图,postman 访问 http://localhost:8080/student/put 插入数据:
下面是控制台打印出了 SQL Insert 插入语句,以及指定日志。说明程序做了缓存。
Hibernate: insert into student (age, name, id) values (?, ?, ?)
2019-02-21 23:12:03.688 INFO 1564 --- [nio-8080-exec-8] c.n.c.service.impl.StudentServiceImpl : 为id、key 为4的数据做了缓存
查看数据库可以找到插入的数据
访问 http://localhost:8080/student/able/4 Postman 结果如下图。控制台无输出,验证了 @CachePut 确实做了缓存,下图数据是从缓存中获取的。
3)测试 @CacheEvict
postman 访问 http://localhost:8080/student/able/3 为 id = 3 的数据做缓存。
postman 再次访问 http://localhost:8080/student/able/3 确认数据是从缓存中获取的。
postman 访问 http://localhost:8080/student/evit/3
从缓存中删除 key 为 3 的缓存数据:
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 23:26:08.516 INFO 8612 --- [nio-8080-exec-2] c.n.c.service.impl.StudentServiceImpl : 为id、key 为3的数据做了缓存
2019-02-21 23:27:01.508 INFO 8612 --- [nio-8080-exec-4] c.n.c.service.impl.StudentServiceImpl : 删除了id、key 为3的数据缓存
再次 postman 访问 http://localhost:8080/student/able/3 观察后台,重新做了数据缓存:
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 23:27:12.320 INFO 8612 --- [nio-8080-exec-5] c.n.c.service.impl.StudentServiceImpl : 为id、key 为3的数据做了缓存
这一套测试流程下来,证明了 @CacheEvict 确实删除了数据缓存。
4.4 切换缓存技术
切换缓存技术除了在 pom 文件加入相关依赖包配置以外,使用方式与上面的代码演示一样。
1)切换 EhCache 在 pom 中添加 Encache 依赖:
<!-- EhCache 依赖 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
Ehcache 所需配置文件 ehcache.xml 只需放在类路径(resource 目录)下,SpringBoot 会自动扫描,如:
<?xml version="1.0" encoding="UTF-8">
<ehcache>
<cache name="student" maxElementsInMmory="1000">
<ehcache>
SpringBoot 会自动配置 EhcacheManager 的 Bean。
2)切换 Guava
只需在 pom 中加入 Guava 依赖即可:
<!-- GuavaCache 依赖 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
SpringBoot 会自动配置 GuavaCacheManager 的 Bean。
3)切换 RedisCache
与 Guava 一样,只需在 pom 加入依赖即可:
<!-- cache 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
SpringBoot 会自动配置 RedisCacheManager 以及 RedisTemplate 的 Bean。 此外,切换其他缓存技术的方式也是类似。
5 非关系型数据库NoSQL
NoSQL是对于不使用关系作为数据管理的数据库系统的统称。NoSQL的主要特点是不使用SQL语言作为查询语言,数据存储也不是固定的表、字段。
NoSQL数据库主要有文档存储型(MongoDB)、图形关系存储型(Neo4j)和键值对存储型(Redis)等。
5.1 MongoDB
5.1.1 介绍
MongoDB是一个是一个基于文档(Document)的存储型数据库,使用面向对象的思想,每一条数据文档的对象。来自菜鸟教程的解释是:
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。
MongoDB是一种开源的文档型数据库管理系统,它使用类似于JSON的BSON格式(Binary JSON)来存储数据。与传统关系型数据库不同,MongoDB不使用表和行的结构,而是采用集合(Collection)(Mysql表)和文档(Document)(MySql行)的概念来组织数据。
5.1.2 使用场景
- 网站数据存储: 实时应用:MongoDB 非常适合需要频繁插入、更新和查询的实时应用程序,比如新闻feed、博客、论坛、评论系统等,其快速的写入速度和高效的查询性能有利于应对高并发访问。
- 游戏开发: 游戏用户信息:存储玩家账户、角色属性、装备、积分等数据,内嵌文档结构能很好地满足这类复杂且动态变化的数据需求。 实时数据分析:游戏事件日志、实时排行榜等场景要求数据库具备快速写入和即时查询的能力。
- 物流与电商: 订单管理:订单信息、商品库存、交易历史等,MongoDB 对频繁更新的状态跟踪表现优秀。 用户行为分析:记录并分析用户浏览、购买、搜索等行为数据。
- 社交网络: 用户资料与社交关系:存储用户个人信息、好友列表、消息记录等半结构化数据。 地理位置服务:利用地理空间索引轻松实现附近的用户、地点查找功能。
- 物联网(IoT): 设备数据存储:收集来自各种智能设备的实时或周期性上报的数据,如温度、湿度、状态变化等信息。 日志记录与分析:处理大量的设备日志数据,进行多维度分析和实时监控。
- 内容管理系统: 博客文章、多媒体内容存储:支持大文本、富媒体类型的内容存储,同时方便实现内容标签、分类等关联查询。
- 视频直播和流媒体: 用户活动记录:存储用户观看历史、互动行为(如送礼、弹幕)等信息。 实时统计与计费:对用户活动数据进行实时统计和计费计算。
- 缓存系统: 高性能缓存:作为高速缓存层,存储经常访问但不需永久保存或可以容忍短时间丢失的数据。
- 大数据分析: 聚合框架:MongoDB 内置了强大的聚合管道功能,可以在数据库层面完成数据预处理和初步分析。
5.1.3 Spring的支持
5.1.3.1 Object/Document 映射注解支持
JPA提供了一套Object/Relation映射的注解(如@Entity,@Id),而Spring Data MongoDB也提供了一套注解:
- @Document:映射领域对象与MongoDB的一个文档,类似于hibernate的@Entity注解
- @Id:主键,不可重复,自带索引
- @Field:为文档的属性定义名称
- @Indexed: 为该字段加索引
- @CompoundIndex:符合索引
- @DBRef:关联另一个document对象,但不会级联表
5.1.3.2 MongoTemplate
类似于jdbcTemplate
5.1.3.3 Repository的支持
和Spring Data JPA的使用方式一样,需要在配置类上加上@EnableMongoRepositories注解
5.1.4 Spring Boot的支持
Spring Boot对MongoDB的支持,位于:org.springframework.boot.autoconfigure.mongo
在配置文件中,以"spring.data.mongodb"为前缀的属性配置MongoDB的信息
Spring Boot提供了一些默认属性以及自动配置,默认端口27017,host为localhost,数据库为test
5.1.5 Spring Boot使用MongoDB示例
5.1.5.1 安装mongodb
- 下载镜像:docker pull mongo
- 运行容器:docker run -d -p 27017:27017 --name mongo mongo:latest
5.1.5.2 SpringBoot工程
(1)依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
(2)application.yml
spring:
data:
mongodb:
host: localhost
port: 27017
database: monge_demo
logging:
level:
org.springframework.data.mongodb.core: debug
(3)启动类
@SpringBootApplication
public class SpringBootDemoMongodbApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoMongodbApplication.class, args);
}
}
(4)实体类: Article.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Article {
/**
* 文章id
*/
@Id
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 文章标题
*/
private String title;
/**
* 文章内容
*/
private String content;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 点赞数量
*/
private Long thumbUp;
/**
* 访客数量
*/
private Long visits;
}
(5)持久层: ArticleRepository.java
public interface ArticleRepository extends MongoRepository<Article, Long> {
/**
* 根据标题模糊查询
*
* @param title 标题
* @return 满足条件的文章列表
*/
List<Article> findByTitleLike(String title);
}
注: 可以通过关键字声明方法, 不用写实现 下标源于文章MongoRepository的生成规则
方法关键字 | 示例 | 等价于SQL |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1(参数绑定附加%) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1(参数与预先绑定%) |
Containing | findByFirstnameContaining | … where x.firstname like ?1(参数绑定%) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
(6) 简单的CURD
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ArticleRepositoryTest {
@Autowired
private ArticleRepository articleRepo;
@Autowired
private MongoTemplate mongoTemplate;
private static Snowflake snowflake = IdUtil.createSnowflake(1, 1);
/**
* 测试新增
*/
@Test
public void testSave() {
Article article = new Article(1L, RandomUtil.randomString(20), RandomUtil.randomString(150), DateUtil.date(), DateUtil.date(), 0L, 0L);
articleRepo.save(article);
// 更具ID属性进行新增或更新
mongoTemplate.save(article);
log.info("【article】= {}", JSONUtil.toJsonStr(article));
}
/**
* 测试批量新增列表
*/
@Test
public void testSaveList() {
List<Article> articles = Lists.newArrayList();
for (int i = 0; i < 10; i++) {
articles.add(new Article(snowflake.nextId(), RandomUtil.randomString(20), RandomUtil.randomString(150), DateUtil.date(), DateUtil.date(), 0L, 0L));
}
articleRepo.saveAll(articles);
log.info("【articles】= {}", JSONUtil.toJsonStr(articles.stream().map(Article::getId).collect(Collectors.toList())));
}
/**
* 测试更新
*/
@Test
public void testUpdate() {
articleRepo.findById(1L).ifPresent(article -> {
article.setTitle(article.getTitle() + "更新之后的标题");
article.setUpdateTime(DateUtil.date());
articleRepo.save(article);
log.info("【article】= {}", JSONUtil.toJsonStr(article));
});
}
/**
* 测试删除
*/
@Test
public void testDelete() {
// 根据主键删除
articleRepo.deleteById(1L);
// 全部删除
articleRepo.deleteAll();
}
/**
* 测试分页排序查询
*/
@Test
public void testQuery() {
Sort sort = Sort.by("thumbUp", "updateTime").descending();
PageRequest pageRequest = PageRequest.of(0, 5, sort);
Page<Article> all = articleRepo.findAll(pageRequest);
log.info("【总页数】= {}", all.getTotalPages());
log.info("【总条数】= {}", all.getTotalElements());
log.info("【当前页数据】= {}", JSONUtil.toJsonStr(all.getContent().stream().map(article -> "文章标题:" + article.getTitle() + "点赞数:" + article.getThumbUp() + "更新时间:" + article.getUpdateTime()).collect(Collectors.toList())));
}
}
(7)更新
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ArticleRepositoryTest {
@Autowired
private ArticleRepository articleRepo;
@Autowired
private MongoTemplate mongoTemplate;
/**
* 测试点赞数、访客数,使用save方式更新点赞、访客
*/
@Test
public void testThumbUp() {
articleRepo.findById(1L).ifPresent(article -> {
article.setThumbUp(article.getThumbUp() + 1);
article.setVisits(article.getVisits() + 1);
articleRepo.save(article);
log.info("【标题】= {}【点赞数】= {}【访客数】= {}", article.getTitle(), article.getThumbUp(), article.getVisits());
});
}
/**
* 测试点赞数、访客数,使用更优雅/高效的方式更新点赞、访客
*/
@Test
public void testThumbUp2() {
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(1L));
Update update = new Update();
update.inc("thumbUp", 1L);
update.inc("visits", 1L);
mongoTemplate.updateFirst(query, update, "article");
articleRepo.findById(1L).ifPresent(article -> log.info("【标题】= {}【点赞数】= {}【访客数】= {}", article.getTitle(), article.getThumbUp(), article.getVisits()));
}
}
(8)高级查询
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ArticleRepositoryTest {
@Autowired
private ArticleRepository articleRepo;
@Autowired
private MongoTemplate mongoTemplate;
/**
* 查询,条件匹配/排序/分页, 基于继承MongoRepository实现
*/
@Test
public void testQuery1() {
/**
* 匹配条件构造
*/
Article article = Article.builder()
.title("ayyg6qetc2jigduentiz")
.content("tx1549k4dbu05ou83tx8te0gx1")
.build();
// 指定字段匹配类型
ExampleMatcher withMatcher = ExampleMatcher.matching()
// 忽略大小写
.withIgnoreCase()
// 指定"title"为精确匹配
.withMatcher("title", ExampleMatcher.GenericPropertyMatcher::exact)
// 指定"content"为模糊匹配
.withMatcher("content", ExampleMatcher.GenericPropertyMatcher::contains);
Example<Article> example = Example.of(article,withMatcher);
/**
* 排序规则
*/
Sort sort = Sort.by("updateTime").descending();
/**
* 分页
*/
PageRequest pageRequest = PageRequest.of(0, 5, sort);
/**
* 分页查询
*/
Page<Article> articleRepoAll = articleRepo.findAll(example, pageRequest);
/**
* 打印
*/
log.info(JSONUtil.toJsonStr(articleRepoAll.getContent()));
}
/**
* 查询,条件匹配/排序/分页, 基于MongoTemplate实现
*/
@Test
public void testQuery2() {
/**
* 查询条件
*/
Criteria criteria = Criteria
// 精确匹配
.where("title").is("ayyg6qetc2jigduentiz")
// 模糊匹配, 用正则: .*[xxx].*
.and("content").regex(".*tx1549k4dbu05ou83tx8te0gx1.*")
// 匹配明细里的字段
.and("ids").elemMatch(Criteria.where("id").is(1))
// 匹配多个并行或
.andOperator(
new Criteria().orOperator(
Criteria.where("visits").exists(false),
Criteria.where("visits").is(1)
),
new Criteria().orOperator(
Criteria.where("thumbUp").exists(false),
Criteria.where("thumbUp").is(1)
)
);
;
/**
* 排序规则
*/
Sort sort = Sort.by("updateTime").descending();
/**
* 分页
*/
PageRequest pageRequest = PageRequest.of(1, 5, sort);
Query query = Query.query(criteria).with(sort).with(pageRequest);
List<Article> articles = mongoTemplate.find(query, Article.class);
PageImpl<Article> page = (PageImpl<Article>) PageableExecutionUtils.getPage(articles, pageRequest, () -> mongoTemplate.count(Query.query(criteria),Article.class));
// 打印
Optional.of(page.getContent()).ifPresent(articles1 -> {
articles1.forEach(article -> {
log.info("打印数据:{}",JSONUtil.toJsonStr(article));
});
});
}
}
(9)MongoTemplate 实现 联表/分页/排序查询
public IPage<Article> pageInfo(){
/**
* 联表查询
* 参数1: 从表表名
* 参数2: 主表关联字段
* 参数3: 从表关联字段
* 参数4: 查出从表数据集合的别名 例如主表数据{"name":"wpr","age":18} , 关联从表后结果{"name":"wpr","age":18,"userInfo":[]}, 从表没数据则为[]
*/
LookupOperation lookup = Aggregation.lookup("user", "userId", "userId", "userInfo");
// 子集合不能为空
Criteria criteria = Criteria.where("userInfo").not().size(0);
// 子集合条件
criteria.and("userInfo.six").is(1);
// 主表条件
criteria.and("title").is("hello_world");
// 条件类型转换
MatchOperation matchOperation = Aggregation.match(criteria);
/**
* 查询总数
*/
CountOperation countOperation = Aggregation.count().as("total");
// project: 表示结果只查询字段:total
ProjectionOperation project = Aggregation.project("total");
// 条件一定要排好先后顺序
Aggregation aggregation = Aggregation.newAggregation(lookup, matchOperation, countOperation, project);
AggregationResults<Map> aggregate = mongoTemplate.aggregate(aggregation, "article", Map.class);
List<Map> aggregateMappedResults = aggregate.getMappedResults();
// 总数
Integer total = CollectionUtils.isEmpty(aggregateMappedResults) ? 0 : (int)aggregateMappedResults.get(0).get("total");
if(Objects.equals(total,0)){
return new Page<>();
}
/**
* 分页查询
*/
// 排序条件
SortOperation sortOperation = Aggregation.sort(Sort.by("updateTime").descending());
// 过滤前n条数据
SkipOperation skipOperation = Aggregation.skip(0L);
// 查询n条数据
LimitOperation limitOperation = Aggregation.limit(10);
Aggregation pageAggregation = Aggregation.newAggregation(lookup, matchOperation, sortOperation,skipOperation,limitOperation);
AggregationResults<Article> result = mongoTemplate.aggregate(pageAggregation,"article", Article.class);
List<Article> articles = result.getMappedResults();
Page<Article> page = new Page<>();
page.setTotal(total);
page.setRecords(articles);
return page;
}
5.2 redis
Redis是一个基于键值对的开源内存数据存储
5.2.1 Spring的支持
5.2.1.1 配置
Spring对Redis的支持也是通过Spring Data Redis来实现的。 根据Redis的不同的Java客户端,Spring Data Redis提供了以下的ConnectionFactory,可以在 org.springframework.data.redis.connection 这个目录下找到各种ConnectionFactory: JedisConnectionFactory,JredisConnectionFactory,LettuceConnectionFactory等等
5.2.1.2 使用
Spring Data Redis提供了RedisTemplate和StringRedisTemplate两个模板进行数据操作,其中StringRedisTemplate只针对键值都是字符串的数据类型进行操作
常用的数据访问方法:
- opsForValue(): 简单属性
- opsForList(): 操作含有list的数据
- opsForSet(): 操作含有set的数据
- opsForZSet(): 操作含有ZSet(有序的set)的数据
- opsForHash(): 操作含有hash的数据
5.2.1.3 定义Serializer
提供了好几种序列化方式,RedisTemplate默认使用的是JdkSerializationSerializer。其他常用的具体详见代码
(1)自定义Redis序列化器
在使用 Redis 作为缓存时,我们经常需要将 Java 对象序列化为字节数组存储到 Redis 中。默认情况下,Spring Boot 提供了一些默认的序列化器,比如 JdkSerializationRedisSerializer 和 StringRedisSerializer。但有时候我们希望自定义序列化器以满足特定的需求。这时候,我们可以使用 RedisSerializer 接口来自定义实现。
(2)RedisSerializer 接口
RedisSerializer 接口是 Spring Data Redis 提供的序列化接口,定义了将对象序列化为字节数组和将字节数组反序列化为对象的方法。我们可以通过实现这个接口来自定义对象的序列化和反序列化过程。
public interface RedisSerializer<T> {
byte[] serialize(T t) throws SerializationException;
T deserialize(byte[] bytes) throws SerializationException;
}
(3)自定义 RedisSerializer
下面我们通过一个示例来演示如何自定义 RedisSerializer。
假设我们有一个 User 类如下:
public class User {
private String username;
private int age;
// 省略 getter 和 setter 方法
}
我们希望将 User 对象序列化为 JSON 字符串存储到 Redis 中。这时候,我们可以自定义一个 JsonSerializer 实现 RedisSerializer 接口。
public class JsonSerializer<T> implements RedisSerializer<T> {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
try {
return objectMapper.writeValueAsBytes(t);
} catch (JsonProcessingException e) {
throw new SerializationException("Error serializing object to JSON", e);
}
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return objectMapper.readValue(bytes, new TypeReference<T>() {
});
} catch (IOException e) {
throw new SerializationException("Error deserializing object from JSON", e);
}
}
}
在上面的代码中,我们使用 Jackson 库将对象序列化为 JSON 字符串。在 serialize 方法中,我们将对象转换为字节数组,而在 deserialize 方法中,我们将字节数组转换为对象。
(4)使用自定义 RedisSerializer 现在我们可以在 Spring Boot 项目中使用我们自定义的 JsonSerializer 了。假设我们已经配置了一个 RedisTemplate,我们可以这样设置自定义的 RedisSerializer:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, User> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, User> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new JsonSerializer<>());
return template;
}
}
在上面的配置中,我们将 key 的序列化器设置为 StringRedisSerializer,将 value 的序列化器设置为我们自定义的 JsonSerializer。
5.2.2 Spring Boot的支持
Spring Boot对Redis做了自动配置,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration。
Spring Boot配置文件"spring.redis"为前缀的可以配置redis相关参数
5.2.2.1 配置类
RedisProperties配置类,提供了Redis集成Boot的相关配置。
# 核心配置
private int database = 0;
private String url;
private String host = "localhost";
private String username;
private String password;
private int port = 6379;
private boolean ssl;
private Duration timeout;
private Duration connectTimeout;
private String clientName;
private RedisProperties.ClientType clientType;
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(-1L);
private Duration timeBetweenEvictionRuns;
}
# 集群模式下Lettuce客户端配置
private boolean dynamicRefreshSources = true;
private Duration period;
private boolean adaptive;
……
}
5.2.2.2 配置项
# redis 配置项
# 连接URL,配置后会覆盖host、port等配置,eg: redis://user:password@example.com:6379
spring.redis.url=
# 连接地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# 连接工厂使用的数据库索引(0-15),默认为0
spring.redis.database=0
# Redis服务器连接用户
spring.redis.username=
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
# 是否启用SSL支持
spring.redis.ssl=false
# 读取超时
spring.redis.timeout=5000
# 连接超时
spring.redis.connect-timeout=10000
# 在与CLIENT SETNAME的连接上设置的客户端名称
spring.redis.client-name=
# 要使用的客户端类型。默认情况下,根据类路径自动检测
spring.redis.client-type=lettuce
# Redis哨兵属性
# Redis服务器名称
spring.redis.sentinel.master=sentinel-redis
# 哨兵节点,以逗号分隔的“ host:port”对列表
spring.redis.sentinel.nodes=127.0.0.1:7000,127.0.0.1:7001
# 用于使用哨兵进行身份验证的密码
spring.redis.sentinel.password=123456
# 集群属性
# 以逗号分隔的“ host:port”对列表,这表示集群节点的“初始”列表,并且要求至少具有一个条目
spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001
# 在集群中执行命令时要遵循的最大重定向数
spring.redis.cluster.max-redirects=1
# 拓扑动态感应即客户端能够根据 redis cluster 集群的变化,动态改变客户端的节点情况,完成故障转移。
spring.redis.lettuce.cluster.refresh.adaptive=true
# 是否发现和查询所有群集节点以获取群集拓扑。设置为false时,仅将初始种子节点用作拓扑发现的源
spring.redis.lettuce.cluster.refresh.dynamic-refresh-sources=false
# 集群拓扑刷新周期
spring.redis.lettuce.cluster.refresh.period=1000
# 连接池配置
# 连接池池中“空闲”连接的最大数量。使用负值表示无限数量的空闲连接,默认为8
spring.redis.lettuce.pool.max-idle=8
# 中维护的最小空闲连接数,默认为0
spring.redis.lettuce.pool.min-idle=0
# 连接池可以分配的最大连接数。使用负值表示没有限制,默认为8
spring.redis.lettuce.pool.max-active=8
# 当连接池耗尽时,在抛出异常之前,连接分配应阻塞的最长时间。使用负值无限期等待,默认为-1
spring.redis.lettuce.pool.max-wait=-1
# 空闲连接从运行到退出的时间间隔。当为正时,空闲连接回收线程启动,否则不执行空闲连接回收
spring.redis.lettuce.pool.time-between-eviction-runs=
# 宕机超时时间,默认100ms
spring.redis.lettuce.shutdown-timeout=100
5.2.2.3 RedisTemplate
在 Spring Data Redis 中,我们可以直接使用 RedisTemplate 及其相关的类来操作 Redis。虽然 RedisConnection 提供了接受和返回二进制值(字节数组)的低级方法,但 RedisTemplate 负责序列化和连接管理,使用户可以无需处理这些细节。
RedisTemplate 还提供了操作视图(按照 Redis 命令参考进行分组),这些视图提供了丰富、通用的接口,用于针对特定类型或特定键进行操作(通过 KeyBound 接口实现),如下表所示:
5.2.3 SpringBoot使用Redis示例
5.2.3.1 安装Redis
docker安装redis,类似于mongoDB的安装,直接关键步骤就行了
docker pull redis
docker run redis --name redis -p 6379:6379 -d redis-server
windows上redis的安装教程很多,自行查找吧
5.2.3.2 增加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置信息:
# Redis服务器地址
spring.redis.host=192.168.4.219
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
5.2.3.3 主要代码
新建一个user类:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID=1L;
private String id;
private String name;
private Integer age;
public User(){
super();
}
public User(String id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
/**省略setter和getter**/
}
service层,用redisTemplate操作
redis有五种数据类型,redisTemplate对其的操作做了封装,可以在实际中看看他们的用法,这里只是测试常用的方法,每一种都有好多方法,可以自己去测试。
- 结构类型 结构存储的值 结构的读写能力
- String 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增(increment)或者自减(decrement)
- List 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值来查找或者移除元素
- Set 包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素
- Hash 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
- Zset 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素
import com.just.springbootnosql.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 结构类型 结构存储的值 结构的读写能力
* String 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增(increment)或者自减(decrement)
* List 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值来查找或者移除元素
* Set 包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素
* Hash 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
* Zset 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素
*/
@Service
@SuppressWarnings("unchecked")
public class UserServiceImpl implements UserService{
// @Autowired
// private JedisCluster jedisCluster;
@Autowired
private RedisTemplate redisTemplate;
@Override
public String findRedis() {
redisTemplate.opsForValue().set("userName","呵呵哒");
return redisTemplate.opsForValue().get("userName").toString();
}
@Override
public List<User> cacheUsers() {
List<User> users=new ArrayList<>();
User user1=new User("1","阿西吧",12);
User user2=new User("2","阿西九",20);
User user3=new User("3","阿西十",18);
users.add(user1);
users.add(user2);
users.add(user3);
redisTemplate.opsForList().leftPush("user.list",user1);
redisTemplate.opsForList().leftPush("user.list",user2);
redisTemplate.opsForList().leftPush("user.list",user3);
//第二种方式
//redisTemplate.delete("users.list");
//redisTemplate.opsForList().leftPushAll("user.list",users);
redisTemplate.expire("user.list",30,TimeUnit.SECONDS);
return redisTemplate.opsForList().range("user.list",0L,-1L);
}
@Override
public Map<String, User> cacheUserMap() {
User user=new User("4","测试hash",66);
User user1=new User("6","测试hash",67);
User user2=new User("7","测试hash",68);
User user3=new User("8","测试hash",69);
Map<String,User> map=new HashMap<>();
map.put(user1.getId(),user1);
map.put(user2.getId(),user2);
map.put(user3.getId(),user3);
//第一种方式,单个加入
redisTemplate.opsForHash().put("user.map","4",user);
//第二种方式,整个map加入
redisTemplate.opsForHash().putAll("user.map",map);
return redisTemplate.opsForHash().entries("user.map");
}
@Override
public User findUser(String id) {
return (User) redisTemplate.opsForHash().get("user.map",id);
}
@Override
public Set<User> cacheUserSet() {
Set<User> userSet=new HashSet<>();
User user1=new User("10","测试set",20);
User user2=new User("11","测试set",21);
User user3=new User("12","测试set",22);
userSet.add(user1);
userSet.add(user2);
userSet.add(user3);
//只能一个一个放进去。。。
redisTemplate.opsForSet().add("user.set",user1,user2,user3);
return redisTemplate.opsForSet().members("user.set");
}
/**
* zSet:字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定
*/
@Override
public Set<User> cacheUserZSet() {
User user=new User("20","测试zset",13);
User user1=new User("21","测试zset",14);
User user2=new User("22","测试zset",15);
User user3=new User("23","测试zset",16);
redisTemplate.opsForZSet().add("user.zset",user,1.0);
ZSetOperations.TypedTuple<Object> typedTuple1=new DefaultTypedTuple<>(user1,Double.valueOf(user1.getId()));
ZSetOperations.TypedTuple<Object> typedTuple2=new DefaultTypedTuple<>(user2,Double.valueOf(user2.getId()));
ZSetOperations.TypedTuple<Object> typedTuple3=new DefaultTypedTuple<>(user3,Double.valueOf(user3.getId()));
Set<ZSetOperations.TypedTuple<Object>> tuples=new HashSet<>();
tuples.add(typedTuple1);
tuples.add(typedTuple2);
tuples.add(typedTuple3);
redisTemplate.opsForZSet().add("user.zset",tuples);
return redisTemplate.opsForZSet().range("user.zset",0,-1);
}
}
controller层,来测试redis各种数据结构效果
import com.just.springbootnosql.dao.UserDao;
import com.just.springbootnosql.domain.User;
import com.just.springbootnosql.service.UserService;
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;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserDao userDao;
@Autowired
UserService userService;
@RequestMapping("/save")
public User saveUser(){
User user=new User("1","哈哈哈",11);
userDao.save(user);
userDao.stringRedisTemplateDemo();
return user;
}
@GetMapping("/getString")
public String getString(){
return userDao.getString();
}
@GetMapping("/getUser")
public User getUser(){
return userDao.getUser("1");
}
@GetMapping("/findRedis")
public String findRedis(){
return userService.findRedis();
}
@GetMapping("/testList")
public List<User> findList(){
return userService.cacheUsers();
}
@GetMapping("/map")
public Map<String,User> findMap(){
return userService.cacheUserMap();
}
@GetMapping("/getFromMap")
public User findOneFromMap(String id){
return userService.findUser(id);
}
@GetMapping("/set")
public Set<User> findFromSet(){
return userService.cacheUserSet();
}
@GetMapping("/zset")
public Set<User> findFromZSet(){
return userService.cacheUserZSet();
}
}
5.2.3.4 测试结果
postman测试结果即可。