Spring 基础知识学习

IoC 容器

Bean的使用

示例代码

<!-- 首先在 ApplicationContext 的配置文件(此处命名为application-context.xml)中添加 Bean -->
<bean id="myId" class="MyClass"></bean>
 // 初始化容器 ApplicationContext context = new
ClassPathXmlApplicationContext("application-context.xml");
// 获取对象
MyClass my = context.getBean("myId", MyClass.class);

Bean 的作用域

Bean 的作用域有 singleton, prototype, request, session, global session, application. 其中后四种是在web应用中使用的。

singleton: bean 的默认作用域, 也可通过 <bean id="myId" class="MyClass" scope="singleton"></bean> 来显式地指定作用域。 保证只会有一个实例存在(后续创建的实例与第一个创建的实例指向同一个对象)_
prototype: 每次引用该bean时都会创建一个新的实例。

Bean实例创建、销毁时对资源的申请和释放

创建 在添加Bean时指定初始化方法的名称,并在类中实现该方法即可

<bean id="myId" class="MyClass" init-method="init"></bean>
public class MyClass {
  public void init() {}
}

销毁 在添加Bean时指定销毁方法的名称,并在类中实现该方法即可

<bean id="myId" class="MyClass" init-method="cleanup"></bean>
public class MyClass {
  public void cleanup() {}
}

注意: scope为singleton的bean的destroy方法则是在容器关闭时执行,而scope为prototype的bean是不会执行destroy方法的

依赖注入

依赖注入的方法

  • 构造函数依赖注入。为需要注入的类增加一个带参数的构造函数来完成依赖注入
public class MyClass {
  Dependency dp = null;
  public MyClass (Dependency instanceOfSubClassOfDependency) {
    dp = instanceOfSubDependency;
  }
}
  • Setter 方法依赖注入。
public class MyClass {
  Dependency dp = null;
  public void setDp(Dependency instanceOfSubClassOfDependency) {
    dp = instanceOfSubClassOfDependency;
  }
}

如果是强依赖,最好选择构造函数注入,如果是可选依赖,最好 选择 Setter 方法注入

示例代码

  • 通过构造函数传入基本类型来实现依赖注入
<!-- 对类实现了依赖注入的构造函数的话需要在Spring 的配置文件中进行描述 -->
<bean id="myId" class="MyClass">
  <!-- 参数按照构造函数参数列表顺序给出 -->
  <constructor-arg value = "..."></constructor-arg>
  <!-- 或者通过index属性显式指定(index从0开始计数) -->
  <constructor-arg index = "0" value = "..."></constructor-arg>
  <!-- 或者通过指定参数类型来确定 -->
  <constructor-arg type = "java.lang.String" value = "..."></constructor-arg>
  <!-- 或者通过参数名称来指定参数值 -->
  <constructor-arg name = "argName" value = "..."></constructor-arg>
</bean>
  • 通过构造函数传入集合来实现依赖注入
public class MyClass {
  String arg0;
  int arg1;
  public MyClass (Map<String, String> paras) {
    this.arg0 = paras.get("arg0");
    this.arg1 = Integer.valueOf(paras.get("arg1"));
  }
}
<!-- 相对应的配置文件 -->
<bean id="myId" class="MyClass">
  <constructor-arg>
    <map>
      <entry key = "arg0" value = "value0"></entry>
      <entry key = "arg1" value = "value1"></entry>
    </map>
  </constructor-arg>
</bean>
  • 通过配置文件来实现依赖注入
<!-- 设置配置文件的位置, Spring就可以从对应位置加载配置文件 -->
<bean id="myClassProperties" class =
  "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="location" value="classpath:myClass.properties"/> </bean>
  <bean id="myId" class="MyClass">
    <constructor-arg name = "arg0" value = "${arg0}"></constructor-arg>
    <constructor-arg name = "arg1" value = "${arg1}"></constructor-arg>
  </bean>
  • 注入自定义类
<bean id="myId2" class="MyClass2">
  <constructor-arg>
    <ref bean="myId"/>
  </constructor-arg>
</bean>
<bean id="myId" class="MyClass">
  <constructor-arg name = "arg0" value = "${arg0}"></constructor-arg>
  <constructor-arg name = "arg1" value = "${arg1}"></constructor-arg>
</bean>
  • 通过 setter 函数来注入
public class MyClass {
  private String arg0;
  private int arg1;
  public void setArg0(String arg0) {
     this.arg0 = arg0;
  }
  public void setArg1(int arg1) {
    this.arg1 = arg1;
  }
}
<bean id="myId" class="MyClass">
  <property name="arg0" value="value0"></property>
  <property name="arg1" value="value1"></property>
</bean>

自动装配与Annotation

自动装配

如果一个bean的依赖很多,或者需要添加依赖,手动修改配置文件会很低效。所以Spring提供了自动装配(Autowiring)的机制 自动装配有几种类型:
注意: Autowire是根据 setter 进行自动注入的,所以需要实现 setter方法

  • byName: 根据Bean名称进行装配
    xml
    <bean id = "myId" class = "myClass" autowire="byName"></bean>
  • byType: 根据Bean类型进行装配
  • constructor: 构造函数,根据类型

Annotation

XML文件配置Bean与Annotation的比较:
* XML: 繁琐,代码独立
* Annotation: 简洁,代码耦合
几种Annotation:
1. @Component: 定义Bean
2. @Value: properties 注入
3. @Autowired & @Resource: 自动装配依赖
4. @PostConstruct & @PreDestroy: 生命周期回调

要使用Annotation 需要在 XML配置文件中添加 <context:component-scan base-package="..."/>

@Component("myId")
// 如果不指定id的话,默认为MyClass
public class MyClass {
  @Value("value0") String para0;
  @Value("value1") int para1;
}

Spring AOP

@AspectJ annotation-based AOP

使用时需要添加aspectj依赖 在spring的配置文件中也需要在beans的namespace中添加aop的tag

<beans xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
  <aop:aspectj-autoproxy />
  <!-- AOP 的使用 -->
  <!-- 定义Aspect: 以定义一个日志功能Aspect为例-->
  <bean id = "loggingAspect" class = "package.LoggingAspect"> </bean>
</beans>
// LoggingAspect.java
import org.aspectj.lang.annotation.Aspect;
// Aspect 的实现类需要添加 @Aspect的Annotation
@Aspect
public class LoggingAspect {
  // 定义Pointcut
  // Pointcut 表达式: designator(modifiers? return-type declaring-type? name(param) throws?) // 1.modifiers 是可选的,表示匹配的函数的修饰符,如public、private等 // 2.declaring-type 是可选的,表示匹配函数所在的包名,类名
  // 例1. 匹配所有的public函数
  // @Pointcut("execution(public * *(..))")
  // private void publicMethod() {}
  // 例2. 所有以save开头的函数
  // @Pointcut("execution(* save*(..))")
  // private void saveMethod() {}
  // 例3. 通过组合已有的Pointcut来实现匹配所有以save开头的public函数
  // @Pointcut("execution(publicMethod() && saveMethod())")
  // private void publicSaveMethod() {}
  @Pointcut("execution(* package.ObjectClass.*(..))")
  private void pointcutName() {}
  // 定义Advice
  @Before("package.LoggingAspect.pointcutName()") // 此处也可以直接用pointcut表达式代替而不是填写pointcut名称
  public void beforeActionName() {}
  // 其他Advice的annotation
  // @AfterReturing
  // @AfterThrowing
  // @After 等等
  // 通过JoinPoint 获取函数上下文信息
  @Before("pointcutName()")
  public void afterActionName(JoinPoint jp) {
    System.out.println(jp.getSignature());
  }
  // 注意@Around与其他有所不同,需要通过ProceedingJoinPoint获取上下文信息
  @Around("pointcutName()")
  public void aroundActionName(ProceedingJoinPoint pjp) {
    System.out.println("Start"); // 调用目标函数执行 pjp.proceed();
    System.out.println("Done");
  }
  // 获取返回值
  @AfterReturing(pointcut="pointcutName()", returning="retVal")
  public void afterReturingActionName(Object retVal) {
    // do something with return value
  }
}

Schema-based AOP

<!-- 在配置文件中配置AOP等 -->
<!-- 定义Aspect -->
 <aop:config>
  <aop:aspect id="loggingAspect" ref="loggingBean">
    <!-- 定义Pointcut -->
    <aop:pointcut id="pointcutName" expression="...">
      <!-- 定义Advice -->
      <aop:before pointcut-ref="pointcutName" method="methodInLoggingAspect"></aop:before>
    </aop:pointcut>
  </aop:aspect>
</aop:config>

<bean id="loggingBean" class="package.LoggingAspect">
</bean>

Spring 数据访问

Spring JDBC

<!-- 定义 JdbcTemplate -->
<bean id="JdbcExampleDao" class="package.JdbcExmapleDao">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!-- 在 Spring 配置文件中配置 DataSource -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="${jdbc.dirverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="db.properties"/>

<!-- db.properties 的内容 -->
jdbc.dirverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost/database
jdbc.username = username
jdbc.password = password

通过 Annotation 配置

@Repository
public class JdbcExampleDao {
    private JdbcTemplate jdbcTemplate;
    @Resource
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
}

Spring 事务管理

  • 统一的事务编程模型
  • 编程式事务及声明式事务(AOP)
// org.springframwork.transaction
// spring 提供的事务接口
public interface PlatformTransactionManager {
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

TransactionDefinition

  • getName: 事务名称
  • getIsolationLevel: 隔离级别
  • getPropagationBehavior: 传播行为
  • getTimeout: 超时时间
  • isReadOnly: 是否只读事务

TransactionStatus

  • isNewTransaction: 是否是新的事务
  • hasSavepoint: 是否有savepoint
  • isCompleted
  • isRollbackOnly: 事务的结果是否是rollback-only
  • setRollbackOnly

隔离级别

  • ISOLATION_READ_UNCOMMITTED: 读未提交
  • ISOLATION_READ_COMMITTED: 读提交
  • ISOLATION_REPEATABLE_READ: 重复读
  • ISOLATION_SERIALIZABLE: 串行化
  • ISOLATION_DEFAULT: 默认

传播行为

  • PROPAGATION_MANATORY 必须在一个事务中运行,不存在事务则抛异常
  • PROPAGATION_NEVER 不应该在事务中运行,存在则抛异常
  • PROPAGATION_NOT_SURPPORTED 不应该在事务中运行,存在则挂起
  • PROPAGATION_SURPPORTS 不需要事务,有则在事务中运行
  • PROPAGATION_REQUIRED 默认的传播行为,必须在事务中执行,如果不存在, 则启动新事物 (内部事务会影响外部事物)
  • PROPAGATION_NESTED 必须在事务中执行,如果不存在,则启动新事物 (事务之间互不影响)
  • PROPAGATION_REQUIRES_NEW 必须在新事务中执行,挂起当前事务

代码示例

声明式事务
<!-- 在spring配置文件中添加 AOP 和 Transaction 的 Schema -->
<beans xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!-- 定义事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="${jdbc.dirverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <context:property-placeholder location="db.properties"/>
    <!-- 定义事务Advie -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- 定义 Pointcut -->
    <aop:config>
        <aop:pointcut id="daoOperation" expression="execution(Pointcut表达式)"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="daoOperation"/>
    </aop:config>
</beans>
  • 通过注解的方式来声明事务
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
    <property name="driverClassName" value="${jdbc.dirverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="db.properties"/>
@Transactionnal(propagation = Propogation.REQUIRED, rollbackFor = Exception.class)
public void doSomething() {
    // do work
}

@Transactional 的配置属性

  • value: 使用的TransactionManager
  • propagation
  • isolation
  • timeout
  • readOnly
  • rollbackFor
  • rollbackForClassName
  • noRollbackFor
  • noRollbackForClassName
编程式事务
  • TransactionTemplate
// 定义TransactionTemplate
public class Service {
    private final TransactionTemplate transactionTemplate;
    public Service(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setIsolationLevel(...);
        this.transactionTemplate.setTimeout(...);
        ...
    }

    // 使用TransactionTemplate
    public Object someMethod() {
        return transactionTemplete.execute(new TransactionCallback() {
            public Object doInTransaction(TransactionStatus status) {
                // doSomething
                // return something
            }
        });
    }
}
  • PlatformTransactionManager 的实现
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("transactionName");
def.setPropagationBehavior(...);

TransactionStatus status = txManager.getTransaction(def);
try {
    // do some trasaction logic
} catch (Exception e) {
    txManager.roolback(status);
    throw ex;
}
txManager.commit(status);

整合MyBatis

添加 mybatis-spring 依赖

定义 SqlSessionFactoryBean

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
</bean>

定义Mapper

public interface UserMapper {
    User getUser(String userId);
}
<mapper namespace="package.UserMapper">
    <select id = "getUser" resultType = "User">
        select * from users where id = #{userId}
    </select>
</mapper>

也可以用 Annotation 定义 Mapper

public interface UserMapper {
    @Select("select * from users where id = #{userId}")
    User getUser(@Param("userId") String userId);
}

定义 Mapper Bean

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="mapperInterface" value="package.UserMapper"/>
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

也可通过Annotation配置

<!-- 首先需要在spring 配置文件中声明mybatis命名空间 -->
<!-- 开始mybatis的扫描 -->
<mybatis:scan base-package="package"/>

使用 Mapper

public class SomeService {
    @Resource
    private UserMapper userMapper;

    public User getUser(String userId) {
        return userMapper.getUser(userId);
    }
}

通过 SqlSessionTemplate 可以使用 MyBatis 中 session 的接口

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
public class UserDAO {
    @Resource
    private SqlSession sqlSession;

    public User getUser(String userId) {
        return (User) sqlSession.selectOne("package.UserMapper.getUser", userId);
    }
}

Spring Web 框架

为什么要使用 Spring Web 框架?

  1. 需要一个框架来处理地址到处理逻辑的路由
  2. 需要一个框架来处理MVC

Web MVC 框架

Servlet 为核心构建的, 其核心是 DispatherServlet, 在其背后提供了一些工具来解决上述的问题。

  • Handler Mapping 用于解决请求路径到处理逻辑的映射关系
  • Controller 用于负责视图和模型之间的关系
  • View ResolverView 则是视图相关的工具

DispatherServlet 的使用

web.xml 文件中进行配置

<servlet>
  <servlet-name>example</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatherServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>example</servlet-name>
  <url-pattern>/api/*</url-pattern>
</servlet-mapping>

同时在 WEB-INF 目录下配置一个名为 [servlet-name]-servlet.xml 的配置文件用于配置DispatherServlet

可配置项:

  • HandlerMapping
  • Controllers
  • View 解析相关

注意: DispatherServlet 不是唯一的,可以定义多个 DispatherServlet

可以通过 ContextLoaderListener 来配置多个 DispatherServlet 之间共同的配置

ContextLoaderListener

首先需要在 web.xml 文件中进行配置

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
  <!-- 指定Spring配置文件 -->
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

实现 Controllers

// 需要在spring配置文件中开启自动发现才能使用Annotation来定义Controller
// <context:component-scan base-package="..."/>
@Controller
@RequestMapping(value = "/hello")
public class HelloController {
  // 当访问地址为 /hello/spring 时就会将请求转接到spring函数的处理中,输出"Hello, Spring Web"
  @RequestMapping(value = "/spring")
  public void spring(HttpServletResponse response) throws IOException {
    response.getWriter().write("Hello, Spring Web!!");
  }
}

RequestMapping 的属性

  • name: 名称
  • value & path : 路径, 比如 “/hello”
    注意:
    以下代码效果完全相同

    @RequestMapping("/hello")
    @RequestMapping(value = "/hello")
    @RequestMapping(path = "/hello")
  • method: 匹配请求的请求方法,如 “GET”
  • params: 请求参数,请求地址中加 “?key=value&key2=value2”. 当请求含有特定参数时才会转发到该请求处理函数
  • headers: 请求头
  • consumes: 请求的媒体类型,处理请求类型是特定数据类型的请求
  • produces: 响应的媒体类型,处理请求特定数据类型的请求

示例:

// RestAPI 的路径参数匹配
@RequestMapping(path="users/{userId}")
public String webMethod(@PathVariable String userId) {
  // do Work
}

函数参数:

  • HttpServletResponse/HttpServletRequest, HttpSession
  • Reader/Writer
  • @PathVariable
  • @RequestParam
  • @RequestHeader
  • HttpEntity
  • @RequestBody
  • Map/Model/ModelMap

函数返回值:

  • void 表示该请求处理函数不用返回view,函数自身会处理与view相关的逻辑
  • String: view 名称, 也可以用 @ResponseBody 这个 Annotation 来说明返回值不是view名称而是响应内容
  • HttpEntity
  • View: spring定义的view对象
  • Map: 代表返回的Model
  • Model
  • ModelAndView

示例代码

@RequestMapping(value = "/spring/{user}")
public void spring(
  @PathVariable("user") String user,
  @RequestParam("msg") String msg,
  @RequestHeader("host") String host,
  HttpServletRequest request,
  Writer writer) throws IOException {
    writer.write("URI: " + request.getRequestURI());
    writer.write("Hello, " + user + ": " + msg + ", host=" + host);
}
  • 表单
@RequestMapping(value = "/spring/login")
public void login(
  @RequestParam("name") String name,
  @RequestParam("password") String password,
  Writer writer)
    throws IOException {
      // do work
}

对于有很多信息的表单,挨个写 RequestParam 就会显得繁琐, Spring 提供了一个 ModelAttribute 的 Annotation
可以从参数匹配一个model

@RequestMapping(value = "/spring/login")
public void login(@ModelAttribute User user, Writer writer) {
  // do work
}
  • 上传文件
    首先定义一个与上传相关的bean
<bean id="multipartResolver"
  class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  <property name="maxUploadSize" value="100000"/>
</bean>
@RequestMapping(path = "/form", method = RequestMethod.POST)
public String handleFormUpload(@RequestParam("file") MultipartFile file) {
  // do work
}
  • HttpEntity
@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) {
  String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader");
  byte[] requestBody = requestEntity.getBody();
  // do something with request header and body
  HttpHeaders responseHeaders = new HttpHeaders();
  responseHeaders.set("MyResponseHeader", "MyValue");
  return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}
  • @RequestBody & @ResponseBody
@RequestMapping(value = "/spring")
@ResponseBody
public String spring(@RequestBody String body) throws IOException {
  return "Hello" + body;
}

View 解析

  1. InternalResourceViewResolver
    一般处理 Servlet, JSP

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <!--指定view所在的相对路径-->
    <proerty name="prefix" value="/WEB-INF/jsp/"/>
    <!--指定view的扩展名-->
    <proerty name="suffix" value=".jsp"/>
    </bean>

    如果请求处理函数返回的view名称为 resultView, 则View的路径为 /WEB-INF/jsp/resultView.jsp

  2. FreeMarkerViewResolver
    针对 Freemarker 模板引擎的 viewResolver

    <!--需要先配置freemarker模板的路径-->
    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
    <proerty name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
    </bean>
    
    <!--然后就与上面类似,定义view的prefix 和 suffix-->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
    <!--是否缓存View的映射-->
    <proerty name="cache" value="true"/>
    <proerty name="prefix" value=""/>
    <proerty name="suffix" value=".ftl"/>
    </bean>
  3. ContentNegotiatingViewResolver
    是 ViewResolver 的组合,可以根据客户端的请求返回不同的view

    • 根据请求路径中的文件扩展名: 如: user.json, user.xml, user.pdf
    • 根据请求头中的Accept头来决定: 如 application/json, application/xml
    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="viewResolvers">
      <list>
        <bean id="viewResolver" class="org.springframwork.web.servlet.view.freemarker.FreeMarkerViewResolver">
          <property name="cache" value="true"/>
          <property name="prefix" value=""/>
          <property name="suffix" value=".ftl"/>
        </bean>
      </list>
    </property>
    <!--如果通过viewResolvers匹配不到合适的view,就会采用defaultView-->
    <property name="defaultViews">
      <list>
        <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
      </list>
    </property>
    </bean>

Freemarker

Freemarker 指令

  • 开始标签: <#directivename parameters>
  • 结束标签: </#directivename parameters>

if 指令:

<#if balance < 10> poor guy
<#elseif balance = 10> not so poor
<#else> haha
</#if>

list 指令: freemarker 的 foreach

<#list animals as animal>
${animal.name}<br>
</#list>

include 指令:
将需要重复使用的固定内容插入

<#include "/copyright_footer.html">
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值