使用spring session框架来统一管理session,该框架支持jdbc、redis存储,使用非常简单,可以去官网查看文档一步步接入即可,
官网文档如下:https://docs.spring.io/spring-session/docs/current/reference/html5/,

不过,我使用的场景官网没有提供方法给予解决,最后,本人只能重写了它的部分源码,来实现分库管理session,好了,上代码。

pom.xml

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<!--
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-redis</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.2.0</version>
</dependency>

application.properties

# spring-session setting, timeout: 2 years
spring.session.timeout.setting=63072000 # sharding number, CONFIG_SHARDING_NUM 环境变量名
mydb.server.sharding.num=${CONFIG_SHARDING_NUM:4} # global setting dbs parameter
mysql.global.pools.MinimumIdle=1
mysql.global.pools.MaximumPoolSize=20
mysql.global.pools.IdleTimeout=600000
mysql.global.pools.MaxLifetime=1800000 # 这里配置分库信息,这里只是demo,本人配置了4个主库,至于分库分表不在本博客中体现。
# master0
sharding.jdbc.datasource.mainshard0.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.mainshard0.driver-class-name=org.mariadb.jdbc.Driver
sharding.jdbc.datasource.mainshard0.jdbc-url=jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding-utf8&allowMutiQueries=true
sharding.jdbc.datasource.mainshard0.username=xxxx
sharding.jdbc.datasource.mainshard0.password=xxxxxx # master1
sharding.jdbc.datasource.mainshard1.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.mainshard1.driver-class-name=org.mariadb.jdbc.Driver
sharding.jdbc.datasource.mainshard1.jdbc-url=jdbc:mysql://127.0.0.1:3307/demo?useUnicode=true&characterEncoding-utf8&allowMutiQueries=true
sharding.jdbc.datasource.mainshard1.username=xxxx
sharding.jdbc.datasource.mainshard1.password=xxxxxx # master2
sharding.jdbc.datasource.mainshard2.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.mainshard2.driver-class-name=org.mariadb.jdbc.Driver
sharding.jdbc.datasource.mainshard2.jdbc-url=jdbc:mysql://127.0.0.1:3308/demo?useUnicode=true&characterEncoding-utf8&allowMutiQueries=true
sharding.jdbc.datasource.mainshard2.username=xxxx
sharding.jdbc.datasource.mainshard2.password=xxxxxx # master3
sharding.jdbc.datasource.mainshard3.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.mainshard3.driver-class-name=org.mariadb.jdbc.Driver
sharding.jdbc.datasource.mainshard3.jdbc-url=jdbc:mysql://127.0.0.1:3309/demo?useUnicode=true&characterEncoding-utf8&allowMutiQueries=true
sharding.jdbc.datasource.mainshard3.username=xxxx
sharding.jdbc.datasource.mainshard3.password=xxxxxx

重写第一个类(源码文件是JdbcHttpSessionConfiguration.java,可以去官网下载),本人重写如下:

package com.szl.demo.spring.session.common.datasource.sessionConfig;

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.MapSession;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource; /**
* @author Jimmy Shan
* @date 2019-06-25
* @desc 重写jdbc session配置类
*/
@Configuration
@EnableScheduling
public class CustomizedJdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
SchedulingConfigurer { static final String DEFAULT_CLEANUP_CRON = "0 0 0/1 * * *";
private String tableName = JdbcOperationsSessionRepository.DEFAULT_TABLE_NAME;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private LobHandler lobHandler;
private ConversionService springSessionConversionService;
private ConversionService conversionService;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver; //--------------Modify by Jimmy Shan, the date is 2019-06-25 start------//
@Value("${mydb.server.sharding.num}")
private String shardingNum;
@Autowired
private Environment env;
private Map<Integer, JdbcTemplate> myJdbcTemplateMap = new HashMap<>();
private Map<Integer, PlatformTransactionManager> myDataSourceTransactionMap = new HashMap<>();
private DataSource dataSource;
// 时间设置,间隔时间为30秒
private Integer maxInactiveIntervalInSeconds;
//--------------Modify by Jimmy Shan, the date is 2019-06-25 end-------// /**
* @desc 创建数据源,手动创建,为了后面的分库
*/
private DataSource convertDataSource(int num) {
HikariConfig hkConfig = new HikariConfig();
hkConfig.setDriverClassName("org.mariadb,jdbc.Driver");
hkConfig.setMinimumIdle(env.getProperty("mysql.global.pools.MinimumIdle") == null ? 5 : env.getProperty("mysql.global.pools.MinimumIdle")));
hkConfig.setMaximumPoolSize(env.getProperty("mysql.global.pools.MaximumPoolSize") == null ? 20 : env.getProperty("mysql.global.pools.MaximumPoolSize")));
hkConfig.setIdleTimeout(env.getProperty("mysql.global.pools.IdleTimeout") == null ? 600000 : Integer.parseInt(env.getProperty("mysql.global.pools.IdleTimeout")));
hkConfig.setMaxLifetime(env.getProperty("mysql.global.pools.MaxLifetime") == null ? 1800000 : Integer.parseInt(env.getProperty("mysql.global.pools.MaxLifetime")));
hkConfig.setJdbcUrl(env.getProperty("sharding.jdbc.datasource.mainshard" + num + ".jdbc-url"));
hkConfig.setUsername(env.getProperty("sharding.jdbc.datasource.mainshard" + num + ".username"));
hkConfig.setPassword(env.getProperty("sharding.jdbc.datasource.mainshard" + num + ".password"));
HikariDataSource ds = new HikariDataSource(hkConfig);
return ds;
} /**
* @desc 重写了部分逻辑
*/
@Bean
public CustomizedJdbcOperationsSessionRepository sessionRepository() {
for (int i = 0; i < Integer.parseInt(shardingNum); i++) {
DataSource ds = convertDataSource(i);
myJdbcTemplateMap.put(i, new JdbcTemplate(ds));
myDataSourceTransactionMap.put(i, new DataSourceTransactionManager(ds));
} this.dataSource = myJdbcTemplateMap.get(0).getDataSource();
CustomizedJdbcOperationsSessionRepository sessionRepository =
new JdbcOperationsSessionRepository(myJdbcTemplateMap, myDataSourceTransactionMap);
if (StringUtils.hasText(this.tableName)) {
sessionRepository.setTableName(this.tableName);
} this.setMaxInactiveIntervalInSeconds(Integer.parseInt(env.getProperty("spring.session.timeout.setting")));
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
} else if (requiresTemporaryLob(this.dataSource)) {
DefaultLobHandler lobHandler = new DefaultLobHandler();
lobHandler.setCreateTemporaryLob(true);
sessionRepository.setLobHandler(lobHandler);
}
if (this.springSessionConversionService != null) {
sessionRepository.setConversionService(this.springSessionConversionService);
} else if (this.conversionService != null) {
sessionRepository.setConversionService(this.conversionService);
} else {
sessionRepository.setConversionService(createConversionServiceWithBeanClassLoader());
}
return sessionRepository;
} private static boolean requiresTemporaryLob(DataSource dataSource) {
try {
String productName = JdbcUtils.extractDatabaseMetaData(dataSource,
"getDatabaseProductName");
return "Oracle".equalsIgnoreCase(JdbcUtils.commonDatabaseName(productName));
}
catch (MetaDataAccessException ex) {
return false;
}
} public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
} public void setTableName(String tableName) {
this.tableName = tableName;
} public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
} @Autowired(required = false)
@Qualifier("springSessionLobHandler")
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
} @Autowired(required = false)
@Qualifier("springSessionConversionService")
public void setSpringSessionConversionService(ConversionService conversionService) {
this.springSessionConversionService = conversionService;
} @Autowired(required = false)
@Qualifier("conversionService")
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
} @Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
} @Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
} @Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> attributeMap = importMetadata
.getAnnotationAttributes(EnableJdbcHttpSession.class.getName());
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
this.maxInactiveIntervalInSeconds = attributes
.getNumber("maxInactiveIntervalInSeconds");
String tableNameValue = attributes.getString("tableName");
if (StringUtils.hasText(tableNameValue)) {
this.tableName = this.embeddedValueResolver
.resolveStringValue(tableNameValue);
}
String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
} @Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> sessionRepository().cleanUpExpiredSessions(),
this.cleanupCron);
} private GenericConversionService createConversionServiceWithBeanClassLoader() {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(Object.class, byte[].class,
new SerializingConverter());
conversionService.addConverter(byte[].class, Object.class,
new DeserializingConverter(this.classLoader));
return conversionService;
}
}

重写第二个类(源码文件是JdbcOperationsSessionRepository.java,可以去官网下载),本人重写如下:

package com.szl.demo.spring.session.common.datasource.sessionConfig;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils; /**
* @author Jimmy Shan
* @date 2019-06-25
* @desc 重写jdbc session类
*/
public class CustomizedJdbcOperationsSessionRepository implements
FindByIndexNameSessionRepository<JdbcOperationsSessionRepository.JdbcSession> { public static final String DEFAULT_TABLE_NAME = "SPRING_SESSION";
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
private static final String CREATE_SESSION_QUERY =
"INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String CREATE_SESSION_ATTRIBUTE_QUERY =
"INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " +
"SELECT PRIMARY_ID, ?, ? " +
"FROM %TABLE_NAME% " +
"WHERE SESSION_ID = ?";
private static final String GET_SESSION_QUERY =
"SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " +
"FROM %TABLE_NAME% S " +
"LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " +
"WHERE S.SESSION_ID = ?";
private static final String UPDATE_SESSION_QUERY =
"UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? " +
"WHERE PRIMARY_ID = ?";
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY =
"UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? " +
"WHERE SESSION_PRIMARY_ID = ? " +
"AND ATTRIBUTE_NAME = ?";
private static final String DELETE_SESSION_ATTRIBUTE_QUERY =
"DELETE FROM %TABLE_NAME%_ATTRIBUTES " +
"WHERE SESSION_PRIMARY_ID = ? " +
"AND ATTRIBUTE_NAME = ?";
private static final String DELETE_SESSION_QUERY =
"DELETE FROM %TABLE_NAME% " +
"WHERE SESSION_ID = ?";
private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY =
"SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " +
"FROM %TABLE_NAME% S " +
"LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " +
"WHERE S.PRINCIPAL_NAME = ?";
private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY =
"DELETE FROM %TABLE_NAME% " +
"WHERE EXPIRY_TIME < ?"; private static final Log logger = LogFactory.getLog(JdbcOperationsSessionRepository.class);
private static final PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
private final ResultSetExtractor<List<JdbcSession>> extractor = new SessionResultSetExtractor();
private String tableName = DEFAULT_TABLE_NAME;
private String createSessionQuery;
private String createSessionAttributeQuery;
private String getSessionQuery;
private String updateSessionQuery;
private String updateSessionAttributeQuery;
private String deleteSessionAttributeQuery;
private String deleteSessionQuery;
private String listSessionsByPrincipalNameQuery;
private String deleteSessionsByExpiryTimeQuery;
private Integer defaultMaxInactiveInterval;
private ConversionService conversionService;
private LobHandler lobHandler = new DefaultLobHandler(); //--------------Modify by Jimmy Shan, the date is 2019-06-25 start--------------//
private Map<Integer, JdbcOperations> jdbcOperationMaps = new Hash<>();
private Map<Integer, TransactionOperations> transOperationMaps = new Hash<>();
@Value("{mydb.server.sharding.num}")
private String shardingNum;
//--------------Modify by Jimmy Shan, the date is 2019-06-25 end----------------// /**
* @desc 重写这个方法
*/
public CustomizedJdbcOperationsSessionRepository(Map<Integer, JdbcTemplate> myJdbcTemplateMap,
Map<Integer, PlatformTransactionManager> myDataSourceTransactionMap) {
if (myJdbcTemplateMap == null || myJdbcTemplateMap.isEmpty()) {
Assert.notNull(myJdbcTemplateMap, "myJdbcTemplateMap must not be null");
}
if (myDataSourceTransactionMap == null || myDataSourceTransactionMap.isEmpty()) {
Assert.notNull(myDataSourceTransactionMap, "myDataSourceTransactionMap must not be null");
}
jdbcOperationMaps.putAll(myJdbcTemplateMap);
this.conversionService = createDefaultConversionService();
prepareQueries();
Set<Integer> setKey = myDataSourceTransactionMap.keySet();
for (Iterator ir = setKey.iterator(); ir.hasNext(); ) {
Integer key = (Integer) ir.next();
TransactionOperations transOperations = createTransactionTemplate(myDataSourceTransactionMap.get(key));
transOperationMaps.put(key, transOperations);
}
} public void setTableName(String tableName) {
Assert.hasText(tableName, "Table name must not be empty");
this.tableName = tableName.trim();
prepareQueries();
} public void setCreateSessionQuery(String createSessionQuery) {
Assert.hasText(createSessionQuery, "Query must not be empty");
this.createSessionQuery = createSessionQuery;
} public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
this.createSessionAttributeQuery = createSessionAttributeQuery;
} public void setGetSessionQuery(String getSessionQuery) {
Assert.hasText(getSessionQuery, "Query must not be empty");
this.getSessionQuery = getSessionQuery;
} public void setUpdateSessionQuery(String updateSessionQuery) {
Assert.hasText(updateSessionQuery, "Query must not be empty");
this.updateSessionQuery = updateSessionQuery;
} public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
} public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
} public void setDeleteSessionQuery(String deleteSessionQuery) {
Assert.hasText(deleteSessionQuery, "Query must not be empty");
this.deleteSessionQuery = deleteSessionQuery;
} public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
} public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
} public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
} public void setLobHandler(LobHandler lobHandler) {
Assert.notNull(lobHandler, "LobHandler must not be null");
this.lobHandler = lobHandler;
} public void setConversionService(ConversionService conversionService) {
Assert.notNull(conversionService, "conversionService must not be null");
this.conversionService = conversionService;
} @Override
public JdbcSession createSession() {
JdbcSession session = new JdbcSession();
if (this.defaultMaxInactiveInterval != null) {
session.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return session;
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
@Override
public void save(final JdbcSession session) {
String sessionId = session.getId();
int hashCode = Math.abs(sessionId.hashCode());
int mod = hashCode % Integer.parseInt(shardingNum);
if (session.isNew()) {
this.transOperationMaps.get(mod).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
CustomizedJdbcOperationsSessionRepository.this.jdbcOperationMaps.get(mod).update(
CustomizedJdbcOperationsSessionRepository.this.createSessionQuery,
(ps) -> {
ps.setString(1, session.primaryKey);
ps.setString(2, session.getId());
ps.setLong(3, session.getCreationTime().toEpochMilli());
ps.setLong(4, session.getLastAccessedTime().toEpochMilli());
ps.setInt(5, (int) session.getMaxInactiveInterval().getSeconds());
ps.setLong(6, session.getExpiryTime().toEpochMilli());
ps.setString(7, session.getPrincipalName());
});
Set<String> attributeNames = session.getAttributeNames();
if (!attributeNames.isEmpty()) {
insertSessionAttributes(session, new ArrayList<>(attributeNames));
}
}
});
} else {
this.transOperationMaps.get(mod).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (session.isChanged()) {
CustomizedJdbcOperationsSessionRepository.this.jdbcOperationMaps.get(mod).update(
CustomizedJdbcOperationsSessionRepository.this.updateSessionQuery,
(ps) -> {
ps.setString(1, session.getId());
ps.setLong(2, session.getLastAccessedTime().toEpochMilli());
ps.setInt(3, (int) session.getMaxInactiveInterval().getSeconds());
ps.setLong(4, session.getExpiryTime().toEpochMilli());
ps.setString(5, session.getPrincipalName());
ps.setString(6, session.primaryKey);
});
}
List<String> addedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.ADDED)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!addedAttributeNames.isEmpty()) {
insertSessionAttributes(session, addedAttributeNames);
}
List<String> updatedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!updatedAttributeNames.isEmpty()) {
updateSessionAttributes(session, updatedAttributeNames);
}
List<String> removedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!removedAttributeNames.isEmpty()) {
deleteSessionAttributes(session, removedAttributeNames);
}
}
});
}
session.clearChangeFlags();
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
@Override
public JdbcSession findById(final String id) {
String sessionId = id;
int hashCode = Math.abs(sessionId.hashCode());
int mod = hashCode % Integer.parseInt(shardingNum);
final JdbcSession session = this.transOperationMaps.get(mod).execute((status) -> {
List<JdbcSession> sessions = CustomizedJdbcOperationsSessionRepository
.this.jdbcOperationMaps.get(mod).query(
CustomizedJdbcOperationsSessionRepository.this.getSessionQuery,
(ps) -> ps.setString(1, id),
CustomizedJdbcOperationsSessionRepository.this.extractor
);
if (sessions.isEmpty()) {
return null;
}
return sessions.get(0);
}); if (session != null) {
if (session.isExpired()) {
deleteById(id);
} else {
return session;
}
}
return null;
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
@Override
public void deleteById(final String id) {
String sessionId = id;
int hashCode = Math.abs(sessionId.hashCode());
int mod = hashCode % Integer.parseInt(shardingNum);
this.transOperationMaps.get(mod).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
CustomizedJdbcOperationsSessionRepository.this.jdbcOperationMaps.get(mod).update(
CustomizedJdbcOperationsSessionRepository.this.deleteSessionQuery, id);
}
});
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
@Override
public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName, final String indexValue) {
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
List<JdbcSession> sessions = new ArrayList<>();
Set<Integer> setKey = transOperationMaps.keySet();
for (Iterator ir = setKey.iterator(); ir.hasNext(); ) {
Integer key = (Integer) ir.next();
TransactionOperations transOperation = (TransactionOperations) transOperationMaps.get(key);
List<JdbcSession> tempSession = transOperation.execute(status) ->
CustomizedJdbcOperationsSessionRepository.this.jdbcOperationMaps.get(key).query(
CustomizedJdbcOperationsSessionRepository.this.listSessionsByPrincipalNameQuery,
(ps) -> ps.setString(1, indexValue),
CustomizedJdbcOperationsSessionRepository.this.extractor));
if (tempSession != null && !tempSession.isEmpty()) {
sessions.addAll(tempSession);
}
}
Map<String, JdbcSession> sessionMap = new HashMap<>(sessions.size());
for (JdbcSession session : sessions) {
sessionMap.put(session.getId(), session);
} return sessionMap;
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
private void insertSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
String sessionId = session.getId();
int hashCode = Math.abs(sessionId.hashCode());
int mod = hashCode % Integer.parseInt(shardingNum);
if (attributeNames.size() > 1) {
this.jdbcOperationMaps.get(mod).batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, attributeName);
setObjectAsBlob(ps, 2, session.getAttribute(attributeName));
ps.setString(3, session.getId());
}
@Override
public int getBatchSize() {
return attributeNames.size();
} });
} else {
this.jdbcOperationMaps.get(mod).update(this.createSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, attributeName);
setObjectAsBlob(ps, 2, session.getAttribute(attributeName));
ps.setString(3, session.getId());
});
}
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
private void updateSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
String sessionId = session.getId();
int hashCode = Math.abs(sessionId.hashCode());
int mod = hashCode % Integer.parseInt(shardingNum);
if (attributeNames.size() > 1) {
this.jdbcOperationMaps.get(mod).batchUpdate(this.updateSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
setObjectAsBlob(ps, 1, session.getAttribute(attributeName));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
} else {
this.jdbcOperationMaps.get(mod).update(this.updateSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
setObjectAsBlob(ps, 1, session.getAttribute(attributeName));
ps.setString(2, session.primaryKey);
ps.setString(3, attributeName);
});
}
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
private void deleteSessionAttributes(JdbcSession session, List<String> attributeNames) {
Assert.notEmpty(attributeNames, "attributeNames must not be null or empty");
String sessionId = session.getId();
int hashCode = Math.abs(sessionId.hashCode());
int mod = hashCode % Integer.parseInt(shardingNum);
if (attributeNames.size() > 1) {
this.jdbcOperationMaps.get(mod).batchUpdate(this.deleteSessionAttributeQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String attributeName = attributeNames.get(i);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
}
@Override
public int getBatchSize() {
return attributeNames.size();
}
});
} else {
this.jdbcOperationMaps.get(mod).update(this.deleteSessionAttributeQuery, (ps) -> {
String attributeName = attributeNames.get(0);
ps.setString(1, session.primaryKey);
ps.setString(2, attributeName);
});
}
} /**
* @desc Modify by Jimmy Shan, the date is 2019-06-25
* Implementing Routing Function
*/
public void cleanUpExpiredSessions() {
Set<Integer> setKey = transOperationMaps.keySet();
for (Iterator ir = setKey.iterator(); ir.hasNext(); ) {
Integer key = (Integer) ir.next();
TransactionOperations transOperation = (TransactionOperations) transOperationMaps.get(key);
Integer deletedCount = transOperation.execute((status) ->
CustomizedJdbcOperationsSessionRepository.this.jdbcOperationMaps.get(key).update(
CustomizedJdbcOperationsSessionRepository.this.deleteSessionsByExpiryTimeQuery,
System.currentTimeMillis()));
if (logger.isDebugEnabled()) {
logger.debug("Cleaned up " + deletedCount + " expired sessions");
}
}
} private static TransactionTemplate createTransactionTemplate(
PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(
transactionManager);
transactionTemplate.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.afterPropertiesSet();
return transactionTemplate;
} private static GenericConversionService createDefaultConversionService() {
GenericConversionService converter = new GenericConversionService();
converter.addConverter(Object.class, byte[].class,
new SerializingConverter());
converter.addConverter(byte[].class, Object.class,
new DeserializingConverter());
return converter;
} private String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
} private void prepareQueries() {
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
this.listSessionsByPrincipalNameQuery =
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
this.deleteSessionsByExpiryTimeQuery =
getQuery(DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY);
} private void setObjectAsBlob(PreparedStatement ps, int paramIndex, Object object)
throws SQLException {
byte[] bytes = (byte[]) this.conversionService.convert(object,
TypeDescriptor.valueOf(Object.class),
TypeDescriptor.valueOf(byte[].class));
this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex, bytes);
} private Object getBlobAsObject(ResultSet rs, String columnName) throws SQLException {
byte[] bytes = this.lobHandler.getBlobAsBytes(rs, columnName);
return this.conversionService.convert(bytes, TypeDescriptor.valueOf(byte[].class),
TypeDescriptor.valueOf(Object.class));
} private enum DeltaValue {
ADDED, UPDATED, REMOVED
} private static <T> Supplier<T> value(T value) {
return (value != null) ? () -> value : null;
} private static <T> Supplier<T> lazily(Supplier<T> supplier) {
Supplier<T> lazySupplier = new Supplier<T>() {
private T value;
@Override
public T get() {
if (this.value == null) {
this.value = supplier.get();
}
return this.value;
}
}; return (supplier != null) ? lazySupplier : null;
} final class JdbcSession implements Session { private final Session delegate; private final String primaryKey; private boolean isNew; private boolean changed; private Map<String, DeltaValue> delta = new HashMap<>(); JdbcSession() {
this.delegate = new MapSession();
this.isNew = true;
this.primaryKey = UUID.randomUUID().toString();
} JdbcSession(String primaryKey, Session delegate) {
Assert.notNull(primaryKey, "primaryKey cannot be null");
Assert.notNull(delegate, "Session cannot be null");
this.primaryKey = primaryKey;
this.delegate = delegate;
} boolean isNew() {
return this.isNew;
} boolean isChanged() {
return this.changed;
} Map<String, DeltaValue> getDelta() {
return this.delta;
} void clearChangeFlags() {
this.isNew = false;
this.changed = false;
this.delta.clear();
} String getPrincipalName() {
return PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
} Instant getExpiryTime() {
return getLastAccessedTime().plus(getMaxInactiveInterval());
} @Override
public String getId() {
return this.delegate.getId();
} @Override
public String changeSessionId() {
this.changed = true;
return this.delegate.changeSessionId();
} @Override
public <T> T getAttribute(String attributeName) {
Supplier<T> supplier = this.delegate.getAttribute(attributeName);
return (supplier != null) ? supplier.get() : null;
} @Override
public Set<String> getAttributeNames() {
return this.delegate.getAttributeNames();
} @Override
public void setAttribute(String attributeName, Object attributeValue) {
boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
boolean attributeRemoved = (attributeValue == null);
if (!attributeExists && attributeRemoved) {
return;
}
if (attributeExists) {
if (attributeRemoved) {
this.delta.merge(attributeName, DeltaValue.REMOVED, (oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null
: deltaValue);
}
else {
this.delta.merge(attributeName, DeltaValue.UPDATED,
(oldDeltaValue,
deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
? oldDeltaValue
: deltaValue);
}
}
else {
this.delta.merge(attributeName, DeltaValue.ADDED,
(oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
? oldDeltaValue
: DeltaValue.UPDATED);
}
this.delegate.setAttribute(attributeName, value(attributeValue));
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) ||
SPRING_SECURITY_CONTEXT.equals(attributeName)) {
this.changed = true;
}
} @Override
public void removeAttribute(String attributeName) {
setAttribute(attributeName, null);
} @Override
public Instant getCreationTime() {
return this.delegate.getCreationTime();
} @Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.changed = true;
} @Override
public Instant getLastAccessedTime() {
return this.delegate.getLastAccessedTime();
} @Override
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.changed = true;
} @Override
public Duration getMaxInactiveInterval() {
return this.delegate.getMaxInactiveInterval();
} @Override
public boolean isExpired() {
return this.delegate.isExpired();
} } static class PrincipalNameResolver { private SpelExpressionParser parser = new SpelExpressionParser(); public String resolvePrincipal(Session session) {
String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
if (principalName != null) {
return principalName;
}
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
Expression expression = this.parser
.parseExpression("authentication?.name");
return expression.getValue(authentication, String.class);
}
return null;
} } private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> { @Override
public List<JdbcSession> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<JdbcSession> sessions = new ArrayList<>();
while (rs.next()) {
String id = rs.getString("SESSION_ID");
JdbcSession session;
if (sessions.size() > 0 && getLast(sessions).getId().equals(id)) {
session = getLast(sessions);
}
else {
MapSession delegate = new MapSession(id);
String primaryKey = rs.getString("PRIMARY_ID");
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
session = new JdbcSession(primaryKey, delegate);
}
String attributeName = rs.getString("ATTRIBUTE_NAME");
if (attributeName != null) {
Object attributeValue = getBlobAsObject(rs, "ATTRIBUTE_BYTES");
session.delegate.setAttribute(attributeName, lazily(() -> attributeValue));
}
sessions.add(session);
}
return sessions;
} private JdbcSession getLast(List<JdbcSession> sessions) {
return sessions.get(sessions.size() - 1);
} } }

以上工作都完成后,让我们来看看如何使用。

代码如下:

package com.szl.demo.spring.session.controller;

import java.io.PrintWriter;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping; @Controller
public class TestSessionController { @RequestMapping("/testGetSession")
public void testGetSession(HttpServletRequest request, HttpServletResponse response, ModelMap model) {
PrintWriter out = null;
try {
out = response.getWriter(); HttpSession session = request.getSession();
String content = (String) session.getAttribute("userId");
System.out.println("session content is : " + content); out.println("session content is : " + content);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
} @RequestMapping("/testSetSession")
public void testSetSession(HttpServletRequest request, HttpServletResponse response, ModelMap model) {
PrintWriter out = null;
try {
out = response.getWriter();
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().replaceAll("-", ""); HttpSession session = request.getSession();
String content = (String) session.getAttribute("userId");
System.out.println("old session content is : " + content); // 设置session内容
session.setAttribute("userId", uid);
out.println("new session content is : " + uid);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
} }

是不是很简单,和平时使用session方式一样。

下面是建表脚本(mysql5.6)

CREATE TABLE SPRING_SESSION (
PRIMARY_ID CHAR(36) NOT NULL,
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
EXPIRY_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME); CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_PRIMARY_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES BLOB NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

好了,备忘录记录到此,还是那句,
仅供有需要的朋友参考,也欢迎转载,但请注明原著,谢谢。

springboot2.1.3+spring-session2.1.4分库处理的更多相关文章

  1. spring整合sharding-jdbc实现分库分表

    1.创建两个库,每个库创建两个分表t_order_1,t_order_2 DROP TABLE IF EXISTS `t_order_1`; CREATE TABLE `t_order_1` ( `i ...

  2. 【Java EE 学习 77 下】【数据采集系统第九天】【使用spring实现答案水平分库】【未解决问题:分库查询问题】

    之前说过,如果一个数据库中要存储的数据量整体比较小,但是其中一个表存储的数据比较多,比如日志表,这时候就要考虑分表存储了:但是如果一个数据库整体存储的容量就比较大,该怎么办呢?这时候就需要考虑分库了, ...

  3. sharding-jdbc集成spring+mybatis分表分库

    maven: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http ...

  4. 在SpringBoot2.0及Spring 5.0 WebMvcConfigurerAdapter已被废弃,目前找到解决方案就有两种

    1 直接实现WebMvcConfigurer (官方推荐) 例如: @Configuration public class WebMvcConfg implements WebMvcConfigure ...

  5. SpringBoot2.x开发案例之整合Quartz任务管理系统

    基于spring-boot 2.x + quartz 的CRUD任务管理系统,适用于中小项目. 基于spring-boot +quartz 的CRUD任务管理系统: https://gitee.com ...

  6. 直接使用security.basic.path无效|——springboot2.0以上的security的配置

    问题 springcloud 版本 为 Finchley.RELEASEspringboot 版本为 2.0.3.RELEASE 现在有需求,/swagger-ui.html 页面需要添加登录认证,但 ...

  7. spring cloud: 升级到spring boot 2.x/Finchley.RELEASE遇到的坑

    spring boot2.x已经出来好一阵了,而且spring cloud 的最新Release版本Finchley.RELEASE,默认集成的就是spring boot 2.x,这几天将一个旧项目尝 ...

  8. Spring Boot 2.2 正式发布,大幅性能提升 + Java 13 支持

    之前 Spring Boot 2.2没能按时发布,是由于 Spring Framework 5.2 的发布受阻而推迟.这次随着 Spring Framework 5.2.0 成功发布之后,Spring ...

  9. springboot学习入门简易版八---springboot2.0多环境配置、整合mybatis mysql8+(19-20)

    2.11 SpringBoot多环境配置(19)  application.properties中配置 Spring.profiles.active=prd 配置环境: Application-dev ...

  10. spring boot 集成 websocket 实现消息主动推送

    spring boot 集成 websocket 实现消息主动 前言 http协议是无状态协议,每次请求都不知道前面发生了什么,而且只可以由浏览器端请求服务器端,而不能由服务器去主动通知浏览器端,是单 ...

随机推荐

  1. Python3基础 函数 无return、return 空或None 的效果相同

             Python : 3.7.3          OS : Ubuntu 18.04.2 LTS         IDE : pycharm-community-2019.1.3    ...

  2. sysfile20191122

    ass_s_ccp_ft:-108; ass_s_ccp_all:-108; ass_tag_ft:-105; ass_tag_all:-105; rept_port:9000; Q_value:0. ...

  3. 为CentOS安装python3

    摘自:https://www.jianshu.com/p/7c2b62c37223 1. 安装依赖 不要复制往下看 yum install openssl-devel bzip2-devel expa ...

  4. Shell获取字符串长度的多种方法总结

    摘自:https://www.jb51.net/article/121290.htm 前言 我们在日常工作中,对于求字符串操作在shell脚本中很常用,实现的方法有很多种,下面就来给大家归纳.汇总了求 ...

  5. 【翻译】Flink Table Api & SQL —Streaming 概念 —— 时态表

    本文翻译自官网: Temporal Tables https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/table/strea ...

  6. [LeetCode] 227. Basic Calculator II 基本计算器 II

    Implement a basic calculator to evaluate a simple expression string. The expression string contains ...

  7. VisualStudio开发UE4工程设置

    转自:http://wangjie.rocks/2016/06/24/ue4-vs-setup/ 推荐插件 Visual Assist X C++ 代码高亮 UnrealVS Extension UE ...

  8. POJ 2106 Boolean Expressions

    总时间限制: 1000ms  内存限制: 65536kB 描述 The objective of the program you are going to produce is to evaluate ...

  9. SpringBoot系列教程JPA之基础环境搭建

    JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Spring Data JPA是在 Hibernat ...

  10. Mac OS备份迁移iBooks图书操作方法

    前段时间换电脑,需要把原本电脑上的一些文件备份.迁移出来,包括iBooks中的电子书. 理论上,苹果体系中通过icloud账号可以把通讯录.备忘录等东西同步过去,但查了一下发现图书支持有限,而且我的e ...