言成言成啊 | Kit Chen's Blog

SpringBoot嵌入自定义Servlet

发布于2022-01-11 19:57:23,更新于2022-01-17 20:57:35,标签:java  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

这个demo是由于上次的实现SpringBoot的jar包嵌入其他项目中,里面涉及到的动态路由通过重定向告诉页面,我的接口路径是啥。这种做法太蠢了,改用自定义的Servlet来实现。

思路

  1. 创建自定义Servlet,作为jar包,作为bean注入springboot项目
  2. 拦截指定的路由下面所有内容。比如拦截/sysParams/*
  3. 拦截后如果是静态资源就读取指定路径下的资源。比如url中访问/sysParams/css/index.css,就提取静态资源/css/index.css,变成返回我指定的路径,如D:/static/css/index.css
  4. 非静态资源,比如接口,就返回指定的crud后的内容

参考

  1. ServletRegistrationBean
  2. Spring boot 添加 Servlet(ServletRegistrationBean)_csdn-JAVA-LIFE的博客-CSDN博客_servletregistrationbean
  3. IntelliJ IDEA创建maven多模块项目 - 请叫我大表哥 - 博客园
  4. Java中return的两种用法_小泽-CSDN博客_java return
  5. sqlite的dialect找不到,降级spring-data-jdbc即可,或者自己实现
  6. IntelliJ IDEA设置code style并自动格式化代码_zw975807058的博客-CSDN博客_idea设置自动格式化
  7. Maven设置JDK版本_weixin_33938733的博客-CSDN博客
  8. 【主要参考】druid/druid-admin at master · alibaba/druid

meethigher/servlet-embedded: 嵌入式Servlet,下面只附上关键逻辑代码

一、实现嵌入jar包

ResourcesServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* 静态资源处理逻辑
*
* @author chenchuancheng
* @since 2021/12/29 15:25
*/
public abstract class ResourcesServlet extends HttpServlet {

/**
* 静态资源路径,如js、css、html、jpg、png等
*/
private final String resourcePath;


protected ResourcesServlet(String resourcePath) {
this.resourcePath = resourcePath;
}

protected String getFilePath(String fileName) {
return this.resourcePath + fileName;
}

/**
* 返回静态资源内容
*
* @param fileName
* @param response
* @throws IOException
*/
protected void returnResourceFile(String fileName, HttpServletResponse response) throws IOException {
String filePath = getFilePath(fileName);
//对静态资源进行处理
byte[] bytes;
//前端字体
if (fileName.endsWith(".ttf") || fileName.endsWith(".eot") || fileName.endsWith(".woff")) {
bytes = Utils.readByteArrayFromResource(filePath);
if (bytes != null) {
response.getOutputStream().write(bytes);
}
return;
}
//图片
if (fileName.endsWith(".jpg") || fileName.endsWith(".png") || fileName.endsWith(".gif")) {
bytes = Utils.readByteArrayFromResource(filePath);
if (bytes != null) {
response.getOutputStream().write(bytes);
}
return;
}
//指定路径下文件存在,则读取内容返回
String content = Utils.readFromResource(filePath);
if (content == null) {
return;
}
if (fileName.endsWith(".html")) {
response.setContentType("text/html;charset=utf-8");
} else if (fileName.endsWith(".css")) {
response.setContentType("text/css;charset=utf-8");
} else if (fileName.endsWith(".js")) {
response.setContentType("text/javascript;charset=utf-8");
} else {
response.setContentType("text/plain;charset=utf-8");
}
response.getWriter().write(content);
}


@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String contextPath = req.getContextPath();
String requestURI = req.getRequestURI();
String servletPath = req.getServletPath();
resp.setCharacterEncoding("utf-8");
if (contextPath == null) {
contextPath = "";
}
String uri = contextPath + servletPath;
String path = requestURI.substring(uri.length());


if (path.endsWith(".ccc")) {
/**
* 这是自定义的接口,对其中进行拦截,然后返回crud
*/
resp.setContentType("application/json;charset=utf-8");
String method = req.getMethod();
if ("POST".equals(method)) {
BufferedReader br = req.getReader();

StringBuffer sb = new StringBuffer();
String line = null;
while ((line = br.readLine()) != null) {
sb.append(line);
}
Map<String, Object> paramMap = (Map<String, Object>) JSON.parse(sb.toString());
resp.getWriter().print(process(path, paramMap));
br.close();
} else {
resp.setContentType("text/plain;charset=utf-8");
resp.getWriter().print("Halo guys, " +
"this request method is unsupported! " +
"Please try POST! ");
}
} else {
if ("/".equals(path)) {
path = "/index.html";
}
/**
* 非接口内容,一律按照静态资源返回
*/
returnResourceFile(path, resp);
}
}

protected abstract String process(String path, Map<String, ? extends Object> paramMap);
}

SysParamsServlet实现静态资源Servlet,添加接口CRUD逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 实现CRUD的逻辑
*
* @author chenchuancheng
* @since 2021/12/31 15:55
*/
public class SysParamsServlet extends ResourcesServlet {

private final SysParamsRepository sysParamsRepository;

/**
* 读取resources目录下的sysparams中的相应url内容
*
* @param sysParamsRepository
*/
public SysParamsServlet(SysParamsRepository sysParamsRepository) {
super("/sysparams");
this.sysParamsRepository = sysParamsRepository;
}

protected SysParamsServlet(String resourcePath, SysParamsRepository sysParamsRepository) {
super(resourcePath);
this.sysParamsRepository = sysParamsRepository;
}

@Override
protected String process(String path, Map<String, ?> paramMap) {
CrudService crud = CrudStrategy.getCrud(path);
if (crud == null) {
return null;
}
return crud.process(sysParamsRepository, paramMap);
}
}

通过策略模式获取对象,避免多重if写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 策略模式获取对象
*
* @author chenchuancheng
* @since 2022/1/5 17:05
*/
public class CrudStrategy {

private static Map<CrudType, CrudService> crudServiceMap;

static {
crudServiceMap = new HashMap<>();
crudServiceMap.put(CrudType.FIND_ALL, new FindAllService());
crudServiceMap.put(CrudType.FIND_BY_NAME, new FindByNameService());
crudServiceMap.put(CrudType.DELETE_BY_NAME, new DeleteByNameService());
crudServiceMap.put(CrudType.INSERT, new InsertService());
crudServiceMap.put(CrudType.UPDATE, new UpdateService());
}

public static CrudService getCrud(String path) {
for (CrudType type : CrudType.values()) {
if (path.endsWith(type.path)) {
return crudServiceMap.get(type);
}
}
return null;
}
}

添加一个接口,可供后续别人的增强实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @author chenchuancheng
* @since 2021/12/31 16:36
*/
public interface SysParamsRepository {
/**
* 查询
*
* @param paramNameLike 模糊匹配paramName
* @return
*/
List<SysParams> findAll(String paramNameLike);

/**
* 查询详情
*
* @param paramName
* @return
*/
SysParams findByName(String paramName);

/**
* 删除
*
* @param paramName
* @return
*/
Integer deleteByName(String paramName);

/**
* 插入
*
* @param sysParams
* @return
*/
Integer insert(SysParams sysParams);

/**
* 更新
*
* @param sysParams
* @return
*/
Integer update(SysParams sysParams);

}

注意

servlet中通过request获取请求体时,需要通过流来拿,直接通过getParameter是拿不到的。

二、实现被嵌入的SpringBoot项目

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server:
port: 9090
ssl:
enabled: false
spring:
datasource:
driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:D:/test.db
type: com.zaxxer.hikari.HikariDataSource
# Hikari 连接池配置
# 最小空闲连接数量
hikari:
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 10
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: MyHikariCP
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
connection-test-query: SELECT 1
jpa:
database-platform: org.sqlite.hibernate.dialect.SQLiteDialect
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true

注入Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class SysParamsServletConfig {


@Bean
public SysParamsRepository sysParamsRepository(DataSource dataSource) {
return new DefaultSysParamsRepository(dataSource);
}

@Bean
public ServletRegistrationBean chenServlet(SysParamsRepository repository) {
ServletRegistrationBean<Servlet> bean = new ServletRegistrationBean<>();
bean.setName("sysParams");
SysParamsServlet servlet = new SysParamsServlet(repository);
bean.setServlet(servlet);
bean.addUrlMappings("/sysParams/*");
return bean;
}
}

三、sqlite使用问题

参考文档

在SpringBoot2.2.7之后,只支持热门的数据库的Dialect,如果想要用Sqlite,需要自己实现Spring接口,然后创建src\main\resources\META-INF\spring.factories指定使用的Bean

自定义Dialect解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 参考文档
* 1. https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.java-config
* 2. https://stackoverflow.com/questions/61851491/spring-data-jdbc-firebird-dialect-not-recognized
*
* @author chenchuancheng
* @since 2021/12/30 15:15
*/
public class CustomDialectResolver implements DialectResolver.JdbcDialectProvider {

private static final Logger log = LoggerFactory.getLogger(CustomDialectResolver.class);

public CustomDialectResolver() {
}

@Override
public Optional<Dialect> getDialect(JdbcOperations operations) {
return Optional.ofNullable(operations.execute((ConnectionCallback<Dialect>) con -> getDialect(con)));
}

@Nullable
private static Dialect getDialect(Connection connection) throws SQLException {
DatabaseMetaData metaData = connection.getMetaData();
String name = metaData.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
if (name.contains("hsql")) {
return HsqlDbDialect.INSTANCE;
} else if (name.contains("h2")) {
return H2Dialect.INSTANCE;
} else if (!name.contains("mysql") && !name.contains("mariadb")) {
if (name.contains("postgresql")) {
return PostgresDialect.INSTANCE;
} else if (name.contains("microsoft")) {
return SqlServerDialect.INSTANCE;
} else if (name.contains("db2")) {
return Db2Dialect.INSTANCE;
} else if (name.contains("oracle")) {
return OracleDialect.INSTANCE;
} else if (name.contains("sqlite")) {
return SqliteDialect.INSTANCE;
} else {
log.error("Halo guys, your spring-data-jdbc not have {}'s dialect, please config it by yourself", name);
return null;
}
} else {
return new MySqlDialect(getIdentifierProcessing(metaData));
}
}

private static IdentifierProcessing getIdentifierProcessing(DatabaseMetaData metaData) throws SQLException {
String quoteString = metaData.getIdentifierQuoteString();
IdentifierProcessing.Quoting quoting = StringUtils.hasText(quoteString) ? new IdentifierProcessing.Quoting(quoteString) : IdentifierProcessing.Quoting.NONE;
IdentifierProcessing.LetterCasing letterCasing;
if (metaData.supportsMixedCaseIdentifiers()) {
letterCasing = IdentifierProcessing.LetterCasing.AS_IS;
} else if (metaData.storesUpperCaseIdentifiers()) {
letterCasing = IdentifierProcessing.LetterCasing.UPPER_CASE;
} else if (metaData.storesLowerCaseIdentifiers()) {
letterCasing = IdentifierProcessing.LetterCasing.LOWER_CASE;
} else {
letterCasing = IdentifierProcessing.LetterCasing.UPPER_CASE;
}

return IdentifierProcessing.create(quoting, letterCasing);
}
}

Sqlite的方言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* SQLiteDialect方言配置
*
* @author chenchuancheng
* @since 2021/12/30 15:51
*/
public class SqliteDialect extends AbstractDialect {
/**
* MySQL defaults for {@link IdentifierProcessing}.
*/
public static final IdentifierProcessing SQLITE_IDENTIFIER_PROCESSING = IdentifierProcessing.create(new IdentifierProcessing.Quoting("`"),
IdentifierProcessing.LetterCasing.LOWER_CASE);

/**
* Singleton instance.
*/
public static final SqliteDialect INSTANCE = new SqliteDialect();

private final IdentifierProcessing identifierProcessing;

protected SqliteDialect() {
this(SQLITE_IDENTIFIER_PROCESSING);
}

/**
* Creates a new {@link MySqlDialect} given {@link IdentifierProcessing}.
*
* @param identifierProcessing must not be null.
* @since 2.0
*/
public SqliteDialect(IdentifierProcessing identifierProcessing) {

Assert.notNull(identifierProcessing, "IdentifierProcessing must not be null");

this.identifierProcessing = identifierProcessing;
}

private static final LimitClause LIMIT_CLAUSE = new LimitClause() {

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LimitClause#getLimit(long)
*/
@Override
public String getLimit(long limit) {
return "LIMIT " + limit;
}

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LimitClause#getOffset(long)
*/
@Override
public String getOffset(long offset) {
// Ugly but the official workaround for offset without limit
// see: https://stackoverflow.com/a/271650
return String.format("LIMIT %d, 18446744073709551615", offset);
}

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LimitClause#getClause(long, long)
*/
@Override
public String getLimitOffset(long limit, long offset) {

// LIMIT {[offset,] row_count}
return String.format("LIMIT %s, %s", offset, limit);
}

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition()
*/
@Override
public Position getClausePosition() {
return Position.AFTER_ORDER_BY;
}
};

private static final LockClause LOCK_CLAUSE = new LockClause() {

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LockClause#getLock(LockOptions)
*/
@Override
public String getLock(LockOptions lockOptions) {
switch (lockOptions.getLockMode()) {

case PESSIMISTIC_WRITE:
return "FOR UPDATE";

case PESSIMISTIC_READ:
return "LOCK IN SHARE MODE";

default:
return "";
}
}

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LockClause#getClausePosition()
*/
@Override
public Position getClausePosition() {
return Position.AFTER_ORDER_BY;
}
};

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#limit()
*/
@Override
public LimitClause limit() {
return LIMIT_CLAUSE;
}

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#lock()
*/
@Override
public LockClause lock() {
return LOCK_CLAUSE;
}

/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#getIdentifierProcessing()
*/
@Override
public IdentifierProcessing getIdentifierProcessing() {
return identifierProcessing;
}
}

spring.factories

1
2
org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=\
top.meethigher.springbootservlet.dialect.CustomDialectResolver
发布:2022-01-11 19:57:23
修改:2022-01-17 20:57:35
链接:https://meethigher.top/blog/2022/servlet-embedded/
标签:java 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏