0%

文章字数:34,阅读全文大约需要1分钟

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
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

/**
* @author colin.cheng
* @date 2021-11-02 18:47
* @since 1.0.0
*/
public class RestTemplateUtil {

public static RestTemplate getRestTemplate()
throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory csf =
new SSLConnectionSocketFactory(sslContext, new String[] {"TLSv1.2"}, null, NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
}

文章字数:455,阅读全文大约需要1分钟

SpringBean从创建、初始化操作、就绪、销毁。组成了一个完整的生命周期。

大致流程

普通的容器和spring的耦合很低,使用过程中不用知道spring的存在,将容器从spring替换成其他容器也可以正常运行。然而部分功能需要用到容器本身的功能和资源,于是spring提供了spring aware系列可以在初始化容器时通过接口的形式提供。

  1. 实例化
  2. 填充属性
  3. 调用BeanNameAwaresetBeanName方法
    如果Bean实现此接口,调用setBeanName方法并传入Beanid
  4. 调用BeanFactoryAwaresetBeanFactory方法
    如果Bean实现此接口,调用方法传入Spring工厂本身
  5. 调用ApplicationContextAwaresetApplicationContext方法
    如果Bean实现此接口,调用方法传入ApplicationContext,即spring容器上下文
  6. 调用BeanPostProcesspostProssBeforeInitialization方法
    如果Bean实现此接口,调用方法传入(Object bean,String beanName)
    这个方法是在初始化结束时调用,所以通常应用于内存或缓存技术

初始化结束

  1. 如果beanSpring配置文件中配置了init-method属性,会自动调用配置的初始化方法

  2. 调用InitializingBeanafterPropertiesSet方法

  3. 调用BeanPostProcesspostProcessAfterInitialization方法
    如果Bean实现此接口,调用方法传入(Object bean,String beanName)
    初始化之后调用的方法

  4. Bean准备就绪


销毁

  1. 调用DispostbleBeandestory方法
    在清理阶段,实现此方法的Bean会调用destory方法销毁
  2. 调用指定的销毁方法
    如果在配置文件指定destory-method方法,会自动销毁

文章字数:488,阅读全文大约需要1分钟

springBoot2.x提供了简便的websocket实现方式。但是如果需要大并发,还是需要使用netty

依赖

1
2
3
4
<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 开启WebSocket支持
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

服务端

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
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import lombok.extern.slf4j.Slf4j;


@ServerEndpoint("/websocket/{sid}")
@Component
public class WebSocketServer {

static Log log=LogFactory.get(WebSocketServer.class);
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

//接收sid
private String sid="";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session,@PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
log.info("有新窗口开始监听:"+sid+",当前在线人数为" + getOnlineCount());
this.sid=sid;
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("websocket IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口"+sid+"的信息:"+message);
//群发消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}


/**
* 群发自定义消息
* */
public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException {
log.info("推送消息到窗口"+sid+",推送内容:"+message);
for (WebSocketServer item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为null则全部推送
if(sid==null) {
item.sendMessage(message);
}else if(item.sid.equals(sid)){
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}

public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}

客户端

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
var socket;  
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
//等同于socket = new WebSocket("ws://localhost:8083/checkcentersys/websocket/20");
socket = new WebSocket("${basePath}websocket/${cid}".replace("http","ws"));
//打开事件
socket.onopen = function() {
console.log("Socket 已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
//此时可以尝试刷新页面
}
//离开页面时,关闭socket
//jquery1.8中已经被废弃,3.0中已经移除
// $(window).unload(function(){
// socket.close();
//});
}

文章字数:245,阅读全文大约需要1分钟

springWebSocket还有一种形式,除了事件绑定的形式还有基于观察者模式(发布-订阅)的绑定topic形式

环境

springBoot的websocket的starter包

1
2
3
4
<dependency>    
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
//添加服务端点,接收服务连接
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 添加一个 /socket 的端点
registry.addEndpoint("/socket").withSockJS();//开启SockJS支持
}

@Override
// 定义消息代理(连接请求的规范)
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端订阅地址前缀(服务端发送)
registry.enableSimpleBroker("/topic");
// 客户端发布地址前缀(服务端接收)
registry.setApplicationDestinationPrefixes("/app");
}
}

端点连接还可以配置允许跨域

1
2
3
4
5
6
7
8
9
registry.addEndpoint("/socket").addInterceptors(new HandshakeInterceptor(){
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes){
response.getHeaders().add("Access-Control-Allow-Origin", "*");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}}).setAllowedOrigins("*").withSockJS();

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class GreetingController {
@Resource
private SimpMessagingTemplate simpMessagingTemplate;

@MessageMapping("/change-notice") // 接收客户端推送信息的地址,配置了头,所以前端发送应该是"/app/change-notice"
public void greeting(String value){
this.simpMessagingTemplate.convertAndSend("/topic/notice", value);
}

//可以简写成这种形式,@SendTo代表返回值推送到那个topic
@MessageMapping("/change-notice")
@SendTo("/topic/notice")
public String greeting(String value) {
return value;
}
}

客户端

准备

  • socketjs.js:如果浏览器不支持webSocket,改库可以模拟对webSocket的支持
  • stomp.js:将webSocket代理代码简易化的框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function connect(){
var socket = new SocketJS('/socket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
//...
});
//断开连接
stompClient.disconnect();
//发送信息
stompClient.send("/app/change-notice", {}, value);
}
//订阅信息
stompClient.subscribe('/topic/notice', function (data) {
$('.message span.content').html(data.body);
});

文章字数:1063,阅读全文大约需要4分钟

依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

使用

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
import com.cenobitor.aop.annotation.Action;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;

@Aspect
@Component
public class LogAspect {

@Pointcut("@annotation(com.cenobitor.aop.annotation.Action)")
public void annotationPoinCut(){}

@After("annotationPoinCut()")
public void after(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Action action = method.getAnnotation(Action.class);
}

@Before("execution(* com.cenobitor.aop.service.DemoMethodService.*(..))")
public void before(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
}
}

注解说明

1
2
3
4
5
6
7
8
@Aspect 定义切面:切面由切点和增强(引介)组成(可以包含多个切点和多个增强),它既包括了横切逻辑的定义,也包括了连接点的定义,SpringAOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的链接点中。
@Pointcut 定义切点:切点是一组连接点的集合。AOP通过“切点”定位特定的连接点。通过数据库查询的概念来理解切点和连接点的关系再适合不过了:连接点相当于数据库中的记录,而切点相当于查询条件。
@Before :在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可。
@AfterReturning : 在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值。
@Afterthrowing: 主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象。
@After: 在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式。
@Around: 环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint。
环绕通知需要ProceedingJoinPoint.proceed放行执行中间的代码

AspectJ表达式

用来解释aop连接点

  • 方法描述指示器
指示器 说明 使用
execution() 匹配方法执行的连接点 execution(方法修饰符 返回值方法名(参数) 抛出异常) 这个颜色表示不可省略部分所有部分都支持*全部匹配
  • 方法参数匹配
指示器 说明
args() 匹配当前执行的方法传入参数为指定类型
@args 匹配当前执行的方法传入的参数有指定注解的执行
  • 当前AOP代理对象类型匹配
指示器 说明
this() 用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
  • 目标类匹配
指示器 说明
target() 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
@target 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
within() 用于匹配指定类型内的方法执行
@within 用于匹配所以持有指定注解类型内的方法
  • 标有此注解的方法匹配
指示器 说明
@annotation 用于匹配当前执行方法持有指定注解的方法
  • 匹配特定名称的Bean对象
指示器 说明
bean() Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法
  • 引用其他命名切入点
指示器 说明
reference pointcut 表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持

文章字数:158,阅读全文大约需要1分钟

ajax提交json数据,springBoot使用有相同类型的实体类接收,但是实体类属性全部是空。最后通过前端F12查看发送的数据,发现发送的是json字符串,而不是键值对形式。原来是我在前端多了一步转换对象为json字符串的操作。。。低级错误。。

正确的代码结构:
前端提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var formdata={};
formdata.name='xxx';
formdata.pwd='zzz';
$.ajax({
data: formdata,
dataType: "json",
type: "post",
url: "/log",
success: function(result) {
console.log(result)
},
error: function(result) {

}
});

springBoot:

1
2
public Mes add(User u,@RequestParam("other") String other){//除了实体类里的信息外指定name获取其他信息
}

其中,User实体类应该完全包含前端发送的json(formdata)的所有属性,否则可能会出错。


文章字数:35,阅读全文大约需要1分钟

启动任务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Order(value = 31)
@Slf4j
public class dataProcessor implements CommandLineRunner{
@Override
public void run(String... strings) throws Exception {
while (true) {
TimeUnit.SECONDS.sleep(10);
//原本这个方法上加的定时,现在改为循环
dataTiming();
}
}
}

文章字数:88,阅读全文大约需要1分钟

开发的时候需要项目启动后执行一些初始化功能,soringBoot中可以添加一个model并实现CommandLineRunner接口,实现功能的代码放在实现的run方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.springboot.sample.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component //作为bean加入spring
@Order(value=2)//多个启动任务的执行顺序
public class MyStartupRunner1 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println(">>>>>>>>>>>>>>>服务启动执行 2222 <<<<<<<<<<<<<");
}
}

文章字数:483,阅读全文大约需要1分钟

开启https可以有效防止中间人攻击。原理是使用非对称加密,连接双方加密的信息只能用对方的秘钥解密。CA颁发的证书还能够保证服务器证书不是被伪造的。

自己生成证书

这种证书可以保证加密连接,但是黑客可以自己生成一个和我们域名一样的证书,并使用DNS劫持,将域名绑定到自己的ip上。使用CA颁发的证书可以解决这种问题。

  1. 使用jdk自带的命令生成证书
  • keytool -genkeypair -alias tomcat -keyalg RSA -keystore E:\tomcat.key完整命令
  • -alias tomcat别名为tomcat
  • -keyalg RSA使用RSA非对称加密
  • -keystore E:\tomcat.key生成后保存位置
  1. 其它命令
    1
    2
    3
    4
    5
    6
    7
    keytool -genkey -alias tomcat  -storetype PKCS12 -keyalg RSA -keysize 2048  -keystore keystore.p12 -validity 3650

    1.-storetype 指定密钥仓库类型
    2.-keyalg 生证书的算法名称,RSA是一种非对称加密算法
    3.-keysize 证书大小
    4.-keystore 生成的证书文件的存储路径
    5.-validity 证书的有效期
  1. 配置application.properties
    1
    2
    3
    4
    5
    6
    7
    # 路径
    # server.ssl.key-store: classpath:tomcat.key
    server.ssl.key-store=tomcat.key
    server.ssl.keyStoreType=JKS
    server.ssl.key-alias=tomcat
    # 生成的时候输入的
    server.ssl.key-store-password=password123

使用CA的证书

  1. 阿里云腾讯云之类的都有,购买证书后下载。
  2. 下载后解压,复制Tomcat文件夹中的.jks文件到资源目录,和配置文件同级
  3. 查看证书相关信息keytool -list -keystore server.p12
  4. 配置
1
2
3
4
5
6
server.port: 8092
server.ssl.key-store= classpath:server.jks
# 密码在文件夹中的txt中
server.ssl.key-store-password=你的密码
# .jks证书的类型,还有一个是PKCS12,需要用crt生成
server.ssl.keyStoreType = JKS

配置Http转发到Https

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
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
tomcat.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}

@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
//Connector监听的http的端口号
connector.setPort(8080);
connector.setSecure(false);
//监听到http的端口号后转向到的https的端口号
connector.setRedirectPort(8092);
return connector;
}

文章字数:276,阅读全文大约需要1分钟

freemarker是一款模板引擎,适用于mvc框架

引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring.freemarker.allow-request-override=false
# 关闭缓存
spring.freemarker.cache=false
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
#spring.freemarker.prefix=
#spring.freemarker.request-context-attribute=
#spring.freemarker.settings.*=
#文件后缀名
#spring.freemarker.suffix=.ftl
#spring.freemarker.template-loader-path=classpath:/templates/ #comma-separated list
#spring.freemarker.view-names= # whitelist of view names that can be resolved

使用

  • 基本使用

    1
    <h1>Hello , ${name}</h1>
  • Controller.java*

    1
    2
    3
    4
    5
    public ModelAndView view(){
    ModelAndView mv = new ModelAndView("hello");
    mv.addObject("name","xxx");
    return mv;
    }
  • 循环

    1
    2
    3
    <#list studentList as student>
    ${student.id}/${studnet.name}
    </#list>
    1
    2

    下标

    <#list studentList as student>
    ${student_index}
    </#list>

  • 判断

    1
    2
    3
    <#if student_index % 2 == 0>
    <#else>
    </#if>
  • 日期格式化

    1
    2
    3
    4
    日期:${date?date}
    时间:${date?time}
    日期时间:${date?datetime}
    自定义格式:${date?string("yyyyMM/dd HH:mm:ss")}
  • null处理

    1
    2
    3
    4
    5
    ${name!"默认..."}
    判断
    <#if name??>
    <#else>
    </#if>
  • 包含其他

    1
    <#include "hello.ftl"/>

宏定义

就是定义一个代码片段,在其它地方可以复用
####不带参数

  1. 定义
    1
    2
    3
    <#macro greet>  
    <font size="+2">Hello Joe!</font>
    </#macro>
  2. 复用
    1
    2
    3
    <@greet></@greet>  
    或者
    <@greet/>

    带参使用

  • 定义
    1
    2
    3
    4
    5
    <#macro header title="默认文字" keywords="默认文字" description="默认文字"> 
    <title>${title}</title>
    <meta name="keywords" content="${keywords}" />
    <meta name="description" content="${description}" />
    </#macro>
  • 使用
    1
    2
    3
    <!-- 引入上面的定义文件 -->
    <#include "/include/public.ftl">
    <@header title="公司简介" keywords="公司简介2" description="公司简介3">

实例

  1. 重复生成固定数量的元素
1
2
3
4
5
6
7
8
9
10
<!-- 定义一个1到28的数组 -->
<#assign months=1..28/>
<select name="" id="">
<!-- 遍历数组 -->
<#list months as month>
<option value="${month}">${month}</option>
</#list>
</select>
<!-- 其它生成方式 -->
<#assign months=[1,2,3,4,5,6,7,8,9,0]/>