springboot2.1.3+spring-session2.1.4分库处理
使用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分库处理的更多相关文章
- spring整合sharding-jdbc实现分库分表
1.创建两个库,每个库创建两个分表t_order_1,t_order_2 DROP TABLE IF EXISTS `t_order_1`; CREATE TABLE `t_order_1` ( `i ...
- 【Java EE 学习 77 下】【数据采集系统第九天】【使用spring实现答案水平分库】【未解决问题:分库查询问题】
之前说过,如果一个数据库中要存储的数据量整体比较小,但是其中一个表存储的数据比较多,比如日志表,这时候就要考虑分表存储了:但是如果一个数据库整体存储的容量就比较大,该怎么办呢?这时候就需要考虑分库了, ...
- sharding-jdbc集成spring+mybatis分表分库
maven: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http ...
- 在SpringBoot2.0及Spring 5.0 WebMvcConfigurerAdapter已被废弃,目前找到解决方案就有两种
1 直接实现WebMvcConfigurer (官方推荐) 例如: @Configuration public class WebMvcConfg implements WebMvcConfigure ...
- SpringBoot2.x开发案例之整合Quartz任务管理系统
基于spring-boot 2.x + quartz 的CRUD任务管理系统,适用于中小项目. 基于spring-boot +quartz 的CRUD任务管理系统: https://gitee.com ...
- 直接使用security.basic.path无效|——springboot2.0以上的security的配置
问题 springcloud 版本 为 Finchley.RELEASEspringboot 版本为 2.0.3.RELEASE 现在有需求,/swagger-ui.html 页面需要添加登录认证,但 ...
- spring cloud: 升级到spring boot 2.x/Finchley.RELEASE遇到的坑
spring boot2.x已经出来好一阵了,而且spring cloud 的最新Release版本Finchley.RELEASE,默认集成的就是spring boot 2.x,这几天将一个旧项目尝 ...
- Spring Boot 2.2 正式发布,大幅性能提升 + Java 13 支持
之前 Spring Boot 2.2没能按时发布,是由于 Spring Framework 5.2 的发布受阻而推迟.这次随着 Spring Framework 5.2.0 成功发布之后,Spring ...
- springboot学习入门简易版八---springboot2.0多环境配置、整合mybatis mysql8+(19-20)
2.11 SpringBoot多环境配置(19) application.properties中配置 Spring.profiles.active=prd 配置环境: Application-dev ...
- spring boot 集成 websocket 实现消息主动推送
spring boot 集成 websocket 实现消息主动 前言 http协议是无状态协议,每次请求都不知道前面发生了什么,而且只可以由浏览器端请求服务器端,而不能由服务器去主动通知浏览器端,是单 ...
随机推荐
- Spark布隆过滤器(bloomFilter)
数据过滤在很多场景都会应用到,特别是在大数据环境下.在数据量很大的场景实现过滤或者全局去重,需要存储的数据量和计算代价是非常庞大的.很多小伙伴第一念头肯定会想到布隆过滤器,有一定的精度损失,但是存储性 ...
- Python的dict字典结构操作方法学习笔记
Python的dict字典结构操作方法学习笔记 这篇文章主要介绍了Python的dict字典结构操作方法学习笔记本,字典的操作是Python入门学习中的基础知识,需要的朋友可以参考下 一.字典的基本方 ...
- jsp 记录
前后端开发好久后,一直没怎么用前端开发了.最近任务比较急,又开始写jsp页面了... 1)jquery.validate.min.js 用法总结 https://www.cnblogs.com/x ...
- ServletRequest与HttpServletRequest
ServletRequest 解析:代表来自客户端的请求.当Servlet容器接收到客户端的要求访问特定Servlet的请求时,容器先解析客户端的原始请求数据,把它包装成一个ServletReques ...
- shell request failed on channel 0
今天普通用户ssh 登录提示shell request failed on channel 0 然后就退出了 幸亏root 用户没有被禁用,在root下 su - 普通 切换提示资源不足 解决方法 ...
- Python 安装 MySQL-python ImportError: No module named 'ConfigParser'
系统: CentOS-6.4-x86_64 Python : Python 3.4.5 和 Python 3.5.2 安装 MySQL-python ,结果出错: ImportError: No mo ...
- GIT 基础 &本地服务器搭建
Git是一款免费.开源的分布式版本控制系统.众所周知的Github便是基于Git的开源代码库以及版本控制系统,由于其远程托管服务仅对开源免费,所以搭建本地Git服务器也是个较好的选择,本文将对此进行详 ...
- LeetCode 1047. 删除字符串中的所有相邻重复项(Remove All Adjacent Duplicates In String)
1047. 删除字符串中的所有相邻重复项 1047. Remove All Adjacent Duplicates In String 题目描述 LeetCode1047. Remove All Ad ...
- SpringBoot(1)
SpringBoot 8/2 CRUD 发送put请求修改数据有三个步骤: SpringMVC中配置HiddenHttpMethodFilter 页面上创建一个post请求(form标签只能写get和 ...
- 使用ImagesPipeline时候报错为:ModuleNotFoundError: No module named 'scrapy.contrib'
刚开始我是这样写的: 报错为: 哈哈,经过查阅资料,其实他是存在的,接下来修改如下: 经过运行结果如下: 问题解决,哈哈哈,搞定!!!!!!!!!!!!